| 1 | // Package sandbox wraps a shell command in an OS-level jail so the model's |
| 2 | // `bash` calls are confined: it may read almost freely but write only inside |
| 3 | // the writable roots (workspace, configured extras, plus temp and toolchain |
| 4 | // caches), with optional forbid-read roots, and reach the network only when |
| 5 | // allowed. This is the *enforcement* layer beneath the permission rules |
| 6 | // (*policy*): a permitted command still cannot escape the box. |
| 7 | // |
| 8 | // macOS uses Seatbelt via sandbox-exec and Linux uses bubblewrap when available. |
| 9 | // Windows does not currently provide an OS-level bash sandbox and resolves the |
| 10 | // product setting to off. When enforce is requested but no OS sandbox backend |
| 11 | // is available, the bash tool fails closed instead of running the command |
| 12 | // unwrapped. |
| 13 | // Confining the in-process file-writer built-ins is handled separately, in |
| 14 | // package tool/builtin. |
| 15 | package sandbox |
| 16 | |
| 17 | import ( |
| 18 | "runtime" |
| 19 | ) |
| 20 | |
| 21 | // Spec describes how to confine one command. The zero value (Mode == "") does |
| 22 | // not enforce, so an unconfigured caller runs commands unchanged. |
| 23 | type Spec struct { |
| 24 | // Mode is "enforce" to wrap the command, anything else (incl. "off" and "") |
| 25 | // to run it unwrapped. |
| 26 | Mode string |
| 27 | // WriteRoots are directories the command may write to (the workspace root |
| 28 | // plus any configured extras). Platforms may add command-scoped temp/cache |
| 29 | // roots so builds and package managers keep working without broad writes. |
| 30 | WriteRoots []string |
| 31 | // ReadRoots are explicit host paths a Windows AppContainer may read. The |
| 32 | // macOS/Linux profiles already mount the host read-only by default. |
| 33 | ReadRoots []string |
| 34 | // AppContainerWriteRoots are the small subset of WriteRoots that a |
| 35 | // read-only Windows AppContainer may write (for MCP this is only its |
| 36 | // private state/temp tree). macOS and Linux already enforce this through |
| 37 | // WriteRoots and ignore this platform-specific distinction. |
| 38 | AppContainerWriteRoots []string |
| 39 | // DirectWrites marks a raw-argv launch as a write-capable command. On |
| 40 | // Windows this selects the low-integrity writer lane; it is deliberately |
| 41 | // false for ordinary read-only helpers such as rg. |
| 42 | DirectWrites bool |
| 43 | // ForbidReadRoots are files or directories the command may not read from |
| 44 | // when confined. The OS sandbox denies access to these paths (macOS Seatbelt |
| 45 | // deny file-read* rules, Linux bubblewrap masks); on other platforms the |
| 46 | // in-process tools enforce this instead. |
| 47 | ForbidReadRoots []string |
| 48 | // Network allows network egress from inside the sandbox. Off blocks it so a |
| 49 | // command cannot exfiltrate or fetch; many dev commands (module/package |
| 50 | // downloads) need it, so it defaults on at the config layer. |
| 51 | Network bool |
| 52 | // MinimalWrites omits the broad build-tool cache write allowances used by |
| 53 | // the bash sandbox. MCP profiles set it and explicitly provide only their |
| 54 | // private state/temp directories (plus approved writer roots). |
| 55 | MinimalWrites bool |
| 56 | // Shell is the interpreter the bash tool runs under. A zero value (empty |
| 57 | // Path) means the tool resolves one itself; the composition root sets it from |
| 58 | // [tools.shell] so the configured choice rides along with the spec. |
| 59 | Shell Shell |
| 60 | // SessionTemp is the absolute path of the logical-session private temporary |
| 61 | // directory for this command. When set, Linux bubblewrap binds it at /tmp |
| 62 | // (instead of a fresh tmpfs), and all platforms export TMPDIR/TMP/TEMP so |
| 63 | // consecutive Bash calls in the same session share temporary files. Empty |
| 64 | // keeps the platform default (ephemeral tmpfs on Linux bwrap, host temp |
| 65 | // elsewhere). MCP and other independent sandboxes leave this empty. |
| 66 | SessionTemp string |
| 67 | } |
| 68 | |
| 69 | // Enforce reports whether the spec asks for confinement. |
| 70 | func (s Spec) Enforce() bool { return s.Mode == "enforce" } |
| 71 | |
| 72 | // UnavailableMessage explains why an enforced bash sandbox cannot run and gives |
| 73 | // the platform-specific remediation. |
| 74 | func UnavailableMessage() string { |
| 75 | return "bash sandbox requested but unavailable on this host; refusing to run unconfined. " + UnavailableRemediation() |
| 76 | } |
| 77 | |
| 78 | // UnavailableRemediation is split out so status surfaces can append the same |
| 79 | // actionable hint without repeating the leading error. |
| 80 | func UnavailableRemediation() string { |
| 81 | switch runtime.GOOS { |
| 82 | case "linux": |
| 83 | return "Install bubblewrap (`bwrap`) or set [sandbox] bash = \"off\" in config.toml / Settings -> Sandbox to restore pre-1.16 unconfined shell execution." |
| 84 | case "darwin": |
| 85 | return "Ensure `sandbox-exec` is available on PATH or set [sandbox] bash = \"off\" in config.toml / Settings -> Sandbox to restore pre-1.16 unconfined shell execution." |
| 86 | case "windows": |
| 87 | return "Windows does not currently provide a Reasonix OS-level Bash sandbox; the effective setting is fixed to \"off\" and shell commands run unconfined." |
| 88 | default: |
| 89 | return "Set [sandbox] bash = \"off\" in config.toml / Settings -> Sandbox to run shell commands unconfined on this platform." |
| 90 | } |
| 91 | } |
| 92 | |
| 93 | // BackendUnavailableReason is safe diagnostic copy for subsystems such as MCP |
| 94 | // that intentionally continue unconfined when the OS backend is missing. |
| 95 | func BackendUnavailableReason() string { |
| 96 | switch runtime.GOOS { |
| 97 | case "linux": |
| 98 | return "bubblewrap (bwrap) is unavailable on PATH" |
| 99 | case "darwin": |
| 100 | return "sandbox-exec is unavailable on PATH" |
| 101 | case "windows": |
| 102 | return "the AppContainer helper or required Windows sandbox APIs are unavailable" |
| 103 | default: |
| 104 | return "this platform has no supported Reasonix sandbox backend" |
| 105 | } |
| 106 | } |
| 107 |