Skip to content

fix: apply the sandboxed and extraBinds settings a roj.config declares - #21

Merged
matej21 merged 9 commits into
mainfrom
fix/sandbox-config-wiring
Aug 31, 2026
Merged

fix: apply the sandboxed and extraBinds settings a roj.config declares#21
matej21 merged 9 commits into
mainfrom
fix/sandbox-config-wiring

Conversation

@matej21

@matej21 matej21 commented Aug 28, 2026

Copy link
Copy Markdown
Member

RojConfig declares sandboxed and extraBinds, and no host used them. Both loaders read sandboxed into the returned config, and every entry point then reduced that config to its presets before handing it on. extraBinds was not even parsed. A user writing defineConfig({ 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 that RojConfig and 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 stays false. extraBinds follow the same rule, and an empty list counts as a declaration at either level. A relative bind path resolves against the config directory, matching how local resources already behave; destPath names 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.ts where 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 declares sandboxed: true and 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.sessionsDir and snapshotter are also declared and read by nobody. snapshotter is parsed by both loaders and never consumed; the snapshotting plugin takes a Snapshotter instance through its own config.

🤖 Generated with Claude Code

https://claude.ai/code/session_01CbVWntVv8xSdtn6DC6ZApM

matej21 and others added 3 commits August 28, 2026 18:05
`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
matej21 force-pushed the fix/sandbox-config-wiring branch from f0e8ea0 to bcf0afd Compare August 28, 2026 16:07
matej21 and others added 6 commits August 28, 2026 19:02
…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
@matej21
matej21 merged commit b7c95f1 into main Aug 31, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant