fix: apply the sandboxed and extraBinds settings a roj.config declares - #21
Merged
Conversation
`RojConfig` has always declared `sandboxed` and `extraBinds`, but nothing turned them into anything a session could read — the effective flag came from the preset alone. Add `applySandboxSettings(config)`, which folds the top-level values into every preset with an explicit precedence: a preset that sets `sandboxed` wins, otherwise the top-level value applies, otherwise `false`. The same order applies to `extraBinds`, which reach the shell plugin config — the only consumer of them — when that config declares none of its own. `describeSandboxPosture(presets)` renders the resolved posture as one line so a host can log it at startup. Defaults are unchanged: a config that declares nothing still resolves to `false`. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CbVWntVv8xSdtn6DC6ZApM
The loader read `sandboxed` and then `main.ts` forwarded only `presets`
and `localResources`, so `defineConfig({ sandboxed: true })` was accepted
and silently dropped. `extraBinds` never made it out of the config file at
all — the loader did not even parse it.
Parse `extraBinds` (validating `path`, `mode` and `destPath` instead of
trusting the shape), run the config through `applySandboxSettings` before
handing the presets to the server, and log the resolved posture next to
the preset list so it is visible rather than inferred.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CbVWntVv8xSdtn6DC6ZApM
Same gap as the standalone host: the loader read `sandboxed`, `main.ts` forwarded only `presets`, and `extraBinds` was never parsed. Parse the binds, resolve both settings through `applySandboxSettings`, and log the resolved posture at startup. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CbVWntVv8xSdtn6DC6ZApM
matej21
force-pushed
the
fix/sandbox-config-wiring
branch
from
August 28, 2026 16:07
f0e8ea0 to
bcf0afd
Compare
…n each host
Folding the config in the host entry points made applying it something every
new caller has to remember, and one already forgot: the bundle the platform CLI
generates calls `startServer({ presets: config.presets })`, so a config shipped
that way still lost its `sandboxed` and `extraBinds`.
`bootstrap` is the choke point every host goes through, so the fold belongs
there. It now resolves the presets and logs the posture it arrived at, which
also gives the startup line one home instead of two.
`SessionDefaults` names the slice a host must forward. Server option types
extend it, so an entry point that forwards its options carries the settings
whether or not its author thought about them.
`extraBinds` now treats an explicit empty top-level list as a declaration, the
way a preset-level one already counted — the two sides were not symmetric.
`Session.environment` exposes what plugins and tools see, so a test can assert
the posture a host resolved rather than the shape of an intermediate object.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CbVWntVv8xSdtn6DC6ZApM
`main.ts` picked fields out of the loaded config, so anything it did not name
was dropped; `server.ts` then rebuilt a config of just `{ presets }` for
bootstrap, dropping them a second time. Both now forward what they were given.
The loader drops its private copy of the bind parser for the shared one, which
also resolves a relative `path` against the config directory the way
`localResources` does. A relative path used to pass validation and then be
resolved against the server's working directory instead.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CbVWntVv8xSdtn6DC6ZApM
Same two drops as the standalone host — `main.ts` naming fields and `server.ts`
rebuilding a `{ presets }` config for bootstrap — and the same move to the
shared bind parser.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CbVWntVv8xSdtn6DC6ZApM
The generated entry called `startServer({ presets: config.presets })`, so a
bundle uploaded through the CLI ran with everything else the user declared
thrown away — the third and last entry point with the drop.
The entry module is now a named function rather than a string built inline, so
a test can hold it to that. Test files leave the build output, and the new test
project joins the type-check step.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CbVWntVv8xSdtn6DC6ZApM
The resolver had unit coverage, but nothing ran a host: reverting the entry points, or hard-coding the flag at the one place a session reads it, left the suite green. Two tests that do run one. `server.test.ts` starts the server and reads the posture off the session it creates. `main.test.ts` boots the CLI as a subprocess and reads the posture off its startup log, which is the only cover for the entry point itself. Each kills a mutant that used to survive: dropping the config in `main.ts`, in `server.ts`, or in `bootstrap`, and hard-coding `sandboxed: false` in `Session.getSessionEnvironment`. The tests directory was outside the type-check project and is now inside it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CbVWntVv8xSdtn6DC6ZApM
…esets The field reached the shell plugin but appeared in neither the config-levels list nor the sandbox reference, so the precedence — a preset's own extraBinds replace it rather than merging — was only visible in the code. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CbVWntVv8xSdtn6DC6ZApM
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
RojConfigdeclaressandboxedandextraBinds, and no host used them. Both loaders readsandboxedinto the returned config, and every entry point then reduced that config to its presets before handing it on.extraBindswas not even parsed. A user writingdefineConfig({ sandboxed: true })got no sandbox, no warning and no error.The fold lives in
bootstrap(), the one place every host already passes through, rather than in each entry point — that is what makes it hard to skip. Three entry points were dropping the config, including the platform-CLI bundle path, which reduced it to presets even though the other branch re-exported it whole.SessionDefaults(presets,sandboxed,extraBinds) is now a named slice thatRojConfigand both server option types extend, so a server forwards{ ...options, presets }instead of naming fields and a new field does not need every caller updated.Precedence:
preset.sandboxed ?? config.sandboxed ?? false. An explicit preset value wins in both directions, the file-level value fills the gap, the default staysfalse.extraBindsfollow the same rule, and an empty list counts as a declaration at either level. A relative bindpathresolves against the config directory, matching how local resources already behave;destPathnames a location inside the sandbox, so it must be absolute.Why wire these rather than drop them. A programmatic host sets both knobs on the preset and that path is unchanged. The top-level fields serve the other entry point — a file-based
roj.config.tswhere one switch for the whole config is the expected ergonomics — so the precedence lets a preset override a file-level default rather than replace it.The knob now has teeth. Since the shell tool moved behind a host port, a command is refused when the session is sandboxed and the host shell declares
confinement: 'none', rather than running unconfined. A config that declaressandboxed: trueand reaches a host that cannot confine now fails loudly — the point of the change, but a visible difference for anyone setting the flag on such a host.Not a default change. A config that declares nothing resolves exactly as before.
Tests pin the wiring, not just the resolver. An earlier revision covered only the shape of the resolved object: reverting the entry points entirely still passed the whole suite. A host is now started from a config and the resulting session's environment is read back, and the bundle entry module is held to what it generates. Each way of severing the chain — at an entry point, at the server, inside
bootstrap, at the session, in the generated bundle — now fails a test.Still open, in the same class and not addressed here:
RojConfig.sessionsDirandsnapshotterare also declared and read by nobody.snapshotteris parsed by both loaders and never consumed; the snapshotting plugin takes aSnapshotterinstance through its own config.🤖 Generated with Claude Code
https://claude.ai/code/session_01CbVWntVv8xSdtn6DC6ZApM