feat(mosaic): install-ordering guard for lease-enforcement hook wiring (#869 Point-1 C2) #879

Merged
Mos merged 1 commits from feat/869-c2-install-ordering-guard into main 2026-07-23 18:48:34 +00:00
Owner

Summary

mosaic-link-runtime-assets copies runtime/claude/settings.json — which
embeds the PreToolUse mutator-gate.py hook and the Stop
receipt-observer-client.py hook — straight into ~/.claude/settings.json,
unconditionally. If the ACTIVATION half of the lease broker (C1's
leaseEnforcementActivatable(): a CLI build advertising launch-runtime
activation + a running broker supervisor) can't be confirmed on the host, the
fail-closed gate then denies every tool call with GATE_UNAVAILABLE — a
bricked host.

This adds the install-ordering guard: before the enforcement hooks are
wired, call leaseEnforcementActivatable() (imported directly from the C1
module, lease-activation-probe.ts) and decide whether they may be written.

The guard (packages/mosaic/src/commands/install-ordering-guard.ts)

  • guardClaudeSettingsWiring() / runInstallOrderingGuard() — pure,
    injectable-deps functions; a hidden CLI subcommand
    (mosaic __link-claude-settings <src> <dest>) bridges the bash wiring
    point to this TS logic.
  • activatable -> wire the hooks as-is, exit 0.
  • NOT activatable, no opt-out (default) -> strip only the two enforcement
    hook entries (every other hook — PreCompact/SessionStart revoke-lease,
    prevent-memory-write.sh, the PostToolUse qa/typecheck hooks, and the
    sibling reflect-stop-hook.sh inside the same Stop trigger — is left
    byte-identical), exit 1, print an actionable message:

    enforcement requested but activation half absent — needs a published CLI
    carrying launch-runtime activation + a broker supervisor; refusing to
    wire a dead gate (see #869)

  • NOT activatable + explicit opt-out -> wire anyway, with a loud warning.

The opt-out: --allow-inactive-enforcement

A real CLI flag, threaded through install.sh -> mosaic-link-runtime-assets
-> the hidden mosaic __link-claude-settings subcommand's own
--allow-inactive-enforcement option. Deliberately never an environment
variable — it can't sit as a silently-inherited default in a shell profile.
Absent (the default) => hard fail-loud path. When set on a non-activatable
host, wiring proceeds but a loud, explicit warning is always logged.

Wiring point located

packages/mosaic/framework/tools/_scripts/mosaic-link-runtime-assets is
where settings.json (and thus the enforcement hooks baked into it) actually
gets copied to ~/.claude/, called from both framework/install.sh's
post-install step and the wizard's finalize.ts. Both callers previously
swallowed stderr (2>&1 >/dev/null / stdio: 'pipe' with no surfacing) —
fixed so the guard's message actually reaches the operator instead of being
silent.

What's unchanged

mutator-gate.py's own fail-closed-on-absent-identity runtime behavior is
untouched — this guard only decides whether the hook gets wired, never
touches the gate's own denial logic. runtime_tools_unittest.py and
fail-closed-regression.spec.ts remain green, unmodified.

Red-first tests

  • install-ordering-guard.spec.ts (19 tests): probe=false => hooks stripped,
    exit 1, actionable message; probe=false + opt-out => hooks wired, loud warn,
    exit 0; probe=true => hooks wired normally, no logs; file-level
    runInstallOrderingGuard exercised against temp dirs only (backup-on-change
    parity with copy_file_managed, idempotent no-op writes) — never touches
    the real ~/.claude/settings.json.
  • test-install-ordering-guard.sh (5 shell scenarios, added to
    test:framework-shell): exercises the actual bash wiring in
    mosaic-link-runtime-assets (fake mosaic stub + the no-mosaic-on-PATH
    python3 fallback), including that every other runtime file still gets
    copied when the settings.json guard degrades.

Test plan

  • pnpm typecheck (via turbo, --filter=@mosaicstack/mosaic) — pass
  • pnpm lint (via turbo, --filter=@mosaicstack/mosaic) — pass
  • pnpm format:check — pass
  • pnpm --filter @mosaicstack/mosaic test (vitest + test:framework-shell,
    including the new shell regression) — pass, 1484+ tests
  • fail-closed-regression.spec.ts / runtime_tools_unittest.py — unchanged, green
  • Full-repo typecheck/lint/format:check via the pre-push hook — pass

Part of #869 (Point-1 C2)

## Summary `mosaic-link-runtime-assets` copies `runtime/claude/settings.json` — which embeds the `PreToolUse` `mutator-gate.py` hook and the `Stop` `receipt-observer-client.py` hook — straight into `~/.claude/settings.json`, unconditionally. If the ACTIVATION half of the lease broker (C1's `leaseEnforcementActivatable()`: a CLI build advertising launch-runtime activation + a running broker supervisor) can't be confirmed on the host, the fail-closed gate then denies every tool call with `GATE_UNAVAILABLE` — a bricked host. This adds the install-ordering guard: before the enforcement hooks are wired, call `leaseEnforcementActivatable()` (imported directly from the C1 module, `lease-activation-probe.ts`) and decide whether they may be written. ### The guard (`packages/mosaic/src/commands/install-ordering-guard.ts`) - `guardClaudeSettingsWiring()` / `runInstallOrderingGuard()` — pure, injectable-deps functions; a hidden CLI subcommand (`mosaic __link-claude-settings <src> <dest>`) bridges the bash wiring point to this TS logic. - **activatable** -> wire the hooks as-is, exit 0. - **NOT activatable, no opt-out (default)** -> strip only the two enforcement hook entries (every other hook — `PreCompact`/`SessionStart` revoke-lease, `prevent-memory-write.sh`, the `PostToolUse` qa/typecheck hooks, and the sibling `reflect-stop-hook.sh` inside the same `Stop` trigger — is left byte-identical), exit 1, print an actionable message: > enforcement requested but activation half absent — needs a published CLI > carrying launch-runtime activation + a broker supervisor; refusing to > wire a dead gate (see #869) - **NOT activatable + explicit opt-out** -> wire anyway, with a loud warning. ### The opt-out: `--allow-inactive-enforcement` A real CLI flag, threaded through `install.sh` -> `mosaic-link-runtime-assets` -> the hidden `mosaic __link-claude-settings` subcommand's own `--allow-inactive-enforcement` option. Deliberately **never** an environment variable — it can't sit as a silently-inherited default in a shell profile. Absent (the default) => hard fail-loud path. When set on a non-activatable host, wiring proceeds but a loud, explicit warning is always logged. ### Wiring point located `packages/mosaic/framework/tools/_scripts/mosaic-link-runtime-assets` is where `settings.json` (and thus the enforcement hooks baked into it) actually gets copied to `~/.claude/`, called from both `framework/install.sh`'s post-install step and the wizard's `finalize.ts`. Both callers previously swallowed stderr (`2>&1 >/dev/null` / `stdio: 'pipe'` with no surfacing) — fixed so the guard's message actually reaches the operator instead of being silent. ### What's unchanged `mutator-gate.py`'s own fail-closed-on-absent-identity runtime behavior is untouched — this guard only decides whether the hook gets **wired**, never touches the gate's own denial logic. `runtime_tools_unittest.py` and `fail-closed-regression.spec.ts` remain green, unmodified. ## Red-first tests - `install-ordering-guard.spec.ts` (19 tests): probe=false => hooks stripped, exit 1, actionable message; probe=false + opt-out => hooks wired, loud warn, exit 0; probe=true => hooks wired normally, no logs; file-level `runInstallOrderingGuard` exercised against temp dirs only (backup-on-change parity with `copy_file_managed`, idempotent no-op writes) — never touches the real `~/.claude/settings.json`. - `test-install-ordering-guard.sh` (5 shell scenarios, added to `test:framework-shell`): exercises the actual bash wiring in `mosaic-link-runtime-assets` (fake `mosaic` stub + the no-`mosaic`-on-PATH python3 fallback), including that every *other* runtime file still gets copied when the settings.json guard degrades. ## Test plan - [x] `pnpm typecheck` (via turbo, `--filter=@mosaicstack/mosaic`) — pass - [x] `pnpm lint` (via turbo, `--filter=@mosaicstack/mosaic`) — pass - [x] `pnpm format:check` — pass - [x] `pnpm --filter @mosaicstack/mosaic test` (vitest + `test:framework-shell`, including the new shell regression) — pass, 1484+ tests - [x] `fail-closed-regression.spec.ts` / `runtime_tools_unittest.py` — unchanged, green - [x] Full-repo typecheck/lint/format:check via the pre-push hook — pass Part of #869 (Point-1 C2)
jason.woltje added 1 commit 2026-07-23 18:33:12 +00:00
feat(mosaic): install-ordering guard for lease-enforcement hook wiring (#869 Point-1 C2)
All checks were successful
ci/woodpecker/pr/ci Pipeline was successful
b6f3656422
mosaic-link-runtime-assets wired the PreToolUse mutator-gate.py and Stop
receipt-observer-client.py hooks into ~/.claude/settings.json unconditionally.
If the activation half (C1's leaseEnforcementActivatable()) can't be
confirmed on the host, the fail-closed gate then denies every tool call —
bricking it.

Add a TS install-ordering guard (guardClaudeSettingsWiring /
runInstallOrderingGuard, hidden CLI bridge `mosaic __link-claude-settings`)
that imports leaseEnforcementActivatable() directly and decides whether the
enforcement hooks get wired:

  - activatable                    -> wire as-is.
  - NOT activatable, no opt-out    -> strip the enforcement hooks, exit
                                       non-zero with an actionable message
                                       (default, fail-loud).
  - NOT activatable + explicit
    --allow-inactive-enforcement   -> wire anyway, loud warning logged.

--allow-inactive-enforcement is a real CLI flag threaded through
install.sh -> mosaic-link-runtime-assets -> the hidden subcommand —
deliberately never an environment variable, so it can't sit as a
silently-inherited default. Wire mosaic-link-runtime-assets's guarded
settings.json copy to call out to the CLI (with a python3 fallback if
`mosaic` isn't resolvable at all), and stop swallowing stderr in
install.sh/finalize.ts so the guard's message actually reaches the operator.

mutator-gate.py's own fail-closed-on-absent-identity runtime behavior is
untouched (this only gates the WIRING); runtime_tools_unittest.py and
fail-closed-regression.spec.ts remain green.

Part of #869 (Point-1 C2)
Mos approved these changes 2026-07-23 18:48:32 +00:00
Mos left a comment
First-time contributor

Gate-16 APPROVE -- stamped by Mos (id-11)

PR author jason.woltje(id2) != approver Mos(id11); commit-author mosaic-coder != reviewer -> no Finding-0. Registers the independent reviewer's APPROVE (#869 Point-1 C2, install-ordering guard); MS-LEAD relayed, Mos stamps+merges.

VERDICT: APPROVE @ b6f3656422 (Part of #869 -- Point-1 card, umbrella stays open).

Independent review, all 8 PASS -- verified against the REAL production runtime/claude/settings.json template (not just fixtures): (1) guard strips ONLY the 2 enforcement hooks per-entry, siblings survive, activatable round-trip byte-identical, C1 probe read-only; (2) opt-out --allow-inactive-enforcement CLI-flag-ONLY (no env escape, strict ===true), absent->hard-fail(exit1,strip,actionable), present-on-non-activatable->wired+loud-warn, no silent path; (3) fail-loud reaches operator (install.sh stderr no longer swallowed); (4) bash wiring routes settings.json through guard + python3 fallback; (5) fail-closed locks untouched+green; (6) red-first 19 vitest + 5 shell, mktemp isolation; (7) firewall/scope clean (8 files, operator-agnostic).

The guard: wires the 2 enforcement hooks ONLY if leaseEnforcementActivatable(), else strips + fails LOUD -- the install-ordering half of the #828/#869 brick-prevention.

6-CHECK re-verified by Mos @b6f365642249390b7dbe9ff983fbc514220b7aab: open/not-merged/mergeable onto current main/base main; CI success wp1988 (8/8) @exact head; reviewed==CI==head; independent APPROVE author!=reviewer no REQUEST-CHANGES; body 'Part of #869'; queue clear.

## Gate-16 APPROVE -- stamped by Mos (id-11) PR author jason.woltje(id2) != approver Mos(id11); commit-author mosaic-coder != reviewer -> no Finding-0. Registers the independent reviewer's APPROVE (#869 Point-1 C2, install-ordering guard); MS-LEAD relayed, Mos stamps+merges. VERDICT: APPROVE @ b6f365642249390b7dbe9ff983fbc514220b7aab (Part of #869 -- Point-1 card, umbrella stays open). Independent review, all 8 PASS -- verified against the REAL production runtime/claude/settings.json template (not just fixtures): (1) guard strips ONLY the 2 enforcement hooks per-entry, siblings survive, activatable round-trip byte-identical, C1 probe read-only; (2) opt-out --allow-inactive-enforcement CLI-flag-ONLY (no env escape, strict ===true), absent->hard-fail(exit1,strip,actionable), present-on-non-activatable->wired+loud-warn, no silent path; (3) fail-loud reaches operator (install.sh stderr no longer swallowed); (4) bash wiring routes settings.json through guard + python3 fallback; (5) fail-closed locks untouched+green; (6) red-first 19 vitest + 5 shell, mktemp isolation; (7) firewall/scope clean (8 files, operator-agnostic). The guard: wires the 2 enforcement hooks ONLY if leaseEnforcementActivatable(), else strips + fails LOUD -- the install-ordering half of the #828/#869 brick-prevention. 6-CHECK re-verified by Mos @b6f365642249390b7dbe9ff983fbc514220b7aab: open/not-merged/mergeable onto current main/base main; CI success wp1988 (8/8) @exact head; reviewed==CI==head; independent APPROVE author!=reviewer no REQUEST-CHANGES; body 'Part of #869'; queue clear.
Mos merged commit d351caad36 into main 2026-07-23 18:48:34 +00:00
Sign in to join this conversation.
No Reviewers
2 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: mosaicstack/stack#879