| 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 | --bind <writable-root> <writable-root> ... \ |
| 62 | --ro-bind <protected-descendant> <protected-descendant> ... \ |
| 63 | --chdir <cwd> \ |
| 64 | -- <program> <args> |
| 65 | ``` |
| 66 | |
| 67 | That gives the child a read-only root view. For `workspace-write`, every safe, |
| 68 | existing policy root is mounted read-write: the working directory, configured |
| 69 | additional roots, `/tmp` and `TMPDIR` unless excluded, and verified Git |
| 70 | worktree metadata roots. Existing `.codewhale` and `.deepseek` descendants are |
| 71 | remounted read-only after their writable parent. Missing paths, non-directory |
| 72 | paths, and `/` are not promoted to writable mounts. |
| 73 | |
| 74 | For `read-only`, there are no writable binds, so the working directory remains |
| 75 | inside the read-only root view. `--unshare-all` isolates the network namespace |
| 76 | by default. Codewhale adds `--share-net` only when the policy's |
| 77 | `network_access` is true. `danger-full-access` and `external-sandbox` bypass the |
| 78 | local wrapper entirely. |
| 79 | |
| 80 | If the user does not opt in, or `/usr/bin/bwrap` is missing or non-executable, |
| 81 | Codewhale reports `none` and launches the command without a Linux OS wrapper. |
| 82 | There is no marker-only fallback to a different Linux sandbox. |
| 83 | |
| 84 | Install bubblewrap separately when this opt-in fits the workflow: |
| 85 | |
| 86 | - Ubuntu/Debian: `apt install bubblewrap` |
| 87 | - Fedora: `dnf install bubblewrap` |
| 88 | - Arch: `pacman -S bubblewrap` |
| 89 | |
| 90 | Codewhale does not vendor bubblewrap. |
| 91 | |
| 92 | ## Windows: no advertised OS sandbox |
| 93 | |
| 94 | The Windows command path currently reports no OS sandbox. The source tree has |
| 95 | a future helper contract for Job Object process-tree cleanup, but it is not |
| 96 | wired into selection and must not be described as any of the following: |
| 97 | |
| 98 | - read-only filesystem or workspace-write enforcement; |
| 99 | - network blocking; |
| 100 | - registry isolation; |
| 101 | - restricted-token or AppContainer isolation. |
| 102 | |
| 103 | Windows host permissions and approval policy still apply, but they are not a |
| 104 | Codewhale OS command sandbox. |
| 105 | |
| 106 | ## Linux process hardening is not a command sandbox |
| 107 | |
| 108 | At startup on Linux, Codewhale best-effort applies `PR_SET_DUMPABLE=0`, |
| 109 | `PR_SET_NO_NEW_PRIVS=1`, and `RLIMIT_CORE=0` to its own process. Each failure is |
| 110 | logged and startup continues. These controls reduce process-inspection, |
| 111 | privilege-escalation, and core-dump risk; they do not create filesystem or |
| 112 | network isolation for a child command and are not listed as a sandbox backend. |
| 113 | |
| 114 | ## External OpenSandbox execution |
| 115 | |
| 116 | When `sandbox_backend = "opensandbox"` is configured, shell execution is sent |
| 117 | to the configured OpenSandbox-compatible HTTP endpoint instead of starting a |
| 118 | local child. Codewhale validates the request/response contract, but isolation |
| 119 | guarantees belong to the configured service and its operator. |
| 120 | |
| 121 | ```toml |
| 122 | sandbox_backend = "opensandbox" |
| 123 | sandbox_url = "http://localhost:8080" |
| 124 | sandbox_api_key = "YOUR_API_KEY" |
| 125 | ``` |
| 126 | |
| 127 | `sandbox_backend = "none"` (or omitting the key) keeps local execution. |
| 128 | |
| 129 | ## Policies and fallbacks |
| 130 | |
| 131 | The local `sandbox_mode` values are: |
| 132 | |
| 133 | ```toml |
| 134 | sandbox_mode = "workspace-write" # read-only | workspace-write | danger-full-access | external-sandbox |
| 135 | ``` |
| 136 | |
| 137 | - `read-only` and `workspace-write` are enforced by Seatbelt or bubblewrap only |
| 138 | when that wrapper is selected and available. |
| 139 | - `danger-full-access` deliberately bypasses the local OS wrapper. |
| 140 | - `external-sandbox` declares that execution is already externally isolated |
| 141 | and bypasses a second local wrapper. |
| 142 | - When no wrapper is selected, the shell command runs without Codewhale OS |
| 143 | isolation. Approval rules and workspace-aware native file tools remain |
| 144 | separate controls. |
| 145 | |
| 146 | Canonical environment overrides exist for `sandbox_mode` and the external |
| 147 | backend: |
| 148 | |
| 149 | - `CODEWHALE_SANDBOX_MODE` |
| 150 | - `CODEWHALE_SANDBOX_BACKEND` |
| 151 | - `CODEWHALE_SANDBOX_URL` |
| 152 | - `CODEWHALE_SANDBOX_API_KEY` |
| 153 | |
| 154 | There is no `CODEWHALE_PREFER_BWRAP` environment override; use the top-level |
| 155 | `prefer_bwrap` config key. |
| 156 | |
| 157 | ## Diagnostics and failure attribution |
| 158 | |
| 159 | `codewhale setup --status`, `codewhale doctor`, `codewhale doctor --json`, and |
| 160 | the `diagnostics` tool report the locally available wrapper after applying the |
| 161 | resolved bubblewrap preference. An individual command can still bypass that |
| 162 | wrapper when its policy does not request sandboxing. On Linux, merely finding |
| 163 | a sandbox-related syscall or source module does not make `sandbox_available` |
| 164 | true. |
| 165 | |
| 166 | Denial attribution is intentionally conservative: |
| 167 | |
| 168 | - Seatbelt uses its wrapper-specific denial patterns. |
| 169 | - Bubblewrap setup errors must be prefixed by `bwrap:`; a read-only-filesystem |
| 170 | error from the bwrap filesystem view can also identify the boundary. |
| 171 | - A child command's generic `Permission denied` or `Operation not permitted` |
| 172 | is not, by itself, proof that Codewhale's sandbox blocked it. |
| 173 | - Unsandboxed command failures are never labeled sandbox denials. |
| 174 | |
| 175 | ## Limitations |
| 176 | |
| 177 | - Availability is checked before launch; the selected wrapper can still fail |
| 178 | because of host policy, container restrictions, or a race after the probe. |
| 179 | - Bubblewrap ignores a configured writable root if it is missing, is not a |
| 180 | directory, or canonicalizes to `/`; a path can also disappear between policy |
| 181 | resolution and wrapper launch. |
| 182 | - Seatbelt profiles are generated at runtime and must be tested against the |
| 183 | commands they are expected to support. |
| 184 | - No current local wrapper is advertised on Windows. |
| 185 | - An external sandbox backend is only as strong as its configured service. |
| 186 | - No sandbox protects against kernel vulnerabilities or all resource-exhaustion |
| 187 | and side-channel attacks. |
| 188 | |
| 189 | ## Implementation references |
| 190 | |
| 191 | - `crates/tui/src/sandbox/mod.rs` — truthful selection and public capability markers |
| 192 | - `crates/tui/src/sandbox/seatbelt.rs` — macOS wrapper and availability probe |
| 193 | - `crates/tui/src/sandbox/bwrap.rs` — Linux opt-in wrapper |
| 194 | - `crates/tui/src/sandbox/process_hardening.rs` — Linux parent-process hardening |
| 195 | - `crates/tui/src/sandbox/backend.rs` — external backend selection |
| 196 | - `crates/tui/src/tools/diagnostics.rs` — machine-readable diagnostics |
| 197 |