Files
stack/docs/scratchpads/829-mutator-gate.md

113 lines
14 KiB
Markdown

# 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.