Skip to content

Commit ede628b

Browse files
Merge pull request #1428 from corbitsdev/cl-10663-shell-sandboxing-adopt-sandbox-runtime-opt-in-fail-closed
feat(shell): opt-in os sandboxing via sandbox-runtime
2 parents 152139f + 1bfc073 commit ede628b

22 files changed

Lines changed: 1650 additions & 40 deletions

‎bun.lock‎

Lines changed: 25 additions & 8 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎docs/IMPLEMENTATION.md‎

Lines changed: 26 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -278,6 +278,22 @@ Provider and model configuration lives in JSON settings files. The global file h
278278
- `timeoutMs` — default bound when foreground `run_shell` omits `timeout`. Unset keeps the built-in 120000ms default. A positive per-call `timeout` is the bound with no ceiling.
279279
- `maxTimeoutMs` — clamps that default path only; it does not clamp a per-call override.
280280

281+
Optional `shell.sandbox` opts shell spawns and stdio MCP servers into an OS-level sandbox via `@anthropic-ai/sandbox-runtime` (path sandboxing on macOS `sandbox-exec`, bubblewrap on Linux). **Default off**; with the sandbox disabled the spawn paths run exactly as before, byte-identical:
282+
283+
```json
284+
"shell": {
285+
"sandbox": {
286+
"enabled": true,
287+
"network": { "allowedHosts": ["api.github.com", "*.example.com"] }
288+
}
289+
}
290+
```
291+
292+
- `enabled` — default **false**. When true, foreground + background `run_shell`, stdio MCP server launches, and lifecycle hooks (discovered under the sandbox-writable session `.corbits/hooks`) wrap into the sandbox: writes are allowed only inside the workspace, the OS tmpdir, and `/tmp/claude`; reads deny the secret-guard denylist classes (`.env*`, `.ssh/`, `.aws/`, key material, credential stores, browser cookie stores, shell history, ...) plus the active `--config` settings path; known provider API-key env vars are unset inside the sandbox. A sandboxed command that cannot be wrapped (missing/broken runtime) **fails closed** — the command never runs — with a `shell sandbox is enabled but unavailable` error; disable the setting to restore raw spawns. Linux additionally needs bubblewrap + socat + ripgrep installed.
293+
- `network.allowedHosts` — operator pre-approved domain allowlist (supports `*.wildcards`). Every other host asks through the normal permission gate (auto mode and yolo auto-allow it as they would a `web_fetch`; yolo relaxes the _asks_, never the sandbox itself).
294+
295+
One sandboxed composition per process: sandbox-runtime's manager is a process-global singleton, so a second `shell.sandbox` composition with a different workspace/allowlist while one is live fails loudly instead of inheriting it. Matching compositions share the singleton by refcount; the last session's teardown resets it.
296+
281297
Optional `tools` block to arm the outer per-tool wall-clock budget (unset leaves the watchdog unarmed):
282298

283299
```json
@@ -313,14 +329,16 @@ Provider and model configuration lives in JSON settings files. The global file h
313329

314330
All `tools.*` keys live in the global settings file only — there is no per-repo override in `.corbits/settings.json` (unlike `sessionMode`).
315331

316-
| Key | Default | Effect |
317-
| ----------------------- | ------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------ |
318-
| `tools.timeoutMs` | unset (watchdog unarmed) | Outer wall-clock budget per tool `run()` when set |
319-
| `tools.maxTimeoutMs` | unset | Cap on the outer budget when set; does not cap a longer requested `run_shell` |
320-
| `tools.waitForApproval` | `true` | Freeze the budget while a permission prompt is open (freeze capped at 30 min); `false` keeps the clock ticking and auto-dismisses the prompt on expiry |
321-
| `mcp.timeoutMs` | **300000** (5 min) — armed even when unset | Outer wall-clock budget for `mcp__*` tool calls specifically; capped by `tools.maxTimeoutMs` when set |
322-
| `shell.timeoutMs` | **120000** (2 min) foreground default | Foreground `run_shell` bound when the call omits `timeout`; background has no default; a per-call `timeout` is the bound with no ceiling |
323-
| `shell.maxTimeoutMs` | unset | Clamps the foreground default path only; does not clamp a per-call `run_shell` timeout |
332+
| Key | Default | Effect |
333+
| ------------------------------------ | ----------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ |
334+
| `tools.timeoutMs` | unset (watchdog unarmed) | Outer wall-clock budget per tool `run()` when set |
335+
| `tools.maxTimeoutMs` | unset | Cap on the outer budget when set; does not cap a longer requested `run_shell` |
336+
| `tools.waitForApproval` | `true` | Freeze the budget while a permission prompt is open (freeze capped at 30 min); `false` keeps the clock ticking and auto-dismisses the prompt on expiry |
337+
| `mcp.timeoutMs` | **300000** (5 min) — armed even when unset | Outer wall-clock budget for `mcp__*` tool calls specifically; capped by `tools.maxTimeoutMs` when set |
338+
| `shell.timeoutMs` | **120000** (2 min) foreground default | Foreground `run_shell` bound when the call omits `timeout`; background has no default; a per-call `timeout` is the bound with no ceiling |
339+
| `shell.maxTimeoutMs` | unset | Clamps the foreground default path only; does not clamp a per-call `run_shell` timeout |
340+
| `shell.sandbox.enabled` | `false` (opt-in, fail closed on runtime errors) | OS sandbox for `run_shell` and stdio MCP launches via `@anthropic-ai/sandbox-runtime` (workspace+tmp writes, secret denylist reads) |
341+
| `shell.sandbox.network.allowedHosts` | `[]` (every other host asks) | Operator pre-approved host allowlist; unmatched hosts ask through the permission gate — yolo auto-allows the ask, the OS sandbox stays on |
324342

325343
The `waitForApproval` default is resolved once at the watchdog boundary (`resolveWaitForApproval`); toggling **Settings → Tools** updates the live config for the next tool call and persists the value here.
326344

‎package.json‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@
8686
"tar": "^7.5.1"
8787
},
8888
"dependencies": {
89+
"@anthropic-ai/sandbox-runtime": "0.0.79",
8990
"@corbits/codex-provider": "0.1.1",
9091
"@corbits/oauth-core": "0.2.0",
9192
"@corbits/openai-responses": "0.2.1",

0 commit comments

Comments
 (0)