feat(shell): L3-gated brush as the confined default engine (#1243 Leg 1)#1244
Merged
Conversation
"We can't rely on string parsing." The confined default was the safe-subset STATIC parser (`config.rs` `#[default] SafeSubset` + the hardcoded fallback in `resolve_shell_engine`), which short-circuits at the first denial and refuses `$(…)`/pipes/subshells — the exact shapes an LLM emits (the "10 largest rust files" fuzzer prompt hits it immediately: grant `printf`, still denied because a *second* pipeline program isn't granted). Brush-ocap intercepts at the real spawn primitive (`before_exec`), collects ALL denials in one run, and handles arbitrary shell; newt's gate is already multi-denial-ready (`exec_denial_requests` iterates the whole array). Promote brush to the confined default, GATED on the L3 fence (the sequencing + gate mechanized in agent-bridle #239): - `confined_default_engine(l3_active)` — brush WHERE a kernel fence enforces (landlock/seatbelt); safe-subset's structural refusal otherwise (brush would run dynamic constructs advisory-only without L3 — a honesty regression). - `resolve_shell_engine_choice(...) -> Option<ShellEngine>` — `Some` only for a FIXED choice (explicit flag/config or the --full-access auto-upgrade); the CLI publishes NEWT_SHELL_ENGINE only for `Some`, leaving the confined default UNPUBLISHED so `shell_engine()` resolves it PER DISPATCH against the live fence. This closes the mechanized TOCTOU (bridle #239 `EnforcementGate.tla`): the grant-time fence is never cached; `bridle_registry()` re-checks at every run_command. `--full-access` keeps the ambient HOST shell (unchanged). - `resolve_shell_engine` kept (delegates, unwrap_or SafeSubset) — back-compat. - doctor reports the L3-gated confined default resolved for THIS host, not a stale hardcoded "safe-subset". Three disable-ocap FLOOR tests pinned NEWT_SHELL_ENGINE=safe-subset: they assert engine-independent floor/bypass logic, and `echo` is a TRUE bash builtin under brush (never spawns → not exec-gated, correctly) which would make them box-dependent on the L3-gated default. Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…t platforms (#1243 Leg 1) Windows CI caught it: ocap_l3_backend() reports AppContainer active UNCONDITIONALLY on Windows (not a per-run probe like landlock_is_supported/ seatbelt_is_supported), so the L3-gate flipped the WINDOWS confined default to brush — and brush-on-Windows has no `rm`, breaking a run_command test. Leg 1's mechanized rationale (agent-bridle #239) is specifically the landlock/seatbelt fence gating brush's dynamic constructs. Introduce resolved_confined_default() — the single source of truth for shell_engine() dispatch AND doctor's display — that applies the L3 gate only on Linux (landlock) and macOS (seatbelt), and keeps safe-subset on Windows (where brush is already the --full-access default; a brush-confined Windows default is its own follow-up). Still evaluated live per dispatch, so the fence is never cached (the #239 TOCTOU obligation holds). doctor's reason string now keys off the RESOLVED engine, not the raw fence bit, so it can't say 'safe-subset … (brush confines …)' on Windows. Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…1243 Leg 1) Windows CI flake: these tests assert the confined dispatch DENIES an ungranted command, but shell_engine() reads NEWT_SHELL_ENGINE first, then full_access_requested(). On multi-threaded Windows, a concurrent test's NEWT_FULL_ACCESS can leak into these (env is process-global), flipping the confined fallback to full_access_default_engine() = brush — whose `echo` builtin runs un-gated instead of the whole compound being atomically denied by safe-subset. Pinning NEWT_SHELL_ENGINE=safe-subset short-circuits shell_engine() before the leak-sensitive branch, making the assertion deterministic. (The leak is latent on main too; the brush-as-default flip just widened the blast radius from host to brush.) Pins floor_refuses_bypass_for_a_compound_command + routed_cat_goes_through_the_fs_floor_not_a_bypass + no_route_bypasses_routing — the three confined-denial assertions that were still unpinned after the first three. Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
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.
What this PR does
Promotes brush-ocap from opt-in to the confined default shell engine,
gated on the L3 kernel fence — the first leg of the brush-as-boundary epic
(#1243). "We can't rely on string parsing": the safe-subset static parser
short-circuits at the first denial and refuses
$(…)/pipes/subshells, so apipelined LLM command (the "10 largest rust files" fuzzer) forces one grant
cycle per program. Brush intercepts at the real spawn primitive (
before_exec),collects all denials in one run, and handles arbitrary shell; newt's gate is
already multi-denial-ready.
confined_default_engine(l3_active)— brush where a kernel fenceenforces (landlock/seatbelt); safe-subset's structural
$(…)-refusalotherwise (without L3, brush would run dynamic constructs advisory-only — a
honesty regression). Pure; unit-tested without a kernel.
resolve_shell_engine_choice(...) -> Option<ShellEngine>—Someonlyfor a FIXED choice (explicit
--shell-engine/[shell] engine, or the--full-accessauto-upgrade). The CLI publishesNEWT_SHELL_ENGINEonly forSome, leaving the confined default unpublished soshell_engine()resolves it per dispatch against the live fence.
EnforcementGate.tla):the grant-time fence is never cached —
bridle_registry()callsshell_engine()on everyrun_command, re-checkingocap_l3_backend()atexec time. A fence that drops between grant and exec cannot leave a stale
brush selection running a dynamic construct advisory.
resolve_shell_engineretained (delegates →unwrap_or(SafeSubset)) —back-compat, existing resolver tests unchanged.
--full-accesskeeps the ambient host shell (unchanged).(
brushwhere the fence is active), not a stale hardcodedsafe-subset.Test plan
cargo test -p newt-core --lib1558/0,-p newt-tui --lib530/0; clippy--all-targets -- -D warnings+ fmt clean.confined_default_is_l3_gated(brush iff L3),choice_is_none_only_for_the_confined_default(the TOCTOU-closing invariant —confined default is
None, unpublished).newt doctor→confined default (L2): brush — L3-gated, resolved per run_command (fence active here, …).NEWT_SHELL_ENGINE=safe-subset: theyassert engine-independent floor/bypass logic, and
echois a TRUE bashbuiltin under brush (never spawns → not exec-gated, correctly), which would
otherwise make them box-dependent.
Out of scope (follow-ups)
sandbox_kindrecheck for the advisory-dynamic-construct residual (the deeper half of the feat: per-session plan files keyed on conversation id (#220) #239 I4 obligation) — a bridle-side
hardening in
before_exec. Practically, bridle already fail-closes onrestrict_selfNotEnforced for restricted axes, so the window is closed forrestricted grants; filed separately.
echo/printf/test/… bypassbefore_execbecause they don't spawn — believed safe since fs/net still hitbefore_open, but deserves an explicit audit) — filed separately.find | sort | headprompts once) — bestrun in the tmux harness; the composition is proven (brush collect-all +
exec_denial_requestsiterate-all).Part of #1243. Sequencing mechanized by agent-bridle #239.