# Sandbox

Every `run_command` / `background_command`, script tool, and `test_tool` execution is wrapped by `go-pkg/sandbox`:

| Platform | Mechanism |
|---|---|
| Linux | bubblewrap (`bwrap`) |
| macOS | `sandbox-exec` |

Sandbox restrictions: no privileged execution, restricted filesystem write scope, per-call network access, configurable CPU/memory limits.

Since v1.0.25 `run_command` (and, since v1.3.1, the background variant; `run_command_background` was renamed `background_command` (Removed in v1.3.2)) runs with networking denied (`NetworkDeny`) unless the call sets `network: true`, which switches that one call to `NetworkAllow`; a networked call never counts as read-only, so it confirms under `safe` and runs without a prompt under `auto`. Since v1.3.0 `sandbox: false` passes `AllowAll`, so that one call runs with no sandbox at all, only after the user's system password is verified. Since v1.3.2 an argv `['sudo', '<binary>', ...]` also runs outside the sandbox, as `sudo -n <binary> ...` on the sudo ticket, once its password-backed `sudo: <cmd>` confirmation is approved. Script tools (`script_*`, `ext_*`) and `test_tool` call `sandbox.Wrap` with no option, so they keep network access. The networked `run_script` tool that briefly existed during v1.0.25 development never shipped in a release; use `run_command` with `network: true`.

## Three callers, one entry point

The sandbox has exactly three call paths, all calling `sandbox.Wrap(ctx, binary, args, workDir, opt)` directly:

1. `run_command` and `background_command` — arbitrary user-issued commands, sharing one `runCommand` path (`internal/tools/runCommand.go`, `runCommandBackground.go`)
2. `toolAdapter/script` execute — script-tool extensions (`script_*`, `ext_*`)
3. `test_tool` — trial runs of a script tool's `script.py` (60 s limit)

Since v1.3.0 the command tools and `test_tool` start the wrapped process with `sandbox.Run` (go-pkg v0.13.17), which kills the process group after the command exits; the script adapter starts and waits on the process itself. There is no wrapper layer between callers and `sandbox.Wrap`. Adding behavior (e.g., new resource limits) means contributing to `go-pkg/sandbox`, not adding shims in agenvoy.

## Policy injection

`filesystem.LoadRuntime()` reads `~/.config/agenvoy/config.json` at startup and hands the merged policy to `go-pkg/sandbox` and `go-pkg/filesystem`. Some policies ship embedded in the binary and merge with the user's entries; others exist only as user config.

| Policy | Embedded default | config.json key | Purpose |
|---|---|---|---|
| Sensitive paths | `configs/jsons/sensitive_path.json` | `sensitive_path` | Paths holding credentials, key material, shell startup files or tool settings (since v1.3.7 also `.git`, `.claude`, `.bashrc` / `.zshrc` and the other shell rc files, `.mcp.json`), for reads and writes alike — reachable only after a per-session grant: a plain confirmation inside `$HOME`, the system password outside it. Four buckets: `dirs`, `files`, `prefixes`, `extensions` |
| Exclusions | `configs/jsons/exclude_list.json` | — | Paths excluded from listing / walking / searching |
| Read-only commands | `configs/jsons/read_only_command.json` (532 entries since v1.3.3, 409 after `cd` was dropped in v1.3.0) | `read_only_command` | Commands that skip the confirm gate. Since v1.0.25 a `run_command` call (since v1.3.1 also the background variant, now `background_command`) matches when the binary alone, the binary plus its first argument, or the binary plus its first two arguments is on the list (`git` global flags such as `-C <dir>` are skipped first); any argument that is a sensitive path, or `network: true`, cancels the match |
| Denied commands | — | `denied_command` | Binaries `run_command` refuses outright, inside `sh -c` too |
| Denied paths | — | `denied_path` | Paths permanently off limits for reads and writes; no prompt can approve them. Entries must be absolute or `~/`-prefixed, the root is refused, and symlink targets are denied as well |
| SSRF exemptions | — | `net_white_list` | Hosts exempt from the `http_request` SSRF guard |

The command policy is a **denylist**. There is no binary allowlist: `white_list` and `path_white_list` were removed, and `sensitive_map` was renamed to `sensitive_path`; a config still carrying any of these keys is ignored silently (the startup warning was dropped in v1.1.2). Path access outside `$HOME` is granted per session through the confirmation prompt, not through a config list.

`denied_path` is handed to `go-pkg/sandbox` as its denied map; `boundary.Resolve` and `boundary.WriteBinds` check it on the agenvoy side before any file tool or `write_paths` bind proceeds.
