Files
stack/docs/scratchpads/829-mutator-gate.md
jason.woltje abd2791f59
Some checks failed
ci/woodpecker/push/ci Pipeline failed
ci/woodpecker/push/publish Pipeline was successful
feat(mosaic): WI-2 mutator-class guard for directive-freshness (#837)
WI-2: mutator-class guard for the compaction directive-freshness mechanism — command-position parser (prefix x var-indirection unified) + primitive-anchored invariant backstop + all-tools-hook fail-close. Parser-complete on principle: realistic evasion matrix RED-regression-covered, residual exotic evasions proven backstop-caught (B-tests), lens-convergence reached.

closes #829
2026-07-18 05:51:58 +00:00

19 KiB
Raw Blame History

WI-2 Scratchpad — Whole mutator-class gate

  • Issue: Gitea #829
  • Branch: feat/829-mutator-gate
  • Base HEAD: 8ec67a1126adb0dcd4c3a2bf5525f3e239c0b201
  • Role: sol author/build lane only; terra code review and Opus security review are coordinator-owned.

Mission prompt

Implement BUILD-BRIEF Deliverable 2 as a framework-native whole mutator-class gate under packages/mosaic/, building against the merged WI-1 lease broker. No consequential mutator may succeed while UNVERIFIED after a compaction observer fires or after TTL. Carry the T-B compromised-tool acceptance criteria. Enforce revoke-first and promote-last structurally. A receipt is only a promotion prerequisite; the mutator-class gate remains the safety mechanism. M1 is Claude + Pi only.

Authority verification

Verified exact SHA-256 before design/code:

  • BUILD-BRIEF: 89fdbc27ed0e5050dc7b52f3ef2ddaea691edf17fd89d51b15e26fb5ed47171b
  • SPEC-v5: a6d07ade835758e8488ca10d3b0631caf0beb93ea3a6733631f151b0c2f01433
  • Ratification: bac58319c9c4028b5b40e1129e0033cdb5a6b7b02033c25f06f4cb77d7779c67
  • sol final red-team: 3da326a4ea91767b731e128a93b13194e8002358101e30de3fcb8ca2f8f54faa

Carried authority chain also verified/read for the locked T-B gate contract: SPEC-v4 a5e9c261…, v4 sol 1e76ee59…, SPEC-v3 e0830ba0…, v3 sol 9f321ade….

Plan

  1. RED real-socket acceptance tests for default-deny whole classes, T-B raw-tool bypass, observer/TTL revocation, and structural revoke-first/promote-last.
  2. Extend the merged WI-1 broker as the sole lease authority; authenticate every transition through existing peercred/ancestry/session logic and consume WI-1 single-use cycle tokens atomically before promotion.
  3. Add one broker-backed runtime gate executable and wire it across all Claude PreToolUse tools and Pi tool_call; unknown/custom tools deny by default.
  4. Add proportional protocol/security/operations documentation and requirements-to-evidence mapping.
  5. Run focused coverage, package/full suites, lint/typecheck/format, then queue-guard, push, open an unmerged PR with closes #829, and hand off the exact head.

Risks and bounds

  • Receipt parsing/builders and compaction observers are later WIs; WI-2 exposes the promotion prerequisite boundary but does not treat a receipt as safety authority.
  • Broker restart intentionally loses volatile VERIFIED leases and therefore restarts UNVERIFIED; persistent WI-1 identity/token state remains unchanged.
  • The gate is whole-class and does not parse shell command strings. T-C extension/hook absence and same-UID broker replacement remain outside the client guarantee and server branch protection remains the backstop.
  • Initial lease TTL is capped at ratified 300 seconds; callers may only shorten it.
  • Working budget assumption: 35K tokens; reduce documentation/refactor breadth before touching locked scope if pressure rises.

Progress and verification

  • RED #1: all 5 initial real-socket contract tests failed on WI-1 with UNKNOWN_ACTION or missing adapter behavior.
  • RED #2: register-before-exec runtime test failed because launch-runtime.py did not exist.
  • GREEN: broker-owned volatile lease state, 300-second maximum monotonic TTL, WI-1 token-backed promotion, all-tools runtime gate, Claude/Pi wiring, and register-before-exec launcher delivered without changing WI-1 peercred/ancestry authority.
  • Focused broker + gate acceptance: 43/43 GREEN.
  • Instrumented Python coverage: 88% total — daemon 89%, register/exec launcher 86%, runtime gate 86%.
  • Full repository suite: 43/43 Turbo tasks GREEN; @mosaicstack/mosaic 72/72 files and 1,377/1,377 tests GREEN.
  • Root typecheck: 42/42; lint: 23/23; format check and git diff --check: GREEN.
  • No author self-review was run. Exact-head terra CODE and Opus SECREV remain coordinator-owned gates.

Acceptance mapping

Acceptance criterion Evidence
No consequential mutator succeeds while UNVERIFIED after observer revoke or TTL observer revocation and monotonic TTL expiry deny the next mutator real-socket acceptance test
T-B compromised-tool bypass is covered by the whole gate T-B raw and custom mutator tools are default-denied without shell parsing across Claude/Pi built-ins, raw Bash class, MCP/custom/unknown tools
Revoke-first / promote-last is structural revoke-first and promote-last structurally bracket mutator authority; direct promotion rejected, pending remains denied, token consumption commits before VERIFIED
Consume WI-1 auth/lease substrate All transitions and decisions traverse merged peercred/ancestry authentication; promotion consumes the exact WI-1 CSPRNG cycle token
M1 Claude + Pi Claude .* PreToolUse and Pi tool_call invoke the same broker gate; register-before-exec test proves broker-minted parent identity reaches runtime descendants

Locked discipline

  • Re-verify and read the four authority artifacts before design or code.
  • RED-first tests must cover unverified mutation refusal, T-B compromised-tool refusal, and revoke-first/promote-last ordering.
  • Consume WI-1 VERIFIED-lease state; do not re-derive kernel identity, ancestry, sessions, or token authentication.
  • Minimum 85% new-code coverage; full suite, lint, typecheck, and format checks green.
  • Build only: no self-review and no merge. Open a PR containing closes #829, report its exact 40-character head, then exit.

Remediation — terra CODE comment 18091

  • Coordinator correction: terra returned REQUEST CHANGES at head 77b137ccc04b5be035cac5ca21bbbf3df8b94f97; Opus SECREV was GO and CI green, but no evidence transfers to the remediated head.
  • BLOCKER 1 verified: first-class Claude/Pi route through execLeaseGatedRuntime, while the supported Claudex path preserves isolation but directly invokes claude; it therefore registers no anchor, injects no lease session, and the isolated config has no guaranteed all-tools gate hook.
  • BLOCKER 2 accepted: prior 88% was aggregate evidence. Remediation must produce independently measured branch coverage of at least 85% for each new executable (launch-runtime.py, mutator-gate.py, and daemon delta evidence), including successful exec-boundary collection and validation/error branches.
  • Remediation discipline: RED tests first; preserve the reviewed-good broker lock/state-transition ordering; update PR #837 on the same branch; no self-review or merge.

Remediation evidence

  • RED commit 046896c6: both mosaic claudex and mosaic yolo claudex behavioral probes exited 1 because the direct path supplied neither a broker session nor the isolated all-tools hook; branch-focused Python tests failed on the absent injectable boundaries. Fresh WI-2 daemon-delta instrumentation also failed the ≥85% branch gate at 75%.
  • GREEN: Claudex now exposes only execLeaseGated, passes the preserved isolated proxy environment through the shared register-before-exec wrapper, and merges the exact .* mutator hook into isolated settings.json with mode 0600. Missing broker/identity, malformed or symlinked settings, and an unverified consequential tool all fail closed. The broker transition/lock implementation was not changed.
  • Behavioral regression: normal and YOLO Claudex both receive a 64-hex broker session, retain their mode-specific arguments, observe the exact all-tools hook, and receive status 2 for unverified Bash.
  • Independent branch coverage: launch-runtime.py 16/18 = 89% (statements 98%); mutator-gate.py 21/22 = 95% (statements 99%); daemon.py WI-2 delta 35/40 = 88% (whole-file branch 80%, statements 90%).
  • Fresh focused real-socket coverage run: WI-1 + WI-2 acceptance 46/46; persistence 10/10; branch unit suite 10/10.
  • Fresh full repository suite: 43/43 Turbo tasks; @mosaicstack/mosaic 72/72 files and 1,381/1,381 tests.
  • Fresh root gates: typecheck 42/42; lint 23/23; format and git diff --check GREEN.
  • PR #837 remains open and unmerged. Terra CODE and Opus SECREV must both rerun from zero on the exact remediated head before coordinator-owned merge authorization.

Remediation round 3 — terra CODE comment 18099 + binding upgrade

  • Locked-good surfaces: Claudex gating and B2 per-executable coverage are verified; do not regress them. Broker state-transition/lock ordering remains untouched.
  • Mechanical repository sweep found direct executing Claude entries in PRDY init, PRDY update, QA remediation, and @mosaicstack/coord task launch. It also found a direct Claude command rendered into the QA report template and documentation examples. Existing Mosaic CLI Claude/Pi/Claudex, orchestrator session-run, and fleet starts already reach the gated boundary.
  • Elevated hard requirements: ship a permanent suite/CI guard that scans production source and fails on any direct Claude/Pi launch; route every executing entry through one common gated wrapper; add real-broker RED/GREEN tests for PRDY init/update and QA; preserve each environment and denial behavior; independently measure all new executable coverage at ≥85%.
  • Round-3 plan: first commit RED behavioral and scanner-contract tests; then add one framework launch-runtime.sh choke-point over launch-runtime.py, make Mosaic CLI and shell launchers use it, make coord route through mosaic, and wire the permanent guard into package tests. Update all discovered operator-facing direct-launch examples so the scanner inventory remains complete.

Round-3 outcome

  • RED commit 7f3418fa: PRDY init, PRDY update, and QA remediation all reached the fake Claude binary without a broker session even when the configured socket did not exist; the permanent-guard contract initially failed because its executable was absent, then failed against the five discovered direct entries (four executing plus the QA command template).
  • Choke-point decision: a new shell layer was unnecessary. Every executing repository entry now converges directly or through mosaic/execLeaseGatedRuntime on the existing single launch-runtime.py register-then-exec wrapper. PRDY and QA preserve their working directories, prompts, flags, logging pipe, and environment. Coord rewrites direct Claude commands to mosaic claude and rejects unknown custom Claude launchers fail-closed.
  • Permanent guard: packages/mosaic/framework/tools/lease-broker/check-runtime-launches.py, invoked by packages/mosaic/package.json test:framework-shell and therefore root pnpm test/CI. It scans production code under packages/, apps/, plugins/, and tools/ and rejects literal, absolute-path, process-API, command-array, and dynamic Claude/Pi launch forms. Synthetic bypass tests are permanent at runtime_launch_guard_unittest.py.
  • Mechanical inventory: 14 gated / 14 total — coord 2, fleet 1, QA 2, orchestrator 3, PRDY 2, Mosaic Claude/Pi/Claudex adapter/boundary 4. No verification-layer fallback or follow-up issue is needed because the single code-level wrapper was achieved.
  • Real-socket behavioral evidence: PRDY init, PRDY update, and QA remediation each fail before runtime execution when the broker is absent; with the broker present they receive a broker-minted 64-hex session and the unverified Bash authorization exits 2. Claudex normal/YOLO and the broker state machine remain GREEN.
  • Fresh branch coverage: launch-runtime.py 18/18 = 100%; mutator-gate.py 22/22 = 100%; permanent guard 36/38 = 95%; daemon.py WI-2 delta 35/40 = 87.5%. All attributable executable statement coverage is at least 98%.
  • Fresh focused suites: broker + mutator real-socket acceptance 49/49; persistence 10/10; launcher/gate branch suite 13/13; permanent guard suite 7/7; coord 19/19.
  • Fresh full repository suite: 43/43 Turbo tasks; @mosaicstack/mosaic 72/72 files and 1,384/1,384 tests. Root typecheck 42/42, lint 23/23, format, and diff checks GREEN.
  • PR #837 remains open and unmerged. Terra CODE and Opus SECREV must both rerun from zero on the exact round-3 head before coordinator-owned merge authorization.

Remediation round 4 — terra CODE comment 18104

  • Locked-good surfaces: the 14/14 launch inventory, single launch-runtime.py choke-point, real-socket launcher behavior, coverage, Claudex gating, and broker state machine must not change.
  • Reproduced RIDER E exactly at head 1792b7934dda7eff64a207b8b0edb9c460d4164b: a temporary production file containing exec claude --dangerously-skip-permissions "terra-r3" # launch-runtime.py made the guard exit 0 and report 1 gated/1 total.
  • Root cause: classification searched the unparsed physical line, and the broad gated regex treated any launch-runtime.py substring—including comments and inert arguments—as an invocation before the direct-launch finding was evaluated.
  • Round-4 plan: add permanent RED cases for the exact comment evasion plus string-argument, echo, and unrelated-variable marker evasions; tokenize/strip comments by launcher syntax; recognize only command-position wrapper invocations with --runtime and the gated command separator; retain 14/14 real inventory; rerun guard coverage and all gates fresh.
  • Mos Rider A/B decision: adopt both defenses. Command-position parsing remains necessary because a normal claude -p launch is consequential even without the dangerous flag. The primitive-location invariant is more mechanically robust for dangerous mode because it does not need to recognize a wrapper marker at all. Move the sole raw --dangerously-skip-permissions literal into launch-runtime.py; any occurrence in another production file is independently RED.
  • Rider-A RED matrix adds heredoc body, backslash continuation, non-first ;/&&/pipe commands, command substitution, eval, and variable-execution indirection in addition to the six marker/comment evasions. Before the augmented implementation, primitive ownership, command substitution, eval, variable execution, and the preserved 14-site inventory all fail.
  • Round-4 GREEN uses both defenses. Quote-aware comment stripping removes shell/Python # and JS/TS line/block comments; shell command prefixes are segmented with shlex; validated wrappers require launch-runtime.py in command position, --runtime, and the -- command separator; multiline TypeScript wrapper calls are validated as complete invocations. Direct command syntax wins over markers, while tracked runtime assignments plus eval/variable execution, command substitution, chained commands, heredocs, continuations, and env/command/nohup prefixes are rejected.
  • Primitive ownership is independently load-bearing: launch-runtime.py is the sole production owner of the raw Claude dangerous flag. Mosaic, Claudex, and PRDY request semantic --dangerous; the wrapper validates Claude and injects the primitive immediately before register/exec. This preserves actual YOLO argv behavior while making any raw primitive elsewhere fail without relying on wrapper-name recognition.
  • Permanent guard suite now has 10 tests and 31 direct-launch forms, including 18 new round-4 marker/comment/indirection/prefix evasions plus harmless-marker and multiline-wrapper controls. Terra's exact add-ungated source is exercised through the CLI effectiveness test. Repository inventory remains exactly 14 gated / 14 total.
  • Fresh round-4 coverage: guard 97% branch-aware aggregate (241 statements, 110 branches); launch-runtime.py 100%; mutator-gate.py 100%. The daemon is byte-unchanged from the round-3 head whose WI-2 delta is 87.5%.
  • Fresh round-4 gates: real-socket acceptance 49/49; persistence 10/10; launcher/gate 14/14; guard 10/10; coord 19/19; Mosaic 1384/1384; root 43/43; typecheck 42/42; lint 23/23; format and diff checks green.

Remediation round 5 — terra 18116 + Opus 18114

  • Both independent gates converged on one guard-only completeness gap at round-4 head 1eb77c17f3147d4fa9944f77f1826243135b9cc0; all round-4 primitive anchoring, command-position parsing, 14/14 inventory, broker ordering, and coverage remain locked-good.
  • Reproduced exactly: a temporary production source containing launcher=claude followed by exec "$launcher" -p x exits 0 with 0 gated / 0 total. The literal command resolver skips prefixes but cannot resolve a tracked variable; the variable resolver handles only bare/eval references and cannot skip prefixes.
  • Round-5 plan: add 10 permanent RED forms (quoted/unquoted exec, command, nohup, and env with assignment, each multiline and same-line), then unify shell command-position resolution so literal and tracked-variable terminal tokens traverse the same prefix parser. Retain an independent variable-reference backstop, the 14/14 inventory, and every round-4 regression.
  • Mos stopping-criterion augment: command parsing is explicitly best-effort rather than a complete shell interpreter. Add B1 proving a parser-exotic alias launch with the raw dangerous flag is still RED by primitive anchoring, and B2 proving a parser-missed non-dangerous alias launch reaches the global .* hook and fails closed with GATE_UNAVAILABLE when no lease session exists. Document A (realistic parser matrix) + B (robust residual backstops); fresh reviewers supply criterion C (no new non-overlapping finding).
  • RED commit 91a4a983: all 10 prefix×variable cases failed as expected before the fix—quoted/unquoted exec, command, nohup, and env A=1, each in multiline and same-line assignment shapes.
  • GREEN structural resolution: shell_command_tokens() now owns command-position prefix skipping for both literal and variable callers, including nested exec/command/nohup/env ordering. runtime_variables is threaded into is_shell_direct_invocation() and the same terminal-token resolver backs executes_runtime_variable(); exact $v and ${v} references are resolved after shlex removes quoting. Same-line runtime assignment delimiters include shell operators.
  • Residual backstops: B1 proves alias-indirected dangerous mode is classified dangerous-primitive even though the parser does not resolve the alias. B2 proves a non-dangerous alias residual remains parser-missed, then verifies the shipped global .* Claude hook and status-2 GATE_UNAVAILABLE denial for representative read, mutator, and custom/MCP tools without a lease session.
  • Stopping-criterion evidence A+B is committed in tests and architecture docs; C remains the fresh exact-head terra/Opus determination. Repository inventory remains exactly 14 gated / 14 total.
  • Fresh round-5 coverage: permanent guard remains 97% branch-aware aggregate (251 statements, 112 branches). Locked-good launcher and mutator-gate executables remain unchanged at their round-4 100% / 100% evidence.
  • Fresh round-5 gates: real-socket acceptance 50/50; persistence 10/10; launcher/gate 14/14; guard 12/12; coord 19/19; Mosaic 1385/1385; root 43/43; typecheck 42/42; lint 23/23; format and diff checks green.