Skip to content

feat: opt-in OS execution sandbox for the agent (Seatbelt / bubblewrap)#136

Merged
Aayam Bansal (aayambansal) merged 5 commits into
mainfrom
feat/agent-sandbox
Jul 8, 2026
Merged

feat: opt-in OS execution sandbox for the agent (Seatbelt / bubblewrap)#136
Aayam Bansal (aayambansal) merged 5 commits into
mainfrom
feat/agent-sandbox

Conversation

@aayambansal

Copy link
Copy Markdown
Member

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 bash tool runs is wrapped in a platform sandbox that confines writes to the workspace and can optionally deny network egress.

  • macOS → Seatbelt via sandbox-exec (a generated SBPL profile)
  • Linux → bubblewrap (bwrap) mount namespaces, with a functional probe (falls back to "none" when unprivileged user namespaces are disabled)
  • elsewhere → no backend; onUnavailable decides whether to warn+run, refuse, or run silently

Off 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

  • Coresrc/sandbox/sandbox.ts: backend detection, SBPL profile + bwrap arg builders, plan() consumed by the bash tool, and a selfTest() that empirically proves containment.
  • Configsandbox { enabled, network, allowWrite, onUnavailable }, off by default; Config.setSandbox JSONC-preserving writer.
  • Execution — the bash tool wraps spawn under the active backend when enabled; a one-time notice if enabled-but-unavailable.
  • CLIopenscience sandbox (status) · enable (--network deny, --allow <path>, --on-unavailable, --project) · disable · test. doctor reports on/off + backend.
  • GUI — Settings → Sandbox: enable toggle, network/fallback policy, extra writable paths, and a "run self-test" button showing per-check results. Backed by routes/settings/sandbox.ts.
  • Docssandbox.mdx, commands.mdx section, and security.mdx updated 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:

✓ pass  write inside the workspace succeeds
✓ pass  write outside the workspace is blocked
✓ pass  network egress blocked in deny mode
Containment verified.

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.tsend-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.

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
@vercel

vercel Bot commented Jul 7, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
openscience Ready Ready Preview, Comment Jul 7, 2026 3:22pm

Request Review

…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.
@aayambansal
Aayam Bansal (aayambansal) merged commit a737ddc into main Jul 8, 2026
12 checks passed
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>
@aayambansal
Aayam Bansal (aayambansal) deleted the feat/agent-sandbox branch July 8, 2026 08:01
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.

2 participants