Expand description
P5-10 (COMPOSABLE-HARNESS-DESIGN.md §2 module 12 permissions.sandbox,
~row 462): the OS-level enforcement BACKSTOP permissions.rules’
rule-layer floor and the file-tool crate::tools::SandboxPolicy both
defer to for full coverage (crate::permissions module doc: “complete
OS-level write confinement of arbitrary bash… is capabilities. permissions.sandbox’s job (P5 module 10, a later unit), not this
one’s” — this IS that unit).
§What this module adds
- Real Linux fs enforcement via Landlock (
landlock_available,apply_linux_confinement): the spawnedbash/shellsubprocess (and its own children) is kernel-confined to the configured tier’s writable set — a genuineEPERMfrom the kernel on a disallowed write, not a path string comparison. Applied via apre_execclosure that runs in the FORKED CHILD afterfork(), beforeexec()— [crate::agent:: Agent]/supercode itself is never confined, only the subprocess tree the tool spawns. - Coarse network cut-off (
netns_available,apply_linux_confinement): whennetwork.enabledis set with no domain allow/deny lists, the subprocess is placed in a fresh, isolated network namespace (unshare(CLONE_NEWUSER|CLONE_NEWNET), self-mapped so file-permission checks are unaffected) — a real kernel-level all-network cutoff. Domain-level allow/deny is OUT OF REACH on this kernel class (that needs the out-of-scope TLS-MITM proxy, or Landlock ABI v4 network scoping, kernel ≥6.7) and is surfaced as an honest gap, never silently dropped. - Fail-closed, never silently-unsandboxed (
decide_fs): a confining tier this platform/kernel genuinely cannot enforce refuses to run the subprocess at all (escalation = "deny", the default), unlessescalationexplicitly says otherwise ("ask"routes throughcrate::permissions::PermissionsApprovalHandler;"allow"runs unconfined with a loud, one-time warning). The worst defect class named for this unit — “a tier claiming enforcement but silently running unconfined” — is structurally impossible here:decide_fsonly ever returnsFsDecision::Confinewhen the caller already told it enforcement IS available; every other input funnels through the escalation gate. env_policy(apply_env_policy):inherit(today’s behavior, byte-identical),filtered(strip a sensitive-var denylist),none(barePATH+ a couple of universally-needed variables).
§Pure decision, real effect
decide_fs/decide_net are pure functions — every availability/
approval input is a PARAMETER, never an internal cfg!/probe call — so
the fail-closed/ask/allow/monotonic-tightening branches are all unit-
testable without touching a real kernel or spawning a process. The real
call sites (crate::tools::builtins::BashTool::execute et al.) supply
real inputs via landlock_available/netns_available (cached,
real-kernel probes) and the installed
crate::permissions::PermissionsApprovalHandler.
Structs§
- Sandbox
Approval Handler - A thin,
Clone+Debugwrapper aroundArc<dyn PermissionsApprovalHandler>socrate::tools::ToolContext(which derives both) can carry one as an ambient field — mirroring the existingwrite_observer: Option<Arc<dyn WriteObserver>>precedent, exceptPermissionsApprovalHandler(a pre-existing P5-1 public trait) doesn’t itself requireDebugas a supertrait, so this newtype supplies a placeholderDebugimpl instead of widening that trait’s contract for every existing implementor.
Enums§
- FsDecision
- What to do about filesystem confinement for one subprocess spawn.
- NetDecision
- What to do about network confinement for one subprocess spawn. Unlike
FsDecision, this never refuses the whole call — network.enabled is an independent, best-effort axis (§build brief item 3): “surface the gap, never claim enforcement you lack”, not a hard fs-style gate. A caller that ALSO has anFsDecision::Refusefor the same call still refuses (that decision wins), but a network-only gap never blocks a call that has no fs confinement problem. - Sandbox
EnvPolicy capabilities.permissions.sandbox.env_policy(§3.1): child-process environment sanitization for the spawnedbash/shellsubprocess.Inherit(the default) is today’s behavior — the parent’s environment (pluscore.shell_env_snapshot, if configured) passes through unchanged.Filteredstrips a sensitive-variable denylist (tokens, keys, cloud credentials).Nonekeeps onlyPATHand a couple of universally-needed variables (HOME,TERM,LANG) — nearest to a bare-metal shell with nothing extra.- Sandbox
Escalation capabilities.permissions.sandbox.escalation(§3.1): what happens when a confining fs tier is requested but this platform/kernel cannot actually enforce it.Deny(the default) refuses to run the subprocess at all — the cardinal “never silently unsandboxed” rule.Askroutes the decision throughcrate::permissions::PermissionsApprovalHandler(P5-1’spermissions.approvalsseam, wired here per this module’s build brief).Allowauto-permits an unconfined run with a loud, one-time warning.
Functions§
- apply_
env_ policy - Build the environment the subprocess should see, starting from
base(the process’s own inherited environment, orctx.shell_env’s snapshot when one is configured — the caller decidesbase, this function only applies the POLICY on top of it).Inheritreturnsbaseunchanged (byte-identical to pre-P5-10 behavior — the common case, sinceenv_policydefaults toInherit). - apply_
linux_ confinement - Real fs/net confinement, installed on
cmdas apre_execclosure that runs in the FORKED CHILD (afterfork(), beforeexec()—crate::agent::Agent/supercode itself is never touched; only the spawned subprocess tree is).cwd/extra_write_dirsMUST already be resolved via [crate::safe_path::resolve_real] (real, symlink-resolved paths) — Landlock rules operate on directory file descriptors opened from these exact paths, so the same dual lexical+resolved discipline every other containment check in this crate uses applies here too (a symlink’dcwdmust grant the REAL target directory, not the symlink’s lexical location). - decide_
fs - Decide what to do about FILESYSTEM confinement for one subprocess spawn.
Pure —
fs_availableis the caller’s REAL probe result (landlock_availableon Linux,trueon macOS via the existing seatbelt path which this function is not consulted for — seetools::builtins::build_sandboxed_sh’s doc comment), never computed internally, so every branch (including the platform-can’t-enforce ones) is directly testable without touching a kernel. - decide_
net - Decide what to do about NETWORK confinement for one subprocess spawn.
Pure —
net_availableis the caller’s real probe result (netns_availableon Linux). Never gates onescalation(seeNetDecision’s doc comment) — a network gap is always a warn, never a refuse, so this needs no approval handler at all. - landlock_
available - Whether real Landlock filesystem confinement is available on THIS
process’s kernel — a genuine, side-effect-free (beyond dropping one
ruleset file descriptor) PARENT-PROCESS probe: it builds a
CompatLevel::HardRequirementruleset requiring exactly the write-access rightsapply_linux_confinementwould later request and checks whetherRuleset::create()succeeds — it deliberately never callsrestrict_self()(that confines the CALLING process/thread permanently and every future child of it — calling it here would confine supercode ITSELF, exactly the “confinement targets the child, not supercode” invariant this module must never violate). Cached for the process lifetime (the kernel’s Landlock support can’t change at runtime). - netns_
available - Whether an unprivileged, self-contained network namespace cut-off is
available on THIS process’s kernel — a real probe, but one that (unlike
landlock_available) genuinely can’t be done risk-free in the calling process itself (unshare(2)acts on the CALLING process/thread, so probing it directly would isolate supercode’s own network, not just check availability). Instead this forks a disposable, single-purpose child that does nothing but attempt the unshare and immediately_exit()with the result — never touches the allocator, locks, or any other state the parent might hold mid-fork (the standard safe shape for a post-fork child that never execs), so it carries none ofpre_exec’s usual multi-threaded-fork hazards. Cached for the process lifetime. - os_
sandbox_ active - Whether the OS-level backstop is ACTIVE for
tier/os_enabledat all — the “turn the sandbox ENUM into an enabled/disabled OS engagement” half of §3.1’s table.DangerFullAccessis an absolute opt-out (never confine, regardless ofos_enabled— §3.1 item 1: “no confinement (opt-out)”). Otherwise: an EXPLICITos_enabledvalue wins;None(never set — the baresandbox = "<tier>"shorthand, or a CLI--sandboxflag, neither of which touch the table’senabledkey at all) preserves the PRE-P5-10 trigger this crate already shipped (tools/builtins.rs’s macOS seatbelt firing offctx.sandboxalone, no separate gate) — so an existing CLI user or thecx-paritypreset (baresandbox = "workspace_write", noenabledkey) keeps its current confining behavior byte-for-byte, whilecc-parity’s explicit table-formenabled = false(§3.1’s own “OS sandbox OFF… opt-in” comment) is honored as a real, independent off-switch. - warn_
once - Print
reasonto stderr ONCE per distinct reason string, for the lifetime of this process — the “loud, one-time persistent warning” the build brief calls for on anescalation = "allow"/approved-askrun, and on a network gap. Deduped by exact text (not a blanket once-per-processOnce) so a DIFFERENT gap later in the same run still gets its own warning — only an EXACT repeat is suppressed.