Compare commits
4 Commits
feat/827-g
...
e0b0bde5ad
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e0b0bde5ad | ||
|
|
080f720dfe | ||
|
|
dede787f6e | ||
|
|
e75bd4ba9d |
@@ -97,10 +97,7 @@ mosaic config path # Print config file path
|
||||
```bash
|
||||
mosaic doctor # Health audit — detect drift and missing files
|
||||
mosaic sync # Sync skills from canonical source
|
||||
mosaic skill list # Audit Claude skill registrations and conflicts
|
||||
mosaic skill register <name> # Register one canonical skill with Claude Code
|
||||
mosaic skill unregister <name> # Remove one Mosaic-owned Claude link
|
||||
mosaic update # Update CLI/framework and auto-register canonical skills
|
||||
mosaic update # Check for and install CLI updates
|
||||
mosaic wizard # Full guided setup wizard
|
||||
mosaic bootstrap <path> # Bootstrap a repo with Mosaic standards
|
||||
mosaic coord init # Initialize a new orchestration mission
|
||||
@@ -352,8 +349,6 @@ bash tools/install.sh --yes # Non-interactive, accept all defaults
|
||||
bash tools/install.sh --no-auto-launch # Skip auto-launch of wizard
|
||||
```
|
||||
|
||||
The installer rejects unrecognized flags or positional arguments before making changes and prints the supported-option usage.
|
||||
|
||||
## Contributing
|
||||
|
||||
```bash
|
||||
|
||||
@@ -1,10 +1,5 @@
|
||||
# Documentation Sitemap
|
||||
|
||||
## 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 bridge developer guide](guides/dev-guide.md#claude-code-skill-bridge) — path-validation, ownership, clobber-protection, install/update wiring, tests, and Pi/Codex scope notes.
|
||||
|
||||
## 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.
|
||||
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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`.
|
||||
@@ -8,9 +8,8 @@
|
||||
4. [Adding New Agent Tools](#adding-new-agent-tools)
|
||||
5. [Adding New MCP Tools](#adding-new-mcp-tools)
|
||||
6. [Database Schema and Migrations](#database-schema-and-migrations)
|
||||
7. [Claude Code Skill Bridge](#claude-code-skill-bridge)
|
||||
8. [API Endpoint Reference](#api-endpoint-reference)
|
||||
9. [Local Fleet Canary](./fleet-local-canary.md)
|
||||
7. [API Endpoint Reference](#api-endpoint-reference)
|
||||
8. [Local Fleet Canary](./fleet-local-canary.md)
|
||||
|
||||
---
|
||||
|
||||
@@ -354,37 +353,6 @@ defined there.
|
||||
|
||||
---
|
||||
|
||||
## Claude Code Skill Bridge
|
||||
|
||||
The framework's canonical skill root is `~/.config/mosaic/skills/`; Claude Code
|
||||
requires registrations under `~/.claude/skills/`. The implementation in
|
||||
`packages/mosaic/src/commands/skill.ts` owns only direct-child symlinks whose
|
||||
resolved target remains inside the canonical root.
|
||||
|
||||
Security invariants:
|
||||
|
||||
1. Validate the user-supplied name before filesystem access against
|
||||
`[A-Za-z0-9][A-Za-z0-9._-]*`. Separators, control characters, whitespace,
|
||||
`..`, absolute paths, and leading `-` are invalid; filesystem-derived invalid
|
||||
names are escaped before terminal output.
|
||||
2. Never replace a real file, directory, foreign symlink, or live misdirected
|
||||
symlink in the Claude skill directory.
|
||||
3. Repair a dangling link only when its lexical target is inside the canonical
|
||||
Mosaic skills root.
|
||||
4. Unregister only a symlink pointing inside that root.
|
||||
5. Enumerate canonical directories at runtime; never hardcode framework skill
|
||||
names.
|
||||
|
||||
`finalizeStage` reconciles after wizard/framework synchronization, and
|
||||
`runFrameworkReseed` reconciles after the sync-only `mosaic update` path. A
|
||||
foreign conflict is reported but does not prevent unrelated canonical skills
|
||||
from registering. Filesystem tests use injected temporary roots in
|
||||
`skill.spec.ts`, `finalize-skills.spec.ts`, and `update-checker.reseed.spec.ts`.
|
||||
|
||||
M1 intentionally manages Claude Code only. Pi's Mosaic launcher can discover the
|
||||
canonical root directly. Codex still relies on the existing full skill-sync
|
||||
linker and needs separate parity analysis before this lifecycle API is extended.
|
||||
|
||||
## API Endpoint Reference
|
||||
|
||||
All endpoints are served by the gateway at `http://localhost:14242` by default.
|
||||
|
||||
@@ -183,8 +183,6 @@ non-interactive use:
|
||||
--no-auto-launch # Skip auto-launch of wizard after install
|
||||
```
|
||||
|
||||
Unrecognized flags or positional arguments fail before installation starts and print the supported-option usage.
|
||||
|
||||
Or if installed globally:
|
||||
|
||||
```bash
|
||||
@@ -309,39 +307,6 @@ mosaic quality-rails
|
||||
|
||||
---
|
||||
|
||||
### Claude Code Skill Registration
|
||||
|
||||
Mosaic stores canonical skills under `~/.config/mosaic/skills/`. Claude Code scans
|
||||
`~/.claude/skills/`, so Mosaic maintains one symlink per skill between those
|
||||
directories.
|
||||
|
||||
```bash
|
||||
mosaic skill list
|
||||
mosaic skill register <name>
|
||||
mosaic skill unregister <name>
|
||||
```
|
||||
|
||||
- `register` is idempotent and repairs a dangling Mosaic-owned link. Names use
|
||||
the safe grammar `[A-Za-z0-9][A-Za-z0-9._-]*`; files, directories, foreign
|
||||
symlinks, path traversal, absolute paths, and names beginning with `-` are
|
||||
refused.
|
||||
- `unregister` is idempotent when no entry exists. It removes only symlinks that
|
||||
point inside `~/.config/mosaic/skills/`; foreign entries are never removed.
|
||||
- `list` reports `registered`, `unregistered`, `dangling`, `foreign`,
|
||||
`foreign-dangling`, or `misdirected` for each canonical or Claude entry.
|
||||
|
||||
Install, wizard finalization, and `mosaic update` framework re-seeding reconcile
|
||||
every canonical skill automatically. A skill directory added after initial
|
||||
setup therefore receives its Claude bridge without a per-skill code change or
|
||||
manual `ln -s`. If Claude Code is already running, use `/reload-skills` or start
|
||||
a new session after registration so its in-process skill registry rescans.
|
||||
|
||||
This command group is Claude-only in M1. Pi can consume Mosaic's canonical skill
|
||||
root through its Mosaic launcher configuration and does not need this Claude
|
||||
bridge. Codex has a separate link path managed by the legacy full skill-sync
|
||||
script; equivalent lifecycle management remains follow-up scope and is not
|
||||
changed here.
|
||||
|
||||
## Sub-package Commands
|
||||
|
||||
Each Mosaic sub-package exposes its full API surface through the `mosaic` CLI.
|
||||
|
||||
@@ -1,86 +0,0 @@
|
||||
# Issue #804 — fail closed on unknown installer arguments
|
||||
|
||||
## Objective
|
||||
|
||||
Implement Part 1 of Gitea issue #804 only: `tools/install.sh` must reject every unrecognized flag or argument with an actionable STDERR error and nonzero exit before installation starts.
|
||||
|
||||
## Scope and constraints
|
||||
|
||||
- Preserve all currently recognized options and behavior, including `-y` and `--ref <branch>`.
|
||||
- No positional arguments are currently accepted by the parser.
|
||||
- Do not add `--next`, `MOSAIC_NEXT`, prerelease routing, or any Part 2 behavior.
|
||||
- TDD is mandatory: add and observe a failing process-level regression test before changing `tools/install.sh`.
|
||||
- Worker lifecycle ends after branch push, PR creation, and coordinator notification; do not merge or close #804.
|
||||
- Existing launcher-owned changes in `.mosaic/orchestrator/mission.json` and `.mosaic/orchestrator/session.lock` are out of scope and must not be committed.
|
||||
|
||||
## Requirements and acceptance criteria
|
||||
|
||||
- Unknown input names the offending argument on STDERR.
|
||||
- STDERR includes a short installer usage hint.
|
||||
- Exit status is nonzero.
|
||||
- The installer does not invoke npm or otherwise proceed into installation.
|
||||
- Existing recognized flags remain unchanged.
|
||||
|
||||
## Plan
|
||||
|
||||
1. Add a process-level Vitest regression using the installer test location under `packages/mosaic/src/commands/`.
|
||||
2. Run the focused test and record the expected RED failure.
|
||||
3. Commit the RED test as `test(#804): ...`.
|
||||
4. Replace the parser catch-all with a fail-closed STDERR error and usage hint.
|
||||
5. Update concise installer-facing documentation without introducing prerelease behavior.
|
||||
6. Run focused tests, shell syntax validation, package tests, lint, typecheck, and format checks.
|
||||
7. Run independent review tooling and remediate findings.
|
||||
8. Commit as `fix(#804): ...`, queue-guard, push, open a PR containing `Closes #804.`, notify the coordinator, and exit.
|
||||
|
||||
## Budget
|
||||
|
||||
- No explicit token cap supplied.
|
||||
- Working estimate: 8K tokens; narrow two-file behavior/test change plus concise docs and delivery gates.
|
||||
|
||||
## Progress
|
||||
|
||||
- 2026-07-17: Loaded mission state, issue #804, delivery/QA/documentation rails, and relevant TDD/Vitest/pnpm/Gitea skills.
|
||||
- 2026-07-17: Confirmed the parser has no legitimate positional arguments and currently drops all unmatched input via `*) shift ;;`.
|
||||
- 2026-07-17: Installed locked workspace dependencies with a worktree-local pnpm store; no lockfile changes.
|
||||
- 2026-07-17: Added the process-level unknown-argument regression with an isolated `$HOME` and npm shim.
|
||||
- 2026-07-17: Replaced the silent catch-all with STDERR error + usage output and exit 2 before preflight or installation.
|
||||
- 2026-07-17: Initial Codex code review found an unknown option could still be consumed as the `--ref` value. Added a second RED reproducer, then rejected option-shaped/missing `--ref` values without changing valid `--ref <branch>` behavior. The review's launcher-state note is handled by excluding both `.mosaic/orchestrator/` files from commits.
|
||||
- 2026-07-17: Updated README, user guide, and packaged framework README with the fail-closed argument contract. No API, auth, admin, sitemap/navigation, or publishing surface changed.
|
||||
|
||||
## Verification
|
||||
|
||||
- RED: `pnpm --filter @mosaicstack/mosaic exec vitest run src/commands/install-arguments.spec.ts` — expected failure: installer exited `0` instead of nonzero at the exit-status assertion; confirms the test reproduces the silent-drop defect before production changes.
|
||||
- Remediation RED: the added `--cli --ref --bogus` case exited `0`, proving `--ref` could swallow an unknown option before the guard was added.
|
||||
- GREEN: `pnpm --filter @mosaicstack/mosaic exec vitest run src/commands/install-arguments.spec.ts src/commands/install-heading.spec.ts` — 2 files, 3 tests passed.
|
||||
- Situational process check: unknown positional input exited 2, named the input on STDERR, printed usage, and did not call the npm shim.
|
||||
- `bash -n tools/install.sh` — passed.
|
||||
- Bare `--ref` process check — exited 2 with `Missing value for --ref` and usage.
|
||||
- `pnpm --filter @mosaicstack/mosaic test` — 69 files, 1,287 tests passed; framework shell checks passed. The first attempt lacked generated `dist/cli.js`; `pnpm --filter @mosaicstack/mosaic build` restored the required test precondition and the full rerun passed.
|
||||
- `pnpm lint` — 23/23 tasks passed.
|
||||
- `pnpm typecheck` — 42/42 tasks passed.
|
||||
- `pnpm format:check` — passed.
|
||||
- Codex code re-review against `origin/main` — `approve`, 0 blockers/should-fix/suggestions.
|
||||
- Codex security re-review against `origin/main` — risk `none`, 0 findings.
|
||||
|
||||
## Acceptance evidence
|
||||
|
||||
| Criterion | Evidence |
|
||||
| --- | --- |
|
||||
| Unknown input is named on STDERR | Process-level Vitest assertions for `--bogus`, including after `--ref` |
|
||||
| Short usage hint is printed on STDERR | Vitest usage regex + manual process output |
|
||||
| Exit is nonzero | Vitest status assertions and manual exit 2 |
|
||||
| Installation does not proceed | Isolated npm shim marker remains absent |
|
||||
| Recognized behavior is preserved | Parser cases are unchanged except validation of malformed `--ref`; full Mosaic package suite passed |
|
||||
| Part 2 is excluded | No `--next`, `MOSAIC_NEXT`, dist-tag, or prerelease routing changes |
|
||||
|
||||
## Documentation checklist
|
||||
|
||||
- Current canonical `docs/PRD.md` remains unchanged; issue #804 and the coordinator brief supply this bounded defect's acceptance contract.
|
||||
- Updated installer behavior in root README, user guide, and packaged framework README in the same logical change set.
|
||||
- API/OpenAPI, auth/permissions, admin operations, developer architecture, sitemap/navigation, and external publishing are not affected.
|
||||
- Scratchpad remains under `docs/scratchpads/`; no root-hygiene changes.
|
||||
|
||||
## Risks and blockers
|
||||
|
||||
- Part 2 remains owner-gated under #805 and is intentionally excluded.
|
||||
- No implementation blocker remains. Independent coordinator RoR, CI, merge, and issue closure remain pending after worker handoff.
|
||||
@@ -1,112 +0,0 @@
|
||||
# Issue #824 — Mosaic skill CLI and Claude bridge auto-sync
|
||||
|
||||
## Objective
|
||||
|
||||
Deliver `mosaic skill register|unregister|list` plus install/upgrade reconciliation of every canonical `~/.config/mosaic/skills/*` entry into `~/.claude/skills/`, without clobbering runtime-owned files or directories.
|
||||
|
||||
## Scope and constraints
|
||||
|
||||
- Issue: mosaicstack/stack#824
|
||||
- Branch: `feat/824-mosaic-skill-cli`
|
||||
- M1 runtime: Claude Code only.
|
||||
- Pi/Codex parity is documentation-only; no non-Claude bridge implementation.
|
||||
- Do not author the downstream `mosaic-context-refresh` skill.
|
||||
- Workers do not modify `docs/TASKS.md`, merge, close #824, or touch `main`.
|
||||
- TDD is mandatory and red-first; filesystem tests use temporary directories only.
|
||||
- Budget: no explicit token cap supplied; use a focused single-worker implementation with no new dependencies.
|
||||
|
||||
## Requirements mapping
|
||||
|
||||
1. Register creates the canonical Claude symlink and is idempotent.
|
||||
2. Names are untrusted: reject empty/escaping/absolute/separator/`..`/leading-dash names before filesystem mutation, with clear CLI stderr and nonzero status.
|
||||
3. Register repairs only Mosaic-owned dangling symlinks and refuses foreign files, directories, and symlinks.
|
||||
4. Unregister removes only symlinks pointing inside the canonical Mosaic skills root and is idempotent when absent.
|
||||
5. List reports registered, dangling, foreign, and canonical-but-unregistered skills.
|
||||
6. Install and upgrade generically reconcile all canonical skills after framework sync/re-seed, continuing past foreign conflicts without clobbering them.
|
||||
7. User/developer documentation describes commands, status meanings, security boundaries, and Claude-only M1 scope.
|
||||
|
||||
## Plan
|
||||
|
||||
1. Add co-located failing Vitest coverage for all filesystem behaviors and auto-sync.
|
||||
2. Run the focused spec and record the expected RED failure.
|
||||
3. Commit the red contract as `test(#824): ...`.
|
||||
4. Implement the skill bridge and Commander command registration.
|
||||
5. Wire reconciliation into wizard finalize and `mosaic update` re-seed, preserving non-clobber behavior.
|
||||
6. Update canonical docs and sitemap if navigation changes.
|
||||
7. Run focused tests, package tests, typecheck, lint, and formatting.
|
||||
8. Commit implementation/docs as `feat(#824): ...`, queue-guard, push, open PR with `Closes #824.`, fire completion event, and notify the coordinator.
|
||||
|
||||
## Progress
|
||||
|
||||
- 2026-07-17: Loaded mission/delivery/TDD/documentation rails, issue #824, active mission state, and relevant installer/update paths.
|
||||
- 2026-07-17: Confirmed `mosaic update` invokes `framework/install.sh` with `MOSAIC_SYNC_ONLY=1`; that path exits before existing post-install skill linking, leaving newly present canonical skills unregistered.
|
||||
- 2026-07-17: Coordinator addendum classified the user-supplied skill name and runtime symlink target as a path-traversal/symlink-injection surface. Expanded the initial red contract to reject traversal before mutation, preserve every foreign entry, and unregister Mosaic-owned links only.
|
||||
- 2026-07-17: Implemented the Commander command group and secure generic bridge; wired wizard finalize and successful framework re-seed reconciliation; updated user/developer/installed/root docs and sitemap.
|
||||
- 2026-07-17: Focused, package-wide, repository baseline, temp-home situational, and independent review gates completed. Ready for scoped feature commit, queue guard, push, and PR handoff.
|
||||
|
||||
## Tests and evidence
|
||||
|
||||
### TDD evidence
|
||||
|
||||
- RED environment attempt: `pnpm --filter @mosaicstack/mosaic exec vitest run src/commands/skill.spec.ts` initially could not locate Vitest because this fresh worktree had no dependencies.
|
||||
- Dependency setup: `pnpm install --frozen-lockfile --store-dir /home/hermes/.local/share/pnpm/store` succeeded. The explicit store was required because machine pnpm config incorrectly resolves the default store under `/root`.
|
||||
- RED behavior: focused Vitest failed with `Failed to load url ./skill.js ... Does the file exist?`, proving the bridge API was absent.
|
||||
- RED integration: finalize/update specs failed because no Claude links or `skillSync` result existed.
|
||||
- RED symlink injection: symlinked Claude/canonical root tests failed because the initial implementation followed ancestor links.
|
||||
- GREEN after review remediation: `skill.spec.ts` 36/36, `finalize-skills.spec.ts` 6/6, and `update-checker.reseed.spec.ts` 30/30.
|
||||
|
||||
### Baseline gates
|
||||
|
||||
- `pnpm --filter '@mosaicstack/mosaic...' run build` — pass (fresh-worktree dependency outputs built).
|
||||
- `pnpm --filter @mosaicstack/mosaic run typecheck` — pass.
|
||||
- `pnpm --filter @mosaicstack/mosaic run lint` — pass.
|
||||
- `pnpm --filter @mosaicstack/mosaic test` — pass: 69 files, 1,325 Vitest tests plus framework shell suite.
|
||||
- `pnpm typecheck` — pass: 42/42 Turbo tasks.
|
||||
- `pnpm lint` — pass: 23/23 Turbo tasks.
|
||||
- `pnpm format:check` — pass.
|
||||
|
||||
### Situational evidence
|
||||
|
||||
A built-CLI temp-home smoke test (no real `~/.claude` or Mosaic config touched) proved:
|
||||
|
||||
- register creates the exact link and a second run reports `already registered`;
|
||||
- list reports registered and unregistered canonical skills;
|
||||
- `../../etc` exits 1 with `Invalid skill name` and creates no escaped path;
|
||||
- unregister removes the managed link and a second run reports `already unregistered`;
|
||||
- a fake successful framework re-seed generically registered both `added-after-setup` and `second-skill` from runtime directory enumeration.
|
||||
|
||||
### Review evidence
|
||||
|
||||
- Initial uncommitted Codex code/security review described name validation/clobber protection as strong; its only finding was the harness-owned, unrelated `.mosaic/orchestrator/session.lock`, which is excluded from all commits and the PR.
|
||||
- Exact branch review then identified two remediations: preserve successful framework re-seed status when bridge-wide reconciliation fails, and reject/escape control-character names to prevent terminal/log injection.
|
||||
- Both findings were reproduced red-first and remediated. A subsequent exact review identified one finalize failure-isolation blocker; a root-wide bridge error now warns and allows wizard doctor/summary/next-steps completion, with a red-first regression.
|
||||
- All remediations passed the full package and repository gates. Final exact-head review is rerun after amending the feature commit.
|
||||
|
||||
### Acceptance mapping
|
||||
|
||||
| Acceptance criterion | Evidence |
|
||||
| --- | --- |
|
||||
| register/unregister/list, idempotent | `skill.spec.ts` and built-CLI temp-home smoke |
|
||||
| traversal/symlink-injection protection | invalid-name matrix, foreign file/dir/link tests, symlinked-root tests |
|
||||
| list flags dangling and foreign entries | deterministic list status test |
|
||||
| install and upgrade auto-sync every canonical directory | finalize + framework re-seed integration specs; two-skill built-module smoke |
|
||||
| newly added skill becomes discoverable without manual link | `added-after-setup` auto-sync creates exact Claude link; Claude can rescan with `/reload-skills` or a new session |
|
||||
| Pi/Codex parity captured as scope note | user guide, developer guide, installed framework README |
|
||||
| documentation gate | root README, user guide, developer guide, framework README, sitemap |
|
||||
|
||||
## Risks
|
||||
|
||||
- Symlink replacement uses `lstat` semantics so dangling links are detectable without following them.
|
||||
- Link ownership is determined lexically against the canonical skills root, and existing symlink ancestors in either managed root are rejected before mutation.
|
||||
- Auto-sync continues across per-skill conflicts while never deleting real files/directories or foreign symlinks.
|
||||
- Claude Code discovers filesystem skills at session launch/reload boundaries; bridge creation makes a later `/reload-skills` or new session able to discover the skill, but cannot mutate an already-cached in-process registry by itself.
|
||||
- Pi does not need this Claude bridge because its Mosaic launcher can consume the canonical root. Codex lifecycle parity remains explicitly deferred.
|
||||
- No deployment surface is affected.
|
||||
|
||||
## PR #826 review remediation
|
||||
|
||||
- 2026-07-17: Exact-head RoR requested changes for two ownership bugs: installer pruning deleted foreign-name links under `MOSAIC_HOME` outside canonical skills, and unregister deleted a same-root link targeting a different skill. It also requested trailing-dot rejection and executable coverage support.
|
||||
- RED evidence: focused regression run failed 4 tests: register/unregister accepted `safe.`, misdirected unregister did not throw, and the install linker deleted the foreign-name link.
|
||||
- GREEN evidence: `skill.spec.ts` passes 43/43, including live and dangling foreign-name links in a temp HOME/MOSAIC_HOME and the misdirected unregister invariant.
|
||||
- Coverage: `vitest run src/commands/skill.spec.ts --coverage` passes configured 85% thresholds for `skill.ts`: 91.05% statements/lines, 86.27% branches, 95.23% functions.
|
||||
- Full gates: package build passed; package tests passed 69 files / 1,332 tests plus framework shell suite; repository typecheck 42/42, lint 23/23, and format check passed.
|
||||
@@ -177,23 +177,15 @@ bash tools/install.sh --cli # npm CLI only (skip framework)
|
||||
bash tools/install.sh --ref v1.0 # Install from a specific git ref
|
||||
```
|
||||
|
||||
The installer rejects unrecognized flags or positional arguments before making changes and prints the supported-option usage.
|
||||
|
||||
## Universal Skills
|
||||
|
||||
The installer syncs skills from `mosaic/agent-skills` into `~/.config/mosaic/skills/`. Install, wizard finalization, and `mosaic update` automatically reconcile every canonical skill into Claude Code's `~/.claude/skills/` directory.
|
||||
The installer syncs skills from `mosaic/agent-skills` into `~/.config/mosaic/skills/`, then links each skill into runtime directories.
|
||||
|
||||
```bash
|
||||
mosaic sync # Full canonical catalog sync
|
||||
mosaic skill list # Show registered, missing, dangling, and foreign entries
|
||||
mosaic skill register <name> # Register or repair one canonical Claude link
|
||||
mosaic skill unregister <name> # Remove one Mosaic-owned Claude link
|
||||
mosaic sync # Full sync (clone + link)
|
||||
~/.config/mosaic/bin/mosaic-sync-skills --link-only # Re-link only
|
||||
```
|
||||
|
||||
Skill names are direct children using `[A-Za-z0-9][A-Za-z0-9._-]*`, not paths. Registration rejects traversal/control characters and never replaces foreign files, directories, or symlinks; unregister removes only links that point inside the canonical Mosaic skill root. After registering during a running Claude Code session, use `/reload-skills` or start a new session.
|
||||
|
||||
M1 lifecycle management targets Claude Code. Pi can discover the canonical Mosaic root through its launcher configuration. Codex parity remains follow-up scope and continues to use the existing full skill-sync linker.
|
||||
|
||||
## Health Audit
|
||||
|
||||
```bash
|
||||
|
||||
@@ -161,7 +161,6 @@ link_targets=(
|
||||
)
|
||||
|
||||
canonical_real="$(readlink -f "$MOSAIC_SKILLS_DIR")"
|
||||
local_real="$(readlink -f "$MOSAIC_LOCAL_SKILLS_DIR")"
|
||||
|
||||
# Build an associative array from the colon-separated whitelist for O(1) lookup.
|
||||
# When MOSAIC_INSTALL_SKILLS is empty, all skills are allowed.
|
||||
@@ -204,14 +203,7 @@ link_skill_into_target() {
|
||||
link_path="$target_dir/$name"
|
||||
|
||||
if [[ -L "$link_path" ]]; then
|
||||
local raw_target resolved_target
|
||||
raw_target="$(readlink "$link_path")"
|
||||
resolved_target="$(node -e 'const p=require("node:path"); process.stdout.write(p.resolve(p.dirname(process.argv[1]), process.argv[2]));' "$link_path" "$raw_target")"
|
||||
if [[ "$resolved_target" == "$canonical_real/"* || "$resolved_target" == "$local_real/"* ]]; then
|
||||
ln -sfn "$skill_path" "$link_path"
|
||||
else
|
||||
echo "[mosaic-skills] Preserve foreign runtime symlink: $link_path"
|
||||
fi
|
||||
ln -sfn "$skill_path" "$link_path"
|
||||
return
|
||||
fi
|
||||
|
||||
@@ -242,10 +234,14 @@ prune_stale_links_in_target() {
|
||||
continue
|
||||
fi
|
||||
|
||||
# -m resolves lexical dangling targets too. If resolution fails, ownership
|
||||
# is unproven and the link must be preserved.
|
||||
resolved="$(readlink -m "$link_path" 2>/dev/null || true)"
|
||||
if [[ -n "$resolved" && "$resolved" == "$canonical_real/"* ]]; then
|
||||
resolved="$(readlink -f "$link_path" 2>/dev/null || true)"
|
||||
if [[ -z "$resolved" ]]; then
|
||||
rm -f "$link_path"
|
||||
echo "[mosaic-skills] Removed stale broken skill link: $link_path"
|
||||
continue
|
||||
fi
|
||||
|
||||
if [[ "$resolved" == "$MOSAIC_HOME/"* ]]; then
|
||||
rm -f "$link_path"
|
||||
echo "[mosaic-skills] Removed stale retired skill link: $link_path"
|
||||
fi
|
||||
|
||||
@@ -79,26 +79,9 @@ function Link-SkillIntoTarget {
|
||||
|
||||
$linkPath = Join-Path $TargetDir $name
|
||||
|
||||
# Recreate only Mosaic-owned junctions/symlinks. Foreign reparse points are
|
||||
# runtime-owned and must never be clobbered by install/upgrade auto-sync.
|
||||
# Already a junction/symlink — recreate
|
||||
$existing = Get-Item $linkPath -Force -ErrorAction SilentlyContinue
|
||||
if ($existing -and ($existing.Attributes -band [System.IO.FileAttributes]::ReparsePoint)) {
|
||||
$rawTarget = @($existing.Target)[0]
|
||||
$candidate = if ([System.IO.Path]::IsPathRooted($rawTarget)) {
|
||||
$rawTarget
|
||||
}
|
||||
else {
|
||||
Join-Path (Split-Path $linkPath -Parent) $rawTarget
|
||||
}
|
||||
$resolvedTarget = [System.IO.Path]::GetFullPath($candidate)
|
||||
$canonicalRoot = [System.IO.Path]::GetFullPath($MosaicSkillsDir).TrimEnd('\') + '\'
|
||||
$localRoot = [System.IO.Path]::GetFullPath($MosaicLocalSkillsDir).TrimEnd('\') + '\'
|
||||
$owned = $resolvedTarget.StartsWith($canonicalRoot, [System.StringComparison]::OrdinalIgnoreCase) -or
|
||||
$resolvedTarget.StartsWith($localRoot, [System.StringComparison]::OrdinalIgnoreCase)
|
||||
if (-not $owned) {
|
||||
Write-Host "[mosaic-skills] Preserve foreign runtime symlink: $linkPath"
|
||||
return
|
||||
}
|
||||
Remove-Item $linkPath -Force
|
||||
}
|
||||
elseif ($existing) {
|
||||
|
||||
@@ -70,8 +70,6 @@ Security vulnerability review focusing on:
|
||||
~/.config/mosaic/tools/codex/codex-security-review.sh -n 42
|
||||
```
|
||||
|
||||
PR mode resolves the provider's PR diff rather than relying on the caller's checked-out branch. On Gitea, it fetches the base and `refs/pull/<number>/head` refs and diffs those explicit refs. If the refs cannot be fetched or the resulting diff is empty, the command exits nonzero before Codex runs or a review is posted.
|
||||
|
||||
### Review Against Base Branch
|
||||
|
||||
```bash
|
||||
@@ -255,7 +253,7 @@ Run the script from inside a git repository.
|
||||
|
||||
### "No changes found to review"
|
||||
|
||||
The specified non-PR mode (`--uncommitted`, `--base`, etc.) found no changes to review. PR mode instead fails closed with an actionable error when it cannot construct a non-empty provider diff; verify the PR number, remote, provider login, and ref access before retrying.
|
||||
The specified mode (--uncommitted, --base, etc.) found no changes to review.
|
||||
|
||||
### "Codex produced no output"
|
||||
|
||||
|
||||
@@ -44,47 +44,38 @@ build_diff_context() {
|
||||
diff_text=$(git show "$value" 2>/dev/null)
|
||||
;;
|
||||
pr)
|
||||
# Provider detection writes its result to stdout; suppress it so it cannot
|
||||
# be mistaken for diff content when this function is used in a substitution.
|
||||
detect_platform >/dev/null
|
||||
# For PRs, we need to fetch the PR diff
|
||||
detect_platform
|
||||
if [[ "$PLATFORM" == "github" ]]; then
|
||||
diff_text=$(gh pr diff "$value" 2>/dev/null) || {
|
||||
echo "Error: Failed to fetch the diff for PR #${value}." >&2
|
||||
return 1
|
||||
}
|
||||
diff_text=$(gh pr diff "$value" 2>/dev/null)
|
||||
elif [[ "$PLATFORM" == "gitea" ]]; then
|
||||
local pr_base base_ref pr_head_ref
|
||||
pr_base=$(tea pr list --fields index,base --output simple 2>/dev/null | awk -v pr="$value" '$1 == pr { print $2; exit }')
|
||||
if [[ -z "$pr_base" ]]; then
|
||||
echo "Error: Could not resolve the base branch for Gitea PR #${value}." >&2
|
||||
return 1
|
||||
# tea doesn't have a direct pr diff command, use git
|
||||
local pr_base
|
||||
pr_base=$(tea pr list --fields index,base --output simple 2>/dev/null | grep "^${value}" | awk '{print $2}')
|
||||
if [[ -n "$pr_base" ]]; then
|
||||
diff_text=$(git diff "${pr_base}...HEAD" 2>/dev/null)
|
||||
else
|
||||
# Fallback: fetch PR info via API
|
||||
local repo_info
|
||||
repo_info=$(get_repo_info)
|
||||
local remote_url
|
||||
remote_url=$(git remote get-url origin 2>/dev/null)
|
||||
local host
|
||||
host=$(echo "$remote_url" | sed -E 's|.*://([^/]+).*|\1|; s|.*@([^:]+).*|\1|')
|
||||
diff_text=$(curl -s "https://${host}/api/v1/repos/${repo_info}/pulls/${value}" \
|
||||
-H "Authorization: token $(tea login list --output simple 2>/dev/null | head -1 | awk '{print $2}')" \
|
||||
2>/dev/null | jq -r '.diff_url // empty')
|
||||
if [[ -n "$diff_text" && "$diff_text" != "null" ]]; then
|
||||
diff_text=$(curl -s "$diff_text" 2>/dev/null)
|
||||
else
|
||||
diff_text=$(git diff "main...HEAD" 2>/dev/null)
|
||||
fi
|
||||
fi
|
||||
|
||||
base_ref="refs/remotes/origin/${pr_base}"
|
||||
pr_head_ref="refs/remotes/origin/pr/${value}/head"
|
||||
if ! git fetch --quiet origin \
|
||||
"+refs/heads/${pr_base}:${base_ref}" \
|
||||
"+refs/pull/${value}/head:${pr_head_ref}"; then
|
||||
echo "Error: Failed to fetch the base and head refs for Gitea PR #${value}." >&2
|
||||
return 1
|
||||
fi
|
||||
diff_text=$(git diff "${base_ref}...${pr_head_ref}") || {
|
||||
echo "Error: Failed to diff the fetched refs for Gitea PR #${value}." >&2
|
||||
return 1
|
||||
}
|
||||
else
|
||||
echo "Error: Unsupported git platform while resolving PR #${value}." >&2
|
||||
return 1
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
|
||||
if [[ "$mode" == "pr" && -z "${diff_text//[[:space:]]/}" ]]; then
|
||||
echo "Error: Unable to construct a non-empty diff for PR #${value}; verify the PR refs and provider access." >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
printf '%s\n' "$diff_text"
|
||||
echo "$diff_text"
|
||||
}
|
||||
|
||||
# Format JSON findings as markdown for PR comments
|
||||
|
||||
@@ -1,158 +0,0 @@
|
||||
#!/bin/bash
|
||||
# Hermetic regression coverage for Gitea PR diff construction and fail-closed reviews.
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
TMP_DIR=$(mktemp -d)
|
||||
trap 'rm -rf "$TMP_DIR"' EXIT
|
||||
|
||||
fail() {
|
||||
echo "not ok - $*" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
assert_contains() {
|
||||
local haystack="$1" needle="$2"
|
||||
if [[ "$haystack" != *"$needle"* ]]; then
|
||||
printf 'actual output:\n%s\n' "$haystack" >&2
|
||||
fail "expected output to contain: $needle"
|
||||
fi
|
||||
}
|
||||
|
||||
# Prevent CI-provided repository context from leaking into the fixture repositories.
|
||||
unset GIT_DIR GIT_WORK_TREE GIT_INDEX_FILE GIT_OBJECT_DIRECTORY \
|
||||
GIT_ALTERNATE_OBJECT_DIRECTORIES GIT_COMMON_DIR
|
||||
export GIT_AUTHOR_NAME="Codex Fixture"
|
||||
export GIT_AUTHOR_EMAIL="codex-fixture@example.test"
|
||||
export GIT_COMMITTER_NAME="$GIT_AUTHOR_NAME"
|
||||
export GIT_COMMITTER_EMAIL="$GIT_AUTHOR_EMAIL"
|
||||
export GITEA_LOGIN="fixture"
|
||||
export GITEA_TOKEN="fixture-token"
|
||||
export GITEA_URL="file://$TMP_DIR"
|
||||
|
||||
create_pr_fixture() {
|
||||
local fixture_root="$1" head_mode="$2"
|
||||
local origin="$fixture_root/origin.git"
|
||||
local seed="$fixture_root/seed"
|
||||
local work="$fixture_root/work"
|
||||
local base_sha head_sha
|
||||
|
||||
mkdir -p "$fixture_root"
|
||||
git init --quiet --bare "$origin"
|
||||
git init --quiet --initial-branch=release/next "$seed"
|
||||
printf 'base\n' > "$seed/pr-change.ts"
|
||||
git -C "$seed" add pr-change.ts
|
||||
git -C "$seed" commit --quiet -m "fixture base"
|
||||
base_sha=$(git -C "$seed" rev-parse HEAD)
|
||||
git -C "$seed" remote add origin "$origin"
|
||||
git -C "$seed" push --quiet origin release/next
|
||||
git --git-dir="$origin" symbolic-ref HEAD refs/heads/release/next
|
||||
|
||||
if [[ "$head_mode" == "changed" ]]; then
|
||||
git -C "$seed" switch --quiet -c feature/pr-795
|
||||
printf 'actual-pr-change\n' > "$seed/pr-change.ts"
|
||||
git -C "$seed" commit --quiet -am "fixture PR head"
|
||||
head_sha=$(git -C "$seed" rev-parse HEAD)
|
||||
git -C "$seed" push --quiet origin HEAD:refs/pull/795/head
|
||||
else
|
||||
head_sha="$base_sha"
|
||||
git --git-dir="$origin" update-ref refs/pull/795/head "$head_sha"
|
||||
fi
|
||||
|
||||
# Gitea's provider-owned PR head ref now exists in the local bare origin.
|
||||
git clone --quiet "$origin" "$work"
|
||||
printf '%s\n' "$work"
|
||||
}
|
||||
|
||||
FAKE_BIN="$TMP_DIR/bin"
|
||||
mkdir -p "$FAKE_BIN"
|
||||
cat > "$FAKE_BIN/tea" <<'STUB'
|
||||
#!/bin/bash
|
||||
if [[ "$*" == "pr list --fields index,base --output simple" ]]; then
|
||||
printf '795 release/next\n'
|
||||
exit 0
|
||||
fi
|
||||
exit 1
|
||||
STUB
|
||||
cat > "$FAKE_BIN/codex" <<'STUB'
|
||||
#!/bin/bash
|
||||
printf 'CODEX %s\n' "$*" >> "$CODEX_LOG"
|
||||
exit 99
|
||||
STUB
|
||||
chmod +x "$FAKE_BIN/tea" "$FAKE_BIN/codex"
|
||||
export PATH="$FAKE_BIN:$PATH"
|
||||
|
||||
# The valid fixture is a fresh clone on the non-main base. The PR head exists only
|
||||
# at refs/pull/795/head, so local HEAD cannot accidentally satisfy the assertion.
|
||||
if [[ "${1:-all}" != "fail-closed" ]]; then
|
||||
VALID_WORK=$(create_pr_fixture "$TMP_DIR/valid" changed)
|
||||
(
|
||||
cd "$VALID_WORK"
|
||||
# shellcheck source=common.sh
|
||||
source "$SCRIPT_DIR/common.sh"
|
||||
diff_context=$(build_diff_context pr 795)
|
||||
assert_contains "$diff_context" "actual-pr-change"
|
||||
|
||||
base_sha=$(git rev-parse refs/remotes/origin/release/next)
|
||||
head_sha=$(git rev-parse refs/remotes/origin/pr/795/head)
|
||||
local_sha=$(git rev-parse HEAD)
|
||||
[[ "$local_sha" == "$base_sha" ]] || fail "fixture clone is not on the PR base"
|
||||
[[ "$head_sha" != "$base_sha" ]] || fail "fixture PR head does not differ from its base"
|
||||
git show-ref --verify --quiet refs/remotes/origin/pr/795/head || \
|
||||
fail "fetched PR head ref is missing"
|
||||
[[ "$(git diff --name-only "${base_sha}...${head_sha}")" == "pr-change.ts" ]] || \
|
||||
fail "explicit PR refs do not contain the fixture change"
|
||||
if git show-ref --verify --quiet refs/heads/main || \
|
||||
git show-ref --verify --quiet refs/remotes/origin/main; then
|
||||
fail "fixture unexpectedly contains a main ref"
|
||||
fi
|
||||
)
|
||||
echo "ok - Gitea PR mode fetches and diffs explicit non-main base and PR head refs"
|
||||
fi
|
||||
|
||||
# Build an empty PR entirely inside another local repository. Both review wrappers
|
||||
# must emit the PR-numbered error before Codex or the stubbed post path can execute.
|
||||
if [[ "${1:-all}" != "pr-head" ]]; then
|
||||
EMPTY_WORK=$(create_pr_fixture "$TMP_DIR/empty" empty)
|
||||
SANDBOX="$TMP_DIR/sandbox"
|
||||
mkdir -p "$SANDBOX/tools/codex/schemas" "$SANDBOX/tools/git"
|
||||
cp "$SCRIPT_DIR/common.sh" \
|
||||
"$SCRIPT_DIR/codex-code-review.sh" \
|
||||
"$SCRIPT_DIR/codex-security-review.sh" \
|
||||
"$SANDBOX/tools/codex/"
|
||||
cp "$SCRIPT_DIR/schemas/code-review-schema.json" \
|
||||
"$SCRIPT_DIR/schemas/security-review-schema.json" \
|
||||
"$SANDBOX/tools/codex/schemas/"
|
||||
cp "$SCRIPT_DIR/../git/detect-platform.sh" "$SANDBOX/tools/git/"
|
||||
|
||||
cat > "$SANDBOX/tools/git/pr-review.sh" <<'STUB'
|
||||
#!/bin/bash
|
||||
printf 'POST %s\n' "$*" >> "$POST_LOG"
|
||||
STUB
|
||||
chmod +x "$SANDBOX/tools/git/pr-review.sh"
|
||||
|
||||
POST_LOG="$TMP_DIR/post.log"
|
||||
CODEX_LOG="$TMP_DIR/codex.log"
|
||||
export POST_LOG CODEX_LOG
|
||||
|
||||
for review_kind in code security; do
|
||||
: > "$POST_LOG"
|
||||
: > "$CODEX_LOG"
|
||||
review_script="$SANDBOX/tools/codex/codex-${review_kind}-review.sh"
|
||||
set +e
|
||||
(
|
||||
cd "$EMPTY_WORK"
|
||||
"$review_script" -n 795
|
||||
) >"$TMP_DIR/${review_kind}.stdout" 2>"$TMP_DIR/${review_kind}.stderr"
|
||||
review_status=$?
|
||||
set -e
|
||||
|
||||
stderr_text=$(cat "$TMP_DIR/${review_kind}.stderr")
|
||||
[[ "$review_status" -ne 0 ]] || fail "${review_kind} review returned success for an empty PR diff"
|
||||
[[ ! -s "$CODEX_LOG" ]] || fail "Codex ran for an empty ${review_kind} PR diff"
|
||||
[[ ! -s "$POST_LOG" ]] || fail "${review_kind} review auto-post ran for an empty PR diff"
|
||||
assert_contains "$stderr_text" "Error:"
|
||||
assert_contains "$stderr_text" "PR #795"
|
||||
echo "ok - empty ${review_kind} PR diff fails closed before Codex and auto-post"
|
||||
done
|
||||
fi
|
||||
@@ -24,8 +24,7 @@
|
||||
"build": "tsc",
|
||||
"lint": "eslint src",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"test": "vitest run --passWithNoTests && pnpm run test:framework-shell",
|
||||
"test:framework-shell": "bash framework/tools/codex/test-pr-diff-context.sh"
|
||||
"test": "vitest run --passWithNoTests"
|
||||
},
|
||||
"dependencies": {
|
||||
"@mosaicstack/brain": "workspace:*",
|
||||
@@ -53,7 +52,6 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.0.0",
|
||||
"@vitest/coverage-v8": "^2.0.0",
|
||||
"@types/react": "^18.3.0",
|
||||
"tsx": "^4.0.0",
|
||||
"typescript": "^5.8.0",
|
||||
|
||||
@@ -18,7 +18,6 @@ import { registerFleetCommand } from './commands/fleet.js';
|
||||
import { registerMissionCommand } from './commands/mission.js';
|
||||
import { registerUninstallCommand } from './commands/uninstall.js';
|
||||
import { registerRestoreCommand } from './commands/restore.js';
|
||||
import { registerSkillCommand } from './commands/skill.js';
|
||||
// prdy is registered via launch.ts
|
||||
import { registerLaunchCommands } from './commands/launch.js';
|
||||
import { registerAuthCommand } from './commands/auth.js';
|
||||
@@ -68,7 +67,7 @@ Command Groups:
|
||||
|
||||
Runtime: tui, login, sessions
|
||||
Gateway: gateway
|
||||
Framework: agent, bootstrap, coord, doctor, fleet, init, launch, mission, prdy, seq, skill, sync, upgrade, wizard, yolo
|
||||
Framework: agent, bootstrap, coord, doctor, fleet, init, launch, mission, prdy, seq, sync, upgrade, wizard, yolo
|
||||
Platform: update
|
||||
Runtimes: claude, codex, opencode, pi
|
||||
`,
|
||||
@@ -412,10 +411,6 @@ registerUninstallCommand(program);
|
||||
|
||||
registerRestoreCommand(program);
|
||||
|
||||
// ─── skill ───────────────────────────────────────────────────────────────────
|
||||
|
||||
registerSkillCommand(program);
|
||||
|
||||
// ─── telemetry ───────────────────────────────────────────────────────────────
|
||||
|
||||
registerTelemetryCommand(program);
|
||||
@@ -476,18 +471,6 @@ program
|
||||
return;
|
||||
}
|
||||
console.log('✔ Framework re-seeded.');
|
||||
if (reseed.skillSyncError) {
|
||||
console.error(` ⚠ Claude skill reconciliation skipped: ${reseed.skillSyncError}`);
|
||||
}
|
||||
const skillConflicts = reseed.skillSync?.conflicts ?? [];
|
||||
const skillChanges =
|
||||
(reseed.skillSync?.registered.length ?? 0) + (reseed.skillSync?.repaired.length ?? 0);
|
||||
if (skillChanges > 0) {
|
||||
console.log(`✔ Registered ${skillChanges.toString()} Mosaic skill(s) with Claude Code.`);
|
||||
}
|
||||
for (const conflict of skillConflicts) {
|
||||
console.error(` ⚠ Skill registration skipped for ${conflict.name}: ${conflict.reason}`);
|
||||
}
|
||||
// Propagate shipped systemd unit fixes to the ACTIVE units (re-seed only
|
||||
// touches ~/.config/mosaic/systemd/user; systemd runs ~/.config/systemd/user).
|
||||
const units = refreshActiveFleetUnits();
|
||||
|
||||
@@ -1,61 +0,0 @@
|
||||
import { spawnSync } from 'node:child_process';
|
||||
import { existsSync } from 'node:fs';
|
||||
import { chmod, mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { join } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
const INSTALLER_PATH = fileURLToPath(new URL('../../../../tools/install.sh', import.meta.url));
|
||||
|
||||
async function runInstaller(args: string[]): Promise<{
|
||||
status: number | null;
|
||||
stderr: string;
|
||||
npmCalled: boolean;
|
||||
}> {
|
||||
const home = await mkdtemp(join(tmpdir(), 'mosaic-installer-args-'));
|
||||
const bin = join(home, 'bin');
|
||||
const npmMarker = join(home, 'npm-called');
|
||||
|
||||
try {
|
||||
await mkdir(bin);
|
||||
const npmShim = join(bin, 'npm');
|
||||
await writeFile(npmShim, `#!/bin/sh\n: > "${npmMarker}"\n`);
|
||||
await chmod(npmShim, 0o755);
|
||||
|
||||
const result = spawnSync('bash', [INSTALLER_PATH, ...args], {
|
||||
encoding: 'utf8',
|
||||
env: {
|
||||
...process.env,
|
||||
HOME: home,
|
||||
MOSAIC_NO_COLOR: '1',
|
||||
PATH: `${bin}:${process.env.PATH ?? ''}`,
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
status: result.status,
|
||||
stderr: result.stderr,
|
||||
npmCalled: existsSync(npmMarker),
|
||||
};
|
||||
} finally {
|
||||
await rm(home, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
|
||||
function expectUnknownArgument(result: Awaited<ReturnType<typeof runInstaller>>): void {
|
||||
expect(result.status).not.toBe(0);
|
||||
expect(result.stderr).toContain('Unknown argument: --bogus');
|
||||
expect(result.stderr).toMatch(/Usage: .*install\.sh/);
|
||||
expect(result.npmCalled).toBe(false);
|
||||
}
|
||||
|
||||
describe('installer arguments', () => {
|
||||
it('rejects an unknown argument before installation starts', async () => {
|
||||
expectUnknownArgument(await runInstaller(['--cli', '--bogus']));
|
||||
});
|
||||
|
||||
it('does not let --ref consume an unknown option', async () => {
|
||||
expectUnknownArgument(await runInstaller(['--cli', '--ref', '--bogus']));
|
||||
});
|
||||
});
|
||||
@@ -1,421 +0,0 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { Command } from 'commander';
|
||||
import { spawnSync } from 'node:child_process';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import {
|
||||
existsSync,
|
||||
lstatSync,
|
||||
mkdirSync,
|
||||
mkdtempSync,
|
||||
readlinkSync,
|
||||
rmSync,
|
||||
symlinkSync,
|
||||
writeFileSync,
|
||||
} from 'node:fs';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { join } from 'node:path';
|
||||
import {
|
||||
listSkills,
|
||||
registerSkill,
|
||||
registerSkillCommand,
|
||||
syncClaudeSkills,
|
||||
unregisterSkill,
|
||||
type SkillPaths,
|
||||
} from './skill.js';
|
||||
|
||||
const LEGACY_SYNC_SCRIPT = fileURLToPath(
|
||||
new URL('../../framework/tools/_scripts/mosaic-sync-skills', import.meta.url),
|
||||
);
|
||||
|
||||
describe('Claude skill bridge', () => {
|
||||
let root: string;
|
||||
let paths: SkillPaths;
|
||||
|
||||
beforeEach(() => {
|
||||
root = mkdtempSync(join(tmpdir(), 'mosaic-skill-cli-'));
|
||||
paths = {
|
||||
mosaicSkillsDir: join(root, '.config', 'mosaic', 'skills'),
|
||||
claudeSkillsDir: join(root, '.claude', 'skills'),
|
||||
};
|
||||
mkdirSync(paths.mosaicSkillsDir, { recursive: true });
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
rmSync(root, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
function createSkill(name: string): string {
|
||||
const skillDir = join(paths.mosaicSkillsDir, name);
|
||||
mkdirSync(skillDir, { recursive: true });
|
||||
writeFileSync(join(skillDir, 'SKILL.md'), `# ${name}\n`);
|
||||
return skillDir;
|
||||
}
|
||||
|
||||
function expectCorrectLink(name: string): void {
|
||||
const linkPath = join(paths.claudeSkillsDir, name);
|
||||
expect(lstatSync(linkPath).isSymbolicLink()).toBe(true);
|
||||
expect(readlinkSync(linkPath)).toBe(join(paths.mosaicSkillsDir, name));
|
||||
}
|
||||
|
||||
describe('name validation', () => {
|
||||
const invalidNames = [
|
||||
'../../etc',
|
||||
'/abs/path',
|
||||
'a/b',
|
||||
String.raw`a\b`,
|
||||
'-rf',
|
||||
'..',
|
||||
'safe.',
|
||||
'space name',
|
||||
'line\nbreak',
|
||||
'escape\u001B[31m',
|
||||
];
|
||||
|
||||
for (const name of invalidNames) {
|
||||
it(`rejects ${JSON.stringify(name)} before register can escape its roots`, () => {
|
||||
expect(() => registerSkill(name, paths)).toThrow(/invalid skill name/i);
|
||||
expect(existsSync(paths.claudeSkillsDir)).toBe(false);
|
||||
});
|
||||
|
||||
it(`rejects ${JSON.stringify(name)} before unregister can escape its roots`, () => {
|
||||
expect(() => unregisterSkill(name, paths)).toThrow(/invalid skill name/i);
|
||||
expect(existsSync(paths.claudeSkillsDir)).toBe(false);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
describe('CLI validation errors', () => {
|
||||
let previousExitCode: number | string | null | undefined;
|
||||
|
||||
beforeEach(() => {
|
||||
previousExitCode = process.exitCode;
|
||||
process.exitCode = undefined;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
process.exitCode = previousExitCode;
|
||||
});
|
||||
|
||||
it.each(['register', 'unregister'])(
|
||||
'reports invalid %s names on stderr and sets a nonzero exit status',
|
||||
async (subcommand) => {
|
||||
const error = vi.spyOn(console, 'error').mockImplementation(() => undefined);
|
||||
const program = new Command().exitOverride();
|
||||
registerSkillCommand(program, paths);
|
||||
|
||||
await program.parseAsync(['node', 'mosaic', 'skill', subcommand, '../../etc']);
|
||||
|
||||
expect(error).toHaveBeenCalledWith(expect.stringMatching(/invalid skill name/i));
|
||||
expect(process.exitCode).toBe(1);
|
||||
expect(existsSync(paths.claudeSkillsDir)).toBe(false);
|
||||
error.mockRestore();
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
describe('CLI status output', () => {
|
||||
let previousExitCode: number | string | null | undefined;
|
||||
|
||||
beforeEach(() => {
|
||||
previousExitCode = process.exitCode;
|
||||
process.exitCode = undefined;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
process.exitCode = previousExitCode;
|
||||
});
|
||||
|
||||
async function run(...args: string[]): Promise<void> {
|
||||
const program = new Command().exitOverride();
|
||||
registerSkillCommand(program, paths);
|
||||
await program.parseAsync(['node', 'mosaic', 'skill', ...args]);
|
||||
}
|
||||
|
||||
it('reports register repair/idempotency and unregister idempotency statuses', async () => {
|
||||
const log = vi.spyOn(console, 'log').mockImplementation(() => undefined);
|
||||
createSkill('status-skill');
|
||||
|
||||
await run('register', 'status-skill');
|
||||
await run('register', 'status-skill');
|
||||
rmSync(join(paths.claudeSkillsDir, 'status-skill'));
|
||||
symlinkSync(
|
||||
join(paths.mosaicSkillsDir, 'retired'),
|
||||
join(paths.claudeSkillsDir, 'status-skill'),
|
||||
);
|
||||
await run('register', 'status-skill');
|
||||
await run('unregister', 'status-skill');
|
||||
await run('unregister', 'status-skill');
|
||||
|
||||
expect(log.mock.calls.flat()).toEqual([
|
||||
'status-skill: registered',
|
||||
'status-skill: already registered',
|
||||
'status-skill: repaired dangling registration',
|
||||
'status-skill: unregistered',
|
||||
'status-skill: already unregistered',
|
||||
]);
|
||||
log.mockRestore();
|
||||
});
|
||||
|
||||
it('reports empty and populated skill lists', async () => {
|
||||
const log = vi.spyOn(console, 'log').mockImplementation(() => undefined);
|
||||
|
||||
await run('list');
|
||||
createSkill('listed');
|
||||
await run('list');
|
||||
|
||||
expect(log).toHaveBeenCalledWith('No Mosaic or Claude Code skills found.');
|
||||
expect(log).toHaveBeenCalledWith(expect.stringMatching(/^unregistered\s+listed$/));
|
||||
log.mockRestore();
|
||||
});
|
||||
});
|
||||
|
||||
describe('registerSkill', () => {
|
||||
it('creates the exact canonical symlink and is idempotent', () => {
|
||||
createSkill('new-skill');
|
||||
|
||||
expect(registerSkill('new-skill', paths).status).toBe('registered');
|
||||
expectCorrectLink('new-skill');
|
||||
|
||||
expect(registerSkill('new-skill', paths).status).toBe('already-registered');
|
||||
expectCorrectLink('new-skill');
|
||||
});
|
||||
|
||||
it('repairs a dangling Mosaic-owned symlink', () => {
|
||||
createSkill('new-skill');
|
||||
mkdirSync(paths.claudeSkillsDir, { recursive: true });
|
||||
symlinkSync(
|
||||
join(paths.mosaicSkillsDir, 'retired-skill'),
|
||||
join(paths.claudeSkillsDir, 'new-skill'),
|
||||
);
|
||||
|
||||
expect(registerSkill('new-skill', paths).status).toBe('repaired');
|
||||
expectCorrectLink('new-skill');
|
||||
});
|
||||
|
||||
it.each(['file', 'directory', 'symlink'] as const)(
|
||||
'refuses to clobber a foreign %s at the target',
|
||||
(kind) => {
|
||||
createSkill('protected');
|
||||
mkdirSync(paths.claudeSkillsDir, { recursive: true });
|
||||
const target = join(paths.claudeSkillsDir, 'protected');
|
||||
const foreign = join(root, 'foreign');
|
||||
|
||||
if (kind === 'file') writeFileSync(target, 'keep me\n');
|
||||
if (kind === 'directory') mkdirSync(target);
|
||||
if (kind === 'symlink') {
|
||||
writeFileSync(foreign, 'keep me\n');
|
||||
symlinkSync(foreign, target);
|
||||
}
|
||||
|
||||
expect(() => registerSkill('protected', paths)).toThrow(/foreign|refus/i);
|
||||
if (kind === 'file') expect(lstatSync(target).isFile()).toBe(true);
|
||||
if (kind === 'directory') expect(lstatSync(target).isDirectory()).toBe(true);
|
||||
if (kind === 'symlink') expect(readlinkSync(target)).toBe(foreign);
|
||||
},
|
||||
);
|
||||
|
||||
it('refuses a symlinked Claude skills ancestor instead of writing outside the bridge root', () => {
|
||||
createSkill('protected');
|
||||
const externalClaude = join(root, 'external-claude');
|
||||
mkdirSync(externalClaude);
|
||||
symlinkSync(externalClaude, join(root, '.claude'));
|
||||
|
||||
expect(() => registerSkill('protected', paths)).toThrow(
|
||||
/symlink.*ancestor|ancestor.*symlink/i,
|
||||
);
|
||||
expect(existsSync(join(externalClaude, 'skills', 'protected'))).toBe(false);
|
||||
});
|
||||
|
||||
it('refuses a symlinked canonical skills root instead of registering an external source', () => {
|
||||
rmSync(paths.mosaicSkillsDir, { recursive: true });
|
||||
const externalSkills = join(root, 'external-skills');
|
||||
mkdirSync(join(externalSkills, 'protected'), { recursive: true });
|
||||
symlinkSync(externalSkills, paths.mosaicSkillsDir);
|
||||
|
||||
expect(() => registerSkill('protected', paths)).toThrow(
|
||||
/symlink.*ancestor|ancestor.*symlink/i,
|
||||
);
|
||||
expect(existsSync(paths.claudeSkillsDir)).toBe(false);
|
||||
});
|
||||
|
||||
it('refuses a dangling foreign symlink rather than treating it as repairable', () => {
|
||||
createSkill('protected');
|
||||
mkdirSync(paths.claudeSkillsDir, { recursive: true });
|
||||
const foreignMissing = join(root, 'foreign-missing');
|
||||
const target = join(paths.claudeSkillsDir, 'protected');
|
||||
symlinkSync(foreignMissing, target);
|
||||
|
||||
expect(() => registerSkill('protected', paths)).toThrow(/foreign|refus/i);
|
||||
expect(readlinkSync(target)).toBe(foreignMissing);
|
||||
});
|
||||
});
|
||||
|
||||
describe('unregisterSkill', () => {
|
||||
it('removes a Mosaic-owned symlink and is idempotent when absent', () => {
|
||||
createSkill('removable');
|
||||
registerSkill('removable', paths);
|
||||
|
||||
expect(unregisterSkill('removable', paths).status).toBe('unregistered');
|
||||
expect(existsSync(join(paths.claudeSkillsDir, 'removable'))).toBe(false);
|
||||
|
||||
expect(unregisterSkill('removable', paths).status).toBe('already-unregistered');
|
||||
});
|
||||
|
||||
it('refuses to remove a misdirected Mosaic-root symlink', () => {
|
||||
createSkill('other');
|
||||
mkdirSync(paths.claudeSkillsDir, { recursive: true });
|
||||
const requested = join(paths.claudeSkillsDir, 'requested');
|
||||
symlinkSync(join(paths.mosaicSkillsDir, 'other'), requested);
|
||||
|
||||
expect(() => unregisterSkill('requested', paths)).toThrow(/misdirected/i);
|
||||
expect(readlinkSync(requested)).toBe(join(paths.mosaicSkillsDir, 'other'));
|
||||
});
|
||||
|
||||
it.each(['file', 'directory', 'symlink'] as const)('refuses to remove a foreign %s', (kind) => {
|
||||
mkdirSync(paths.claudeSkillsDir, { recursive: true });
|
||||
const target = join(paths.claudeSkillsDir, 'protected');
|
||||
const foreign = join(root, 'foreign');
|
||||
|
||||
if (kind === 'file') writeFileSync(target, 'keep me\n');
|
||||
if (kind === 'directory') mkdirSync(target);
|
||||
if (kind === 'symlink') {
|
||||
writeFileSync(foreign, 'keep me\n');
|
||||
symlinkSync(foreign, target);
|
||||
}
|
||||
|
||||
expect(() => unregisterSkill('protected', paths)).toThrow(/foreign|refus/i);
|
||||
expect(lstatSync(target)).toBeDefined();
|
||||
if (kind === 'symlink') expect(readlinkSync(target)).toBe(foreign);
|
||||
});
|
||||
});
|
||||
|
||||
describe('listSkills', () => {
|
||||
it('flags registered, unregistered, Mosaic-owned dangling, and foreign entries', () => {
|
||||
createSkill('registered');
|
||||
createSkill('unregistered');
|
||||
registerSkill('registered', paths);
|
||||
symlinkSync(join(paths.mosaicSkillsDir, 'retired'), join(paths.claudeSkillsDir, 'dangling'));
|
||||
writeFileSync(join(paths.claudeSkillsDir, 'foreign-file'), 'keep me\n');
|
||||
symlinkSync(join(root, 'missing-foreign'), join(paths.claudeSkillsDir, 'foreign-link'));
|
||||
|
||||
expect(listSkills(paths)).toEqual([
|
||||
expect.objectContaining({ name: 'dangling', status: 'dangling' }),
|
||||
expect.objectContaining({ name: 'foreign-file', status: 'foreign' }),
|
||||
expect.objectContaining({ name: 'foreign-link', status: 'foreign-dangling' }),
|
||||
expect.objectContaining({ name: 'registered', status: 'registered' }),
|
||||
expect.objectContaining({ name: 'unregistered', status: 'unregistered' }),
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('install linker compatibility', () => {
|
||||
it('preserves foreign-name links into Mosaic home but outside canonical skills', () => {
|
||||
createSkill('missing');
|
||||
mkdirSync(paths.claudeSkillsDir, { recursive: true });
|
||||
const mosaicHome = join(root, '.config', 'mosaic');
|
||||
const liveForeignTarget = join(mosaicHome, 'foreign-non-skill-target');
|
||||
mkdirSync(liveForeignTarget);
|
||||
const liveForeignLink = join(paths.claudeSkillsDir, 'foreign-tool');
|
||||
const danglingForeignLink = join(paths.claudeSkillsDir, 'unresolvable-foreign');
|
||||
symlinkSync(liveForeignTarget, liveForeignLink);
|
||||
symlinkSync(join(mosaicHome, 'foreign-missing'), danglingForeignLink);
|
||||
|
||||
const result = spawnSync('bash', [LEGACY_SYNC_SCRIPT, '--link-only'], {
|
||||
encoding: 'utf8',
|
||||
env: { ...process.env, HOME: root, MOSAIC_HOME: mosaicHome },
|
||||
});
|
||||
|
||||
expect(result.status, result.stderr).toBe(0);
|
||||
expect(readlinkSync(liveForeignLink)).toBe(liveForeignTarget);
|
||||
expect(readlinkSync(danglingForeignLink)).toBe(join(mosaicHome, 'foreign-missing'));
|
||||
expect(readlinkSync(join(paths.claudeSkillsDir, 'missing'))).toBe(
|
||||
join(paths.mosaicSkillsDir, 'missing'),
|
||||
);
|
||||
});
|
||||
|
||||
it('preserves live and dangling foreign Claude symlinks while linking missing skills', () => {
|
||||
createSkill('dangling-foreign');
|
||||
createSkill('live-foreign');
|
||||
createSkill('missing');
|
||||
mkdirSync(paths.claudeSkillsDir, { recursive: true });
|
||||
const external = join(root, 'external');
|
||||
mkdirSync(external);
|
||||
const liveLink = join(paths.claudeSkillsDir, 'live-foreign');
|
||||
const danglingLink = join(paths.claudeSkillsDir, 'dangling-foreign');
|
||||
symlinkSync(external, liveLink);
|
||||
symlinkSync(join(root, 'external-missing'), danglingLink);
|
||||
|
||||
const result = spawnSync('bash', [LEGACY_SYNC_SCRIPT, '--link-only'], {
|
||||
encoding: 'utf8',
|
||||
env: { ...process.env, HOME: root, MOSAIC_HOME: join(root, '.config', 'mosaic') },
|
||||
});
|
||||
|
||||
expect(result.status, result.stderr).toBe(0);
|
||||
expect(readlinkSync(liveLink)).toBe(external);
|
||||
expect(readlinkSync(danglingLink)).toBe(join(root, 'external-missing'));
|
||||
expect(readlinkSync(join(paths.claudeSkillsDir, 'missing'))).toBe(
|
||||
join(paths.mosaicSkillsDir, 'missing'),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('syncClaudeSkills', () => {
|
||||
it('generically creates every missing canonical link and repairs managed broken links', () => {
|
||||
createSkill('added-after-setup');
|
||||
createSkill('another-new-skill');
|
||||
mkdirSync(paths.claudeSkillsDir, { recursive: true });
|
||||
symlinkSync(
|
||||
join(paths.mosaicSkillsDir, 'retired'),
|
||||
join(paths.claudeSkillsDir, 'added-after-setup'),
|
||||
);
|
||||
|
||||
const result = syncClaudeSkills(paths);
|
||||
|
||||
expect(result).toEqual({
|
||||
registered: ['another-new-skill'],
|
||||
repaired: ['added-after-setup'],
|
||||
unchanged: [],
|
||||
conflicts: [],
|
||||
});
|
||||
expectCorrectLink('added-after-setup');
|
||||
expectCorrectLink('another-new-skill');
|
||||
});
|
||||
|
||||
it('escapes an invalid filesystem-derived name in conflict output', () => {
|
||||
createSkill('line\nbreak');
|
||||
|
||||
const result = syncClaudeSkills(paths);
|
||||
|
||||
expect(result.registered).toEqual([]);
|
||||
expect(result.conflicts).toEqual([
|
||||
expect.objectContaining({
|
||||
name: '"line\\nbreak"',
|
||||
reason: expect.stringMatching(/invalid/i),
|
||||
}),
|
||||
]);
|
||||
expect(existsSync(paths.claudeSkillsDir)).toBe(false);
|
||||
});
|
||||
|
||||
it('continues syncing other skills without clobbering foreign entries', () => {
|
||||
createSkill('blocked');
|
||||
createSkill('link-me');
|
||||
mkdirSync(paths.claudeSkillsDir, { recursive: true });
|
||||
const blocked = join(paths.claudeSkillsDir, 'blocked');
|
||||
writeFileSync(blocked, 'keep me\n');
|
||||
|
||||
const result = syncClaudeSkills(paths);
|
||||
|
||||
expect(result.registered).toEqual(['link-me']);
|
||||
expect(result.conflicts).toEqual([
|
||||
expect.objectContaining({
|
||||
name: 'blocked',
|
||||
reason: expect.stringMatching(/foreign|refus/i),
|
||||
}),
|
||||
]);
|
||||
expect(readlinkSync(join(paths.claudeSkillsDir, 'link-me'))).toBe(
|
||||
join(paths.mosaicSkillsDir, 'link-me'),
|
||||
);
|
||||
expect(lstatSync(blocked).isFile()).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,419 +0,0 @@
|
||||
import {
|
||||
existsSync,
|
||||
lstatSync,
|
||||
mkdirSync,
|
||||
readdirSync,
|
||||
readlinkSync,
|
||||
symlinkSync,
|
||||
unlinkSync,
|
||||
type Stats,
|
||||
} from 'node:fs';
|
||||
import { homedir } from 'node:os';
|
||||
import { dirname, isAbsolute, join, parse, relative, resolve, sep } from 'node:path';
|
||||
import type { Command } from 'commander';
|
||||
import { DEFAULT_MOSAIC_HOME } from '../constants.js';
|
||||
|
||||
export interface SkillPaths {
|
||||
mosaicSkillsDir: string;
|
||||
claudeSkillsDir: string;
|
||||
}
|
||||
|
||||
export type SkillRegistrationStatus = 'registered' | 'already-registered' | 'repaired';
|
||||
export type SkillUnregistrationStatus = 'unregistered' | 'already-unregistered';
|
||||
export type SkillListStatus =
|
||||
| 'registered'
|
||||
| 'unregistered'
|
||||
| 'dangling'
|
||||
| 'foreign'
|
||||
| 'foreign-dangling'
|
||||
| 'misdirected';
|
||||
|
||||
export interface SkillRegistrationResult {
|
||||
name: string;
|
||||
status: SkillRegistrationStatus;
|
||||
sourcePath: string;
|
||||
linkPath: string;
|
||||
}
|
||||
|
||||
export interface SkillUnregistrationResult {
|
||||
name: string;
|
||||
status: SkillUnregistrationStatus;
|
||||
linkPath: string;
|
||||
}
|
||||
|
||||
export interface SkillListEntry {
|
||||
name: string;
|
||||
status: SkillListStatus;
|
||||
sourcePath?: string;
|
||||
linkPath: string;
|
||||
targetPath?: string;
|
||||
}
|
||||
|
||||
export interface SkillSyncConflict {
|
||||
name: string;
|
||||
reason: string;
|
||||
}
|
||||
|
||||
export interface SkillSyncResult {
|
||||
registered: string[];
|
||||
repaired: string[];
|
||||
unchanged: string[];
|
||||
conflicts: SkillSyncConflict[];
|
||||
}
|
||||
|
||||
const SAFE_SKILL_NAME = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
|
||||
|
||||
export class SkillBridgeError extends Error {
|
||||
public constructor(message: string) {
|
||||
super(message);
|
||||
this.name = 'SkillBridgeError';
|
||||
}
|
||||
}
|
||||
|
||||
/** Resolve the production bridge paths while keeping tests injectable. */
|
||||
export function getDefaultSkillPaths(): SkillPaths {
|
||||
const mosaicHome = process.env['MOSAIC_HOME'] ?? DEFAULT_MOSAIC_HOME;
|
||||
const claudeHome = process.env['CLAUDE_HOME'] ?? join(homedir(), '.claude');
|
||||
return {
|
||||
mosaicSkillsDir: join(mosaicHome, 'skills'),
|
||||
claudeSkillsDir: join(claudeHome, 'skills'),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Reject a user-supplied name before any filesystem operation.
|
||||
* A skill name must identify one direct child in both managed roots.
|
||||
*/
|
||||
export function validateSkillName(name: string): void {
|
||||
if (
|
||||
name.length === 0 ||
|
||||
name.startsWith('-') ||
|
||||
name.endsWith('.') ||
|
||||
name.includes('..') ||
|
||||
name.includes('/') ||
|
||||
name.includes('\\') ||
|
||||
isAbsolute(name) ||
|
||||
!SAFE_SKILL_NAME.test(name)
|
||||
) {
|
||||
throw new SkillBridgeError(
|
||||
`Invalid skill name ${JSON.stringify(name)}: use letters, numbers, dots, underscores, or hyphens; start with a letter or number; and do not use paths, "..", or a leading "-".`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function displaySkillName(name: string): string {
|
||||
return SAFE_SKILL_NAME.test(name) ? name : JSON.stringify(name);
|
||||
}
|
||||
|
||||
function lstatIfPresent(path: string): Stats | undefined {
|
||||
try {
|
||||
return lstatSync(path);
|
||||
} catch (error: unknown) {
|
||||
if (error instanceof Error && 'code' in error && error.code === 'ENOENT') return undefined;
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
function assertNoSymlinkAncestors(path: string): void {
|
||||
const absolute = resolve(path);
|
||||
const pathRoot = parse(absolute).root;
|
||||
let current = pathRoot;
|
||||
|
||||
for (const segment of relative(pathRoot, absolute).split(sep)) {
|
||||
if (segment.length === 0) continue;
|
||||
current = join(current, segment);
|
||||
const entry = lstatIfPresent(current);
|
||||
if (!entry) break;
|
||||
if (entry.isSymbolicLink()) {
|
||||
throw new SkillBridgeError(
|
||||
`Refusing symlink ancestor at ${current}; managed skill roots must resolve without symlink traversal.`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function assertManagedRoots(paths: SkillPaths): void {
|
||||
assertNoSymlinkAncestors(paths.mosaicSkillsDir);
|
||||
assertNoSymlinkAncestors(paths.claudeSkillsDir);
|
||||
}
|
||||
|
||||
function directChild(root: string, name: string): string {
|
||||
const resolvedRoot = resolve(root);
|
||||
const child = resolve(resolvedRoot, name);
|
||||
if (dirname(child) !== resolvedRoot) {
|
||||
throw new SkillBridgeError(`Invalid skill name "${name}": resolved path escapes its root.`);
|
||||
}
|
||||
return child;
|
||||
}
|
||||
|
||||
function resolveLinkTarget(linkPath: string): string {
|
||||
return resolve(dirname(linkPath), readlinkSync(linkPath));
|
||||
}
|
||||
|
||||
function isInsideSkillsRoot(targetPath: string, skillsRoot: string): boolean {
|
||||
const rel = relative(resolve(skillsRoot), resolve(targetPath));
|
||||
return rel.length > 0 && rel !== '..' && !rel.startsWith(`..${sep}`) && !isAbsolute(rel);
|
||||
}
|
||||
|
||||
function isDangling(linkPath: string): boolean {
|
||||
return !existsSync(linkPath);
|
||||
}
|
||||
|
||||
function assertSourceSkill(name: string, paths: SkillPaths): string {
|
||||
const sourcePath = directChild(paths.mosaicSkillsDir, name);
|
||||
const source = lstatIfPresent(sourcePath);
|
||||
if (!source?.isDirectory()) {
|
||||
throw new SkillBridgeError(
|
||||
`Canonical skill directory not found: ${sourcePath}. Add the skill under the Mosaic skills directory before registering it.`,
|
||||
);
|
||||
}
|
||||
return sourcePath;
|
||||
}
|
||||
|
||||
function foreignTargetError(linkPath: string): SkillBridgeError {
|
||||
return new SkillBridgeError(
|
||||
`Refusing to modify foreign entry at ${linkPath}; only symlinks pointing inside the Mosaic skills directory are managed.`,
|
||||
);
|
||||
}
|
||||
|
||||
/** Register one canonical skill with Claude Code without clobbering foreign entries. */
|
||||
export function registerSkill(
|
||||
name: string,
|
||||
paths: SkillPaths = getDefaultSkillPaths(),
|
||||
): SkillRegistrationResult {
|
||||
validateSkillName(name);
|
||||
assertManagedRoots(paths);
|
||||
const sourcePath = assertSourceSkill(name, paths);
|
||||
const linkPath = directChild(paths.claudeSkillsDir, name);
|
||||
const existing = lstatIfPresent(linkPath);
|
||||
|
||||
if (!existing) {
|
||||
mkdirSync(paths.claudeSkillsDir, { recursive: true });
|
||||
symlinkSync(sourcePath, linkPath);
|
||||
return { name, status: 'registered', sourcePath, linkPath };
|
||||
}
|
||||
|
||||
if (!existing.isSymbolicLink()) throw foreignTargetError(linkPath);
|
||||
|
||||
const existingTarget = resolveLinkTarget(linkPath);
|
||||
if (!isInsideSkillsRoot(existingTarget, paths.mosaicSkillsDir)) {
|
||||
throw foreignTargetError(linkPath);
|
||||
}
|
||||
|
||||
if (existingTarget === resolve(sourcePath) && !isDangling(linkPath)) {
|
||||
return { name, status: 'already-registered', sourcePath, linkPath };
|
||||
}
|
||||
|
||||
if (!isDangling(linkPath)) {
|
||||
throw new SkillBridgeError(
|
||||
`Refusing to replace live Mosaic skill symlink at ${linkPath}; it points to ${existingTarget}, not ${sourcePath}.`,
|
||||
);
|
||||
}
|
||||
|
||||
unlinkSync(linkPath);
|
||||
symlinkSync(sourcePath, linkPath);
|
||||
return { name, status: 'repaired', sourcePath, linkPath };
|
||||
}
|
||||
|
||||
/** Unregister only a symlink owned by the canonical Mosaic skills root. */
|
||||
export function unregisterSkill(
|
||||
name: string,
|
||||
paths: SkillPaths = getDefaultSkillPaths(),
|
||||
): SkillUnregistrationResult {
|
||||
validateSkillName(name);
|
||||
assertManagedRoots(paths);
|
||||
const linkPath = directChild(paths.claudeSkillsDir, name);
|
||||
const existing = lstatIfPresent(linkPath);
|
||||
|
||||
if (!existing) return { name, status: 'already-unregistered', linkPath };
|
||||
if (!existing.isSymbolicLink()) throw foreignTargetError(linkPath);
|
||||
|
||||
const targetPath = resolveLinkTarget(linkPath);
|
||||
if (!isInsideSkillsRoot(targetPath, paths.mosaicSkillsDir)) throw foreignTargetError(linkPath);
|
||||
|
||||
const expectedTarget = resolve(directChild(paths.mosaicSkillsDir, name));
|
||||
if (targetPath !== expectedTarget) {
|
||||
throw new SkillBridgeError(
|
||||
`Refusing to unregister misdirected Mosaic skill symlink at ${linkPath}; it points to ${targetPath}, not ${expectedTarget}.`,
|
||||
);
|
||||
}
|
||||
|
||||
unlinkSync(linkPath);
|
||||
return { name, status: 'unregistered', linkPath };
|
||||
}
|
||||
|
||||
function canonicalSkillNames(paths: SkillPaths): string[] {
|
||||
assertNoSymlinkAncestors(paths.mosaicSkillsDir);
|
||||
const root = lstatIfPresent(paths.mosaicSkillsDir);
|
||||
if (!root) return [];
|
||||
if (!root.isDirectory()) {
|
||||
throw new SkillBridgeError(
|
||||
`Canonical skills path is not a directory: ${paths.mosaicSkillsDir}`,
|
||||
);
|
||||
}
|
||||
return readdirSync(paths.mosaicSkillsDir, { withFileTypes: true })
|
||||
.filter((entry) => entry.isDirectory())
|
||||
.map((entry) => entry.name)
|
||||
.sort();
|
||||
}
|
||||
|
||||
function claudeEntryNames(paths: SkillPaths): string[] {
|
||||
assertNoSymlinkAncestors(paths.claudeSkillsDir);
|
||||
const root = lstatIfPresent(paths.claudeSkillsDir);
|
||||
if (!root) return [];
|
||||
if (!root.isDirectory()) {
|
||||
throw new SkillBridgeError(`Claude skills path is not a directory: ${paths.claudeSkillsDir}`);
|
||||
}
|
||||
return readdirSync(paths.claudeSkillsDir)
|
||||
.filter((name) => name.length > 0)
|
||||
.sort();
|
||||
}
|
||||
|
||||
/** Return a deterministic union of canonical skills and Claude bridge entries. */
|
||||
export function listSkills(paths: SkillPaths = getDefaultSkillPaths()): SkillListEntry[] {
|
||||
const canonicalNames = new Set(canonicalSkillNames(paths));
|
||||
const names = new Set([...canonicalNames, ...claudeEntryNames(paths)]);
|
||||
const entries: SkillListEntry[] = [];
|
||||
|
||||
for (const name of [...names].sort()) {
|
||||
const sourcePath = canonicalNames.has(name)
|
||||
? directChild(paths.mosaicSkillsDir, name)
|
||||
: undefined;
|
||||
const linkPath = directChild(paths.claudeSkillsDir, name);
|
||||
const installed = lstatIfPresent(linkPath);
|
||||
|
||||
if (!installed) {
|
||||
if (sourcePath) entries.push({ name, status: 'unregistered', sourcePath, linkPath });
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!installed.isSymbolicLink()) {
|
||||
entries.push({ name, status: 'foreign', sourcePath, linkPath });
|
||||
continue;
|
||||
}
|
||||
|
||||
const targetPath = resolveLinkTarget(linkPath);
|
||||
const owned = isInsideSkillsRoot(targetPath, paths.mosaicSkillsDir);
|
||||
const dangling = isDangling(linkPath);
|
||||
|
||||
if (!owned) {
|
||||
entries.push({
|
||||
name,
|
||||
status: dangling ? 'foreign-dangling' : 'foreign',
|
||||
sourcePath,
|
||||
linkPath,
|
||||
targetPath,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
if (dangling) {
|
||||
entries.push({ name, status: 'dangling', sourcePath, linkPath, targetPath });
|
||||
continue;
|
||||
}
|
||||
|
||||
entries.push({
|
||||
name,
|
||||
status: sourcePath && targetPath === resolve(sourcePath) ? 'registered' : 'misdirected',
|
||||
sourcePath,
|
||||
linkPath,
|
||||
targetPath,
|
||||
});
|
||||
}
|
||||
|
||||
return entries;
|
||||
}
|
||||
|
||||
/** Reconcile every canonical skill directory while preserving all foreign entries. */
|
||||
export function syncClaudeSkills(paths: SkillPaths = getDefaultSkillPaths()): SkillSyncResult {
|
||||
const result: SkillSyncResult = {
|
||||
registered: [],
|
||||
repaired: [],
|
||||
unchanged: [],
|
||||
conflicts: [],
|
||||
};
|
||||
|
||||
for (const name of canonicalSkillNames(paths)) {
|
||||
try {
|
||||
const registration = registerSkill(name, paths);
|
||||
if (registration.status === 'registered') result.registered.push(name);
|
||||
if (registration.status === 'repaired') result.repaired.push(name);
|
||||
if (registration.status === 'already-registered') result.unchanged.push(name);
|
||||
} catch (error: unknown) {
|
||||
result.conflicts.push({
|
||||
name: displaySkillName(name),
|
||||
reason: error instanceof Error ? error.message : String(error),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
function reportCommandError(error: unknown): void {
|
||||
console.error(error instanceof Error ? error.message : String(error));
|
||||
process.exitCode = 1;
|
||||
}
|
||||
|
||||
/** Register the `mosaic skill` command group. */
|
||||
export function registerSkillCommand(
|
||||
program: Command,
|
||||
paths: SkillPaths = getDefaultSkillPaths(),
|
||||
): void {
|
||||
const skill = program
|
||||
.command('skill')
|
||||
.description('Manage Claude Code skill registrations')
|
||||
.configureHelp({ sortSubcommands: true });
|
||||
|
||||
skill
|
||||
.command('register <name>')
|
||||
.description('Register a Mosaic skill with Claude Code')
|
||||
.action((name: string) => {
|
||||
try {
|
||||
const result = registerSkill(name, paths);
|
||||
if (result.status === 'already-registered') {
|
||||
console.log(`${name}: already registered`);
|
||||
} else if (result.status === 'repaired') {
|
||||
console.log(`${name}: repaired dangling registration`);
|
||||
} else {
|
||||
console.log(`${name}: registered`);
|
||||
}
|
||||
} catch (error: unknown) {
|
||||
reportCommandError(error);
|
||||
}
|
||||
});
|
||||
|
||||
skill
|
||||
.command('unregister <name>')
|
||||
.description('Unregister a Mosaic skill from Claude Code')
|
||||
.action((name: string) => {
|
||||
try {
|
||||
const result = unregisterSkill(name, paths);
|
||||
console.log(
|
||||
result.status === 'already-unregistered'
|
||||
? `${name}: already unregistered`
|
||||
: `${name}: unregistered`,
|
||||
);
|
||||
} catch (error: unknown) {
|
||||
reportCommandError(error);
|
||||
}
|
||||
});
|
||||
|
||||
skill
|
||||
.command('list')
|
||||
.description('List registered, dangling, foreign, and unregistered skills')
|
||||
.action(() => {
|
||||
try {
|
||||
const entries = listSkills(paths);
|
||||
if (entries.length === 0) {
|
||||
console.log('No Mosaic or Claude Code skills found.');
|
||||
return;
|
||||
}
|
||||
for (const entry of entries) {
|
||||
console.log(`${entry.status.padEnd(17)} ${displaySkillName(entry.name)}`);
|
||||
}
|
||||
} catch (error: unknown) {
|
||||
reportCommandError(error);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -7,7 +7,6 @@ import {
|
||||
mkdirSync,
|
||||
readdirSync,
|
||||
readFileSync,
|
||||
readlinkSync,
|
||||
rmSync,
|
||||
statSync,
|
||||
symlinkSync,
|
||||
@@ -301,50 +300,6 @@ describe('repairFleetCommsTools', () => {
|
||||
});
|
||||
|
||||
describe('runFrameworkReseed', () => {
|
||||
it('auto-registers every canonical skill after a successful upgrade re-seed', () => {
|
||||
const root = mkdtempSync(join(tmpdir(), 'mosaic-reseed-skills-'));
|
||||
const framework = join(root, 'framework');
|
||||
const home = join(root, 'mosaic');
|
||||
const claudeSkills = join(root, '.claude', 'skills');
|
||||
mkdirSync(framework, { recursive: true });
|
||||
mkdirSync(join(home, 'skills', 'added-after-setup'), { recursive: true });
|
||||
mkdirSync(join(home, 'skills', 'another-new-skill'), { recursive: true });
|
||||
writeFileSync(join(framework, 'install.sh'), '#!/usr/bin/env bash\nexit 0\n', { mode: 0o755 });
|
||||
|
||||
const res = runFrameworkReseed(framework, home, claudeSkills);
|
||||
|
||||
expect(res.ok).toBe(true);
|
||||
expect(res.skillSync).toMatchObject({
|
||||
registered: ['added-after-setup', 'another-new-skill'],
|
||||
conflicts: [],
|
||||
});
|
||||
expect(readlinkSync(join(claudeSkills, 'added-after-setup'))).toBe(
|
||||
join(home, 'skills', 'added-after-setup'),
|
||||
);
|
||||
expect(readlinkSync(join(claudeSkills, 'another-new-skill'))).toBe(
|
||||
join(home, 'skills', 'another-new-skill'),
|
||||
);
|
||||
rmSync(root, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
it('keeps a successful framework re-seed successful when bridge reconciliation fails', () => {
|
||||
const root = mkdtempSync(join(tmpdir(), 'mosaic-reseed-bridge-failure-'));
|
||||
const framework = join(root, 'framework');
|
||||
const home = join(root, 'mosaic');
|
||||
const claudeSkills = join(root, '.claude', 'skills');
|
||||
mkdirSync(framework, { recursive: true });
|
||||
mkdirSync(home, { recursive: true });
|
||||
writeFileSync(join(home, 'skills'), 'invalid canonical root\n');
|
||||
writeFileSync(join(framework, 'install.sh'), '#!/usr/bin/env bash\nexit 0\n', { mode: 0o755 });
|
||||
|
||||
const res = runFrameworkReseed(framework, home, claudeSkills);
|
||||
|
||||
expect(res.ok).toBe(true);
|
||||
expect(res.skillSync).toBeUndefined();
|
||||
expect(res.skillSyncError).toMatch(/not a directory/i);
|
||||
rmSync(root, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
it('reports not-ok (not throw) when the installer is absent', () => {
|
||||
const missing = mkdtempSync(join(tmpdir(), 'mosaic-noinstaller-'));
|
||||
const res = runFrameworkReseed(missing, join(missing, 'home'));
|
||||
|
||||
@@ -43,7 +43,6 @@ import {
|
||||
ensureManagedDirectory,
|
||||
readRegularFileSecure,
|
||||
} from '../fleet/secure-file.js';
|
||||
import { getDefaultSkillPaths, syncClaudeSkills, type SkillSyncResult } from '../commands/skill.js';
|
||||
|
||||
// ─── Types ──────────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -872,39 +871,19 @@ export function repairFleetCommsTools(
|
||||
* describing what happened (so callers can message + decide on relaunch).
|
||||
* Best-effort: a missing installer or a non-zero exit is reported, not thrown.
|
||||
*/
|
||||
export interface FrameworkReseedResult {
|
||||
ok: boolean;
|
||||
reason?: string;
|
||||
skillSync?: SkillSyncResult;
|
||||
skillSyncError?: string;
|
||||
}
|
||||
|
||||
export function runFrameworkReseed(
|
||||
frameworkRoot = resolveBundledFrameworkRoot(),
|
||||
mosaicHome = join(homedir(), '.config', 'mosaic'),
|
||||
claudeSkillsDir = getDefaultSkillPaths().claudeSkillsDir,
|
||||
): FrameworkReseedResult {
|
||||
): { ok: boolean; reason?: string } {
|
||||
const { installer, command, env } = buildReseedCommand(frameworkRoot, mosaicHome);
|
||||
if (!existsSync(installer)) {
|
||||
return { ok: false, reason: `installer not found: ${installer}` };
|
||||
}
|
||||
try {
|
||||
execSync(command, { stdio: 'inherit', env: { ...process.env, ...env }, timeout: 120_000 });
|
||||
} catch (error: unknown) {
|
||||
return { ok: false, reason: error instanceof Error ? error.message : String(error) };
|
||||
}
|
||||
|
||||
try {
|
||||
const skillSync = syncClaudeSkills({
|
||||
mosaicSkillsDir: join(mosaicHome, 'skills'),
|
||||
claudeSkillsDir,
|
||||
});
|
||||
return { ok: true, skillSync };
|
||||
} catch (error: unknown) {
|
||||
return {
|
||||
ok: true,
|
||||
skillSyncError: error instanceof Error ? error.message : String(error),
|
||||
};
|
||||
return { ok: true };
|
||||
} catch (err) {
|
||||
return { ok: false, reason: err instanceof Error ? err.message : String(err) };
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
*/
|
||||
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
||||
import { mkdtempSync, mkdirSync, readlinkSync, writeFileSync, rmSync } from 'node:fs';
|
||||
import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from 'node:fs';
|
||||
import { join } from 'node:path';
|
||||
import { tmpdir } from 'node:os';
|
||||
import type { WizardState } from '../types.js';
|
||||
@@ -113,40 +113,6 @@ describe('finalizeStage — skill installer', () => {
|
||||
);
|
||||
}
|
||||
|
||||
it('auto-registers every canonical skill even when it was added after initial setup', async () => {
|
||||
const claudeHome = join(tmp, '.claude');
|
||||
const previousClaudeHome = process.env['CLAUDE_HOME'];
|
||||
process.env['CLAUDE_HOME'] = claudeHome;
|
||||
mkdirSync(join(tmp, 'skills', 'added-after-setup'), { recursive: true });
|
||||
mkdirSync(join(tmp, 'skills', 'another-new-skill'), { recursive: true });
|
||||
|
||||
try {
|
||||
await finalizeStage(buildPrompter(), makeState(tmp, []), makeConfigService());
|
||||
|
||||
expect(readlinkSync(join(claudeHome, 'skills', 'added-after-setup'))).toBe(
|
||||
join(tmp, 'skills', 'added-after-setup'),
|
||||
);
|
||||
expect(readlinkSync(join(claudeHome, 'skills', 'another-new-skill'))).toBe(
|
||||
join(tmp, 'skills', 'another-new-skill'),
|
||||
);
|
||||
} finally {
|
||||
if (previousClaudeHome === undefined) delete process.env['CLAUDE_HOME'];
|
||||
else process.env['CLAUDE_HOME'] = previousClaudeHome;
|
||||
}
|
||||
});
|
||||
|
||||
it('warns and completes finalization when bridge-wide reconciliation fails', async () => {
|
||||
writeFileSync(join(tmp, 'skills'), 'invalid canonical root\n');
|
||||
const p = buildPrompter();
|
||||
|
||||
await finalizeStage(p, makeState(tmp, []), makeConfigService());
|
||||
|
||||
expect(p.warn).toHaveBeenCalledWith(
|
||||
expect.stringMatching(/Claude skill reconciliation skipped.*not a directory/i),
|
||||
);
|
||||
expect(p.outro).toHaveBeenCalledWith('Mosaic is ready.');
|
||||
});
|
||||
|
||||
it('passes MOSAIC_INSTALL_SKILLS with the selected skill list', async () => {
|
||||
const state = makeState(tmp, ['brainstorming', 'lint', 'systematic-debugging']);
|
||||
const p = buildPrompter();
|
||||
|
||||
@@ -7,11 +7,6 @@ import type { ConfigService } from '../config/config-service.js';
|
||||
import type { WizardState } from '../types.js';
|
||||
import { getShellProfilePath } from '../platform/detect.js';
|
||||
import { ManifestError } from '../framework/manifest.js';
|
||||
import {
|
||||
getDefaultSkillPaths,
|
||||
syncClaudeSkills,
|
||||
type SkillSyncResult as ClaudeSkillSyncResult,
|
||||
} from '../commands/skill.js';
|
||||
|
||||
function linkRuntimeAssets(mosaicHome: string, skipClaudeHooks: boolean): void {
|
||||
const script = join(mosaicHome, 'bin', 'mosaic-link-runtime-assets');
|
||||
@@ -210,27 +205,7 @@ export async function finalizeStage(
|
||||
skillsResult = syncSkills(state.mosaicHome, state.selectedSkills);
|
||||
}
|
||||
|
||||
// 5. Reconcile every canonical Mosaic skill into Claude Code. This is
|
||||
// intentionally independent of the first-run selected-skill fetch above:
|
||||
// framework installs/upgrades must also register skills added after setup.
|
||||
spin.update('Registering Mosaic skills with Claude Code...');
|
||||
let bridgeResult: ClaudeSkillSyncResult = {
|
||||
registered: [],
|
||||
repaired: [],
|
||||
unchanged: [],
|
||||
conflicts: [],
|
||||
};
|
||||
let bridgeFailure: string | undefined;
|
||||
try {
|
||||
bridgeResult = syncClaudeSkills({
|
||||
mosaicSkillsDir: join(state.mosaicHome, 'skills'),
|
||||
claudeSkillsDir: getDefaultSkillPaths().claudeSkillsDir,
|
||||
});
|
||||
} catch (error: unknown) {
|
||||
bridgeFailure = error instanceof Error ? error.message : String(error);
|
||||
}
|
||||
|
||||
// 6. Run doctor
|
||||
// 5. Run doctor
|
||||
spin.update('Running health audit...');
|
||||
const doctorResult = runDoctor(state.mosaicHome);
|
||||
|
||||
@@ -242,15 +217,10 @@ export async function finalizeStage(
|
||||
p.warn("Run 'mosaic sync' manually after installation to install skills.");
|
||||
}
|
||||
|
||||
if (bridgeFailure) p.warn(`Claude skill reconciliation skipped: ${bridgeFailure}`);
|
||||
for (const conflict of bridgeResult.conflicts) {
|
||||
p.warn(`Skill registration skipped for ${conflict.name}: ${conflict.reason}`);
|
||||
}
|
||||
|
||||
// 7. PATH setup
|
||||
// 6. PATH setup
|
||||
const pathAction = setupPath(state.mosaicHome, p);
|
||||
|
||||
// 8. Summary
|
||||
// 7. Summary
|
||||
const skillsSummary = skillsResult.success
|
||||
? skillsResult.installedCount > 0
|
||||
? `${skillsResult.installedCount.toString()} installed`
|
||||
@@ -275,7 +245,7 @@ export async function finalizeStage(
|
||||
|
||||
p.note(summary.join('\n'), 'Installation Summary');
|
||||
|
||||
// 9. Next steps
|
||||
// 8. Next steps
|
||||
const nextSteps: string[] = [];
|
||||
if (pathAction === 'added') {
|
||||
const profilePath = getShellProfilePath();
|
||||
|
||||
@@ -5,16 +5,5 @@ export default defineConfig({
|
||||
globals: true,
|
||||
environment: 'node',
|
||||
testTimeout: 30_000,
|
||||
coverage: {
|
||||
provider: 'v8',
|
||||
include: ['src/commands/skill.ts'],
|
||||
reporter: ['text', 'json-summary'],
|
||||
thresholds: {
|
||||
statements: 85,
|
||||
branches: 85,
|
||||
functions: 85,
|
||||
lines: 85,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
3
pnpm-lock.yaml
generated
3
pnpm-lock.yaml
generated
@@ -607,9 +607,6 @@ importers:
|
||||
'@types/react':
|
||||
specifier: ^18.3.0
|
||||
version: 18.3.28
|
||||
'@vitest/coverage-v8':
|
||||
specifier: ^2.0.0
|
||||
version: 2.1.9(vitest@2.1.9(@types/node@22.19.15)(jsdom@29.0.0(@noble/hashes@2.0.1))(lightningcss@1.31.1))
|
||||
tsx:
|
||||
specifier: ^4.0.0
|
||||
version: 4.21.0
|
||||
|
||||
@@ -61,38 +61,17 @@ if [[ "${MOSAIC_DEV:-0}" == "1" ]]; then
|
||||
FLAG_DEV=true
|
||||
fi
|
||||
|
||||
installer_usage() {
|
||||
printf 'Usage: install.sh [--check] [--framework] [--cli] [--ref <branch>] [--dev] [--yes|-y] [--no-auto-launch] [--uninstall]\n' >&2
|
||||
}
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--check) FLAG_CHECK=true; shift ;;
|
||||
--framework) FLAG_CLI=false; shift ;;
|
||||
--cli) FLAG_FRAMEWORK=false; shift ;;
|
||||
--ref)
|
||||
if [[ $# -lt 2 ]] || [[ -z "$2" ]]; then
|
||||
printf 'Error: Missing value for --ref\n' >&2
|
||||
installer_usage
|
||||
exit 2
|
||||
fi
|
||||
if [[ "$2" == -* ]]; then
|
||||
printf 'Error: Unknown argument: %s\n' "$2" >&2
|
||||
installer_usage
|
||||
exit 2
|
||||
fi
|
||||
GIT_REF="$2"
|
||||
shift 2
|
||||
;;
|
||||
--ref) GIT_REF="${2:-main}"; shift 2 ;;
|
||||
--dev) FLAG_DEV=true; shift ;;
|
||||
--yes|-y) FLAG_YES=true; shift ;;
|
||||
--no-auto-launch) FLAG_NO_AUTO_LAUNCH=true; shift ;;
|
||||
--uninstall) FLAG_UNINSTALL=true; shift ;;
|
||||
*)
|
||||
printf 'Error: Unknown argument: %s\n' "$1" >&2
|
||||
installer_usage
|
||||
exit 2
|
||||
;;
|
||||
*) shift ;;
|
||||
esac
|
||||
done
|
||||
|
||||
|
||||
Reference in New Issue
Block a user