Compare commits

..
Author SHA1 Message Date
jason.woltjeandMos b981b4ec10 fix(wake): #934 mount-free, privilege-invariant seq-integrity fault injection (T9/T11 run in non-priv CI) (#936)
Co-authored-by: jason.woltje <[email protected]>
Co-committed-by: jason.woltje <[email protected]>
2026-07-26 16:34:58 +00:00
jason.woltjeandMos 9e81ffd7fc fix(wake): #932 stop reconciler re-enumerating already-CONSUMED detector state (#935)
Co-authored-by: jason.woltje <[email protected]>
Co-committed-by: jason.woltje <[email protected]>
2026-07-26 15:48:40 +00:00
jason.woltjeandMos 9becaf877f fix(wake): #917 gate the final observed_seq cursor write + observed.set/cursor consistency (defense-in-depth) (#933)
Co-authored-by: jason.woltje <[email protected]>
Co-committed-by: jason.woltje <[email protected]>
2026-07-26 15:14:38 +00:00
jason.woltjeandMos 17087efe15 fix(wake): #925 framework-ship canon fallback-wake (systemd timer + schema bound + A10 install/validate) — F7 out of the box (#931)
Co-authored-by: jason.woltje <[email protected]>
Co-committed-by: jason.woltje <[email protected]>
2026-07-26 13:44:51 +00:00
jason.woltjeandMos 0ea41e848b fix(wake): #913 installer adoption gaps — _lib dep-check fail-loud + mosaic-wake.service systemd-search-path link+validate (#930)
Co-authored-by: jason.woltje <[email protected]>
Co-committed-by: jason.woltje <[email protected]>
2026-07-26 12:37:29 +00:00
jason.woltjeandMos 347c1d57c1 fix(wake): #924 route dead-letter quarantine alarm via WAKE_ALARM_SINK_CMD with per-observed_seq dedup (G2a) (#929)
Co-authored-by: jason.woltje <[email protected]>
Co-committed-by: jason.woltje <[email protected]>
2026-07-26 11:45:09 +00:00
jason.woltjeandMos 13e6ce5e5c fix(wake): #927 enqueue TOCTOU — move stale-tmp cleanup off the hot enqueue path (no concurrent in-flight-write clobber) (#928)
Co-authored-by: jason.woltje <[email protected]>
Co-committed-by: jason.woltje <[email protected]>
2026-07-26 10:56:40 +00:00
jason.woltjeandMos 90265ef550 test(wake): #923 de-flake T10 concurrent-enqueue race (deterministic barrier) (#926)
Co-authored-by: jason.woltje <[email protected]>
Co-committed-by: jason.woltje <[email protected]>
2026-07-26 10:23:07 +00:00
jason.woltjeandMos 937a276208 fix(wake): #920 quarantine render-refused drain entry (no head-of-line block) + reconciler enumerations render orientation-tier (reconciled:true, render-tier not class=digest) (#922)
Co-authored-by: jason.woltje <[email protected]>
Co-committed-by: jason.woltje <[email protected]>
2026-07-26 08:45:51 +00:00
jason.woltjeandMos 712c770b7a fix(wake): #912 exercise the digest/HMAC trust suite in real CI (fix runner divergence + openssl + hard-require) (#921)
Co-authored-by: jason.woltje <[email protected]>
Co-committed-by: jason.woltje <[email protected]>
2026-07-26 08:11:28 +00:00
jason.woltjeandMos d967a4a926 fix(wake): #914 digest renderer — WAKE_AGENT-prefixed ack line + digest-class locator threading (#916)
Co-authored-by: jason.woltje <[email protected]>
Co-committed-by: jason.woltje <[email protected]>
2026-07-26 07:13:07 +00:00
jason.woltjeandMos c585ac3326 test(wake): #918 de-flake T7 ack-no-network-block (sub-second timing) (#919)
Co-authored-by: jason.woltje <[email protected]>
Co-committed-by: jason.woltje <[email protected]>
2026-07-26 06:46:28 +00:00
jason.woltjeandMos e2ec927b1c fix(wake): #908 unify observed_seq on a single store-side allocator (dissolve detector-private-counter seam) (#915)
Co-authored-by: jason.woltje <[email protected]>
Co-committed-by: jason.woltje <[email protected]>
2026-07-26 06:02:02 +00:00
jason.woltjeandMos 2378665eaf feat(wake): W7 A10 idempotent installer + mosaic-wake.service (component-manifest, Gate-A, blank-reset retire, snapshot-guard, fail-closed install-validate) (#911)
Co-authored-by: jason.woltje <[email protected]>
Co-committed-by: jason.woltje <[email protected]>
2026-07-26 04:19:09 +00:00
jason.woltjeandMos 003cdaa1a6 feat(wake): W6 — off-host dead-man beacon + pluggable alarm-sink adapter (#910)
Co-authored-by: jason.woltje <[email protected]>
Co-committed-by: jason.woltje <[email protected]>
2026-07-26 02:30:28 +00:00
jason.woltjeandMos 320f5bfb6f feat(wake): W5 — synthetic-canary FN-oracle + source-parity reconciler (#909)
Co-authored-by: jason.woltje <[email protected]>
Co-committed-by: jason.woltje <[email protected]>
2026-07-26 02:04:19 +00:00
jason.woltjeandMos 5df47e735e feat(wake): W4 — per-host delta-gated detector daemon (fail-loud source semantics, enqueues to W2 store) (#907)
Co-authored-by: jason.woltje <[email protected]>
Co-committed-by: jason.woltje <[email protected]>
2026-07-26 01:04:55 +00:00
jason.woltjeandMos dd1391fd76 fix(wake): digest hard-locator gate covers top-level .claim entries (Closes #905) (#906)
Co-authored-by: jason.woltje <[email protected]>
Co-committed-by: jason.woltje <[email protected]>
2026-07-26 00:58:40 +00:00
jason.woltjeandMos 10d957d095 feat(wake): W3 — cumulative-state digest renderer + non-circular HMAC signer (#904)
Co-authored-by: jason.woltje <[email protected]>
Co-committed-by: jason.woltje <[email protected]>
2026-07-26 00:32:12 +00:00
jason.woltjeandMos dc45eb7c30 feat(kbn): land KBN-101 Envelope A v6 (rc.20) — declarative sink-RBAC + RLS write-source (Form A) (#902)
Co-authored-by: jason.woltje <[email protected]>
Co-committed-by: jason.woltje <[email protected]>
2026-07-25 23:58:18 +00:00
jason.woltjeandMos 28f022d9c0 feat(wake): W2 — three-cursor durable store + RECEIVED/CONSUMED ack-wrapper + watch-list schema (#903)
Co-authored-by: jason.woltje <[email protected]>
Co-committed-by: jason.woltje <[email protected]>
2026-07-25 23:58:08 +00:00
jason.woltjeandMos 2726fab5e0 chore(framework): wire agent-send.test.sh into CI test:framework-shell (W1 follow-up) (#901)
Co-authored-by: jason.woltje <[email protected]>
Co-committed-by: jason.woltje <[email protected]>
2026-07-25 23:23:24 +00:00
jason.woltjeandMos ab6e8e80dc fix(framework): send-message.sh fail-loud submission verdict + regression tests (Patch 6) (#895)
Co-authored-by: jason.woltje <[email protected]>
Co-committed-by: jason.woltje <[email protected]>
2026-07-25 22:57:11 +00:00
jason.woltjeandMos 1933c6cb1d feat(framework): accept digest message class in agent-send.sh (W1) (#894)
Co-authored-by: jason.woltje <[email protected]>
Co-committed-by: jason.woltje <[email protected]>
2026-07-25 22:56:46 +00:00
jason.woltjeandMos 48a0c86093 fix(lease-broker): recovery_runtime_unittest wait_ready() connect-probe (co-equal CI flake, cherry-pick #898) (#900)
Co-authored-by: jason.woltje <[email protected]>
Co-committed-by: jason.woltje <[email protected]>
2026-07-25 22:26:38 +00:00
jason.woltjeandMos 79c8647fd9 fix(lease-broker): wait_ready() polls real connect-readiness not socket-file existence (flaky CI race) (#898)
Co-authored-by: jason.woltje <[email protected]>
Co-committed-by: jason.woltje <[email protected]>
2026-07-25 22:10:15 +00:00
jason.woltjeandMos 2483dada33 fix(framework): pr-review.sh -r/--repo + -H/--host overrides + UA + repo preflight (Patches 5/5c) (#896)
Co-authored-by: jason.woltje <[email protected]>
Co-committed-by: jason.woltje <[email protected]>
2026-07-25 22:08:08 +00:00
jason.woltjeandMos 4c117afe03 docs(framework): add WAKE-DOCTRINE.md guide (W0) (#893)
Co-authored-by: jason.woltje <[email protected]>
Co-committed-by: jason.woltje <[email protected]>
2026-07-25 21:57:51 +00:00
jason.woltjeandMos 2698ddb7b5 feat(comms): P1 presence — minimal Synapse + fleet presence room + mosaic.presence heartbeat + liveness (#888)
Co-authored-by: jason.woltje <[email protected]>
Co-committed-by: jason.woltje <[email protected]>
2026-07-25 21:03:18 +00:00
jason.woltjeandMos fabde1c834 docs(rfc): add RFC-001 (MACP/Matrix-native comms) + RFC-002 (install/config/topology) (#886)
Co-authored-by: jason.woltje <[email protected]>
Co-committed-by: jason.woltje <[email protected]>
2026-07-25 21:02:54 +00:00
jason.woltjeandMos 3c7890f17f fix(framework): detect-platform get_gitea_token fail-loud on absent per-slot token (Patch 2b) (#890)
Co-authored-by: jason.woltje <[email protected]>
Co-committed-by: jason.woltje <[email protected]>
2026-07-25 20:51:19 +00:00
jason.woltjeandMos 529c177830 fix(update): mosaic update runs the install-ordering guard post-reseed (#882 --sync-only bypass) (#883)
Co-authored-by: jason.woltje <[email protected]>
Co-committed-by: jason.woltje <[email protected]>
2026-07-23 22:18:34 +00:00
jason.woltjeandMos a32ce4c8f9 feat(869-c4): activation version-coupling assertion (Part of #869)
Part of #869

Mos (id-11) Gate-16 merge: independent APPROVE @90eb48fa (fail-closed identity locks byte-unchanged verified), author id2 != approver id11, clean mosaic-coder author, CI green wp1992. #869 Point-1 CODE COMPLETE (C1/C3/C5/C2/C4).

Co-authored-by: jason.woltje <[email protected]>
Co-committed-by: jason.woltje <[email protected]>
2026-07-23 19:07:27 +00:00
jason.woltjeandMos d351caad36 feat(869-c2): install-ordering enforcement-hook guard (Part of #869)
Part of #869

Mos (id-11) Gate-16 merge: independent APPROVE @b6f36564 (8/8, verified vs real production settings template), author id2 != approver id11, clean mosaic-coder author, CI green wp1988.

Co-authored-by: jason.woltje <[email protected]>
Co-committed-by: jason.woltje <[email protected]>
2026-07-23 18:48:33 +00:00
jason.woltjeandMos 76b86a246e feat(869-c5): mosaic doctor activation-check (Part of #869)
Part of #869

Mos (id-11) Gate-16 merge: independent APPROVE @e75e3238 (8/8), author id2 != approver id11, clean mosaic-coder author, CI green wp1987.

Co-authored-by: jason.woltje <[email protected]>
Co-committed-by: jason.woltje <[email protected]>
2026-07-23 18:38:21 +00:00
jason.woltjeandMos 4422231bdb feat: per-agent Gitea identity resolution (#873)
Closes #873

Mos (id-11) Gate-16 merge: independent APPROVE @4b472a22 (author-blocker dissolved via (a) re-author, identical tree hash to tech-approved head), author id2 != approver id11, clean mosaic-coder commit-author, CI green wp1985. Framework train COMPLETE 6/6.

Co-authored-by: jason.woltje <[email protected]>
Co-committed-by: jason.woltje <[email protected]>
2026-07-23 18:09:34 +00:00
jason.woltjeandMos 8504216964 fix(pr-review): case-insensitive _belongs slug compare (#875)
Closes #875

Mos (id-11) Gate-16 merge: independent APPROVE @9d8d58ae, author id2 != approver id11, clean mosaic-coder commit-author, CI green wp1982.

Co-authored-by: jason.woltje <[email protected]>
Co-committed-by: jason.woltje <[email protected]>
2026-07-23 17:53:26 +00:00
jason.woltjeandMos 7edc9b3121 fix(gitea): direct REST comment/review with fail-closed read-back (#865)
Closes #865

Mos (id-11) Gate-16 merge: fresh confirmatory independent APPROVE @8ac7e70f (1241-case fuzz 0 fail-open), author id2 != approver id11, clean commit-author, CI green wp1966.

Co-authored-by: jason.woltje <[email protected]>
Co-committed-by: jason.woltje <[email protected]>
2026-07-23 17:25:32 +00:00
jason.woltjeandMos 2f50c0876b feat(869-c3): lease-broker supervisor unit (Part of #869)
Part of #869

Mos (id-11) Gate-16 merge: independent APPROVE @75235ef8 (9/9, no live host mutation), author id2 != approver id11, CI green wp1971.

Co-authored-by: jason.woltje <[email protected]>
Co-committed-by: jason.woltje <[email protected]>
2026-07-23 17:19:48 +00:00
jason.woltjeandMos db90da347e feat(869-c1): activation-capability probe (Part of #869)
Part of #869

Mos (id-11) Gate-16 merge: independent 3-round APPROVE @c5a2bcc5, author id2 != approver id11, CI green wp1973.

Co-authored-by: jason.woltje <[email protected]>
Co-committed-by: jason.woltje <[email protected]>
2026-07-23 17:14:57 +00:00
jason.woltjeandMos 48fd1df28a fix(ci-queue-wait): treat absent branch (404) as queue-clear (#872)
Closes #872

Mos (id-11) Gate-16 merge: independent review APPROVE @23cbdaf8, author jason.woltje(id2) != approver Mos(id11), CI green wp1974.

Co-authored-by: jason.woltje <[email protected]>
Co-committed-by: jason.woltje <[email protected]>
2026-07-23 17:08:57 +00:00
jason.woltje b79336a8c1 feat(orchestrator): board-roll.sh — auto-roll LIVE board to LEDGER under byte cap (#868)
feat(orchestrator): board-roll.sh - auto-roll LIVE board to LEDGER under byte cap

Closes #868
2026-07-22 09:20:03 +00:00
jason.woltje 4e5af23214 Merge pull request 'skills: add glpi-* family (solve, followup, sweep, list, create)' (#863) from feat/glpi-skills into main 2026-07-21 01:09:50 +00:00
Hermes Agent 880c28b191 docs(glpi-skills): genericize operator-specific content per review 2026-07-20 19:45:50 -05:00
Jason WoltjeandClaude Opus 4.8 7bc2dfb6c8 skills: add glpi-* family (solve, followup, sweep, list, create)
GLPI helpdesk workflow skills written against the portable
tools/glpi/ tooling (session-init.sh, ticket-list.sh, ticket-create.sh),
cross-linked via [[glpi-*]]:

- glpi-solve    — close a ticket by setting status Solved (5); GLPI auto-closes
- glpi-followup — add a followup via the top-level /ITILFollowup endpoint
- glpi-sweep    — read-only hunt for done-but-open tickets needing Solve
- glpi-list     — query tickets by status/recency
- glpi-create   — open a new ticket

Core rule encoded: completing work means setting status Solved, not just
posting a resolution followup (a followup documents; only Solved auto-closes).

Note: illustrative examples in the bodies are USC-flavored (M2M / helpdesk
ticket numbers) and can be genericized in review if preferred.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
Claude-Session: https://claude.ai/code/session_019GjBgrb9tHgvq414Fqj37c
2026-07-20 18:04:53 -05:00
jason.woltje b0d78d8632 fix(mosaic): de-flake mutator-class lease gate TTL-expiry test (#861) 2026-07-20 10:32:45 +00:00
jason.woltje 344d86a635 fix(#812 follow-up): normalize detect-platform.sh host-match port comparison by scheme (#859) 2026-07-20 10:13:29 +00:00
jason.woltje acd7d380f6 fix(framework): install deps on worktree bootstrap + legible deps-preflight at gate seam (#856) (#858) 2026-07-20 09:38:12 +00:00
jason.woltje 3b70c66c07 fix(framework): drop unsupported --comment from tea pr approve/reject; route review body via durable comment (#835) (#857) 2026-07-20 09:19:48 +00:00
jason.woltje 11d2818453 docs(tasks): FCM-M5-001 done — verified completion evidence (supersedes #848) (#853) 2026-07-20 07:45:08 +00:00
jason.woltje aa999daf1b fix(framework): durable Gitea comment posting in pr-review.sh via REST + read-back verify (#812) (#852) 2026-07-20 06:45:48 +00:00
jason.woltje 77c9a82614 fix(lease-broker): de-flake recovery_runtime b2 broker-socket ConnectionRefused race (#849) (#851) 2026-07-20 06:44:37 +00:00
jason.woltje 627cf2bb38 docs(fleet): add operator configuration guide (#789) 2026-07-20 05:22:25 +00:00
jason.woltje 0582a8912b WI-7 #834: T-C server-side branch-protection posture + R1 honesty amendment (#847) 2026-07-20 04:20:02 +00:00
jason.woltje 2509eb7646 WI-6 (#833): constrained recovery command + mosaic-context-refresh skill wrapper (#846) 2026-07-20 03:33:00 +00:00
jason.woltje 07553ead33 WI-5 #832: Receipt-challenge protocol (compaction-refresh, milestone 188) (#845) 2026-07-19 23:18:56 +00:00
jason.woltje e522b22fa4 WI-4 (#831): verbatim-hashed normative fragments (B_payload/H_payload) (#844) 2026-07-19 20:34:20 +00:00
jason.woltje e4d7d4502d WI-3 (#830): compaction observers → revoke + D4 same-PID generation auto-revoke (#842) 2026-07-19 19:46:28 +00:00
jason.woltje 8dfcf1903e fix(#838): bound broker reply deadlines + fail-close empty-read; de-flake acceptance harness (#839)
Bound broker reply deadlines (separate read/lock/send budgets, BROKER_BUSY-before-mutation, fresh post-handle send budget → closes drop-after-commit window); fail-close empty/truncated read → GATE_UNAVAILABLE deny. De-flakes the 1917 acceptance surface. terra CODE APPROVE (pi) + Opus SECREV APPROVED (claude) — RoR comment 18143. Promote-lease-lost-ACK residual = fail-safe two-generals observability-gap, routed to WI-3 D2-v5 as named-disclosed-bounded-residual (route i). Gate-16 3-principal author=gpt-sol.

closes #838
2026-07-18 07:15:50 +00:00
jason.woltje abd2791f59 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
jason.woltje 8ec67a1126 feat(mosaic): add authenticated external lease broker (#836) 2026-07-18 03:12:23 +00:00
265 changed files with 33559 additions and 5498 deletions
+1
View File
@@ -7,3 +7,4 @@ pnpm-lock.yaml
.claude/
docs/tess/TASKS.md
docs/scratchpads/
packages/mosaic/src/fleet/testdata/documentation-publication-v1/inline-migration-v1.json
+6
View File
@@ -98,6 +98,12 @@ steps:
DATABASE_URL: postgresql://mosaic:mosaic@ci-postgres:5432/mosaic
commands:
- *enable_pnpm
# openssl (#912) is the wake HMAC signer: the digest H1/H2, beacon B12,
# and install I8 legs hard-require it in CI. It is baked into ci-base via
# Dockerfile.ci, but ci-base only rebuilds on push-to-main/tag — this
# `apk add` guarantees openssl is present on PR pipelines too (and is a
# fast no-op once the rebuilt image already ships it).
- apk add --no-cache openssl
# postgresql-client (pg_isready) is baked into ci-base.
# Wait up to 60s for CI postgres to be ready; fail fast if it never comes up.
- |
+4 -1
View File
@@ -25,7 +25,10 @@ FROM node:24-alpine
# postgresql-client used by the test step's pg_isready readiness probe. `bash`,
# `git`, and `jq` are baked here too — framework shell tests and the shipped
# Codex review wrappers require them without per-run installation in ci.yml.
RUN apk add --no-cache python3 make g++ postgresql-client bash git jq
# `openssl` (#912) is the non-circular HMAC signer for the wake trust layer:
# the digest H1/H2, beacon B12, and install I8 legs hard-require it in CI so the
# §4 G6 evidence comes from an actually-run HMAC leg, not a skipped one.
RUN apk add --no-cache python3 make g++ postgresql-client bash git jq openssl
# Pin pnpm to the repo's packageManager version via corepack.
RUN corepack enable && corepack prepare [email protected] --activate
+23
View File
@@ -79,6 +79,29 @@ Jarvis (v0.2.0) is a self-hosted AI assistant with a Python FastAPI backend and
---
## Compaction Refresh Trust Lifecycle (M1, #827#830)
### Problem and objective
Context compaction, session replacement, and same-PID runtime reloads can leave a previously VERIFIED runtime lease attached to stale directives. M1 must revoke that authority mechanically for Claude (including Claudex) and Pi without trusting caller-asserted identity or forking the external broker state machine.
### Requirements
1. `CR-REQ-01`: Claude `PreCompact` and `SessionStart` with matcher `compact`, plus Pi `session_before_compact` and the first post-`session_compact` `context`, SHALL independently revoke the active broker lease.
2. `CR-REQ-02`: Runtime generation increases—including same-PID Pi reload/new/resume/fork and Claude resume/clear—SHALL monotonically replace the prior broker incarnation and inherit no VERIFIED lease.
3. `CR-REQ-03`: A fired observer that cannot confirm broker revocation SHALL fail closed through lifecycle cancellation, a private local generation fence, and/or a runtime-local tool latch. The existing all-tools broker gate remains authoritative.
4. `CR-REQ-04`: The lease TTL SHALL remain monotonic and capped at 300 seconds. If both observers are missed, within-TTL consequential actions remain allowed and after-TTL actions are denied. This named bounded residual stale window SHALL be documented without claiming a mutator-action bound inside the window.
5. `CR-REQ-05`: Hook descendants SHALL use the broker-minted session and owner-only current-generation state inherited from register-before-exec. Caller-minted sessions and parallel lease state machines remain forbidden.
### Acceptance criteria
1. `AC-CR-01`: Real-socket tests prove each Claude observer revokes, Pi lifecycle tests prove both observer paths, and Claudex isolated settings preserve and install the mandatory hooks.
2. `AC-CR-02`: A same-PID generation test proves the old generation is stale and the replacement generation is UNVERIFIED across reload/resume/fork-equivalent lifecycle events.
3. `AC-CR-03`: RED-first T12b/T30 evidence explicitly reports dual-hook miss within TTL as **ALLOWED** and after TTL as **DENIED**.
4. `AC-CR-04`: Attributable executable coverage is at least 85%, the full repository suite is green on deterministic main, and independent code/security review completes before merge.
---
## Fleet Declarative Configuration Management Workstream (FCM, #758)
### Problem and objective
+32 -4
View File
@@ -1,5 +1,14 @@
# Documentation Sitemap
## Compaction refresh lease broker
- [Internal broker protocol](architecture/lease-broker-protocol.md) — kernel identity, ancestry and generation invariants, framed requests, responses, and persisted cycle bindings.
- [Broker operations](guides/lease-broker-operations.md) — protected paths, startup, constrained recovery, fail-closed posture, distinct-principal deployment, and residual risk.
- [Constrained recovery skill](../packages/mosaic/framework/skills/mosaic-context-refresh/SKILL.md) — source-resident thin wrapper, receipt scope, C4 replay boundary, and T-C middle-drop disclosure.
- [Lease-broker security notes](architecture/lease-broker-security.md) — identity, whole-class authorization, threat boundaries, and coordinator review requirements.
- [Whole mutator-class gate](architecture/mutator-class-gate.md) — default-deny policy, revoke-first/promote-last state machine, TTL, runtime adapters, and T-B/T-C assurance boundary.
- [Compaction revocation lifecycle](architecture/compaction-revocation.md) — Claude/Pi observer matrix, same-PID generation rollover, failure fencing, and the named bounded residual stale window.
## CLI and skill management
- [Skill registration user guide](guides/user-guide.md#claude-code-skill-registration) — register, unregister, list statuses, automatic install/update reconciliation, and Claude reload behavior.
@@ -7,10 +16,24 @@
## Fleet configuration management
- [Generated environment boundary](fleet/reference/generated-env-boundary.md) — roster-derived launch projection, strict local data, legacy quarantine, and downstream interface evidence.
- [Roster v2 structural contract](fleet/reference/roster-v2-fields.md) — local-tmux schema v2 parsing and structural validation.
- [Role classes and authority](fleet/reference/role-classes.md) — canonical role resolver and protected authority boundaries.
- [Executable asset dispositions](fleet/migration/example-profile-disposition.md) — shipped v1 fixture/profile/service validation posture.
- [Fleet configuration entry point](fleet/README.md) — desired-versus-observed decision tree and complete operator link map.
- [Desired, derived, and observed state](fleet/concepts/desired-vs-observed-state.md) — roster authority, generation, ownership, and drift.
- [Identity, class, and runtime](fleet/concepts/identity-class-runtime.md) — stable name, display alias, class, runtime, provider, and model separation.
- [Role authority and leases](fleet/concepts/role-authority-and-leases.md) — validator/merge-gate separation and bounded lease authority.
- [Generated launch chain](fleet/concepts/generated-env-launch-chain.md) — strict data parsing, precedence, and quarantine.
- [Roster v2 structural contract](fleet/reference/roster-v2-fields.md) — schema, supported values, required fields, defaults, and constraints.
- [Fleet CLI reference](fleet/reference/cli.md) — local desired-state commands, JSON/exit behavior, and gateway-catalog separation.
- [Lifecycle transitions](fleet/reference/lifecycle-transitions.md) — create/apply/reboot/migration/rollback boundaries.
- [Status and drift](fleet/reference/status-and-drift.md) — desired/managed/observed state and current/future classifications.
- [Safe agent CRUD](fleet/how-to/create-update-delete-agent.md) — expected generation, dry-run, and partial-failure recovery.
- [Local lifecycle operations](fleet/how-to/start-stop-restart.md) — persisted versus one-shot actions.
- [Configurable interaction instance](fleet/how-to/configure-tess-interaction.md) and [validator instance](fleet/how-to/configure-ultron-validator.md) — generic identities and protected limits.
- [Reconcile and recover](fleet/operations/reconcile-and-recover.md) — plan/apply lock and recovery behavior.
- [Environment quarantine](fleet/operations/env-quarantine.md) — private evidence and value-free diagnostics.
- [Systemd/tmux troubleshooting](fleet/operations/systemd-tmux-troubleshooting.md) — socket, holder, unmanaged-session, and lock decisions.
- [Backup/restore boundary](fleet/operations/backup-restore.md) and [upgrade-assets hold](fleet/operations/upgrade-assets.md).
- [v1-to-v2 migration preview](fleet/migration/v1-to-v2.md) and [executable artifact dispositions](fleet/migration/example-profile-disposition.md).
- [FCM M5 closure evidence](reports/documentation/758-fleet-config-ia-closure.md) and [approved deferrals](reports/deferred/758-fleet-config-deferrals.md).
## Official channel plugins
@@ -73,3 +96,8 @@
- [Logical identity and connector lease/fencing implementation](https://git.mosaicstack.dev/mosaicstack/stack/issues/755)
- [M1 logical identity and fencing architecture](architecture/mos-runtime-portability-m1.md)
- [M1 connector lease operations](guides/mos-connector-lease-operations.md)
## Comms evolution — Matrix-native MACP (design, draft)
- [RFC-001 — MACP: a Mosaic-native, Matrix-native comms layer](rfcs/RFC-001-MACP-MATRIX-NATIVE.md) — Synapse + Mosaic appservice backbone, MACP v1 protocol, presence/escalation, federation, strangler migration off the Hermes MCP bridge.
- [RFC-002 — Install, configuration & topology for the Matrix/MACP comms system](rfcs/RFC-002-INSTALL-CONFIG-TOPOLOGY.md) — open-source install topology modes, ACME cert provisioning, pluggable secret backend, and config precedence.
+14 -14
View File
@@ -52,20 +52,20 @@ Active workstream is **W1 — Federation v1**. Workers should:
> the repository quality gates, independent code and security review, terminal-green CI, and
> the applicable acceptance evidence before merge. Issue #758 remains open until M5 closes.
| id | status | description | issue | agent | repo | branch | depends_on | estimate | notes |
| ---------- | ----------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----- | ------------- | ----------------- | --------------------------------------- | ---------------------------------------------- | -------- | ---------------------------------------------------------------------------------------------------------------- |
| FCM-M0-001 | done | Publish normative PRD requirements/acceptance criteria, this M0M5 DAG, docs-IA checklist, and legacy example/profile disposition inventory; no implementation changes | #758 | sonnet | mosaicstack/stack | `docs/758-fleet-config-management` | — | 18K | Merged via #760 (`c32d85a`); parent #758 intentionally remains open through M5 |
| FCM-M1-001 | done | Implement narrow local-tmux v2 roster structural contract/compiler with YAML/JSON canonicalization and schema/parser parity tests | #758 | coder0 | mosaicstack/stack | `feat/758-roster-v2-compiler` | FCM-M0-001 | 30K | #764 squash `aa5b43b`; exact-head RoR and PR/main terminal-green CI; no lifecycle or live mutation |
| FCM-M1-002 | done | Reuse existing profile/persona/provision resolver for roster semantics; add canonical class/authority validation and approved aliases | #758 | native-sonnet | mosaicstack/stack | `feat/758-shared-role-resolution` | FCM-M0-001 | 25K | #768 squash `a5e8e55`; shared resolver and canonical authority/alias validation delivered |
| FCM-M1-003 | done | Convert the M0 legacy inventory into executable example/profile/service-preset validation and explicit v1-version/retirement checks | #758 | codex | mosaicstack/stack | `test/758-example-profile-dispositions` | FCM-M1-001, FCM-M1-002 | 20K | #770 squash `e9c4aa3`; shipped artifact disposition validation delivered |
| FCM-M2-001 | done | Migrate generic launch chain to deterministic `.env.generated` plus strict data-only `.env.local`; quarantine forbidden legacy keys | #758 | codex | mosaicstack/stack | `feat/758-generated-env-boundary` | FCM-M1-001, FCM-M1-002 | 30K | #772 squash `191efae`; generated/local boundary and private quarantine delivered |
| FCM-M2-002 | done | Add generation-guarded local fleet agent create/get/update/delete mutations with plan/dry-run, atomic roster writes, and recovery output | #758 | codex | mosaicstack/stack | `feat/758-fleet-agent-crud` | FCM-M1-001, FCM-M2-001 | 30K | #773 squash `bc5e736`; generation-guarded atomic CRUD and recovery contracts delivered |
| FCM-M3-001 | done | Implement local roster-owned reconcile/apply plus lifecycle/status/verify/doctor contracts and stable JSON/exit codes | #758 | codex | mosaicstack/stack | `feat/758-local-reconciler` | FCM-M2-001, FCM-M2-002 | 35K | #785 squash `4990905`; exact roster-owned systemd/tmux reconcile and lifecycle contracts delivered |
| FCM-M3-002 | in-progress | Add isolated systemd/tmux lifecycle, drift, socket, unmanaged-session, crash, and rollback acceptance coverage | #758 | sonnet | mosaicstack/stack | `test/758-reconciler-lifecycle-gates` | FCM-M3-001 | 25K | Canonical v2 named-socket + legacy-v1 default-server boundaries; fake adapters/temp fixtures only |
| FCM-M4-001 | not-started | Implement field-complete v1-to-v2 inventory/preview/migrator with alias, lifecycle, env-quarantine, and remote/connector disposition evidence | #758 | codex | mosaicstack/stack | `feat/758-v1-v2-migrator` | FCM-M1-003, FCM-M3-001 | 35K | Preview first; no unreviewed lifecycle inference |
| FCM-M4-002 | not-started | Add reversible canary migration, rollback, stale-projection/orphan classification, and current-host 9-managed/3-unmanaged fixture coverage | #758 | sonnet | mosaicstack/stack | `test/758-migration-rollback-gates` | FCM-M4-001, FCM-M3-002 | 25K | Never starts a previously stopped agent or kills an unproven unmanaged session |
| FCM-M5-001 | not-started | Deliver the accepted fleet documentation IA, how-to/operations/migration references, and link/example validation | #758 | haiku | mosaicstack/stack | `docs/758-fleet-config-operator-docs` | FCM-M1-003, FCM-M2-002, FCM-M3-001, FCM-M4-001 | 24K | Must close every checklist item or record an approved deferral |
| FCM-M5-002 | not-started | Package/update asset-drift checks, rolling local canary, independent validation certificate, and release evidence | #758 | sonnet | mosaicstack/stack | `feat/758-fleet-config-release-gate` | FCM-M3-002, FCM-M4-002, FCM-M5-001 | 30K | Final #758 gate: quality, independent code/security review, validator certificate, merge-gate approval, green CI |
| id | status | description | issue | agent | repo | branch | depends_on | estimate | notes |
| ---------- | ----------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----- | ------------- | ----------------- | --------------------------------------- | ---------------------------------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| FCM-M0-001 | done | Publish normative PRD requirements/acceptance criteria, this M0M5 DAG, docs-IA checklist, and legacy example/profile disposition inventory; no implementation changes | #758 | sonnet | mosaicstack/stack | `docs/758-fleet-config-management` | — | 18K | Merged via #760 (`c32d85a`); parent #758 intentionally remains open through M5 |
| FCM-M1-001 | done | Implement narrow local-tmux v2 roster structural contract/compiler with YAML/JSON canonicalization and schema/parser parity tests | #758 | coder0 | mosaicstack/stack | `feat/758-roster-v2-compiler` | FCM-M0-001 | 30K | #764 squash `aa5b43b`; exact-head RoR and PR/main terminal-green CI; no lifecycle or live mutation |
| FCM-M1-002 | done | Reuse existing profile/persona/provision resolver for roster semantics; add canonical class/authority validation and approved aliases | #758 | native-sonnet | mosaicstack/stack | `feat/758-shared-role-resolution` | FCM-M0-001 | 25K | #768 squash `a5e8e55`; shared resolver and canonical authority/alias validation delivered |
| FCM-M1-003 | done | Convert the M0 legacy inventory into executable example/profile/service-preset validation and explicit v1-version/retirement checks | #758 | codex | mosaicstack/stack | `test/758-example-profile-dispositions` | FCM-M1-001, FCM-M1-002 | 20K | #770 squash `e9c4aa3`; shipped artifact disposition validation delivered |
| FCM-M2-001 | done | Migrate generic launch chain to deterministic `.env.generated` plus strict data-only `.env.local`; quarantine forbidden legacy keys | #758 | codex | mosaicstack/stack | `feat/758-generated-env-boundary` | FCM-M1-001, FCM-M1-002 | 30K | #772 squash `191efae`; generated/local boundary and private quarantine delivered |
| FCM-M2-002 | done | Add generation-guarded local fleet agent create/get/update/delete mutations with plan/dry-run, atomic roster writes, and recovery output | #758 | codex | mosaicstack/stack | `feat/758-fleet-agent-crud` | FCM-M1-001, FCM-M2-001 | 30K | #773 squash `bc5e736`; generation-guarded atomic CRUD and recovery contracts delivered |
| FCM-M3-001 | done | Implement local roster-owned reconcile/apply plus lifecycle/status/verify/doctor contracts and stable JSON/exit codes | #758 | codex | mosaicstack/stack | `feat/758-local-reconciler` | FCM-M2-001, FCM-M2-002 | 35K | #785 squash `4990905`; exact roster-owned systemd/tmux reconcile and lifecycle contracts delivered |
| FCM-M3-002 | in-progress | Add isolated systemd/tmux lifecycle, drift, socket, unmanaged-session, crash, and rollback acceptance coverage | #758 | sonnet | mosaicstack/stack | `test/758-reconciler-lifecycle-gates` | FCM-M3-001 | 25K | Canonical v2 named-socket + legacy-v1 default-server boundaries; fake adapters/temp fixtures only |
| FCM-M4-001 | done | Implement field-complete v1-to-v2 inventory/preview/migrator with alias, lifecycle, env-quarantine, and remote/connector disposition evidence | #758 | codex | mosaicstack/stack | `feat/758-v1-v2-migrator` | FCM-M1-003, FCM-M3-001 | 35K | PR #788; final head `d63bb0206a1d312ab8352ec1d3ca3631146b0baa`; tree `4da210da9a71b035130d4160a4a2e691bdfde2da`; squash `9745bc3f29c26b021a478b7ad03cfb494f6c9de3`; descendant-main pipeline 1855 terminal success |
| FCM-M4-002 | not-started | Add reversible canary migration, rollback, stale-projection/orphan classification, and current-host 9-managed/3-unmanaged fixture coverage | #758 | sonnet | mosaicstack/stack | `test/758-migration-rollback-gates` | FCM-M4-001, FCM-M3-002 | 25K | HOLD: never starts a previously stopped agent or kills an unproven unmanaged session; not authorized by FCM-M5-001 |
| FCM-M5-001 | done | Deliver the accepted fleet documentation IA, how-to/operations/migration references, and link/example validation | #758 | haiku | mosaicstack/stack | `docs/758-fleet-config-operator-docs` | FCM-M1-003, FCM-M2-002, FCM-M3-001, FCM-M4-001 | 24K | #789 content squash 627cf2bb; de-flake repair PR#851/#849 squash 77c9a826; completion proof wp1937 @aa999daf push/ci step 49632 recovery_runtime_unittest.py 3/3 OK (closes wp1932 step 49576 Errno111) |
| FCM-M5-002 | not-started | Package/update asset-drift checks, rolling local canary, independent validation certificate, and release evidence | #758 | sonnet | mosaicstack/stack | `feat/758-fleet-config-release-gate` | FCM-M3-002, FCM-M4-002, FCM-M5-001 | 30K | HOLD: final #758 gate; quality, independent code/security review, validator certificate, merge-gate approval, and green CI remain out of M5-001 |
## Thin-core prompt diet (#528) — feat/contract-thin-core
@@ -0,0 +1,59 @@
# Compaction observer revocation and runtime generations
WI-3 connects Claude and Pi compaction/session lifecycle events to the existing authenticated lease-broker state machine. It does not add a second lease store or let runtime hooks assert identity. Each observer inherits the broker-minted session, resolves the current private runtime generation, and sends the existing `revoke_lease` action over the authenticated Unix socket.
## Observer matrix
| Runtime | Lifecycle signal | Action |
| ---------------- | ---------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------- |
| Claude / Claudex | `PreCompact` | Revoke the current lease before compaction. A non-zero hook result blocks the lifecycle transition. |
| Claude / Claudex | `SessionStart` with matcher `compact` | Revoke again after compacted context starts. |
| Claude / Claudex | `SessionStart` with matcher `resume\|clear` | Atomically advance the private generation, then revoke the replacement incarnation. |
| Pi | `session_before_compact` | Revoke before compaction; return `{ cancel: true }` if revocation cannot be confirmed. |
| Pi | `session_compact` then the first `context` | Arm and run an independent post-compaction revoke. A failed post observer blocks later tools locally until a retry succeeds. |
| Pi | `session_start` with reason `reload`, `new`, `resume`, or `fork` | Atomically advance the private generation, then revoke the replacement incarnation before reuse. |
The first observer that reaches the broker deletes pending promotion tokens and makes the lease `UNVERIFIED`. The second compaction observer is deliberate redundancy, not a prerequisite for the first. Claudex receives the same mandatory hooks in its isolated `CLAUDE_CONFIG_DIR`; hook merging preserves unrelated isolated settings and rejects malformed or symlinked settings fail-closed.
## Private generation authority
`launch-runtime.py` still registers before `exec`, preserving the kernel-authenticated PID/starttime anchor. It now also creates `generation-<broker-session>.state` beside the broker socket. The file is owner-only mode `0600` under the broker's mode-`0700` directory. Hook descendants read that file instead of relying only on an immutable inherited environment value.
Generation changes use an exclusive file lock, validate owner/type/mode/size, increment monotonically, truncate and write the complete new value, and `fsync` before contacting the broker. Therefore reload, new-session, resume, and fork events may retain the same PID/starttime while still becoming a new broker incarnation. The higher generation causes the broker to atomically discard prior tokens and lease authority; the replacement generation inherits no VERIFIED lease.
If an observer fires while broker transport is unavailable, `revoke-lease.py` advances the private generation as a local fence before returning non-zero. Every later all-tools gate reads that higher value. When the broker is reachable again, authentication of that value performs the same old-generation revocation before authorization. Pi also keeps a process-local post-compaction/rollover failure latch that blocks tool calls. An unsafe or unreadable generation file itself makes both lifecycle revocation and tool authorization fail closed.
## Threat contract and stopping boundary
### BOUNDED RESIDUAL STALE WINDOW
If **both** pre- and post-compaction observers are missed entirely, no revocation signal exists. During the remaining unexpired lease, **within-TTL consequential actions are allowed**. Their count and timing are **bounded by lease expiry, not by the mutator gate**. WI-3 makes no claim that it bounds mutator actions inside this stale interval. The broker's monotonic lease TTL is capped at 300 seconds; after expiry, the next consequential tool is denied with `LEASE_EXPIRED`.
This is the named D2-v5 T-A residual. It is distinct from an observer that fires but cannot contact the broker: the latter creates a local generation fence and fails closed. It is also distinct from T-C total rot, where the lifecycle observers and the all-tools gate are both absent or replaced. Server-side branch protection, required CI, and independent review remain the irreducible backstop for T-C.
| Condition | Result |
| --------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------- |
| Either compaction observer succeeds | Existing lease and pending promotion tokens are revoked immediately. |
| Observer runs but broker confirmation fails | Lifecycle transition is denied where supported; local generation fence and runtime latch prevent inherited authority. |
| Both observers are missed, lease unexpired | **ALLOWED** inside the bounded residual stale window. No within-window mutator bound is claimed. |
| Both observers are missed, lease expired | **DENIED** by monotonic TTL expiry. |
| Generation advances on reload/new/resume/fork | Prior incarnation revoked; replacement starts `UNVERIFIED`. |
| Lifecycle observers and all-tools gate both fail or are removed | T-C total-hook-miss residual; protected-branch controls remain required. |
## T-C server-side branch-protection posture
The required posture is that `main` is push-blocked and PR-only-merge is **MANDATORY**, regardless
of client-gate state. The client-side gate narrows the exposure window only; it is not the T-C
guarantee. The server-side protected-branch configuration is the irreducible guarantee for protected
repository actions. Status-check enforcement and approval enforcement are **RECOMMENDED**.
## Current-vs-required gap (recorded, not enacted)
The current empirical configuration is recorded here without re-probing or mutating live branch
protection. `enable_push=False` (push-block present), so the mandatory push-block/PR-only-merge core
holds. `require_approvals=0` (approvals not enforced), `enable_status_check=False` (status checks not
enforced), and `block_on_official_review=False` (official review not enforced). Those recommended
merge-quality controls are the current gap; changing them is a separate, owner-gated operations
decision and is not enacted by this documentation change.
The permanent T12b/T30 acceptance case prints both required outcomes: dual-hook miss within TTL is **ALLOWED**, and the same lease after TTL is **DENIED**. Separate real-socket tests prove each Claude observer and same-PID generation rollover; Pi lifecycle tests exercise pre/post observers, all four replacement reasons, and local failure closure.
@@ -0,0 +1,35 @@
# Authenticated external lease broker protocol
The compaction-refresh lease broker is a Linux-only, newline-framed JSON protocol over a Unix stream socket. It is runtime-neutral; M1 consumers are limited to Claude and Pi. This is an internal process boundary, not an HTTP API, so it is intentionally absent from OpenAPI.
The broker, never the caller, obtains `(pid, uid, gid)` from kernel `SO_PEERCRED`. It correlates the PID with `/proc/<pid>/stat` field 22 (`starttime`) and mints `session_id` on `register_anchor`. Presence of `session_id` in that request is refused even when its value is `null` or empty. Later requests must originate from the anchor or a descendant. The broker walks parent PIDs to the `(pid,starttime)` anchor and then rereads every walked PID's starttime before accepting the chain.
## Request and response boundary
Each connection carries exactly one UTF-8 JSON object followed by one newline, capped at 64 KiB. The protocol deliberately uses EOF to prove that there is exactly one frame: immediately after writing the newline, the client **MUST half-close its write side** with `shutdown(SHUT_WR)` (or Node `socket.end()`) before awaiting the response. A client that writes a newline but leaves its write side open receives no successful response; the broker's one-second connection deadline fails closed. Malformed, unterminated, multiple (including a delayed second frame), or oversized frames fail closed. Responses are one JSON object and one newline. Success has `{"ok":true,...}`; refusal has `{"ok":false,"code":"TYPED_CODE"}`. Requests are:
- `register_anchor`: `action`, non-negative `runtime_generation`; no `session_id` field.
- `authenticate`: `action`, broker-minted `session_id`, non-negative `runtime_generation`.
- `mint_token`: authenticated identity plus `binding` containing exactly `compaction_epoch`, `request_epoch`, `h_source`, `h_payload`, and `schema_version`.
- `consume_token`: authenticated identity plus `token`.
- `begin_verification`: authenticated identity, runtime (`claude` or `pi`), cycle `binding`, and a TTL no greater than 300 seconds. The broker revokes existing authority first, enters `PENDING_VERIFICATION`, and returns a single-use promotion token.
- `begin_recovery`: the constrained recovery entrypoint. It rejects caller-provided receipt/challenge fields and delegates to the same `begin_verification` transition, but reports `PENDING_DELIVERY` and marks the volatile cycle as recovery-owned.
- `complete_recovery`: authenticated identity only. It rejects caller-provided receipt/challenge fields, obtains the current recovery challenge only from broker state, and delegates to the same trusted-observer → evidence → consume → promote sequence. An observation failure revokes recovery authority; retry starts a fresh challenge.
The daemon owns a second protected production observer socket (mode `0600`) unless a private `--test-observer-file` fixture is selected. That transport accepts only the exact `record_runtime_observation` schema after kernel `SO_PEERCRED` plus the existing anchor/ancestry authentication; it validates the pending runtime/generation before storing one finalized assistant entry for the in-process `RuntimeReceiptObserver`. It is **not** a broker request action. Claude sends its latest assistant entry from the Stop-hook transport; Pi sends only finalized `message_end` assistant content. The public broker socket continues to reject request-supplied `latest_assistant_message` in begin, observe, and complete paths.
- `promote_lease`: authenticated identity plus the exact pending promotion token. The broker commits token consumption before making `VERIFIED` visible.
- `revoke_lease`: authenticated observer signal; deletes pending tokens and makes the session `UNVERIFIED` immediately. WI-3 Claude/Pi hooks send this existing action; `runtime` and bounded `reason` fields are diagnostic input only and never identity authority.
- `authorize_tool`: authenticated identity, runtime, and exact runtime-reported tool name. The broker returns an explicit allow/deny decision from the whole-class policy and current lease.
A higher generation for the same anchor atomically replaces the stored incarnation and deletes all prior tokens and lease authority for that session. A lower generation is stale. Runtime descendants resolve the current generation from an owner-only, locked generation file created by the register-before-exec launcher; reload/new/resume/fork observers advance and `fsync` it before broker revocation. This supports generation replacement even when PID/starttime do not change. Tokens are 256-bit values from the operating-system cryptographic RNG and are single use. At most 256 pending tokens may be persisted; another mint fails with `TOKEN_CAPACITY` before mutation. Successful consumption deletes the token, while a replay still fails with `TOKEN_REPLAY`. Live v1 token records retain the existing `consumed: false` schema.
VERIFIED leases are volatile and monotonic-time bounded: broker restart, generation change, explicit observer revocation, or expiry returns the session to `UNVERIFIED`. `begin_verification` always revokes before minting a new prerequisite. `begin_recovery` reuses that exact transition and mints a new challenge, so a normal-path receipt/challenge cannot be replayed through recovery. `promote_lease` is valid only from the matching pending cycle; persistence failure rolls token and lease state back, while post-rename durability uncertainty terminates the broker. The WI-1 token is the atomic promotion prerequisite substrate.
## Receipt boundary and T-C residual (R1)
Receipt evidence is a T-A delivery/liveness prerequisite only; it cannot replace the mechanical
mutator gate as safety authority. The receipt detects an **ABSENT** or **PREFIX-TRUNCATED** terminal
token. A **MIDDLE-DROP** that preserves the tail is a T-C contract violation that is **NOT receipt-detectable**. It is covered by server-side protected-branch controls, **NOT** by the receipt; no category-wide receipt-detection claim is made for that tail-preserving transformation.
State replacement serializes and enforces the 4 MiB maximum before opening a temporary file, then uses a mode-`0600` temporary file, `fsync`, atomic rename, and parent-directory `fsync`. Every broker mutation snapshots the prior v1 state. A commit failure before rename restores that snapshot and leaves durable state unchanged. A failure after rename makes durability uncertain, so the store is poisoned without rolling memory back and the daemon terminates rather than serving with divergent state. Existing state is opened without following symlinks, must be a bounded regular file at mode `0600`, and is fully schema- and invariant-validated before use. Persisted tokens must be unconsumed, match their session's current generation, and remain within the 256-token cap. Session identity is uniquely keyed by `(anchor_pid,anchor_starttime)`; duplicate logical sessions for one anchor refuse startup. State integrity or mode failures refuse startup. The daemon does not log session IDs or tokens.
@@ -0,0 +1,26 @@
# WI-1 lease broker security notes
- Trusted identity comes only from Linux `SO_PEERCRED` plus `/proc` starttime, never request identity fields.
- Descendant authorization is anchored to `(pid,starttime)` and uses a complete second starttime pass to fail closed on disappearance or PID-reuse races.
- Runtime generations are monotonic per anchor; a bump revokes prior-incarnation tokens before persistence commits. WI-3 stores the live generation in an owner-only locked file so same-PID Pi reload/new/resume/fork and Claude resume/clear transitions cannot inherit a VERIFIED lease.
- Session IDs and cycle tokens use the OS cryptographic RNG. `Math.random` and model output are not token sources.
- Framing and persistence failures fail closed. Sensitive tokens are not logged.
- Built-in `0700`/`0600` filesystem modes provide same-principal hardening only, not socket authenticity against the same UID. WI-1 provides no distinct-principal isolation. That stronger deployment requires an external protected proxy, ACL, or service boundary, and the boundary must preserve authenticated client identity for the broker's `SO_PEERCRED` and ancestry authorization rather than substituting a shared proxy identity.
- WI-2 whole-class authorization denies every consequential, unknown, and custom tool while UNVERIFIED; it does not inspect shell strings or trust wrapper selection. First-class Claude/Pi, both Claudex dispatch modes, PRDY, QA remediation, coord, orchestrator, and fleet starts converge on broker register-before-exec; Claudex additionally installs the mandatory all-tools hook inside its preserved isolated config and fails closed on unsafe settings.
- The permanent `check-runtime-launches.py` suite/CI guard scans production source for direct literal, absolute-path, process-API, command-array, and dynamic Claude/Pi launches. It has no bypass allowlist: an unrecognized launch form fails CI until routed through the common boundary.
- WI-2 promotion consumes a WI-1 cycle token before VERIFIED becomes visible. Observer revocation, runtime-generation replacement, broker restart, and monotonic TTL expiry remove authority.
- WI-3 wires redundant Claude `PreCompact`/`SessionStart(compact)` and Pi `session_before_compact`/post-`session_compact` `context` observers to that same revoke action. If broker confirmation fails after an observer fires, the revoker advances the private generation as a local fence; subsequent authorization revokes the stale broker incarnation before any consequential allow.
- Dual observer absence while a lease remains live is the named **bounded residual stale window**: consequential tools remain allowed until monotonic expiry, with no claimed within-window action bound. After expiry they are denied. Total observer-plus-gate absence remains T-C.
- Receipt observation, payload construction, and constrained recovery implementation remain later surfaces. A receipt can become a promotion prerequisite but is never the safety mechanism.
## Named residual: promote-lease-lost-ACK (WI-3 D2-v5)
A valid `promote_lease` can leave a session `VERIFIED` in the broker while the client never learns of it. This is a named, bounded D2-v5 T-A residual — an **authority-observability divergence, not an authority divergence, not an ALLOW-risk, and not a retry double-apply**. It is disclosed here, not laundered.
**Window — where it can occur.** The broker commits token consumption and durable `VERIFIED` state _before_ the success reply becomes visible (see the promotion order in `lease-broker-protocol.md`). The residual is confined to the interval after that commit+fsync when the broker→client reply or peer-ACK is lost — for example an extreme-contention send failure or peer disconnect after `handle()` has already mutated and persisted state (the #838 fail-closed transport path). The lease mutation is already durable broker-side; only the acknowledgement to the client is lost. No uncommitted or partially-applied state is involved: the commit either happened (and is authoritative) or it did not (and no lease exists).
**Fail-safe direction — the client can only under-claim.** Broker intent is the ceiling; client authority is always ≤ broker intent, never more. Client-side authority-belief is granted only by a _received_ acknowledgement; a lost acknowledgement conveys nothing, so the client cannot conclude "verified" and continues to treat itself as `UNVERIFIED` (it re-verifies or recovers). If the client retries `promote_lease` with the same token, the token is already consumed and the broker rejects the retry (`PROMOTION_TOKEN_MISMATCH` / `INVALID_LEASE_TRANSITION`); there is no double-apply. The committed `VERIFIED` state the broker holds is authority the lease _legitimately earned_ from a real promotion — the broker authorizing consequential tools under it is correct, not inflation. Divergence is therefore strictly toward _less_ client authority than the broker granted; it never produces authority the broker did not grant.
**Bound — TTL plus the observer/gen-bump revoke backstop, self-healing.** The orphaned `VERIFIED` lease is indistinguishable to the broker from any other legitimately verified lease, so the identical D2-v5 revocation backstops dispose of it: any compaction observer (`PreCompact` / `SessionStart(compact)` for Claude; `session_before_compact` / post-`session_compact` `context` for Pi), any same-PID runtime-generation bump (reload/new/resume/fork), broker restart, or monotonic-time expiry returns the session to `UNVERIFIED`. Monotonic TTL expiry (capped at 300 seconds) is **unconditional** — it requires no observer at all — so the maximum exposure of the orphaned lease is one TTL, ≤ 300 s, after which the next consequential tool is denied with `LEASE_EXPIRED`. Any observer that fires shortens the window further. The residual self-heals: "≥1 observer fires OR expiry ⇒ revoke" catches the lost-ACK lease on the same terms as every other stale lease. As with the dual-observer-miss stale window, WI-3 makes no claim that the mutator gate bounds actions inside the residual interval; the interval is bounded by TTL and the revoke backstop, and the server-side branch-protection / required-CI / independent-review line remains the irreducible backstop for protected-repository mutations.
Coordinator security review must rerun the real socket/peercred and mutator-gate acceptance suites on an unrestricted Linux runner and obtain the mandated independent Opus-SECREV review before integration.
+72
View File
@@ -0,0 +1,72 @@
# 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. WI-3 installs the [compaction observer and generation lifecycle](compaction-revocation.md). Dual compaction-hook miss within an unexpired lease remains the ratified bounded T-A residual: consequential tools are allowed until expiry, with no claimed within-window action bound; 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 `exec`s Claude or Pi so PID/starttime remain the authenticated parent anchor. It exports the broker-minted session ID and an owner-only generation-file reference to descendants; lifecycle hooks advance that file for same-PID replacement generations.
- 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 `mosaic``execLeaseGatedRuntime`. 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:
```bash
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.
-352
View File
@@ -1,352 +0,0 @@
# Compaction-Refresh WI-0 Gate0 Evidence Pack
- **Issue:** Gitea #827
- **Milestone:** 188 — Compaction-Refresh Mechanism
- **Branch:** `feat/827-gate0-probe`
- **Starting HEAD:** `d801d6c4c8a984d6a95033c49714210018d3d9a8`
- **Host/runtime:** Linux 6.1.0-48-amd64; Mosaic 0.0.48; Pi 0.80.7; Claude Code 2.1.205
- **Scope:** Probe fixtures and evidence only. No WI-1..WI-7 feature implementation.
## Verdict — 5/6 PASS; BUILD ADMISSION: **NO**
| Probe | Verdict | Short result |
| --- | --- | --- |
| P1 launcher topology + ancestry | **PASS** | Real Mosaic→Pi and Mosaic→Claude chains reached the registered anchor; real Claude `SessionStart` hook ancestry accepted; same-UID sibling with the minted victim ID rejected. |
| P2 Pi last-position + nonce map | **PASS** | Real Pi proved last-or-closed; `message_end` mapped exact `toolCallId → requestNonce` before `tool_call`; provider-response hook occurred before stream consumption/content completion. |
| P3 same-PID generation revocation | **PASS** | Same Pi PID/starttime persisted through reload/fork/new/resume while broker generations increased; reload revoked a prior `VERIFIED` generation. |
| P4 `SO_PEERCRED` + socket posture | **PASS** | Real Unix socket peer PID/UID/starttime matched `/proc`; 0700 directory + 0600 socket demonstrated. Same-UID counterfeit replacement remains explicitly T-C without a distinct principal/authenticated response. |
| P5 source invalidation | **PASS** | Missing, oversize, and hash-mismatched fragments each refused injection/promotion, revoked broker state, and blocked the exact emitted tool call. |
| P6 atomic injection | **T-C GAP** | Both runtimes empirically delivered a complete single block/message, but neither installed runtime contract states an **atomic/prefix-preserving** transport guarantee. Observation is not a guarantee; A-v5-1/T27 cannot be admitted. |
**Planner return item:** P6. The evidence establishes successful complete delivery in these runs, not the required invariant that the harness cannot middle-drop/replace bytes while preserving the terminal token. Per R1, such a middle-drop is not receipt-detectable. It is therefore classed **T-C**, not assumed away.
## STEP 0 — Authority re-verification
Command:
```bash
sha256sum \
~/agent-work/reviews/compaction-refresh-BUILD-BRIEF.md \
~/agent-work/reviews/compaction-refresh-SPEC-v5.md \
~/agent-work/reviews/compaction-refresh-SPEC-RATIFICATION.md
```
Captured result:
```text
89fdbc27ed0e5050dc7b52f3ef2ddaea691edf17fd89d51b15e26fb5ed47171b .../compaction-refresh-BUILD-BRIEF.md
a6d07ade835758e8488ca10d3b0631caf0beb93ea3a6733631f151b0c2f01433 .../compaction-refresh-SPEC-v5.md
bac58319c9c4028b5b40e1129e0033cdb5a6b7b02033c25f06f4cb77d7779c67 .../compaction-refresh-SPEC-RATIFICATION.md
```
All three **MATCH**. They were read in full before probe construction. Raw artifact: [`evidence/raw/STEP0-authority-hashes.txt`](./evidence/raw/STEP0-authority-hashes.txt).
## Evidence method
The scripts under [`probes/`](./probes/) are isolated Gate0 instrumentation, not product implementation. They run the installed `mosaic yolo` launcher and real installed runtime binaries. Broker prototypes use Linux `SO_PEERCRED` and `/proc`; runtime adapters are temporary Claude hooks/Pi extensions. No product source under `packages/mosaic` was changed.
Raw-output artifact integrity is indexed at [`evidence/RAW-SHA256SUMS.txt`](./evidence/RAW-SHA256SUMS.txt).
---
## P1 — Launcher exec/parent topology + supported-hook ancestry (D1)
**Verdict: PASS**
### Commands
```bash
python3 docs/compaction-refresh/probes/p1_run.py --runtime both
rg -n "spawnSync|execRuntime" \
~/.npm-global/lib/node_modules/@mosaicstack/mosaic/dist/commands/launch.js | tail -8
```
Full outputs:
- [`evidence/raw/P1-launch-ancestry.txt`](./evidence/raw/P1-launch-ancestry.txt)
- [`evidence/raw/P1-claude-hook-events.txt`](./evidence/raw/P1-claude-hook-events.txt)
### Real topology observed
The installed Mosaic launcher does **not** replace itself with the runtime despite its `execRuntime` name; installed `launch.js:668` uses `spawnSync`. The Gate0 anchor first registered, then `execvpe` replaced the anchor with the real `mosaic yolo` process (PID/starttime retained). Mosaic remained the stable parent while it spawned the runtime.
Pi run:
```text
anchor before exec: pid=4010843 starttime=365919858 exe=/usr/bin/python3.11
anchor after exec: pid=4010843 starttime=365919858 exe=/usr/bin/node
Pi runtime: pid=4011046 ppid=4010843 starttime=365920219 exe=/usr/bin/node
ps:
4010843 4010840 Fri Jul 17 19:35:18 2026 1001 1001 node
4011046 4010843 Fri Jul 17 19:35:22 2026 1001 1001 pi
```
Claude supported-hook run (latest capture):
```text
hook python pid=4011380 starttime=365920845
-> /bin/sh pid=4011379 starttime=365920845
-> claude pid=4011285 starttime=365920748
-> node/mosaic anchor pid=4011129 starttime=365920380
```
The stream independently recorded the real hook firing:
```json
{"type":"system","subtype":"hook_started","hook_name":"SessionStart:startup","hook_event":"SessionStart"}
{"type":"system","subtype":"hook_response","hook_name":"SessionStart:startup","exit_code":0,"outcome":"success","stdout":"...GATE0_P1_SUPPORTED_HOOK_ANCESTRY_ACCEPTED..."}
```
### Authentication and sibling substitution
The broker minted the logical ID on first peercred contact and keyed the anchor by `(pid,starttime)`. It took the hook/extension PID from `SO_PEERCRED`, walked `/proc/<pid>/stat`, and re-read every starttime before accepting.
Real Pi acceptance excerpt:
```json
{"peercred":{"pid":4011046,"uid":1001,"gid":1001},"decision":"ACCEPT","reason":"ancestry-reaches-registered-anchor","starttimes_rechecked":true}
```
A separately spawned same-UID sibling was given the real minted victim ID. Its ancestry did not reach the anchor:
```json
{"attacker_uid":1001,"victim_session_id_known":true,"broker_decision":"REJECT","broker_reason":"victim-id-known-but-ancestry-mismatch"}
```
The same rejection occurred in both Pi and Claude runs. This is positive runtime evidence for D1/T15a under the supported non-daemonizing topology.
---
## P2 — Pi last-position invariant + nonce map (D5)
**Verdict: PASS**
### Commands
```bash
python3 docs/compaction-refresh/probes/pi_gate0_run.py
python3 docs/compaction-refresh/probes/p2_provider_timing_run.py
```
Full outputs:
- [`evidence/raw/P2-P3-P5-P6-pi.txt`](./evidence/raw/P2-P3-P5-P6-pi.txt)
- [`evidence/raw/P2-provider-timing.txt`](./evidence/raw/P2-provider-timing.txt)
### Last-or-closed evidence
Real Pi argv/load order with the probe last:
```json
{"extensions":[".../mosaic-extension.ts",".../pi_gate0_extension.ts"],"lastPosition":true,"gateState":"UNVERIFIED_READY","pid":4004545}
```
A second real Pi launch deliberately appended a later handler:
```json
{"extensions":[".../mosaic-extension.ts",".../pi_gate0_extension.ts",".../pi_later_extension.ts"],"lastPosition":false,"gateState":"CLOSED_NOT_LAST","pid":4005692}
```
Thus the invariant observed is exactly **last or closed**, not an asserted registration order.
### Exact nonce → tool-call-ID map
In one real GPT-5.6 Sol Pi response, sequence 5 completed the assistant tool-call message and bound its exact ID:
```json
{"seq":5,"event":"message_end","requestNonce":"e5a82358-a6c9-490b-a0de-2e1f1d9b8d79","toolCallIds":["call_bgGE...57c"],"nonceMappings":[{"toolCallId":"call_bgGE...57c","requestNonce":"e5a82358-a6c9-490b-a0de-2e1f1d9b8d79"}]}
```
The following `tool_call` was sequence 6 and carried the same ID/nonce:
```json
{"seq":6,"event":"tool_call","toolCallId":"call_bgGE...57c","mapping":{"nonce":"e5a82358-a6c9-490b-a0de-2e1f1d9b8d79","verified":true},"allowed":true}
```
The harmless tool executed at sequence 7 with that same tool-call ID. No session-global “current epoch” was borrowed.
### `after_provider_response` is not assistant-content observation
A deterministic localhost HTTP provider was used only to force headers/status exposure through the real Pi transport. Actual order:
```json
{"seq":4,"event":"before_provider_request"}
{"seq":5,"event":"after_provider_response","status":200,"assistantContentAvailableAtThisHook":false,"timing":"headers/status before stream consumption"}
{"seq":6,"event":"message_end","role":"assistant","assistantContentObserved":true}
```
```text
headers_hook_precedes_completed_message=True
```
This positively confirms SPEC-v5s precision correction: receipt content is observed at `message_end`; `after_provider_response` is status/headers before stream consumption.
---
## P3 — Same-PID `runtime_generation` bump revokes prior lease (D4)
**Verdict: PASS**
### Command
```bash
python3 docs/compaction-refresh/probes/pi_gate0_run.py
```
Full output: [`evidence/raw/P2-P3-P5-P6-pi.txt`](./evidence/raw/P2-P3-P5-P6-pi.txt).
The real Pi process identity remained:
```text
pid=4004545 starttime_ticks=365907677 uid=1001
```
Broker state around reload:
```json
{"event":"runtime_generation_bump","reason":"startup","old_generation":0,"new_generation":1,"new_lease_state":"UNVERIFIED"}
{"event":"probe_lease_promoted","generation":1,"new_lease_state":"VERIFIED"}
{"event":"runtime_generation_bump","phase":"shutdown","reason":"reload","old_generation":1,"new_generation":2,"prior_lease":"VERIFIED","prior_lease_revoked":true,"new_lease_state":"REVOKED"}
{"event":"runtime_generation_bump","phase":"start","reason":"reload","old_generation":2,"new_generation":3,"new_lease_state":"UNVERIFIED"}
```
The same `(pid,starttime)` then emitted monotonic bumps for real `fork`, `new`, and `resume` replacement flows, reaching generation 12. Pi 0.80.7 emitted an additional conservative `session_start` callback in each of those replacement flows; the broker bumped again rather than reusing authority. This is an availability/idempotence consideration for implementation, not a fail-open result.
---
## P4 — `SO_PEERCRED` + socket authenticity posture
**Verdict: PASS, with the specs named same-UID T-C residual**
### Command
```bash
python3 docs/compaction-refresh/probes/p4_peercred_probe.py
```
Full output: [`evidence/raw/P4-so-peercred.txt`](./evidence/raw/P4-so-peercred.txt).
Captured real socket result:
```text
server_pid=4013762 server_uid=1001 server_gid=1001
directory_mode=0700 socket_mode=0600
SO_PEERCRED pid=4013768 uid=1001 gid=1001
client_claim={"pid":4013768,"starttime_ticks":365927069,"uid":1001,...}
proc_observed={"pid":4013768,"starttime_ticks":365927069,"uid":1001,...}
pid_match=True
uid_match=True
starttime_match=True
client_exit_status=0
```
Achievable unprivileged posture on this host is a user-owned 0700 parent plus 0600 socket. That excludes other UIDs and positively authenticates the connecting kernel PID/UID/GID. It does **not** stop another process running as `hermes` from unlinking/rebinding the socket. A claim stronger than T-C against counterfeit replacement therefore requires the ratified distinct-principal system service or authenticated broker responses. No stronger claim is made.
---
## P5 — Source invalidation fail-closed
**Verdict: PASS**
### Command
```bash
python3 docs/compaction-refresh/probes/pi_gate0_run.py
```
Full output: [`evidence/raw/P2-P3-P5-P6-pi.txt`](./evidence/raw/P2-P3-P5-P6-pi.txt).
Each fault was injected into the manifest/source read by the real Pi `context` hook. Each run reached an actual model-produced `toolCallId`, then the runtime gate refused it:
| Fault | Runtime validation | Injection/promotion | Broker | Tool result |
| --- | --- | --- | --- | --- |
| Missing path | `reason=missing` | `injectionDecision=REFUSED`, `promotion=false` | `source_invalidation_revoke` | `allowed=false`, `unverified-source:missing` |
| 65 bytes with 64-byte max | `reason=oversize` | `REFUSED`, `promotion=false` | revoked | `allowed=false`, `unverified-source:oversize` |
| Bytes differ from pinned SHA-256 | `reason=hash-mismatch` | `REFUSED`, `promotion=false` | revoked | `allowed=false`, `unverified-source:hash-mismatch` |
Missing example:
```json
{"event":"context_return","sourceValidation":{"ok":false,"reason":"missing"},"injectionDecision":"REFUSED","promotion":false,"sourceBroker":{"event":"source_invalidation_revoke","new_lease_state":"REVOKED"}}
{"event":"tool_call","mapping":{"verified":false,"sourceReason":"missing"},"allowed":false,"reason":"unverified-source:missing"}
```
No fault case reached tool execution or promotion.
---
## P6 — Atomic Claude `additionalContext` + Pi `context` injection (A-v5-1 / T27)
**Verdict: T-C GAP — returns to planner**
### Commands
```bash
python3 docs/compaction-refresh/probes/pi_gate0_run.py
python3 docs/compaction-refresh/probes/p6_claude_run.py
rg -n -i "atomic|prefix-preserv" <installed Pi and Claude hook docs>
```
Full outputs:
- [`evidence/raw/P2-P3-P5-P6-pi.txt`](./evidence/raw/P2-P3-P5-P6-pi.txt)
- [`evidence/raw/P6-claude-additional-context.txt`](./evidence/raw/P6-claude-additional-context.txt)
- [`evidence/raw/P6-contract-gap.txt`](./evidence/raw/P6-contract-gap.txt)
### Positive empirical observations
**Pi:** The real `context` hook returned exactly one additional `AgentMessage`; the prior message prefix hash was unchanged. The real final provider payload contained exactly one occurrence in one content item, and the real model copied all bytes exactly:
```json
{"event":"context_return","inputCount":1,"outputCount":2,"injectionDecision":"ONE_ATOMIC_AGENT_MESSAGE","prefixPreservedByReturn":true,"blockLength":108,"blockSha256":"99c3...a0dd"}
{"event":"before_provider_request","markerOccurrences":1,"markerPaths":["$.input[1].content[0].text"],"finalPayloadValid":true}
{"event":"message_end","exactContextBlockCopied":true,"assistantTextSha256":"99c3...a0dd"}
```
**Claude:** The real `SessionStart` hook emitted one `hookSpecificOutput.additionalContext` string. Claudes stream recorded successful hook execution, and the real models exact copied block matched byte length and SHA-256:
```text
block_length=116
block_sha256=ef6377d63552af075f4f4adec00165988418c5f46a992f4dce8e678b56fd34ac
assistant_copy_length=116
assistant_copy_sha256=ef6377d63552af075f4f4adec00165988418c5f46a992f4dce8e678b56fd34ac
assistant_copy_exact=True
```
### Why this is not a PASS
The installed Pi documentation says only that `context` receives a deep copy and may return `{ messages }`. The installed Claude documentation says only that `additionalContext` enters/adds to context/system prompt. The exact search result was:
```text
NO MATCH: neither installed runtime document states an atomic/prefix-preserving transport guarantee.
```
One or several successful complete deliveries cannot prove the transport invariant needed by A-v5-1. In particular, a harness-side middle deletion/replacement that preserves the terminal receipt is not detectable by the receipt. That is precisely R1s assurance boundary. Therefore:
- absent or prefix-truncated terminal token: receipt-detectable;
- middle-drop preserving the tail token: **not receipt-detectable**;
- no documented runtime contract excludes that transform;
- classification: **T-C contract gap**.
No atomicity claim is inferred from empirical success.
---
## Independent probe review
After an initial review identified a session-global P2 correlation flaw, the probe was changed to queue request-scoped cycles from `before_provider_request` through assistant `message_end`; all runtime probes were re-run and raw checksums regenerated. The final independent review command was:
```bash
~/.config/mosaic/tools/codex/codex-code-review.sh \
-b d801d6c4c8a984d6a95033c49714210018d3d9a8 \
-o /tmp/827-gate0-rereview.json
```
Final review: **APPROVE**, confidence 0.91, 18 files reviewed, 0 blockers, 0 should-fix findings, 0 suggestions.
## Final admission decision
Gate0 requires every item to produce positive runtime evidence. P6 does not. **Do not admit WI-1..WI-7. Return A-v5-1/T27 to planner review.**
No feature work, push, PR, merge, or issue closure was performed.
@@ -1,8 +0,0 @@
d19ed51612b52d8f5f4957321776e05157008d048b693217c03d71318dc4c763 docs/compaction-refresh/evidence/raw/P1-claude-hook-events.txt
c2d7bc21200063a4a0e61c67ba91abaf958ee88aa686e86f3f71c2717732b413 docs/compaction-refresh/evidence/raw/P1-launch-ancestry.txt
6efb12d908e9e20badcfda5b070aa1873409bd5a05f533f0b08bb1b4ef53d1a7 docs/compaction-refresh/evidence/raw/P2-P3-P5-P6-pi.txt
a9df6cc9f5d45f60d7d914ad1f80b9601574b82831101b3a10eccf1b93787e94 docs/compaction-refresh/evidence/raw/P2-provider-timing.txt
92e7aa7d69d53e58a151f9d56cfb583d90c206ecc0bc8a1b185e172c598fb177 docs/compaction-refresh/evidence/raw/P4-so-peercred.txt
047d235c6b6553158e27378c4ace081b094e5746734f4b5db6a6dc8ef9e05ff2 docs/compaction-refresh/evidence/raw/P6-claude-additional-context.txt
7df20b2878fc87aa4d1fc89121e494d8f4f7bf313b89e1e3147f16fdaa567cdd docs/compaction-refresh/evidence/raw/P6-contract-gap.txt
405bf3a06bf355d7f4f4d7b29d45a1ae70d93a690af5f7d0fc4819249e9f408f docs/compaction-refresh/evidence/raw/STEP0-authority-hashes.txt
@@ -1,28 +0,0 @@
$ python3 docs/compaction-refresh/probes/p1_run.py --runtime claude
=== P1 CLAUDE REAL LAUNCH ===
$ python3 docs/compaction-refresh/probes/p1_anchor_exec.py --socket <protected-socket> claude <runtime args>
registered_anchor={"argc": 17, "argv0": "/usr/bin/python3", "comm": "python3", "exe": "/usr/bin/python3.11", "pid": 3933580, "ppid": 3933578, "starttime_ticks": 365788075}
broker_minted_session_id=ebe9f9146ad1ba5b9fd757fe9517d24b
hook_or_extension_record={"ancestry": [{"argc": 2, "argv0": "python3", "comm": "python3", "exe": "/usr/bin/python3.11", "pid": 3933983, "ppid": 3933982, "starttime_ticks": 365788568}, {"argc": 3, "argv0": "/bin/sh", "comm": "sh", "exe": "/usr/bin/dash", "pid": 3933982, "ppid": 3933751, "starttime_ticks": 365788568}, {"argc": 16, "argv0": "claude", "comm": "claude", "exe": "/home/hermes/.local/share/claude/versions/2.1.205", "pid": 3933751, "ppid": 3933580, "starttime_ticks": 365788474}, {"argc": 16, "argv0": "node", "comm": "node", "exe": "/usr/bin/node", "pid": 3933580, "ppid": 3933578, "starttime_ticks": 365788075}], "anchor": {"argc": 17, "argv0": "/usr/bin/python3", "comm": "python3", "exe": "/usr/bin/python3.11", "pid": 3933580, "ppid": 3933578, "starttime_ticks": 365788075}, "claimed_session_id": null, "decision": "ACCEPT", "event": "resolve-hook", "peercred": {"gid": 1001, "pid": 3933983, "uid": 1001}, "reason": "ancestry-reaches-registered-anchor", "resolved_session_id": "ebe9f9146ad1ba5b9fd757fe9517d24b", "starttimes_rechecked": true}
sibling_attack_record={"ancestry": [{"argc": 6, "argv0": "/usr/bin/python3", "comm": "python3", "exe": "/usr/bin/python3.11", "pid": 3933581, "ppid": 3933578, "starttime_ticks": 365788080}, {"argc": 4, "argv0": "python3", "comm": "python3", "exe": "/usr/bin/python3.11", "pid": 3933578, "ppid": 3933576, "starttime_ticks": 365788059}, {"argc": 3, "argv0": "/bin/bash", "comm": "bash", "exe": "/usr/bin/bash", "pid": 3933576, "ppid": 3933575, "starttime_ticks": 365788058}, {"argc": 3, "argv0": "/bin/bash", "comm": "bash", "exe": "/usr/bin/bash", "pid": 3933575, "ppid": 3888118, "starttime_ticks": 365788058}, {"argc": 1, "argv0": "pi", "comm": "pi", "exe": "/usr/bin/node", "pid": 3888118, "ppid": 3887912, "starttime_ticks": 365707392}, {"argc": 6, "argv0": "node", "comm": "node", "exe": "/usr/bin/node", "pid": 3887912, "ppid": 3887869, "starttime_ticks": 365707050}, {"argc": 1, "argv0": "-bash", "comm": "bash", "exe": "/usr/bin/bash", "pid": 3887869, "ppid": 1244054, "starttime_ticks": 365706948}, {"argc": 10, "argv0": "tmux", "comm": "tmux: server", "exe": "/usr/bin/tmux", "pid": 1244054, "ppid": 745, "starttime_ticks": 114078803}, {"argc": 2, "argv0": "/lib/systemd/systemd", "comm": "systemd", "exe": "/usr/lib/systemd/systemd", "pid": 745, "ppid": 1, "starttime_ticks": 627}], "anchor": {"argc": 17, "argv0": "/usr/bin/python3", "comm": "python3", "exe": "/usr/bin/python3.11", "pid": 3933580, "ppid": 3933578, "starttime_ticks": 365788075}, "claimed_session_id": "ebe9f9146ad1ba5b9fd757fe9517d24b", "decision": "REJECT", "event": "claim-session", "peercred": {"gid": 1001, "pid": 3933581, "uid": 1001}, "reason": "victim-id-known-but-ancestry-mismatch", "resolved_session_id": null, "starttimes_rechecked": false}
sibling_process_stdout={"attacker_pid": 3933581, "attacker_uid": 1001, "broker_decision": "REJECT", "broker_reason": "victim-id-known-but-ancestry-mismatch", "victim_session_id_known": true}
sibling_process_exit=0
ps_snapshot=<hook chain exited; broker /proc snapshot above is authoritative>
launcher_stderr_excerpt:
{"argv": ["mosaic", "yolo", "claude", "<12 runtime args>"], "event": "anchor-exec", "note": "os.execvpe retains pid and /proc starttime", "pid": 3933580}
runtime_stdout_excerpt:
[mosaic] Claude Code settings audit:
⚠ Missing PreToolUse hook: prevent-memory-write.sh
⚠ Missing PostToolUse hook: qa-hook-stdin.sh
⚠ Missing PostToolUse hook: typecheck-hook.sh
⚠ Missing plugin: feature-dev
⚠ Missing plugin: pr-review-toolkit
⚠ Missing plugin: code-review
runtime_hook_event_excerpt:
⚠ Missing PreToolUse hook: prevent-memory-write.sh
⚠ Missing PostToolUse hook: qa-hook-stdin.sh
⚠ Missing PostToolUse hook: typecheck-hook.sh
{"type":"system","subtype":"hook_started","hook_id":"cadd5ded-a869-4b05-85fc-cfd1a4988217","hook_name":"SessionStart:startup","hook_event":"SessionStart","uuid":"b63d67bf-2247-4e1c-b16b-7ccffa73180b","session_id":"97e1224c-7c1c-42c7-9fb5-598d2cd3dfaf"}
{"type":"system","subtype":"hook_response","hook_id":"cadd5ded-a869-4b05-85fc-cfd1a4988217","hook_name":"SessionStart:startup","hook_event":"SessionStart","output":"{\"hookSpecificOutput\": {\"hookEventName\": \"SessionStart\", \"additionalContext\": \"GATE0_P1_SUPPORTED_HOOK_ANCESTRY_ACCEPTED\"}}\n","stdout":"{\"hookSpecificOutput\": {\"hookEventName\": \"SessionStart\", \"additionalContext\": \"GATE0_P1_SUPPORTED_HOOK_ANCESTRY_ACCEPTED\"}}\n","stderr":"","exit_code":0,"outcome":"success","uuid":"b2bb583d-d287-4b56-8061-09153a32adc2","session_id":"97e1224c-7c1c-42c7-9fb5-598d2cd3dfaf"}
@@ -1,62 +0,0 @@
$ python3 docs/compaction-refresh/probes/p1_run.py --runtime both
=== P1 PI REAL LAUNCH ===
machine_assertions=PASS
$ python3 docs/compaction-refresh/probes/p1_anchor_exec.py --socket <protected-socket> pi <runtime args>
registered_anchor={"argc": 13, "argv0": "/usr/bin/python3", "comm": "python3", "exe": "/usr/bin/python3.11", "pid": 4010843, "ppid": 4010840, "starttime_ticks": 365919858}
broker_minted_session_id=5207bd0d8251b616fe4df4c68f438830
hook_or_extension_record={"ancestry": [{"argc": 1, "argv0": "pi", "comm": "pi", "exe": "/usr/bin/node", "pid": 4011046, "ppid": 4010843, "starttime_ticks": 365920219}, {"argc": 12, "argv0": "node", "comm": "node", "exe": "/usr/bin/node", "pid": 4010843, "ppid": 4010840, "starttime_ticks": 365919858}], "anchor": {"argc": 13, "argv0": "/usr/bin/python3", "comm": "python3", "exe": "/usr/bin/python3.11", "pid": 4010843, "ppid": 4010840, "starttime_ticks": 365919858}, "claimed_session_id": null, "decision": "ACCEPT", "event": "resolve-hook", "peercred": {"gid": 1001, "pid": 4011046, "uid": 1001}, "reason": "ancestry-reaches-registered-anchor", "resolved_session_id": "5207bd0d8251b616fe4df4c68f438830", "starttimes_rechecked": true}
sibling_attack_record={"ancestry": [{"argc": 6, "argv0": "/usr/bin/python3", "comm": "python3", "exe": "/usr/bin/python3.11", "pid": 4010844, "ppid": 4010840, "starttime_ticks": 365919864}, {"argc": 4, "argv0": "python3", "comm": "python3", "exe": "/usr/bin/python3.11", "pid": 4010840, "ppid": 4010838, "starttime_ticks": 365919843}, {"argc": 3, "argv0": "/bin/bash", "comm": "bash", "exe": "/usr/bin/bash", "pid": 4010838, "ppid": 4010837, "starttime_ticks": 365919843}, {"argc": 3, "argv0": "/bin/bash", "comm": "bash", "exe": "/usr/bin/bash", "pid": 4010837, "ppid": 3888118, "starttime_ticks": 365919842}, {"argc": 1, "argv0": "pi", "comm": "pi", "exe": "/usr/bin/node", "pid": 3888118, "ppid": 3887912, "starttime_ticks": 365707392}, {"argc": 6, "argv0": "node", "comm": "node", "exe": "/usr/bin/node", "pid": 3887912, "ppid": 3887869, "starttime_ticks": 365707050}, {"argc": 1, "argv0": "-bash", "comm": "bash", "exe": "/usr/bin/bash", "pid": 3887869, "ppid": 1244054, "starttime_ticks": 365706948}, {"argc": 10, "argv0": "tmux", "comm": "tmux: server", "exe": "/usr/bin/tmux", "pid": 1244054, "ppid": 745, "starttime_ticks": 114078803}, {"argc": 2, "argv0": "/lib/systemd/systemd", "comm": "systemd", "exe": "/usr/lib/systemd/systemd", "pid": 745, "ppid": 1, "starttime_ticks": 627}], "anchor": {"argc": 13, "argv0": "/usr/bin/python3", "comm": "python3", "exe": "/usr/bin/python3.11", "pid": 4010843, "ppid": 4010840, "starttime_ticks": 365919858}, "claimed_session_id": "5207bd0d8251b616fe4df4c68f438830", "decision": "REJECT", "event": "claim-session", "peercred": {"gid": 1001, "pid": 4010844, "uid": 1001}, "reason": "victim-id-known-but-ancestry-mismatch", "resolved_session_id": null, "starttimes_rechecked": false}
sibling_process_stdout={"attacker_pid": 4010844, "attacker_uid": 1001, "broker_decision": "REJECT", "broker_reason": "victim-id-known-but-ancestry-mismatch", "victim_session_id_known": true}
sibling_process_exit=0
$ ps -o pid=,ppid=,lstart=,uid=,gid=,comm= -p 4011046,4010843
4010843 4010840 Fri Jul 17 19:35:18 2026 1001 1001 node
4011046 4010843 Fri Jul 17 19:35:22 2026 1001 1001 pi
launcher_stderr_excerpt:
{"argv": ["mosaic", "yolo", "pi", "<8 runtime args>"], "event": "anchor-exec", "note": "os.execvpe retains pid and /proc starttime", "pid": 4010843}
runtime_stdout_excerpt:
[mosaic] Launching Pi in YOLO mode...
{"type":"extension_ui_request","id":"4cf086c7-299e-4734-9264-6ad2964f3664","method":"notify","message":"Mosaic framework loaded","notifyType":"info"}
{"id":"state","type":"response","command":"get_state","success":true,"data":{"model":{"id":"gpt-5.6-sol","name":"GPT-5.6 Sol","api":"openai-codex-responses","provider":"openai-codex","baseUrl":"https://chatgpt.com/backend-api","compat":{"supportsToolSearch":true},"reasoning":true,"thinkingLevelMap":{"xhigh":"xhigh","max":"max","minimal":"low"},"input":["text","image"],"cost":{"input":5,"output":30,"cacheRead":0.5,"cacheWrite":6.25,"tiers":[{"inputTokensAbove":272000,"input":10,"output":45,"cache
runtime_hook_event_excerpt:
=== P1 CLAUDE REAL LAUNCH ===
machine_assertions=PASS
$ python3 docs/compaction-refresh/probes/p1_anchor_exec.py --socket <protected-socket> claude <runtime args>
registered_anchor={"argc": 17, "argv0": "/usr/bin/python3", "comm": "python3", "exe": "/usr/bin/python3.11", "pid": 4011129, "ppid": 4010840, "starttime_ticks": 365920380}
broker_minted_session_id=f382fa5f4b2142ef79bb76204521ff2a
hook_or_extension_record={"ancestry": [{"argc": 2, "argv0": "python3", "comm": "python3", "exe": "/usr/bin/python3.11", "pid": 4011380, "ppid": 4011379, "starttime_ticks": 365920845}, {"argc": 3, "argv0": "/bin/sh", "comm": "sh", "exe": "/usr/bin/dash", "pid": 4011379, "ppid": 4011285, "starttime_ticks": 365920845}, {"argc": 16, "argv0": "claude", "comm": "claude", "exe": "/home/hermes/.local/share/claude/versions/2.1.205", "pid": 4011285, "ppid": 4011129, "starttime_ticks": 365920748}, {"argc": 16, "argv0": "node", "comm": "node", "exe": "/usr/bin/node", "pid": 4011129, "ppid": 4010840, "starttime_ticks": 365920380}], "anchor": {"argc": 17, "argv0": "/usr/bin/python3", "comm": "python3", "exe": "/usr/bin/python3.11", "pid": 4011129, "ppid": 4010840, "starttime_ticks": 365920380}, "claimed_session_id": null, "decision": "ACCEPT", "event": "resolve-hook", "peercred": {"gid": 1001, "pid": 4011380, "uid": 1001}, "reason": "ancestry-reaches-registered-anchor", "resolved_session_id": "f382fa5f4b2142ef79bb76204521ff2a", "starttimes_rechecked": true}
sibling_attack_record={"ancestry": [{"argc": 6, "argv0": "/usr/bin/python3", "comm": "python3", "exe": "/usr/bin/python3.11", "pid": 4011130, "ppid": 4010840, "starttime_ticks": 365920385}, {"argc": 4, "argv0": "python3", "comm": "python3", "exe": "/usr/bin/python3.11", "pid": 4010840, "ppid": 4010838, "starttime_ticks": 365919843}, {"argc": 3, "argv0": "/bin/bash", "comm": "bash", "exe": "/usr/bin/bash", "pid": 4010838, "ppid": 4010837, "starttime_ticks": 365919843}, {"argc": 3, "argv0": "/bin/bash", "comm": "bash", "exe": "/usr/bin/bash", "pid": 4010837, "ppid": 3888118, "starttime_ticks": 365919842}, {"argc": 1, "argv0": "pi", "comm": "pi", "exe": "/usr/bin/node", "pid": 3888118, "ppid": 3887912, "starttime_ticks": 365707392}, {"argc": 6, "argv0": "node", "comm": "node", "exe": "/usr/bin/node", "pid": 3887912, "ppid": 3887869, "starttime_ticks": 365707050}, {"argc": 1, "argv0": "-bash", "comm": "bash", "exe": "/usr/bin/bash", "pid": 3887869, "ppid": 1244054, "starttime_ticks": 365706948}, {"argc": 10, "argv0": "tmux", "comm": "tmux: server", "exe": "/usr/bin/tmux", "pid": 1244054, "ppid": 745, "starttime_ticks": 114078803}, {"argc": 2, "argv0": "/lib/systemd/systemd", "comm": "systemd", "exe": "/usr/lib/systemd/systemd", "pid": 745, "ppid": 1, "starttime_ticks": 627}], "anchor": {"argc": 17, "argv0": "/usr/bin/python3", "comm": "python3", "exe": "/usr/bin/python3.11", "pid": 4011129, "ppid": 4010840, "starttime_ticks": 365920380}, "claimed_session_id": "f382fa5f4b2142ef79bb76204521ff2a", "decision": "REJECT", "event": "claim-session", "peercred": {"gid": 1001, "pid": 4011130, "uid": 1001}, "reason": "victim-id-known-but-ancestry-mismatch", "resolved_session_id": null, "starttimes_rechecked": false}
sibling_process_stdout={"attacker_pid": 4011130, "attacker_uid": 1001, "broker_decision": "REJECT", "broker_reason": "victim-id-known-but-ancestry-mismatch", "victim_session_id_known": true}
sibling_process_exit=0
ps_snapshot=<hook chain exited; broker /proc snapshot above is authoritative>
launcher_stderr_excerpt:
{"argv": ["mosaic", "yolo", "claude", "<12 runtime args>"], "event": "anchor-exec", "note": "os.execvpe retains pid and /proc starttime", "pid": 4011129}
runtime_stdout_excerpt:
[mosaic] Claude Code settings audit:
⚠ Missing PreToolUse hook: prevent-memory-write.sh
⚠ Missing PostToolUse hook: qa-hook-stdin.sh
⚠ Missing PostToolUse hook: typecheck-hook.sh
⚠ Missing plugin: feature-dev
⚠ Missing plugin: pr-review-toolkit
⚠ Missing plugin: code-review
runtime_hook_event_excerpt:
⚠ Missing PreToolUse hook: prevent-memory-write.sh
⚠ Missing PostToolUse hook: qa-hook-stdin.sh
⚠ Missing PostToolUse hook: typecheck-hook.sh
{"type":"system","subtype":"hook_started","hook_id":"2a5f7dab-a064-4610-a6b1-4ad151ddcdd9","hook_name":"SessionStart:startup","hook_event":"SessionStart","uuid":"c6e0690c-f0c9-4d60-a8fb-5f0c25ea3208","session_id":"167d104d-907a-4120-9b07-bdf4762818a9"}
{"type":"system","subtype":"hook_response","hook_id":"2a5f7dab-a064-4610-a6b1-4ad151ddcdd9","hook_name":"SessionStart:startup","hook_event":"SessionStart","output":"{\"hookSpecificOutput\": {\"hookEventName\": \"SessionStart\", \"additionalContext\": \"GATE0_P1_SUPPORTED_HOOK_ANCESTRY_ACCEPTED\"}}\n","stdout":"{\"hookSpecificOutput\": {\"hookEventName\": \"SessionStart\", \"additionalContext\": \"GATE0_P1_SUPPORTED_HOOK_ANCESTRY_ACCEPTED\"}}\n","stderr":"","exit_code":0,"outcome":"success","uuid":"167b2a5b-a45a-4e70-a72b-1f4a609bb979","session_id":"167d104d-907a-4120-9b07-bdf4762818a9"}
$ readlink -f "$(command -v mosaic)"
/home/hermes/.npm-global/lib/node_modules/@mosaicstack/mosaic/dist/cli.js
$ rg -n "spawnSync|execRuntime" ~/.npm-global/lib/node_modules/@mosaicstack/mosaic/dist/commands/launch.js | tail -8
63: spawnSync(initBin, [], { stdio: 'inherit' });
131: const result = spawnSync(checker, ['--check', '--runtime', runtime], { stdio: 'ignore' });
624: execRuntime('claude', cliArgs);
637: execRuntime('codex', cliArgs);
643: execRuntime('opencode', args);
658: execRuntime('pi', cliArgs);
665:function execRuntime(cmd, args) {
668: const result = spawnSync(cmd, args, {
@@ -1,52 +0,0 @@
$ python3 docs/compaction-refresh/probes/pi_gate0_run.py
machine_assertions=PASS
runtime_versions:
0.80.7
0.0.48
P2_EVENT_ORDER_AND_NONCE_MAP:
{"assistantContentObserved": true, "assistantTextSha256": "a36f1eb364f062cad2f9f7d7e2b62ef7715d2aef79caafcfccd3a227cecf3e61", "event": "message_end", "exactContextBlockCopied": false, "inFlightDepthAfter": 0, "nonceMappings": [{"requestNonce": "e5a82358-a6c9-490b-a0de-2e1f1d9b8d79", "toolCallId": "call_bgGEFnBJOmwJPEfmzMo1eHOy|fc_0fb3d12b5404a73c016a5ac9d6f9a4819b9ddf70296c0cf57c"}], "pid": 4004545, "requestNonce": "e5a82358-a6c9-490b-a0de-2e1f1d9b8d79", "role": "assistant", "seq": 5, "starttime_ticks": 365907677, "toolCallIds": ["call_bgGEFnBJOmwJPEfmzMo1eHOy|fc_0fb3d12b5404a73c016a5ac9d6f9a4819b9ddf70296c0cf57c"]}
{"allowed": true, "event": "tool_call", "mapping": {"nonce": "e5a82358-a6c9-490b-a0de-2e1f1d9b8d79", "sourceReason": "all-fragments-valid", "verified": true}, "pid": 4004545, "reason": "exact-tool-call-id-mapped-to-verified-request-nonce", "seq": 6, "starttime_ticks": 365907677, "toolCallId": "call_bgGEFnBJOmwJPEfmzMo1eHOy|fc_0fb3d12b5404a73c016a5ac9d6f9a4819b9ddf70296c0cf57c", "toolName": "gate0_nonce_probe"}
{"broker": {"event": "probe_lease_promoted", "generation": 1, "new_lease_state": "VERIFIED", "peercred": {"gid": 1001, "pid": 4004545, "uid": 1001}, "starttime_ticks": 365907677}, "event": "tool_execute", "label": "p2", "pid": 4004545, "seq": 7, "starttime_ticks": 365907677, "toolCallId": "call_bgGEFnBJOmwJPEfmzMo1eHOy|fc_0fb3d12b5404a73c016a5ac9d6f9a4819b9ddf70296c0cf57c"}
{"assistantContentObserved": true, "assistantTextSha256": "99c3dce194b16405dfb555f126ee5ccc014fdc184d0882aee1a903cbc700a0dd", "event": "message_end", "exactContextBlockCopied": true, "inFlightDepthAfter": 0, "nonceMappings": [], "pid": 4004545, "requestNonce": "cca4b1e3-296a-4e4c-9805-a395c270c01f", "role": "assistant", "seq": 11, "starttime_ticks": 365907677, "toolCallIds": []}
P2_LAST_OR_CLOSED:
{"broker": {"event": "runtime_generation_bump", "new_generation": 1, "new_lease_state": "UNVERIFIED", "old_generation": 0, "peercred": {"gid": 1001, "pid": 4004545, "uid": 1001}, "phase": "start", "prior_lease": "NONE", "prior_lease_revoked": true, "reason": "startup", "starttime_ticks": 365907677}, "event": "session_start", "extensions": ["/home/hermes/.config/mosaic/runtime/pi/mosaic-extension.ts", "/home/hermes/agent-work/stack-cr-wi0-gate0/docs/compaction-refresh/probes/pi_gate0_extension.ts"], "gateState": "UNVERIFIED_READY", "lastPosition": true, "pid": 4004545, "reason": "startup", "self": "/home/hermes/agent-work/stack-cr-wi0-gate0/docs/compaction-refresh/probes/pi_gate0_extension.ts", "seq": 1, "starttime_ticks": 365907677}
{"broker": {"skipped": true}, "event": "session_start", "extensions": ["/home/hermes/.config/mosaic/runtime/pi/mosaic-extension.ts", "/home/hermes/agent-work/stack-cr-wi0-gate0/docs/compaction-refresh/probes/pi_gate0_extension.ts", "/home/hermes/agent-work/stack-cr-wi0-gate0/docs/compaction-refresh/probes/pi_later_extension.ts"], "gateState": "CLOSED_NOT_LAST", "lastPosition": false, "pid": 4005692, "reason": "startup", "self": "/home/hermes/agent-work/stack-cr-wi0-gate0/docs/compaction-refresh/probes/pi_gate0_extension.ts", "seq": 1, "starttime_ticks": 365910545}
P3_GENERATION_BROKER:
{"event": "runtime_generation_bump", "new_generation": 1, "new_lease_state": "UNVERIFIED", "old_generation": 0, "peercred": {"gid": 1001, "pid": 4004545, "uid": 1001}, "phase": "start", "prior_lease": "NONE", "prior_lease_revoked": true, "reason": "startup", "starttime_ticks": 365907677}
{"event": "probe_lease_promoted", "generation": 1, "new_lease_state": "VERIFIED", "peercred": {"gid": 1001, "pid": 4004545, "uid": 1001}, "starttime_ticks": 365907677}
{"event": "runtime_generation_bump", "new_generation": 2, "new_lease_state": "REVOKED", "old_generation": 1, "peercred": {"gid": 1001, "pid": 4004545, "uid": 1001}, "phase": "shutdown", "prior_lease": "VERIFIED", "prior_lease_revoked": true, "reason": "reload", "starttime_ticks": 365907677}
{"event": "runtime_generation_bump", "new_generation": 3, "new_lease_state": "UNVERIFIED", "old_generation": 2, "peercred": {"gid": 1001, "pid": 4004545, "uid": 1001}, "phase": "start", "prior_lease": "REVOKED", "prior_lease_revoked": true, "reason": "reload", "starttime_ticks": 365907677}
{"event": "runtime_generation_bump", "new_generation": 4, "new_lease_state": "REVOKED", "old_generation": 3, "peercred": {"gid": 1001, "pid": 4004545, "uid": 1001}, "phase": "shutdown", "prior_lease": "UNVERIFIED", "prior_lease_revoked": true, "reason": "fork", "starttime_ticks": 365907677}
{"event": "runtime_generation_bump", "new_generation": 5, "new_lease_state": "UNVERIFIED", "old_generation": 4, "peercred": {"gid": 1001, "pid": 4004545, "uid": 1001}, "phase": "start", "prior_lease": "REVOKED", "prior_lease_revoked": true, "reason": "fork", "starttime_ticks": 365907677}
{"event": "runtime_generation_bump", "new_generation": 6, "new_lease_state": "UNVERIFIED", "old_generation": 5, "peercred": {"gid": 1001, "pid": 4004545, "uid": 1001}, "phase": "start", "prior_lease": "UNVERIFIED", "prior_lease_revoked": true, "reason": "fork", "starttime_ticks": 365907677}
{"event": "runtime_generation_bump", "new_generation": 7, "new_lease_state": "REVOKED", "old_generation": 6, "peercred": {"gid": 1001, "pid": 4004545, "uid": 1001}, "phase": "shutdown", "prior_lease": "UNVERIFIED", "prior_lease_revoked": true, "reason": "new", "starttime_ticks": 365907677}
{"event": "runtime_generation_bump", "new_generation": 8, "new_lease_state": "UNVERIFIED", "old_generation": 7, "peercred": {"gid": 1001, "pid": 4004545, "uid": 1001}, "phase": "start", "prior_lease": "REVOKED", "prior_lease_revoked": true, "reason": "new", "starttime_ticks": 365907677}
{"event": "runtime_generation_bump", "new_generation": 9, "new_lease_state": "UNVERIFIED", "old_generation": 8, "peercred": {"gid": 1001, "pid": 4004545, "uid": 1001}, "phase": "start", "prior_lease": "UNVERIFIED", "prior_lease_revoked": true, "reason": "new", "starttime_ticks": 365907677}
{"event": "runtime_generation_bump", "new_generation": 10, "new_lease_state": "REVOKED", "old_generation": 9, "peercred": {"gid": 1001, "pid": 4004545, "uid": 1001}, "phase": "shutdown", "prior_lease": "UNVERIFIED", "prior_lease_revoked": true, "reason": "resume", "starttime_ticks": 365907677}
{"event": "runtime_generation_bump", "new_generation": 11, "new_lease_state": "UNVERIFIED", "old_generation": 10, "peercred": {"gid": 1001, "pid": 4004545, "uid": 1001}, "phase": "start", "prior_lease": "REVOKED", "prior_lease_revoked": true, "reason": "resume", "starttime_ticks": 365907677}
{"event": "runtime_generation_bump", "new_generation": 12, "new_lease_state": "UNVERIFIED", "old_generation": 11, "peercred": {"gid": 1001, "pid": 4004545, "uid": 1001}, "phase": "start", "prior_lease": "UNVERIFIED", "prior_lease_revoked": true, "reason": "resume", "starttime_ticks": 365907677}
P5_SOURCE_INVALIDATION:
{"blockLength": 108, "blockSha256": "99c3dce194b16405dfb555f126ee5ccc014fdc184d0882aee1a903cbc700a0dd", "event": "context_return", "injectionDecision": "REFUSED", "inputCount": 5, "lastPosition": true, "outputCount": 5, "pid": 4004545, "prefixHashAfter": "4f339e3e45989486374b75d8a40abad22a3f3091f1099e3b4112f3afd1c60eb0", "prefixHashBefore": "4f339e3e45989486374b75d8a40abad22a3f3091f1099e3b4112f3afd1c60eb0", "prefixPreservedByReturn": true, "promotion": false, "requestNonce": "24ef5352-bcc6-4418-b65f-c2763453cc46", "seq": 12, "sourceBroker": {"event": "source_invalidation_revoke", "generation": 12, "new_lease_state": "REVOKED", "peercred": {"gid": 1001, "pid": 4004545, "uid": 1001}, "prior_lease": "UNVERIFIED", "promotion": false, "source_reason": "missing", "starttime_ticks": 365907677}, "sourceValidation": {"fragment": "/tmp/gate0-pi-g_3gsk34/absent-fragment.md", "ok": false, "reason": "missing"}, "starttime_ticks": 365907677}
{"allowed": false, "event": "tool_call", "mapping": {"nonce": "24ef5352-bcc6-4418-b65f-c2763453cc46", "sourceReason": "missing", "verified": false}, "pid": 4004545, "reason": "unverified-source:missing", "seq": 15, "starttime_ticks": 365907677, "toolCallId": "call_XBwBkiv5tZx2ayuDxHKD5vSB|fc_0fb3d12b5404a73c016a5ac9dc341c819bb6ed3e00ca2cf1a5", "toolName": "gate0_nonce_probe"}
{"blockLength": 108, "blockSha256": "99c3dce194b16405dfb555f126ee5ccc014fdc184d0882aee1a903cbc700a0dd", "event": "context_return", "injectionDecision": "REFUSED", "inputCount": 9, "lastPosition": true, "outputCount": 9, "pid": 4004545, "prefixHashAfter": "1a39911018caefe8f5b5acb652cece9f92d937e7384109f5c1559266349480b7", "prefixHashBefore": "1a39911018caefe8f5b5acb652cece9f92d937e7384109f5c1559266349480b7", "prefixPreservedByReturn": true, "promotion": false, "requestNonce": "319646e8-59e2-4021-9b27-de1376b13c32", "seq": 22, "sourceBroker": {"event": "source_invalidation_revoke", "generation": 12, "new_lease_state": "REVOKED", "peercred": {"gid": 1001, "pid": 4004545, "uid": 1001}, "prior_lease": "REVOKED", "promotion": false, "source_reason": "oversize", "starttime_ticks": 365907677}, "sourceValidation": {"fragment": "/tmp/gate0-pi-g_3gsk34/oversize.md", "ok": false, "reason": "oversize"}, "starttime_ticks": 365907677}
{"allowed": false, "event": "tool_call", "mapping": {"nonce": "319646e8-59e2-4021-9b27-de1376b13c32", "sourceReason": "oversize", "verified": false}, "pid": 4004545, "reason": "unverified-source:oversize", "seq": 25, "starttime_ticks": 365907677, "toolCallId": "call_P9d0wR5TSXSqZHydVHclh6Cg|fc_0fb3d12b5404a73c016a5ac9e02a28819bb39df373b7c9e23b", "toolName": "gate0_nonce_probe"}
{"blockLength": 108, "blockSha256": "99c3dce194b16405dfb555f126ee5ccc014fdc184d0882aee1a903cbc700a0dd", "event": "context_return", "injectionDecision": "REFUSED", "inputCount": 13, "lastPosition": true, "outputCount": 13, "pid": 4004545, "prefixHashAfter": "224c8777dd0cd5fcf1ae02f0fc46198548b48647dbfd044ed131533d72086f16", "prefixHashBefore": "224c8777dd0cd5fcf1ae02f0fc46198548b48647dbfd044ed131533d72086f16", "prefixPreservedByReturn": true, "promotion": false, "requestNonce": "23f3125f-6e62-4f3c-aa60-3eaed705ddc1", "seq": 32, "sourceBroker": {"event": "source_invalidation_revoke", "generation": 12, "new_lease_state": "REVOKED", "peercred": {"gid": 1001, "pid": 4004545, "uid": 1001}, "prior_lease": "REVOKED", "promotion": false, "source_reason": "hash-mismatch", "starttime_ticks": 365907677}, "sourceValidation": {"fragment": "/tmp/gate0-pi-g_3gsk34/mismatch.md", "ok": false, "reason": "hash-mismatch"}, "starttime_ticks": 365907677}
{"allowed": false, "event": "tool_call", "mapping": {"nonce": "23f3125f-6e62-4f3c-aa60-3eaed705ddc1", "sourceReason": "hash-mismatch", "verified": false}, "pid": 4004545, "reason": "unverified-source:hash-mismatch", "seq": 35, "starttime_ticks": 365907677, "toolCallId": "call_QUMvqBRnzv6HNqEd37jU5NUw|fc_0fb3d12b5404a73c016a5ac9e37510819b8506879faf287aa3", "toolName": "gate0_nonce_probe"}
{"event": "source_invalidation_revoke", "generation": 12, "new_lease_state": "REVOKED", "peercred": {"gid": 1001, "pid": 4004545, "uid": 1001}, "prior_lease": "UNVERIFIED", "promotion": false, "source_reason": "missing", "starttime_ticks": 365907677}
{"event": "source_invalidation_revoke", "generation": 12, "new_lease_state": "REVOKED", "peercred": {"gid": 1001, "pid": 4004545, "uid": 1001}, "prior_lease": "REVOKED", "promotion": false, "source_reason": "oversize", "starttime_ticks": 365907677}
{"event": "source_invalidation_revoke", "generation": 12, "new_lease_state": "REVOKED", "peercred": {"gid": 1001, "pid": 4004545, "uid": 1001}, "prior_lease": "REVOKED", "promotion": false, "source_reason": "hash-mismatch", "starttime_ticks": 365907677}
P6_PI_CONTEXT_ATOMIC_OBSERVATION:
{"blockLength": 108, "blockSha256": "99c3dce194b16405dfb555f126ee5ccc014fdc184d0882aee1a903cbc700a0dd", "event": "context_return", "injectionDecision": "ONE_ATOMIC_AGENT_MESSAGE", "inputCount": 1, "lastPosition": true, "outputCount": 2, "pid": 4004545, "prefixHashAfter": "095a5415879b0d4006d1485dba3398fee6bf39850711ba0dc2e9cfa312e865dc", "prefixHashBefore": "095a5415879b0d4006d1485dba3398fee6bf39850711ba0dc2e9cfa312e865dc", "prefixPreservedByReturn": true, "promotion": false, "requestNonce": "e5a82358-a6c9-490b-a0de-2e1f1d9b8d79", "seq": 3, "sourceBroker": {"action": "none", "reason": "source-valid"}, "sourceValidation": {"ok": true, "reason": "all-fragments-valid"}, "starttime_ticks": 365907677}
{"event": "before_provider_request", "finalPayloadValid": true, "inFlightDepth": 1, "markerOccurrences": 1, "markerPaths": ["$.input[1].content[0].text"], "pid": 4004545, "requestNonce": "e5a82358-a6c9-490b-a0de-2e1f1d9b8d79", "seq": 4, "starttime_ticks": 365907677}
{"blockLength": 108, "blockSha256": "99c3dce194b16405dfb555f126ee5ccc014fdc184d0882aee1a903cbc700a0dd", "event": "context_return", "injectionDecision": "ONE_ATOMIC_AGENT_MESSAGE", "inputCount": 3, "lastPosition": true, "outputCount": 4, "pid": 4004545, "prefixHashAfter": "7c40cce3664af7581b21ea007a4a44764237fce1e4f16b031e96d60df2229855", "prefixHashBefore": "7c40cce3664af7581b21ea007a4a44764237fce1e4f16b031e96d60df2229855", "prefixPreservedByReturn": true, "promotion": false, "requestNonce": "cca4b1e3-296a-4e4c-9805-a395c270c01f", "seq": 9, "sourceBroker": {"action": "none", "reason": "source-valid"}, "sourceValidation": {"ok": true, "reason": "all-fragments-valid"}, "starttime_ticks": 365907677}
{"event": "before_provider_request", "finalPayloadValid": true, "inFlightDepth": 1, "markerOccurrences": 1, "markerPaths": ["$.input[5].content[0].text"], "pid": 4004545, "requestNonce": "cca4b1e3-296a-4e4c-9805-a395c270c01f", "seq": 10, "starttime_ticks": 365907677}
{"assistantContentObserved": true, "assistantTextSha256": "99c3dce194b16405dfb555f126ee5ccc014fdc184d0882aee1a903cbc700a0dd", "event": "message_end", "exactContextBlockCopied": true, "inFlightDepthAfter": 0, "nonceMappings": [], "pid": 4004545, "requestNonce": "cca4b1e3-296a-4e4c-9805-a395c270c01f", "role": "assistant", "seq": 11, "starttime_ticks": 365907677, "toolCallIds": []}
RPC_EVENT_COUNTS:
{"agent_end": 4, "agent_settled": 4, "agent_start": 4, "extension_ui_request": 8, "message_end": 16, "message_start": 16, "message_update": 105, "response": 9, "tool_execution_end": 4, "tool_execution_start": 4, "turn_end": 8, "turn_start": 8}
stderr_nonempty=False
@@ -1,9 +0,0 @@
$ python3 docs/compaction-refresh/probes/p2_provider_timing_run.py
local_http_endpoint=http://127.0.0.1:42823/v1/chat/completions
{"event": "before_provider_request", "finalPayloadValid": true, "inFlightDepth": 1, "markerOccurrences": 1, "markerPaths": ["$.messages[2].content[0].text"], "pid": 4008778, "requestNonce": "056b82c4-36eb-420b-94cf-b2c73813ef79", "seq": 4, "starttime_ticks": 365916346}
{"assistantContentAvailableAtThisHook": false, "event": "after_provider_response", "pid": 4008778, "requestNonce": "056b82c4-36eb-420b-94cf-b2c73813ef79", "seq": 5, "starttime_ticks": 365916346, "status": 200, "timing": "headers/status before stream consumption"}
{"assistantContentObserved": true, "assistantTextSha256": "fb4ebaab26d63661040dc15925a99e22dc07ee2b33df5c6b2ca93a5b34f08b1d", "event": "message_end", "exactContextBlockCopied": false, "inFlightDepthAfter": 0, "nonceMappings": [], "pid": 4008778, "requestNonce": "056b82c4-36eb-420b-94cf-b2c73813ef79", "role": "assistant", "seq": 6, "starttime_ticks": 365916346, "toolCallIds": []}
machine_assertions=PASS
after_provider_response_seq=5
message_end_seq=6
headers_hook_precedes_completed_message=True
@@ -1,23 +0,0 @@
$ python3 docs/compaction-refresh/probes/p4_peercred_probe.py
machine_assertions=PASS
server_pid=4013762 server_uid=1001 server_gid=1001
socket_path=/tmp/gate0-p4-nl1_8ap2/broker.sock
directory_mode=0700 socket_mode=0600
SO_PEERCRED pid=4013768 uid=1001 gid=1001
client_claim={"exe": "/usr/bin/python3.11", "pid": 4013768, "ppid": 4013762, "starttime_ticks": 365927069, "uid": 1001}
proc_observed={"exe": "/usr/bin/python3.11", "pid": 4013768, "ppid": 4013762, "starttime_ticks": 365927069, "uid": 1001}
pid_match=True
uid_match=True
starttime_match=True
client_exit_status=0
same_principal_socket=true
posture=0700 parent + 0600 socket excludes other UIDs, but does not prevent the same UID from unlinking/rebinding; distinct-principal system service remains required for a claim stronger than T-C against same-UID counterfeit replacement
$ id
uid=1001(hermes) gid=1001(hermes) groups=1001(hermes),40(src),100(users),996(docker)
$ uname -srmo
Linux 6.1.0-48-amd64 x86_64 GNU/Linux
$ getconf CLK_TCK
100
@@ -1,21 +0,0 @@
$ python3 docs/compaction-refresh/probes/p6_claude_run.py
machine_assertions=PASS
command=mosaic yolo claude --settings <isolated> --model haiku --print --output-format stream-json --verbose --include-hook-events <prompt>
claude_version=2.1.205 (Claude Code)
mosaic_version=0.0.48
exit_code=0
hook_process_log={"block_length": 116, "block_sha256": "ef6377d63552af075f4f4adec00165988418c5f46a992f4dce8e678b56fd34ac", "emission": "one hookSpecificOutput.additionalContext string field", "hook_event_name": "SessionStart", "pid": 4015703, "ppid": 4015701, "starttime_ticks": 365930489}
hook_stream_event={"hook_event": "SessionStart", "hook_id": "557d613e-574f-4523-8bfb-8c6e51946035", "hook_name": "SessionStart:startup", "session_id": "f821d0db-1177-4237-8ff5-83b2a46996a6", "subtype": "hook_started", "type": "system", "uuid": "59449134-e2d4-43a9-9d34-b59e74622c08"}
hook_stream_event={"exit_code": 0, "hook_event": "SessionStart", "hook_id": "557d613e-574f-4523-8bfb-8c6e51946035", "hook_name": "SessionStart:startup", "outcome": "success", "output": "{\"hookSpecificOutput\": {\"hookEventName\": \"SessionStart\", \"additionalContext\": \"GATE0_CLAUDE_ATOMIC_BEGIN\\nsegment-01=alpha-2d11\\nsegment-02=middle-8e22\\nsegment-03=omega-4f33\\nGATE0_CLAUDE_ATOMIC_END\"}}\n", "session_id": "f821d0db-1177-4237-8ff5-83b2a46996a6", "stderr": "", "stdout": "{\"hookSpecificOutput\": {\"hookEventName\": \"SessionStart\", \"additionalContext\": \"GATE0_CLAUDE_ATOMIC_BEGIN\\nsegment-01=alpha-2d11\\nsegment-02=middle-8e22\\nsegment-03=omega-4f33\\nGATE0_CLAUDE_ATOMIC_END\"}}\n", "subtype": "hook_response", "type": "system", "uuid": "e05842c1-813a-41dd-93c9-768eb834f260"}
block_length=116
block_sha256=ef6377d63552af075f4f4adec00165988418c5f46a992f4dce8e678b56fd34ac
stream_fields_containing_full_block=3
stream_fields_exactly_equal_block=2
assistant_copy_length=449
assistant_copy_sha256=a65febbb3ad8fa4952894d94406a83a520ef712c0ecb9ab43be3212094b21ba1
assistant_copy_exact=False
assistant_copy="The user is asking me to return the exact GATE0_CLAUDE_ATOMIC block that was injected by SessionStart. This block was provided in the system-reminder at the beginning of the conversation:\n\n```\nGATE0_CLAUDE_ATOMIC_BEGIN\nsegment-01=alpha-2d11\nsegment-02=middle-8e22\nsegment-03=omega-4f33\nGATE0_CLAUDE_ATOMIC_END\n```\n\nThe user wants me to return ONLY this exact block, with no code fence or commentary. So I should just output it exactly as it appears."
assistant_copy_length=116
assistant_copy_sha256=ef6377d63552af075f4f4adec00165988418c5f46a992f4dce8e678b56fd34ac
assistant_copy_exact=True
assistant_copy="GATE0_CLAUDE_ATOMIC_BEGIN\nsegment-01=alpha-2d11\nsegment-02=middle-8e22\nsegment-03=omega-4f33\nGATE0_CLAUDE_ATOMIC_END"
@@ -1,47 +0,0 @@
$ rg -n -i "atomic|prefix-preserv" <Pi extensions docs> <Claude hook docs>
NO MATCH: neither installed runtime document states an atomic/prefix-preserving transport guarantee.
$ rg -n -C 3 "#### context|event.messages - deep copy|return \{ messages" <Pi extensions docs>
638-});
639-```
640-
641:#### context
642-
643-Fired before each LLM call. Modify messages non-destructively. See [Session Format](session-format.md) for message types.
644-
645-```typescript
646-pi.on("context", async (event, ctx) => {
647: // event.messages - deep copy, safe to modify
648- const filtered = event.messages.filter(m => !shouldPrune(m));
649: return { messages: filtered };
650-});
651-```
652-
$ rg -n -C 3 "additionalContext|add to the default system prompt" <Claude installed docs>
/home/hermes/.config/mosaic/runtime/claude/RUNTIME.md-58- tiered models via the Task `model` param).
/home/hermes/.config/mosaic/runtime/claude/RUNTIME.md-59-
/home/hermes/.config/mosaic/runtime/claude/RUNTIME.md-60-Note: PostToolUse hook plain stdout on exit 0 goes to the debug log, not model context — only
/home/hermes/.config/mosaic/runtime/claude/RUNTIME.md:61:`hookSpecificOutput.additionalContext` (or exit-2 stderr) enters context.
--
/home/hermes/.claude/plugins/marketplaces/claude-plugins-official/plugins/explanatory-output-style/README.md-59-expressed as
/home/hermes/.claude/plugins/marketplaces/claude-plugins-official/plugins/explanatory-output-style/README.md-60-[subagents](https://docs.claude.com/en/docs/claude-code/sub-agents), not as
/home/hermes/.claude/plugins/marketplaces/claude-plugins-official/plugins/explanatory-output-style/README.md-61-SessionStart hooks. Subagents change the system prompt while SessionStart hooks
/home/hermes/.claude/plugins/marketplaces/claude-plugins-official/plugins/explanatory-output-style/README.md:62:add to the default system prompt.
/home/hermes/.claude/plugins/marketplaces/claude-plugins-official/plugins/explanatory-output-style/README.md-63-
/home/hermes/.claude/plugins/marketplaces/claude-plugins-official/plugins/explanatory-output-style/README.md-64-## Managing changes
/home/hermes/.claude/plugins/marketplaces/claude-plugins-official/plugins/explanatory-output-style/README.md-65-
--
/home/hermes/.claude/plugins/marketplaces/claude-plugins-official/plugins/explanatory-output-style/hooks-handlers/session-start.sh-1-#!/usr/bin/env bash
/home/hermes/.claude/plugins/marketplaces/claude-plugins-official/plugins/explanatory-output-style/hooks-handlers/session-start.sh-2-
/home/hermes/.claude/plugins/marketplaces/claude-plugins-official/plugins/explanatory-output-style/hooks-handlers/session-start.sh:3:# Output the explanatory mode instructions as additionalContext
/home/hermes/.claude/plugins/marketplaces/claude-plugins-official/plugins/explanatory-output-style/hooks-handlers/session-start.sh-4-# This mimics the deprecated Explanatory output style
/home/hermes/.claude/plugins/marketplaces/claude-plugins-official/plugins/explanatory-output-style/hooks-handlers/session-start.sh-5-
/home/hermes/.claude/plugins/marketplaces/claude-plugins-official/plugins/explanatory-output-style/hooks-handlers/session-start.sh-6-cat << 'EOF'
/home/hermes/.claude/plugins/marketplaces/claude-plugins-official/plugins/explanatory-output-style/hooks-handlers/session-start.sh-7-{
/home/hermes/.claude/plugins/marketplaces/claude-plugins-official/plugins/explanatory-output-style/hooks-handlers/session-start.sh-8- "hookSpecificOutput": {
/home/hermes/.claude/plugins/marketplaces/claude-plugins-official/plugins/explanatory-output-style/hooks-handlers/session-start.sh-9- "hookEventName": "SessionStart",
/home/hermes/.claude/plugins/marketplaces/claude-plugins-official/plugins/explanatory-output-style/hooks-handlers/session-start.sh:10: "additionalContext": "You are in 'explanatory' output style mode, where you should provide educational insights about the codebase as you help with the user's task.\n\nYou should be clear and educational, providing helpful explanations while remaining focused on the task. Balance educational content with task completion. When providing insights, you may exceed typical length constraints, but remain focused and relevant.\n\n## Insights\nIn order to encourage learning, before and after writing code, always provide brief educational explanations about implementation choices using (with backticks):\n\"`★ Insight ─────────────────────────────────────`\n[2-3 key educational points]\n`─────────────────────────────────────────────────`\"\n\nThese insights should be included in the conversation, not in the codebase. You should generally focus on interesting insights that are specific to the codebase or the code you just wrote, rather than general programming concepts. Do not wait until the end to provide insights. Provide them as you write code."
/home/hermes/.claude/plugins/marketplaces/claude-plugins-official/plugins/explanatory-output-style/hooks-handlers/session-start.sh-11- }
/home/hermes/.claude/plugins/marketplaces/claude-plugins-official/plugins/explanatory-output-style/hooks-handlers/session-start.sh-12-}
/home/hermes/.claude/plugins/marketplaces/claude-plugins-official/plugins/explanatory-output-style/hooks-handlers/session-start.sh-13-EOF
@@ -1,9 +0,0 @@
$ sha256sum ~/agent-work/reviews/compaction-refresh-BUILD-BRIEF.md ~/agent-work/reviews/compaction-refresh-SPEC-v5.md ~/agent-work/reviews/compaction-refresh-SPEC-RATIFICATION.md
89fdbc27ed0e5050dc7b52f3ef2ddaea691edf17fd89d51b15e26fb5ed47171b /home/hermes/agent-work/reviews/compaction-refresh-BUILD-BRIEF.md
a6d07ade835758e8488ca10d3b0631caf0beb93ea3a6733631f151b0c2f01433 /home/hermes/agent-work/reviews/compaction-refresh-SPEC-v5.md
bac58319c9c4028b5b40e1129e0033cdb5a6b7b02033c25f06f4cb77d7779c67 /home/hermes/agent-work/reviews/compaction-refresh-SPEC-RATIFICATION.md
Expected:
89fdbc27ed0e5050dc7b52f3ef2ddaea691edf17fd89d51b15e26fb5ed47171b BUILD-BRIEF
a6d07ade835758e8488ca10d3b0631caf0beb93ea3a6733631f151b0c2f01433 SPEC-v5
bac58319c9c4028b5b40e1129e0033cdb5a6b7b02033c25f06f4cb77d7779c67 RATIFICATION
@@ -1,2 +0,0 @@
__pycache__/
*.pyc
@@ -1,51 +0,0 @@
#!/usr/bin/env python3
"""Register this PID as anchor, then exec the real `mosaic yolo` launcher."""
from __future__ import annotations
import argparse
import json
import os
import socket
import sys
def request(socket_path: str, payload: dict[str, object]) -> dict[str, object]:
conn = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
conn.connect(socket_path)
conn.sendall((json.dumps(payload) + "\n").encode())
response = json.loads(conn.makefile("r", encoding="utf-8").readline())
conn.close()
return response
def main() -> None:
parser = argparse.ArgumentParser()
parser.add_argument("--socket", required=True)
parser.add_argument("runtime", choices=["pi", "claude"])
parser.add_argument("args", nargs=argparse.REMAINDER)
ns = parser.parse_args()
response = request(ns.socket, {"action": "register-anchor", "runtime": ns.runtime})
if response.get("decision") != "ACCEPT":
raise SystemExit("anchor registration refused")
os.environ["GATE0_SESSION_ID"] = str(response["session_id"])
argv = ["mosaic", "yolo", ns.runtime, *ns.args]
print(
json.dumps(
{
"event": "anchor-exec",
"pid": os.getpid(),
"argv": ["mosaic", "yolo", ns.runtime, f"<{len(ns.args)} runtime args>"],
"note": "os.execvpe retains pid and /proc starttime",
},
sort_keys=True,
),
file=sys.stderr,
flush=True,
)
os.execvpe("mosaic", argv, os.environ)
if __name__ == "__main__":
main()
-180
View File
@@ -1,180 +0,0 @@
#!/usr/bin/env python3
"""Gate0 P1 broker prototype: peercred anchor minting and /proc ancestry checks."""
from __future__ import annotations
import argparse
import json
import os
import secrets
import socket
import stat
import struct
import sys
from pathlib import Path
from typing import Any
def proc_node(pid: int) -> dict[str, Any]:
text = Path(f"/proc/{pid}/stat").read_text()
close = text.rfind(")")
comm = text[text.find("(") + 1 : close]
fields = text[close + 2 :].split()
cmdline = Path(f"/proc/{pid}/cmdline").read_bytes().split(b"\0")
return {
"pid": pid,
"ppid": int(fields[1]),
"starttime_ticks": int(fields[19]),
"comm": comm,
"exe": os.readlink(f"/proc/{pid}/exe"),
"argv0": cmdline[0].decode(errors="replace") if cmdline and cmdline[0] else "",
"argc": len([part for part in cmdline if part]),
}
def ancestry(peer_pid: int, anchor: dict[str, Any] | None) -> tuple[list[dict[str, Any]], bool, str]:
chain: list[dict[str, Any]] = []
pid = peer_pid
seen: set[int] = set()
try:
while pid > 0 and pid not in seen:
seen.add(pid)
node = proc_node(pid)
chain.append(node)
if anchor and pid == anchor["pid"]:
if node["starttime_ticks"] != anchor["starttime_ticks"]:
return chain, False, "anchor-starttime-mismatch"
break
pid = node["ppid"]
else:
return chain, False, "anchor-not-reached"
if not anchor or chain[-1]["pid"] != anchor["pid"]:
return chain, False, "anchor-not-reached"
# Re-read every node after the walk. A disappearing PID or changed
# starttime invalidates the complete chain (PID-reuse/race closure).
for original in chain:
again = proc_node(original["pid"])
if again["starttime_ticks"] != original["starttime_ticks"]:
return chain, False, f"starttime-race:{original['pid']}"
return chain, True, "ancestry-reaches-registered-anchor"
except (FileNotFoundError, ProcessLookupError, PermissionError) as exc:
return chain, False, f"proc-walk-failed:{type(exc).__name__}"
def emit(log_file: Path, record: dict[str, Any]) -> None:
line = json.dumps(record, sort_keys=True)
with log_file.open("a", encoding="utf-8") as out:
out.write(line + "\n")
print(line, flush=True)
def main() -> None:
parser = argparse.ArgumentParser()
parser.add_argument("--socket", required=True)
parser.add_argument("--log", required=True)
parser.add_argument("--state", required=True)
args = parser.parse_args()
socket_path = Path(args.socket)
log_file = Path(args.log)
state_file = Path(args.state)
socket_path.parent.mkdir(parents=True, exist_ok=True)
os.chmod(socket_path.parent, 0o700)
socket_path.unlink(missing_ok=True)
log_file.unlink(missing_ok=True)
state_file.unlink(missing_ok=True)
server = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
server.bind(str(socket_path))
os.chmod(socket_path, 0o600)
server.listen(8)
anchor: dict[str, Any] | None = None
session_id: str | None = None
emit(
log_file,
{
"event": "broker-listen",
"pid": os.getpid(),
"socket": str(socket_path),
"directory_mode": f"{stat.S_IMODE(socket_path.parent.stat().st_mode):04o}",
"socket_mode": f"{stat.S_IMODE(socket_path.stat().st_mode):04o}",
},
)
while True:
conn, _ = server.accept()
with conn:
raw = conn.getsockopt(socket.SOL_SOCKET, socket.SO_PEERCRED, 12)
peer_pid, peer_uid, peer_gid = struct.unpack("3i", raw)
request = json.loads(conn.makefile("r", encoding="utf-8").readline())
action = request.get("action")
if action == "register-anchor" and anchor is None:
anchor = proc_node(peer_pid)
session_id = secrets.token_hex(16)
state = {"session_id": session_id, "anchor": anchor}
state_file.write_text(json.dumps(state, sort_keys=True) + "\n")
record = {
"event": "anchor-minted",
"decision": "ACCEPT",
"peercred": {"pid": peer_pid, "uid": peer_uid, "gid": peer_gid},
"anchor": anchor,
"session_id": session_id,
}
emit(log_file, record)
conn.sendall((json.dumps(record) + "\n").encode())
continue
if action in {"resolve-hook", "claim-session"}:
chain, reaches, reason = ancestry(peer_pid, anchor)
claimed = request.get("session_id")
claim_ok = action == "resolve-hook" or claimed == session_id
accepted = bool(anchor and session_id and reaches and claim_ok)
if action == "claim-session" and claimed != session_id:
reason = "unknown-session-id"
elif action == "claim-session" and claimed == session_id and not reaches:
reason = "victim-id-known-but-ancestry-mismatch"
record = {
"event": action,
"decision": "ACCEPT" if accepted else "REJECT",
"reason": reason,
"peercred": {"pid": peer_pid, "uid": peer_uid, "gid": peer_gid},
"claimed_session_id": claimed,
"resolved_session_id": session_id if accepted else None,
"anchor": anchor,
"ancestry": chain,
"starttimes_rechecked": reaches,
}
emit(log_file, record)
conn.sendall((json.dumps(record) + "\n").encode())
continue
if action == "shutdown":
record = {
"event": "broker-shutdown",
"peercred": {"pid": peer_pid, "uid": peer_uid, "gid": peer_gid},
}
emit(log_file, record)
conn.sendall((json.dumps(record) + "\n").encode())
break
record = {
"event": "invalid-request",
"decision": "REJECT",
"peercred": {"pid": peer_pid, "uid": peer_uid, "gid": peer_gid},
}
emit(log_file, record)
conn.sendall((json.dumps(record) + "\n").encode())
server.close()
socket_path.unlink(missing_ok=True)
if __name__ == "__main__":
try:
main()
except Exception as exc:
print(f"P1 broker fatal: {type(exc).__name__}: {exc}", file=sys.stderr)
raise
@@ -1,38 +0,0 @@
#!/usr/bin/env python3
"""Claude SessionStart hook client for P1 ancestry evidence."""
from __future__ import annotations
import json
import os
import socket
import sys
def main() -> None:
# Consume the real Claude hook payload without recording transcript paths or
# prompt content in the evidence artifact.
hook_input = json.load(sys.stdin)
conn = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
conn.connect(os.environ["GATE0_BROKER_SOCKET"])
conn.sendall((json.dumps({"action": "resolve-hook"}) + "\n").encode())
response = json.loads(conn.makefile("r", encoding="utf-8").readline())
conn.close()
event_name = hook_input.get("hook_event_name")
if response.get("decision") != "ACCEPT":
print(f"Gate0 broker rejected {event_name} hook ancestry", file=sys.stderr)
raise SystemExit(2)
print(
json.dumps(
{
"hookSpecificOutput": {
"hookEventName": event_name,
"additionalContext": "GATE0_P1_SUPPORTED_HOOK_ANCESTRY_ACCEPTED",
}
}
)
)
if __name__ == "__main__":
main()
@@ -1,35 +0,0 @@
import type { ExtensionAPI } from '@earendil-works/pi-coding-agent';
import net from 'node:net';
async function brokerRequest(payload: Record<string, unknown>): Promise<Record<string, unknown>> {
const socketPath = process.env['GATE0_BROKER_SOCKET'];
if (!socketPath) throw new Error('GATE0_BROKER_SOCKET missing');
return await new Promise((resolve, reject) => {
const socket = net.createConnection(socketPath);
let buffer = '';
socket.setEncoding('utf8');
socket.on('connect', () => socket.write(`${JSON.stringify(payload)}\n`));
socket.on('data', (chunk) => {
buffer += chunk;
const newline = buffer.indexOf('\n');
if (newline < 0) return;
socket.end();
resolve(JSON.parse(buffer.slice(0, newline)) as Record<string, unknown>);
});
socket.on('error', reject);
});
}
export default function register(pi: ExtensionAPI) {
pi.on('session_start', async () => {
const response = await brokerRequest({ action: 'resolve-hook', runtime: 'pi-extension' });
if (response['decision'] !== 'ACCEPT') {
throw new Error(`P1 broker rejected Pi extension ancestry: ${response['reason']}`);
}
});
pi.registerCommand('gate0-p1-ready', {
description: 'Return only after the P1 session_start ancestry hook completed',
handler: async () => undefined,
});
}
-274
View File
@@ -1,274 +0,0 @@
#!/usr/bin/env python3
"""Run P1 against the real installed Mosaic→Pi and Mosaic→Claude chains."""
from __future__ import annotations
import argparse
import json
import os
import shutil
import signal
import socket
import subprocess
import sys
import tempfile
import time
from pathlib import Path
from typing import Any
HERE = Path(__file__).resolve().parent
def wait_for(predicate, description: str, timeout: float = 30.0) -> None:
deadline = time.monotonic() + timeout
while time.monotonic() < deadline:
if predicate():
return
time.sleep(0.05)
raise TimeoutError(f"timed out waiting for {description}")
def read_records(path: Path) -> list[dict[str, Any]]:
if not path.exists():
return []
return [json.loads(line) for line in path.read_text().splitlines() if line]
def socket_request(path: Path, payload: dict[str, object]) -> dict[str, object]:
conn = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
conn.connect(str(path))
conn.sendall((json.dumps(payload) + "\n").encode())
response = json.loads(conn.makefile("r", encoding="utf-8").readline())
conn.close()
return response
def start_broker(root: Path) -> tuple[subprocess.Popen[str], Path, Path, Path]:
socket_path = root / "broker.sock"
log_path = root / "broker.jsonl"
state_path = root / "state.json"
broker = subprocess.Popen(
[
sys.executable,
str(HERE / "p1_broker.py"),
"--socket",
str(socket_path),
"--log",
str(log_path),
"--state",
str(state_path),
],
text=True,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
)
wait_for(socket_path.exists, "broker socket")
return broker, socket_path, log_path, state_path
def print_ps(record: dict[str, Any]) -> None:
chain = record.get("ancestry", [])
pids = [str(node["pid"]) for node in chain if Path(f"/proc/{node['pid']}").exists()]
if not pids:
print("ps_snapshot=<hook chain exited; broker /proc snapshot above is authoritative>")
return
command = [
"ps",
"-o",
"pid=,ppid=,lstart=,uid=,gid=,comm=",
"-p",
",".join(pids),
]
print("$ " + " ".join(command))
print(subprocess.check_output(command, text=True).rstrip())
def run_runtime(runtime: str) -> None:
with tempfile.TemporaryDirectory(prefix=f"gate0-p1-{runtime}-") as temp:
root = Path(temp)
workspace = root / "workspace"
workspace.mkdir()
broker, socket_path, log_path, state_path = start_broker(root)
env = os.environ.copy()
env.update(
{
"GATE0_BROKER_SOCKET": str(socket_path),
"MOSAIC_PI_FORCE_SKILLS": "",
"PI_SKIP_VERSION_CHECK": "1",
}
)
stdout_path = root / f"{runtime}.stdout"
stderr_path = root / f"{runtime}.stderr"
if runtime == "pi":
runtime_args = [
"--mode",
"rpc",
"--no-session",
"--no-extensions",
"--no-context-files",
"--no-prompt-templates",
"--extension",
str(HERE / "p1_pi_extension.ts"),
]
else:
settings = root / "claude-settings.json"
settings.write_text(
json.dumps(
{
"hooks": {
"SessionStart": [
{
"hooks": [
{
"type": "command",
"command": f'python3 "{HERE / "p1_hook_client.py"}"',
"timeout": 20,
}
]
}
]
}
}
)
)
runtime_args = [
"--settings",
str(settings),
"--model",
"haiku",
"--print",
"--output-format",
"stream-json",
"--verbose",
"--include-hook-events",
"--max-budget-usd",
"0.03",
"Reply exactly: OK",
]
out = stdout_path.open("w", encoding="utf-8")
err = stderr_path.open("w", encoding="utf-8")
anchor = subprocess.Popen(
[
sys.executable,
str(HERE / "p1_anchor_exec.py"),
"--socket",
str(socket_path),
runtime,
*runtime_args,
],
cwd=workspace,
env=env,
stdin=subprocess.PIPE if runtime == "pi" else subprocess.DEVNULL,
stdout=out,
stderr=err,
text=True,
start_new_session=True,
)
try:
wait_for(state_path.exists, "anchor registration")
attacker = subprocess.run(
[
sys.executable,
str(HERE / "p1_sibling_attacker.py"),
"--socket",
str(socket_path),
"--state",
str(state_path),
],
text=True,
capture_output=True,
check=False,
)
if runtime == "pi":
assert anchor.stdin is not None
anchor.stdin.write('{"id":"state","type":"get_state"}\n')
anchor.stdin.flush()
wait_for(
lambda: any(r.get("event") == "resolve-hook" for r in read_records(log_path)),
f"{runtime} supported hook/extension broker contact",
timeout=60,
)
if runtime == "claude":
try:
anchor.wait(timeout=90)
except subprocess.TimeoutExpired:
pass
records = read_records(log_path)
state = json.loads(state_path.read_text())
resolve = next(r for r in records if r.get("event") == "resolve-hook")
reject = next(r for r in records if r.get("event") == "claim-session")
if resolve.get("decision") != "ACCEPT":
raise AssertionError(f"{runtime} hook ancestry was not accepted: {resolve}")
if reject.get("decision") != "REJECT":
raise AssertionError(f"{runtime} sibling substitution was not rejected: {reject}")
if attacker.returncode != 0:
raise AssertionError(f"{runtime} sibling probe did not observe rejection: {attacker.stderr}")
print(f"=== P1 {runtime.upper()} REAL LAUNCH ===")
print("machine_assertions=PASS")
print(
"$ python3 docs/compaction-refresh/probes/p1_anchor_exec.py "
f"--socket <protected-socket> {runtime} <runtime args>"
)
print("registered_anchor=" + json.dumps(state["anchor"], sort_keys=True))
print("broker_minted_session_id=" + state["session_id"])
print("hook_or_extension_record=" + json.dumps(resolve, sort_keys=True))
print("sibling_attack_record=" + json.dumps(reject, sort_keys=True))
print("sibling_process_stdout=" + attacker.stdout.strip())
print(f"sibling_process_exit={attacker.returncode}")
print_ps(resolve)
print("launcher_stderr_excerpt:")
for line in stderr_path.read_text(errors="replace").splitlines()[:12]:
print(" " + line[:500])
runtime_lines = stdout_path.read_text(errors="replace").splitlines()
print("runtime_stdout_excerpt:")
for line in runtime_lines[:8]:
print(" " + line[:500])
hook_lines = [
line
for line in runtime_lines
if "hook" in line.lower() or "GATE0_P1_SUPPORTED_HOOK" in line
]
print("runtime_hook_event_excerpt:")
for line in hook_lines[:8]:
print(" " + line[:1000])
print()
finally:
if anchor.poll() is None:
try:
os.killpg(anchor.pid, signal.SIGTERM)
except ProcessLookupError:
pass
try:
anchor.wait(timeout=5)
except subprocess.TimeoutExpired:
os.killpg(anchor.pid, signal.SIGKILL)
anchor.wait(timeout=5)
out.close()
err.close()
try:
socket_request(socket_path, {"action": "shutdown"})
except OSError:
pass
try:
broker.wait(timeout=5)
except subprocess.TimeoutExpired:
broker.kill()
broker.wait()
def main() -> None:
parser = argparse.ArgumentParser()
parser.add_argument("--runtime", choices=["pi", "claude", "both"], default="both")
ns = parser.parse_args()
if ns.runtime in {"pi", "both"}:
run_runtime("pi")
if ns.runtime in {"claude", "both"}:
run_runtime("claude")
if __name__ == "__main__":
main()
@@ -1,54 +0,0 @@
#!/usr/bin/env python3
"""Same-UID sibling that attempts to claim the anchor's broker-minted id."""
from __future__ import annotations
import argparse
import json
import os
import socket
import time
from pathlib import Path
def main() -> None:
parser = argparse.ArgumentParser()
parser.add_argument("--socket", required=True)
parser.add_argument("--state", required=True)
ns = parser.parse_args()
state_path = Path(ns.state)
for _ in range(200):
if state_path.exists():
break
time.sleep(0.025)
state = json.loads(state_path.read_text())
conn = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
conn.connect(ns.socket)
conn.sendall(
(
json.dumps(
{"action": "claim-session", "session_id": state["session_id"]},
sort_keys=True,
)
+ "\n"
).encode()
)
response = json.loads(conn.makefile("r", encoding="utf-8").readline())
conn.close()
print(
json.dumps(
{
"attacker_pid": os.getpid(),
"attacker_uid": os.getuid(),
"victim_session_id_known": True,
"broker_decision": response.get("decision"),
"broker_reason": response.get("reason"),
},
sort_keys=True,
)
)
raise SystemExit(0 if response.get("decision") == "REJECT" else 1)
if __name__ == "__main__":
main()
@@ -1,135 +0,0 @@
#!/usr/bin/env python3
"""Force a real Pi HTTP provider response to prove response-hook timing."""
from __future__ import annotations
import json
import os
import tempfile
import threading
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from pathlib import Path
from pi_gate0_run import PiRpc, jsonl
HERE = Path(__file__).resolve().parent
class Handler(BaseHTTPRequestHandler):
protocol_version = "HTTP/1.1"
def log_message(self, _format: str, *_args: object) -> None:
return
def do_POST(self) -> None: # noqa: N802
length = int(self.headers.get("content-length", "0"))
self.rfile.read(length)
chunks = [
{
"id": "gate0-response",
"object": "chat.completion.chunk",
"created": 1,
"model": "gate0-model",
"choices": [{"index": 0, "delta": {"role": "assistant"}, "finish_reason": None}],
},
{
"id": "gate0-response",
"object": "chat.completion.chunk",
"created": 1,
"model": "gate0-model",
"choices": [
{"index": 0, "delta": {"content": "TIMING_OK"}, "finish_reason": None}
],
},
{
"id": "gate0-response",
"object": "chat.completion.chunk",
"created": 1,
"model": "gate0-model",
"choices": [{"index": 0, "delta": {}, "finish_reason": "stop"}],
"usage": {"prompt_tokens": 10, "completion_tokens": 2, "total_tokens": 12},
},
]
body = "".join(f"data: {json.dumps(chunk)}\n\n" for chunk in chunks) + "data: [DONE]\n\n"
encoded = body.encode()
self.send_response(200)
self.send_header("Content-Type", "text/event-stream")
self.send_header("Content-Length", str(len(encoded)))
self.send_header("X-Gate0-Response", "headers-before-stream")
self.end_headers()
self.wfile.write(encoded)
self.wfile.flush()
def main() -> None:
server = ThreadingHTTPServer(("127.0.0.1", 0), Handler)
thread = threading.Thread(target=server.serve_forever, daemon=True)
thread.start()
port = server.server_address[1]
with tempfile.TemporaryDirectory(prefix="gate0-p2-timing-") as temp:
root = Path(temp)
workspace = root / "workspace"
workspace.mkdir()
log = root / "hooks.jsonl"
env = os.environ.copy()
env.update(
{
"GATE0_PI_LOG": str(log),
"GATE0_LOCAL_PROVIDER_URL": f"http://127.0.0.1:{port}/v1",
"MOSAIC_PI_FORCE_SKILLS": "",
"PI_SKIP_VERSION_CHECK": "1",
}
)
command = [
"mosaic",
"yolo",
"pi",
"--mode",
"rpc",
"--no-session",
"--no-extensions",
"--no-context-files",
"--no-prompt-templates",
"--provider",
"gate0-local",
"--model",
"gate0-model",
"--extension",
str(HERE / "pi_gate0_extension.ts"),
]
pi = PiRpc(command, workspace, env)
try:
pi.prompt_and_settle("timing", "Reply with TIMING_OK")
records = jsonl(log)
selected = [
record
for record in records
if record["event"] in {"before_provider_request", "after_provider_response", "message_end"}
and (record["event"] != "message_end" or record.get("role") == "assistant")
]
print("$ python3 docs/compaction-refresh/probes/p2_provider_timing_run.py")
print(f"local_http_endpoint=http://127.0.0.1:{port}/v1/chat/completions")
for record in selected:
print(json.dumps(record, sort_keys=True))
after = next(record for record in selected if record["event"] == "after_provider_response")
message = next(record for record in selected if record["event"] == "message_end")
if not (
after["seq"] < message["seq"]
and after["assistantContentAvailableAtThisHook"] is False
and message["assistantContentObserved"] is True
):
raise AssertionError("provider response/content observation ordering failed")
print("machine_assertions=PASS")
print(f"after_provider_response_seq={after['seq']}")
print(f"message_end_seq={message['seq']}")
print(f"headers_hook_precedes_completed_message={after['seq'] < message['seq']}")
finally:
pi.close()
server.shutdown()
server.server_close()
if __name__ == "__main__":
main()
@@ -1,849 +0,0 @@
#!/usr/bin/env python3
"""D4-only same-PID runtime-generation revocation harness.
AUTHORING NOTE: this file is intentionally not executed until the separately
ratified FIRE authorization. When run later, every invocation creates its own
/tmp fixture and launches the real Pi RPC runtime with only the D4 extension
and ``p3_generation_broker.py``. It does not use the broader Gate0 runner.
"""
from __future__ import annotations
import argparse
import ast
import hashlib
import json
import os
import queue
import shutil
import signal
import socket
import subprocess
import sys
import tempfile
import threading
import time
from dataclasses import dataclass
from pathlib import Path
from typing import Any, Callable
HERE = Path(__file__).resolve().parent
# WI-3 remains in a reviewed worktree until the release package contains the
# gated launcher. The probe resolves that worktree portably and never falls
# back to the released `mosaic` binary.
GATED_WI_ROOT_OVERRIDE = os.environ.get("GATED_WI_ROOT")
GATED_WI_BRANCH = "refs/heads/feat/830-compaction-revoke"
GATED_WI_HEAD = "f400830738998db105107a2a4c69c7f2a2a6fd5d"
GATED_WI_ANCESTOR = "66b1e0a0"
GATED_BROKER_HEAD = "23c0caca9b5d44002e6184cd7f2b6c837e8795b2"
LEASE_BROKER_DIRECTORY = "packages/mosaic/framework/tools/lease-broker"
BROKER_RELATIVE_PATH = "docs/compaction-refresh/probes/p3_generation_broker.py"
GATED_LAUNCHER_SHA256 = "e950e4224e280f16979d90cabb89aa1896c5ee28bed2df957e14d018d43cda82"
GATED_GENERATION_SHA256 = "061625402f08488eac47acd23272904e71fd1a71fd15b3bdab158632c801be4c"
GATED_BROKER_SHA256 = "4db4fef1ac6658a8ca79ad5091cefc901d2aa26003265c3d6726c294cf895cad"
class PiRpc:
"""Small JSON-RPC client for an isolated real Pi process."""
def __init__(self, command: list[str], cwd: Path, env: dict[str, str]) -> None:
self.process = subprocess.Popen(
command,
cwd=cwd,
env=env,
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
bufsize=1,
start_new_session=True,
)
self.events: queue.Queue[dict[str, Any]] = queue.Queue()
self.stderr_lines: list[str] = []
threading.Thread(target=self._read_stdout, daemon=True).start()
threading.Thread(target=self._read_stderr, daemon=True).start()
def _read_stdout(self) -> None:
if self.process.stdout is None:
raise RuntimeError("Pi stdout pipe is unavailable")
for line in self.process.stdout:
try:
self.events.put(json.loads(line))
except json.JSONDecodeError:
continue
def _read_stderr(self) -> None:
if self.process.stderr is None:
raise RuntimeError("Pi stderr pipe is unavailable")
for line in self.process.stderr:
self.stderr_lines.append(line.rstrip("\n"))
def send(self, payload: dict[str, object]) -> None:
if self.process.stdin is None:
raise RuntimeError("Pi stdin pipe is unavailable")
self.process.stdin.write(json.dumps(payload) + "\n")
self.process.stdin.flush()
def wait(
self,
predicate: Callable[[dict[str, Any]], bool],
description: str,
timeout: float = 180,
) -> dict[str, Any]:
deadline = time.monotonic() + timeout
while time.monotonic() < deadline:
if self.process.poll() is not None and self.events.empty():
detail = " | ".join(self.stderr_lines[-5:])
raise RuntimeError(
f"Pi exited {self.process.returncode} while waiting for {description}: {detail}"
)
try:
event = self.events.get(timeout=0.2)
except queue.Empty:
continue
if predicate(event):
return event
raise TimeoutError(f"timed out waiting for {description}")
def response(self, request_id: str, timeout: float = 180) -> dict[str, Any]:
return self.wait(
lambda event: event.get("type") == "response" and event.get("id") == request_id,
f"response {request_id}",
timeout,
)
def prompt_and_settle(self, request_id: str, message: str) -> None:
self.send({"id": request_id, "type": "prompt", "message": message})
response = self.response(request_id)
if not response.get("success"):
raise RuntimeError(f"prompt rejected: {response}")
self.wait(
lambda event: event.get("type") == "agent_settled",
f"agent_settled {request_id}",
)
def close(self) -> None:
if self.process.poll() is None:
try:
os.killpg(self.process.pid, signal.SIGTERM)
except ProcessLookupError:
pass
try:
self.process.wait(timeout=8)
except subprocess.TimeoutExpired:
os.killpg(self.process.pid, signal.SIGKILL)
self.process.wait(timeout=5)
def wait_path(path: Path, timeout: float = 20) -> None:
deadline = time.monotonic() + timeout
while time.monotonic() < deadline:
if path.exists():
return
time.sleep(0.05)
raise TimeoutError(f"timed out waiting for {path}")
def request(path: Path, payload: dict[str, object]) -> dict[str, Any]:
with socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) as conn:
conn.connect(str(path))
conn.sendall((json.dumps(payload) + "\n").encode())
reply = conn.makefile("r", encoding="utf-8").readline()
return json.loads(reply)
def jsonl(path: Path) -> list[dict[str, Any]]:
return [json.loads(line) for line in path.read_text().splitlines() if line]
def write_extension(path: Path) -> None:
"""Write the minimal Pi lifecycle bridge into the isolated fixture only."""
path.write_text(
"""import type { ExtensionAPI } from '@earendil-works/pi-coding-agent';
import { Type } from 'typebox';
import { appendFileSync, readFileSync } from 'node:fs';
import net from 'node:net';
const socketPath = process.env['D4_GENERATION_SOCKET'];
const logPath = process.env['D4_PI_LOG'];
function starttime(): number {
const text = readFileSync(`/proc/${process.pid}/stat`, 'utf8');
const close = text.lastIndexOf(')');
return Number(text.slice(close + 2).trim().split(/\\s+/)[19]);
}
function log(event: string, details: Record<string, unknown> = {}): void {
if (!logPath) return;
appendFileSync(logPath, `${JSON.stringify({ event, pid: process.pid, starttime_ticks: starttime(), ...details })}\\n`);
}
function broker(payload: Record<string, unknown>): Promise<Record<string, unknown>> {
if (!socketPath) return Promise.reject(new Error('D4_GENERATION_SOCKET is required'));
return new Promise((resolve, reject) => {
const connection = net.createConnection(socketPath);
let buffer = '';
connection.setEncoding('utf8');
connection.on('connect', () => connection.write(`${JSON.stringify(payload)}\\n`));
connection.on('data', (chunk) => {
buffer += chunk;
const newline = buffer.indexOf('\\n');
if (newline < 0) return;
connection.end();
resolve(JSON.parse(buffer.slice(0, newline)) as Record<string, unknown>);
});
connection.on('error', reject);
});
}
export default function register(pi: ExtensionAPI): void {
let initialStartup = true;
async function lifecycle(
phase: 'start' | 'shutdown',
reason: string,
): Promise<Record<string, unknown>> {
if (!(phase === 'start' && reason === 'startup' && initialStartup)) {
const bump = await broker({ action: 'bump-generation' });
log('generation_state_bump', { phase, reason, bump });
}
initialStartup = false;
return broker({ action: 'lifecycle', phase, reason });
}
pi.on('session_start', async (event) => {
const lifecycleResult = await lifecycle('start', event.reason);
log('session_start', { reason: event.reason, lifecycle: lifecycleResult });
if (event.reason === 'reload') {
const generation = lifecycleResult['new_generation'];
if (typeof generation !== 'number') throw new Error('broker did not return new_generation');
const current = await broker({ action: 'authorize-probe', generation });
const superseded = await broker({ action: 'authorize-probe', generation: generation - 1 });
log('d4_generation_authorization', { generation, current, superseded });
}
});
pi.on('session_shutdown', async (event) => {
const lifecycleResult = await lifecycle('shutdown', event.reason);
log('session_shutdown', { reason: event.reason, lifecycle: lifecycleResult });
});
pi.registerTool({
name: 'd4_fixture_promote',
label: 'D4 Fixture Promotion',
description: 'Promotes only the fixture lease needed for the D4 revocation check.',
parameters: Type.Object({}),
async execute() {
// the promotion step is a D4 test fixture, not a P2 evidence-gathering authorization.
const promotion = await broker({ action: 'promote-probe' });
log('fixture_promotion', { promotion });
return { content: [{ type: 'text', text: 'D4 fixture promotion complete' }] };
},
});
pi.registerCommand('d4-reload', {
description: 'D4-only same-PID reload boundary.',
handler: async (_args, context) => {
await context.reload();
},
});
}
""",
encoding="utf-8",
)
def repository_root() -> Path:
for candidate in HERE.parents:
if (candidate / ".git").exists():
return candidate
raise RuntimeError("D4 precondition: probe repository root is unavailable")
def resolve_gated_wi_root() -> Path:
"""Resolve an explicit override or the unique checked-out WI-3 branch."""
if GATED_WI_ROOT_OVERRIDE:
candidate = Path(GATED_WI_ROOT_OVERRIDE).expanduser()
candidates = [candidate]
else:
try:
listing = subprocess.check_output(
["git", "-C", str(repository_root()), "worktree", "list", "--porcelain"],
text=True,
)
except (OSError, subprocess.CalledProcessError) as error:
raise RuntimeError("D4 precondition: cannot enumerate WI-3 worktrees") from error
candidates = []
worktree: Path | None = None
head: str | None = None
branch: str | None = None
for line in [*listing.splitlines(), ""]:
if line.startswith("worktree "):
worktree = Path(line.removeprefix("worktree "))
head = None
branch = None
elif line.startswith("HEAD "):
head = line.removeprefix("HEAD ")
elif line.startswith("branch "):
branch = line.removeprefix("branch ")
elif not line and worktree is not None:
if head == GATED_WI_HEAD and branch == GATED_WI_BRANCH:
candidates.append(worktree)
worktree = None
if len(candidates) != 1:
raise RuntimeError("D4 precondition: WI-3 worktree is ambiguous or unavailable")
gated_root = candidates[0]
try:
if not gated_root.is_dir():
raise RuntimeError("D4 precondition: GATED_WI_ROOT is not a directory")
is_worktree = subprocess.check_output(
["git", "-C", str(gated_root), "rev-parse", "--is-inside-work-tree"],
text=True,
).strip()
head = subprocess.check_output(
["git", "-C", str(gated_root), "rev-parse", "HEAD"], text=True
).strip()
except (OSError, subprocess.CalledProcessError) as error:
raise RuntimeError("D4 precondition: GATED_WI_ROOT is not a git worktree") from error
if is_worktree != "true":
raise RuntimeError("D4 precondition: GATED_WI_ROOT is not a git worktree")
if head != GATED_WI_HEAD:
raise RuntimeError(f"D4 precondition: gated WI head mismatch: {head}")
try:
forward_contains = subprocess.run(
[
"git",
"-C",
str(gated_root),
"merge-base",
"--is-ancestor",
GATED_WI_ANCESTOR,
GATED_WI_HEAD,
],
check=False,
).returncode == 0
except OSError as error:
raise RuntimeError("D4 precondition: cannot verify WI-3 ancestry") from error
if not forward_contains:
raise RuntimeError("D4 precondition: gated WI lacks required ancestor")
return gated_root
@dataclass(frozen=True)
class PinnedClosure:
launcher: Path
generation: Path
broker: Path
def git_object_bytes(git_root: Path, commit: str, relative_path: str) -> bytes:
try:
return subprocess.check_output(
["git", "-C", str(git_root), "show", f"{commit}:{relative_path}"]
)
except (OSError, subprocess.CalledProcessError) as error:
raise RuntimeError(f"D4 precondition: missing pinned source {relative_path}") from error
def closure_import_guard(member_sources: dict[str, str]) -> None:
"""Refuse an incomplete project-code closure before materializing it."""
allowed_nonstdlib = {"lease_generation"}
stdlib = getattr(sys, "stdlib_module_names", frozenset())
for name, source in member_sources.items():
try:
tree = ast.parse(source, filename=name)
except SyntaxError as error:
raise RuntimeError(f"D4 precondition: pinned {name} does not parse") from error
for node in ast.walk(tree):
module: str | None = None
if isinstance(node, ast.Import):
for alias in node.names:
module = alias.name.split(".", maxsplit=1)[0]
if module not in stdlib and module not in allowed_nonstdlib:
raise RuntimeError(f"D4 precondition: unpinned import {module} in {name}")
elif isinstance(node, ast.ImportFrom):
if node.level:
raise RuntimeError(f"D4 precondition: relative import in {name}")
if node.module:
module = node.module.split(".", maxsplit=1)[0]
if module not in stdlib and module not in allowed_nonstdlib:
raise RuntimeError(f"D4 precondition: unpinned import {module} in {name}")
def write_pinned_file(path: Path, data: bytes) -> None:
descriptor = os.open(
path,
os.O_WRONLY | os.O_CREAT | os.O_EXCL | os.O_CLOEXEC,
0o600,
)
try:
remaining = memoryview(data)
while remaining:
written = os.write(descriptor, remaining)
if written <= 0:
raise OSError("pinned write made no progress")
remaining = remaining[written:]
finally:
os.close(descriptor)
def materialize_closure(root: Path, gated_root: Path, gate0_root: Path) -> PinnedClosure:
"""Pin the complete project-authored runtime closure inside this fixture."""
launcher_relative = f"{LEASE_BROKER_DIRECTORY}/launch-runtime.py"
generation_relative = f"{LEASE_BROKER_DIRECTORY}/lease_generation.py"
members = (
(
"launch-runtime.py",
gated_root,
GATED_WI_HEAD,
launcher_relative,
GATED_LAUNCHER_SHA256,
),
(
"lease_generation.py",
gated_root,
GATED_WI_HEAD,
generation_relative,
GATED_GENERATION_SHA256,
),
(
"p3_generation_broker.py",
gate0_root,
GATED_BROKER_HEAD,
BROKER_RELATIVE_PATH,
GATED_BROKER_SHA256,
),
)
member_bytes: dict[str, bytes] = {}
member_sources: dict[str, str] = {}
for name, git_root, commit, relative_path, digest in members:
data = git_object_bytes(git_root, commit, relative_path)
if hashlib.sha256(data).hexdigest() != digest:
raise RuntimeError(f"D4 precondition: {name} hash mismatch")
try:
member_sources[name] = data.decode("utf-8")
except UnicodeDecodeError as error:
raise RuntimeError(f"D4 precondition: pinned {name} is not UTF-8") from error
member_bytes[name] = data
closure_import_guard(member_sources)
pinned = root / "pinned"
pinned.mkdir(mode=0o700)
paths = {name: pinned / name for name, *_ in members}
for name, path in paths.items():
write_pinned_file(path, member_bytes[name])
return PinnedClosure(
launcher=paths["launch-runtime.py"],
generation=paths["lease_generation.py"],
broker=paths["p3_generation_broker.py"],
)
def gated_launcher_precondition(
root: Path, socket_path: Path, environment: dict[str, str]
) -> PinnedClosure:
"""Verify and materialize the full WI-3/probe closure before execution."""
if environment.get("MOSAIC_LEASE_BROKER_SOCKET") != str(socket_path):
raise RuntimeError("D4 precondition: lease broker socket is not this fixture")
if environment.get("MOSAIC_LEASE_GENERATION_FILE"):
raise RuntimeError("D4 precondition: inherited generation file is forbidden")
fixture_path_vars = (
"HOME",
"XDG_CONFIG_HOME",
"XDG_CACHE_HOME",
"XDG_STATE_HOME",
"XDG_RUNTIME_DIR",
"TMPDIR",
"D4_PI_LOG",
"MOSAIC_AGENT_WORKDIR",
"MOSAIC_HEARTBEAT_RUN_DIR",
"MOSAIC_HOME",
)
if any(
not (value := environment.get(name)) or not Path(value).is_relative_to(root)
for name in fixture_path_vars
):
raise RuntimeError("D4 precondition: child write path escapes fixture root")
if socket_path.parent != root or root.parent != Path(tempfile.gettempdir()):
raise RuntimeError("D4 precondition: fixture socket is outside this run's temporary root")
gated_root = resolve_gated_wi_root()
closure = materialize_closure(root, gated_root, repository_root())
launcher_source = closure.launcher.read_text(encoding="utf-8")
generation_source = closure.generation.read_text(encoding="utf-8")
# Exact hashes in materialize_closure are the trust anchor. These marker
# checks are belt-and-suspenders diagnostics only.
behavior_markers = (
'"action": "register_anchor"',
"initialize_generation: Callable[[Path, int], None] = initialize_runtime_generation",
"execute(command[0], command, environment)",
'source_environment["MOSAIC_LEASE_BROKER_SOCKET"]',
'socket_path.parent / f"generation-{session_id}.state"',
'environment["MOSAIC_LEASE_GENERATION_FILE"]',
)
if not all(marker in launcher_source for marker in behavior_markers) or not (
"def read_runtime_generation" in generation_source
and "def bump_runtime_generation" in generation_source
):
raise RuntimeError("D4 precondition: pinned launcher lacks file-generation markers")
return closure
def reject_pinned_bytecode(pinned_directory: Path) -> None:
cache_directory = pinned_directory / "__pycache__"
if cache_directory.exists() or any(pinned_directory.rglob("*.pyc")):
raise RuntimeError("D4 precondition: pinned bytecode cache is forbidden")
def launch_verified_pi(
launcher: Path,
workspace: Path,
sessions: Path,
extension: Path,
environment: dict[str, str],
) -> PiRpc:
command = [
sys.executable,
# -s preserves sys.path[0]=pinned/ for the launcher's sibling helper.
"-s",
"-S",
"-B",
str(launcher),
"--runtime",
"pi",
"--",
"pi",
"--mode",
"rpc",
"--session-dir",
str(sessions),
"--no-extensions",
"--no-context-files",
"--no-prompt-templates",
"--model",
"openai-codex/gpt-5.6-sol",
"--thinking",
"medium",
"--extension",
str(extension),
]
reject_pinned_bytecode(launcher.parent)
# This is deliberately the statement immediately before Popen (inside
# PiRpc): the fixture-pinned launcher bytes are re-hashed then executed.
if hashlib.sha256(launcher.read_bytes()).hexdigest() != GATED_LAUNCHER_SHA256:
raise RuntimeError("D4 precondition: adjacent launcher hash mismatch")
return PiRpc(command, workspace, environment)
def launch_verified_broker(
broker_path: Path,
generation_path: Path,
socket_path: Path,
log_path: Path,
environment: dict[str, str],
) -> subprocess.Popen[str]:
command = [
sys.executable,
"-I",
"-S",
"-B",
str(broker_path),
"--socket",
str(socket_path),
"--log",
str(log_path),
"--generation-module",
str(generation_path),
]
reject_pinned_bytecode(broker_path.parent)
if hashlib.sha256(broker_path.read_bytes()).hexdigest() != GATED_BROKER_SHA256:
raise RuntimeError("D4 precondition: pinned broker hash mismatch")
# The final helper re-hash is immediately adjacent to the broker Popen.
if hashlib.sha256(generation_path.read_bytes()).hexdigest() != GATED_GENERATION_SHA256:
raise RuntimeError("D4 precondition: pinned helper hash mismatch")
return subprocess.Popen(
command,
env=environment,
text=True,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
)
def assert_d4(records: list[dict[str, Any]]) -> dict[str, object]:
def record_where(description: str, candidates: list[dict[str, Any]]) -> dict[str, Any]:
if not candidates:
raise AssertionError(f"missing D4 evidence record: {description}")
return candidates[0]
fixture_listen = record_where(
"fixture listen", [r for r in records if r.get("event") == "listen"]
)
fixture_root = Path(fixture_listen["socket"]).parent
lifecycle = [record for record in records if record.get("event") == "runtime_generation_bump"]
state_bumps = [record for record in records if record.get("event") == "generation_state_bumped"]
promotion = record_where(
"fixture promotion", [r for r in records if r.get("event") == "probe_lease_promoted"]
)
launcher_registration = record_where(
"lease anchor", [r for r in records if r.get("event") == "lease_anchor_registered"]
)
reload_revoke = record_where(
"reload shutdown",
[
r
for r in lifecycle
if r.get("reason") == "reload" and r.get("phase") == "shutdown"
],
)
reload_start = record_where(
"reload start",
[
r
for r in lifecycle
if r.get("reason") == "reload" and r.get("phase") == "start"
],
)
authorization = [
record for record in records if record.get("event") == "generation_authorization"
]
current_generation = reload_start["new_generation"]
current_authorization = record_where(
"current-generation authorization",
[r for r in authorization if r.get("requested_generation") == current_generation],
)
superseded_authorization = record_where(
"superseded-generation authorization",
[r for r in authorization if r.get("requested_generation") == current_generation - 1],
)
identities = {
(record["peercred"]["pid"], record["starttime_ticks"])
for record in [*lifecycle, *state_bumps, promotion, launcher_registration, *authorization]
}
generations = [record["new_generation"] for record in lifecycle]
file_records = [*lifecycle, *state_bumps, promotion, *authorization]
observed_reasons = {record.get("reason") for record in lifecycle}
checks = {
"same_pid_starttime": len(identities) == 1,
"strictly_increasing_generation": all(
previous < current for previous, current in zip(generations, generations[1:])
),
"state_file_drives_lifecycle": [record["generation"] for record in state_bumps]
== generations[1:],
"state_file_source": all(
record.get("generation_source") == "state-file" for record in file_records
),
"state_file_in_fixture_root": all(
Path(record["generation_file"]).parent == fixture_root for record in file_records
)
and Path(launcher_registration["generation_file"]).parent == fixture_root,
"all_lifecycle_boundaries": {"startup", "reload", "fork", "new", "resume"}
<= observed_reasons,
"lease_anchor_fixture": launcher_registration.get("session_id_shape") == "hex-256",
"verified_revoked_on_reload": reload_revoke.get("prior_lease") == "VERIFIED"
and reload_revoke.get("prior_lease_revoked") is True,
"new_generation_unverified": current_authorization.get("code") == "MUTATOR_UNVERIFIED",
"prior_generation_stale": superseded_authorization.get("code") == "STALE_GENERATION",
}
failed = [name for name, passed in checks.items() if not passed]
if failed:
raise AssertionError(f"D4 checks failed: {', '.join(failed)}")
passed = all(checks.values())
if not passed:
raise AssertionError("D4 PASS derivation failed")
return {
"machine_assertions": "PASS" if passed else "FAIL",
"checks": checks,
"same_pid_starttime": next(iter(identities)),
"generations": generations,
"reload_revoke_verified": checks["verified_revoked_on_reload"],
"lease_anchor_fixture": checks["lease_anchor_fixture"],
"file_backed_generation": checks["state_file_drives_lifecycle"],
"new_generation_code": current_authorization["code"],
"superseded_generation_code": superseded_authorization["code"],
}
def isolated_environment(
root: Path, index: int, workspace: Path, socket_path: Path, pi_log: Path
) -> dict[str, str]:
"""Build a write-confined child environment; no inherited path variable survives."""
fixture_home = root / "home"
fixture_config = root / "config"
fixture_cache = root / "cache"
fixture_state = root / "state"
fixture_runtime = root / "runtime"
fixture_tmp = root / "tmp"
fixture_heartbeat = root / "heartbeat"
fixture_mosaic_home = root / "mosaic-home"
for directory in (
fixture_home,
fixture_config,
fixture_cache,
fixture_state,
fixture_runtime,
fixture_tmp,
fixture_heartbeat,
fixture_mosaic_home,
):
directory.mkdir(mode=0o700)
# Authentication/settings are copied into fixture HOME so Pi never writes
# under the operator's HOME. They are not emitted or modified in place.
source_agent = Path.home() / ".pi" / "agent"
target_agent = fixture_home / ".pi" / "agent"
target_agent.mkdir(parents=True, mode=0o700)
for name in ("settings.json", "auth.json", "bin/fd"):
source = source_agent / name
target = target_agent / name
if source.is_file():
target.parent.mkdir(parents=True, mode=0o700, exist_ok=True)
shutil.copy2(source, target)
environment = {
"HOME": str(fixture_home),
"XDG_CONFIG_HOME": str(fixture_config),
"XDG_CACHE_HOME": str(fixture_cache),
"XDG_STATE_HOME": str(fixture_state),
"XDG_RUNTIME_DIR": str(fixture_runtime),
"TMPDIR": str(fixture_tmp),
"PATH": os.environ.get("PATH", ""),
"LANG": os.environ.get("LANG", "C.UTF-8"),
"TERM": os.environ.get("TERM", "dumb"),
"D4_GENERATION_SOCKET": str(socket_path),
"MOSAIC_LEASE_BROKER_SOCKET": str(socket_path),
"D4_PI_LOG": str(pi_log),
"MOSAIC_AGENT_NAME": f"d4-fixture-{index}",
"MOSAIC_AGENT_WORKDIR": str(workspace),
"MOSAIC_HEARTBEAT_RUN_DIR": str(fixture_heartbeat),
"MOSAIC_HOME": str(fixture_mosaic_home),
"MOSAIC_PI_FORCE_SKILLS": "",
"PI_SKIP_VERSION_CHECK": "1",
"PYTHONDONTWRITEBYTECODE": "1",
"PYTHONNOUSERSITE": "1",
}
if "PI_CODING_AGENT" in os.environ:
environment["PI_CODING_AGENT"] = os.environ["PI_CODING_AGENT"]
return environment
def scrub_fixture_credentials(root: Path) -> None:
"""Remove the copied Pi credential/config subtree before retaining evidence."""
copied_agent = root / "home" / ".pi" / "agent"
if copied_agent.exists():
shutil.rmtree(copied_agent)
if copied_agent.exists():
raise RuntimeError("D4 credential scrub failed")
def run_once(index: int) -> Path:
root = Path(tempfile.mkdtemp(prefix=f"gate0-d4-{index}-"))
workspace = root / "workspace"
sessions = root / "sessions"
workspace.mkdir(mode=0o700)
sessions.mkdir(mode=0o700)
socket_path = root / "generation.sock"
generation_log = root / "generation.jsonl"
pi_log = root / "pi.jsonl"
extension = root / "d4_extension.ts"
write_extension(extension)
broker: subprocess.Popen[str] | None = None
pi: PiRpc | None = None
try:
environment = isolated_environment(root, index, workspace, socket_path, pi_log)
# Must run before the fixture broker or Pi process is launched. It proves
# the launcher registers before exec and can only read this fixture socket.
closure = gated_launcher_precondition(root, socket_path, environment)
broker = launch_verified_broker(
closure.broker, closure.generation, socket_path, generation_log, environment
)
wait_path(socket_path)
pi = launch_verified_pi(closure.launcher, workspace, sessions, extension, environment)
pi.send({"id": "state", "type": "get_state"})
state = pi.response("state")
original_session = state["data"]["sessionFile"]
pi.prompt_and_settle(
"fixture-promote",
"Call d4_fixture_promote exactly once, then stop.",
)
pi.send({"id": "reload", "type": "prompt", "message": "/d4-reload"})
reload_response = pi.response("reload")
if not reload_response.get("success"):
raise RuntimeError(f"reload failed: {reload_response}")
for request_id, request_payload in [
("clone", {"id": "clone", "type": "clone"}),
("new", {"id": "new", "type": "new_session"}),
(
"resume",
{"id": "resume", "type": "switch_session", "sessionPath": original_session},
),
]:
pi.send(request_payload)
response = pi.response(request_id)
if not response.get("success") or response.get("data", {}).get("cancelled"):
raise RuntimeError(f"{request_id} failed: {response}")
results = assert_d4(jsonl(generation_log))
verdict = results.get("machine_assertions")
if verdict != "PASS":
raise RuntimeError(f"D4 checks did not derive PASS: {verdict}")
(root / "machine-assertions.json").write_text(
json.dumps(results, sort_keys=True, indent=2) + "\n",
encoding="utf-8",
)
print(f"run={index} evidence_dir={root}")
print(f"machine_assertions={verdict}")
print(json.dumps(results, sort_keys=True))
except Exception as error:
(root / "machine-assertions.json").write_text(
json.dumps({"error": f"{type(error).__name__}: {error}"}, sort_keys=True, indent=2)
+ "\n",
encoding="utf-8",
)
print(f"run={index} evidence_dir={root}")
print("machine_assertions=FAIL")
print(f"error={type(error).__name__}: {error}")
raise
finally:
try:
try:
if pi is not None:
pi.close()
finally:
if broker is not None:
try:
request(socket_path, {"action": "shutdown-broker"})
except OSError:
pass
try:
broker.wait(timeout=5)
except subprocess.TimeoutExpired:
broker.kill()
broker.wait()
finally:
scrub_fixture_credentials(root)
return root
def main() -> None:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--runs", type=int, default=3, choices=(3,))
args = parser.parse_args()
roots: list[Path] = []
for index in range(1, args.runs + 1):
roots.append(run_once(index))
print("d4_isolation_runs=" + ",".join(str(root) for root in roots))
if __name__ == "__main__":
main()
@@ -1,215 +0,0 @@
#!/usr/bin/env python3
"""P3 broker prototype: peercred-keyed runtime_generation and lease revocation."""
from __future__ import annotations
import argparse
import importlib.util
import json
import os
import secrets
import socket
import struct
from collections.abc import Callable, Mapping
from pathlib import Path
from typing import Any
def proc_starttime(pid: int) -> int:
text = Path(f"/proc/{pid}/stat").read_text()
close = text.rfind(")")
return int(text[close + 2 :].split()[19])
def emit(log: Path, value: dict[str, Any]) -> None:
with log.open("a", encoding="utf-8") as out:
out.write(json.dumps(value, sort_keys=True) + "\n")
def load_generation_functions(
path: Path,
) -> tuple[Callable[[Mapping[str, str]], int], Callable[[Mapping[str, str]], int]]:
spec = importlib.util.spec_from_file_location("d4_lease_generation", path)
if spec is None or spec.loader is None:
raise ValueError("generation module is unavailable")
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
reader = getattr(module, "read_runtime_generation", None)
bumper = getattr(module, "bump_runtime_generation", None)
if not callable(reader) or not callable(bumper):
raise ValueError("generation module has no read/bump functions")
return reader, bumper
def main() -> None:
parser = argparse.ArgumentParser()
parser.add_argument("--socket", required=True)
parser.add_argument("--log", required=True)
parser.add_argument("--generation-module", required=True, type=Path)
ns = parser.parse_args()
socket_path = Path(ns.socket)
log_path = Path(ns.log)
read_runtime_generation, bump_runtime_generation = load_generation_functions(
ns.generation_module
)
socket_path.parent.mkdir(parents=True, exist_ok=True)
os.chmod(socket_path.parent, 0o700)
socket_path.unlink(missing_ok=True)
log_path.unlink(missing_ok=True)
server = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
server.bind(str(socket_path))
os.chmod(socket_path, 0o600)
server.listen(8)
generations: dict[tuple[int, int], int] = {}
lease_state: dict[tuple[int, int], str] = {}
# The gated launcher registers its own exec-preserved PID here. This is
# deliberately volatile fixture state; nothing is written outside root.
launcher_sessions: dict[tuple[int, int], str] = {}
generation_files: dict[tuple[int, int], Path] = {}
def generation_environment(identity: tuple[int, int]) -> dict[str, str]:
state_path = generation_files.get(identity)
if state_path is None or state_path.parent != socket_path.parent:
raise ValueError("generation file is outside the fixture root")
return {"MOSAIC_LEASE_GENERATION_FILE": str(state_path)}
def file_generation(identity: tuple[int, int]) -> int:
return read_runtime_generation(generation_environment(identity))
emit(log_path, {"event": "listen", "pid": os.getpid(), "socket": str(socket_path)})
while True:
conn, _ = server.accept()
with conn:
raw = conn.getsockopt(socket.SOL_SOCKET, socket.SO_PEERCRED, 12)
pid, uid, gid = struct.unpack("3i", raw)
starttime = proc_starttime(pid)
request = json.loads(conn.makefile("r", encoding="utf-8").readline())
if request.get("action") == "shutdown-broker":
conn.sendall(b'{"ok":true}\n')
break
identity = (pid, starttime)
if request.get("action") == "register_anchor":
generation = request.get("runtime_generation")
if type(generation) is not int or generation < 0:
conn.sendall(b'{"ok":false,"code":"INVALID_GENERATION"}\n')
continue
session_id = launcher_sessions.setdefault(identity, secrets.token_hex(32))
generation_file = socket_path.parent / f"generation-{session_id}.state"
generation_files[identity] = generation_file
record = {
"event": "lease_anchor_registered",
"peercred": {"pid": pid, "uid": uid, "gid": gid},
"starttime_ticks": starttime,
"runtime_generation": generation,
"session_id_shape": "hex-256",
"generation_file": str(generation_file),
}
emit(log_path, record)
reply = {
"ok": True,
"session_id": session_id,
"peer": {"pid": pid, "uid": uid, "gid": gid, "starttime": str(starttime)},
}
conn.sendall((json.dumps(reply, sort_keys=True) + "\n").encode())
continue
# The D4 extension requests this at each post-start lifecycle
# boundary; the exact WI-3 helper mutates the launcher-created file.
if request.get("action") == "bump-generation":
generation = bump_runtime_generation(generation_environment(identity))
record = {
"event": "generation_state_bumped",
"peercred": {"pid": pid, "uid": uid, "gid": gid},
"starttime_ticks": starttime,
"generation": generation,
"generation_file": str(generation_files[identity]),
"generation_source": "state-file",
}
emit(log_path, record)
conn.sendall((json.dumps(record, sort_keys=True) + "\n").encode())
continue
if request.get("action") == "promote-probe":
generation = file_generation(identity)
lease_state[identity] = "VERIFIED"
record = {
"event": "probe_lease_promoted",
"peercred": {"pid": pid, "uid": uid, "gid": gid},
"starttime_ticks": starttime,
"generation": generation,
"generation_file": str(generation_files[identity]),
"generation_source": "state-file",
"new_lease_state": "VERIFIED",
}
emit(log_path, record)
conn.sendall((json.dumps(record, sort_keys=True) + "\n").encode())
continue
# D4 fixture-only authorization observation. It exposes the broker's
# current versus superseded generation disposition without changing it.
if request.get("action") == "authorize-probe":
generation = request.get("generation")
if type(generation) is not int or generation < 0:
conn.sendall(b'{"ok":false,"code":"INVALID_GENERATION"}\n')
continue
current_generation = file_generation(identity)
current_lease = lease_state.get(identity, "NONE")
if generation < current_generation:
code = "STALE_GENERATION"
elif generation > current_generation:
code = "FUTURE_GENERATION"
elif current_lease != "VERIFIED":
code = "MUTATOR_UNVERIFIED"
else:
code = "ALLOW"
record = {
"event": "generation_authorization",
"peercred": {"pid": pid, "uid": uid, "gid": gid},
"starttime_ticks": starttime,
"requested_generation": generation,
"current_generation": current_generation,
"generation_file": str(generation_files[identity]),
"generation_source": "state-file",
"lease_state": current_lease,
"ok": code == "ALLOW",
"code": code,
}
emit(log_path, record)
conn.sendall((json.dumps(record, sort_keys=True) + "\n").encode())
continue
if request.get("action") != "lifecycle":
conn.sendall(b'{"ok":false,"reason":"invalid-action"}\n')
continue
old_generation = generations.get(identity, 0)
old_lease = lease_state.get(identity, "NONE")
new_generation = file_generation(identity)
if new_generation <= old_generation:
conn.sendall(b'{"ok":false,"code":"NON_MONOTONIC_STATE_FILE"}\n')
continue
generations[identity] = new_generation
# Every lifecycle boundary revokes first. A start establishes a new
# UNVERIFIED incarnation; it never inherits prior VERIFIED state.
lease_state[identity] = "UNVERIFIED" if request.get("phase") == "start" else "REVOKED"
record = {
"event": "runtime_generation_bump",
"peercred": {"pid": pid, "uid": uid, "gid": gid},
"starttime_ticks": starttime,
"phase": request.get("phase"),
"reason": request.get("reason"),
"old_generation": old_generation,
"new_generation": new_generation,
"generation_file": str(generation_files[identity]),
"generation_source": "state-file",
"prior_lease": old_lease,
"prior_lease_revoked": True,
"new_lease_state": lease_state[identity],
}
emit(log_path, record)
conn.sendall((json.dumps(record, sort_keys=True) + "\n").encode())
server.close()
socket_path.unlink(missing_ok=True)
if __name__ == "__main__":
main()
@@ -1,97 +0,0 @@
#!/usr/bin/env python3
"""Gate0 P4: exercise Linux SO_PEERCRED and correlate it to /proc."""
from __future__ import annotations
import json
import os
import socket
import stat
import tempfile
from pathlib import Path
def proc_identity(pid: int) -> dict[str, int | str]:
stat_text = Path(f"/proc/{pid}/stat").read_text()
close = stat_text.rfind(")")
fields = stat_text[close + 2 :].split()
# fields[0] is field 3 (state); ppid is field 4 and starttime is field 22.
return {
"pid": pid,
"ppid": int(fields[1]),
"starttime_ticks": int(fields[19]),
"uid": int(Path(f"/proc/{pid}/status").read_text().split("Uid:", 1)[1].split()[0]),
"exe": os.readlink(f"/proc/{pid}/exe"),
}
def main() -> None:
with tempfile.TemporaryDirectory(prefix="gate0-p4-") as tmp:
root = Path(tmp)
os.chmod(root, 0o700)
socket_path = root / "broker.sock"
server = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
server.bind(str(socket_path))
os.chmod(socket_path, 0o600)
server.listen(1)
child = os.fork()
if child == 0:
client = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
client.connect(str(socket_path))
identity = proc_identity(os.getpid())
client.sendall((json.dumps(identity, sort_keys=True) + "\n").encode())
# Keep /proc/<pid> alive until the server has correlated peercred.
if client.recv(2) != b"OK":
os._exit(2)
client.close()
os._exit(0)
conn, _ = server.accept()
raw = conn.getsockopt(socket.SOL_SOCKET, socket.SO_PEERCRED, 12)
peer_pid = int.from_bytes(raw[0:4], byteorder="little", signed=True)
peer_uid = int.from_bytes(raw[4:8], byteorder="little", signed=True)
peer_gid = int.from_bytes(raw[8:12], byteorder="little", signed=True)
claimed = json.loads(conn.makefile("r", encoding="utf-8").readline())
observed = proc_identity(peer_pid)
conn.sendall(b"OK")
_, status = os.waitpid(child, 0)
root_mode = stat.S_IMODE(root.stat().st_mode)
socket_mode = stat.S_IMODE(socket_path.stat().st_mode)
if not (
peer_pid == claimed["pid"] == observed["pid"]
and peer_uid == claimed["uid"] == observed["uid"]
and claimed["starttime_ticks"] == observed["starttime_ticks"]
and root_mode == 0o700
and socket_mode == 0o600
and os.waitstatus_to_exitcode(status) == 0
):
raise AssertionError("SO_PEERCRED, /proc identity, or socket-mode correlation failed")
print("machine_assertions=PASS")
print(f"server_pid={os.getpid()} server_uid={os.getuid()} server_gid={os.getgid()}")
print(f"socket_path={socket_path}")
print(f"directory_mode={root_mode:04o} socket_mode={socket_mode:04o}")
print(f"SO_PEERCRED pid={peer_pid} uid={peer_uid} gid={peer_gid}")
print("client_claim=" + json.dumps(claimed, sort_keys=True))
print("proc_observed=" + json.dumps(observed, sort_keys=True))
print(f"pid_match={peer_pid == claimed['pid'] == observed['pid']}")
print(f"uid_match={peer_uid == claimed['uid'] == observed['uid']}")
print(
"starttime_match="
+ str(claimed["starttime_ticks"] == observed["starttime_ticks"])
)
print(f"client_exit_status={os.waitstatus_to_exitcode(status)}")
print("same_principal_socket=true")
print(
"posture=0700 parent + 0600 socket excludes other UIDs, but does not prevent "
"the same UID from unlinking/rebinding; distinct-principal system service remains "
"required for a claim stronger than T-C against same-UID counterfeit replacement"
)
conn.close()
server.close()
if __name__ == "__main__":
main()
@@ -0,0 +1,228 @@
#!/usr/bin/env python3
"""P5 Gate0 replay probe; BUILT ONLY, execution is Mos-gated.
Run only under fresh-executor authorization:
python3 -I -S -B docs/compaction-refresh/probes/p5_receipt_replay.py
Each of the default three isolated runs launches the shipped lease-broker daemon
in a distinct private temporary directory. This driver never changes broker
state directly and does not replace the promote gate: every transition is sent
over the daemon's real Unix socket. It proves the shipped order is
PENDING_DELIVERY -> observe/evidence commit -> consume -> VERIFIED and that a
consumed challenge cannot be replayed or reopen/renew its lease.
"""
from __future__ import annotations
import argparse
import base64
import importlib.util
import json
import os
import shutil
import socket
import subprocess
import sys
import tempfile
import time
from pathlib import Path
HERE = Path(__file__).resolve().parent
REPOSITORY = HERE.parents[2]
TOOLS = REPOSITORY / "packages/mosaic/framework/tools/lease-broker"
DAEMON = TOOLS / "daemon.py"
FRAGMENTS = TOOLS / "normative_fragments.py"
def load_shipped_fragments():
if not FRAGMENTS.is_file():
raise RuntimeError(f"shipped normative construction missing: {FRAGMENTS}")
spec = importlib.util.spec_from_file_location("p5_shipped_normative_fragments", FRAGMENTS)
if spec is None or spec.loader is None:
raise RuntimeError("unable to load shipped normative construction")
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
return module
def request(socket_path: Path, value: dict[str, object]) -> dict[str, object]:
with socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) as connection:
connection.settimeout(3.0)
connection.connect(str(socket_path))
connection.sendall((json.dumps(value, separators=(",", ":")) + "\n").encode())
connection.shutdown(socket.SHUT_WR)
response = bytearray()
while True:
chunk = connection.recv(4096)
if not chunk:
break
response.extend(chunk)
if not response.endswith(b"\n") or response.count(b"\n") != 1:
raise AssertionError(f"unframed broker reply: {bytes(response)!r}")
parsed = json.loads(response[:-1])
if not isinstance(parsed, dict):
raise AssertionError(f"non-object broker reply: {parsed!r}")
return parsed
def wait_ready(process: subprocess.Popen[str], socket_path: Path) -> None:
deadline = time.monotonic() + 5.0
while time.monotonic() < deadline:
if socket_path.exists():
return
if process.poll() is not None:
output = process.stdout.read() if process.stdout is not None else ""
raise RuntimeError(f"shipped daemon exited before READY: {output}")
time.sleep(0.02)
raise TimeoutError("shipped daemon did not create private probe socket")
def expect_refused(reply: dict[str, object], code: str) -> None:
if reply != {"ok": False, "code": code}:
raise AssertionError(f"expected refusal {code}, got {reply!r}")
def run_once(index: int) -> str:
fragments = load_shipped_fragments()
root = Path(tempfile.mkdtemp(prefix=f"mosaic-p5-replay-{index}-"))
os.chmod(root, 0o700)
socket_path = root / "broker.sock"
state_path = root / "state.json"
observer_path = root / "test-observer.json"
process = subprocess.Popen(
[
sys.executable, "-I", "-S", "-B", str(DAEMON), "--socket", str(socket_path),
"--state", str(state_path), "--test-observer-file", str(observer_path),
],
stdin=subprocess.DEVNULL,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
text=True,
)
try:
wait_ready(process, socket_path)
registered = request(socket_path, {"action": "register_anchor", "runtime_generation": 1})
if registered.get("ok") is not True or not isinstance(registered.get("session_id"), str):
raise AssertionError(f"registration failed: {registered!r}")
session_id = registered["session_id"]
construction = fragments.build_payload(
manifest_version=1,
generator_version="p5-replay-probe",
fragments=[
fragments.NormativeFragment(
"authority/probe",
b"P5 shipped transition driver\n",
"63537df1a6cb0d80195a96757ab11d629e5b5e1f23be167218b84cb195b1c1d6",
),
],
)
if construction.injectionDecision != "ACCEPTED" or not construction.promotion:
raise AssertionError("shipped normative construction refused P5 fixture")
binding = {
"compaction_epoch": index,
"request_epoch": index + 100,
"h_source": construction.h_source,
"h_payload": construction.h_payload,
"schema_version": 1,
}
construction_request = {
"manifest_version": 1,
"generator_version": "p5-replay-probe",
"fragments": [{
"source_id": "authority/probe",
"content_base64": base64.b64encode(b"P5 shipped transition driver\n").decode("ascii"),
"expected_sha256": "63537df1a6cb0d80195a96757ab11d629e5b5e1f23be167218b84cb195b1c1d6",
}],
}
pending = request(socket_path, {
"action": "begin_verification",
"session_id": session_id,
"runtime_generation": 1,
"runtime": "pi",
"binding": binding,
"construction": construction_request,
})
if pending.get("ok") is not True or pending.get("state") != "PENDING_VERIFICATION":
raise AssertionError(f"shipped pending-delivery transition failed: {pending!r}")
challenge = pending.get("receipt_challenge")
receipt = pending.get("receipt")
if not isinstance(challenge, str) or not isinstance(receipt, str):
raise AssertionError(f"shipped broker did not mint a receipt challenge: {pending!r}")
# Promotion before observation/evidence/consumption is forbidden.
expect_refused(request(socket_path, {
"action": "promote_lease",
"session_id": session_id,
"runtime_generation": 1,
"receipt_challenge": challenge,
}), "INVALID_LEASE_TRANSITION")
observer_path.write_text(json.dumps({
"session_id": session_id,
"runtime_generation": 1,
"latest_assistant_message": receipt,
}), encoding="utf-8")
os.chmod(observer_path, 0o600)
observed = request(socket_path, {
"action": "observe_receipt",
"session_id": session_id,
"runtime_generation": 1,
"receipt_challenge": challenge,
})
if observed.get("ok") is not True or observed.get("state") != "PENDING_PROMOTION":
raise AssertionError(f"shipped evidence transition failed: {observed!r}")
durable = json.loads(state_path.read_text(encoding="utf-8"))
evidence = durable["tokens"][challenge].get("evidence")
if not isinstance(evidence, dict) or not isinstance(evidence.get("h_latest_assistant"), str):
raise AssertionError("shipped receipt evidence was not committed before consume/promote")
promoted = request(socket_path, {
"action": "promote_lease",
"session_id": session_id,
"runtime_generation": 1,
"receipt_challenge": challenge,
})
if promoted.get("ok") is not True or promoted.get("state") != "VERIFIED":
raise AssertionError(f"shipped consume-before-promote transition failed: {promoted!r}")
# T25/T28: the actual consumed challenge, re-presented through the
# shipped daemon, can neither be observed again nor re-promote/reopen.
expect_refused(request(socket_path, {
"action": "observe_receipt",
"session_id": session_id,
"runtime_generation": 1,
"receipt_challenge": challenge,
}), "RECEIPT_REPLAY")
expect_refused(request(socket_path, {
"action": "promote_lease",
"session_id": session_id,
"runtime_generation": 1,
"receipt_challenge": challenge,
}), "RECEIPT_REPLAY")
return challenge
finally:
if process.poll() is None:
process.terminate()
try:
process.wait(timeout=3.0)
except subprocess.TimeoutExpired:
process.kill()
process.wait()
shutil.rmtree(root, ignore_errors=True)
def main() -> None:
parser = argparse.ArgumentParser()
parser.add_argument("--runs", type=int, default=3)
arguments = parser.parse_args()
if arguments.runs != 3:
raise SystemExit("P5 requires exactly three isolated runs")
challenges = [run_once(index) for index in range(arguments.runs)]
if len(set(challenges)) != arguments.runs:
raise AssertionError("separate shipped cycles did not mint unique challenges")
print("P5 receipt replay probe PASS: 3 isolated shipped-daemon runs")
if __name__ == "__main__":
main()
@@ -1,54 +0,0 @@
#!/usr/bin/env python3
"""Claude SessionStart additionalContext producer for P6 observation."""
from __future__ import annotations
import hashlib
import json
import os
import sys
from pathlib import Path
BLOCK = "\n".join(
[
"GATE0_CLAUDE_ATOMIC_BEGIN",
"segment-01=alpha-2d11",
"segment-02=middle-8e22",
"segment-03=omega-4f33",
"GATE0_CLAUDE_ATOMIC_END",
]
)
def starttime(pid: int) -> int:
text = Path(f"/proc/{pid}/stat").read_text()
return int(text[text.rfind(")") + 2 :].split()[19])
def main() -> None:
hook_input = json.load(sys.stdin)
log = Path(os.environ["GATE0_CLAUDE_HOOK_LOG"])
record = {
"hook_event_name": hook_input.get("hook_event_name"),
"pid": os.getpid(),
"ppid": os.getppid(),
"starttime_ticks": starttime(os.getpid()),
"block_length": len(BLOCK.encode()),
"block_sha256": hashlib.sha256(BLOCK.encode()).hexdigest(),
"emission": "one hookSpecificOutput.additionalContext string field",
}
log.write_text(json.dumps(record, sort_keys=True) + "\n")
print(
json.dumps(
{
"hookSpecificOutput": {
"hookEventName": "SessionStart",
"additionalContext": BLOCK,
}
}
)
)
if __name__ == "__main__":
main()
@@ -1,145 +0,0 @@
#!/usr/bin/env python3
"""Run real Claude 2.1.x through `mosaic yolo` for P6 observation."""
from __future__ import annotations
import hashlib
import json
import os
import subprocess
import sys
import tempfile
from pathlib import Path
from typing import Any
HERE = Path(__file__).resolve().parent
BLOCK = "\n".join(
[
"GATE0_CLAUDE_ATOMIC_BEGIN",
"segment-01=alpha-2d11",
"segment-02=middle-8e22",
"segment-03=omega-4f33",
"GATE0_CLAUDE_ATOMIC_END",
]
)
def strings(value: Any):
if isinstance(value, str):
yield value
elif isinstance(value, list):
for item in value:
yield from strings(item)
elif isinstance(value, dict):
for item in value.values():
yield from strings(item)
def main() -> None:
with tempfile.TemporaryDirectory(prefix="gate0-p6-claude-") as temp:
root = Path(temp)
workspace = root / "workspace"
workspace.mkdir()
settings = root / "settings.json"
hook_log = root / "hook.jsonl"
settings.write_text(
json.dumps(
{
"hooks": {
"SessionStart": [
{
"hooks": [
{
"type": "command",
"command": f'python3 "{HERE / "p6_claude_hook.py"}"',
"timeout": 20,
}
]
}
]
}
}
)
)
env = os.environ.copy()
env["GATE0_CLAUDE_HOOK_LOG"] = str(hook_log)
command = [
"mosaic",
"yolo",
"claude",
"--settings",
str(settings),
"--model",
"haiku",
"--print",
"--output-format",
"stream-json",
"--verbose",
"--include-hook-events",
"--max-budget-usd",
"0.10",
"Return only the exact full GATE0_CLAUDE_ATOMIC_BEGIN through GATE0_CLAUDE_ATOMIC_END block injected by SessionStart, with no code fence or commentary.",
]
result = subprocess.run(
command,
cwd=workspace,
env=env,
stdin=subprocess.DEVNULL,
text=True,
capture_output=True,
timeout=150,
check=False,
)
events: list[dict[str, Any]] = []
for line in result.stdout.splitlines():
try:
events.append(json.loads(line))
except json.JSONDecodeError:
continue
hook_events = [
event
for event in events
if event.get("type") == "system"
and event.get("subtype") in {"hook_started", "hook_response"}
]
full_matches = [text for event in events for text in strings(event) if BLOCK in text]
exact_matches = [text for event in events for text in strings(event) if text == BLOCK]
assistant_texts: list[str] = []
for event in events:
if event.get("type") != "assistant":
continue
for text in strings(event.get("message", {})):
if "GATE0_CLAUDE_ATOMIC_BEGIN" in text:
assistant_texts.append(text)
if result.returncode != 0:
raise AssertionError(f"Claude probe exited {result.returncode}")
if not any(event.get("subtype") == "hook_response" and event.get("outcome") == "success" for event in hook_events):
raise AssertionError("Claude SessionStart hook did not complete successfully")
if BLOCK not in exact_matches:
raise AssertionError("Claude did not return an exact full-block field")
print("$ python3 docs/compaction-refresh/probes/p6_claude_run.py")
print("machine_assertions=PASS")
print("command=mosaic yolo claude --settings <isolated> --model haiku --print --output-format stream-json --verbose --include-hook-events <prompt>")
print("claude_version=" + subprocess.check_output(["claude", "--version"], text=True).strip())
print("mosaic_version=" + subprocess.check_output(["mosaic", "--version"], text=True).strip())
print(f"exit_code={result.returncode}")
print("hook_process_log=" + hook_log.read_text().strip())
for event in hook_events:
print("hook_stream_event=" + json.dumps(event, sort_keys=True))
print(f"block_length={len(BLOCK.encode())}")
print(f"block_sha256={hashlib.sha256(BLOCK.encode()).hexdigest()}")
print(f"stream_fields_containing_full_block={len(full_matches)}")
print(f"stream_fields_exactly_equal_block={len(exact_matches)}")
for text in assistant_texts:
print(f"assistant_copy_length={len(text.encode())}")
print(f"assistant_copy_sha256={hashlib.sha256(text.encode()).hexdigest()}")
print(f"assistant_copy_exact={text == BLOCK}")
print("assistant_copy=" + json.dumps(text))
if result.stderr.strip():
print("stderr_excerpt=" + json.dumps(result.stderr.splitlines()[:10]))
if __name__ == "__main__":
main()
@@ -0,0 +1,255 @@
#!/usr/bin/env python3
"""P6 constrained-recovery probe; BUILT ONLY and Mos-gated.
DO NOT self-fire. Under Mos authorization only:
python3 -I -S -B docs/compaction-refresh/probes/p6_constrained_recovery.py
The default three isolated runs launch the shipped daemon plus its production
observer transport on private sockets. The driver invokes the shipped recovery
command and adapter gate identity; it never resets broker state, mocks promote,
or taps a live model-output stream.
"""
from __future__ import annotations
import argparse
import base64
import hashlib
import importlib.util
import json
import os
import shutil
import socket
import subprocess
import sys
import tempfile
import time
from pathlib import Path
HERE = Path(__file__).resolve().parent
REPOSITORY = HERE.parents[2]
TOOLS = REPOSITORY / "packages/mosaic/framework/tools/lease-broker"
DAEMON = TOOLS / "daemon.py"
GATE = TOOLS / "mutator-gate.py"
RECOVERY_COMMAND = TOOLS / "recover-context.py"
OBSERVER_CLIENT = TOOLS / "receipt-observer-client.py"
FRAGMENTS = TOOLS / "normative_fragments.py"
CLAUDE_SETTINGS = REPOSITORY / "packages/mosaic/framework/runtime/claude/settings.json"
PI_EXTENSION = REPOSITORY / "packages/mosaic/framework/runtime/pi/mosaic-extension.ts"
def load_shipped_fragments():
spec = importlib.util.spec_from_file_location("p6_shipped_fragments", FRAGMENTS)
if spec is None or spec.loader is None:
raise RuntimeError("shipped normative construction unavailable")
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
return module
def request(socket_path: Path, value: dict[str, object]) -> dict[str, object]:
with socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) as connection:
connection.settimeout(3.0)
connection.connect(str(socket_path))
connection.sendall((json.dumps(value, separators=(",", ":")) + "\n").encode())
connection.shutdown(socket.SHUT_WR)
response = bytearray()
while True:
chunk = connection.recv(4096)
if not chunk:
break
response.extend(chunk)
if not response.endswith(b"\n") or response.count(b"\n") != 1:
raise AssertionError(f"unframed broker reply: {bytes(response)!r}")
reply = json.loads(response[:-1])
if not isinstance(reply, dict):
raise AssertionError("broker reply is not an object")
return reply
def wait_ready(process: subprocess.Popen[str], socket_path: Path) -> None:
deadline = time.monotonic() + 5.0
while time.monotonic() < deadline:
if socket_path.exists():
return
if process.poll() is not None:
output = process.stdout.read() if process.stdout is not None else ""
raise RuntimeError(f"shipped daemon exited before READY: {output}")
time.sleep(0.02)
raise TimeoutError("shipped daemon did not create private probe socket")
def run_json(command: list[str], environment: dict[str, str], input_value: object | None = None) -> dict[str, object]:
completed = subprocess.run(
command,
input=None if input_value is None else json.dumps(input_value),
text=True,
capture_output=True,
env=environment,
check=False,
)
if not completed.stdout.endswith("\n"):
raise AssertionError(f"command omitted framed result: {completed.stderr!r}")
reply = json.loads(completed.stdout)
if not isinstance(reply, dict):
raise AssertionError("command result is not an object")
return reply
def gate_recovery(runtime: str, phase: str, environment: dict[str, str]) -> None:
command = [sys.executable, "-I", "-S", "-B", str(GATE), "--runtime", runtime]
if runtime == "claude":
command.extend(["--recovery-command", str(RECOVERY_COMMAND)])
recovery_invocation = (
f"python3 {RECOVERY_COMMAND} begin --construction /tmp/p6.json "
"--compaction-epoch 1 --request-epoch 1"
if phase == "begin"
else f"python3 {RECOVERY_COMMAND} complete"
)
value = {"tool_name": "Bash", "tool_input": {"command": recovery_invocation}}
else:
value = {"tool_name": "mosaic_context_recover"}
completed = subprocess.run(command, input=json.dumps(value), text=True, capture_output=True, env=environment, check=False)
if completed.returncode != 0:
raise AssertionError(f"{runtime} recovery invocation remained gated: {completed.stderr!r}")
def record_production_observation(runtime: str, message: str, root: Path, environment: dict[str, str]) -> None:
command = [sys.executable, "-I", "-S", "-B", str(OBSERVER_CLIENT), "--runtime", runtime]
if runtime == "claude":
transcript = root / "claude-transcript.jsonl"
transcript.write_text(json.dumps({"message": {"role": "assistant", "content": message}}) + "\n", encoding="utf-8")
payload = {"transcript_path": str(transcript)}
command.append("--latest-entry")
else:
payload = {"latest_assistant_message": message}
completed = subprocess.run(command, input=json.dumps(payload), text=True, capture_output=True, env=environment, check=False)
if completed.returncode != 0:
raise AssertionError(f"{runtime} production observer transport refused: {completed.stderr!r}")
def run_once(index: int, runtime: str) -> None:
# Parity guard: drive the shipped command and the repaired adapter/observer
# bytes, not a shadow receipt or promotion implementation.
recovery_source = RECOVERY_COMMAND.read_text(encoding="utf-8")
if '"action": "begin_recovery"' not in recovery_source or '"action": "complete_recovery"' not in recovery_source:
raise AssertionError("P6 parity guard: recovery command no longer drives shipped broker entrypoints")
gate_source = GATE.read_text(encoding="utf-8")
if "--recovery-command" not in CLAUDE_SETTINGS.read_text(encoding="utf-8"):
raise AssertionError("P6 parity guard: Claude recovery mapping is missing")
if "_SHELL_ACTIVE" not in gate_source or "argv[1] != str(recovery_command)" not in gate_source:
raise AssertionError("P6 parity guard: Claude mapping is not literal-only")
if "const RECOVERY_TOOL = 'mosaic_context_recover'" not in PI_EXTENSION.read_text(encoding="utf-8"):
raise AssertionError("P6 parity guard: Pi recovery tool mapping is missing")
fragments = load_shipped_fragments()
root = Path(tempfile.mkdtemp(prefix=f"mosaic-p6-recovery-{index}-"))
os.chmod(root, 0o700)
socket_path = root / "broker.sock"
observer_socket = root / "observer.sock"
state_path = root / "state.json"
construction_path = root / "construction.json"
content = b"P6 constrained recovery fixture\n"
construction = {
"manifest_version": 1,
"generator_version": "p6-constrained-recovery",
"fragments": [{
"source_id": "authority/p6",
"content_base64": base64.b64encode(content).decode("ascii"),
"expected_sha256": hashlib.sha256(content).hexdigest(),
}],
}
construction_path.write_text(json.dumps(construction), encoding="utf-8")
os.chmod(construction_path, 0o600)
process = subprocess.Popen(
[sys.executable, "-I", "-S", "-B", str(DAEMON), "--socket", str(socket_path),
"--state", str(state_path), "--observer-socket", str(observer_socket)],
stdin=subprocess.DEVNULL,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
text=True,
)
try:
wait_ready(process, socket_path)
registered = request(socket_path, {"action": "register_anchor", "runtime_generation": 1})
session_id = registered.get("session_id")
if registered.get("ok") is not True or not isinstance(session_id, str):
raise AssertionError(f"broker anchor registration failed: {registered!r}")
built = fragments.build_payload_from_wire(construction)
normal = request(socket_path, {
"action": "begin_verification", "session_id": session_id, "runtime_generation": 1,
"runtime": runtime, "construction": construction,
"binding": {"compaction_epoch": index, "request_epoch": index + 100,
"h_source": built.h_source, "h_payload": built.h_payload, "schema_version": 1},
})
normal_challenge = normal.get("receipt_challenge")
normal_receipt = normal.get("receipt")
if not isinstance(normal_challenge, str) or not isinstance(normal_receipt, str):
raise AssertionError("normal path did not mint a receipt challenge")
environment = {
**os.environ,
"MOSAIC_LEASE_BROKER_SOCKET": str(socket_path),
"MOSAIC_RECEIPT_OBSERVER_SOCKET": str(observer_socket),
"MOSAIC_LEASE_SESSION_ID": session_id,
"MOSAIC_RUNTIME_GENERATION": "1",
"MOSAIC_LEASE_RUNTIME": runtime,
}
gate_recovery(runtime, "begin", environment)
recovery = run_json([
sys.executable, "-I", "-S", "-B", str(RECOVERY_COMMAND), "begin", "--construction", str(construction_path),
"--compaction-epoch", str(index + 10), "--request-epoch", str(index + 110),
], environment)
challenge = recovery.get("receipt_challenge")
receipt = recovery.get("receipt")
if recovery.get("state") != "PENDING_DELIVERY" or not isinstance(challenge, str) or not isinstance(receipt, str):
raise AssertionError(f"recovery command did not drive pending delivery: {recovery!r}")
if challenge == normal_challenge:
raise AssertionError("recovery reused a normal-path challenge")
# C4: production observer content is still exact-current-cycle only.
record_production_observation(runtime, normal_receipt, root, environment)
refused = run_json([sys.executable, "-I", "-S", "-B", str(RECOVERY_COMMAND), "complete"], environment)
if refused.get("ok") is not False or refused.get("code") != "RECEIPT_MISMATCH":
raise AssertionError(f"normal-path receipt replay was not refused: {refused!r}")
gate_recovery(runtime, "begin", environment)
recovery = run_json([
sys.executable, "-I", "-S", "-B", str(RECOVERY_COMMAND), "begin", "--construction", str(construction_path),
"--compaction-epoch", str(index + 20), "--request-epoch", str(index + 120),
], environment)
receipt = recovery.get("receipt")
if recovery.get("state") != "PENDING_DELIVERY" or not isinstance(receipt, str):
raise AssertionError(f"fresh recovery retry did not pend: {recovery!r}")
record_production_observation(runtime, receipt, root, environment)
gate_recovery(runtime, "complete", environment)
promoted = run_json([sys.executable, "-I", "-S", "-B", str(RECOVERY_COMMAND), "complete"], environment)
if promoted.get("ok") is not True or promoted.get("state") != "VERIFIED":
raise AssertionError(f"recovery consume-before-promote failed: {promoted!r}")
replay = run_json([sys.executable, "-I", "-S", "-B", str(RECOVERY_COMMAND), "complete"], environment)
if replay.get("ok") is not False or replay.get("code") != "INVALID_LEASE_TRANSITION":
raise AssertionError(f"consumed recovery challenge re-promoted: {replay!r}")
finally:
if process.poll() is None:
process.terminate()
try:
process.wait(timeout=3.0)
except subprocess.TimeoutExpired:
process.kill()
process.wait()
shutil.rmtree(root, ignore_errors=True)
def main() -> None:
parser = argparse.ArgumentParser()
parser.add_argument("--runs", type=int, default=3)
arguments = parser.parse_args()
if arguments.runs != 3:
raise SystemExit("P6 requires exactly three isolated runs")
for index, runtime in enumerate(("pi", "claude", "pi")):
run_once(index, runtime)
print("P6 constrained recovery probe PASS: 3 isolated shipped recovery-command runs")
if __name__ == "__main__":
main()
@@ -1,350 +0,0 @@
import type { ExtensionAPI } from '@earendil-works/pi-coding-agent';
import { Type } from 'typebox';
import { createHash, randomUUID } from 'node:crypto';
import { readFileSync, appendFileSync, statSync } from 'node:fs';
import net from 'node:net';
import { fileURLToPath } from 'node:url';
import { resolve } from 'node:path';
const SELF = resolve(fileURLToPath(import.meta.url).split('?')[0]!);
const LOG = process.env['GATE0_PI_LOG'];
const CONTEXT_BLOCK =
process.env['GATE0_PI_CONTEXT_BLOCK'] ??
[
'GATE0_PI_ATOMIC_BEGIN',
'segment-01=alpha-7e31',
'segment-02=middle-9c42',
'segment-03=omega-5b83',
'GATE0_PI_ATOMIC_END',
].join('\n');
let sequence = 0;
function sha(value: string | Buffer): string {
return createHash('sha256').update(value).digest('hex');
}
function procStarttime(): number {
const text = readFileSync(`/proc/${process.pid}/stat`, 'utf8');
const close = text.lastIndexOf(')');
const fields = text.slice(close + 2).trim().split(/\s+/);
return Number(fields[19]);
}
function log(event: string, details: Record<string, unknown> = {}): void {
if (!LOG) return;
sequence += 1;
appendFileSync(
LOG,
`${JSON.stringify({ seq: sequence, event, pid: process.pid, starttime_ticks: procStarttime(), ...details })}\n`,
);
}
function argvExtensions(): string[] {
const result: string[] = [];
for (let i = 0; i < process.argv.length; i += 1) {
if (process.argv[i] === '--extension' || process.argv[i] === '-e') {
const candidate = process.argv[i + 1];
if (candidate) result.push(resolve(candidate));
}
}
return result;
}
interface SourceValidation {
ok: boolean;
reason: string;
fragment?: string;
}
function validateSources(): SourceValidation {
const manifestPath = process.env['GATE0_SOURCE_MANIFEST'];
if (!manifestPath) return { ok: true, reason: 'no-manifest-probe-disabled' };
try {
const manifest = JSON.parse(readFileSync(manifestPath, 'utf8')) as {
maxBytes: number;
fragments: Array<{ path: string; sha256: string }>;
};
for (const fragment of manifest.fragments) {
let fileStat;
try {
fileStat = statSync(fragment.path);
} catch {
return { ok: false, reason: 'missing', fragment: fragment.path };
}
if (!fileStat.isFile()) {
return { ok: false, reason: 'not-regular-file', fragment: fragment.path };
}
if (fileStat.size > manifest.maxBytes) {
return { ok: false, reason: 'oversize', fragment: fragment.path };
}
const bytes = readFileSync(fragment.path);
if (sha(bytes) !== fragment.sha256) {
return { ok: false, reason: 'hash-mismatch', fragment: fragment.path };
}
}
return { ok: true, reason: 'all-fragments-valid' };
} catch (error) {
return { ok: false, reason: `manifest-error:${error instanceof Error ? error.name : 'unknown'}` };
}
}
function brokerRequest(payload: Record<string, unknown>): Promise<Record<string, unknown>> {
const socketPath = process.env['GATE0_GENERATION_SOCKET'];
if (!socketPath) return Promise.resolve({ skipped: true });
return new Promise((resolvePromise, reject) => {
const socket = net.createConnection(socketPath);
let buffer = '';
socket.setEncoding('utf8');
socket.on('connect', () => socket.write(`${JSON.stringify(payload)}\n`));
socket.on('data', (chunk) => {
buffer += chunk;
const newline = buffer.indexOf('\n');
if (newline < 0) return;
socket.end();
resolvePromise(JSON.parse(buffer.slice(0, newline)) as Record<string, unknown>);
});
socket.on('error', reject);
});
}
function markerPaths(value: unknown, path = '$'): string[] {
const matches: string[] = [];
if (typeof value === 'string') {
if (value.includes(CONTEXT_BLOCK)) matches.push(path);
return matches;
}
if (Array.isArray(value)) {
value.forEach((item, index) => matches.push(...markerPaths(item, `${path}[${index}]`)));
return matches;
}
if (value && typeof value === 'object') {
for (const [key, item] of Object.entries(value as Record<string, unknown>)) {
matches.push(...markerPaths(item, `${path}.${key}`));
}
}
return matches;
}
function assistantToolIds(message: unknown): string[] {
if (!message || typeof message !== 'object') return [];
const candidate = message as { role?: string; content?: unknown };
if (candidate.role !== 'assistant' || !Array.isArray(candidate.content)) return [];
return candidate.content
.filter(
(block): block is { type: 'toolCall'; id: string } =>
Boolean(
block &&
typeof block === 'object' &&
(block as { type?: string }).type === 'toolCall' &&
typeof (block as { id?: unknown }).id === 'string',
),
)
.map((block) => block.id);
}
function assistantText(message: unknown): string {
if (!message || typeof message !== 'object') return '';
const candidate = message as { role?: string; content?: unknown };
if (candidate.role !== 'assistant' || !Array.isArray(candidate.content)) return '';
return candidate.content
.filter(
(block): block is { type: 'text'; text: string } =>
Boolean(
block &&
typeof block === 'object' &&
(block as { type?: string }).type === 'text' &&
typeof (block as { text?: unknown }).text === 'string',
),
)
.map((block) => block.text)
.join('');
}
export default function register(pi: ExtensionAPI) {
const localProviderUrl = process.env['GATE0_LOCAL_PROVIDER_URL'];
if (localProviderUrl) {
pi.registerProvider('gate0-local', {
baseUrl: localProviderUrl,
apiKey: 'gate0-probe-not-a-secret',
api: 'openai-completions',
models: [
{
id: 'gate0-model',
name: 'Gate0 deterministic local model',
reasoning: false,
input: ['text'],
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
contextWindow: 32_000,
maxTokens: 1_024,
},
],
});
}
const extensions = argvExtensions();
const lastPosition = extensions.length > 0 && extensions.at(-1) === SELF;
interface RequestCycle {
nonce: string;
verified: boolean;
sourceReason: string;
}
let buildingCycle: RequestCycle | undefined;
const inFlightCycles: RequestCycle[] = [];
const toolNonce = new Map<string, { nonce: string; verified: boolean; sourceReason: string }>();
pi.on('session_start', async (event) => {
const broker = await brokerRequest({ action: 'lifecycle', phase: 'start', reason: event.reason });
log('session_start', {
reason: event.reason,
extensions,
self: SELF,
lastPosition,
gateState: lastPosition ? 'UNVERIFIED_READY' : 'CLOSED_NOT_LAST',
broker,
});
});
pi.on('session_shutdown', async (event) => {
const broker = await brokerRequest({ action: 'lifecycle', phase: 'shutdown', reason: event.reason });
log('session_shutdown', { reason: event.reason, broker });
});
pi.on('context', async (event) => {
const validation = validateSources();
buildingCycle = {
nonce: randomUUID(),
sourceReason: validation.reason,
verified: lastPosition && validation.ok,
};
const inputJson = JSON.stringify(event.messages);
const injected = {
role: 'custom' as const,
customType: 'gate0-context',
content: CONTEXT_BLOCK,
display: false,
timestamp: Date.now(),
};
const outputMessages = buildingCycle.verified
? [...event.messages, injected]
: [...event.messages];
const outputPrefix = outputMessages.slice(0, event.messages.length);
const sourceBroker = validation.ok
? { action: 'none', reason: 'source-valid' }
: await brokerRequest({ action: 'source-invalid', reason: validation.reason });
log('context_return', {
requestNonce: buildingCycle.nonce,
sourceValidation: validation,
sourceBroker,
lastPosition,
promotion: false,
injectionDecision: buildingCycle.verified ? 'ONE_ATOMIC_AGENT_MESSAGE' : 'REFUSED',
inputCount: event.messages.length,
outputCount: outputMessages.length,
prefixHashBefore: sha(inputJson),
prefixHashAfter: sha(JSON.stringify(outputPrefix)),
prefixPreservedByReturn: sha(inputJson) === sha(JSON.stringify(outputPrefix)),
blockLength: CONTEXT_BLOCK.length,
blockSha256: sha(CONTEXT_BLOCK),
});
return { messages: outputMessages };
});
pi.on('before_provider_request', async (event) => {
const paths = markerPaths(event.payload);
const cycle = buildingCycle;
buildingCycle = undefined;
if (cycle) inFlightCycles.push(cycle);
log('before_provider_request', {
requestNonce: cycle?.nonce,
inFlightDepth: inFlightCycles.length,
markerOccurrences: paths.length,
markerPaths: paths,
finalPayloadValid: Boolean(cycle?.verified && paths.length === 1),
});
});
pi.on('after_provider_response', async (event) => {
const cycle = inFlightCycles[0];
log('after_provider_response', {
requestNonce: cycle?.nonce,
status: event.status,
assistantContentAvailableAtThisHook: false,
timing: 'headers/status before stream consumption',
});
});
pi.on('message_end', async (event) => {
const role = (event.message as { role?: string }).role;
const ids = assistantToolIds(event.message);
const text = assistantText(event.message);
const cycle = role === 'assistant' ? inFlightCycles.shift() : undefined;
if (ids.length > 0 && cycle) {
for (const id of ids) {
toolNonce.set(id, {
nonce: cycle.nonce,
verified: cycle.verified,
sourceReason: cycle.sourceReason,
});
}
}
log('message_end', {
role,
assistantContentObserved: role === 'assistant',
requestNonce: cycle?.nonce,
inFlightDepthAfter: inFlightCycles.length,
toolCallIds: ids,
nonceMappings: ids.map((id) => ({ toolCallId: id, requestNonce: cycle?.nonce })),
exactContextBlockCopied: text.includes(CONTEXT_BLOCK),
assistantTextSha256: text ? sha(text) : null,
});
});
pi.on('tool_call', async (event) => {
const mapping = toolNonce.get(event.toolCallId);
const allowed = Boolean(lastPosition && mapping?.verified);
log('tool_call', {
toolCallId: event.toolCallId,
toolName: event.toolName,
mapping: mapping ?? null,
allowed,
reason: !lastPosition
? 'closed-not-last'
: !mapping
? 'unknown-tool-call-id'
: !mapping.verified
? `unverified-source:${mapping.sourceReason}`
: 'exact-tool-call-id-mapped-to-verified-request-nonce',
});
if (!allowed) return { block: true, reason: 'Gate0 probe refused unverified tool batch' };
});
pi.on('agent_settled', async () => {
log('agent_settled', { retainedNonceMappingsBeforeClear: toolNonce.size });
toolNonce.clear();
});
pi.registerTool({
name: 'gate0_nonce_probe',
label: 'Gate0 Nonce Probe',
description: 'Gate0-only harmless tool used to prove toolCallId to request-nonce correlation.',
parameters: Type.Object({ label: Type.String() }),
async execute(toolCallId, params) {
const broker = await brokerRequest({ action: 'promote-probe' });
log('tool_execute', { toolCallId, label: params.label, broker });
return {
content: [{ type: 'text', text: `gate0_nonce_probe executed for ${params.label}` }],
details: { harmless: true },
};
},
});
pi.registerCommand('gate0-reload', {
description: 'Trigger a real same-PID Pi extension/runtime reload.',
handler: async (_args, ctx) => {
log('reload_command_before');
await ctx.reload();
return;
},
});
}
@@ -1,450 +0,0 @@
#!/usr/bin/env python3
"""Drive real Pi 0.80.x RPC for P2/P3/P5/P6 runtime evidence."""
from __future__ import annotations
import hashlib
import json
import os
import queue
import shutil
import signal
import socket
import subprocess
import sys
import tempfile
import threading
import time
from pathlib import Path
from typing import Any, Callable
HERE = Path(__file__).resolve().parent
BLOCK = "\n".join(
[
"GATE0_PI_ATOMIC_BEGIN",
"segment-01=alpha-7e31",
"segment-02=middle-9c42",
"segment-03=omega-5b83",
"GATE0_PI_ATOMIC_END",
]
)
def wait_path(path: Path, timeout: float = 20) -> None:
deadline = time.monotonic() + timeout
while time.monotonic() < deadline:
if path.exists():
return
time.sleep(0.05)
raise TimeoutError(f"timed out waiting for {path}")
def socket_request(path: Path, payload: dict[str, object]) -> None:
conn = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
conn.connect(str(path))
conn.sendall((json.dumps(payload) + "\n").encode())
conn.makefile("r", encoding="utf-8").readline()
conn.close()
def jsonl(path: Path) -> list[dict[str, Any]]:
if not path.exists():
return []
return [json.loads(line) for line in path.read_text().splitlines() if line]
class PiRpc:
def __init__(self, command: list[str], cwd: Path, env: dict[str, str]):
self.process = subprocess.Popen(
command,
cwd=cwd,
env=env,
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
bufsize=1,
start_new_session=True,
)
self.events: queue.Queue[dict[str, Any]] = queue.Queue()
self.raw_lines: list[str] = []
self.stderr_lines: list[str] = []
threading.Thread(target=self._read_stdout, daemon=True).start()
threading.Thread(target=self._read_stderr, daemon=True).start()
def _read_stdout(self) -> None:
assert self.process.stdout is not None
for line in self.process.stdout:
stripped = line.rstrip("\n")
self.raw_lines.append(stripped)
try:
event = json.loads(stripped)
except json.JSONDecodeError:
continue
self.events.put(event)
def _read_stderr(self) -> None:
assert self.process.stderr is not None
for line in self.process.stderr:
self.stderr_lines.append(line.rstrip("\n"))
def send(self, payload: dict[str, object]) -> None:
assert self.process.stdin is not None
self.process.stdin.write(json.dumps(payload) + "\n")
self.process.stdin.flush()
def wait(self, predicate: Callable[[dict[str, Any]], bool], description: str, timeout: float = 180) -> dict[str, Any]:
deadline = time.monotonic() + timeout
while time.monotonic() < deadline:
if self.process.poll() is not None and self.events.empty():
raise RuntimeError(
f"Pi exited {self.process.returncode} while waiting for {description}: "
+ " | ".join(self.stderr_lines[-5:])
)
try:
event = self.events.get(timeout=0.2)
except queue.Empty:
continue
if predicate(event):
return event
raise TimeoutError(f"timed out waiting for {description}")
def response(self, request_id: str, timeout: float = 180) -> dict[str, Any]:
return self.wait(
lambda event: event.get("type") == "response" and event.get("id") == request_id,
f"response {request_id}",
timeout,
)
def prompt_and_settle(self, request_id: str, message: str) -> None:
self.send({"id": request_id, "type": "prompt", "message": message})
response = self.response(request_id)
if not response.get("success"):
raise RuntimeError(f"prompt rejected: {response}")
self.wait(lambda event: event.get("type") == "agent_settled", f"agent_settled {request_id}")
def close(self) -> None:
if self.process.poll() is None:
try:
os.killpg(self.process.pid, signal.SIGTERM)
except ProcessLookupError:
pass
try:
self.process.wait(timeout=8)
except subprocess.TimeoutExpired:
os.killpg(self.process.pid, signal.SIGKILL)
self.process.wait(timeout=5)
def manifest(path: Path, fragment: Path, expected_hash: str, max_bytes: int = 64) -> None:
path.write_text(
json.dumps(
{
"maxBytes": max_bytes,
"fragments": [{"path": str(fragment), "sha256": expected_hash}],
},
sort_keys=True,
)
)
def run_open(root: Path) -> tuple[list[dict[str, Any]], list[dict[str, Any]], list[str], list[str]]:
workspace = root / "workspace"
workspace.mkdir()
session_dir = root / "sessions"
session_dir.mkdir()
pi_log = root / "pi-hooks.jsonl"
generation_log = root / "generation.jsonl"
generation_socket = root / "generation.sock"
source_manifest = root / "manifest.json"
valid_fragment = root / "fragment.md"
valid_fragment.write_text("NORMATIVE-FRAGMENT-v1\n")
expected = hashlib.sha256(valid_fragment.read_bytes()).hexdigest()
manifest(source_manifest, valid_fragment, expected)
broker = subprocess.Popen(
[
sys.executable,
str(HERE / "p3_generation_broker.py"),
"--socket",
str(generation_socket),
"--log",
str(generation_log),
],
text=True,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
)
wait_path(generation_socket)
env = os.environ.copy()
env.update(
{
"GATE0_PI_LOG": str(pi_log),
"GATE0_GENERATION_SOCKET": str(generation_socket),
"GATE0_SOURCE_MANIFEST": str(source_manifest),
"GATE0_PI_CONTEXT_BLOCK": BLOCK,
"MOSAIC_PI_FORCE_SKILLS": "",
"PI_SKIP_VERSION_CHECK": "1",
}
)
command = [
"mosaic",
"yolo",
"pi",
"--mode",
"rpc",
"--session-dir",
str(session_dir),
"--no-extensions",
"--no-context-files",
"--no-prompt-templates",
"--model",
"openai-codex/gpt-5.6-sol",
"--thinking",
"medium",
"--extension",
str(HERE / "pi_gate0_extension.ts"),
]
pi = PiRpc(command, workspace, env)
try:
pi.send({"id": "state-0", "type": "get_state"})
state0 = pi.response("state-0")
original_session = state0["data"]["sessionFile"]
pi.prompt_and_settle(
"p2",
"Call gate0_nonce_probe exactly once with label p2. After the tool finishes, copy the exact full GATE0_PI_ATOMIC_BEGIN through GATE0_PI_ATOMIC_END block from context, with no commentary.",
)
# P3 immediately follows the valid P2 promotion so reload must revoke a
# genuinely VERIFIED prior generation, not an already-invalid source run.
pi.send({"id": "reload", "type": "prompt", "message": "/gate0-reload"})
reload_response = pi.response("reload")
if not reload_response.get("success"):
raise RuntimeError(f"reload command failed: {reload_response}")
pi.send({"id": "clone", "type": "clone"})
clone_response = pi.response("clone")
if not clone_response.get("success") or clone_response.get("data", {}).get("cancelled"):
raise RuntimeError(f"clone failed: {clone_response}")
pi.send({"id": "new", "type": "new_session"})
new_response = pi.response("new")
if not new_response.get("success") or new_response.get("data", {}).get("cancelled"):
raise RuntimeError(f"new session failed: {new_response}")
pi.send(
{
"id": "resume",
"type": "switch_session",
"sessionPath": original_session,
}
)
resume_response = pi.response("resume")
if not resume_response.get("success") or resume_response.get("data", {}).get("cancelled"):
raise RuntimeError(f"resume failed: {resume_response}")
# P5 missing fragment: action-time source validation must revoke/refuse.
manifest(source_manifest, root / "absent-fragment.md", expected)
pi.prompt_and_settle(
"p5-missing",
"Call gate0_nonce_probe exactly once with label p5-missing, then stop.",
)
# P5 oversize fragment: expected hash is correct, size limit is not.
oversize = root / "oversize.md"
oversize.write_text("X" * 65)
manifest(source_manifest, oversize, hashlib.sha256(oversize.read_bytes()).hexdigest(), 64)
pi.prompt_and_settle(
"p5-oversize",
"Call gate0_nonce_probe exactly once with label p5-oversize, then stop.",
)
# P5 hash mismatch: size is valid but bytes differ from expected.
mismatch = root / "mismatch.md"
mismatch.write_text("tampered\n")
manifest(source_manifest, mismatch, expected, 64)
pi.prompt_and_settle(
"p5-hash",
"Call gate0_nonce_probe exactly once with label p5-hash-mismatch, then stop.",
)
time.sleep(1)
return jsonl(pi_log), jsonl(generation_log), list(pi.raw_lines), list(pi.stderr_lines)
finally:
pi.close()
try:
socket_request(generation_socket, {"action": "shutdown-broker"})
except OSError:
pass
try:
broker.wait(timeout=5)
except subprocess.TimeoutExpired:
broker.kill()
broker.wait()
def run_closed(root: Path) -> list[dict[str, Any]]:
workspace = root / "closed-workspace"
workspace.mkdir()
pi_log = root / "closed-hooks.jsonl"
env = os.environ.copy()
env.update(
{
"GATE0_PI_LOG": str(pi_log),
"MOSAIC_PI_FORCE_SKILLS": "",
"PI_SKIP_VERSION_CHECK": "1",
}
)
command = [
"mosaic",
"yolo",
"pi",
"--mode",
"rpc",
"--no-session",
"--no-extensions",
"--no-context-files",
"--no-prompt-templates",
"--extension",
str(HERE / "pi_gate0_extension.ts"),
"--extension",
str(HERE / "pi_later_extension.ts"),
]
pi = PiRpc(command, workspace, env)
try:
pi.send({"id": "closed-state", "type": "get_state"})
pi.response("closed-state")
time.sleep(0.5)
return jsonl(pi_log)
finally:
pi.close()
def main() -> None:
with tempfile.TemporaryDirectory(prefix="gate0-pi-") as temp:
root = Path(temp)
records, generations, rpc_lines, stderr_lines = run_open(root)
closed = run_closed(root)
p2_message = next(
r for r in records if r["event"] == "message_end" and r.get("nonceMappings")
)
p2_tool = next(r for r in records if r["event"] == "tool_call" and r.get("allowed"))
mapped = p2_message["nonceMappings"][0]
assert mapped["toolCallId"] == p2_tool["toolCallId"]
assert mapped["requestNonce"] == p2_tool["mapping"]["nonce"]
assert next(r for r in records if r["event"] == "session_start")["lastPosition"] is True
assert next(r for r in closed if r["event"] == "session_start")["gateState"] == "CLOSED_NOT_LAST"
reload_revoke = next(
r
for r in generations
if r["event"] == "runtime_generation_bump"
and r.get("reason") == "reload"
and r.get("phase") == "shutdown"
)
assert reload_revoke["prior_lease"] == "VERIFIED"
assert reload_revoke["prior_lease_revoked"] is True
for reason in {"missing", "oversize", "hash-mismatch"}:
assert any(
r["event"] == "context_return"
and r.get("sourceValidation", {}).get("reason") == reason
and r.get("injectionDecision") == "REFUSED"
and r.get("promotion") is False
for r in records
)
assert any(
r["event"] == "tool_call"
and r.get("mapping", {}).get("sourceReason") == reason
and r.get("allowed") is False
for r in records
)
assert any(
r["event"] == "message_end" and r.get("exactContextBlockCopied") is True
for r in records
)
print("$ python3 docs/compaction-refresh/probes/pi_gate0_run.py")
print("machine_assertions=PASS")
print("runtime_versions:")
print(" " + subprocess.check_output(["pi", "--version"], text=True).strip())
print(" " + subprocess.check_output(["mosaic", "--version"], text=True).strip())
print("\nP2_EVENT_ORDER_AND_NONCE_MAP:")
for record in records:
if record["seq"] <= 12 and record["event"] in {
"after_provider_response",
"message_end",
"tool_call",
"tool_execute",
} and (
record["event"] != "message_end"
or record.get("role") == "assistant"
):
print(json.dumps(record, sort_keys=True))
print("\nP2_LAST_OR_CLOSED:")
print(json.dumps(next(r for r in records if r["event"] == "session_start"), sort_keys=True))
print(json.dumps(next(r for r in closed if r["event"] == "session_start"), sort_keys=True))
print("\nP3_GENERATION_BROKER:")
for record in generations:
if record["event"] in {"probe_lease_promoted", "runtime_generation_bump"}:
print(json.dumps(record, sort_keys=True))
print("\nP5_SOURCE_INVALIDATION:")
fault_reasons = {"missing", "oversize", "hash-mismatch"}
emitted_context: set[str] = set()
emitted_tool: set[str] = set()
for record in records:
source_reason = record.get("sourceValidation", {}).get("reason")
if (
record["event"] == "context_return"
and source_reason in fault_reasons
and source_reason not in emitted_context
):
print(json.dumps(record, sort_keys=True))
emitted_context.add(source_reason)
mapping_reason = record.get("mapping", {}).get("sourceReason")
if (
record["event"] == "tool_call"
and not record.get("allowed")
and mapping_reason in fault_reasons
and mapping_reason not in emitted_tool
):
print(json.dumps(record, sort_keys=True))
emitted_tool.add(mapping_reason)
emitted_broker: set[str] = set()
for record in generations:
reason = record.get("source_reason")
if record["event"] == "source_invalidation_revoke" and reason not in emitted_broker:
print(json.dumps(record, sort_keys=True))
emitted_broker.add(str(reason))
print("\nP6_PI_CONTEXT_ATOMIC_OBSERVATION:")
for record in records:
include = (
(record["event"] == "context_return" and record.get("injectionDecision") == "ONE_ATOMIC_AGENT_MESSAGE")
or (record["event"] == "before_provider_request" and record.get("finalPayloadValid"))
or (record["event"] == "message_end" and record.get("exactContextBlockCopied"))
)
if include and record["seq"] <= 12:
print(json.dumps(record, sort_keys=True))
print("\nRPC_EVENT_COUNTS:")
counts: dict[str, int] = {}
for line in rpc_lines:
try:
event = json.loads(line)
except json.JSONDecodeError:
continue
key = str(event.get("type"))
counts[key] = counts.get(key, 0) + 1
print(json.dumps(counts, sort_keys=True))
print("stderr_nonempty=" + str(bool(stderr_lines)))
for line in stderr_lines[:10]:
print("stderr: " + line[:500])
if __name__ == "__main__":
main()
@@ -1,8 +0,0 @@
import type { ExtensionAPI } from '@earendil-works/pi-coding-agent';
// Deliberately loaded after pi_gate0_extension.ts. The Gate0 extension must
// observe its argv position and remain CLOSED rather than claiming finality.
export default function register(pi: ExtensionAPI) {
pi.on('context', async (event) => ({ messages: [...event.messages] }));
pi.on('before_provider_request', async () => undefined);
}
@@ -1,60 +0,0 @@
# TERRA §3 — Probe-3 Bounded-Repair Re-Review v11
**Verdict: PASS (byte review only; no harness, broker, or runtime was launched).**
## Reviewed object (GUARD 1)
- **Branch:** `feat/827-gate0-probe`
- **Reviewed object = `93a5fd018d26f16aa58a3ec1afc0c9bc83935fed`**
- **v10 baseline:** `ce5ba762051354338889959bfce2b0381f4a4e2a`
- **Harness:** `docs/compaction-refresh/probes/p3_d4_focused_run.py`
- **Verified harness sha256:** `3a4b625a474ab31c53af7758464f51fe729a14201a77c049cceb9eafb607b5c8`
Fresh verification fetched `origin/feat/827-gate0-probe`; local `HEAD` and remote branch both resolved to the reviewed full SHA before hashing the Git object.
## TERRA §3 null-delta gate
### (a) Exact authorized diff — PASS
`git diff ce5ba762051354338889959bfce2b0381f4a4e2a 93a5fd018d26f16aa58a3ec1afc0c9bc83935fed -- docs/compaction-refresh/probes/p3_d4_focused_run.py` reports exactly `1 insertion, 1 deletion`, one hunk:
```diff
@@ -706,7 +706,7 @@ def isolated_environment(
- target.parent.mkdir(parents=True, mode=0o700)
+ target.parent.mkdir(parents=True, mode=0o700, exist_ok=True)
```
The zero-context hunk is `@@ -709 +709 @@`. No other harness hunk or changed line exists. This is the authorized fixture-copy-loop repair only.
### (b) Security-bearing bytes / D4 mechanism unchanged — PASS
The hunk range is **old L709 / new L709**, which does not intersect these byte-compared ranges:
| Surface | Line range | v10 SHA-256 = repair SHA-256 |
| --- | --- | --- |
| B5B9 closure, bytecode, environment, and child-launch controls | L351L575 | `3dcdcf75c8b1dea0a1f8888cf1e49a52aba51e7e41f447b80cf407d84533eb2e` |
| D4 generated lifecycle extension | L159L251 | `4bdc047f7494029d951763555a797103ca25da647b255b287378771d20da209b` |
| D4 runtime assertions, including `lease_anchor_registered` | L579L665 | `75620f25829fa7e176af8b342b2935d45afb546d821d45bf478c51e95f6918ca` |
| D4 orchestration and `--runs` parser | L749L849 | `a28c36296b2ba8fffe77912b7e25b793047393bae79a9242df1727bd21ab2f92` |
Thus B5B9 assertions and all D4 generation-mechanism bytes are identical to v10. The sole hunk is inside `isolated_environment`, outside those assertion/mechanism ranges.
## Required negative checks
- **L704 unchanged:** byte-identical (`target_agent.mkdir(parents=True, mode=0o700)`); the L704-only slice hash is `5c077090950c1cc68b53ac4d06280dd2b5d91bd34b28f1cc8d26d0d9eb8dbe41` on both commits.
- **Launcher flags unchanged:** B5B9 slice includes launcher L511L517: `-s -S -B` unchanged.
- **Broker flags unchanged:** B5B9 slice includes broker L551L556: `-I -S -B` unchanged.
- **`--runs` unchanged:** L840 remains `default=3, choices=(3,)` in the byte-identical L749L849 slice.
- **Assertions intact:** `lease_anchor_registered` remains at L594L596 in the byte-identical L579L665 slice; no B5B9 assertion is softened, removed, or repointed.
- **Absent from the harness delta:** P5, P6, P2-banking, retry-laundering, and live/prod-path changes. The exact one-line diff contains only the fixture directory `exist_ok=True` addition.
- **Fixture scope:** no other fixture line changed; `git diff --check` is clean.
## Guard 2 — authorship independence
These review-document bytes were authored by this independent review session, not by repair author `ms-p3-fix`; this session did not build or modify the reviewed harness. The review was static/byte-only and did not execute the harness or launch a broker.
## Guard 3 — publication boundary
This document is committed separately on top of `93a5fd018d26f16aa58a3ec1afc0c9bc83935fed`. Its publication commit must leave the reviewed harness Git-object SHA-256 at `3a4b625a474ab31c53af7758464f51fe729a14201a77c049cceb9eafb607b5c8`.
**TERRA §3 v11 PASS.** This is a null-security-delta byte review only; FIRE remains separately gated on Mos.
@@ -1,83 +0,0 @@
# TERRA §3 — Probe-3 Bounded-Repair Re-Review v12
**Verdict: PASS — static byte review only.** No harness, broker, socket, or fixture state was launched or created.
## Reviewed object (GUARD 1)
- **Branch:** `feat/827-gate0-probe`
- **reviewed object = `ca7b522e7beb09fbff64485510201b372d305d34`**
- **v11 baseline:** `93a5fd018d26f16aa58a3ec1afc0c9bc83935fed`
- **Harness:** `docs/compaction-refresh/probes/p3_d4_focused_run.py`
- **Verified harness SHA-256:** `92ff11bd35a72c3bda8f9722837cd9058a711f688cc75bf5e5df106645f81553`
- **Pre-audit reviewed:** `docs/compaction-refresh/reviews/GATE0-PROBE3-PREAUDIT-v12.md` at the reviewed object.
Fresh `git fetch origin feat/827-gate0-probe -q` resolved both local HEAD and `origin/feat/827-gate0-probe` to the reviewed full SHA before the object hash was trusted.
## Null-delta gate
### (a) Exact authorized diff — PASS
The exact harness diff from v11 has `1 insertion, 1 deletion`, one zero-context hunk only:
```diff
@@ -484 +484 @@ def gated_launcher_precondition(
- "initialize_runtime_generation(generation_file, generation)",
+ "initialize_generation: Callable[[Path, int], None] = initialize_runtime_generation",
```
No other harness hunk or line changed. This is the authorized diagnostic `behavior_markers` entry at L484.
### (b) B5B9 / D4 security-bearing bytes — PASS
The sole hunk is **old/new L484**, inside the diagnostic marker tuple (L481489), and does not intersect B5B9 assertion/control or D4 logic ranges. Independent slice SHA-256 comparisons from v11 to the reviewed object were identical:
| Surface | Range | SHA-256 on both revisions |
| --- | --- | --- |
| B5B9 controls before marker tuple | L351L480 | `85eda967698af5699593daa12f761abd4b759a11a03323dc867905d286455aec` |
| B5B9 controls after marker tuple | L490L575 | `b92bd920d896450f89a14979dd9a557a82ad590d7c9a73e900ebc89979a48986` |
| D4 generated lifecycle extension | L159L251 | `4bdc047f7494029d951763555a797103ca25da647b255b287378771d20da209b` |
| D4 runtime assertions (`lease_anchor_registered` included) | L579L672 | `ddaae13f0302b401ada76f1c96b9f66f2938d7afa614928f4cffb187da832856` |
| D4 orchestration / `--runs` parser | L749L849 | `a28c36296b2ba8fffe77912b7e25b793047393bae79a9242df1727bd21ab2f92` |
Therefore the diagnostic-only L484 hunk is non-intersecting and all B5B9/D4 security-bearing bytes are byte-identical to v11.
## Hardened-marker correctness — PASS
- The replacement is exactly `initialize_generation: Callable[[Path, int], None] = initialize_runtime_generation`.
- The exact string is byte-present in the pinned launcher at `f400830738998db105107a2a4c69c7f2a2a6fd5d:packages/mosaic/framework/tools/lease-broker/launch-runtime.py:49`.
- The same launcher imports the real function at L15 and invokes the DI-bound parameter at L90; the marker consequently proves the real helper binding, not merely a same-named call.
- Pinned launcher SHA-256 was independently verified as `e950e4224e280f16979d90cabb89aa1896c5ee28bed2df957e14d018d43cda82`.
## Pre-audit independently re-verified — PASS
All remaining precondition checks pass against the real pinned closure bytes:
| Requirement | Satisfying real bytes |
| --- | --- |
| Remaining launcher markers | registration action L77; DI binding L49; fixture socket env lookup L70; fixture generation path L89; generation-file env assignment L98; exec L101. |
| Generation markers | `lease_generation.py` at `f4008307`: `read_runtime_generation` L78 and `bump_runtime_generation` L90; SHA-256 `061625402f08488eac47acd23272904e71fd1a71fd15b3bdab158632c801be4c`. |
| Pinned broker closure | `p3_generation_broker.py` at `23c0caca`: explicit generation-module load L2941; fixture socket bind/permissions L4663; fixture-parent generation-file guard L7175; SHA-256 `4db4fef1ac6658a8ca79ad5091cefc901d2aa26003265c3d6726c294cf895cad`. |
| Harness path/socket checks | Fixture broker socket equality and no inherited generation file: L452455; all ten fixture-relative variables: L456471; temporary-root socket requirement: L473474. |
| Fixture construction satisfying those checks | Fixture paths/environment L675736; root-owned workspace/sessions, socket, logs, and extension construction L749759. |
| Complete pinned closure and hash gates | Exact three-object selection and SHA gate L397433; materialized pinned files L435443. |
The six remaining launcher marker substrings and both generation-function substrings were independently checked as byte-present in the pinned objects. Thus no remaining marker or path/socket precondition has a static mismatch with the real closure.
## Required unchanged / absent checks
- **SHA pins:** L41L43 byte-identical (`7396f46357620118d3da27aa074d422186600e8a4320fc0f56774439af14e877`).
- **SHA gate / before-exec rehashes / bytecode rejection:** unchanged: materialize SHA gate L426; rehashes L539/L565/L568; `reject_pinned_bytecode` L498501.
- **Fixture:** L704 pre-create and L709 `exist_ok=True` are byte-identical to v11 (L700L710 slice SHA-256 `3137ef255435ac613fe1332138bbb13111a9d3efc502a34403b3dce21287b04e`).
- **Flags and runs:** unchanged launcher `-s -S -B` (L511L517), broker `-I -S -B` (L551L556), and `--runs default=3, choices=(3,)` (L840).
- **Assertions:** `lease_anchor_registered` is intact at L594L596 in the byte-identical D4 assertion slice; no B5B9/D4 assertion is softened, removed, or repointed.
- **Absent from the exact one-line harness delta:** P5, P6, P2-banking, retry-laundering, and live/prod-path changes.
## Guard 2 — independent authorship
These review-document bytes were authored by this reviewer session. Repair author `ms-p3-fix` did not author this document; this reviewer did not build or alter the harness and is not Mos.
## Guard 3 — separate publication
This review is committed separately on top of `ca7b522e7beb09fbff64485510201b372d305d34`; its publication commit must preserve the reviewed harness object SHA-256 `92ff11bd35a72c3bda8f9722837cd9058a711f688cc75bf5e5df106645f81553`.
**TERRA §3 v12 PASS.** FIRE remains separately gated on Mos.
@@ -1,65 +0,0 @@
# Gate0 Probe-3 (D4) Class-B — §3-Conformance Review v3
**Verdict: ✅ PASS**
## Pin (G1 — reviewed object, mandatory)
- **Reviewed object = `ace6066762c088f4b9729860da71b4c84451a7c3`** (harness commit, branch `feat/827-gate0-probe`, `mosaicstack/stack` @ git.mosaicstack.dev).
- **Reviewed file:** `docs/compaction-refresh/probes/p3_d4_focused_run.py`
- **Harness sha256 (pushed provider bytes, fetched `-o FILE`, FULL-40 ref, verified before trust):**
`2f11c9391c0eef203f26b1206bee8bc4cd106e8c1192399c5e7b71f41a3f6b75` (17162 bytes; no not-found sentinel).
- **§3 amendment authority read at pin:** `GATE0-PROBE3-EXEC-AMENDMENT.md` @ ref `571f239154c6793fb1a5eac0d1cd4182f286a3ac`,
sha256 `9ac9ff873fad41a6e15763cc89cb94d0bc4a6b0cf9b6770561d1781b03f63276` (7699 bytes). MUST-HAVE/MUST-BE-ABSENT
confirmed against the actual fetched §3 text, not a paraphrase.
## Independence (G2)
Distinct Opus §3-conformance reviewer (Gate-16 author≠reviewer). I did **not** build this harness (author =
ms-rev-826); I am not Mos. This verdict is my own; the author did not author or edit it. Byte review only — **ran
nothing** (no harness, no broker, no sockets/state). Reviewed across v1 (FAIL, live-broker launch path) → v2 (PASS,
later found runtime-dead producer) → this v3 (closes the live-path review-gap).
## Why v3 (the review-gap closed)
v2 PASS @`839d156f` credited the static presence of `lease_anchor_registered` as isolation proof. At FIRE the
producing path was **dead**: the harness drove the *released* `mosaic` binary, which launched Pi **ungated**, so
`register_anchor` never ran. Static presence of an assertion ≠ its producing path executing. v3 requires the
producing path to be **live at runtime**.
## Surface-by-surface
| Surface | Result | Evidence (file:line) |
|---|---|---|
| Pushed bytes fetched + sha-verified | ✅ | sha256==`2f11c939…`, 17162B, no sentinel |
| (a) LIVE-PATH — drives the **gated** launcher, producer in the exec chain, NOT released `mosaic`/plain `execRuntime` | ✅ | launch = `python3 <GATED_LAUNCHER> --runtime pi -- pi …` :357-364; `GATED_LAUNCHER=…/launch-runtime.py` :32, pinned `GATED_WI_HEAD=abd2791f…` :31; `mosaic yolo`/`execRuntime` = 0 hits. `launch-runtime.py` unconditionally `register_anchor`s before `execvpe`, so the producer is in the invoked chain |
| (b) Fail-closed precondition present + correct (gated + fixture-socket, refuses otherwise), invoked before all launches | ✅ | `gated_launcher_precondition` :229-251, called :342 **before** broker Popen :343 and Pi launch :357. Verifies (ii) `MOSAIC_LEASE_BROKER_SOCKET==fixture` :232 + fixture in tempdir :234; (i) launcher HEAD==`abd2791f` :243 + source has `register_anchor` **before** `execute(command[0]…)` and reads `MOSAIC_LEASE_BROKER_SOCKET` :246-250. Raises `RuntimeError` (no run) on any miss :233/:235/:242/:244/:250 |
| (c) Fixture-socket isolation (no live/default broker reachable, single p3 fixture, non-destructive) | ✅ | `pop("MOSAIC_LEASE_BROKER_SOCKET")` :330 + set to fixture socket :334; harness invokes `launch-runtime.py` directly so it reads `MOSAIC_LEASE_BROKER_SOCKET`=fixture with **no** `defaultLeaseBrokerSocket`/XDG/`/run/user` fallback in the path; `register_anchor` served by the single p3 fixture; `p3_generation_broker.py` **zero diff** vs `839d156f` (in-memory volatile hex-256 session `secrets.token_hex(32)`, nothing durable outside tempdir) |
| (d) Assertion INTACT (`lease_anchor_registered` + `session_id_shape=="hex-256"`, not softened/optional/repointed) | ✅ | :256-258 (event), :298 (`hex-256`), folded into single-PID/starttime identity set :286. Not Case C |
| spawns ONLY p3_generation_broker.py | ✅ | broker Popen = `HERE/p3_generation_broker.py` :343-346; the pinned launcher is a register-before-exec launch wrapper, not a 2nd broker |
| promotion = fixture-only (not P2-banked) | ✅ | `d4_fixture_promote` "not a P2 evidence-gathering authorization"; `promote-probe` in-memory; precondition target only; no P2 import |
| D4 assertions complete | ✅ | same-PID+starttime persist (incl. launcher registration) :284-288; gen strictly increases :290-293; reload revokes genuinely-VERIFIED prior :299-300; new→`MUTATOR_UNVERIFIED` :301; prior→`STALE_GENERATION` :302; lifecycle boundaries :294-297 |
| P5 / P6 / P2-bank / retry-launder / live-effect / mechanism-change / scope-widen ABSENT | ✅ | 0 hits: `source-invalid`/`run_open`/`atomic`/`p2_provider`/`p5`/`p6`/`pi_gate0_run`/`retry`; no real-broker path; the only mechanism change is the required launch-routing fix (release→pinned gated launcher), which narrows scope, not widens |
| non-destructive | ✅ | per-run `tempfile.mkdtemp` fixtures; p3 in-memory + tempdir socket/log only; reads `/proc/<pid>/stat` (read-only) |
| deterministic | ✅ | isolated tempdir per run; deterministic assertions; session-id randomness is **shape**-asserted only (`hex-256`), `setdefault` idempotent |
| hidden exec-at-import | ✅ absent | only `if __name__ == "__main__": main()`; docstring: not executed until FIRE |
## Verdict
**PASS @ `ace60667`** — (a) LIVE-PATH, (b) fail-closed precondition, (c) fixture-socket isolation, and
(d) intact assertion all hold, with zero out-of-scope surface. The v2 review-gap (runtime-dead producer via the
released ungated `mosaic`) is structurally closed: the harness no longer invokes `mosaic` at all — it invokes the
pinned `abd2791f` `launch-runtime.py` directly (register-before-exec), and refuses to launch unless the launcher is
that pinned gated register-before-exec binary bound to this run's fixture socket.
**Findings: none.**
## Scope reminder (not a finding)
Per §3/§5 of the amendment, producing this evidence **executes** the Gate0 mechanism (launches processes, creates
socket/state artifacts, exercises revocation). This review clears the **bytes**; **FIRE remains separately gated on
Mos's explicit post-clear GO** and is not authorized by this review.
---
**Reviewer:** independent Opus §3-conformance reviewer (Gate-16 author≠reviewer). Byte review only; ran nothing.
**Reviewed object (pin):** `ace6066762c088f4b9729860da71b4c84451a7c3` · harness sha256 `2f11c9391c0eef203f26b1206bee8bc4cd106e8c1192399c5e7b71f41a3f6b75`.
@@ -1,95 +0,0 @@
# Gate0 Probe-3 (D4) Observation-Fidelity — §3-Conformance Review v4
**Verdict: ✅ PASS**
## Pin (G1 — reviewed object, mandatory)
- **Reviewed object = `484849387006ab5561798506fd6042ddbd5617de`** (harness commit, branch `feat/827-gate0-probe`, `mosaicstack/stack` @ git.mosaicstack.dev).
- **Reviewed file:** `docs/compaction-refresh/probes/p3_d4_focused_run.py` — sha256 (pushed provider bytes, `-o FILE`, FULL-40 ref, verified before trust): `9095eab7a4ddf11bb92bb5971d49e1facad12f4692ce2081665b0af47cbe5098` (23698 bytes, no not-found sentinel). Worktree bytes at `48484938` re-hashed identical.
- **Co-reviewed fixture broker:** `docs/compaction-refresh/probes/p3_generation_broker.py` (sha256 `fd5327d2e9a2808282cbc9c4a4ccef42d2a806482b27cc48035191b9b11607c8`).
- **Traceability anchor (independently recomputed):** `GATED_LAUNCHER_SHA256 = e950e4224e280f16979d90cabb89aa1896c5ee28bed2df957e14d018d43cda82` equals the sha256 of `packages/mosaic/framework/tools/lease-broker/launch-runtime.py` at WI-3 #830 reviewed head `f400830738998db105107a2a4c69c7f2a2a6fd5d` (computed from two independent worktrees).
## Independence (G2)
Distinct Opus §3-conformance reviewer (Gate-16 author≠reviewer). I did not build this harness (author = ms-rev-826);
I am not Mos. This verdict is my own; the author did not author or edit it. Byte review only — **ran nothing** (no
harness, no broker, no sockets/state). Prior v3 PASS (`ace60667`, pinned `abd2791f`) is correctly **superseded**:
pinning a pre-`66b1e0a0` ancestor made D4 an in-memory socket simulation (hollow gate). v4 requires
mechanism-fidelity within isolation (Jason R1).
## BAR1 — Traceability (evidence attests the SHIPPED WI-3 D4 mechanism)
| Check | Result | Evidence |
|---|---|---|
| `GATED_WI_HEAD == f4008307` (not `abd2791f`) | ✅ | :33 |
| Launcher pinned by git-HEAD **and** sha256 | ✅ | precondition :285-288 (`head != GATED_WI_HEAD` raise; `sha256(launcher) != GATED_LAUNCHER_SHA256` raise); sha256 independently == f4008307's `launch-runtime.py` |
| Launcher bytes contain the file-backed mechanism | ✅ | precondition requires `register_anchor` :290, `initialize_runtime_generation(generation_file, generation)` :291, `generation-{session_id}.state` :294, `MOSAIC_LEASE_GENERATION_FILE` :295, `read_runtime_generation`+`bump_runtime_generation` in `lease_generation.py` :296-299; order `register < initialize < execute` :302-303 |
## BAR2 — Fidelity (file-backed generation, not in-memory simulation)
| # | Requirement | Result | Evidence |
|---|---|---|---|
| i | Extension bumps `generation-{sid}.state` via the real helper, not in-mem | ✅ | ext `lifecycle()` calls `broker({action:'bump-generation'})` at every post-start boundary (harness :200-205); broker `bump-generation``bump_runtime_generation(generation_environment(identity))` (broker :119-120) |
| ii | Broker reads generation via `read_runtime_generation`, not an in-mem counter | ✅ | broker loads the pinned module (`--generation-module`, :52-54); `file_generation()` = `read_runtime_generation(...)` (:77-78); authorize-probe reads `current_generation = file_generation(identity)` (:154); lifecycle `new_generation = file_generation(identity)` (:201). The in-mem `generations` dict is only an old-value cache for the monotonic guard (:199-204), never the authorization authority |
| iii | `assert_d4` observes the FILE-BACKED transition | ✅ | `state_file_source` = all records `generation_source=="state-file"` (harness :370-372); `state_file_drives_lifecycle` = state-bumps == lifecycle generations[1:] (:368-369); `new_generation_unverified→MUTATOR_UNVERIFIED` :382; `prior_generation_stale→STALE_GENERATION` :383; `verified_revoked_on_reload` :380-381 — not a socket-only bump |
| iv | `.state` bound to per-run fixture temp root | ✅ | broker `generation_environment` **raises if `state_path.parent != socket_path.parent`** (:73-74); `state_file_in_fixture_root` (:373-376); precondition forbids inherited `MOSAIC_LEASE_GENERATION_FILE` (:255-256) and requires socket in `gettempdir()` (:274-275); launcher/broker agree on `socket_path.parent / generation-{sid}.state` |
## BAR3 — Carry-over
| # | Result | Evidence |
|---|---|---|
| a LIVE-PATH (gated launcher @f4008307 at runtime, not released/plain execRuntime) | ✅ | Pi launched via `python3 <GATED_LAUNCHER> --runtime pi -- pi …` :502-505; `mosaic yolo`/`execRuntime` = 0 hits; register-before-exec producer in the invoked chain (precondition order gate) |
| b Fail-closed precondition present+correct | ✅ | `gated_launcher_precondition` :250-306, invoked :485 **before** broker/Pi; raises on socket≠fixture / gen-file-inherited / write-path-escape / head-mismatch / hash-mismatch / not-register-before-exec-file-bound |
| c Fixture-socket isolation, single p3 broker | ✅ | one broker Popen :486-500; `MOSAIC_LEASE_BROKER_SOCKET=socket_path` (fixture); direct launcher invocation ⇒ no default/XDG/`/run/user` fallback in path |
| d `lease_anchor_registered` INTACT | ✅ | broker emits event + `session_id_shape=="hex-256"` (:102-107); `record_where` requires it (:324-326); `lease_anchor_fixture` check (:379) — not deleted/softened/optional/repointed (not Case-C) |
## BAR4 — Homelab Gate-B carry-forward findings
| # | Result | Evidence |
|---|---|---|
| b4-1 gated launcher @f4008307, not released/plain execRuntime | ✅ | :502-505; 0 `mosaic yolo`/`execRuntime` |
| b4-2 **affirmative no-escape** (allow-list base, not deny-list) | ✅ | `isolated_environment` builds the child env from a **literal allow-list dict** (:442-461), NOT `os.environ.copy()`; only PATH/LANG/TERM/PI_CODING_AGENT (non-write-bearing) pass through; every write-bearing var (HOME/XDG*/TMPDIR/MOSAIC_AGENT_WORKDIR/HEARTBEAT_RUN_DIR/MOSAIC_HOME/D4_PI_LOG/socket) redirected under `root`; precondition double-checks each is `is_relative_to(root)` (:257-273). No unnamed/future inherited var survives |
| b4-3 `--runs` exactly 3, fail-closed otherwise | ✅ | `add_argument("--runs", type=int, default=3, choices=(3,))` :591 (argparse rejects any other value) |
| b4-4 cleanup try/finally spans the whole launch | ✅ | `broker=pi=None` :479-480; `try` opens **before** precondition/broker/PiRpc :482; nested `finally` always closes pi then broker+socket even on early `wait_path`/`PiRpc` failure :571-585 |
| b4-5 `-O`-safe integrity + derived PASS | ✅ | load-bearing checks in a `checks` dict; `if failed: raise AssertionError` :385-387 and `if not passed: raise` :388-390 (NO bare `assert` anywhere — grep-confirmed); PASS = `"PASS" if passed else "FAIL"` derived from `all(checks.values())` :393, re-derived+checked in `run_once` :550-553 |
## MUST-BE-ABSENT sweep
`P5` / `P6` / `P2-bank` / `retry-launder` / `mosaic yolo` / `execRuntime` / `run_open` / `atomic-observation` /
`pi_gate0_run` = **0 hits** (both files). Extension invokes only `bump-generation` / `lifecycle` /
`authorize-probe` / `promote-probe`. No live/prod/real-broker path (single fixture broker; allow-list env; launcher
pinned to fixture socket). No `.state`/gen-file path outside the fixture temp root (broker `generation_environment`
raises otherwise). §4 live effect: none. No extra broker/socket beyond the single p3. No exec-at-import (both files
`__main__`-guarded). Mechanism change is confined to the mandated R1 observation-fidelity deepening + BAR4 hardening;
no scope-widen of what the probe touches.
## Observations (transparency — not findings)
1. The fixture broker retains a **dormant `source-invalid` action** (:179-194, P5-adjacent, in-mem). It is
**never invoked** by the harness or its embedded extension (verified: extension actions are only
bump/lifecycle/authorize/promote), and `assert_d4` never observes it — so the probe does **not** exercise or bank
P5. Pre-existing shared-fixture code, unchanged. Surfaced so Mos may, if desired, apply a stricter
purge-dormant-P5-from-the-fixture standard; under the "what the probe TOUCHES/does" framing it is not a violation.
2. Fixture `HOME` receives a **read-only copy** of the operator's `~/.pi/agent` `settings.json`/`auth.json`/`bin/fd`
(:430-440, `shutil.copy2` into the fixture) so real Pi can authenticate to the model provider. It reads operator
state; it does not write/mutate operator HOME and does not emit/log credential material. Confined to the fixture.
## Verdict
**PASS @ `48484938`** — BAR1 (traceability to shipped f4008307 mechanism) + BAR2 (genuine file-backed generation,
iiv) + BAR3 (live-path / fail-closed precondition / isolation / intact assertion) + BAR4 (b4-1..b4-5) all hold,
with zero out-of-scope surface exercised. The v3 hollow-gate (ancestor pin, in-mem simulation) is structurally
closed: evidence now attests the shipped WI-3 D4 file-backed generation mechanism, launcher pinned by head+sha256,
child env write-confined by allow-list, integrity `-O`-safe with a derived PASS.
**Findings: none.**
## Scope reminder (not a finding)
Per §3/§5 of the amendment, producing this evidence **executes** the Gate0 mechanism. This review clears the
**bytes**; **FIRE remains separately gated on Mos's explicit post-clear GO** and is not authorized by this review.
---
**Reviewer:** independent Opus §3-conformance reviewer (Gate-16 author≠reviewer). Byte review only; ran nothing.
**Reviewed object (pin):** `484849387006ab5561798506fd6042ddbd5617de` · harness sha256 `9095eab7a4ddf11bb92bb5971d49e1facad12f4692ce2081665b0af47cbe5098`.
@@ -1,80 +0,0 @@
# Gate0 Probe-3 (D4) Hygiene-Delta — §3-Conformance Review v5
**Verdict: ❌ FAIL** (hygiene delta (a)+(b) landed correctly and (c)+(d) hold, but homelab findings **NEW-5** and **NEW-6** are present in these bytes; both must close for PASS).
## Pin (G1 — reviewed object, mandatory)
- **Reviewed object = `7f975b95ad39096463a7548bd6be0dbb387cb61b`** (harness commit, branch `feat/827-gate0-probe`).
- **Reviewed file:** `docs/compaction-refresh/probes/p3_d4_focused_run.py` — sha256 (pushed provider bytes, `-o FILE`, FULL-40 ref, verified before trust): `c3a09a342a4b367184d44472ec6fc11f8a3aabb7e90d5a72aa6b7044b1d9b91e` (24174 bytes, no not-found sentinel).
- **Co-reviewed fixture broker:** `p3_generation_broker.py` sha256 `4db4fef1ac6658a8ca79ad5091cefc901d2aa26003265c3d6726c294cf895cad`.
## Reviewer identity / lane (independence — on the record)
This review is produced by a **distinct independent Opus §3-conformance / SECREV session** (Gate-16 author≠reviewer),
**byte review only, ran nothing**, that **did not build** this harness (author = ms-rev-826) and **is not Mos**. The
PROCESS/LANE separation (build lane ≠ review lane) holds and is attested here. Homelab's separate observation — that
the published PASS commits and the repair commits share the `ms-lead-reviewer` **Git signer identity** — is a
git-identity-signer question I do **not** self-resolve; per instruction it is routed to Mos. My lane attestation is
independent of the git signer.
## Hygiene delta (v4 `48484938` → v5 `7f975b95`) — items (a)+(b): CLOSED
Full `diff` of both files shows the delta touches **only** these:
- **(a) Creds scrubbed — CLOSED.** New `scrub_fixture_credentials(root)` (:467-475): `shutil.rmtree(root/"home"/".pi"/"agent")` then a fail-closed re-check `if copied_agent.exists(): raise` (:473-474). Invoked in the **outermost `finally`** (:598), **after** pi/broker cleanup and **before** `return root` (:600) — so it runs on the **success path too**. The scrub target exactly matches the only copy site (`target_agent = fixture_home/".pi"/"agent"`, :433). `isolated_environment` was moved inside the `try` (:485) so the scrub finally also covers a partial env-setup failure. No copied agent credential/config subtree (incl. `auth.json`) survives in any retained root. ✅
- **(b) `source-invalid` handler REMOVED — CLOSED.** `grep source-invalid / source_invalidation_revoke == 0` across **both** files. Broker delta is solely the deletion of the old `:179-194` block. ✅
## (c) Everything-else byte-stable vs `48484938` — HOLDS
Diff confirms **no other hunks**. Re-swept intact: traceability `GATED_WI_HEAD=f4008307` (:33) + `GATED_LAUNCHER_SHA256=e950e4224e…` (:35); fidelity `read_runtime_generation`/`bump_runtime_generation` + `generation_source=="state-file"` + `new→MUTATOR_UNVERIFIED`/`prior→STALE_GENERATION`; `lease_anchor_registered` + `hex-256`; `-O`-safe (0 bare `assert`, PASS derived `all(checks.values())`); allow-list literal env (0 `os.environ.copy`); `--runs choices=(3,)`; fail-closed precondition; single p3 broker. Blockers 1-4 (inherited env / `--runs`≠3 / cleanup-misses-setup-failure / `-O`-strips-checks) remain closed.
## (d) ABSENT sweep — 0
`P5`/`P6`/`P2-bank`/`retry-launder`/`mosaic yolo`/`execRuntime`/`run_open`/`atomic`/`pi_gate0` = 0 (both files);
extension invokes only `bump-generation`/`lifecycle`/`authorize-probe`/`promote-probe`; no exec-at-import.
## Homelab carry-forward — NEW-5 / NEW-6: **PRESENT → FAIL**
### 🔴 NEW-6 — GATED_WI_ROOT resolves wrong / non-portably (CONFIRMED)
`GATED_WI_ROOT = HERE.parents[3].parent / "stack-cr-wi3-revoke"` (:32). For the harness at
`…/agent-work/stack-cr-wi0-gate0/docs/compaction-refresh/probes/`, this computes
**`/home/hermes/stack-cr-wi3-revoke` — which does not exist**. The actual f4008307 worktree is
`/home/hermes/agent-work/stack-cr-wi3-revoke` = `HERE.parents[3] / "stack-cr-wi3-revoke"`. The traversal is
**off-by-one** (`.parents[3].parent` should be `.parents[3]`), and it additionally hardcodes the worktree name — a
non-portable relative assumption. On this host the precondition therefore fail-closes ("gated WI launcher is
unavailable") and the probe cannot locate/execute the pinned launcher at all. **Real resolution defect** (independently
computed by path arithmetic; harness not run). **file:line — :32.**
*Fix:* derive `GATED_WI_ROOT` from a portable, explicit anchor (e.g. an env-provided path validated to be the
f4008307 worktree, or `HERE.parents[3] / "stack-cr-wi3-revoke"` with existence+HEAD assertion), not `.parents[3].parent`.
### 🔴 NEW-5 — launcher precondition is check-then-exec, not pinned-executed-bytes (CONFIRMED)
The precondition hashes `launcher_bytes = GATED_LAUNCHER.read_bytes()` (:280) against `GATED_LAUNCHER_SHA256` (:287),
but the launcher is **executed separately** via `PiRpc([sys.executable, str(GATED_LAUNCHER), …])` (:514-515), which
opens and **re-reads the file at exec time**. There is **no fd-handoff and no exec-from-verified-copy**, so the
verified snapshot does **not** bind the executed bytes. The window between check (:287) and exec (:514-515) spans the
broker `Popen` + `wait_path` (≤20 s) — a genuine **check-then-exec TOCTOU / mutable-path trust**; the `git rev-parse
HEAD` check (:285-286) is likewise on a mutable HEAD, not the executed bytes. Per the bar this is a real gap.
**file:line — hash :280/:287 vs exec :514-515.**
*Fix:* execute the exact verified bytes with no window — e.g. read once, verify, and exec from a fixture-private
copy of the verified bytes (or `python3 /proc/self/fd/<verified-fd>`), so the hashed bytes == executed bytes.
## Verdict
**FAIL @ `7f975b95`.** The hygiene delta itself is correct — (a) creds scrub (fail-closed finally, success path,
every retained root) and (b) `source-invalid` removal both landed cleanly, (c) everything else is byte-stable vs
`48484938`, and (d) the absent sweep is 0. **However**, homelab's NEW-5 (check-then-exec launcher TOCTOU / not
pinned-executed-bytes) and NEW-6 (GATED_WI_ROOT off-by-one/non-portable resolution) are **present in these bytes**;
the addendum requires both **closed** for PASS. Not softened. Returns to author (ms-rev-826) — not to a builder
re-review, no PASS-launder.
**Findings:** NEW-6 (`p3_d4_focused_run.py:32`); NEW-5 (`p3_d4_focused_run.py:280/:287` vs `:514-515`).
## Scope reminder (not a finding)
Producing this evidence **executes** the Gate0 mechanism (§3/§5). This review clears **bytes** only; FIRE remains
separately gated on Mos's explicit post-clear GO — and is moot until this FAIL is remediated.
---
**Reviewer:** independent Opus §3-conformance/SECREV reviewer (Gate-16 author≠reviewer). Byte review only; ran nothing.
**Reviewed object (pin):** `7f975b95ad39096463a7548bd6be0dbb387cb61b` · harness sha256 `c3a09a342a4b367184d44472ec6fc11f8a3aabb7e90d5a72aa6b7044b1d9b91e`.
@@ -1,116 +0,0 @@
# GATE0 Probe-3 (#827) — Mos byte-scope-verify CO-ATTESTATION (v-final)
**Principal:** Mos (orchestrator, merge authority for the mosaic-stack governance lane).
**Committed under a DISTINCT git identity** (`[email protected]`) — deliberately NOT the
`[email protected]` lane signer — so this record stands as a *distinct-identity*
co-attestation, not a same-signer duplicate. See "Independence" below.
**Verify class:** independent provider-byte read (guarded `git show <full-40>:path | sha256sum` from a
read-only clone of `mosaicstack/stack`). Not a re-build, not a re-run — a byte/scope/hygiene audit of
the exact committed objects on the provider branch.
## Package under attestation
| Artifact | Ref |
|---|---|
| Branch | `feat/827-gate0-probe` |
| Harness commit-40 | `2d54a9dd14cb924701b2ae4ed72dae4df760c4e3` |
| Harness `p3_d4_focused_run.py` sha256 | `15a154df55273f51301763a984485fd63813f6d1f05d2728abb9fb8b9c040b1a` (27366 B) |
| §3-review-v6 commit-40 | `23c0caca9b5d44002e6184cd7f2b6c837e8795b2` |
| Review path | `docs/compaction-refresh/reviews/GATE0-PROBE3-NEW56-S3-REVIEW-v6.md` |
sha256 re-confirmed against the checked-out object at `HEAD:docs/compaction-refresh/probes/p3_d4_focused_run.py`
(git object id `8c68cd07…`) — matches the relayed value byte-for-byte.
## Findings — VERDICT: byte-scope + mechanism + hygiene **PASS**
**Anchors.** Harness sha256 matches (27366 B). review-v6 (`23c0caca`) parent == harness commit
`2d54a9dd`; review touches only the review `.md` (+82 lines, 1 file). Broker
(`p3_generation_broker.py`) delta vs `48484938…` = **exactly** the 16-line `action=="source-invalid"`
handler purge, byte-stable otherwise.
**NEW-6 (GATED_WI_ROOT off-by-one) — CLOSED.** `resolve_gated_wi_root()` selects the worktree by
`git worktree list --porcelain` enumeration, requires a UNIQUE match on `HEAD==GATED_WI_HEAD`
(`f400830738998db105107a2a4c69c7f2a2a6fd5d`) AND `branch==refs/heads/feat/830-compaction-revoke`,
then fail-closed re-validates (`is-inside-work-tree==true`, `rev-parse HEAD==GATED_WI_HEAD`);
`RuntimeError` on ambiguity/mismatch. The `HERE.parents[3].parent / "stack-cr-wi3-revoke"` off-by-one
and the hardcoded `/home/hermes/...` literal are **gone** — portable, zero hardcoded path.
**NEW-5 (TOCTOU / pinned-executed-bytes) — CLOSED via approach (i), as mandated.** The `git`-object
sha256 pin (`GATED_LAUNCHER_SHA256 = e950e422…`) is the trust anchor. Ordering/marker `.find()`
heuristics are downgraded to explicitly diagnostic-only ("never a substitute for the pin"). In
`launch_verified_pi()` the executed working-tree file is re-hashed against the pin **in the statement
immediately before `Popen`** (no interleaved yield/IO), and the launcher is executed **in place at the
pinned worktree path** — the higher-risk approach (ii) copy-to-fixture (previously at `7ff63cd5` /
`6164dc07`) is **reverted** (the only remaining `shutil.copy2` is the legitimate credential copy, not a
launcher copy). Residual sub-statement TOCTOU window on a local file inside a non-adversarial operator
fixture is within this probe's threat model; the gross precondition→much-later-exec gap homelab flagged
is closed.
**Hygiene — CLOSED.** `scrub_fixture_credentials(root)` removes the entire `.pi/agent` subtree in a
`finally` block (nested try/finally, after `pi.close()` + broker shutdown, before `return root`) and
`RuntimeError`s if the scrub fails — credentials are removed from retained evidence; logs retained.
**Invariants byte-stable (all INTACT):** assertion `lease_anchor_registered`; file-backed fidelity
checks (`generation_source=="state-file"`, `state_file_in_fixture_root`, `MUTATOR_UNVERIFIED`,
`STALE_GENERATION`); `-O`-safe (0 bare `assert`); allow-list env (0 `os.environ.copy`); single broker;
`--runs choices=(3,)`; ABSENT-sweep (`yolo`/`execRuntime`/`p3_bank`/`promote_p2`/`retry`) = 0.
**R1 mechanism-fidelity boundary — HELD.** D4 exercises #830's real file-backed revocation
(`generation-{sid}.state` / `MOSAIC_LEASE_GENERATION_FILE`, from `66b1e0a0`); `.state` stays inside the
fixture temp root; fixture-socket / child-write-escape / gettempdir isolation preconditions preserved;
launcher exec is in place at the pinned worktree (same surface as R1, not a new production/live
surface). **No path escapes the fixture root.** BEYOND-R1 tripwire NOT tripped.
## Independence
- **Substantive principal-independence** of review-v6 is satisfied by an orchestrator-dispatched,
builder-distinct Opus SECREV (`ms-secrev-828`, byte-only, non-builder) — that IS the substance of
Gate-16.
- The shared `ms-lead-reviewer` git signer on harness+review commits is **evidentiary, not
substantive**. It is resolved by (1) this Mos co-attestation committed under a **distinct** identity
(`mos-orchestrator`), and (2) a homelab third-principal verify under its own distinct identity —
i.e. three distinct-identity principals of record.
- The shared signer is a tracked **fleet-infra tooling-gap** (durable fix = per-lane distinct signers),
**not a blocker**.
## Scope of this record — byte-clear, NOT fire-authorization
Producing probe evidence **executes** the Gate0 mechanism; a byte-clear is not a fire-authorization.
This co-attestation clears the **bytes/scope/hygiene**. FIRE remains gated on: **homelab
third-principal verify** + **Mos transparency-to-Jason** (real-Pi consumes operator model creds inside
the isolated fixture, scrubbed post-run, never emitted) + **Mos explicit FIRE GO**. Until then: nothing
banked, WI-3 #830 held at `f4008307` (unmoved), C-hatch armed (if 3× isolation still no-fire /
wrong-value / isolation-FAIL → possible Case-C → STOP + escalate to Jason).
**Mos verdict: byte-scope + mechanism + hygiene PASS. Co-attestation of record — committed.**
---
## ⚠️ SUPERSEDED — homelab third-principal FAIL raised a stricter bar (evidence-integrity note)
This co-attestation was **byte-clear on the v6 bar ONLY** and self-limited above to *"byte-clear
ONLY, NOT fire-authorization; FIRE remains gated on homelab third-principal verify."* Homelab (the
required third principal) subsequently returned **FAIL @2d54a9dd**, and Mos **UPHELD** it — so the v6
byte-clear this document records is **SUPERSEDED** and does **NOT** authorize FIRE.
Homelab's substantively-correct deepening (accepted as gate-**strengthening**, not softening):
1. `launch_verified_pi` hashes the launcher then `Popen`/execve **reopens the path** → statement
adjacency shrinks but does not eliminate TOCTOU; hashed-snapshot ≠ executed-bytes.
2. `lease_generation.py` helper is unpinned, loaded from the mutable worktree → HEAD + launcher-pin
do not bind the helper bytes.
3. `p3_generation_broker.py` executes from the mutable worktree unhashed → reviewed broker bytes need
not be the evidence-producing bytes.
For a fail-closed DO-178C evidence gate, **hashed==executed must hold on the FULL executed closure**
(launcher + helper + broker), which v6 (approach (i) adjacency) does not meet. Mos therefore
**authorized approach (ii) full-closure materialization** (SHA-pin + materialize the full closure into
a fixture-private 0700/0600 dir or held verified fds, exec from there, launcher+broker consume the same
pinned helper; re-hash==f4008307 pins immediately before exec, fail-closed). This rides the existing R1
authorization + Mos adjudication authority (it deepens isolation of already-authorized touch and stays
inside the fixture temp root → R1 owner tripwire not tripped; no fresh owner window).
**Live target = v7** (materialized-closure harness, forthcoming). `2d54a9dd` / `23c0caca` / this
co-attestation (`12914d8`) are **SUPERSEDED**. A fresh Mos co-attestation will be committed on v7
byte-verify PASS. WI-3 #830 remains HELD at `f4008307`; nothing banked; C-hatch armed.
@@ -1,97 +0,0 @@
# GATE0 Probe-3 (#827) — Mos byte-scope-verify CO-ATTESTATION (v10 no-site startup closure)
**Principal:** Mos (orchestrator, merge authority for the mosaic-stack governance lane).
**Committed under a DISTINCT git identity** (`[email protected]`) — deliberately NOT the
`[email protected]` lane signer that authored the harness and the §3 review — so this
record stands as a *distinct-identity* co-attestation. See "Independence".
**Verify class:** independent provider-byte read (`git show <full-40>:path | sha256sum`) plus a
git-diff byte-comparison of the v9→v10 delta and source inspection of the executed command lines. Not
a re-build, not a re-run. This SUPERSEDES the v9 co-attestation `f320d075` (byte-clear on the v9
env-iso + bytecode-pin bar), which was overtaken by homelab's 4th-round FAIL @1c34e3cb
(site-startup-closure hole) and the resulting B9 bar.
## Package under attestation
| Artifact | Ref |
|---|---|
| Branch | `feat/827-gate0-probe` (mosaicstack/stack) |
| Harness commit-40 | `ce5ba762051354338889959bfce2b0381f4a4e2a` |
| Harness `p3_d4_focused_run.py` sha256 | `7e14ead89a7b2a297fcc17e7653291b3bcace1d2002a8f90a989db74f6985b6f` (32753 B) |
| §3-review-v10 commit-40 | `ffc3b573a8258a86e3ece9ef92bac925fb5a7a5b` (parent == harness `ce5ba762`, byte-stable; adds only the review `.md`, +86; ms-secrev-828 authored) |
| Review path | `docs/compaction-refresh/reviews/GATE0-PROBE3-V10-NOSITE-S3-REVIEW-v10.md` |
**Pinned closure (all 3 sha256 INDEPENDENTLY recomputed by Mos from git objects == pins == embedded harness constants; UNCHANGED from v9):**
| Member | Source (commit:path) | Pin sha256 | Size |
|---|---|---|---|
| launcher `launch-runtime.py` | `f4008307:packages/mosaic/framework/tools/lease-broker/launch-runtime.py` | `e950e422…` | 4237 B |
| helper `lease_generation.py` | `f4008307:packages/mosaic/framework/tools/lease-broker/lease_generation.py` | `061625402f…` | 3769 B |
| broker `p3_generation_broker.py` | `23c0caca:docs/compaction-refresh/probes/p3_generation_broker.py` | `4db4fef1…` | — |
> `f4008307` (held WI-3 #830 gated head) is **not on origin** — recomputed launcher+helper from the
> local `stack-cr-wi3-revoke` worktree (HEAD == `f4008307`); broker from origin `23c0caca`.
## Findings — VERDICT: byte-scope + mechanism + hygiene **PASS** (v10 = v9 + B9 no-site)
The v9→v10 delta is **exactly 2 insertions / 0 deletions** (git-diff byte-compared), a `+28 B`
harness growth **fully accounted** (2 × ` "-S",\n` = 14 B each). Every prior invariant is
byte-stable — the diff touches nothing outside the two command lists.
**B9 — no-site startup closure (homelab 4th-round finding CLOSED).** Both child command lists now
carry `-S`, which disables the `site` module entirely (no `.pth` executable lines, no
`sitecustomize`/`usercustomize`) so no unpinned startup code runs before the exact launcher/broker or
outside `closure_import_guard`:
- launcher `launch_verified_pi` (`:515`): `-s` + **`-S`** + `-B`, **no `-I`** (`-I` appears exactly
once in the harness, `:553`, the broker only).
- broker `launch_verified_broker` (`:554`): `-I` + **`-S`** + `-B`.
Homelab's finding was **independently reproduced by Mos** and the fix **empirically verified** on host
Py3.11.2 (throwaway, not the harness): `[-s -B]``no_site=0, site imported` (the v9 hole); `[-I -B]`
`no_site=0, site imported` (proves `-I` does **not** imply `-S`); `[-s -S -B]``no_site=1`, `site` not
in `sys.modules`, **and the launcher's bare `from lease_generation import` still resolves** (`sys.path[0]`
untouched by `-S` → no B6c regression); `[-I -S -B]``no_site=1`. The launcher deliberately omits `-I`
(B6c: on 3.11+ `-I` implies `-P`, dropping the script dir from `sys.path[0]` → sibling import breaks);
its env isolation comes from the `PiRpc` `env=` allow-list, not `-I`.
**All priors — byte-stable (outside the 2-line delta, re-confirmed from the v9 verify):**
B5 conjunction (materialize-from-pin / `mkdir(0o700)`+`O_EXCL` no-writer-window / re-hash==pin
immediately-before-exec); B6 (single pinned helper bound; `closure_import_guard` AST-reject); B6c
(launcher no `-I`); B7 (broker `env=environment` strict allow-list `:570`); B8 (`reject_pinned_bytecode`
fail-closed `:498-501` before each consumer `:535/:562` + `PYTHONDONTWRITEBYTECODE=1` `:729` + `-B` on
both); fidelity asserts (`generation_source=='state-file'`, `state_file_in_fixture_root`,
`MUTATOR_UNVERIFIED`, `STALE_GENERATION`); `lease_anchor_registered`; BAR1 `GATED_WI_HEAD==f4008307`
(`:36`) + `merge-base --is-ancestor 66b1e0a0 f4008307` = **YES**; `--runs choices=(3,)`; `-O`-safe
(0 bare `assert`); allow-list env (0 `os.environ.copy`); single broker (1 def + 1 call); the only
`shutil.copy2` is the `.pi/agent` credential copy.
**Closure = exactly 3 files, materialized inside the fixture root.** No path escapes the fixture temp
root; no live/default broker; `.state` fixture-bound. **R1 owner tripwire NOT tripped**`-S`
deepens startup-closure isolation of an already-authorized touch; it does not widen the touched surface.
## Independence
Substantive principal-independence of review-v10 is satisfied by an orchestrator-dispatched,
builder-distinct Opus SECREV (`ms-secrev-828`, byte-only, non-builder, non-Mos). The shared
`ms-lead-reviewer` git signer on harness+review commits is evidentiary, not substantive — resolved by
(1) this Mos co-attestation under a **distinct** identity (`mos-orchestrator`) and (2) a homelab
third-principal verify under its own distinct identity = three distinct-identity principals of record.
The prior 2-of-3 (`ms-secrev-828` v9 §3 PASS + `f320d075`) does **not** carry — all three re-verify
this v10 SHA. Shared signer = tracked fleet-infra tooling-gap, not a blocker.
## Scope of this record — byte-clear, NOT fire-authorization
Producing probe evidence **executes** the Gate0 mechanism; a byte-clear is not a fire-authorization.
This clears **bytes / scope / mechanism / hygiene on the v10 (v9 + B9 no-site) bar**. FIRE remains
gated on: **homelab third-principal re-verify** (5th round, own distinct identity) + **Mos
transparency-to-Jason** + **Mos explicit FIRE GO**. The FIRE GO additionally carries an
**execution-procedure constraint**: the 3× isolation dispatch must launch the runner under
externally-enforced **`python -I -S -B p3_d4_focused_run.py`** — a self-reexec is too late, the
harness's own `site` runs before it could re-add `-S` to itself. Until FIRE GO: nothing banked, WI-3
#830 held at `f4008307` (unmoved), C-hatch armed (fired-rig only: no-fire / wrong-value /
assertion-FAIL / isolation-FAIL → possible Case-C → STOP + escalate to Jason).
Prior v10-superseded set: `1c34e3cb` / `e1c9a468` / `f320d075` (and transitively the v7 chain).
**Mos verdict: v10 (v9 + B9 no-site) byte-scope + mechanism + hygiene PASS. Co-attestation of
record — committed.**
@@ -1,131 +0,0 @@
# GATE0 Probe-3 (#827) — Mos byte-scope-verify CO-ATTESTATION (v7 full-closure)
**Principal:** Mos (orchestrator, merge authority for the mosaic-stack governance lane).
**Committed under a DISTINCT git identity** (`[email protected]`) — deliberately NOT the
`[email protected]` lane signer that authored both the harness and the §3 review — so this
record stands as a *distinct-identity* co-attestation. See "Independence" below.
**Verify class:** independent provider-byte read (guarded `git show <full-40>:path | sha256sum`) plus
source-level inspection of the executed mechanism. Not a re-build, not a re-run. This SUPERSEDES the v6
co-attestation `12914d8` (and its SUPERSEDED-note `b6bd0cd`), which was byte-clear on the v6 bar only
and was overtaken by homelab's third-principal FAIL @2d54a9dd + the resulting stricter full-closure bar.
## Package under attestation
| Artifact | Ref |
|---|---|
| Branch | `feat/827-gate0-probe` |
| Harness commit-40 | `f609a44953f5ae61916805fcb45ca337de00b0b0` |
| Harness `p3_d4_focused_run.py` sha256 | `0f1bd1b39399b32f243d901230e2d840794a2144edd723a095dab716833a7a9b` (32071 B) |
| §3-review-v7 commit-40 | `2bba933f67c821899d320a938a9473a73a136422` (adds only the review `.md`; harness parent byte-stable) |
| Review path | `docs/compaction-refresh/reviews/GATE0-PROBE3-V7-FULLCLOSURE-S3-REVIEW-v7.md` |
**Pinned closure (all 3 sha256 INDEPENDENTLY recomputed by Mos from git objects == pins):**
| Member | Source (commit:path) | Pin sha256 | Size |
|---|---|---|---|
| launcher `launch-runtime.py` | `f4008307:packages/mosaic/framework/tools/lease-broker/launch-runtime.py` | `e950e422…` | 4237 B |
| helper `lease_generation.py` | `f4008307:packages/mosaic/framework/tools/lease-broker/lease_generation.py` | `061625402f…` | 3769 B |
| broker `p3_generation_broker.py` | `23c0caca:docs/compaction-refresh/probes/p3_generation_broker.py` | `4db4fef1…` | — |
> Note: `f4008307` (the held WI-3 #830 gated head) is **not on origin** — it exists only as a local
> `git worktree` on the build host. Mos recomputed the launcher+helper pins from that worktree
> (`stack-cr-wi3-revoke`, HEAD == `f4008307`) rather than passing over a clone-completeness gap. The
> broker pin was recomputed from origin `23c0caca`.
## Findings — VERDICT: byte-scope + mechanism + hygiene **PASS** (v7 full-closure bar)
Homelab's stricter bar — **hashed==executed on the FULL executed closure (launcher + helper + broker)**
— is met. Verified at the source, not accepted on the review's assertion:
**B5 conjunction (the load-bearing repair) — HELD, all three legs:**
- **(a) materialized from pinned git-object bytes, NOT the mutable worktree.** `materialize_closure`
fetches each member via `git_object_bytes` (`git show {commit}:{path}`), then
`sha256(data) == pin` **fail-closed** (`RuntimeError` on mismatch) before use.
- **(b) no writable window hash→consume.** `pinned/` is `mkdir(mode=0o700)`; each file is written with
`os.open(O_WRONLY|O_CREAT|O_EXCL|O_CLOEXEC, 0o600)``O_EXCL` refuses a pre-planted file. No `chmod`,
no `os.rename/replace`, no `symlink`, and nothing re-opens a pinned file for write (grep = 0). The
pinned bytes are immutable within the fixture threat model between hash and exec.
- **(c) re-hash == pin IMMEDIATELY before each exec, no interleaved yield.** Launcher: re-hash then
`return PiRpc(command,…)` whose `__init__` **first statement** is `subprocess.Popen(command,…)`
zero IO/yield/reopen between. Broker + helper: both re-hashed then `subprocess.Popen` on the next
line. Adjacency-alone-without-materialization (the v6 defect) is **absent** — all three exec from
`pinned/`.
**B6 — helper pinned AND bound to the SAME single copy.** The broker receives
`--generation-module {closure.generation}` (the pinned helper); the launcher runs from `pinned/` so its
`import lease_generation` resolves to the sibling pinned copy via `sys.path[0]`. `closure_import_guard`
AST-parses every member and raises on any non-stdlib import other than the allowed `lease_generation`
— proving the dependency closure is complete and no unpinned module can enter at runtime.
**Closure = exactly 3 files, materialized inside the fixture root** (`root / "pinned"`). No path escapes
the fixture temp root; no live/default broker; `.state` remains fixture-bound. **R1 owner tripwire NOT
tripped** — this deepened isolation of an already-authorized touch, it did not widen the touched surface.
**Invariants (all INTACT):** BAR1 `GATED_WI_HEAD == f4008307` and `merge-base --is-ancestor 66b1e0a0
f4008307` = YES (file-backed `.state` revocation fidelity present); BAR2 `.state` =
`socket_path.parent / generation-{sid}.state`, `state_file_in_fixture_root` + `generation_source ==
"state-file"` checks present; BAR3 `lease_anchor_registered` / live-path / fixture-socket isolation
intact. `-O`-safe (0 bare `assert`); ABSENT-sweep (`yolo`/`execRuntime`/`p3_bank`/`promote_p2`/`retry`)
= 0; single broker (1 def + 1 call site); `--runs choices=(3,)`; allow-list env (0 `os.environ.copy`);
the only `shutil.copy2` is the legitimate `.pi/agent` credential copy (settings/auth/fd), **not** a
launcher/helper/broker copy — the v6 copy-to-fixture concern is gone.
## Independence
- **Substantive principal-independence** of review-v7 is satisfied by an orchestrator-dispatched,
builder-distinct Opus SECREV (`ms-secrev-828`, byte-only, non-builder) — that IS the substance of
Gate-16.
- The shared `ms-lead-reviewer` git signer on both the harness (`ms-rev-826` build) and the review
commit is **evidentiary, not substantive**. It is resolved by (1) this Mos co-attestation under a
**distinct** identity (`mos-orchestrator`), and (2) a homelab third-principal verify under its own
distinct identity — three distinct-identity principals of record. Tracked fleet-infra tooling-gap
(durable fix = per-lane distinct signers), **not a blocker**.
## Scope of this record — byte-clear, NOT fire-authorization
Producing probe evidence **executes** the Gate0 mechanism; a byte-clear is not a fire-authorization.
This co-attestation clears the **bytes / scope / mechanism / hygiene on the v7 full-closure bar**. FIRE
remains gated on: **homelab third-principal re-verify** (under its own distinct identity) + **Mos
transparency-to-Jason** + **Mos explicit FIRE GO**. Until then: nothing banked, WI-3 #830 held at
`f4008307` (unmoved), C-hatch armed (if the materialized-closure rig still no-fire / wrong-value /
assertion-FAIL / isolation-FAIL → possible Case-C → STOP + escalate to Jason).
**Mos verdict: v7 full-closure byte-scope + mechanism + hygiene PASS. Co-attestation of record —
committed.**
---
## ⚠️ SUPERSEDED — homelab v7 third-principal FAIL @f609a449 raised a stricter bar (evidence-integrity note)
This co-attestation was **byte-clear on the v7 full-closure bar ONLY** and self-limited above to
*"byte-clear NOT fire-authorization; FIRE remains gated on homelab third-principal re-verify."*
Homelab (the required third principal) subsequently returned **FAIL @f609a449** (static verify, no
code run), and Mos **UPHELD** it after independently confirming both findings in source — so the v7
byte-clear this document records is **SUPERSEDED** and does **NOT** authorize FIRE.
Two residual isolation/binding holes WITHIN the materialized closure (both independently reproduced
by Mos in the harness source; accepted as gate-**strengthening**, not softening):
1. **Broker child env not isolated.** `launch_verified_broker` (`:551`) calls `Popen` with **no
`env=`** (only `PiRpc.__init__` `:50` passes an allow-listed `env`) → the broker child inherits
ambient `os.environ` (PYTHONPATH/PYTHONHOME/PYTHONPYCACHEPREFIX). `closure_import_guard` is a
static AST check and cannot bind the child's runtime stdlib resolution.
2. **Executed bytecode-cache outside the pin.** No `PYTHONDONTWRITEBYTECODE`/`-I`/`-B`/`__pycache__`
handling anywhere. `exec_module` on the pinned helper writes derived `.pyc` the pin never covers;
only the `.py` is re-hashed → executed bytecode ≠ pinned-source-hash.
Both break "hashed==executed on the FULL executed closure" on **fidelity** grounds even in a
non-adversarial fixture. Mos **authorized the bounded repair** (broker `Popen` with strict
allow-listed `env=` + `-I` + `PYTHON*` stripped; bytecode-cache suppressed via
`PYTHONDONTWRITEBYTECODE=1`/`-B` + reject stray `__pycache__`/`.pyc` fail-closed before each
consumer; launcher sibling-import binding to the pinned helper preserved). This **rides the existing
(ii)-full-closure authorization + R1 + Mos adjudication** — it deepens isolation/binding of an
already-authorized touch, stays inside the fixture temp root, no fresh Jason owner-window. It is
**NOT a Case-C escalation** (no probe fired, no evidence produced — a static pre-fire catch, exactly
what the review gate is for). Added review bars **B7** (broker child env-isolated) and **B8**
(executed bytecode pinned-or-suppressed) on top of B5+B6+all priors.
**Live target = v8** (env-isolated + bytecode-pinned harness, forthcoming). `f609a449` /
`2bba933f` / this co-attestation (`e08ad03`) are **SUPERSEDED**. A fresh Mos co-attestation will be
committed on v8 byte-verify PASS. WI-3 #830 remains HELD at `f4008307`; nothing banked; C-hatch
armed; NO FIRE.
@@ -1,145 +0,0 @@
# GATE0 Probe-3 (#827) — Mos byte-scope-verify CO-ATTESTATION (v9 full-closure, env-isolated + bytecode-pinned)
**Principal:** Mos (orchestrator, merge authority for the mosaic-stack governance lane).
**Committed under a DISTINCT git identity** (`[email protected]`) — deliberately NOT the
`[email protected]` lane signer that authored the harness and the §3 review — so this
record stands as a *distinct-identity* co-attestation. See "Independence".
**Verify class:** independent provider-byte read (`git show <full-40>:path | sha256sum`) plus
source-level inspection of the executed mechanism and the v7→v9 delta. Not a re-build, not a re-run.
This SUPERSEDES the v7 co-attestation `e08ad03` (and its SUPERSEDED note `2ae379e`), which was
byte-clear on the v7 bar and was overtaken by homelab's third-principal FAIL @f609a449 (broker
env-isolation + executed-bytecode-cache) and the resulting B7/B8 bar.
## Package under attestation
| Artifact | Ref |
|---|---|
| Branch | `feat/827-gate0-probe` |
| Harness commit-40 | `1c34e3cb3172acdcd094e683e847d7c984afc96c` |
| Harness `p3_d4_focused_run.py` sha256 | `29e5c7bfbe1911b52984bd94c79036bb1200ee82588318367b13c2b1053a0103` (32725 B) |
| §3-review-v9 commit-40 | `e1c9a4682da2892ca5f5381012caffe1dd7b43a7` (parent == harness `1c34e3cb`, byte-stable; adds only the review `.md`; ms-secrev-828 authored) |
| Review path | `docs/compaction-refresh/reviews/GATE0-PROBE3-V9-LAUNCHERFIX-S3-REVIEW-v9.md` |
**Pinned closure (all 3 sha256 INDEPENDENTLY recomputed by Mos from git objects == pins == embedded harness constants):**
| Member | Source (commit:path) | Pin sha256 | Size |
|---|---|---|---|
| launcher `launch-runtime.py` | `f4008307:packages/mosaic/framework/tools/lease-broker/launch-runtime.py` | `e950e422…` | 4237 B |
| helper `lease_generation.py` | `f4008307:packages/mosaic/framework/tools/lease-broker/lease_generation.py` | `061625402f…` | 3769 B |
| broker `p3_generation_broker.py` | `23c0caca:docs/compaction-refresh/probes/p3_generation_broker.py` | `4db4fef1…` | — |
> `f4008307` (held WI-3 #830 gated head) is **not on origin** — recomputed launcher+helper from the
> local `stack-cr-wi3-revoke` worktree (HEAD == `f4008307`); broker from origin `23c0caca`.
## Findings — VERDICT: byte-scope + mechanism + hygiene **PASS** (v9 = v7 full-closure + B7 + B8)
The v7→v9 delta is **exactly 22 insertions / 2 deletions**, confined to the intended B7+B8+B6c
surface; every prior invariant is byte-stable (outside the delta) from the v7 verify.
**B7 — broker child env-ISOLATED (homelab finding 1 CLOSED).** `launch_verified_broker` (`:568`) now
passes `env=environment` (the strict allow-list, `:570`) — the ambient-`os.environ`-inheritance hole
is gone — AND runs the broker with `-I` (`:552`, isolated: ignores `PYTHON*`/user-site) + `-B`
(`:553`). Both children are env-controlled: the launcher was already `env=env` at `PiRpc` (`:53`).
**B8 — executed bytecode PINNED/SUPPRESSED (homelab finding 2 CLOSED).** `reject_pinned_bytecode`
(`:498`) raises `RuntimeError` fail-closed if a `__pycache__` dir or any `*.pyc` exists in the pinned
dir, and is called before **each** consumer (launcher `:535`, broker `:562`). Bytecode writes are
disabled via `PYTHONDONTWRITEBYTECODE=1` (`:729`) in the allow-list env **and** `-B` on both command
lines. No unpinned `.pyc` can be executed; only the pinned `.py` re-hash governs.
**B6c — launcher sibling-import PRESERVED (v8 regression FIXED).** v8 over-applied `-I` to the
launcher; on Py3.11+ `-I` implies `-P`, dropping the script dir from `sys.path[0]`, so the pinned
launcher's bare `from lease_generation import` (launch-runtime.py:15) would `ModuleNotFoundError`. v9
uses `-s` (`:514`) + `-B` (`:515`) on the launcher (NO `-I`) — neither touches `sys.path[0]`, so the
sibling import still resolves to `pinned/lease_generation.py`. **Mos empirically re-verified on host
Py3.11.2** (throwaway, not the harness): `-I` launcher → `ModuleNotFoundError`; `-s`+`PYTHONNOUSERSITE`
→ import OK. The launcher's env isolation comes from the `PiRpc` `env=` allow-list, NOT `-I`, so
dropping `-I` does **not** reopen B7. My earlier constraint-(c) assumption ("`-I` does not strip the
script dir") was FALSIFIED for 3.11+; the author≠reviewer gate (ms-secrev-828) caught it — recorded.
**B5 conjunction (load-bearing repair) — HELD, all three legs (byte-stable from v7):**
(a) materialized from pinned git-object bytes via `materialize_closure`/`git_object_bytes`, `sha256==pin`
fail-closed; (b) `pinned/` `mkdir(0o700)` + `O_EXCL|O_CLOEXEC` `0o600`, no writable window — now also
`reject_pinned_bytecode` closes the `.pyc` side-channel; (c) re-hash == pin IMMEDIATELY before each
exec, no interleaved yield: launcher re-hash (`:538`) → `return PiRpc(command,…)` whose `__init__`
first statement is `Popen` (`:50`); broker re-hash (`:563`) + helper re-hash (`:566`) → `Popen`
(`:568`) on the next line.
**B6 — single pinned helper, complete closure.** Broker gets `--generation-module {closure.generation}`;
launcher resolves `import lease_generation` to the sibling pinned copy via `sys.path[0]`.
`closure_import_guard` (`:351`, called `:433`) AST-rejects any non-stdlib import other than
`lease_generation`. Single broker: `launch_verified_broker` 1 def (`:543`) + 1 call (`:766`).
**Invariants (all INTACT):** BAR1 `GATED_WI_HEAD == f4008307` (`:36`) and `merge-base --is-ancestor
66b1e0a0 f4008307` = YES (file-backed `.state` revocation fidelity present); fidelity
`generation_source=='state-file'` (`:639`), `state_file_in_fixture_root` (`:641`),
`MUTATOR_UNVERIFIED` (`:650`), `STALE_GENERATION` (`:651`); `lease_anchor_registered` (`:593`);
`-O`-safe (0 bare `assert`); `--runs choices=(3,)` (`:838`); allow-list env (0 `os.environ.copy`);
ABSENT-sweep (`yolo`/`execRuntime`/`p3_bank`/`promote_p2`/`retry`) = 0; the only `shutil.copy2`
(`:708`) is the `.pi/agent` credential copy, not a closure copy.
**Closure = exactly 3 files, materialized inside the fixture root.** No path escapes the fixture temp
root; no live/default broker; `.state` fixture-bound. **R1 owner tripwire NOT tripped** — B7/B8
deepen isolation/binding of an already-authorized touch, they do not widen the touched surface.
## Independence
Substantive principal-independence of review-v9 is satisfied by an orchestrator-dispatched,
builder-distinct Opus SECREV (`ms-secrev-828`, byte-only, non-builder, non-Mos). The shared
`ms-lead-reviewer` git signer on harness+review commits is evidentiary, not substantive — resolved by
(1) this Mos co-attestation under a **distinct** identity (`mos-orchestrator`) and (2) a homelab
third-principal verify under its own distinct identity = three distinct-identity principals of record.
Shared signer = tracked fleet-infra tooling-gap (durable fix = per-lane distinct signers), not a blocker.
## Scope of this record — byte-clear, NOT fire-authorization
Producing probe evidence **executes** the Gate0 mechanism; a byte-clear is not a fire-authorization.
This clears **bytes / scope / mechanism / hygiene on the v9 (full-closure + B7 + B8) bar**. FIRE
remains gated on: **homelab third-principal re-verify** (4th round, own distinct identity) + **Mos
transparency-to-Jason** + **Mos explicit FIRE GO**. Until then: nothing banked, WI-3 #830 held at
`f4008307` (unmoved), C-hatch armed (materialized-closure rig still no-fire / wrong-value /
assertion-FAIL / isolation-FAIL → possible Case-C → STOP + escalate to Jason).
**Mos verdict: v9 full-closure + B7 + B8 byte-scope + mechanism + hygiene PASS. Co-attestation of
record — committed.**
---
## ⚠️ SUPERSEDED — homelab v9 4th-round FAIL @1c34e3cb raised a stricter *startup-closure* bar
This co-attestation was **byte-clear on the v9 (env-iso + bytecode-pin) bar ONLY** and self-limited
above to *"byte-clear NOT fire-authorization; FIRE remains gated on homelab 4th-round re-verify."*
Homelab (the required third principal) returned **FAIL @1c34e3cb** (static, nothing executed), and Mos
**UPHELD** it after independently confirming the finding in-source AND empirically on host Py3.11.2 —
so the v9 byte-clear this document records is **SUPERSEDED** and does **NOT** authorize FIRE.
**Residual startup-closure hole (empirically reproduced by Mos; accepted as gate-STRENGTHENING):**
neither child carries `-S`, so CPython imports the `site` module **before** the script runs. `-s`
(launcher) suppresses only *user*-site; `-I` (broker) implies `-s -E -P` but **NOT** `-S`. Proven:
[-s -B ] no_site=0 site_imported=True ← v9 launcher: site runs
[-I -B ] no_site=0 site_imported=True ← v9 broker: -I does NOT imply -S
[-s -S -B] no_site=1 site_imported=False ← v10 launcher fix (sibling import STILL resolves)
[-I -S -B] no_site=1 site_imported=False ← v10 broker fix (additive)
System-site executable `.pth` lines + sitecustomize/usercustomize can therefore run **unpinned startup
code** before the exact launcher/broker and **outside** `closure_import_guard`, while every hash +
`reject_pinned_bytecode` + import-guard still pass — defeating hashed==executed on the full *startup*
closure (strictly wider than the module-import closure v9 cleared). A genuine fidelity hole for a
fail-closed DO-178C evidence gate.
Mos **authorized the bounded v10 repair**: add `-S` to the **launcher** (keep `-s -B`, NOT `-I`) and
to the **broker** (keep `-I -B`) — a minimal 2-line delta; no B6c regression (launcher `-s -S -B`
sibling import empirically intact; `-S` does not touch `sys.path[0]`). Added review bar **B9**
(no-site startup closure). This **rides the existing (ii)-full-closure authorization + R1 + Mos
adjudication** (deepens startup-closure isolation of an already-authorized touch, inside the fixture
temp root, no fresh Jason owner-window) and is **NOT a Case-C escalation** (static pre-fire catch, no
probe fired). A separate **FIRE-time** constraint is captured: the 3× isolation dispatch must launch
the runner under externally-enforced `python -I -S -B` (a self-reexec is too late).
**Live target = v10** (no-site harness, forthcoming). `1c34e3cb` / `e1c9a468` / this co-attestation
(`f320d075`) are **SUPERSEDED**; the prior 2-of-3 (ms-secrev-828 v9 §3 PASS + `f320d075`) does NOT
carry — all three distinct-identity principals re-verify the new v10 SHA. A fresh Mos co-attestation
will be committed on v10 byte-verify PASS. WI-3 #830 remains HELD at `f4008307`; nothing banked;
C-hatch armed (fired-rig only); NO FIRE.
@@ -1,82 +0,0 @@
# Gate0 Probe-3 (D4) NEW-5/NEW-6 Closure — §3-Conformance Review v6
**Verdict: ✅ PASS**
## Pin (G1 — reviewed object, mandatory)
- **Reviewed object = `2d54a9dd14cb924701b2ae4ed72dae4df760c4e3`** (harness commit, branch `feat/827-gate0-probe`, approach-**(i)** build; supersedes the reverted (ii) copy-to-fixture builds `7ff63cd5`/`6164dc07`, which were NOT reviewed to a verdict).
- **Reviewed file:** `docs/compaction-refresh/probes/p3_d4_focused_run.py` — sha256 (pushed provider bytes, `-o FILE`, FULL-40 ref, verified before trust): `15a154df55273f51301763a984485fd63813f6d1f05d2728abb9fb8b9c040b1a` (27366 bytes, no not-found sentinel).
- **Co-reviewed fixture broker:** `p3_generation_broker.py` sha256 `4db4fef1ac6658a8ca79ad5091cefc901d2aa26003265c3d6726c294cf895cad`**byte-identical to v5** (unchanged by this delta).
## Principal-independence attestation (Mos independence ruling — process of record)
This review is produced by a **distinct Opus SECREV session, orchestrator-dispatched** — the **`ms-secrev-828`
reviewer lane, dispatched by `mosaic-100`** — **byte review only, ran nothing**, and **did NOT build** this harness
(builder = ms-rev-826). Author ≠ reviewer (Gate-16). This is one of three principals: **Mos commits his own
distinct-identity byte-scope-verify co-attestation at v-final**, and **homelab's independent verify is the third
principal**. (The `ms-lead-reviewer` **Git signer identity** shared across published review commits is a git-signer
question routed to Mos; it does not bear on this lane's process/dispatch independence, attested here.)
## NEW-5 CLOSED — approach (i): exact-byte pin, adjacent re-hash, exec in place
- **Exact-byte sha256 is the trust anchor, NOT substring heuristics.** Launcher bytes are read from the immutable
git object (`git show f4008307:<path>`) and gated on `sha256(launcher_bytes) == GATED_LAUNCHER_SHA256`
(`e950e422…`) (:354). The `behavior_markers` `in`-checks (:364-372) are explicitly commented "the exact launcher
digest above is the trust anchor. These marker checks are diagnostic belt-and-suspenders only, never a substitute
for the pin" (:362-363). The old ordered `.find()` heuristic (`register < initialize < execute`, min<0) is **gone**.
- **Final re-hash immediately adjacent to `Popen`, no interleaved yield.** `launch_verified_pi` assembles `command`,
then — as the statement **immediately before** `return PiRpc(command, …)` (which performs the `Popen`) — re-hashes
the launcher: `if hashlib.sha256(launcher.read_bytes()).hexdigest() != GATED_LAUNCHER_SHA256: raise` (:411-412),
`return PiRpc(...)` (:413). **No harness-controlled step (no `wait_path`, no broker spawn) sits between the re-hash
and the exec** — the broker `Popen` + `wait_path` occur *before* `launch_verified_pi` is called (:611-621). Window
narrowed to the fork/exec itself.
- **Exec stays IN PLACE at the pinned f4008307-worktree path.** The precondition returns the worktree paths
`gated_root / launcher_relative`, `gated_root / generation_relative` (:378); Pi execs `str(launcher)` = that
worktree launch-runtime.py (:390,:621), and the broker `--generation-module` = the worktree lease_generation.py
(:614). The reverted (ii) machinery is **gone**: `grep pinned-lease-broker / PYTHONPATH / fixture_launcher /
fixture_generation / write_bytes == 0`. Launcher import resolution and the file-backed fidelity surface are
therefore **unperturbed** (this is the lower-risk approach Mos mandated over copy-to-fixture).
## NEW-6 CLOSED — portable, validated, off-by-one gone
`GATED_WI_ROOT` is no longer the off-by-one `HERE.parents[3].parent / "stack-cr-wi3-revoke"`. It is resolved by
`resolve_gated_wi_root()` (:257-307): an explicit `GATED_WI_ROOT` env override, else **repo-relative** `git worktree
list --porcelain` (from `repository_root()`, first parent containing `.git`) selecting the **unique** worktree whose
`HEAD == f4008307` **and** `branch == refs/heads/feat/830-compaction-revoke` (raise if ambiguous/absent). It then
**fail-closes** unless `gated_root.is_dir()`, `git rev-parse --is-inside-work-tree == "true"` (:304-305), and
`HEAD == GATED_WI_HEAD` (:306-307). Independently recomputed on this host (git query, harness not run): it resolves
to the real worktree **`/home/hermes/agent-work/stack-cr-wi3-revoke`**. Portable + validated; the off-by-one is gone.
## Full v4/v5 carry-over re-sweep (byte-stable vs `7f975b95` except the NEW-5/6 delta)
`diff 7f975b95 → 2d54a9dd` confines changes to launcher resolution (NEW-6) + adjacent-rehash-exec-in-place (NEW-5);
nothing else moved. Re-swept intact: **creds-scrub** (`scrub_fixture_credentials` + outermost `finally`); **`source-invalid`
ABSENT** (grep=0 both files); **fidelity file-backed** unperturbed (broker `read_runtime_generation`/`bump_runtime_generation`
on the fixture `.state`; `assert_d4` `generation_source=="state-file"` + `state_file_drives_lifecycle` +
`state_file_in_fixture_root` + `new→MUTATOR_UNVERIFIED`/`prior→STALE_GENERATION`); **`lease_anchor_registered`** INTACT
(event + `hex-256`); **live-path** gated launcher; **fail-closed precondition**; **fixture-socket isolation**; **`-O`-safe**
(0 bare `assert`, PASS derived); **allow-list env** (0 `os.environ.copy`); **`--runs choices=(3,)`**; **single p3 broker**
(broker byte-identical to v5). **ABSENT sweep = 0** (P5/P6/P2-bank/retry-launder/mosaic-yolo/execRuntime/pi_gate0/run_open/
atomic; extension actions only bump/lifecycle/authorize/promote; no exec-at-import). **Beyond-R1 tripwire: not tripped**
exec is in place, imports and the file-backed observation surface untouched; no isolation crossing.
## Verdict
**PASS @ `2d54a9dd`.** NEW-5 (approach (i): exact-byte sha256 pin as trust anchor; adjacent re-hash immediately
before `Popen` with no interleaved yield; exec in place at the pinned f4008307-worktree path; (ii) copy-to-fixture/
PYTHONPATH machinery reverted) and NEW-6 (portable, validated, off-by-one-gone root resolution) are both **closed**;
the full v4/v5 carry-over holds byte-stable except the two intended surfaces; ABSENT sweep is 0; the R1 file-backed
fidelity surface is unperturbed. Zero out-of-scope surface.
**Findings: none.**
## Scope reminder (not a finding)
Producing this evidence **executes** the Gate0 mechanism (§3/§5). This review clears **bytes** only; **FIRE remains
separately gated on Mos's explicit post-clear GO** and is not authorized by this review.
---
**Reviewer:** distinct Opus SECREV session (`ms-secrev-828` lane, dispatched by `mosaic-100`), Gate-16 author≠reviewer,
byte review only; ran nothing; did not build.
**Reviewed object (pin):** `2d54a9dd14cb924701b2ae4ed72dae4df760c4e3` · harness sha256 `15a154df55273f51301763a984485fd63813f6d1f05d2728abb9fb8b9c040b1a`.
@@ -1,50 +0,0 @@
# Gate0 Probe-3 Pre-Audit v12
**Author:** ms-p3-fix
**Scope:** Static audit only of the D4 fixture-construction and precondition path in `docs/compaction-refresh/probes/p3_d4_focused_run.py`. No harness execution occurred.
## Pinned closure verified
| Member | Pinned revision | SHA-256 |
| --- | --- | --- |
| `launch-runtime.py` | `f400830738998db105107a2a4c69c7f2a2a6fd5d` | `e950e4224e280f16979d90cabb89aa1896c5ee28bed2df957e14d018d43cda82` |
| `lease_generation.py` | `f400830738998db105107a2a4c69c7f2a2a6fd5d` | `061625402f08488eac47acd23272904e71fd1a71fd15b3bdab158632c801be4c` |
| `p3_generation_broker.py` | `23c0caca9b5d44002e6184cd7f2b6c837e8795b2` | `4db4fef1ac6658a8ca79ad5091cefc901d2aa26003265c3d6726c294cf895cad` |
The launcher and generation bytes were read from the exact `f4008307` object in the resolved WI-3 worktree. The broker bytes were checked from this repository's exact `23c0caca` object. Their SHA-256 values match the three unchanged pin constants.
## Behavior-marker audit
| Harness marker / check | Result | Satisfying real closure byte location |
| --- | --- | --- |
| `"action": "register_anchor"` | PASS | `launch-runtime.py:77` emits the registration action. |
| `initialize_generation: Callable[[Path, int], None] = initialize_runtime_generation` | PASS | `launch-runtime.py:49` is the DI binding to the imported real helper; import is at `launch-runtime.py:15`, and the bound parameter is invoked at `launch-runtime.py:90`. |
| `execute(command[0], command, environment)` | PASS | `launch-runtime.py:101`. |
| `source_environment["MOSAIC_LEASE_BROKER_SOCKET"]` | PASS | `launch-runtime.py:70`. |
| `socket_path.parent / f"generation-{session_id}.state"` | PASS | `launch-runtime.py:89`; broker independently retains the same fixture-parent construction at `p3_generation_broker.py:99`. |
| `environment["MOSAIC_LEASE_GENERATION_FILE"]` | PASS | `launch-runtime.py:98`. |
| `def read_runtime_generation` | PASS | `lease_generation.py:78`; broker loads and calls it at `p3_generation_broker.py:37-41`, `77-78`, and `133`. |
| `def bump_runtime_generation` | PASS | `lease_generation.py:90`; broker loads and calls it at `p3_generation_broker.py:37-41` and `119-120`. |
## Fixture and precondition-path audit
| Harness precondition | Result | Static construction / real closure satisfaction |
| --- | --- | --- |
| Fixture broker socket equals `MOSAIC_LEASE_BROKER_SOCKET` | PASS | Harness sets the variable from `socket_path` at `p3_d4_focused_run.py:723` and rejects mismatch at `:452-453`. Launcher consumes that exact variable at `launch-runtime.py:70`; broker accepts the fixture value via `--socket` at `p3_generation_broker.py:46-50` and binds it at `:60-63`. |
| `MOSAIC_LEASE_GENERATION_FILE` is absent before launcher execution | PASS | The isolated environment dictionary has no such key (`p3_d4_focused_run.py:712-737`) and precondition rejects any inherited value at `:454-455`. The launcher creates and assigns it only after successful registration and initialization (`launch-runtime.py:74-90`, `95-99`). |
| `HOME` is fixture-relative | PASS | Constructed as `root / "home"` at `p3_d4_focused_run.py:680,713`; validated by the fixture-path loop at `:456-471`. Launcher copies the source environment only after receiving this fixture value (`launch-runtime.py:68,95`). |
| `XDG_CONFIG_HOME` is fixture-relative | PASS | `root / "config"` at `p3_d4_focused_run.py:681,714`; validated at `:456-471`. |
| `XDG_CACHE_HOME` is fixture-relative | PASS | `root / "cache"` at `p3_d4_focused_run.py:682,715`; validated at `:456-471`. |
| `XDG_STATE_HOME` is fixture-relative | PASS | `root / "state"` at `p3_d4_focused_run.py:683,716`; validated at `:456-471`. |
| `XDG_RUNTIME_DIR` is fixture-relative | PASS | `root / "runtime"` at `p3_d4_focused_run.py:684,717`; validated at `:456-471`. |
| `TMPDIR` is fixture-relative | PASS | `root / "tmp"` at `p3_d4_focused_run.py:685,718`; validated at `:456-471`. |
| `D4_PI_LOG` is fixture-relative | PASS | The run creates `root / "pi.jsonl"` at `p3_d4_focused_run.py:758` and assigns it at `:724`; validated at `:456-471`. |
| `MOSAIC_AGENT_WORKDIR` is fixture-relative | PASS | The run creates `root / "workspace"` at `p3_d4_focused_run.py:755` and assigns it at `:726`; validated at `:456-471`. Launcher executes with the copied fixture environment at `launch-runtime.py:95-101`. |
| `MOSAIC_HEARTBEAT_RUN_DIR` is fixture-relative | PASS | `root / "heartbeat"` at `p3_d4_focused_run.py:687,727`; validated at `:456-471`. |
| `MOSAIC_HOME` is fixture-relative | PASS | `root / "mosaic-home"` at `p3_d4_focused_run.py:688,728`; validated at `:456-471`. |
| Socket parent is this temporary root | PASS | The run derives `socket_path = root / "generation.sock"` at `p3_d4_focused_run.py:757`; precondition requires `socket_path.parent == root` and `root.parent == tempfile.gettempdir()` at `:472-474`. Broker additionally refuses a generation path whose parent differs from `socket_path.parent` at `p3_generation_broker.py:71-75`. |
| Complete pinned closure can be materialized without an unpinned project import | PASS | `materialize_closure` reads the three exact objects and verifies their hashes (`p3_d4_focused_run.py:397-431`); `closure_import_guard` allows only `lease_generation` as non-stdlib (`:342-371`). Launcher has that sole project import at `launch-runtime.py:15`; broker imports its supplied generation file using an explicit path at `p3_generation_broker.py:29-41`. |
**All remaining preconditions pass against real closure bytes: Y.**
No B5-B9/D4 assertion, closure hash gate, before-exec re-hash, fixture/socket precondition, bytecode rejection, launch/broker flag, or run-count setting was changed by this audit artifact.
@@ -1,86 +0,0 @@
# Gate0 Probe-3 (D4) No-Site Startup Closure — §3-Conformance Review v10
**Verdict: ✅ PASS**
## Pin (GUARD 1 — reviewed object)
- **Reviewed object = `ce5ba762051354338889959bfce2b0381f4a4e2a`** (harness commit, branch `feat/827-gate0-probe`).
- **Reviewed file:** `docs/compaction-refresh/probes/p3_d4_focused_run.py` — sha256 (pushed provider bytes, `-o FILE`, FULL-40 ref, verified before trust): `7e14ead89a7b2a297fcc17e7653291b3bcace1d2002a8f90a989db74f6985b6f` (32753 bytes, no not-found sentinel).
- **Closure pins (unchanged):** launcher `e950e422…` @f4008307 · helper `061625402f08488eac47acd23272904e71fd1a71fd15b3bdab158632c801be4c` @f4008307 · broker `4db4fef1…` @23c0caca.
## Independence (GUARD 2 — principal-independence attestation)
Distinct Opus SECREV session, orchestrator-dispatched — the **`ms-secrev-828` reviewer lane, dispatched by
`mosaic-100`** — **byte review only, ran nothing** (harness/broker not executed); did **not** build this harness
(builder = ms-rev-826); is not Mos; distinct principal from both. This re-verifies from scratch on the v10 SHA after
homelab's 4th-round FAIL @`1c34e3cb` (no `-S``site` startup-closure hole) superseded my v9 PASS + Mos's co-attest.
The `-S`/`-s`/`-I` behavior checks below use a *throwaway* script to observe interpreter startup — not the harness.
## ★ B9 — No-site startup closure (the homelab 4th-round FAIL)
The delta vs `1c34e3cb` is **exactly two `-S` insertions**, byte-confirmed by `diff` (nothing else; +28 B fully
accounted by the two ` "-S",\n` lines):
- **(i) Launcher command** (`launch_verified_pi`, :512-516): `sys.executable, "-s", "-S", "-B", str(launcher), …`
carries `-s` + **`-S`** + `-B`, and **no `-I`**.
- **(ii) Broker command** (`launch_verified_broker`, :551-554): `sys.executable, "-I", "-S", "-B", str(broker_path), …`
— carries `-I` + **`-S`** + `-B`.
- **(iii) `site` not imported at child startup** — empirically confirmed (Python 3.11.2, throwaway script):
`python3 -s -S -B main.py``sys.flags.no_site == 1`, `'site' in sys.modules == False`; `python3 -I -S -B main.py`
`no_site == 1`. So system-site `.pth` executable lines and `sitecustomize`/`usercustomize` **cannot run unpinned
startup code** before the pinned launcher/broker. (Contrast without `-S`: `python3 -s -B``no_site == 0`, `site`
imported — the exact v9 hole this closes.)
- **(iv)** No harness reliance on any site-injected path/hook (env is the constructed allow-list; children execute
pinned bytes).
- **(v) `-S` does not touch `sys.path[0]`** (unlike `-I`/`-P`) — empirically confirmed: `python3 -s -S -B main.py`
keeps `sys.path[0]` = the script's directory, so the launcher's bare `from lease_generation import
initialize_runtime_generation` (`f4008307:launch-runtime.py:15`) **still binds `pinned/lease_generation.py`** with
`-S` present. The broker's explicit `--generation-module` import (via `importlib`) binds the pinned helper
regardless of `sys.path`/site, so `-I -S` is correct there.
- **(vi) Delta = exact 2-line `-S` only** vs `1c34e3cb` (git-diff/byte-compared, not accepted on assertion).
## All prior bars — byte-stable (delta was only the two `-S` lines)
- **B6(c):** launcher still carries no `-I`; sibling import binds `pinned/` (confirmed above with `-S` present). ✅
- **B7:** broker `Popen` `env=environment` (allow-list, **not** `os.environ`; no `PYTHONPATH`/`PYTHONHOME`/
`PYTHONPYCACHEPREFIX`) + `-I`. ✅
- **B8:** `reject_pinned_bytecode` fail-closed before each consumer; `PYTHONDONTWRITEBYTECODE=1` + `PYTHONNOUSERSITE=1`
in env; `-B` on both children. ✅
- **B5:** 3-leg conjunction — materialize each of launcher/helper/broker from git-object bytes with `sha256==pin`
fail-closed; `pinned/` `0o700` in fixture root, files `O_EXCL 0o600` (no writable window); re-hash `==pin`
immediately before each `Popen`; launcher + broker consume the same single pinned helper. ✅
- **B6:** `closure_import_guard` AST present; single pinned helper; broker `--generation-module = closure.generation`. ✅
- **Fidelity:** extension bumps `generation-{sid}.state` via `MOSAIC_LEASE_GENERATION_FILE` (not in-mem); broker
`read_runtime_generation`; `assert_d4` `generation_source=="state-file"` / `state_file_in_fixture_root` /
`new→MUTATOR_UNVERIFIED` / `prior→STALE_GENERATION`; `.state` fixture-root-bound. ✅
- **Traceability:** `GATED_WI_HEAD == f4008307` + `merge-base --is-ancestor 66b1e0a0 f4008307`. ✅
- `lease_anchor_registered` + `hex-256` INTACT; LIVE-PATH (pinned gated launcher); single p3 broker;
promotion=fixture-only; `--runs choices=(3,)`; `-O`-safe (0 bare `assert`); `copy2` = creds-only; allow-list env
(0 `os.environ.copy`). ✅
## ABSENT sweep
P5/P6/P2-bank/retry-launder/mosaic-yolo/execRuntime = 0; `source-invalid` = 0; no live/real-broker path; no `.state`
outside fixture root; no exec-at-import (`__main__`-guarded); no adjacency-only-exec-from-worktree; the only change is
the authorized `-S` no-site isolation-deepening (no mechanism change, no scope-widen); `-O`-safe.
## Verdict
**PASS @ `ce5ba762`.** B9 (no-site startup closure) is closed — both children carry `-S`, `site` is not imported at
startup (so system-site `.pth`/`sitecustomize` cannot execute unpinned code before the pinned launcher/broker),
`-S` leaves `sys.path[0]` intact so the launcher sibling import and the broker explicit-path import both still bind
the pinned helper, and the delta vs `1c34e3cb` is exactly the two `-S` insertions. All prior bars (B5/B6/B6c/B7/B8/
fidelity/traceability/lease_anchor/live-path/single-broker/promotion/`--runs`/`-O`-safe) are byte-stable. Zero
out-of-scope surface. **Findings: none.**
## Scope reminder (not a finding)
Producing this evidence **executes** the Gate0 mechanism (§3/§5). This review clears **bytes** only; **FIRE remains
separately gated on Mos's explicit post-clear GO**, his byte-scope-verify co-attestation, and homelab's
third-principal verify — not authorized by this review.
---
**Reviewer:** distinct Opus SECREV session (`ms-secrev-828` lane, dispatched by `mosaic-100`), Gate-16 author≠reviewer,
byte review only; ran nothing.
**Reviewed object (pin):** `ce5ba762051354338889959bfce2b0381f4a4e2a` · harness sha256 `7e14ead89a7b2a297fcc17e7653291b3bcace1d2002a8f90a989db74f6985b6f`.
@@ -1,120 +0,0 @@
> ⚠ SUPERSEDED by v8 (homelab 3rd-principal FAIL @f609a449: broker env-inherit + unpinned .pyc; PASS overtaken by stricter B7+B8 bar). This v7 PASS record is NOT fire-authorization.
# Gate0 Probe-3 (D4) Full-Closure Materialization — §3-Conformance Review v7
**Verdict: ✅ PASS** *(superseded — see banner above)*
## Pin (GUARD 1 — reviewed object)
- **Reviewed object = `f609a44953f5ae61916805fcb45ca337de00b0b0`** (harness commit, branch `feat/827-gate0-probe`).
- **Reviewed file:** `docs/compaction-refresh/probes/p3_d4_focused_run.py` — sha256 (pushed provider bytes, `-o FILE`, FULL-40 ref, verified before trust): `0f1bd1b39399b32f243d901230e2d840794a2144edd723a095dab716833a7a9b` (32071 bytes, no not-found sentinel).
- **Closure pins (independently recomputed from the git objects):**
- launcher `packages/mosaic/framework/tools/lease-broker/launch-runtime.py` @ `f4008307` = `e950e4224e280f16979d90cabb89aa1896c5ee28bed2df957e14d018d43cda82`
- **helper** `packages/mosaic/framework/tools/lease-broker/lease_generation.py` @ `f4008307` = `061625402f08488eac47acd23272904e71fd1a71fd15b3bdab158632c801be4c`
- **broker** `docs/compaction-refresh/probes/p3_generation_broker.py` @ `23c0caca` = `4db4fef1ac6658a8ca79ad5091cefc901d2aa26003265c3d6726c294cf895cad`
## Independence (GUARD 2 — principal-independence attestation)
This review is produced by a **distinct Opus SECREV session, orchestrator-dispatched — the `ms-secrev-828`
reviewer lane, dispatched by `mosaic-100`** — **byte review only, ran nothing**, that **did NOT build** this harness
(builder = ms-rev-826) and **is not Mos**. Three distinct principals: this reviewer, the builder, and Mos (whose
own distinct-identity byte-scope-verify follows); homelab's independent verify is a further principal — it is
homelab's third-principal FAIL @`2d54a9dd` (upheld by Mos) that correctly retired the approach-(i) adjacency
re-hash and authorized this full-closure. v6/`2d54a9dd`/`23c0caca`/`12914d8` are superseded.
## Why v7 (the reopen-after-hash hole)
Approach (i) re-hashed the launcher then let `Popen` **reopen the worktree path** — hashed-snapshot ≠ executed-bytes
(the worktree file is a shared, same-UID-mutable path). Statement adjacency alone did not bind. v7 closes it for the
**full project-code closure** (launcher + `lease_generation.py` helper + `p3_generation_broker.py`).
## B5 — HASHED == EXECUTED on the full closure (binding conjunction, stated verbatim)
The reopen-after-hash shape is unavoidable for imported/exec'd files, so closure rests on the **conjunction of all
three legs**, each byte-verified here:
> **(a)** bytes are materialized **from the pinned git-object @ `f4008307`** (helper/launcher) and **@ `23c0caca`**
> (broker) — `git show <commit>:<path>`, the trusted immutable object, **never the mutable worktree file**; **AND**
> **(b)** into a **fixture-private `0o700` dir with `0o600` files created via `O_CREAT|O_EXCL`** — no writer exists in
> the threat model between hash and exec; **AND** **(c)** each member is **re-hashed == its pin IMMEDIATELY before
> exec/import, fail-closed (`RuntimeError`)**.
Byte evidence:
- **(a)** `git_object_bytes(git_root, commit, relative)` = `git show <commit>:<path>` (:322-327); `materialize_closure`
reads all three members from git objects and asserts `sha256(data) == digest` else `RuntimeError` (:415-433). Worktree
working-tree files are never read.
- **(b)** `pinned = root / "pinned"; pinned.mkdir(mode=0o700)` (:435-436); `write_pinned_file` uses
`os.open(path, O_WRONLY|O_CREAT|O_EXCL|O_CLOEXEC, 0o600)` (:379-382). **No `os.chmod`/`os.rename`/`shutil.move`
anywhere** (grep=0); `O_EXCL` refuses a pre-planted file/symlink, so no symlink-follow or hijack gap; the dir is a
fresh per-run `mkdtemp` child, owner-only. **No code re-opens the pinned files for write between materialize and
consume** — there is no writable window.
- **(c)** launcher re-hash `sha256(launcher.read_bytes()) == GATED_LAUNCHER_SHA256` is the statement immediately before
`return PiRpc(command,…)` (:528-530); broker **and** helper re-hashes (`== GATED_BROKER_SHA256`,
`== GATED_GENERATION_SHA256`) are the two statements immediately before `return subprocess.Popen(command,…)`
(:546-551). No interleaved harness yield.
Adjacency-only exec-from-worktree is **absent** for every member (all three exec/import from `pinned/`; grep worktree-exec=0).
## B6 — helper + broker pinned and bound to execution (one shared helper)
`materialize_closure` writes exactly **one** `pinned/lease_generation.py` (:442). The broker executes the **pinned**
broker with `--generation-module = closure.generation` = that pinned helper (`launch_verified_broker`, :533-551, called
:746-747). The launcher executes the **pinned** launcher (`python3 pinned/launch-runtime.py`), whose
`import lease_generation` resolves via `sys.path[0]` = the script's own `pinned/` dir to the **same** sibling
`pinned/lease_generation.py`. Launcher-import and broker-`--generation-module` therefore resolve the **same single
pinned helper copy**, not two copies and not the worktree. Worktree helper/broker are not re-read at runtime.
**Closure-import guard:** `closure_import_guard` AST-parses each member and refuses any non-stdlib import outside the
allow-set `{"lease_generation"}` (and any relative import) → `RuntimeError` (:341-364). The 3-member closure is
therefore provably complete — no unpinned project-code dependency can slip in.
## BAR1 — Traceability
`GATED_WI_HEAD == f4008307`; the precondition asserts `git merge-base --is-ancestor 66b1e0a0 f4008307` (:315-330),
independently confirmed **YES** — the pinned launcher forward-contains the `66b1e0a0` file-backed generation mechanism.
## BAR2 — Fidelity file-backed, `.state` in fixture root, UNTOUCHED
`pinned/` holds **code bytes only** (launcher/helper/broker). The `.state` generation file is written by the launcher
to `socket_path.parent` (the fixture root), **not** `pinned/`. The broker (pinned, byte-identical `4db4fef1`) still
enforces `generation_environment` raising if `state_path.parent != socket_path.parent` (grep=2), and `assert_d4`
still checks `state_file_source == "state-file"` / `state_file_drives_lifecycle` / `state_file_in_fixture_root` +
`new→MUTATOR_UNVERIFIED` / `prior→STALE_GENERATION` (grep=4, unchanged). The v7 change did not move `.state` into
`pinned/` or perturb these asserts.
## BAR3 — Carry-over
(a) **live-path:** Pi launched via `python3 pinned/launch-runtime.py --runtime pi -- pi …` (gated register-before-exec);
`mosaic yolo`/`execRuntime` = 0. (b) **fail-closed precondition:** `gated_launcher_precondition` (resolve+materialize+
verify) runs before any launch, fail-closed. (c) **fixture-socket isolation:** `MOSAIC_LEASE_BROKER_SOCKET` = per-run
fixture socket; single pinned p3 broker serves `register_anchor`; no live/default broker reachable; non-destructive.
(d) **`lease_anchor_registered` INTACT:** event + `session_id_shape=="hex-256"` unchanged (broker byte-identical);
`assert_d4` folds it into the single-identity set — not deleted/softened/optional/repointed.
## Re-confirm + ABSENT sweep
Spawns ONLY the single pinned p3 broker; promotion=fixture-only; full D4 asserts; `--runs choices=(3,)`; allow-list
env (0 `os.environ.copy`); **`-O`-safe** (all new checks `RuntimeError`, **0 bare `assert`**); creds-scrub intact;
non-destructive (fixture tempdir only); deterministic (git objects + fixed pins); closure-import guard present.
**ABSENT = 0:** P5/P6/P2-bank/retry-launder/mosaic-yolo/execRuntime/pi_gate0; `source-invalid` grep=0; no live/real-broker
path; no `.state`/gen path outside the fixture root; no extra broker/socket; no exec-at-import (`__main__`-guarded); no
adjacency-only exec-from-worktree for any member; the only mechanism change is materialization; no scope-widen.
## Verdict
**PASS @ `f609a449`.** B5 (full-closure hashed==executed via the (a)+(b)+(c) conjunction with no writable window),
B6 (one shared pinned helper bound to both launcher-import and broker-`--generation-module`; complete closure), BAR1,
BAR2 (fidelity `.state`-in-fixture-root untouched), and BAR3 all hold, with zero out-of-scope surface. **Findings: none.**
## Scope reminder (not a finding)
Producing this evidence **executes** the Gate0 mechanism (§3/§5). This review clears **bytes** only; **FIRE remains
separately gated on Mos's explicit post-clear GO**, his v-final byte-scope-verify co-attestation, and homelab's
third-principal verify — not authorized by this review.
---
**Reviewer:** distinct Opus SECREV session (`ms-secrev-828` lane, dispatched by `mosaic-100`), Gate-16 author≠reviewer,
byte review only; ran nothing; did not build.
**Reviewed object (pin):** `f609a44953f5ae61916805fcb45ca337de00b0b0` · harness sha256 `0f1bd1b39399b32f243d901230e2d840794a2144edd723a095dab716833a7a9b`.
**Pinned closure:** launcher `e950e422…` @f4008307 · helper `06162540…be4c` @f4008307 · broker `4db4fef1…` @23c0caca.
@@ -1,92 +0,0 @@
> ⚠ SUPERSEDED by v9: the reviewed harness `a92ad090` is superseded by the narrow fix `1c34e3cb` (my v8 B6(c) FAIL — `-I` on the launcher — was remediated by `-I``-s` + `PYTHONNOUSERSITE=1`; re-review v9 = PASS). This v8 FAIL record pertains to the superseded commit.
# Gate0 Probe-3 (D4) Broker Env-Isolation + Bytecode Binding — §3-Conformance Review v8
**Verdict: ❌ FAIL** (B7 and B8 land correctly, but the same change breaks **B6(c)**: the launcher is run with `-I`, which strips the script directory from `sys.path` on Python 3.11+, so its bare `import lease_generation` cannot resolve the pinned helper — empirically confirmed).
## Pin (GUARD 1 — reviewed object)
- **Reviewed object = `a92ad090ae3828c643f961c7628d809b8521185f`** (harness commit, branch `feat/827-gate0-probe`).
- **Reviewed file:** `docs/compaction-refresh/probes/p3_d4_focused_run.py` — sha256 (pushed provider bytes, `-o FILE`, FULL-40 ref, verified before trust): `915ebeb5aeab108cb60c5f629c1db520623ab4914eed427ca34ee66f9aa08390` (32614 bytes, no not-found sentinel).
- **Closure pins (unchanged from v7):** launcher `e950e422…` @f4008307 · helper `061625402f08488eac47acd23272904e71fd1a71fd15b3bdab158632c801be4c` @f4008307 · broker `4db4fef1…` @23c0caca.
## Independence (GUARD 2 — principal-independence attestation)
Distinct Opus SECREV session, orchestrator-dispatched — the **`ms-secrev-828` reviewer lane, dispatched by
`mosaic-100`** — **byte review only, ran nothing** (the harness/broker were not executed); did **not** build this
harness (builder = ms-rev-826); is not Mos. This verdict is my own. (The `-I` semantics check below runs a *throwaway*
two-line script to observe the interpreter's `sys.path` behavior — it does not run the harness, broker, or any part of
the reviewed closure.)
## 🔴 BLOCKING FINDING — B6(c) broken: `-I` on the launcher strips the pinned-helper import path
**File:line — `p3_d4_focused_run.py:512`** (the `"-I"` added to `launch_verified_pi`'s launcher command).
The pinned launcher `launch-runtime.py` @`f4008307` imports its helper with a **bare top-level import**:
`from lease_generation import initialize_runtime_generation` (launcher line 15) — no `sys.path` manipulation. Under
v7 this bound because `python3 pinned/launch-runtime.py` put the script's directory (`pinned/`) at `sys.path[0]`, so
the sibling `lease_generation` resolved to `pinned/lease_generation.py`.
v8 now runs the launcher as `python3 -I -B pinned/launch-runtime.py …` (:512-513). **`-I` implies `-P` (Python 3.11+),
which does NOT prepend the script's directory to `sys.path`.** Empirically confirmed on this host (Python 3.11.2),
using a throwaway script (not the harness):
```
python3 -I -B main.py → sys.path[0] = '/usr/lib/python311.zip'
import sibling → ModuleNotFoundError: No module named '…'
python3 -B main.py → sys.path[0] = '<script dir>' → sibling import: OK
```
Therefore, at FIRE on Python 3.11+, the launcher's line-15 `from lease_generation import …` raises
`ModuleNotFoundError` at module load — the pinned helper does **not** resolve (neither pinned nor worktree; the import
simply fails). **B6(c) — "launcher sibling-import to `pinned/` via `sys.path[0]` STILL BINDS" — does not hold.** The
build report's assertion "`-I` keeps script dir" is false on 3.11+, and could not have been observed under the
correct "never run" boundary.
Note: the env allow-list carries no `PYTHONPATH` (correct for B7), and `-I` ignores `PYTHON*` env regardless, so there
is no alternate resolution path — the launcher import is unrecoverable under `-I`.
**Fix:** remove `-I` from the **launcher** command only (keep `-B` + the `env=` allow-list — the launcher's
env-isolation is already provided by the constructed allow-list, which contains no `PYTHONPATH`/`PYTHONHOME`/
`PYTHONPYCACHEPREFIX`, and it needs `pinned/` at `sys.path[0]` for the sibling import). Keep `-I` on the **broker**
command (it loads the helper by explicit `--generation-module` path via `importlib`, so it never needs the script
dir on `sys.path`). Alternatively, inject the pinned dir explicitly (e.g. `PYTHONPATH=pinned/` — but that reintroduces
a `PYTHON*` passthrough B7 forbids, so dropping `-I` on the launcher is the clean fix).
## What DID land correctly (for the author's fast turnaround)
- **B7 — broker child env-isolated: correct.** `launch_verified_broker` now takes `environment` and passes
`env=environment` (the constructed allow-list, **not** `os.environ`) to `Popen` (:566-568); the broker command
includes `-I` (:551); the allow-list contains no `PYTHONPATH`/`PYTHONHOME`/`PYTHONPYCACHEPREFIX` passthrough. The
broker child cannot inherit ambient env or resolve stdlib imports to ambient code. ✅
- **B8 — bytecode pinned-or-suppressed: correct.** `PYTHONDONTWRITEBYTECODE=1` is in the allow-list env (:728) and
`-B` is on **both** child commands (:512-513 launcher, :551-552 broker); `reject_pinned_bytecode` fails closed
(`RuntimeError`) on any pre-existing `pinned/__pycache__` or `*.pyc` (:498-501) and is called **before each
consumer** (:534 launcher, :561 broker). No unpinned `.pyc` can be executed. ✅
- **B5 conjunction / B6 single-helper / closure-import-guard / BAR1 / BAR2 (`.state` fidelity untouched) / BAR3
(live-path, fail-closed precondition, fixture-socket isolation, `lease_anchor_registered` + hex-256) / single p3
broker / `-O`-safe / allow-list env / `--runs==(3,)` / ABSENT sweep:** all intact/unperturbed (the delta touches only
the env/`-I`/`-B`/bytecode-reject surfaces). These are **not** the failing item.
## Verdict
**FAIL @ `a92ad090`.** B7 (broker env isolation) and B8 (bytecode pinned-or-suppressed) are correctly implemented,
but the `-I` added to the **launcher** command breaks B6(c): the launcher's bare `from lease_generation import` at
`f4008307:launch-runtime.py:15` cannot resolve the pinned helper because `-I`/`-P` strips `sys.path[0]` on Python
3.11+ (empirically confirmed, 3.11.2 → `ModuleNotFoundError`). PASS requires **all** of B7+B8+B5+B6+BAR1/2/3; B6(c)
does not hold. Not softened → returns to author (ms-rev-826). The fix is narrow: drop `-I` from the launcher command
(retain `-B` + allow-list env), keep `-I` on the broker.
**Findings:** B6(c) — `p3_d4_focused_run.py:512` (`-I` on the launcher command; breaks the pinned-helper sibling
import under Python 3.11+).
## Scope reminder (not a finding)
Producing this evidence **executes** the Gate0 mechanism (§3/§5). This review clears **bytes** only; FIRE remains
separately gated on Mos's post-clear GO — moot until this FAIL is remediated.
---
**Reviewer:** distinct Opus SECREV session (`ms-secrev-828` lane, dispatched by `mosaic-100`), Gate-16 author≠reviewer,
byte review only; ran nothing (harness/broker not executed).
**Reviewed object (pin):** `a92ad090ae3828c643f961c7628d809b8521185f` · harness sha256 `915ebeb5aeab108cb60c5f629c1db520623ab4914eed427ca34ee66f9aa08390`.
@@ -1,95 +0,0 @@
> ⚠ SUPERSEDED: homelab 4th-round FAIL @`1c34e3cb` — no `-S` → Python imports `site` at startup, running unpinned system-site `.pth` executable lines + `sitecustomize`/`usercustomize` before the pinned launcher/broker (site startup-closure hole). This v9 PASS record is overtaken by the stricter B9 (no-site) bar and is NOT fire-authorization; superseded by v10.
# Gate0 Probe-3 (D4) Launcher-Import Fix — §3-Conformance Review v9
**Verdict: ✅ PASS** *(superseded — see banner above)*
## Pin (GUARD 1 — reviewed object)
- **Reviewed object = `1c34e3cb3172acdcd094e683e847d7c984afc96c`** (harness commit, branch `feat/827-gate0-probe`).
- **Reviewed file:** `docs/compaction-refresh/probes/p3_d4_focused_run.py` — sha256 (pushed provider bytes, `-o FILE`, FULL-40 ref, verified before trust): `29e5c7bfbe1911b52984bd94c79036bb1200ee82588318367b13c2b1053a0103` (32725 bytes, no not-found sentinel).
- **Closure pins (unchanged):** launcher `e950e422…` @f4008307 · helper `061625402f08488eac47acd23272904e71fd1a71fd15b3bdab158632c801be4c` @f4008307 · broker `4db4fef1…` @23c0caca.
## Independence (GUARD 2 — principal-independence attestation)
Distinct Opus SECREV session, orchestrator-dispatched — the **`ms-secrev-828` reviewer lane, dispatched by
`mosaic-100`** — **byte review only, ran nothing** (harness/broker not executed); did **not** build this harness
(builder = ms-rev-826); is not Mos. This is the re-review after **my own** v8 FAIL @`a92ad090` (B6(c): `-I` on the
launcher broke the sibling import); the author applied the narrow fix and I verify it here. The `-s`/`-I` `sys.path`
checks below use a *throwaway* two-line script to observe interpreter behavior — not the harness/broker/closure.
## ★ B6(c) — THE FIX (was the v8 FAIL): launcher `-I` dropped; sibling import binds to `pinned/`
The launcher command no longer carries `-I`; it now uses **`-s`** (`:514`, commented "`-s` preserves `sys.path[0]=pinned/`
for the launcher's sibling helper") + `-B` (`:515`), and `PYTHONNOUSERSITE=1` is added to the allow-list env (`:730`).
`-s` and `PYTHONNOUSERSITE` disable **user site-packages only** — they do **not** strip the script's directory from
`sys.path` (unlike `-I`/`-P`). Empirically confirmed on this host (Python 3.11.2), throwaway script:
```
python3 -s -B main.py → sys.path[0] = '<script dir>' → sibling import: OK
PYTHONNOUSERSITE=1 python3 -s -B main.py → sys.path[0] = '<script dir>' → sibling import: OK
python3 -I -B main.py (the v8 FAIL form) → sys.path[0] = stdlib zip → ModuleNotFoundError
```
Therefore `python3 -s -B pinned/launch-runtime.py …` puts `pinned/` at `sys.path[0]`, so the pinned launcher's bare
top-level `from lease_generation import initialize_runtime_generation` (`f4008307:launch-runtime.py:15`, no `sys.path`
manipulation) resolves to the **pinned** `pinned/lease_generation.py` — not the worktree, not a miss. **B6(c) holds.**
## B7 — Broker env-isolation (still holds)
`launch_verified_broker` passes `env=environment` (the constructed allow-list, **not** `os.environ`; contains no
`PYTHONPATH`/`PYTHONHOME`/`PYTHONPYCACHEPREFIX`) to `Popen` (`:570`), and the broker command includes `-I` (`:552`).
The broker imports the helper by explicit `--generation-module` path via `importlib`, so it never needs `sys.path[0]`
`-I` is correct there and does not affect it. (The env's `PYTHONNOUSERSITE`/`PYTHONDONTWRITEBYTECODE` are hardening
flags, not path/home passthrough, and `-I` ignores all `PYTHON*` env anyway.)
## B8 — Bytecode pinned-or-suppressed (still holds)
`PYTHONDONTWRITEBYTECODE=1` (`:729`) and `PYTHONNOUSERSITE=1` (`:730`) in the allow-list env; `-B` on **both** child
commands (`:515` launcher, `:553` broker); `reject_pinned_bytecode` fails closed (`RuntimeError`) on any pre-existing
`pinned/__pycache__` or `*.pyc` (`:498-501`) and is called **before each consumer** (`:535` launcher, `:562` broker).
No unpinned `.pyc` can be executed.
## B5 — 3-leg conjunction (still holds)
`materialize_closure` reads launcher+helper+broker from **git-object bytes** (`git show <commit>:<path>`) and asserts
`sha256 == pin` for each, fail-closed; `pinned/` is a fixture-private `0o700` dir inside the per-run fixture temp root;
files created `O_WRONLY|O_CREAT|O_EXCL|O_CLOEXEC 0o600` (no chmod/rename/symlink gap → no writable window); each member
re-hashed `== pin` immediately before its `Popen` (launcher; broker + helper). Launcher and broker consume the **same
single** pinned helper. `closure_import_guard` AST-rejects any unpinned non-stdlib import.
## Fidelity + traceability + carry-over (still hold)
`GATED_WI_HEAD == f4008307` + `merge-base --is-ancestor 66b1e0a0 f4008307` (forward-contains). Extension bumps
`generation-{sid}.state` via `MOSAIC_LEASE_GENERATION_FILE` (not in-mem); broker reads via `read_runtime_generation`;
`assert_d4` observes the file-backed transition (`generation_source=="state-file"`, `state_file_drives_lifecycle`,
`state_file_in_fixture_root`, new→`MUTATOR_UNVERIFIED`, prior→`STALE_GENERATION`); `.state` stays in the fixture temp
root. `lease_anchor_registered` INTACT (event + `session_id_shape=="hex-256"`). LIVE-PATH drives the pinned gated
launcher (no released `mosaic`/`execRuntime`). Fail-closed precondition before any launch. Single pinned p3 broker.
Promotion=fixture-only. `--runs choices=(3,)`. `copy2` = creds-only. Allow-list env (0 `os.environ.copy`).
## ABSENT sweep
P5/P6/P2-bank/retry-launder/mosaic-yolo/execRuntime = 0; `source-invalid` = 0; no live/real-broker path; no `.state`
outside the fixture root; no exec-at-import (`__main__`-guarded); no adjacency-only-exec-from-worktree; the only change
is the authorized launcher-flag isolation fix (no mechanism change, no scope-widen); **`-O`-safe** (0 bare `assert`).
## Verdict
**PASS @ `1c34e3cb`.** The v8 FAIL is remediated by the narrow fix (launcher `-I``-s` + `PYTHONNOUSERSITE=1`),
empirically verified to preserve `sys.path[0]=pinned/` so the pinned launcher's sibling import binds to the pinned
helper; the broker retains `-I` (explicit-path import). B7, B8, B5, B6-rest, fidelity, traceability, and all carry-over
bars remain intact; zero out-of-scope surface. **Findings: none.**
## Scope reminder (not a finding)
Producing this evidence **executes** the Gate0 mechanism (§3/§5). This review clears **bytes** only; **FIRE remains
separately gated on Mos's explicit post-clear GO**, his byte-scope-verify co-attestation, and homelab's
third-principal verify — not authorized by this review.
---
**Reviewer:** distinct Opus SECREV session (`ms-secrev-828` lane, dispatched by `mosaic-100`), Gate-16 author≠reviewer,
byte review only; ran nothing.
**Reviewed object (pin):** `1c34e3cb3172acdcd094e683e847d7c984afc96c` · harness sha256 `29e5c7bfbe1911b52984bd94c79036bb1200ee82588318367b13c2b1053a0103`.
+23 -31
View File
@@ -5,28 +5,17 @@
This checklist is an acceptance contract for documentation and examples. It does not authorize
schema, runtime, systemd, role, profile, or live-fleet changes. An item is complete only when its
named artifact exists, is linked from the fleet documentation entry point, and its evidence is
recorded in the M0 task/PR.
recorded in the M5 closure report and linked deferral evidence.
## M0 baseline acceptance
- [ ] `docs/PRD.md` states the roster as desired-state SSOT; generated environment, systemd,
tmux, and heartbeat artifacts as non-authoritative projections; and fail-closed handling of
unsupported or quarantined legacy input.
- [ ] `docs/PRD.md` defines the required classes and authority boundary: `validator` certifies but
does not merge; `merge-gate` remains sole approve-to-land/merge authority; `team-leader`
capacity is lease-bounded; `interaction` is request/status only; instance names such as Tess
and Ultron remain configurable.
- [ ] `docs/PRD.md` defines local lifecycle semantics for `enabled`, persisted desired state, and
observed state, including stopped-state preservation through migration, apply, and reboot.
- [ ] `docs/PRD.md` defines the generated-env/local-override boundary, explicitly denies arbitrary
command overrides in M1M5, and requires key-name/hash-only quarantine diagnostics.
- [ ] `docs/PRD.md` identifies the M1M5 local-tmux scope and excludes remote reconciliation,
connector mutation, secret references, arbitrary commands/channels, gateway convergence, and
UI configuration storage.
- [ ] `docs/TASKS.md` contains the complete M0M5 one-card/one-PR dependency DAG for #758 with
agent tier, branch, dependency, estimate, and evidence expectations.
- [ ] `docs/fleet/LEGACY-EXAMPLE-PROFILE-DISPOSITION-INVENTORY.md` classifies every current shipped
fleet example, profile, and service preset before M1 implementation starts.
- [x] `docs/PRD.md` states the roster as desired-state SSOT; generated environment, systemd, tmux, and heartbeat artifacts as non-authoritative projections; and fail-closed handling of unsupported or quarantined legacy input.
- [x] `docs/PRD.md` defines the required classes and authority boundary: `validator` certifies but does not merge; `merge-gate` remains sole approve-to-land/merge authority; `team-leader` capacity is lease-bounded; `interaction` is request/status only; instance names such as Tess and Ultron remain configurable.
- [x] `docs/PRD.md` defines local lifecycle semantics for `enabled`, persisted desired state, and observed state, including stopped-state preservation through migration, apply, and reboot.
- [x] `docs/PRD.md` defines the generated-env/local-override boundary, explicitly denies arbitrary command overrides in M1M5, and requires key-name/hash-only quarantine diagnostics.
- [x] `docs/PRD.md` identifies the M1M5 local-tmux scope and excludes remote reconciliation, connector mutation, secret references, arbitrary commands/channels, gateway convergence, and UI configuration storage.
- [x] `docs/TASKS.md` contains the complete M0M5 one-card/one-PR dependency DAG for #758 with agent tier, branch, dependency, estimate, and evidence expectations.
- [x] `docs/fleet/LEGACY-EXAMPLE-PROFILE-DISPOSITION-INVENTORY.md` classifies every current shipped fleet example, profile, and service preset before M1 implementation starts.
## Required documentation IA for M1M5
@@ -72,15 +61,18 @@ recorded in the M0 task/PR.
## Cross-cutting evidence gates
- [ ] Every retained or migrated YAML/JSON example, profile, and service preset validates through the
same executable schema and shared baseline-plus-`roles.local` resolver used by the CLI.
- [ ] Every retired example/profile/service preset has a replacement link and deprecation note; no
unresolved legacy class or tool-policy alias remains silently shipped.
- [ ] Documentation examples contain no secret values, arbitrary command override, or product-hardcoded
Tess/Ultron identity.
- [ ] CLI snippets distinguish local fleet desired-state commands from the separate gateway-backed
`mosaic agent` catalog.
- [ ] Migration, quarantine, lifecycle, status, and troubleshooting documentation state that values of
legacy sensitive keys are never printed.
- [ ] M5 release review verifies links, schema/example validation, and that all checklist rows have
owner/evidence or an explicit approved deferral.
- [x] Every retained or migrated YAML/JSON example, profile, and service preset validates through the same declared executable production parser/resolver path recorded by the disposition inventory; versioned v1 fixtures are not forced through the v2 compiler.
- [x] Every retired example/profile/service preset has a replacement link and deprecation note; no unresolved legacy class or tool-policy alias remains silently shipped.
- [x] Documentation examples contain no secret values, arbitrary command override, or product-hardcoded Tess/Ultron identity.
- [x] CLI snippets distinguish local fleet desired-state commands from the separate gateway-backed mosaic agent catalog.
- [x] Migration, quarantine, lifecycle, status, and troubleshooting documentation state that values of legacy sensitive keys are never printed.
- [x] M5 documentation validation verifies required IA paths, local file and heading-fragment links, the canonical roster through the production compiler/resolver, and fenced/canonical-example safety checks.
- [ ] FCM-M5-001 does not deterministically assert owner/evidence/deferral metadata for every checklist row. Closure and deferral reports provide human-reviewable evidence only; broader assertion coverage remains unclaimed.
## Held downstream gates
These unchecked items are intentionally outside FCM-M5-001 and are not authorized by this checklist:
- [ ] FCM-M4-002 executes and evidences live cutover, canary, stopped-state preservation, and rollback.
- [ ] FCM-M5-002 completes independent exact-head review and issues the validator certificate.
- [ ] The exact PR head reaches terminal-green CI after independent review.
+11 -11
View File
@@ -8,11 +8,11 @@ Generated environment files are rebuildable projections, not an operator-editabl
| Layer | Responsibility |
| ------------------- | ------------------------------------------------------------------------------------------------------------------------------- |
| Roster | `fleet/roster.yaml` supplies the agent name, class, supported runtime, model, reasoning, tool policy, workdir, and tmux socket. |
| Projection writer | Renders deterministic `fleet/agents/<name>.env.generated` from the roster. |
| Optional local data | Reads a strict, data-only `fleet/agents/<name>.env.local`; it cannot shadow generated keys. |
| systemd | Starts the launcher with `env -i` and fixed bootstrap data. It does not preload either environment file. |
| Projection writer | Renders deterministic fleet/agents/<name>.env.generated from the roster. |
| Optional local data | Reads a strict, data-only fleet/agents/<name>.env.local; it cannot shadow generated keys. |
| systemd | Starts the launcher with env -i and fixed bootstrap data. It does not preload either environment file. |
| session launcher | Validates generated and local data before it queries, creates, or stops an exact tmux session. |
| runtime launch | Derives the fixed `mosaic yolo <runtime>` argument array from validated roster data, then seeds the runtime contract. |
| runtime launch | Derives the fixed mosaic yolo <runtime> argument array from validated roster data, then seeds the runtime contract. |
The launcher never `source`s or `eval`s an environment file and never accepts an environment-supplied
command. `MOSAIC_AGENT_COMMAND`, command/channel overrides, unknown keys, generated-key shadowing,
@@ -20,7 +20,7 @@ secret-like key names, duplicate keys, comments, quoted/export syntax, and unsaf
## Generated and local files
`<name>.env.generated` is complete, deterministic, and written only by Mosaic. Its ordered keys are:
<name>.env.generated is complete, deterministic, and written only by Mosaic. Its ordered keys are:
```dotenv
MOSAIC_AGENT_NAME=<roster name>
@@ -33,12 +33,12 @@ MOSAIC_AGENT_WORKDIR=<absolute roster work directory>
MOSAIC_TMUX_SOCKET=<roster socket or empty>
```
The generated launch contract supports `claude`, `codex`, `opencode`, and `pi`. `mosaic fleet add`
The generated launch contract supports `claude`, `codex`, `opencode`, and `pi`. mosaic fleet add
rejects another runtime before it writes the roster or modifies generated, local, or quarantine state.
The legacy dogfood stub remains an observability-only canary on its separate `mosaic-factory` socket;
it has no generated-launch adapter and cannot be added through this path.
`<name>.env.local` is optional and may contain only non-secret machine data:
<name>.env.local is optional and may contain only non-secret machine data:
- `MOSAIC_RUNTIME_BIN`
- `MOSAIC_HEARTBEAT_RUN_DIR`
@@ -52,9 +52,9 @@ private, non-symlink paths. Violations fail closed before tmux interaction.
## Legacy input and diagnostics
A legacy `<name>.env` is input only during projection generation. Roster-owned keys are regenerated;
A legacy <name>.env is input only during projection generation. Roster-owned keys are regenerated;
valid allowed local data can move to `.env.local`; invalid legacy input is privately retained at
`<name>.env.quarantine`. Neither legacy nor quarantine files are launch authority.
<name>.env.quarantine. Neither legacy nor quarantine files are launch authority.
Diagnostics expose only rule code, key name, and a SHA-256 content hash. They do not reveal command
text, credentials, or other values.
@@ -62,11 +62,11 @@ text, credentials, or other values.
## Launch and stop behavior
The launcher obtains the agent's socket only from the validated generated projection. It creates or
checks the exact `=<agent-name>` tmux target; it never uses an ambient socket or fuzzy session match.
checks the exact =<agent-name> tmux target; it never uses an ambient socket or fuzzy session match.
The same strict parser runs before exact-stop behavior. A fresh native Pi heartbeat remains authoritative;
the shell sidecar only provides fallback state when the native marker is stale or absent.
`mosaic agent comms-block <exact-member>` can inspect that exact roster member's resolved Fleet-Comms
mosaic agent comms-block <exact-member> can inspect that exact roster member's resolved Fleet-Comms
block. It is a read-only inspection tool and fails loudly for an unknown exact member or missing roster.
On Linux, the installed roster, TOOLS contract, and executable helper are opened through a held
descriptor chain rooted at `/`; every managed path component uses no-follow traversal, and content plus
@@ -14,7 +14,7 @@ parallel resolver. The current executable implementation and per-artifact outcom
| Shipped file | Current class evidence | M0 disposition decision | Required M1/M4 evidence |
| ---------------------------------------------------- | ------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------- |
| `framework/fleet/examples/coding.yaml` | `orchestrator`, `enhancer`, `implementer`, `reviewer` | Migrate: `implementer → code`, `reviewer → review`; retain orchestration/enhancer intent | v2 fixture validates; role aliases and authority matrix tested |
| `framework/fleet/examples/coding.yaml` | `orchestrator`, `enhancer`, `implementer`, `reviewer` | Migrate: implementer → code, reviewer → review; retain orchestration/enhancer intent | v2 fixture validates; role aliases and authority matrix tested |
| `framework/fleet/examples/general.yaml` | `orchestrator`, `enhancer`, `worker` | Migrate only after operator chooses a concrete canonical role for `worker`; no implicit conversion | Explicit replacement class, or versioned v1 fixture/retirement note |
| `framework/fleet/examples/hybrid.yaml` | `orchestrator`, `enhancer`, `implementer`, `researcher`, `reviewer` | Migrate aliases; resolve `researcher` through existing role resolver or retain/version | Shared resolver validation; no ad-hoc class scanner |
| `framework/fleet/examples/local-canary.yaml` | `orchestrator`, `implementer`, `reviewer` | Migrate aliases; preserve its local-tmux canary purpose | v2 fixture validates and preserves safe stopped/running behavior |
@@ -34,14 +34,14 @@ parallel resolver. The current executable implementation and per-artifact outcom
## Service presets
| Shipped file | Current policy evidence | M0 disposition decision | Required M1/M4 evidence |
| ---------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `framework/fleet/services/operator-interaction.yaml` | Generic policy only: `runtime: pi`, `model: openai/gpt-5.6-sol`, `reasoning: high`, `tool_policy: operator-interaction`; provisioning supplies the agent name as data | Retain as a generic service policy, not a Tess identity. Migrate `tool_policy: operator-interaction` only through the approved interaction tool-policy alias/semantic resolver; do not infer a class or machine name from this file. | Service-policy fixture validates runtime/model/reasoning and alias behavior; generic provisioning proves a configured interaction instance is supplied without a hardcoded Tess name. |
| Shipped file | Current policy evidence | M0 disposition decision | Required M1/M4 evidence |
| ---------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `framework/fleet/services/operator-interaction.yaml` | Generic policy only: runtime: pi, model: openai/gpt-5.6-sol, reasoning: high, tool_policy: operator-interaction; provisioning supplies the agent name as data | Retain as a generic service policy, not a Tess identity. Migrate tool_policy: operator-interaction only through the approved interaction tool-policy alias/semantic resolver; do not infer a class or machine name from this file. | Service-policy fixture validates runtime/model/reasoning and alias behavior; generic provisioning proves a configured interaction instance is supplied without a hardcoded Tess name. |
## Required disposition controls
1. **No silent aliasing:** only `implementer → code`, `reviewer → review`, and
`operator-interaction → interaction` are approved deterministic aliases in this M0 baseline.
1. **No silent aliasing:** only implementer → code, reviewer → review, and
operator-interaction → interaction are approved deterministic aliases in this M0 baseline.
`worker`, `analyst`, `canary`, and domain-specific classes require resolver evidence or an
explicit version/retirement decision.
2. **No identity hardcoding:** Tess and Ultron are optional instance/display names. An example/profile
+1 -1
View File
@@ -33,7 +33,7 @@ The Mosaic Backlog is the backlog of record + dispatch engine, built on Mosaic's
- **AC-NS-4** — TTL is enforced on claims; token caps remain advisory until a real meter exists.
- **AC-NS-5** — Flipping fleet/run/PAUSED halts dispatch and merges within one tick.
- **AC-NS-6** — A user can declare a system type and the fleet provisions the matching persona roster + topology from the baseline library, with no code change.
- **AC-NS-7** — A user-customized persona (edited or added via the orchestrator) survives `mosaic update`: baseline reseed never clobbers user overrides.
- **AC-NS-7** — A user-customized persona (edited or added via the orchestrator) survives mosaic update: baseline reseed never clobbers user overrides.
## Workstreams
+1 -1
View File
@@ -97,7 +97,7 @@ success_criteria:
- id: AC-NS-7
text: >-
A user-customized persona (edited or added via the orchestrator) survives
`mosaic update`: baseline reseed never clobbers user overrides.
mosaic update: baseline reseed never clobbers user overrides.
workstreams:
- id: A
+34 -34
View File
@@ -8,7 +8,7 @@
## Mission
Turn the proven fleet primitives into a **user-installable, AI-free-configurable fleet product**:
a user runs `mosaic fleet init`, answers a few questions (general / coding / research / hybrid),
a user runs mosaic fleet init, answers a few questions (general / coding / research / hybrid),
gets a recommended set of agents plus one always-on orchestrator wired for chat-ops, and can
operate, mutate, re-create, and observe the fleet — over tmux today and Matrix tomorrow — from
CLI/TUI and (designed-for) the webUI.
@@ -21,25 +21,25 @@ functional, we use the fleet itself to continue the work.
### A. Configure-without-AI CLI
| ID | Requirement |
| --- | ------------------------------------------------------------------------------------------------------------- |
| R1 | `mosaic fleet` command set is functional end-to-end (init/install/start/stop/status/ps/verify + agent verbs). |
| R2 | `mosaic fleet init` is an interactive, **AI-free** CLI wizard. |
| R3 | Init asks the **configuration type**: `general`, `coding`, `research`, `hybrid`, … (extensible). |
| R4 | Based on the answer, the fleet is populated with a **recommended set of agents** (a preset). |
| R5 | **Exactly one main orchestrator agent** is always configured, regardless of type. |
| R10 | A set of **recommended configurations (presets)** ships for easy duplication. |
| R8 | User can **re-create** the fleet when config needs change (idempotent re-init / reconfigure). |
| R17 | Fleet controls are **simple and intuitive**. |
| ID | Requirement |
| --- | ----------------------------------------------------------------------------------------------------------- |
| R1 | mosaic fleet command set is functional end-to-end (init/install/start/stop/status/ps/verify + agent verbs). |
| R2 | mosaic fleet init is an interactive, **AI-free** CLI wizard. |
| R3 | Init asks the **configuration type**: `general`, `coding`, `research`, `hybrid`, … (extensible). |
| R4 | Based on the answer, the fleet is populated with a **recommended set of agents** (a preset). |
| R5 | **Exactly one main orchestrator agent** is always configured, regardless of type. |
| R10 | A set of **recommended configurations (presets)** ships for easy duplication. |
| R8 | User can **re-create** the fleet when config needs change (idempotent re-init / reconfigure). |
| R17 | Fleet controls are **simple and intuitive**. |
### B. Comms & orchestrator chat-ops
| ID | Requirement |
| --- | --------------------------------------------------------------------------------------------------------------------------------- |
| R6 | Init can wire the orchestrator to a chat connector — **Telegram / Discord / Matrix / Slack** — for command + comms. |
| R7 | Designed with the end-goal of **Matrix comms on a locally-controlled server**. |
| R16 | Fleet supports **tmux AND Matrix** comms, **user-configurable** at init or any time. Not all users want Matrix. |
| R19 | **"Mos" orchestrator on Discord** (`chan 1517622518662434996` / `srv 1112631390438166618`) on `w-jarvis` — the first live target. |
| ID | Requirement |
| --- | ----------------------------------------------------------------------------------------------------------------------------- |
| R6 | Init can wire the orchestrator to a chat connector — **Telegram / Discord / Matrix / Slack** — for command + comms. |
| R7 | Designed with the end-goal of **Matrix comms on a locally-controlled server**. |
| R16 | Fleet supports **tmux AND Matrix** comms, **user-configurable** at init or any time. Not all users want Matrix. |
| R19 | **"Mos" orchestrator on Discord** (chan 1517622518662434996 / srv 1112631390438166618) on `w-jarvis` — the first live target. |
### C. Runtime, health, lifecycle
@@ -64,46 +64,46 @@ functional, we use the fleet itself to continue the work.
- **Orchestrator agent:** always present; carries the chat connector config (connector type + target IDs) so it can be commanded over chat. tmux is the substrate; the connector bridges chat ↔ the orchestrator session.
- **Comms layers (R16):** (1) **tmux** inter-agent (`agent-send`, proven) — default, always available. (2) **chat connector** for human↔orchestrator (Discord now; Matrix the strategic target). (3) **Matrix** as the locally-controlled cross-agent bus (future). Connector is pluggable + reconfigurable.
- **Heartbeat (R15):** runtime-agnostic launcher sidecar already covers pi/claude/codex (#584). Refine per-runtime (native HB) with the **custom Pi harness** (R14) + a Claude path.
- **Updates (R13):** `mosaic update` (CLI) + a fleet-aware harness-update step that refreshes pi/claude/codex and re-launches agents safely (drain → update → relaunch via the durable launcher).
- **webUI (R18):** the fleet exposes machine-readable state (`fleet ps --json` already carries tenant/host/heartbeat/managed) + control verbs (start/stop/watch/send); webUI consumes these (control plane rides federation per north star). Ensure a stable JSON contract + a terminate/attach(butt-in) path.
- **Updates (R13):** mosaic update (CLI) + a fleet-aware harness-update step that refreshes pi/claude/codex and re-launches agents safely (drain → update → relaunch via the durable launcher).
- **webUI (R18):** the fleet exposes machine-readable state (fleet ps --json already carries tenant/host/heartbeat/managed) + control verbs (start/stop/watch/send); webUI consumes these (control plane rides federation per north star). Ensure a stable JSON contract + a terminate/attach(butt-in) path.
## Phases (incremental, each shippable)
| Phase | Deliverable | Notes |
| --------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------- |
| **F1 Presets + init wizard** | preset rosters (general/coding/research/hybrid) + always-orchestrator + AI-free `fleet init` selecting a preset; re-init idempotent | R1R5, R8, R10, R17 |
| **F2 Connector + Mos-on-Discord** | orchestrator chat-connector config (Discord first) + **Mos live on Discord `1517…`/`1112…`** on w-jarvis | R6, R19, partial R16 |
| **F3 Heartbeat + harness** | HB confirmed for claude + pi/gpt; **custom Pi harness** (tool usage, native HB, model self-report); graceful harness updates | R13, R14, R15 |
| **F4 Matrix + comms toggle** | Matrix connector (local server) + user toggle tmux/Matrix at init/anytime | R7, R16 |
| **F5 Orchestrator-mutable fleet** | orchestrator can add/remove agents at runtime | R9 |
| **F6 webUI hooks** | stable JSON contract + terminate/attach surface for webUI view/monitor/terminate/butt-in | R18 |
| **F7 Test + docs** | install+test on w-jarvis AND dragon-lin; user docs (install/configure/use) | R11, R12 (runs alongside every phase) |
| Phase | Deliverable | Notes |
| --------------------------------- | --------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------- |
| **F1 Presets + init wizard** | preset rosters (general/coding/research/hybrid) + always-orchestrator + AI-free fleet init selecting a preset; re-init idempotent | R1R5, R8, R10, R17 |
| **F2 Connector + Mos-on-Discord** | orchestrator chat-connector config (Discord first) + **Mos live on Discord 1517…/1112…** on w-jarvis | R6, R19, partial R16 |
| **F3 Heartbeat + harness** | HB confirmed for claude + pi/gpt; **custom Pi harness** (tool usage, native HB, model self-report); graceful harness updates | R13, R14, R15 |
| **F4 Matrix + comms toggle** | Matrix connector (local server) + user toggle tmux/Matrix at init/anytime | R7, R16 |
| **F5 Orchestrator-mutable fleet** | orchestrator can add/remove agents at runtime | R9 |
| **F6 webUI hooks** | stable JSON contract + terminate/attach surface for webUI view/monitor/terminate/butt-in | R18 |
| **F7 Test + docs** | install+test on w-jarvis AND dragon-lin; user docs (install/configure/use) | R11, R12 (runs alongside every phase) |
## Work division (proposed — confirm with dragon-lin)
- **Jarvis @ w-jarvis (Lead):** F1 presets+wizard, F2 connector+Mos-on-Discord, F5 mutability, F6 webUI hooks; merge authority + dual-engine reviews; co-testing on w-jarvis.
- **coder @ dragon-lin:** F3 custom Pi harness + harness-update flow (pi/codex-savvy); plus its in-flight constitution P4P6 (P4 installer rework underpins `fleet init`/updates — coordinate the install path). Co-testing on dragon-lin (R11).
- **coder @ dragon-lin:** F3 custom Pi harness + harness-update flow (pi/codex-savvy); plus its in-flight constitution P4P6 (P4 installer rework underpins fleet init/updates — coordinate the install path). Co-testing on dragon-lin (R11).
- **Shared:** F4 Matrix (whoever has bandwidth); F7 testing/docs continuous.
## Immediate target: Mos on Discord (F2 first slice)
The discord plugin is available (`~/.claude.json`). Path: configure the **orchestrator** as a durable
The discord plugin is available (~/.claude.json). Path: configure the **orchestrator** as a durable
fleet session running Claude Code with the discord plugin bridged to channel `1517622518662434996`
(server `1112631390438166618`) on w-jarvis, with the existing Discord Bridge Protocol (ack within
~3s, reply via `mcp__discord__reply`, no `AskUserQuestion`). Heartbeat via the launcher sidecar.
## Success criteria
- A non-AI user can `mosaic fleet init`, pick a type, and get a working fleet + orchestrator.
- **Mos answers in Discord `1517…`** on w-jarvis.
- Fleet runs + is observable (`fleet ps`) on **both** w-jarvis and dragon-lin.
- A non-AI user can mosaic fleet init, pick a type, and get a working fleet + orchestrator.
- **Mos answers in Discord 1517…** on w-jarvis.
- Fleet runs + is observable (fleet ps) on **both** w-jarvis and dragon-lin.
- Harness updates handled gracefully; HB healthy for claude + pi/gpt agents.
- Docs let a new operator install/configure/use the fleet.
- Re-init + orchestrator mutation work.
## Assumptions (veto-able)
- `ASSUMPTION:` presets ship as example rosters under the framework (`fleet/examples/*.yaml`), selected by `init`.
- `ASSUMPTION:` presets ship as example rosters under the framework (fleet/examples/\*.yaml), selected by `init`.
- `ASSUMPTION:` chat connectors are pluggable; Discord first (target exists), Matrix is the strategic default later.
- `ASSUMPTION:` "Mos" = a Claude Code orchestrator session with the discord plugin (reuses the documented Discord Bridge Protocol).
- `ASSUMPTION:` per north star, runtimes default to Codex/pi-on-Codex for workers; the orchestrator "Mos" runs Claude Code (in Claude Code, which is allowed).
+30 -30
View File
@@ -10,8 +10,8 @@
The durable tmux fleet runs on the isolated `mosaic-fleet` socket. That isolation
(which protects the operator's default tmux) makes the fleet **invisible** to default
tooling, and truth is split across three planes no single command joins — systemd
(`systemctl --user`), tmux (`-L mosaic-fleet`), and the process tree (`pstree`).
`agent tail` (`capture-pane`) returns **blank for full-screen TUIs**, and `agent send`
(systemctl --user), tmux (-L mosaic-fleet), and the process tree (`pstree`).
agent tail (`capture-pane`) returns **blank for full-screen TUIs**, and agent send
confirms only keystroke injection, not acceptance. Net: the operator has near-zero
observability and no safe way to watch a session.
@@ -32,22 +32,22 @@ observability and no safe way to watch a session.
## Functional requirements
| ID | Requirement |
| ---- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| FR-1 | `mosaic fleet ps [--json]` prints one row per roster agent joining: name · tenant · host · runtime · systemd(active/enabled) · pane(alive/dead) · pid · idle · **last-heartbeat age** · **drift** flag (roster runtime ≠ actual pane command) · **boot-enable** warning (active but `UnitFileState=disabled`). |
| FR-2 | **Heartbeat protocol v1** (see below); `dogfood-agent.py` implements the responder. `fleet ps` issues probes (or reads last-seen) and reports health per FR-1. |
| FR-3 | `mosaic agent watch <name>` opens a **read-only** view of the pane (grouped session or `tmux attach -r`) that cannot send keystrokes and does not shrink the agent's window. |
| FR-4 | `mosaic agent attach <name>` remains the **explicit** interactive-takeover path (separate verb, documented as the only one that can type). |
| FR-5 | `mosaic agent send <name> --verify` confirms the message was **accepted** (not left as an unsubmitted draft) and returns non-zero if delivery cannot be verified. |
| FR-6 | All structured output (`--json`) includes `tenant_id` and `host` fields. |
| ID | Requirement |
| ---- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| FR-1 | mosaic fleet ps [--json] prints one row per roster agent joining: name · tenant · host · runtime · systemd(active/enabled) · pane(alive/dead) · pid · idle · **last-heartbeat age** · **drift** flag (roster runtime ≠ actual pane command) · **boot-enable** warning (active but `UnitFileState=disabled`). |
| FR-2 | **Heartbeat protocol v1** (see below); `dogfood-agent.py` implements the responder. fleet ps issues probes (or reads last-seen) and reports health per FR-1. |
| FR-3 | mosaic agent watch <name> opens a **read-only** view of the pane (grouped session or tmux attach -r) that cannot send keystrokes and does not shrink the agent's window. |
| FR-4 | mosaic agent attach <name> remains the **explicit** interactive-takeover path (separate verb, documented as the only one that can type). |
| FR-5 | mosaic agent send <name> --verify confirms the message was **accepted** (not left as an unsubmitted draft) and returns non-zero if delivery cannot be verified. |
| FR-6 | All structured output (`--json`) includes `tenant_id` and `host` fields. |
## Heartbeat protocol v1
- **Probe:** operator/`fleet ps` writes a sentinel line to the agent's input or a
well-known per-agent heartbeat file path `~/.config/mosaic/fleet/run/<agent>.hb`.
- **Response:** the runtime updates `<agent>.hb` with `ts=<iso8601> pid=<pid> status=<ok|busy>`
- **Probe:** operator/fleet ps writes a sentinel line to the agent's input or a
well-known per-agent heartbeat file path ~/.config/mosaic/fleet/run/<agent>.hb.
- **Response:** the runtime updates <agent>.hb with ts=<iso8601> pid=<pid> status=<ok|busy>
on a fixed interval (default 15s) and on demand when probed.
- **Health rule:** `healthy` if `now - ts <= 3 × interval`; else `stale`; missing file = `unknown`.
- **Health rule:** `healthy` if now - ts <= 3 × interval; else `stale`; missing file = `unknown`.
- **Contract:** every runtime (dogfood stub now; claude/codex/pi/opencode in Phase 3)
MUST emit the heartbeat. The protocol is file-based so it works for headless stubs and
full-screen TUIs alike (no `capture-pane` dependency).
@@ -56,15 +56,15 @@ observability and no safe way to watch a session.
## Acceptance criteria
- `mosaic fleet ps` shows all 5 live sessions on `mosaic-fleet` with correct
- mosaic fleet ps shows all 5 live sessions on `mosaic-fleet` with correct
pane/pid/idle and flags the dogfood **drift** (`canary-pi` runtime=pi but pane runs
`dogfood-agent.py`) and the **boot-enable** gap (active but disabled).
- Killing one agent's pane flips its row to dead/stale within one `interval`.
- `agent watch` shows live output and provably cannot type into the pane; detaching
- agent watch shows live output and provably cannot type into the pane; detaching
leaves the agent's window size unchanged.
- `agent send --verify` returns success on an accepting pane and non-zero on a wedged/draft pane.
- Quality gates green: `pnpm typecheck`, `pnpm lint`, `pnpm format:check`, plus
`pnpm --filter @mosaicstack/mosaic test`.
- agent send --verify returns success on an accepting pane and non-zero on a wedged/draft pane.
- Quality gates green: pnpm typecheck, pnpm lint, pnpm format:check, plus
pnpm --filter @mosaicstack/mosaic test.
- Independent review passed; dogfood evidence captured against the live fleet.
## Test plan
@@ -72,18 +72,18 @@ observability and no safe way to watch a session.
- Unit/CLI specs in `packages/mosaic/src/commands/fleet.spec.ts` (and a new
`fleet-ps`/`watch`/`send-verify` spec) using the injected `CommandRunner` to assert
exact tmux/systemd command construction and JSON shape (tenant+host present).
- Situational: run against the live `mosaic-fleet` fleet; capture `fleet ps` output,
a kill-and-detect cycle, a read-only `watch`, and a `send --verify` pass/fail pair.
- Situational: run against the live `mosaic-fleet` fleet; capture fleet ps output,
a kill-and-detect cycle, a read-only `watch`, and a send --verify pass/fail pair.
## Known limitations
- **Verify heuristic is best-effort:** `agent send --verify` uses a `>` -prefix draft
- **Verify heuristic is best-effort:** agent send --verify uses a > -prefix draft
heuristic that is specific to pi/claude TUIs. Draft detection for codex and opencode
TUIs is best-effort only; those runtimes may not use the same input-line indicator.
- **Pane-change check is the best Phase-2 signal; verify now polls up to a bounded
timeout:** `agent send --verify` captures a BEFORE snapshot, sends the message, then
timeout:** agent send --verify captures a BEFORE snapshot, sends the message, then
polls `capture-pane` every ~400 ms up to a configurable total timeout (default ~6 s,
controlled by `--verify-timeout <ms>`). On each poll it runs classifySendResult: if
controlled by --verify-timeout <ms>). On each poll it runs classifySendResult: if
the pane shows 'accepted' or 'draft' the loop exits immediately; while the result is
'unverifiable' (no pane change yet) it keeps polling. After the timeout with no
definitive result, it fails closed: exit 1 with "no pane change after send". This
@@ -92,15 +92,15 @@ observability and no safe way to watch a session.
requires a runtime acknowledgement (Phase-3 heartbeat-ack); the bounded pane-change
poll is the best signal available against an opaque TUI for Phase-2.
- **Blank AFTER capture fails closed:** Full-screen TUIs (claude, codex, opencode, pi)
render blank for `tmux capture-pane`. When the AFTER snapshot is empty, `send --verify`
render blank for tmux capture-pane. When the AFTER snapshot is empty, send --verify
returns non-zero with an "unverifiable" message rather than silently succeeding. This
is an intentional fail-closed design (FR-5).
- **`agent watch` uses a grouped viewer session:** `tmux attach -r` directly against the
agent session lets the viewer terminal shrink the agent's window. `agent watch` instead
creates a throwaway grouped session (`tmux new-session -d -t '=<agent>' -s
'<agent>-watch-<pid>'`), attaches read-only to that session, and kills it on detach.
- **agent watch uses a grouped viewer session:** tmux attach -r directly against the
agent session lets the viewer terminal shrink the agent's window. agent watch instead
creates a throwaway grouped session (tmux new-session -d -t '=<agent>' -s
'<agent>-watch-<pid>'), attaches read-only to that session, and kills it on detach.
The grouped session shares the agent's windows but has independent sizing, so the
agent's window is never affected. `tmux attach` is still interactive and requires
agent's window is never affected. tmux attach is still interactive and requires
inherited stdio; the `interactiveRunner` handles TTY passthrough.
## Surfaces & parity (MVP-X1)
+63
View File
@@ -0,0 +1,63 @@
# Fleet Configuration Management
This book documents the local roster-v2 desired-state control plane delivered under issue #758. The normative requirements are the [FCM section of the repository PRD](../PRD.md#fleet-declarative-configuration-management-workstream-fcm-758), not the older fleet-suite or observability planning pages.
## Authority boundary
<MOSAIC_HOME>/fleet/roster.yaml is the sole writable desired-state authority for local fleet membership, launch policy, and persisted lifecycle. Generated environment files, systemd enablement, tmux sessions, heartbeat files, and status output are derived or observed. Rebuild projections from the roster; never edit them as desired state.
This control plane is local tmux/systemd only. Remote/SSH entries and connectors are inventory, not reconciliation targets. Arbitrary commands, channels, secret references, gateway catalog convergence, and UI configuration storage are outside this workstream. `mos-comms` is temporary transport glue, not permanent fleet architecture.
## Choose the right workflow
1. **Need to inspect intent?** Read the roster and use mosaic fleet get; see [desired versus observed state](concepts/desired-vs-observed-state.md).
2. **Need to inspect reality?** Use `status` or `doctor`; use `verify` for a strict non-zero drift/ownership gate. These commands do not repair anything.
3. **Need to change membership or persisted policy?** Use generation-guarded `plan`, `create`, `update`, or `delete`; see [safe CRUD](how-to/create-update-delete-agent.md).
4. **Need a one-time runtime action?** Use `start`, `stop`, or `restart`. These do not change persisted desired state.
5. **Need convergence?** Review apply --dry-run, resolve blockers, then use `apply` with the same current generation; see [reconcile and recover](operations/reconcile-and-recover.md).
6. **Need v1 migration evidence?** Use preview only. Cutover, canary, and rollback remain held for FCM-M4-002.
7. **Need the gateway-backed agent catalog?** That is the separate mosaic agent surface, not local fleet desired state.
## Concepts
- [Desired versus observed state](concepts/desired-vs-observed-state.md)
- [Identity, class, runtime, provider, and model](concepts/identity-class-runtime.md)
- [Role authority and leases](concepts/role-authority-and-leases.md)
- [Generated environment launch chain](concepts/generated-env-launch-chain.md)
## Operator how-to
- [Create, inspect, update, and delete](how-to/create-update-delete-agent.md)
- [Start, stop, restart, and reconcile](how-to/start-stop-restart.md)
- [Configure an interaction instance](how-to/configure-tess-interaction.md)
- [Configure a validator instance](how-to/configure-ultron-validator.md)
- [Customize roles](how-to/customize-roles.md)
## Operations and recovery
- [Reconcile and recover](operations/reconcile-and-recover.md)
- [Environment quarantine](operations/env-quarantine.md)
- [Systemd/tmux troubleshooting](operations/systemd-tmux-troubleshooting.md)
- [Backup and restore boundary](operations/backup-restore.md)
- [Upgrade and asset-drift hold](operations/upgrade-assets.md)
## Reference and migration
- [Roster v2 fields](reference/roster-v2-fields.md) · [executable JSON Schema](reference/roster-v2.schema.json) · [validated example](examples/roster-v2.yaml)
- [CLI and exit codes](reference/cli.md)
- [Role classes](reference/role-classes.md)
- [Lifecycle transitions](reference/lifecycle-transitions.md)
- [Status and drift](reference/status-and-drift.md)
- [Generated environment boundary](reference/generated-env-boundary.md)
- [v1-to-v2 preview](migration/v1-to-v2.md)
- [Example/profile dispositions](migration/example-profile-disposition.md)
- [Legacy class aliases](migration/legacy-class-aliases.md)
## Acceptance evidence and holds
- [M0/M5 IA checklist](FLEET-CONFIG-DOCS-IA-CHECKLIST.md)
- [Legacy example/profile inventory](LEGACY-EXAMPLE-PROFILE-DISPOSITION-INVENTORY.md)
- [M5 closure evidence](../reports/documentation/758-fleet-config-ia-closure.md)
- [Approved-existing deferrals and live-action holds](../reports/deferred/758-fleet-config-deferrals.md)
The canonical publishing source remains this repository. This card does not publish externally, run a migration, operate a live fleet, or close parent issue #758.
+13 -13
View File
@@ -7,21 +7,21 @@
> Mission: `mvp-20260312` · PRD: [docs/fleet/PRD.md](./PRD.md) · North star: [docs/fleet/north-star.md](./north-star.md)
> Status: `not-started` | `in-progress` | `done` | `blocked` | `failed`
| id | status | description | depends_on | agent | pr | notes |
| ------------- | ----------- | ------------------------------------------------------------------------------------------------------------------ | --------------------- | ----------- | --- | --------------------------------------------------------------------------------------------------------------------------- |
| FLEET-OBS-000 | done | Plan: north-star + Phase-2 PRD + workstream scaffolding | — | lead | — | persisted 2026-06-20 on `feat/fleet-observability` |
| FLEET-OBS-001 | done | Heartbeat protocol v1 spec finalized in PRD + framework doc | FLEET-OBS-000 | lead | — | file-based `~/.config/mosaic/fleet/run/<agent>.hb`; spec in PRD |
| FLEET-OBS-002 | in-progress | Implement heartbeat responder in `dogfood-agent.py` | FLEET-OBS-001 | fleet-coder | — | dispatched to ad-hoc `mosaic yolo` fleet agent (dogfood) |
| FLEET-OBS-003 | done | `mosaic fleet ps` — join systemd+tmux+proc+idle+heartbeat; tenant+host tagged; drift + boot-enable flags; `--json` | FLEET-OBS-001 | worker | — | commit ab47831; LIVE-verified on mosaic-fleet; caught canary-pi DRIFT + BOOT-ENABLE. Polish: idleSeconds parse returns null |
| FLEET-OBS-004 | done | `mosaic agent watch <name>` — read-only join (no resize, no keystrokes) | FLEET-OBS-000 | worker | — | `attach -r`; verb wired |
| FLEET-OBS-005 | done | `mosaic agent send --verify` — delivery/acceptance receipt | FLEET-OBS-000 | worker | — | --verify flag; draft-heuristic verify |
| FLEET-OBS-006 | done | CLI specs for ps/watch/send-verify (tenant+host shape, command construction) | FLEET-OBS-003,004,005 | worker | — | 62 tests green (31 new); re-verified by lead |
| FLEET-OBS-007 | not-started | Framework doc: fleet observability guide + verbs | FLEET-OBS-003,004,005 | lead | — | `docs/guides/` or `framework/tools/.../README` |
| FLEET-OBS-008 | not-started | Independent review + dogfood verification on live fleet | FLEET-OBS-002..007 | reviewer | — | author ≠ reviewer; capture evidence in scratchpad |
| FLEET-OBS-009 | not-started | Open PR → green CI (queue guard) → squash-merge → close `fleet-observability-1` | FLEET-OBS-008 | lead | — | trunk merge; no direct push to main |
| id | status | description | depends_on | agent | pr | notes |
| ------------- | ----------- | ---------------------------------------------------------------------------------------------------------------- | --------------------- | ----------- | --- | --------------------------------------------------------------------------------------------------------------------------- |
| FLEET-OBS-000 | done | Plan: north-star + Phase-2 PRD + workstream scaffolding | — | lead | — | persisted 2026-06-20 on `feat/fleet-observability` |
| FLEET-OBS-001 | done | Heartbeat protocol v1 spec finalized in PRD + framework doc | FLEET-OBS-000 | lead | — | file-based ~/.config/mosaic/fleet/run/<agent>.hb; spec in PRD |
| FLEET-OBS-002 | in-progress | Implement heartbeat responder in `dogfood-agent.py` | FLEET-OBS-001 | fleet-coder | — | dispatched to ad-hoc mosaic yolo fleet agent (dogfood) |
| FLEET-OBS-003 | done | mosaic fleet ps — join systemd+tmux+proc+idle+heartbeat; tenant+host tagged; drift + boot-enable flags; `--json` | FLEET-OBS-001 | worker | — | commit ab47831; LIVE-verified on mosaic-fleet; caught canary-pi DRIFT + BOOT-ENABLE. Polish: idleSeconds parse returns null |
| FLEET-OBS-004 | done | mosaic agent watch <name> — read-only join (no resize, no keystrokes) | FLEET-OBS-000 | worker | — | attach -r; verb wired |
| FLEET-OBS-005 | done | mosaic agent send --verify — delivery/acceptance receipt | FLEET-OBS-000 | worker | — | --verify flag; draft-heuristic verify |
| FLEET-OBS-006 | done | CLI specs for ps/watch/send-verify (tenant+host shape, command construction) | FLEET-OBS-003,004,005 | worker | — | 62 tests green (31 new); re-verified by lead |
| FLEET-OBS-007 | not-started | Framework doc: fleet observability guide + verbs | FLEET-OBS-003,004,005 | lead | — | `docs/guides/` or `framework/tools/.../README` |
| FLEET-OBS-008 | not-started | Independent review + dogfood verification on live fleet | FLEET-OBS-002..007 | reviewer | — | author ≠ reviewer; capture evidence in scratchpad |
| FLEET-OBS-009 | not-started | Open PR → green CI (queue guard) → squash-merge → close `fleet-observability-1` | FLEET-OBS-008 | lead | — | trunk merge; no direct push to main |
## Proposed MVP rollup row (for the MVP orchestrator — not written by this workstream)
```
```text-table
| W-FLEET | in-progress | Fleet (agent-session execution layer) | Phase 2/5 | docs/fleet/TASKS.md | observability dogfooded on live stub fleet; control plane rides federation (W1) |
```
+28 -30
View File
@@ -2,10 +2,10 @@
The **backlog** is Mosaic's native backlog-of-record for fleet work. It is built
end-to-end on Mosaic's own storage layer (`@mosaicstack/db`, drizzle/Postgres)
and surfaced as `mosaic fleet backlog <sub> --json`.
and surfaced as mosaic fleet backlog <sub> --json.
> **Mosaic-native, no Hermes.** This backlog REPLACES the former Hermes adapter.
> There is **no** runtime dependency on Hermes, `hermes kanban`, or `~/.hermes`
> There is **no** runtime dependency on Hermes, hermes kanban, or ~/.hermes
> anywhere in this feature. Anything previously delegated to Hermes is recreated
> here on Mosaic's own Postgres storage layer.
@@ -18,7 +18,7 @@ engine (no sqlite, no raw client).
| ---------------------------------- | -------------------- | ---------------------------------------------------------------- |
| `DATABASE_URL` injected at runtime | Full server Postgres | the verified runtime database; it never authorizes migration/DDL |
| `PGLITE_DATA_DIR` set (no URL) | Embedded PGlite | that directory |
| neither (default) | Embedded PGlite | `~/.config/mosaic/fleet/backlog` |
| neither (default) | Embedded PGlite | ~/.config/mosaic/fleet/backlog |
PGlite is real Postgres semantics in-process — including the row locks the atomic
claim relies on — so the **same code** runs on a laptop (embedded, single-host
@@ -28,9 +28,9 @@ For embedded PGlite only, the local backlog routine may prepare its local schema
### Update safety
The embedded PGlite store lives under `~/.config/mosaic/fleet/backlog`, which is
The embedded PGlite store lives under ~/.config/mosaic/fleet/backlog, which is
listed in `PRESERVE_PATHS` in `packages/mosaic/framework/install.sh`. This means
`mosaic update` (which runs the framework sync with `rsync --delete`) will **not**
mosaic update (which runs the framework sync with rsync --delete) will **not**
wipe the operator's backlog — same protection as the roster, per-agent env, and
heartbeat run dir.
@@ -46,10 +46,10 @@ A card is one row in the `backlog` table:
| `phase` | text (nullable) | Board/phase grouping (see below). |
| `priority` | int (default 0) | **Higher = sooner.** Claim picks the max-priority ready card. |
| `status` | enum | `ready` \| `claimed` \| `blocked` \| `done`. |
| `depends_on` | jsonb `string[]` | DAG edges — ids of cards this one depends on. |
| `depends_on` | jsonb string[] | DAG edges — ids of cards this one depends on. |
| `claim_owner` | text (nullable) | Owner token of the active claim. |
| `claim_ttl_seconds` | int (nullable) | TTL of the active claim. |
| `claimed_at` | timestamptz (null) | When the claim was taken. `claimed_at + ttl` = expiry. |
| `claimed_at` | timestamptz (null) | When the claim was taken. claimed_at + ttl = expiry. |
| `attempts` | int (default 0) | Incremented each time the card is claimed. |
| `idempotency_key` | text (unique, null) | Dedups `create`; NULLs are distinct in Postgres. |
| `acceptance` | jsonb (nullable) | Acceptance criteria (array of strings or object). |
@@ -65,12 +65,12 @@ would add ceremony without benefit.
### Board / phase convention
`phase` is a free-form grouping string used as the board column / milestone label
(e.g. `M1`, `fleet`, `infra`). `list --phase <phase>` filters to one board lane.
(e.g. `M1`, `fleet`, `infra`). list --phase <phase> filters to one board lane.
`priority` orders cards **within** the ready pool regardless of phase.
## Status lifecycle
```
```text-diagram
create
@@ -87,51 +87,49 @@ would add ceremony without benefit.
- **blocked** — explicitly parked; never auto-claimed.
- **done** — completed; satisfies dependents.
## Atomic claim (`FOR UPDATE SKIP LOCKED`) + TTL
## Atomic claim (FOR UPDATE SKIP LOCKED) + TTL
`claim` is atomic. Inside a single transaction it locks candidate `ready` rows
with `SELECT ... FOR UPDATE SKIP LOCKED` (via the drizzle `sql` operator), picks
with SELECT ... FOR UPDATE SKIP LOCKED (via the drizzle `sql` operator), picks
the highest-priority deps-satisfied card, and flips it to `claimed`. Because a row
already locked by a concurrent claimer is **skipped**, two claimers can **never**
both win the same card — the loser falls through to the next candidate or gets
`null`. (Proven by the concurrency tests in `packages/db/src/backlog.spec.ts`.)
- **Deps gate:** a card is only claimable when every id in `depends_on` is `done`.
- **TTL:** `claim --ttl <sec>` (default **900s**) records `claim_ttl_seconds`.
- **reclaim:** releases claims whose `claimed_at + ttl` is in the past (expired)
back to `ready`, clearing the claim fields. `reclaim --id <id>` force-releases a
- **TTL:** claim --ttl <sec> (default **900s**) records `claim_ttl_seconds`.
- **reclaim:** releases claims whose claimed_at + ttl is in the past (expired)
back to `ready`, clearing the claim fields. reclaim --id <id> force-releases a
specific card regardless of expiry. This is how a crashed worker's card returns
to the pool.
## CLI — `mosaic fleet backlog <sub> --json`
## CLI — mosaic fleet backlog <sub> --json
All subcommands support `--json`.
| Subcommand | Purpose |
| --------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------- |
| `create --id --title [--body --phase --priority --depends-on --acceptance --idempotency-key]` | Create a card; `idempotency_key` dedups (repeat returns the existing card). |
| `list [--status --phase --ready-only]` | List cards. `--ready-only` = status `ready` AND all deps `done`. |
| `claim --owner [--ttl <sec> --id <id>]` | Atomically claim the highest-priority ready card (or `--id`). Returns the card or `null`. |
| `reclaim [--id <id>]` | Release expired claims (or a specific card) back to `ready`. |
| `link --from --to` | Add a `depends_on` edge (`--from` depends on `--to`). |
| `stats` | Counts by status, oldest-ready age, expired-claim count. |
| `block --id` | Set a card to `blocked`. |
| `complete --id` | Set a card to `done` (releases any claim). |
| Subcommand | Purpose |
| ------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------- |
| create --id --title [--body --phase --priority --depends-on --acceptance --idempotency-key] | Create a card; `idempotency_key` dedups (repeat returns the existing card). |
| list [--status --phase --ready-only] | List cards. `--ready-only` = status `ready` AND all deps `done`. |
| claim --owner [--ttl <sec> --id <id>] | Atomically claim the highest-priority ready card (or `--id`). Returns the card or `null`. |
| reclaim [--id <id>] | Release expired claims (or a specific card) back to `ready`. |
| link --from --to | Add a `depends_on` edge (`--from` depends on `--to`). |
| `stats` | Counts by status, oldest-ready age, expired-claim count. |
| block --id | Set a card to `blocked`. |
| complete --id | Set a card to `done` (releases any claim). |
### Example
```sh
# Seed two cards, the second depends on the first.
Seed two cards; the second depends on the first. Because A2 is gated on A1, claim returns A1 first. Finish A1, then list A2 as ready. Recover stalled work.
```fleet-command
mosaic fleet backlog create --id A1 --title "schema" --priority 5
mosaic fleet backlog create --id A2 --title "service" --depends-on A1 --priority 9
# A2 is gated on A1, so claim returns A1 first.
mosaic fleet backlog claim --owner worker-1 --ttl 600 --json
# Finish A1; now A2 is ready.
mosaic fleet backlog complete --id A1
mosaic fleet backlog list --ready-only --json
# Recover stalled work.
mosaic fleet backlog reclaim --json
```
@@ -0,0 +1,42 @@
# Desired, Derived, and Observed Fleet State
## One writable authority
The canonical local v2 roster at <MOSAIC_HOME>/fleet/roster.yaml is desired state. Membership, stable identity, class, runtime/provider/model selection, launch policy, enablement, and persisted `running`/`stopped` intent are written only through generation-guarded roster mutations.
Derived projections are reproducible consequences of that authority:
- <name>.env.generated;
- exact roster-named tmux sessions on the configured socket after reconciliation;
- systemd service targets managed by installation/reconciliation.
Current systemd unit enablement is not yet lifecycle-conformant at boot: installation can enable every
agent unit, and the launcher projection does not carry `enabled` or `desired_state`. Therefore reboot
preservation for stopped/disabled agents remains an FCM-M3-002 acceptance hold, not a guaranteed
projection behavior.
Observed evidence available to current roster-v2 status commands includes systemd active state, tmux
presence, holder ownership, and unmanaged sessions. Heartbeat files are observational in the wider fleet,
but roster-v2 `status`, `doctor`, and `verify` do not currently read them. Observation never writes back
to the roster.
## Generation and ownership
`generation` is a positive integer concurrency fence. A mutating request must provide the current value. Successful changed CRUD increments it exactly once; stale or concurrent writers fail before mutation. Apply/reconcile rereads the canonical roster under a private exclusive lock and uses only that generation and content for effects.
Ownership is exact, never fuzzy. Reconciliation is limited to roster names, the configured socket, the exact holder session, a private installation identity, and private managed paths. An ownership mismatch, unmanaged session, unsafe path, stale generation, or ambiguous lock fails closed.
## Drift decisions
| Observation | Interpretation | Safe response |
| ---------------------------------------- | ------------------------ | ---------------------------------------------------------------------- |
| Generated file differs or is missing | Derived projection drift | Review apply --dry-run; regenerate from the roster. |
| Desired `running`, exact session missing | `missing-session` | Diagnose ownership/runtime, then reconcile if safe. |
| Desired `stopped`, exact session present | `unexpected-session` | Inspect; reconciliation may stop only the proven roster target. |
| Disabled agent running | `disabled-running` | Inspect; disabled state wins during explicit safe reconciliation. |
| Unknown session on the configured socket | Unmanaged state | Report only. Do not adopt, rename, or kill it. |
| Heartbeat stale in the wider fleet | Liveness evidence | Diagnose separately; current roster-v2 status does not read heartbeat. |
`status` and `doctor` classify. `verify` is also observational but exits non-zero when ownership, drift, or unmanaged-state checks fail. `plan`/apply --dry-run validates proposed projection and lifecycle work without mutation. `apply` and `reconcile` converge only after all preconditions pass.
A partial projection failure does not roll the roster back. Treat the committed roster as authority and regenerate. A lifecycle failure after projection completion preserves both roster and projections for inspection. Sensitive legacy values are never printed; diagnostics are bounded to stable codes, key names where applicable, and hashes.
@@ -0,0 +1,23 @@
# Generated Environment Launch Chain
The launcher consumes validated data, not shell configuration.
1. Read and validate the canonical roster.
2. Render deterministic <name>.env.generated data from that roster.
3. Parse optional <name>.env.local through a strict allowlist.
4. Reject generated-key shadowing, unknown or sensitive-looking keys, unsafe paths/values, duplicates, malformed lines, shell syntax, and command overrides.
5. Derive the runtime command from validated runtime/model/reasoning data.
6. Target only the exact configured tmux socket and roster session after ownership checks.
## File precedence and ownership
| File | Owner | Use |
| ----------------- | ------------------------ | --------------------------------------------------------------------------- |
| `.env.generated` | Mosaic projection writer | Complete deterministic roster projection. Rebuild; do not edit. |
| `.env.local` | Operator | Optional, private, strict machine-local data. Cannot shadow generated keys. |
| `.env` | Legacy input | One-time migration input only; never launch authority. |
| `.env.quarantine` | Private quarantine | Retained unsafe legacy evidence; never loaded by the launcher. |
Neither systemd nor the launcher sources these files. No `eval`, shell expansion, arbitrary `MOSAIC_AGENT_COMMAND`, channel, or secret-reference compatibility path exists. Safe legacy generated keys are regenerated, allowed local keys are relocated, and unsafe material is quarantined.
Diagnostics never expose the rejected value, credential material, or command text. They are bounded to stable rule code, key name where safe, and SHA-256 content identity. See [generated environment reference](../reference/generated-env-boundary.md) and [quarantine operations](../operations/env-quarantine.md).
@@ -0,0 +1,20 @@
# Fleet Identity, Class, and Runtime
Each roster field has one job. Do not use names or model strings as authority shortcuts.
| Concern | Field | Contract |
| ----------------------- | ----------------------------- | -------------------------------------------------------------------------------------------- |
| Stable machine identity | agents[].name | Unique, immutable mutation target and exact service/session name. |
| Display identity | agents[].alias | Human-facing label only; may be changed and grants no authority. |
| Behavioral contract | agents[].class | Resolves through the shared baseline plus `roles.local` persona library. |
| Tool boundary | agents[].tool_policy | Must match protected canonical classes; cannot independently grant authority. |
| Harness | agents[].runtime | One of `claude`, `codex`, `opencode`, or `pi`, declared in `runtimes`. |
| Backend selection | agents[].provider and `model` | Explicit non-empty data; capability validity is not inferred from the display name or class. |
| Effort | agents[].reasoning | `low`, `medium`, or `high`. |
| Local placement | `working_directory` | Explicit safe local work path; not remote placement authority. |
Tess and Ultron are conventional instance/display names only. They are not products, required machine identities, role aliases, or authority-bearing classes. A configurable interaction instance uses class: interaction; a configurable validation instance uses class: validator. Any stable name and alias satisfying the structural contract may be used.
Class aliases are deliberately narrow: implementer → code, reviewer → review, and operator-interaction → interaction. No runtime, provider, model, persona prose, or instance name changes this mapping. See [role classes](../reference/role-classes.md) and the [validated generic example](../examples/roster-v2.yaml).
Roster v2 is local-only. It contains no host/SSH placement, connector, channel, secret-reference, arbitrary-command, per-agent socket, or gateway mapping fields. Those concerns require separate requirements and threat models.
@@ -0,0 +1,22 @@
# Fleet Role Authority and Leases
Role content describes behavior; protected authority is immutable code metadata derived only from the canonical class.
## Required workstream classes
`code`, `review`, `validator`, `orchestrator`, `team-leader`, `enhancer`, and `interaction` are required FCM classes. `merge-gate` is additionally protected because it remains the sole approve-to-land and merge authority.
| Class | Authority | Boundary |
| -------------------------------------------- | ------------------------------------------------- | -------------------------------------------------------------------------------------- |
| `merge-gate` | Approve-to-land and merge | Sole merge authority. |
| `validator` | Issue independent validation evidence/certificate | Never approves landing or merges. |
| `orchestrator` | Orchestrate topology and issue bounded leases | Does not gain merge authority. |
| `team-leader` | Use explicitly leased capacity | Cannot issue leases or mutate roster, credentials, topology authority, or merge state. |
| `interaction` | Receive requests and report status | Cannot orchestrate, issue leases, mutate configuration, or merge. |
| `code`, `review`, `enhancer`, custom classes | No protected authority by default | Persona prose cannot grant protected powers. |
A lease is capacity authorization from an orchestrator, not ownership. It must identify a bounded task or period and does not alter the leased agent's roster identity, role contract, credentials, authority, or persisted lifecycle. Expiry/revocation returns capacity; it does not rewrite the roster.
Semantic validation rejects protected class/tool-policy mismatch in either direction. An instance named Ultron with class: validator remains validation-only. An instance named Tess with class: interaction remains request/status-only. Renaming either instance changes no authority.
For resolver layering and safe customization, see [role classes](../reference/role-classes.md) and [customize roles](../how-to/customize-roles.md).
+61
View File
@@ -0,0 +1,61 @@
version: 2
generation: 1
transport: tmux
tmux:
socket_name: mosaic-fleet
holder_session: _holder
defaults:
working_directory: ~/src
runtime: pi
runtimes:
pi:
reset_command: /new
agents:
- name: code-example
alias: Code Example
class: code
runtime: pi
provider: example-provider
model: example-model
reasoning: medium
tool_policy: code
working_directory: ~/src
persistent_persona: false
reset_between_tasks: true
lifecycle:
enabled: true
desired_state: stopped
launch:
yolo: false
- name: interaction-example
alias: Interaction Example
class: interaction
runtime: pi
provider: example-provider
model: example-model
reasoning: low
tool_policy: interaction
working_directory: ~/src
persistent_persona: true
reset_between_tasks: false
lifecycle:
enabled: true
desired_state: stopped
launch:
yolo: false
- name: validator-example
alias: Validator Example
class: validator
runtime: pi
provider: example-provider
model: example-model
reasoning: high
tool_policy: validator
working_directory: ~/src
persistent_persona: false
reset_between_tasks: true
lifecycle:
enabled: true
desired_state: stopped
launch:
yolo: false
+11 -11
View File
@@ -15,7 +15,7 @@ core.
Connectors implement one small, uniform interface (`src/fleet/connectors/types.ts`):
```ts
```typescript
interface OrchestratorConnector {
readonly kind: 'tmux' | 'discord' | 'matrix';
send(message: OutboundMessage): Promise<SendResult>; // orchestrator → human
@@ -25,11 +25,11 @@ interface OrchestratorConnector {
```
- **send / subscribe / health** — the only surface fleet core depends on. `SendResult` is the
ack half; `health()` is the liveness half.
ack half; health() is the liveness half.
- **Thread-aware by metadata**`OutboundMessage.threadId` / `InboundMessage.threadId` are
optional, so thread-capable connectors (Matrix rooms/threads, the future first-party Mosaic
Discord plugin) fit **without an interface change**.
- **Registry** (`registry.ts`) — implementations register a factory by kind; `createConnector(config)`
- **Registry** (`registry.ts`) — implementations register a factory by kind; createConnector(config)
resolves one from roster config. Phase 1 ships the registry + `resolveConnectorKind` (defaults
`tmux` when a roster declares no connector — **back-compat**); the factories land in Phase 2.
@@ -39,7 +39,7 @@ A roster may carry an optional `connector` block (`roster.schema.json`); absent
```yaml
connector:
kind: matrix # tmux | discord | matrix
kind: matrix
matrix:
homeserver_url: https://matrix.example.internal
user_id: '@mos:example.internal'
@@ -55,12 +55,12 @@ reject a token committed to a shipped file anyway.
The connector speaks the **Matrix client-server API** directly over HTTPS (`fetch` — no SDK needed
for MVP), so it is **homeserver-agnostic**:
| Op | Matrix CS-API |
| ----------- | ------------------------------------------------------------------------ |
| `send` | `PUT /_matrix/client/v3/rooms/{roomId}/send/m.room.message/{txnId}` |
| `subscribe` | `GET /_matrix/client/v3/sync` (long-poll, `since` token) → room timeline |
| `health` | `GET /_matrix/client/versions` (reachable) + `…/account/whoami` (authed) |
| threads | `m.thread` relations ↔ `threadId` |
| Op | Matrix CS-API |
| ----------- | ----------------------------------------------------------------------- |
| `send` | PUT /\_matrix/client/v3/rooms/{roomId}/send/m.room.message/{txnId} |
| `subscribe` | GET /\_matrix/client/v3/sync (long-poll, `since` token) → room timeline |
| `health` | GET /\_matrix/client/versions (reachable) + …/account/whoami (authed) |
| threads | `m.thread` relations ↔ `threadId` |
## Local homeserver (infra, not connector code)
@@ -79,7 +79,7 @@ homeserver choice is a **deployment** concern (a Phase-2 deploy guide), not conn
| ----- | --------------------------------------------------------------------------------------- | ------- |
| **1** | Connector interface + types, registry + kind resolution, roster `connector` schema, doc | ✅ yes |
| 2 | Matrix CS-API client (fetch-based send/sync/health) + registered factory + tests | follow |
| 2 | `fleet init` / `configure` connector-selection UX; roster parse wires the block | follow |
| 2 | fleet init / `configure` connector-selection UX; roster parse wires the block | follow |
| 2 | systemd launch wiring so the orchestrator starts on the chosen connector | follow |
| 3 | Conduit deploy guide; first-party Mosaic Discord (threads) registers as a connector | follow |
@@ -0,0 +1,21 @@
# Configure an Interaction Instance
An interaction instance is a configurable local roster member with canonical class: interaction and matching tool_policy: interaction. “Tess” may be used as a display alias, but neither that alias nor the stable name is required or authority-bearing.
Use the [validated generic roster](../examples/roster-v2.yaml) as the safe shape. Choose a unique stable `name`, any descriptive `alias`, a supported declared runtime, explicit provider/model/reasoning, and a safe work directory. Start with:
```yaml
name: interaction-example
alias: Interaction Example
class: interaction
tool_policy: interaction
lifecycle:
enabled: true
desired_state: stopped
```
Plan the complete agent payload with the current roster generation, then create it without `--persisted-start`. Creation defaults to enabled/stopped and performs no runtime action. Review the resulting roster and projection plan before any later lifecycle decision.
The interaction class is request/status only. It cannot orchestrate, issue leases, mutate the roster/configuration, grant credentials, certify validation, approve landing, or merge. Connector and channel configuration are outside roster v2; do not add connector, channel, secret, command, remote-host, or gateway fields.
See [safe CRUD](create-update-delete-agent.md), [identity separation](../concepts/identity-class-runtime.md), and [role authority](../concepts/role-authority-and-leases.md).
@@ -0,0 +1,21 @@
# Configure a Validator Instance
A validator instance is a configurable local roster member with canonical class: validator and matching tool_policy: validator. “Ultron” may be used as a display alias, but it is not a required identity, class alias, product name, or source of authority.
Use the [validated generic roster](../examples/roster-v2.yaml) as the safe shape. Choose a unique stable name and explicit supported runtime/provider/model/reasoning values. Start stopped:
```yaml
name: validator-example
alias: Validator Example
class: validator
tool_policy: validator
lifecycle:
enabled: true
desired_state: stopped
```
Plan the full payload with the current generation and create without `--persisted-start`. Creation writes desired state and projections only; it does not launch a validator.
`validator` may issue independent validation evidence or a certificate. It has no approve-to-land or merge authority. `merge-gate` remains the sole protected merge authority, and changing the validator's name, alias, persona prose, runtime, provider, model, or tool-policy text cannot elevate it.
Certificate consumption and final release evidence remain FCM-M5-002 gates. This page does not create a certificate or authorize merge. See [safe CRUD](create-update-delete-agent.md) and [role authority](../concepts/role-authority-and-leases.md).
+10 -10
View File
@@ -4,20 +4,20 @@ Use the local roster-v2 control plane only. These commands change desired state
## Read and plan first
```sh
```fleet-synopsis
mosaic fleet get <name>
mosaic fleet plan create --expected-generation <n> --agent '<json>'
mosaic fleet plan update <name> --expected-generation <n> --agent '<json>'
mosaic fleet plan delete <name> --expected-generation <n>
```
`plan create` takes the name from `--agent`. `plan update` and `plan delete` require the target name immediately after the operation. A plan is deterministic and side-effect free: it validates the complete proposed roster and projection targets without changing files. Use `--dry-run` on `create`, `update`, or `delete` for the same no-write result.
plan create takes the name from `--agent`. plan update and plan delete require the target name immediately after the operation. A plan is deterministic and side-effect free: it validates the complete proposed roster and projection targets without changing files. Use `--dry-run` on `create`, `update`, or `delete` for the same no-write result.
Every successful command prints JSON. `get` returns `{ "generation", "agent" }`; mutation results contain `plan`, `applied`, `authoritativeRoster`, and `projections`.
Every successful command prints JSON. `get` returns { "generation", "agent" }; mutation results contain `plan`, `applied`, `authoritativeRoster`, and `projections`.
## Create safely
```sh
```fleet-command
mosaic fleet create --expected-generation 7 --agent '{
"name":"coder0",
"alias":"Coder 0",
@@ -34,20 +34,20 @@ mosaic fleet create --expected-generation 7 --agent '{
}'
```
Create defaults to `enabled: true` and `desired_state: stopped`. It does not start a process. Add `--persisted-start` only to persist `desired_state: running`; that still does not start a runtime in this M2 command. The JSON payload is an allowlist of the roster-v2 fields shown above plus `launch.yolo`; command, channel, secret-reference, and other unknown keys are rejected rather than ignored. The JSON error exposes only a stable code, never the rejected value.
Create defaults to enabled: true and desired_state: stopped. It does not start a process. Add `--persisted-start` only to persist desired_state: running; that still does not start a runtime in this M2 command. The JSON payload is an allowlist of the roster-v2 fields shown above plus `launch.yolo`; command, channel, secret-reference, and other unknown keys are rejected rather than ignored. The JSON error exposes only a stable code, never the rejected value.
## Update and delete safely
```sh
mosaic fleet update coder0 --expected-generation 8 --agent '<complete JSON agent payload>'
mosaic fleet delete coder0 --expected-generation 9
```fleet-synopsis
mosaic fleet update <name> --expected-generation <n> --agent '<complete JSON agent payload>'
mosaic fleet delete <name> --expected-generation <n>
```
Updates require a complete agent JSON payload and preserve the stable name. Delete removes only the exact roster-owned `coder0.env.generated` projection. It retains `coder0.env.local`, legacy `coder0.env`, `coder0.env.quarantine`, and every unrelated projection. A delete dry-run leaves all of those files byte-identical.
## Handle generation conflicts
Every mutation requires the current authoritative `--expected-generation`. A stale value returns JSON `error.code: "stale-generation"` with a non-zero exit. Reload with `mosaic fleet get <name>` or reread the roster, plan again using the returned generation, then retry. A concurrent mutation returns `concurrent-mutation`; do not force or bypass the lock.
Every mutation requires the current authoritative `--expected-generation`. A stale value returns JSON error.code: "stale-generation" with a non-zero exit. Reload with mosaic fleet get <name> or reread the roster, plan again using the returned generation, then retry. A concurrent mutation returns `concurrent-mutation`; do not force or bypass the lock.
## Interpret partial failures
@@ -71,4 +71,4 @@ This is not a rollback and not a no-op: reload the roster because its generation
Handled validation errors and partial projection failures exit non-zero. `plan`/`--dry-run` and normal mutation JSON make the state explicit; scripts should use both the exit code and `authoritativeRoster`/`projections`, not `applied` alone.
The commands operate only on `<mosaic-home>/fleet/roster.yaml`, the local roster desired-state authority. They do not accept arbitrary commands, channels, secrets, remote/connector actions, migration/canary actions, or runtime lifecycle operations.
The commands operate only on <mosaic-home>/fleet/roster.yaml, the local roster desired-state authority. They do not accept arbitrary commands, channels, secrets, remote/connector actions, migration/canary actions, or runtime lifecycle operations.
+5 -5
View File
@@ -2,8 +2,8 @@
Mosaic resolves persona contracts through two layers:
1. `fleet/roles/<canonical-class>.md` — seeded baseline contract.
2. `fleet/roles.local/<canonical-class>.md` — operator override or custom role; this layer wins.
1. fleet/roles/<canonical-class>.md — seeded baseline contract.
2. fleet/roles.local/<canonical-class>.md — operator override or custom role; this layer wins.
The same shared resolver is used by profile validation, provisioning, roster-v2 semantic validation,
and launch-time persona injection.
@@ -34,11 +34,11 @@ A custom class remains supported when a readable contract exists for the exact i
The release-notes role (`class: release-notes`) prepares operator-reviewed release copy.
```
Save it as `fleet/roles.local/release-notes.md`, then reference `class: release-notes` and a matching
`tool_policy: release-notes` in roster v2. Adding only a `LIBRARY.md` row is insufficient.
Save it as `fleet/roles.local/release-notes.md`, then reference class: release-notes and a matching
tool_policy: release-notes in roster v2. Adding only a `LIBRARY.md` row is insufficient.
Names such as `worker`, `analyst`, and `canary` are not built-in aliases; they need genuine custom
contracts. `agents[].alias`, Tess, and Ultron are display names and cannot select a class.
contracts. agents[].alias, Tess, and Ultron are display names and cannot select a class.
## Validation and authority boundaries
+5 -5
View File
@@ -2,22 +2,22 @@
Use the canonical local roster-v2 command surface:
```sh
```fleet-synopsis
mosaic fleet apply --expected-generation <n> --dry-run
mosaic fleet apply --expected-generation <n>
mosaic fleet reconcile --expected-generation <n>
mosaic fleet start <name> --expected-generation <n>
mosaic fleet stop <name> --expected-generation <n>
mosaic fleet restart <name> --expected-generation <n>
mosaic fleet status [name]
mosaic fleet status [<name>]
mosaic fleet verify
mosaic fleet doctor
```
Start with `--dry-run`. It validates roster semantics, deterministic projections, private managed paths, exact holder ownership, and named-socket state without changing files or lifecycle state. `apply` and `reconcile` rebuild derived projections and enforce only persisted roster state: enabled `running` agents may start, while stopped or disabled agents are not started.
Start with `--dry-run`. It validates roster semantics, deterministic projections, private managed paths, exact holder ownership, and named-socket state without changing files or lifecycle state. Explicit `apply` and `reconcile` rebuild derived projections and enforce persisted roster state: enabled `running` agents may start, while stopped or disabled agents are not started. This guarantee does not extend to reboot/service activation yet; boot preservation remains an FCM-M3-002 hold.
`start`, `stop`, and `restart` are explicit one-shot exact-service actions. They do not persist a lifecycle change. Roster CRUD is the only way to change persisted desired state.
`start`, `stop`, and `restart` are explicit one-shot exact-service actions. They do not persist a lifecycle change. `update` preserves the agent's existing lifecycle, and no delivered operation changes durable lifecycle after creation.
Every command prints JSON. Observation commands report drift without mutation; `verify` exits non-zero on ownership mismatch, unmanaged sessions, or drift. A failed apply that wrote some derived projections reports `projections: "incomplete"` with bounded recovery to regenerate from the roster. A lifecycle failure after projections reports incomplete lifecycle work; it is never represented as a rollback or no-op.
Every command prints JSON. Observation commands report drift without mutation; `verify` exits non-zero on ownership mismatch, unmanaged sessions, or drift. A failed apply that wrote some derived projections reports projections: "incomplete" with bounded recovery to regenerate from the roster. A lifecycle failure after projections reports incomplete lifecycle work; it is never represented as a rollback or no-op.
These commands are local only. Remote/SSH/connector entries are inventory/validation-only. Commands do not accept arbitrary runtime commands, channels, secrets, generated-file desired state, or arbitrary tmux sockets.
@@ -11,7 +11,7 @@ artifact is added, removed, or left without one of the dispositions below.
## Disposition rules
- **Explicit v1 fixture:** the artifact is loaded through the existing v1 roster parser and must
declare `version: 1`. It remains a compatibility fixture; it is not silently treated as a v2
declare version: 1. It remains a compatibility fixture; it is not silently treated as a v2
roster or given inferred aliases.
- **Canonical profile:** the artifact is loaded through `loadProfiles`, which uses the shared
baseline-plus-`roles.local` persona resolver and rejects unreadable or unresolved classes.
@@ -59,7 +59,7 @@ rollback; those gates belong to FCM-M4-002. See [v1-to-v2 preview](./v1-to-v2.md
## Running the guard
```bash
```fleet-command
pnpm --filter @mosaicstack/mosaic test -- v1-v2-migration.spec.ts \
-t "validates all 13 shipped artifacts and executes ready previews for every v1 fixture"
```
+20 -20
View File
@@ -2,14 +2,14 @@
**Issue:** #758 · **Card:** FCM-M4-001 · **Effect boundary:** preview only
`mosaic fleet migrate-v1 preview` inventories a v1 roster and emits a canonical v2 candidate plus
mosaic fleet migrate-v1 preview inventories a v1 roster and emits a canonical v2 candidate plus
recovery evidence. It does not write a roster, apply environment projections, invoke systemd or
`tmux`, contact connectors or remote hosts, launch an agent, run a canary, or execute rollback.
FCM-M4-002 owns reversible cutover and rollback.
## Inputs
```bash
```fleet-command
mosaic fleet migrate-v1 preview \
--source roster-v1.yaml \
--decisions migration-decisions.json \
@@ -44,25 +44,25 @@ be marked disabled. Observed-stopped agents always remain stopped.
## Field disposition
| v1 field | v2 disposition |
| ------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `version`, `transport`, `tmux`, `defaults`, `runtimes` | Inventoried and structurally compiled; omitted runtimes retain v1 built-in defaults, while each explicitly declared runtime without a reset field follows the production v1 `/clear` fallback; present-empty holder/work-directory/reset values block |
| agent `name`, `alias`, `runtime`, working directory, persona/reset flags | Copied or explicitly defaulted only when absent; present-empty alias/work-directory values block for explicit disposition. Canonical `~`/`~/...` values stay unchanged in roster evidence and traversal-free forms expand only at the shared production environment-projection boundary before unchanged absolute-path validation |
| `provider`, `model_hint`, `reasoning_level` | Explicit provider/model/reasoning decisions; no model-hint inference |
| `class`, `tool_policy` | Only approved aliases canonicalize automatically; other classes require explicit preserve/replace disposition and shared-resolver validation |
| `kickstart_template` | No v2 field; explicit inventory-only disposition required |
| agent `host`, `ssh` | `host != fleetHost` is demonstrably remote and inventory-only; `host == fleetHost` stays local; SSH targets with or without an explicit user must agree with `host`; ssh-only, missing fleet-host evidence, or contradictory targets block |
| agent `socket` | Same-host candidate only when it matches the canonical fleet socket; conflicts block for explicit future disposition |
| root `connector` | Inventory-only; never contacted or reconciled |
| unknown fields or snake/camel synonym collisions | Inventoried and block readiness |
| `.env.generated` | Rebuild from canonical roster data |
| no legacy `.env` | `absent`; no legacy action required |
| legacy `.env` containing generated keys only | `regenerate-only`; replace later from canonical roster data |
| legacy `.env` containing strict local keys | `relocate-local`; preserve those keys in `.env.local` during a later reviewed cutover |
| legacy `.env` containing forbidden/unsafe/sensitive/malformed keys | `quarantine`; private input only, with diagnostics limited to code, key, and SHA-256 |
| v1 field | v2 disposition |
| ------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `version`, `transport`, `tmux`, `defaults`, `runtimes` | Inventoried and structurally compiled; omitted runtimes retain v1 built-in defaults, while each explicitly declared runtime without a reset field follows the production v1 `/clear` fallback; present-empty holder/work-directory/reset values block |
| agent `name`, `alias`, `runtime`, working directory, persona/reset flags | Copied or explicitly defaulted only when absent; present-empty alias/work-directory values block for explicit disposition. Canonical ~/~/... values stay unchanged in roster evidence and traversal-free forms expand only at the shared production environment-projection boundary before unchanged absolute-path validation |
| `provider`, `model_hint`, `reasoning_level` | Explicit provider/model/reasoning decisions; no model-hint inference |
| `class`, `tool_policy` | Only approved aliases canonicalize automatically; other classes require explicit preserve/replace disposition and shared-resolver validation |
| `kickstart_template` | No v2 field; explicit inventory-only disposition required |
| agent `host`, `ssh` | host != fleetHost is demonstrably remote and inventory-only; host == fleetHost stays local; SSH targets with or without an explicit user must agree with `host`; ssh-only, missing fleet-host evidence, or contradictory targets block |
| agent `socket` | Same-host candidate only when it matches the canonical fleet socket; conflicts block for explicit future disposition |
| root `connector` | Inventory-only; never contacted or reconciled |
| unknown fields or snake/camel synonym collisions | Inventoried and block readiness |
| `.env.generated` | Rebuild from canonical roster data |
| no legacy `.env` | `absent`; no legacy action required |
| legacy `.env` containing generated keys only | `regenerate-only`; replace later from canonical roster data |
| legacy `.env` containing strict local keys | `relocate-local`; preserve those keys in `.env.local` during a later reviewed cutover |
| legacy `.env` containing forbidden/unsafe/sensitive/malformed keys | `quarantine`; private input only, with diagnostics limited to code, key, and SHA-256 |
The only automatic aliases are `implementer → code`, `reviewer → review`, and
`operator-interaction → interaction`. Similar or domain-specific names are never inferred. Automatic
The only automatic aliases are implementer → code, reviewer → review, and
operator-interaction → interaction. Similar or domain-specific names are never inferred. Automatic
classes do not accept competing disposition records. Semantic validation delegates to the existing
baseline-plus-`roles.local` resolver after the candidate is compiled by the existing v2 compiler.
+45 -46
View File
@@ -44,9 +44,9 @@ The Fleet inherits — does not re-invent — the MVP's hard requirements:
| MVP req | What it means for the Fleet |
| ----------------------------- | ----------------------------------------------------------------------------------------------------------------------- |
| MVP-X1 three-surface parity | fleet observability/control reachable via **CLI + TUI + webUI** (CLI first; webUI is required for parity, not optional) |
| MVP-X2 multi-tenant isolation | one tenant = one **Linux uid** (own `systemd --user`, socket, `~/.config/mosaic`); no cross-tenant leakage |
| MVP-X2 multi-tenant isolation | one tenant = one **Linux uid** (own systemd --user, socket, ~/.config/mosaic); no cross-tenant leakage |
| MVP-X3 auth (BetterAuth/SSO) | operator→fleet and cross-host views are auth-gated through the platform's existing auth |
| MVP-X4 quality gates | `pnpm typecheck`/`lint`/`format:check` green before any push |
| MVP-X4 quality gates | pnpm typecheck/`lint`/`format:check` green before any push |
| MVP-X5 federated topology | cross-host fleet visibility rides the **federation** boundary (W1), not a bespoke broker |
| MVP-X6 OTEL tracing | heartbeats, sends, and lifecycle events emit spans; `traceparent` crosses the federation boundary |
| MVP-X7 trunk merge | branch from `main`, squash-merge via PR, never push to `main` |
@@ -55,20 +55,20 @@ The Fleet inherits — does not re-invent — the MVP's hard requirements:
One **definition** is the source of truth; the **session** is how it runs.
| Layer | Owner | Phase-2 reality | Destination |
| -------------------------------- | ------------------------------------------------------------------------------------------- | ------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------- |
| **Definition + identity + auth** | gateway / `mosaic-as` (scoped tokens, #541) | `roster.yaml` (tenant-tagged) | one definition; `mosaic agent --new` materializes it |
| **Tenancy boundary** | **Linux uid per tenant** (linger, own `systemd --user`, own socket, own `~/.config/mosaic`) | one tenant: `jarvis` = tenant zero | uid-per-tenant; federation aggregates across hosts |
| **Runtime** | per-tenant tmux session on isolated socket | dogfood stub sessions (live now on `mosaic-factory`) | claude/codex/pi/opencode TUIs |
| **Liveness** | **heartbeat protocol** every runtime answers | protocol defined + dogfood stub answers it | all runtimes answer; "healthy" ≠ "pane alive" |
| **Observation** | read-only `watch` (native tmux) + `pipe-pane` stream | CLI `watch`/`ps`; explicit opt-in `attach` for control | + auth-gated webUI streams |
| **Control plane** | **federation** across hosts × tenants | records already carry `tenant_id` + `host` | federated gateways expose fleet state; webUI in Phase 5 |
| **Central register** | Postgres `fleet` schema (gateway instance); access via gateway API only | _none in PoC_ (files + `roster.yaml`) | agents, missions, tasks, heartbeats, spend — single network-accessible SSOT; docs = generated projections |
| **Budget / spend governance** | **per-tenant budget policy** ingested by the orchestrator + routing layer | none today (spend is unmetered) | usage-vs-limit feedback ingested; spend auto-paced to the limit window; per-provider/per-account/concurrency/API-$ budgets enforced |
| Layer | Owner | Phase-2 reality | Destination |
| -------------------------------- | --------------------------------------------------------------------------------------- | ------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------- |
| **Definition + identity + auth** | gateway / `mosaic-as` (scoped tokens, #541) | `roster.yaml` (tenant-tagged) | one definition; mosaic agent --new materializes it |
| **Tenancy boundary** | **Linux uid per tenant** (linger, own systemd --user, own socket, own ~/.config/mosaic) | one tenant: `jarvis` = tenant zero | uid-per-tenant; federation aggregates across hosts |
| **Runtime** | per-tenant tmux session on isolated socket | dogfood stub sessions (live now on `mosaic-factory`) | claude/codex/pi/opencode TUIs |
| **Liveness** | **heartbeat protocol** every runtime answers | protocol defined + dogfood stub answers it | all runtimes answer; "healthy" ≠ "pane alive" |
| **Observation** | read-only `watch` (native tmux) + `pipe-pane` stream | CLI `watch`/`ps`; explicit opt-in `attach` for control | + auth-gated webUI streams |
| **Control plane** | **federation** across hosts × tenants | records already carry `tenant_id` + `host` | federated gateways expose fleet state; webUI in Phase 5 |
| **Central register** | Postgres `fleet` schema (gateway instance); access via gateway API only | _none in PoC_ (files + `roster.yaml`) | agents, missions, tasks, heartbeats, spend — single network-accessible SSOT; docs = generated projections |
| **Budget / spend governance** | **per-tenant budget policy** ingested by the orchestrator + routing layer | none today (spend is unmetered) | usage-vs-limit feedback ingested; spend auto-paced to the limit window; per-provider/per-account/concurrency/API-$ budgets enforced |
> **PoC socket hygiene:** the PoC fleet runs on the **default tmux socket** (no `-L`).
> The named production-isolation socket is **`mosaic-fleet`** (matches the product brand);
> an absent roster `socket_name` means the default socket everywhere (spawn, `fleet ps`,
> an absent roster `socket_name` means the default socket everywhere (spawn, fleet ps,
> onboarding cheat-sheet). The legacy dogfood canary still runs on the old `mosaic-factory`
> socket pending migration.
@@ -177,22 +177,22 @@ routing flow**, **concurrency** (the spend multiplier), and **hard API-token $-l
are enforced at the orchestrator + routing boundary, not inside individual workers (a worker never
decides its own budget — see delegation discipline).
**Budget CLI UX (#558):** `mosaic budget set --reset-at` sets the window reset; reset-datetimes
**Budget CLI UX (#558):** mosaic budget set --reset-at sets the window reset; reset-datetimes
carry **confidence tags** (`user` / `provider` / `estimated` / `unknown`); and **urgency/criticality
is a dispatch-gate modifier** — high-urgency work may override even-spread pacing **within
authorization**. (Also feeds the budgeting workstream, not only this doc.)
## Observation model
| Verb | Behavior |
| ----------------------------------- | -------------------------------------------------------------------------------------------------- |
| `mosaic fleet ps` | one table joining systemd + tmux + process + idle + last-heartbeat, with drift + boot-enable flags |
| `mosaic agent watch <name>` | **read-only** join (grouped session / `-r`), no resize tyranny, no keystrokes |
| `mosaic agent attach <name>` | explicit interactive takeover (the only path that can type) |
| `mosaic agent send <name> --verify` | confirms message **accepted**, not merely keystroke-injected |
| Verb | Behavior |
| --------------------------------- | -------------------------------------------------------------------------------------------------- |
| mosaic fleet ps | one table joining systemd + tmux + process + idle + last-heartbeat, with drift + boot-enable flags |
| mosaic agent watch <name> | **read-only** join (grouped session / `-r`), no resize tyranny, no keystrokes |
| mosaic agent attach <name> | explicit interactive takeover (the only path that can type) |
| mosaic agent send <name> --verify | confirms message **accepted**, not merely keystroke-injected |
> Why the current PoC blocks observation: sessions live on the isolated `mosaic-factory`
> socket (invisible to default `tmux ls`), the only sanctioned read is `capture-pane`
> socket (invisible to default tmux ls), the only sanctioned read is `capture-pane`
> (blank for full-screen TUIs), and `attach` is read-write + resizes the session. The
> verbs above restore "join and observe" safely.
@@ -214,7 +214,7 @@ compromised pane cannot corrupt or exfiltrate the register.
| Layer | Responsibility | Implementation |
| ---------------------- | ------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Register** | Source of truth: agents, missions, tasks, heartbeats, spend | Postgres `fleet` schema — existing stack instance (`@mosaicstack/db`) |
| **Access** | Typed, auth-gated API | Gateway `fleet/*` routes |
| **Access** | Typed, auth-gated API | Gateway fleet/\* routes |
| **Dispatcher** | Brief classification, BOD review, planning/coding/review/test/deploy sequencing + gates → fleet task dispatch | **forge pipeline engine** (`runPipeline`/`resumePipeline`, brief classifier, BOD) **+ thin `forge-exec` adapter → `agent-send.sh`**; NOT a new daemon — forge is reused, only stage→agent dispatch is new |
| **Orchestrator (Mos)** | Goals, missions, judgment, user/PA interface | Context-light; sets intent → re-engages only for decisions |
@@ -236,7 +236,7 @@ role implementation.
`docs/TASKS.md` and `MISSION-MANIFEST.md` are **generated projections** of the DB,
not hand-maintained. The dispatcher (or a scheduled job) renders Markdown from
`fleet.*` tables and commits the output. DB is authoritative; docs are for human
fleet.\* tables and commits the output. DB is authoritative; docs are for human
reference.
### Spend
@@ -266,13 +266,13 @@ re-evaluate if isolation or write-volume demands it.
## Phased roadmap
| Phase | Outcome | Status |
| ---------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------- |
| 01 | tmux PoC, hardening, published CLI v0.0.34 (#565#568) | ✅ done |
| **2 — Observability** | `fleet ps` (host+tenant aware join), heartbeat protocol + dogfood stub answers it, `agent watch` (read-only), `agent send --verify` receipts | ▶ now |
| 3 — Real runtimes | claude/codex/pi/opencode answer heartbeat; **hybrid lifecycle** (core always-on: **orchestrator + enhancer**; ephemeral workers per lane) | planned |
| 4 — Unified definition | one agent schema in gateway; `mosaic agent --new` → materialized per-tenant session; uid-tenant provisioning; **`fleet` schema migration + `forge-exec` TaskExecutor adapter (forge → `agent-send.sh`)** | planned |
| 5 — Control plane | federation-backed cross-host × cross-tenant fleet view; **webUI** (surface chosen then) for MVP-X1 parity; **central register live (spend ledger, docs-as-projections, multi-host Kanban)** | planned |
| Phase | Outcome | Status |
| ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------- |
| 01 | tmux PoC, hardening, published CLI v0.0.34 (#565#568) | ✅ done |
| **2 — Observability** | fleet ps (host+tenant aware join), heartbeat protocol + dogfood stub answers it, agent watch (read-only), agent send --verify receipts | ▶ now |
| 3 — Real runtimes | claude/codex/pi/opencode answer heartbeat; **hybrid lifecycle** (core always-on: **orchestrator + enhancer**; ephemeral workers per lane) | planned |
| 4 — Unified definition | one agent schema in gateway; mosaic agent --new → materialized per-tenant session; uid-tenant provisioning; **`fleet` schema migration + `forge-exec` TaskExecutor adapter (forge → `agent-send.sh`)** | planned |
| 5 — Control plane | federation-backed cross-host × cross-tenant fleet view; **webUI** (surface chosen then) for MVP-X1 parity; **central register live (spend ledger, docs-as-projections, multi-host Kanban)** | planned |
## Decisions of record (2026-06-20, with Jason)
@@ -285,9 +285,9 @@ re-evaluate if isolation or write-volume demands it.
- Delivery: **CLI-first now**, dogfood against the live stub fleet; webUI deferred to Phase 5.
- Runtimes: fleet agents default to **Codex / pi-on-Codex**; **Claude is reserved for Claude
Code only** (avoid alternate-harness API pricing). Validated durable recipe:
`mosaic yolo pi --model openai-codex/gpt-5.5:high`. Durable detached launch requires the
mosaic yolo pi --model openai-codex/gpt-5.5:high. Durable detached launch requires the
runtime-bin on PATH (baked into the pane command) + boot-survival (`enable` + linger),
which `fleet init` should automate.
which fleet init should automate.
## Decisions of record (2026-06-22, with Jason)
@@ -304,19 +304,18 @@ re-evaluate if isolation or write-volume demands it.
- **Session context cap = 200k tokens (GLOBAL to all Claude sessions):** Claude Code sessions are
capped at a **max 200k-token context window**. Long-running sessions extended toward 1M tokens
have proven **worse in practice** (degraded steering, off-plan divergence); 200k is the standard.
**Enforcement split:** the _window_ lives in **`~/.claude/settings.json`** (host-global) as
`"autoCompactWindow": 200000` + `"autoCompactEnabled": true`; the _1M-disable_ lives in **launch
**Enforcement split:** the _window_ lives in **~/.claude/settings.json** (host-global) as
"autoCompactWindow": 200000 + "autoCompactEnabled": true; the _1M-disable_ lives in **launch
ENV** (`CLAUDE_CODE_DISABLE_1M_CONTEXT=1`, plus `CLAUDE_CODE_AUTO_COMPACT_WINDOW=200000`) wherever
a `[1m]` model can be selected (`mos-claude.service` + the fleet Claude launcher), so every Claude
a [1m] model can be selected (`mos-claude.service` + the fleet Claude launcher), so every Claude
agent is capped at spawn. (settings = window; env = 1M-disable.)
- **Worker context bound (#8):** workers are kept context-bounded via the **ephemeral-per-lane
lifecycle + native compaction**, not via the 200k knob. The explicit `autoCompactWindow` 200k knob
**stays Claude-specific** — the _principle_ (bounded context) extends to workers, the _knob_ does not.
- **Orchestrator delegation discipline:** the orchestrator **delegates all delivery work** to
subagents / workflows / ultracode / coder agents and confines its own context to \*\*orchestration
- the personal-assistant lane\*\*. Keeping delivery out of the orchestrator's window keeps its
context unpolluted and measurably reduces off-plan divergence. The orchestrator coordinates and
decides; it does not implement.
subagents / workflows / ultracode / coder agents and confines its own context to the personal-assistant
lane. Keeping delivery out of the orchestrator's window keeps its context unpolluted and measurably
reduces off-plan divergence. The orchestrator coordinates and decides; it does not implement.
- **Budget governance is fleet doctrine:** token/API-dollar budgeting is a first-class fleet concern
(see "Budget & token governance"). OAuth-sub usage-vs-limit feedback is ingested per account, spend
is **auto-paced EVEN-SPREAD over remaining time** (rapid/overspend only on explicit authorization),
@@ -344,7 +343,7 @@ re-evaluate if isolation or write-volume demands it.
### Control plane & central register
- **Store:** Postgres (existing stack instance, dedicated `fleet` schema via `@mosaicstack/db`). SQLite rejected: (1) it is a local file — structurally incompatible with a multi-host fleet; (2) concurrent multi-agent writes caused repeated corruption in Hermes. "SQLite + access service" rejected as reinventing a DB server badly; "LLM agent gating DB access" rejected as slow, expensive, and a single point of failure.
- **Access:** gateway API only (`apps/gateway`, `fleet/*` routes). No raw DB credentials in any agent/dispatcher pane — directly mitigates the tmux attack-surface concern.
- **Access:** gateway API only (`apps/gateway`, fleet/\* routes). No raw DB credentials in any agent/dispatcher pane — directly mitigates the tmux attack-surface concern.
- **Dispatcher = forge (reuse, not a new build):** the dispatcher IS `@mosaicstack/forge`'s pipeline engine (`runPipeline`/`resumePipeline` + brief classifier + BOD persona loader), a fully-implemented software-factory pipeline (brief → BOD review → 3 planning stages → coding → review/remediation → testing → deploy). We do **not** design/build a new dispatcher and do **not** re-implement sequencing, gate logic, or brief classification. The only new fleet-owned piece is a thin **`forge-exec` TaskExecutor adapter** (suggested package `packages/forge-exec`) mapping a `ForgeTask``agent-send.sh` dispatch to a named fleet agent — forge's single missing piece. It is tracked as a Gitea issue and built **post-PoC** (not now).
- **Register backs forge:** the Postgres `fleet` register is genuinely new (neither forge nor the fleet has cross-project state). It BACKS forge's pipeline state (durable `resumePipeline`, cross-host) plus cross-project missions/tasks/Kanban.
- **'board' role = forge BOD:** the north-star role-library 'board' role IS forge's Board-of-Directors — reused, not reinvented.
@@ -357,9 +356,9 @@ re-evaluate if isolation or write-volume demands it.
- **Per-agent model switch (operator-configurable, NOT a global lock):** model selection is
**per-agent**, never a host-global pin. Claude sessions MUST NOT be locked to a single model in
`~/.claude/settings.json`; each agent chooses its model independently. The plumbing already exists —
roster `model_hint``MOSAIC_AGENT_MODEL``start-agent-session.sh` appends `--model <hint>` to that
agent's harness (claude or pi); settable today via `mosaic fleet add|edit <agent> --model <hint>`.
~/.claude/settings.json; each agent chooses its model independently. The plumbing already exists —
roster `model_hint``MOSAIC_AGENT_MODEL``start-agent-session.sh` appends --model <hint> to that
agent's harness (claude or pi); settable today via mosaic fleet add|edit <agent> --model <hint>.
**North-star target:** surface this as a **per-agent model switch in the webUI** (with CLI/TUI parity
per MVP-X1) — read the roster, expose a per-agent model dropdown, write `model_hint` back, and restart
that one agent to apply. Unset = inherit the harness default. This **composes with** the budget
@@ -385,7 +384,7 @@ re-evaluate if isolation or write-volume demands it.
self-hosted homeserver (Conduit default, Synapse alt). Matrix is named here as the strategic
future transport — peer to tmux/Discord, not superseded by them.
- **tmux fleet attack-surface hardening.** Many always-on tmux sessions are an attack surface;
`tmux send-keys` / socket access could enable malicious action against agents directly.
tmux send-keys / socket access could enable malicious action against agents directly.
Mitigations to build toward: socket ownership/perms, per-tenant socket isolation (already an
invariant), authenticated `agent-send`, and an audit of who can write to any pane. **Post-MVP
unless a P0 surfaces.** The control-plane register reinforces this (gateway-API access = no raw
@@ -418,9 +417,9 @@ re-evaluate if isolation or write-volume demands it.
---
> **Release procedure (drift re-capture, 2026-06-22):** `mosaic update` only propagates new fleet
> **Release procedure (drift re-capture, 2026-06-22):** mosaic update only propagates new fleet
> commands when the **CLI version is bumped** — without a version bump, fleet command changes never
> reach installed hosts. The release/version-bump procedure (bump → publish → `mosaic update`
> reach installed hosts. The release/version-bump procedure (bump → publish → mosaic update
> [→ `--relaunch`]) must be documented so fleet changes actually land. (Also feeds the budgeting
> workstream.)
>
+1 -1
View File
@@ -30,7 +30,7 @@ connector entry.
The preview evidence deliberately records:
- `executable: false`;
- executable: false;
- required backup artifacts;
- source and candidate identities;
- lifecycle observations and resulting desired states;
+20
View File
@@ -0,0 +1,20 @@
# Environment Quarantine Operations
Legacy <name>.env is input evidence, never current launch authority. Projection preparation classifies it deterministically:
- generated roster keys → discard and regenerate;
- allowed strict local keys → relocate to private `.env.local`;
- malformed, duplicate, unknown, sensitive-looking, shell-bearing, unsafe, or command-override entries → move the legacy input to private `.env.quarantine`.
## Safe response
1. Stop and read the stable error code and reported key name/hash. Do not request or paste the value.
2. Confirm the canonical roster contains the intended non-sensitive desired state.
3. If the key is an allowed local machine-data field, place only its validated data form in `.env.local` under private permissions.
4. Remove unsupported intent rather than translating it into commands, channels, secret references, or unknown MOSAIC*AGENT*\* keys.
5. Regenerate `.env.generated` from the roster and rerun a dry-run/verification gate.
6. Retain quarantine evidence privately until the operator's normal retention process permits removal.
The launcher never reads quarantine. Public/JSON diagnostics expose stable code, key name where safe, and SHA-256 only—never a legacy sensitive value, credential, rejected command, or full line. Quarantine does not prove remediation, backup, migration, or rollback.
See [generated launch chain](../concepts/generated-env-launch-chain.md), [generated environment boundary](../reference/generated-env-boundary.md), and [migration field disposition](../migration/v1-to-v2.md#field-disposition).
@@ -2,10 +2,12 @@
## Safe sequence
1. Read `mosaic fleet doctor` and `mosaic fleet status`.
2. Run `mosaic fleet apply --expected-generation <n> --dry-run`.
1. Read mosaic fleet doctor and mosaic fleet status.
2. Run mosaic fleet apply --expected-generation <n> --dry-run.
3. Resolve stale generation, ownership mismatch, unsafe path, projection validation, or unmanaged-session findings before applying.
4. Run `mosaic fleet apply --expected-generation <n>` only after the plan is understood.
4. Run mosaic fleet apply --expected-generation <n> only after the plan is understood.
This is per-generation convergence, not a rolling canary. Executable canary cutover/rollback remains held for FCM-M4-002; rolling local release evidence remains FCM-M5-002. Do not approximate either with repeated live apply commands.
The reconciler uses the exact roster tmux socket, exact holder session, private installation holder identity, and the complete expected global environment. For mutations it acquires its exclusive lock before rereading the canonical roster and fencing its generation; only that under-lock roster drives validation, planning, projections, and lifecycle effects. Before effects, its exclusive lock proves real private `MOSAIC_HOME` and `fleet` ancestors, uses a private `0600` lock leaf, and binds cleanup to the created file identity and ownership token. A fake holder, contaminated global environment, missing identity, unsafe lock path, or unmanaged session fails closed. It does not adopt, kill, or rename any unproven session. A crash can leave a stale lock for explicit operator inspection; reconciliation deliberately does not guess ownership or remove it.
@@ -23,4 +25,4 @@ The roster is never changed by reconciliation. If derived projection application
}
```
If projections completed but lifecycle work failed, JSON reports `projections: "complete"`, `lifecycle: "incomplete"`, and the bounded action `rerun-after-inspecting-owned-resources`. If lock cleanup cannot be proven after an effect result, it adds `cleanup: { "code": "lock-cleanup-failed", "action": "inspect-lock-before-retry" }` without changing the known projection, lifecycle, or primary recovery truth. Inspect the retained lock before retrying; no rollback, release, or stale-lock removal is implied. Results do not include environment values, secrets, or privileged command content.
If projections completed but lifecycle work failed, JSON reports projections: "complete", lifecycle: "incomplete", and the bounded action `rerun-after-inspecting-owned-resources`. If lock cleanup cannot be proven after an effect result, it adds cleanup: { "code": "lock-cleanup-failed", "action": "inspect-lock-before-retry" } without changing the known projection, lifecycle, or primary recovery truth. Inspect the retained lock before retrying; no rollback, release, or stale-lock removal is implied. Results do not include environment values, secrets, or privileged command content.
@@ -0,0 +1,24 @@
# Systemd and tmux Troubleshooting
Start with read-only mosaic fleet status, `doctor`, and `verify`. Do not manually adopt, rename, terminate, or recreate sessions while ownership is ambiguous.
## Decision table
| Finding | Meaning | Safe next step |
| ------------------------------- | ---------------------------------------------------- | ------------------------------------------------------------------------------------- |
| Empty roster `tmux.socket_name` | Literal default tmux server | Do not substitute the named `mosaic-fleet` socket. Use roster-derived commands only. |
| Non-empty socket | Exact named socket | Never target another socket or infer a per-agent socket. |
| holder: missing | Required exact holder absent | Inspect installation/projection readiness; do not create an unproven holder manually. |
| `ownership-mismatch` | Holder identity or global environment differs | Stop. Verify private install identity and managed paths before retry. |
| `missing-session` | Desired-running roster agent lacks exact session | Check service/runtime preconditions; review apply dry-run. |
| `unexpected-session` | Desired-stopped roster agent still has exact session | Confirm ownership; only reconciler may target the exact proven roster member. |
| `disabled-running` | Disabled roster member is observed running | Inspect and reconcile only after ownership proof. |
| `unmanagedSessions` | Unknown session exists on configured named socket | Report and investigate separately. Reconciler will not kill or adopt it. |
| stale/concurrent generation | Desired state changed since plan | Reload roster/generation and recompute the plan. |
| stale or ambiguous lock | Prior writer/cleanup cannot be proven | Inspect ownership; do not blindly remove the lock. |
| projection failure | Derived files incomplete | Keep roster as authority and regenerate projections. |
| lifecycle failure | Projections complete, runtime convergence incomplete | Inspect the exact owned resource, then rerun with current generation. |
Systemd state, tmux state, heartbeat, and generated files are observations/projections, not alternate desired state. Explicit apply/reconcile honors stopped/disabled intent, but current unit enablement and launcher projections do not yet prove lifecycle-safe reboot; inspect unit enablement before reboot and treat stopped/disabled boot preservation as an FCM-M3-002 hold. Current roster-v2 status commands also do not read heartbeat files. Executable gates do not provide site cutover/rollback or package asset-revision repair.
Errors and troubleshooting output never print legacy sensitive values, credential contents, or privileged command text. Use stable codes, key names/hashes, exact roster identities, and bounded recovery actions. See [status and drift](../reference/status-and-drift.md) and [reconcile and recover](reconcile-and-recover.md).
+18
View File
@@ -0,0 +1,18 @@
# Upgrade and Installed-Asset Drift
Fleet source assets and installed assets can differ after an update, but FCM-M5-001 does not add a trustworthy source-versus-installed revision detector or refresh command. Do not infer freshness from checkout presence, timestamps, generated environment files, running sessions, or a ready migration preview.
## Current safe boundary
- The canonical roster remains authority and must survive package/framework refresh.
- Generated projections are rebuilt from that roster after the installed contract is independently verified.
- Operator `roles.local`, `.env.local`, and private quarantine evidence are not generated assets and must not be overwritten.
- Baseline roles, schemas, examples, service presets, launcher helpers, and systemd templates must move as one reviewed release set.
- Remote/connector inventory and `mos-comms` are not promoted into permanent architecture by an update.
- No update may start an agent persisted stopped, adopt an unmanaged session, or bypass generation/ownership checks.
## Explicit hold
FCM-M5-002 owns deterministic asset-drift checks, safe package/update refresh evidence, rolling local canary, independent validation certificate, and release evidence. Until that card lands, this page is an operational hold rather than an executable procedure: use the repository/release review path, preserve backups, and do not claim source/installed parity without exact revision evidence from the future validator.
See [approved deferrals](../../reports/deferred/758-fleet-config-deferrals.md) and [backup/restore boundary](backup-restore.md).
+6 -6
View File
@@ -4,17 +4,17 @@ FCM-M2-002 provides local roster-v2 create, get, update, delete, and plan operat
## CLI contract
The commands operate only on the canonical `<mosaic-home>/fleet/roster.yaml` v2 authority and print one JSON object to stdout. `--agent` is a JSON object with the roster agent fields expressed as `className`, `toolPolicy`, `workingDirectory`, `persistentPersona`, `resetBetweenTasks`, and `launch: { "yolo": boolean }`.
The commands operate only on the canonical <mosaic-home>/fleet/roster.yaml v2 authority and print one JSON object to stdout. `--agent` is a JSON object with the roster agent fields expressed as `className`, `toolPolicy`, `workingDirectory`, `persistentPersona`, `resetBetweenTasks`, and launch: { "yolo": boolean }.
```sh
```fleet-synopsis
mosaic fleet get <name>
mosaic fleet plan <create|update|delete> [name] --expected-generation <n> [--agent '<json>'] [--persisted-start]
mosaic fleet plan <create|update|delete> [<name>] --expected-generation <n> [--agent '<json>'] [--persisted-start]
mosaic fleet create --expected-generation <n> --agent '<json>' [--dry-run] [--persisted-start]
mosaic fleet update <name> --expected-generation <n> --agent '<json>' [--dry-run]
mosaic fleet delete <name> --expected-generation <n> [--dry-run]
```
`get` returns the authoritative generation and the selected agent. `plan create` derives its name from `--agent`; `plan update <name>` and `plan delete <name>` require the target name. `--agent` accepts only the documented roster-v2 request fields and `launch.yolo`; unknown keys such as commands, channels, or secret references are rejected. Rejection diagnostics return only the stable `invalid-request` code and never echo a rejected value. `plan` and `--dry-run` validate the complete proposed roster and projections but write neither the roster nor projections. `--persisted-start` is available only for a create request: it records `desired_state: running`, but does not start a process. Without it, create records `enabled: true` and `desired_state: stopped`. Handled failures return JSON with `error.code` and exit non-zero; unclassified validation/projection failures use the redacted `mutation-failed` code.
`get` returns the authoritative generation and the selected agent. plan create derives its name from `--agent`; plan update <name> and plan delete <name> require the target name. `--agent` accepts only the documented roster-v2 request fields and `launch.yolo`; unknown keys such as commands, channels, or secret references are rejected. Rejection diagnostics return only the stable `invalid-request` code and never echo a rejected value. `plan` and `--dry-run` validate the complete proposed roster and projections but write neither the roster nor projections. `--persisted-start` is available only for a create request: it records desired_state: running, but does not start a process. Without it, create records enabled: true and desired_state: stopped. Handled failures return JSON with `error.code` and exit non-zero; unclassified validation/projection failures use the redacted `mutation-failed` code.
## Generation, validation, and idempotency
@@ -22,7 +22,7 @@ Each create, update, or delete request includes `expectedGeneration`. A request
`planFleetAgentMutation` is deterministic and side-effect free. `executeFleetAgentMutation` validates the complete proposed roster through the existing structural and shared persona resolver, prepares generated/local/quarantine projections, and writes the roster authority atomically before applying derived projections. Equivalent create retries and delete requests for an already-absent agent are idempotent no-ops.
Delete removes only the exact `<name>.env.generated` projection for the removed roster entry. Operator-owned `<name>.env.local`, legacy `<name>.env`, quarantine records, and unrelated projections remain untouched. An already-absent generated projection is treated as stale derived state, not as a failed mutation.
Delete removes only the exact <name>.env.generated projection for the removed roster entry. Operator-owned <name>.env.local, legacy <name>.env, quarantine records, and unrelated projections remain untouched. An already-absent generated projection is treated as stale derived state, not as a failed mutation.
## Result and recovery
@@ -40,4 +40,4 @@ Mutation results are JSON-safe objects with `applied`, `authoritativeRoster`, `p
}
```
Dry-runs and idempotent no-ops report `authoritativeRoster: "unchanged"` and `projections: "not-applied"`; a complete mutation reports `"committed"` and `"complete"`. Recovery output identifies the authoritative roster path and regeneration action only. It never contains generated/local/quarantine values, credentials, or command text. A recovery result exits non-zero because the authoritative roster was persisted but derived projections require regeneration. Regenerate projections from the roster before attempting another mutation.
Dry-runs and idempotent no-ops report authoritativeRoster: "unchanged" and projections: "not-applied"; a complete mutation reports "committed" and "complete". Recovery output identifies the authoritative roster path and regeneration action only. It never contains generated/local/quarantine values, credentials, or command text. A recovery result exits non-zero because the authoritative roster was persisted but derived projections require regeneration. Regenerate projections from the roster before attempting another mutation.
+35 -16
View File
@@ -1,27 +1,46 @@
# Fleet Control-Plane CLI
The local roster-v2 control plane is `mosaic fleet`.
The local desired-state surface is mosaic fleet. It is distinct from the gateway-backed mosaic agent catalog and from legacy compatibility commands that act on roster v1.
```text
mosaic fleet apply --expected-generation <n> [--dry-run]
mosaic fleet reconcile --expected-generation <n> [--dry-run]
mosaic fleet start [name] --expected-generation <n> [--dry-run]
mosaic fleet stop [name] --expected-generation <n> [--dry-run]
mosaic fleet restart [name] --expected-generation <n> [--dry-run]
mosaic fleet status [name]
## Roster-v2 desired-state commands
| Command | Effect | Generation | Output |
| ---------------------------------------------------------- | ---------------------------------------------- | ---------- | -------------------------- |
| mosaic fleet get <name> | Read one authoritative agent | no | One JSON object |
| mosaic fleet plan <create\|update\|delete> ... | Validate proposed CRUD and projections | required | One JSON object; no writes |
| mosaic fleet create ... [--dry-run] [--persisted-start] | Add desired state; default enabled/stopped | required | One JSON object |
| mosaic fleet update <name> ... [--dry-run] | Replace mutable agent fields | required | One JSON object |
| mosaic fleet delete <name> ... [--dry-run] | Remove roster member/generated projection | required | One JSON object |
| mosaic fleet apply ... [--dry-run] | Plan or converge projections/lifecycle | required | One JSON object |
| mosaic fleet reconcile ... [--dry-run] | Alias of the same convergence contract | required | One JSON object |
| mosaic fleet start\|stop\|restart [<name>] ... [--dry-run] | Exact one-shot lifecycle action | required | One JSON object |
| mosaic fleet status [<name>] | Observe desired/managed/runtime state | no | One JSON object |
| mosaic fleet verify | Strict observational drift/ownership gate | no | One JSON object |
| mosaic fleet doctor | Classify local drift and recovery context | no | One JSON object |
| mosaic fleet migrate-v1 preview ... | Non-mutating field-complete migration evidence | no | One JSON object |
CRUD syntax and full payload shape are documented in [agent mutations](agent-mutations.md). Reconciliation syntax:
```fleet-synopsis
mosaic fleet apply --expected-generation <n>
mosaic fleet reconcile --expected-generation <n>
mosaic fleet start [<name>] --expected-generation <n> [--dry-run]
mosaic fleet stop [<name>] --expected-generation <n> [--dry-run]
mosaic fleet restart [<name>] --expected-generation <n> [--dry-run]
mosaic fleet status [<name>]
mosaic fleet verify
mosaic fleet doctor
mosaic fleet migrate-v1 preview --source <path> --decisions <path> --observations <path>
```
`migrate-v1 preview` is non-mutating: it emits value-free v1 inventory, a canonical semantically
validated v2 candidate when ready, sanitized environment dispositions, and non-executable recovery
evidence. It has no write, apply, canary, or rollback option. Missing preview inputs also return one stable
blocked JSON object and a non-zero exit, rather than Commander text. See
[the migration preview contract](../migration/v1-to-v2.md).
`get` is the read/show operation for one v2 agent. Full roster parsing and semantic validation occur on every v2 mutation/reconcile path; there is no separate mutable “config store.” The executable JSON Schema and validated example provide offline structural evidence. The PRD requires an explicit programmatic mosaic fleet validate operation, but the current CLI does not expose one; do not substitute another command or claim that requirement is delivered. This remains an implementation gap for #758.
`apply` and `reconcile` use roster desired state. `start`, `stop`, and `restart` are exact local one-shot lifecycle effects and never persist a desired-state edit. `status`, `verify`, and `doctor` are observational.
## JSON and exit behavior
Commands emit one JSON object. Handled precondition errors emit `{ "error": { "code": "..." } }` and exit non-zero. Partial derived/lifecycle effects use explicit `authoritativeRoster`, `projections`, `lifecycle`, and bounded `recovery` fields; they never claim rollback. Any additive `cleanup` diagnostic also exits non-zero, even where known effects are complete: it is not a clean completion and the lock requires inspection before retry.
Roster-v2 CRUD and reconciler precondition failures emit { "error": { "code": "..." } } and exit non-zero. Migration preview has its own result envelope: a non-ready preview emits { "status": "blocked", "blockers": [...] } and exits non-zero rather than using the CRUD/reconciler error object. Use both exit status and command-specific state fields. A partial reconciliation result distinguishes `authoritativeRoster`, `projections`, `lifecycle`, `recovery`, and optional `cleanup`; it never claims automatic rollback. `verify` exits non-zero for drift, ownership failure, or unmanaged sessions. Sensitive legacy values, credentials, and rejected command text are never printed.
This control plane is separate from the gateway-backed `mosaic agent` catalog. It is local-only and rejects remote/connector lifecycle mutation, arbitrary command/channel/secret input, and unproven tmux ownership.
## Compatibility and scope
Roster-v1 initialization, provisioning, profiles/personas, and historical fleet add/remove remain compatibility surfaces, not roster-v2 CRUD aliases. New v2 automation should use the table above. migrate-v1 preview writes nothing and has no cutover, canary, or rollback option.
mosaic agent is a separate catalog/transport surface; it does not own <MOSAIC_HOME>/fleet/roster.yaml desired state. Remote/SSH reconciliation, connector mutation, arbitrary commands/channels, secret references, and gateway convergence are rejected or outside scope.
+17 -17
View File
@@ -1,6 +1,6 @@
# Fleet Generated Environment Boundary
**Card:** FCM-M2-001 · **Issue:** #758 · **Status:** unreleased/card-local
**Card:** FCM-M2-001 · **Issue:** #758 · **Status:** merged contract
The local fleet roster is the desired-state authority. A launch reads a deterministic,
roster-derived generated projection and an optional strictly data-only local file; neither file is
@@ -8,14 +8,14 @@ a second roster or a command configuration surface.
## Paths and ownership
For agent `<name>` under `<MOSAIC_HOME>/fleet/agents/`:
For agent <name> under <MOSAIC_HOME>/fleet/agents/:
| Path | Owner | Purpose |
| ----------------------- | ------------------------ | ---------------------------------------------------------------------------------------------------------------------------- |
| `<name>.env.generated` | Mosaic projection writer | Complete deterministic launch data rendered from the authoritative roster. |
| `<name>.env.local` | Operator | Optional, constrained local machine data. It cannot shadow generated keys. |
| `<name>.env` | Legacy input only | Read once during projection generation, then regenerated/relocated or privately quarantined. It is never a launch authority. |
| `<name>.env.quarantine` | Mosaic quarantine | Mode-`0600` private record of forbidden legacy input; it is never read by the launcher. |
| Path | Owner | Purpose |
| --------------------- | ------------------------ | ---------------------------------------------------------------------------------------------------------------------------- |
| <name>.env.generated | Mosaic projection writer | Complete deterministic launch data rendered from the authoritative roster. |
| <name>.env.local | Operator | Optional, constrained local machine data. It cannot shadow generated keys. |
| <name>.env | Legacy input only | Read once during projection generation, then regenerated/relocated or privately quarantined. It is never a launch authority. |
| <name>.env.quarantine | Mosaic quarantine | Mode-`0600` private record of forbidden legacy input; it is never read by the launcher. |
The systemd templates do not load either environment file. They invoke Bash with a fixed, cleared
bootstrap environment; the launcher reads and validates `.env.generated` and `.env.local` itself before
@@ -44,7 +44,7 @@ MOSAIC_AGENT_WORKDIR=<absolute roster work directory>
MOSAIC_TMUX_SOCKET=<roster socket or empty>
```
The generated launch contract supports only `claude`, `codex`, `opencode`, and `pi`. `fleet add`
The generated launch contract supports only `claude`, `codex`, `opencode`, and `pi`. fleet add
uses that same runtime authority and rejects any other runtime before it writes the roster or changes
projection, local, or quarantine files. The legacy dogfood stub on its separate `mosaic-factory`
socket remains an observability canary; it has no generated-launch adapter and cannot be added through
@@ -62,7 +62,7 @@ Local paths must be safe absolute paths and the interval must be a positive inte
quoted/export syntax, duplicate keys, unknown keys, generated-key shadowing, sensitive key names,
and `MOSAIC_AGENT_COMMAND` are rejected. The launcher derives the only executable command from the
validated runtime, model, and reasoning data; no arbitrary command compatibility path exists. When a
Pi runtime writes a fresh `<name>.hb.native` marker, its native heartbeat remains authoritative; the
Pi runtime writes a fresh <name>.hb.native marker, its native heartbeat remains authoritative; the
shell sidecar resumes its `status=ok` fallback only after that marker is stale or absent.
## Legacy disposition
@@ -78,14 +78,14 @@ This card does not add a USC site file, write a USC roster, or run a site canary
consolidated downstream interface packet. Status is deliberately separated from checkout presence: no
product release version has been evidenced for this interface set.
| Interface | Canonical public path and version | Tracker/release status | Downstream limit |
| ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------- |
| M1 structural compiler | `parseRosterV2` in `packages/mosaic/src/fleet/roster-v2.ts`; schema `docs/fleet/reference/roster-v2.schema.json`; roster `version: 2` | FCM-M1-001 is recorded done, merged as #764 (`aa5b43b`); no released product version is asserted here. | Parse YAML/JSON and canonicalize a supplied v2 site roster without writes. |
| M1 semantic resolver | `validateRosterV2Semantics` in `packages/mosaic/src/fleet/roster-v2.ts`; baseline `framework/fleet/roles/` plus `roles.local/` | FCM-M1-002 remains `in-progress` in `docs/TASKS.md`; unreleased. | Reuse the shared resolver only; no parallel role resolver or lifecycle action. |
| M1 disposition evidence | `packages/mosaic/src/fleet/example-profile-dispositions.ts`; `docs/fleet/migration/example-profile-disposition.md`; retained fixture `version: 1` | FCM-M1-003 remains `not-started` in `docs/TASKS.md`; unreleased even though these checkout artifacts are inspectable. | Inspect fixture/profile/service disposition evidence only; it is not migration authorization. |
| M2 generated boundary | `packages/mosaic/src/fleet/generated-env-boundary.ts`; generated projection contract in this document | FCM-M2-001 card-local and uncommitted; unreleased. | Render/write a roster-derived projection; local input is never authority. |
| Interface | Canonical public path and version | Tracker/release status | Downstream limit |
| ----------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------- |
| M1 structural compiler | `parseRosterV2` in `packages/mosaic/src/fleet/roster-v2.ts`; schema `docs/fleet/reference/roster-v2.schema.json`; roster version: 2 | FCM-M1-001 is recorded done, merged as #764 (`aa5b43b`); no released product version is asserted here. | Parse YAML/JSON and canonicalize a supplied v2 site roster without writes. |
| M1 semantic resolver | `validateRosterV2Semantics` in `packages/mosaic/src/fleet/roster-v2.ts`; baseline `framework/fleet/roles/` plus `roles.local/` | FCM-M1-002 merged as #768 (`a5e8e55`); no released product version is asserted here. | Reuse the shared resolver only; no parallel role resolver or lifecycle action. |
| M1 disposition evidence | `packages/mosaic/src/fleet/example-profile-dispositions.ts`; `docs/fleet/migration/example-profile-disposition.md`; retained fixture version: 1 | FCM-M1-003 merged as #770 (`e9c4aa3`); checkout evidence remains validation, not migration authorization. | Inspect fixture/profile/service disposition evidence only; it is not migration authorization. |
| M2 generated boundary | `packages/mosaic/src/fleet/generated-env-boundary.ts`; generated projection contract in this document | FCM-M2-001 merged as #772 (`191efae`); no released product version is asserted here. | Render/write a roster-derived projection; local input is never authority. |
The canonical source remains `<MOSAIC_HOME>/fleet/roster.yaml` for the current local fleet path.
The canonical source remains <MOSAIC_HOME>/fleet/roster.yaml for the current local fleet path.
Generated environment data is a rebuildable projection, not an operator-editable source of membership,
runtime policy, or lifecycle state.
+16 -9
View File
@@ -1,14 +1,21 @@
# Local Fleet Lifecycle Transitions
FCM-M3-001 uses the roster-v2 `lifecycle.enabled` and `lifecycle.desired_state` fields as the only desired-state authority. Systemd, tmux, generated environment files, and heartbeats are derived or observed state.
Roster-v2 `lifecycle.enabled` and `lifecycle.desired_state` are the only persisted lifecycle authority. Systemd, tmux, generated environment, and heartbeat state are derived or observed.
| Command | Desired-state write | Runtime effect | Preconditions |
| --------------------------------- | ------------------- | -------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------- |
| `fleet apply` / `fleet reconcile` | Never | Rebuilds projections, then starts only enabled agents desired `running`; stops disabled or desired-`stopped` roster agents | Current generation; private managed paths; valid projections; proven holder ownership; no unmanaged named-socket sessions |
| `fleet start <name>` | Never | One-shot exact `mosaic-agent@<name>.service` start | Current generation; exact enabled roster name; proven ownership |
| `fleet stop <name>` | Never | One-shot exact service stop | Current generation; exact roster name; proven ownership |
| `fleet restart <name>` | Never | One-shot exact service restart | Current generation; exact roster name; proven ownership |
| Event | Desired-state write | Runtime effect | Safety boundary |
| ------------------------- | -------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- |
| fleet create | Adds enabled/stopped by default; `--persisted-start` records running | None | Generation-guarded; validates full roster/projections. |
| fleet update | Preserves the existing enabled/desired state; updates other mutable fields | None | Generation-guarded; stable name and lifecycle are immutable on this path. |
| fleet delete | Removes exact roster member | None | Removes only generated projection; retains local/quarantine evidence. |
| fleet apply / `reconcile` | Never | Rebuilds projections; starts only enabled/running; stops disabled or stopped roster members | Current generation, private lock/paths, semantic validity, holder ownership, no unmanaged named-socket sessions. |
| fleet start <name> | Never | One-shot exact service start | Exact enabled roster name and proven ownership. |
| fleet stop <name> | Never | One-shot exact service stop | Exact roster name and proven ownership. |
| fleet restart <name> | Never | One-shot exact service restart | Exact enabled roster name and proven ownership. |
| Reboot/service activation | Never | Current installation may activate enabled units without honoring roster lifecycle | **Held for FCM-M3-002:** boot preservation for stopped/disabled agents is not yet proven; inspect/disable units rather than assuming lifecycle-safe reboot. |
| v1 migration preview | Never | None | Observed active+present maps running; inactive+missing maps stopped; ambiguity blocks. |
| Cutover/canary | Held for FCM-M4-002 | Not implemented by preview | Must preserve every observed stopped state. |
| Rollback | Held for FCM-M4-002 | Not implemented | Must restore selected authority/projections without surprise starts or unmanaged targeting. |
A stopped roster agent is never started by `apply` or `reconcile`. Direct lifecycle commands are explicit one-shot actions and do not change persisted desired state. Use roster CRUD with the explicit persisted-start option to change that desired state.
Explicit apply/reconcile never starts a stopped roster agent. Direct lifecycle commands are explicit one-shot actions and do not persist intent. The current update operation preserves `existing.lifecycle`; there is no delivered generation-guarded CRUD operation for changing durable lifecycle after creation. Reboot preservation for stopped/disabled agents is not yet guaranteed because current enabled units and launcher projections do not carry the persisted lifecycle fence; that acceptance evidence remains FCM-M3-002.
All mutations require `--expected-generation <n>` and acquire one private roster-adjacent reconciliation lock before projection or lifecycle effects. Missing or stale generations and concurrent writers fail before effects; the lock is released after success, partial failure, or thrown lifecycle failure. Stale, ownership, unmanaged-session, unsupported-runtime, path, projection, and lifecycle-precondition failures return stable redacted JSON errors and a non-zero exit. No command targets a fuzzy tmux name, arbitrary socket, arbitrary command, channel, secret, or generated file as authority.
Missing/stale generation, concurrent writer, unsafe path, ownership mismatch, unmanaged session, unsupported runtime, invalid projection, and lifecycle precondition failures return stable redacted JSON and non-zero status. No command targets fuzzy names, arbitrary sockets/commands/channels/secrets, or generated files as authority. Legacy sensitive values are never printed.
+1 -1
View File
@@ -16,7 +16,7 @@ Only these legacy class aliases are recognized:
No other alias is inferred. In particular, `worker`, `analyst`, and `canary` are custom classes only
when an operator supplies a readable contract for that exact class. Tess and Ultron are instance
names, not classes. `agents[].alias` is display-only and cannot grant authority.
names, not classes. agents[].alias is display-only and cannot grant authority.
Canonicalization happens before role lookup. For example, requesting `implementer` resolves
`code.md`; a separate `roles.local/implementer.md` cannot redefine the legacy alias. A canonical
+32 -30
View File
@@ -50,36 +50,38 @@ agents:
## Root fields
| Field | Required | Constraint | Meaning |
| ------------ | -------- | --------------------- | ------------------------------------------------------------------------------------------------------------------------------------- |
| `version` | yes | integer constant `2` | Identifies this contract. Version `1` is explicitly rejected by this compiler and remains on the existing v1 path until M4 migration. |
| `generation` | yes | positive safe integer | Desired-state generation. M2 uses it for mutation guards; M1 does not mutate it. |
| `transport` | yes | constant `tmux` | M1M5 support local tmux only. |
| `tmux` | yes | strict object | Explicit local socket and holder-session configuration. |
| `defaults` | yes | strict object | Default work directory and one supported local runtime. |
| `runtimes` | yes | non-empty object | Declared local runtime reset policy map. |
| `agents` | yes | non-empty array | Local fleet entries. Duplicate stable names are rejected. |
| Field | Required | Default | Constraint | Meaning |
| ------------ | -------- | ------- | --------------------- | ------------------------------------------------------------------------------------------------- |
| `version` | yes | none | integer constant `2` | Identifies this contract. Version `1` stays on the compatibility path pending explicit migration. |
| `generation` | yes | none | positive safe integer | Desired-state generation and mutation/reconcile concurrency fence. |
| `transport` | yes | none | constant `tmux` | M1M5 support local tmux only. |
| `tmux` | yes | none | strict object | Explicit local socket and holder-session configuration. |
| `defaults` | yes | none | strict object | Default work directory and one supported local runtime. |
| `runtimes` | yes | none | non-empty object | Declared local runtime reset policy map. |
| `agents` | yes | none | non-empty array | Local fleet entries. Duplicate stable names are rejected. |
## Nested fields
| Path | Required | Constraint |
| ---------------------------------------------------- | -------- | ------------------------------------------------------------------------------------------------------------- |
| `tmux.socket_name` | yes | `[A-Za-z0-9_.-]*`; empty string means the literal default tmux server, while a non-empty value names a socket |
| `tmux.holder_session` | yes | non-empty `[A-Za-z0-9_.-]+` |
| `defaults.working_directory` | yes | non-empty string |
| `defaults.runtime` | yes | `claude`, `codex`, `opencode`, or `pi`; it must be declared in `runtimes` |
| `runtimes.<runtime>.reset_command` | yes | non-empty string; runtime key must be a supported local runtime |
| `agents[].name` | yes | unique `[A-Za-z0-9][A-Za-z0-9_.-]*` stable machine identity |
| `agents[].alias` | yes | non-empty display string |
| `agents[].class` | yes | `[a-z][a-z0-9-]*`; structural only in M1, semantic role resolution is FCM-M1-002 |
| `agents[].runtime` | yes | `claude`, `codex`, `opencode`, or `pi`; it must be declared in `runtimes` |
| `agents[].provider`, `model`, `working_directory` | yes | non-empty strings; provider/model capability resolution is a later card |
| `agents[].reasoning` | yes | `low`, `medium`, or `high` |
| `agents[].tool_policy` | yes | `[a-z][a-z0-9-]*`; structural only in M1 |
| `agents[].persistent_persona`, `reset_between_tasks` | yes | booleans |
| `agents[].lifecycle.enabled` | yes | boolean; stored now, reconciled in FCM-M3-001 |
| `agents[].lifecycle.desired_state` | yes | `running` or `stopped` |
| `agents[].launch.yolo` | yes | boolean; structured data only, not an arbitrary command escape hatch |
All nested fields in the v2 schema are required and have no implicit default. CRUD `create` is the only higher-level convenience: it records lifecycle.enabled: true and desired_state: stopped unless `--persisted-start` explicitly records running. That convenience still performs no runtime action.
| Path | Required | Constraint |
| -------------------------------------------------- | -------- | ------------------------------------------------------------------------------------------------------------ |
| `tmux.socket_name` | yes | [A-Za-z0-9_.-]\*; empty string means the literal default tmux server, while a non-empty value names a socket |
| `tmux.holder_session` | yes | non-empty [A-Za-z0-9_.-]+ |
| `defaults.working_directory` | yes | non-empty string |
| `defaults.runtime` | yes | `claude`, `codex`, `opencode`, or `pi`; it must be declared in `runtimes` |
| runtimes.<runtime>.reset_command | yes | non-empty string; runtime key must be a supported local runtime |
| agents[].name | yes | unique [A-Za-z0-9][A-Za-z0-9_.-]\* stable machine identity |
| agents[].alias | yes | non-empty display string |
| agents[].class | yes | [a-z][a-z0-9-]\*; structural only in M1, semantic role resolution is FCM-M1-002 |
| agents[].runtime | yes | `claude`, `codex`, `opencode`, or `pi`; it must be declared in `runtimes` |
| agents[].provider, `model`, `working_directory` | yes | non-empty strings; provider/model capability resolution is a later card |
| agents[].reasoning | yes | `low`, `medium`, or `high` |
| agents[].tool_policy | yes | [a-z][a-z0-9-]\*; structural only in M1 |
| agents[].persistent_persona, `reset_between_tasks` | yes | booleans |
| agents[].lifecycle.enabled | yes | boolean; stored now, reconciled in FCM-M3-001 |
| agents[].lifecycle.desired_state | yes | `running` or `stopped` |
| agents[].launch.yolo | yes | boolean; structured data only, not an arbitrary command escape hatch |
## Semantic handoff
@@ -97,12 +99,12 @@ Semantic validation:
`operator-interaction` to `interaction`;
- canonicalizes `tool_policy` with the same exact alias table;
- rejects protected class/tool-policy mismatches in either direction, while accepting
`class: operator-interaction` with `tool_policy: operator-interaction` as canonical
class: operator-interaction with tool_policy: operator-interaction as canonical
`interaction`;
- derives immutable protected authority only from canonical class; and
- accepts custom baseline or `roles.local` classes without granting protected authority.
`agents[].alias` remains display-only. Tess and Ultron are instance names, never semantic classes.
agents[].alias remains display-only. Tess and Ultron are instance names, never semantic classes.
Canonicalization happens before role-layer lookup, so a legacy-named override cannot redefine an
alias as separate authority. See [Role Classes and Authority](./role-classes.md) and
[Customize Fleet Roles](../how-to/customize-roles.md).
@@ -112,7 +114,7 @@ lifecycle mutation.
## Fail-closed boundary
Every object is `additionalProperties: false`. The compiler rejects unknown, missing, malformed,
Every object is additionalProperties: false. The compiler rejects unknown, missing, malformed,
and wrong-type fields before producing a model. It specifically rejects remote/SSH/host/socket
per-agent fields, connector blocks, secret references, channel fields, arbitrary command fields,
and gateway fields because they are unsupported in the local-tmux M1 contract. It does not silently
+20 -8
View File
@@ -1,13 +1,25 @@
# Local Fleet Status and Drift
`mosaic fleet status [name]`, `verify`, and `doctor` are observational roster-v2 commands. They emit one JSON result and do not write projections, change desired state, start services, stop services, restart services, or mutate tmux.
mosaic fleet status [<name>], `verify`, and `doctor` are observational roster-v2 commands. They emit one JSON result and do not write projections, mutate desired state, operate lifecycle, or change tmux.
The report distinguishes:
## State dimensions
- `missing-session`: an enabled agent desired `running` has no exact roster-named tmux session.
- `unexpected-session`: a desired-`stopped` agent still has its exact session.
- `disabled-running`: a disabled roster agent has its exact session.
- `unmanagedSessions`: sessions on the configured named socket that are neither the exact holder nor an exact roster agent.
- `holder`: `owned`, `missing`, or `ownership-mismatch` after exact holder, private install identity, and complete global tmux environment validation.
- **Desired:** roster membership, generation, enabled flag, and persisted running/stopped target.
- **Managed/derived:** generated environment and expected exact service/session topology.
- **Observed by current roster-v2 commands:** systemd active state, tmux presence, exact holder ownership, and unmanaged sessions.
`doctor` and `status` classify rather than adopt, destroy, or repair unmanaged state. `verify` is observational too, but exits non-zero if ownership cannot be proven, unmanaged sessions exist, or drift is present. Reconciliation fails closed under those conditions and never kills or adopts an unmanaged session.
Implemented drift classifications include:
- `missing-session`: enabled/desired-running agent lacks its exact session;
- `unexpected-session`: desired-stopped agent has its exact session;
- `disabled-running`: disabled roster agent has its exact session;
- `unmanagedSessions`: named-socket sessions that are neither exact holder nor roster agent;
- `holder`: `owned`, `missing`, or `ownership-mismatch` after private identity and global environment checks.
Generated projection failures/staleness are surfaced by plan/apply preparation and bounded recovery fields rather than adopted as configuration. Heartbeat remains wider-fleet observational evidence, never desired state, but the current roster-v2 `status`, `doctor`, and `verify` commands do not read heartbeat files. A provable removed-agent projection may be treated as stale derived state during deletion, but general projection-orphan classification and installed source-versus-asset revision mismatch remain FCM-M4-002/M5-002 holds; current commands must not claim those future checks.
## Command behavior
`status` and `doctor` classify rather than adopt, destroy, or repair. `verify` is observational too, but exits non-zero if ownership cannot be proven, unmanaged sessions exist, or drift is present. Reconciliation fails closed under those conditions and never kills or adopts an unmanaged session.
Doctor/error output uses stable codes and bounded recovery context. Migration, quarantine, lifecycle, status, and troubleshooting output never prints a legacy sensitive value, credential, or privileged command text.
+44
View File
@@ -0,0 +1,44 @@
# Lease broker operations
Place the socket and state file in a dedicated directory with mode `0700`. Start the packaged daemon with:
```bash
python3 "$MOSAIC_HOME/tools/lease-broker/daemon.py" \
--socket /run/user/1000/mosaic-lease/broker.sock \
--state /run/user/1000/mosaic-lease/state.json
```
The broker refuses an existing parent directory whose mode is not exactly `0700`, an existing state file not at `0600`, corrupt/incompatible state, or an already-existing socket path. After bind it sets the socket to `0600`. It never silently unlinks a pre-existing socket. On normal termination it unlinks only the socket inode it created, so it does not remove a replacement path.
Before launching Claude, Claudex, or Pi, export the socket path; `mosaic` then runs the runtime through the packaged register-and-exec wrapper:
```bash
export MOSAIC_LEASE_BROKER_SOCKET=/run/user/1000/mosaic-lease/broker.sock
mosaic claude # or: mosaic claudex, mosaic yolo claudex, mosaic pi
```
The wrapper obtains a broker-minted session ID, creates a private `generation-<session>.state` file beside the socket, and `exec`s the runtime without changing its PID/starttime anchor. The all-tools Claude `PreToolUse` hook and Pi `tool_call` handler inherit that identity and read the current generation from the file. Claudex retains its isolated proxy environment and config directory; Mosaic merges the mandatory all-tools and compaction-lifecycle hooks into that isolated `settings.json` before invoking the same wrapper. PRDY init/update, QA remediation, coord, orchestrator, and fleet launchers also converge on this boundary. Broker registration failure, unsafe isolated settings, unsafe generation state, or missing identity denies launch/tool execution fail-closed; broker timeout/unavailability and malformed replies also block tools.
Claude `PreCompact` and `SessionStart(compact)` hooks and Pi pre-/post-compaction handlers invoke `revoke-lease.py`. Pi `session_start` reload/new/resume/fork and Claude resume/clear advance the locked generation before revocation, so a replacement session inherits no lease even when PID/starttime stay unchanged. Do not invoke the revoker manually as a way to restore authority; it only removes authority. If a lifecycle hook reports failure, stop consequential work and repair broker/generation-state availability before re-verification.
Run the permanent launch inventory locally with:
```bash
python3 packages/mosaic/framework/tools/lease-broker/check-runtime-launches.py --root .
```
The same check runs in the Mosaic package test suite and therefore in root CI. Any direct Claude/Pi binary launch must be replaced with `launch-runtime.py`, `execLeaseGatedRuntime`, or the gated `mosaic` runtime command; do not add static allowlist exceptions.
Clients must complete the request boundary before waiting for a reply. After sending the single JSON object and its terminating newline, the client **MUST half-close the socket's write side** (`shutdown(SHUT_WR)` in POSIX clients; `socket.end()` in Node) and only then await the response. Merely calling `write()` and waiting is invalid: the broker waits for EOF to enforce the exact-one-frame contract and fails closed at its one-second deadline. Do not replace `end()` with `write()` in client helpers. A delayed second frame remains malformed and is rejected.
`mosaic_context_recover` is the only unverified mutator class. Its durable `mosaic-context-refresh` skill is a thin wrapper over `tools/lease-broker/recover-context.py`: `begin` has the broker rebuild the validated `B_payload`/`H_payload`, revoke first, and mint a new `PENDING_DELIVERY` receipt challenge; `complete` accepts neither receipt text nor a challenge argument. Claude maps only the exact direct recovery executable/validated arguments to this exempt tool identity; ordinary `Bash` remains gated. Pi exposes only the `mosaic_context_recover` custom tool; ordinary `bash` and all other tools remain gated. A normal-path receipt cannot be replayed through recovery because each retry begins a distinct recovery cycle and recovery completion cannot receive caller-presented evidence.
Production daemon startup creates a separate private observer socket unless a test-only `--test-observer-file` fixture is selected. Claude's Stop hook sends its exact latest assistant entry and Pi's `message_end` handler sends only finalized assistant content to that authenticated transport; the broker public socket never accepts message text. This is byte-build and private out-of-process harness wiring only: do not activate it against a live daemon, live socket, systemd service, tmux session, or model-output stream outside the controlled integration procedure.
Receipt honesty is load-bearing: absent, malformed, prefix-truncated, and observable adapter-mutated terminal receipts do not promote. A tail-only case is non-promoting only where the concrete terminal payload is malformed or observably incomplete. A tail-preserving middle drop is **not receipt-detectable**; it is the disclosed T-C injection-contract residual deferred to WI-7 server-side evidence. The receipt remains a T-A delivery/liveness prerequisite, never a safety, obedience, or residency proof. The framework skill is source-resident and bridge-projected on install/upgrade; do not hand-create a live runtime symlink.
After a runtime exits, its `generation-<session>.state` file may be removed only after verifying that no process for that broker-minted session remains; stale files carry no lease authority but should be retained during incident analysis. After a broker crash, preserve the protected state file and restart only after verifying that no broker owns the socket. Restart intentionally clears all volatile VERIFIED leases. A leftover socket requires an operator to verify the owning service is stopped and remove that exact socket deliberately. Corrupt, oversized, symlinked, or non-regular state fails closed; do not overwrite it. Preserve it for incident review and establish new state only through an explicit operational decision, which invalidates prior sessions and tokens.
## Security posture
Directory `0700` plus socket/state `0600` is built-in same-principal hardening only: it excludes other UIDs but does **not** stop the same UID from unlinking and counterfeiting the socket. It therefore does not close T-C same-UID replacement. WI-1 does not provide a distinct-principal boundary. A stronger distinct-principal deployment requires an external protected proxy, ACL, or service boundary that clients cannot unlink or rebind and that preserves the authenticated client identity required by the broker's `SO_PEERCRED` and ancestry checks. Server-side branch protection remains the irreducible backstop.
+1
View File
@@ -12,6 +12,7 @@
| [`MISSION-MANIFEST.md`](./MISSION-MANIFEST.md) | Mission/authority boundaries, exact role chain, gate model, mandatory SecReview triggers, Certifier final/no-merge rule, and collision-free slice ownership |
| [`TASKS.md`](./TASKS.md) | Dependency-ordered, bounded P0P3 slices with IN/OUT scope, dependencies, shared contracts, file ownership, evidence, and USC coder2/3/4/5 parallelization |
| [`KBN-101-DB-ROLE-SPLIT.md`](./KBN-101-DB-ROLE-SPLIT.md) | rc.16 direct-Drizzle current storage-wrapper hold: legacy N-1/uncertified/non-operative pending -02/-03/-06/-08; exact README commented/user-guide executable forms fail before masking and source-consistency rejects runner-delegation copy; held future bootstrap → TLS/roles → run → verify → readiness; plus prior production boundary, pgvector owner, attestation, inventory, manifests, DDL classifier, TLS/bootstrap, activation, and certification contract; foundation prerequisite of KBN-100 and real-role gate before KBN-105 |
| [`KBN-101-ENVELOPE-A.md`](./KBN-101-ENVELOPE-A.md) | KBN-101 Envelope A (v6) — RATIFIED, part of the frozen SSOT: rc.20 declarative sink-RBAC + per-role connection-selection + RLS `WITH CHECK`/`USING` write-source + `FORCE ROW LEVEL SECURITY` + sink-resident `task_status_write_override`; adds owner card KBN-101-10 + responsibility-widenings; authority Jason B1 + Mos OPTION A/Q1/Q2 |
| [`SHARED-CONTRACT.md`](./SHARED-CONTRACT.md) | Remediated v1 integration contract: proof authority, exact failures/routes/DTOs/MCP ownership, concrete current-main field migration map, relational invariants, Coordinator split, recovery delivery |
| [`contracts/kanban-schema.v1.ts`](./contracts/kanban-schema.v1.ts) | Drizzle target declarations including exact owner/principal membership, project congruence, tags/archive, proposals, persisted assignments, monotonic fences, durable retry, immutable evidence/audit |
| [`contracts/mechanical-coordinator.v1.ts`](./contracts/mechanical-coordinator.v1.ts) | Pure snapshot decision engine separated from persistence/service adapter; ID-bound approvals, bigint-safe fences, durable retry/quarantine, artifact-backed checkpoints, exact failures |
+53 -38
View File
@@ -138,16 +138,24 @@ Before any preflight that can decide migration state, `mosaic-db-migrator` acqui
Role creation, passwords, membership, database ownership, certificates, and Vault values are platform/IaC/operator work—not Drizzle/application migrations. Application SQL must not issue credential/role management statements or embed credentials.
| Role | Attributes and ownership | Membership / session use |
| --------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `mosaic_platform_database_owner` | `NOLOGIN NOSUPERUSER NOCREATEDB NOCREATEROLE NOREPLICATION NOBYPASSRLS`; platform-only database owner after bootstrap. | Never granted to application roles. |
| external platform bootstrap actor | Provider/operator/IaC-controlled, externally audited **superuser** identity outside the Mosaic role graph and Vault/application configuration. | Creates/transitions the database and roles, then retires from application use. It alone `SET ROLE`s the extension owner for `CREATE EXTENSION`, `ALTER EXTENSION ... UPDATE`, or `ALTER EXTENSION ... SET SCHEMA`, records the action, and `RESET ROLE`s. |
| `mosaic_schema_owner` | `NOLOGIN NOSUPERUSER NOCREATEDB NOCREATEROLE NOREPLICATION NOBYPASSRLS`; owns only `mosaic`/`drizzle` schemas and application/ledger objects. It has only `USAGE` on `mosaic_extensions` for fixed legacy type resolution. | Never an application login; no ownership, `CREATE`, `ALTER`, `DROP`, extension/member-change, or default-privilege authority in `mosaic_extensions`; its migrator subphase never receives temporary `CREATE` there. |
| `mosaic_extension_owner` | Dedicated `NOLOGIN SUPERUSER NOCREATEDB NOCREATEROLE NOREPLICATION NOBYPASSRLS` extension owner, distinct from platform/schema/migrator/runtime. It is used solely to own/maintain untrusted `vector`, `mosaic_extensions`, and every owner-bearing extension member there. | `rolcanlogin=false`, `rolsuper=true`, and **zero members** are catalog-proven. No application role has membership, `SET ROLE`, credential, or inheritable grant. An externally controlled, audited platform-bootstrap **superuser** session alone executes `SET ROLE mosaic_extension_owner` for fresh creation, approved-owner update/relocation, or shadow bootstrap, then `RESET ROLE`; no persistent membership is ever granted. |
| `mosaic_migrator` | `LOGIN NOINHERIT NOSUPERUSER NOCREATEDB NOCREATEROLE NOREPLICATION NOBYPASSRLS`. | Only `mosaic_schema_owner`; runner verifies `session_user=mosaic_migrator`, `SET ROLE mosaic_schema_owner`, then `current_user=mosaic_schema_owner`. |
| `mosaic_data_importer` | `LOGIN NOINHERIT NOSUPERUSER NOCREATEDB NOCREATEROLE NOREPLICATION NOBYPASSRLS`; dedicated data-copy identity, not a DDL or schema owner. | It is used only after runner preparation/verification through the KBN-101-02 file-reference interface. It has no owner/migrator/extension membership and cannot `SET ROLE mosaic_extension_owner`, or `ALTER`/catalog-or-extension-member `UPDATE`/`DROP`/change extension membership. Its bounded data-copy DML is not extension authority. |
| `mosaic_runtime_capability` | `NOLOGIN` and no ownership/administrative attributes. | Holds only named runtime grants. |
| `mosaic_runtime` | `LOGIN INHERIT` with no ownership/administrative attributes. | Only `mosaic_runtime_capability WITH INHERIT TRUE, SET FALSE, ADMIN FALSE`; never owner/migrator member. |
| Role | Attributes and ownership | Membership / session use |
| ---------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `mosaic_platform_database_owner` | `NOLOGIN NOSUPERUSER NOCREATEDB NOCREATEROLE NOREPLICATION NOBYPASSRLS`; platform-only database owner after bootstrap. | Never granted to application roles. |
| external platform bootstrap actor | Provider/operator/IaC-controlled, externally audited **superuser** identity outside the Mosaic role graph and Vault/application configuration. | Creates/transitions the database and roles, then retires from application use. It alone `SET ROLE`s the extension owner for `CREATE EXTENSION`, `ALTER EXTENSION ... UPDATE`, or `ALTER EXTENSION ... SET SCHEMA`, records the action, and `RESET ROLE`s. |
| `mosaic_schema_owner` | `NOLOGIN NOSUPERUSER NOCREATEDB NOCREATEROLE NOREPLICATION NOBYPASSRLS`; owns only `mosaic`/`drizzle` schemas and application/ledger objects. It has only `USAGE` on `mosaic_extensions` for fixed legacy type resolution. | Never an application login; no ownership, `CREATE`, `ALTER`, `DROP`, extension/member-change, or default-privilege authority in `mosaic_extensions`; its migrator subphase never receives temporary `CREATE` there. |
| `mosaic_extension_owner` | Dedicated `NOLOGIN SUPERUSER NOCREATEDB NOCREATEROLE NOREPLICATION NOBYPASSRLS` extension owner, distinct from platform/schema/migrator/runtime. It is used solely to own/maintain untrusted `vector`, `mosaic_extensions`, and every owner-bearing extension member there. | `rolcanlogin=false`, `rolsuper=true`, and **zero members** are catalog-proven. No application role has membership, `SET ROLE`, credential, or inheritable grant. An externally controlled, audited platform-bootstrap **superuser** session alone executes `SET ROLE mosaic_extension_owner` for fresh creation, approved-owner update/relocation, or shadow bootstrap, then `RESET ROLE`; no persistent membership is ever granted. |
| `mosaic_migrator` | `LOGIN NOINHERIT NOSUPERUSER NOCREATEDB NOCREATEROLE NOREPLICATION NOBYPASSRLS`. | Only `mosaic_schema_owner`; runner verifies `session_user=mosaic_migrator`, `SET ROLE mosaic_schema_owner`, then `current_user=mosaic_schema_owner`. |
| `mosaic_data_importer` | `LOGIN NOINHERIT NOSUPERUSER NOCREATEDB NOCREATEROLE NOREPLICATION NOBYPASSRLS`; dedicated data-copy identity, not a DDL or schema owner. | It is used only after runner preparation/verification through the KBN-101-02 file-reference interface. It has no owner/migrator/extension membership and cannot `SET ROLE mosaic_extension_owner`, or `ALTER`/catalog-or-extension-member `UPDATE`/`DROP`/change extension membership. Its bounded data-copy DML is not extension authority. |
| `mosaic_runtime_capability` | `NOLOGIN` and no ownership/administrative attributes. | Holds only named runtime grants. |
| `mosaic_runtime` | `LOGIN INHERIT` with no ownership/administrative attributes. | Only `mosaic_runtime_capability WITH INHERIT TRUE, SET FALSE, ADMIN FALSE`; never owner/migrator member. |
| `mosaic_runtime_user_capability` | `NOLOGIN`, no ownership/admin attrs. | Holds the **User (god)** rung's named grants incl. `INSERT(status)`, `UPDATE(status)` + the status-transition RLS `WITH CHECK` policy on `tasks`; **plus the SOLE `INSERT/UPDATE/DELETE` grant on `task_status_write_override`** (B-1); never granted to Orchestrator/base. |
| `mosaic_runtime_orchestrator_capability` | `NOLOGIN`, no ownership/admin attrs. | Holds the **Orchestrator (near-god)** rung's grants; identical to User **except** (i) every `tasks` write policy **subqueries** the override table and is REJECTED at the sink when an active User deny row exists for that `(workspace_id, task_id, orchestrator)` (B-1); (ii) `INSERT(status)` is pinned by `WITH CHECK` to a **non-terminal initial status** (N-3) — only User may INSERT an arbitrary/terminal status; (iii) it has only `SELECT` (never write) on the override table. Scope is tier/task-identity only (F6). |
| `mosaic_runtime_user` | **`LOGIN INHERIT NOSUPERUSER NOCREATEDB NOCREATEROLE NOREPLICATION NOBYPASSRLS`**, no ownership; **non-owner of every sink table + the override table**. | Member of `mosaic_runtime_user_capability` only (`WITH INHERIT TRUE, SET FALSE, ADMIN FALSE`); the connection-selection **User-rung** credential. **[AD-1]** Requires `CONNECT`, denied `TEMPORARY`. |
| `mosaic_runtime_orchestrator` | **`LOGIN INHERIT NOSUPERUSER NOCREATEDB NOCREATEROLE NOREPLICATION NOBYPASSRLS`**, no ownership; **non-owner of every sink table + the override table**. | Member of `mosaic_runtime_orchestrator_capability` only (same INHERIT/SET/ADMIN flags); the **Orchestrator-rung** credential. **[AD-1]** Requires `CONNECT`, denied `TEMPORARY`. |
> The existing `mosaic_runtime` is confirmed already `LOGIN INHERIT` with no admin attrs (frozen L150) and non-owner; it becomes the **others / deny-by-default** base rung. On the sink it retains `SELECT` (workspace-scoped `USING`, B-2), `INSERT (explicit non-status column list)` (F1), and column-level `UPDATE` on **non-status, non-invariant, non-key, non-tenancy** columns only (F7), each `USING` workspace-scoped; it has **no** `INSERT(status)`, **no** `UPDATE(status)`, **no** `DELETE`, **no** `TRUNCATE` on `tasks`, **no** write on the override table, and RLS denies its status/override writes by default. The rung set is **fixed at three**.
>
> **F2 note:** the three rung LOGIN roles' `NOBYPASSRLS` + `NOSUPERUSER` + **non-ownership** are load-bearing — `FORCE ROW LEVEL SECURITY` collapses for any SUPERUSER/BYPASSRLS role and RLS is silently bypassed by a table **owner**. These attributes are declared in -00 bootstrap, catalog-proven in -00 tests, and re-verified at every checkout by -01 (extended L222 assertion).
The fixed application/runtime schema is **`mosaic`**. Every application/runtime pooled connection executes and verifies exactly `SET search_path TO pg_catalog, mosaic` before its first application query; connection checkout repeats this after reset/reconnect. Transactional application operations use `SET LOCAL search_path TO pg_catalog, mosaic` and verify it before query execution. `public` and `$user` are forbidden in all runtime paths.
@@ -173,30 +181,36 @@ Before relocation, the runner records a parameterized catalog inventory and depe
No SQL identifier may come from URL/config/environment/operator input. Catalog comparisons use parameter values. The fixed identifiers above are constants; the external bootstrap artifact alone may use server-side `format('%I', fixed_allowlisted_identifier)` after allowlist validation. Raw client-side interpolation for identifiers, `SET search_path`, database, schema, role, table, or extension names is forbidden. Tests include injection-shaped values, a poisoned pooled-session reset, and transaction `SET LOCAL` restoration negatives.
External bootstrap executes `REVOKE CONNECT, TEMPORARY ON DATABASE <fixed_database> FROM PUBLIC`, then grants `CONNECT` only to `mosaic_runtime`, `mosaic_migrator`, and the time-bounded external bootstrap actor while it is required. Certification fails if an unrelated login retains `CONNECT` or either application login retains `TEMPORARY`. Runtime receives `USAGE` on `mosaic`, named table/sequence grants through `mosaic_runtime_capability`, and `USAGE` on `drizzle` plus `SELECT` only on `drizzle.__drizzle_migrations` and `drizzle.__mosaic_migration_manifest`. `INSERT`, `UPDATE`, `DELETE`, `TRUNCATE`, and DDL rights on ledger/manifest are revoked. Revoke public CREATE and function EXECUTE; `SECURITY DEFINER` is forbidden unless a separately reviewed exception pins trusted path and grants only the capability role. Database TEMPORARY, role management, extension, schema, and object ownership are denied.
External bootstrap executes `REVOKE CONNECT, TEMPORARY ON DATABASE <fixed_database> FROM PUBLIC`, then grants `CONNECT` only to `mosaic_runtime`, `mosaic_runtime_user`, `mosaic_runtime_orchestrator`, `mosaic_migrator`, and the time-bounded external bootstrap actor while it is required. Certification fails if an unrelated login retains `CONNECT` or any of the three application logins retains `TEMPORARY`. Runtime receives `USAGE` on `mosaic`, named table/sequence grants through `mosaic_runtime_capability`, and `USAGE` on `drizzle` plus `SELECT` only on `drizzle.__drizzle_migrations` and `drizzle.__mosaic_migration_manifest`. **[NB-5]** The two new capability roles `mosaic_runtime_user_capability` and `mosaic_runtime_orchestrator_capability` additionally receive the identical baseline grants: `USAGE` on `mosaic`, `USAGE` on `drizzle` plus `SELECT` only on `drizzle.__drizzle_migrations` and `drizzle.__mosaic_migration_manifest`, and the relevant sequence `USAGE`/`SELECT` grants — mirroring `mosaic_runtime_capability` — so the frozen §6 L222 runtime verify does not fail closed on missing inherited/sequence/ledger grants for the two new rungs. `INSERT`, `UPDATE`, `DELETE`, `TRUNCATE`, and DDL rights on ledger/manifest are revoked. Revoke public CREATE and function EXECUTE; `SECURITY DEFINER` is forbidden unless a separately reviewed exception pins trusted path and grants only the capability role. Database TEMPORARY, role management, extension, schema, and object ownership are denied.
Immutable KBN relations, after KBN-100 creates them, grant runtime only `SELECT, INSERT` and explicitly deny `UPDATE, DELETE, TRUNCATE, REFERENCES, TRIGGER`: `task_events`, `artifacts`, `task_checkpoints`, `task_checkpoint_artifacts`, and `approval_decision_artifacts`. KBN-100 retains RESTRICT/no-cascade semantics. Foundation certification verifies the role/schema boundary only; post-KBN-100 certification verifies this real deployed-role matrix.
> **rc.20 (Envelope A) — authorized by Jason's declarative-RBAC B1 ruling + Mos OPTION A + Mos Q1 (TIER-LEVEL) + Mos Q2 (RLS):** adds (i) the fixed **User/Orchestrator/others** runtime rung-roles (per-ROLE, deny-by-default); each rung **LOGIN** role is `NOSUPERUSER NOCREATEDB NOCREATEROLE NOREPLICATION NOBYPASSRLS`, **NON-OWNER** of every sink/override table, **added to the §4 L176 `CONNECT` allowlist**, and **asserted `TEMPORARY`-denied**. **No per-federated-user role or credential is created** — federated identity maps to a rung **at authorization time only**, and per-writer **attribution lives in metadata columns** (app-supplied `actor_id` on `task_events`), **never** in DB credentials/roles. (ii) Two new fixed rung DSN secrets **`mosaic-db-runtime-user-url`** and **`mosaic-db-runtime-orchestrator-url`** (Gateway-only, `0600 10001:10001`), extending the frozen runtime-URL secret row. (iii) **RLS `WITH CHECK` write-source policies (including the B2-INSERT per-rung INSERT tenant binding `workspace_id = current_setting('mosaic.workspace_id', true)::uuid`, composed as a SINGLE compound `WITH CHECK` or `AS RESTRICTIVE` — never multi-permissive-intended-to-AND, per the v5 composition mandate) + per-rung `SELECT`/`UPDATE` `USING` tenant policies + `FORCE ROW LEVEL SECURITY`** on the sink table `tasks` **and on the new sink-resident `task_status_write_override` table** (tighten-only, deny-by-default) — a §10 schema-v1 **mechanism addition** beyond the prior grant/revoke-only model, authorized by Mos Q2 as coordinator co-authorization with Jason's B1. **The two new rung capability roles additionally receive the baseline frozen §4 L176 runtime grants (`USAGE ON mosaic`, `drizzle` ledger `SELECT`, sequence grants) [NB-5] so they satisfy the frozen §6 L222 runtime verify.** (iv) The **`task_status_write_override` table** (keyed `(workspace_id, task_id, tier)`, no per-writer key), written only by the User rung, read by the Orchestrator/base rung `tasks` write-policy subquery, homes the User-override **at the sink**. The sink DDL (grants + RLS `USING`/`WITH CHECK` + override table + FORCE + unique key + trigger/view/rule invariants) is homed in the **KBN-100 producer** after it creates `tasks`, with `FORCE RLS` as the **terminal** migration step after all backfill/repair DML, per the §4 L178 producer/consumer precedent.
>
> **Frozen-invariant note (authorization basis):** touching the frozen role graph, the §4 L176 CONNECT allowlist, and adding RLS + the override table are §10 contract-change classes (MISSION-MANIFEST §10; SHARED-CONTRACT L82 (rc.6 non-effect) / L88-90 (rc.5 non-effect), both amended by this rc.20; frozen §4 L176). They are authorized here by **Jason B1 + Mos OPTION A + Mos Q1 + Mos Q2**. The addition is strictly a **fixed per-ROLE** set + a **tighten-only** RLS mechanism + a **task/tier-scoped** override table. It stops exactly at the HALT boundary: **no** per-federated-user topology is introduced.
## 5. Deployable verified-TLS bootstrap
`mosaicstack/stack` is the named repository/control plane. Ownership is intentionally non-overlapping: **KBN-101-00 exclusively owns** the versioned external bootstrap interface `infra/pg-bootstrap/roles.sql`, `infra/pg-bootstrap/extensions.sql`, and `infra/pg-bootstrap/README.md`, plus its bootstrap tests—roles, extension-owner transition, and no renderer/deployment manifests. **KBN-101-05 exclusively owns** `tools/db/render-postgres-secrets.ts`, its tests, the current `docker-compose.yml`, `docker-compose.federated.yml`, `deploy/portainer/federated-test.stack.yml`, `tools/federation-harness/docker-compose.two-gateways.yml`, and `apps/gateway/Dockerfile`; it consumes the versioned KBN-101-00 bootstrap interface and owns no bootstrap SQL. The named **Mosaic deployment control plane / Jason** is activation authority; the environment-specific IaC/Vault owner supplies only approved input secret versions and may not substitute an unreviewed current-repository artifact. The KBN-101-05 renderer is the only deployment handoff: it reads secret-provider references, validates owners/modes/digests/SANs, writes each output atomically (`mkstemp` on the target tmpfs, `fsync`, `chmod`/`chown`, atomic rename), and records only secret-version identifiers and hashes.
`KBN-101-05` changes the Gateway image to fixed non-root `USER 10001:10001`, the importer image to fixed non-root `USER 10002:10002`, and the migrator process to fixed non-root `USER 10003:10003`; image and renderer tests freeze every UID:GID. A root-only migrator launch wrapper may open the private signing key before dropping to `10003:10003`, but the migrator process never regains root. Gateway CA and Gateway leaf-certificate mounts, and its own Gateway private key only when it terminates its HTTPS listener, must be readable by `10001:10001`; PostgreSQL private keys and migration-only material are never mounted there, and no secret is world-readable. PostgreSQL is not assigned a guessed UID/GID: its image must first be pinned by digest, and an image-inspection plus rendered Compose/Swarm test freezes the image's effective PostgreSQL UID:GID before the renderer selects mount owner/group. A digest, service UID/GID, rendered secret `uid`/`gid`/`mode`, or container `USER` mismatch is a KBN-101-05 failure. Mosaic applications never generate, self-sign, copy, or persist production certificates; the external bootstrap actor receives them only through the deployment secret mechanism and no plaintext development exception exists for production-like modes.
| Material | Vault target / deployment secret | Mount, injection, and authorized consumer |
| -------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --- |
| Runtime URL | `secret-{env}/mosaic-stack/database/runtime` (`url`) → `mosaic-db-runtime-url-v1` | Gateway only: `/run/secrets/mosaic-db-runtime-url`, `0600`, `10001:10001`; entrypoint maps it to `DATABASE_URL` only at process exec. It is denied to every migrator, storage runtime, fleet, ordinary CLI, and test except an explicit runtime-negative fixture. |
| Migration URL | `secret-{env}/mosaic-stack/database/migrator` (`url`) → `mosaic-db-migrator-url-v1` | Each one-shot migrator only: `/run/secrets/mosaic-db-migrator-url`, `0600`, fixed migrator UID:GID asserted by the image/render test; entrypoint maps it only to `DATABASE_MIGRATION_URL`. It is denied to Gateway, storage runtime, fleet, and ordinary CLI. |
| Importer target URL + provider version | **Vault KV-v2** `secret-{env}/mosaic-stack/database/importer` (`url`) with the same successful response `data.metadata.version` → generation-pinned `mosaic-db-importer-url-v<version>` / `mosaic-db-importer-version-v<version>` | The renderer consumes value and authenticated provider version as one generation, never derives version from DSN bytes. It renders separate immutable copies: migrator-attestation producer only gets `/run/secrets/mosaic-migrate-target-url` and `/run/secrets/mosaic-migrate-target-version`, each `10003:10003` `0400`; importer only gets the same two fixed paths, each `10002:10002` `0400`. Each consumer receives its own read-only mount, never a shared writable file. Runtime, Gateway, ordinary CLI, fleet, and unrelated containers receive neither file nor version. |
| Attestation signing key | `secret-{env}/mosaic-stack/database/migrate-target-attestation` (`private_key`) → `mosaic-db-migrate-target-ed25519-v1` | Runner root-wrapper only: fixed `/run/secrets/mosaic-db-migrate-target-ed25519`, `root:root` `0400`, referenced only by `MOSAIC_DB_ATTESTATION_SIGNING_KEY_FILE`; it opens once then drops to migrator `10003:10003`. No importer/runtime/Gateway mount or log/export is permitted. |
| Attestation public key ring | versioned deployment public-key bundle → `mosaic-db-migrate-target-ed25519-public-v1` | Importer only: pinned `/run/mosaic-attestations/migrate-target.ed25519.pub`, `10002:10002` `0400`; active/overlap key IDs are explicit and revoked IDs fail closed. It contains no private key. |
| Handoff verifier inputs | root-owned non-secret generation descriptor (expected provider version, URL SHA-256, generation ID) plus pinned public verifier key | Privileged controller only: `0400`; no importer URL bytes or private key. It verifies artifact bindings before copy/start. |
| Target attestation artifact | runner-produced non-secret file | Migrator writes only `/run/mosaic-attestations-producer/migrate-target.v1.json`, `10003:10003` `0400`, on a producer-only mount. After runner success, privileged deployment handoff controller verifies and atomically copies the exact signed bytes to a distinct importer-only `/run/mosaic-attestations/migrate-target.v1.json` mount, `10002:10002` `0400`, seals it read-only, then creates importer. No importer write access, shared writable mount, Gateway/runtime access, or unrelated-container mount exists. | |
| CA bundle | `secret-{env}/mosaic-stack/database/tls-ca` (`certificate`) → generation-pinned `mosaic-db-ca-v<version>` | Explicit DB-client consumers only: Gateway `/run/secrets/mosaic-db-ca.crt` `10001:10001` `0444`; migrator same path `10003:10003` `0444`; importer at the exact `DATABASE_TLS_CA_CERT_PATH=/run/secrets/mosaic-db-ca.crt`, `10002:10002` `0444`. PostgreSQL receives a distinct read-only CA copy only when client-cert validation is enabled; no unrelated container receives it. |
| Gateway leaf certificate | `secret-{env}/mosaic-stack/federation/gateway-server-tls` (`certificate`) → `mosaic-gateway-server-cert-v1` | Gateway only: `/run/secrets/mosaic-gateway-server.crt`, `0444`, `10001:10001`; renderer emits this exact Compose and Swarm target and validates it before start. |
| Gateway private key | same Vault record (`private_key`) → `mosaic-gateway-server-key-v1` | Gateway only: `/run/secrets/mosaic-gateway-server.key`, `0400`, `10001:10001`; not mounted to migrator or PostgreSQL and never world-readable. |
| PostgreSQL leaf certificate | `secret-{env}/mosaic-stack/database/postgres-server-tls` (`certificate`) → `mosaic-postgres-server-cert-v1` | PostgreSQL only: `/run/secrets/mosaic-postgres-server.crt`, `0444`, frozen verified postgres UID:GID. |
| PostgreSQL private key | same Vault record (`private_key`) → `mosaic-postgres-server-key-v1` | PostgreSQL only: `/run/secrets/mosaic-postgres-server.key`, `0400`, frozen verified postgres UID:GID; never mounted to Gateway or migrator. |
| Material | Vault target / deployment secret | Mount, injection, and authorized consumer |
| -------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --- |
| Runtime URL | `secret-{env}/mosaic-stack/database/runtime` (`url`) → `mosaic-db-runtime-url-v1` | Gateway only: `/run/secrets/mosaic-db-runtime-url`, `0600`, `10001:10001`; entrypoint maps it to `DATABASE_URL` only at process exec. It is denied to every migrator, storage runtime, fleet, ordinary CLI, and test except an explicit runtime-negative fixture. |
| Runtime URL (User rung) | `secret-{env}/mosaic-stack/database/runtime-user` (`url`) → `mosaic-db-runtime-user-url-v1` **(PROVISIONAL-CONVENTION — the exact Vault subpath is inferred from the Runtime URL row's naming pattern and is TO-CONFIRM against the live Vault layout at KBN-101-03/-05 implementation; the secret NAME `mosaic-db-runtime-user-url` is envelope-fixed, the `.../database/runtime-user` subpath is not.)** | Gateway only: `/run/secrets/mosaic-db-runtime-user-url`, `0600`, `10001:10001`; entrypoint maps it to the User-rung pool DSN only at process exec. Denied to every migrator, storage runtime, fleet, ordinary CLI, and test except an explicit runtime-negative fixture. |
| Runtime URL (Orchestrator rung) | `secret-{env}/mosaic-stack/database/runtime-orchestrator` (`url`) → `mosaic-db-runtime-orchestrator-url-v1` **(PROVISIONAL-CONVENTION — the exact Vault subpath is inferred from the Runtime URL row's naming pattern and is TO-CONFIRM against the live Vault layout at KBN-101-03/-05 implementation; the secret NAME `mosaic-db-runtime-orchestrator-url` is envelope-fixed, the `.../database/runtime-orchestrator` subpath is not.)** | Gateway only: `/run/secrets/mosaic-db-runtime-orchestrator-url`, `0600`, `10001:10001`; entrypoint maps it to the Orchestrator-rung pool DSN only at process exec. Denied to every migrator, storage runtime, fleet, ordinary CLI, and test except an explicit runtime-negative fixture. |
| Migration URL | `secret-{env}/mosaic-stack/database/migrator` (`url`) → `mosaic-db-migrator-url-v1` | Each one-shot migrator only: `/run/secrets/mosaic-db-migrator-url`, `0600`, fixed migrator UID:GID asserted by the image/render test; entrypoint maps it only to `DATABASE_MIGRATION_URL`. It is denied to Gateway, storage runtime, fleet, and ordinary CLI. |
| Importer target URL + provider version | **Vault KV-v2** `secret-{env}/mosaic-stack/database/importer` (`url`) with the same successful response `data.metadata.version` → generation-pinned `mosaic-db-importer-url-v<version>` / `mosaic-db-importer-version-v<version>` | The renderer consumes value and authenticated provider version as one generation, never derives version from DSN bytes. It renders separate immutable copies: migrator-attestation producer only gets `/run/secrets/mosaic-migrate-target-url` and `/run/secrets/mosaic-migrate-target-version`, each `10003:10003` `0400`; importer only gets the same two fixed paths, each `10002:10002` `0400`. Each consumer receives its own read-only mount, never a shared writable file. Runtime, Gateway, ordinary CLI, fleet, and unrelated containers receive neither file nor version. |
| Attestation signing key | `secret-{env}/mosaic-stack/database/migrate-target-attestation` (`private_key`) → `mosaic-db-migrate-target-ed25519-v1` | Runner root-wrapper only: fixed `/run/secrets/mosaic-db-migrate-target-ed25519`, `root:root` `0400`, referenced only by `MOSAIC_DB_ATTESTATION_SIGNING_KEY_FILE`; it opens once then drops to migrator `10003:10003`. No importer/runtime/Gateway mount or log/export is permitted. |
| Attestation public key ring | versioned deployment public-key bundle → `mosaic-db-migrate-target-ed25519-public-v1` | Importer only: pinned `/run/mosaic-attestations/migrate-target.ed25519.pub`, `10002:10002` `0400`; active/overlap key IDs are explicit and revoked IDs fail closed. It contains no private key. |
| Handoff verifier inputs | root-owned non-secret generation descriptor (expected provider version, URL SHA-256, generation ID) plus pinned public verifier key | Privileged controller only: `0400`; no importer URL bytes or private key. It verifies artifact bindings before copy/start. |
| Target attestation artifact | runner-produced non-secret file | Migrator writes only `/run/mosaic-attestations-producer/migrate-target.v1.json`, `10003:10003` `0400`, on a producer-only mount. After runner success, privileged deployment handoff controller verifies and atomically copies the exact signed bytes to a distinct importer-only `/run/mosaic-attestations/migrate-target.v1.json` mount, `10002:10002` `0400`, seals it read-only, then creates importer. No importer write access, shared writable mount, Gateway/runtime access, or unrelated-container mount exists. | |
| CA bundle | `secret-{env}/mosaic-stack/database/tls-ca` (`certificate`) → generation-pinned `mosaic-db-ca-v<version>` | Explicit DB-client consumers only: Gateway `/run/secrets/mosaic-db-ca.crt` `10001:10001` `0444`; migrator same path `10003:10003` `0444`; importer at the exact `DATABASE_TLS_CA_CERT_PATH=/run/secrets/mosaic-db-ca.crt`, `10002:10002` `0444`. PostgreSQL receives a distinct read-only CA copy only when client-cert validation is enabled; no unrelated container receives it. |
| Gateway leaf certificate | `secret-{env}/mosaic-stack/federation/gateway-server-tls` (`certificate`) → `mosaic-gateway-server-cert-v1` | Gateway only: `/run/secrets/mosaic-gateway-server.crt`, `0444`, `10001:10001`; renderer emits this exact Compose and Swarm target and validates it before start. |
| Gateway private key | same Vault record (`private_key`) → `mosaic-gateway-server-key-v1` | Gateway only: `/run/secrets/mosaic-gateway-server.key`, `0400`, `10001:10001`; not mounted to migrator or PostgreSQL and never world-readable. |
| PostgreSQL leaf certificate | `secret-{env}/mosaic-stack/database/postgres-server-tls` (`certificate`) → `mosaic-postgres-server-cert-v1` | PostgreSQL only: `/run/secrets/mosaic-postgres-server.crt`, `0444`, frozen verified postgres UID:GID. |
| PostgreSQL private key | same Vault record (`private_key`) → `mosaic-postgres-server-key-v1` | PostgreSQL only: `/run/secrets/mosaic-postgres-server.key`, `0400`, frozen verified postgres UID:GID; never mounted to Gateway or migrator. |
Compose renders each value-plus-provider-version generation into a non-repository temporary generation directory, `fsync`s every file and directory, atomically renames the complete generation, and mounts immutable per-consumer copies only after the pair is complete. Swarm declares distinct versioned secret/config references for each migrator/importer consumer and generation. A deployment may not start, reload, or combine any URL/version/CA/attestation/public-key material across generations. The privileged handoff controller is the only bridge from migrator producer mount to importer mount: it verifies the signed producer artifact plus generation, copies atomically, seals importer read-only, and only then starts importer; it is tested for failed/partial copy, wrong generation, wrong owner/mode, and no importer start. KBN-101-05 rejects bind-mounted committed cert/key files, environment-encoded PEM, missing secrets, non-atomic renderer output, mixed generations, shared writable files, private-key access outside its named consumer (Gateway for Gateway key; PostgreSQL for PostgreSQL key), importer URL/version/attestation/key access outside their named consumer, CA outside explicit DB clients, and any world-readable URL/key. The existing target Vault names are planned canonical paths and must be verified/provisioned by the deployment-owner input; the planning card does not claim they exist.
@@ -229,20 +243,21 @@ Stable sanitized codes are `DATABASE_RUNTIME_URL_REQUIRED`, `DATABASE_MIGRATION_
Every KBN-101 card remains one PR with exclusive ownership. Cards `00``07` may merge only as **prepared, inactive capability**: no current owner-runtime deployment consumes their image/config, and no compatibility switch is exposed to a runtime operator. They must not retain `ALLOW_LEGACY_*`, runtime DDL, `DATABASE_URL` migration fallback, plaintext TLS, direct Drizzle, or test-only bypass flags. Current owner-runtime deployments remain on their known N-1 release until final activation.
| Card | Depends on | Complete, disjoint file/glob manifest and required test/evidence paths |
| ----------------------------------------------------------------------- | ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | --------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `KBN-101-00` platform bootstrap / IaC | contract | **Only:** `infra/pg-bootstrap/roles.sql`, `infra/pg-bootstrap/extensions.sql`, `infra/pg-bootstrap/README.md`, `infra/pg-bootstrap/tests/**`. It creates the extension-owner role/schema/extension interface and proves fresh, approved-owner, legacy-shadow, catalog/default-privilege, and direct-denial bootstrap cases. No renderer, runner, Compose, CI, or deployment path. |
| `KBN-101-01` typed runtime config and verifier | 00 | **Only:** `packages/config/src/index.ts`, `packages/config/src/mosaic-config.ts`, `packages/config/src/mosaic-config.spec.ts`; `packages/db/src/client.ts`, `packages/db/src/defaults.ts`, `packages/db/src/connection-identity.ts`, `packages/db/src/client.spec.ts`, `packages/db/src/defaults.spec.ts`, `packages/db/src/connection-identity.spec.ts`; `apps/gateway/src/database/database.module.ts`, `apps/gateway/src/database/database.module.spec.ts`. It supplies runtime/migration/TLS DTO parsing plus runtime identity/search-path/readiness verification. No migrator, storage, installer, deploy, or CI path. |
| `KBN-101-03` sole runner, manifest, and schema foundation | 00,01 | **Only:** `.gitattributes`; `packages/db/package.json`; `packages/db/drizzle.config.ts`; `packages/db/src/cli.ts`, `packages/db/src/cli.spec.ts`, `packages/db/src/index.ts`, `packages/db/src/index.import-negative.spec.ts`, `packages/db/src/migrate.ts`, `packages/db/src/migrate.test.ts`, `packages/db/src/schema.ts`, `packages/db/src/schema.spec.ts`; `packages/db/src/migrator/**`; `packages/db/drizzle/*.sql`, `packages/db/drizzle/meta/*.json`; `docker/db-migrator.Dockerfile`, `docker/db-migrator.Dockerfile.spec.ts`; `packages/db/package-bin.spec.ts`. It alone publishes `"mosaic-db-migrator": "./dist/cli.js"`, verifies source/build/pack/discovery, and sets `ENTRYPOINT ["mosaic-db-migrator"]`; it exclusively owns `packages/db/src/migrator/target-attestation.dto.ts`, `target-attestation-signer.ts`, and their specs: fixed-key reference validation, JCS canonical payload, Ed25519 signing, producer TLS/identity/manifest binding, atomic artifact emission, and producer tests. It owns journal/manifest/ledger/lock/relocation/shadow tests. Shipped `0009` bytes stay unchanged. |
| `KBN-101-02` runtime DDL closure | 01,03 | **Only:** `docker/init-db.sql`, `infra/pg-init/01-extensions.sql`; `packages/storage/src/adapters/postgres.ts`, `packages/storage/src/adapters/postgres.spec.ts`, `packages/storage/src/factory.ts`, `packages/storage/src/factory.spec.ts`, `packages/storage/src/types.ts`, `packages/storage/src/tier-detection.ts`, `packages/storage/src/tier-detection.spec.ts`, `packages/storage/src/cli.ts`, `packages/storage/src/cli.spec.ts`, `packages/storage/src/migrate-tier.ts`, `packages/storage/src/migrate-tier.spec.ts`, `packages/storage/src/migrate-tier.integration.test.ts`; `apps/gateway/src/main.ts`, `apps/gateway/src/__tests__/integration/federated-boot.pg-unreachable.integration.test.ts`, `apps/gateway/src/__tests__/integration/federated-boot.success.integration.test.ts`, `apps/gateway/src/__tests__/integration/federated-pgvector.integration.test.ts`; `packages/db/src/federation.integration.test.ts`; `packages/mosaic/src/commands/fleet-backlog.ts`, `packages/mosaic/src/commands/fleet-backlog.spec.ts`. It consumes the -03 runner and exclusively owns importer verification/interface in `packages/storage/src/{cli,migrate-tier}.ts` and the named unit/integration specs: fixed URL/version/attestation/public-key safe-open fds (`O_RDONLY | O_CLOEXEC | O_NOFOLLOW`plus post-open regular-owner-mode-link-count validation), one-read protected-memory URL connection, fd zeroization/close, signature/key/expiry/replay/authenticated-provider-version/digest/generation/CA/binding validation, no forwarding/logging/oracle, post-TLS zero-DML comparison, consumer isolation, and DDL classifier. It closes runtime/retired-init DDL only; removes the current Gateway production`.env`/monorepo auto-load path in favor of the -05 renderer-backed process-exec or `LoadCredential` consumer boundary; and excludes every -03 signer/runner, index, migrate, and Drizzle-config asset, and every deployment/CI/doc path. |
| `KBN-101-04` installer/wizard | 01 | **Only:** `packages/mosaic/src/stages/gateway-config.ts`, `packages/mosaic/src/stages/gateway-config.spec.ts`, `packages/mosaic/src/stages/gateway-config-cors.spec.ts`, `packages/mosaic/src/stages/wizard-menu.spec.ts`, `packages/mosaic/src/wizard.ts`. It persists only non-secret references/injected-variable contracts; source inspection excludes `tools/install.sh`, which does not read/write the database DSN. |
| `KBN-101-05` renderer and deployment | 00,03 | **Only:** `tools/db/render-postgres-secrets.ts`, `tools/db/render-postgres-secrets.spec.ts`; `apps/gateway/Dockerfile`, `apps/gateway/Dockerfile.spec.ts`; `docker-compose.yml`, `docker-compose.spec.ts`; `docker-compose.federated.yml`, `docker-compose.federated.spec.ts`; `deploy/portainer/federated-test.stack.yml`, `deploy/portainer/federated-test.stack.spec.ts`; `tools/federation-harness/docker-compose.two-gateways.yml`, `tools/federation-harness/docker-compose.two-gateways.spec.ts`. It consumes the -00 bootstrap interface and -03 immutable runner image, and exclusively renders/tests fixed Gateway/importer/migrator UIDs; runner-only root-owned signing-key reference; KV-v2 importer URL plus same-response `data.metadata.version`; separate immutable generation-pinned URL/version mounts for `10003:10003` migrator and `10002:10002` importer; importer-only CA at `DATABASE_TLS_CA_CERT_PATH`, public-key/key-ring, and controlled producer-only-to-importer-only attestation handoff mount; privileged controller verification/copy/fsync/atomic-rename/seal-before-importer-start behavior; Compose generation-dir fsync/atomic-rename and Swarm versioned-secret/config no-mixed-generation behavior. It owns no bootstrap, runner, config, storage, or CI file. |
| `KBN-101-07` operator/runbook/docs | 02,03,04,05 | **Only:** `README.md`, `CLAUDE.md`, `docs/guides/admin-guide.md`, `docs/guides/dev-guide.md`, `docs/guides/deployment.md`, **`docs/guides/migrate-tier.md`**, `docs/guides/user-guide.md`, `docs/federation/MILESTONES.md`, `docs/federation/SETUP.md`, `docs/federation/TASKS.md`, `docs/fleet/backlog-conventions.md`, `docs/PERFORMANCE.md`, `docs/design/storage-abstraction-middleware.md`, `docs/plans/2026-03-15-agent-platform-architecture.md`, `docs/plans/2026-03-15-wave2-tui-layout-navigation.md`, `docs/runbooks/kbn-101-database-role-split.md`, `docs/reports/native-kanban-sot/kbn-101-operator-readiness-report.md`, `docs/native-kanban-sot/tests/kbn-101-operator-docs.spec.ts`. It exclusively owns the active migrate-tier operator route; local PGlite/non-PostgreSQL Compose disposition; the held PostgreSQL/federated activation order; and the non-operative production renderer/Vault generation-pinned process-exec or `LoadCredential` consumer-isolation schematic. It documents interfaces produced by -02/-03/-04/-05, including both file references, signing-key isolation, attestation bindings, rotation/replay, no-connection/no-DML errors, and the ban on current production `.env`, monorepo auto-load, environment-file, credential export/argv, or restart-as-secret-activation guidance; it owns no source, storage, CLI, runner, or CI file. |
| `KBN-101-06` CI classifier and command matrix | 02,03,05,07 | **Only:** `.woodpecker/ci.yml`, `tools/ci/kbn101-ddl-inventory.ts`, `tools/ci/kbn101-ddl-inventory.spec.ts`, `tools/ci/fixtures/kbn101-ddl-inventory.json`, `tools/ci/kbn101-entrypoint-matrix.ts`, `tools/ci/kbn101-entrypoint-matrix.spec.ts`. It invokes the already-produced bin/image/deployment/doc artifacts and edits no producer file. Its inventory test enforces manifest overlap, ownerless, duplicate-owner, path-existence, allowlist, active-route field completeness, finite operator-document inventory, normative/status-only non-masking, unknown-command, and historical/status-only masking failures. Before inventory, ownership, or status masking, its semantic suite fails the exact README commented code-fence generic-wrapper form and exact user-guide executable generic-wrapper form recorded in §2.2, and source-consistency opens `packages/storage/src/cli.ts` to prove its current direct-Drizzle `pnpm --filter @mosaicstack/db db:migrate` `execSync` implementation and absence of a `mosaic-db-migrator` bin make runner-delegation copy false. It also fails former `SETUP.md` automatic-first-boot/startup, the exact former `MILESTONES.md` wording, and the exact former README/dev/deployment Compose-first sequences before an inventory record can mask any path; it fails Compose-up-before-runner, extension/schema/migration automatic wording, init-script authority, production `.env`, `EnvironmentFile=`, credential shell export/argv, and restart-as-secret-activation. It passes the rc.16 held PGlite-data-layer/non-PostgreSQL-Compose disposition (and fails Gateway/Web local start while daemon/inherited/root/app-local DSN or non-local-tier input could select PostgreSQL), the one `Held future procedure` section with KBN-101-00/-03/-05 and external bootstrap → TLS/roles → `mosaic-db-migrator --run``mosaic-db-migrator --verify` → Gateway/Compose-readiness, and the README legacy-N-1 CI status assertion; every runner hit outside that section and every current-CI authority variant fails before masking. Its matrix invokes the -02 secure target route and verifies URL/version generation mismatch, provider rotation/revocation, consumer isolation, safe-open TOCTOU/link/mode/owner, absent/wrong importer CA, no log/oracle, every declared attestation refusal before target connection/DDL, and post-connect mismatch with zero DML. |
| `KBN-101-08` foundation certification and **atomic activation release** | 00…07 | **Only evidence:** `docs/reports/native-kanban-sot/kbn-101-foundation-activation-certificate.md`, `docs/reports/native-kanban-sot/kbn-101-foundation-activation-evidence.json`. It changes no implementation path. Independent review and terminal-green CI must verify prepared artifacts before Mosaic control plane/Jason authorizes backup → drain/scale-zero N-1 → TLS → roles → runner → verified readiness → rolling runtime; any red result aborts. |
| `KBN-101-09` post-KBN-100 certification | KBN-100,08 | **Only evidence:** `docs/reports/native-kanban-sot/kbn-101-immutable-role-certificate.md`, `docs/reports/native-kanban-sot/kbn-101-immutable-role-evidence.json`. It changes no implementation path and records real deployed runtime INSERT/SELECT plus UPDATE/DELETE-denial evidence and independent security/Ultron approval. |
| Card | Depends on | Complete, disjoint file/glob manifest and required test/evidence paths |
| --------------------------------------------------------------------------------------- | -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | --------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `KBN-101-00` platform bootstrap / IaC | contract | **Only:** `infra/pg-bootstrap/roles.sql`, `infra/pg-bootstrap/extensions.sql`, `infra/pg-bootstrap/README.md`, `infra/pg-bootstrap/tests/**`. It creates the extension-owner role/schema/extension interface and proves fresh, approved-owner, legacy-shadow, catalog/default-privilege, and direct-denial bootstrap cases. No renderer, runner, Compose, CI, or deployment path. |
| `KBN-101-01` typed runtime config and verifier | 00 | **Only:** `packages/config/src/index.ts`, `packages/config/src/mosaic-config.ts`, `packages/config/src/mosaic-config.spec.ts`; `packages/db/src/client.ts`, `packages/db/src/defaults.ts`, `packages/db/src/connection-identity.ts`, `packages/db/src/client.spec.ts`, `packages/db/src/defaults.spec.ts`, `packages/db/src/connection-identity.spec.ts`; `apps/gateway/src/database/database.module.ts`, `apps/gateway/src/database/database.module.spec.ts`. It supplies runtime/migration/TLS DTO parsing plus runtime identity/search-path/readiness verification. No migrator, storage, installer, deploy, or CI path. |
| `KBN-101-10` declarative sink-RBAC + per-role connection-selection + credential-handoff | 00,01 | **Only:** `packages/db/src/sink-rbac/**`, `packages/db/src/credential-handoff/**`. It homes: the source-controlled declarative RBAC policy artifact + schema + evaluator; the fixed **rung ladder** + federation-aware identity→rung resolver; the rung→DB-credential **connection-selection** module (consumes -01 pools; import direction `10→01`); the **status-preserving-UPSERT** contract + **invariant field-set** module + **no-status view/rule/trigger enumeration**; and the **SCM_RIGHTS** credential-handoff + **non-dumpable** assert/verify library. It creates no roles, no DDL, no Compose/CI/deploy path; it is consumed by -00 (role names), -01 (connection wiring + identity/attribute verify), -02/-03/-05 (write path + fd handoff), specified-against by **KBN-100** (sink DDL: grants/RLS/`USING`+`WITH CHECK` policies/override-table/FORCE/unique-key/trigger-view-rule-invariant on `tasks`), and certified deployed by KBN-101-09. |
| `KBN-101-03` sole runner, manifest, and schema foundation | 00,01,10 | **Only:** `.gitattributes`; `packages/db/package.json`; `packages/db/drizzle.config.ts`; `packages/db/src/cli.ts`, `packages/db/src/cli.spec.ts`, `packages/db/src/index.ts`, `packages/db/src/index.import-negative.spec.ts`, `packages/db/src/migrate.ts`, `packages/db/src/migrate.test.ts`, `packages/db/src/schema.ts`, `packages/db/src/schema.spec.ts`; `packages/db/src/migrator/**`; `packages/db/drizzle/*.sql`, `packages/db/drizzle/meta/*.json`; `docker/db-migrator.Dockerfile`, `docker/db-migrator.Dockerfile.spec.ts`; `packages/db/package-bin.spec.ts`. It alone publishes `"mosaic-db-migrator": "./dist/cli.js"`, verifies source/build/pack/discovery, and sets `ENTRYPOINT ["mosaic-db-migrator"]`; it exclusively owns `packages/db/src/migrator/target-attestation.dto.ts`, `target-attestation-signer.ts`, and their specs: fixed-key reference validation, JCS canonical payload, Ed25519 signing, producer TLS/identity/manifest binding, atomic artifact emission, and producer tests. It owns journal/manifest/ledger/lock/relocation/shadow tests. Shipped `0009` bytes stay unchanged. |
| `KBN-101-02` runtime DDL closure | 01,03,10 | **Only:** `docker/init-db.sql`, `infra/pg-init/01-extensions.sql`; `packages/storage/src/adapters/postgres.ts`, `packages/storage/src/adapters/postgres.spec.ts`, `packages/storage/src/factory.ts`, `packages/storage/src/factory.spec.ts`, `packages/storage/src/types.ts`, `packages/storage/src/tier-detection.ts`, `packages/storage/src/tier-detection.spec.ts`, `packages/storage/src/cli.ts`, `packages/storage/src/cli.spec.ts`, `packages/storage/src/migrate-tier.ts`, `packages/storage/src/migrate-tier.spec.ts`, `packages/storage/src/migrate-tier.integration.test.ts`; `apps/gateway/src/main.ts`, `apps/gateway/src/__tests__/integration/federated-boot.pg-unreachable.integration.test.ts`, `apps/gateway/src/__tests__/integration/federated-boot.success.integration.test.ts`, `apps/gateway/src/__tests__/integration/federated-pgvector.integration.test.ts`; `packages/db/src/federation.integration.test.ts`; `packages/mosaic/src/commands/fleet-backlog.ts`, `packages/mosaic/src/commands/fleet-backlog.spec.ts`. It consumes the -03 runner and exclusively owns importer verification/interface in `packages/storage/src/{cli,migrate-tier}.ts` and the named unit/integration specs: fixed URL/version/attestation/public-key safe-open fds (`O_RDONLY | O_CLOEXEC | O_NOFOLLOW`plus post-open regular-owner-mode-link-count validation), one-read protected-memory URL connection, fd zeroization/close, signature/key/expiry/replay/authenticated-provider-version/digest/generation/CA/binding validation, no forwarding/logging/oracle, post-TLS zero-DML comparison, consumer isolation, and DDL classifier. It closes runtime/retired-init DDL only; removes the current Gateway production`.env`/monorepo auto-load path in favor of the -05 renderer-backed process-exec or `LoadCredential` consumer boundary; and excludes every -03 signer/runner, index, migrate, and Drizzle-config asset, and every deployment/CI/doc path. |
| `KBN-101-04` installer/wizard | 01 | **Only:** `packages/mosaic/src/stages/gateway-config.ts`, `packages/mosaic/src/stages/gateway-config.spec.ts`, `packages/mosaic/src/stages/gateway-config-cors.spec.ts`, `packages/mosaic/src/stages/wizard-menu.spec.ts`, `packages/mosaic/src/wizard.ts`. It persists only non-secret references/injected-variable contracts; source inspection excludes `tools/install.sh`, which does not read/write the database DSN. |
| `KBN-101-05` renderer and deployment | 00,03,10 | **Only:** `tools/db/render-postgres-secrets.ts`, `tools/db/render-postgres-secrets.spec.ts`; `apps/gateway/Dockerfile`, `apps/gateway/Dockerfile.spec.ts`; `docker-compose.yml`, `docker-compose.spec.ts`; `docker-compose.federated.yml`, `docker-compose.federated.spec.ts`; `deploy/portainer/federated-test.stack.yml`, `deploy/portainer/federated-test.stack.spec.ts`; `tools/federation-harness/docker-compose.two-gateways.yml`, `tools/federation-harness/docker-compose.two-gateways.spec.ts`. It consumes the -00 bootstrap interface and -03 immutable runner image, and exclusively renders/tests fixed Gateway/importer/migrator UIDs; runner-only root-owned signing-key reference; KV-v2 importer URL plus same-response `data.metadata.version`; separate immutable generation-pinned URL/version mounts for `10003:10003` migrator and `10002:10002` importer; importer-only CA at `DATABASE_TLS_CA_CERT_PATH`, public-key/key-ring, and controlled producer-only-to-importer-only attestation handoff mount; privileged controller verification/copy/fsync/atomic-rename/seal-before-importer-start behavior; Compose generation-dir fsync/atomic-rename and Swarm versioned-secret/config no-mixed-generation behavior. It owns no bootstrap, runner, config, storage, or CI file. |
| `KBN-101-07` operator/runbook/docs | 02,03,04,05 | **Only:** `README.md`, `CLAUDE.md`, `docs/guides/admin-guide.md`, `docs/guides/dev-guide.md`, `docs/guides/deployment.md`, **`docs/guides/migrate-tier.md`**, `docs/guides/user-guide.md`, `docs/federation/MILESTONES.md`, `docs/federation/SETUP.md`, `docs/federation/TASKS.md`, `docs/fleet/backlog-conventions.md`, `docs/PERFORMANCE.md`, `docs/design/storage-abstraction-middleware.md`, `docs/plans/2026-03-15-agent-platform-architecture.md`, `docs/plans/2026-03-15-wave2-tui-layout-navigation.md`, `docs/runbooks/kbn-101-database-role-split.md`, `docs/reports/native-kanban-sot/kbn-101-operator-readiness-report.md`, `docs/native-kanban-sot/tests/kbn-101-operator-docs.spec.ts`. It exclusively owns the active migrate-tier operator route; local PGlite/non-PostgreSQL Compose disposition; the held PostgreSQL/federated activation order; and the non-operative production renderer/Vault generation-pinned process-exec or `LoadCredential` consumer-isolation schematic. It documents interfaces produced by -02/-03/-04/-05, including both file references, signing-key isolation, attestation bindings, rotation/replay, no-connection/no-DML errors, and the ban on current production `.env`, monorepo auto-load, environment-file, credential export/argv, or restart-as-secret-activation guidance; it owns no source, storage, CLI, runner, or CI file. |
| `KBN-101-06` CI classifier and command matrix | 02,03,05,07,10 | **Only:** `.woodpecker/ci.yml`, `tools/ci/kbn101-ddl-inventory.ts`, `tools/ci/kbn101-ddl-inventory.spec.ts`, `tools/ci/fixtures/kbn101-ddl-inventory.json`, `tools/ci/kbn101-entrypoint-matrix.ts`, `tools/ci/kbn101-entrypoint-matrix.spec.ts`. It invokes the already-produced bin/image/deployment/doc artifacts and edits no producer file. Its inventory test enforces manifest overlap, ownerless, duplicate-owner, path-existence, allowlist, active-route field completeness, finite operator-document inventory, normative/status-only non-masking, unknown-command, and historical/status-only masking failures. Before inventory, ownership, or status masking, its semantic suite fails the exact README commented code-fence generic-wrapper form and exact user-guide executable generic-wrapper form recorded in §2.2, and source-consistency opens `packages/storage/src/cli.ts` to prove its current direct-Drizzle `pnpm --filter @mosaicstack/db db:migrate` `execSync` implementation and absence of a `mosaic-db-migrator` bin make runner-delegation copy false. It also fails former `SETUP.md` automatic-first-boot/startup, the exact former `MILESTONES.md` wording, and the exact former README/dev/deployment Compose-first sequences before an inventory record can mask any path; it fails Compose-up-before-runner, extension/schema/migration automatic wording, init-script authority, production `.env`, `EnvironmentFile=`, credential shell export/argv, and restart-as-secret-activation. It passes the rc.16 held PGlite-data-layer/non-PostgreSQL-Compose disposition (and fails Gateway/Web local start while daemon/inherited/root/app-local DSN or non-local-tier input could select PostgreSQL), the one `Held future procedure` section with KBN-101-00/-03/-05 and external bootstrap → TLS/roles → `mosaic-db-migrator --run``mosaic-db-migrator --verify` → Gateway/Compose-readiness, and the README legacy-N-1 CI status assertion; every runner hit outside that section and every current-CI authority variant fails before masking. Its matrix invokes the -02 secure target route and verifies URL/version generation mismatch, provider rotation/revocation, consumer isolation, safe-open TOCTOU/link/mode/owner, absent/wrong importer CA, no log/oracle, every declared attestation refusal before target connection/DDL, and post-connect mismatch with zero DML. |
| `KBN-101-08` foundation certification and **atomic activation release** | 00…07,10 | **Only evidence:** `docs/reports/native-kanban-sot/kbn-101-foundation-activation-certificate.md`, `docs/reports/native-kanban-sot/kbn-101-foundation-activation-evidence.json`. It changes no implementation path. Independent review and terminal-green CI must verify prepared artifacts before Mosaic control plane/Jason authorizes backup → drain/scale-zero N-1 → TLS → roles → runner → verified readiness → rolling runtime; any red result aborts. |
| `KBN-101-09` post-KBN-100 certification | KBN-100,08 | **Only evidence:** `docs/reports/native-kanban-sot/kbn-101-immutable-role-certificate.md`, `docs/reports/native-kanban-sot/kbn-101-immutable-role-evidence.json`. It changes no implementation path and records real deployed runtime INSERT/SELECT plus UPDATE/DELETE-denial evidence and independent security/Ultron approval. |
The manifests above are the complete ownership universe for KBN-101 implementation paths; the KBN-101-06 inventory test fails on overlap, an ownerless in-scope path, or a nonexistent declared path. Cards `00``07` are prepared artifacts, not independently deployed releases: the immutable N-1 owner-runtime image stays live until KBN-101-08 control-plane atomic activation. No activation card edits a source-changing path, and no runtime bypass or broken deployed intermediate exists.
The manifests above are the complete ownership universe for KBN-101 implementation paths **including KBN-101-10**; the KBN-101-06 inventory test fails on overlap, an ownerless in-scope path, or a nonexistent declared path. Cards `00``07` are prepared artifacts, not independently deployed releases: the immutable N-1 owner-runtime image stays live until KBN-101-08 control-plane atomic activation. No activation card edits a source-changing path, and no runtime bypass or broken deployed intermediate exists.
**Authority:** Mosaic control plane/Jason is the sole activation and rollback authority. CI, Gateway, migrator, Coordinator, and Certifier cannot activate, waive a red result, or force release. Before an incompatible KBN-100 switch, the authority stops/scales runtime, uses the approved backup/restore or separately reviewed runner artifact, restores only a known TLS-compatible runtime with its runtime secret after `hostssl` enforcement, and verifies no plaintext sessions plus TLS/readiness. Migration URL is never injected into Gateway to enable rollback. KBN-100 starts only after KBN-101-08; KBN-105 starts only after KBN-101-09.
@@ -0,0 +1,377 @@
# KBN-101 — B1/B2 Envelope A (v6, FINAL) — Declarative Sink-RBAC + Per-Role Credential/Connection-Selection + RLS Write-Source (INSERT tenant-bound, single-compound-or-RESTRICTIVE composition) + Sink-Resident User-Override + Read/USING Enforcement
**Ratification status:** RATIFIED — part of the frozen SSOT as of this PR (KBN-101 Envelope A, landed **FORM A — apply-in-place**). This is the converged **v6** envelope, ratified as the authoritative record of the **rc.20** contract amendment now inlined into [`KBN-101-DB-ROLE-SPLIT.md`](./KBN-101-DB-ROLE-SPLIT.md) §4/§10, [`SHARED-CONTRACT.md`](./SHARED-CONTRACT.md) (`### 1.0.0-rc.20`), and [`MISSION-MANIFEST.md`](./MISSION-MANIFEST.md) §10.
**Ratification lineage (authoritative):** Jason B1 ruling (2026-07-20T23:08Z) + Mos coordinator **OPTION A** (home the layer) + **Mos Q1** ruling (TIER-LEVEL) + **Mos Q2** ruling (RLS `WITH CHECK` AUTHORIZED, two mandatory conditions — `ENABLE`/`FORCE ROW LEVEL SECURITY` as the terminal migration step, and non-owner rung roles) + **Gate A** (`a11a1e2c`) + **Gate B** (`a6aac2cd`).
> **Ratification note.** The sections below are the converged v6 envelope text, retained **VERBATIM** for traceability. Their design-time framing — e.g. "**Scope:** DESIGN / CONTRACT-AMENDMENT only. No implementation code, no PRs, no SSOT edits." and "**Base:** … this doc modifies nothing there." — describes the envelope as originally authored against base `b0d78d86`. As of this PR that rc.20 amendment is **ratified and applied in-place (FORM A)** to the frozen SSOT; the design-time "no SSOT edits" wording is historical authoring context, not a live constraint on this ratified record. The Ratification lineage above is authoritative.
---
**Status:** v6 envelope (design / contract-amendment), FINAL. **v6 = v5 + N1/N2/F-NB4 non-blocking test/lint/cert hardening ONLY; NO design predicate/policy/grant/role/table change.** v5 CONVERGED — BOTH terminal re-gate-4 gates returned GO (Gate A-delta-4 `a4ce075a`; Gate B-delta-4 `a379e0de`), NO HALT; Gate B-delta-4 flagged 3 EXPLICITLY NON-BLOCKING hardening notes "for the trunk-commit record," and v6 folds exactly those 3 as additive test/lint/cert wording on an already-BOTH-GO design: **N1** (strengthen the `pg_policy.polpermissive` lint — group by effective role incl. `PUBLIC`/inheritance; reject an internally-disjunctive single `WITH CHECK`; behavioral positive-controls remain the PRIMARY proof), **N2** (state the option-(b) vacuous-deny guard invariant — ≥1 permissive policy per writable (rung, command)), and **F-NB4** (extend owner non-reachability to SET-ROLE assumability — conjoin the `MEMBER` variant of `pg_has_role`). No design predicate, policy, grant, role, or table is changed. v6 supersedes v5 (`a6e790fd`), which supersedes v4 (`a48ad69c`). Preserves EVERY verified v4 closure (all checks passed on v4 EXCEPT one bounded item) and closes the SINGLE remaining BLOCKING finding raised identically by BOTH re-gate-3 gates (Gate A-delta-3 `aa5cbf73` = CHANGES-NEEDED; Gate B-delta-3 `aa881074` = 1 blocking): the **RLS `WITH CHECK` composition semantics** — v4's construction is right but it justifies the composition with a FALSE PostgreSQL claim ("PostgreSQL requires ALL applicable `WITH CHECK` clauses to pass / they AND") and leaves the _realization_ unspecified, so an implementation could realize the status-pin, the workspace-bind, and the override as SEPARATE PERMISSIVE policies which combine with **OR** (not AND) → a foreign-workspace INSERT satisfying only the status-pin would be admitted (B2-INSERT / B-1 re-open). v5 (a) corrects the semantics, (b) **mandates** the composition be realized as a SINGLE compound `WITH CHECK` OR as `AS RESTRICTIVE` policies and **explicitly forbids** the multi-permissive-intended-to-AND realization, and (c) hardens the red-first negatives to an otherwise-valid row + adds a positive-control. It also folds Gate B-delta-3's NB-4-transitive nit (state owner non-reachability as TRANSITIVE membership). This is a PRECISE, LIGHT revision: every v4 closure (3-rung ladder / zero per-user; F1F7; GA-1/3/4; B-1/B-2/B-3; B2-INSERT workspace bind itself; N-1..N-5; AD-1/AD-2; NB-1..NB-5; continuous -06 scan + L174 GUC fold; rc.20) carries forward intact. Prior rc.18/rc.19 evidence does NOT carry.
**Authoring authority:** Jason B1 ruling (2026-07-20T23:08Z) + Mos coordinator **OPTION A** (home the layer) + **Mos Q1 ruling (TIER-LEVEL)** + **Mos Q2 ruling (RLS `WITH CHECK` AUTHORIZED, two mandatory conditions)**. No new coordinator authority is required for v5: the composition fix is a DDL-realization detail + corrected PostgreSQL-semantics statement + test-wording hardening entirely inside the already-authorized `WITH CHECK`/`FORCE RLS` mechanism (Q2); it introduces no new principal, key, or authority axis. **Both delta-3 gates independently confirmed the per-user/same-tier boundary HOLDS** (the crit-1 defect is a TENANT-ISOLATION correctness risk, NOT a per-user authority axis).
**Base:** frozen SSOT at `b0d78d86`, `/src/mosaic-stack/docs/native-kanban-sot/` (READ-ONLY; this doc modifies nothing there).
**Scope:** DESIGN / CONTRACT-AMENDMENT only. No implementation code, no PRs, no SSOT edits.
**HALT self-check result (re-run for v5): NO HALT.** No v5 change forces two SAME-TIER writers to hold DIFFERENT WRITE AUTHORITY. The v5 composition fix pins **how** the already-uniform predicates are physically composed (one compound `WITH CHECK`, or `AS RESTRICTIVE`) — it changes no predicate's _content_ and adds no writer-varying axis; both delta-3 gates confirmed this is a tenant-isolation correctness pin, not a per-user distinction. The B2-INSERT fix ANDs a **shared, per-request workspace predicate** (`workspace_id = current_setting('mosaic.workspace_id', true)::uuid`) into every rung's INSERT `WITH CHECK` — applied **uniformly** to every writer in a tier, exactly like the B-2 read/`USING` predicate; it is tenant isolation, never a per-writer distinction. The B-1 sink-resident User-override remains keyed on **(workspace_id, task_id, tier)** — task-identity + tier ONLY, **NO per-federated-writer key** — so every writer inside a given tier is subject to the _identical_ veto for a given task. The five NB fixes (override-relation no-status enumeration; two claim-scopings; owner-toggle-FORCE containment naming; baseline runtime grants for the two capability roles) introduce **zero** per-writer authority. Per-writer _attribution_ remains an AUDIT concern routed to metadata (`task_events.actor_id`), never a DB credential/role/RLS key. The design remains satisfiable with the **fixed 3-rung role ladder** (User / Orchestrator / others), federated identity → rung at authorization time. The Q1 hard re-open trigger (same-tier / different write-authority) is **not** hit. See §5.
---
## 0. What the "raw task-status sink" is (grounding — unchanged from v1/v2)
- The canonical task-status store is **`tasks.status`** (SHARED-CONTRACT §5.4 L214 "canonical authority"); legacy **`mission_tasks.status`** is frozen read-only / prohibited as a write source (SHARED-CONTRACT §5.4 L219; §5.1 phase 1 L165). `tasks` is a **mutable** relation created by **KBN-100**. Canonical statuses are `backlog | ready | in_progress | blocked | in_review | done | cancelled` (SHARED-CONTRACT §3 L134); initial creation state is `backlog` (§5.4 L214 not-started→backlog).
- The append-only event relations `task_events, artifacts, task_checkpoints, task_checkpoint_artifacts, approval_decision_artifacts` receive runtime-only `SELECT, INSERT` with `UPDATE, DELETE, TRUNCATE, REFERENCES, TRIGGER` **denied** (frozen §4 L178). That producer/consumer precedent — grants applied **after** KBN-100 creates the relations — is the mechanism this envelope extends to the mutable `tasks.status` sink and to the new sink-resident override table (§2.9).
- Today there is exactly one runtime login, `mosaic_runtime` (LOGIN INHERIT), sole member of NOLOGIN `mosaic_runtime_capability` (§4 L149-150). The sink therefore has **no writer-authority discrimination** — the root of RC19-B1-01.
The three B1 findings are writes to this sink; the two B2 findings are the privileged-fd → UID-drop handoff used by the runner/importer (§5 L184/L191). This envelope homes one coherent layer over both, now with the User-override and the SELECT/tenant predicate **resident at the sink**.
**[NB-2 / NB-3 — precise compromise-resistance scope (Mos Q1 accepted co-resident-pool residual; NO HALT).]** The prior "a compromised Gateway cannot bypass a User deny or read cross-workspace" framing is **overstated** and is corrected here. All three rung credentials are co-resident in one Gateway process (DB authenticates the _credential_, never the federated end-user — the accepted Mos Q1 residual), and the workspace GUC is _app-set_ with no per-tenant DB check. Precisely:
- **B-1 sink-enforces the User veto against the _Orchestrator-rung path_** — an Orchestrator-pool write to a task carrying an active User deny ERRORS in PostgreSQL regardless of app-layer behavior (a _buggy_ or Orchestrator-path-compromised Gateway cannot skip it). It does **NOT** stop a Gateway compromised badly enough to hold the **User** pool: User = god and may itself clear any veto (User-write-only on the override table). That is the accepted co-resident-pool residual, not a new hole.
- **B-2 + B2-INSERT sink-enforce tenant isolation against a _buggy_ Gateway** — an omitted/wrong workspace predicate fails **closed** (reads return zero rows; INSERTs ERROR on the NULL/mismatched GUC). They do **NOT** defend a _compromised_ Gateway that forges `mosaic.workspace_id` to a victim tenant, because the GUC carries no per-tenant DB authentication (accepted Mos Q1 residual). The genuine, real closure B2-INSERT delivers is the **buggy / unbound-INSERT** hole: without it, _any_ rung could write a foreign `workspace_id` on INSERT even with a correct GUC set — that is now impossible.
Defending against a fully compromised Gateway forging identity/tenant requires per-federated-user DB credentials = the HALT boundary; Mos Q1 explicitly accepts this residual and rules NO HALT. The claims in §2.9 (B-1) and §2.2/§2.4 (B-2/INSERT) are scoped to match.
---
## 1. Contract amendment (exact ownership + text changes)
### 1.1 Homing decision — one NEW owner card + minimal responsibility-widenings
The layer is genuinely **new scope**. The **minimal** closed/disjoint expansion adds **one new owner card, `KBN-101-10`**, owning two brand-new, currently-unowned globs (disjoint from every existing manifest → KBN-101-06 overlap/ownerless/path-existence still pass):
- `packages/db/src/sink-rbac/**` — B1 layer (policy artifact + schema + evaluator + ladder + connection-selection + status-UPSERT + invariant-set + view/rule-forbid enumeration).
- `packages/db/src/credential-handoff/**` — B2 library (SCM_RIGHTS + non-dumpable assert/verify).
Plus **responsibility-widenings of already-owned files** (no manifest-glob change → no overlap):
| Existing card | Already-owned path(s) touched | Widened responsibility (v4; v3 items preserved) |
| ---------------------------------------- | --------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `KBN-101-00` | `infra/pg-bootstrap/roles.sql`, `infra/pg-bootstrap/tests/**` | **[GA-1 FIX]** Create the **fixed rung-ROLES** (3 LOGIN roles + their capability roles) with **deny-by-default base attributes** and **CONNECT** grants; bootstrap catalog tests that each rung role carries the safe attributes (F2) and is **non-owner** of the sink. **[AD-1 FIX]** Amend the frozen §4 **L176 CONNECT-allowlist certification** so the two new LOGIN rungs (`mosaic_runtime_user`, `mosaic_runtime_orchestrator`) are admitted to the `CONNECT` allowlist **and asserted `TEMPORARY`-denied**; otherwise the frozen L176 cert test REDS (it fails if an app login retains TEMPORARY, and it enumerates the CONNECT allowlist). **[NB-5 FIX]** Grant the two new capability roles (`mosaic_runtime_user_capability`, `mosaic_runtime_orchestrator_capability`) the **baseline frozen §4 L176 runtime grants** they need to function: `USAGE ON SCHEMA mosaic`, `USAGE ON SCHEMA drizzle` + `SELECT ON drizzle.__drizzle_migrations, drizzle.__mosaic_migration_manifest` (ledger read), and the relevant sequence `USAGE`/`SELECT` grants — mirroring what `mosaic_runtime_capability` already holds; without these the two new rungs cannot connect/operate and would FAIL the frozen §6 L222 runtime verify (missing inherited capability/sequence/ledger grants → fail-closed). **It owns NO DDL on `tasks` or the override table** — those relations do not exist at bootstrap time, so RLS/column-grant/policy/FORCE/unique-key/trigger DDL **cannot** live here. |
| `KBN-100` (producer; SPEC target of -10) | its own `packages/db/src/schema.ts` + generated `packages/db/drizzle/**` migration + migration tests | **[GA-1 + GA-2/Q2 + B-1 + B-2 + B-3 + B2-INSERT FIX]** Homes ALL `tasks`-specific sink DDL **and the new `task_status_write_override` table DDL** **after** it creates `tasks`, per the frozen §4 L178 producer/consumer precedent: the rung column-grant matrix (F1/F7/N-3), the **RLS `WITH CHECK` write-source policies** (Q2/F1/N-3), **[B2-INSERT] the workspace-predicate `workspace_id = current_setting('mosaic.workspace_id', true)::uuid` as a conjunct of EVERY rung's INSERT enforced check on `tasks` — realized as a single compound `WITH CHECK` OR `AS RESTRICTIVE`, NEVER as separate permissive policies (v5 composition mandate, §1.4 item 3-bis) — (and thus into the UPSERT INSERT arm), and the symmetric INSERT workspace binding on `task_status_write_override`** so tenant isolation is homed at the sink on the write path too, the **per-rung `SELECT`/`UPDATE` `USING` tenant policies** (B-2), the **override-table subquery** in every `tasks` write policy (B-1), the `tasks (workspace_id,id)` **UNIQUE** candidate key (F4), the `actor_id` attribution metadata column (Q1 audit channel), the **no-status-normalizing-trigger / no-status-view / no-status-rule** invariants (F3/N-1/N-2) — **[NB-1] extended to cover the `task_status_write_override` relation as well** (no status-writing trigger/RULE/SECURITY DEFINER function on the override relation may write `tasks.status`), and — as the **TERMINAL migration step**`ENABLE`/`FORCE ROW LEVEL SECURITY` on `tasks` and on the override table, run strictly **AFTER** all expand/backfill/repair DML (B-3). **KBN-100's DDL scope statement is explicitly amended** to include RLS + the override table on `tasks` under Mos Q2 coordinator co-authorization (§1.4). Certified deployed by KBN-101-09. |
| `KBN-101-01` | `packages/db/src/connection-identity.ts`(+spec), `apps/gateway/src/database/database.module.ts`(+spec) | Provision **only the three generic rung pools** at boot; set the per-request **workspace session predicate** (B-2) on each checked-out connection. **[F2 FIX]** Extend the frozen L222 unsafe-attribute checkout assertion to **each** rung connection: effective role == authorized rung **AND** fail closed on SUPERUSER / CREATEROLE / CREATEDB / REPLICATION / **BYPASSRLS** / ownership of the sink tables (`DATABASE_ROLE_UNSAFE`). **[AD-2/N-5 FIX]** -01 **provisions pools only**; it does **not** import -10. The rung→pool SELECTION lives in -10's `connection-selection.ts`, which **consumes** -01's provisioned pools (import direction `10→01`; §2.2). |
| `KBN-101-03` | `packages/db/src/migrator/**` (glob), **`docker/db-migrator.Dockerfile`** | Consume -10's credential-handoff: **SCM_RIGHTS-only** fd acquisition + **non-dumpable re-verify** after the drop to `10003:10003`. **[GA-4(ii) FIX]** The root-only **migrator launch wrapper** homes here (the migrator image). |
| `KBN-101-05` | `apps/gateway/Dockerfile`(+spec), renderer secret matrix | Renderer mounts **three** rung DSN secrets (`mosaic-db-runtime-url` + `mosaic-db-runtime-user-url` + `mosaic-db-runtime-orchestrator-url`), each `0600 10001:10001`, Gateway-only. Gateway rung-connection selection is a **DB-role choice, not a UID drop**, so -05 owns **no** SCM_RIGHTS/dumpable behavior (moved to -03 per GA-4(ii)). |
| `KBN-101-02` | `packages/storage/src/{cli,migrate-tier}.ts`, `packages/storage/src/adapters/postgres.ts` (already-owned) | Importer privileged-fd case uses SCM_RIGHTS (extends existing safe-open, drop to `10002:10002`); status writes go through the -10 status-preserving-UPSERT helper. |
| `KBN-101-06` | `tools/ci/fixtures/kbn101-ddl-inventory.json`, `.woodpecker/ci.yml` | Add `KBN-101-10` to the inventory fixture/command matrix; add the rung-selection + SCM_RIGHTS + **per-tier RLS negatives** + **override-veto negative** + **cross-workspace read negative** + **[B2-INSERT] cross-workspace INSERT/UPSERT negative** + **NULL-GUC INSERT negative** + **owner/backfill-ordering** cases to the matrix. **[Gate A-delta-2 obs #2 / N-2 continuous — ADOPTED]** Home the **continuous no-status catalog scan as a -06 CI gate that runs on EVERY migration** (promoted from the v3 one-shot -09 recommendation): the scan asserts no status-writing trigger/RULE/`SECURITY DEFINER` function/view on `tasks` **or on `task_status_write_override`** (NB-1), so a future migration that adds a status-write path fails CI, not only the one-time cert. |
### 1.2 §7 manifest expansion (KBN-101-06 must still pass)
Add exactly one row to the §7 card table (complete, disjoint ownership universe):
> | `KBN-101-10` declarative sink-RBAC + per-role connection-selection + credential-handoff | 00,01 | **Only:** `packages/db/src/sink-rbac/**`, `packages/db/src/credential-handoff/**`. It homes: the source-controlled declarative RBAC policy artifact + schema + evaluator; the fixed **rung ladder** + federation-aware identity→rung resolver; the rung→DB-credential **connection-selection** module (consumes -01 pools; import direction `10→01`); the **status-preserving-UPSERT** contract + **invariant field-set** module + **no-status view/rule/trigger enumeration**; and the **SCM_RIGHTS** credential-handoff + **non-dumpable** assert/verify library. It creates no roles, no DDL, no Compose/CI/deploy path; it is consumed by -00 (role names), -01 (connection wiring + identity/attribute verify), -02/-03/-05 (write path + fd handoff), specified-against by **KBN-100** (sink DDL: grants/RLS/`USING`+`WITH CHECK` policies/override-table/FORCE/unique-key/trigger-view-rule-invariant on `tasks`), and certified deployed by KBN-101-09. |
Amend the §7 L245 closing sentence (added clause **bold**): "The manifests above are the complete ownership universe for KBN-101 implementation paths **including KBN-101-10**; the KBN-101-06 inventory test fails on overlap, an ownerless in-scope path, or a nonexistent declared path."
**Dependency edges (v3, unchanged from v2 — acyclic; -10 depends only on 00,01):**
- `KBN-101-10 depends on 00,01`
- `KBN-101-02 depends on 01,03,10`
- `KBN-101-03 depends on 00,01,10`
- `KBN-101-05 depends on 00,03,10`
- `KBN-101-06 depends on 02,03,05,07,10`
- `KBN-101-08 depends on 00…07,10`
- `KBN-101-09 depends on KBN-100,08` (unchanged; evidence-only)
**Why still disjoint (KBN-101-06 green — UNCHANGED from v2):** the only NEW ownership is the two -10 globs. The B-1 override table, B-2 `USING` policies, and B-3 terminal-FORCE ordering are **all producer DDL homed in KBN-100's already-owned `schema.ts`/`drizzle/**`** (a *responsibility* widening on files KBN-100 already owns, exactly like the `tasks`sink DDL). The N-1/N-5 additions live inside -10's already-declared`sink-rbac/**`glob. AD-1 amends -00's already-owned`roles.sql`/tests. **No glob is split, narrowed, or shared → KBN-101-06 overlap/ownerless/path-existence stay green.\*\*
### 1.3 §4 identity-model amendment (per-role, closed additive set — F2 hardened; AD-1 CONNECT cert)
Add these rows to the §4 role table (§4 L141-151). **Per-ROLE, not per-user.** Implements Jason's B1 (User = god; main Orchestrator near-god, User-overridable; others deny-by-default):
| Role (added) | Attributes (F2-hardened) | Membership / session use |
| ---------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `mosaic_runtime_user_capability` | `NOLOGIN`, no ownership/admin attrs. | Holds the **User (god)** rung's named grants incl. `INSERT(status)`, `UPDATE(status)` + the status-transition RLS `WITH CHECK` policy on `tasks`; **plus the SOLE `INSERT/UPDATE/DELETE` grant on `task_status_write_override`** (B-1); never granted to Orchestrator/base. |
| `mosaic_runtime_orchestrator_capability` | `NOLOGIN`, no ownership/admin attrs. | Holds the **Orchestrator (near-god)** rung's grants; identical to User **except** (i) every `tasks` write policy **subqueries** the override table and is REJECTED at the sink when an active User deny row exists for that `(workspace_id, task_id, orchestrator)` (B-1); (ii) `INSERT(status)` is pinned by `WITH CHECK` to a **non-terminal initial status** (N-3) — only User may INSERT an arbitrary/terminal status; (iii) it has only `SELECT` (never write) on the override table. Scope is tier/task-identity only (F6). |
| `mosaic_runtime_user` | **`LOGIN INHERIT NOSUPERUSER NOCREATEDB NOCREATEROLE NOREPLICATION NOBYPASSRLS`**, no ownership; **non-owner of every sink table + the override table**. | Member of `mosaic_runtime_user_capability` only (`WITH INHERIT TRUE, SET FALSE, ADMIN FALSE`); the connection-selection **User-rung** credential. **[AD-1]** Requires `CONNECT`, denied `TEMPORARY`. |
| `mosaic_runtime_orchestrator` | **`LOGIN INHERIT NOSUPERUSER NOCREATEDB NOCREATEROLE NOREPLICATION NOBYPASSRLS`**, no ownership; **non-owner of every sink table + the override table**. | Member of `mosaic_runtime_orchestrator_capability` only (same INHERIT/SET/ADMIN flags); the **Orchestrator-rung** credential. **[AD-1]** Requires `CONNECT`, denied `TEMPORARY`. |
The existing `mosaic_runtime` is confirmed already `LOGIN INHERIT` with no admin attrs (frozen L150) and non-owner; it becomes the **others / deny-by-default** base rung. On the sink it retains `SELECT` (workspace-scoped `USING`, B-2), `INSERT (explicit non-status column list)` (F1), and column-level `UPDATE` on **non-status, non-invariant, non-key, non-tenancy** columns only (F7), each `USING` workspace-scoped; it has **no** `INSERT(status)`, **no** `UPDATE(status)`, **no** `DELETE`, **no** `TRUNCATE` on `tasks`, **no** write on the override table, and RLS denies its status/override writes by default. The rung set is **fixed at three**.
**F2 note:** the three rung LOGIN roles' `NOBYPASSRLS` + `NOSUPERUSER` + **non-ownership** are load-bearing — `FORCE ROW LEVEL SECURITY` collapses for any SUPERUSER/BYPASSRLS role and RLS is silently bypassed by a table **owner**. These attributes are declared in -00 bootstrap, catalog-proven in -00 tests, and re-verified at every checkout by -01 (extended L222 assertion).
**[AD-1] CONNECT-allowlist certification amendment (frozen §4 L176):** L176 today grants `CONNECT` only to `mosaic_runtime`, `mosaic_migrator`, and the bootstrap actor, and its cert **fails if an unrelated login retains `CONNECT` or either application login retains `TEMPORARY`.** The two new LOGIN rungs are additional application logins that require `CONNECT` and must be `TEMPORARY`-denied. The -00 row + rc.20 text **explicitly amend the L176 allowlist** to `{mosaic_runtime, mosaic_runtime_user, mosaic_runtime_orchestrator, mosaic_migrator, bootstrap-actor}`, and the -00 cert asserts `TEMPORARY` denial for all three runtime rungs. Without this amendment the frozen -00 cert test REDS on the two new logins.
**rc.20 amendment text (v3 — extends v2)** — amend §4's closing note and SHARED-CONTRACT rc.5 L82/L88-90 ("Non-effect: role graph … unchanged / neither creates roles/secrets") with:
> **rc.20 (Envelope A) — authorized by Jason's declarative-RBAC B1 ruling + Mos OPTION A + Mos Q1 (TIER-LEVEL) + Mos Q2 (RLS):** adds (i) the fixed **User/Orchestrator/others** runtime rung-roles (per-ROLE, deny-by-default); each rung **LOGIN** role is `NOSUPERUSER NOCREATEDB NOCREATEROLE NOREPLICATION NOBYPASSRLS`, **NON-OWNER** of every sink/override table, **added to the §4 L176 `CONNECT` allowlist**, and **asserted `TEMPORARY`-denied**. **No per-federated-user role or credential is created** — federated identity maps to a rung **at authorization time only**, and per-writer **attribution lives in metadata columns** (app-supplied `actor_id` on `task_events`), **never** in DB credentials/roles. (ii) Two new fixed rung DSN secrets **`mosaic-db-runtime-user-url`** and **`mosaic-db-runtime-orchestrator-url`** (Gateway-only, `0600 10001:10001`), extending the frozen runtime-URL secret row. (iii) **RLS `WITH CHECK` write-source policies (including the B2-INSERT per-rung INSERT tenant binding `workspace_id = current_setting('mosaic.workspace_id', true)::uuid`, composed as a SINGLE compound `WITH CHECK` or `AS RESTRICTIVE` — never multi-permissive-intended-to-AND, per the v5 composition mandate §1.4 item 3-bis) + per-rung `SELECT`/`UPDATE` `USING` tenant policies + `FORCE ROW LEVEL SECURITY`** on the sink table `tasks` **and on the new sink-resident `task_status_write_override` table** (tighten-only, deny-by-default) — a §10 schema-v1 **mechanism addition** beyond the prior grant/revoke-only model, authorized by Mos Q2 as coordinator co-authorization with Jason's B1. **The two new rung capability roles additionally receive the baseline frozen §4 L176 runtime grants (`USAGE ON mosaic`, `drizzle` ledger `SELECT`, sequence grants) [NB-5] so they satisfy the frozen §6 L222 runtime verify.** (iv) The **`task_status_write_override` table** (keyed `(workspace_id, task_id, tier)`, no per-writer key), written only by the User rung, read by the Orchestrator/base rung `tasks` write-policy subquery, homes the User-override **at the sink**. The sink DDL (grants + RLS `USING`/`WITH CHECK` + override table + FORCE + unique key + trigger/view/rule invariants) is homed in the **KBN-100 producer** after it creates `tasks`, with `FORCE RLS` as the **terminal** migration step after all backfill/repair DML, per the §4 L178 producer/consumer precedent.
> **Frozen-invariant note (authorization basis):** touching the frozen role graph, the §4 L176 CONNECT allowlist, and adding RLS + the override table are §10 contract-change classes (MISSION-MANIFEST §10; SHARED-CONTRACT L82/L88-90; frozen §4 L176). They are authorized here by **Jason B1 + Mos OPTION A + Mos Q1 + Mos Q2**. The addition is strictly a **fixed per-ROLE** set + a **tighten-only** RLS mechanism + a **task/tier-scoped** override table. It stops exactly at the HALT boundary: **no** per-federated-user topology is introduced (§5 proof).
### 1.4 §10 mechanism amendment — RLS `WITH CHECK` (write-source + INSERT tenant-bound) + `USING` + sink-resident override at the sink (Q2), with the two mandatory conditions
The frozen write-authority model is grant/revoke-only; **RLS appears nowhere in the frozen contract.** Mos Q2 **AUTHORIZES** adding declarative, deny-by-default, **enforce-AT-THE-SINK, fail-closed** RLS policies, because grant/revoke alone cannot express (a) row-level _write-source_ enforcement (pin new-row status, F1/N-3), (b) a _sink-enforced_ User-override that the Orchestrator-rung path cannot skip (B-1), (c) _tenant-scoped reads/updates_ (B-2), or (d) **[B2-INSERT] _tenant-scoped INSERTs_ — binding `workspace_id` on the write/create path**, which neither column grants nor the `USING` read policy can do (PostgreSQL does not apply `USING` to INSERT). RLS is **tighten-only**: it can only further restrict beyond the column grants, never widen them.
**KBN-100 producer DDL scope is explicitly amended** to include, on `tasks` (and, where noted, the override table):
1. `ALTER TABLE tasks ENABLE ROW LEVEL SECURITY;` — and the same on `task_status_write_override`.
2. **CONDITION (b) — EXPLICIT DECISION: `ALTER TABLE tasks FORCE ROW LEVEL SECURITY;`** (and on the override table) — DECIDED **YES/ON**, stated explicitly and not left defaulted, so that **even the table owner (`mosaic_schema_owner`) and any maintenance connection cannot silently bypass RLS**. (Directly closes the live U-Connect failure mode where an owner connection with `rls_forced=false` silently VOIDED RLS.)
3. A **deny-all default** posture (no permissive policy) plus per-rung policies:
- **Write (`WITH CHECK`):** base/others rung → new-row `status = <initial state 'backlog'>` only, no status UPDATE; **Orchestrator rung → status INSERT pinned to a non-terminal initial status (N-3), and every write (`INSERT`/`UPDATE`/transition) additionally `WITH CHECK`'d against `NOT EXISTS(<active User deny row for (workspace_id, id, 'orchestrator')>)` (B-1)**; User rung → status INSERT (any legal status) / transition. Every write policy is `WITH CHECK` (write-source), tighten-only.
- **[B2-INSERT — tenant isolation on the write path] Every rung's INSERT `WITH CHECK` on `tasks` MUST include the workspace predicate `workspace_id = current_setting('mosaic.workspace_id', true)::uuid` as a conjunct of the _same_ enforced check that carries the status-pin (and, for the Orchestrator rung, the B-1 override subquery) — see the COMPOSITION MANDATE (item 3-bis) for the exact realization.** This is REQUIRED because PostgreSQL does **NOT** apply `USING` to INSERT, and the explicit status-pinning `WITH CHECK` **suppresses** the `USING``WITH CHECK` substitution — so in v3 `workspace_id` was **unbound on the INSERT path** and any rung could `INSERT INTO tasks (workspace_id, …) VALUES (<foreign W2>, …)` (directly or via the sanctioned status-upsert INSERT arm) into a victim workspace, defeating B-2's "tenant isolation homed at the sink" goal and evading SHARED-CONTRACT §7.1 (body workspace forbidden). **CORRECTED SEMANTICS (v5 — the delta-3 fix):** multiple **PERMISSIVE** policies for the same command combine with **OR** — a row is admitted if it satisfies **ANY** one permissive policy's `WITH CHECK`, ANDed with all `AS RESTRICTIVE` policies' checks. "All applicable `WITH CHECK` clauses must pass / they AND" is **FALSE as a general rule** and is struck from this envelope; the AND-conjunction the B2-INSERT closure depends on holds ONLY (a) _within a single policy_ whose `WITH CHECK` is one AND-conjoined expression, or (b) _across `AS RESTRICTIVE` policies_. Realizing the status-pin, the workspace-bind, and the override as separate _permissive_ policies would let a foreign-workspace INSERT that satisfies only the status-pin be admitted by OR — re-opening B2-INSERT/B-1 — which item 3-bis forbids. **Fail-closed on NULL:** an absent GUC makes `current_setting(...,true)` return NULL, the equality yields NULL (not true), and — provided the predicate is a conjunct of the single enforced check (item 3-bis) — the INSERT **ERRORS at the sink**. The **same INSERT workspace binding is applied symmetrically to `task_status_write_override`**, under the identical composition mandate (its own single compound `WITH CHECK` or `AS RESTRICTIVE`).
- **Read/lock (`USING`) — B-2:** per-rung `SELECT` and `UPDATE` `USING` policies **workspace-scoped** by the session predicate `workspace_id = current_setting('mosaic.workspace_id', true)::uuid` (set per request by -01, §2.2). **`USING(true)` is explicitly FORBIDDEN** — tenant isolation rides on this predicate; a lazy `USING(true)` would expose every workspace's rows to every rung and is a fail-closed contract violation caught by a negative test.
- **3-bis. [v5 — LOAD-BEARING] COMPOSITION MANDATE (how the sink check MUST be realized; both delta-3 gates prescribed it).** Per rung, per command (INSERT / UPDATE / transition), the sink enforcement of the status-pin **AND** the workspace-bind **AND** (Orchestrator rung only) the User-override subquery MUST be realized as EITHER:
- **(a) a SINGLE policy** whose `WITH CHECK` is the _full conjunction_`<status-pin> AND workspace_id = current_setting('mosaic.workspace_id', true)::uuid` and, for the Orchestrator rung, `AND NOT EXISTS(<active User-override deny row for (workspace_id, id, 'orchestrator')>)`; **OR**
- **(b)** the status-pin as the **SOLE PERMISSIVE** policy for that command, **plus** the workspace-bind and (Orchestrator) the override subquery authored **`AS RESTRICTIVE`** (restrictive policies AND with everything, so a row must pass every restrictive check _and_ at least one permissive check).
- **EXPLICITLY FORBIDDEN:** realizing the status-pin, the workspace-bind, and the override as **multiple _permissive_ policies whose predicates are intended to AND.** Permissive policies combine with **OR**, so that realization admits a foreign-workspace INSERT that satisfies only the status-pin (cross-tenant injection) and equally bypasses the B-1 override — exactly the delta-3 defect. This mandate applies to the `tasks` INSERT/UPDATE/transition policies **AND** to the `task_status_write_override` table's own policies (same single-compound-`WITH CHECK`-or-`AS RESTRICTIVE` requirement; never multi-permissive-intended-to-AND). The producer DDL and the -09/-06 certification assert the realized policies are either single-compound or `AS RESTRICTIVE` (never separate permissive policies expected to AND) — a lint/catalog check over `pg_policy.polpermissive` backs it.
- **[v6 — N1: strengthened `pg_policy.polpermissive` lint (supplementary, NOT sufficient).]** The `pg_policy.polpermissive` lint (continuous -06 scan, §2.7) MUST (a) count permissive policies **grouped by EFFECTIVE role — including `PUBLIC` and role inheritance**, not merely by the policy's named `TO` role: a `TO PUBLIC` (or inherited-through-a-capability-role) permissive policy applicable to a rung's command is counted **alongside** that rung's status-pin, because at evaluation time it OR-widens exactly like a second same-role permissive policy — so a `TO PUBLIC` permissive next to a rung's status-pin is a **lint failure**; and (b) ideally **reject an internally-disjunctive single `WITH CHECK`** — a single policy whose expression is itself `status_pin OR workspace_bind` satisfies the naive one-permissive-policy count yet violates the composition mandate exactly as two permissive policies would, so the lint should detect a top-level `OR` between the status-pin and the workspace-bind/override conjuncts within one policy expression. **The lint is SUPPLEMENTARY, not sufficient:** the **behavioral positive-controls remain the PRIMARY proof** of correct composition (dropping the workspace conjunct — resp. the override subquery — alone flips the hardened negative to a PASS; §2.4 clause 5 / §2.9). The lint backstops the catalog shape; it does not replace the behavioral proof.
- **[v6 — N2: option-(b) vacuous-deny guard invariant — ≥1 permissive per writable (rung, command).]** Under option (b) (status-pin as the **sole PERMISSIVE** policy + workspace-bind/override `AS RESTRICTIVE`), PostgreSQL admits a row only if **≥1 PERMISSIVE `WITH CHECK` is TRUE AND all RESTRICTIVE checks pass** — so if a writable (rung, command) has **zero** permissive policies, every restrictive-only evaluation denies and the command **over-denies / fails closed** (nothing can ever be written). Therefore there **MUST be ≥1 PERMISSIVE policy per writable (rung, command)** — this "≥1 permissive per writable (rung, command)" is the **load-bearing option-(b) guard invariant** (the workspace-bind/override being restrictive is safe _only_ because the status-pin supplies the required permissive). It is verified by the mandated **"same-workspace valid-status write succeeds" positive tests** for each writable command — INSERT, transition/UPDATE, and SELECT — which would FAIL (spurious over-deny) if the permissive were dropped, and by the N1 lint confirming exactly one permissive status-pin (grouped by effective role) is present per writable (rung, command).
4. **B-3 migration ordering (TERMINAL FORCE):** all expand/backfill/repair DML on `tasks` (SHARED-CONTRACT §5.1 phase-3 L165-171, §5.3 L188) runs **FIRST**, while `tasks` has RLS **disabled**; the `ENABLE`/`FORCE ROW LEVEL SECURITY` statements are the **TERMINAL** DDL of the activation migration. **No standing `mosaic_schema_owner` ALLOW write policy exists after activation** (that would recreate the unconstrained owner write path = U-Connect mode). Any schema_owner maintenance policy, if ever needed, is **migration-only** and must be proven **unreachable by the 3 runtime LOGIN roles** + a negative test.
**CONDITION (a):** every runtime per-role LOGIN role (`mosaic_runtime`, `mosaic_runtime_user`, `mosaic_runtime_orchestrator`) **MUST remain NON-OWNER** of every sink table **and the override table** (owner stays `mosaic_schema_owner`). Enforced by -00 bootstrap catalog assertion + re-verified at each checkout by -01 (F2). A rung role that owns a sink/override table is a fail-closed contract violation.
**Break-glass boundary (B-3, named explicitly in KBN-101-09 cert):** the ONLY identities that legitimately bypass `FORCE RLS` are the sanctioned break-glass actors named in the frozen contract — `mosaic_extension_owner` (`NOLOGIN SUPERUSER`, §4.1 L146) and the external platform bootstrap actor (superuser, §4/§7; §9 L266 residual authority). Both are `NOLOGIN`/external, carry no runtime credential, and are outside application containment. The -09 cert **names these two as the accepted, audited boundary** — not a new hole — and asserts the three runtime LOGIN rungs are NOT superuser/NOT BYPASSRLS and cannot assume either.
**[NB-4 — owner-toggle-FORCE containment is NON-REACHABILITY, not incapability.]** The -09 break-glass enumeration must **additionally name `mosaic_schema_owner`** (frozen §4 L145, `NOLOGIN NOSUPERUSER NOCREATEDB NOCREATEROLE NOREPLICATION NOBYPASSRLS`) as a **contained-but-CAPABLE** path. A table **owner** — regardless of `NOSUPERUSER`/`NOBYPASSRLS` — can `ALTER TABLE tasks DISABLE ROW LEVEL SECURITY` or `ALTER TABLE tasks NO FORCE ROW LEVEL SECURITY` and then write freely; `FORCE`/`NOBYPASSRLS` do **not** make an owner incapable. So the containment claim is **NOT** "the owner cannot bypass RLS" — it is **"the owner role is NOT REACHABLE from the three runtime LOGIN rungs."** The -09 cert asserts this non-reachability **explicitly**: (i) `mosaic_schema_owner` is `NOLOGIN` (no direct credential); (ii) it is reachable only via `mosaic_migrator`'s `SET ROLE` (frozen §4 L147, migrator runner-before-Gateway, `max:1` locked migration session), which is **not** one of the three runtime rungs and holds no runtime pool; (iii) none of `mosaic_runtime` / `mosaic_runtime_user` / `mosaic_runtime_orchestrator` is a member of, or can `SET ROLE` to, `mosaic_schema_owner` or `mosaic_migrator` (catalog-proven at -00, re-verified at every checkout by -01's L222 non-owner/assumability assertion). **[NB-4-transitive — Gate B-delta-3 nit, folded]** the non-reachability assertion in the -09 cert MUST be stated as **TRANSITIVE membership**, not merely the absence of a **direct** `pg_auth_members` row: PostgreSQL's ownership/privilege check returns true for an **inherited** membership reached _through a capability role_, which would confer the owner's `ALTER TABLE … NO FORCE` / `DISABLE ROW LEVEL SECURITY` right. The assertion therefore uses the recursive predicates — `NOT pg_has_role('mosaic_runtime_user', 'mosaic_schema_owner', 'USAGE')` (and the same for each runtime rung against `mosaic_schema_owner` and `mosaic_migrator`), equivalently `NOT has_privs_of_role(<rung>, 'mosaic_schema_owner')` — which fold in transitive/inherited membership through any intermediate (capability) role. The frozen §6 L222 fail-closed-on "owner/migrator identity **or assumability**" check plausibly already covers this transitive case; v5 states "**transitive membership**" **explicitly** in the -09 cert assertion so no implementation reads "non-member" as only the direct-edge check. **[v6 — F-NB4: cover SET-ROLE assumability, not only inherited privilege.]** The v5 predicates `NOT pg_has_role(<rung>, 'mosaic_schema_owner', 'USAGE')` / `NOT has_privs_of_role(<rung>, 'mosaic_schema_owner')` capture the **inherited-privilege** axis, but a membership granted `WITH INHERIT FALSE, SET TRUE` yields `has_privs_of_role = FALSE` **yet still allows `SET ROLE mosaic_schema_owner`** followed by `ALTER TABLE … NO FORCE ROW LEVEL SECURITY` — an assumability path the `USAGE`/`has_privs_of_role` predicate alone does **not** close. The -09 cert assertion therefore **conjoins the `MEMBER` variant**`NOT pg_has_role(<rung>, 'mosaic_schema_owner', 'MEMBER')` **AND** `NOT pg_has_role(<rung>, 'mosaic_migrator', 'MEMBER')` — for **each of the three runtime rungs** (`mosaic_runtime`, `mosaic_runtime_user`, `mosaic_runtime_orchestrator`); the `MEMBER` privilege captures SET-ROLE reachability **regardless of `INHERIT`**, so `pg_has_role(rung, owner, 'USAGE')` **AND** `pg_has_role(rung, owner, 'MEMBER')` are asserted false together. This is stated explicitly rather than leaning on "frozen §6 L222 assumability plausibly covers it." (The intended role graph is already safe — per envelope §1.3 + frozen §4 L150, each rung is a member of **only** its own capability role, `WITH INHERIT TRUE, SET FALSE, ADMIN FALSE`, and of no owner/migrator role — so no live `SET TRUE` edge to the owner/migrator exists; this fold is **assertion-completeness hardening, not a live hole.**) The owner's toggle-then-write capability is therefore real but confined to the migrator/owner phase (frozen §5, runner-before-Gateway), never to a live runtime rung — that is the containment, stated as **transitive non-reachability** rather than incapability.
**Red-team requirement (baked into §3/§4; v5-HARDENED so the negatives cannot false-green under the OR-trap):** each per-rung policy is proven **deny-by-default via NEGATIVE tests PER TIER** in real PostgreSQL:
- an unauthorized write-source (base terminal-status INSERT/transition) that **ERRORS**;
- **[B2-INSERT — hardened] a cross-workspace INSERT/UPSERT using a row OTHERWISE VALID for the rung** — a status the rung's pin **ACCEPTS** (e.g. **base rung + `status='backlog'`**, and no active override), differing from a passing insert ONLY in `workspace_id = <foreign W2>` — so the rejection **isolates the workspace bind** (not a status/override rejection). Tested both directly and via the status-upsert INSERT arm; it **ERRORS at the sink**. **Positive-control:** removing the workspace-bind conjunct **alone** (leaving the status-pin) flips this negative to a **PASS**, proving the _workspace bind_, not the status-pin, is doing the rejecting (and proving the composition is a single enforced conjunction / RESTRICTIVE, not an OR of separate permissive policies).
- **[B-1 — hardened] an Orchestrator write vetoed by an active User-override deny row, using a transition the Orchestrator pin OTHERWISE ACCEPTS** (a legal non-terminal transition on `(W, T)` that would succeed absent the deny row) — so the rejection **isolates the override subquery**. It **ERRORS at the sink**. **Positive-control:** removing the override subquery **alone** flips this negative to a **PASS**, proving the _override_, not the status-pin/workspace-bind, is doing the rejecting.
- a **cross-workspace `SELECT` under the wrong/absent session predicate** → zero rows;
- **[B2-INSERT] a NULL-GUC INSERT** (absent `mosaic.workspace_id`) that **ERRORS** (fail-closed on NULL);
- a **same-workspace INSERT with a valid status** that **succeeds** (proving the predicate composes with F1/N-3 and does not over-deny);
- an **owner/maintenance connection attempting a post-activation `tasks` write** that must **fail under `FORCE`**.
All rejections **ERROR at the sink**. The two positive-controls are load-bearing: they are the direct proof that the composition is single-compound-`WITH CHECK`-or-`AS RESTRICTIVE` (delta-3), because under the forbidden multi-permissive-OR realization the hardened cross-workspace / override negatives would already PASS (false-green) without removing anything.
---
## 2. Design
### 2.1 Declarative RBAC policy (source-controlled artifact + schema)
- **Artifact:** `packages/db/src/sink-rbac/policy.task-status.v1.ts` — a source-controlled, versioned, deny-by-default policy object; the single source of truth for "who may write the sink." No policy lives in the database except the _derived_ GRANT/RLS state the KBN-100 producer emits from it.
- **Schema:** `packages/db/src/sink-rbac/policy.schema.ts` — a typed schema (zod/TS) validating: `rungs` (exactly the fixed ladder), per-rung `allow`/`deny` verbs (`insert`, `insert:status`, `update:<column-set>`, `transition:status`, `delete` — always deny), `federationMap` (federated-identity-claim → **rung**, never a role/credential per user), and `userOverride`.
- **[F6 FIX] `userOverride` scope guardrail:** the schema **constrains** override predicates to **tier- + task-identity (row) scope `(workspace_id, task_id, tier)`** and **explicitly FORBIDS** a per-federated-writer identity as an override or authority key (the Q1 re-open trigger). A policy that names a per-federated-writer identity as an override/authority key **fails schema validation** (fail-closed at load). The override is realized as the sink-resident `task_status_write_override` table (§2.9), NOT as app-resolved state. Per-writer attribution is not an authority input; it flows only to the audit metadata column (§2.6).
- **Evaluator:** `packages/db/src/sink-rbac/evaluate.ts` — pure `(identityClaims, requestedWrite) → Decision ∈ {allow(rung), deny(reason)}`, **deny-by-default**, tier resolution only. It **only chooses a rung**; it never mints credentials and — critically (B-1) — **it is NOT the enforcement point for the User-override**: the override is enforced in PostgreSQL by the write-policy subquery, so a compromised Gateway that skips the evaluator still cannot bypass a User deny.
- **Ladder / federation-awareness:** `packages/db/src/sink-rbac/ladder.ts` — fixed order `User > Orchestrator > others` and `resolveRung(federatedIdentity) → rung` at authorization time (Q1: federation-awareness lives in the resolver; the sink authorizes by the writer's declared ROLE/tier).
- **Enforcement is AT THE SINK, fail-closed:** the evaluator's rung choice only selects _which pre-provisioned connection_ is used; PostgreSQL then enforces via (a) revoked base privileges, (b) column-level `INSERT(status)`/`UPDATE(status)` granted only to the User/Orchestrator capabilities, (c) `FORCE ROW LEVEL SECURITY` + deny-all default + per-rung `WITH CHECK` policies **including the override subquery** (B-1), (d) per-rung `SELECT`/`UPDATE` `USING` **workspace predicate** (B-2, tenant isolation at the sink), (e) `DELETE/TRUNCATE` revoked from all runtime rungs, and (f) `UPDATE` on key/tenancy/invariant columns revoked from **all** rungs (F7). A mis-authorized OR override-vetoed OR cross-workspace write **fails closed in PostgreSQL** because the connection's effective DB role lacks the privilege or the policy predicate rejects the row — caller discipline is not trusted.
### 2.2 Per-role credential + connection-selection model (NO per-user roles; import direction pinned)
```
federated writer ──(verified identity claim)──► evaluate.ts ──► rung (one of 3, fixed)
connection-selection.ts ───┤ selects the PRE-PROVISIONED
(in -10; consumes -01) │ pool for that rung; sets
│ SET LOCAL mosaic.workspace_id
mosaic_runtime_user | mosaic_runtime_orchestrator | mosaic_runtime
└── PostgreSQL enforces grants + FORCE RLS WITH CHECK(+override) + USING(workspace) ──┘
```
- **`packages/db/src/sink-rbac/connection-selection.ts`** maps `rung → pooled connection`. **Exactly three** connection pools, one per rung, each opened with that rung's **fixed** mounted-secret DSN. Credentials = rungs = **3**, constant.
- **[AD-2/N-5 — import direction pinned]** `-01` (`database.module.ts`) **provisions ONLY the three generic rung pools** at boot and knows nothing of rung _selection_. The rung→pool SELECTION lives entirely in **-10's `connection-selection.ts`**, which is **consumed by the write-path cards (`-02`/`-03`/`-05`, which already `depends on 10`)** and is **NEVER imported by `-01`**. Direction is therefore `10 → 01` (10's selection consumes 01's pools) — **acyclic; there is no `01 → 10` cycle.** An impl lane must not read "01 wires 10" as `01 → 10`.
- **[B-2 — workspace session predicate]** on checkout, before the first query, -01 sets `SET LOCAL mosaic.workspace_id = <verified request workspace>` inside the request transaction; the per-rung `USING` policies **and the B2-INSERT `WITH CHECK`** read `current_setting('mosaic.workspace_id', true)::uuid`. The predicate is set from the **server-verified** tenant context, never from a body field (SHARED-CONTRACT §7.1 "Body workspace fields are forbidden"). A missing/invalid setting → `USING` yields no rows (fail-closed read) and INSERT `WITH CHECK` yields NULL→ERROR (fail-closed write), never cross-workspace exposure.
- **[NB-3 — compromise scope]** the GUC is **app-set with no per-tenant DB authentication**, so B-2 (reads/UPDATE `USING`) and B2-INSERT protect a **buggy** Gateway (omitted/wrong predicate → fail-closed), NOT a **compromised** Gateway that forges `mosaic.workspace_id` to a victim tenant (accepted Mos Q1 co-resident-pool residual; defending it requires per-user DB creds = the HALT boundary). The genuine closure B2-INSERT delivers is the **buggy / unbound-INSERT** hole: even with a correct GUC, v3 left `workspace_id` unbound on INSERT so any rung could stamp a foreign workspace — now impossible.
- **[Gate B 2(a) — poisoned-pool / SET LOCAL negative]** the `mosaic.workspace_id` GUC is folded into the frozen L174 **poisoned-pooled-session-reset + transaction `SET LOCAL` restoration** negative test (which already covers `search_path`): a negative proves `mosaic.workspace_id` is **transaction-scoped via `SET LOCAL`** (mirroring the frozen `SET LOCAL search_path` discipline, §4 L152) so a pooled connection **cannot leak a stale workspace** across requests — an un-reset/poisoned session fails closed, and a committed/rolled-back transaction does not carry the prior request's workspace.
- **[F2 FIX] Identity + attribute verification at checkout** (`connection-identity.ts`): verify effective role == the rung's expected DB role **AND** assert the safe attributes — fail closed (`DATABASE_ROLE_UNSAFE`) on SUPERUSER, CREATEROLE, CREATEDB, REPLICATION, **BYPASSRLS**, or ownership of any sink/override table. Extends the frozen L222 check to **each** rung connection.
- **No per-federated-user role/credential:** a new federated user needs **zero** new DB roles/credentials/secrets/pools — it resolves to an existing rung. Federation growth is O(1) in DB-role count.
### 2.3 Status-preserving UPSERT on a stable natural key (F4 bound)
- **`packages/db/src/sink-rbac/status-upsert.ts`** — the single sanctioned refresh helper: `INSERT INTO tasks (natural_key…, <mutable non-status cols…>) VALUES (…) ON CONFLICT (<natural key>) DO UPDATE SET <mutable non-status cols…>`**`status` is never in the `DO UPDATE SET` list**, so a refresh preserves existing `status` byte-for-byte.
- **[B-2 dependency]** the `ON CONFLICT` conflict-probe/refresh reads the existing row; under `FORCE RLS` this requires the per-rung `SELECT`/`UPDATE` `USING` policy to admit the row (workspace-scoped). Without B-2's `USING` policies the probe would see zero rows and the UPSERT would spuriously INSERT-conflict or mis-refresh — B-2 is what makes the sanctioned UPSERT actually work post-activation.
- **[B2-INSERT dependency]** the UPSERT's **INSERT arm** is a `tasks` INSERT and is therefore subject to the B2-INSERT workspace `WITH CHECK` (§1.4/§2.4): a status-upsert that supplies a foreign `workspace_id` (or runs under an absent GUC) **ERRORS at the sink**, closing the sanctioned-UPSERT INSERT path that v3 left tenant-unbound. The helper sets `workspace_id` from the server-verified tenant context (never a body field, §7.1); it matches the session GUC by construction.
- **[F4 FIX] Natural key:** the tenant-scoped stable identity **`(workspace_id, id)`**. No frozen `tasks(workspace_id,id)` unique key exists (only `missions_workspace_id_uidx`, rc.4 L94) — `ON CONFLICT` against a nonexistent unique index is a **HARD planning error** that fails loud. Therefore **KBN-100 MUST create `tasks (workspace_id,id)` UNIQUE** (aligned to the SI-001 pattern, SHARED-CONTRACT §5.2). **Fallback:** if KBN-100 keys `tasks` on global `id` only, the UPSERT natural key **re-binds to `(id)`** (spec-level, no HALT).
- **Status changes only via an authorized transition:** `transitionStatus()` guarded by (a) the rung's `transition:status` policy verb, (b) `UPDATE(status)` present only on User/Orchestrator capabilities, (c) the RLS `WITH CHECK` transition policy (admits the update only as an explicit transition, never as a refresh/grant side effect) **plus the override subquery for the Orchestrator rung (B-1)**, and (d) the `USING` workspace predicate to see/lock the row (B-2). `DELETE` is denied to all runtime rungs → **DELETE + default-INSERT status reset is impossible at the sink** (fails closed on the DELETE).
### 2.4 INSERT-time status constraint (F1 BLOCKING + N-3 hardening)
Base `mosaic_runtime` must not stamp an arbitrary terminal status at creation, **and the Orchestrator rung must not create a task directly at a terminal status** (N-3). Layered fail-closed controls (homed in the KBN-100 producer DDL):
1. **Column-privilege:** `GRANT INSERT (<explicit non-status column list>) ON tasks TO mosaic_runtime_capability;` — the base rung **cannot name `status` on INSERT**, so `status` takes its column **DEFAULT** (`backlog`). `INSERT(status)` is granted **only** to the User/Orchestrator capabilities.
2. **RLS `INSERT … WITH CHECK` (base):** pins **new-row `status = 'backlog'`** for the base rung.
3. **[N-3 FIX] RLS `INSERT … WITH CHECK` (Orchestrator):** pins the Orchestrator rung's new-row status to a **non-terminal initial status** (`status IN {backlog, ready}`; never `done`/`cancelled`/`in_review`). **Only the User (god) rung may INSERT an arbitrary/terminal status.** This prevents an Orchestrator creating a task directly at `done`/`cancelled`, skipping the transition/lease/review trail (SHARED-CONTRACT §7.2 `POST /tasks` L272 vs `POST /tasks/:taskId/transition` L275). The exact terminal/non-terminal set is bound by KBN-100 to the §3 L134 vocabulary.
4. **[B2-INSERT FIX] RLS `INSERT … WITH CHECK` (EVERY rung, tenant binding):** every rung's INSERT sink-check MUST include `workspace_id = current_setting('mosaic.workspace_id', true)::uuid` as a conjunct of the **same enforced check** that carries the status-pin (base=`backlog`, Orch=non-terminal, User=any) **and** the B-1 override subquery on the Orchestrator rung. `status` is bound by clause 2/3; `workspace_id` is bound by this conjunct; they are orthogonal columns so there is no conflict. **[v5 — corrected PostgreSQL semantics; the delta-3 fix]** the earlier justification "because PostgreSQL requires all applicable `WITH CHECK` clauses to pass, they AND" is **FALSE and is struck**: multiple **PERMISSIVE** policies for a command combine with **OR** (a row is admitted if it satisfies **ANY** one permissive `WITH CHECK`, ANDed with all `AS RESTRICTIVE` checks). The AND this closure needs holds ONLY (a) within a _single_ policy whose `WITH CHECK` is one AND-conjoined expression, or (b) across `AS RESTRICTIVE` policies. **Fail-closed on NULL:** an absent GUC → NULL equality → INSERT ERRORS — _provided_ the workspace conjunct sits in the single enforced check (clause 5). This closes the v3 gap where the explicit status `WITH CHECK` suppressed the `USING``WITH CHECK` substitution, leaving `workspace_id` unbound on INSERT and allowing any rung to INSERT into a foreign workspace.
5. **[v5 COMPOSITION MANDATE — LOAD-BEARING] Single-compound-`WITH CHECK`-or-`AS RESTRICTIVE`, never multi-permissive-intended-to-AND.** The status-pin (clause 2/3), the workspace-bind (clause 4), and (Orchestrator) the B-1 override subquery MUST be realized per rung, per command as EITHER **(a)** a **SINGLE policy** whose `WITH CHECK` is the full conjunction `<status-pin> AND workspace_id = current_setting('mosaic.workspace_id', true)::uuid [AND NOT EXISTS(<active override deny row>)]`; **OR (b)** the status-pin as the **sole PERMISSIVE** policy **plus** the workspace-bind and (Orchestrator) the override authored **`AS RESTRICTIVE`**. Realizing them as **multiple _permissive_ policies intended to AND is EXPLICITLY FORBIDDEN** — permissive policies OR, so a foreign-workspace INSERT satisfying only the status-pin (e.g. base + `backlog` + foreign `workspace_id`) would be OR-admitted, re-opening B2-INSERT, and an override-vetoed Orchestrator write satisfying the status-pin would likewise slip the B-1 veto. The **identical mandate applies to `task_status_write_override`'s own policies** (single compound `WITH CHECK` or `AS RESTRICTIVE`; never multi-permissive-AND). A `pg_policy.polpermissive` catalog check + the hardened positive-control negatives (below) prove the realization. **[v6 — N1]** the `pg_policy.polpermissive` check is strengthened to count permissive policies **grouped by EFFECTIVE role (including `PUBLIC` and role inheritance)** — a `TO PUBLIC`/inherited permissive applicable to a rung's command counts alongside its status-pin — and to **reject an internally-disjunctive single `WITH CHECK`** (a lone policy whose expression is `status_pin OR workspace_bind` OR-widens exactly like two permissive policies); the lint is **supplementary, and the behavioral positive-controls below remain the PRIMARY proof** (§1.4 item 3-bis). **[v6 — N2]** because option (b) admits a row only when **≥1 permissive `WITH CHECK` is TRUE AND all restrictive checks pass**, there **MUST be ≥1 PERMISSIVE policy per writable (rung, command)** — the load-bearing option-(b) vacuous-deny guard — else the command over-denies (fails closed); this invariant is proven by the mandated "same-workspace valid-status write succeeds" positive tests (INSERT, transition/UPDATE, SELECT).
**Red-first tests (v5-hardened):** base-rung `INSERT … status='done'` **ERRORS** (column-privilege and/or RLS); base-rung INSERT omitting status succeeds at DEFAULT; **Orchestrator `INSERT … status='done'` ERRORS (N-3)**, Orchestrator `INSERT … status='backlog'` succeeds; User-rung `INSERT … status='done'` succeeds. **[B2-INSERT — hardened valid-status form]** the cross-workspace negative uses a row **otherwise valid for the rung****base rung + `status='backlog'` + `workspace_id=<foreign W2>`, no active override** — differing from a passing insert ONLY in `workspace_id`, so the **ERROR** isolates the _workspace bind_ (not a status/override rejection); tested direct and via the status-upsert INSERT arm. **Positive-control:** removing the workspace-bind conjunct **alone** flips this to a **PASS** (proving the bind — not the status-pin — rejects, hence a single enforced conjunction / RESTRICTIVE, not a permissive OR). An INSERT under an absent `mosaic.workspace_id` GUC **ERRORS** (fail-closed on NULL); a same-workspace INSERT with a valid status **succeeds** (predicate composes with F1/N-3, does not over-deny).
### 2.5 Invariant / key / tenancy field-set (F7 — closure)
- **`packages/db/src/sink-rbac/invariant-set.ts`** defines the invariant field-set = every sink column NOT in the explicitly-mutable set: `{ natural-key columns (workspace_id, id), status, creation/tenancy columns, immutable metadata }`. The complement (title, tags-normalized, due_at, rank, …) is the mutable set the UPSERT `DO UPDATE` touches.
- **[F7 FIX] `UPDATE` on key/tenancy/invariant columns (`workspace_id`, `id`, tenancy columns) is REVOKED from ALL rungs — including User and Orchestrator.** A re-key would otherwise **launder status** (relocate a row into a new identity to escape the status invariant). Column-level `UPDATE` grants for every rung exclude these columns.
- **Grant/data-plane disjointness (RC19-B1-03):** grants are declarative + physically separate from the data plane, so a grant/policy mutation writes **zero** `tasks` rows. A test applies an arbitrary policy/grant delta → asserts **zero byte delta** on any `tasks` row (closes RC19-B1-03 by construction).
### 2.6 Per-writer attribution → metadata column (Q1 audit channel)
- Per-writer attribution (which federated writer inside a tier acted) is an **AUDIT** concern, not an authority concern. It is captured as an **app-supplied `actor_id` metadata column on `task_events`** (append-only, already runtime `INSERT/SELECT`-only per frozen §4 L178). KBN-100 owns the column; the application supplies the verified federated-writer identity as **data**.
- It is **never** a DB credential, role, or RLS predicate key, and (per F6) never a `userOverride`/authority key (the override is keyed on task+tier, not writer). This keeps "which writer acted" auditable **without** a per-writer DB topology — i.e. without hitting the Q1 HALT trigger.
### 2.7 No-status-write via trigger / view / rule (F3 BLOCKING + N-1 + N-2)
Column-level `UPDATE(status)` alone does not close every indirect status-write path. Three complementary invariants (homed in KBN-100 producer; enumerated by -10's `invariant-set.ts`/no-status enumeration; certified deployed by KBN-101-09):
- **F3 — no status-normalizing trigger on `tasks`:** no trigger on `tasks` writes/normalizes `status` outside the sanctioned `transitionStatus()` path. The transition path is the **only** status writer.
- **[N-2 FIX + NB-1 + continuous-scan ADOPTED] Complete no-status-write enumeration (F3 completeness):** the cert asserts, in addition to F3: **no AFTER trigger** on `tasks` executing `UPDATE tasks SET status`; **no `SECURITY DEFINER` function** that writes `tasks.status` (frozen §4 L176 already forbids `SECURITY DEFINER` unless a separately reviewed exception — cited as the backstop); **no trigger on a RELATED runtime-writable table** (`task_events` and the other §4 L178 relations) that writes `tasks.status`; and **no `CREATE RULE`** on `tasks`. **[NB-1]** The enumeration is **extended to the `task_status_write_override` relation**: no status-writing trigger / RULE / `SECURITY DEFINER` function on the override relation may write `tasks.status` (the override table is runtime-writable by the User rung, so it is exactly the class §2.7's related-table clause must cover). The enumeration **scans function BODIES** (`pg_proc.prosrc` / dependency graph) for `tasks.status` writes, not merely `pg_trigger` rows. **[Gate A-delta-2 obs #2 — ADOPTED, not just recommended]** this enumeration is now a **continuous -06 CI catalog scan run on EVERY migration** (promoted from the v3 one-shot -09 recommendation), so a _later_ migration that adds a status-writing trigger/function/rule/view on `tasks` **or** on `task_status_write_override` **fails CI**, not only the one-time -09 cert. The -09 deployed cert still records the final deployed proof; the -06 scan is the standing gate. **[v6 — N1]** the same continuous -06 scan family carries the **strengthened `pg_policy.polpermissive` composition lint** (§1.4 item 3-bis / §2.4 clause 5): on every migration it counts permissive policies **grouped by effective role — including `PUBLIC` and role inheritance** (a `TO PUBLIC`/inherited permissive applicable to a writable command counts alongside that rung's status-pin) and flags an **internally-disjunctive single `WITH CHECK`** (`status_pin OR workspace_bind` within one policy). This lint is **supplementary**: it backstops the catalog shape but is **not sufficient** — the behavioral positive-controls (§2.4 clause 5 / §2.9) remain the PRIMARY proof that the composition is single-compound/`AS RESTRICTIVE` rather than an OR of permissive policies.
- **[N-1 FIX] no status-write via VIEW or RULE:** forbid any `VIEW` or `RULE` on `tasks` that yields a status-write path. An owner-owned view runs with the owner's privileges (`security_invoker = false` by default), side-stepping rung grants; an `ON INSERT/UPDATE DO INSTEAD` rule rewrites a write onto `tasks` similarly. The `invariant-set.ts` enumeration **forbids such views/rules OR mandates `security_invoker = true` + ZERO runtime grants** on any view over `tasks`. A negative test proves a rung cannot mutate `tasks.status` through any view/rule.
### 2.8 B2 — SCM_RIGHTS credential-acquisition + non-dumpable re-verify (F5 hardened — unchanged from v2)
- **`packages/db/src/credential-handoff/scm-rights.ts`** — the privileged process (the **-03 migrator launch wrapper**) opens the privileged fd (attestation signing key, §5 L191) **before** the UID drop and passes the **descriptor itself** over a Unix-domain socket via an `SCM_RIGHTS` ancillary message. The receiver **never** re-opens `/proc/self/fd/N` (the kernel re-checks permission and returns EACCES after the drop — the FD5 failure). `O_CLOEXEC` managed explicitly on both ends.
- **[F5 FIX] Socket authentication:** the transfer socket **MUST** be a `socketpair()` created **pre-fork** (no filesystem socket) **OR** a filesystem socket in a `0700` directory verified with **`SO_PEERCRED`** (assert peer UID/GID/PID). The receiver sets **`MSG_CMSG_CLOEXEC`** on `recvmsg`. An unauthenticated/anonymous peer is rejected fail-closed.
- **`packages/db/src/credential-handoff/process-hardening.ts`** — after **every** credential/UID transition, re-assert `prctl(PR_SET_DUMPABLE, 0)` and **verify** `prctl(PR_GET_DUMPABLE) == 0`; treat `dumpable != 0` as a fail-closed abort.
- **Consumers:** KBN-101-03 migrator wrapper (drop to `10003:10003`), KBN-101-02 importer (privileged-fd case, drop to `10002:10002`). The runtime rung-connection selection is a **DB-role choice, NOT an OS-UID transition** — no new setuid surface, no dumpability handling, reinforcing the no-per-user / no-new-privilege-drop property.
### 2.9 [B-1 FIX] Sink-resident, task/tier-scoped User-override table (the User deny is now enforced IN PostgreSQL)
**Problem (Gate B-delta B-1):** in v2 the evaluator resolved the User-override in-app _before_ selecting a connection, so a compromised/buggy Gateway on the Orchestrator pool could bypass a User deny — contradicting v2 §1.4/Q2, which mandates that "an Orchestrator write vetoed by a User-override deny must ERROR at the sink." App-resolution is not sink-enforcement.
**Fix (option a — DB-resident override, RLS-forced):**
- **New relation `mosaic.task_status_write_override`** (owned/created by the KBN-100 producer, exactly like `tasks`), columns: `workspace_id`, `task_id`, `tier` (enum `{orchestrator}` for v1 — the only vetoable sub-god tier; extensible to a future 4th rung), `active boolean`, `created_by_actor_id` (audit metadata, NOT an authority key), timestamps. **Primary/unique key `(workspace_id, task_id, tier)`****task-identity + tier ONLY; NO per-federated-writer column is part of the key or an authority input** (preserves F6 + the Q1 HALT boundary: every Orchestrator-tier writer is vetoed identically for a given task).
- **Who may WRITE it (sink-enforced):** `INSERT/UPDATE/DELETE` on the override table is granted **ONLY** to `mosaic_runtime_user_capability` (the User god rung), with the table under `FORCE RLS` and a `WITH CHECK` policy binding the row's `workspace_id` to the session workspace predicate. **[B2-INSERT parity]** that `WITH CHECK` workspace binding applies to the override table's **INSERT** path too (`workspace_id = current_setting('mosaic.workspace_id', true)::uuid`, fail-closed on NULL), so the override table's write path carries the identical symmetric tenant isolation as `tasks` — a User-rung INSERT of a veto row for a foreign workspace, or under an absent GUC, **ERRORS at the sink**. **[v5 COMPOSITION MANDATE parity]** the override table's write policies are subject to the SAME composition rule as `tasks` (§1.4 item 3-bis / §2.4 clause 5): the workspace-bind (and any status/authority conjunct) MUST be realized as a **single compound `WITH CHECK`** OR **`AS RESTRICTIVE`** — **never as multiple _permissive_ policies intended to AND** (which would OR-admit a foreign-workspace veto row). The Orchestrator and base rungs have **no** write privilege — a non-User attempt to author/clear a veto **ERRORS at the sink**. Thus only the User rung can raise or lift a veto.
- **Who READS it:** all three rung capabilities get **`SELECT`** on the override table (workspace-scoped `USING`), because the `tasks` write policy subquery evaluates as the current (querying) rung role. The read is confined to the querying connection's workspace by the override table's own `USING` predicate.
- **How it vetoes (the subquery):** every `tasks` **write policy** for the **Orchestrator rung** (`INSERT … WITH CHECK`, `UPDATE … WITH CHECK`, and the transition `WITH CHECK`) is extended with:
```sql
AND NOT EXISTS (
SELECT 1 FROM mosaic.task_status_write_override o
WHERE o.workspace_id = tasks.workspace_id
AND o.task_id = tasks.id
AND o.tier = 'orchestrator'
AND o.active
)
```
So an Orchestrator-tier write to a task carrying an active User deny is **REJECTED in PostgreSQL**, regardless of app-layer behavior. **[v5 COMPOSITION MANDATE]** this `NOT EXISTS(<override>)` subquery MUST be a **conjunct of the Orchestrator rung's single compound `WITH CHECK`** (alongside the status-pin and the workspace-bind), OR authored **`AS RESTRICTIVE`** — it may **NOT** be a separate _permissive_ policy intended to AND, because permissive policies OR and an Orchestrator write satisfying the status-pin permissive policy would then be OR-admitted despite an active deny (bypassing B-1). The User rung's own `tasks` policies do **not** subquery the override (User = god; the User authored the deny and overrides it). The base rung already cannot write status.
- **Per-tier NEGATIVE test (real PostgreSQL, red-first; v5-HARDENED):** insert an active override row for `(W, T, orchestrator)` via the User rung; then an Orchestrator-rung write on `(W, T)` **using a transition the Orchestrator pin OTHERWISE ACCEPTS** (a legal non-terminal `UPDATE`/transition that would succeed absent the deny row, and under the correct workspace GUC) **must ERROR at the sink** — so the rejection **isolates the override subquery** (not a status/workspace rejection). **Positive-control:** removing the override subquery **alone** flips this negative to a **PASS**, proving the _override_ — not the status-pin or workspace-bind — is doing the rejecting (and proving the subquery is a single-enforced conjunct / RESTRICTIVE, not an OR of separate permissive policies). Clearing the row (`active=false`, User rung only) re-permits the Orchestrator write; a non-User attempt to write the override table **ERRORS**. This proves the User-override is **sink-enforced** (satisfies v2 Q2) and stays within Mos Q1 tier-level (task/tier-scoped, not per-writer).
**[NB-2 — compromise-resistance scope of B-1.]** B-1 sink-enforces the veto against the **Orchestrator-rung path** — an Orchestrator-pool write to a vetoed task ERRORS in PostgreSQL regardless of app behavior. It does **NOT** claim to stop a Gateway compromised badly enough to hold the **User** pool: User = god and holds the sole write on the override table, so a compromised-User-pool Gateway can itself clear the veto. That is the **accepted co-resident-pool residual** (Mos Q1, NO HALT), not a defended boundary. The v3 §0 framing "a compromised Gateway cannot bypass a User deny" is corrected to "an Orchestrator-**path** write cannot bypass a User deny at the sink." Defending against a compromised User-pool Gateway would require per-federated-user DB credentials = the HALT boundary, which Mos Q1 explicitly declines.
**HALT note:** because the key is `(workspace_id, task_id, tier)` and never `writer`, this fix does **not** give two same-tier writers different authority — it gives the _whole_ Orchestrator tier the _same_ per-task veto. The B2-INSERT workspace predicate is likewise applied uniformly to every writer in a tier. NO HALT.
---
## 3. Findings-closure map (each → fail-closed, red-first testable)
| Finding | Closure | Fail-closed enforcement point | Test (red-first, real PostgreSQL / real UID drop) |
| -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **RC19-B1-01** raw sink has no enforceable authority boundary | Declarative deny-by-default RBAC (§2.1) → fixed rung → per-rung DB credential; PG enforces via revoked base privileges + `FORCE RLS` deny-all + per-rung `WITH CHECK`. | At the sink: effective DB role lacks the privilege; mis-authorized write errors in PG. | -00: rung roles created, safe attrs, non-owner. KBN-100: base `UPDATE(status)`/`DELETE` denied, User/Orch allowed. -01: checkout identity==rung + safe-attr. |
| **RC19-B1-02** DELETE+default-INSERT resets status | Status-preserving UPSERT on `(workspace_id,id)`; `status` excluded from `DO UPDATE`; `DELETE` revoked all rungs; status only via authorized transition. | At the sink: DELETE fails closed; refresh cannot touch `status`. | KBN-100 + -09: refresh preserves `status` byte-stable; DELETE denied; transition-only status change. |
| **RC19-B1-03** grant change breaks non-status compatibility | Invariant field-set (§2.5); grants declarative + physically separate → grant mutation writes zero `tasks` rows. | Structural: data plane and grant plane disjoint. | Apply arbitrary policy/grant delta → **zero byte delta** on all `tasks` rows + invariant-set membership test. |
| **RC19-B2-01** FD5 `/proc/self/fd` EACCES after UID drop | SCM_RIGHTS descriptor-passing (§2.8); never re-open `/proc/self/fd/N` post-drop; explicit `O_CLOEXEC`; authenticated socket (F5). | Post-drop process receives a live fd; no privileged re-open. | Drop UID → `/proc/self/fd` re-open EACCES/absent vs SCM_RIGHTS receive succeeds; unauthenticated peer rejected. |
| **RC19-B2-02** dumpability resets after credential transition | Re-assert + **verify** `PR_SET_DUMPABLE=0` after every transition; fail closed if `PR_GET_DUMPABLE != 0`. | Process aborts if dumpable ≠ 0 post-transition. | After simulated setuid: assert `PR_GET_DUMPABLE == 0`; inject reset → fail-closed abort. |
| **F1 (BLOCKING)** INSERT-time status unconstrained for base rung | `GRANT INSERT (non-status list)` → status DEFAULT for base; RLS `INSERT … WITH CHECK` pins new-row status to initial; `INSERT(status)` only User/Orch (§2.4). | At the sink: base status-on-insert denied by column-privilege and/or RLS `WITH CHECK`. | **Red-first:** base `INSERT … status='done'` ERRORS; base INSERT omitting status → DEFAULT; User/Orch initial-status INSERT succeeds. |
| **F2 (BLOCKING)** rung LOGIN roles lack declared/verified safe attributes | Declare both rung roles `NOSUPERUSER NOCREATEDB NOCREATEROLE NOREPLICATION NOBYPASSRLS` + non-owner (§1.3); extend L222 checkout assertion (§2.2). | Bootstrap catalog + every checkout: fail closed on SUPERUSER/BYPASSRLS/ownership. | -00 catalog asserts attrs + non-ownership; -01 injects unsafe attr/ownership → `DATABASE_ROLE_UNSAFE`. |
| **F3 (BLOCKING)** BEFORE-trigger status bypass | Invariant: no trigger on `tasks` writes/normalizes `status` outside `transitionStatus()` (§2.7); homed in KBN-100 producer. | Producer DDL admits no status-normalizing trigger; transition path is only status writer. | KBN-100 static: enumerate triggers on `tasks`, none assign status. **-09 deployed:** base UPDATE of a permitted col cannot change status via any trigger. |
| **F4 (non-blocking)** no frozen `tasks(workspace_id,id)` unique key | Bind `tasks (workspace_id,id)` UNIQUE as explicit KBN-100 req; else re-bind UPSERT to `(id)` (§2.3). | `ON CONFLICT` resolves to a real unique index; else hard planning error (fails loud). | KBN-100 migration: unique key exists; UPSERT plans/executes; missing-key variant fails loud. |
| **F5 (non-blocking)** SCM_RIGHTS socket unauthenticated | `socketpair()` pre-fork **or** `SO_PEERCRED` + `0700` dir; `MSG_CMSG_CLOEXEC` on `recvmsg` (§2.8). | Fd transfer only over an authenticated peer; received fd close-on-exec. | Unauthenticated/wrong-peer rejected; `SO_PEERCRED` mismatch fails closed; `MSG_CMSG_CLOEXEC` asserted. |
| **F6 (non-blocking)** `userOverride` scope | Schema constrains override to `(workspace_id,task_id,tier)`; **forbids** per-writer key (§2.1); realized as the sink-resident override table (§2.9). Attribution → metadata (§2.6). | Fail-closed at policy load: a per-writer authority key **fails schema validation**. | Schema test: per-writer override/authority key **rejected**; tier/task-scoped accepted. |
| **F7 (non-blocking)** re-key launders status | Revoke `UPDATE(workspace_id,id,tenancy)` from **all** rungs incl. User/Orch (§2.5). | At the sink: no rung may UPDATE key/tenancy columns. | Each rung (incl. User) `UPDATE workspace_id`/`id` **ERRORS**. |
| **B-1 (BLOCKING; composition PINNED in v5)** User-override was app-enforced, not sink-enforced | Sink-resident `task_status_write_override` table `(workspace_id,task_id,tier)`, User-rung-write-only; the `NOT EXISTS(<active deny>)` subquery is a **conjunct of the Orchestrator rung's single compound `WITH CHECK`** (or authored **`AS RESTRICTIVE`**), **never a separate permissive policy** — else an Orchestrator write satisfying the status-pin permissive policy is OR-admitted despite the deny (§2.9 / §1.4 item 3-bis). | At the sink: an active User deny → Orchestrator write REJECTED in PG (single enforced check, not OR-bypassable); non-User cannot author the veto. | **Per-tier NEGATIVE (real PG; v5-hardened):** User inserts deny row → Orchestrator write **using a transition the pin OTHERWISE ACCEPTS** **ERRORS** (isolates the override). **Positive-control:** dropping the override subquery alone flips it to PASS (proves single-conjunction/RESTRICTIVE, not permissive-OR). Clear row → re-permitted; Orch/base write to override table **ERRORS**. |
| **B-2 (BLOCKING)** FORCE + `WITH CHECK`-only bricks all reads (SELECT returns 0 rows for every rung and owner) | Add per-rung `SELECT`/`UPDATE` `USING` policies, **workspace-scoped** via `current_setting('mosaic.workspace_id')`; forbid `USING(true)` (§1.4/§2.2). Homes tenant isolation at the sink. | At the sink: reads succeed only for the session's workspace; wrong/absent predicate → zero rows (no cross-tenant leak). | Post-activation: each rung `SELECT`/UPSERT-probe within its workspace **succeeds**; a `SELECT` under a foreign/absent `mosaic.workspace_id` returns **zero** rows; a `USING(true)` variant **fails the isolation negative**. |
| **B-3 (BLOCKING)** owner/backfill write path under FORCE RLS | Backfill/repair DML runs FIRST (RLS disabled); `ENABLE`/`FORCE RLS` is the **TERMINAL** migration step; **no standing schema_owner ALLOW write policy** after activation; break-glass (`mosaic_extension_owner` §4.1 L146 + external bootstrap actor §9 L266) named in -09 cert (§1.4). | Migration ordering: backfill completes before FORCE binds; post-activation owner write has no policy → fails closed. | KBN-100 migration test: backfill INSERTs succeed pre-FORCE; **post-activation `mosaic_schema_owner` `tasks` write ERRORS under FORCE**; no runtime rung is superuser/BYPASSRLS; -09 names the two break-glass actors as the accepted boundary. |
| **N-1 (non-blocking)** VIEW/RULE status-write path | Forbid views/rules on `tasks` yielding a status write, OR mandate `security_invoker=true` + zero runtime grants; enumerated by `invariant-set.ts` (§2.7). | Enumeration admits no status-writable view/rule; any view over `tasks` is invoker-rights + ungranted. | Negative: a rung cannot mutate `tasks.status` through any view/rule; enumeration fails an owner-rights view with runtime grants. |
| **N-2 (non-blocking)** F3 enumeration incomplete | Extend enumeration: no AFTER trigger `UPDATE tasks SET status`; no `SECURITY DEFINER` fn writing `tasks.status` (L176 backstop); no trigger on related runtime-writable table (`task_events`, L178) writing `tasks.status`; no `CREATE RULE`; scan function **BODIES** (§2.7). One-shot -09 cert; recommend continuous -06 CI scan. | Producer/-09 cert admits no indirect status-write path. | Catalog scan of `pg_proc.prosrc`/triggers/rules asserts none write `tasks.status`; deployed behavioral proof; (recommended) -06 CI re-scan per migration. |
| **N-3 (non-blocking)** Orchestrator can INSERT terminal status | Orchestrator `INSERT(status)` `WITH CHECK` pinned to non-terminal initial status; only User may INSERT arbitrary/terminal (§2.4). | At the sink: Orchestrator terminal-status INSERT rejected by `WITH CHECK`. | Orch `INSERT … status='done'` **ERRORS**; Orch `status='backlog'` succeeds; User `status='done'` succeeds. |
| **N-4 (non-blocking)** transition-graph legality not DB-enforced (`done→backlog` reset by another name) | **DECISION (documented, §2.10):** edge-legality is **app-enforced** within near-god/god authority; the sink closes RC19-B1-02 **for base only** (DELETE-denial + no base status write). No status-writing trigger is added (would collide with F3/N-2); a validation-only guard is deferred. | Documented boundary: User/Orch hold near-god/god authority; illegal edges are an app-layer concern, not a sink guarantee. | -09 records the decision + asserts base cannot reset status at the sink; app-layer transition-graph tests are owned by the Gateway command lane (out of KBN-101 scope). |
| **N-5 / AD-2 (non-blocking)** import direction ambiguity | `-01` provisions generic pools only; rung→pool SELECTION lives in `-10`'s `connection-selection.ts`, consumed by `-02/-03/-05`; **never imported by `-01`**`10→01`, acyclic (§2.2). | Structural: no `01→10` edge exists. | Static import test: `-01` has no import of `sink-rbac/connection-selection`; `-02/-03/-05` do; graph acyclic. |
| **AD-1 (completeness)** frozen §4 L176 CONNECT cert reds on the 2 new logins | Amend the L176 allowlist to admit `mosaic_runtime_user`/`mosaic_runtime_orchestrator` + assert their `TEMPORARY` denial; named in -00 row/rc.20 (§1.3). | -00 cert: CONNECT allowlist includes the 2 new logins; all 3 runtime rungs `TEMPORARY`-denied. | -00 cert asserts CONNECT granted to the 2 new logins and no runtime rung retains `TEMPORARY`; an un-amended allowlist REDS. |
| **B2-INSERT (BLOCKING; composition PINNED in v5)** tenant isolation not enforced on the INSERT write path (v3 bound workspace only in `SELECT`/`UPDATE` `USING`; PostgreSQL does not apply `USING` to INSERT, and the explicit status `WITH CHECK` suppresses `USING` substitution → `workspace_id` unbound on INSERT → any rung can INSERT/UPSERT into a foreign workspace). **[v5 delta-3]** the v4 justification ("all `WITH CHECK` AND") was FALSE — multiple permissive policies OR — so the workspace conjunct must be pinned into a _single compound check or `AS RESTRICTIVE`_, else the bind is bypassable. | Include `workspace_id = current_setting('mosaic.workspace_id', true)::uuid` as a **conjunct of the single compound INSERT `WITH CHECK`** (with the F1/N-3 status-pin and, Orchestrator, the B-1 override subquery) **OR** author it **`AS RESTRICTIVE`**, on `tasks` (and the UPSERT INSERT arm) + symmetric on `task_status_write_override`. **Multiple _permissive_ policies intended to AND are EXPLICITLY FORBIDDEN** (they OR). Corrected semantics + realization mandate in §1.4 item 3-bis / §2.4 clauses 45 / §2.9. | At the sink: an INSERT/UPSERT with a foreign `workspace_id` fails the single enforced check; a NULL GUC → NULL predicate → INSERT ERRORS (fail-closed). Write-side tenant isolation homed at the sink and no longer OR-bypassable. | **Red-first (real PG; v5-hardened):** the cross-workspace negative uses an **otherwise-valid row** (base + `status='backlog'` + no override, differing only in `workspace_id=<foreign W2>`) so the **ERROR** isolates the workspace bind; direct and via status-upsert INSERT arm. **Positive-control:** dropping the workspace conjunct alone flips it to PASS (proves single-conjunction/RESTRICTIVE, not permissive-OR). NULL-`mosaic.workspace_id` INSERT **ERRORS**; same-workspace valid-status INSERT **succeeds** (no over-deny); override-table foreign-workspace INSERT **ERRORS**. |
| **NB-1 (non-blocking)** override relation absent from the no-status enumeration | Extend the F3/N-1/N-2 no-status-write enumeration **and the -09 cert / continuous -06 scan** to `task_status_write_override`: no status-writing trigger / RULE / `SECURITY DEFINER` function on the override relation writes `tasks.status` (§2.7). | Enumeration/CI admits no indirect status-write path via the override relation. | Catalog + fn-body scan asserts no trigger/rule/SECURITY-DEFINER on `task_status_write_override` writes `tasks.status`; a planted one REDS the -06 scan. |
| **NB-2 (non-blocking)** overstated B-1 compromise claim | Scope §0/§2.9 wording: B-1 sink-enforces the veto against the **Orchestrator-rung path**; a compromised **User**-pool Gateway (User=god) can clear a veto — accepted Mos Q1 co-resident residual, NO HALT (§0/§2.9). | Documented boundary: sink-enforcement is Orchestrator-path-scoped; the User-pool residual is accepted, not defended. | Doc/claim assertion in -09 cert notes: the override-veto negative proves the **Orchestrator-path** ERROR; the framing no longer claims User-pool compromise resistance. |
| **NB-3 (non-blocking)** overstated B-2 compromise claim | Scope §0/§2.2 wording: the workspace GUC is app-set with no per-tenant DB check → B-2 + B2-INSERT protect a **buggy** Gateway (omitted predicate → fail-closed), NOT a **compromised** one forging the GUC (accepted residual). The B2-INSERT unbound-INSERT closure remains a real fail-closed win (§0/§2.2). | Documented boundary: fail-closed on omission/NULL; forged-GUC cross-tenant is the accepted per-user residual. | Cross-workspace read → zero rows; NULL/foreign-GUC INSERT ERRORS (buggy-Gateway closure proven); doc states forged-GUC is out of scope (HALT boundary). |
| **NB-4 (non-blocking; v5 states transitive)** owner-toggle-FORCE containment mis-stated as incapability | Name `mosaic_schema_owner` (reachable via `mosaic_migrator` `SET ROLE`) in the -09 break-glass enum as **contained-but-CAPABLE**: an owner can `ALTER TABLE tasks DISABLE/NO FORCE RLS` then write regardless of NOSUPERUSER/NOBYPASSRLS → containment = **transitive non-reachability from the 3 runtime LOGIN rungs**, asserted explicitly (§1.4). | -09 cert: owner toggle-then-write is real but confined to the migrator/owner phase; runtime rungs cannot reach `mosaic_schema_owner`/`mosaic_migrator` through any inherited (capability-role) path. | **[NB-4-transitive]** -09 asserts non-reachability as **TRANSITIVE membership**`NOT pg_has_role(<rung>, 'mosaic_schema_owner', 'USAGE')` / `NOT has_privs_of_role(...)` recursion, NOT merely the absence of a direct `pg_auth_members` row (ownership check returns true for an _inherited_ membership through a capability role). Frozen §6 L222 "assumability" plausibly already covers this; "transitive membership" is stated explicitly. **[v6 — F-NB4]** the assertion additionally **conjoins the `MEMBER` variant**`NOT pg_has_role(<rung>, 'mosaic_schema_owner', 'MEMBER')` **AND** `NOT pg_has_role(<rung>, 'mosaic_migrator', 'MEMBER')` for each of the 3 runtime rungs — because a `WITH INHERIT FALSE, SET TRUE` membership yields `has_privs_of_role = FALSE` yet still permits `SET ROLE … ; ALTER TABLE … NO FORCE`; the `MEMBER` privilege captures SET-ROLE reachability regardless of `INHERIT` (the intended graph has each rung `SET FALSE` to only its capability role, so this is assertion-completeness, not a live hole). `mosaic_schema_owner` is `NOLOGIN`, reachable only via `mosaic_migrator` `SET ROLE`; the 3 runtime rungs are transitive non-members and non-assumers (catalog + L222 checkout). |
| **NB-5 (non-blocking)** 2 new capability roles lack baseline runtime grants → fail frozen §6 L222 verify | Grant `mosaic_runtime_user_capability` / `mosaic_runtime_orchestrator_capability` the frozen §4 L176 baseline runtime grants (`USAGE ON SCHEMA mosaic`; `USAGE ON SCHEMA drizzle` + `SELECT` on the two ledger relations; relevant sequence `USAGE`/`SELECT`) at -00 (§1.1/§1.3). | Bootstrap: the 2 new rungs hold the baseline grants → they connect/operate and pass the L222 runtime verify. | -00 catalog asserts each new capability role holds `USAGE ON mosaic`, ledger `SELECT`, sequence grants; an omission → L222 runtime verify fails closed (missing inherited grant). |
| **Q2 per-tier deny-by-default (red-team requirement; v5 composition-pinned)** | Per-rung **single compound `WITH CHECK`** (status-pin + B2-INSERT workspace bind + Orchestrator override) **or `AS RESTRICTIVE`****never multi-permissive-intended-to-AND** — plus `USING` + `FORCE RLS`; deny-all default (§1.4 item 3-bis). | At the sink, every tier: unauthorized write-source rejected even for owner/maintenance (`FORCE`); tenant/override binds not OR-bypassable. | **NEGATIVE per tier (v5-hardened):** base terminal-status INSERT/transition ERROR; **Orchestrator write under User-override deny, using an otherwise-accepted transition, ERROR (+ positive-control: drop override subquery → PASS)**; **cross-workspace INSERT/UPSERT with an otherwise-valid status ERROR (+ positive-control: drop workspace conjunct → PASS)**; NULL-GUC INSERT ERROR; owner/maintenance post-activation write ERROR (`FORCE` proven); cross-workspace read → zero rows; **same-workspace valid-status write SUCCEEDS per writable command (INSERT, transition/UPDATE, SELECT) — the [v6 — N2] ≥1-permissive-per-writable(rung,command) option-(b) vacuous-deny guard**; `pg_policy.polpermissive` check confirms single-compound-or-RESTRICTIVE, **[v6 — N1] grouped by effective role incl. `PUBLIC`/inheritance and rejecting an internally-disjunctive single `WITH CHECK` — supplementary to, never a substitute for, the behavioral positive-controls (PRIMARY proof)**. |
---
## 4. Implementation card DAG (owner + disjoint manifest, red-first at Gate A/B)
```
contract(amended) ─► KBN-101-00 (rung-ROLES + capability roles, safe attrs [F2],
│ deny-default base, CONNECT + [AD-1] L176 allowlist
│ amend (2 new logins CONNECT, TEMPORARY-denied);
│ bootstrap catalog tests. NO tasks/override DDL [GA-1])
KBN-101-01 ──┤ (provisions 3 generic rung pools; sets mosaic.workspace_id
│ session predicate [B-2]; checkout identity==rung AND
│ safe-attribute/non-owner verify [F2, extended L222].
│ Does NOT import -10 [AD-2/N-5])
KBN-101-10 ──┘ depends 00,01:
packages/db/src/sink-rbac/** (policy+schema[F6]+evaluator+ladder+
connection-selection[consumes 01; 10→01, N-5]+
status-upsert[F4]+invariant-set[F7]+
no-status trigger/view/rule enum[N-1,N-2])
packages/db/src/credential-handoff/** (scm-rights[F5] + process-hardening +specs)
┌─────────────┬────────┼──────────────────────┬──────────────────────────────┐
KBN-101-03 KBN-101-05 KBN-101-02 KBN-100 (producer lane) KBN-101-09 (evidence-only)
(dep 00,01,10) (dep 00,03, (dep 01,03,10) SPEC target of -10: (dep KBN-100,08)
migrator 10) importer fd + homes tasks + override DDL DEPLOYED cert:
wrapper: renderer: sink adapter uses AFTER create: real rung INSERT/
SCM_RIGHTS 3 rung DSN status-upsert - rung column-grant matrix transition success;
SEND + dumpable secret (drop 10002) [F1,F7,N-3] override-veto [B-1];
re-assert mounts; - RLS WITH CHECK(+override) cross-workspace read
(drop 10003) Gateway + USING workspace pred zero-rows [B-2];
[GA-4(ii): Dockerfile; [Q2,B-1,B-2, conds a&b] backfill-then-FORCE
docker/db- NO SCM_RIGHTS - task_status_write_override ordering + owner-write
migrator. (DB-role, no table [B-1] denial [B-3] + break-
Dockerfile] UID drop) - tasks(workspace_id,id) UNIQUE glass named;
│ [F4] base status
KBN-101-06 (dep 02,03,05,07,10): inventory += KBN-101-10; INSERT/UPDATE/DELETE
overlap/ownerless/path-existence green; denial; refresh
matrix += rung selection + SCM_RIGHTS[F5] + status-preservation;
per-tier RLS negatives + override-veto[B-1] + no-status trigger/
cross-workspace read[B-2] + backfill-order[B-3] view/rule [F3,N-1,N-2];
+ ADOPTED continuous no-status scan[N-2,NB-1] grant-delta zero-byte;
+ cross-workspace/NULL-GUC INSERT neg[B2-INSERT]
│ dumpable=0 evidence
KBN-101-08 (dep 00…07,10): foundation + atomic activation cert
```
- **Gate A (re-review):** every element traces to a manifest owner; disjointness/overlap/ownerless proven against amended §7; the SPEC-vs-implement split for `tasks`+override sink DDL matches the frozen §4 L178 precedent (producer = KBN-100, applied **after** table creation — GA-1); dep edges acyclic with `10→01` import direction (AD-2/N-5); the L176 CONNECT amendment named (AD-1).
- **Gate B (red-team):** each B1/B2 finding **and** F1F7 **and** B-1/B-2/B-3 **and** B2-INSERT **and** N-1..N-4 **and** NB-1..NB-5 **and** the Q2 per-tier deny-by-default has a **red-first** test that fails on the current design and passes only with the enforcement point; sink privilege/RLS/override/tenant denials — **including cross-workspace and NULL-GUC INSERT/UPSERT [B2-INSERT]** — proven **in PostgreSQL** (not PGlite — §5 L218); SCM_RIGHTS/dumpable against a real UID drop with an authenticated socket.
---
## 5. Executability self-audit (re-run for v5)
**Every element has an owner in the amended manifest — nothing ownerless:**
| Element | Owner |
| ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------- |
| Declarative RBAC policy artifact + schema [F6] + evaluator + ladder + no-status trigger/view/rule enum [N-1,N-2] | KBN-101-10 (`packages/db/src/sink-rbac/**`) |
| Rung→credential connection-selection module (consumes -01 pools; `10→01` [N-5]) | KBN-101-10; pools provisioned in KBN-101-01 (`database.module.ts`) |
| Status-preserving-UPSERT [F4] + invariant-set [F7] module | KBN-101-10 (`packages/db/src/sink-rbac/**`) |
| SCM_RIGHTS handoff [F5] + non-dumpable assert/verify | KBN-101-10 (`packages/db/src/credential-handoff/**`) |
| Fixed rung DB roles + safe attributes [F2] + deny-default base + CONNECT + **[AD-1] L176 allowlist amend + TEMPORARY denial** + **[NB-5] baseline L176 runtime grants (`USAGE mosaic`, ledger `SELECT`, sequence grants) on the 2 new capability roles** | KBN-101-00 (`infra/pg-bootstrap/roles.sql` + tests) — **no tasks/override DDL [GA-1]** |
| `tasks` sink DDL: rung grants [F1,F7,N-3] + RLS `WITH CHECK`(+override) [Q2,B-1] + **INSERT `WITH CHECK` workspace bind on every rung [B2-INSERT]** + `SELECT`/`UPDATE` `USING` workspace predicate [B-2] + `FORCE RLS` terminal-after-backfill [B-3, conds a&b] + `(workspace_id,id)` UNIQUE [F4] + `actor_id` col [Q1] + no-status trigger/view/rule invariant on `tasks` **and override relation** [F3,N-1,N-2,NB-1] | **KBN-100 producer** (SPEC by -10; §4 L178 precedent; scope amended per Q2) |
| **`task_status_write_override` table [B-1]** (create + grants + FORCE RLS + policies; User-write-only incl. **INSERT `WITH CHECK` workspace bind [B2-INSERT parity]**, all-rung-SELECT) | **KBN-100 producer** (SPEC by -10; same L178 precedent) |
| `mosaic.workspace_id` session predicate set at checkout [B-2] | KBN-101-01 (`database.module.ts` / `connection-identity.ts`) |
| Rung DSN secret mounts (3 fixed) | KBN-101-05 (renderer, extends runtime-URL row) |
| Checkout identity==rung + safe-attribute/non-owner verify [F2] | KBN-101-01 (`connection-identity.ts`, extended L222) |
| Migrator launch wrapper SCM_RIGHTS send + dumpable re-assert | KBN-101-03 (`docker/db-migrator.Dockerfile` [GA-4(ii)]) |
| Importer privileged-fd case | KBN-101-02 |
| Deployed enforcement certification (F3/N-1/N-2 **+ NB-1 override-relation scan**, per-tier RLS negatives, B-1 override-veto, B-2 cross-workspace read, **B2-INSERT cross-workspace/NULL-GUC INSERT negatives**, B-3 ordering + break-glass naming **incl. [NB-4] `mosaic_schema_owner` contained-but-capable / non-reachability assertion**, N-4 decision) | KBN-101-09 (evidence-only) |
| Inventory/overlap/path-existence + command matrix (rung sel + SCM_RIGHTS + per-tier RLS + override-veto + cross-workspace read + **[B2-INSERT] cross-workspace/NULL-GUC INSERT negatives** + backfill-order + **[ADOPTED] continuous no-status scan on `tasks` + override relation**) | KBN-101-06 (fixture += -10) |
**Manifest closure (KBN-101-06 green):** the only NEW ownership is the two disjoint -10 globs. The B-1 override table, B-2 `USING` policies, B-3 terminal-FORCE ordering, **and the v4 B2-INSERT INSERT `WITH CHECK` workspace binding (+ its override-table parity) and the NB-1 override-relation no-status enumeration** are ALL producer DDL homed in KBN-100's already-owned `schema.ts`/`drizzle/**` (a _responsibility_ widening, not a glob change — identical treatment to the v3 `tasks` sink DDL). The **NB-5 baseline capability-role grants** amend -00's already-owned `roles.sql`/tests (like AD-1); the **continuous -06 no-status scan** lives in -06's already-owned `tools/ci/fixtures/kbn101-ddl-inventory.json` + `.woodpecker/ci.yml`; the NB-2/NB-3/NB-4 claim-scopings/enumeration are doc + -09 cert wording on already-owned paths. N-1/N-2/N-5 additions live in -10's already-declared `sink-rbac/**`. **No glob is split/shared/narrowed → overlap/ownerless/path-existence stay green (KBN-101-06 unchanged-green vs v3).**
**NO per-federated-user topology (HALT boundary respected — re-verified for v5):** DB-role/credential cardinality = **3** (User / Orchestrator / others), fixed and independent of the federated-user count. Federated identity → rung happens in the evaluator at authorization time. A new federated user creates **zero** roles/credentials/secrets/pools. The **B-1 override table is keyed `(workspace_id, task_id, tier)` — task+tier, never writer** — so it homes a tier-level veto, not a per-user distinction. The **B-2 read `USING` predicate AND the B2-INSERT INSERT check predicate are the SAME shared per-request session var** (`mosaic.workspace_id`), applied uniformly to whichever rung role holds the connection — tenant isolation, not writer discrimination (and, per v5, composed into a single compound check or `AS RESTRICTIVE` rather than an OR of separate permissive policies). Per-writer **attribution** is a metadata column (`task_events.actor_id`, §2.6), not a DB principal. `userOverride` forbids a per-writer authority key (F6, fail-closed at load). The NB-1..NB-5 fixes add no principal, key, or predicate that varies by writer. Enforce-at-sink is satisfied by the fixed rung roles' grants + `FORCE RLS WITH CHECK`(+override, +workspace-bound INSERT) + `USING` — it does **not** require, and this design does **not** use, a DB role/credential per federated user.
**Q1 same-tier / different-authority check (the ONLY HALT trigger):** no v5 change gives two SAME-TIER writers different WRITE authority. The v5 composition pin fixes only **how** the already-uniform predicates are physically composed (single compound `WITH CHECK` or `AS RESTRICTIVE`, never multi-permissive-OR) — it changes no predicate's content and introduces no writer-varying axis. F1/F2/F3/F7/N-3 tighten authority uniformly per tier; **B2-INSERT binds the same workspace predicate uniformly into every rung's INSERT check** (tenant isolation, not writer discrimination); B-1 vetoes the _entire_ Orchestrator tier per task (not a subset of Orchestrator writers); B-2 isolates by workspace uniformly; F6 forbids per-writer authority keys; attribution is audit-only. The NB-4-transitive nit only _strengthens_ the owner non-reachability assertion (transitive membership) — no authority axis. **No same-tier-different-authority requirement exists → NO HALT — an executable envelope is delivered.**
---
## 6. Flagged ambiguities in the frozen contract (for reviewer awareness)
1. **Sink identity of the status store.** The RBAC-guarded sink is `tasks.status`; legacy `mission_tasks.status` stays frozen read-only. Confirm no reviewer reads "raw sink" as the legacy column.
2. **Frozen "role graph unchanged" + "no RLS" + L176 CONNECT allowlist vs. the rulings.** SHARED-CONTRACT rc.5 L82/L88-90 froze the role graph and grant/revoke-only (no RLS); frozen §4 L176 froze the CONNECT allowlist. Jason B1 + Mos OPTION A add fixed rung-roles; **Mos Q2** authorizes RLS `WITH CHECK`/`USING` + `FORCE RLS` + the override table; **AD-1** amends the L176 allowlist for the 2 new logins. The rc.20 amendment text (§1.3/§1.4) records all three as deliberate, human-authorized departures. Reviewer should confirm rc.20 is **adopted** (not treated as a violation), that KBN-100's DDL scope is amended to include RLS + the override table on `tasks`, and that the -00 row amends the L176 allowlist.
3. **Natural-key column list for `tasks` (F4).** `tasks (workspace_id, id)` UNIQUE is asserted by the SI-001 pattern but must be **bound + created by KBN-100**. If KBN-100's `tasks` identity differs, the UPSERT natural key re-binds to `(id)` (spec-level, no HALT).
4. **Initial/terminal status values (F1/N-3).** F1 pins base new-row status to `backlog` (§5.4 L214); N-3 pins the Orchestrator's INSERT set to non-terminal (`{backlog, ready}`) and reserves terminal (`{done, cancelled}`; `in_review` treated as non-initial) to the User rung. KBN-100 binds the exact column DEFAULT and the RLS `WITH CHECK` literals against the §3 L134 vocabulary so they agree.
5. **Workspace session-var name + set-site (B-2 + B2-INSERT).** The `USING` predicate **and the v4 INSERT `WITH CHECK` predicate** both use `current_setting('mosaic.workspace_id', true)`. -01 sets it per request from the server-verified tenant (never a body field, §7.1). Reviewer should confirm the exact GUC name and that a transaction-local `SET LOCAL` is used so a pooled connection cannot leak a stale workspace across requests (mirrors the frozen `SET LOCAL search_path` discipline, §4 L152) — now **explicitly folded into the frozen L174 poisoned-pooled-session-reset + `SET LOCAL` restoration negative test** (Gate B 2(a), §2.2). Reviewer should also confirm the accepted scope (NB-3): fail-closed on an omitted/NULL GUC (buggy Gateway), but a _forged_ GUC to a victim tenant is the accepted per-user residual (Mos Q1), not defended.
6. **N-4 transition-graph decision.** Edge-legality (illegal/backward transitions such as `done→backlog`) is **app-enforced** within near-god/god authority; the sink closes RC19-B1-02 for **base only** (DELETE-denial + no base status write). A validation-only sink guard is deferred to avoid colliding with the F3/N-2 no-status-trigger invariant. Reviewer should confirm this decision is acceptable (it is stated, not silently dropped).
7. **N-2 continuous scan — now ADOPTED.** The complete no-status-write enumeration (trigger/view/rule/SECURITY DEFINER/function-body), covering `tasks` **and `task_status_write_override` (NB-1)**, is now a **continuous -06 CI catalog scan run on every migration** (promoted from the v3 one-shot -09 recommendation, per Gate A-delta-2 obs #2); the -09 deployed cert still records the final proof. Reviewer should confirm the -06 scan is owned/wired in `tools/ci/fixtures/kbn101-ddl-inventory.json` + `.woodpecker/ci.yml` (already-owned -06 paths).
8. **KBN-101-09 is evidence-only.** B-1/B-2/B-3/F3/N-1/N-2 negative tests are _authored/owned_ by the producing implementation cards (KBN-100 producer for `tasks`/override triggers/RLS; -10/-01 for the rung path); KBN-101-09 records the **deployed** certification evidence and changes no implementation path. Confirm the test-ownership split is acceptable (mirrors the frozen -09 "evidence-only" role).
9. **B2-INSERT / B-1 `WITH CHECK` composition — SEMANTICS CORRECTED + REALIZATION MANDATED (v5; the delta-3 fix).** v4 justified the composition with a **FALSE** PostgreSQL claim — "multiple applicable `WITH CHECK` clauses must all pass (they AND)". **That is struck.** The **correct** semantics: multiple **PERMISSIVE** policies for a command combine with **OR** (a row is admitted if it satisfies **ANY** one permissive `WITH CHECK`, ANDed with all `AS RESTRICTIVE` checks); the AND this closure needs holds **ONLY** (a) within a _single_ policy whose `WITH CHECK` is one AND-conjoined expression, or (b) across `AS RESTRICTIVE` policies. Accordingly v5 **MANDATES** (§1.4 item 3-bis / §2.4 clause 5 / §2.9) that, per rung per command, the status-pin **AND** the workspace-bind **AND** (Orchestrator) the override subquery be realized as EITHER a **single compound `WITH CHECK`** OR the status-pin-as-sole-permissive **plus** the workspace-bind/override **`AS RESTRICTIVE`**, and **EXPLICITLY FORBIDS** realizing them as multiple _permissive_ policies intended to AND (which would OR-admit a foreign-workspace INSERT satisfying only the status-pin — cross-tenant injection — and equally bypass the B-1 veto). Reviewer should confirm: (i) the corrected OR-semantics statement replaces every "all `WITH CHECK` AND" assertion; (ii) the realized policies are single-compound or `AS RESTRICTIVE` (a `pg_policy.polpermissive` catalog check backs it); (iii) the hardened red-first negatives use an _otherwise-valid_ row (cross-workspace: base + `backlog` + foreign ws; override: an otherwise-accepted Orchestrator transition) so each rejection isolates the bind/override under test, **and each carries a positive-control** (dropping the workspace conjunct — resp. the override subquery — alone flips the negative to a PASS, proving the bind/override, not the status-pin, is rejecting). Confirm the accepted scope (NB-2/NB-3) is unchanged: write-side sink tenant isolation is complete against a _buggy_ Gateway; a _compromised_ Gateway forging the GUC or holding the User pool is the accepted Mos Q1 residual.
---
_Envelope A **v6** authored from base `b0d78d86`. **v6 = v5 + N1/N2/F-NB4 non-blocking test/lint/cert hardening ONLY — NO design predicate, policy, grant, role, or table is changed** (only test/lint/cert wording). v5 CONVERGED with BOTH terminal re-gate-4 gates GO (Gate A-delta-4 `a4ce075a`; Gate B-delta-4 `a379e0de`), NO HALT; Gate B-delta-4 flagged 3 EXPLICITLY NON-BLOCKING hardening notes "for the trunk-commit record," folded here additively: **N1** — the `pg_policy.polpermissive` lint is strengthened to count permissive policies grouped by EFFECTIVE role (incl. `PUBLIC`/inheritance) and to reject an internally-disjunctive single `WITH CHECK`, stated as SUPPLEMENTARY with the behavioral positive-controls remaining the PRIMARY proof (§1.4 item 3-bis / §2.4 clause 5 / §2.7 / §3 Q2 row); **N2** — the option-(b) vacuous-deny guard is stated explicitly as the load-bearing "≥1 PERMISSIVE policy per writable (rung, command)" invariant, verified by the "same-workspace valid-status write succeeds" positive tests for INSERT / transition-UPDATE / SELECT (§1.4 item 3-bis / §2.4 clause 5 / §3 Q2 row); **F-NB4** — owner non-reachability is extended to SET-ROLE assumability by conjoining the `MEMBER` variant `NOT pg_has_role(<rung>, 'mosaic_schema_owner', 'MEMBER')` AND `NOT pg_has_role(<rung>, 'mosaic_migrator', 'MEMBER')` for each of the 3 runtime rungs in the -09 cert (§1.4 NB-4 para / §3 NB-4 row). All three are assertion/test/lint completeness on an already-BOTH-GO design; the intended role graph is already safe. The v6 body otherwise preserves the v5 text verbatim, which in turn preserved EVERY v4 closure (all v4 checks passed except one bounded item) and closed the SINGLE remaining BLOCKING finding raised identically by BOTH re-gate-3 gates (Gate A-delta-3 `aa5cbf73` CHANGES-NEEDED; Gate B-delta-3 `aa881074` 1 blocking) — the **RLS `WITH CHECK` composition semantics**: (1) **corrected semantics** — struck the FALSE "PostgreSQL requires ALL applicable `WITH CHECK` to pass / they AND" from §1.4/§2.4/§6.9 and stated the true rule (multiple PERMISSIVE policies OR; AND holds only within a single compound `WITH CHECK` or across `AS RESTRICTIVE`); (2) **mandated the realization** — per rung per command, status-pin + workspace-bind + (Orchestrator) override subquery MUST be a **single compound `WITH CHECK`** OR the status-pin-as-sole-permissive **plus** the bind/override **`AS RESTRICTIVE`**, with **multi-permissive-intended-to-AND EXPLICITLY FORBIDDEN**, applied to `tasks` **and** `task_status_write_override` (§1.4 item 3-bis, §2.4 clauses 45, §2.9); (3) **hardened the red-first negatives** — cross-workspace INSERT uses an otherwise-valid row (base + `backlog` + foreign ws) and the B-1 override negative uses an otherwise-accepted Orchestrator transition, so each rejection isolates the bind/override, **each with a positive-control** (dropping the workspace conjunct — resp. override subquery — alone flips the negative to PASS); plus **NB-4-transitive** (owner non-reachability stated as TRANSITIVE membership — `NOT pg_has_role(...)`/`has_privs_of_role(...)` recursion, not a direct `pg_auth_members` edge; frozen L222 "assumability" noted as plausibly already covering it). Every other v4 closure carries forward intact (3-rung ladder / 0 per-user; F1F7; GA-1/3/4; B-1/B-2/B-3; the B2-INSERT workspace bind itself; N-1..N-5; AD-1/AD-2; NB-1..NB-5; continuous -06 no-status scan; L174 poisoned-pool `SET LOCAL` GUC fold; rc.20). Manifest disjoint (KBN-101-06 green) — all v5 changes land on already-owned KBN-100-producer / -09 / -06 paths (DDL realization detail + test wording; no glob change). Design/contract only; no SSOT file modified, no code, no PR. HALT self-check: NO HALT (fixed 3-rung ladder; override keyed task+tier not writer; workspace predicate shared per-request and uniform per tier, now composed as single-compound/`RESTRICTIVE`; attribution → metadata; no per-federated-user DB role/credential; no same-tier different-authority requirement — both delta-3 gates independently confirmed the boundary HOLDS). **v6 changes NOTHING in this HALT calculus: N1/N2/F-NB4 add only lint-scope, a vacuous-deny test invariant, and a `MEMBER` assumability assertion — zero new principal, key, predicate, grant, role, or table, and no writer-varying axis — so NO HALT is re-confirmed for v6.** Manifest remains disjoint (KBN-101-06 green): every v6 fold lands on already-owned paths — the -06 continuous scan / `pg_policy.polpermissive` lint in `tools/ci/fixtures/kbn101-ddl-inventory.json` + `.woodpecker/ci.yml`; the option-(b) positive tests in the KBN-100-producer / -09 evidence paths; the `MEMBER` assertion in the -09 cert — no glob split/shared/narrowed. NO code, NO PR, NO SSOT edit._
@@ -192,4 +192,6 @@ No role can transform a missing gate into a warning by changing status, editing
This manifest is derived from the ratified source plan. Any change to SOT authority, workspace tenancy, fixed statuses, Coordinator/Certifier authority, health-state semantics, schema v1, migration direction, or recovery-tier field set is a contract change. Contract changes require Jason/Mos authorization and cannot be inferred by an implementation lane.
> KBN-101 Envelope A (rc.20, `KBN-101-DB-ROLE-SPLIT.md` §4/§10) is the specific authorized instance of a schema-v1 contract change under this clause, ruled by Jason B1 + Mos OPTION A/Q1/Q2; see `KBN-101-DB-ROLE-SPLIT.md` rc.20.
No coder lane may start while the build hold is active. KBN-010 must complete before KBN-100; KBN-105 exact endpoint/DTO freeze must complete before any API consumer implementation.
+10 -2
View File
@@ -8,6 +8,14 @@
## Amendment record
### 1.0.0-rc.20 — KBN-101 Envelope A: declarative sink-RBAC + per-role connection-selection + RLS write-source + sink-resident User-override
- **Choice:** adds the fixed User/Orchestrator/others runtime rung-roles (per-ROLE, deny-by-default; `mosaic_runtime_user`, `mosaic_runtime_orchestrator` + their capability roles), amends the frozen §4 L176 CONNECT allowlist for the two new logins (TEMPORARY-denied), and authorizes declarative RLS `WITH CHECK`/`USING` + `FORCE ROW LEVEL SECURITY` + a sink-resident `task_status_write_override` table on `tasks` — a schema-v1 mechanism addition beyond the prior grant/revoke-only model.
- **No per-federated-user topology:** DB-role/credential cardinality remains fixed at 3, independent of federated-user count; the override table is keyed `(workspace_id, task_id, tier)`, never per-writer; per-writer attribution is a metadata column (`task_events.actor_id`), never a DB principal.
- **Non-effect:** rc.4 SI-001, all KCR-001016 decisions, and every prior rc (rc.5rc.16) invariant not explicitly named above remain unchanged. Introduces no new principal, key, or per-user authority axis (Q1 HALT boundary not hit).
- **Authority:** Jason B1 ruling + Mos OPTION A (home the layer) + Mos Q1 (TIER-LEVEL) + Mos Q2 (RLS `WITH CHECK` authorized, two mandatory conditions — `ENABLE`/`FORCE ROW LEVEL SECURITY` as terminal migration step, non-owner rung roles). Exact implementation detail is normative in [`KBN-101-DB-ROLE-SPLIT.md`](./KBN-101-DB-ROLE-SPLIT.md) §4/§10 and the KBN-101 Envelope A v6 record.
- **Gate:** lands as `KBN-101-10` (new owner card) + responsibility-widenings on `KBN-101-00/01/02/03/05/06`; the `tasks`/override-table DDL itself is homed in the **KBN-100** producer (starts after KBN-101-08) per the existing §4 L178 producer/consumer precedent — a SPEC note today, not an immediate implementation.
### 1.0.0-rc.16 — Current generic storage-wrapper authority closure
- **Current-source truth:** `packages/storage/src/cli.ts` currently shells `storage migrate --run` directly to `pnpm --filter @mosaicstack/db db:migrate` through `execSync`; no `mosaic-db-migrator` executable exists. README and user-guide command guidance therefore remove that command and any runner-delegation claim. The current wrapper is legacy N-1, uncertified, non-operative, and MUST NOT be invoked pending KBN-101-02/-03/-06/-08 activation.
@@ -79,14 +87,14 @@
- **Exact ledger:** manifest v1 canonically serializes journal logical index/tag and SHA-256 of exact shipped migration bytes. It maps each observed ledger hash to one tuple; physical insertion order is non-normative, while missing/unknown/duplicate/ambiguous/corrupt/stale states fail closed. Shipped `0009` bytes remain unchanged; a missing/effects-absent `0009` runs normally, an applied-late hash maps normally, and partial/full effects with missing hash require backup restoration or separately reviewed repair—not manual adoption.
- **TLS/search path:** operator/IaC owns CA and server leaf lifecycle, exact compose/Swarm secret mounts, server TLS activation, service-DNS SANs, verified-TLS readiness, transition, CA overlap rotation, and rollback. Runtime/migrator use `verify-full`; PGlite is not PostgreSQL TLS evidence. Application sessions use only `pg_catalog,mosaic`; no URL/config-derived identifier reaches SQL.
- **Safe release:** cards 0007 land prepared but inactive; owner-runtime deployments remain N-1. Mosaic control plane/Jason alone authorizes one atomic TLS/roles → runner → readiness → runtime activation or rollback. No runtime-operator compatibility switch, bypass, plaintext interval, or force-on-red exists; all temporary support is removed before KBN-101-08.
- **Non-effect:** role graph, immutable certification after KBN-100, KBN-105 gate, rc.5s preserved rc.4 SI-001 invariants, and all KCR-001016 decisions remain unchanged. Exact detail is normative in [`KBN-101-DB-ROLE-SPLIT.md`](./KBN-101-DB-ROLE-SPLIT.md).
- **Non-effect:** role graph (**except as amended by rc.20 — see below**), immutable certification after KBN-100, KBN-105 gate, rc.5s preserved rc.4 SI-001 invariants, and all KCR-001016 decisions remain unchanged. Exact detail is normative in [`KBN-101-DB-ROLE-SPLIT.md`](./KBN-101-DB-ROLE-SPLIT.md).
### 1.0.0-rc.5 — KBN-101 role/connection split
- **Choice:** PostgreSQL `standalone` and `federated` runtime uses `DATABASE_URL` only as a non-owner `mosaic_runtime` login; an explicit migration phase uses `DATABASE_MIGRATION_URL` only as `mosaic_migrator`, which `SET ROLE`s to non-login `mosaic_schema_owner` for DDL. Local PGlite remains an explicit embedded exception.
- **No fallback / no startup DDL:** missing migration URL fails the migration phase; it never falls back to runtime URL/default/config. Gateway replicas do not run migrations. An advisory-locked migration phase verifies the exact ordered Drizzle ledger fingerprint before replicas may become ready.
- **Privilege model:** non-login `mosaic_platform_database_owner` is outside application paths; `mosaic_schema_owner` owns only application/ledger schemas. `mosaic_runtime` has only `mosaic_runtime_capability`, owns no object/schema, cannot assume owner/migrator, has no TEMPORARY privilege, has only read access to the Drizzle ledger, and must fail startup if effective identity, unsafe attributes, authenticated TLS, search path, schema version, grants, or immutable relation privileges differ from the frozen contract. `task_events`, `artifacts`, `task_checkpoints`, `task_checkpoint_artifacts`, and `approval_decision_artifacts` grant runtime only INSERT/SELECT; KBN-100 retains RESTRICT/no-cascade semantics.
- **Non-effect:** rc.4 SI-001 candidate-key/FK order and all KCR-001016 tenancy, SOT, proposal-audit, approval, fence, recovery, no-cascade, endpoint, and wire invariants are unchanged. This amendment neither creates roles/secrets nor changes production deployment.
- **Non-effect:** rc.4 SI-001 candidate-key/FK order and all KCR-001016 tenancy, SOT, proposal-audit, approval, fence, recovery, no-cascade, endpoint, and wire invariants are unchanged. This amendment neither creates roles/secrets nor changes production deployment (**except as amended by rc.20 — see below**).
- **Gate:** KBN-101s role/schema-boundary foundation certificate, Vault/redaction/rotation, N-1/rollback, and independent security GO are mandatory before KBN-100. After KBN-100 creates the immutable relations, KBN-101 real deployed-role immutable-operation certification plus Ultron GO is mandatory before KBN-105; synthetic test-role success alone is insufficient. Exact implementation detail is normative in [`KBN-101-DB-ROLE-SPLIT.md`](./KBN-101-DB-ROLE-SPLIT.md).
### 1.0.0-rc.4 — KBN010-SI-001 (preserved)
@@ -0,0 +1,34 @@
# #830 Documentation Completion Checklist
## Required artifacts
- [x] `docs/PRD.md` contains the M1 compaction-refresh trust-lifecycle requirements and acceptance criteria.
- [x] Operator behavior and recovery are documented in `docs/guides/lease-broker-operations.md`.
- [x] Developer architecture and protocol behavior are documented in `docs/architecture/compaction-revocation.md`, `lease-broker-protocol.md`, and `mutator-class-gate.md`.
- [x] Security boundaries and residuals are documented in `docs/architecture/lease-broker-security.md` and `compaction-revocation.md`.
- [x] `docs/SITEMAP.md` links the new architecture page.
- [x] User-guide changes are not applicable: observers are mandatory internal runtime controls with no end-user workflow.
- [x] OpenAPI/endpoint changes are not applicable: the broker remains an internal Unix-socket protocol, not a public HTTP API.
## Contract coverage
- [x] Claude and Claudex lifecycle signals, matchers, commands, and fail-closed behavior are documented.
- [x] Pi pre-/post-compaction signals and session replacement reasons are documented.
- [x] Private generation-file ownership, monotonic update, same-PID replacement, and failure fencing are documented.
- [x] `revoke_lease` input purpose, broker response state, and denial behavior are documented.
- [x] T12b/T30 explicitly names the bounded residual stale window and reports within-TTL **ALLOWED** / after-TTL **DENIED**.
- [x] Documentation explicitly disclaims a within-window mutator-action bound.
- [x] T-A, T-C, same-principal, and protected-branch boundaries are retained.
## Structure and review
- [x] New architecture content is under `docs/architecture/`.
- [x] This report is under `docs/reports/compaction-refresh/`.
- [x] Session evidence is under `docs/scratchpads/`.
- [x] Documentation changes are in the same logical change set as code and tests.
- [ ] Independent exact-head code and Opus security reviews pending coordinator sequencing after the deterministic-main rebase gate.
## Publishing
- [x] Canonical documentation remains in-repository.
- [x] No external publishing target is required for this internal M1 control.
@@ -0,0 +1,54 @@
# FCM-M5-001 Fleet Documentation Deferrals and Holds
**Issue:** #758 · **Branch:** `docs/758-fleet-config-operator-docs`
These are accepted existing DAG boundaries, not omissions silently claimed as delivered.
## FCM-M3-002 hold
- Boot/reboot preservation for roster members persisted stopped or disabled.
- Current installation may enable all agent units, while the launcher projection does not yet carry
`lifecycle.enabled` or `desired_state`; documentation therefore does not claim lifecycle-safe reboot.
- Heartbeat/liveness integration into roster-v2 `status`, `doctor`, and `verify`; current observations
cover systemd active state, tmux sessions, holder ownership, and unmanaged sessions only.
## FCM-M4-002 hold
- Executable v1-to-v2 cutover, reversible canary, and rollback.
- Stale-projection/orphan migration classification and current-host managed/unmanaged fixture coverage.
- Any live migration, lifecycle, systemd/tmux/session, or rollback action.
M5 docs describe prerequisites and the preview boundary only. A ready preview is not migration or rollback evidence.
## Explicit validate-operation gap
- `FCM-REQ-03` requires a documented programmatic `mosaic fleet validate` operation.
- The current CLI does not expose that operation. Existing mutation/reconcile validation and the
documentation example test are not a replacement for the missing command.
- FCM-M5-001 documents this implementation gap without inventing syntax, JSON, exit behavior, or an
owning implementation card. Parent #758 must remain open until the requirement is implemented and
evidenced or the PRD/DAG is explicitly revised through the authoritative process.
## FCM-M5-002 hold
- Deterministic source-versus-installed asset revision detection and safe refresh implementation.
- Rolling local canary, independent validator certificate, final release evidence, merge-gate approval, and parent #758 closure.
`operations/upgrade-assets.md` is therefore a fail-closed hold, not an invented procedure.
## Compatibility interpretation
The M0 cross-cutting row requiring every retained/migrated artifact to validate through the executable contract is satisfied by each artifact's declared executable disposition, not by forcing versioned v1 fixtures through the v2 parser:
- retained examples are explicit `version: 1` fixtures validated by the production v1 parser;
- canonical profiles validate through the shared baseline plus `roles.local` resolver;
- the service preset validates through its production service-policy reader;
- migration candidates validate through the production v2 compiler and shared semantic resolver.
The executable disposition inventory rejects undeclared additions/removals and prevents silent legacy drift.
## Repository-wide documentation structure
The accepted #758 IA is the domain book under `docs/fleet/`. Creating global `USER-GUIDE`, `ADMIN-GUIDE`, or `DEVELOPER-GUIDE` books and cleaning unrelated pre-existing `docs/` root files are outside this bounded card. The repository sitemap links the fleet book. No HTTP/API/auth contract changed, so OpenAPI and endpoint-index updates are not applicable.
Canonical documentation remains in-repository; no external publishing or generated publishing output is in scope. Parent issue #758 stays open through M5.
@@ -0,0 +1,44 @@
# FCM-M5-001 Fleet Documentation IA Closure Evidence
**Issue:** #758 · **Task:** FCM-M5-001
## Artifact map
- Fleet entry point and desired/observed decision tree: `docs/fleet/README.md`.
- Concepts: `docs/fleet/concepts/` covers authority/projections, identity separation, role authority/leases, and the generated launch chain.
- Operator workflows: `docs/fleet/how-to/` covers CRUD, lifecycle, interaction and validator instances, and role overrides.
- Operations: `docs/fleet/operations/` covers reconciliation/recovery, quarantine, systemd/tmux troubleshooting, backup/restore boundaries, and upgrade-asset holds.
- References: executable schema, complete field/default/constraint reference, CLI/JSON/exit behavior, lifecycle/status/drift, role authority, and generated environment boundary under `docs/fleet/reference/`.
- Migration: preview field map, lifecycle preservation, backup/recovery prerequisites, aliases, and executable artifact dispositions under `docs/fleet/migration/`.
- Navigation: `docs/SITEMAP.md` and the fleet entry point.
## Acceptance mapping
| Checklist area | Evidence |
| ------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Roster authority and fail-closed legacy handling | Root PRD FCM-REQ-01/05/08; desired/observed and quarantine pages. |
| Classes and authority | Root PRD FCM-REQ-07; role authority concept/reference; configurable interaction/validator how-tos. |
| Lifecycle | Root PRD FCM-REQ-04; lifecycle transition table and operator lifecycle how-to. |
| Local-only generated launch boundary | Root PRD FCM-REQ-05/09; generated launch concept/reference. |
| Complete DAG and artifact inventory | `docs/TASKS.md`; M0 inventory; executable disposition tests. |
| IA pages | Every path named by the M0 checklist exists and is linked from `docs/fleet/README.md`. |
| Examples | `docs/fleet/examples/roster-v2.yaml` validates through production v2 compiler/shared resolver; shipped artifact dispositions validate through declared production readers. |
| Links | Deterministic local Markdown link test covers the entire fleet book and sitemap, including local heading-fragment resolution. |
| Sensitive/example safety | Validator scans backtick- and tilde-fenced fleet-book examples plus the canonical roster for sensitive-looking keys, common credential formats (including Anthropic, OpenAI project, and Stripe restricted keys), path-qualified privileged commands, package-manager/root commands, arbitrary command override, and hardcoded Tess/Ultron identities; findings report only file/block and violation kind, never matched values. |
| Holds | `docs/reports/deferred/758-fleet-config-deferrals.md` records M3-002, M4-002, M5-002, compatibility, and repository-structure boundaries. |
## Documentation completion checklist
- [x] Root PRD exists and remains the #758 requirements authority.
- [ ] The accepted project-specific fleet book is indexed, but it is not complete against `FCM-REQ-03`: the required explicit programmatic `mosaic fleet validate` operation is not implemented. The CLI reference and deferral report record this gap without inventing behavior.
- [x] Sitemap links the fleet entry point and operator-critical pages.
- [x] No HTTP/API/auth contract changed; OpenAPI/endpoint rows are not applicable.
- [x] Working evidence remains under `docs/scratchpads/`; closure and deferral evidence remains under `docs/reports/`.
- [x] Canonical source remains in-repository; no external publishing action is in scope.
- [ ] Independent exact-head documentation review, PR CI, and FCM-M5-002 release certificate remain post-PR gates and are not claimed here.
## Live-action boundary
No migration, canary, rollback, deployment, systemd/tmux/session operation, generated projection, or product mutation was performed. `roster.yaml` remains the sole writable desired-state authority. `mos-comms` remains temporary. Parent issue #758 remains open.
Validation command results and exact commit/tree evidence are recorded in the task scratchpad and PR body after execution.
+449
View File
@@ -0,0 +1,449 @@
# RFC-001 — MACP: A Mosaic-Native, Matrix-Native Comms Layer
- **Status:** DRAFT — for Team Lead → Orchestrator staffing
- **Author:** MS-LEAD (reviewer identity `ms-lead-reviewer`)
- **Sponsor / veto:** Jason (human lead)
- **Date:** 2026-07-24
- **Program:** Mosaic Stack comms-evolution
- **Supersedes backbone:** the Hermes MCP chat bridge (strangler-retired, see §9)
- **Audience:** Team Leads, the Mosaic orchestrator, infra, and any harness maintainer (Claude Code / Codex / Pi / Goose)
> This is a **design document**. No code ships from this RFC. It exists to be decomposed into missions (P1→P5, §10) with per-phase acceptance criteria. Where a claim is uncertain or needs live validation, it is flagged **[VERIFY]**.
---
## 0. TL;DR
We are building a **Mosaic-native comms layer on Matrix**. We self-host a **Synapse** homeserver and register a privileged **Mosaic Application Service** (the "appservice") that the orchestrator controls. The appservice bulk-provisions one Matrix identity per agent-spin, creates and manages rooms, posts agent introductions, and tracks presence/liveness. Agents talk to it through a thin `packages/comms` client SDK. **tmux stays the P0 same-host fast path**; Matrix is the durable, presence-aware, federated layer above it, and **MACP** (the Mosaic Agent Comms Protocol) is the standard that says which path to use when. Federation is **per-site Synapse homeservers federated over TLS we control** — a direct answer to "the homelab agent went dark and took comms with it." We adopt **Buzz's patterns** (auto-detect/enroll, signed identity, unified event log, humans-and-agents on one surface) without adopting Buzz/Nostr as transport. We migrate off Hermes by the **strangler** pattern: stand native alongside, move channels as proven, retire at parity.
The **first standalone shippable slice is presence** (P1).
---
## 1. Goals / Non-Goals
### 1.1 Goals
- **G1 — Presence & liveness first.** A Team Lead must be able to answer "is my coordinator online, away, or dead?" in seconds, not by polling for 13 hours. Presence is the P1 slice and ships before anything else.
- **G2 — A native backbone we own.** Replace _Hermes-as-backbone_ with a self-hosted Synapse + a Mosaic-controlled appservice. External chat bridging becomes an optional edge, not the spine.
- **G3 — Turnkey harness enrollment.** `mosaic enroll` auto-detects the harness and self-registers the agent via the appservice on spin. No hand-rolled per-bot identity juggling.
- **G4 — A real protocol (MACP v1).** Structured, versioned event schema over Matrix custom event types; a documented routing contract for tmux vs Matrix; a documented escalation policy.
- **G5 — No central SPOF.** Per-site homeservers federated over TLS/DNS we already control, so one site going dark cannot take the fleet's comms with it.
- **G6 — Gate-action integrity.** Reviews / merges / approvals carry **signed authorship** (Buzz pattern) so a gate-critical action is cryptographically attributable, retiring the fragile "distinct bot identity" juggling.
### 1.2 Non-Goals
- **NG1 — Do NOT rip out working comms mid-MVP.** tmux fast-path and the existing `mos-comms` git-branch channel keep working until their replacement is proven at parity. This RFC is strangler, not big-bang.
- **NG2 — tmux is NOT being replaced.** tmux inter-agent comms remains **P0**. Matrix is _above_ it, not instead of it. MACP defines the boundary; it does not move it.
- **NG3 — Not adopting Buzz/Nostr as transport.** We adopt Buzz's _patterns_; the wire is Matrix.
- **NG4 — Not building a new chat client in P1P4.** HIL uses an existing Matrix client (Element or equivalent) until/unless a custom client is justified (open question, §11).
- **NG5 — Not federating to the public Matrix network.** Federation is Mosaic-site-to-Mosaic-site over infrastructure we control. Public `matrix.org` federation is out of scope (and should likely be firewalled off).
- **NG6 — Not a Hermes feature-clone.** We reach _parity on the channels that matter_ (§9 checklist), not bug-for-bug Hermes compatibility.
---
## 2. Architecture
### 2.1 Layer diagram
```
┌───────────────────────────────────────────────┐
│ HUMAN (Jason / HIL) │
│ Element (or custom client) — §11 │
└───────────────────────┬───────────────────────┘
│ (same Matrix surface as agents)
┌──────────────────────────────────────────────▼──────────────────────────────────────────────┐
│ SYNAPSE HOMESERVER (self-hosted, ours) │
│ - Client-Server API (agents + humans send/receive events) │
│ - Application Service API (privileged AS hooks: transactions, user/room namespaces) │
│ - Presence EDUs, receipts, typing │
│ - Federation API (S2S) over TLS ── to peer site homeservers (§6, P4) │
└───────▲───────────────────────────────────▲──────────────────────────────────────▲───────────┘
│ AS API (hs_token / as_token) │ C-S API (per-agent access_token) │ S2S
│ │ │
┌───────┴───────────────────────┐ ┌────────┴─────────────────┐ ┌─────────┴──────────┐
│ MOSAIC APPSERVICE │ │ packages/comms (SDK) │ │ PEER SITE Synapse │
│ (apps/matrix-appservice) │ │ used by every harness │ │ (site-B, site-C…) │
│ THE "native layer" │ │ - login/whoami │ │ own appservice │
│ replacing Hermes-backbone │ │ - send MACP events │ │ own agents │
│ │ │ - subscribe/sync │ └────────────────────┘
│ - bulk-provision MXIDs │◄───┤ - presence heartbeat │
│ (@mosaic_<agent>:site) │ │ - signed-authorship │
│ - create/manage rooms │ │ envelope (gate acts) │
│ - post introductions │ └────────────┬─────────────┘
│ - track presence/liveness │ │ in-process / IPC
│ - enforce room taxonomy │ ┌─────────▼──────────────────────────────────────────┐
│ - escalation watchdog (§5) │ │ AGENT HARNESS │
│ - controlled by ORCHESTRATOR │ │ Claude Code / Codex / Pi / Goose │
└───────▲───────────────────────┘ │ `mosaic enroll` runs on spin (§4.1) │
│ orchestrator drives AS └─────────┬───────────────────────────────────────────┘
┌───────┴───────────────────────┐ │
│ MOSAIC ORCHESTRATOR │ │ P0 FAST PATH (same host, low-latency)
│ (~/.config/mosaic) │ ┌─────────▼──────────┐ tmux send-keys / pane I/O
│ spins agents, owns rooms, │◄──────►│ tmux (P0) │◄─►│ peer agent on same host │
│ sets escalation policy │ MACP └────────────────────┘ └─────────────────────────┘
└───────────────────────────────┘ routing rules decide tmux vs Matrix per message (§4.6)
```
Key idea: **the appservice is the backbone.** It is a long-lived privileged process registered with Synapse via an appservice registration file (`hs_token`/`as_token`, namespaces). It is the thing that used to be "Hermes-as-backbone," except we own it, it is inside the orchestrator's control plane, and it speaks native Matrix.
### 2.2 Message flow: agent spin-up → auto-enroll → room join → introduction → presence-online
```
Orchestrator Harness (mosaic enroll) Mosaic Appservice Synapse
│ │ │ │
1. spin agent ─────────────────► │ │ │
│ │ 2. auto-detect harness │ │
│ │ (Claude/Codex/Pi/Goose) │ │
│ │ 3. POST /enroll {agent meta} ─► │
│ │ │ 4. provision MXID │
│ │ │ @mosaic_<agent>:site │
│ │ │ via AS API register ─► (201, in namespace)
│ │ │ 5. mint access_token │
│ │ 6. ◄── {mxid, token, rooms}──┤ (or as_token masq) │
│ │ │ 7. invite+join rooms ─► (mission/team/fleet)
│ │ 8. /sync (via packages/comms)─────────────────────────► (joined state)
│ │ │ 9. post introduction ─► m.room.message +
│ │ │ (mosaic.introduction) custom event → rooms
│ │ 10. set presence ONLINE ─────────────────────────────► presence EDU
│ │ 11. start heartbeat loop │ │
│ │ (mosaic.presence ping) │ │
│ 12. appservice reports agent │ │ │
│ ◄──── live in fleet room ────┤ (watchdog now tracks liveness) │
```
Notes on the steps that matter:
- **Step 4/5** use the **Application Service API**: the appservice can register users inside its namespace (`@mosaic_*:site`) and act on their behalf. Two viable modes: (a) mint a real per-agent `access_token` via appservice login, or (b) have the appservice **masquerade** using `user_id` query param on C-S calls with the `as_token`. **Recommendation: mint per-agent tokens** for P2 so the agent process holds only its own credential (blast-radius containment, §8); reserve masquerade for bulk/bootstrap operations the appservice itself performs. **[VERIFY]** exact token-lifetime and refresh behavior against the running Synapse version.
- **Step 9** — the introduction is both a human-readable `m.room.message` _and_ a structured `mosaic.introduction` custom event (so other agents can machine-parse capabilities without scraping prose).
- **Step 10/11** — presence goes online immediately, then a **heartbeat** keeps liveness fresh. Native Matrix presence auto-decays to `unavailable`/`offline`, but we do **not** rely solely on it (Synapse presence timeouts are coarse and federation presence is lossy **[VERIFY]**); MACP adds an explicit `mosaic.presence` heartbeat event for deterministic liveness (§4.5, §5).
---
## 3. Repo-home decision (RESOLVED — recommendation)
The core tension: **product monorepo** (`mosaicstack/stack`, this checkout `/src/mosaic-stack`) vs **framework** (`~/.config/mosaic`, the agent/harness runtime that every agent shares regardless of product). The boundary rule I am ratifying:
> **Product-monorepo owns the deployed _services and libraries_. Framework owns the _agent/harness contract_ — anything an agent needs the moment it spins, before any product code is checked out.**
Applying that rule:
| Piece | Home | Rationale |
| ---------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Synapse deployment** (compose/helm, config, TLS, `.well-known`, pipelines) | **Product monorepo**`infra/matrix/` + CI in the monorepo's pipeline dir | It is deployed infrastructure with the same lifecycle/observability as Gateway/Web/DB. Lives beside other `infra/` and Woodpecker pipelines. |
| **Mosaic Appservice** | **Product monorepo**`apps/matrix-appservice` | It is a first-class deployed service (NestJS-style app, same conventions: ESM, `@Inject()`, DTOs, OTEL-before-bootstrap). It talks to Synapse, holds `hs_token`/`as_token`, and is operated like any other app. It is **controlled by** the orchestrator but **is not** the orchestrator. |
| **Client SDK** | **Product monorepo**`packages/comms` | A workspace library consumed by product apps _and_ by harnesses. Published/linked like `packages/queue`, `packages/db`. Versioned with the product. |
| **MACP spec** (the standard doc + JSON schemas) | **Framework**`~/.config/mosaic/spec/macp/` (mirrored/vendored into `packages/comms` at build) | The protocol is an agent-level contract that must exist independent of any one product checkout. Framework is the source of truth; `packages/comms` vendors a pinned copy so the SDK and spec cannot drift silently. |
| **`mosaic enroll` harness glue** (auto-detect, spin hook) | **Framework**`~/.config/mosaic/tools/enroll/` | Agents/harnesses are framework-level. Enrollment must run _before_ the agent has product context; it cannot depend on `/src/mosaic-stack` being present. This is exactly parallel to the existing `~/.config/mosaic/tools/*` wrappers. |
**Boundary summary:** _the wire and the services are product; the contract and the spin-time glue are framework._ The one deliberate coupling is **MACP**: framework is authoritative, but `packages/comms` pins a vendored copy and CI fails if they diverge, so an agent enrolling via framework and a service validating via `packages/comms` agree on the schema by construction.
**Rejected alternative:** putting the appservice in the framework. Rejected because the appservice is a stateful, deployed, secret-holding network service that needs the product's CI/observability/secret plumbing; burying it in `~/.config/mosaic` would split its operational story from every other Mosaic service.
---
## 4. MACP v1 — the standard
MACP (Mosaic Agent Comms Protocol) v1 is a **profile of Matrix**: it does not invent a transport, it constrains how Mosaic agents use Matrix so that behavior is uniform across harnesses. Versioned via a `macp_version` field on every custom event; v1 is frozen at ratification (P3).
### 4.1 Enrollment contract
`mosaic enroll` MUST, on agent spin, in order:
1. **Auto-detect harness.** Detection order + signal:
- Claude Code — presence of the Claude Code runtime/env (e.g. `CLAUDE_CODE_*` env, `~/.claude`) **[VERIFY exact signal per harness]**
- Codex — Codex runtime markers
- Pi — Pi SDK runtime (`packages/agent` / `packages/mosaic` context)
- Goose — Goose runtime markers
- Fallback: explicit `--harness` flag; if undetectable, enroll as `generic` and warn.
2. **Provision identity** — call appservice `POST /enroll` with `{agent_slug, harness, host, mission_id?, team_id?, capabilities[]}`. Appservice returns `{mxid, access_token, homeserver, rooms[]}` (§2.2 step 46).
3. **Join rooms** — accept invites / join the returned room set per taxonomy (§4.6).
4. **Introduce** — post `mosaic.introduction` (+ human-readable `m.room.message`) to each joined room.
5. **Go present** — set Matrix presence `online` and start the `mosaic.presence` heartbeat loop.
Enrollment is **idempotent**: re-running `mosaic enroll` for an existing agent slug rebinds to the same MXID (re-mints token if needed) rather than creating a duplicate identity. This is what retires the "distinct bot identity juggling."
### 4.2 Structured event schema (Matrix custom event types)
All MACP events carry a common envelope in `content`:
```jsonc
{
"macp_version": "1.0",
"macp_type": "<see below>",
"agent": { "mxid": "@mosaic_teamlead-3:site-a", "slug": "teamlead-3", "harness": "claude-code" },
"ts": 1753300000000,
"mission_id": "KBN-101", // optional
"signature": { ... } // present ONLY for gate actions, §4.4
// ...type-specific fields...
}
```
Event types (Matrix `type` shown; timeline events use `m.room.message` with a custom `msgtype` where a human-visible fallback is desirable, state events use a dotted custom `type`):
| MACP type | Matrix carrier | Purpose | Notable fields |
| --------------------- | --------------------------------------------------------------------------------------- | -------------------------------------------- | ---------------------------------------------------------------------------------------------- |
| **message** | `m.room.message`, `msgtype: "mosaic.message"` (+ `body` fallback) | ordinary agent/human chat | `body`, `thread?` |
| **presence** | `m.room.message` msgtype `mosaic.presence` in fleet room **or** custom EDU-backed state | heartbeat/liveness ping | `status: online\|away\|offline`, `seq`, `interval_ms` |
| **workflow-step** | state event `mosaic.workflow.step` (state_key = step id) | durable record of a workflow/mission step | `step`, `status: started\|blocked\|done\|failed`, `detail` |
| **review / approval** | `m.room.message` msgtype `mosaic.review` | a review verdict or merge/approval | `subject` (PR/commit ref), `verdict: approve\|reject\|request-changes`, `signature` (REQUIRED) |
| **escalation** | `m.room.message` msgtype `mosaic.escalation` | raise a stuck/dark condition to HIL/fallback | `reason`, `target`, `severity`, `since_ts` |
Rationale for the carrier split: **timeline events** (`m.room.message` variants) are the durable, receipted, replayable log (this _is_ Buzz's "unified event log," §7). **State events** (`mosaic.workflow.step`, presence-as-state) give last-writer-wins current status that a newly-joined agent reads instantly from room state without replaying history.
Every custom event is validated against a JSON Schema shipped in the MACP spec (§3). Unknown/newer `macp_version` → consumers MUST degrade gracefully (render `body`, ignore unknown fields).
### 4.3 Agent identity model
- **MXID:** appservice-namespaced `@mosaic_<slug>:<site-domain>`. The `mosaic_` prefix is the AS **user namespace** declared in the registration file (`namespaces.users` regex `@mosaic_.*`), so Synapse routes those users to our appservice and no human can squat the namespace.
- **Provisioning:** exclusively via the appservice (AS API register). Never hand-created. One MXID per agent-spin; idempotent rebind on re-enroll (§4.1).
- **Signed authorship** overlays identity for gate actions only (§4.4). MXID answers "who is this account"; signature answers "did the real key-holder authorize this gate action."
### 4.4 Signed-authorship for gate-critical actions (Buzz pattern, scoped)
Gate-critical = **reviews, merges, approvals** — anything that can move code to `main` or unblock a mission gate.
- Each enrolled agent is issued (or generates) an **Ed25519 keypair**; the **public** key is registered with the appservice at enrollment and published as agent profile state (`mosaic.identity.pubkey`). Private key custody per §8.
- A gate action event carries `content.signature = { alg: "ed25519", key_id, sig }` over a canonical serialization of the envelope (canonical-JSON of `{macp_type, agent.mxid, subject, verdict, ts, mission_id}`).
- Verifiers (the appservice gate-watcher, and any agent acting on a verdict) MUST reject an unsigned or bad-signature gate event. Non-gate events are unsigned (keeps the hot path cheap).
This is deliberately **narrow**: we do not sign every chat line (Buzz signs everything; we take the pattern only where forgery has teeth). Scope may widen post-P5 if warranted.
### 4.5 Presence & liveness model
Three visible states plus an explicit heartbeat:
- **online** — agent set presence online AND last `mosaic.presence` heartbeat within `heartbeat_interval` (default **30s [VERIFY tuning]**) × miss-tolerance (default 2).
- **away** — presence `unavailable`, or heartbeats late but < dark threshold.
- **offline / dark** — no heartbeat for `dark_threshold` (default **N minutes**, policy value, §5/§11) OR presence `offline`.
Why not rely on native Matrix presence alone: Synapse presence is (a) coarse-grained, (b) can be disabled for load reasons, and (c) **degrades across federation** **[VERIFY]**. So MACP layers an explicit heartbeat carried as a lightweight timeline/state event in the **fleet presence room**, giving a deterministic, federation-safe liveness signal the escalation watchdog (§5) can reason about. Native presence EDUs are still emitted (they make Element show the right dot for humans) but the _authoritative_ liveness source is the heartbeat.
### 4.6 Room / channel taxonomy (orchestrator-owned)
The **orchestrator** (via the appservice) owns room lifecycle. Agents never create backbone rooms ad hoc.
| Room | Scope | Membership | Purpose |
| ----------------------- | ------------------------------------------ | ---------------------------------------- | ----------------------------------------------------------------------- |
| **Fleet presence room** | one per site (federated view across sites) | every enrolled agent + HIL | heartbeats, the single "who's alive" board. This is the P1 deliverable. |
| **Per-mission room** | one per mission (e.g. `#mission-KBN-101`) | agents on that mission + Team Lead + HIL | workflow-steps, mission chat, reviews for that mission |
| **Per-team room** | one per team | team members + Team Lead | intra-team coordination |
| **HIL room** | one (or one per site) | humans + escalation-privileged agents | where escalations land; Jason's pane on the fleet |
Rooms are created with orchestrator-controlled power levels: appservice = admin (PL100), Team Leads elevated, worker agents default. Room aliases (`#mission-KBN-101:site-a`) are stable handles.
### 4.7 tmux ↔ Matrix routing rules (the fast-path/durable boundary)
MACP mandates this decision per message. **Default bias: if it must survive the agent, be seen by an offline party, cross a host, or be audited — Matrix. If it is same-host, synchronous, and ephemeral — tmux.**
| Signal | Route | Why |
| ---------------------------------------------------------------- | ---------------------------------------------- | --------------------------------------------------------------------------- |
| Same-host, live pane-to-pane prompt/nudge, sub-second need | **tmux (P0)** | lowest latency, no server round-trip; this is the working fast path we keep |
| Recipient may be offline / on another host | **Matrix** | durability + store-and-forward; tmux can't reach a dark or remote pane |
| Presence / heartbeat / liveness | **Matrix** | must be observable fleet-wide, including by the watchdog and HIL |
| Workflow-step, review/approval, escalation | **Matrix** | must be durable, receipted, auditable, signed (gate acts) |
| Cross-site anything | **Matrix (federated)** | tmux is same-host only |
| Bulk log spew / high-frequency scratch between co-located agents | **tmux**, with periodic **Matrix** checkpoints | avoid flooding the durable log; keep an audit checkpoint |
Rule of thumb encoded in the SDK: `comms.send()` takes a `durability` hint; `ephemeral+same-host` short-circuits to tmux, everything else goes Matrix. A message can be **dual-routed** (tmux for immediacy + a Matrix checkpoint) when both speed and durability matter.
---
## 5. Coordinator-availability + HIL escalation (the 13h-stall / homelab-dark fix)
**The failure we are killing:** a Team Lead blocked ~13h, polling every 15 min, unable to distinguish "coordinator offline" from "coordinator busy"; and a homelab agent that goes dark taking comms with it.
**The fix — presence-driven, policy-encoded escalation:**
1. **Deterministic liveness (§4.5).** Every agent heartbeats into the fleet presence room. The appservice **escalation watchdog** subscribes and maintains `last_seen` per agent. No polling by the Team Lead — it _subscribes_ (Matrix `/sync` long-poll) and is pushed state changes.
2. **Encoded policy in MACP:** a machine-readable escalation policy attached to each agent/role, e.g.:
```jsonc
{
"role": "team-lead",
"coordinator": "@mosaic_coordinator-1:site-a",
"dark_threshold_min": 10, // OPEN QUESTION §11 — Jason/Mos to set N
"on_coordinator_dark": {
"action": "escalate",
"fallback": "@mosaic_coordinator-2:site-b", // cross-site fallback
"then": "notify-HIL",
"hil_room": "#hil:site-a",
},
}
```
3. **Detection & action by the Team Lead:** when the watchdog (or the Team Lead's own SDK) observes `coordinator.last_seen > dark_threshold`, it:
- emits a `mosaic.escalation` event into the mission room and the HIL room (`reason: "coordinator dark", since_ts, severity`),
- **re-routes** to the declared fallback coordinator (possibly on another site — this is why federation matters),
- if no fallback answers within a second threshold, pages **HIL** (Jason) in the HIL room.
The Team Lead **never sits blocked polling**; a dark coordinator is a _pushed event_, and the fallback/HIL path is automatic.
4. **Homelab-dark specifically:** because heartbeats are federated into a cross-site fleet room, a whole _site_ going dark is visible from other sites — the watchdog on site-B sees site-A's coordinator stop heartbeating and triggers the same escalation. A dark homelab can no longer silently strand its agents, because the liveness signal and the fallback live _off that host_.
**Design invariant:** liveness authority and fallback targets must never be co-located with the thing they monitor. The watchdog for site-A's coordinator should also run (or be mirrored) on site-B.
---
## 6. Federation (P4)
**Model:** each site runs its **own Synapse homeserver** with its **own Mosaic appservice** and its own agents. Sites **federate** with each other over the standard Matrix server-to-server (S2S) API, restricted to Mosaic sites.
**Why per-site, not one central server:**
- **No SPOF.** The homelab going dark is the founding trauma of this program. A single central homeserver would recreate exactly that risk at fleet scale. Per-site means a site outage is contained: its agents drop, but every other site's comms and the cross-site fleet room survive.
- **Locality.** Same-site agents get low-latency local homeserver traffic; only cross-site events pay the federation cost.
- **Blast radius.** A compromised or misbehaving site can be defederated without touching the rest.
**How federation is wired (real Matrix mechanics):**
- **Server discovery** via `https://<domain>/.well-known/matrix/server` returning `{"m.server": "matrix.<domain>:443"}`, and/or an `_matrix._tcp` **SRV** record. We control the DNS/domains, so we control the federation graph. **[VERIFY]** current `.well-known` vs SRV precedence for the deployed Synapse version.
- **TLS:** federation requires valid TLS on the federation endpoint; we terminate with certs from our own CA/ACME on domains we own.
- **Allowlist:** use Synapse `federation_domain_whitelist` to restrict federation to the set of Mosaic site domains — **no public-network federation** (NG5). This is a hard security boundary.
- **Cross-site rooms:** the fleet presence room and any cross-mission rooms are federated rooms whose membership spans site homeservers. Room state replicates via S2S; presence heartbeats propagate as events (not relying on lossy presence EDUs across federation, §4.5).
**Cross-site identity:** an agent on site-B is `@mosaic_<slug>:site-b`. The signed-authorship pubkey travels in profile state, so a site-A verifier can validate a site-B agent's gate action without trusting site-B's homeserver blindly (signature ≠ homeserver trust).
---
## 7. Buzz-pattern adoption map
We adopt Buzz's **ideas**, on Matrix rails, phased:
| Buzz idea | Adopt? | How, on Matrix | Phase |
| --------------------------------------------------- | ----------------- | ------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------- |
| **Harness auto-detect / enroll** | **Yes** | `mosaic enroll` detects Claude/Codex/Pi/Goose and self-registers via the appservice (§4.1) | P2 (enroll v1); scan-machine-and-offer-integrate is **v2** |
| **Keypair signed identity** | **Yes, scoped** | Ed25519 signed-authorship on **gate actions only** (reviews/merges/approvals), pubkey in profile state (§4.4) | P5 (hardening); keys issued at enroll from P2 |
| **Unified event log (humans + agents, one log)** | **Yes** | Matrix room timeline _is_ the unified, receipted, replayable event log; MACP custom events are first-class entries (§4.2) | P2→P3 |
| **Humans and agents on the same surface** | **Yes** | HIL uses the same Matrix rooms via Element/custom client; escalations land where Jason already is (§4.6, §5) | P1 (fleet room) → P2 |
| **Scan machine, offer to integrate existing tools** | **Deferred (v2)** | enroll v1 auto-detects the _harness_; scanning a host for other integratable tools is explicitly **enroll v2**, post-P5 | v2 |
| **Buzz/Nostr as transport** | **No** | transport is Matrix; only patterns are borrowed (NG3) | — |
---
## 8. Security
- **Homeserver hardening.** Disable open registration (`enable_registration: false`); agents come only via the appservice. `federation_domain_whitelist` to Mosaic sites only (§6). Rate-limiting on. Media repo locked down or disabled if unused. Admin API bound to localhost / behind auth. Run Synapse behind our reverse proxy with TLS termination we control. **[VERIFY]** current recommended hardening flags for the deployed Synapse version.
- **Appservice token custody (`hs_token` / `as_token`).** These are the crown jewels — `as_token` lets the holder act as any user in the namespace; `hs_token` authenticates the homeserver → appservice callbacks. They live **only** in the appservice's secret store (see Vault interaction below), never in an agent process, never in the SDK, never in git. Registration file (which contains them) is a secret artifact, mounted at deploy, not committed. Rotate on suspected exposure; rotation requires updating both Synapse's registration and the appservice.
- **Per-agent credentials (the hard part — fleet key management).**
- Agents hold **only their own** per-agent `access_token` (minted by the appservice at enroll), not the `as_token`. Compromise of one agent ≠ compromise of the namespace.
- **Issuance:** at `mosaic enroll`, the appservice mints the token and returns it over the enroll channel (which must itself be authenticated — enroll requests carry a spin-time bootstrap secret / orchestrator-signed nonce **[VERIFY design in P2]**, so a random process can't enroll a rogue agent).
- **Signed-authorship keys:** Ed25519 private keys should be generated agent-side and only the public key leaves the agent (best custody: private key never transits the network). Where agents are ephemeral, keys are minted per-spin and discarded on teardown; the pubkey-in-profile record remains for audit.
- **Rotation:** tokens are short-lived where the Synapse version supports refresh **[VERIFY]**; otherwise the appservice supports explicit re-issue on re-enroll. A rotation runbook is a P2 deliverable.
- **Federation TLS.** Valid certs on federation endpoints; whitelist-only federation; monitor for cert expiry (a cert lapse silently defederates a site — add to observability).
- **Interaction with existing Vault/secrets.** The appservice's `hs_token`/`as_token` and the enroll bootstrap secret are stored in the existing secret manager (Vault or the project's chosen store — **open question §11**) and injected at deploy, consistent with how Gateway/DB secrets are handled today. No new bespoke secret store. Per-agent tokens are _transient runtime_ secrets, not persisted to Vault (they're re-mintable). **[VERIFY]** current Mosaic secret-management choice — the CLAUDE.md notes secrets/KBN work in flight, so align with whatever KBN-101 lands.
---
## 9. Migration — strangler off Hermes
**Principle:** stand the native layer up _alongside_ Hermes, move channels over **as each is proven at parity**, retire Hermes only when the parity checklist is green. Never a flag-day cutover (NG1).
**Sequence:**
1. **Stand alongside.** Native Synapse + appservice + fleet presence room live in parallel; Hermes still carries everything it carries today. Presence (P1) is _additive_ — it gives us something Hermes never had, at zero risk to existing flows.
2. **Move channels as proven.** Per channel (fleet presence → mission coordination → reviews/approvals → external-chat edge), cut traffic to Matrix, keep Hermes as hot fallback until the channel meets parity for a soak period.
3. **Retire at parity.** When every checklist item is green and soaked, decommission the Hermes MCP bridge as backbone. (External chat platforms, if still needed, can be re-attached as a _bridge at the edge_ of Matrix rather than the spine.)
**Parity checklist (must all be green before Hermes retires):**
- [ ] Every message class Hermes carries today has a MACP equivalent (message, review/approval, escalation, workflow-step).
- [ ] Presence/liveness is strictly better than today (it is: today = none).
- [ ] Cross-site delivery works over federation with no central SPOF.
- [ ] HIL (Jason) can see and act on escalations on the Matrix surface.
- [ ] Signed-authorship enforced on gate actions (no unsigned merge/approve accepted).
- [ ] `mosaic enroll` auto-onboards all four harnesses (Claude/Codex/Pi/Goose) with zero manual identity setup.
- [ ] Delivery receipts / durability demonstrably ≥ Hermes (no lost messages over a soak window).
- [ ] Runbooks exist: appservice token rotation, site defederation, dark-site escalation, homeserver restore.
- [ ] Observability: appservice + Synapse traced into OTEL/Jaeger like the rest of the stack.
- [ ] Rollback path documented (re-enable Hermes channel) for the soak period.
---
## 10. Phased delivery plan (P1 → P5) with acceptance criteria
> This section is the decomposition surface: each phase → one or more missions for the orchestrator.
### P1 — Presence / availability on a minimal single-site Synapse _(the first standalone shippable slice)_
**Scope:** one Synapse homeserver, a **minimal** appservice (or even a scripted provisioner) whose only job is: register a handful of agent MXIDs, create the **fleet presence room**, and carry heartbeats; a minimal `packages/comms` slice that sets presence and heartbeats; native presence surfaced to a human via Element.
**Acceptance criteria:**
- A1. Single-site Synapse deployed (`infra/matrix/`), reachable over TLS, open registration OFF.
- A2. ≥3 agents enroll (even if semi-manually) and appear in a **fleet presence room** with a live online/away/offline indicator.
- A3. `mosaic.presence` heartbeat implemented; an agent killed hard flips to **offline/dark** within `dark_threshold` deterministically (not dependent on native presence timeout alone).
- A4. A human (Jason) can open Element, join the fleet room, and see fleet liveness at a glance.
- A5. Zero impact to existing tmux + `mos-comms` flows (they still work untouched).
### P2 — Native appservice + orchestrator auto-enroll / room-provisioning
**Scope:** full **`apps/matrix-appservice`** registered with Synapse (`hs_token`/`as_token`, namespaces); `mosaic enroll` harness auto-detect; orchestrator-owned room taxonomy; per-agent token minting; introductions.
**Acceptance criteria:**
- B1. Appservice registered with Synapse via registration file; owns `@mosaic_*` user namespace and room-alias namespace.
- B2. `mosaic enroll` auto-detects all four harnesses (Claude/Codex/Pi/Goose) and self-registers on spin, idempotently.
- B3. On spin, an agent is provisioned an MXID, minted its **own** access token, joined to the correct mission/team/fleet rooms, and posts a `mosaic.introduction`.
- B4. Orchestrator can create/destroy mission & team rooms with correct power levels via the appservice.
- B5. Enroll bootstrap is authenticated (a rogue local process cannot enroll a rogue agent).
- B6. Appservice + Synapse traced into OTEL/Jaeger.
### P3 — MACP v1 spec ratified
**Scope:** freeze the standard (§4): envelope, event types + JSON Schemas, identity model, presence model, room taxonomy, tmux↔Matrix routing rules. Spec lives in framework (`~/.config/mosaic/spec/macp`), vendored+pinned into `packages/comms` with CI drift-check.
**Acceptance criteria:**
- C1. MACP v1 document ratified (MS-LEAD sign-off, Jason veto window closed).
- C2. JSON Schemas for all five event types published; `packages/comms` validates outbound/inbound against them.
- C3. CI fails if framework spec and vendored `packages/comms` copy diverge.
- C4. Routing-rule conformance test: SDK provably sends ephemeral+same-host over tmux, everything else over Matrix.
- C5. Unknown-`macp_version` graceful-degrade behavior tested.
### P4 — Federation
**Scope:** a second site homeserver + appservice; S2S federation over our DNS/TLS; cross-site fleet room; whitelist-only federation; cross-site escalation.
**Acceptance criteria:**
- D1. Two sites federate via `.well-known`/SRV over TLS we control; `federation_domain_whitelist` restricts to Mosaic sites (no public federation).
- D2. A cross-site fleet presence room shows agents from both sites; heartbeats propagate as events across federation.
- D3. **Homelab-dark test:** killing site-A's coordinator is observed from site-B within `dark_threshold`, and the escalation/fallback fires cross-site (§5).
- D4. Cross-site gate action: a site-B agent's signed review is verified by a site-A verifier.
- D5. Defederation runbook proven (a site can be cut off cleanly).
### P5 — Buzz-hardening + signed-authorship + Hermes retired
**Scope:** Ed25519 signed-authorship enforced on gate actions; security hardening pass; complete the strangler and retire Hermes at parity.
**Acceptance criteria:**
- E1. Every merge/approve/review gate action is signed; unsigned or bad-sig gate events are rejected by the appservice watcher and by consuming agents.
- E2. Token/key rotation runbooks executed at least once in anger (rotate `as_token`, rotate a per-agent key).
- E3. Security review complete: homeserver hardening flags, token custody, federation TLS/whitelist all verified.
- E4. **Parity checklist (§9) fully green + soaked.**
- E5. Hermes MCP bridge retired as backbone (optionally re-attached as an edge bridge only).
---
## 11. Open questions for Jason / Mos (need a human/coordinator ruling)
1. **DNS / domains per site.** What domain(s) do we own and want to use per site for homeserver names and `.well-known` (e.g. `site-a.mosaicstack.dev`)? Federation identity is permanent-ish once agents mint MXIDs against it — this needs a ruling **before P1 hardens** because MXIDs bake in the domain.
2. **Secret-management choice.** Is it Vault, or whatever KBN-101 lands? The appservice `hs_token`/`as_token` and enroll bootstrap secret custody depend on this (§8). CLAUDE.md signals secrets work is in flight — need the authoritative target.
3. **N-minute escalation threshold.** What is `dark_threshold_min` for a coordinator, and the second threshold before HIL is paged (§5)? Default proposed: 10 min → fallback, +5 min → HIL. Jason/Mos to confirm per role.
4. **HIL client: Element vs custom.** Do humans use off-the-shelf **Element** (fast, free, P1-ready) or do we invest in a custom HIL client? Proposed: **Element for P1P4**, revisit custom only if HIL ergonomics demand it.
5. **Ephemeral vs persistent agent keys.** For signed-authorship, do we mint Ed25519 keys per-spin (simplest custody, no long-term private key at rest) or issue durable per-agent keys (stable identity across spins, but key-at-rest custody problem)? Proposed: **per-spin**, pubkey retained for audit.
6. **Federation topology / trust.** Full mesh between all sites, or hub-and-spoke-with-redundancy? Full mesh maximizes no-SPOF but grows O(n²); needs a call once site count is known.
7. **Fallback-coordinator assignment authority.** Who assigns each Team Lead's fallback coordinator, and is it always cross-site? (Design invariant §5 wants the fallback off the monitored host — confirm this is acceptable operationally.)
8. **Retention / compliance.** How long do we retain the Matrix event log (the unified audit trail)? Affects Synapse storage sizing and any purge policy.
---
## Appendix A — Real Matrix concepts this RFC leans on (quick reference)
- **Application Service (AS) API** — a privileged service registered with the homeserver via a registration YAML declaring `id`, `url`, `as_token`, `hs_token`, and `namespaces` (users/aliases/rooms regexes). The homeserver pushes events to the AS in transactions; the AS can register/act-as users in its namespace. _(This is our appservice backbone.)_
- **`hs_token` / `as_token`** — `hs_token`: homeserver→AS authentication on pushed transactions; `as_token`: AS→homeserver authentication, grants acting-as any namespaced user. Both are high-value secrets (§8).
- **Masquerade (`user_id` query param)** — the AS may act as a namespaced user on C-S calls using `as_token` + `?user_id=`. We prefer per-agent tokens for blast-radius; masquerade for AS-internal bulk ops.
- **Custom event types** — timeline events via `m.room.message` with a custom `msgtype` (keeps a human-visible `body` fallback) and/or fully custom `type` (dotted, e.g. `mosaic.workflow.step`); **state events** for last-writer-wins current status readable from room state without history replay.
- **Presence EDUs** — native online/unavailable/offline signals; coarse and lossy over federation, so MACP adds an explicit heartbeat event as the authoritative liveness source (§4.5).
- **Federation (S2S API)** — server-to-server over TLS; discovery via `/.well-known/matrix/server` and/or `_matrix._tcp` SRV; restrictable with `federation_domain_whitelist`.
- **Synapse config knobs cited**`enable_registration`, `federation_domain_whitelist`, appservice registration file, rate-limiting, admin API binding. **[VERIFY]** exact flags/paths against the deployed Synapse version at implementation time.
_All Matrix mechanics above are cited from architecture knowledge and MUST be re-verified against the actual deployed Synapse version during P1 — every **[VERIFY]** in this document is a checkpoint, not an assumption._
@@ -0,0 +1,622 @@
# RFC-002 — Install, Configuration & Topology for the Mosaic Matrix/MACP Comms System
- **Status:** DRAFT — for Team Lead → Orchestrator staffing
- **Author:** MS-LEAD (reviewer identity `ms-lead-reviewer`)
- **Sponsor / veto:** Jason (human lead)
- **Date:** 2026-07-24
- **Program:** Mosaic Stack comms-evolution
- **Companion to:** RFC-001 — _MACP: A Mosaic-Native, Matrix-Native Comms Layer_. RFC-001 is the architecture (self-hosted Synapse + Mosaic appservice backbone + `packages/comms` SDK + MACP standard + per-site federation). **RFC-002 is the config substrate the whole thing installs and runs on.**
- **Audience:** Team Leads, the Mosaic orchestrator, infra, harness maintainers, and — critically — **strangers who install this open-source product on hardware we will never see.**
> This is a **design document**. No code ships from this RFC. It is written to be decomposed into missions with per-phase acceptance criteria, and it slots under RFC-001's P1→P5. Every uncertain or must-live-validate claim is flagged **[VERIFY]**.
> **The one framing that governs every decision below:** this is an **open-source product**. Someone we have never met will `git clone` it and run it on their own domains, their own DNS, their own certs, their own hardware. **NOTHING may hardcode our fleet's topology.** There is no `woltje.com` in the code, no assumption that DNS exists, no assumption that a second site exists. Every topology fact is **user-supplied config**. Where this doc uses `mosaic.woltje.com` / `mosaic.uscllc.com`, those are **illustrative operator values** (Jason's real installs), never defaults and never literals in the product.
---
## 0. TL;DR
The comms system installs against a **user-supplied topology**, never a baked-in one. At install the operator declares exactly one of **three topology modes**: **(A) split-domain** (identity `server_name` ≠ homeserver host, wired via Matrix delegation — this is Jason's `mosaic.woltje.com` identity + `matrix.woltje.com` host setup), **(B) single-domain** (`server_name` == homeserver host), or **(C) IP-only standalone** (no DNS, no federation, fully supported for local/airgapped). **The PRIMARY/home instance is ALWAYS configured; federation is OPTIONAL.** A single standalone instance MUST work with zero federation.
**Federation is a hard-gated capability: it REQUIRES DNS + valid certificates. IP-only federation is not possible and is not supported.** IP-only means standalone-only, forever, until the operator acquires DNS + certs.
Certificates are **one ACME integration** with a user-chosen **directory URL**: either **step-ca** (self-hosted private ACME CA, for total control and private/internal domains public CAs can't issue for) or **Let's Encrypt** (public ACME, ease-of-use). The operator also picks a challenge type (HTTP-01 / DNS-01 / TLS-ALPN-01); **DNS-01 is the answer for private/split-horizon domains.**
Secrets go through a **pluggable `SecretBackend` interface** — no forced paid dependency. Ships with a **Vault** implementation and a **Vaultwarden** implementation; the operator picks at install. The Vaultwarden model (org + orchestrator enrolled as authority + per-agent scoped access) is designed-for, with an honest **[VERIFY]** on how far Vaultwarden's machine-account coverage has matured.
Config is **DB-backed with sane defaults and install-time overrides**. Precedence: **install-time → DB override → default.** Config is split into **install-time-immutable** (e.g. `server_name`, which is baked into every MXID and cannot change without re-homing every identity) and **runtime-tunable** (e.g. dark-threshold).
RFC-002 is the substrate; **RFC-001's P1 (presence) needs only Mode A/B single-instance clean-domain and does NOT require federation, IP-only, or the secret-backend rotation story resolved.**
---
## 1. Goals / Non-Goals
### 1.1 Goals
- **G1 — Installable by a stranger.** A person with no relationship to our fleet can install, configure, and run the comms system from published artifacts and a guided installer, on their own hardware and domains, with no edits to product code.
- **G2 — Zero hardcoded topology.** Every topology fact — `server_name`, homeserver host/IP, delegation method, federation peers, cert mode, secret backend — is **user-supplied config**, validated at install, stored in the product DB. No fleet-specific literal ships in the product.
- **G3 — Standalone MUST work.** The PRIMARY instance is always fully functional with **zero federation**, including with **no DNS at all** (Mode C, IP-only). Presence, rooms, MACP, HIL-via-Element all work single-instance.
- **G4 — Federation is optional but honestly gated.** Federation is opt-in and, when opted into, **requires DNS + valid certificates as a hard precondition.** The installer must refuse to _claim_ federation is working when the DNS/cert preconditions aren't met.
- **G5 — One ACME integration, two CA choices.** Build a single ACME cert-provisioning path; the operator selects step-ca or Let's Encrypt by supplying an **ACME directory URL** plus a challenge type. No second, bespoke cert path.
- **G6 — No forced paid dependency for secrets.** A pluggable `SecretBackend` with at least Vault and Vaultwarden implementations, chosen at install. Open-source ethos: the free/self-hostable path must be first-class.
- **G7 — Defaults that just work, overrides where they matter.** DB-backed config with sane defaults so most operators change little; install-time overrides for the topology-critical values; a clear immutable-vs-tunable boundary so operators can't foot-gun `server_name`.
- **G8 — A clean upgrade path.** An operator who starts standalone can later turn on federation with a documented, honest procedure (including the real cost if they started IP-only and must now acquire a stable `server_name`).
### 1.2 Non-Goals
- **NG1 — Not hosting a managed service.** This RFC is about _self-install_. We are not building multi-tenant SaaS provisioning; each operator runs their own instance(s).
- **NG2 — Not a new cert stack.** We do not write our own CA, our own ACME client protocol, or a non-ACME cert path. We integrate ACME and let the operator point it at step-ca or Let's Encrypt. (We _may_ bundle/recommend step-ca as the self-hosted CA, but via its standard ACME provisioner, not a fork.)
- **NG3 — Not a new secret manager.** We define an interface and ship adapters. We do not build a secret store; we do not force one.
- **NG4 — Not public-network Matrix federation.** Consistent with RFC-001 NG5: federation is Mosaic-site-to-Mosaic-site over infrastructure the operator controls, allowlisted. No `matrix.org` federation.
- **NG5 — Not making IP-only federate.** We will not ship a hack (self-signed S2S trust bundles, `/etc/hosts` federation) that pretends IP-only can federate. IP-only is standalone. This is a deliberate, honest boundary (§2.4, §7).
- **NG6 — Not re-homing identities silently.** We will not offer a "just change your `server_name`" button that quietly orphans every MXID. Any path that changes `server_name` is a flagged, gated, documented identity re-home (§5.3, §7).
---
## 2. The topology model
### 2.1 The core split: `server_name` vs homeserver host
Matrix has exactly the split Jason described, natively:
- **`server_name`** — the Synapse config value that is the server's **identity domain**. It is the part after the colon in every MXID (`@mosaic_agent:mosaic.woltje.com`) and every room alias (`#mission-KBN-101:mosaic.woltje.com`). It is **baked into every identity the moment that identity is minted.** Changing it re-homes everything. This is `server_name` in Synapse's `homeserver.yaml`.
- **Homeserver host** — the actual network location (hostname:port or IP:port) where the Synapse process answers federation and (optionally proxied) client traffic. It **can differ** from `server_name`. Matrix reconciles the difference through **delegation**: `https://<server_name>/.well-known/matrix/server` returning `{"m.server": "matrix.woltje.com:443"}`, and/or a `_matrix._tcp.<server_name>` **SRV** record. **[VERIFY]** `.well-known` vs SRV precedence on the deployed Synapse version (RFC-001 §6 flags the same).
So Jason's "mosaic._ app-domain + matrix._ homeserver-domain" split maps precisely: **`server_name = mosaic.woltje.com` (identity, in MXIDs), homeserver runs at `matrix.woltje.com` (discovered via delegation).** That is **Mode A**.
### 2.2 The topology config schema
One canonical config object, stored in the product DB (§5), populated at install (§6). Illustrative shape (field names decomposition-ready, not frozen):
```jsonc
{
"topology": {
"mode": "split-domain | single-domain | ip-only-standalone", // A | B | C — install-time-immutable
"identity": {
"server_name": "mosaic.woltje.com", // INSTALL-TIME-IMMUTABLE. In MXIDs. Never change without re-home (§5.3).
"server_name_kind": "domain | ip", // "ip" only legal in Mode C
},
"homeserver": {
"host": "matrix.woltje.com", // where Synapse actually listens (Mode A: differs from server_name)
"port": 8448, // federation port (default 8448) or 443 if proxied — [VERIFY] per deploy
"client_bind": "https://matrix.woltje.com", // C-S API public URL (proxied)
"bind_ip": null, // Mode C: e.g. "192.168.1.50" ; Modes A/B: null (DNS-resolved)
},
"delegation": {
"method": "well-known | srv | none", // Mode A: well-known or srv ; Mode B/C: none
"well_known_server": { "m.server": "matrix.woltje.com:443" }, // if method=well-known
"srv_record": "_matrix._tcp.mosaic.woltje.com. 3600 IN SRV 10 0 443 matrix.woltje.com.", // if method=srv (documented, operator provisions)
},
"federation": {
"enabled": true, // OPTIONAL. Mode C forces false.
"domain_whitelist": [
// Synapse federation_domain_whitelist — allowlist ONLY
"mosaic.woltje.com",
"mosaic.uscllc.com",
],
"peers": [
// operator-declared peer sites (for room/fleet wiring)
{ "server_name": "mosaic.uscllc.com", "role": "secondary", "fleet_room": true },
],
},
"tls": {
"acme": {
"directory_url": "https://acme.mosaic.woltje.com/acme/acme/directory", // step-ca OR https://acme-v02.api.letsencrypt.org/directory
"ca_kind": "step-ca | letsencrypt", // informational label; the directory_url is the real switch
"challenge": "dns-01 | http-01 | tls-alpn-01",
"account_email": "ops@woltje.com", // ACME account contact
"eab": { "kid": null, "hmac_key_ref": null }, // External Account Binding if the CA requires it (some step-ca provisioners) — secret via SecretBackend
},
"client_tls_mode": "acme | self-signed", // Mode C may use self-signed for local C-S TLS (weaker trust, §8)
},
"secrets": {
"backend": "vault | vaultwarden", // pluggable, install-time choice (§4)
"connection": {
"address": "https://vault.woltje.com:8200", // or Vaultwarden/Bitwarden server URL
"auth_ref": "…", // how the appservice authenticates to the backend (bootstrap, §4/§8)
"namespace_or_org": "mosaic-fleet", // Vault namespace / mount, OR Vaultwarden org id
},
},
},
}
```
### 2.3 The three modes, concretely
Exactly three supported modes (Jason's ruling — no others):
**Mode A — split-domain (identity ≠ host, delegated).** _Jason's PRIMARY._ Federation-capable. This is the recommended production shape because it lets identity live on a clean app-domain while the homeserver runs on a separate operational host.
```jsonc
// Mode A — mosaic.woltje.com identity, matrix.woltje.com host, federated with a second site
{
"topology": {
"mode": "split-domain",
"identity": { "server_name": "mosaic.woltje.com", "server_name_kind": "domain" },
"homeserver": {
"host": "matrix.woltje.com",
"port": 443,
"client_bind": "https://matrix.woltje.com",
"bind_ip": null,
},
"delegation": {
"method": "well-known",
"well_known_server": { "m.server": "matrix.woltje.com:443" },
},
"federation": {
"enabled": true,
"domain_whitelist": ["mosaic.woltje.com", "mosaic.uscllc.com"],
"peers": [{ "server_name": "mosaic.uscllc.com", "role": "secondary", "fleet_room": true }],
},
"tls": {
"acme": {
"directory_url": "https://acme-v02.api.letsencrypt.org/directory", // public LE, or a step-ca directory
"ca_kind": "letsencrypt",
"challenge": "dns-01",
"account_email": "ops@woltje.com",
},
"client_tls_mode": "acme",
},
"secrets": {
"backend": "vaultwarden",
"connection": { "address": "https://vw.woltje.com", "namespace_or_org": "mosaic-fleet" },
},
},
}
```
MXIDs on this instance: `@mosaic_coordinator-1:mosaic.woltje.com`. A human/agent's homeserver is discovered by resolving `.well-known/matrix/server` on `mosaic.woltje.com``matrix.woltje.com:443`.
**Mode B — single-domain (identity == host).** Simpler; the `server_name` _is_ the host. No delegation needed. Federation-capable (still needs DNS + cert on that one domain).
```jsonc
// Mode B — one domain does everything
{
"topology": {
"mode": "single-domain",
"identity": { "server_name": "matrix.example.org", "server_name_kind": "domain" },
"homeserver": {
"host": "matrix.example.org",
"port": 8448,
"client_bind": "https://matrix.example.org",
"bind_ip": null,
},
"delegation": { "method": "none" },
"federation": { "enabled": false, "domain_whitelist": [], "peers": [] }, // optional — off here
"tls": {
"acme": {
"directory_url": "https://acme-v02.api.letsencrypt.org/directory",
"ca_kind": "letsencrypt",
"challenge": "http-01",
"account_email": "admin@example.org",
},
"client_tls_mode": "acme",
},
"secrets": {
"backend": "vault",
"connection": { "address": "https://vault.example.org:8200", "namespace_or_org": "mosaic" },
},
},
}
```
MXIDs: `@mosaic_coordinator-1:matrix.example.org`. Here `server_name == host`, so `@:matrix.example.org` is both the identity domain and where Synapse actually answers.
**Mode C — IP-only standalone (no DNS, no federation).** Fully supported for local/airgapped/homelab-without-DNS. `server_name` is an IP:port literal. **Cannot federate — ever — in this mode** (federation requires a real domain + valid CA cert; §2.4). Client TLS may be self-signed (weaker trust, §8).
```jsonc
// Mode C — airgapped / local, no DNS, standalone only
{
"topology": {
"mode": "ip-only-standalone",
"identity": { "server_name": "192.168.1.50:8448", "server_name_kind": "ip" },
"homeserver": {
"host": "192.168.1.50",
"port": 8448,
"client_bind": "https://192.168.1.50:8448",
"bind_ip": "192.168.1.50",
},
"delegation": { "method": "none" },
"federation": { "enabled": false, "domain_whitelist": [], "peers": [] }, // FORCED false in Mode C
"tls": { "acme": null, "client_tls_mode": "self-signed" }, // may use a private step-ca or self-signed for C-S TLS
"secrets": {
"backend": "vaultwarden",
"connection": { "address": "http://192.168.1.51:8080", "namespace_or_org": "mosaic-local" },
},
},
}
```
MXIDs: `@mosaic_coordinator-1:192.168.1.50:8448`. **Warning surfaced at install:** this `server_name` is an IP literal; if the operator ever wants federation they must move to a domain, which is an **identity re-home** (§5.3, §7).
> **[VERIFY]** Synapse accepts an `ip:port` `server_name` and mints usable MXIDs against it for local/standalone use. This is believed workable for non-federated operation but must be validated against the deployed Synapse version; some Synapse versions/tools assume a DNS-resolvable `server_name`. If an IP literal is rejected, Mode C falls back to a **fabricated local domain** (e.g. `mosaic.local`) resolved via `/etc/hosts` or a local resolver — still standalone-only, same re-home caveat.
### 2.4 The hard federation gate (Jason's HARD STOP)
**Federation REQUIRES DNS + valid certificates. This is a hard stop, enforced by the installer and by the config validator, not a suggestion.**
| Precondition | Why | Enforced where |
| -------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------- |
| `server_name` resolves in public/peer DNS (or delegated target does) | S2S discovery uses `.well-known`/SRV over DNS; peers must resolve you | installer reachability check (§6); config validator rejects `federation.enabled=true` with `server_name_kind=ip` |
| Valid TLS cert on the federation endpoint, chained to a CA the peer trusts | S2S is TLS; a peer validates your cert. Self-signed/untrusted ⇒ peer refuses ⇒ you are defederated | installer cert probe (§6); cert monitor (§3.4) |
| Federation `domain_whitelist` non-empty and mutually consistent with peers | allowlist-only federation (RFC-001 NG5/§6) | config validator |
**IP-only ⇒ federation is impossible.** There is no valid public/peer CA cert for a bare IP in our trust model (and we will not ship a self-signed S2S trust hack — NG5). Therefore **Mode C is standalone-only by construction**, and the config validator makes `mode=ip-only-standalone ∧ federation.enabled=true` an **illegal state that cannot be persisted.**
This is the honest, load-bearing boundary of the whole topology model:
```
DNS + valid cert?
┌─────────────┴─────────────┐
YES NO
│ │
Mode A or B Mode C (IP-only)
federation OPTIONAL STANDALONE ONLY
(opt-in, allowlisted) (federation impossible)
```
---
## 3. Certificate provisioning — one ACME integration
### 3.1 Single integration, directory-URL as the switch
We build **exactly one** cert-provisioning integration: an **ACME client integration**. Both supported CAs are ACME CAs. The operator does **not** choose between two code paths; they choose an **ACME directory URL** and a **challenge type**. That is the entire surface.
| CA choice | What it is | ACME directory URL (illustrative) | Why an operator picks it |
| ----------------------- | ------------------------------- | ------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| **step-ca (Smallstep)** | self-hosted **private** ACME CA | `https://acme.<internal-domain>/acme/<provisioner>/directory` | **Total control**; issues for **private/internal/split-horizon domains** a public CA can't (e.g. `mosaic.internal`, RFC-1918 split-horizon); airgap-friendly; you own the root |
| **Let's Encrypt** | public ACME CA | `https://acme-v02.api.letsencrypt.org/directory` (staging: `.../acme-staging-v02...`) | **Ease of use**; universally trusted chain (ISRG Root X1); zero CA to operate; ideal for public domains |
Because both speak ACME, the same client (account key, order, authorization, challenge, finalize, cert-fetch, renew) drives either. The `ca_kind` label in config is informational for UX; the **`directory_url` is the real determinant**. **[VERIFY]** whether the chosen ACME library requires per-CA quirks (LE rate limits, staging switch; step-ca **External Account Binding** on some provisioners — if EAB is required the `kid`/`hmac_key` come from the SecretBackend, §4).
### 3.2 Challenge-type matrix (which challenge for which topology)
The operator picks one challenge type per the domains they're covering. This is the crux for **public vs private/split-horizon**:
| Challenge | How it proves control | Best for | Cannot / caveat |
| --------------- | ----------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **HTTP-01** | CA GETs `http://<domain>/.well-known/acme-challenge/<token>` on port 80 | **Public, single hostname**, port 80 reachable from CA (Mode B, or Mode A's homeserver host) | Needs inbound :80 from the CA; **cannot** do wildcards; **useless for private domains** a public CA can't reach |
| **DNS-01** | CA checks a `_acme-challenge.<domain>` **TXT** record you publish | **Private / split-horizon / internal domains**, **wildcards**, and any domain where inbound HTTP from the CA is impossible | Requires **programmatic DNS API** access to publish TXT (or manual for step-ca where you own the resolver). **This is the answer for step-ca on private domains** and for Mode A when the homeserver host isn't publicly HTTP-reachable |
| **TLS-ALPN-01** | CA connects TLS on :443 with ALPN `acme-tls/1` | Public host where **:443 is free** but :80 is blocked | Needs the ACME client to own the :443 TLS handshake briefly; awkward behind some reverse proxies — **[VERIFY]** against our proxy (RFC-001 terminates TLS at a reverse proxy) |
**Guidance baked into the installer:**
- **Let's Encrypt + public domain, port 80 open →** HTTP-01 (simplest). Wildcard or no inbound :80 → DNS-01.
- **step-ca + private/internal/split-horizon domain →** **DNS-01** (the private CA can validate against a resolver you control; public HTTP reachability is irrelevant). This is the combination that lets a private homelab domain get real certs.
- **:443-only public host →** TLS-ALPN-01.
### 3.3 Delegation setup for split-domain (Mode A)
For Mode A, the cert and the delegation must agree. Concretely, at install for `server_name=mosaic.woltje.com`, host `matrix.woltje.com`:
1. **Cert(s):** obtain a valid cert for **`matrix.woltje.com`** (the federation/host endpoint — this is where the TLS handshake actually lands). If serving `.well-known` over HTTPS on `mosaic.woltje.com`, that origin **also** needs a valid cert for `mosaic.woltje.com`. So Mode A typically provisions **two** SANs/certs: identity-domain (`mosaic.woltje.com`, serves `.well-known`) and host (`matrix.woltje.com`, serves S2S+C-S). **[VERIFY]** whether a single multi-SAN cert is preferable operationally.
2. **Delegation record**, one of:
- **`.well-known`:** serve `https://mosaic.woltje.com/.well-known/matrix/server``{"m.server":"matrix.woltje.com:443"}` (and `.well-known/matrix/client` for C-S discovery so agents/Element find the host).
- **SRV:** `_matrix._tcp.mosaic.woltje.com. IN SRV 10 0 443 matrix.woltje.com.` The installer **documents and validates** the record but the operator provisions it in their DNS (we don't run their DNS). **[VERIFY]** `.well-known` vs SRV precedence on the deployed Synapse.
3. **Validate:** installer fetches the operator's own `.well-known`/SRV and confirms it points at the configured host, and that the host presents a valid cert (§6). Only then does it declare Mode A "federation-ready."
### 3.4 Renewal & monitoring — a lapsed federation cert silently defederates
**This is the operational trap and it must alarm.** ACME certs are short-lived (LE = 90 days; step-ca often shorter by policy). A federation cert that lapses does **not** throw a loud error — peers simply **stop trusting the S2S handshake and the site silently drops out of federation.** From inside, everything looks fine; from peers, the site went dark. That is exactly the "homelab went dark and took comms with it" trauma (RFC-001 §5), but caused by a cert, not a host.
Requirements:
- **Auto-renew** on the standard ACME schedule (renew at ~⅓ lifetime remaining; LE guidance ~30 days before expiry). The ACME integration owns this loop.
- **Expiry monitoring as a first-class alarm.** Emit cert-days-remaining into OTEL/Jaeger metrics (consistent with RFC-001 §8's "monitor for cert expiry — a cert lapse silently defederates"). Alarm thresholds (e.g. warn <14d, critical <3d) are **runtime-tunable** config (§5).
- **Federation-health probe:** periodically resolve our own delegation and validate our own cert _as a peer would_ (external vantage where possible), so a broken renewal is caught as "we would fail a peer's validation" before a peer notices.
- **Escalation tie-in:** a critical cert-expiry or federation-health failure raises a `mosaic.escalation` (RFC-001 §4.2/§5) into the HIL room. A cert lapse is a fleet-visibility incident, not a silent config drift.
---
## 4. Secret backend interface
### 4.1 The `SecretBackend` contract
A single pluggable interface. The appservice and orchestrator depend on the **interface**, never on Vault or Vaultwarden directly. Chosen at install; swappable without touching callers. Illustrative contract (decomposition-ready, not frozen):
```ts
interface SecretBackend {
// --- static secret CRUD (appservice tokens, ACME EAB, DB creds) ---
get(ref: SecretRef): Promise<SecretValue>;
put(ref: SecretRef, value: SecretValue, opts?: { immutable?: boolean }): Promise<void>;
rotate(
ref: SecretRef,
next: SecretValue,
): Promise<{ previous: SecretVersion; current: SecretVersion }>;
list(prefix: SecretRef): Promise<SecretRef[]>;
delete(ref: SecretRef): Promise<void>;
// --- agent-credential lifecycle (the fleet-identity part) ---
enrollAgent(input: {
agentSlug: string;
scope: CredentialScope; // which rooms/secrets this agent may read
ttl?: Duration; // ephemeral-by-default per RFC-001 §8
}): Promise<AgentCredentialHandle>; // wraps the per-agent access_token + optional pubkey record
revokeAgent(agentSlug: string): Promise<void>; // must be authoritative & immediate
// --- health / bootstrap ---
health(): Promise<BackendHealth>;
authenticateSelf(bootstrap: BootstrapAuth): Promise<void>; // how the appservice/orchestrator logs into the backend
}
```
Design intent: **`get/put/rotate`** cover the static crown-jewel secrets (appservice `hs_token`/`as_token`, ACME account/EAB keys, DB DSN). **`enrollAgent/revokeAgent`** cover the _fleet-identity_ lifecycle — this is where RFC-001's "mint per-agent token at enroll, discard on teardown" (RFC-001 §4.1, §8) actually lands.
### 4.2 How appservice / agent tokens map onto it
RFC-001 defines three tiers of Matrix secret. They map cleanly:
| RFC-001 secret | Sensitivity | `SecretBackend` treatment |
| ------------------------------------------------------- | ------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **`as_token`** (AS→HS, acts as any namespaced user) | crown jewel | `put(immutable-ish)` + `rotate`; **only** the appservice may `get` it; stored under a fleet-admin scope; never handed to an agent |
| **`hs_token`** (HS→AS callback auth) | crown jewel | same as `as_token`; both live only in appservice scope (RFC-001 §8) |
| **enroll bootstrap secret / orchestrator-signed nonce** | high | `get` by orchestrator + appservice only; used to authenticate `POST /enroll` so a rogue process can't enroll a rogue agent (RFC-001 §8, B5) |
| **per-agent `access_token`** | transient runtime | minted via `enrollAgent`, scoped to that agent, **short-TTL / re-mintable**, discarded on teardown; **not** durably persisted (RFC-001 §8 says per-agent tokens are transient) — the backend may hold a short-lived handle or a personal-vault entry for the agent's own lifetime |
| **Ed25519 signed-authorship keypair** | high (private key) | private key generated **agent-side**, only pubkey leaves the agent (RFC-001 §4.4/§8); the SecretBackend stores the **pubkey record** for audit; per-spin keys need no at-rest custody |
The key blast-radius property (RFC-001 §8) is preserved: agents receive **only their own** credential via `enrollAgent`; the `as_token` never leaves appservice scope.
### 4.3 Vault implementation
Vault maps naturally:
- Static secrets → **KV v2** at a mount/namespace (`mosaic-fleet/`), with versioning giving `rotate` semantics for free.
- **`enrollAgent`** → issue a scoped, TTL'd token or use **AppRole** / a scoped policy per agent; Vault's native TTL + revocation is exactly the transient per-agent model. `revokeAgent` → Vault token/lease revoke (authoritative, immediate).
- **`authenticateSelf`** → the appservice authenticates to Vault via AppRole (role_id from config, secret_id injected at deploy) or a platform auth method; consistent with how Gateway/DB secrets are handled today (RFC-001 §8). **[VERIFY]** align with whatever KBN-101 lands for Mosaic secret management (CLAUDE.md flags secrets work in flight).
- **Trade-off (honesty):** Vault is the most capable backend but is **not** the free-and-simple default for a hobbyist stranger; hence it must not be _forced_ (G6).
### 4.4 Vaultwarden implementation + the org/enroll/revoke agent-account model
Vaultwarden (self-hostable Bitwarden-compatible server) is the **open-source-ethos default candidate** — free, self-hostable, familiar. Jason's model, mapped onto Bitwarden/Vaultwarden's org primitives:
1. **Operator creates one or more Bitwarden orgs** at install (e.g. `mosaic-fleet`).
2. **The orchestrator is enrolled into the org and granted authority** to enroll/revoke agent sessions — it is the org's automation principal (admin/manager over an agents **collection**).
3. **Agents get scoped credential access:** each agent gets access to a **collection** (or a personal vault provisioned for its spin) holding exactly the secrets its scope allows. `enrollAgent` = grant the agent principal access to its collection + provision its per-agent Matrix token entry; `revokeAgent` = remove the agent principal / revoke its access, immediately.
4. **User + agents share scoped access:** the human operator and the agents both hold credentials in the same org, scoped by collection — humans and agents on one secret surface, mirroring RFC-001's "humans and agents on one comms surface" pattern.
Mapping to the interface:
| Interface op | Vaultwarden mechanism |
| ------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `get/put/rotate` (static) | items in an org **collection**; rotate = new item version / replace + old-version audit |
| `enrollAgent` | create/attach agent principal to its **collection**; provision per-agent token item, TTL enforced by our teardown (Vaultwarden itself is not TTL-native — see [VERIFY]) |
| `revokeAgent` | revoke the agent principal's org membership / collection access |
| `authenticateSelf` | orchestrator authenticates as the org automation principal |
**HONEST MATURITY FLAG — [VERIFY]:** Bitwarden's clean **machine-account / service-account** primitive lives in **Bitwarden Secrets Manager**, and **Vaultwarden's coverage of Secrets Manager / machine accounts is partial and evolving.** What is known to work today on Vaultwarden: **orgs, collections, per-user (incl. a per-agent "user") vaults, and collection-scoped sharing.** What **may not** be fully there: the polished **machine-account API**, native short-TTL service credentials, and fine-grained programmatic access-token issuance equivalent to hosted Bitwarden Secrets Manager. **[VERIFY]** current Vaultwarden version's Secrets Manager / machine-account support before P-level commitment.
**Why this is not a blocker:** the interface is designed so **either backend is viable**. If Vaultwarden's machine-account API isn't ready, the Vaultwarden adapter implements `enrollAgent` via the **personal-vault-per-agent + org-collection** model that works _today_ (create an agent principal, share the scoped collection, we enforce TTL via orchestrator teardown rather than backend-native TTL). If an operator needs backend-native short-TTL machine credentials now, they choose the **Vault** adapter. **We are not blocked on Vaultwarden maturing**, because the `SecretBackend` abstraction lets the polished-machine-account behavior land later without changing any caller.
---
## 5. Config system
### 5.1 Storage & precedence
Config is **DB-backed** (Postgres, per the stack — CLAUDE.md/RFC-001), with sane defaults compiled into the product and install-time overrides. **Precedence, highest wins:**
```
install-time value > DB override (runtime) > compiled default
```
- **Compiled default** — ships in the product; what a stranger gets with zero config for every non-topology-critical key.
- **Install-time value** — captured by the installer (§6), written to DB, and for **install-time-immutable** keys, **locked** (marked non-overridable).
- **DB override** — runtime tuning via admin surface, allowed **only** for keys classified runtime-tunable.
> Nuance: "install-time > DB override" applies to **immutable** keys — the install-time value is frozen and a DB override of it is rejected. For **tunable** keys, the DB override is the live value and the install-time value is just the initial seed. The classification (§5.3) is what makes the precedence unambiguous per key.
### 5.2 DB schema shape
Illustrative (Drizzle/Postgres, per stack conventions):
```sql
-- one row per config key
CREATE TABLE comms_config (
key text PRIMARY KEY, -- e.g. 'topology.identity.server_name'
value jsonb NOT NULL, -- current effective value
source text NOT NULL, -- 'install' | 'db-override' | 'default'
mutability text NOT NULL, -- 'install-immutable' | 'runtime-tunable'
set_by text, -- operator/agent/system that set it
set_at timestamptz NOT NULL DEFAULT now(),
CONSTRAINT immutable_not_overridable
CHECK (NOT (mutability = 'install-immutable' AND source = 'db-override'))
);
-- append-only audit of every change (esp. attempted immutable changes)
CREATE TABLE comms_config_audit (
id bigserial PRIMARY KEY,
key text NOT NULL,
old_value jsonb,
new_value jsonb,
actor text NOT NULL,
action text NOT NULL, -- 'set' | 'override' | 'rejected-immutable'
at timestamptz NOT NULL DEFAULT now()
);
```
The DB `CHECK` is a belt-and-braces backstop; the application-layer config service enforces mutability and records rejected immutable-change attempts in the audit table. Secrets are **referenced** here (a `SecretRef`), never stored inline — actual secret values live in the `SecretBackend` (§4).
### 5.3 Install-time-immutable vs runtime-tunable — the key table
The single most important classification: **what can never change after install** vs **what an operator tunes anytime.** Getting `server_name` on the wrong side of this line is a foot-gun that orphans every identity.
| Config key | Mutability | Rationale / cost of change |
| ------------------------------------------------ | ------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------- |
| `topology.mode` (A/B/C) | **install-immutable** | changing mode changes identity/federation semantics; a mode change is effectively a reinstall/re-home |
| `topology.identity.server_name` | **install-immutable** | **baked into every MXID and room alias.** Changing it re-homes every identity — see re-home note below. **This is THE immutable value.** |
| `topology.identity.server_name_kind` | **install-immutable** | domain↔ip change is a re-home (Mode C→A/B) |
| `topology.homeserver.host` | install-immutable (**delegation-tunable**) | in Mode A you _can_ move the host if you update delegation to match — the identity is unchanged; treat as immutable-with-migration, not free |
| `topology.homeserver.bind_ip` / `port` | runtime-tunable (ops) | operational network binding; no identity impact |
| `topology.delegation.method` / records | tunable-with-care | can switch well-known↔SRV as long as both still resolve to the same host; validated on change |
| `topology.federation.enabled` | **runtime-tunable (gated)** | can flip **on** only if DNS+cert preconditions pass (§2.4); flipping standalone→federated is the §7 upgrade path |
| `topology.federation.domain_whitelist` / `peers` | runtime-tunable | add/remove peers over time; each add re-validated |
| `tls.acme.directory_url` / `ca_kind` | runtime-tunable | can switch CA (e.g. LE→step-ca); triggers re-issue; monitor for trust-chain change |
| `tls.acme.challenge` | runtime-tunable | switch challenge type if DNS/HTTP reachability changes |
| `tls.client_tls_mode` | tunable-with-care | self-signed→acme is fine; acme→self-signed weakens trust (§8) |
| `secrets.backend` | install-immutable (**migration-only**) | switching Vault↔Vaultwarden requires a secret migration; not a live flip |
| `secrets.connection.*` | runtime-tunable | rotate backend address/auth without changing which backend |
| `presence.heartbeat_interval_ms` | runtime-tunable | RFC-001 §4.5 default ~30s; pure tuning |
| `presence.miss_tolerance` | runtime-tunable | RFC-001 §4.5 default 2 |
| `escalation.dark_threshold_min` | runtime-tunable | RFC-001 §5/§11 — default 10min → fallback |
| `escalation.hil_threshold_min` | runtime-tunable | RFC-001 §5 — default +5min → HIL |
| `cert.expiry_warn_days` / `expiry_critical_days` | runtime-tunable | §3.4 alarm thresholds (default 14 / 3) |
| `federation.health_probe_interval` | runtime-tunable | §3.4 |
**Identity re-home note (the cost of changing `server_name`):** because every MXID (`@mosaic_*:server_name`) and room alias (`#…:server_name`) embeds `server_name`, changing it means: every agent gets a **new identity**, all rooms must be **recreated/re-aliased**, signed-authorship pubkey records re-published, and federation peers re-pointed. There is **no in-place rename** in Matrix. Hence `server_name` is install-immutable and the installer gates it behind an explicit warning (§6.7). Changing it is a **migration/reinstall**, honestly (§7).
### 5.4 How the appservice / homeserver read config
- **Synapse (homeserver)** reads a _rendered_ `homeserver.yaml`. The config service **renders** Synapse's config (server_name, listeners, `federation_domain_whitelist`, `enable_registration: false`, appservice registration path, TLS/delegation) from the DB-backed config at deploy/reconfigure time. Synapse itself is not DB-config-aware; the source of truth is the product DB, and Synapse config is a **generated artifact**. Certain Synapse values (notably `server_name`) require a **Synapse restart** and are exactly the immutable ones — reinforcing §5.3.
- **The appservice** reads config **live** from the DB config service for runtime-tunable values (thresholds, whitelist changes, cert alarm thresholds) and from the `SecretBackend` for secrets. Immutable topology values are read once at boot (they can't change under it).
- **`packages/comms` SDK** receives the values it needs (homeserver client URL, presence intervals) from the appservice at enroll (RFC-001 §4.1 returns `{mxid, access_token, homeserver, rooms[]}`), so agents never read the config DB directly.
- **Delegation artifacts** (`.well-known/matrix/server`, `.well-known/matrix/client`) are likewise **rendered** from config and served by the reverse proxy / homeserver.
---
## 6. Installer UX flow
A guided installer (`mosaic comms install` or equivalent) that captures topology, provisions certs, wires secrets, and **validates before declaring success.** It must never report success it hasn't proven. Steps:
**6.1 — Preflight & detection.** Detect existing DNS records for a candidate domain, existing certs, an existing reachable Synapse, an existing Vault/Vaultwarden. Offer detected values as suggestions (never as silent defaults). Detect whether the host has public inbound :80/:443 (informs challenge-type guidance, §3.2).
**6.2 — Primary instance (ALWAYS).** The PRIMARY/home instance is always configured — there is no "skip primary." Prompt for its identity. This is non-optional and is what makes standalone work out of the box.
**6.3 — Pick topology mode (A/B/C).** Ask the shape:
- Do you have a domain? **No →** Mode C (IP-only standalone); warn federation is impossible here (§2.4) and that `server_name` will be an IP (re-home cost if they later want federation).
- Yes, and identity domain differs from the homeserver host? **Yes →** Mode A (split-domain); capture `server_name` + host + delegation method.
- Yes, one domain does everything? **→** Mode B (single-domain).
**6.4 — Pick CA (ACME directory).** step-ca vs Let's Encrypt → capture `directory_url`, account email, and challenge type with the §3.2 guidance surfaced (e.g. "private/internal domain? → DNS-01"). Capture EAB if the CA requires it (→ SecretBackend). For step-ca, offer to point at an existing step-ca or document standing one up.
**6.5 — Pick secret backend.** Vault vs Vaultwarden → capture connection (address, org/namespace, bootstrap auth). If Vaultwarden, walk the org/collection setup (§4.4) and **surface the machine-account maturity [VERIFY]** honestly so the operator chooses eyes-open.
**6.6 — Federation (OPTIONAL).** Only offered if Mode A/B. Ask whether to enable federation now; if yes, capture peer `server_name`s and build the `domain_whitelist`. If Mode C, federation is not offered (greyed out with the explanation). Federation-off is a first-class, fully-supported outcome.
**6.7 — The "what can't be changed later" warning gate.** Before writing immutable config, present an explicit confirmation:
```
⚠ IMMUTABLE CHOICES — read before confirming
server_name = "mosaic.woltje.com"
This becomes part of every agent identity (e.g. @mosaic_coordinator-1:mosaic.woltje.com)
and every room alias. It CANNOT be changed later without re-homing every identity
(new MXIDs for all agents, recreating all rooms). There is no in-place rename in Matrix.
topology.mode = "split-domain" — changing modes later is a reinstall.
secrets.backend = "vaultwarden" — switching backends later requires a secret migration.
Type the server_name to confirm you understand it is permanent: ____________
```
The operator must **retype `server_name`** to proceed — a deliberate friction gate on the one truly permanent value.
**6.8 — Provision & validate (no success claim until proven).** The installer then:
1. Renders Synapse config + delegation artifacts; brings up Synapse with `enable_registration: false`.
2. Runs the ACME flow; obtains cert(s); verifies they're valid and installed.
3. Authenticates to the SecretBackend; stores `hs_token`/`as_token`, enroll bootstrap; runs `health()`.
4. **Reachability & cert validation** appropriate to mode:
- Mode A: fetch our own `.well-known`/SRV, confirm it points at host; TLS-probe host cert as a peer would; confirm C-S discovery.
- Mode B: TLS-probe the single domain; confirm C-S + (if federation) S2S.
- Mode C: confirm local C-S reachability over the bind IP; confirm (self-signed or private-CA) client TLS; **explicitly report "standalone — federation not available."**
- If federation enabled: validate each peer resolves + presents a peer-trusted cert; confirm `domain_whitelist` mutual consistency. If any peer fails, **federation is reported NOT-ready** — the primary still succeeds standalone.
5. **Only now** declare success, with a per-capability report: `PRIMARY: ✅ | CERT: ✅ (expires in 90d, auto-renew on) | SECRETS: ✅ (vaultwarden) | FEDERATION: ✅ 1 peer / ⚠ not-ready / ⛔ n-a (Mode C)`.
**6.9 — Post-install.** Emit the cert-expiry monitor + federation-health probe (§3.4) into OTEL; write config to DB with correct mutability flags; print the immutable-values summary again for the record.
---
## 7. Standalone → federated upgrade path
An operator who started standalone later wants federation. The path depends on **whether they started with a domain**:
**Case 1 — started Mode A or B (had a domain), federation was just off.** _Cheap, no re-home._ `server_name` is already a real domain and identities are already minted against it. To federate:
1. Ensure DNS resolves for peers (their `server_name`s and yours) — likely already true.
2. Ensure a **valid, peer-trusted cert** on the federation endpoint (if they were running client-only self-signed, they now need a real ACME cert; if already ACME, done).
3. Set `federation.enabled = true`, populate `domain_whitelist` + `peers` (all runtime-tunable, §5.3).
4. Re-run the installer's **federation validation** (§6.8 step 4) against each peer. On green, federation is live. **No identity change** — existing MXIDs simply become reachable cross-site. This is the intended, low-friction upgrade.
**Case 2 — started Mode C (IP-only), now wants federation.** _Expensive — an identity re-home, and we say so plainly._ Federation requires DNS + a valid cert (§2.4), which an IP `server_name` can never satisfy. So the operator must:
1. **Acquire a domain** and DNS, and provision a **valid ACME cert** (LE public, or step-ca if the domain is private — but note a _private_ domain can only federate with peers who trust that private CA root, §8).
2. **Change `server_name` from the IP literal to the domain** — this is the **install-immutable value**, so this is a **re-home, not a config tweak**:
- Every agent identity `@mosaic_*:192.168.1.50:8448` becomes `@mosaic_*:newdomain` — **all new MXIDs.**
- Every room + alias must be **recreated** under the new `server_name`.
- Signed-authorship pubkey records re-published under the new identities.
- Any durable references to old MXIDs (escalation policies, fallback-coordinator targets, RFC-001 §5) must be re-pointed.
3. Effectively: **treat it as a fresh install in Mode A/B with a data migration of rooms/history**, not an in-place flip. Matrix has **no in-place `server_name` rename**; this cost is intrinsic to Matrix, not to our design.
**Honest guidance the installer gives Mode C operators up front (§6.3):** "If there is _any_ chance you'll want to federate later, start with a domain (Mode A/B) even if you keep federation off — flipping federation on later is free, but changing an IP `server_name` to a domain later is a full identity re-home." This lets an informed operator avoid the expensive path by choosing Mode B-with-federation-off instead of Mode C.
---
## 8. Security
**8.1 — Cert trust model per CA choice.**
- **Let's Encrypt (public):** chains to a universally-trusted root (ISRG). Peers, humans' browsers, and Element trust it with no extra distribution. Best for public domains; nothing to distribute.
- **step-ca (private):** chains to a **root you operate**. Nothing trusts it by default. Therefore the **step-ca root must be distributed** to everyone who validates certs: peer homeservers (so cross-site S2S validates — a peer must add your root to its federation trust store, **[VERIFY]** Synapse's mechanism for trusting a custom federation CA), agent hosts, and any human client. This is the price of "total control" and airgap capability. For **federation between two private-CA sites**, both sites must trust each other's roots (or a shared root). Getting this wrong reproduces the silent-defederation failure (§3.4) — a peer that doesn't trust your root silently refuses your S2S.
- **Mode C self-signed client TLS:** weakest — see 8.4.
**8.2 — Federation whitelist.** `federation_domain_whitelist` is a **hard allowlist** (RFC-001 §6/NG5): only listed Mosaic site domains may federate; no public-network federation. The installer/config validator keeps the whitelist consistent with the declared peer list. Adding a peer is an explicit, audited config change.
**8.3 — Secret-backend auth.** The appservice/orchestrator authenticate to the `SecretBackend` via a **bootstrap credential injected at deploy** (Vault AppRole secret_id, or Vaultwarden org automation principal), never committed, consistent with existing Gateway/DB secret handling (RFC-001 §8). The `as_token`/`hs_token` live **only** in backend + appservice memory; agents get only their own scoped, re-mintable token (§4.2). Enroll is authenticated (RFC-001 B5) so a rogue local process can't enroll a rogue agent. Backend access is scoped: an agent's credential can read only its collection/policy, never the fleet-admin scope holding the crown jewels.
**8.4 — Honest note: IP-only standalone with self-signed client TLS is a weaker-trust local mode.** In Mode C, client TLS may be self-signed (or a local private CA). This means: no third party vouches for the endpoint; clients must be told to trust the self-signed cert (TOFU or manual root import); there is no external validation of who's on the other end. This is **acceptable and supported for local/airgapped/homelab** use where the network is already trusted, but it is **explicitly a weaker trust posture** than a real CA. The installer states this plainly at install (§6.8 Mode C). It is one more reason Mode C cannot federate: we will not extend this weaker-trust local posture across sites (NG5).
**8.5 — Homeserver hardening** (inherited from RFC-001 §8, config-rendered here): `enable_registration: false` always (agents come only via the appservice), rate-limiting on, admin API bound to localhost/behind auth, media repo locked/disabled if unused, TLS terminated at our controlled proxy. These are **rendered from config** (§5.4) so a stranger gets them by default, not by remembering to set them. **[VERIFY]** current recommended Synapse hardening flags at implementation.
---
## 9. How RFC-002 integrates with RFC-001's P1P5
RFC-002 is the **substrate**. Each RFC-001 phase consumes a subset of it. Critically, **P1 does not need the hard parts** — presence ships on a single clean-domain instance with no federation, no IP-only, and no secret-rotation story resolved.
| RFC-001 phase | RFC-002 pieces it NEEDS | RFC-002 pieces it does NOT need yet |
| ------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **P1 — Presence (first shippable slice)** | **Mode A or B, single-instance, clean domain** (§2.3). One ACME cert (LE or step-ca) via the single integration (§3). Config system minimal: `server_name` immutable + presence thresholds tunable (§5.3). Installer path 6.16.3, 6.4 (cert), 6.7 (immutable gate), 6.8 (validate single-instance). A **minimal** SecretBackend just holding the appservice token. | **No federation** (§2.4 gate irrelevant — single site). **No IP-only** needed for P1 (P1 wants a clean domain so Element/humans connect cleanly). **No agent-credential rotation** maturity. **No peer/whitelist** config. Vaultwarden machine-account [VERIFY] does **not** block P1. |
| **P2 — Appservice + auto-enroll** | Full `SecretBackend` **`enrollAgent`/`revokeAgent`** (§4.1), `hs_token`/`as_token` custody (§4.2), enroll-bootstrap secret. Config: room taxonomy, per-agent token classification. Chosen backend (Vault or Vaultwarden) real. | Federation, cross-site, IP-only. |
| **P3 — MACP v1 spec** | Config keys for MACP versioning/thresholds as runtime-tunable (§5.3); nothing new topology-wise. | Federation, secret rotation-in-anger. |
| **P4 — Federation** | **The whole federation half of RFC-002:** Mode A delegation (§3.3), the hard DNS+cert gate (§2.4), `domain_whitelist`+peers config, cert monitoring/silent-defederation alarm (§3.4), per-CA trust distribution for cross-site (§8.1). This is where standalone→federated (§7 Case 1) and Jason's `mosaic.woltje.com``mosaic.uscllc.com` shape land. | IP-only (federation excludes it by construction). |
| **P5 — Hardening + signed-authorship + Hermes retired** | Secret **rotation runbooks** executed in anger (§4, RFC-001 E2), pubkey-record custody for Ed25519 (§4.2), cert-rotation runbook (§3.4), full homeserver hardening validated (§8.5), backend auth review (§8.3). | — |
**One-line integration statement:** P1 rides on the _smallest_ slice of RFC-002 (single clean-domain instance + one cert + minimal config + minimal secret storage); the federation/IP-only/backend-maturity complexity is deferred to exactly the phases that need it (mostly P4/P5). RFC-002 therefore does not gate P1.
---
## 10. Open questions
Deliberately few — most topology/cert/secret decisions are resolved by Jason's rulings and baked in above.
1. **[VERIFY] IP-only `server_name` acceptance.** Does the deployed Synapse version accept an `ip:port` `server_name` and mint usable MXIDs for standalone (§2.3)? If not, Mode C uses a fabricated local domain (`mosaic.local` via local resolver) — confirm which, since it affects the re-home wording for Mode C→A/B (§7).
2. **[VERIFY] Vaultwarden machine-account maturity.** Confirm the current Vaultwarden version's Secrets Manager / machine-account coverage (§4.4). Determines whether the Vaultwarden adapter's `enrollAgent` uses native machine accounts or the personal-vault-per-agent + collection model. Does **not** block (interface absorbs either), but sets P2 expectations.
3. **[VERIFY] step-ca root distribution for cross-site federation.** Confirm Synapse's supported mechanism for trusting a **custom federation CA root** (§8.1) so two private-CA sites can federate. If Synapse won't easily trust a private federation CA, private-domain federation may in practice require public certs (LE) on the federation SANs even when internal traffic uses step-ca.
4. **Default secret backend for the published installer.** Given the open-source ethos (G6), should the installer _default-suggest_ Vaultwarden (free, self-hostable) while clearly offering Vault, or present them neutrally? Recommendation: suggest Vaultwarden as the zero-cost path with the maturity caveat surfaced, Vault as the "I need native short-TTL machine creds now" path. Jason to confirm the framing.
5. **Single multi-SAN cert vs two certs in Mode A** (§3.3) — operational preference for identity-domain + host coverage. Minor; validate during P4.
6. **Reconfigure-time Synapse restart policy.** Which rendered-config changes (§5.4) require a Synapse restart vs hot-reload on the deployed version, so the config service knows when a tunable change needs a bounce. **[VERIFY]** at implementation.
---
## Appendix A — Real mechanics this RFC leans on (quick reference)
- **`server_name`** — Synapse identity domain; the `:suffix` of every MXID/alias; install-immutable; changing it = re-home (no in-place rename). Distinct from where Synapse _listens_.
- **Delegation**`https://<server_name>/.well-known/matrix/server``{"m.server":"host:port"}` and/or `_matrix._tcp.<server_name>` **SRV**; how identity-domain ≠ host is reconciled (Mode A). **[VERIFY]** precedence on deployed Synapse.
- **`.well-known/matrix/client`** — C-S discovery so agents/Element find the homeserver host from the identity domain.
- **`federation_domain_whitelist`** — Synapse allowlist; only listed domains federate; our hard no-public-federation boundary.
- **ACME** — single provisioning protocol for both CAs; operator supplies **directory URL** (step-ca vs Let's Encrypt) + **challenge type**.
- **HTTP-01 / DNS-01 / TLS-ALPN-01** — challenge types; **DNS-01 is the one for private/split-horizon/wildcard**; HTTP-01 for public single host with :80; TLS-ALPN-01 for :443-only public.
- **step-ca ACME provisioner** — Smallstep's self-hosted CA exposing an ACME directory; enables private/internal-domain certs and total control; may require **EAB**; root must be distributed to validators.
- **Let's Encrypt** — public ACME CA; universally-trusted chain; 90-day certs; staging endpoint for testing.
- **Bitwarden/Vaultwarden org + collection + machine/service-account** — org holds collections; collections scope access; machine/service accounts (Bitwarden **Secrets Manager**) are the clean automation primitive but **Vaultwarden coverage is partial/evolving [VERIFY]**; personal-vault-per-agent + org-collection works today.
- **Vault KV v2 / AppRole / lease-TTL / revoke** — the capable backend; native versioning=rotate, TTL+revoke=transient per-agent creds.
- **Silent defederation** — a lapsed/renewal-failed federation cert causes peers to stop trusting S2S with no local error; must be monitored + alarmed (§3.4).
_All Matrix/ACME/secret-backend mechanics above are cited from architecture knowledge and MUST be re-verified against the actually deployed versions during implementation — every **[VERIFY]** is a checkpoint, not an assumption. Every illustrative domain (`mosaic.woltje.com`, `mosaic.uscllc.com`, `matrix.woltje.com`) is an operator-supplied example, never a product default or literal._

Some files were not shown because too many files have changed in this diff Show More