feat(core): spawn_confined_subprocess — confine an arbitrary child under Caveats#56
Merged
Merged
Conversation
…der Caveats
WHAT: New `spawn` module in agent-bridle-core: `ConfinedCommand` (a Command-like
builder) and the `spawn_confined_subprocess` convenience fn, returning a
`ConfinedChild { child, sandbox_kind }`. It admission-checks `exec`, clears the
environment (only explicitly-granted vars reach the child), and applies the best
available OS sandbox on a fresh throwaway thread before spawning — so the child
(and its descendants) inherit the Landlock `fs_write` domain via fork/exec
inheritance. Fails closed: a restrictive `fs_write` that no OS sandbox can
enforce is refused rather than spawned unconfined.
WHY (Fixes #55): bridle confinement was hardwired to the in-process brush shell;
there was no way for a host to put an *arbitrary* subprocess under the leash. The
gila capabilities run as `gilacap` Python subprocesses spawned by the
gilamonster-agent harness — a deputy crossing a process boundary, exactly the
confused-deputy case. This is the keystone primitive: the parent attenuates
before spawn (the child is never trusted to confine itself; I3), env is scrubbed
(secrets never move ambiently; I12), and absence of enforcement denies (I5).
Honest about scope: only `fs_write` is L3-enforced today, so it is the sole axis
in the fail-closed decision; `fs_read`/`exec`/`net` confinement of the child is
advisory until those backends land (net needs netns/seccomp; see ADR 0001/0006).
Tests: exec denied before spawn; env scrubbed to the allow-list (parent HOME does
not leak); fail-closed refuse when no sandbox (no-default-features build); and a
Linux+landlock kernel test proving the SPAWNED CHILD's out-of-scope write is
kernel-denied while an in-scope write succeeds. `#![forbid(unsafe_code)]` upheld —
no pre_exec; the thread+restrict_self pattern mirrors sandbox.rs.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018R92DDLCsWorb4fSJQrdvD
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #55.
Summary
Adds the missing primitive to put an arbitrary subprocess under the leash —
the keystone for running gila capabilities as confined
gilacapsubprocesses(the deputy-crosses-a-process-boundary case).
New
agent-bridle-core::spawnmodule:ConfinedCommand— astd::process::Command-like builder whoseenvironment starts empty (only vars added via
.env()reach the child) andwhose
.spawn(&cx):cx.check_exec(program)— deny before doing anything;fs_writeis restricted (Only(..)) but no OS sandboxcan enforce it, refuse rather than spawn unconfined;
best_available_sandbox()on a fresh throwaway thread, thenspawns on it, so the child (and its descendants) inherit the Landlock
fs_writedomain via fork/exec inheritance — then the thread exits, leavingthe caller's threads unrestricted.
spawn_confined_subprocess(program, args, cx, env_allow, cwd)— theconvenience free function from the issue (inherited stdio).
ConfinedChild { child, sandbox_kind }— the caller owns theChild(wait/kill/pipe), and
sandbox_kindis the honest record of what confinementwas achieved.
OCAP invariants upheld: attenuate-before-spawn / no self-confinement (I3), env
scrub so secrets never move ambiently (I12), absence-of-enforcement-denies (I5).
#![forbid(unsafe_code)]preserved — nopre_exec; the thread +restrict_selfpattern mirrors
sandbox.rs.Scope honesty: only
fs_writeis L3-enforced today, so it is the sole axis inthe fail-closed decision;
fs_read/exec/netconfinement of a subprocess isadvisory until those backends land (net needs netns/seccomp — ADR 0001/0006). The
PR docs say so rather than overclaim.
Test plan
just checkgreen (fmt + clippy-D warnings×{all-features, no-default-features}exec_outside_scope_is_denied_before_any_spawn(cross-platform)environment_is_scrubbed_to_the_granted_allow_list— parentHOMEdoes notleak; the granted var is present (unix)
restrictive_write_refused_when_no_sandbox_available— fail-closed (runs on theno-landlock build)
unenforceable_predicate_only_trips_on_restricted_write_without_sandbox(pure)child_inherits_fs_write_domain_…— kernel test: the spawned child'sout-of-scope write is
EACCES-denied while an in-scope write succeeds(Linux +
linux-landlock; verified on 6.8)Follow-ups (out of scope)
ConfinedCommand(piped for MCP); the free fn usesinherited stdio.
net/fs_read/execL3 confinement of the child (netns/seccomp; ADR 0006).gila cap run→ mint per-cap caveats vianewt_identity::delegate_for_plugin→ spawngilacapconfined) lands in thegilamonster-capabilities repo.
risk:high — security boundary primitive.