feat: opt-in OS execution sandbox for the agent (Seatbelt / bubblewrap)#136
Merged
Conversation
The permission system is an approval layer, not an isolation boundary: an
approved command runs with the user's full authority. Add a real boundary that
confines the bash tool's writes to the workspace (macOS Seatbelt via
sandbox-exec, Linux bubblewrap) and can optionally deny network egress.
- src/sandbox/sandbox.ts: backend detection (functional bwrap probe), SBPL
profile + bwrap args builders, plan() consumed by the bash tool
- config: sandbox { enabled, network, allowWrite, onUnavailable }, off by
default; Config.setSandbox JSONC-preserving writer
- bash tool wraps spawn under the active backend when enabled
- unit tests for the profile/args/plan logic
… doctor - sandbox status: backend + platform + current config - sandbox enable [--network deny] [--allow PATH] [--on-unavailable ...] [--project] - sandbox disable - sandbox test: empirical self-test that writes inside/outside the workspace and checks network-deny egress, printing per-check PASS/FAIL (Sandbox.selfTest) - doctor now reports sandbox on/off and backend availability
- routes/settings/sandbox.ts: GET / (config + backend availability), PUT / (persist config, global), POST /test (empirical self-test); mounted at /settings/sandbox - Settings ▸ Sandbox panel: enable toggle, network allow/deny, on-missing-backend behaviour, extra writable paths, and a 'run self-test' button that shows per-check containment results - route test covering GET / and POST /test
…; bash e2e test - docs/sandbox.mdx: what it is, quick start, backends, CLI, config, GUI, honest limitations (write-containment, not a full jail) - security.mdx: 'not a sandbox by default' now points at the opt-in feature - commands.mdx: Sandbox command section; docs.json nav entry - bash-sandbox.test.ts: end-to-end proof the bash tool blocks writes outside the workspace when sandbox is enabled
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
KB (KB-syntheticsciences)
force-pushed
the
feat/agent-sandbox
branch
from
July 7, 2026 15:16
14da08c to
253390d
Compare
…llow-up)
Addresses review findings on the execution-sandbox feature:
- Linux self-test/e2e failed because a workspace under /tmp was shadowed by
the `--tmpfs /tmp` mount. bwrap now binds writable paths on top of the tmpfs
(skipping only the bare /tmp root), so workspaces under /tmp work and
`openscience sandbox test` reports "Containment verified" on Linux.
- Sandbox policy is read only from global + managed config
(Config.trustedSandbox), never project config — an untrusted repo's
openscience.json can no longer disable or weaken it. Dropped the now-
meaningless `sandbox enable/disable --project` flag; setSandbox is always
global.
- The notebook (Python) and R kernels are now wrapped too (Sandbox.wrapArgv),
so confinement covers all agent code execution, not just the bash tool.
- Over-broad writable roots are refused (worktree="/", $HOME, /etc, TMPDIR=/,
…) and an out-of-workspace cwd is no longer auto-granted write access.
- bwrap gains --unshare-pid to close the /proc/<pid>/root escape; the runtime
probe matches so backend detection tracks real enforcement.
- selfTest() is async (no longer blocks the server event loop), runs under the
real $SHELL (Shell.acceptable()), and its outside-write check now requires
the write to be attempted-and-denied rather than merely absent.
- GUI settings route reads the trusted policy directly; Config.get() needs an
Instance context the route is mounted before and always returned {}.
- Docs: coverage now includes the kernels, the global/managed trust model is
documented, the UNIX-socket egress limitation is noted, and --project is gone.
Verified on Linux with bwrap: real Sandbox.selfTest() passes; 21 sandbox unit +
bash/kernel e2e + settings-route tests green; config suite (53) green; typecheck
clean.
KB (KB-syntheticsciences)
force-pushed
the
feat/agent-sandbox
branch
from
July 7, 2026 15:21
253390d to
ce9c3bc
Compare
KB (KB-syntheticsciences)
approved these changes
Jul 7, 2026
Aayam Bansal (aayambansal)
added a commit
that referenced
this pull request
Jul 8, 2026
…p) (#136) * feat(sandbox): OS execution sandbox for the agent's shell commands The permission system is an approval layer, not an isolation boundary: an approved command runs with the user's full authority. Add a real boundary that confines the bash tool's writes to the workspace (macOS Seatbelt via sandbox-exec, Linux bubblewrap) and can optionally deny network egress. - src/sandbox/sandbox.ts: backend detection (functional bwrap probe), SBPL profile + bwrap args builders, plan() consumed by the bash tool - config: sandbox { enabled, network, allowWrite, onUnavailable }, off by default; Config.setSandbox JSONC-preserving writer - bash tool wraps spawn under the active backend when enabled - unit tests for the profile/args/plan logic * feat(sandbox): openscience sandbox CLI (status/enable/disable/test) + doctor - sandbox status: backend + platform + current config - sandbox enable [--network deny] [--allow PATH] [--on-unavailable ...] [--project] - sandbox disable - sandbox test: empirical self-test that writes inside/outside the workspace and checks network-deny egress, printing per-check PASS/FAIL (Sandbox.selfTest) - doctor now reports sandbox on/off and backend availability * feat(sandbox): server route + workspace Settings panel - routes/settings/sandbox.ts: GET / (config + backend availability), PUT / (persist config, global), POST /test (empirical self-test); mounted at /settings/sandbox - Settings ▸ Sandbox panel: enable toggle, network allow/deny, on-missing-backend behaviour, extra writable paths, and a 'run self-test' button that shows per-check containment results - route test covering GET / and POST /test * docs(sandbox): execution sandbox page + commands/security cross-links; bash e2e test - docs/sandbox.mdx: what it is, quick start, backends, CLI, config, GUI, honest limitations (write-containment, not a full jail) - security.mdx: 'not a sandbox by default' now points at the opt-in feature - commands.mdx: Sandbox command section; docs.json nav entry - bash-sandbox.test.ts: end-to-end proof the bash tool blocks writes outside the workspace when sandbox is enabled * fix(sandbox): close containment gaps + fix Linux self-test (review follow-up) Addresses review findings on the execution-sandbox feature: - Linux self-test/e2e failed because a workspace under /tmp was shadowed by the `--tmpfs /tmp` mount. bwrap now binds writable paths on top of the tmpfs (skipping only the bare /tmp root), so workspaces under /tmp work and `openscience sandbox test` reports "Containment verified" on Linux. - Sandbox policy is read only from global + managed config (Config.trustedSandbox), never project config — an untrusted repo's openscience.json can no longer disable or weaken it. Dropped the now- meaningless `sandbox enable/disable --project` flag; setSandbox is always global. - The notebook (Python) and R kernels are now wrapped too (Sandbox.wrapArgv), so confinement covers all agent code execution, not just the bash tool. - Over-broad writable roots are refused (worktree="/", $HOME, /etc, TMPDIR=/, …) and an out-of-workspace cwd is no longer auto-granted write access. - bwrap gains --unshare-pid to close the /proc/<pid>/root escape; the runtime probe matches so backend detection tracks real enforcement. - selfTest() is async (no longer blocks the server event loop), runs under the real $SHELL (Shell.acceptable()), and its outside-write check now requires the write to be attempted-and-denied rather than merely absent. - GUI settings route reads the trusted policy directly; Config.get() needs an Instance context the route is mounted before and always returned {}. - Docs: coverage now includes the kernels, the global/managed trust model is documented, the UNIX-socket egress limitation is noted, and --project is gone. Verified on Linux with bwrap: real Sandbox.selfTest() passes; 21 sandbox unit + bash/kernel e2e + settings-route tests green; config suite (53) green; typecheck clean. --------- Co-authored-by: KB <keertan@syntheticsciences.ai>
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.
The problem
The permission system is an approval layer, not an isolation boundary. When the agent runs a shell command, the prompt tells you whether it runs — but once approved (or auto-approved), the command executes with your full user authority. It can write
~/.ssh, rewrite~/.bashrc, delete another project, or phone home. The docs even said so: "For real isolation, run OpenScience inside a container or a VM."What this adds
An opt-in OS execution sandbox that gives the permission system a real boundary. When enabled, every command the
bashtool runs is wrapped in a platform sandbox that confines writes to the workspace and can optionally deny network egress.sandbox-exec(a generated SBPL profile)bwrap) mount namespaces, with a functional probe (falls back to "none" when unprivileged user namespaces are disabled)onUnavailabledecides whether to warn+run, refuse, or run silentlyOff by default. Reads stay open (the threat is tampering/exfiltration, not hiding files from tools). It is deliberately write-containment, not a deny-by-default syscall jail — research workflows run arbitrary compilers, package managers, and interpreters, and a strict jail breaks far more than it protects.
Every surface
src/sandbox/sandbox.ts: backend detection, SBPL profile + bwrap arg builders,plan()consumed by the bash tool, and aselfTest()that empirically proves containment.sandbox { enabled, network, allowWrite, onUnavailable }, off by default;Config.setSandboxJSONC-preserving writer.bashtool wrapsspawnunder the active backend when enabled; a one-time notice if enabled-but-unavailable.openscience sandbox(status) ·enable(--network deny,--allow <path>,--on-unavailable,--project) ·disable·test.doctorreports on/off + backend.routes/settings/sandbox.ts.sandbox.mdx,commands.mdxsection, andsecurity.mdxupdated to point at the feature.Proof it holds
openscience sandbox test(and the equivalent GUI button /Sandbox.selfTest) runs real sandboxed commands and reports pass/fail:Verified on macOS: a write to
~returns Operation not permitted, reads stay open, network egress is blocked in deny mode and allowed otherwise, and nothing escapes to home.Tests
test/sandbox/sandbox.test.ts— profile/args/plan logic (security assertions: deny-by-default writes, allowlist, network deny, quote-escaping).test/server/settings-sandbox.test.ts— route GET / + POST /test (asserts real containment when a backend exists).test/tool/bash-sandbox.test.ts— end-to-end through the real bash tool: inside-workspace write succeeds, outside-workspace write is blocked, no file escapes.Full suite: 992 pass / 0 fail. Typecheck (backend + frontend) and prettier clean.
Limitations (documented)
Reads unrestricted; not a container/VM substitute for hostile code; Linux needs bubblewrap + user namespaces; Windows has no backend yet.
Opening for review — not merging.