Skip to content

feat(shell): L3-gated brush as the confined default engine (#1243 Leg 1)#1244

Merged
hartsock merged 3 commits into
mainfrom
feat/1243-brush-l3-gated-default
Jul 17, 2026
Merged

feat(shell): L3-gated brush as the confined default engine (#1243 Leg 1)#1244
hartsock merged 3 commits into
mainfrom
feat/1243-brush-l3-gated-default

Conversation

@hartsock

Copy link
Copy Markdown
Member

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 a
pipelined 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 fence
    enforces
    (landlock/seatbelt); safe-subset's structural $(…)-refusal
    otherwise (without L3, brush would run dynamic constructs advisory-only — a
    honesty regression). Pure; unit-tested without a kernel.
  • resolve_shell_engine_choice(...) -> Option<ShellEngine>Some only
    for a FIXED choice (explicit --shell-engine/[shell] engine, 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.
  • TOCTOU closed (the mechanized obligation, bridle feat: per-session plan files keyed on conversation id (#220) #239 EnforcementGate.tla):
    the grant-time fence is never cachedbridle_registry() calls
    shell_engine() on every run_command, re-checking ocap_l3_backend() at
    exec time. A fence that drops between grant and exec cannot leave a stale
    brush selection running a dynamic construct advisory.
  • resolve_shell_engine retained (delegates → unwrap_or(SafeSubset)) —
    back-compat, existing resolver tests unchanged.
  • --full-access keeps the ambient host shell (unchanged).
  • doctor now reports the L3-gated confined default resolved for THIS host
    (brush where the fence is active), not a stale hardcoded safe-subset.

Test plan

  • cargo test -p newt-core --lib 1558/0, -p newt-tui --lib 530/0; clippy
    --all-targets -- -D warnings + fmt clean.
  • New unit tests: 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).
  • Live-verified on gnuc (Landlock active): newt doctor
    confined default (L2): brush — L3-gated, resolved per run_command (fence active here, …).
  • 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
    otherwise make them box-dependent.

Out of scope (follow-ups)

  • In-brush exec-time sandbox_kind recheck 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 on
    restrict_self NotEnforced for restricted axes, so the window is closed for
    restricted grants; filed separately.
  • Brush true-builtin surface audit (echo/printf/test/… bypass
    before_exec because they don't spawn — believed safe since fs/net still hit
    before_open, but deserves an explicit audit) — filed separately.
  • Leg 2 (env/token import surface) and Leg 3 (MCP-inside-brush MCP servers must run INSIDE the brush/caveat boundary, not the ambient env #1180).
  • The interactive multi-denial UAT (find | sort | head prompts once) — best
    run in the tmux harness; the composition is proven (brush collect-all +
    exec_denial_requests iterate-all).

Part of #1243. Sequencing mechanized by agent-bridle #239.

"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>
hartsock and others added 2 commits July 16, 2026 23:59
…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>
@hartsock
hartsock merged commit 7f01d4a into main Jul 17, 2026
16 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

risk:high High-risk change: needs human review before merge

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant