Sandbox manager: compile a permission profile into one command
Workspace writable, network tightened. The same git status gets sandbox-exec on Mac, a helper on Linux, and may walk out unchanged on Windows. The difference is the compiler.
environment_context the model sees.
Not rendered yet.
- Read the PermissionProfile triadmodels.rs L411
- Layer on additional permissionspolicy_transforms.rs L525
- If Auto, run should_require_platform_sandboxpolicy_transforms.rs L541
- select_initial then asks get_platform_sandboxmanager.rs L62
- None passes argv through unchangedmanager.rs L366
- macOS prefixes /usr/bin/sandbox-execmanager.rs L401
- Linux serializes the profile to the helperlandlock.rs L23
- Windows beat one leaves argv alonemanager.rs L447
- The same profile renders into environment_contextenvironment_context.rs L96
Same repo, three machines, the model issues the same git status. Config writes the same permission profile: workspace writable, network tightened.
Someone debugging will think the sandbox never took. The profile is still in the logs, and that environment_context is still in the model context. The platform layer just never compiled the profile into a wrapping command. On Windows with the switch off, the compiler hands back the original argv; the policy layer is still there.
SandboxManager asks “do we want it” first, then “do we have it.” should_sandbox returns one boolean. Forbid is always false, Require always true, Auto looks at the profile shape. A managed-network requirement forces it on. When the network is tightened, it goes on unless the caller owns the filesystem. Only when the network is open and the filesystem is unrestricted does it skip.
Source: codex-rs/sandboxing/src/manager.rs lines 310–329 · codex-rs/sandboxing/src/policy_transforms.rs lines 541–561
Then get_platform_sandbox names a type by OS. macOS gets Seatbelt, Linux a seccomp helper. Windows has an extra switch; off means empty. Empty means this host has no implementation to dispatch.
Source: codex-rs/sandboxing/src/manager.rs lines 62–76
select_initial asks the first question, then folds an empty second answer into SandboxType::None. The profile says it needs one; the host cannot provide it; the type is still None.
Source: codex-rs/sandboxing/src/manager.rs lines 293–306
Intent in config and the backend on the host will part ways. If product copy, the status bar, and the model brief all read the “we hope it’s on” side of config, debugging starts crooked here. Two functions: one returns a boolean, one an optional backend name. Rewrite it in another language and you still want this.
If permissions are written twice, you change one side and forget the other, and the model plans the next step against a stale boundary. The model seeing no outer sandbox, and Windows falling to None when the sandbox is off, should be two exits of the same intent.
The three PermissionProfile variants say who builds the outer sandbox. Managed: Codex itself stitches the wrapping command. Disabled: no outer layer. External: the caller owns the filesystem; the network may still belong to Codex.
Source: codex-rs/protocol/src/models.rs lines 411–425
transform dispatches by type. None passes the user argv through and does not even prepare a local cwd, because an unsandboxed request may carry a remote-machine path. macOS trusts only /usr/bin/sandbox-exec; policy goes on -p, paths on -D, the user command after --. Linux serializes the whole profile into --permission-profile; a missing helper errors out and does not degrade to no-sandbox. Windows beat one only validates and leaves argv alone; the wrapper is the current exe, and the shell is deferred to transform_for_direct_spawn.
Source: codex-rs/sandboxing/src/manager.rs lines 365–459 · codex-rs/sandboxing/src/seatbelt.rs lines 52–56 · codex-rs/sandboxing/src/landlock.rs lines 15–23 · codex-rs/sandboxing/src/manager.rs lines 484–518
The same profile is folded by FileSystemContext into an internal enum and rendered into environment_context. Disabled becomes type="disabled" plus an unrestricted filesystem. Model-visible text cannot stop privilege escape; it only cuts wasted attempts.
Source: codex-rs/core/src/context/environment_context.rs lines 96–116
There should be one runtime fact. Isolation happens at the child-process door; the orchestration layer keeps holding paths and the profile, and it lands only at the exec boundary. Platform dialects will change; this profile shape, fed to both the wrapping command and the model, still earns its keep.
A single command can also carry an overlay. If a human approval uses union, one mistaken yes writes a path the request never asked for into session grants.
Merge uses union: if either side opens the network, the result is open; filesystem entries are concatenated and deduped. Intersection is the other set: the network stays only if both sides are open; the filesystem keeps only already-approved entries that fall inside the request. An empty intersection is not booked on the session; the base profile stays in force. Empty means this extra opening left no widening. Whether the command can still run is up to later policy and the sandbox.
Source: codex-rs/sandboxing/src/policy_transforms.rs lines 90–142 · codex-rs/sandboxing/src/policy_transforms.rs lines 144–214 · codex-rs/core/src/session/mod.rs lines 2833–2846
Widening and approval are two jobs. Union makes “this command opened one more path” speakable. Intersection keeps what a human approved no wider than the request. Empty means the opening failed — do not read it as “switch to strictest,” and do not read it as “forbid the whole command.”
DeepSeek Harness: no backend, refuse to run
DSH also cuts at the child-process door. confine picks a runner for the current host, compiles policy into runner args, and puts the user command after --. The profile vocabulary is three file modes; danger-full-access never enters the wrap.
Missing a backend fails closed and does not fall back to the original argv. The copy says refusing to run the command unconfined. Codex also will not run when the Unix helper is missing; when the Windows sandbox is off it folds “cannot provide” into None and hands it to the policy layer.
Grok: one apply, lock the current process
Grok also has a type named SandboxManager; its job is apply() then install(). The comment is blunt: it acts on the current process, irreversibly. The compile product is a CapabilitySet, not a wrap around each command’s argv.
Unsupported platform, or apply failure: warn, record apply_failed, keep running. Later tool calls share the same capability. Grok does not recompile argv per command, and cannot give two commands two boundaries inside one process.
Source: packages/sandbox/sandbox-local/src/index.ts lines 316–333 · crates/codegen/xai-grok-sandbox/src/lib.rs lines 117–136
Four returns, three inputs
Open should_require_platform_sandbox and sketch the four returns on paper. Then predict these three: a Disabled profile plus managed network; writable root plus one deny, network Enabled; an External profile, network Restricted.
Advanced: the same Disabled profile, the tool declares Require, Windows tier still off. What is the type, and will the XML the model sees change its story?