Compare commits
16 Commits
feat/827-g
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| b0d78d8632 | |||
| 344d86a635 | |||
| acd7d380f6 | |||
| 3b70c66c07 | |||
| 11d2818453 | |||
| aa999daf1b | |||
| 77c9a82614 | |||
| 627cf2bb38 | |||
| 0582a8912b | |||
| 2509eb7646 | |||
| 07553ead33 | |||
| e522b22fa4 | |||
| e4d7d4502d | |||
| 8dfcf1903e | |||
| abd2791f59 | |||
| 8ec67a1126 |
@@ -7,3 +7,4 @@ pnpm-lock.yaml
|
|||||||
.claude/
|
.claude/
|
||||||
docs/tess/TASKS.md
|
docs/tess/TASKS.md
|
||||||
docs/scratchpads/
|
docs/scratchpads/
|
||||||
|
packages/mosaic/src/fleet/testdata/documentation-publication-v1/inline-migration-v1.json
|
||||||
|
|||||||
23
docs/PRD.md
23
docs/PRD.md
@@ -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)
|
## Fleet Declarative Configuration Management Workstream (FCM, #758)
|
||||||
|
|
||||||
### Problem and objective
|
### Problem and objective
|
||||||
|
|||||||
@@ -1,5 +1,14 @@
|
|||||||
# Documentation Sitemap
|
# 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
|
## 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.
|
- [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
|
## 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.
|
- [Fleet configuration entry point](fleet/README.md) — desired-versus-observed decision tree and complete operator link map.
|
||||||
- [Roster v2 structural contract](fleet/reference/roster-v2-fields.md) — local-tmux schema v2 parsing and structural validation.
|
- [Desired, derived, and observed state](fleet/concepts/desired-vs-observed-state.md) — roster authority, generation, ownership, and drift.
|
||||||
- [Role classes and authority](fleet/reference/role-classes.md) — canonical role resolver and protected authority boundaries.
|
- [Identity, class, and runtime](fleet/concepts/identity-class-runtime.md) — stable name, display alias, class, runtime, provider, and model separation.
|
||||||
- [Executable asset dispositions](fleet/migration/example-profile-disposition.md) — shipped v1 fixture/profile/service validation posture.
|
- [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
|
## Official channel plugins
|
||||||
|
|
||||||
|
|||||||
@@ -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 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.
|
> the applicable acceptance evidence before merge. Issue #758 remains open until M5 closes.
|
||||||
|
|
||||||
| id | status | description | issue | agent | repo | branch | depends_on | estimate | notes |
|
| id | status | description | issue | agent | repo | branch | depends_on | estimate | notes |
|
||||||
| ---------- | ----------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----- | ------------- | ----------------- | --------------------------------------- | ---------------------------------------------- | -------- | ---------------------------------------------------------------------------------------------------------------- |
|
| ---------- | ----------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----- | ------------- | ----------------- | --------------------------------------- | ---------------------------------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||||
| FCM-M0-001 | done | Publish normative PRD requirements/acceptance criteria, this M0–M5 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-M0-001 | done | Publish normative PRD requirements/acceptance criteria, this M0–M5 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-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-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-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-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-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-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-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-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 | Never starts a previously stopped agent or kills an unproven unmanaged session |
|
| 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 | 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-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 | Final #758 gate: quality, independent code/security review, validator certificate, merge-gate approval, green CI |
|
| 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
|
## Thin-core prompt diet (#528) — feat/contract-thin-core
|
||||||
|
|
||||||
|
|||||||
59
docs/architecture/compaction-revocation.md
Normal file
59
docs/architecture/compaction-revocation.md
Normal file
@@ -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.
|
||||||
35
docs/architecture/lease-broker-protocol.md
Normal file
35
docs/architecture/lease-broker-protocol.md
Normal file
@@ -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.
|
||||||
26
docs/architecture/lease-broker-security.md
Normal file
26
docs/architecture/lease-broker-security.md
Normal file
@@ -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
docs/architecture/mutator-class-gate.md
Normal file
72
docs/architecture/mutator-class-gate.md
Normal 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.
|
||||||
@@ -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-v5’s 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 spec’s 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. Claude’s stream recorded successful hook execution, and the real model’s 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 R1’s 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
|
|
||||||
2
docs/compaction-refresh/probes/.gitignore
vendored
2
docs/compaction-refresh/probes/.gitignore
vendored
@@ -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()
|
|
||||||
@@ -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,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@@ -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()
|
|
||||||
228
docs/compaction-refresh/probes/p5_receipt_replay.py
Normal file
228
docs/compaction-refresh/probes/p5_receipt_replay.py
Normal file
@@ -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()
|
|
||||||
255
docs/compaction-refresh/probes/p6_constrained_recovery.py
Normal file
255
docs/compaction-refresh/probes/p6_constrained_recovery.py
Normal file
@@ -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 |
|
|
||||||
| --- | --- | --- |
|
|
||||||
| B5–B9 closure, bytecode, environment, and child-launch controls | L351–L575 | `3dcdcf75c8b1dea0a1f8888cf1e49a52aba51e7e41f447b80cf407d84533eb2e` |
|
|
||||||
| D4 generated lifecycle extension | L159–L251 | `4bdc047f7494029d951763555a797103ca25da647b255b287378771d20da209b` |
|
|
||||||
| D4 runtime assertions, including `lease_anchor_registered` | L579–L665 | `75620f25829fa7e176af8b342b2935d45afb546d821d45bf478c51e95f6918ca` |
|
|
||||||
| D4 orchestration and `--runs` parser | L749–L849 | `a28c36296b2ba8fffe77912b7e25b793047393bae79a9242df1727bd21ab2f92` |
|
|
||||||
|
|
||||||
Thus B5–B9 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:** B5–B9 slice includes launcher L511–L517: `-s -S -B` unchanged.
|
|
||||||
- **Broker flags unchanged:** B5–B9 slice includes broker L551–L556: `-I -S -B` unchanged.
|
|
||||||
- **`--runs` unchanged:** L840 remains `default=3, choices=(3,)` in the byte-identical L749–L849 slice.
|
|
||||||
- **Assertions intact:** `lease_anchor_registered` remains at L594–L596 in the byte-identical L579–L665 slice; no B5–B9 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) B5–B9 / D4 security-bearing bytes — PASS
|
|
||||||
|
|
||||||
The sole hunk is **old/new L484**, inside the diagnostic marker tuple (L481–489), and does not intersect B5–B9 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 |
|
|
||||||
| --- | --- | --- |
|
|
||||||
| B5–B9 controls before marker tuple | L351–L480 | `85eda967698af5699593daa12f761abd4b759a11a03323dc867905d286455aec` |
|
|
||||||
| B5–B9 controls after marker tuple | L490–L575 | `b92bd920d896450f89a14979dd9a557a82ad590d7c9a73e900ebc89979a48986` |
|
|
||||||
| D4 generated lifecycle extension | L159–L251 | `4bdc047f7494029d951763555a797103ca25da647b255b287378771d20da209b` |
|
|
||||||
| D4 runtime assertions (`lease_anchor_registered` included) | L579–L672 | `ddaae13f0302b401ada76f1c96b9f66f2938d7afa614928f4cffb187da832856` |
|
|
||||||
| D4 orchestration / `--runs` parser | L749–L849 | `a28c36296b2ba8fffe77912b7e25b793047393bae79a9242df1727bd21ab2f92` |
|
|
||||||
|
|
||||||
Therefore the diagnostic-only L484 hunk is non-intersecting and all B5–B9/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 L29–41; fixture socket bind/permissions L46–63; fixture-parent generation-file guard L71–75; SHA-256 `4db4fef1ac6658a8ca79ad5091cefc901d2aa26003265c3d6726c294cf895cad`. |
|
|
||||||
| Harness path/socket checks | Fixture broker socket equality and no inherited generation file: L452–455; all ten fixture-relative variables: L456–471; temporary-root socket requirement: L473–474. |
|
|
||||||
| Fixture construction satisfying those checks | Fixture paths/environment L675–736; root-owned workspace/sessions, socket, logs, and extension construction L749–759. |
|
|
||||||
| Complete pinned closure and hash gates | Exact three-object selection and SHA gate L397–433; materialized pinned files L435–443. |
|
|
||||||
|
|
||||||
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:** L41–L43 byte-identical (`7396f46357620118d3da27aa074d422186600e8a4320fc0f56774439af14e877`).
|
|
||||||
- **SHA gate / before-exec rehashes / bytecode rejection:** unchanged: materialize SHA gate L426; rehashes L539/L565/L568; `reject_pinned_bytecode` L498–501.
|
|
||||||
- **Fixture:** L704 pre-create and L709 `exist_ok=True` are byte-identical to v11 (L700–L710 slice SHA-256 `3137ef255435ac613fe1332138bbb13111a9d3efc502a34403b3dce21287b04e`).
|
|
||||||
- **Flags and runs:** unchanged launcher `-s -S -B` (L511–L517), broker `-I -S -B` (L551–L556), and `--runs default=3, choices=(3,)` (L840).
|
|
||||||
- **Assertions:** `lease_anchor_registered` is intact at L594–L596 in the byte-identical D4 assertion slice; no B5–B9/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,
|
|
||||||
i–iv) + 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** (`mos-orchestrator@mosaic.local`) — deliberately NOT the
|
|
||||||
`ms-lead-reviewer@mosaic.local` 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** (`mos-orchestrator@mosaic.local`) — deliberately NOT the
|
|
||||||
`ms-lead-reviewer@mosaic.local` 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** (`mos-orchestrator@mosaic.local`) — deliberately NOT the
|
|
||||||
`ms-lead-reviewer@mosaic.local` 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** (`mos-orchestrator@mosaic.local`) — deliberately NOT the
|
|
||||||
`ms-lead-reviewer@mosaic.local` 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`.
|
|
||||||
@@ -5,28 +5,17 @@
|
|||||||
This checklist is an acceptance contract for documentation and examples. It does not authorize
|
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
|
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
|
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
|
## M0 baseline acceptance
|
||||||
|
|
||||||
- [ ] `docs/PRD.md` states the roster as desired-state SSOT; generated environment, systemd,
|
- [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.
|
||||||
tmux, and heartbeat artifacts as non-authoritative projections; and fail-closed handling of
|
- [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.
|
||||||
unsupported or quarantined legacy input.
|
- [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.
|
||||||
- [ ] `docs/PRD.md` defines the required classes and authority boundary: `validator` certifies but
|
- [x] `docs/PRD.md` defines the generated-env/local-override boundary, explicitly denies arbitrary command overrides in M1–M5, and requires key-name/hash-only quarantine diagnostics.
|
||||||
does not merge; `merge-gate` remains sole approve-to-land/merge authority; `team-leader`
|
- [x] `docs/PRD.md` identifies the M1–M5 local-tmux scope and excludes remote reconciliation, connector mutation, secret references, arbitrary commands/channels, gateway convergence, and UI configuration storage.
|
||||||
capacity is lease-bounded; `interaction` is request/status only; instance names such as Tess
|
- [x] `docs/TASKS.md` contains the complete M0–M5 one-card/one-PR dependency DAG for #758 with agent tier, branch, dependency, estimate, and evidence expectations.
|
||||||
and Ultron remain configurable.
|
- [x] `docs/fleet/LEGACY-EXAMPLE-PROFILE-DISPOSITION-INVENTORY.md` classifies every current shipped fleet example, profile, and service preset before M1 implementation starts.
|
||||||
- [ ] `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 M1–M5, and requires key-name/hash-only quarantine diagnostics.
|
|
||||||
- [ ] `docs/PRD.md` identifies the M1–M5 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 M0–M5 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.
|
|
||||||
|
|
||||||
## Required documentation IA for M1–M5
|
## Required documentation IA for M1–M5
|
||||||
|
|
||||||
@@ -72,15 +61,18 @@ recorded in the M0 task/PR.
|
|||||||
|
|
||||||
## Cross-cutting evidence gates
|
## Cross-cutting evidence gates
|
||||||
|
|
||||||
- [ ] Every retained or migrated YAML/JSON example, profile, and service preset validates through the
|
- [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.
|
||||||
same executable schema and shared baseline-plus-`roles.local` resolver used by the CLI.
|
- [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.
|
||||||
- [ ] Every retired example/profile/service preset has a replacement link and deprecation note; no
|
- [x] Documentation examples contain no secret values, arbitrary command override, or product-hardcoded Tess/Ultron identity.
|
||||||
unresolved legacy class or tool-policy alias remains silently shipped.
|
- [x] CLI snippets distinguish local fleet desired-state commands from the separate gateway-backed mosaic agent catalog.
|
||||||
- [ ] Documentation examples contain no secret values, arbitrary command override, or product-hardcoded
|
- [x] Migration, quarantine, lifecycle, status, and troubleshooting documentation state that values of legacy sensitive keys are never printed.
|
||||||
Tess/Ultron identity.
|
- [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.
|
||||||
- [ ] CLI snippets distinguish local fleet desired-state commands from the separate gateway-backed
|
- [ ] 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.
|
||||||
`mosaic agent` catalog.
|
|
||||||
- [ ] Migration, quarantine, lifecycle, status, and troubleshooting documentation state that values of
|
## Held downstream gates
|
||||||
legacy sensitive keys are never printed.
|
|
||||||
- [ ] M5 release review verifies links, schema/example validation, and that all checklist rows have
|
These unchecked items are intentionally outside FCM-M5-001 and are not authorized by this checklist:
|
||||||
owner/evidence or an explicit approved deferral.
|
|
||||||
|
- [ ] 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.
|
||||||
|
|||||||
@@ -8,11 +8,11 @@ Generated environment files are rebuildable projections, not an operator-editabl
|
|||||||
| Layer | Responsibility |
|
| Layer | Responsibility |
|
||||||
| ------------------- | ------------------------------------------------------------------------------------------------------------------------------- |
|
| ------------------- | ------------------------------------------------------------------------------------------------------------------------------- |
|
||||||
| Roster | `fleet/roster.yaml` supplies the agent name, class, supported runtime, model, reasoning, tool policy, workdir, and tmux socket. |
|
| 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. |
|
| 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. |
|
| 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. |
|
| 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. |
|
| 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
|
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,
|
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
|
## 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
|
```dotenv
|
||||||
MOSAIC_AGENT_NAME=<roster name>
|
MOSAIC_AGENT_NAME=<roster name>
|
||||||
@@ -33,12 +33,12 @@ MOSAIC_AGENT_WORKDIR=<absolute roster work directory>
|
|||||||
MOSAIC_TMUX_SOCKET=<roster socket or empty>
|
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.
|
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;
|
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.
|
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_RUNTIME_BIN`
|
||||||
- `MOSAIC_HEARTBEAT_RUN_DIR`
|
- `MOSAIC_HEARTBEAT_RUN_DIR`
|
||||||
@@ -52,9 +52,9 @@ private, non-symlink paths. Violations fail closed before tmux interaction.
|
|||||||
|
|
||||||
## Legacy input and diagnostics
|
## 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
|
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
|
Diagnostics expose only rule code, key name, and a SHA-256 content hash. They do not reveal command
|
||||||
text, credentials, or other values.
|
text, credentials, or other values.
|
||||||
@@ -62,11 +62,11 @@ text, credentials, or other values.
|
|||||||
## Launch and stop behavior
|
## Launch and stop behavior
|
||||||
|
|
||||||
The launcher obtains the agent's socket only from the validated generated projection. It creates or
|
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 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.
|
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.
|
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
|
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
|
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 |
|
| 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/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/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 |
|
| `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
|
## Service presets
|
||||||
|
|
||||||
| Shipped file | Current policy evidence | M0 disposition decision | Required M1/M4 evidence |
|
| 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. |
|
| `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
|
## Required disposition controls
|
||||||
|
|
||||||
1. **No silent aliasing:** only `implementer → code`, `reviewer → review`, and
|
1. **No silent aliasing:** only implementer → code, reviewer → review, and
|
||||||
`operator-interaction → interaction` are approved deterministic aliases in this M0 baseline.
|
operator-interaction → interaction are approved deterministic aliases in this M0 baseline.
|
||||||
`worker`, `analyst`, `canary`, and domain-specific classes require resolver evidence or an
|
`worker`, `analyst`, `canary`, and domain-specific classes require resolver evidence or an
|
||||||
explicit version/retirement decision.
|
explicit version/retirement decision.
|
||||||
2. **No identity hardcoding:** Tess and Ultron are optional instance/display names. An example/profile
|
2. **No identity hardcoding:** Tess and Ultron are optional instance/display names. An example/profile
|
||||||
|
|||||||
@@ -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-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-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-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
|
## Workstreams
|
||||||
|
|
||||||
|
|||||||
@@ -97,7 +97,7 @@ success_criteria:
|
|||||||
- id: AC-NS-7
|
- id: AC-NS-7
|
||||||
text: >-
|
text: >-
|
||||||
A user-customized persona (edited or added via the orchestrator) survives
|
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:
|
workstreams:
|
||||||
- id: A
|
- id: A
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
## Mission
|
## Mission
|
||||||
|
|
||||||
Turn the proven fleet primitives into a **user-installable, AI-free-configurable fleet product**:
|
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
|
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
|
operate, mutate, re-create, and observe the fleet — over tmux today and Matrix tomorrow — from
|
||||||
CLI/TUI and (designed-for) the webUI.
|
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
|
### A. Configure-without-AI CLI
|
||||||
|
|
||||||
| ID | Requirement |
|
| ID | Requirement |
|
||||||
| --- | ------------------------------------------------------------------------------------------------------------- |
|
| --- | ----------------------------------------------------------------------------------------------------------- |
|
||||||
| R1 | `mosaic fleet` command set is functional end-to-end (init/install/start/stop/status/ps/verify + agent verbs). |
|
| 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. |
|
| R2 | mosaic fleet init is an interactive, **AI-free** CLI wizard. |
|
||||||
| R3 | Init asks the **configuration type**: `general`, `coding`, `research`, `hybrid`, … (extensible). |
|
| 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). |
|
| 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. |
|
| R5 | **Exactly one main orchestrator agent** is always configured, regardless of type. |
|
||||||
| R10 | A set of **recommended configurations (presets)** ships for easy duplication. |
|
| 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). |
|
| R8 | User can **re-create** the fleet when config needs change (idempotent re-init / reconfigure). |
|
||||||
| R17 | Fleet controls are **simple and intuitive**. |
|
| R17 | Fleet controls are **simple and intuitive**. |
|
||||||
|
|
||||||
### B. Comms & orchestrator chat-ops
|
### B. Comms & orchestrator chat-ops
|
||||||
|
|
||||||
| ID | Requirement |
|
| ID | Requirement |
|
||||||
| --- | --------------------------------------------------------------------------------------------------------------------------------- |
|
| --- | ----------------------------------------------------------------------------------------------------------------------------- |
|
||||||
| R6 | Init can wire the orchestrator to a chat connector — **Telegram / Discord / Matrix / Slack** — for command + comms. |
|
| 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**. |
|
| 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. |
|
| 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. |
|
| R19 | **"Mos" orchestrator on Discord** (chan 1517622518662434996 / srv 1112631390438166618) on `w-jarvis` — the first live target. |
|
||||||
|
|
||||||
### C. Runtime, health, lifecycle
|
### 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.
|
- **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.
|
- **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.
|
- **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).
|
- **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.
|
- **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)
|
## Phases (incremental, each shippable)
|
||||||
|
|
||||||
| Phase | Deliverable | Notes |
|
| 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 | R1–R5, R8, R10, R17 |
|
| **F1 Presets + init wizard** | preset rosters (general/coding/research/hybrid) + always-orchestrator + AI-free fleet init selecting a preset; re-init idempotent | R1–R5, 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 |
|
| **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 |
|
| **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 |
|
| **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 |
|
| **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 |
|
| **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) |
|
| **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)
|
## 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.
|
- **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 P4–P6 (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 P4–P6 (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.
|
- **Shared:** F4 Matrix (whoever has bandwidth); F7 testing/docs continuous.
|
||||||
|
|
||||||
## Immediate target: Mos on Discord (F2 first slice)
|
## 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`
|
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
|
(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.
|
~3s, reply via `mcp__discord__reply`, no `AskUserQuestion`). Heartbeat via the launcher sidecar.
|
||||||
|
|
||||||
## Success criteria
|
## Success criteria
|
||||||
|
|
||||||
- A non-AI user can `mosaic fleet init`, pick a type, and get a working fleet + orchestrator.
|
- 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.
|
- **Mos answers in Discord 1517…** on w-jarvis.
|
||||||
- Fleet runs + is observable (`fleet ps`) on **both** w-jarvis and dragon-lin.
|
- Fleet runs + is observable (fleet ps) on **both** w-jarvis and dragon-lin.
|
||||||
- Harness updates handled gracefully; HB healthy for claude + pi/gpt agents.
|
- Harness updates handled gracefully; HB healthy for claude + pi/gpt agents.
|
||||||
- Docs let a new operator install/configure/use the fleet.
|
- Docs let a new operator install/configure/use the fleet.
|
||||||
- Re-init + orchestrator mutation work.
|
- Re-init + orchestrator mutation work.
|
||||||
|
|
||||||
## Assumptions (veto-able)
|
## 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:` 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:` "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).
|
- `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).
|
||||||
|
|||||||
@@ -10,8 +10,8 @@
|
|||||||
The durable tmux fleet runs on the isolated `mosaic-fleet` socket. That isolation
|
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
|
(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
|
tooling, and truth is split across three planes no single command joins — systemd
|
||||||
(`systemctl --user`), tmux (`-L mosaic-fleet`), and the process tree (`pstree`).
|
(systemctl --user), tmux (-L mosaic-fleet), and the process tree (`pstree`).
|
||||||
`agent tail` (`capture-pane`) returns **blank for full-screen TUIs**, and `agent send`
|
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
|
confirms only keystroke injection, not acceptance. Net: the operator has near-zero
|
||||||
observability and no safe way to watch a session.
|
observability and no safe way to watch a session.
|
||||||
|
|
||||||
@@ -32,22 +32,22 @@ observability and no safe way to watch a session.
|
|||||||
|
|
||||||
## Functional requirements
|
## Functional requirements
|
||||||
|
|
||||||
| ID | Requirement |
|
| 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-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-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-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-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-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. |
|
| FR-6 | All structured output (`--json`) includes `tenant_id` and `host` fields. |
|
||||||
|
|
||||||
## Heartbeat protocol v1
|
## Heartbeat protocol v1
|
||||||
|
|
||||||
- **Probe:** operator/`fleet ps` writes a sentinel line to the agent's input or a
|
- **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`.
|
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>`
|
- **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.
|
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)
|
- **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
|
MUST emit the heartbeat. The protocol is file-based so it works for headless stubs and
|
||||||
full-screen TUIs alike (no `capture-pane` dependency).
|
full-screen TUIs alike (no `capture-pane` dependency).
|
||||||
@@ -56,15 +56,15 @@ observability and no safe way to watch a session.
|
|||||||
|
|
||||||
## Acceptance criteria
|
## 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
|
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).
|
`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`.
|
- 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.
|
leaves the agent's window size unchanged.
|
||||||
- `agent send --verify` returns success on an accepting pane and non-zero on a wedged/draft pane.
|
- 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
|
- Quality gates green: pnpm typecheck, pnpm lint, pnpm format:check, plus
|
||||||
`pnpm --filter @mosaicstack/mosaic test`.
|
pnpm --filter @mosaicstack/mosaic test.
|
||||||
- Independent review passed; dogfood evidence captured against the live fleet.
|
- Independent review passed; dogfood evidence captured against the live fleet.
|
||||||
|
|
||||||
## Test plan
|
## 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
|
- 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
|
`fleet-ps`/`watch`/`send-verify` spec) using the injected `CommandRunner` to assert
|
||||||
exact tmux/systemd command construction and JSON shape (tenant+host present).
|
exact tmux/systemd command construction and JSON shape (tenant+host present).
|
||||||
- Situational: run against the live `mosaic-fleet` fleet; capture `fleet ps` output,
|
- 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.
|
a kill-and-detect cycle, a read-only `watch`, and a send --verify pass/fail pair.
|
||||||
|
|
||||||
## Known limitations
|
## 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
|
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.
|
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
|
- **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,
|
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
|
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
|
'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
|
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
|
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.
|
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)
|
- **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
|
returns non-zero with an "unverifiable" message rather than silently succeeding. This
|
||||||
is an intentional fail-closed design (FR-5).
|
is an intentional fail-closed design (FR-5).
|
||||||
- **`agent watch` uses a grouped viewer session:** `tmux attach -r` directly against the
|
- **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
|
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
|
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-<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
|
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.
|
inherited stdio; the `interactiveRunner` handles TTY passthrough.
|
||||||
|
|
||||||
## Surfaces & parity (MVP-X1)
|
## Surfaces & parity (MVP-X1)
|
||||||
|
|||||||
63
docs/fleet/README.md
Normal file
63
docs/fleet/README.md
Normal 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.
|
||||||
@@ -7,21 +7,21 @@
|
|||||||
> Mission: `mvp-20260312` · PRD: [docs/fleet/PRD.md](./PRD.md) · North star: [docs/fleet/north-star.md](./north-star.md)
|
> 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`
|
> Status: `not-started` | `in-progress` | `done` | `blocked` | `failed`
|
||||||
|
|
||||||
| id | status | description | depends_on | agent | pr | notes |
|
| 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-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-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-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-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-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-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-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-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-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 |
|
| 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)
|
## 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) |
|
| 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) |
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -2,10 +2,10 @@
|
|||||||
|
|
||||||
The **backlog** is Mosaic's native backlog-of-record for fleet work. It is built
|
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)
|
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.
|
> **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
|
> anywhere in this feature. Anything previously delegated to Hermes is recreated
|
||||||
> here on Mosaic's own Postgres storage layer.
|
> 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 |
|
| `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 |
|
| `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
|
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
|
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
|
### 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
|
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
|
wipe the operator's backlog — same protection as the roster, per-agent env, and
|
||||||
heartbeat run dir.
|
heartbeat run dir.
|
||||||
|
|
||||||
@@ -46,10 +46,10 @@ A card is one row in the `backlog` table:
|
|||||||
| `phase` | text (nullable) | Board/phase grouping (see below). |
|
| `phase` | text (nullable) | Board/phase grouping (see below). |
|
||||||
| `priority` | int (default 0) | **Higher = sooner.** Claim picks the max-priority ready card. |
|
| `priority` | int (default 0) | **Higher = sooner.** Claim picks the max-priority ready card. |
|
||||||
| `status` | enum | `ready` \| `claimed` \| `blocked` \| `done`. |
|
| `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_owner` | text (nullable) | Owner token of the active claim. |
|
||||||
| `claim_ttl_seconds` | int (nullable) | TTL 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. |
|
| `attempts` | int (default 0) | Incremented each time the card is claimed. |
|
||||||
| `idempotency_key` | text (unique, null) | Dedups `create`; NULLs are distinct in Postgres. |
|
| `idempotency_key` | text (unique, null) | Dedups `create`; NULLs are distinct in Postgres. |
|
||||||
| `acceptance` | jsonb (nullable) | Acceptance criteria (array of strings or object). |
|
| `acceptance` | jsonb (nullable) | Acceptance criteria (array of strings or object). |
|
||||||
@@ -65,12 +65,12 @@ would add ceremony without benefit.
|
|||||||
### Board / phase convention
|
### Board / phase convention
|
||||||
|
|
||||||
`phase` is a free-form grouping string used as the board column / milestone label
|
`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.
|
`priority` orders cards **within** the ready pool regardless of phase.
|
||||||
|
|
||||||
## Status lifecycle
|
## Status lifecycle
|
||||||
|
|
||||||
```
|
```text-diagram
|
||||||
create
|
create
|
||||||
│
|
│
|
||||||
▼
|
▼
|
||||||
@@ -87,51 +87,49 @@ would add ceremony without benefit.
|
|||||||
- **blocked** — explicitly parked; never auto-claimed.
|
- **blocked** — explicitly parked; never auto-claimed.
|
||||||
- **done** — completed; satisfies dependents.
|
- **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
|
`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
|
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**
|
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
|
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`.)
|
`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`.
|
- **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`.
|
- **TTL:** claim --ttl <sec> (default **900s**) records `claim_ttl_seconds`.
|
||||||
- **reclaim:** releases claims whose `claimed_at + ttl` is in the past (expired)
|
- **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
|
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
|
specific card regardless of expiry. This is how a crashed worker's card returns
|
||||||
to the pool.
|
to the pool.
|
||||||
|
|
||||||
## CLI — `mosaic fleet backlog <sub> --json`
|
## CLI — mosaic fleet backlog <sub> --json
|
||||||
|
|
||||||
All subcommands support `--json`.
|
All subcommands support `--json`.
|
||||||
|
|
||||||
| Subcommand | Purpose |
|
| Subcommand | Purpose |
|
||||||
| --------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------- |
|
| ------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------- |
|
||||||
| `create --id --title [--body --phase --priority --depends-on --acceptance --idempotency-key]` | Create a card; `idempotency_key` dedups (repeat returns the existing card). |
|
| 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`. |
|
| 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`. |
|
| 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`. |
|
| 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`). |
|
| link --from --to | Add a `depends_on` edge (`--from` depends on `--to`). |
|
||||||
| `stats` | Counts by status, oldest-ready age, expired-claim count. |
|
| `stats` | Counts by status, oldest-ready age, expired-claim count. |
|
||||||
| `block --id` | Set a card to `blocked`. |
|
| block --id | Set a card to `blocked`. |
|
||||||
| `complete --id` | Set a card to `done` (releases any claim). |
|
| complete --id | Set a card to `done` (releases any claim). |
|
||||||
|
|
||||||
### Example
|
### Example
|
||||||
|
|
||||||
```sh
|
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.
|
||||||
# Seed two cards, the second depends on the first.
|
|
||||||
|
```fleet-command
|
||||||
mosaic fleet backlog create --id A1 --title "schema" --priority 5
|
mosaic fleet backlog create --id A1 --title "schema" --priority 5
|
||||||
mosaic fleet backlog create --id A2 --title "service" --depends-on A1 --priority 9
|
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
|
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 complete --id A1
|
||||||
mosaic fleet backlog list --ready-only --json
|
mosaic fleet backlog list --ready-only --json
|
||||||
|
|
||||||
# Recover stalled work.
|
|
||||||
mosaic fleet backlog reclaim --json
|
mosaic fleet backlog reclaim --json
|
||||||
```
|
```
|
||||||
|
|||||||
42
docs/fleet/concepts/desired-vs-observed-state.md
Normal file
42
docs/fleet/concepts/desired-vs-observed-state.md
Normal file
@@ -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.
|
||||||
23
docs/fleet/concepts/generated-env-launch-chain.md
Normal file
23
docs/fleet/concepts/generated-env-launch-chain.md
Normal file
@@ -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).
|
||||||
20
docs/fleet/concepts/identity-class-runtime.md
Normal file
20
docs/fleet/concepts/identity-class-runtime.md
Normal file
@@ -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.
|
||||||
22
docs/fleet/concepts/role-authority-and-leases.md
Normal file
22
docs/fleet/concepts/role-authority-and-leases.md
Normal file
@@ -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
docs/fleet/examples/roster-v2.yaml
Normal file
61
docs/fleet/examples/roster-v2.yaml
Normal 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
|
||||||
@@ -15,7 +15,7 @@ core.
|
|||||||
|
|
||||||
Connectors implement one small, uniform interface (`src/fleet/connectors/types.ts`):
|
Connectors implement one small, uniform interface (`src/fleet/connectors/types.ts`):
|
||||||
|
|
||||||
```ts
|
```typescript
|
||||||
interface OrchestratorConnector {
|
interface OrchestratorConnector {
|
||||||
readonly kind: 'tmux' | 'discord' | 'matrix';
|
readonly kind: 'tmux' | 'discord' | 'matrix';
|
||||||
send(message: OutboundMessage): Promise<SendResult>; // orchestrator → human
|
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
|
- **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
|
- **Thread-aware by metadata** — `OutboundMessage.threadId` / `InboundMessage.threadId` are
|
||||||
optional, so thread-capable connectors (Matrix rooms/threads, the future first-party Mosaic
|
optional, so thread-capable connectors (Matrix rooms/threads, the future first-party Mosaic
|
||||||
Discord plugin) fit **without an interface change**.
|
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
|
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.
|
`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
|
```yaml
|
||||||
connector:
|
connector:
|
||||||
kind: matrix # tmux | discord | matrix
|
kind: matrix
|
||||||
matrix:
|
matrix:
|
||||||
homeserver_url: https://matrix.example.internal
|
homeserver_url: https://matrix.example.internal
|
||||||
user_id: '@mos: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
|
The connector speaks the **Matrix client-server API** directly over HTTPS (`fetch` — no SDK needed
|
||||||
for MVP), so it is **homeserver-agnostic**:
|
for MVP), so it is **homeserver-agnostic**:
|
||||||
|
|
||||||
| Op | Matrix CS-API |
|
| Op | Matrix CS-API |
|
||||||
| ----------- | ------------------------------------------------------------------------ |
|
| ----------- | ----------------------------------------------------------------------- |
|
||||||
| `send` | `PUT /_matrix/client/v3/rooms/{roomId}/send/m.room.message/{txnId}` |
|
| `send` | PUT /\_matrix/client/v3/rooms/{roomId}/send/m.room.message/{txnId} |
|
||||||
| `subscribe` | `GET /_matrix/client/v3/sync` (long-poll, `since` token) → room timeline |
|
| `subscribe` | GET /\_matrix/client/v3/sync (long-poll, `since` token) → room timeline |
|
||||||
| `health` | `GET /_matrix/client/versions` (reachable) + `…/account/whoami` (authed) |
|
| `health` | GET /\_matrix/client/versions (reachable) + …/account/whoami (authed) |
|
||||||
| threads | `m.thread` relations ↔ `threadId` |
|
| threads | `m.thread` relations ↔ `threadId` |
|
||||||
|
|
||||||
## Local homeserver (infra, not connector code)
|
## 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 |
|
| **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 | 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 |
|
| 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 |
|
| 3 | Conduit deploy guide; first-party Mosaic Discord (threads) registers as a connector | follow |
|
||||||
|
|
||||||
|
|||||||
21
docs/fleet/how-to/configure-tess-interaction.md
Normal file
21
docs/fleet/how-to/configure-tess-interaction.md
Normal file
@@ -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).
|
||||||
21
docs/fleet/how-to/configure-ultron-validator.md
Normal file
21
docs/fleet/how-to/configure-ultron-validator.md
Normal file
@@ -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).
|
||||||
@@ -4,20 +4,20 @@ Use the local roster-v2 control plane only. These commands change desired state
|
|||||||
|
|
||||||
## Read and plan first
|
## Read and plan first
|
||||||
|
|
||||||
```sh
|
```fleet-synopsis
|
||||||
mosaic fleet get <name>
|
mosaic fleet get <name>
|
||||||
mosaic fleet plan create --expected-generation <n> --agent '<json>'
|
mosaic fleet plan create --expected-generation <n> --agent '<json>'
|
||||||
mosaic fleet plan update <name> --expected-generation <n> --agent '<json>'
|
mosaic fleet plan update <name> --expected-generation <n> --agent '<json>'
|
||||||
mosaic fleet plan delete <name> --expected-generation <n>
|
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
|
## Create safely
|
||||||
|
|
||||||
```sh
|
```fleet-command
|
||||||
mosaic fleet create --expected-generation 7 --agent '{
|
mosaic fleet create --expected-generation 7 --agent '{
|
||||||
"name":"coder0",
|
"name":"coder0",
|
||||||
"alias":"Coder 0",
|
"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
|
## Update and delete safely
|
||||||
|
|
||||||
```sh
|
```fleet-synopsis
|
||||||
mosaic fleet update coder0 --expected-generation 8 --agent '<complete JSON agent payload>'
|
mosaic fleet update <name> --expected-generation <n> --agent '<complete JSON agent payload>'
|
||||||
mosaic fleet delete coder0 --expected-generation 9
|
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.
|
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
|
## 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
|
## 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.
|
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.
|
||||||
|
|||||||
@@ -2,8 +2,8 @@
|
|||||||
|
|
||||||
Mosaic resolves persona contracts through two layers:
|
Mosaic resolves persona contracts through two layers:
|
||||||
|
|
||||||
1. `fleet/roles/<canonical-class>.md` — seeded baseline contract.
|
1. fleet/roles/<canonical-class>.md — seeded baseline contract.
|
||||||
2. `fleet/roles.local/<canonical-class>.md` — operator override or custom role; this layer wins.
|
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,
|
The same shared resolver is used by profile validation, provisioning, roster-v2 semantic validation,
|
||||||
and launch-time persona injection.
|
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.
|
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
|
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.
|
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
|
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
|
## Validation and authority boundaries
|
||||||
|
|
||||||
|
|||||||
@@ -2,22 +2,22 @@
|
|||||||
|
|
||||||
Use the canonical local roster-v2 command surface:
|
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> --dry-run
|
||||||
mosaic fleet apply --expected-generation <n>
|
mosaic fleet apply --expected-generation <n>
|
||||||
mosaic fleet reconcile --expected-generation <n>
|
mosaic fleet reconcile --expected-generation <n>
|
||||||
mosaic fleet start <name> --expected-generation <n>
|
mosaic fleet start <name> --expected-generation <n>
|
||||||
mosaic fleet stop <name> --expected-generation <n>
|
mosaic fleet stop <name> --expected-generation <n>
|
||||||
mosaic fleet restart <name> --expected-generation <n>
|
mosaic fleet restart <name> --expected-generation <n>
|
||||||
mosaic fleet status [name]
|
mosaic fleet status [<name>]
|
||||||
mosaic fleet verify
|
mosaic fleet verify
|
||||||
mosaic fleet doctor
|
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.
|
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
|
## Disposition rules
|
||||||
|
|
||||||
- **Explicit v1 fixture:** the artifact is loaded through the existing v1 roster parser and must
|
- **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.
|
roster or given inferred aliases.
|
||||||
- **Canonical profile:** the artifact is loaded through `loadProfiles`, which uses the shared
|
- **Canonical profile:** the artifact is loaded through `loadProfiles`, which uses the shared
|
||||||
baseline-plus-`roles.local` persona resolver and rejects unreadable or unresolved classes.
|
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
|
## Running the guard
|
||||||
|
|
||||||
```bash
|
```fleet-command
|
||||||
pnpm --filter @mosaicstack/mosaic test -- v1-v2-migration.spec.ts \
|
pnpm --filter @mosaicstack/mosaic test -- v1-v2-migration.spec.ts \
|
||||||
-t "validates all 13 shipped artifacts and executes ready previews for every v1 fixture"
|
-t "validates all 13 shipped artifacts and executes ready previews for every v1 fixture"
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -2,14 +2,14 @@
|
|||||||
|
|
||||||
**Issue:** #758 · **Card:** FCM-M4-001 · **Effect boundary:** preview only
|
**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
|
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.
|
`tmux`, contact connectors or remote hosts, launch an agent, run a canary, or execute rollback.
|
||||||
FCM-M4-002 owns reversible cutover and rollback.
|
FCM-M4-002 owns reversible cutover and rollback.
|
||||||
|
|
||||||
## Inputs
|
## Inputs
|
||||||
|
|
||||||
```bash
|
```fleet-command
|
||||||
mosaic fleet migrate-v1 preview \
|
mosaic fleet migrate-v1 preview \
|
||||||
--source roster-v1.yaml \
|
--source roster-v1.yaml \
|
||||||
--decisions migration-decisions.json \
|
--decisions migration-decisions.json \
|
||||||
@@ -44,25 +44,25 @@ be marked disabled. Observed-stopped agents always remain stopped.
|
|||||||
|
|
||||||
## Field disposition
|
## Field disposition
|
||||||
|
|
||||||
| v1 field | v2 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 |
|
| `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 |
|
| 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 |
|
| `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 |
|
| `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 |
|
| `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 `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 |
|
| 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 |
|
| root `connector` | Inventory-only; never contacted or reconciled |
|
||||||
| unknown fields or snake/camel synonym collisions | Inventoried and block readiness |
|
| unknown fields or snake/camel synonym collisions | Inventoried and block readiness |
|
||||||
| `.env.generated` | Rebuild from canonical roster data |
|
| `.env.generated` | Rebuild from canonical roster data |
|
||||||
| no legacy `.env` | `absent`; no legacy action required |
|
| 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 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 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 |
|
| 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
|
The only automatic aliases are implementer → code, reviewer → review, and
|
||||||
`operator-interaction → interaction`. Similar or domain-specific names are never inferred. Automatic
|
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
|
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.
|
baseline-plus-`roles.local` resolver after the candidate is compiled by the existing v2 compiler.
|
||||||
|
|
||||||
|
|||||||
@@ -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 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-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-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-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-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` |
|
| 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.
|
One **definition** is the source of truth; the **session** is how it runs.
|
||||||
|
|
||||||
| Layer | Owner | Phase-2 reality | Destination |
|
| 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 |
|
| **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 |
|
| **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 |
|
| **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" |
|
| **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 |
|
| **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 |
|
| **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 |
|
| **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 |
|
| **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`).
|
> **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);
|
> 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`
|
> onboarding cheat-sheet). The legacy dogfood canary still runs on the old `mosaic-factory`
|
||||||
> socket pending migration.
|
> 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
|
are enforced at the orchestrator + routing boundary, not inside individual workers (a worker never
|
||||||
decides its own budget — see delegation discipline).
|
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
|
carry **confidence tags** (`user` / `provider` / `estimated` / `unknown`); and **urgency/criticality
|
||||||
is a dispatch-gate modifier** — high-urgency work may override even-spread pacing **within
|
is a dispatch-gate modifier** — high-urgency work may override even-spread pacing **within
|
||||||
authorization**. (Also feeds the budgeting workstream, not only this doc.)
|
authorization**. (Also feeds the budgeting workstream, not only this doc.)
|
||||||
|
|
||||||
## Observation model
|
## Observation model
|
||||||
|
|
||||||
| Verb | Behavior |
|
| Verb | Behavior |
|
||||||
| ----------------------------------- | -------------------------------------------------------------------------------------------------- |
|
| --------------------------------- | -------------------------------------------------------------------------------------------------- |
|
||||||
| `mosaic fleet ps` | one table joining systemd + tmux + process + idle + last-heartbeat, with drift + boot-enable flags |
|
| 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 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 attach <name> | explicit interactive takeover (the only path that can type) |
|
||||||
| `mosaic agent send <name> --verify` | confirms message **accepted**, not merely keystroke-injected |
|
| 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`
|
> 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
|
> (blank for full-screen TUIs), and `attach` is read-write + resizes the session. The
|
||||||
> verbs above restore "join and observe" safely.
|
> verbs above restore "join and observe" safely.
|
||||||
|
|
||||||
@@ -214,7 +214,7 @@ compromised pane cannot corrupt or exfiltrate the register.
|
|||||||
| Layer | Responsibility | Implementation |
|
| Layer | Responsibility | Implementation |
|
||||||
| ---------------------- | ------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
| ---------------------- | ------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||||
| **Register** | Source of truth: agents, missions, tasks, heartbeats, spend | Postgres `fleet` schema — existing stack instance (`@mosaicstack/db`) |
|
| **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 |
|
| **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 |
|
| **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,
|
`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
|
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.
|
reference.
|
||||||
|
|
||||||
### Spend
|
### Spend
|
||||||
@@ -266,13 +266,13 @@ re-evaluate if isolation or write-volume demands it.
|
|||||||
|
|
||||||
## Phased roadmap
|
## Phased roadmap
|
||||||
|
|
||||||
| Phase | Outcome | Status |
|
| Phase | Outcome | Status |
|
||||||
| ---------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------- |
|
| ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------- |
|
||||||
| 0–1 | tmux PoC, hardening, published CLI v0.0.34 (#565–#568) | ✅ done |
|
| 0–1 | 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 |
|
| **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 |
|
| 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 |
|
| 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 |
|
| 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)
|
## 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.
|
- 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
|
- Runtimes: fleet agents default to **Codex / pi-on-Codex**; **Claude is reserved for Claude
|
||||||
Code only** (avoid alternate-harness API pricing). Validated durable recipe:
|
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),
|
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)
|
## 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
|
- **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
|
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.
|
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
|
**Enforcement split:** the _window_ lives in **~/.claude/settings.json** (host-global) as
|
||||||
`"autoCompactWindow": 200000` + `"autoCompactEnabled": true`; the _1M-disable_ lives in **launch
|
"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
|
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.)
|
agent is capped at spawn. (settings = window; env = 1M-disable.)
|
||||||
- **Worker context bound (#8):** workers are kept context-bounded via the **ephemeral-per-lane
|
- **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
|
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.
|
**stays Claude-specific** — the _principle_ (bounded context) extends to workers, the _knob_ does not.
|
||||||
- **Orchestrator delegation discipline:** the orchestrator **delegates all delivery work** to
|
- **Orchestrator delegation discipline:** the orchestrator **delegates all delivery work** to
|
||||||
subagents / workflows / ultracode / coder agents and confines its own context to \*\*orchestration
|
subagents / workflows / ultracode / coder agents and confines its own context to the personal-assistant
|
||||||
- the personal-assistant lane\*\*. Keeping delivery out of the orchestrator's window keeps its
|
lane. Keeping delivery out of the orchestrator's window keeps its context unpolluted and measurably
|
||||||
context unpolluted and measurably reduces off-plan divergence. The orchestrator coordinates and
|
reduces off-plan divergence. The orchestrator coordinates and decides; it does not implement.
|
||||||
decides; it does not implement.
|
|
||||||
- **Budget governance is fleet doctrine:** token/API-dollar budgeting is a first-class fleet concern
|
- **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
|
(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),
|
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
|
### 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.
|
- **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).
|
- **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.
|
- **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.
|
- **'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 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
|
**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 —
|
~/.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
|
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>`.
|
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
|
**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
|
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
|
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
|
self-hosted homeserver (Conduit default, Synapse alt). Matrix is named here as the strategic
|
||||||
future transport — peer to tmux/Discord, not superseded by them.
|
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 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
|
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
|
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
|
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
|
> 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
|
> [→ `--relaunch`]) must be documented so fleet changes actually land. (Also feeds the budgeting
|
||||||
> workstream.)
|
> workstream.)
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ connector entry.
|
|||||||
|
|
||||||
The preview evidence deliberately records:
|
The preview evidence deliberately records:
|
||||||
|
|
||||||
- `executable: false`;
|
- executable: false;
|
||||||
- required backup artifacts;
|
- required backup artifacts;
|
||||||
- source and candidate identities;
|
- source and candidate identities;
|
||||||
- lifecycle observations and resulting desired states;
|
- lifecycle observations and resulting desired states;
|
||||||
|
|||||||
20
docs/fleet/operations/env-quarantine.md
Normal file
20
docs/fleet/operations/env-quarantine.md
Normal 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
|
## Safe sequence
|
||||||
|
|
||||||
1. Read `mosaic fleet doctor` and `mosaic fleet status`.
|
1. Read mosaic fleet doctor and mosaic fleet status.
|
||||||
2. Run `mosaic fleet apply --expected-generation <n> --dry-run`.
|
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.
|
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.
|
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.
|
||||||
|
|||||||
24
docs/fleet/operations/systemd-tmux-troubleshooting.md
Normal file
24
docs/fleet/operations/systemd-tmux-troubleshooting.md
Normal file
@@ -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
docs/fleet/operations/upgrade-assets.md
Normal file
18
docs/fleet/operations/upgrade-assets.md
Normal 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).
|
||||||
@@ -4,17 +4,17 @@ FCM-M2-002 provides local roster-v2 create, get, update, delete, and plan operat
|
|||||||
|
|
||||||
## CLI contract
|
## 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 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 create --expected-generation <n> --agent '<json>' [--dry-run] [--persisted-start]
|
||||||
mosaic fleet update <name> --expected-generation <n> --agent '<json>' [--dry-run]
|
mosaic fleet update <name> --expected-generation <n> --agent '<json>' [--dry-run]
|
||||||
mosaic fleet delete <name> --expected-generation <n> [--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
|
## 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.
|
`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
|
## 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.
|
||||||
|
|||||||
@@ -1,27 +1,46 @@
|
|||||||
# Fleet Control-Plane CLI
|
# 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
|
## Roster-v2 desired-state commands
|
||||||
mosaic fleet apply --expected-generation <n> [--dry-run]
|
|
||||||
mosaic fleet reconcile --expected-generation <n> [--dry-run]
|
| Command | Effect | Generation | Output |
|
||||||
mosaic fleet start [name] --expected-generation <n> [--dry-run]
|
| ---------------------------------------------------------- | ---------------------------------------------- | ---------- | -------------------------- |
|
||||||
mosaic fleet stop [name] --expected-generation <n> [--dry-run]
|
| mosaic fleet get <name> | Read one authoritative agent | no | One JSON object |
|
||||||
mosaic fleet restart [name] --expected-generation <n> [--dry-run]
|
| mosaic fleet plan <create\|update\|delete> ... | Validate proposed CRUD and projections | required | One JSON object; no writes |
|
||||||
mosaic fleet status [name]
|
| 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 verify
|
||||||
mosaic fleet doctor
|
mosaic fleet doctor
|
||||||
mosaic fleet migrate-v1 preview --source <path> --decisions <path> --observations <path>
|
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
|
`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.
|
||||||
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).
|
|
||||||
|
|
||||||
`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.
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# Fleet Generated Environment Boundary
|
# 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,
|
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
|
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
|
## Paths and ownership
|
||||||
|
|
||||||
For agent `<name>` under `<MOSAIC_HOME>/fleet/agents/`:
|
For agent <name> under <MOSAIC_HOME>/fleet/agents/:
|
||||||
|
|
||||||
| Path | Owner | Purpose |
|
| Path | Owner | Purpose |
|
||||||
| ----------------------- | ------------------------ | ---------------------------------------------------------------------------------------------------------------------------- |
|
| --------------------- | ------------------------ | ---------------------------------------------------------------------------------------------------------------------------- |
|
||||||
| `<name>.env.generated` | Mosaic projection writer | Complete deterministic launch data rendered from the authoritative roster. |
|
| <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.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 | 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. |
|
| <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
|
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
|
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>
|
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
|
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`
|
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
|
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,
|
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
|
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
|
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.
|
shell sidecar resumes its `status=ok` fallback only after that marker is stale or absent.
|
||||||
|
|
||||||
## Legacy disposition
|
## 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
|
consolidated downstream interface packet. Status is deliberately separated from checkout presence: no
|
||||||
product release version has been evidenced for this interface set.
|
product release version has been evidenced for this interface set.
|
||||||
|
|
||||||
| Interface | Canonical public path and version | Tracker/release status | Downstream limit |
|
| 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 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 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 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. |
|
| 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 card-local and uncommitted; unreleased. | Render/write a roster-derived projection; local input is never authority. |
|
| 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,
|
Generated environment data is a rebuildable projection, not an operator-editable source of membership,
|
||||||
runtime policy, or lifecycle state.
|
runtime policy, or lifecycle state.
|
||||||
|
|
||||||
|
|||||||
@@ -1,14 +1,21 @@
|
|||||||
# Local Fleet Lifecycle Transitions
|
# 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 |
|
| Event | Desired-state write | Runtime effect | Safety boundary |
|
||||||
| --------------------------------- | ------------------- | -------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------- |
|
| ------------------------- | -------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||||
| `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 create | Adds enabled/stopped by default; `--persisted-start` records running | None | Generation-guarded; validates full roster/projections. |
|
||||||
| `fleet start <name>` | Never | One-shot exact `mosaic-agent@<name>.service` start | Current generation; exact enabled roster name; proven ownership |
|
| 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 stop <name>` | Never | One-shot exact service stop | Current generation; exact roster name; proven ownership |
|
| fleet delete | Removes exact roster member | None | Removes only generated projection; retains local/quarantine evidence. |
|
||||||
| `fleet restart <name>` | Never | One-shot exact service restart | Current generation; exact roster name; proven ownership |
|
| 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.
|
||||||
|
|||||||
@@ -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
|
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
|
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
|
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
|
`code.md`; a separate `roles.local/implementer.md` cannot redefine the legacy alias. A canonical
|
||||||
|
|||||||
@@ -50,36 +50,38 @@ agents:
|
|||||||
|
|
||||||
## Root fields
|
## Root fields
|
||||||
|
|
||||||
| Field | Required | Constraint | Meaning |
|
| Field | Required | Default | 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. |
|
| `version` | yes | none | integer constant `2` | Identifies this contract. Version `1` stays on the compatibility path pending explicit migration. |
|
||||||
| `generation` | yes | positive safe integer | Desired-state generation. M2 uses it for mutation guards; M1 does not mutate it. |
|
| `generation` | yes | none | positive safe integer | Desired-state generation and mutation/reconcile concurrency fence. |
|
||||||
| `transport` | yes | constant `tmux` | M1–M5 support local tmux only. |
|
| `transport` | yes | none | constant `tmux` | M1–M5 support local tmux only. |
|
||||||
| `tmux` | yes | strict object | Explicit local socket and holder-session configuration. |
|
| `tmux` | yes | none | strict object | Explicit local socket and holder-session configuration. |
|
||||||
| `defaults` | yes | strict object | Default work directory and one supported local runtime. |
|
| `defaults` | yes | none | strict object | Default work directory and one supported local runtime. |
|
||||||
| `runtimes` | yes | non-empty object | Declared local runtime reset policy map. |
|
| `runtimes` | yes | none | non-empty object | Declared local runtime reset policy map. |
|
||||||
| `agents` | yes | non-empty array | Local fleet entries. Duplicate stable names are rejected. |
|
| `agents` | yes | none | non-empty array | Local fleet entries. Duplicate stable names are rejected. |
|
||||||
|
|
||||||
## Nested fields
|
## Nested fields
|
||||||
|
|
||||||
| Path | Required | Constraint |
|
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.
|
||||||
| ---------------------------------------------------- | -------- | ------------------------------------------------------------------------------------------------------------- |
|
|
||||||
| `tmux.socket_name` | yes | `[A-Za-z0-9_.-]*`; empty string means the literal default tmux server, while a non-empty value names a socket |
|
| Path | Required | Constraint |
|
||||||
| `tmux.holder_session` | yes | non-empty `[A-Za-z0-9_.-]+` |
|
| -------------------------------------------------- | -------- | ------------------------------------------------------------------------------------------------------------ |
|
||||||
| `defaults.working_directory` | yes | non-empty string |
|
| `tmux.socket_name` | yes | [A-Za-z0-9_.-]\*; empty string means the literal default tmux server, while a non-empty value names a socket |
|
||||||
| `defaults.runtime` | yes | `claude`, `codex`, `opencode`, or `pi`; it must be declared in `runtimes` |
|
| `tmux.holder_session` | yes | non-empty [A-Za-z0-9_.-]+ |
|
||||||
| `runtimes.<runtime>.reset_command` | yes | non-empty string; runtime key must be a supported local runtime |
|
| `defaults.working_directory` | yes | non-empty string |
|
||||||
| `agents[].name` | yes | unique `[A-Za-z0-9][A-Za-z0-9_.-]*` stable machine identity |
|
| `defaults.runtime` | yes | `claude`, `codex`, `opencode`, or `pi`; it must be declared in `runtimes` |
|
||||||
| `agents[].alias` | yes | non-empty display string |
|
| runtimes.<runtime>.reset_command | yes | non-empty string; runtime key must be a supported local runtime |
|
||||||
| `agents[].class` | yes | `[a-z][a-z0-9-]*`; structural only in M1, semantic role resolution is FCM-M1-002 |
|
| agents[].name | yes | unique [A-Za-z0-9][A-Za-z0-9_.-]\* stable machine identity |
|
||||||
| `agents[].runtime` | yes | `claude`, `codex`, `opencode`, or `pi`; it must be declared in `runtimes` |
|
| agents[].alias | yes | non-empty display string |
|
||||||
| `agents[].provider`, `model`, `working_directory` | yes | non-empty strings; provider/model capability resolution is a later card |
|
| agents[].class | yes | [a-z][a-z0-9-]\*; structural only in M1, semantic role resolution is FCM-M1-002 |
|
||||||
| `agents[].reasoning` | yes | `low`, `medium`, or `high` |
|
| agents[].runtime | yes | `claude`, `codex`, `opencode`, or `pi`; it must be declared in `runtimes` |
|
||||||
| `agents[].tool_policy` | yes | `[a-z][a-z0-9-]*`; structural only in M1 |
|
| agents[].provider, `model`, `working_directory` | yes | non-empty strings; provider/model capability resolution is a later card |
|
||||||
| `agents[].persistent_persona`, `reset_between_tasks` | yes | booleans |
|
| agents[].reasoning | yes | `low`, `medium`, or `high` |
|
||||||
| `agents[].lifecycle.enabled` | yes | boolean; stored now, reconciled in FCM-M3-001 |
|
| agents[].tool_policy | yes | [a-z][a-z0-9-]\*; structural only in M1 |
|
||||||
| `agents[].lifecycle.desired_state` | yes | `running` or `stopped` |
|
| agents[].persistent_persona, `reset_between_tasks` | yes | booleans |
|
||||||
| `agents[].launch.yolo` | yes | boolean; structured data only, not an arbitrary command escape hatch |
|
| 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
|
## Semantic handoff
|
||||||
|
|
||||||
@@ -97,12 +99,12 @@ Semantic validation:
|
|||||||
`operator-interaction` to `interaction`;
|
`operator-interaction` to `interaction`;
|
||||||
- canonicalizes `tool_policy` with the same exact alias table;
|
- canonicalizes `tool_policy` with the same exact alias table;
|
||||||
- rejects protected class/tool-policy mismatches in either direction, while accepting
|
- 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`;
|
`interaction`;
|
||||||
- derives immutable protected authority only from canonical class; and
|
- derives immutable protected authority only from canonical class; and
|
||||||
- accepts custom baseline or `roles.local` classes without granting protected authority.
|
- 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
|
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
|
alias as separate authority. See [Role Classes and Authority](./role-classes.md) and
|
||||||
[Customize Fleet Roles](../how-to/customize-roles.md).
|
[Customize Fleet Roles](../how-to/customize-roles.md).
|
||||||
@@ -112,7 +114,7 @@ lifecycle mutation.
|
|||||||
|
|
||||||
## Fail-closed boundary
|
## 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
|
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,
|
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
|
and gateway fields because they are unsupported in the local-tmux M1 contract. It does not silently
|
||||||
|
|||||||
@@ -1,13 +1,25 @@
|
|||||||
# Local Fleet Status and Drift
|
# 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.
|
- **Desired:** roster membership, generation, enabled flag, and persisted running/stopped target.
|
||||||
- `unexpected-session`: a desired-`stopped` agent still has its exact session.
|
- **Managed/derived:** generated environment and expected exact service/session topology.
|
||||||
- `disabled-running`: a disabled roster agent has its exact session.
|
- **Observed by current roster-v2 commands:** systemd active state, tmux presence, exact holder ownership, and unmanaged sessions.
|
||||||
- `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.
|
|
||||||
|
|
||||||
`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
docs/guides/lease-broker-operations.md
Normal file
44
docs/guides/lease-broker-operations.md
Normal 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.
|
||||||
@@ -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.
|
||||||
54
docs/reports/deferred/758-fleet-config-deferrals.md
Normal file
54
docs/reports/deferred/758-fleet-config-deferrals.md
Normal file
@@ -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.
|
||||||
44
docs/reports/documentation/758-fleet-config-ia-closure.md
Normal file
44
docs/reports/documentation/758-fleet-config-ia-closure.md
Normal file
@@ -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.
|
||||||
58
docs/scratchpads/812-pr-review-comment.md
Normal file
58
docs/scratchpads/812-pr-review-comment.md
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
# Issue #812 — durable Gitea PR review comments
|
||||||
|
|
||||||
|
- **Lane:** ms-812
|
||||||
|
- **Branch:** `fix/812-pr-review-comment`
|
||||||
|
- **Issue:** mosaicstack/stack#812
|
||||||
|
- **Budget:** 15K working estimate; single focused shell-wrapper/test/docs change.
|
||||||
|
|
||||||
|
## Objective
|
||||||
|
|
||||||
|
Make the Gitea `comment` action in `packages/mosaic/framework/tools/git/pr-review.sh` use the supported Gitea comments REST API and report success only after provider read-back verifies the created comment against the intended repository, PR, and exact body.
|
||||||
|
|
||||||
|
## Plan
|
||||||
|
|
||||||
|
1. Add and commit a failing shell regression harness before production changes.
|
||||||
|
2. Verify RED against the nonexistent `tea pr comment` fallback false-positive.
|
||||||
|
3. Implement the minimal supported write plus ID-based provider read-back.
|
||||||
|
4. Document that wrapper write output is not durable provenance until read-back succeeds.
|
||||||
|
5. Run focused regression tests, touched-package tests, and repository quality gates.
|
||||||
|
6. Remediate review findings, queue-guard, and push for coordinator-owned independent review. Do not open or merge a PR.
|
||||||
|
|
||||||
|
## Progress checkpoints
|
||||||
|
|
||||||
|
- [x] RED regression committed and reported to mosaic-100 (rebased commit `770e3f57`)
|
||||||
|
- [x] Initial minimal fix implemented (rebased commit `ea7f8c57`)
|
||||||
|
- [x] Rebased cleanly onto main `627cf2bb387f7c84a532d88819903a7679ce0d72`
|
||||||
|
- [x] Codex blocker remediated by replacing unsupported `tea api` with authenticated REST write/read-back
|
||||||
|
- [x] Focused, package, and repository gates green
|
||||||
|
- [ ] Coordinator-owned independent review pending after push
|
||||||
|
- [x] No PR opened; no self-review or self-merge
|
||||||
|
|
||||||
|
## Tests run
|
||||||
|
|
||||||
|
- RED after rebase: the regression harness failed against `origin/main` with status 1 after reproducing the old `tea pr comment` zero-exit fallback and false success echo.
|
||||||
|
- GREEN at resumed head: the same harness passed with REST POST 201 plus GET 200 read-back.
|
||||||
|
- All `packages/mosaic/framework/tools/git/test-*.sh` harnesses passed.
|
||||||
|
- `shellcheck -x` passed for the changed scripts; `bash -n` passed.
|
||||||
|
- Manifest resolver returned `framework` for `tools/git/test-pr-review-gitea-comment.sh`.
|
||||||
|
- `pnpm test` passed (43/43 Turbo tasks; Mosaic 75 files/1434 tests; Gateway 56 files/628 tests plus documented skips).
|
||||||
|
- `pnpm typecheck` passed (42/42 tasks), `pnpm lint` passed (23/23), and `pnpm format:check` passed.
|
||||||
|
- Firewall checks found no user-home paths or operator identities in changed shipped files; no token value is logged or echoed.
|
||||||
|
|
||||||
|
## Risks / blockers
|
||||||
|
|
||||||
|
- No active implementation blocker. #789 reached terminal merged state and the coordination hold was lifted.
|
||||||
|
- Review round 1 found one portability blocker: the API base reconstructed `https://$host` and discarded configured schemes/path prefixes.
|
||||||
|
- Review round 2 found a second subpath portability blocker: clone-derived `get_repo_slug` retained the deployment prefix, duplicating it under `/api/v1/repos/`.
|
||||||
|
- Round 3 resolves owner/repo relative to the configured Gitea base path for HTTP(S) clones while preserving root-mounted and SSH clone forms. Host matching now compares non-default ports consistently.
|
||||||
|
- REST transport failures, non-201 writes, malformed/missing created IDs, non-200 read-backs, and read-back mismatches all fail closed.
|
||||||
|
- Existing approve/request-changes behavior remains covered.
|
||||||
|
- Independent exact-head re-review remains coordinator-owned.
|
||||||
|
|
||||||
|
## Final verification evidence
|
||||||
|
|
||||||
|
- URL-portability regression was RED before remediation at the new `http://git.mosaicstack.dev` case and GREEN afterward.
|
||||||
|
- Round-3 genuine subpath regression was RED against round-2 head `1b190201` and GREEN after the fix: `https://git.example/gitea/owner/repo.git` maps to API repository `owner/repo` under configured base `/gitea`.
|
||||||
|
- Regression coverage verifies POST and read-back GET for root-mounted HTTP(S), path-prefixed HTTP(S), non-default HTTP port, scp-style SSH, and `ssh://` clone forms.
|
||||||
|
- Focused shell checks, all git-wrapper harnesses, and full repository test/typecheck/lint/format gates passed after remediation.
|
||||||
|
- Branch will be force-pushed with lease for coordinator re-verification; no PR opened.
|
||||||
86
docs/scratchpads/828-lease-broker.md
Normal file
86
docs/scratchpads/828-lease-broker.md
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
# WI-1 Scratchpad — Authenticated external lease broker
|
||||||
|
|
||||||
|
- **Issue:** Gitea #828
|
||||||
|
- **Milestone:** 188 — Compaction-Refresh Mechanism (M1: Claude + Pi)
|
||||||
|
- **Branch:** `feat/828-lease-broker`
|
||||||
|
- **Starting HEAD:** `d801d6c4c8a984d6a95033c49714210018d3d9a8`
|
||||||
|
- **Session role:** Orchestrator coordinating implementation; Mos retains merge authority.
|
||||||
|
|
||||||
|
## Objective
|
||||||
|
|
||||||
|
Implement the ratified WI-1 product lease broker under `packages/mosaic/`: Linux `SO_PEERCRED` identity, broker-minted logical session IDs, `(pid,starttime)` launcher anchors with per-hop `/proc` starttime revalidation, sibling-substitution rejection, same-PID runtime-generation revocation, crypto-RNG single-use token persistence, and protected Unix-socket posture.
|
||||||
|
|
||||||
|
## Authority verification
|
||||||
|
|
||||||
|
Verified before code on session start; all exact SHA-256 values matched:
|
||||||
|
|
||||||
|
- BUILD-BRIEF: `89fdbc27ed0e5050dc7b52f3ef2ddaea691edf17fd89d51b15e26fb5ed47171b`
|
||||||
|
- SPEC-v5: `a6d07ade835758e8488ca10d3b0631caf0beb93ea3a6733631f151b0c2f01433`
|
||||||
|
- Ratification: `bac58319c9c4028b5b40e1129e0033cdb5a6b7b02033c25f06f4cb77d7779c67`
|
||||||
|
- P6 planner ruling: `b7bbb6ea6e8d9a5c3366993642ab4e4f65b961af04936dcac20bfbcdcbaf1a09`
|
||||||
|
- WI-0 Gate0 evidence: `5d418306fcc597fd514e500bee40d1509f0bf467e46ee13fc5c280ed8274759d`
|
||||||
|
|
||||||
|
## Locked constraints
|
||||||
|
|
||||||
|
- Build against the ratified design; do not re-derive it.
|
||||||
|
- Product code only in `packages/mosaic`; Gate0 Python probes are reference prototypes and are not shipped.
|
||||||
|
- Caller-supplied/asserted `session_id` is refused.
|
||||||
|
- Tokens use the operating-system CSPRNG via Python `secrets`; never `Math.random` or model output.
|
||||||
|
- Socket parent directory mode `0700`, socket mode `0600` minimum; document distinct-principal deployment as the stronger T-C-closing posture.
|
||||||
|
- Red-first TDD for six named cases; new-code coverage >=85%.
|
||||||
|
- No merge. PR must say `closes #828`; exact 40-character head handed to Mos for Opus-SECREV and independent review.
|
||||||
|
|
||||||
|
## Plan
|
||||||
|
|
||||||
|
1. Load security/testing/docs guidance and inspect existing `packages/mosaic` architecture.
|
||||||
|
2. Write the six required tests first and capture RED evidence.
|
||||||
|
3. Implement minimal broker modules and CLI/runtime integration necessary for product use.
|
||||||
|
4. Run focused tests with coverage, package gates, then full repository gates/suite.
|
||||||
|
5. Run author-side review/remediation, commit `closes #828`, queue guard, push, and open PR through Mosaic wrappers.
|
||||||
|
6. Send PR number + exact head SHA to `web1:mosaic-100`; stop without merging.
|
||||||
|
|
||||||
|
## Risks / boundaries
|
||||||
|
|
||||||
|
- Same-UID counterfeit socket replacement remains the disclosed T-C residual unless broker runs under a distinct principal; filesystem modes alone are minimum hardening, not a complete authenticity proof.
|
||||||
|
- `.mosaic/orchestrator/mission.json` and `.mosaic/orchestrator/session.lock` were already modified at session start and must not be included in this PR.
|
||||||
|
- Repository Woodpecker pipelines exist; CI is the canonical build path. No manual image build/deploy is in scope.
|
||||||
|
|
||||||
|
## Progress / evidence
|
||||||
|
|
||||||
|
- 2026-07-18 session start: mandatory mission files and orchestration guides loaded.
|
||||||
|
- STEP 0: all four authority hashes matched; artifacts read in full.
|
||||||
|
- Branch/HEAD confirmed; issue #828 open; Gate0 evidence hash confirmed.
|
||||||
|
- Initial RED: focused Vitest acceptance suite failed 11/11 because the product daemon did not exist; the expected missing-product failure was observed before implementation.
|
||||||
|
- Review-remediation RED: partial/zero-progress state writes, nested corrupt state, symlink state, canonical starttime, and duplicate-anchor generation behavior failed before their fixes. Real socket RED/GREEN runs were executed by the unrestricted parent harness because the delegated worker sandbox denies `AF_UNIX.bind()`.
|
||||||
|
- Product implementation added at `packages/mosaic/framework/tools/lease-broker/daemon.py`; Gate0 probe scripts were read as references but not copied or shipped.
|
||||||
|
- Independent Codex code review round 1 found 2 blockers + 1 should-fix (connection stall/crash, partial writes, packet-dependent framing); all were remediated with tests.
|
||||||
|
- Independent Codex code review round 2 found 2 blockers + 1 relevant should-fix (half-close contract ambiguity, incomplete persisted-state validation, symlink/non-regular state); all were remediated with tests and documentation. Pre-existing `.mosaic/*` session dirt remains excluded from the PR.
|
||||||
|
- Unrestricted focused situational suite: `35/35` GREEN.
|
||||||
|
- New Python product module coverage: `90%` (`356` statements, `36` missed), above the user-required 85%.
|
||||||
|
- Root typecheck: `42/42` Turbo tasks GREEN.
|
||||||
|
- Root lint: `23/23` Turbo tasks GREEN.
|
||||||
|
- Root format check: GREEN.
|
||||||
|
- Package build + suite: `71/71` files and `1,369/1,369` tests GREEN, including framework shell tests.
|
||||||
|
- Full root suite: `43/43` Turbo tasks GREEN after the oversized-frame production race fix.
|
||||||
|
- Focused acceptance suite: `35/35` GREEN in three consecutive unrestricted runs; exact-head instrumented run also `35/35` GREEN.
|
||||||
|
- Exact-head Python product coverage: `90%` (`365` statements, `37` missed), above the required 85%.
|
||||||
|
- Review-triggered oversized-frame race was fixed in production by bounded drain-to-EOF; tests were not changed.
|
||||||
|
- Commits banked in red/green cadence: `d61c5441` (RED contract), `deb11df7` (GREEN implementation/docs), `57770e34` (oversized-frame production fix).
|
||||||
|
- Final-review blocker remediated: added a 256-token pending-state cap, deletion on consume/generation revocation, pre-open serialized-size enforcement, and request-wide in-memory rollback for every broker mutation/commit failure while retaining the v1 live-token schema.
|
||||||
|
- Distinct-principal docs now state built-in `0700`/`0600` is same-principal only; WI-1 does not provide the external identity-preserving proxy/ACL/service boundary needed for the stronger deployment.
|
||||||
|
- Exact Python unit suite: `8/8` GREEN. Unrestricted focused acceptance: `35/35` GREEN.
|
||||||
|
- Exact-head package build/suite: `71/71` files and `1,369/1,369` tests GREEN.
|
||||||
|
- Exact-head Python product coverage: `90%` (`376` statements, `36` missed), above required 85%.
|
||||||
|
- Root typecheck: `42/42` GREEN. Root lint: `23/23` GREEN. Root format check and `git diff --check`: GREEN.
|
||||||
|
- Final exact-head rereview found two persistence blockers: post-rename directory-fsync uncertainty and acceptance of impossible persisted token records. RED was captured as three invariant failures plus one missing fail-stop error; commits `a94b1220` (RED) and `d05465e5` (GREEN) remediate both without weakening tests.
|
||||||
|
- Post-remediation evidence: Python unit suite `10/10`, focused real-socket acceptance `35/35`, full root suite `43/43` Turbo tasks, broker coverage `90%` (`395` statements, `38` missed), lint `23/23`, typecheck `42/42`, format check and `git diff --check` GREEN.
|
||||||
|
- Independent Codex review of remediation commit `d05465e54736c4966294c4af8fbd6a4ad8fe81aa`: APPROVE, confidence `0.94`, zero findings. Reviewer sandbox could not allocate temp directories; unrestricted parent test evidence above is canonical.
|
||||||
|
|
||||||
|
- Remediation session: terra review comment `18072` reproduced a SERIAL-ACCEPT DoS; scope is RED regressions plus bounded concurrent connection handling on PR #836, preserving all existing broker security properties.
|
||||||
|
- RED evidence against reviewed daemon: four silent peers delayed registration `3920 ms` beyond the `1500 ms` bound; 16 silent peers were not reaped within `2500 ms`. The first bounded implementation then exposed slot exhaustion by rejecting the valid queued caller with `EPIPE`; admission was corrected to wait for a reclaimed bounded slot. GREEN evidence: queued-peer test `211 ms`; strengthened cap/reap/reclaim test `1118 ms`; complete real-socket acceptance `37/37` and Python persistence suite `10/10`.
|
||||||
|
|
||||||
|
## Coordinator handoff requirements
|
||||||
|
|
||||||
|
1. Mandatory Opus-SECREV on the exact PR head; no GPT/terra substitute.
|
||||||
|
2. Independent exact-head code review and exact-head RoR before Mos-authorized merge.
|
||||||
|
3. Mos retains merge authority; this WI author stops after PR + full 40-character head handoff.
|
||||||
130
docs/scratchpads/829-mutator-gate.md
Normal file
130
docs/scratchpads/829-mutator-gate.md
Normal file
@@ -0,0 +1,130 @@
|
|||||||
|
# WI-2 Scratchpad — Whole mutator-class gate
|
||||||
|
|
||||||
|
- **Issue:** Gitea #829
|
||||||
|
- **Branch:** `feat/829-mutator-gate`
|
||||||
|
- **Base HEAD:** `8ec67a1126adb0dcd4c3a2bf5525f3e239c0b201`
|
||||||
|
- **Role:** sol author/build lane only; terra code review and Opus security review are coordinator-owned.
|
||||||
|
|
||||||
|
## Mission prompt
|
||||||
|
|
||||||
|
Implement BUILD-BRIEF Deliverable 2 as a framework-native whole mutator-class gate under `packages/mosaic/`, building against the merged WI-1 lease broker. No consequential mutator may succeed while UNVERIFIED after a compaction observer fires or after TTL. Carry the T-B compromised-tool acceptance criteria. Enforce revoke-first and promote-last structurally. A receipt is only a promotion prerequisite; the mutator-class gate remains the safety mechanism. M1 is Claude + Pi only.
|
||||||
|
|
||||||
|
## Authority verification
|
||||||
|
|
||||||
|
Verified exact SHA-256 before design/code:
|
||||||
|
|
||||||
|
- BUILD-BRIEF: `89fdbc27ed0e5050dc7b52f3ef2ddaea691edf17fd89d51b15e26fb5ed47171b`
|
||||||
|
- SPEC-v5: `a6d07ade835758e8488ca10d3b0631caf0beb93ea3a6733631f151b0c2f01433`
|
||||||
|
- Ratification: `bac58319c9c4028b5b40e1129e0033cdb5a6b7b02033c25f06f4cb77d7779c67`
|
||||||
|
- sol final red-team: `3da326a4ea91767b731e128a93b13194e8002358101e30de3fcb8ca2f8f54faa`
|
||||||
|
|
||||||
|
Carried authority chain also verified/read for the locked T-B gate contract: SPEC-v4 `a5e9c261…`, v4 sol `1e76ee59…`, SPEC-v3 `e0830ba0…`, v3 sol `9f321ade…`.
|
||||||
|
|
||||||
|
## Plan
|
||||||
|
|
||||||
|
1. RED real-socket acceptance tests for default-deny whole classes, T-B raw-tool bypass, observer/TTL revocation, and structural revoke-first/promote-last.
|
||||||
|
2. Extend the merged WI-1 broker as the sole lease authority; authenticate every transition through existing peercred/ancestry/session logic and consume WI-1 single-use cycle tokens atomically before promotion.
|
||||||
|
3. Add one broker-backed runtime gate executable and wire it across all Claude `PreToolUse` tools and Pi `tool_call`; unknown/custom tools deny by default.
|
||||||
|
4. Add proportional protocol/security/operations documentation and requirements-to-evidence mapping.
|
||||||
|
5. Run focused coverage, package/full suites, lint/typecheck/format, then queue-guard, push, open an unmerged PR with `closes #829`, and hand off the exact head.
|
||||||
|
|
||||||
|
## Risks and bounds
|
||||||
|
|
||||||
|
- Receipt parsing/builders and compaction observers are later WIs; WI-2 exposes the promotion prerequisite boundary but does not treat a receipt as safety authority.
|
||||||
|
- Broker restart intentionally loses volatile VERIFIED leases and therefore restarts UNVERIFIED; persistent WI-1 identity/token state remains unchanged.
|
||||||
|
- The gate is whole-class and does not parse shell command strings. T-C extension/hook absence and same-UID broker replacement remain outside the client guarantee and server branch protection remains the backstop.
|
||||||
|
- Initial lease TTL is capped at ratified 300 seconds; callers may only shorten it.
|
||||||
|
- Working budget assumption: 35K tokens; reduce documentation/refactor breadth before touching locked scope if pressure rises.
|
||||||
|
|
||||||
|
## Progress and verification
|
||||||
|
|
||||||
|
- RED #1: all 5 initial real-socket contract tests failed on WI-1 with `UNKNOWN_ACTION` or missing adapter behavior.
|
||||||
|
- RED #2: register-before-exec runtime test failed because `launch-runtime.py` did not exist.
|
||||||
|
- GREEN: broker-owned volatile lease state, 300-second maximum monotonic TTL, WI-1 token-backed promotion, all-tools runtime gate, Claude/Pi wiring, and register-before-exec launcher delivered without changing WI-1 peercred/ancestry authority.
|
||||||
|
- Focused broker + gate acceptance: `43/43` GREEN.
|
||||||
|
- Instrumented Python coverage: `88%` total — daemon `89%`, register/exec launcher `86%`, runtime gate `86%`.
|
||||||
|
- Full repository suite: `43/43` Turbo tasks GREEN; `@mosaicstack/mosaic` `72/72` files and `1,377/1,377` tests GREEN.
|
||||||
|
- Root typecheck: `42/42`; lint: `23/23`; format check and `git diff --check`: GREEN.
|
||||||
|
- No author self-review was run. Exact-head terra CODE and Opus SECREV remain coordinator-owned gates.
|
||||||
|
|
||||||
|
## Acceptance mapping
|
||||||
|
|
||||||
|
| Acceptance criterion | Evidence |
|
||||||
|
| --- | --- |
|
||||||
|
| No consequential mutator succeeds while UNVERIFIED after observer revoke or TTL | `observer revocation and monotonic TTL expiry deny the next mutator` real-socket acceptance test |
|
||||||
|
| T-B compromised-tool bypass is covered by the whole gate | `T-B raw and custom mutator tools are default-denied without shell parsing` across Claude/Pi built-ins, raw Bash class, MCP/custom/unknown tools |
|
||||||
|
| Revoke-first / promote-last is structural | `revoke-first and promote-last structurally bracket mutator authority`; direct promotion rejected, pending remains denied, token consumption commits before VERIFIED |
|
||||||
|
| Consume WI-1 auth/lease substrate | All transitions and decisions traverse merged peercred/ancestry authentication; promotion consumes the exact WI-1 CSPRNG cycle token |
|
||||||
|
| M1 Claude + Pi | Claude `.*` PreToolUse and Pi `tool_call` invoke the same broker gate; register-before-exec test proves broker-minted parent identity reaches runtime descendants |
|
||||||
|
|
||||||
|
## Locked discipline
|
||||||
|
|
||||||
|
- Re-verify and read the four authority artifacts before design or code.
|
||||||
|
- RED-first tests must cover unverified mutation refusal, T-B compromised-tool refusal, and revoke-first/promote-last ordering.
|
||||||
|
- Consume WI-1 VERIFIED-lease state; do not re-derive kernel identity, ancestry, sessions, or token authentication.
|
||||||
|
- Minimum 85% new-code coverage; full suite, lint, typecheck, and format checks green.
|
||||||
|
- Build only: no self-review and no merge. Open a PR containing `closes #829`, report its exact 40-character head, then exit.
|
||||||
|
|
||||||
|
## Remediation — terra CODE comment 18091
|
||||||
|
|
||||||
|
- Coordinator correction: terra returned REQUEST CHANGES at head `77b137ccc04b5be035cac5ca21bbbf3df8b94f97`; Opus SECREV was GO and CI green, but no evidence transfers to the remediated head.
|
||||||
|
- BLOCKER 1 verified: first-class Claude/Pi route through `execLeaseGatedRuntime`, while the supported Claudex path preserves isolation but directly invokes `claude`; it therefore registers no anchor, injects no lease session, and the isolated config has no guaranteed all-tools gate hook.
|
||||||
|
- BLOCKER 2 accepted: prior 88% was aggregate evidence. Remediation must produce independently measured branch coverage of at least 85% for each new executable (`launch-runtime.py`, `mutator-gate.py`, and daemon delta evidence), including successful exec-boundary collection and validation/error branches.
|
||||||
|
- Remediation discipline: RED tests first; preserve the reviewed-good broker lock/state-transition ordering; update PR #837 on the same branch; no self-review or merge.
|
||||||
|
|
||||||
|
### Remediation evidence
|
||||||
|
|
||||||
|
- RED commit `046896c6`: both `mosaic claudex` and `mosaic yolo claudex` behavioral probes exited 1 because the direct path supplied neither a broker session nor the isolated all-tools hook; branch-focused Python tests failed on the absent injectable boundaries. Fresh WI-2 daemon-delta instrumentation also failed the ≥85% branch gate at 75%.
|
||||||
|
- GREEN: Claudex now exposes only `execLeaseGated`, passes the preserved isolated proxy environment through the shared register-before-exec wrapper, and merges the exact `.*` mutator hook into isolated `settings.json` with mode `0600`. Missing broker/identity, malformed or symlinked settings, and an unverified consequential tool all fail closed. The broker transition/lock implementation was not changed.
|
||||||
|
- Behavioral regression: normal and YOLO Claudex both receive a 64-hex broker session, retain their mode-specific arguments, observe the exact all-tools hook, and receive status 2 for unverified `Bash`.
|
||||||
|
- Independent branch coverage: `launch-runtime.py` 16/18 = **89%** (statements 98%); `mutator-gate.py` 21/22 = **95%** (statements 99%); `daemon.py` WI-2 delta 35/40 = **88%** (whole-file branch 80%, statements 90%).
|
||||||
|
- Fresh focused real-socket coverage run: WI-1 + WI-2 acceptance `46/46`; persistence `10/10`; branch unit suite `10/10`.
|
||||||
|
- Fresh full repository suite: `43/43` Turbo tasks; `@mosaicstack/mosaic` `72/72` files and `1,381/1,381` tests.
|
||||||
|
- Fresh root gates: typecheck `42/42`; lint `23/23`; format and `git diff --check` GREEN.
|
||||||
|
- PR #837 remains open and unmerged. Terra CODE and Opus SECREV must both rerun from zero on the exact remediated head before coordinator-owned merge authorization.
|
||||||
|
|
||||||
|
## Remediation round 3 — terra CODE comment 18099 + binding upgrade
|
||||||
|
|
||||||
|
- Locked-good surfaces: Claudex gating and B2 per-executable coverage are verified; do not regress them. Broker state-transition/lock ordering remains untouched.
|
||||||
|
- Mechanical repository sweep found direct executing Claude entries in PRDY init, PRDY update, QA remediation, and `@mosaicstack/coord` task launch. It also found a direct Claude command rendered into the QA report template and documentation examples. Existing Mosaic CLI Claude/Pi/Claudex, orchestrator session-run, and fleet starts already reach the gated boundary.
|
||||||
|
- Elevated hard requirements: ship a permanent suite/CI guard that scans production source and fails on any direct Claude/Pi launch; route every executing entry through one common gated wrapper; add real-broker RED/GREEN tests for PRDY init/update and QA; preserve each environment and denial behavior; independently measure all new executable coverage at ≥85%.
|
||||||
|
- Round-3 plan: first commit RED behavioral and scanner-contract tests; then add one framework `launch-runtime.sh` choke-point over `launch-runtime.py`, make Mosaic CLI and shell launchers use it, make coord route through `mosaic`, and wire the permanent guard into package tests. Update all discovered operator-facing direct-launch examples so the scanner inventory remains complete.
|
||||||
|
|
||||||
|
### Round-3 outcome
|
||||||
|
|
||||||
|
- RED commit `7f3418fa`: PRDY init, PRDY update, and QA remediation all reached the fake Claude binary without a broker session even when the configured socket did not exist; the permanent-guard contract initially failed because its executable was absent, then failed against the five discovered direct entries (four executing plus the QA command template).
|
||||||
|
- Choke-point decision: a new shell layer was unnecessary. Every executing repository entry now converges directly or through `mosaic`/`execLeaseGatedRuntime` on the existing single `launch-runtime.py` register-then-exec wrapper. PRDY and QA preserve their working directories, prompts, flags, logging pipe, and environment. Coord rewrites direct Claude commands to `mosaic claude` and rejects unknown custom Claude launchers fail-closed.
|
||||||
|
- Permanent guard: `packages/mosaic/framework/tools/lease-broker/check-runtime-launches.py`, invoked by `packages/mosaic/package.json` `test:framework-shell` and therefore root `pnpm test`/CI. It scans production code under `packages/`, `apps/`, `plugins/`, and `tools/` and rejects literal, absolute-path, process-API, command-array, and dynamic Claude/Pi launch forms. Synthetic bypass tests are permanent at `runtime_launch_guard_unittest.py`.
|
||||||
|
- Mechanical inventory: **14 gated / 14 total** — coord 2, fleet 1, QA 2, orchestrator 3, PRDY 2, Mosaic Claude/Pi/Claudex adapter/boundary 4. No verification-layer fallback or follow-up issue is needed because the single code-level wrapper was achieved.
|
||||||
|
- Real-socket behavioral evidence: PRDY init, PRDY update, and QA remediation each fail before runtime execution when the broker is absent; with the broker present they receive a broker-minted 64-hex session and the unverified `Bash` authorization exits 2. Claudex normal/YOLO and the broker state machine remain GREEN.
|
||||||
|
- Fresh branch coverage: `launch-runtime.py` **18/18 = 100%**; `mutator-gate.py` **22/22 = 100%**; permanent guard **36/38 = 95%**; `daemon.py` WI-2 delta **35/40 = 87.5%**. All attributable executable statement coverage is at least 98%.
|
||||||
|
- Fresh focused suites: broker + mutator real-socket acceptance `49/49`; persistence `10/10`; launcher/gate branch suite `13/13`; permanent guard suite `7/7`; coord `19/19`.
|
||||||
|
- Fresh full repository suite: `43/43` Turbo tasks; `@mosaicstack/mosaic` `72/72` files and `1,384/1,384` tests. Root typecheck `42/42`, lint `23/23`, format, and diff checks GREEN.
|
||||||
|
- PR #837 remains open and unmerged. Terra CODE and Opus SECREV must both rerun from zero on the exact round-3 head before coordinator-owned merge authorization.
|
||||||
|
|
||||||
|
## Remediation round 4 — terra CODE comment 18104
|
||||||
|
|
||||||
|
- Locked-good surfaces: the 14/14 launch inventory, single `launch-runtime.py` choke-point, real-socket launcher behavior, coverage, Claudex gating, and broker state machine must not change.
|
||||||
|
- Reproduced RIDER E exactly at head `1792b7934dda7eff64a207b8b0edb9c460d4164b`: a temporary production file containing `exec claude --dangerously-skip-permissions "terra-r3" # launch-runtime.py` made the guard exit 0 and report `1 gated/1 total`.
|
||||||
|
- Root cause: classification searched the unparsed physical line, and the broad gated regex treated any `launch-runtime.py` substring—including comments and inert arguments—as an invocation before the direct-launch finding was evaluated.
|
||||||
|
- Round-4 plan: add permanent RED cases for the exact comment evasion plus string-argument, echo, and unrelated-variable marker evasions; tokenize/strip comments by launcher syntax; recognize only command-position wrapper invocations with `--runtime` and the gated command separator; retain 14/14 real inventory; rerun guard coverage and all gates fresh.
|
||||||
|
- Mos Rider A/B decision: adopt **both** defenses. Command-position parsing remains necessary because a normal `claude -p` launch is consequential even without the dangerous flag. The primitive-location invariant is more mechanically robust for dangerous mode because it does not need to recognize a wrapper marker at all. Move the sole raw `--dangerously-skip-permissions` literal into `launch-runtime.py`; any occurrence in another production file is independently RED.
|
||||||
|
- Rider-A RED matrix adds heredoc body, backslash continuation, non-first `;`/`&&`/pipe commands, command substitution, `eval`, and variable-execution indirection in addition to the six marker/comment evasions. Before the augmented implementation, primitive ownership, command substitution, `eval`, variable execution, and the preserved 14-site inventory all fail.
|
||||||
|
- Round-4 GREEN uses both defenses. Quote-aware comment stripping removes shell/Python `#` and JS/TS line/block comments; shell command prefixes are segmented with `shlex`; validated wrappers require `launch-runtime.py` in command position, `--runtime`, and the `--` command separator; multiline TypeScript wrapper calls are validated as complete invocations. Direct command syntax wins over markers, while tracked runtime assignments plus `eval`/variable execution, command substitution, chained commands, heredocs, continuations, and `env`/`command`/`nohup` prefixes are rejected.
|
||||||
|
- Primitive ownership is independently load-bearing: `launch-runtime.py` is the sole production owner of the raw Claude dangerous flag. Mosaic, Claudex, and PRDY request semantic `--dangerous`; the wrapper validates Claude and injects the primitive immediately before register/exec. This preserves actual YOLO argv behavior while making any raw primitive elsewhere fail without relying on wrapper-name recognition.
|
||||||
|
- Permanent guard suite now has 10 tests and 31 direct-launch forms, including 18 new round-4 marker/comment/indirection/prefix evasions plus harmless-marker and multiline-wrapper controls. Terra's exact add-ungated source is exercised through the CLI effectiveness test. Repository inventory remains exactly **14 gated / 14 total**.
|
||||||
|
- Fresh round-4 coverage: guard **97%** branch-aware aggregate (241 statements, 110 branches); `launch-runtime.py` **100%**; `mutator-gate.py` **100%**. The daemon is byte-unchanged from the round-3 head whose WI-2 delta is **87.5%**.
|
||||||
|
- Fresh round-4 gates: real-socket acceptance **49/49**; persistence **10/10**; launcher/gate **14/14**; guard **10/10**; coord **19/19**; Mosaic **1384/1384**; root **43/43**; typecheck **42/42**; lint **23/23**; format and diff checks green.
|
||||||
|
|
||||||
|
## Remediation round 5 — terra 18116 + Opus 18114
|
||||||
|
|
||||||
|
- Both independent gates converged on one guard-only completeness gap at round-4 head `1eb77c17f3147d4fa9944f77f1826243135b9cc0`; all round-4 primitive anchoring, command-position parsing, 14/14 inventory, broker ordering, and coverage remain locked-good.
|
||||||
|
- Reproduced exactly: a temporary production source containing `launcher=claude` followed by `exec "$launcher" -p x` exits 0 with `0 gated / 0 total`. The literal command resolver skips prefixes but cannot resolve a tracked variable; the variable resolver handles only bare/eval references and cannot skip prefixes.
|
||||||
|
- Round-5 plan: add 10 permanent RED forms (quoted/unquoted `exec`, `command`, `nohup`, and `env` with assignment, each multiline and same-line), then unify shell command-position resolution so literal and tracked-variable terminal tokens traverse the same prefix parser. Retain an independent variable-reference backstop, the 14/14 inventory, and every round-4 regression.
|
||||||
|
- Mos stopping-criterion augment: command parsing is explicitly best-effort rather than a complete shell interpreter. Add B1 proving a parser-exotic alias launch with the raw dangerous flag is still RED by primitive anchoring, and B2 proving a parser-missed non-dangerous alias launch reaches the global `.*` hook and fails closed with `GATE_UNAVAILABLE` when no lease session exists. Document A (realistic parser matrix) + B (robust residual backstops); fresh reviewers supply criterion C (no new non-overlapping finding).
|
||||||
|
- RED commit `91a4a983`: all 10 prefix×variable cases failed as expected before the fix—quoted/unquoted `exec`, `command`, `nohup`, and `env A=1`, each in multiline and same-line assignment shapes.
|
||||||
|
- GREEN structural resolution: `shell_command_tokens()` now owns command-position prefix skipping for both literal and variable callers, including nested `exec`/`command`/`nohup`/`env` ordering. `runtime_variables` is threaded into `is_shell_direct_invocation()` and the same terminal-token resolver backs `executes_runtime_variable()`; exact `$v` and `${v}` references are resolved after `shlex` removes quoting. Same-line runtime assignment delimiters include shell operators.
|
||||||
|
- Residual backstops: B1 proves alias-indirected dangerous mode is classified `dangerous-primitive` even though the parser does not resolve the alias. B2 proves a non-dangerous alias residual remains parser-missed, then verifies the shipped global `.*` Claude hook and status-2 `GATE_UNAVAILABLE` denial for representative read, mutator, and custom/MCP tools without a lease session.
|
||||||
|
- Stopping-criterion evidence A+B is committed in tests and architecture docs; C remains the fresh exact-head terra/Opus determination. Repository inventory remains exactly **14 gated / 14 total**.
|
||||||
|
- Fresh round-5 coverage: permanent guard remains **97%** branch-aware aggregate (251 statements, 112 branches). Locked-good launcher and mutator-gate executables remain unchanged at their round-4 **100% / 100%** evidence.
|
||||||
|
- Fresh round-5 gates: real-socket acceptance **50/50**; persistence **10/10**; launcher/gate **14/14**; guard **12/12**; coord **19/19**; Mosaic **1385/1385**; root **43/43**; typecheck **42/42**; lint **23/23**; format and diff checks green.
|
||||||
95
docs/scratchpads/830-compaction-revoke.md
Normal file
95
docs/scratchpads/830-compaction-revoke.md
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
# WI-3 Scratchpad — Compaction revocation and runtime-generation rollover
|
||||||
|
|
||||||
|
- **Issue:** Gitea #830
|
||||||
|
- **Branch:** `feat/830-compaction-revoke`
|
||||||
|
- **Base HEAD:** `abd2791f59b3f06f46dd08e55298ced72f6aa7c2`
|
||||||
|
- **Role:** sol author/build lane only; terra CODE and Opus SECREV are coordinator-owned.
|
||||||
|
|
||||||
|
## Mission prompt
|
||||||
|
|
||||||
|
Implement BUILD-BRIEF Deliverable 3.3 and D4 on merged WI-1/WI-2 under `packages/mosaic/`. Claude `PreCompact` and `SessionStart(matcher=compact)` plus Pi `session_before_compact`/`context` equivalents must revoke the active lease through the existing broker state machine. Any `runtime_generation` bump—including same-PID reload/resume/fork—must auto-revoke the prior incarnation so the new generation inherits no prior lease. M1 is Claude + Pi only.
|
||||||
|
|
||||||
|
Honor amended D2-v5 exactly: hard fail-closure when at least one observer fires or after lease expiry; both observers missing within TTL is an explicitly named bounded residual stale window (maximum 300 seconds, soak-tighten only), with no claim that the mutator gate bounds actions inside that window; total gate-hook miss is T-C. T12b/T30 must report both the within-TTL ALLOWED outcome and after-TTL DENIED outcome.
|
||||||
|
|
||||||
|
## Session start verification
|
||||||
|
|
||||||
|
- Worktree is clean on `feat/830-compaction-revoke` at exact required base `abd2791f59b3f06f46dd08e55298ced72f6aa7c2`; `origin/main` is the same SHA and includes merged WI-2 atop WI-1.
|
||||||
|
- Authority SHA-256 verified:
|
||||||
|
- BUILD-BRIEF: `89fdbc27ed0e5050dc7b52f3ef2ddaea691edf17fd89d51b15e26fb5ed47171b`
|
||||||
|
- SPEC-v5: `a6d07ade835758e8488ca10d3b0631caf0beb93ea3a6733631f151b0c2f01433`
|
||||||
|
- Ratification: `bac58319c9c4028b5b40e1129e0033cdb5a6b7b02033c25f06f4cb77d7779c67`
|
||||||
|
- sol red-team: `3da326a4ea91767b731e128a93b13194e8002358101e30de3fcb8ca2f8f54faa`
|
||||||
|
- WI-0 evidence pack SHA-256 `5d418306fcc597fd514e500bee40d1509f0bf467e46ee13fc5c280ed8274759d` read directly. Probe P3 is **PASS**: real Pi retained the same PID/starttime through reload/fork/new/resume while generations advanced and a prior VERIFIED generation was revoked.
|
||||||
|
- P6 planner-return ruling SHA-256 `b7bbb6ea6e8d9a5c3366993642ab4e4f65b961af04936dcac20bfbcdcbaf1a09` read directly: feature WI admission is GO with the exact-delivery empirical compatibility fact and disclosed T-C middle-drop residual; no receipt redesign.
|
||||||
|
|
||||||
|
## Plan and budget
|
||||||
|
|
||||||
|
1. RED real-socket acceptance for T12b/T30, each Claude observer, same-PID generation rollover, Claude/Claudex hook wiring, and Pi lifecycle wiring.
|
||||||
|
2. Add one broker client executable for observer revocation plus a private monotonic generation-file helper shared by launcher, gate, and revoker.
|
||||||
|
3. Wire Claude `PreCompact`, `SessionStart(compact)`, and resume/clear generation rollover; merge equivalent mandatory hooks into isolated Claudex settings.
|
||||||
|
4. Wire Pi pre/post compaction observers and reload/new/resume/fork generation rollover with local fail-closed tool blocking if lifecycle revocation fails.
|
||||||
|
5. Document the D2-v5 bounded stale window without claiming the mutator gate bounds within-TTL actions; update protocol/security/operations/sitemap/checklist.
|
||||||
|
6. Run focused real tests, independently measured executable coverage ≥85%, full repository gates, commit/push, open an unmerged `closes #830` PR, and hand off for terra CODE + mandatory Opus SECREV.
|
||||||
|
|
||||||
|
Working estimate: **35K tokens**. No explicit hard cap was supplied; reduce refactor breadth before touching locked broker authority/state-machine semantics.
|
||||||
|
|
||||||
|
## RED evidence
|
||||||
|
|
||||||
|
- New T12b/T30 test already reports the inherited primitive honestly: within-TTL **ALLOWED**, after-TTL **DENIED**. The complete AC remains RED because the mandatory threat-contract document is absent.
|
||||||
|
- Focused real-socket suite is RED with 7 expected failures: missing revoker executable (both Claude observers + generation bump), missing Claude/Pi wiring, missing isolated Claudex observers, and missing D2-v5 disclosure.
|
||||||
|
- Branch-focused Python suite is RED on the wished generation initializer/resolver interfaces and missing `lease_generation.py` / `revoke-lease.py`.
|
||||||
|
- Pi lifecycle suite is RED because the wished standalone `lease-lifecycle.ts` observer/generation module does not exist.
|
||||||
|
|
||||||
|
## Locked discipline
|
||||||
|
|
||||||
|
- RED-first T12b/T30 and observer/generation tests; test commit precedes implementation.
|
||||||
|
- Reuse broker `revoke_lease`; do not fork identity, lease, or transition authority.
|
||||||
|
- Preserve revoke-first/promote-last and WI-1/WI-2 reviewed state machine.
|
||||||
|
- ≥85% attributable executable coverage with real tests.
|
||||||
|
- No author self-review, no merge, no `--no-verify`.
|
||||||
|
|
||||||
|
## Local implementation complete (push held)
|
||||||
|
|
||||||
|
Implemented on the WI-3 base `abd2791f59b3f06f46dd08e55298ced72f6aa7c2` without changing the reviewed broker state machine:
|
||||||
|
|
||||||
|
- Added `revoke-lease.py`, which authenticates through the existing broker session/generation and invokes `revoke_lease`. A fired observer that cannot confirm broker revocation advances the private generation as a local fence before returning non-zero.
|
||||||
|
- Added `lease_generation.py`: owner/type/mode/size validation, no-follow opens, exclusive bump lock, monotonic `int64` generation, write-all + `fsync`, and fail-closed exhaustion/corruption handling.
|
||||||
|
- `launch-runtime.py` creates `generation-<broker-session>.state` mode `0600` beside the socket before `exec`; `mutator-gate.py` resolves that current file value on every tool check.
|
||||||
|
- Claude settings and isolated Claudex settings now preserve/install `PreCompact`, `SessionStart(compact)`, and resume/clear rollover hooks in addition to the global all-tools gate.
|
||||||
|
- Pi now registers tested `session_before_compact`, `session_compact`→first `context`, and `session_start(reload|new|resume|fork)` handlers. Failed pre-compact revocation cancels compaction; failed post-compact/rollover revocation latches local all-tool denial.
|
||||||
|
- Added PRD requirements, architecture/security/protocol/operations updates, sitemap entry, and the ignored-by-default documentation checklist (force-add required at commit).
|
||||||
|
|
||||||
|
### Acceptance and coverage evidence
|
||||||
|
|
||||||
|
- Focused acceptance: `19/19`; T12b/T30 prints within-TTL **ALLOWED** and after-TTL **DENIED**.
|
||||||
|
- Pi lifecycle: `8/8`, with **100% statements/branches/functions/lines** attributable coverage.
|
||||||
|
- New Python generation/revoker: `24/24`, **99% branch-aware aggregate coverage** (`lease_generation.py` 98%, `revoke-lease.py` 100%).
|
||||||
|
- Mosaic package: `1399/1399`; framework shell Python `24/24`, launch guard `12/12`, permanent launch inventory `14 gated/14 total`.
|
||||||
|
- Existing lease-broker real-socket acceptance: `37/37` within the package run.
|
||||||
|
- Full repository: `43/43` Turbo tasks green; gateway `628 passed / 12 skipped`; Mosaic `1399/1399`.
|
||||||
|
- Root typecheck: `42/42`; lint: `23/23`; format and `git diff --check` green.
|
||||||
|
- Initial direct package test without first building the package reproduced the known missing-`dist/cli.js` harness condition; the canonical root Turbo test (which schedules `@mosaicstack/mosaic#build`) and explicit package build+test are green. No test was weakened.
|
||||||
|
|
||||||
|
### Review evidence
|
||||||
|
|
||||||
|
- Codex uncommitted code review: **APPROVE**, confidence `0.88`, zero findings. Its read-only sandbox could not rerun Vitest, but the author-side focused and full suites above were green.
|
||||||
|
- Codex uncommitted security review: risk **none**, confidence `0.91`, zero findings.
|
||||||
|
- Coordinator-mandated fresh exact-head terra CODE and Opus SECREV remain pending after rebase/push clearance; these local reviews do not replace that final gate.
|
||||||
|
|
||||||
|
### Hold and residuals
|
||||||
|
|
||||||
|
- **DO NOT PUSH OR OPEN A PR YET.** Coordinator requires flake-fix #838 to land, then WI-3 must rebase onto deterministic-green `main` before push.
|
||||||
|
- Merge remains gated on #838, #827 Probe 3, and combined GO.
|
||||||
|
- Named residual retained verbatim: when both observers are entirely missed, within-TTL consequential actions remain allowed; only lease expiry denies after the bounded stale window. No within-window mutator-action bound is claimed.
|
||||||
|
|
||||||
|
## Deterministic-main rebase evidence
|
||||||
|
|
||||||
|
- Fetched and confirmed `origin/main` at `8dfcf1903e385f977121069f798f476eb671fffc` (`#838` bounded broker deadlines, empty-read fail-closure, and de-flaked acceptance client).
|
||||||
|
- Linear rebase completed. The only content conflict was `packages/mosaic/src/mutator-gate/runtime_tools_unittest.py`; resolution retained #838's `subprocess`/`threading` deadline regressions and WI-3's `stat` generation-state coverage. No authority/state-machine choice was ambiguous.
|
||||||
|
- `packages/mosaic/src/mutator-gate/mutator-gate.acceptance.spec.ts` auto-merged on top of #838's shared `requestBrokerReply` helper. No inline socket/`JSON.parse` client was resurrected.
|
||||||
|
- Verified WI-3 has zero diff from `origin/main` for #838-owned `daemon.py`, `broker-test-client.ts`, `lease-broker.acceptance.spec.ts`, `vitest.config.ts`, and `packages/mosaic/package.json`; bounded deadlines and the de-flaked harness are preserved byte-for-byte.
|
||||||
|
- Required verbose acceptance command: **2 files / 56 tests green**. T12b/T30 still prints within-TTL **ALLOWED** and after-TTL **DENIED**.
|
||||||
|
- Full Mosaic package after explicit build: **74 files / 1408 tests green**; deadline unit `2/2`, runtime tools `25/25`, launch guard `12/12`, inventory `14/14`.
|
||||||
|
- Full repository: **43/43 Turbo tasks green**. Root typecheck `42/42`, lint `23/23`, format and diff checks green.
|
||||||
|
- Attributable coverage remains Python **99%** branch-aware and Pi lifecycle **100%** statements/branches/functions/lines.
|
||||||
|
- Push and PR remain held pending combined GO and all WI-3 merge gates. The coordinator-owned promote-lease-lost-ACK SPEC amendment/backstop is acknowledged as a future merge prerequisite and was not retro-expanded into this core rebase/build.
|
||||||
13
docs/scratchpads/832-receipt-challenge-protocol.md
Normal file
13
docs/scratchpads/832-receipt-challenge-protocol.md
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
# #832 Receipt-challenge protocol — build scratchpad
|
||||||
|
|
||||||
|
- **Objective:** Deliver WI-5 receipt-challenge protocol ACs T25, T26, T28, and T29 only.
|
||||||
|
- **Authority:** BUILD-BRIEF, SPEC-v5, ratification, and red-team hashes verified in STEP-0.
|
||||||
|
- **Base:** `e522b22fa4492861b0fcd4a956a8795c54eb9bfe` (`origin/main`).
|
||||||
|
- **Constraints:** Byte-build only: no live broker/socket/systemd/tmux mutation. No PR, self-review, or probe fire. T27/T30 are out of scope.
|
||||||
|
- **Plan:**
|
||||||
|
1. Add red-first deterministic T26/T29 in-build tests that call shipped normative construction and broker path.
|
||||||
|
2. Add an unexecuted, isolated P5 out-of-process replay harness that drives the shipped daemon and asserts consume-before-promote for T25/T28.
|
||||||
|
3. Implement the broker-minted receipt challenge and exact receipt observation/consume/promote path.
|
||||||
|
4. Run unit, framework-shell, compile, lint, and type checks; push after the required queue guard; report to `mosaic-100`.
|
||||||
|
- **Risks:** The standalone harness must drive the real daemon without a divergent fixture. If that is impossible, stop and flag Mos.
|
||||||
|
- **Evidence:** Initial RED recorded in `/home/hermes/agent-work/reviews/832-wi5-red-receipt-challenge.log`; initial green checks passed. Remediation RED recorded in `/home/hermes/agent-work/reviews/832-wi5-remediation-red.log` before observer/payload implementation; remediation green passed. Remediation-2 RED recorded in `/home/hermes/agent-work/reviews/832-wi5-remediation2-red.log`: each rejected begin restored prior VERIFIED authority. Remediation-2 GREEN: receipt unittest (5: all `INVALID_CONSTRUCTION`, `PAYLOAD_CONSTRUCTION_REFUSED`, and `PAYLOAD_BINDING_MISMATCH` cases preserve UNVERIFIED and deny the next mutator), normative-fragments unittest (5), state-store regression (10), full mutator-gate acceptance (20, including the real begin → observer → consume → promote path), `py_compile`, Mosaic package lint/typecheck, and targeted Prettier check. The P5 harness remains unfired. Coverage tooling remains unavailable (`python3 -m coverage`: module not installed). Push pending.
|
||||||
15
docs/scratchpads/833-constrained-recovery-command.md
Normal file
15
docs/scratchpads/833-constrained-recovery-command.md
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
# #833 constrained recovery command — build scratchpad
|
||||||
|
|
||||||
|
- **Objective:** Deliver WI-6 plus Mos-ruled B1/B2 and R2 Claude-only literal-argv repair: no shell-active recovery mapping bypass, unchanged Pi gate/B2 observer, AC-1/C4 preservation, and an unfired P6 probe.
|
||||||
|
- **Authority:** STEP-0 SHA-256 verified 4/4 against the supplied BUILD-BRIEF, SPEC-v5, ratification, and red-team records.
|
||||||
|
- **Base:** exact `07553ead337a70a9241f826d27571650262b289c`; new branch `feat/833-constrained-recovery-command`; merge-base assertion passed before any commit.
|
||||||
|
- **Constraints:** No rebase/pull during build; no live install/symlink or live broker/socket/tmux/systemd/model-stream activation; no self-review, PR, merge, push, or P6 fire. `docs/TASKS.md` is orchestrator-owned and will not be modified.
|
||||||
|
- **Plan:**
|
||||||
|
1. Add red-first unit tests against the recovery broker entrypoint for fresh recovery challenge, normal-receipt replay refusal, observable partial-delivery refusal, and the explicit middle-drop negative capability; commit the RED test and preserve its command output.
|
||||||
|
2. Implement the recovery command as a thin driver over shared WI-5 broker transitions and the trusted observer seam; it never accepts caller receipt text.
|
||||||
|
3. Add the source-resident skill under `packages/mosaic/framework/skills/`, plus a tmp-only #824 bridge projection test.
|
||||||
|
4. Build an unfired, default-3-run P6 standalone real-socket driver; it is not added to package scripts and will not be run.
|
||||||
|
5. Run targeted broker/mutator/receipt suites, lint, and format check; report head to `mosaic-100` and stop.
|
||||||
|
- **Risks:** The observer can only represent an exact latest message. Tail-preserving middle-drop is intentionally not claimed receipt-detectable (T-C residual deferred to WI-7 server evidence).
|
||||||
|
- **Evidence:** Original RED test committed at `3b5513bd6efad0c06b599fc759a66cff4286db04`; expected `UNKNOWN_ACTION` is logged in `/home/hermes/agent-work/reviews/833-wi6-red.log`. B1/B2 repair RED is `f4beedc3e7ac2e142dcbdeefb0e5ee40c20d9b86` in `/home/hermes/agent-work/reviews/833-wi6-repair-red.log`. R2 adversarial RED is committed at `65e2bd71cf360b6f45c86eec0b06ae24494832d8` in `/home/hermes/agent-work/reviews/833-wi6-repair-R2-red.log` before the literal-only gate source: the private real-gate/real-daemon battery covers every argv position (executable, path, phase, each flag, each value) for command substitution, backticks, parameter/arithmetic expansion, brace/tilde, process substitution, glob, redirects, control operations, embedded newline, and quotes. P6 remains rebuilt and unfired. The ordinary package Vitest/lint/typecheck/root-format commands cannot resolve their executables in this intentionally dependency-free fresh worktree (`node_modules` absent); no install/symlink workaround was used.
|
||||||
|
- **Budget:** No explicit task token cap was supplied; scope is fixed to WI-6 and no unrelated behavior will be added.
|
||||||
44
docs/scratchpads/838-broker-acceptance-flake.md
Normal file
44
docs/scratchpads/838-broker-acceptance-flake.md
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
# Issue #838 — Broker acceptance socket flake
|
||||||
|
|
||||||
|
## Objective
|
||||||
|
|
||||||
|
Eliminate high-contention uncaught JSON parse failures in lease-broker and mutator-gate acceptance helpers without laundering malformed/empty replies into passing assertions.
|
||||||
|
|
||||||
|
## Constraints
|
||||||
|
|
||||||
|
- Branch: `fix/838-broker-acceptance-flake` in `/home/hermes/agent-work/stack-838-flakefix`.
|
||||||
|
- Red-first TDD with a forced empty/truncated reply path and deterministic rejection or documented retry.
|
||||||
|
- Read newline-framed replies completely; reject malformed broker replies with byte length/content context.
|
||||||
|
- Determine RIDER2 branch before finalizing: test-harness-only `(a)` or daemon write truncation `(b)`.
|
||||||
|
- If product-side, prove the real Claude/Pi adapter read path fails closed; fix any allow-risk.
|
||||||
|
- Coverage >=85% per changed executable through real tests.
|
||||||
|
- Full suite and repository gates green; independent exact-head review required.
|
||||||
|
- Push and open a PR containing `closes #838`; do not merge.
|
||||||
|
|
||||||
|
## Progress
|
||||||
|
|
||||||
|
- 2026-07-17: Reclaimed from #824. Confirmed clean worktree on `fix/838-broker-acceptance-flake` at main base `abd2791f59b3f06f46dd08e55298ced72f6aa7c2`.
|
||||||
|
- RED evidence: after dependency setup, `broker-test-client.spec.ts` failed to load the intentionally absent shared client module. Its contract forces empty-close retry, repeated truncated-close rejection with byte context, and newline-terminated malformed-reply rejection.
|
||||||
|
- RIDER2 verdict: branch **(b)**. `daemon.py` starts one connection deadline before request reading, then may spend that budget waiting for `broker_lock`; after handling, `remaining <= 0` returns without writing. A timed `sendall` failure can likewise close after a partial write. A deterministic socketpair probe held the lock past `CONNECTION_DEADLINE_SECONDS` and observed `deadline_probe_reply_length=0`.
|
||||||
|
- Adapter tripwire: real subprocess executions of `mutator-gate.py` for both `--runtime claude` and `--runtime pi` against actual Unix servers returning empty and truncated replies all exited 2 with `GATE_UNAVAILABLE`. Adapter fail-close is proven; there is no ALLOW risk.
|
||||||
|
- Residual: production broker reply loss remains an availability-denial path under extreme contention, but cannot grant mutator authority. The acceptance-only client retries early closes and otherwise rejects with response byte length, escaped bytes, and hex; malformed newline-framed replies are never retried or converted to reply objects.
|
||||||
|
- Shared client now owns newline framing and parsing for both acceptance suites. Focused suites pass 60 tests, including the real adapter tripwire.
|
||||||
|
- Coverage gate: changed helper is 100% statements/lines/functions and 90% branches; existing `skill.ts` remains above 85% per-file thresholds.
|
||||||
|
|
||||||
|
## Scope corrections and bounded product repair
|
||||||
|
|
||||||
|
- Coordinator correction superseded the initial push/PR and retry language: this lane is BUILD-ONLY, and early-close retries are forbidden because they can mask a committed broker transaction. Nothing may be pushed or opened until explicitly cleared.
|
||||||
|
- Revised RED evidence: the no-retry empty/truncated tests failed because the first failed exchange was retried into `{ ok: true }`; the daemon regression failed because a slow completed `broker.handle()` produced `b''` instead of a newline-framed reply.
|
||||||
|
- Client repair: both former duplicated helpers use one shared reader. Empty, truncated, malformed, oversized, timed-out, and socket-error replies reject `BrokerTransportError` with a typed `kind`, attempt count fixed at one, response length, escaped bytes, and hex. No retry and no catch-to-reply conversion exists.
|
||||||
|
- Product repair: `daemon.py` now has independent bounded read, broker-lock queue, and send budgets. Lock queue exhaustion returns explicit `BROKER_BUSY` before `broker.handle()` can mutate state. Once handling starts it finishes atomically, and its reply always receives a fresh send timeout instead of being skipped because read/lock/fsync consumed a shared deadline.
|
||||||
|
- Product GREEN evidence: deterministic socketpair tests prove lock saturation returns framed `BROKER_BUSY` without invoking `handle()`, and a handle that completes after the former one-second shared deadline still returns its complete framed reply.
|
||||||
|
- RIDER2b remains **fail-closed**: real Claude and Pi `mutator-gate.py` subprocesses against empty and truncated Unix-socket replies exit 2 with `GATE_UNAVAILABLE`; no malformed/default ALLOW was observed.
|
||||||
|
- Residual/tripwire: an unavoidable peer disconnect or send failure can still lose acknowledgement after a valid transaction commits. The affected adapter call fails closed. A valid `promote_lease` may nevertheless remain VERIFIED after its acknowledgement is lost; that is authority-observability divergence requiring WI-3/Opus security review rather than expansion of #838. #838 does not add retries or attempt a protocol redesign.
|
||||||
|
- Final package evidence: recursive Mosaic dependency build passed; 73 Vitest files / 1,392 tests passed; deadline unit tests 2/2, real runtime tool tests 15/15, launch guard tests 12/12, inventory 14/14, and shell regressions passed.
|
||||||
|
- Final coverage: `broker-test-client.ts` 99.24% statements/lines, 86.11% branches, 100% functions under per-file >=85% thresholds. Repository typecheck 42/42, lint 23/23, and format check passed.
|
||||||
|
- Independent Codex exact-head review requested one framing fix: a valid frame followed by trailing bytes in a later socket chunk could resolve before the garbage arrived. Security review also flagged complete token-bearing reply bodies in diagnostic properties/logs.
|
||||||
|
- Review RED evidence: delayed cross-chunk garbage resolved `{ ok: true }` instead of rejecting, and a truncated `promotion_token` remained in the typed error. The tests use separate timed writes to prevent kernel/event-loop coalescing.
|
||||||
|
- Review remediation: the shared client now accumulates through EOF, requires exactly one terminal newline, then parses inside a rejecting error boundary. Diagnostic bodies are capped at 256 bytes, sensitive broker fields are fully redacted, and a SHA-256 digest preserves correlation without credential disclosure.
|
||||||
|
- Post-review coverage: `broker-test-client.ts` 99.35% statements/lines, 86.66% branches, 100% functions. Final full suite is 73 files / 1,394 tests plus all Python/shell gates; recursive build, typecheck, lint, and format are green.
|
||||||
|
- Fresh exact-head Codex security review: risk `none`, no findings. Fresh code review found only that the real-adapter subprocess proof lacked a timeout; it now has a five-second bound and fails with runtime/wire context while closing the fake server. The focused Python suite remains 15/15 green.
|
||||||
|
- Mandatory Opus SECREV remains coordinator-owned and pending before any push/PR decision; this build is intentionally local-only.
|
||||||
85
docs/scratchpads/fcm-m5-001-fleet-config-operator-docs.md
Normal file
85
docs/scratchpads/fcm-m5-001-fleet-config-operator-docs.md
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
# FCM-M5-001 — Fleet configuration operator documentation
|
||||||
|
|
||||||
|
- Task: `FCM-M5-001`
|
||||||
|
- Issue: `#758`
|
||||||
|
- Branch: `docs/758-fleet-config-operator-docs`
|
||||||
|
- Exact base: `9745bc3f29c26b021a478b7ad03cfb494f6c9de3` (tree `4da210da9a71b035130d4160a4a2e691bdfde2da`)
|
||||||
|
|
||||||
|
## Objective
|
||||||
|
|
||||||
|
Deliver the accepted fleet documentation information architecture, operator workflows, operations and migration references, comprehensive contract documentation, and deterministic link/example validation without live fleet action or product mutation.
|
||||||
|
|
||||||
|
## Scope and constraints
|
||||||
|
|
||||||
|
- Documentation, examples, documentation validation, and tracking only.
|
||||||
|
- `roster.yaml` remains the sole writable desired-state authority; generated state is derived/observed.
|
||||||
|
- No M4-002 implementation or execution; no canary, migration, rollback, deployment, systemd/tmux/session, generated projection, or product mutation.
|
||||||
|
- `mos-comms` is temporary and is not permanent architecture.
|
||||||
|
- Parent issue `#758` remains open through M5.
|
||||||
|
- No credentials, sensitive values, or privileged command content.
|
||||||
|
|
||||||
|
## Plan
|
||||||
|
|
||||||
|
1. Update tracking first with exact M4-001 evidence and mark M5-001 in progress.
|
||||||
|
2. Map the M0 checklist and current implementation behavior to documentation pages.
|
||||||
|
3. Author operator, operations, migration, schema/reference, recovery, troubleshooting, and security/authority docs.
|
||||||
|
4. Add or extend deterministic documentation/link/example validation if required, red-first.
|
||||||
|
5. Run repository documentation, link, example, and relevant package checks; review and remediate.
|
||||||
|
6. Commit, queue-guard, push one branch, and open one wrapper-created PR; stop for independent review.
|
||||||
|
|
||||||
|
## Budget
|
||||||
|
|
||||||
|
- Task estimate: `24K`.
|
||||||
|
- Working cap: stay within the card estimate by parallelizing read-only discovery and limiting edits to checklist-required artifacts.
|
||||||
|
|
||||||
|
## Progress checkpoints
|
||||||
|
|
||||||
|
- [x] Loaded repository/global delivery and documentation contracts.
|
||||||
|
- [x] Verified `origin/main` is exact required base and created isolated worktree.
|
||||||
|
- [x] Tracking updated first.
|
||||||
|
- [x] Checklist mapped and docs authored.
|
||||||
|
- [x] Validation green.
|
||||||
|
- [x] Review/remediation complete.
|
||||||
|
- [x] Commit, queue guard, push, PR #789.
|
||||||
|
- [x] Rejected exact-head RoR findings repaired on a new descendant commit candidate.
|
||||||
|
- [ ] New exact-head review and CI after repair push.
|
||||||
|
|
||||||
|
## Tests and verification
|
||||||
|
|
||||||
|
- Red-first documentation validator initially failed for the absent fleet entry point and canonical
|
||||||
|
example, then passed after the IA and example were added.
|
||||||
|
- `pnpm --filter @mosaicstack/mosaic exec vitest run src/fleet/roster-v2.spec.ts src/fleet/example-profile-dispositions.spec.ts src/fleet/fleet-documentation.spec.ts src/fleet/v1-v2-migration.spec.ts src/fleet/generated-env-boundary.spec.ts src/fleet/fleet-agent-crud.spec.ts src/fleet/fleet-reconciler.spec.ts` — 7 files, 195 tests passed after building workspace dependencies.
|
||||||
|
- `pnpm format:check` — passed.
|
||||||
|
- `pnpm lint` — 23 tasks passed.
|
||||||
|
- `pnpm typecheck` — 42 tasks passed.
|
||||||
|
- `pnpm test` — 43 tasks passed; `@mosaicstack/mosaic` contributed 61 files and 1,045 tests.
|
||||||
|
- `bash packages/mosaic/framework/tools/quality/scripts/verify-sanitized.sh` — passed.
|
||||||
|
- `bash packages/mosaic/framework/tools/quality/scripts/check-resident-budget.sh` — passed.
|
||||||
|
- `git diff --check` — passed before final review.
|
||||||
|
- Independent staged-snapshot review identified four documentation/validation blockers: reboot safety,
|
||||||
|
heartbeat observation, migration failure envelope, and example-scan coverage. All were remediated;
|
||||||
|
focused rereview approved the staged remediations with no blockers. Exact committed-head review remains
|
||||||
|
a post-PR gate.
|
||||||
|
- Post-remediation `@mosaicstack/mosaic` lint/typecheck passed; package test passed 61 files / 1,045
|
||||||
|
tests; sanitization and resident-budget gates passed again.
|
||||||
|
|
||||||
|
- Post-PR exact-head RoR on rejected head `0aee2c09819fd06e28f927384ea56fa2ef374edf`
|
||||||
|
identified five blockers: update-lifecycle overclaim, missing explicit `fleet validate` gap,
|
||||||
|
fragment-blind link validation, unsupported checklist-evidence claim, and insufficient example safety
|
||||||
|
validation. Red-first regressions failed before implementation for missing-heading, privileged-command,
|
||||||
|
and credential-format fixtures. Repairs now preserve/document implementation truth, validate heading
|
||||||
|
fragments, narrow checklist claims, and scan fenced/canonical examples for common credential formats
|
||||||
|
and privileged commands without printing fixture values.
|
||||||
|
- Repair-focused fleet contracts: 7 files, 192 tests passed after review remediation; documentation
|
||||||
|
validator contributed 11 tests. Full gates passed: format; lint 23/23; typecheck 42/42; test 43/43
|
||||||
|
tasks with `@mosaicstack/mosaic` 61 files / 1,052 tests; sanitization; resident budget; and
|
||||||
|
`git diff --check`. New exact-head review/CI remain pending until the repair commit is pushed.
|
||||||
|
|
||||||
|
## Risks/blockers
|
||||||
|
|
||||||
|
- Checklist may include behavior intentionally deferred to M4-002/M5-002; such items must be recorded as approved-existing holds rather than claimed delivered.
|
||||||
|
- Commands/examples must remain non-live and avoid privileged/sensitive content.
|
||||||
|
|
||||||
|
## Final evidence
|
||||||
|
|
||||||
|
- Pending.
|
||||||
41
packages/coord/src/__tests__/runtime-launch-gate.test.ts
Normal file
41
packages/coord/src/__tests__/runtime-launch-gate.test.ts
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
import { describe, expect, it } from 'vitest';
|
||||||
|
|
||||||
|
import { resolveLaunchCommand } from '../runner.js';
|
||||||
|
|
||||||
|
describe('coord consequential-runtime launch gate', () => {
|
||||||
|
it('routes default and direct configured Claude commands through mosaic', () => {
|
||||||
|
expect(resolveLaunchCommand('claude', 'continue', undefined)).toEqual([
|
||||||
|
'mosaic',
|
||||||
|
'claude',
|
||||||
|
'-p',
|
||||||
|
'continue',
|
||||||
|
]);
|
||||||
|
expect(resolveLaunchCommand('claude', 'continue', ['claude', '-p', '{prompt}'])).toEqual([
|
||||||
|
'mosaic',
|
||||||
|
'claude',
|
||||||
|
'-p',
|
||||||
|
'continue',
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('preserves an already-gated Claude command and rejects unknown launchers', () => {
|
||||||
|
expect(
|
||||||
|
resolveLaunchCommand('claude', 'continue', ['mosaic', 'yolo', 'claude', '{prompt}']),
|
||||||
|
).toEqual(['mosaic', 'yolo', 'claude', 'continue']);
|
||||||
|
expect(() => resolveLaunchCommand('claude', 'continue', ['custom-launcher'])).toThrow(
|
||||||
|
/must use `mosaic claude`/,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not change the out-of-scope Codex command contract', () => {
|
||||||
|
expect(resolveLaunchCommand('codex', 'continue', undefined)).toEqual([
|
||||||
|
'codex',
|
||||||
|
'-p',
|
||||||
|
'continue',
|
||||||
|
]);
|
||||||
|
expect(resolveLaunchCommand('codex', 'continue', ['codex', '{prompt}'])).toEqual([
|
||||||
|
'codex',
|
||||||
|
'continue',
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user