diff --git a/docs/compaction-refresh/GATE0-EVIDENCE.md b/docs/compaction-refresh/GATE0-EVIDENCE.md new file mode 100644 index 0000000..219b28d --- /dev/null +++ b/docs/compaction-refresh/GATE0-EVIDENCE.md @@ -0,0 +1,340 @@ +# 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//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" +``` + +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. + +--- + +## 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. diff --git a/docs/compaction-refresh/evidence/RAW-SHA256SUMS.txt b/docs/compaction-refresh/evidence/RAW-SHA256SUMS.txt new file mode 100644 index 0000000..21b29f0 --- /dev/null +++ b/docs/compaction-refresh/evidence/RAW-SHA256SUMS.txt @@ -0,0 +1,8 @@ +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 diff --git a/docs/compaction-refresh/evidence/raw/P1-claude-hook-events.txt b/docs/compaction-refresh/evidence/raw/P1-claude-hook-events.txt new file mode 100644 index 0000000..f4fa963 --- /dev/null +++ b/docs/compaction-refresh/evidence/raw/P1-claude-hook-events.txt @@ -0,0 +1,28 @@ +$ python3 docs/compaction-refresh/probes/p1_run.py --runtime claude +=== P1 CLAUDE REAL LAUNCH === +$ python3 docs/compaction-refresh/probes/p1_anchor_exec.py --socket claude +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= +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"} + diff --git a/docs/compaction-refresh/evidence/raw/P1-launch-ancestry.txt b/docs/compaction-refresh/evidence/raw/P1-launch-ancestry.txt new file mode 100644 index 0000000..ee961f1 --- /dev/null +++ b/docs/compaction-refresh/evidence/raw/P1-launch-ancestry.txt @@ -0,0 +1,62 @@ +$ 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 pi +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 claude +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= +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, { diff --git a/docs/compaction-refresh/evidence/raw/P2-P3-P5-P6-pi.txt b/docs/compaction-refresh/evidence/raw/P2-P3-P5-P6-pi.txt new file mode 100644 index 0000000..69c2ab0 --- /dev/null +++ b/docs/compaction-refresh/evidence/raw/P2-P3-P5-P6-pi.txt @@ -0,0 +1,52 @@ +$ 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 diff --git a/docs/compaction-refresh/evidence/raw/P2-provider-timing.txt b/docs/compaction-refresh/evidence/raw/P2-provider-timing.txt new file mode 100644 index 0000000..4eab930 --- /dev/null +++ b/docs/compaction-refresh/evidence/raw/P2-provider-timing.txt @@ -0,0 +1,9 @@ +$ 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 diff --git a/docs/compaction-refresh/evidence/raw/P4-so-peercred.txt b/docs/compaction-refresh/evidence/raw/P4-so-peercred.txt new file mode 100644 index 0000000..eaa749f --- /dev/null +++ b/docs/compaction-refresh/evidence/raw/P4-so-peercred.txt @@ -0,0 +1,23 @@ +$ 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 diff --git a/docs/compaction-refresh/evidence/raw/P6-claude-additional-context.txt b/docs/compaction-refresh/evidence/raw/P6-claude-additional-context.txt new file mode 100644 index 0000000..9a440ec --- /dev/null +++ b/docs/compaction-refresh/evidence/raw/P6-claude-additional-context.txt @@ -0,0 +1,21 @@ +$ python3 docs/compaction-refresh/probes/p6_claude_run.py +machine_assertions=PASS +command=mosaic yolo claude --settings --model haiku --print --output-format stream-json --verbose --include-hook-events +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" diff --git a/docs/compaction-refresh/evidence/raw/P6-contract-gap.txt b/docs/compaction-refresh/evidence/raw/P6-contract-gap.txt new file mode 100644 index 0000000..f4d0ce4 --- /dev/null +++ b/docs/compaction-refresh/evidence/raw/P6-contract-gap.txt @@ -0,0 +1,47 @@ +$ rg -n -i "atomic|prefix-preserv" +NO MATCH: neither installed runtime document states an atomic/prefix-preserving transport guarantee. + +$ rg -n -C 3 "#### context|event.messages - deep copy|return \{ messages" +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" +/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 diff --git a/docs/compaction-refresh/evidence/raw/STEP0-authority-hashes.txt b/docs/compaction-refresh/evidence/raw/STEP0-authority-hashes.txt new file mode 100644 index 0000000..ab3d87f --- /dev/null +++ b/docs/compaction-refresh/evidence/raw/STEP0-authority-hashes.txt @@ -0,0 +1,9 @@ +$ 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 diff --git a/docs/compaction-refresh/probes/__pycache__/pi_gate0_run.cpython-311.pyc b/docs/compaction-refresh/probes/__pycache__/pi_gate0_run.cpython-311.pyc new file mode 100644 index 0000000..b1e5db5 Binary files /dev/null and b/docs/compaction-refresh/probes/__pycache__/pi_gate0_run.cpython-311.pyc differ diff --git a/docs/compaction-refresh/probes/p1_anchor_exec.py b/docs/compaction-refresh/probes/p1_anchor_exec.py new file mode 100644 index 0000000..224876c --- /dev/null +++ b/docs/compaction-refresh/probes/p1_anchor_exec.py @@ -0,0 +1,51 @@ +#!/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() diff --git a/docs/compaction-refresh/probes/p1_broker.py b/docs/compaction-refresh/probes/p1_broker.py new file mode 100644 index 0000000..a9f0b0b --- /dev/null +++ b/docs/compaction-refresh/probes/p1_broker.py @@ -0,0 +1,180 @@ +#!/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 diff --git a/docs/compaction-refresh/probes/p1_hook_client.py b/docs/compaction-refresh/probes/p1_hook_client.py new file mode 100644 index 0000000..9fdd331 --- /dev/null +++ b/docs/compaction-refresh/probes/p1_hook_client.py @@ -0,0 +1,38 @@ +#!/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() diff --git a/docs/compaction-refresh/probes/p1_pi_extension.ts b/docs/compaction-refresh/probes/p1_pi_extension.ts new file mode 100644 index 0000000..ef2bdfe --- /dev/null +++ b/docs/compaction-refresh/probes/p1_pi_extension.ts @@ -0,0 +1,35 @@ +import type { ExtensionAPI } from '@earendil-works/pi-coding-agent'; +import net from 'node:net'; + +async function brokerRequest(payload: Record): Promise> { + 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); + }); + 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, + }); +} diff --git a/docs/compaction-refresh/probes/p1_run.py b/docs/compaction-refresh/probes/p1_run.py new file mode 100644 index 0000000..017feaa --- /dev/null +++ b/docs/compaction-refresh/probes/p1_run.py @@ -0,0 +1,274 @@ +#!/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=") + 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 {runtime} " + ) + 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() diff --git a/docs/compaction-refresh/probes/p1_sibling_attacker.py b/docs/compaction-refresh/probes/p1_sibling_attacker.py new file mode 100644 index 0000000..f3383f5 --- /dev/null +++ b/docs/compaction-refresh/probes/p1_sibling_attacker.py @@ -0,0 +1,54 @@ +#!/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() diff --git a/docs/compaction-refresh/probes/p2_provider_timing_run.py b/docs/compaction-refresh/probes/p2_provider_timing_run.py new file mode 100644 index 0000000..24423e6 --- /dev/null +++ b/docs/compaction-refresh/probes/p2_provider_timing_run.py @@ -0,0 +1,135 @@ +#!/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() diff --git a/docs/compaction-refresh/probes/p3_generation_broker.py b/docs/compaction-refresh/probes/p3_generation_broker.py new file mode 100644 index 0000000..6b335be --- /dev/null +++ b/docs/compaction-refresh/probes/p3_generation_broker.py @@ -0,0 +1,117 @@ +#!/usr/bin/env python3 +"""P3 broker prototype: peercred-keyed runtime_generation and lease revocation.""" + +from __future__ import annotations + +import argparse +import json +import os +import socket +import struct +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 main() -> None: + parser = argparse.ArgumentParser() + parser.add_argument("--socket", required=True) + parser.add_argument("--log", required=True) + ns = parser.parse_args() + socket_path = Path(ns.socket) + log_path = Path(ns.log) + 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] = {} + 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") == "promote-probe": + generation = generations.get(identity, 0) + lease_state[identity] = "VERIFIED" + record = { + "event": "probe_lease_promoted", + "peercred": {"pid": pid, "uid": uid, "gid": gid}, + "starttime_ticks": starttime, + "generation": generation, + "new_lease_state": "VERIFIED", + } + emit(log_path, record) + conn.sendall((json.dumps(record, sort_keys=True) + "\n").encode()) + continue + if request.get("action") == "source-invalid": + old_lease = lease_state.get(identity, "NONE") + lease_state[identity] = "REVOKED" + record = { + "event": "source_invalidation_revoke", + "peercred": {"pid": pid, "uid": uid, "gid": gid}, + "starttime_ticks": starttime, + "generation": generations.get(identity, 0), + "source_reason": request.get("reason"), + "prior_lease": old_lease, + "new_lease_state": "REVOKED", + "promotion": False, + } + 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 = old_generation + 1 + 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, + "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() diff --git a/docs/compaction-refresh/probes/p4_peercred_probe.py b/docs/compaction-refresh/probes/p4_peercred_probe.py new file mode 100644 index 0000000..3650911 --- /dev/null +++ b/docs/compaction-refresh/probes/p4_peercred_probe.py @@ -0,0 +1,97 @@ +#!/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/ 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() diff --git a/docs/compaction-refresh/probes/p6_claude_hook.py b/docs/compaction-refresh/probes/p6_claude_hook.py new file mode 100644 index 0000000..3fe50ec --- /dev/null +++ b/docs/compaction-refresh/probes/p6_claude_hook.py @@ -0,0 +1,54 @@ +#!/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() diff --git a/docs/compaction-refresh/probes/p6_claude_run.py b/docs/compaction-refresh/probes/p6_claude_run.py new file mode 100644 index 0000000..1e3477e --- /dev/null +++ b/docs/compaction-refresh/probes/p6_claude_run.py @@ -0,0 +1,145 @@ +#!/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 --model haiku --print --output-format stream-json --verbose --include-hook-events ") + 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() diff --git a/docs/compaction-refresh/probes/pi_gate0_extension.ts b/docs/compaction-refresh/probes/pi_gate0_extension.ts new file mode 100644 index 0000000..2a269be --- /dev/null +++ b/docs/compaction-refresh/probes/pi_gate0_extension.ts @@ -0,0 +1,350 @@ +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 = {}): 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): Promise> { + 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); + }); + 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)) { + 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(); + + 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; + }, + }); +} diff --git a/docs/compaction-refresh/probes/pi_gate0_run.py b/docs/compaction-refresh/probes/pi_gate0_run.py new file mode 100644 index 0000000..cbd4154 --- /dev/null +++ b/docs/compaction-refresh/probes/pi_gate0_run.py @@ -0,0 +1,450 @@ +#!/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() diff --git a/docs/compaction-refresh/probes/pi_later_extension.ts b/docs/compaction-refresh/probes/pi_later_extension.ts new file mode 100644 index 0000000..9fabfcb --- /dev/null +++ b/docs/compaction-refresh/probes/pi_later_extension.ts @@ -0,0 +1,8 @@ +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); +}