Files
stack/docs/architecture/mutator-class-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

7.5 KiB

Whole mutator-class lease gate

WI-2 adds the framework-native authorization boundary for Claude (including the supported Claudex overlay) and Pi. Every runtime-reported tool name reaches the lease broker before execution. The gate classifies capabilities by the whole tool class; it never parses a Bash command to decide whether that particular string looks read-only.

Default-deny policy

While a session is not VERIFIED, only these exact classes are allowed:

  • Claude: Read, Grep, Glob, Ls, Find
  • Pi: read, grep, find, ls
  • Both runtimes: the fixed mosaic_context_recover primitive

Every other built-in, unknown tool, and custom/MCP tool is consequential by default and is denied. This includes Claude Bash, Edit, Write, and NotebookEdit, plus Pi bash, edit, and write. A compromised model therefore cannot bypass Mosaic wrappers by selecting raw git, curl, kubectl, provider, deployment, or filesystem commands inside a generic mutator—the generic mutator itself is blocked before its input executes.

Broker-owned transition order

The authenticated broker is the sole lease writer:

  1. begin_verification revokes existing authority and pending tokens first, then records PENDING_VERIFICATION and mints one WI-1 single-use promotion token bound to the exact cycle.
  2. promote_lease accepts only that session/generation/binding/token combination.
  3. Token consumption commits before the volatile lease becomes VERIFIED. Promotion is last and cannot be reached directly from UNVERIFIED.
  4. revoke_lease, a runtime-generation increase, broker restart, or monotonic expiry removes mutator authority.

The initial TTL is capped at the ratified 300-second maximum. A caller may request a shorter positive TTL but cannot lengthen the maximum. Dual compaction-hook miss within an unexpired lease remains the ratified bounded T-A residual; once either observer revokes or TTL expires, the next consequential tool is denied.

A receipt is only a future promotion prerequisite. It is not an obedience, residency, or safety proof and never replaces this mechanical gate.

Runtime adapters

launch-runtime.py registers itself with the broker and then execs Claude or Pi so PID/starttime remain the authenticated parent anchor. It exports only the broker-minted session ID and current generation to descendants.

  • Claude installs mutator-gate.py as an all-tools (.*) PreToolUse hook.
  • mosaic claudex and mosaic yolo claudex preserve their isolated CLAUDE_CONFIG_DIR, merge the mandatory hook into that isolated settings.json, and use the same register-before-exec launcher. Malformed or symlinked isolated settings deny launch.
  • Pi invokes the same executable from its tool_call handler.

The executable submits the runtime's actual tool name to authorize_tool. Missing identity, malformed input/reply, timeout, broker unavailability, or denial exits with status 2 and blocks fail-closed.

Runtime-launch choke-point and permanent guard

Every repository-owned Claude/Pi launch entry converges on launch-runtime.py, either directly or through mosaicexecLeaseGatedRuntime. PRDY init/update and QA remediation invoke the wrapper directly so their existing prompts, dangerous-permission behavior, working directory, and environment survive without skipping broker registration. The raw Claude --dangerously-skip-permissions primitive is owned only by launch-runtime.py; callers request semantic --dangerous mode, and the wrapper validates Claude before injecting the primitive. @mosaicstack/coord rewrites direct Claude commands to mosaic claude and rejects unknown custom Claude launchers.

check-runtime-launches.py is the permanent completeness guard. It scans production shell, TypeScript/JavaScript, Python, and data launch definitions under packages/, apps/, plugins/, and tools/; direct literal, absolute-path, process-API, dynamic, command-substitution, eval, and variable-execution runtime launches fail. Shell comments are stripped with quote awareness, wrapper prefixes are tokenized with shlex, and only an invocation in command position with --runtime before the command separator is gated. Literal and tracked-variable command tokens use one terminal resolver after any nesting of exec, command, nohup, or env plus assignments. A direct command always wins over an inert marker on the same line. Independently, the raw dangerous primitive anywhere outside the choke-point is RED.

The command parser is a best-effort CI defense, not a complete shell interpreter. Alias/function redefinition, sourced commands, generated scripts, and encoded pipelines are intentionally residual rather than an invitation to chase an unbounded shell language. Two runtime controls backstop that residual surface: primitive ownership rejects a dangerous launch even when command identity is alias-indirected, and Claude's global .* PreToolUse hook invokes the broker gate for non-dangerous launches. Without MOSAIC_LEASE_SESSION_ID, representative read, mutator, and custom/MCP tools all fail closed with GATE_UNAVAILABLE. Hook absence or replacement remains in the documented T-C boundary.

Parser stopping criterion

  • A — realistic parser matrix: comments, inert strings/assignments, heredocs, continuations, chained commands, command substitution, eval, bare tracked variables, and quoted/unquoted tracked variables behind exec, command, nohup, or env are permanent RED regressions. Prefix-variable forms are covered in both multiline and same-line assignment shapes.
  • B — residual backstops: a dangerous alias-indirected launch is RED solely through primitive anchoring; a parser-missed non-dangerous alias launch is paired with an acceptance test proving the global all-tools hook denies every representative tool class as GATE_UNAVAILABLE without a lease.
  • C — independent fresh review: the parser class is considered complete only when reviewers find no new non-overlapping realistic evasion on the exact head. A and B are repository evidence; C is supplied by the fresh review round.

All three layers are load-bearing and complementary. The guard is mandatory in @mosaicstack/mosaic's test script, so root CI fails on a future realistic bypass. Real-socket tests separately prove PRDY init/update and QA receive broker sessions and deny an unverified mutator.

The live inventory is emitted by:

python3 packages/mosaic/framework/tools/lease-broker/check-runtime-launches.py --root . --json
Production launch family Gated entries
@mosaicstack/coord default/configured Claude command 2
Fleet runtime start 1
QA remediation + generated QA command 2
Orchestrator command construction/session launches 3
PRDY init/update 2
Mosaic Claude/Pi/Claudex adapter and wrapper boundary 4
Total 14 / 14

Assurance boundary

This closes T-A after an observer fires or lease expiry and T-B for in-runtime tool calls. Hook/extension absence, a runtime executing outside the gated launcher, ptrace/same-UID broker replacement, and other fully rotted behavior remain T-C. Server-side branch protection and required PR review/CI remain the irreducible line for protected repository mutations.