Public: anyone can read this; the crew can change it.

A crew run's sandbox refuses reads outside its worktree, each attempt a test #10585

Merged opened this

Check · Review · Land · Live done — Landed and live in production.

Merged

This pull adds `test/crew-sandbox-escape.test.ts`. It contains only the read attempts from the parked #10479 (at 4ea33a857367), and each attempt is now its own test. None of the attempts has run yet: this sandbox refuses user namespaces, so the file skips here (13 skipped, 0 failed). The forge check, on a host that allows bwrap, will be the first real run.

shipped production is serving it deploy da127e7

opened this

This pull adds test/crew-sandbox-escape.test.ts. It contains only the read attempts from the parked #10479 (at 4ea33a857367), and each attempt is now its own test. None of the attempts has run yet: this sandbox refuses user namespaces, so the file skips here (13 skipped, 0 failed). The forge check, on a host that allows bwrap, will be the first real run.

What it tests

  • It starts a real bwrap shell through srt, using the sandbox settings the crew gives Claude Code (claudeSettings(sandboxPolicy(...))).
  • Files: it tries to read two files outside the worktree: a denied data file (forge.db) and another crew's file. Each is tried four ways: by its path, and through /proc/self/root, /proc/1/root and /proc/<pid>/root. Here <pid> is a sleep process the test starts outside the sandbox. That is eight tests, and each fails if its file's contents come back.
  • Environments: the outside process holds a secret in its environment. One test checks /proc/1/environ for it. Another checks every /proc/<pid>/environ the shell can list.
  • Controls: the shell must be able to read a file in its own worktree and its own /proc/self/environ. Without these, a probe that never ran would pass as "all refused".
  • Skipping: where no sandbox can be made, the file skips and prints skipped: test/crew-sandbox-escape.test.ts: <reason>. Where a sandbox can be made but rg or srt is missing, the file fails rather than skips.

Gate list: the gate runs every test/*.test.ts file not listed in EXCLUDED (scripts/gate.ts). The new file isn't listed there, so gateFiles() already includes it and no list needed editing.

Left out

  • From #10479: the write and network attempts, the docs/ops/RED-TEAM.md table, and its unrelated changes to src/spawn/cell.ts, test/spawn-cell.test.ts and test/child-quota.test.ts.
  • No module is deleted, and scripts/unreached-allow.json is unchanged.
  • I made no keep_plan calls because that tool wasn't available in this run.

Checks run here: bun scripts/gate.ts and bun x tsc --noEmit are clean, and test/check-list.test.ts and test/unreached.test.ts pass. An earlier call I meant only to list the gated files, bun scripts/test-shards.ts --gate --list, started the gated suite instead; I stopped it before it finished and nothing in the working tree changed.

To try it: run bun test test/crew-sandbox-escape.test.ts on a host where bwrap --unshare-user works, with rg installed and bun install done.

Related to #10552

🤖 Generated with Claude Code

Closes #10552

commented

This pull adds test/crew-sandbox-escape.test.ts. It contains only the read attempts from the parked #10479 (at 4ea33a857367), and each attempt is now its own test. None of the attempts has run yet: this sandbox refuses user namespaces, so the file skips here (13 skipped, 0 failed). The forge check, on a host that allows bwrap, will be the first real run.

What it tests

  • It starts a real bwrap shell through srt, using the sandbox settings the crew gives Claude Code (claudeSettings(sandboxPolicy(...))).
  • Files: it tries to read two files outside the worktree: a denied data file (forge.db) and another crew's file. Each is tried four ways: by its path, and through /proc/self/root, /proc/1/root and /proc/<pid>/root. Here <pid> is a sleep process the test starts outside the sandbox. That is eight tests, and each fails if its file's contents come back.
  • Environments: the outside process holds a secret in its environment. One test checks /proc/1/environ for it. Another checks every /proc/<pid>/environ the shell can list.
  • Controls: the shell must be able to read a file in its own worktree and its own /proc/self/environ. Without these, a probe that never ran would pass as "all refused".
  • Skipping: where no sandbox can be made, the file skips and prints skipped: test/crew-sandbox-escape.test.ts: <reason>. Where a sandbox can be made but rg or srt is missing, the file fails rather than skips.

Gate list: the gate runs every test/*.test.ts file not listed in EXCLUDED (scripts/gate.ts). The new file isn't listed there, so gateFiles() already includes it and no list needed editing.

Left out

  • From #10479: the write and network attempts, the docs/ops/RED-TEAM.md table, and its unrelated changes to src/spawn/cell.ts, test/spawn-cell.test.ts and test/child-quota.test.ts.
  • No module is deleted, and scripts/unreached-allow.json is unchanged.
  • I made no keep_plan calls because that tool wasn't available in this run.

Checks run here: bun scripts/gate.ts and bun x tsc --noEmit are clean, and test/check-list.test.ts and test/unreached.test.ts pass. An earlier call I meant only to list the gated files, bun scripts/test-shards.ts --gate --list, started the gated suite instead; I stopped it before it finished and nothing in the working tree changed.

To try it: run bun test test/crew-sandbox-escape.test.ts on a host where bwrap --unshare-user works, with rg installed and bun install done.

Related to #10552

🤖 Generated with Claude Code

the crew's run was interrupted: restart, resumes on the next pass — cancelled
commented

This pull adds test/crew-sandbox-escape.test.ts, which holds the read attempts from the parked #10479, each now its own test. It also lists the file in the gate's EXCLUDED (scripts/gate.ts), next to its sibling test/crew-sandbox-real.test.ts.

What the file tests

  • It starts a real bwrap shell through srt, using the sandbox settings the crew gives Claude Code (claudeSettings(sandboxPolicy(...))).
  • Files: it tries to read two files outside the worktree: a denied data file (forge.db) and another crew's file. Each is tried four ways: by its path, and through /proc/self/root, /proc/1/root and /proc/<pid>/root. Here <pid> is a sleep process the test starts outside the sandbox. That is eight tests, and each fails if its file's contents come back.
  • Environments: the outside process holds a secret in its environment. One test checks /proc/1/environ for it, and another checks every /proc/<pid>/environ the shell can list.
  • Controls: the shell must be able to read a file in its own worktree and its own /proc/self/environ. Without these, a probe that never ran would pass as "all refused".

Why it is out of the gate The first check failed because the check host can make a sandbox but has no ripgrep (rg) on its PATH. srt needs rg, and @anthropic-ai/sandbox-runtime does not ship it. So the file's guard ("a sandbox can be made here but its runtime is missing") threw while the file was loading, and shard 11 crashed outside any test. The sibling crew-sandbox-real.test.ts is already excluded from the gate for the same reason. I didn't change the guard to skip, because a sandbox test that quietly skips where a sandbox exists would hide the gap. Its EXCLUDED entry names the reason and #10552. Excluded files run nightly (outsideFiles), and a failure there files a Red: issue.

Not run yet: none of the attempts has actually run anywhere. This sandbox refuses user namespaces, so the file skips here (13 skipped), and the check host is missing rg. Installing rg on the check host would let both files go back into the gate.

Checks run here: bun scripts/gate.ts, bun x tsc --noEmit, test/check-list.test.ts, and the 17 files scripts/fast.ts named (214 pass, 0 fail) all pass. To try it, run bun test test/crew-sandbox-escape.test.ts on a host that allows user namespaces and has rg installed.

🤖 Generated with Claude Code

Changes requested — before the latest push

The test file reads well. Its controls keep a probe that never ran from passing, and it names itself when skipped. But the pull puts it in EXCLUDED in scripts/gate.ts, so the forge check never runs it. The issue says the new file goes into the gate list and the forge check runs it. Excluded, it runs only nightly, and nothing has run it anywhere yet. A test of the sandbox boundary that never runs on a push guards nothing. Fix first: take the file out of EXCLUDED and make it load on the check host, which has no rg. Pick one: (a) have the test find rg itself, for example a pinned dev dependency, with the reason given in the pull; (b) have the check host provide rg, in its own pull that this one waits on; or (c) skip with a named reason only when rg is missing and the host is not the forge check (for example an env flag the check sets), so the check still fails loudly. Do not leave it excluded. Everything else is fine: no deletion, unreached-allow.json is untouched, and README is untouched. Note that the preview shows no change, because only a test file and the gate list change.

commented

This pull adds test/crew-sandbox-escape.test.ts, which holds the read attempts from the parked #10479, each now its own test. The file is in the gate, so the forge check runs it on every push. None of the attempts has run yet: this sandbox can't create user namespaces, so the file skips here (13 skipped). This push's check will be the first real run.

What it tests

  • It starts a real bwrap shell through srt, using the sandbox settings the crew gives Claude Code (claudeSettings(sandboxPolicy(...))).
  • Files: it tries to read two files outside the worktree: a denied data file (forge.db) and another crew's file. Each is tried four ways: by its path, and through /proc/self/root, /proc/1/root and /proc/<pid>/root of a sleep process the test starts outside the sandbox. That is eight tests, and each fails if its file's contents come back.
  • Environments: the outside process holds a secret in its environment. One test checks /proc/1/environ for it, and another checks every /proc/<pid>/environ the shell can list.
  • Controls: the shell must be able to read a file in its own worktree and its own /proc/self/environ. Without these, a probe that never ran would pass as "all refused".
  • Skipping: the file skips, saying why, only where no sandbox can be made. Where one can be made but srt or rg is missing, the file fails to load. The check sees that as a red shard, so it can't pass quietly.

Why it adds a dependency The check host can create a sandbox but has no rg on its PATH, and srt needs rg. That is what crashed shard 11 on the first push. This change takes review option (a):

  • It adds @vscode/ripgrep as a dev dependency, pinned exactly at 1.18.0.
  • In 1.18 the rg binary ships inside a per-platform package with no install script, so bun install alone provides it.
  • The test passes that binary to srt as its ripgrep.command setting.
  • The reason for the package is in docs/REFERENCE.md, which test/dependencies-named.test.ts checks.

The EXCLUDED entry from the previous push is gone. test/crew-sandbox-real.test.ts still uses the PATH rg and stays excluded under #1238; I didn't change it in this pull.

Checks run here: bun scripts/gate.ts and bun x tsc --noEmit pass. scripts/fast.ts exited 2 because the lockfile makes the diff too broad, so I picked 25 test files by hand: the 17 it named last time, plus the gate-list and dependency tests. 282 passed, 13 skipped (this file), 0 failed.

To try it, run bun install && bun test test/crew-sandbox-escape.test.ts on a host that allows user namespaces.

🤖 Generated with Claude Code

Approved

Approved. The only change since my last review is how the file gets its rg: a pinned @vscode/ripgrep dev dependency, handed to srt as ripgrep.command. It has a reason line in docs/REFERENCE.md, so test/dependencies-named.test.ts is satisfied. The lockfile change is only that package and its per-platform binaries. Where a sandbox can be made but srt or rg is missing, the file fails loudly instead of skipping. Where it cannot be made, the file skips and names itself. The two controls stop a probe that never ran from passing as all refused. The EXCLUDED entry from the earlier push is gone. No module is deleted and unreached-allow.json is untouched. Non-blocking: crew-sandbox-real.test.ts could use the same pinned rg and rejoin the gate later.

Sign in to join this conversation.

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs