| 1 | # Sandbox threat model |
| 2 | |
| 3 | Codewhale can launch shell commands proposed by a model. Approval policy, |
| 4 | workspace-aware tools, and an operating-system command wrapper are separate |
| 5 | controls: an approval is not a sandbox, and selecting `workspace-write` does |
| 6 | not prove that the current platform has an OS wrapper available. |
| 7 | |
| 8 | This document describes only behavior wired into the command execution path. |
| 9 | See [Authorization order](AUTHORIZATION_ORDER.md) for the policy layers that |
| 10 | run before execution reaches this boundary. |
| 11 | |
| 12 | ## Platform overview |
| 13 | |
| 14 | | Mechanism | Platform | Selection | What Codewhale reports | |
| 15 | |---|---|---|---| |
| 16 | | Seatbelt (`sandbox-exec`) | macOS | Automatic when the runtime probe succeeds | `macos-seatbelt` | |
| 17 | | Bubblewrap (`/usr/bin/bwrap`) | Linux | `prefer_bwrap = true` and the file is executable | `linux-bwrap` | |
| 18 | | No OS wrapper | Linux without usable opt-in bwrap | Default | `none` | |
| 19 | | No OS wrapper | Windows | Current implementation | `none` | |
| 20 | | OpenSandbox-compatible service | Any supported host | `sandbox_backend = "opensandbox"` | External execution path | |
| 21 | |
| 22 | The repository contains a seccomp implementation module plus a future Windows |
| 23 | helper contract. They are not wired into child-command launch, so Codewhale |
| 24 | does not advertise them as active sandboxes. Source-only sandbox code is not |
| 25 | evidence that a command was restricted. |
| 26 | |
| 27 | ## macOS: Seatbelt |
| 28 | |
| 29 | Codewhale probes `/usr/bin/sandbox-exec` by running a minimal profile. When the |
| 30 | probe succeeds and the selected `SandboxPolicy` requests a sandbox, the child |
| 31 | command is wrapped with a generated Seatbelt profile. |
| 32 | |
| 33 | The profile can provide: |
| 34 | |
| 35 | - broad filesystem reads; |
| 36 | - writes limited by the selected policy, including the workspace and specific |
| 37 | runtime/cache paths needed by supported tools; |
| 38 | - network access only when the policy enables it. |
| 39 | |
| 40 | If the probe fails or `sandbox-exec` is unavailable, Codewhale reports no OS |
| 41 | sandbox and launches the command without a Seatbelt wrapper. It does not set a |
| 42 | Seatbelt marker on that fallback. |
| 43 | |
| 44 | ## Linux: opt-in bubblewrap |
| 45 | |
| 46 | Linux command sandboxing is opt-in. Set the top-level configuration key: |
| 47 | |
| 48 | ```toml |
| 49 | prefer_bwrap = true |
| 50 | ``` |
| 51 | |
| 52 | Codewhale selects bubblewrap only when `/usr/bin/bwrap` is a regular executable |
| 53 | file. The wrapper derives its mounts and network namespace from the resolved |
| 54 | `SandboxPolicy`: |
| 55 | |
| 56 | ```text |
| 57 | /usr/bin/bwrap \ |
| 58 | --unshare-all \ |
| 59 | [--share-net] \ |
| 60 | --ro-bind / / \ |
| 61 | --dev /dev \ |
| 62 | --proc /proc \ |
| 63 | --tmpfs /tmp \ |
| 64 | [--dev-bind <device-root> <device-root> ...] \ |
| 65 | --bind <writable-root> <writable-root> ... \ |
| 66 | --ro-bind <protected-descendant> <protected-descendant> ... \ |
| 67 | [--ro-bind <extra-ro-root> <extra-ro-root> ...] \ |
| 68 | --chdir <cwd> \ |
| 69 | -- <program> <args> |
| 70 | ``` |
| 71 | |
| 72 | The sandbox always gets a private `/dev` (fresh device nodes, so `>/dev/null` |
| 73 | works), a private `/proc`, and a tmpfs `/tmp` (#5410). Two optional top-level |
| 74 | config keys extend the mounts: `bwrap_ro_roots` (extra host paths bind-mounted |
| 75 | read-only, applied last so they can narrow a policy-writable path) and |
| 76 | `bwrap_dev_roots` (host character/block device nodes bind-mounted read-write; |
| 77 | directories are never honored). Missing paths are skipped silently. |
| 78 | |
| 79 | That gives the child a read-only root view. For `workspace-write`, every safe, |
| 80 | existing policy root is mounted read-write: the working directory, configured |
| 81 | additional roots, `/tmp` and `TMPDIR` unless excluded, and verified Git |
| 82 | worktree metadata roots. Existing `.codewhale` and `.deepseek` descendants are |
| 83 | remounted read-only after their writable parent. Missing paths, non-directory |
| 84 | paths, and `/` are not promoted to writable mounts. |
| 85 | |
| 86 | For `read-only`, there are no writable binds, so the working directory remains |
| 87 | inside the read-only root view. `--unshare-all` isolates the network namespace |
| 88 | by default. Codewhale adds `--share-net` only when the policy's |
| 89 | `network_access` is true. `danger-full-access` and `external-sandbox` bypass the |
| 90 | local wrapper entirely. |
| 91 | |
| 92 | If the user does not opt in, or `/usr/bin/bwrap` is missing or non-executable, |
| 93 | Codewhale reports `none` and launches the command without a Linux OS wrapper. |
| 94 | There is no marker-only fallback to a different Linux sandbox. |
| 95 | |
| 96 | Install bubblewrap separately when this opt-in fits the workflow: |
| 97 | |
| 98 | - Ubuntu/Debian: `apt install bubblewrap` |
| 99 | - Fedora: `dnf install bubblewrap` |
| 100 | - Arch: `pacman -S bubblewrap` |
| 101 | |
| 102 | Codewhale does not vendor bubblewrap. |
| 103 | |
| 104 | ## Windows: no advertised OS sandbox |
| 105 | |
| 106 | The Windows command path currently reports no OS sandbox. The source tree has |
| 107 | a future helper contract for Job Object process-tree cleanup, but it is not |
| 108 | wired into selection and must not be described as any of the following: |
| 109 | |
| 110 | - read-only filesystem or workspace-write enforcement; |
| 111 | - network blocking; |
| 112 | - registry isolation; |
| 113 | - restricted-token or AppContainer isolation. |
| 114 | |
| 115 | Windows host permissions and approval policy still apply, but they are not a |
| 116 | Codewhale OS command sandbox. |
| 117 | |
| 118 | ## Linux process hardening is not a command sandbox |
| 119 | |
| 120 | At startup on Linux, Codewhale best-effort applies `PR_SET_DUMPABLE=0`, |
| 121 | `PR_SET_NO_NEW_PRIVS=1`, and `RLIMIT_CORE=0` to its own process. Each failure is |
| 122 | logged and startup continues. These controls reduce process-inspection, |
| 123 | privilege-escalation, and core-dump risk; they do not create filesystem or |
| 124 | network isolation for a child command and are not listed as a sandbox backend. |
| 125 | |
| 126 | The one exception is the startup posture itself: when the startup sandbox mode |
| 127 | resolves to `danger-full-access` (via `CODEWHALE_SANDBOX_MODE` or the config |
| 128 | file's `sandbox_mode` key), `PR_SET_NO_NEW_PRIVS` is skipped so that |
| 129 | `sudo`/`su`/setuid helpers keep working from the agent shell (#5723) — "full |
| 130 | access" means it. Every narrower posture keeps the flag as defense-in-depth, |
| 131 | and `CODEWHALE_NO_NEW_PRIVS` overrides the posture in both directions |
| 132 | (#5413): a falsey value always skips the flag, a truthy value always sets it. |
| 133 | The flag is irreversible for the process tree, so the decision can only be |
| 134 | made at launch; per-call sandbox escalation inside a session cannot lift it. |
| 135 | |
| 136 | ## External OpenSandbox execution |
| 137 | |
| 138 | When `sandbox_backend = "opensandbox"` is configured, shell execution is sent |
| 139 | to the configured OpenSandbox-compatible HTTP endpoint instead of starting a |
| 140 | local child. Codewhale validates the request/response contract, but isolation |
| 141 | guarantees belong to the configured service and its operator. |
| 142 | |
| 143 | ```toml |
| 144 | sandbox_backend = "opensandbox" |
| 145 | sandbox_url = "http://localhost:8080" |
| 146 | sandbox_api_key = "YOUR_API_KEY" |
| 147 | ``` |
| 148 | |
| 149 | `sandbox_backend = "none"` (or omitting the key) keeps local execution. |
| 150 | Unsupported backend settings refuse shell execution; they never silently select |
| 151 | local execution. Choose a supported backend or explicitly select `none`. |
| 152 | |
| 153 | ## Policies and fallbacks |
| 154 | |
| 155 | The local `sandbox_mode` values are: |
| 156 | |
| 157 | ```toml |
| 158 | sandbox_mode = "workspace-write" # read-only | workspace-write | danger-full-access | external-sandbox |
| 159 | ``` |
| 160 | |
| 161 | - `read-only` and `workspace-write` are enforced by Seatbelt or bubblewrap only |
| 162 | when that wrapper is selected and available. |
| 163 | - `danger-full-access` deliberately bypasses the local OS wrapper. On Linux it |
| 164 | also skips the `PR_SET_NO_NEW_PRIVS` process-hardening flag at startup so |
| 165 | `sudo`/setuid workflows keep running (#5723); see the process-hardening |
| 166 | section above. |
| 167 | - `external-sandbox` declares that execution is already externally isolated |
| 168 | and bypasses a second local wrapper. |
| 169 | - When no wrapper is selected, the shell command runs without Codewhale OS |
| 170 | isolation. Approval rules and workspace-aware native file tools remain |
| 171 | separate controls. |
| 172 | |
| 173 | Canonical environment overrides exist for `sandbox_mode` and the external |
| 174 | backend: |
| 175 | |
| 176 | - `CODEWHALE_SANDBOX_MODE` |
| 177 | - `CODEWHALE_SANDBOX_BACKEND` |
| 178 | - `CODEWHALE_SANDBOX_URL` |
| 179 | - `CODEWHALE_SANDBOX_API_KEY` |
| 180 | |
| 181 | There is no `CODEWHALE_PREFER_BWRAP` environment override; use the top-level |
| 182 | `prefer_bwrap` config key. |
| 183 | |
| 184 | ## Diagnostics and failure attribution |
| 185 | |
| 186 | `codewhale setup --status`, `codewhale doctor`, `codewhale doctor --json`, and |
| 187 | the `diagnostics` tool report the locally available wrapper after applying the |
| 188 | resolved bubblewrap preference. An individual command can still bypass that |
| 189 | wrapper when its policy does not request sandboxing. On Linux, merely finding |
| 190 | a sandbox-related syscall or source module does not make `sandbox_available` |
| 191 | true. |
| 192 | |
| 193 | Denial attribution is intentionally conservative: |
| 194 | |
| 195 | - Seatbelt uses its wrapper-specific denial patterns. |
| 196 | - Bubblewrap setup errors must be prefixed by `bwrap:`; a read-only-filesystem |
| 197 | error from the bwrap filesystem view can also identify the boundary. |
| 198 | - A child command's generic `Permission denied` or `Operation not permitted` |
| 199 | is not, by itself, proof that Codewhale's sandbox blocked it. |
| 200 | - Unsandboxed command failures are never labeled sandbox denials. |
| 201 | |
| 202 | ## Limitations |
| 203 | |
| 204 | - Availability is checked before launch; the selected wrapper can still fail |
| 205 | because of host policy, container restrictions, or a race after the probe. |
| 206 | - Bubblewrap ignores a configured writable root if it is missing, is not a |
| 207 | directory, or canonicalizes to `/`; a path can also disappear between policy |
| 208 | resolution and wrapper launch. |
| 209 | - Seatbelt profiles are generated at runtime and must be tested against the |
| 210 | commands they are expected to support. |
| 211 | - No current local wrapper is advertised on Windows. |
| 212 | - An external sandbox backend is only as strong as its configured service. |
| 213 | - No sandbox protects against kernel vulnerabilities or all resource-exhaustion |
| 214 | and side-channel attacks. |
| 215 | |
| 216 | ## Implementation references |
| 217 | |
| 218 | - `crates/tui/src/sandbox/mod.rs` — truthful selection and public capability markers |
| 219 | - `crates/tui/src/sandbox/seatbelt.rs` — macOS wrapper and availability probe |
| 220 | - `crates/tui/src/sandbox/bwrap.rs` — Linux opt-in wrapper |
| 221 | - `crates/tui/src/sandbox/process_hardening.rs` — Linux parent-process hardening |
| 222 | - `crates/tui/src/sandbox/backend.rs` — external backend selection |
| 223 | - `crates/tui/src/tools/diagnostics.rs` — machine-readable diagnostics |
| 224 |