返回 DeepSeek-Reasonix
sandbox.go
根目录 / internal / sandbox / sandbox.go
1 // Package sandbox wraps a shell command in an OS-level jail so the model's
2 // `bash` calls are confined: reads stay mostly free, writes stay inside the
3 // writable roots (workspace, extras, temp and toolchain caches), forbid-read
4 // roots are hidden, and network egress is optional. It is the *enforcement*
5 // layer beneath the permission rules: a permitted command still cannot escape.
6 // macOS uses Seatbelt and Linux uses bubblewrap; where a backend is missing,
7 // restricted presets fail closed. Windows has no OS-level shell sandbox (see
8 // OSSandboxSupported). File-writer built-ins are confined in tool/builtin.
9 package sandbox
10
11 import "runtime"
12
13 // Spec describes how to confine one command. The zero value (Mode == "") does
14 // not enforce, so an unconfigured caller runs commands unchanged.
15 type Spec struct {
16 // Mode is "enforce" to wrap the command, anything else (incl. "off" and "")
17 // to run it unwrapped.
18 Mode string
19 // ReadOnly removes every ordinary writable mount/allowance. It is distinct
20 // from an empty WriteRoots slice, whose historical meaning is unconfigured.
21 ReadOnly bool
22 // WriteRoots are directories the command may write to (the workspace root
23 // plus any configured extras). Platforms may add command-scoped temp/cache
24 // roots so builds and package managers keep working without broad writes.
25 WriteRoots []string
26 // ForbidReadRoots are files or directories the command may not read from
27 // when confined. The OS sandbox denies access to these paths (macOS Seatbelt
28 // deny file-read* rules, Linux bubblewrap masks); on other platforms the
29 // in-process tools enforce this instead.
30 ForbidReadRoots []string
31 // Network allows network egress from inside the sandbox. Off blocks it so a
32 // command cannot exfiltrate or fetch; many dev commands (module/package
33 // downloads) need it, so it defaults on at the config layer.
34 Network bool
35 // MinimalWrites omits the broad build-tool cache write allowances used by
36 // the bash sandbox. MCP profiles set it and explicitly provide only their
37 // private state/temp directories (plus approved writer roots).
38 MinimalWrites bool
39 // Shell is the interpreter the bash tool runs under. A zero value (empty
40 // Path) means the tool resolves one itself; the composition root sets it from
41 // [tools.shell] so the configured choice rides along with the spec.
42 Shell Shell
43 // SessionTemp is the absolute path of the logical-session private temporary
44 // directory for this command. When set, Linux bubblewrap binds it at /tmp
45 // (instead of a fresh tmpfs), and all platforms export TMPDIR/TMP/TEMP so
46 // consecutive Bash calls in the same session share temporary files. Empty
47 // keeps the platform default (ephemeral tmpfs on Linux bwrap, host temp
48 // elsewhere). MCP and other independent sandboxes leave this empty.
49 SessionTemp string
50 // ProtectedWriteRoots are Reasonix session/state paths that stay read-only
51 // even when a broader WriteRoot such as the user's home directory would
52 // otherwise cover them.
53 ProtectedWriteRoots []string
54 }
55
56 // Enforce reports whether the spec asks for confinement.
57 func (s Spec) Enforce() bool { return s.Mode == "enforce" }
58
59 // OSSandboxSupported reports whether this platform can confine shell commands
60 // at the OS level. Windows cannot: its restricted-token backend is retired
61 // (same-user ACL denies locked hosts out; the token broke common toolchains).
62 func OSSandboxSupported() bool { return osSandboxSupportedForGOOS(runtime.GOOS) }
63
64 func osSandboxSupportedForGOOS(goos string) bool { return goos != "windows" }
65
66 // UnavailableMessage explains why an enforced shell sandbox cannot run and gives
67 // the platform-specific remediation.
68 func UnavailableMessage() string {
69 return "shell sandbox requested but unavailable on this host; refusing to run unconfined. " + UnavailableRemediation()
70 }
71
72 // UnavailableRemediation is split out so status surfaces can append the same
73 // actionable hint without repeating the leading error.
74 func UnavailableRemediation() string {
75 switch runtime.GOOS {
76 case "linux":
77 return "Install bubblewrap (`bwrap`), or explicitly select Full access for an unconfined session."
78 case "darwin":
79 return "Ensure `sandbox-exec` is installed and usable (the host must allow `sandbox_apply`), or explicitly select Full access for an unconfined session."
80 case "windows":
81 return "Windows has no OS-level shell sandbox. Permission presets are enforced by Reasonix file tools and shell commands run as the current OS user; select Full access only when ordinary approval prompts should also be skipped."
82 default:
83 return "Restricted permission presets are unavailable on this platform; explicitly select Full access only when unconfined execution is intended."
84 }
85 }
86
87 // BackendUnavailableReason is safe diagnostic copy for subsystems such as MCP
88 // that intentionally continue unconfined when the OS backend is missing.
89 func BackendUnavailableReason() string {
90 switch runtime.GOOS {
91 case "linux":
92 return "bubblewrap (bwrap) is unavailable on PATH"
93 case "darwin":
94 return "sandbox-exec is missing from PATH or unusable (sandbox_apply is restricted)"
95 case "windows":
96 return "Reasonix does not ship an OS-level sandbox on Windows"
97 default:
98 return "this platform has no supported Reasonix sandbox backend"
99 }
100 }
101
101 lines GO