Compare commits
7 Commits
feat/827-g
...
governance
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0db8f7f3cb | ||
| 571f239154 | |||
| dc0307d0ce | |||
| 33f14bbfdc | |||
| 8dfcf1903e | |||
| abd2791f59 | |||
| 8ec67a1126 |
@@ -1,5 +1,12 @@
|
||||
# Documentation Sitemap
|
||||
|
||||
## Compaction refresh lease broker
|
||||
|
||||
- [Internal broker protocol](architecture/lease-broker-protocol.md) — kernel identity, ancestry and generation invariants, framed requests, responses, and persisted cycle bindings.
|
||||
- [Broker operations](guides/lease-broker-operations.md) — protected paths, startup, fail-closed recovery posture, distinct-principal deployment, and residual risk.
|
||||
- [Lease-broker security notes](architecture/lease-broker-security.md) — identity, whole-class authorization, threat boundaries, and coordinator review requirements.
|
||||
- [Whole mutator-class gate](architecture/mutator-class-gate.md) — default-deny policy, revoke-first/promote-last state machine, TTL, runtime adapters, and T-B/T-C assurance boundary.
|
||||
|
||||
## CLI and skill management
|
||||
|
||||
- [Skill registration user guide](guides/user-guide.md#claude-code-skill-registration) — register, unregister, list statuses, automatic install/update reconciliation, and Claude reload behavior.
|
||||
|
||||
24
docs/architecture/lease-broker-protocol.md
Normal file
24
docs/architecture/lease-broker-protocol.md
Normal file
@@ -0,0 +1,24 @@
|
||||
# Authenticated external lease broker protocol
|
||||
|
||||
The compaction-refresh lease broker is a Linux-only, newline-framed JSON protocol over a Unix stream socket. It is runtime-neutral; M1 consumers are limited to Claude and Pi. This is an internal process boundary, not an HTTP API, so it is intentionally absent from OpenAPI.
|
||||
|
||||
The broker, never the caller, obtains `(pid, uid, gid)` from kernel `SO_PEERCRED`. It correlates the PID with `/proc/<pid>/stat` field 22 (`starttime`) and mints `session_id` on `register_anchor`. Presence of `session_id` in that request is refused even when its value is `null` or empty. Later requests must originate from the anchor or a descendant. The broker walks parent PIDs to the `(pid,starttime)` anchor and then rereads every walked PID's starttime before accepting the chain.
|
||||
|
||||
## Request and response boundary
|
||||
|
||||
Each connection carries exactly one UTF-8 JSON object followed by one newline, capped at 64 KiB. The protocol deliberately uses EOF to prove that there is exactly one frame: immediately after writing the newline, the client **MUST half-close its write side** with `shutdown(SHUT_WR)` (or Node `socket.end()`) before awaiting the response. A client that writes a newline but leaves its write side open receives no successful response; the broker's one-second connection deadline fails closed. Malformed, unterminated, multiple (including a delayed second frame), or oversized frames fail closed. Responses are one JSON object and one newline. Success has `{"ok":true,...}`; refusal has `{"ok":false,"code":"TYPED_CODE"}`. Requests are:
|
||||
|
||||
- `register_anchor`: `action`, non-negative `runtime_generation`; no `session_id` field.
|
||||
- `authenticate`: `action`, broker-minted `session_id`, non-negative `runtime_generation`.
|
||||
- `mint_token`: authenticated identity plus `binding` containing exactly `compaction_epoch`, `request_epoch`, `h_source`, `h_payload`, and `schema_version`.
|
||||
- `consume_token`: authenticated identity plus `token`.
|
||||
- `begin_verification`: authenticated identity, runtime (`claude` or `pi`), cycle `binding`, and a TTL no greater than 300 seconds. The broker revokes existing authority first, enters `PENDING_VERIFICATION`, and returns a single-use promotion token.
|
||||
- `promote_lease`: authenticated identity plus the exact pending promotion token. The broker commits token consumption before making `VERIFIED` visible.
|
||||
- `revoke_lease`: authenticated observer signal; deletes pending tokens and makes the session `UNVERIFIED` immediately.
|
||||
- `authorize_tool`: authenticated identity, runtime, and exact runtime-reported tool name. The broker returns an explicit allow/deny decision from the whole-class policy and current lease.
|
||||
|
||||
A higher generation for the same anchor atomically replaces the stored incarnation and deletes all prior tokens and lease authority for that session. A lower generation is stale. Tokens are 256-bit values from the operating-system cryptographic RNG and are single use. At most 256 pending tokens may be persisted; another mint fails with `TOKEN_CAPACITY` before mutation. Successful consumption deletes the token, while a replay still fails with `TOKEN_REPLAY`. Live v1 token records retain the existing `consumed: false` schema.
|
||||
|
||||
VERIFIED leases are volatile and monotonic-time bounded: broker restart, generation change, explicit observer revocation, or expiry returns the session to `UNVERIFIED`. `begin_verification` always revokes before minting a new prerequisite. `promote_lease` is valid only from the matching pending cycle; persistence failure rolls token and lease state back, while post-rename durability uncertainty terminates the broker. The WI-1 token is the atomic promotion prerequisite substrate. A later receipt implementation must satisfy that prerequisite but cannot replace the mechanical mutator gate as safety authority.
|
||||
|
||||
State replacement serializes and enforces the 4 MiB maximum before opening a temporary file, then uses a mode-`0600` temporary file, `fsync`, atomic rename, and parent-directory `fsync`. Every broker mutation snapshots the prior v1 state. A commit failure before rename restores that snapshot and leaves durable state unchanged. A failure after rename makes durability uncertain, so the store is poisoned without rolling memory back and the daemon terminates rather than serving with divergent state. Existing state is opened without following symlinks, must be a bounded regular file at mode `0600`, and is fully schema- and invariant-validated before use. Persisted tokens must be unconsumed, match their session's current generation, and remain within the 256-token cap. Session identity is uniquely keyed by `(anchor_pid,anchor_starttime)`; duplicate logical sessions for one anchor refuse startup. State integrity or mode failures refuse startup. The daemon does not log session IDs or tokens.
|
||||
14
docs/architecture/lease-broker-security.md
Normal file
14
docs/architecture/lease-broker-security.md
Normal file
@@ -0,0 +1,14 @@
|
||||
# WI-1 lease broker security notes
|
||||
|
||||
- Trusted identity comes only from Linux `SO_PEERCRED` plus `/proc` starttime, never request identity fields.
|
||||
- Descendant authorization is anchored to `(pid,starttime)` and uses a complete second starttime pass to fail closed on disappearance or PID-reuse races.
|
||||
- Runtime generations are monotonic per anchor; a bump revokes prior-incarnation tokens before persistence commits.
|
||||
- Session IDs and cycle tokens use the OS cryptographic RNG. `Math.random` and model output are not token sources.
|
||||
- Framing and persistence failures fail closed. Sensitive tokens are not logged.
|
||||
- Built-in `0700`/`0600` filesystem modes provide same-principal hardening only, not socket authenticity against the same UID. WI-1 provides no distinct-principal isolation. That stronger deployment requires an external protected proxy, ACL, or service boundary, and the boundary must preserve authenticated client identity for the broker's `SO_PEERCRED` and ancestry authorization rather than substituting a shared proxy identity.
|
||||
- WI-2 whole-class authorization denies every consequential, unknown, and custom tool while UNVERIFIED; it does not inspect shell strings or trust wrapper selection. First-class Claude/Pi, both Claudex dispatch modes, PRDY, QA remediation, coord, orchestrator, and fleet starts converge on broker register-before-exec; Claudex additionally installs the mandatory all-tools hook inside its preserved isolated config and fails closed on unsafe settings.
|
||||
- The permanent `check-runtime-launches.py` suite/CI guard scans production source for direct literal, absolute-path, process-API, command-array, and dynamic Claude/Pi launches. It has no bypass allowlist: an unrecognized launch form fails CI until routed through the common boundary.
|
||||
- WI-2 promotion consumes a WI-1 cycle token before VERIFIED becomes visible. Observer revocation, runtime-generation replacement, broker restart, and monotonic TTL expiry remove authority.
|
||||
- Receipt observation, payload construction, compaction observers, and constrained recovery implementation remain later surfaces. A receipt can become a promotion prerequisite but is never the safety mechanism.
|
||||
|
||||
Coordinator security review must rerun the real socket/peercred and mutator-gate acceptance suites on an unrestricted Linux runner and obtain the mandated independent Opus-SECREV review before integration.
|
||||
72
docs/architecture/mutator-class-gate.md
Normal file
72
docs/architecture/mutator-class-gate.md
Normal file
@@ -0,0 +1,72 @@
|
||||
# Whole mutator-class lease gate
|
||||
|
||||
WI-2 adds the framework-native authorization boundary for Claude (including the supported Claudex overlay) and Pi. Every runtime-reported tool name reaches the lease broker before execution. The gate classifies capabilities by the whole tool class; it never parses a Bash command to decide whether that particular string looks read-only.
|
||||
|
||||
## Default-deny policy
|
||||
|
||||
While a session is not VERIFIED, only these exact classes are allowed:
|
||||
|
||||
- Claude: `Read`, `Grep`, `Glob`, `Ls`, `Find`
|
||||
- Pi: `read`, `grep`, `find`, `ls`
|
||||
- Both runtimes: the fixed `mosaic_context_recover` primitive
|
||||
|
||||
Every other built-in, unknown tool, and custom/MCP tool is consequential by default and is denied. This includes Claude `Bash`, `Edit`, `Write`, and `NotebookEdit`, plus Pi `bash`, `edit`, and `write`. A compromised model therefore cannot bypass Mosaic wrappers by selecting raw `git`, `curl`, `kubectl`, provider, deployment, or filesystem commands inside a generic mutator—the generic mutator itself is blocked before its input executes.
|
||||
|
||||
## Broker-owned transition order
|
||||
|
||||
The authenticated broker is the sole lease writer:
|
||||
|
||||
1. `begin_verification` revokes existing authority and pending tokens first, then records `PENDING_VERIFICATION` and mints one WI-1 single-use promotion token bound to the exact cycle.
|
||||
2. `promote_lease` accepts only that session/generation/binding/token combination.
|
||||
3. Token consumption commits before the volatile lease becomes VERIFIED. Promotion is last and cannot be reached directly from UNVERIFIED.
|
||||
4. `revoke_lease`, a runtime-generation increase, broker restart, or monotonic expiry removes mutator authority.
|
||||
|
||||
The initial TTL is capped at the ratified 300-second maximum. A caller may request a shorter positive TTL but cannot lengthen the maximum. Dual compaction-hook miss within an unexpired lease remains the ratified bounded T-A residual; once either observer revokes or TTL expires, the next consequential tool is denied.
|
||||
|
||||
A receipt is only a future promotion prerequisite. It is not an obedience, residency, or safety proof and never replaces this mechanical gate.
|
||||
|
||||
## Runtime adapters
|
||||
|
||||
`launch-runtime.py` registers itself with the broker and then `exec`s Claude or Pi so PID/starttime remain the authenticated parent anchor. It exports only the broker-minted session ID and current generation to descendants.
|
||||
|
||||
- Claude installs `mutator-gate.py` as an all-tools (`.*`) `PreToolUse` hook.
|
||||
- `mosaic claudex` and `mosaic yolo claudex` preserve their isolated `CLAUDE_CONFIG_DIR`, merge the mandatory hook into that isolated `settings.json`, and use the same register-before-exec launcher. Malformed or symlinked isolated settings deny launch.
|
||||
- Pi invokes the same executable from its `tool_call` handler.
|
||||
|
||||
The executable submits the runtime's actual tool name to `authorize_tool`. Missing identity, malformed input/reply, timeout, broker unavailability, or denial exits with status 2 and blocks fail-closed.
|
||||
|
||||
## Runtime-launch choke-point and permanent guard
|
||||
|
||||
Every repository-owned Claude/Pi launch entry converges on `launch-runtime.py`, either directly or through `mosaic` → `execLeaseGatedRuntime`. PRDY init/update and QA remediation invoke the wrapper directly so their existing prompts, dangerous-permission behavior, working directory, and environment survive without skipping broker registration. The raw Claude `--dangerously-skip-permissions` primitive is owned only by `launch-runtime.py`; callers request semantic `--dangerous` mode, and the wrapper validates Claude before injecting the primitive. `@mosaicstack/coord` rewrites direct Claude commands to `mosaic claude` and rejects unknown custom Claude launchers.
|
||||
|
||||
`check-runtime-launches.py` is the permanent completeness guard. It scans production shell, TypeScript/JavaScript, Python, and data launch definitions under `packages/`, `apps/`, `plugins/`, and `tools/`; direct literal, absolute-path, process-API, dynamic, command-substitution, `eval`, and variable-execution runtime launches fail. Shell comments are stripped with quote awareness, wrapper prefixes are tokenized with `shlex`, and only an invocation in command position with `--runtime` before the command separator is gated. Literal and tracked-variable command tokens use one terminal resolver after any nesting of `exec`, `command`, `nohup`, or `env` plus assignments. A direct command always wins over an inert marker on the same line. Independently, the raw dangerous primitive anywhere outside the choke-point is RED.
|
||||
|
||||
The command parser is a best-effort CI defense, not a complete shell interpreter. Alias/function redefinition, sourced commands, generated scripts, and encoded pipelines are intentionally residual rather than an invitation to chase an unbounded shell language. Two runtime controls backstop that residual surface: primitive ownership rejects a dangerous launch even when command identity is alias-indirected, and Claude's global `.*` `PreToolUse` hook invokes the broker gate for non-dangerous launches. Without `MOSAIC_LEASE_SESSION_ID`, representative read, mutator, and custom/MCP tools all fail closed with `GATE_UNAVAILABLE`. Hook absence or replacement remains in the documented T-C boundary.
|
||||
|
||||
### Parser stopping criterion
|
||||
|
||||
- **A — realistic parser matrix:** comments, inert strings/assignments, heredocs, continuations, chained commands, command substitution, `eval`, bare tracked variables, and quoted/unquoted tracked variables behind `exec`, `command`, `nohup`, or `env` are permanent RED regressions. Prefix-variable forms are covered in both multiline and same-line assignment shapes.
|
||||
- **B — residual backstops:** a dangerous alias-indirected launch is RED solely through primitive anchoring; a parser-missed non-dangerous alias launch is paired with an acceptance test proving the global all-tools hook denies every representative tool class as `GATE_UNAVAILABLE` without a lease.
|
||||
- **C — independent fresh review:** the parser class is considered complete only when reviewers find no new non-overlapping realistic evasion on the exact head. A and B are repository evidence; C is supplied by the fresh review round.
|
||||
|
||||
All three layers are load-bearing and complementary. The guard is mandatory in `@mosaicstack/mosaic`'s test script, so root CI fails on a future realistic bypass. Real-socket tests separately prove PRDY init/update and QA receive broker sessions and deny an unverified mutator.
|
||||
|
||||
The live inventory is emitted by:
|
||||
|
||||
```bash
|
||||
python3 packages/mosaic/framework/tools/lease-broker/check-runtime-launches.py --root . --json
|
||||
```
|
||||
|
||||
| Production launch family | Gated entries |
|
||||
| ------------------------------------------------------ | ------------: |
|
||||
| `@mosaicstack/coord` default/configured Claude command | 2 |
|
||||
| Fleet runtime start | 1 |
|
||||
| QA remediation + generated QA command | 2 |
|
||||
| Orchestrator command construction/session launches | 3 |
|
||||
| PRDY init/update | 2 |
|
||||
| Mosaic Claude/Pi/Claudex adapter and wrapper boundary | 4 |
|
||||
| **Total** | **14 / 14** |
|
||||
|
||||
## Assurance boundary
|
||||
|
||||
This closes T-A after an observer fires or lease expiry and T-B for in-runtime tool calls. Hook/extension absence, a runtime executing outside the gated launcher, ptrace/same-UID broker replacement, and other fully rotted behavior remain T-C. Server-side branch protection and required PR review/CI remain the irreducible line for protected repository mutations.
|
||||
@@ -1,352 +0,0 @@
|
||||
# Compaction-Refresh WI-0 Gate0 Evidence Pack
|
||||
|
||||
- **Issue:** Gitea #827
|
||||
- **Milestone:** 188 — Compaction-Refresh Mechanism
|
||||
- **Branch:** `feat/827-gate0-probe`
|
||||
- **Starting HEAD:** `d801d6c4c8a984d6a95033c49714210018d3d9a8`
|
||||
- **Host/runtime:** Linux 6.1.0-48-amd64; Mosaic 0.0.48; Pi 0.80.7; Claude Code 2.1.205
|
||||
- **Scope:** Probe fixtures and evidence only. No WI-1..WI-7 feature implementation.
|
||||
|
||||
## Verdict — 5/6 PASS; BUILD ADMISSION: **NO**
|
||||
|
||||
| Probe | Verdict | Short result |
|
||||
| --- | --- | --- |
|
||||
| P1 launcher topology + ancestry | **PASS** | Real Mosaic→Pi and Mosaic→Claude chains reached the registered anchor; real Claude `SessionStart` hook ancestry accepted; same-UID sibling with the minted victim ID rejected. |
|
||||
| P2 Pi last-position + nonce map | **PASS** | Real Pi proved last-or-closed; `message_end` mapped exact `toolCallId → requestNonce` before `tool_call`; provider-response hook occurred before stream consumption/content completion. |
|
||||
| P3 same-PID generation revocation | **PASS** | Same Pi PID/starttime persisted through reload/fork/new/resume while broker generations increased; reload revoked a prior `VERIFIED` generation. |
|
||||
| P4 `SO_PEERCRED` + socket posture | **PASS** | Real Unix socket peer PID/UID/starttime matched `/proc`; 0700 directory + 0600 socket demonstrated. Same-UID counterfeit replacement remains explicitly T-C without a distinct principal/authenticated response. |
|
||||
| P5 source invalidation | **PASS** | Missing, oversize, and hash-mismatched fragments each refused injection/promotion, revoked broker state, and blocked the exact emitted tool call. |
|
||||
| P6 atomic injection | **T-C GAP** | Both runtimes empirically delivered a complete single block/message, but neither installed runtime contract states an **atomic/prefix-preserving** transport guarantee. Observation is not a guarantee; A-v5-1/T27 cannot be admitted. |
|
||||
|
||||
**Planner return item:** P6. The evidence establishes successful complete delivery in these runs, not the required invariant that the harness cannot middle-drop/replace bytes while preserving the terminal token. Per R1, such a middle-drop is not receipt-detectable. It is therefore classed **T-C**, not assumed away.
|
||||
|
||||
## STEP 0 — Authority re-verification
|
||||
|
||||
Command:
|
||||
|
||||
```bash
|
||||
sha256sum \
|
||||
~/agent-work/reviews/compaction-refresh-BUILD-BRIEF.md \
|
||||
~/agent-work/reviews/compaction-refresh-SPEC-v5.md \
|
||||
~/agent-work/reviews/compaction-refresh-SPEC-RATIFICATION.md
|
||||
```
|
||||
|
||||
Captured result:
|
||||
|
||||
```text
|
||||
89fdbc27ed0e5050dc7b52f3ef2ddaea691edf17fd89d51b15e26fb5ed47171b .../compaction-refresh-BUILD-BRIEF.md
|
||||
a6d07ade835758e8488ca10d3b0631caf0beb93ea3a6733631f151b0c2f01433 .../compaction-refresh-SPEC-v5.md
|
||||
bac58319c9c4028b5b40e1129e0033cdb5a6b7b02033c25f06f4cb77d7779c67 .../compaction-refresh-SPEC-RATIFICATION.md
|
||||
```
|
||||
|
||||
All three **MATCH**. They were read in full before probe construction. Raw artifact: [`evidence/raw/STEP0-authority-hashes.txt`](./evidence/raw/STEP0-authority-hashes.txt).
|
||||
|
||||
## Evidence method
|
||||
|
||||
The scripts under [`probes/`](./probes/) are isolated Gate0 instrumentation, not product implementation. They run the installed `mosaic yolo` launcher and real installed runtime binaries. Broker prototypes use Linux `SO_PEERCRED` and `/proc`; runtime adapters are temporary Claude hooks/Pi extensions. No product source under `packages/mosaic` was changed.
|
||||
|
||||
Raw-output artifact integrity is indexed at [`evidence/RAW-SHA256SUMS.txt`](./evidence/RAW-SHA256SUMS.txt).
|
||||
|
||||
---
|
||||
|
||||
## P1 — Launcher exec/parent topology + supported-hook ancestry (D1)
|
||||
|
||||
**Verdict: PASS**
|
||||
|
||||
### Commands
|
||||
|
||||
```bash
|
||||
python3 docs/compaction-refresh/probes/p1_run.py --runtime both
|
||||
rg -n "spawnSync|execRuntime" \
|
||||
~/.npm-global/lib/node_modules/@mosaicstack/mosaic/dist/commands/launch.js | tail -8
|
||||
```
|
||||
|
||||
Full outputs:
|
||||
|
||||
- [`evidence/raw/P1-launch-ancestry.txt`](./evidence/raw/P1-launch-ancestry.txt)
|
||||
- [`evidence/raw/P1-claude-hook-events.txt`](./evidence/raw/P1-claude-hook-events.txt)
|
||||
|
||||
### Real topology observed
|
||||
|
||||
The installed Mosaic launcher does **not** replace itself with the runtime despite its `execRuntime` name; installed `launch.js:668` uses `spawnSync`. The Gate0 anchor first registered, then `execvpe` replaced the anchor with the real `mosaic yolo` process (PID/starttime retained). Mosaic remained the stable parent while it spawned the runtime.
|
||||
|
||||
Pi run:
|
||||
|
||||
```text
|
||||
anchor before exec: pid=4010843 starttime=365919858 exe=/usr/bin/python3.11
|
||||
anchor after exec: pid=4010843 starttime=365919858 exe=/usr/bin/node
|
||||
Pi runtime: pid=4011046 ppid=4010843 starttime=365920219 exe=/usr/bin/node
|
||||
|
||||
ps:
|
||||
4010843 4010840 Fri Jul 17 19:35:18 2026 1001 1001 node
|
||||
4011046 4010843 Fri Jul 17 19:35:22 2026 1001 1001 pi
|
||||
```
|
||||
|
||||
Claude supported-hook run (latest capture):
|
||||
|
||||
```text
|
||||
hook python pid=4011380 starttime=365920845
|
||||
-> /bin/sh pid=4011379 starttime=365920845
|
||||
-> claude pid=4011285 starttime=365920748
|
||||
-> node/mosaic anchor pid=4011129 starttime=365920380
|
||||
```
|
||||
|
||||
The stream independently recorded the real hook firing:
|
||||
|
||||
```json
|
||||
{"type":"system","subtype":"hook_started","hook_name":"SessionStart:startup","hook_event":"SessionStart"}
|
||||
{"type":"system","subtype":"hook_response","hook_name":"SessionStart:startup","exit_code":0,"outcome":"success","stdout":"...GATE0_P1_SUPPORTED_HOOK_ANCESTRY_ACCEPTED..."}
|
||||
```
|
||||
|
||||
### Authentication and sibling substitution
|
||||
|
||||
The broker minted the logical ID on first peercred contact and keyed the anchor by `(pid,starttime)`. It took the hook/extension PID from `SO_PEERCRED`, walked `/proc/<pid>/stat`, and re-read every starttime before accepting.
|
||||
|
||||
Real Pi acceptance excerpt:
|
||||
|
||||
```json
|
||||
{"peercred":{"pid":4011046,"uid":1001,"gid":1001},"decision":"ACCEPT","reason":"ancestry-reaches-registered-anchor","starttimes_rechecked":true}
|
||||
```
|
||||
|
||||
A separately spawned same-UID sibling was given the real minted victim ID. Its ancestry did not reach the anchor:
|
||||
|
||||
```json
|
||||
{"attacker_uid":1001,"victim_session_id_known":true,"broker_decision":"REJECT","broker_reason":"victim-id-known-but-ancestry-mismatch"}
|
||||
```
|
||||
|
||||
The same rejection occurred in both Pi and Claude runs. This is positive runtime evidence for D1/T15a under the supported non-daemonizing topology.
|
||||
|
||||
---
|
||||
|
||||
## P2 — Pi last-position invariant + nonce map (D5)
|
||||
|
||||
**Verdict: PASS**
|
||||
|
||||
### Commands
|
||||
|
||||
```bash
|
||||
python3 docs/compaction-refresh/probes/pi_gate0_run.py
|
||||
python3 docs/compaction-refresh/probes/p2_provider_timing_run.py
|
||||
```
|
||||
|
||||
Full outputs:
|
||||
|
||||
- [`evidence/raw/P2-P3-P5-P6-pi.txt`](./evidence/raw/P2-P3-P5-P6-pi.txt)
|
||||
- [`evidence/raw/P2-provider-timing.txt`](./evidence/raw/P2-provider-timing.txt)
|
||||
|
||||
### Last-or-closed evidence
|
||||
|
||||
Real Pi argv/load order with the probe last:
|
||||
|
||||
```json
|
||||
{"extensions":[".../mosaic-extension.ts",".../pi_gate0_extension.ts"],"lastPosition":true,"gateState":"UNVERIFIED_READY","pid":4004545}
|
||||
```
|
||||
|
||||
A second real Pi launch deliberately appended a later handler:
|
||||
|
||||
```json
|
||||
{"extensions":[".../mosaic-extension.ts",".../pi_gate0_extension.ts",".../pi_later_extension.ts"],"lastPosition":false,"gateState":"CLOSED_NOT_LAST","pid":4005692}
|
||||
```
|
||||
|
||||
Thus the invariant observed is exactly **last or closed**, not an asserted registration order.
|
||||
|
||||
### Exact nonce → tool-call-ID map
|
||||
|
||||
In one real GPT-5.6 Sol Pi response, sequence 5 completed the assistant tool-call message and bound its exact ID:
|
||||
|
||||
```json
|
||||
{"seq":5,"event":"message_end","requestNonce":"e5a82358-a6c9-490b-a0de-2e1f1d9b8d79","toolCallIds":["call_bgGE...57c"],"nonceMappings":[{"toolCallId":"call_bgGE...57c","requestNonce":"e5a82358-a6c9-490b-a0de-2e1f1d9b8d79"}]}
|
||||
```
|
||||
|
||||
The following `tool_call` was sequence 6 and carried the same ID/nonce:
|
||||
|
||||
```json
|
||||
{"seq":6,"event":"tool_call","toolCallId":"call_bgGE...57c","mapping":{"nonce":"e5a82358-a6c9-490b-a0de-2e1f1d9b8d79","verified":true},"allowed":true}
|
||||
```
|
||||
|
||||
The harmless tool executed at sequence 7 with that same tool-call ID. No session-global “current epoch” was borrowed.
|
||||
|
||||
### `after_provider_response` is not assistant-content observation
|
||||
|
||||
A deterministic localhost HTTP provider was used only to force headers/status exposure through the real Pi transport. Actual order:
|
||||
|
||||
```json
|
||||
{"seq":4,"event":"before_provider_request"}
|
||||
{"seq":5,"event":"after_provider_response","status":200,"assistantContentAvailableAtThisHook":false,"timing":"headers/status before stream consumption"}
|
||||
{"seq":6,"event":"message_end","role":"assistant","assistantContentObserved":true}
|
||||
```
|
||||
|
||||
```text
|
||||
headers_hook_precedes_completed_message=True
|
||||
```
|
||||
|
||||
This positively confirms SPEC-v5’s precision correction: receipt content is observed at `message_end`; `after_provider_response` is status/headers before stream consumption.
|
||||
|
||||
---
|
||||
|
||||
## P3 — Same-PID `runtime_generation` bump revokes prior lease (D4)
|
||||
|
||||
**Verdict: PASS**
|
||||
|
||||
### Command
|
||||
|
||||
```bash
|
||||
python3 docs/compaction-refresh/probes/pi_gate0_run.py
|
||||
```
|
||||
|
||||
Full output: [`evidence/raw/P2-P3-P5-P6-pi.txt`](./evidence/raw/P2-P3-P5-P6-pi.txt).
|
||||
|
||||
The real Pi process identity remained:
|
||||
|
||||
```text
|
||||
pid=4004545 starttime_ticks=365907677 uid=1001
|
||||
```
|
||||
|
||||
Broker state around reload:
|
||||
|
||||
```json
|
||||
{"event":"runtime_generation_bump","reason":"startup","old_generation":0,"new_generation":1,"new_lease_state":"UNVERIFIED"}
|
||||
{"event":"probe_lease_promoted","generation":1,"new_lease_state":"VERIFIED"}
|
||||
{"event":"runtime_generation_bump","phase":"shutdown","reason":"reload","old_generation":1,"new_generation":2,"prior_lease":"VERIFIED","prior_lease_revoked":true,"new_lease_state":"REVOKED"}
|
||||
{"event":"runtime_generation_bump","phase":"start","reason":"reload","old_generation":2,"new_generation":3,"new_lease_state":"UNVERIFIED"}
|
||||
```
|
||||
|
||||
The same `(pid,starttime)` then emitted monotonic bumps for real `fork`, `new`, and `resume` replacement flows, reaching generation 12. Pi 0.80.7 emitted an additional conservative `session_start` callback in each of those replacement flows; the broker bumped again rather than reusing authority. This is an availability/idempotence consideration for implementation, not a fail-open result.
|
||||
|
||||
---
|
||||
|
||||
## P4 — `SO_PEERCRED` + socket authenticity posture
|
||||
|
||||
**Verdict: PASS, with the spec’s named same-UID T-C residual**
|
||||
|
||||
### Command
|
||||
|
||||
```bash
|
||||
python3 docs/compaction-refresh/probes/p4_peercred_probe.py
|
||||
```
|
||||
|
||||
Full output: [`evidence/raw/P4-so-peercred.txt`](./evidence/raw/P4-so-peercred.txt).
|
||||
|
||||
Captured real socket result:
|
||||
|
||||
```text
|
||||
server_pid=4013762 server_uid=1001 server_gid=1001
|
||||
directory_mode=0700 socket_mode=0600
|
||||
SO_PEERCRED pid=4013768 uid=1001 gid=1001
|
||||
client_claim={"pid":4013768,"starttime_ticks":365927069,"uid":1001,...}
|
||||
proc_observed={"pid":4013768,"starttime_ticks":365927069,"uid":1001,...}
|
||||
pid_match=True
|
||||
uid_match=True
|
||||
starttime_match=True
|
||||
client_exit_status=0
|
||||
```
|
||||
|
||||
Achievable unprivileged posture on this host is a user-owned 0700 parent plus 0600 socket. That excludes other UIDs and positively authenticates the connecting kernel PID/UID/GID. It does **not** stop another process running as `hermes` from unlinking/rebinding the socket. A claim stronger than T-C against counterfeit replacement therefore requires the ratified distinct-principal system service or authenticated broker responses. No stronger claim is made.
|
||||
|
||||
---
|
||||
|
||||
## P5 — Source invalidation fail-closed
|
||||
|
||||
**Verdict: PASS**
|
||||
|
||||
### Command
|
||||
|
||||
```bash
|
||||
python3 docs/compaction-refresh/probes/pi_gate0_run.py
|
||||
```
|
||||
|
||||
Full output: [`evidence/raw/P2-P3-P5-P6-pi.txt`](./evidence/raw/P2-P3-P5-P6-pi.txt).
|
||||
|
||||
Each fault was injected into the manifest/source read by the real Pi `context` hook. Each run reached an actual model-produced `toolCallId`, then the runtime gate refused it:
|
||||
|
||||
| Fault | Runtime validation | Injection/promotion | Broker | Tool result |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| Missing path | `reason=missing` | `injectionDecision=REFUSED`, `promotion=false` | `source_invalidation_revoke` | `allowed=false`, `unverified-source:missing` |
|
||||
| 65 bytes with 64-byte max | `reason=oversize` | `REFUSED`, `promotion=false` | revoked | `allowed=false`, `unverified-source:oversize` |
|
||||
| Bytes differ from pinned SHA-256 | `reason=hash-mismatch` | `REFUSED`, `promotion=false` | revoked | `allowed=false`, `unverified-source:hash-mismatch` |
|
||||
|
||||
Missing example:
|
||||
|
||||
```json
|
||||
{"event":"context_return","sourceValidation":{"ok":false,"reason":"missing"},"injectionDecision":"REFUSED","promotion":false,"sourceBroker":{"event":"source_invalidation_revoke","new_lease_state":"REVOKED"}}
|
||||
{"event":"tool_call","mapping":{"verified":false,"sourceReason":"missing"},"allowed":false,"reason":"unverified-source:missing"}
|
||||
```
|
||||
|
||||
No fault case reached tool execution or promotion.
|
||||
|
||||
---
|
||||
|
||||
## P6 — Atomic Claude `additionalContext` + Pi `context` injection (A-v5-1 / T27)
|
||||
|
||||
**Verdict: T-C GAP — returns to planner**
|
||||
|
||||
### Commands
|
||||
|
||||
```bash
|
||||
python3 docs/compaction-refresh/probes/pi_gate0_run.py
|
||||
python3 docs/compaction-refresh/probes/p6_claude_run.py
|
||||
rg -n -i "atomic|prefix-preserv" <installed Pi and Claude hook docs>
|
||||
```
|
||||
|
||||
Full outputs:
|
||||
|
||||
- [`evidence/raw/P2-P3-P5-P6-pi.txt`](./evidence/raw/P2-P3-P5-P6-pi.txt)
|
||||
- [`evidence/raw/P6-claude-additional-context.txt`](./evidence/raw/P6-claude-additional-context.txt)
|
||||
- [`evidence/raw/P6-contract-gap.txt`](./evidence/raw/P6-contract-gap.txt)
|
||||
|
||||
### Positive empirical observations
|
||||
|
||||
**Pi:** The real `context` hook returned exactly one additional `AgentMessage`; the prior message prefix hash was unchanged. The real final provider payload contained exactly one occurrence in one content item, and the real model copied all bytes exactly:
|
||||
|
||||
```json
|
||||
{"event":"context_return","inputCount":1,"outputCount":2,"injectionDecision":"ONE_ATOMIC_AGENT_MESSAGE","prefixPreservedByReturn":true,"blockLength":108,"blockSha256":"99c3...a0dd"}
|
||||
{"event":"before_provider_request","markerOccurrences":1,"markerPaths":["$.input[1].content[0].text"],"finalPayloadValid":true}
|
||||
{"event":"message_end","exactContextBlockCopied":true,"assistantTextSha256":"99c3...a0dd"}
|
||||
```
|
||||
|
||||
**Claude:** The real `SessionStart` hook emitted one `hookSpecificOutput.additionalContext` string. Claude’s stream recorded successful hook execution, and the real model’s exact copied block matched byte length and SHA-256:
|
||||
|
||||
```text
|
||||
block_length=116
|
||||
block_sha256=ef6377d63552af075f4f4adec00165988418c5f46a992f4dce8e678b56fd34ac
|
||||
assistant_copy_length=116
|
||||
assistant_copy_sha256=ef6377d63552af075f4f4adec00165988418c5f46a992f4dce8e678b56fd34ac
|
||||
assistant_copy_exact=True
|
||||
```
|
||||
|
||||
### Why this is not a PASS
|
||||
|
||||
The installed Pi documentation says only that `context` receives a deep copy and may return `{ messages }`. The installed Claude documentation says only that `additionalContext` enters/adds to context/system prompt. The exact search result was:
|
||||
|
||||
```text
|
||||
NO MATCH: neither installed runtime document states an atomic/prefix-preserving transport guarantee.
|
||||
```
|
||||
|
||||
One or several successful complete deliveries cannot prove the transport invariant needed by A-v5-1. In particular, a harness-side middle deletion/replacement that preserves the terminal receipt is not detectable by the receipt. That is precisely R1’s assurance boundary. Therefore:
|
||||
|
||||
- absent or prefix-truncated terminal token: receipt-detectable;
|
||||
- middle-drop preserving the tail token: **not receipt-detectable**;
|
||||
- no documented runtime contract excludes that transform;
|
||||
- classification: **T-C contract gap**.
|
||||
|
||||
No atomicity claim is inferred from empirical success.
|
||||
|
||||
---
|
||||
|
||||
## Independent probe review
|
||||
|
||||
After an initial review identified a session-global P2 correlation flaw, the probe was changed to queue request-scoped cycles from `before_provider_request` through assistant `message_end`; all runtime probes were re-run and raw checksums regenerated. The final independent review command was:
|
||||
|
||||
```bash
|
||||
~/.config/mosaic/tools/codex/codex-code-review.sh \
|
||||
-b d801d6c4c8a984d6a95033c49714210018d3d9a8 \
|
||||
-o /tmp/827-gate0-rereview.json
|
||||
```
|
||||
|
||||
Final review: **APPROVE**, confidence 0.91, 18 files reviewed, 0 blockers, 0 should-fix findings, 0 suggestions.
|
||||
|
||||
## Final admission decision
|
||||
|
||||
Gate0 requires every item to produce positive runtime evidence. P6 does not. **Do not admit WI-1..WI-7. Return A-v5-1/T27 to planner review.**
|
||||
|
||||
No feature work, push, PR, merge, or issue closure was performed.
|
||||
@@ -1,8 +0,0 @@
|
||||
d19ed51612b52d8f5f4957321776e05157008d048b693217c03d71318dc4c763 docs/compaction-refresh/evidence/raw/P1-claude-hook-events.txt
|
||||
c2d7bc21200063a4a0e61c67ba91abaf958ee88aa686e86f3f71c2717732b413 docs/compaction-refresh/evidence/raw/P1-launch-ancestry.txt
|
||||
6efb12d908e9e20badcfda5b070aa1873409bd5a05f533f0b08bb1b4ef53d1a7 docs/compaction-refresh/evidence/raw/P2-P3-P5-P6-pi.txt
|
||||
a9df6cc9f5d45f60d7d914ad1f80b9601574b82831101b3a10eccf1b93787e94 docs/compaction-refresh/evidence/raw/P2-provider-timing.txt
|
||||
92e7aa7d69d53e58a151f9d56cfb583d90c206ecc0bc8a1b185e172c598fb177 docs/compaction-refresh/evidence/raw/P4-so-peercred.txt
|
||||
047d235c6b6553158e27378c4ace081b094e5746734f4b5db6a6dc8ef9e05ff2 docs/compaction-refresh/evidence/raw/P6-claude-additional-context.txt
|
||||
7df20b2878fc87aa4d1fc89121e494d8f4f7bf313b89e1e3147f16fdaa567cdd docs/compaction-refresh/evidence/raw/P6-contract-gap.txt
|
||||
405bf3a06bf355d7f4f4d7b29d45a1ae70d93a690af5f7d0fc4819249e9f408f docs/compaction-refresh/evidence/raw/STEP0-authority-hashes.txt
|
||||
@@ -1,28 +0,0 @@
|
||||
$ python3 docs/compaction-refresh/probes/p1_run.py --runtime claude
|
||||
=== P1 CLAUDE REAL LAUNCH ===
|
||||
$ python3 docs/compaction-refresh/probes/p1_anchor_exec.py --socket <protected-socket> claude <runtime args>
|
||||
registered_anchor={"argc": 17, "argv0": "/usr/bin/python3", "comm": "python3", "exe": "/usr/bin/python3.11", "pid": 3933580, "ppid": 3933578, "starttime_ticks": 365788075}
|
||||
broker_minted_session_id=ebe9f9146ad1ba5b9fd757fe9517d24b
|
||||
hook_or_extension_record={"ancestry": [{"argc": 2, "argv0": "python3", "comm": "python3", "exe": "/usr/bin/python3.11", "pid": 3933983, "ppid": 3933982, "starttime_ticks": 365788568}, {"argc": 3, "argv0": "/bin/sh", "comm": "sh", "exe": "/usr/bin/dash", "pid": 3933982, "ppid": 3933751, "starttime_ticks": 365788568}, {"argc": 16, "argv0": "claude", "comm": "claude", "exe": "/home/hermes/.local/share/claude/versions/2.1.205", "pid": 3933751, "ppid": 3933580, "starttime_ticks": 365788474}, {"argc": 16, "argv0": "node", "comm": "node", "exe": "/usr/bin/node", "pid": 3933580, "ppid": 3933578, "starttime_ticks": 365788075}], "anchor": {"argc": 17, "argv0": "/usr/bin/python3", "comm": "python3", "exe": "/usr/bin/python3.11", "pid": 3933580, "ppid": 3933578, "starttime_ticks": 365788075}, "claimed_session_id": null, "decision": "ACCEPT", "event": "resolve-hook", "peercred": {"gid": 1001, "pid": 3933983, "uid": 1001}, "reason": "ancestry-reaches-registered-anchor", "resolved_session_id": "ebe9f9146ad1ba5b9fd757fe9517d24b", "starttimes_rechecked": true}
|
||||
sibling_attack_record={"ancestry": [{"argc": 6, "argv0": "/usr/bin/python3", "comm": "python3", "exe": "/usr/bin/python3.11", "pid": 3933581, "ppid": 3933578, "starttime_ticks": 365788080}, {"argc": 4, "argv0": "python3", "comm": "python3", "exe": "/usr/bin/python3.11", "pid": 3933578, "ppid": 3933576, "starttime_ticks": 365788059}, {"argc": 3, "argv0": "/bin/bash", "comm": "bash", "exe": "/usr/bin/bash", "pid": 3933576, "ppid": 3933575, "starttime_ticks": 365788058}, {"argc": 3, "argv0": "/bin/bash", "comm": "bash", "exe": "/usr/bin/bash", "pid": 3933575, "ppid": 3888118, "starttime_ticks": 365788058}, {"argc": 1, "argv0": "pi", "comm": "pi", "exe": "/usr/bin/node", "pid": 3888118, "ppid": 3887912, "starttime_ticks": 365707392}, {"argc": 6, "argv0": "node", "comm": "node", "exe": "/usr/bin/node", "pid": 3887912, "ppid": 3887869, "starttime_ticks": 365707050}, {"argc": 1, "argv0": "-bash", "comm": "bash", "exe": "/usr/bin/bash", "pid": 3887869, "ppid": 1244054, "starttime_ticks": 365706948}, {"argc": 10, "argv0": "tmux", "comm": "tmux: server", "exe": "/usr/bin/tmux", "pid": 1244054, "ppid": 745, "starttime_ticks": 114078803}, {"argc": 2, "argv0": "/lib/systemd/systemd", "comm": "systemd", "exe": "/usr/lib/systemd/systemd", "pid": 745, "ppid": 1, "starttime_ticks": 627}], "anchor": {"argc": 17, "argv0": "/usr/bin/python3", "comm": "python3", "exe": "/usr/bin/python3.11", "pid": 3933580, "ppid": 3933578, "starttime_ticks": 365788075}, "claimed_session_id": "ebe9f9146ad1ba5b9fd757fe9517d24b", "decision": "REJECT", "event": "claim-session", "peercred": {"gid": 1001, "pid": 3933581, "uid": 1001}, "reason": "victim-id-known-but-ancestry-mismatch", "resolved_session_id": null, "starttimes_rechecked": false}
|
||||
sibling_process_stdout={"attacker_pid": 3933581, "attacker_uid": 1001, "broker_decision": "REJECT", "broker_reason": "victim-id-known-but-ancestry-mismatch", "victim_session_id_known": true}
|
||||
sibling_process_exit=0
|
||||
ps_snapshot=<hook chain exited; broker /proc snapshot above is authoritative>
|
||||
launcher_stderr_excerpt:
|
||||
{"argv": ["mosaic", "yolo", "claude", "<12 runtime args>"], "event": "anchor-exec", "note": "os.execvpe retains pid and /proc starttime", "pid": 3933580}
|
||||
runtime_stdout_excerpt:
|
||||
|
||||
[mosaic] Claude Code settings audit:
|
||||
⚠ Missing PreToolUse hook: prevent-memory-write.sh
|
||||
⚠ Missing PostToolUse hook: qa-hook-stdin.sh
|
||||
⚠ Missing PostToolUse hook: typecheck-hook.sh
|
||||
⚠ Missing plugin: feature-dev
|
||||
⚠ Missing plugin: pr-review-toolkit
|
||||
⚠ Missing plugin: code-review
|
||||
runtime_hook_event_excerpt:
|
||||
⚠ Missing PreToolUse hook: prevent-memory-write.sh
|
||||
⚠ Missing PostToolUse hook: qa-hook-stdin.sh
|
||||
⚠ Missing PostToolUse hook: typecheck-hook.sh
|
||||
{"type":"system","subtype":"hook_started","hook_id":"cadd5ded-a869-4b05-85fc-cfd1a4988217","hook_name":"SessionStart:startup","hook_event":"SessionStart","uuid":"b63d67bf-2247-4e1c-b16b-7ccffa73180b","session_id":"97e1224c-7c1c-42c7-9fb5-598d2cd3dfaf"}
|
||||
{"type":"system","subtype":"hook_response","hook_id":"cadd5ded-a869-4b05-85fc-cfd1a4988217","hook_name":"SessionStart:startup","hook_event":"SessionStart","output":"{\"hookSpecificOutput\": {\"hookEventName\": \"SessionStart\", \"additionalContext\": \"GATE0_P1_SUPPORTED_HOOK_ANCESTRY_ACCEPTED\"}}\n","stdout":"{\"hookSpecificOutput\": {\"hookEventName\": \"SessionStart\", \"additionalContext\": \"GATE0_P1_SUPPORTED_HOOK_ANCESTRY_ACCEPTED\"}}\n","stderr":"","exit_code":0,"outcome":"success","uuid":"b2bb583d-d287-4b56-8061-09153a32adc2","session_id":"97e1224c-7c1c-42c7-9fb5-598d2cd3dfaf"}
|
||||
|
||||
@@ -1,62 +0,0 @@
|
||||
$ python3 docs/compaction-refresh/probes/p1_run.py --runtime both
|
||||
=== P1 PI REAL LAUNCH ===
|
||||
machine_assertions=PASS
|
||||
$ python3 docs/compaction-refresh/probes/p1_anchor_exec.py --socket <protected-socket> pi <runtime args>
|
||||
registered_anchor={"argc": 13, "argv0": "/usr/bin/python3", "comm": "python3", "exe": "/usr/bin/python3.11", "pid": 4010843, "ppid": 4010840, "starttime_ticks": 365919858}
|
||||
broker_minted_session_id=5207bd0d8251b616fe4df4c68f438830
|
||||
hook_or_extension_record={"ancestry": [{"argc": 1, "argv0": "pi", "comm": "pi", "exe": "/usr/bin/node", "pid": 4011046, "ppid": 4010843, "starttime_ticks": 365920219}, {"argc": 12, "argv0": "node", "comm": "node", "exe": "/usr/bin/node", "pid": 4010843, "ppid": 4010840, "starttime_ticks": 365919858}], "anchor": {"argc": 13, "argv0": "/usr/bin/python3", "comm": "python3", "exe": "/usr/bin/python3.11", "pid": 4010843, "ppid": 4010840, "starttime_ticks": 365919858}, "claimed_session_id": null, "decision": "ACCEPT", "event": "resolve-hook", "peercred": {"gid": 1001, "pid": 4011046, "uid": 1001}, "reason": "ancestry-reaches-registered-anchor", "resolved_session_id": "5207bd0d8251b616fe4df4c68f438830", "starttimes_rechecked": true}
|
||||
sibling_attack_record={"ancestry": [{"argc": 6, "argv0": "/usr/bin/python3", "comm": "python3", "exe": "/usr/bin/python3.11", "pid": 4010844, "ppid": 4010840, "starttime_ticks": 365919864}, {"argc": 4, "argv0": "python3", "comm": "python3", "exe": "/usr/bin/python3.11", "pid": 4010840, "ppid": 4010838, "starttime_ticks": 365919843}, {"argc": 3, "argv0": "/bin/bash", "comm": "bash", "exe": "/usr/bin/bash", "pid": 4010838, "ppid": 4010837, "starttime_ticks": 365919843}, {"argc": 3, "argv0": "/bin/bash", "comm": "bash", "exe": "/usr/bin/bash", "pid": 4010837, "ppid": 3888118, "starttime_ticks": 365919842}, {"argc": 1, "argv0": "pi", "comm": "pi", "exe": "/usr/bin/node", "pid": 3888118, "ppid": 3887912, "starttime_ticks": 365707392}, {"argc": 6, "argv0": "node", "comm": "node", "exe": "/usr/bin/node", "pid": 3887912, "ppid": 3887869, "starttime_ticks": 365707050}, {"argc": 1, "argv0": "-bash", "comm": "bash", "exe": "/usr/bin/bash", "pid": 3887869, "ppid": 1244054, "starttime_ticks": 365706948}, {"argc": 10, "argv0": "tmux", "comm": "tmux: server", "exe": "/usr/bin/tmux", "pid": 1244054, "ppid": 745, "starttime_ticks": 114078803}, {"argc": 2, "argv0": "/lib/systemd/systemd", "comm": "systemd", "exe": "/usr/lib/systemd/systemd", "pid": 745, "ppid": 1, "starttime_ticks": 627}], "anchor": {"argc": 13, "argv0": "/usr/bin/python3", "comm": "python3", "exe": "/usr/bin/python3.11", "pid": 4010843, "ppid": 4010840, "starttime_ticks": 365919858}, "claimed_session_id": "5207bd0d8251b616fe4df4c68f438830", "decision": "REJECT", "event": "claim-session", "peercred": {"gid": 1001, "pid": 4010844, "uid": 1001}, "reason": "victim-id-known-but-ancestry-mismatch", "resolved_session_id": null, "starttimes_rechecked": false}
|
||||
sibling_process_stdout={"attacker_pid": 4010844, "attacker_uid": 1001, "broker_decision": "REJECT", "broker_reason": "victim-id-known-but-ancestry-mismatch", "victim_session_id_known": true}
|
||||
sibling_process_exit=0
|
||||
$ ps -o pid=,ppid=,lstart=,uid=,gid=,comm= -p 4011046,4010843
|
||||
4010843 4010840 Fri Jul 17 19:35:18 2026 1001 1001 node
|
||||
4011046 4010843 Fri Jul 17 19:35:22 2026 1001 1001 pi
|
||||
launcher_stderr_excerpt:
|
||||
{"argv": ["mosaic", "yolo", "pi", "<8 runtime args>"], "event": "anchor-exec", "note": "os.execvpe retains pid and /proc starttime", "pid": 4010843}
|
||||
runtime_stdout_excerpt:
|
||||
[mosaic] Launching Pi in YOLO mode...
|
||||
{"type":"extension_ui_request","id":"4cf086c7-299e-4734-9264-6ad2964f3664","method":"notify","message":"Mosaic framework loaded","notifyType":"info"}
|
||||
{"id":"state","type":"response","command":"get_state","success":true,"data":{"model":{"id":"gpt-5.6-sol","name":"GPT-5.6 Sol","api":"openai-codex-responses","provider":"openai-codex","baseUrl":"https://chatgpt.com/backend-api","compat":{"supportsToolSearch":true},"reasoning":true,"thinkingLevelMap":{"xhigh":"xhigh","max":"max","minimal":"low"},"input":["text","image"],"cost":{"input":5,"output":30,"cacheRead":0.5,"cacheWrite":6.25,"tiers":[{"inputTokensAbove":272000,"input":10,"output":45,"cache
|
||||
runtime_hook_event_excerpt:
|
||||
|
||||
=== P1 CLAUDE REAL LAUNCH ===
|
||||
machine_assertions=PASS
|
||||
$ python3 docs/compaction-refresh/probes/p1_anchor_exec.py --socket <protected-socket> claude <runtime args>
|
||||
registered_anchor={"argc": 17, "argv0": "/usr/bin/python3", "comm": "python3", "exe": "/usr/bin/python3.11", "pid": 4011129, "ppid": 4010840, "starttime_ticks": 365920380}
|
||||
broker_minted_session_id=f382fa5f4b2142ef79bb76204521ff2a
|
||||
hook_or_extension_record={"ancestry": [{"argc": 2, "argv0": "python3", "comm": "python3", "exe": "/usr/bin/python3.11", "pid": 4011380, "ppid": 4011379, "starttime_ticks": 365920845}, {"argc": 3, "argv0": "/bin/sh", "comm": "sh", "exe": "/usr/bin/dash", "pid": 4011379, "ppid": 4011285, "starttime_ticks": 365920845}, {"argc": 16, "argv0": "claude", "comm": "claude", "exe": "/home/hermes/.local/share/claude/versions/2.1.205", "pid": 4011285, "ppid": 4011129, "starttime_ticks": 365920748}, {"argc": 16, "argv0": "node", "comm": "node", "exe": "/usr/bin/node", "pid": 4011129, "ppid": 4010840, "starttime_ticks": 365920380}], "anchor": {"argc": 17, "argv0": "/usr/bin/python3", "comm": "python3", "exe": "/usr/bin/python3.11", "pid": 4011129, "ppid": 4010840, "starttime_ticks": 365920380}, "claimed_session_id": null, "decision": "ACCEPT", "event": "resolve-hook", "peercred": {"gid": 1001, "pid": 4011380, "uid": 1001}, "reason": "ancestry-reaches-registered-anchor", "resolved_session_id": "f382fa5f4b2142ef79bb76204521ff2a", "starttimes_rechecked": true}
|
||||
sibling_attack_record={"ancestry": [{"argc": 6, "argv0": "/usr/bin/python3", "comm": "python3", "exe": "/usr/bin/python3.11", "pid": 4011130, "ppid": 4010840, "starttime_ticks": 365920385}, {"argc": 4, "argv0": "python3", "comm": "python3", "exe": "/usr/bin/python3.11", "pid": 4010840, "ppid": 4010838, "starttime_ticks": 365919843}, {"argc": 3, "argv0": "/bin/bash", "comm": "bash", "exe": "/usr/bin/bash", "pid": 4010838, "ppid": 4010837, "starttime_ticks": 365919843}, {"argc": 3, "argv0": "/bin/bash", "comm": "bash", "exe": "/usr/bin/bash", "pid": 4010837, "ppid": 3888118, "starttime_ticks": 365919842}, {"argc": 1, "argv0": "pi", "comm": "pi", "exe": "/usr/bin/node", "pid": 3888118, "ppid": 3887912, "starttime_ticks": 365707392}, {"argc": 6, "argv0": "node", "comm": "node", "exe": "/usr/bin/node", "pid": 3887912, "ppid": 3887869, "starttime_ticks": 365707050}, {"argc": 1, "argv0": "-bash", "comm": "bash", "exe": "/usr/bin/bash", "pid": 3887869, "ppid": 1244054, "starttime_ticks": 365706948}, {"argc": 10, "argv0": "tmux", "comm": "tmux: server", "exe": "/usr/bin/tmux", "pid": 1244054, "ppid": 745, "starttime_ticks": 114078803}, {"argc": 2, "argv0": "/lib/systemd/systemd", "comm": "systemd", "exe": "/usr/lib/systemd/systemd", "pid": 745, "ppid": 1, "starttime_ticks": 627}], "anchor": {"argc": 17, "argv0": "/usr/bin/python3", "comm": "python3", "exe": "/usr/bin/python3.11", "pid": 4011129, "ppid": 4010840, "starttime_ticks": 365920380}, "claimed_session_id": "f382fa5f4b2142ef79bb76204521ff2a", "decision": "REJECT", "event": "claim-session", "peercred": {"gid": 1001, "pid": 4011130, "uid": 1001}, "reason": "victim-id-known-but-ancestry-mismatch", "resolved_session_id": null, "starttimes_rechecked": false}
|
||||
sibling_process_stdout={"attacker_pid": 4011130, "attacker_uid": 1001, "broker_decision": "REJECT", "broker_reason": "victim-id-known-but-ancestry-mismatch", "victim_session_id_known": true}
|
||||
sibling_process_exit=0
|
||||
ps_snapshot=<hook chain exited; broker /proc snapshot above is authoritative>
|
||||
launcher_stderr_excerpt:
|
||||
{"argv": ["mosaic", "yolo", "claude", "<12 runtime args>"], "event": "anchor-exec", "note": "os.execvpe retains pid and /proc starttime", "pid": 4011129}
|
||||
runtime_stdout_excerpt:
|
||||
|
||||
[mosaic] Claude Code settings audit:
|
||||
⚠ Missing PreToolUse hook: prevent-memory-write.sh
|
||||
⚠ Missing PostToolUse hook: qa-hook-stdin.sh
|
||||
⚠ Missing PostToolUse hook: typecheck-hook.sh
|
||||
⚠ Missing plugin: feature-dev
|
||||
⚠ Missing plugin: pr-review-toolkit
|
||||
⚠ Missing plugin: code-review
|
||||
runtime_hook_event_excerpt:
|
||||
⚠ Missing PreToolUse hook: prevent-memory-write.sh
|
||||
⚠ Missing PostToolUse hook: qa-hook-stdin.sh
|
||||
⚠ Missing PostToolUse hook: typecheck-hook.sh
|
||||
{"type":"system","subtype":"hook_started","hook_id":"2a5f7dab-a064-4610-a6b1-4ad151ddcdd9","hook_name":"SessionStart:startup","hook_event":"SessionStart","uuid":"c6e0690c-f0c9-4d60-a8fb-5f0c25ea3208","session_id":"167d104d-907a-4120-9b07-bdf4762818a9"}
|
||||
{"type":"system","subtype":"hook_response","hook_id":"2a5f7dab-a064-4610-a6b1-4ad151ddcdd9","hook_name":"SessionStart:startup","hook_event":"SessionStart","output":"{\"hookSpecificOutput\": {\"hookEventName\": \"SessionStart\", \"additionalContext\": \"GATE0_P1_SUPPORTED_HOOK_ANCESTRY_ACCEPTED\"}}\n","stdout":"{\"hookSpecificOutput\": {\"hookEventName\": \"SessionStart\", \"additionalContext\": \"GATE0_P1_SUPPORTED_HOOK_ANCESTRY_ACCEPTED\"}}\n","stderr":"","exit_code":0,"outcome":"success","uuid":"167b2a5b-a45a-4e70-a72b-1f4a609bb979","session_id":"167d104d-907a-4120-9b07-bdf4762818a9"}
|
||||
|
||||
|
||||
$ readlink -f "$(command -v mosaic)"
|
||||
/home/hermes/.npm-global/lib/node_modules/@mosaicstack/mosaic/dist/cli.js
|
||||
|
||||
$ rg -n "spawnSync|execRuntime" ~/.npm-global/lib/node_modules/@mosaicstack/mosaic/dist/commands/launch.js | tail -8
|
||||
63: spawnSync(initBin, [], { stdio: 'inherit' });
|
||||
131: const result = spawnSync(checker, ['--check', '--runtime', runtime], { stdio: 'ignore' });
|
||||
624: execRuntime('claude', cliArgs);
|
||||
637: execRuntime('codex', cliArgs);
|
||||
643: execRuntime('opencode', args);
|
||||
658: execRuntime('pi', cliArgs);
|
||||
665:function execRuntime(cmd, args) {
|
||||
668: const result = spawnSync(cmd, args, {
|
||||
@@ -1,52 +0,0 @@
|
||||
$ python3 docs/compaction-refresh/probes/pi_gate0_run.py
|
||||
machine_assertions=PASS
|
||||
runtime_versions:
|
||||
0.80.7
|
||||
0.0.48
|
||||
|
||||
P2_EVENT_ORDER_AND_NONCE_MAP:
|
||||
{"assistantContentObserved": true, "assistantTextSha256": "a36f1eb364f062cad2f9f7d7e2b62ef7715d2aef79caafcfccd3a227cecf3e61", "event": "message_end", "exactContextBlockCopied": false, "inFlightDepthAfter": 0, "nonceMappings": [{"requestNonce": "e5a82358-a6c9-490b-a0de-2e1f1d9b8d79", "toolCallId": "call_bgGEFnBJOmwJPEfmzMo1eHOy|fc_0fb3d12b5404a73c016a5ac9d6f9a4819b9ddf70296c0cf57c"}], "pid": 4004545, "requestNonce": "e5a82358-a6c9-490b-a0de-2e1f1d9b8d79", "role": "assistant", "seq": 5, "starttime_ticks": 365907677, "toolCallIds": ["call_bgGEFnBJOmwJPEfmzMo1eHOy|fc_0fb3d12b5404a73c016a5ac9d6f9a4819b9ddf70296c0cf57c"]}
|
||||
{"allowed": true, "event": "tool_call", "mapping": {"nonce": "e5a82358-a6c9-490b-a0de-2e1f1d9b8d79", "sourceReason": "all-fragments-valid", "verified": true}, "pid": 4004545, "reason": "exact-tool-call-id-mapped-to-verified-request-nonce", "seq": 6, "starttime_ticks": 365907677, "toolCallId": "call_bgGEFnBJOmwJPEfmzMo1eHOy|fc_0fb3d12b5404a73c016a5ac9d6f9a4819b9ddf70296c0cf57c", "toolName": "gate0_nonce_probe"}
|
||||
{"broker": {"event": "probe_lease_promoted", "generation": 1, "new_lease_state": "VERIFIED", "peercred": {"gid": 1001, "pid": 4004545, "uid": 1001}, "starttime_ticks": 365907677}, "event": "tool_execute", "label": "p2", "pid": 4004545, "seq": 7, "starttime_ticks": 365907677, "toolCallId": "call_bgGEFnBJOmwJPEfmzMo1eHOy|fc_0fb3d12b5404a73c016a5ac9d6f9a4819b9ddf70296c0cf57c"}
|
||||
{"assistantContentObserved": true, "assistantTextSha256": "99c3dce194b16405dfb555f126ee5ccc014fdc184d0882aee1a903cbc700a0dd", "event": "message_end", "exactContextBlockCopied": true, "inFlightDepthAfter": 0, "nonceMappings": [], "pid": 4004545, "requestNonce": "cca4b1e3-296a-4e4c-9805-a395c270c01f", "role": "assistant", "seq": 11, "starttime_ticks": 365907677, "toolCallIds": []}
|
||||
|
||||
P2_LAST_OR_CLOSED:
|
||||
{"broker": {"event": "runtime_generation_bump", "new_generation": 1, "new_lease_state": "UNVERIFIED", "old_generation": 0, "peercred": {"gid": 1001, "pid": 4004545, "uid": 1001}, "phase": "start", "prior_lease": "NONE", "prior_lease_revoked": true, "reason": "startup", "starttime_ticks": 365907677}, "event": "session_start", "extensions": ["/home/hermes/.config/mosaic/runtime/pi/mosaic-extension.ts", "/home/hermes/agent-work/stack-cr-wi0-gate0/docs/compaction-refresh/probes/pi_gate0_extension.ts"], "gateState": "UNVERIFIED_READY", "lastPosition": true, "pid": 4004545, "reason": "startup", "self": "/home/hermes/agent-work/stack-cr-wi0-gate0/docs/compaction-refresh/probes/pi_gate0_extension.ts", "seq": 1, "starttime_ticks": 365907677}
|
||||
{"broker": {"skipped": true}, "event": "session_start", "extensions": ["/home/hermes/.config/mosaic/runtime/pi/mosaic-extension.ts", "/home/hermes/agent-work/stack-cr-wi0-gate0/docs/compaction-refresh/probes/pi_gate0_extension.ts", "/home/hermes/agent-work/stack-cr-wi0-gate0/docs/compaction-refresh/probes/pi_later_extension.ts"], "gateState": "CLOSED_NOT_LAST", "lastPosition": false, "pid": 4005692, "reason": "startup", "self": "/home/hermes/agent-work/stack-cr-wi0-gate0/docs/compaction-refresh/probes/pi_gate0_extension.ts", "seq": 1, "starttime_ticks": 365910545}
|
||||
|
||||
P3_GENERATION_BROKER:
|
||||
{"event": "runtime_generation_bump", "new_generation": 1, "new_lease_state": "UNVERIFIED", "old_generation": 0, "peercred": {"gid": 1001, "pid": 4004545, "uid": 1001}, "phase": "start", "prior_lease": "NONE", "prior_lease_revoked": true, "reason": "startup", "starttime_ticks": 365907677}
|
||||
{"event": "probe_lease_promoted", "generation": 1, "new_lease_state": "VERIFIED", "peercred": {"gid": 1001, "pid": 4004545, "uid": 1001}, "starttime_ticks": 365907677}
|
||||
{"event": "runtime_generation_bump", "new_generation": 2, "new_lease_state": "REVOKED", "old_generation": 1, "peercred": {"gid": 1001, "pid": 4004545, "uid": 1001}, "phase": "shutdown", "prior_lease": "VERIFIED", "prior_lease_revoked": true, "reason": "reload", "starttime_ticks": 365907677}
|
||||
{"event": "runtime_generation_bump", "new_generation": 3, "new_lease_state": "UNVERIFIED", "old_generation": 2, "peercred": {"gid": 1001, "pid": 4004545, "uid": 1001}, "phase": "start", "prior_lease": "REVOKED", "prior_lease_revoked": true, "reason": "reload", "starttime_ticks": 365907677}
|
||||
{"event": "runtime_generation_bump", "new_generation": 4, "new_lease_state": "REVOKED", "old_generation": 3, "peercred": {"gid": 1001, "pid": 4004545, "uid": 1001}, "phase": "shutdown", "prior_lease": "UNVERIFIED", "prior_lease_revoked": true, "reason": "fork", "starttime_ticks": 365907677}
|
||||
{"event": "runtime_generation_bump", "new_generation": 5, "new_lease_state": "UNVERIFIED", "old_generation": 4, "peercred": {"gid": 1001, "pid": 4004545, "uid": 1001}, "phase": "start", "prior_lease": "REVOKED", "prior_lease_revoked": true, "reason": "fork", "starttime_ticks": 365907677}
|
||||
{"event": "runtime_generation_bump", "new_generation": 6, "new_lease_state": "UNVERIFIED", "old_generation": 5, "peercred": {"gid": 1001, "pid": 4004545, "uid": 1001}, "phase": "start", "prior_lease": "UNVERIFIED", "prior_lease_revoked": true, "reason": "fork", "starttime_ticks": 365907677}
|
||||
{"event": "runtime_generation_bump", "new_generation": 7, "new_lease_state": "REVOKED", "old_generation": 6, "peercred": {"gid": 1001, "pid": 4004545, "uid": 1001}, "phase": "shutdown", "prior_lease": "UNVERIFIED", "prior_lease_revoked": true, "reason": "new", "starttime_ticks": 365907677}
|
||||
{"event": "runtime_generation_bump", "new_generation": 8, "new_lease_state": "UNVERIFIED", "old_generation": 7, "peercred": {"gid": 1001, "pid": 4004545, "uid": 1001}, "phase": "start", "prior_lease": "REVOKED", "prior_lease_revoked": true, "reason": "new", "starttime_ticks": 365907677}
|
||||
{"event": "runtime_generation_bump", "new_generation": 9, "new_lease_state": "UNVERIFIED", "old_generation": 8, "peercred": {"gid": 1001, "pid": 4004545, "uid": 1001}, "phase": "start", "prior_lease": "UNVERIFIED", "prior_lease_revoked": true, "reason": "new", "starttime_ticks": 365907677}
|
||||
{"event": "runtime_generation_bump", "new_generation": 10, "new_lease_state": "REVOKED", "old_generation": 9, "peercred": {"gid": 1001, "pid": 4004545, "uid": 1001}, "phase": "shutdown", "prior_lease": "UNVERIFIED", "prior_lease_revoked": true, "reason": "resume", "starttime_ticks": 365907677}
|
||||
{"event": "runtime_generation_bump", "new_generation": 11, "new_lease_state": "UNVERIFIED", "old_generation": 10, "peercred": {"gid": 1001, "pid": 4004545, "uid": 1001}, "phase": "start", "prior_lease": "REVOKED", "prior_lease_revoked": true, "reason": "resume", "starttime_ticks": 365907677}
|
||||
{"event": "runtime_generation_bump", "new_generation": 12, "new_lease_state": "UNVERIFIED", "old_generation": 11, "peercred": {"gid": 1001, "pid": 4004545, "uid": 1001}, "phase": "start", "prior_lease": "UNVERIFIED", "prior_lease_revoked": true, "reason": "resume", "starttime_ticks": 365907677}
|
||||
|
||||
P5_SOURCE_INVALIDATION:
|
||||
{"blockLength": 108, "blockSha256": "99c3dce194b16405dfb555f126ee5ccc014fdc184d0882aee1a903cbc700a0dd", "event": "context_return", "injectionDecision": "REFUSED", "inputCount": 5, "lastPosition": true, "outputCount": 5, "pid": 4004545, "prefixHashAfter": "4f339e3e45989486374b75d8a40abad22a3f3091f1099e3b4112f3afd1c60eb0", "prefixHashBefore": "4f339e3e45989486374b75d8a40abad22a3f3091f1099e3b4112f3afd1c60eb0", "prefixPreservedByReturn": true, "promotion": false, "requestNonce": "24ef5352-bcc6-4418-b65f-c2763453cc46", "seq": 12, "sourceBroker": {"event": "source_invalidation_revoke", "generation": 12, "new_lease_state": "REVOKED", "peercred": {"gid": 1001, "pid": 4004545, "uid": 1001}, "prior_lease": "UNVERIFIED", "promotion": false, "source_reason": "missing", "starttime_ticks": 365907677}, "sourceValidation": {"fragment": "/tmp/gate0-pi-g_3gsk34/absent-fragment.md", "ok": false, "reason": "missing"}, "starttime_ticks": 365907677}
|
||||
{"allowed": false, "event": "tool_call", "mapping": {"nonce": "24ef5352-bcc6-4418-b65f-c2763453cc46", "sourceReason": "missing", "verified": false}, "pid": 4004545, "reason": "unverified-source:missing", "seq": 15, "starttime_ticks": 365907677, "toolCallId": "call_XBwBkiv5tZx2ayuDxHKD5vSB|fc_0fb3d12b5404a73c016a5ac9dc341c819bb6ed3e00ca2cf1a5", "toolName": "gate0_nonce_probe"}
|
||||
{"blockLength": 108, "blockSha256": "99c3dce194b16405dfb555f126ee5ccc014fdc184d0882aee1a903cbc700a0dd", "event": "context_return", "injectionDecision": "REFUSED", "inputCount": 9, "lastPosition": true, "outputCount": 9, "pid": 4004545, "prefixHashAfter": "1a39911018caefe8f5b5acb652cece9f92d937e7384109f5c1559266349480b7", "prefixHashBefore": "1a39911018caefe8f5b5acb652cece9f92d937e7384109f5c1559266349480b7", "prefixPreservedByReturn": true, "promotion": false, "requestNonce": "319646e8-59e2-4021-9b27-de1376b13c32", "seq": 22, "sourceBroker": {"event": "source_invalidation_revoke", "generation": 12, "new_lease_state": "REVOKED", "peercred": {"gid": 1001, "pid": 4004545, "uid": 1001}, "prior_lease": "REVOKED", "promotion": false, "source_reason": "oversize", "starttime_ticks": 365907677}, "sourceValidation": {"fragment": "/tmp/gate0-pi-g_3gsk34/oversize.md", "ok": false, "reason": "oversize"}, "starttime_ticks": 365907677}
|
||||
{"allowed": false, "event": "tool_call", "mapping": {"nonce": "319646e8-59e2-4021-9b27-de1376b13c32", "sourceReason": "oversize", "verified": false}, "pid": 4004545, "reason": "unverified-source:oversize", "seq": 25, "starttime_ticks": 365907677, "toolCallId": "call_P9d0wR5TSXSqZHydVHclh6Cg|fc_0fb3d12b5404a73c016a5ac9e02a28819bb39df373b7c9e23b", "toolName": "gate0_nonce_probe"}
|
||||
{"blockLength": 108, "blockSha256": "99c3dce194b16405dfb555f126ee5ccc014fdc184d0882aee1a903cbc700a0dd", "event": "context_return", "injectionDecision": "REFUSED", "inputCount": 13, "lastPosition": true, "outputCount": 13, "pid": 4004545, "prefixHashAfter": "224c8777dd0cd5fcf1ae02f0fc46198548b48647dbfd044ed131533d72086f16", "prefixHashBefore": "224c8777dd0cd5fcf1ae02f0fc46198548b48647dbfd044ed131533d72086f16", "prefixPreservedByReturn": true, "promotion": false, "requestNonce": "23f3125f-6e62-4f3c-aa60-3eaed705ddc1", "seq": 32, "sourceBroker": {"event": "source_invalidation_revoke", "generation": 12, "new_lease_state": "REVOKED", "peercred": {"gid": 1001, "pid": 4004545, "uid": 1001}, "prior_lease": "REVOKED", "promotion": false, "source_reason": "hash-mismatch", "starttime_ticks": 365907677}, "sourceValidation": {"fragment": "/tmp/gate0-pi-g_3gsk34/mismatch.md", "ok": false, "reason": "hash-mismatch"}, "starttime_ticks": 365907677}
|
||||
{"allowed": false, "event": "tool_call", "mapping": {"nonce": "23f3125f-6e62-4f3c-aa60-3eaed705ddc1", "sourceReason": "hash-mismatch", "verified": false}, "pid": 4004545, "reason": "unverified-source:hash-mismatch", "seq": 35, "starttime_ticks": 365907677, "toolCallId": "call_QUMvqBRnzv6HNqEd37jU5NUw|fc_0fb3d12b5404a73c016a5ac9e37510819b8506879faf287aa3", "toolName": "gate0_nonce_probe"}
|
||||
{"event": "source_invalidation_revoke", "generation": 12, "new_lease_state": "REVOKED", "peercred": {"gid": 1001, "pid": 4004545, "uid": 1001}, "prior_lease": "UNVERIFIED", "promotion": false, "source_reason": "missing", "starttime_ticks": 365907677}
|
||||
{"event": "source_invalidation_revoke", "generation": 12, "new_lease_state": "REVOKED", "peercred": {"gid": 1001, "pid": 4004545, "uid": 1001}, "prior_lease": "REVOKED", "promotion": false, "source_reason": "oversize", "starttime_ticks": 365907677}
|
||||
{"event": "source_invalidation_revoke", "generation": 12, "new_lease_state": "REVOKED", "peercred": {"gid": 1001, "pid": 4004545, "uid": 1001}, "prior_lease": "REVOKED", "promotion": false, "source_reason": "hash-mismatch", "starttime_ticks": 365907677}
|
||||
|
||||
P6_PI_CONTEXT_ATOMIC_OBSERVATION:
|
||||
{"blockLength": 108, "blockSha256": "99c3dce194b16405dfb555f126ee5ccc014fdc184d0882aee1a903cbc700a0dd", "event": "context_return", "injectionDecision": "ONE_ATOMIC_AGENT_MESSAGE", "inputCount": 1, "lastPosition": true, "outputCount": 2, "pid": 4004545, "prefixHashAfter": "095a5415879b0d4006d1485dba3398fee6bf39850711ba0dc2e9cfa312e865dc", "prefixHashBefore": "095a5415879b0d4006d1485dba3398fee6bf39850711ba0dc2e9cfa312e865dc", "prefixPreservedByReturn": true, "promotion": false, "requestNonce": "e5a82358-a6c9-490b-a0de-2e1f1d9b8d79", "seq": 3, "sourceBroker": {"action": "none", "reason": "source-valid"}, "sourceValidation": {"ok": true, "reason": "all-fragments-valid"}, "starttime_ticks": 365907677}
|
||||
{"event": "before_provider_request", "finalPayloadValid": true, "inFlightDepth": 1, "markerOccurrences": 1, "markerPaths": ["$.input[1].content[0].text"], "pid": 4004545, "requestNonce": "e5a82358-a6c9-490b-a0de-2e1f1d9b8d79", "seq": 4, "starttime_ticks": 365907677}
|
||||
{"blockLength": 108, "blockSha256": "99c3dce194b16405dfb555f126ee5ccc014fdc184d0882aee1a903cbc700a0dd", "event": "context_return", "injectionDecision": "ONE_ATOMIC_AGENT_MESSAGE", "inputCount": 3, "lastPosition": true, "outputCount": 4, "pid": 4004545, "prefixHashAfter": "7c40cce3664af7581b21ea007a4a44764237fce1e4f16b031e96d60df2229855", "prefixHashBefore": "7c40cce3664af7581b21ea007a4a44764237fce1e4f16b031e96d60df2229855", "prefixPreservedByReturn": true, "promotion": false, "requestNonce": "cca4b1e3-296a-4e4c-9805-a395c270c01f", "seq": 9, "sourceBroker": {"action": "none", "reason": "source-valid"}, "sourceValidation": {"ok": true, "reason": "all-fragments-valid"}, "starttime_ticks": 365907677}
|
||||
{"event": "before_provider_request", "finalPayloadValid": true, "inFlightDepth": 1, "markerOccurrences": 1, "markerPaths": ["$.input[5].content[0].text"], "pid": 4004545, "requestNonce": "cca4b1e3-296a-4e4c-9805-a395c270c01f", "seq": 10, "starttime_ticks": 365907677}
|
||||
{"assistantContentObserved": true, "assistantTextSha256": "99c3dce194b16405dfb555f126ee5ccc014fdc184d0882aee1a903cbc700a0dd", "event": "message_end", "exactContextBlockCopied": true, "inFlightDepthAfter": 0, "nonceMappings": [], "pid": 4004545, "requestNonce": "cca4b1e3-296a-4e4c-9805-a395c270c01f", "role": "assistant", "seq": 11, "starttime_ticks": 365907677, "toolCallIds": []}
|
||||
|
||||
RPC_EVENT_COUNTS:
|
||||
{"agent_end": 4, "agent_settled": 4, "agent_start": 4, "extension_ui_request": 8, "message_end": 16, "message_start": 16, "message_update": 105, "response": 9, "tool_execution_end": 4, "tool_execution_start": 4, "turn_end": 8, "turn_start": 8}
|
||||
stderr_nonempty=False
|
||||
@@ -1,9 +0,0 @@
|
||||
$ python3 docs/compaction-refresh/probes/p2_provider_timing_run.py
|
||||
local_http_endpoint=http://127.0.0.1:42823/v1/chat/completions
|
||||
{"event": "before_provider_request", "finalPayloadValid": true, "inFlightDepth": 1, "markerOccurrences": 1, "markerPaths": ["$.messages[2].content[0].text"], "pid": 4008778, "requestNonce": "056b82c4-36eb-420b-94cf-b2c73813ef79", "seq": 4, "starttime_ticks": 365916346}
|
||||
{"assistantContentAvailableAtThisHook": false, "event": "after_provider_response", "pid": 4008778, "requestNonce": "056b82c4-36eb-420b-94cf-b2c73813ef79", "seq": 5, "starttime_ticks": 365916346, "status": 200, "timing": "headers/status before stream consumption"}
|
||||
{"assistantContentObserved": true, "assistantTextSha256": "fb4ebaab26d63661040dc15925a99e22dc07ee2b33df5c6b2ca93a5b34f08b1d", "event": "message_end", "exactContextBlockCopied": false, "inFlightDepthAfter": 0, "nonceMappings": [], "pid": 4008778, "requestNonce": "056b82c4-36eb-420b-94cf-b2c73813ef79", "role": "assistant", "seq": 6, "starttime_ticks": 365916346, "toolCallIds": []}
|
||||
machine_assertions=PASS
|
||||
after_provider_response_seq=5
|
||||
message_end_seq=6
|
||||
headers_hook_precedes_completed_message=True
|
||||
@@ -1,23 +0,0 @@
|
||||
$ python3 docs/compaction-refresh/probes/p4_peercred_probe.py
|
||||
machine_assertions=PASS
|
||||
server_pid=4013762 server_uid=1001 server_gid=1001
|
||||
socket_path=/tmp/gate0-p4-nl1_8ap2/broker.sock
|
||||
directory_mode=0700 socket_mode=0600
|
||||
SO_PEERCRED pid=4013768 uid=1001 gid=1001
|
||||
client_claim={"exe": "/usr/bin/python3.11", "pid": 4013768, "ppid": 4013762, "starttime_ticks": 365927069, "uid": 1001}
|
||||
proc_observed={"exe": "/usr/bin/python3.11", "pid": 4013768, "ppid": 4013762, "starttime_ticks": 365927069, "uid": 1001}
|
||||
pid_match=True
|
||||
uid_match=True
|
||||
starttime_match=True
|
||||
client_exit_status=0
|
||||
same_principal_socket=true
|
||||
posture=0700 parent + 0600 socket excludes other UIDs, but does not prevent the same UID from unlinking/rebinding; distinct-principal system service remains required for a claim stronger than T-C against same-UID counterfeit replacement
|
||||
|
||||
$ id
|
||||
uid=1001(hermes) gid=1001(hermes) groups=1001(hermes),40(src),100(users),996(docker)
|
||||
|
||||
$ uname -srmo
|
||||
Linux 6.1.0-48-amd64 x86_64 GNU/Linux
|
||||
|
||||
$ getconf CLK_TCK
|
||||
100
|
||||
@@ -1,21 +0,0 @@
|
||||
$ python3 docs/compaction-refresh/probes/p6_claude_run.py
|
||||
machine_assertions=PASS
|
||||
command=mosaic yolo claude --settings <isolated> --model haiku --print --output-format stream-json --verbose --include-hook-events <prompt>
|
||||
claude_version=2.1.205 (Claude Code)
|
||||
mosaic_version=0.0.48
|
||||
exit_code=0
|
||||
hook_process_log={"block_length": 116, "block_sha256": "ef6377d63552af075f4f4adec00165988418c5f46a992f4dce8e678b56fd34ac", "emission": "one hookSpecificOutput.additionalContext string field", "hook_event_name": "SessionStart", "pid": 4015703, "ppid": 4015701, "starttime_ticks": 365930489}
|
||||
hook_stream_event={"hook_event": "SessionStart", "hook_id": "557d613e-574f-4523-8bfb-8c6e51946035", "hook_name": "SessionStart:startup", "session_id": "f821d0db-1177-4237-8ff5-83b2a46996a6", "subtype": "hook_started", "type": "system", "uuid": "59449134-e2d4-43a9-9d34-b59e74622c08"}
|
||||
hook_stream_event={"exit_code": 0, "hook_event": "SessionStart", "hook_id": "557d613e-574f-4523-8bfb-8c6e51946035", "hook_name": "SessionStart:startup", "outcome": "success", "output": "{\"hookSpecificOutput\": {\"hookEventName\": \"SessionStart\", \"additionalContext\": \"GATE0_CLAUDE_ATOMIC_BEGIN\\nsegment-01=alpha-2d11\\nsegment-02=middle-8e22\\nsegment-03=omega-4f33\\nGATE0_CLAUDE_ATOMIC_END\"}}\n", "session_id": "f821d0db-1177-4237-8ff5-83b2a46996a6", "stderr": "", "stdout": "{\"hookSpecificOutput\": {\"hookEventName\": \"SessionStart\", \"additionalContext\": \"GATE0_CLAUDE_ATOMIC_BEGIN\\nsegment-01=alpha-2d11\\nsegment-02=middle-8e22\\nsegment-03=omega-4f33\\nGATE0_CLAUDE_ATOMIC_END\"}}\n", "subtype": "hook_response", "type": "system", "uuid": "e05842c1-813a-41dd-93c9-768eb834f260"}
|
||||
block_length=116
|
||||
block_sha256=ef6377d63552af075f4f4adec00165988418c5f46a992f4dce8e678b56fd34ac
|
||||
stream_fields_containing_full_block=3
|
||||
stream_fields_exactly_equal_block=2
|
||||
assistant_copy_length=449
|
||||
assistant_copy_sha256=a65febbb3ad8fa4952894d94406a83a520ef712c0ecb9ab43be3212094b21ba1
|
||||
assistant_copy_exact=False
|
||||
assistant_copy="The user is asking me to return the exact GATE0_CLAUDE_ATOMIC block that was injected by SessionStart. This block was provided in the system-reminder at the beginning of the conversation:\n\n```\nGATE0_CLAUDE_ATOMIC_BEGIN\nsegment-01=alpha-2d11\nsegment-02=middle-8e22\nsegment-03=omega-4f33\nGATE0_CLAUDE_ATOMIC_END\n```\n\nThe user wants me to return ONLY this exact block, with no code fence or commentary. So I should just output it exactly as it appears."
|
||||
assistant_copy_length=116
|
||||
assistant_copy_sha256=ef6377d63552af075f4f4adec00165988418c5f46a992f4dce8e678b56fd34ac
|
||||
assistant_copy_exact=True
|
||||
assistant_copy="GATE0_CLAUDE_ATOMIC_BEGIN\nsegment-01=alpha-2d11\nsegment-02=middle-8e22\nsegment-03=omega-4f33\nGATE0_CLAUDE_ATOMIC_END"
|
||||
@@ -1,47 +0,0 @@
|
||||
$ rg -n -i "atomic|prefix-preserv" <Pi extensions docs> <Claude hook docs>
|
||||
NO MATCH: neither installed runtime document states an atomic/prefix-preserving transport guarantee.
|
||||
|
||||
$ rg -n -C 3 "#### context|event.messages - deep copy|return \{ messages" <Pi extensions docs>
|
||||
638-});
|
||||
639-```
|
||||
640-
|
||||
641:#### context
|
||||
642-
|
||||
643-Fired before each LLM call. Modify messages non-destructively. See [Session Format](session-format.md) for message types.
|
||||
644-
|
||||
645-```typescript
|
||||
646-pi.on("context", async (event, ctx) => {
|
||||
647: // event.messages - deep copy, safe to modify
|
||||
648- const filtered = event.messages.filter(m => !shouldPrune(m));
|
||||
649: return { messages: filtered };
|
||||
650-});
|
||||
651-```
|
||||
652-
|
||||
|
||||
$ rg -n -C 3 "additionalContext|add to the default system prompt" <Claude installed docs>
|
||||
/home/hermes/.config/mosaic/runtime/claude/RUNTIME.md-58- tiered models via the Task `model` param).
|
||||
/home/hermes/.config/mosaic/runtime/claude/RUNTIME.md-59-
|
||||
/home/hermes/.config/mosaic/runtime/claude/RUNTIME.md-60-Note: PostToolUse hook plain stdout on exit 0 goes to the debug log, not model context — only
|
||||
/home/hermes/.config/mosaic/runtime/claude/RUNTIME.md:61:`hookSpecificOutput.additionalContext` (or exit-2 stderr) enters context.
|
||||
--
|
||||
/home/hermes/.claude/plugins/marketplaces/claude-plugins-official/plugins/explanatory-output-style/README.md-59-expressed as
|
||||
/home/hermes/.claude/plugins/marketplaces/claude-plugins-official/plugins/explanatory-output-style/README.md-60-[subagents](https://docs.claude.com/en/docs/claude-code/sub-agents), not as
|
||||
/home/hermes/.claude/plugins/marketplaces/claude-plugins-official/plugins/explanatory-output-style/README.md-61-SessionStart hooks. Subagents change the system prompt while SessionStart hooks
|
||||
/home/hermes/.claude/plugins/marketplaces/claude-plugins-official/plugins/explanatory-output-style/README.md:62:add to the default system prompt.
|
||||
/home/hermes/.claude/plugins/marketplaces/claude-plugins-official/plugins/explanatory-output-style/README.md-63-
|
||||
/home/hermes/.claude/plugins/marketplaces/claude-plugins-official/plugins/explanatory-output-style/README.md-64-## Managing changes
|
||||
/home/hermes/.claude/plugins/marketplaces/claude-plugins-official/plugins/explanatory-output-style/README.md-65-
|
||||
--
|
||||
/home/hermes/.claude/plugins/marketplaces/claude-plugins-official/plugins/explanatory-output-style/hooks-handlers/session-start.sh-1-#!/usr/bin/env bash
|
||||
/home/hermes/.claude/plugins/marketplaces/claude-plugins-official/plugins/explanatory-output-style/hooks-handlers/session-start.sh-2-
|
||||
/home/hermes/.claude/plugins/marketplaces/claude-plugins-official/plugins/explanatory-output-style/hooks-handlers/session-start.sh:3:# Output the explanatory mode instructions as additionalContext
|
||||
/home/hermes/.claude/plugins/marketplaces/claude-plugins-official/plugins/explanatory-output-style/hooks-handlers/session-start.sh-4-# This mimics the deprecated Explanatory output style
|
||||
/home/hermes/.claude/plugins/marketplaces/claude-plugins-official/plugins/explanatory-output-style/hooks-handlers/session-start.sh-5-
|
||||
/home/hermes/.claude/plugins/marketplaces/claude-plugins-official/plugins/explanatory-output-style/hooks-handlers/session-start.sh-6-cat << 'EOF'
|
||||
/home/hermes/.claude/plugins/marketplaces/claude-plugins-official/plugins/explanatory-output-style/hooks-handlers/session-start.sh-7-{
|
||||
/home/hermes/.claude/plugins/marketplaces/claude-plugins-official/plugins/explanatory-output-style/hooks-handlers/session-start.sh-8- "hookSpecificOutput": {
|
||||
/home/hermes/.claude/plugins/marketplaces/claude-plugins-official/plugins/explanatory-output-style/hooks-handlers/session-start.sh-9- "hookEventName": "SessionStart",
|
||||
/home/hermes/.claude/plugins/marketplaces/claude-plugins-official/plugins/explanatory-output-style/hooks-handlers/session-start.sh:10: "additionalContext": "You are in 'explanatory' output style mode, where you should provide educational insights about the codebase as you help with the user's task.\n\nYou should be clear and educational, providing helpful explanations while remaining focused on the task. Balance educational content with task completion. When providing insights, you may exceed typical length constraints, but remain focused and relevant.\n\n## Insights\nIn order to encourage learning, before and after writing code, always provide brief educational explanations about implementation choices using (with backticks):\n\"`★ Insight ─────────────────────────────────────`\n[2-3 key educational points]\n`─────────────────────────────────────────────────`\"\n\nThese insights should be included in the conversation, not in the codebase. You should generally focus on interesting insights that are specific to the codebase or the code you just wrote, rather than general programming concepts. Do not wait until the end to provide insights. Provide them as you write code."
|
||||
/home/hermes/.claude/plugins/marketplaces/claude-plugins-official/plugins/explanatory-output-style/hooks-handlers/session-start.sh-11- }
|
||||
/home/hermes/.claude/plugins/marketplaces/claude-plugins-official/plugins/explanatory-output-style/hooks-handlers/session-start.sh-12-}
|
||||
/home/hermes/.claude/plugins/marketplaces/claude-plugins-official/plugins/explanatory-output-style/hooks-handlers/session-start.sh-13-EOF
|
||||
@@ -1,9 +0,0 @@
|
||||
$ sha256sum ~/agent-work/reviews/compaction-refresh-BUILD-BRIEF.md ~/agent-work/reviews/compaction-refresh-SPEC-v5.md ~/agent-work/reviews/compaction-refresh-SPEC-RATIFICATION.md
|
||||
89fdbc27ed0e5050dc7b52f3ef2ddaea691edf17fd89d51b15e26fb5ed47171b /home/hermes/agent-work/reviews/compaction-refresh-BUILD-BRIEF.md
|
||||
a6d07ade835758e8488ca10d3b0631caf0beb93ea3a6733631f151b0c2f01433 /home/hermes/agent-work/reviews/compaction-refresh-SPEC-v5.md
|
||||
bac58319c9c4028b5b40e1129e0033cdb5a6b7b02033c25f06f4cb77d7779c67 /home/hermes/agent-work/reviews/compaction-refresh-SPEC-RATIFICATION.md
|
||||
|
||||
Expected:
|
||||
89fdbc27ed0e5050dc7b52f3ef2ddaea691edf17fd89d51b15e26fb5ed47171b BUILD-BRIEF
|
||||
a6d07ade835758e8488ca10d3b0631caf0beb93ea3a6733631f151b0c2f01433 SPEC-v5
|
||||
bac58319c9c4028b5b40e1129e0033cdb5a6b7b02033c25f06f4cb77d7779c67 RATIFICATION
|
||||
@@ -0,0 +1,109 @@
|
||||
# Gate0 Charter Amendment — Probe-3 (D4) Evidence-Class EXECUTION Authorization
|
||||
|
||||
**Status:** DRAFT (revision 2) — supersedes prior candidate pin `8fca5b7a`; pending Mos re-scope-verify BEFORE
|
||||
re-pin is ratified. Revised per Mos adjudication directive (2026-07-18): §3 = D4-focused Option A; §1 = explicit binding model.
|
||||
**Amendment type:** Additive durable-authority amendment (does NOT mutate any ratified pinned doc in place).
|
||||
**Milestone:** 188 — Compaction-Refresh Mechanism · **Issue:** Gitea #827 (WI-0 Gate0) · gates WI-3 #830 merge.
|
||||
|
||||
---
|
||||
|
||||
## 1. Authorized source (why this amendment is valid)
|
||||
|
||||
Per the Mosaic governance principle (OpenBrain `f0cb61b6`, permanent; fail-closed / DO-178C): a durable
|
||||
Gate0 execution-hold clears ONLY by **(i) explicit user (owner) authorization** OR **(ii) a durable-authority
|
||||
amendment from an authorized source**. A runtime coordinator's verbal GO does NOT qualify.
|
||||
|
||||
- **Owner authorization (i):** Jason (owner / north-star) directed in `#mos` **2026-07-18 17:28Z** — *"amend the
|
||||
charter"* (option **B** from the WI-3 escalation), quoting the escalation back. This is the direct-user
|
||||
authorization artifact that homelab's durable-Gate0 objection (2026-07-18 06:34:56Z) explicitly named as a
|
||||
valid resolution condition (*"user artifact OR auth amendment"*). Relayed + authenticity-adjudicated by Mos
|
||||
(`web1:mos-claude`). Owner outranks any peer-lane durable authority.
|
||||
- **Amendment (ii):** this document, authorized BY that owner directive.
|
||||
|
||||
Both qualifying conditions are therefore satisfied. Homelab's stated condition is met by the owner artifact;
|
||||
homelab is NOTIFIED as a transparency step (it holds durable-Gate0 standing and raised the original catch),
|
||||
but its co-sign is not a gate (owner > peer).
|
||||
|
||||
**Binding model (explicit — how this SHA is authorized).** The authority binding for this amendment's pinned
|
||||
bytes is the composition of two distinct acts:
|
||||
1. **Owner class-authorization (PRE-SHA):** Jason (owner) authorized the *class of action* — "amend the
|
||||
charter" (option B), `#mos` 2026-07-18 17:28Z. This occurred **before** these amendment bytes (and hence
|
||||
this SHA256) existed; the owner authorized the amendment, not a specific hash.
|
||||
2. **Independent-adjudicator byte-verification:** Mos (`web1:mos-claude`), acting as the independent
|
||||
adjudicator (author ≠ verifier — MS-LEAD authored, Mos verifies), scope-verifies the **exact bytes** of
|
||||
this document and confirms they express only the owner-authorized narrow scope.
|
||||
|
||||
There is therefore **NO single "Jason-signs-the-SHA" artifact, and none is required** under this binding
|
||||
model: owner-authorized-class + independent-adjudicator-byte-verify **is** the binding. This is the durable,
|
||||
inspectable chain of custody for the pin.
|
||||
|
||||
## 2. Charter provisions amended (sha-pinned, unmutated)
|
||||
|
||||
This amendment attaches to — and does NOT rewrite — the ratified Gate0 charter provisions:
|
||||
|
||||
- `compaction-refresh-BUILD-BRIEF.md` §5 / R4 "Gate0 = BUILD-ADMISSION GATE", specifically the item:
|
||||
*"Same-PID `runtime_generation` bump on reload/resume/fork revokes prior lease (D4)."*
|
||||
sha256 `89fdbc27ed0e5050dc7b52f3ef2ddaea691edf17fd89d51b15e26fb5ed47171b` (intact/unchanged).
|
||||
- `compaction-refresh-SPEC-RATIFICATION.md` R4.
|
||||
sha256 `bac58319c9c4028b5b40e1129e0033cdb5a6b7b02033c25f06f4cb77d7779c67` (intact/unchanged).
|
||||
|
||||
The two pinned docs remain byte-identical; this amendment is the delta of record.
|
||||
|
||||
## 3. What is authorized (NARROW — D4-focused harness ONLY)
|
||||
|
||||
Adjudicator ruling (Mos, `web1:mos-claude`): "D4-only" against the existing runner is non-executable — the
|
||||
only runner (`pi_gate0_run.py`) drives P2+P5+P6+D4 in one flow, and D4 intrinsically requires a
|
||||
promoted-to-VERIFIED baseline (spec L219: "P3 immediately follows the valid P2 promotion so reload must
|
||||
revoke a genuinely VERIFIED prior gen") to revoke a genuine prior generation. Therefore authorize EXACTLY the
|
||||
minimal executable form (Option A) required as WI-3 (#830) merge-gate (2):
|
||||
|
||||
- **D4-focused harness:** spawns **ONLY** `p3_generation_broker.py` (not the full `pi_gate0_run.py` runner).
|
||||
- **Minimal promotion-to-VERIFIED, as a FIXTURE PRECONDITION ONLY:** established solely so the D4 revoke has a
|
||||
genuine `VERIFIED` target. This is **NOT** authorization to bank P2 as an independent evidence class.
|
||||
- **D4 same-PID `runtime_generation`-bump revoke observation:** new generation → `MUTATOR_UNVERIFIED`;
|
||||
prior (superseded) generation → `STALE_GENERATION`.
|
||||
- **Explicitly EXCLUDED:** P5 (missing / oversize / hash-invalidation) and P6 (atomic-observation) — not
|
||||
needed for the WI-3 D4 gate and **not authorized** by this amendment.
|
||||
- **Isolation / integrity (per §4/§5):** 3× repeated, isolated runs; hardened deterministic harness;
|
||||
non-destructive; launches broker/socket/state artifacts in an isolated fixture ONLY; **does NOT touch
|
||||
production**, mutates no live lease/broker/deployment, creates no durable side effect outside the fixture;
|
||||
a **FAIL** returns to planner — no retry-launder.
|
||||
|
||||
**Intrinsic-precondition boundary (verbatim):** the promotion step is a D4 test fixture, not a P2
|
||||
evidence-gathering authorization; P5/P6 remain unauthorized.
|
||||
|
||||
## 4. What is NOT authorized (all other holds remain in force)
|
||||
|
||||
- This is **NOT** a blanket Gate0-execution release. Every other Gate0 execution item and every standing
|
||||
execution hold remains exactly as-is.
|
||||
- No live mutation, migration, canary, deployment, systemd, tmux-fleet, or connector/socket/Hermes action is
|
||||
authorized by this amendment.
|
||||
- Authorizes producing THIS evidence class once (3× isolation is the evidence-integrity requirement, not a
|
||||
license to re-run on failure): a **FAIL** returns to planner — **no retry-launder**, no severity-laundering.
|
||||
|
||||
## 5. Preserved fail-closed / DO-178C invariants (verbatim, unchanged)
|
||||
|
||||
- Producing probe evidence **EXECUTES** the Gate0 mechanism (launches processes, creates socket/state
|
||||
artifacts, exercises revocation transitions); "isolated + non-destructive" does **not** make it
|
||||
non-execution. This amendment authorizes that execution for the narrow class in §3 — it does not redefine
|
||||
execution as non-execution.
|
||||
- A sha-pinned DURABLE authority OUTRANKS a runtime coordinator's verbal GO; when they conflict the durable
|
||||
artifact wins. This amendment derives its authority from the OWNER, not from a coordinator.
|
||||
- Card-advancement gates and execution-holds are distinct; this amendment lifts ONLY the §3 execution-hold,
|
||||
not any advancement/review/SECREV gate. WI-3's independent-review + Opus-SECREV + green-suite gates stand.
|
||||
|
||||
## 6. Effect on WI-3 (#830) merge chain
|
||||
|
||||
On this amendment being pinned (Mos-verified + Jason-shown): a fresh lane (ms-rev-826) executes the §3
|
||||
probe-3 evidence class (3× isolation) → PASS/FAIL reported to Mos. **On PASS**, and only after re-confirming
|
||||
WI-3 head `f4008307` UNMOVED + FIRE-PRECONDITION-0 (fresh `mosaic-context-refresh` attestation for
|
||||
mosaic-100 + planner-opus), the existing chain runs: CI queue-guard → push → PR → mirror RoR → relay full-40
|
||||
head + pr/ci# → Mos independent 6-check → squash `closes #830` → chain WI-4→7. **On FAIL**: planner-return.
|
||||
|
||||
---
|
||||
|
||||
**Amendment author:** MS-LEAD (`web1:mosaic-100`), Gate0 governance interface / charter custodian.
|
||||
**Pin protocol:** this file's sha256 becomes the amendment pin ONLY after Mos re-scope-verify. Reported to Mos
|
||||
before any ratification/publication, per the transparency checkpoint on a durable-authority change. Ratification
|
||||
and durable publication (to a homelab-inspectable provider ref) are the adjudicator's (Mos), NOT the author's
|
||||
(MS-LEAD) — the author does not self-ratify and does not publish.
|
||||
49
docs/compaction-refresh/governance/OWNER-WINDOW.md
Normal file
49
docs/compaction-refresh/governance/OWNER-WINDOW.md
Normal file
@@ -0,0 +1,49 @@
|
||||
# Gate0 Probe-3 Amendment — Owner Transparency Window (durable record)
|
||||
|
||||
**Purpose.** Durable evidence (defined duration + closure) that the owner-transparency window on the
|
||||
probe-3 amendment **scope-correction** has a bounded, inspectable lifecycle — per homelab's GATE-A
|
||||
requirement (2026-07-18 18:13:14Z). This is NOT the owner's core authorization (that is durable in the
|
||||
amendment §1); it is the objection-window on the narrowing disclosed afterward.
|
||||
|
||||
## What was disclosed
|
||||
The owner (Jason) explicitly authorized the amendment **class** — "amend the charter" (option B),
|
||||
`#mos` 2026-07-18 17:28Z (amendment §1). My initial narrowing was "D4-only"; on adjudication that proved
|
||||
non-executable (D4 intrinsically needs a promoted-to-VERIFIED baseline). The **corrected** scope
|
||||
(rev 2, pin `9ac9ff87`, §3) adds a *minimal promotion-to-VERIFIED as a test fixture only* (P5/P6 still
|
||||
excluded, P2 not banked as evidence). Because this adds a fixture step beyond the "D4-only" I first told
|
||||
the owner, I disclosed the correction to him for transparency.
|
||||
|
||||
## Window (bounded, durable)
|
||||
- **Opened:** 2026-07-18T18:00:25Z — anchored to the adjudicator's Discord scope-correction disclosure to the
|
||||
owner (message id `1528099140100296736`, snowflake-decoded; not an informal estimate).
|
||||
- **Duration:** 2 hours (defined).
|
||||
- **Scheduled closure:** 2026-07-18T20:00:25Z.
|
||||
- **Closure condition (fail-closed):**
|
||||
- No owner objection received by scheduled closure → window **CLOSED-CLEAR** (the disclosed correction
|
||||
stands; it is within the already-granted class authorization).
|
||||
- Any owner objection before closure → window does **NOT** clear → re-adjudicate the corrected scope.
|
||||
- An affirmative owner "proceed" before closure → **early CLOSED-CLEAR** (recorded here).
|
||||
|
||||
## Closure attestation
|
||||
Closure will be confirmed by a dated adjudicator tick appended below (and to the orchestration ledger)
|
||||
at/after 2026-07-18T20:00:25Z. Until that attestation exists, GATE A is OPEN and probe-3 execution stays HELD.
|
||||
|
||||
- [x] **CLOSED-CLEAR attestation — recorded 2026-07-18T20:08Z by adjudicator Mos (`web1:mos-claude`).**
|
||||
|
||||
### CLOSED-CLEAR attestation (2026-07-18T20:08Z)
|
||||
|
||||
The owner-transparency window on the probe-3 amendment scope-correction is **CLOSED-CLEAR**.
|
||||
|
||||
- **Scheduled closure:** 2026-07-18T20:00:25Z — reached (authoritative UTC at attestation: 2026-07-18T20:07Z, past closure).
|
||||
- **Closure condition met:** *No owner objection received by scheduled closure.* Verified fail-closed against the
|
||||
**durable Discord #mos record** (not memory): the channel's last message is the scope-correction disclosure itself
|
||||
(`id 1528099140100296736`, 2026-07-18T18:00:25.709Z). From window-open (18:00:25Z) through closure (20:00:25Z) and
|
||||
to this attestation, there are **zero owner (jetrich) messages** — no objection, no affirmative early-GO. The last
|
||||
owner message preceding the window was the class authorization *"Amend the charter"* (17:28Z, amendment §1).
|
||||
- **Effect:** the disclosed corrected scope (rev 2, pin `9ac9ff87`, §3 — minimal promotion-to-VERIFIED as a D4 test
|
||||
fixture only; P5/P6 excluded; P2 not banked as evidence) **stands**, within the already-granted class authorization.
|
||||
- **GATE A is now CLOSED.** Probe-3 execution proceeds ONLY under the full remaining chain: GATE-B fresh §3-review PASS
|
||||
(concurred @839d156f, author≠reviewer) + adjudicator explicit FIRE GO. No other Gate0 hold is lifted by this tick.
|
||||
|
||||
*Adjudicator: Mos (`web1:mos-claude`). This record is durable and provider-inspectable alongside the
|
||||
amendment (`9ac9ff87`) and its RATIFICATION.md.*
|
||||
37
docs/compaction-refresh/governance/RATIFICATION.md
Normal file
37
docs/compaction-refresh/governance/RATIFICATION.md
Normal file
@@ -0,0 +1,37 @@
|
||||
# Gate0 Probe-3 (D4) Amendment — Adjudicator Ratification Record
|
||||
|
||||
**Ratifies pin:** `9ac9ff873fad41a6e15763cc89cb94d0bc4a6b0cf9b6770561d1781b03f63276`
|
||||
(`GATE0-PROBE3-EXEC-AMENDMENT.md`, 7699 bytes, this branch).
|
||||
**Supersedes:** prior candidate pin `8fca5b7a` (never executed against; withdrawn on scope + durability grounds).
|
||||
|
||||
## Re-scope-verify — PASSED
|
||||
Verified by **Mos (`web1:mos-claude`)**, acting as independent adjudicator (author ≠ verifier: MS-LEAD
|
||||
authored, Mos verified), 2026-07-18. SHA256 recomputed = pin (exact). Scope checks, all PASS:
|
||||
- §3 = Option A, D4-focused: harness spawns ONLY `p3_generation_broker.py`; minimal promotion-to-VERIFIED
|
||||
is a FIXTURE PRECONDITION only (not P2 evidence-banking); D4 gen-bump revoke observation
|
||||
(new→`MUTATOR_UNVERIFIED`, prior→`STALE_GENERATION`); **P5/P6 explicitly excluded + unauthorized**;
|
||||
verbatim intrinsic-precondition boundary line present.
|
||||
- §1 binding model explicit; §2 charter provisions pinned unmutated; §4/§5 invariants intact.
|
||||
- No scope creep beyond the owner-authorized narrow class.
|
||||
|
||||
## Authority binding
|
||||
- **Owner class-authorization (PRE-SHA):** Jason (owner) — "amend the charter" (option B), `#mos`
|
||||
2026-07-18 17:28Z. Authorized the amendment class before these bytes existed.
|
||||
- **Independent-adjudicator byte-verification:** Mos scope-verified the exact bytes.
|
||||
- No single "Jason-signs-the-SHA" artifact exists or is required under this binding model.
|
||||
|
||||
## Round-trip integrity (this branch/commit)
|
||||
All three files fetched back via the Gitea raw API and SHA-matched (each > 200B, no not-found sentinel):
|
||||
- `GATE0-PROBE3-EXEC-AMENDMENT.md` → `9ac9ff87…f63276` ✓
|
||||
- `charter-BUILD-BRIEF.md` → `89fdbc27…71b` ✓
|
||||
- `charter-SPEC-RATIFICATION.md` → `bac58319…67` ✓
|
||||
|
||||
## Status: RATIFIED — probe-3 execution remains HELD
|
||||
Publication here closes the governance-infra gap (the charter provisions were previously local-only,
|
||||
not inspectable). Probe-3 execution stays HELD pending both: (1) homelab independent verification of
|
||||
these published bytes; (2) owner transparency window. On both clearing → ms-rev-826 builds + runs the
|
||||
§3 D4-focused harness (3× isolation, non-destructive) → PASS/FAIL to Mos → WI-3 (#830) chain per §6.
|
||||
|
||||
*Note: the amendment file's own status line reads "DRAFT (revision 2)"; that internal text is
|
||||
superseded by THIS external ratification record (the pinned bytes are deliberately not edited, so the
|
||||
SHA stays stable). Ratification status lives here, not in the pinned artifact.*
|
||||
106
docs/compaction-refresh/governance/charter-BUILD-BRIEF.md
Normal file
106
docs/compaction-refresh/governance/charter-BUILD-BRIEF.md
Normal file
@@ -0,0 +1,106 @@
|
||||
# Compaction-Refresh Mechanism — BUILD BRIEF (Mos → MS-LEAD)
|
||||
|
||||
**Status:** STAGED — do NOT dispatch until PR #826 (#824 skill-CLI bridge) MERGES. Mos routes this to
|
||||
MS-LEAD at #824 land. PLAN phase COMPLETE + RATIFIED; this is the BUILD-execution brief.
|
||||
**Routed by:** Mos (main orchestrator). **Owner:** MS-LEAD (mosaic-100). **Runtime scope M1:** Claude + Pi.
|
||||
|
||||
## 1. AUTHORITY (sha256-pinned — build AGAINST these, do not re-derive)
|
||||
| Artifact | Path | SHA-256 |
|
||||
|---|---|---|
|
||||
| **Ratification record (SSOT)** | `~/agent-work/reviews/compaction-refresh-SPEC-RATIFICATION.md` | `bac58319c9c4028b5b40e1129e0033cdb5a6b7b02033c25f06f4cb77d7779c67` |
|
||||
| Ratified SPEC v5 | `~/agent-work/reviews/compaction-refresh-SPEC-v5.md` | `a6d07ade835758e8488ca10d3b0631caf0beb93ea3a6733631f151b0c2f01433` |
|
||||
| sol FINAL red-team (GO) | `~/agent-work/reviews/compaction-refresh-SPEC-v5-redteam-sol.md` | `3da326a4ea91767b731e128a93b13194e8002358101e30de3fcb8ca2f8f54faa` |
|
||||
|
||||
The coder MUST verify these hashes before building. The in-context working spec is DERIVED and
|
||||
compaction-fragile — the sha256'd records above are the recovery SSOT (durable-authority discipline).
|
||||
|
||||
## 2. TARGET & SCOPE
|
||||
- **Framework-native `packages/mosaic/…`** in `mosaicstack/stack`. NOT jarvis-brain. NOT
|
||||
`~/.config/mosaic/` directly (that tree is WIPED on framework upgrade — durable home is SOURCE).
|
||||
- **M1 = Claude + Pi orchestrators only.** Codex/other runtimes = scope-note only.
|
||||
- **Hard dependency (now satisfied at dispatch):** the `mosaic skill` register/list + install/upgrade
|
||||
symlink auto-sync (#824 / PR #826) — so the durable `mosaic-context-refresh` skill ships REACHABLE
|
||||
(auto-symlinked into `~/.claude/skills/`). Do not dispatch before #826 merges.
|
||||
|
||||
## 3. DELIVERABLES (locked architecture — from the ratification record; do NOT redesign)
|
||||
1. **Authenticated external lease broker** bound to kernel `SO_PEERCRED` `(pid, starttime,
|
||||
runtime_generation)` — the only unforgeable identity on a same-uid tmux fleet. Broker **MINTS** the
|
||||
logical `session_id` at first peercred contact (NEVER caller-asserted). Distinct-principal /
|
||||
protected socket.
|
||||
2. **Whole mutator-class gate** (not per-wrapper): no consequential mutator succeeds without a valid
|
||||
VERIFIED lease. **Revoke-first / promote-last.**
|
||||
3. **Compaction observers → revoke:** Claude `PreCompact` + `SessionStart(matcher=compact)`; Pi
|
||||
`session_before_compact`/`context` equivalents. Any `runtime_generation` bump (reload/resume/fork,
|
||||
same PID included) auto-revokes the prior incarnation.
|
||||
4. **Verbatim-hashed normative fragments:** `B_payload` (exact fragments + deterministic metadata) and
|
||||
`H_payload = SHA256(domain_sep ‖ length_framed(B_payload))` — NO self-reference (byte-identical
|
||||
across the Claude and Pi builders).
|
||||
5. **Receipt-challenge protocol (promotion prerequisite, NOT the safety mechanism):** broker mints a
|
||||
single-use `receipt_challenge` per cycle bound to `(session_id, runtime_generation,
|
||||
compaction_epoch, request_epoch, H_source, H_payload, schema_version)`. **COMPUTE = broker**
|
||||
(observes latest-assistant message, computes the binding incl `H(latest-assistant-message)`);
|
||||
**COPY = model** (verbatim copy of challenge + `H_payload` — an LLM cannot hash its own output;
|
||||
T29). Observed in the EXACT latest assistant message (Claude: latest assistant entry + pending
|
||||
challenge, NO transcript grep; Pi: `message_end`, NOT `after_provider_response`). Single-use,
|
||||
consumed before the tool batch. Broker transition order (mandatory): `revoke → build
|
||||
B_payload/H_payload → PENDING_DELIVERY(mint unique challenge) → deliver → observe exact receipt →
|
||||
evidence commit → consume challenge → promote VERIFIED (last)`.
|
||||
6. **Constrained recovery command** = the single ungated mutator; the `mosaic-context-refresh` skill
|
||||
is its wrapper (uses a newly-minted challenge + identical protocol; cannot replay normal-path
|
||||
receipt). Durable skill lands here, symlink-reachable via #824.
|
||||
7. **T-C server-side line = branch protection** (the irreducible guarantee; client gate is
|
||||
window-narrowing only). If ops-config rather than code, DOCUMENT the required posture explicitly.
|
||||
|
||||
## 4. BINDING BUILD CONDITIONS R1–R6 (MANDATORY — verbatim from ratification record)
|
||||
- **R1 (honesty):** state that the receipt detects ABSENT or PREFIX-TRUNCATED terminal token, but a
|
||||
MIDDLE-DROP preserving the tail is a **T-C contract violation, NOT receipt-detectable** (covered by
|
||||
server-side). No over-claim.
|
||||
- **R2 (atomicity):** evidence-commit → consume-challenge → promote-VERIFIED is ONE atomic broker
|
||||
transaction; a crash leaves neither VERIFIED-with-live-challenge nor consumed-with-ambiguous-promote;
|
||||
recovery mints a NEW challenge, never reuses.
|
||||
- **R3 (parsing):** exact single current-cycle receipt parse — no unbounded grep; reject
|
||||
quoted/tool-output receipts, wrong generation/epoch, unknown/stale challenge, multiple receipts.
|
||||
- **R4 (Gate0 = BUILD-ADMISSION GATE):** see §5 — runtime evidence BEFORE build admission; a failed
|
||||
Gate0 item RETURNS the design to planner review, it is NOT waived by the GO.
|
||||
- **R5 (TTL):** 300 s MAX lease TTL + soak-tighten; soak may only SHORTEN, never lengthen. Mos-accepted.
|
||||
- **R6 (receipt semantics):** receipt = T-A delivery/liveness ONLY, never obedience/safety/residency.
|
||||
Permanent invariant.
|
||||
|
||||
## 5. R4 GATE0 — RUNTIME EVIDENCE BEFORE BUILD ADMISSION (WI-0, do this FIRST)
|
||||
Structure the build like gitwatch: **WI-0 is a Gate0 runtime-evidence probe; no feature build admitted
|
||||
until every item produces POSITIVE runtime evidence** (not a design assertion). A failed item RETURNS
|
||||
to planner — do not paper over:
|
||||
- Launcher `exec`/parent topology + supported-hook ancestry (D1); broker authenticates the launcher
|
||||
chain, rejects sibling-substitution.
|
||||
- Pi lifecycle: last-position invariant (last-or-closed), per-tool nonce → tool-call-id map (D5).
|
||||
- Same-PID `runtime_generation` bump on reload/resume/fork revokes prior lease (D4).
|
||||
- Broker socket authenticity posture (protected / distinct-principal); `SO_PEERCRED` returns the true
|
||||
`(pid, starttime)`.
|
||||
- Source-invalidation fail-closed (missing/oversize/hash-mismatch fragment → no promotion).
|
||||
- Claude `additionalContext` + Pi `context` inject atomically (A-v5-1) — the transport assumption
|
||||
T27 rests on; verify or class the gap T-C.
|
||||
|
||||
## 6. ACCEPTANCE TESTS (red-first TDD)
|
||||
Carry all v4 ACs + T12b; add **T24–T30**: T24 hash-construction no-self-reference byte-identical across
|
||||
builders · T25 replay rejected · T26 transcript-stale-match cannot promote · T27 partial-delivery
|
||||
(prefix-trunc/tail-only/middle-drop/malformed) none promote · T28 single-use (no renew/reopen) · T29
|
||||
model copies not computes · T30 dual-hook-miss matches AMENDED threat table within+after TTL.
|
||||
Acceptance = full green suite + Gate0 evidence pack.
|
||||
|
||||
## 7. REVIEW & MERGE DISCIPLINE
|
||||
- **This IS an Opus-SECREV-mandatory surface** — UNLIKE #824 (local same-uid FS). The broker is an
|
||||
authentication/identity/authorization mechanism (peercred binding, session_id minting, mutator-class
|
||||
gating, receipt protocol). Security review = **Opus-SECREV, no GPT/terra substitute** on the
|
||||
broker/gate/receipt/socket surfaces. Functional/non-security parts may take GPT review.
|
||||
- **Author ≠ reviewer**, independent review, no self-merge, exact-head RoR (reviewed-SHA=merged-SHA),
|
||||
`closes #<issue>` close-keyword, full 40-char head. Never edit tests to pass / never force-merge red
|
||||
/ never `--no-verify`.
|
||||
- **Mos merges** (per ratification record) after Opus-SECREV + independent review pass + green suite +
|
||||
Gate0 pack. Decompose into work-items (WI-0 Gate0 first); likely a milestone, not one monolithic PR.
|
||||
|
||||
## 8. SEQUENCING
|
||||
#826 (#824 bridge) MERGES → Mos routes this brief → MS-LEAD decomposes (WI-0 Gate0 first) → build M1
|
||||
(Claude+Pi) → Gate0 evidence pack (failed item → planner) → Opus-SECREV + independent review → Mos
|
||||
merges → durable `mosaic-context-refresh` skill lands symlink-reachable → jarvis-brain `CLAUDE.md`
|
||||
mandate PR aligns to the shipped receipt/hash contract. Interim skill + CLAUDE.md mandate stay LIVE
|
||||
until the mechanism ships.
|
||||
@@ -0,0 +1,75 @@
|
||||
# Compaction-Refresh Mechanism — SPEC RATIFICATION RECORD
|
||||
|
||||
**Ratifier:** Mos (main fleet orchestrator, `mos-claude`)
|
||||
**Date:** 2026-07-17 ~22:2xZ
|
||||
**Decision:** ✅ **RATIFIED — GO WITH BINDING CONDITIONS R1–R6.** Architecture converged v1→v5 via
|
||||
oppositional adversarial planning (planner-opus authors / planner-sol red-teams). No v6 required.
|
||||
|
||||
## Authenticated authority artifacts (sha256-pinned)
|
||||
|
||||
| Artifact | Path | SHA-256 |
|
||||
|---|---|---|
|
||||
| Ratified SPEC v5 | `~/agent-work/reviews/compaction-refresh-SPEC-v5.md` | `a6d07ade835758e8488ca10d3b0631caf0beb93ea3a6733631f151b0c2f01433` |
|
||||
| sol FINAL red-team (GO) | `~/agent-work/reviews/compaction-refresh-SPEC-v5-redteam-sol.md` | `3da326a4ea91767b731e128a93b13194e8002358101e30de3fcb8ca2f8f54faa` |
|
||||
| (base) SPEC v4 | `~/agent-work/reviews/compaction-refresh-SPEC-v4.md` | `a5e9c261a613974fc0d853d69ad76f616f5f1c3b6b63a436daecdd15b4a72b80` |
|
||||
| (base) sol v4 red-team | `~/agent-work/reviews/compaction-refresh-SPEC-v4-redteam-sol.md` | `1e76ee5942241d9520b9ff8f39a628488578ec2d9b91cc09b9f0e7cc6091ef2f` |
|
||||
| Ratification-Input (R1-R6 + rulings, provenance-clean) | `~/agent-work/reviews/compaction-refresh-SPEC-RATIFICATION-INPUT.md` | `d01411a5c966f513e992a941deef0ff5cc6f29d37adab97ea2fbd3c5e6aef266` |
|
||||
|
||||
## Convergence trail
|
||||
v1 NO-GO (9 architectural) → v2 NO-GO (7 enforcement, arch accepted) → v3 NO-GO (5 surgical, arch
|
||||
locked) → v4 NO-GO (narrow; echo-lock narrowing APPROVED) → **v5 GO-with-conditions**.
|
||||
|
||||
## Locked architecture (do not redesign)
|
||||
Authenticated external **lease broker** bound to kernel `SO_PEERCRED` (pid, starttime,
|
||||
runtime_generation) — the only unforgeable identity fact on a same-uid tmux fleet. Broker MINTS the
|
||||
logical session_id at first peercred contact (never caller-asserted). **Revoke-first / promote-last.**
|
||||
Whole **mutator-class gate** (not per-wrapper). Verbatim-hashed normative fragments. Single
|
||||
**constrained recovery command** = the only ungated mutator; the `mosaic-context-refresh` skill is its
|
||||
wrapper. **Threat tiers:** T-A honest-stale → client gate · T-B compromised-tool → mutator-class · T-C
|
||||
fully-rotted → **server-side branch-protection = the irreducible line** (client gate is
|
||||
window-narrowing, NOT a complete guarantee).
|
||||
|
||||
**Delivery-receipt (echo-lock, sol §8-final):** broker mints a single-use `receipt_challenge` per
|
||||
cycle bound to (session, runtime_generation, epochs, H_source, H_payload); **COMPUTE = broker**
|
||||
(observes + computes the full binding incl H(latest-assistant-message) as the cycle-audit fact),
|
||||
**COPY = model** (copies challenge + H_payload verbatim — model never computes a hash; sol T29
|
||||
copy-not-compute preserved). Receipt is a **one-cycle T-A delivery/liveness proof ONLY — never an
|
||||
obedience, residency, or safety proof.** The mechanical mutator-class gate remains THE safety
|
||||
mechanism; §8 intact for T-B/T-C.
|
||||
|
||||
## Binding ratification conditions (R1–R6) — MANDATORY for build acceptance
|
||||
- **R1 (honesty / T27 amendment):** state honestly that the receipt detects an ABSENT or
|
||||
PREFIX-TRUNCATED terminal token, but a MIDDLE-DROP that preserves the tail token is a **T-C
|
||||
contract violation, NOT receipt-detectable** (covered by server-side, not the receipt). No
|
||||
over-claim. Fold as a doc amendment — no v6.
|
||||
- **R2 (atomicity):** evidence-commit → consume-challenge → promote-VERIFIED is ONE atomic broker
|
||||
transaction. A crash leaves NEITHER a VERIFIED-with-live-challenge NOR a
|
||||
consumed-with-ambiguous-promotion state; recovery mints a **NEW** challenge, never reuses one.
|
||||
- **R3 (parsing):** exact single current-cycle receipt parse — no unbounded grep; reject
|
||||
quoted/tool-output receipts, wrong generation/epoch, unknown/stale challenge, and multiple receipts.
|
||||
- **R4 (Gate0 runtime evidence — BUILD-ADMISSION GATE):** launcher-ancestry/D1,
|
||||
sibling-substitution rejection, Pi last-or-closed, same-PID runtime_generation bump,
|
||||
nonce→tool-call-id map, socket/authenticity posture, and source-invalidation fail-closed require
|
||||
**runtime Gate0 evidence BEFORE build admission**, not design assertion. **A failed Gate0 item
|
||||
RETURNS the design to planner review — it is NOT waived by this GO.**
|
||||
- **R5 (TTL — RATIFIER ACCEPTED):** proposed **300s MAX lease TTL + soak-tighten** is **ACCEPTED by
|
||||
Mos (ratifier)**. Safe-direction default: soak data may only SHORTEN it, never lengthen. Operator
|
||||
(Jason) may tighten at will; flagged for operator awareness, non-blocking (not an escalation
|
||||
trigger — a proposed max that only tightens).
|
||||
- **R6 (receipt semantics — REAFFIRMED):** receipt remains T-A delivery/liveness ONLY, never
|
||||
obedience/safety proof. Reaffirmed as a permanent invariant of the design.
|
||||
|
||||
## Build sequencing (PLAN-ONLY until these clear)
|
||||
1. **HARD DEP:** `mosaic skill` register/unregister/list + generic install/upgrade symlink auto-sync
|
||||
(mosaicstack/stack **#824**, in flight on ms-824) MUST land first — else the durable
|
||||
`mosaic-context-refresh` skill ships unreachable (no `~/.claude/skills/` bridge symlink).
|
||||
2. Then MS-LEAD builds the mechanism in **packages/mosaic** (M1 = Claude + Pi orchestrators) against
|
||||
THIS ratified spec + R1–R6, producing R4 Gate0 evidence. Author≠reviewer independent review; no
|
||||
self-merge; Mos merges.
|
||||
3. The durable `mosaic-context-refresh` skill (+ jarvis-brain CLAUDE.md mandate PR) lands aligned to
|
||||
the ratified receipt/hash contract, AFTER the #824 bridge exists.
|
||||
|
||||
**Interim protection (live now, until the mechanism ships):** `mosaic-context-refresh` skill
|
||||
(fail-closed 7-line residency attestation) + jarvis-brain CLAUDE.md "Directive Freshness After
|
||||
Compaction" mandate — self-run manual re-read + attestation. Symlink hand-created this session; loads
|
||||
and attests 7/7.
|
||||
2
docs/compaction-refresh/probes/.gitignore
vendored
2
docs/compaction-refresh/probes/.gitignore
vendored
@@ -1,2 +0,0 @@
|
||||
__pycache__/
|
||||
*.pyc
|
||||
@@ -1,51 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Register this PID as anchor, then exec the real `mosaic yolo` launcher."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import socket
|
||||
import sys
|
||||
|
||||
|
||||
def request(socket_path: str, payload: dict[str, object]) -> dict[str, object]:
|
||||
conn = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
||||
conn.connect(socket_path)
|
||||
conn.sendall((json.dumps(payload) + "\n").encode())
|
||||
response = json.loads(conn.makefile("r", encoding="utf-8").readline())
|
||||
conn.close()
|
||||
return response
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--socket", required=True)
|
||||
parser.add_argument("runtime", choices=["pi", "claude"])
|
||||
parser.add_argument("args", nargs=argparse.REMAINDER)
|
||||
ns = parser.parse_args()
|
||||
|
||||
response = request(ns.socket, {"action": "register-anchor", "runtime": ns.runtime})
|
||||
if response.get("decision") != "ACCEPT":
|
||||
raise SystemExit("anchor registration refused")
|
||||
os.environ["GATE0_SESSION_ID"] = str(response["session_id"])
|
||||
argv = ["mosaic", "yolo", ns.runtime, *ns.args]
|
||||
print(
|
||||
json.dumps(
|
||||
{
|
||||
"event": "anchor-exec",
|
||||
"pid": os.getpid(),
|
||||
"argv": ["mosaic", "yolo", ns.runtime, f"<{len(ns.args)} runtime args>"],
|
||||
"note": "os.execvpe retains pid and /proc starttime",
|
||||
},
|
||||
sort_keys=True,
|
||||
),
|
||||
file=sys.stderr,
|
||||
flush=True,
|
||||
)
|
||||
os.execvpe("mosaic", argv, os.environ)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,180 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Gate0 P1 broker prototype: peercred anchor minting and /proc ancestry checks."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import secrets
|
||||
import socket
|
||||
import stat
|
||||
import struct
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
|
||||
def proc_node(pid: int) -> dict[str, Any]:
|
||||
text = Path(f"/proc/{pid}/stat").read_text()
|
||||
close = text.rfind(")")
|
||||
comm = text[text.find("(") + 1 : close]
|
||||
fields = text[close + 2 :].split()
|
||||
cmdline = Path(f"/proc/{pid}/cmdline").read_bytes().split(b"\0")
|
||||
return {
|
||||
"pid": pid,
|
||||
"ppid": int(fields[1]),
|
||||
"starttime_ticks": int(fields[19]),
|
||||
"comm": comm,
|
||||
"exe": os.readlink(f"/proc/{pid}/exe"),
|
||||
"argv0": cmdline[0].decode(errors="replace") if cmdline and cmdline[0] else "",
|
||||
"argc": len([part for part in cmdline if part]),
|
||||
}
|
||||
|
||||
|
||||
def ancestry(peer_pid: int, anchor: dict[str, Any] | None) -> tuple[list[dict[str, Any]], bool, str]:
|
||||
chain: list[dict[str, Any]] = []
|
||||
pid = peer_pid
|
||||
seen: set[int] = set()
|
||||
try:
|
||||
while pid > 0 and pid not in seen:
|
||||
seen.add(pid)
|
||||
node = proc_node(pid)
|
||||
chain.append(node)
|
||||
if anchor and pid == anchor["pid"]:
|
||||
if node["starttime_ticks"] != anchor["starttime_ticks"]:
|
||||
return chain, False, "anchor-starttime-mismatch"
|
||||
break
|
||||
pid = node["ppid"]
|
||||
else:
|
||||
return chain, False, "anchor-not-reached"
|
||||
|
||||
if not anchor or chain[-1]["pid"] != anchor["pid"]:
|
||||
return chain, False, "anchor-not-reached"
|
||||
|
||||
# Re-read every node after the walk. A disappearing PID or changed
|
||||
# starttime invalidates the complete chain (PID-reuse/race closure).
|
||||
for original in chain:
|
||||
again = proc_node(original["pid"])
|
||||
if again["starttime_ticks"] != original["starttime_ticks"]:
|
||||
return chain, False, f"starttime-race:{original['pid']}"
|
||||
return chain, True, "ancestry-reaches-registered-anchor"
|
||||
except (FileNotFoundError, ProcessLookupError, PermissionError) as exc:
|
||||
return chain, False, f"proc-walk-failed:{type(exc).__name__}"
|
||||
|
||||
|
||||
def emit(log_file: Path, record: dict[str, Any]) -> None:
|
||||
line = json.dumps(record, sort_keys=True)
|
||||
with log_file.open("a", encoding="utf-8") as out:
|
||||
out.write(line + "\n")
|
||||
print(line, flush=True)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--socket", required=True)
|
||||
parser.add_argument("--log", required=True)
|
||||
parser.add_argument("--state", required=True)
|
||||
args = parser.parse_args()
|
||||
|
||||
socket_path = Path(args.socket)
|
||||
log_file = Path(args.log)
|
||||
state_file = Path(args.state)
|
||||
socket_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
os.chmod(socket_path.parent, 0o700)
|
||||
socket_path.unlink(missing_ok=True)
|
||||
log_file.unlink(missing_ok=True)
|
||||
state_file.unlink(missing_ok=True)
|
||||
|
||||
server = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
||||
server.bind(str(socket_path))
|
||||
os.chmod(socket_path, 0o600)
|
||||
server.listen(8)
|
||||
anchor: dict[str, Any] | None = None
|
||||
session_id: str | None = None
|
||||
emit(
|
||||
log_file,
|
||||
{
|
||||
"event": "broker-listen",
|
||||
"pid": os.getpid(),
|
||||
"socket": str(socket_path),
|
||||
"directory_mode": f"{stat.S_IMODE(socket_path.parent.stat().st_mode):04o}",
|
||||
"socket_mode": f"{stat.S_IMODE(socket_path.stat().st_mode):04o}",
|
||||
},
|
||||
)
|
||||
|
||||
while True:
|
||||
conn, _ = server.accept()
|
||||
with conn:
|
||||
raw = conn.getsockopt(socket.SOL_SOCKET, socket.SO_PEERCRED, 12)
|
||||
peer_pid, peer_uid, peer_gid = struct.unpack("3i", raw)
|
||||
request = json.loads(conn.makefile("r", encoding="utf-8").readline())
|
||||
action = request.get("action")
|
||||
|
||||
if action == "register-anchor" and anchor is None:
|
||||
anchor = proc_node(peer_pid)
|
||||
session_id = secrets.token_hex(16)
|
||||
state = {"session_id": session_id, "anchor": anchor}
|
||||
state_file.write_text(json.dumps(state, sort_keys=True) + "\n")
|
||||
record = {
|
||||
"event": "anchor-minted",
|
||||
"decision": "ACCEPT",
|
||||
"peercred": {"pid": peer_pid, "uid": peer_uid, "gid": peer_gid},
|
||||
"anchor": anchor,
|
||||
"session_id": session_id,
|
||||
}
|
||||
emit(log_file, record)
|
||||
conn.sendall((json.dumps(record) + "\n").encode())
|
||||
continue
|
||||
|
||||
if action in {"resolve-hook", "claim-session"}:
|
||||
chain, reaches, reason = ancestry(peer_pid, anchor)
|
||||
claimed = request.get("session_id")
|
||||
claim_ok = action == "resolve-hook" or claimed == session_id
|
||||
accepted = bool(anchor and session_id and reaches and claim_ok)
|
||||
if action == "claim-session" and claimed != session_id:
|
||||
reason = "unknown-session-id"
|
||||
elif action == "claim-session" and claimed == session_id and not reaches:
|
||||
reason = "victim-id-known-but-ancestry-mismatch"
|
||||
record = {
|
||||
"event": action,
|
||||
"decision": "ACCEPT" if accepted else "REJECT",
|
||||
"reason": reason,
|
||||
"peercred": {"pid": peer_pid, "uid": peer_uid, "gid": peer_gid},
|
||||
"claimed_session_id": claimed,
|
||||
"resolved_session_id": session_id if accepted else None,
|
||||
"anchor": anchor,
|
||||
"ancestry": chain,
|
||||
"starttimes_rechecked": reaches,
|
||||
}
|
||||
emit(log_file, record)
|
||||
conn.sendall((json.dumps(record) + "\n").encode())
|
||||
continue
|
||||
|
||||
if action == "shutdown":
|
||||
record = {
|
||||
"event": "broker-shutdown",
|
||||
"peercred": {"pid": peer_pid, "uid": peer_uid, "gid": peer_gid},
|
||||
}
|
||||
emit(log_file, record)
|
||||
conn.sendall((json.dumps(record) + "\n").encode())
|
||||
break
|
||||
|
||||
record = {
|
||||
"event": "invalid-request",
|
||||
"decision": "REJECT",
|
||||
"peercred": {"pid": peer_pid, "uid": peer_uid, "gid": peer_gid},
|
||||
}
|
||||
emit(log_file, record)
|
||||
conn.sendall((json.dumps(record) + "\n").encode())
|
||||
|
||||
server.close()
|
||||
socket_path.unlink(missing_ok=True)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
main()
|
||||
except Exception as exc:
|
||||
print(f"P1 broker fatal: {type(exc).__name__}: {exc}", file=sys.stderr)
|
||||
raise
|
||||
@@ -1,38 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Claude SessionStart hook client for P1 ancestry evidence."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import socket
|
||||
import sys
|
||||
|
||||
|
||||
def main() -> None:
|
||||
# Consume the real Claude hook payload without recording transcript paths or
|
||||
# prompt content in the evidence artifact.
|
||||
hook_input = json.load(sys.stdin)
|
||||
conn = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
||||
conn.connect(os.environ["GATE0_BROKER_SOCKET"])
|
||||
conn.sendall((json.dumps({"action": "resolve-hook"}) + "\n").encode())
|
||||
response = json.loads(conn.makefile("r", encoding="utf-8").readline())
|
||||
conn.close()
|
||||
event_name = hook_input.get("hook_event_name")
|
||||
if response.get("decision") != "ACCEPT":
|
||||
print(f"Gate0 broker rejected {event_name} hook ancestry", file=sys.stderr)
|
||||
raise SystemExit(2)
|
||||
print(
|
||||
json.dumps(
|
||||
{
|
||||
"hookSpecificOutput": {
|
||||
"hookEventName": event_name,
|
||||
"additionalContext": "GATE0_P1_SUPPORTED_HOOK_ANCESTRY_ACCEPTED",
|
||||
}
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,35 +0,0 @@
|
||||
import type { ExtensionAPI } from '@earendil-works/pi-coding-agent';
|
||||
import net from 'node:net';
|
||||
|
||||
async function brokerRequest(payload: Record<string, unknown>): Promise<Record<string, unknown>> {
|
||||
const socketPath = process.env['GATE0_BROKER_SOCKET'];
|
||||
if (!socketPath) throw new Error('GATE0_BROKER_SOCKET missing');
|
||||
return await new Promise((resolve, reject) => {
|
||||
const socket = net.createConnection(socketPath);
|
||||
let buffer = '';
|
||||
socket.setEncoding('utf8');
|
||||
socket.on('connect', () => socket.write(`${JSON.stringify(payload)}\n`));
|
||||
socket.on('data', (chunk) => {
|
||||
buffer += chunk;
|
||||
const newline = buffer.indexOf('\n');
|
||||
if (newline < 0) return;
|
||||
socket.end();
|
||||
resolve(JSON.parse(buffer.slice(0, newline)) as Record<string, unknown>);
|
||||
});
|
||||
socket.on('error', reject);
|
||||
});
|
||||
}
|
||||
|
||||
export default function register(pi: ExtensionAPI) {
|
||||
pi.on('session_start', async () => {
|
||||
const response = await brokerRequest({ action: 'resolve-hook', runtime: 'pi-extension' });
|
||||
if (response['decision'] !== 'ACCEPT') {
|
||||
throw new Error(`P1 broker rejected Pi extension ancestry: ${response['reason']}`);
|
||||
}
|
||||
});
|
||||
|
||||
pi.registerCommand('gate0-p1-ready', {
|
||||
description: 'Return only after the P1 session_start ancestry hook completed',
|
||||
handler: async () => undefined,
|
||||
});
|
||||
}
|
||||
@@ -1,274 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Run P1 against the real installed Mosaic→Pi and Mosaic→Claude chains."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import shutil
|
||||
import signal
|
||||
import socket
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import time
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
HERE = Path(__file__).resolve().parent
|
||||
|
||||
|
||||
def wait_for(predicate, description: str, timeout: float = 30.0) -> None:
|
||||
deadline = time.monotonic() + timeout
|
||||
while time.monotonic() < deadline:
|
||||
if predicate():
|
||||
return
|
||||
time.sleep(0.05)
|
||||
raise TimeoutError(f"timed out waiting for {description}")
|
||||
|
||||
|
||||
def read_records(path: Path) -> list[dict[str, Any]]:
|
||||
if not path.exists():
|
||||
return []
|
||||
return [json.loads(line) for line in path.read_text().splitlines() if line]
|
||||
|
||||
|
||||
def socket_request(path: Path, payload: dict[str, object]) -> dict[str, object]:
|
||||
conn = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
||||
conn.connect(str(path))
|
||||
conn.sendall((json.dumps(payload) + "\n").encode())
|
||||
response = json.loads(conn.makefile("r", encoding="utf-8").readline())
|
||||
conn.close()
|
||||
return response
|
||||
|
||||
|
||||
def start_broker(root: Path) -> tuple[subprocess.Popen[str], Path, Path, Path]:
|
||||
socket_path = root / "broker.sock"
|
||||
log_path = root / "broker.jsonl"
|
||||
state_path = root / "state.json"
|
||||
broker = subprocess.Popen(
|
||||
[
|
||||
sys.executable,
|
||||
str(HERE / "p1_broker.py"),
|
||||
"--socket",
|
||||
str(socket_path),
|
||||
"--log",
|
||||
str(log_path),
|
||||
"--state",
|
||||
str(state_path),
|
||||
],
|
||||
text=True,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.STDOUT,
|
||||
)
|
||||
wait_for(socket_path.exists, "broker socket")
|
||||
return broker, socket_path, log_path, state_path
|
||||
|
||||
|
||||
def print_ps(record: dict[str, Any]) -> None:
|
||||
chain = record.get("ancestry", [])
|
||||
pids = [str(node["pid"]) for node in chain if Path(f"/proc/{node['pid']}").exists()]
|
||||
if not pids:
|
||||
print("ps_snapshot=<hook chain exited; broker /proc snapshot above is authoritative>")
|
||||
return
|
||||
command = [
|
||||
"ps",
|
||||
"-o",
|
||||
"pid=,ppid=,lstart=,uid=,gid=,comm=",
|
||||
"-p",
|
||||
",".join(pids),
|
||||
]
|
||||
print("$ " + " ".join(command))
|
||||
print(subprocess.check_output(command, text=True).rstrip())
|
||||
|
||||
|
||||
def run_runtime(runtime: str) -> None:
|
||||
with tempfile.TemporaryDirectory(prefix=f"gate0-p1-{runtime}-") as temp:
|
||||
root = Path(temp)
|
||||
workspace = root / "workspace"
|
||||
workspace.mkdir()
|
||||
broker, socket_path, log_path, state_path = start_broker(root)
|
||||
env = os.environ.copy()
|
||||
env.update(
|
||||
{
|
||||
"GATE0_BROKER_SOCKET": str(socket_path),
|
||||
"MOSAIC_PI_FORCE_SKILLS": "",
|
||||
"PI_SKIP_VERSION_CHECK": "1",
|
||||
}
|
||||
)
|
||||
stdout_path = root / f"{runtime}.stdout"
|
||||
stderr_path = root / f"{runtime}.stderr"
|
||||
|
||||
if runtime == "pi":
|
||||
runtime_args = [
|
||||
"--mode",
|
||||
"rpc",
|
||||
"--no-session",
|
||||
"--no-extensions",
|
||||
"--no-context-files",
|
||||
"--no-prompt-templates",
|
||||
"--extension",
|
||||
str(HERE / "p1_pi_extension.ts"),
|
||||
]
|
||||
else:
|
||||
settings = root / "claude-settings.json"
|
||||
settings.write_text(
|
||||
json.dumps(
|
||||
{
|
||||
"hooks": {
|
||||
"SessionStart": [
|
||||
{
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": f'python3 "{HERE / "p1_hook_client.py"}"',
|
||||
"timeout": 20,
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
)
|
||||
)
|
||||
runtime_args = [
|
||||
"--settings",
|
||||
str(settings),
|
||||
"--model",
|
||||
"haiku",
|
||||
"--print",
|
||||
"--output-format",
|
||||
"stream-json",
|
||||
"--verbose",
|
||||
"--include-hook-events",
|
||||
"--max-budget-usd",
|
||||
"0.03",
|
||||
"Reply exactly: OK",
|
||||
]
|
||||
|
||||
out = stdout_path.open("w", encoding="utf-8")
|
||||
err = stderr_path.open("w", encoding="utf-8")
|
||||
anchor = subprocess.Popen(
|
||||
[
|
||||
sys.executable,
|
||||
str(HERE / "p1_anchor_exec.py"),
|
||||
"--socket",
|
||||
str(socket_path),
|
||||
runtime,
|
||||
*runtime_args,
|
||||
],
|
||||
cwd=workspace,
|
||||
env=env,
|
||||
stdin=subprocess.PIPE if runtime == "pi" else subprocess.DEVNULL,
|
||||
stdout=out,
|
||||
stderr=err,
|
||||
text=True,
|
||||
start_new_session=True,
|
||||
)
|
||||
try:
|
||||
wait_for(state_path.exists, "anchor registration")
|
||||
attacker = subprocess.run(
|
||||
[
|
||||
sys.executable,
|
||||
str(HERE / "p1_sibling_attacker.py"),
|
||||
"--socket",
|
||||
str(socket_path),
|
||||
"--state",
|
||||
str(state_path),
|
||||
],
|
||||
text=True,
|
||||
capture_output=True,
|
||||
check=False,
|
||||
)
|
||||
if runtime == "pi":
|
||||
assert anchor.stdin is not None
|
||||
anchor.stdin.write('{"id":"state","type":"get_state"}\n')
|
||||
anchor.stdin.flush()
|
||||
|
||||
wait_for(
|
||||
lambda: any(r.get("event") == "resolve-hook" for r in read_records(log_path)),
|
||||
f"{runtime} supported hook/extension broker contact",
|
||||
timeout=60,
|
||||
)
|
||||
if runtime == "claude":
|
||||
try:
|
||||
anchor.wait(timeout=90)
|
||||
except subprocess.TimeoutExpired:
|
||||
pass
|
||||
records = read_records(log_path)
|
||||
state = json.loads(state_path.read_text())
|
||||
resolve = next(r for r in records if r.get("event") == "resolve-hook")
|
||||
reject = next(r for r in records if r.get("event") == "claim-session")
|
||||
if resolve.get("decision") != "ACCEPT":
|
||||
raise AssertionError(f"{runtime} hook ancestry was not accepted: {resolve}")
|
||||
if reject.get("decision") != "REJECT":
|
||||
raise AssertionError(f"{runtime} sibling substitution was not rejected: {reject}")
|
||||
if attacker.returncode != 0:
|
||||
raise AssertionError(f"{runtime} sibling probe did not observe rejection: {attacker.stderr}")
|
||||
|
||||
print(f"=== P1 {runtime.upper()} REAL LAUNCH ===")
|
||||
print("machine_assertions=PASS")
|
||||
print(
|
||||
"$ python3 docs/compaction-refresh/probes/p1_anchor_exec.py "
|
||||
f"--socket <protected-socket> {runtime} <runtime args>"
|
||||
)
|
||||
print("registered_anchor=" + json.dumps(state["anchor"], sort_keys=True))
|
||||
print("broker_minted_session_id=" + state["session_id"])
|
||||
print("hook_or_extension_record=" + json.dumps(resolve, sort_keys=True))
|
||||
print("sibling_attack_record=" + json.dumps(reject, sort_keys=True))
|
||||
print("sibling_process_stdout=" + attacker.stdout.strip())
|
||||
print(f"sibling_process_exit={attacker.returncode}")
|
||||
print_ps(resolve)
|
||||
print("launcher_stderr_excerpt:")
|
||||
for line in stderr_path.read_text(errors="replace").splitlines()[:12]:
|
||||
print(" " + line[:500])
|
||||
runtime_lines = stdout_path.read_text(errors="replace").splitlines()
|
||||
print("runtime_stdout_excerpt:")
|
||||
for line in runtime_lines[:8]:
|
||||
print(" " + line[:500])
|
||||
hook_lines = [
|
||||
line
|
||||
for line in runtime_lines
|
||||
if "hook" in line.lower() or "GATE0_P1_SUPPORTED_HOOK" in line
|
||||
]
|
||||
print("runtime_hook_event_excerpt:")
|
||||
for line in hook_lines[:8]:
|
||||
print(" " + line[:1000])
|
||||
print()
|
||||
finally:
|
||||
if anchor.poll() is None:
|
||||
try:
|
||||
os.killpg(anchor.pid, signal.SIGTERM)
|
||||
except ProcessLookupError:
|
||||
pass
|
||||
try:
|
||||
anchor.wait(timeout=5)
|
||||
except subprocess.TimeoutExpired:
|
||||
os.killpg(anchor.pid, signal.SIGKILL)
|
||||
anchor.wait(timeout=5)
|
||||
out.close()
|
||||
err.close()
|
||||
try:
|
||||
socket_request(socket_path, {"action": "shutdown"})
|
||||
except OSError:
|
||||
pass
|
||||
try:
|
||||
broker.wait(timeout=5)
|
||||
except subprocess.TimeoutExpired:
|
||||
broker.kill()
|
||||
broker.wait()
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--runtime", choices=["pi", "claude", "both"], default="both")
|
||||
ns = parser.parse_args()
|
||||
if ns.runtime in {"pi", "both"}:
|
||||
run_runtime("pi")
|
||||
if ns.runtime in {"claude", "both"}:
|
||||
run_runtime("claude")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,54 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Same-UID sibling that attempts to claim the anchor's broker-minted id."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import socket
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--socket", required=True)
|
||||
parser.add_argument("--state", required=True)
|
||||
ns = parser.parse_args()
|
||||
state_path = Path(ns.state)
|
||||
for _ in range(200):
|
||||
if state_path.exists():
|
||||
break
|
||||
time.sleep(0.025)
|
||||
state = json.loads(state_path.read_text())
|
||||
conn = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
||||
conn.connect(ns.socket)
|
||||
conn.sendall(
|
||||
(
|
||||
json.dumps(
|
||||
{"action": "claim-session", "session_id": state["session_id"]},
|
||||
sort_keys=True,
|
||||
)
|
||||
+ "\n"
|
||||
).encode()
|
||||
)
|
||||
response = json.loads(conn.makefile("r", encoding="utf-8").readline())
|
||||
conn.close()
|
||||
print(
|
||||
json.dumps(
|
||||
{
|
||||
"attacker_pid": os.getpid(),
|
||||
"attacker_uid": os.getuid(),
|
||||
"victim_session_id_known": True,
|
||||
"broker_decision": response.get("decision"),
|
||||
"broker_reason": response.get("reason"),
|
||||
},
|
||||
sort_keys=True,
|
||||
)
|
||||
)
|
||||
raise SystemExit(0 if response.get("decision") == "REJECT" else 1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,135 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Force a real Pi HTTP provider response to prove response-hook timing."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import tempfile
|
||||
import threading
|
||||
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
||||
from pathlib import Path
|
||||
|
||||
from pi_gate0_run import PiRpc, jsonl
|
||||
|
||||
HERE = Path(__file__).resolve().parent
|
||||
|
||||
|
||||
class Handler(BaseHTTPRequestHandler):
|
||||
protocol_version = "HTTP/1.1"
|
||||
|
||||
def log_message(self, _format: str, *_args: object) -> None:
|
||||
return
|
||||
|
||||
def do_POST(self) -> None: # noqa: N802
|
||||
length = int(self.headers.get("content-length", "0"))
|
||||
self.rfile.read(length)
|
||||
chunks = [
|
||||
{
|
||||
"id": "gate0-response",
|
||||
"object": "chat.completion.chunk",
|
||||
"created": 1,
|
||||
"model": "gate0-model",
|
||||
"choices": [{"index": 0, "delta": {"role": "assistant"}, "finish_reason": None}],
|
||||
},
|
||||
{
|
||||
"id": "gate0-response",
|
||||
"object": "chat.completion.chunk",
|
||||
"created": 1,
|
||||
"model": "gate0-model",
|
||||
"choices": [
|
||||
{"index": 0, "delta": {"content": "TIMING_OK"}, "finish_reason": None}
|
||||
],
|
||||
},
|
||||
{
|
||||
"id": "gate0-response",
|
||||
"object": "chat.completion.chunk",
|
||||
"created": 1,
|
||||
"model": "gate0-model",
|
||||
"choices": [{"index": 0, "delta": {}, "finish_reason": "stop"}],
|
||||
"usage": {"prompt_tokens": 10, "completion_tokens": 2, "total_tokens": 12},
|
||||
},
|
||||
]
|
||||
body = "".join(f"data: {json.dumps(chunk)}\n\n" for chunk in chunks) + "data: [DONE]\n\n"
|
||||
encoded = body.encode()
|
||||
self.send_response(200)
|
||||
self.send_header("Content-Type", "text/event-stream")
|
||||
self.send_header("Content-Length", str(len(encoded)))
|
||||
self.send_header("X-Gate0-Response", "headers-before-stream")
|
||||
self.end_headers()
|
||||
self.wfile.write(encoded)
|
||||
self.wfile.flush()
|
||||
|
||||
|
||||
def main() -> None:
|
||||
server = ThreadingHTTPServer(("127.0.0.1", 0), Handler)
|
||||
thread = threading.Thread(target=server.serve_forever, daemon=True)
|
||||
thread.start()
|
||||
port = server.server_address[1]
|
||||
|
||||
with tempfile.TemporaryDirectory(prefix="gate0-p2-timing-") as temp:
|
||||
root = Path(temp)
|
||||
workspace = root / "workspace"
|
||||
workspace.mkdir()
|
||||
log = root / "hooks.jsonl"
|
||||
env = os.environ.copy()
|
||||
env.update(
|
||||
{
|
||||
"GATE0_PI_LOG": str(log),
|
||||
"GATE0_LOCAL_PROVIDER_URL": f"http://127.0.0.1:{port}/v1",
|
||||
"MOSAIC_PI_FORCE_SKILLS": "",
|
||||
"PI_SKIP_VERSION_CHECK": "1",
|
||||
}
|
||||
)
|
||||
command = [
|
||||
"mosaic",
|
||||
"yolo",
|
||||
"pi",
|
||||
"--mode",
|
||||
"rpc",
|
||||
"--no-session",
|
||||
"--no-extensions",
|
||||
"--no-context-files",
|
||||
"--no-prompt-templates",
|
||||
"--provider",
|
||||
"gate0-local",
|
||||
"--model",
|
||||
"gate0-model",
|
||||
"--extension",
|
||||
str(HERE / "pi_gate0_extension.ts"),
|
||||
]
|
||||
pi = PiRpc(command, workspace, env)
|
||||
try:
|
||||
pi.prompt_and_settle("timing", "Reply with TIMING_OK")
|
||||
records = jsonl(log)
|
||||
selected = [
|
||||
record
|
||||
for record in records
|
||||
if record["event"] in {"before_provider_request", "after_provider_response", "message_end"}
|
||||
and (record["event"] != "message_end" or record.get("role") == "assistant")
|
||||
]
|
||||
print("$ python3 docs/compaction-refresh/probes/p2_provider_timing_run.py")
|
||||
print(f"local_http_endpoint=http://127.0.0.1:{port}/v1/chat/completions")
|
||||
for record in selected:
|
||||
print(json.dumps(record, sort_keys=True))
|
||||
after = next(record for record in selected if record["event"] == "after_provider_response")
|
||||
message = next(record for record in selected if record["event"] == "message_end")
|
||||
if not (
|
||||
after["seq"] < message["seq"]
|
||||
and after["assistantContentAvailableAtThisHook"] is False
|
||||
and message["assistantContentObserved"] is True
|
||||
):
|
||||
raise AssertionError("provider response/content observation ordering failed")
|
||||
print("machine_assertions=PASS")
|
||||
print(f"after_provider_response_seq={after['seq']}")
|
||||
print(f"message_end_seq={message['seq']}")
|
||||
print(f"headers_hook_precedes_completed_message={after['seq'] < message['seq']}")
|
||||
finally:
|
||||
pi.close()
|
||||
|
||||
server.shutdown()
|
||||
server.server_close()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,849 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""D4-only same-PID runtime-generation revocation harness.
|
||||
|
||||
AUTHORING NOTE: this file is intentionally not executed until the separately
|
||||
ratified FIRE authorization. When run later, every invocation creates its own
|
||||
/tmp fixture and launches the real Pi RPC runtime with only the D4 extension
|
||||
and ``p3_generation_broker.py``. It does not use the broader Gate0 runner.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import ast
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import queue
|
||||
import shutil
|
||||
import signal
|
||||
import socket
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import threading
|
||||
import time
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from typing import Any, Callable
|
||||
|
||||
HERE = Path(__file__).resolve().parent
|
||||
# WI-3 remains in a reviewed worktree until the release package contains the
|
||||
# gated launcher. The probe resolves that worktree portably and never falls
|
||||
# back to the released `mosaic` binary.
|
||||
GATED_WI_ROOT_OVERRIDE = os.environ.get("GATED_WI_ROOT")
|
||||
GATED_WI_BRANCH = "refs/heads/feat/830-compaction-revoke"
|
||||
GATED_WI_HEAD = "f400830738998db105107a2a4c69c7f2a2a6fd5d"
|
||||
GATED_WI_ANCESTOR = "66b1e0a0"
|
||||
GATED_BROKER_HEAD = "23c0caca9b5d44002e6184cd7f2b6c837e8795b2"
|
||||
LEASE_BROKER_DIRECTORY = "packages/mosaic/framework/tools/lease-broker"
|
||||
BROKER_RELATIVE_PATH = "docs/compaction-refresh/probes/p3_generation_broker.py"
|
||||
GATED_LAUNCHER_SHA256 = "e950e4224e280f16979d90cabb89aa1896c5ee28bed2df957e14d018d43cda82"
|
||||
GATED_GENERATION_SHA256 = "061625402f08488eac47acd23272904e71fd1a71fd15b3bdab158632c801be4c"
|
||||
GATED_BROKER_SHA256 = "4db4fef1ac6658a8ca79ad5091cefc901d2aa26003265c3d6726c294cf895cad"
|
||||
|
||||
|
||||
class PiRpc:
|
||||
"""Small JSON-RPC client for an isolated real Pi process."""
|
||||
|
||||
def __init__(self, command: list[str], cwd: Path, env: dict[str, str]) -> None:
|
||||
self.process = subprocess.Popen(
|
||||
command,
|
||||
cwd=cwd,
|
||||
env=env,
|
||||
stdin=subprocess.PIPE,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
text=True,
|
||||
bufsize=1,
|
||||
start_new_session=True,
|
||||
)
|
||||
self.events: queue.Queue[dict[str, Any]] = queue.Queue()
|
||||
self.stderr_lines: list[str] = []
|
||||
threading.Thread(target=self._read_stdout, daemon=True).start()
|
||||
threading.Thread(target=self._read_stderr, daemon=True).start()
|
||||
|
||||
def _read_stdout(self) -> None:
|
||||
if self.process.stdout is None:
|
||||
raise RuntimeError("Pi stdout pipe is unavailable")
|
||||
for line in self.process.stdout:
|
||||
try:
|
||||
self.events.put(json.loads(line))
|
||||
except json.JSONDecodeError:
|
||||
continue
|
||||
|
||||
def _read_stderr(self) -> None:
|
||||
if self.process.stderr is None:
|
||||
raise RuntimeError("Pi stderr pipe is unavailable")
|
||||
for line in self.process.stderr:
|
||||
self.stderr_lines.append(line.rstrip("\n"))
|
||||
|
||||
def send(self, payload: dict[str, object]) -> None:
|
||||
if self.process.stdin is None:
|
||||
raise RuntimeError("Pi stdin pipe is unavailable")
|
||||
self.process.stdin.write(json.dumps(payload) + "\n")
|
||||
self.process.stdin.flush()
|
||||
|
||||
def wait(
|
||||
self,
|
||||
predicate: Callable[[dict[str, Any]], bool],
|
||||
description: str,
|
||||
timeout: float = 180,
|
||||
) -> dict[str, Any]:
|
||||
deadline = time.monotonic() + timeout
|
||||
while time.monotonic() < deadline:
|
||||
if self.process.poll() is not None and self.events.empty():
|
||||
detail = " | ".join(self.stderr_lines[-5:])
|
||||
raise RuntimeError(
|
||||
f"Pi exited {self.process.returncode} while waiting for {description}: {detail}"
|
||||
)
|
||||
try:
|
||||
event = self.events.get(timeout=0.2)
|
||||
except queue.Empty:
|
||||
continue
|
||||
if predicate(event):
|
||||
return event
|
||||
raise TimeoutError(f"timed out waiting for {description}")
|
||||
|
||||
def response(self, request_id: str, timeout: float = 180) -> dict[str, Any]:
|
||||
return self.wait(
|
||||
lambda event: event.get("type") == "response" and event.get("id") == request_id,
|
||||
f"response {request_id}",
|
||||
timeout,
|
||||
)
|
||||
|
||||
def prompt_and_settle(self, request_id: str, message: str) -> None:
|
||||
self.send({"id": request_id, "type": "prompt", "message": message})
|
||||
response = self.response(request_id)
|
||||
if not response.get("success"):
|
||||
raise RuntimeError(f"prompt rejected: {response}")
|
||||
self.wait(
|
||||
lambda event: event.get("type") == "agent_settled",
|
||||
f"agent_settled {request_id}",
|
||||
)
|
||||
|
||||
def close(self) -> None:
|
||||
if self.process.poll() is None:
|
||||
try:
|
||||
os.killpg(self.process.pid, signal.SIGTERM)
|
||||
except ProcessLookupError:
|
||||
pass
|
||||
try:
|
||||
self.process.wait(timeout=8)
|
||||
except subprocess.TimeoutExpired:
|
||||
os.killpg(self.process.pid, signal.SIGKILL)
|
||||
self.process.wait(timeout=5)
|
||||
|
||||
|
||||
def wait_path(path: Path, timeout: float = 20) -> None:
|
||||
deadline = time.monotonic() + timeout
|
||||
while time.monotonic() < deadline:
|
||||
if path.exists():
|
||||
return
|
||||
time.sleep(0.05)
|
||||
raise TimeoutError(f"timed out waiting for {path}")
|
||||
|
||||
|
||||
def request(path: Path, payload: dict[str, object]) -> dict[str, Any]:
|
||||
with socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) as conn:
|
||||
conn.connect(str(path))
|
||||
conn.sendall((json.dumps(payload) + "\n").encode())
|
||||
reply = conn.makefile("r", encoding="utf-8").readline()
|
||||
return json.loads(reply)
|
||||
|
||||
|
||||
def jsonl(path: Path) -> list[dict[str, Any]]:
|
||||
return [json.loads(line) for line in path.read_text().splitlines() if line]
|
||||
|
||||
|
||||
def write_extension(path: Path) -> None:
|
||||
"""Write the minimal Pi lifecycle bridge into the isolated fixture only."""
|
||||
|
||||
path.write_text(
|
||||
"""import type { ExtensionAPI } from '@earendil-works/pi-coding-agent';
|
||||
import { Type } from 'typebox';
|
||||
import { appendFileSync, readFileSync } from 'node:fs';
|
||||
import net from 'node:net';
|
||||
|
||||
const socketPath = process.env['D4_GENERATION_SOCKET'];
|
||||
const logPath = process.env['D4_PI_LOG'];
|
||||
|
||||
function starttime(): number {
|
||||
const text = readFileSync(`/proc/${process.pid}/stat`, 'utf8');
|
||||
const close = text.lastIndexOf(')');
|
||||
return Number(text.slice(close + 2).trim().split(/\\s+/)[19]);
|
||||
}
|
||||
|
||||
function log(event: string, details: Record<string, unknown> = {}): void {
|
||||
if (!logPath) return;
|
||||
appendFileSync(logPath, `${JSON.stringify({ event, pid: process.pid, starttime_ticks: starttime(), ...details })}\\n`);
|
||||
}
|
||||
|
||||
function broker(payload: Record<string, unknown>): Promise<Record<string, unknown>> {
|
||||
if (!socketPath) return Promise.reject(new Error('D4_GENERATION_SOCKET is required'));
|
||||
return new Promise((resolve, reject) => {
|
||||
const connection = net.createConnection(socketPath);
|
||||
let buffer = '';
|
||||
connection.setEncoding('utf8');
|
||||
connection.on('connect', () => connection.write(`${JSON.stringify(payload)}\\n`));
|
||||
connection.on('data', (chunk) => {
|
||||
buffer += chunk;
|
||||
const newline = buffer.indexOf('\\n');
|
||||
if (newline < 0) return;
|
||||
connection.end();
|
||||
resolve(JSON.parse(buffer.slice(0, newline)) as Record<string, unknown>);
|
||||
});
|
||||
connection.on('error', reject);
|
||||
});
|
||||
}
|
||||
|
||||
export default function register(pi: ExtensionAPI): void {
|
||||
let initialStartup = true;
|
||||
|
||||
async function lifecycle(
|
||||
phase: 'start' | 'shutdown',
|
||||
reason: string,
|
||||
): Promise<Record<string, unknown>> {
|
||||
if (!(phase === 'start' && reason === 'startup' && initialStartup)) {
|
||||
const bump = await broker({ action: 'bump-generation' });
|
||||
log('generation_state_bump', { phase, reason, bump });
|
||||
}
|
||||
initialStartup = false;
|
||||
return broker({ action: 'lifecycle', phase, reason });
|
||||
}
|
||||
|
||||
pi.on('session_start', async (event) => {
|
||||
const lifecycleResult = await lifecycle('start', event.reason);
|
||||
log('session_start', { reason: event.reason, lifecycle: lifecycleResult });
|
||||
if (event.reason === 'reload') {
|
||||
const generation = lifecycleResult['new_generation'];
|
||||
if (typeof generation !== 'number') throw new Error('broker did not return new_generation');
|
||||
const current = await broker({ action: 'authorize-probe', generation });
|
||||
const superseded = await broker({ action: 'authorize-probe', generation: generation - 1 });
|
||||
log('d4_generation_authorization', { generation, current, superseded });
|
||||
}
|
||||
});
|
||||
|
||||
pi.on('session_shutdown', async (event) => {
|
||||
const lifecycleResult = await lifecycle('shutdown', event.reason);
|
||||
log('session_shutdown', { reason: event.reason, lifecycle: lifecycleResult });
|
||||
});
|
||||
|
||||
pi.registerTool({
|
||||
name: 'd4_fixture_promote',
|
||||
label: 'D4 Fixture Promotion',
|
||||
description: 'Promotes only the fixture lease needed for the D4 revocation check.',
|
||||
parameters: Type.Object({}),
|
||||
async execute() {
|
||||
// the promotion step is a D4 test fixture, not a P2 evidence-gathering authorization.
|
||||
const promotion = await broker({ action: 'promote-probe' });
|
||||
log('fixture_promotion', { promotion });
|
||||
return { content: [{ type: 'text', text: 'D4 fixture promotion complete' }] };
|
||||
},
|
||||
});
|
||||
|
||||
pi.registerCommand('d4-reload', {
|
||||
description: 'D4-only same-PID reload boundary.',
|
||||
handler: async (_args, context) => {
|
||||
await context.reload();
|
||||
},
|
||||
});
|
||||
}
|
||||
""",
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
|
||||
def repository_root() -> Path:
|
||||
for candidate in HERE.parents:
|
||||
if (candidate / ".git").exists():
|
||||
return candidate
|
||||
raise RuntimeError("D4 precondition: probe repository root is unavailable")
|
||||
|
||||
|
||||
def resolve_gated_wi_root() -> Path:
|
||||
"""Resolve an explicit override or the unique checked-out WI-3 branch."""
|
||||
|
||||
if GATED_WI_ROOT_OVERRIDE:
|
||||
candidate = Path(GATED_WI_ROOT_OVERRIDE).expanduser()
|
||||
candidates = [candidate]
|
||||
else:
|
||||
try:
|
||||
listing = subprocess.check_output(
|
||||
["git", "-C", str(repository_root()), "worktree", "list", "--porcelain"],
|
||||
text=True,
|
||||
)
|
||||
except (OSError, subprocess.CalledProcessError) as error:
|
||||
raise RuntimeError("D4 precondition: cannot enumerate WI-3 worktrees") from error
|
||||
candidates = []
|
||||
worktree: Path | None = None
|
||||
head: str | None = None
|
||||
branch: str | None = None
|
||||
for line in [*listing.splitlines(), ""]:
|
||||
if line.startswith("worktree "):
|
||||
worktree = Path(line.removeprefix("worktree "))
|
||||
head = None
|
||||
branch = None
|
||||
elif line.startswith("HEAD "):
|
||||
head = line.removeprefix("HEAD ")
|
||||
elif line.startswith("branch "):
|
||||
branch = line.removeprefix("branch ")
|
||||
elif not line and worktree is not None:
|
||||
if head == GATED_WI_HEAD and branch == GATED_WI_BRANCH:
|
||||
candidates.append(worktree)
|
||||
worktree = None
|
||||
if len(candidates) != 1:
|
||||
raise RuntimeError("D4 precondition: WI-3 worktree is ambiguous or unavailable")
|
||||
|
||||
gated_root = candidates[0]
|
||||
try:
|
||||
if not gated_root.is_dir():
|
||||
raise RuntimeError("D4 precondition: GATED_WI_ROOT is not a directory")
|
||||
is_worktree = subprocess.check_output(
|
||||
["git", "-C", str(gated_root), "rev-parse", "--is-inside-work-tree"],
|
||||
text=True,
|
||||
).strip()
|
||||
head = subprocess.check_output(
|
||||
["git", "-C", str(gated_root), "rev-parse", "HEAD"], text=True
|
||||
).strip()
|
||||
except (OSError, subprocess.CalledProcessError) as error:
|
||||
raise RuntimeError("D4 precondition: GATED_WI_ROOT is not a git worktree") from error
|
||||
if is_worktree != "true":
|
||||
raise RuntimeError("D4 precondition: GATED_WI_ROOT is not a git worktree")
|
||||
if head != GATED_WI_HEAD:
|
||||
raise RuntimeError(f"D4 precondition: gated WI head mismatch: {head}")
|
||||
try:
|
||||
forward_contains = subprocess.run(
|
||||
[
|
||||
"git",
|
||||
"-C",
|
||||
str(gated_root),
|
||||
"merge-base",
|
||||
"--is-ancestor",
|
||||
GATED_WI_ANCESTOR,
|
||||
GATED_WI_HEAD,
|
||||
],
|
||||
check=False,
|
||||
).returncode == 0
|
||||
except OSError as error:
|
||||
raise RuntimeError("D4 precondition: cannot verify WI-3 ancestry") from error
|
||||
if not forward_contains:
|
||||
raise RuntimeError("D4 precondition: gated WI lacks required ancestor")
|
||||
return gated_root
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class PinnedClosure:
|
||||
launcher: Path
|
||||
generation: Path
|
||||
broker: Path
|
||||
|
||||
|
||||
def git_object_bytes(git_root: Path, commit: str, relative_path: str) -> bytes:
|
||||
try:
|
||||
return subprocess.check_output(
|
||||
["git", "-C", str(git_root), "show", f"{commit}:{relative_path}"]
|
||||
)
|
||||
except (OSError, subprocess.CalledProcessError) as error:
|
||||
raise RuntimeError(f"D4 precondition: missing pinned source {relative_path}") from error
|
||||
|
||||
|
||||
def closure_import_guard(member_sources: dict[str, str]) -> None:
|
||||
"""Refuse an incomplete project-code closure before materializing it."""
|
||||
|
||||
allowed_nonstdlib = {"lease_generation"}
|
||||
stdlib = getattr(sys, "stdlib_module_names", frozenset())
|
||||
for name, source in member_sources.items():
|
||||
try:
|
||||
tree = ast.parse(source, filename=name)
|
||||
except SyntaxError as error:
|
||||
raise RuntimeError(f"D4 precondition: pinned {name} does not parse") from error
|
||||
for node in ast.walk(tree):
|
||||
module: str | None = None
|
||||
if isinstance(node, ast.Import):
|
||||
for alias in node.names:
|
||||
module = alias.name.split(".", maxsplit=1)[0]
|
||||
if module not in stdlib and module not in allowed_nonstdlib:
|
||||
raise RuntimeError(f"D4 precondition: unpinned import {module} in {name}")
|
||||
elif isinstance(node, ast.ImportFrom):
|
||||
if node.level:
|
||||
raise RuntimeError(f"D4 precondition: relative import in {name}")
|
||||
if node.module:
|
||||
module = node.module.split(".", maxsplit=1)[0]
|
||||
if module not in stdlib and module not in allowed_nonstdlib:
|
||||
raise RuntimeError(f"D4 precondition: unpinned import {module} in {name}")
|
||||
|
||||
|
||||
def write_pinned_file(path: Path, data: bytes) -> None:
|
||||
descriptor = os.open(
|
||||
path,
|
||||
os.O_WRONLY | os.O_CREAT | os.O_EXCL | os.O_CLOEXEC,
|
||||
0o600,
|
||||
)
|
||||
try:
|
||||
remaining = memoryview(data)
|
||||
while remaining:
|
||||
written = os.write(descriptor, remaining)
|
||||
if written <= 0:
|
||||
raise OSError("pinned write made no progress")
|
||||
remaining = remaining[written:]
|
||||
finally:
|
||||
os.close(descriptor)
|
||||
|
||||
|
||||
def materialize_closure(root: Path, gated_root: Path, gate0_root: Path) -> PinnedClosure:
|
||||
"""Pin the complete project-authored runtime closure inside this fixture."""
|
||||
|
||||
launcher_relative = f"{LEASE_BROKER_DIRECTORY}/launch-runtime.py"
|
||||
generation_relative = f"{LEASE_BROKER_DIRECTORY}/lease_generation.py"
|
||||
members = (
|
||||
(
|
||||
"launch-runtime.py",
|
||||
gated_root,
|
||||
GATED_WI_HEAD,
|
||||
launcher_relative,
|
||||
GATED_LAUNCHER_SHA256,
|
||||
),
|
||||
(
|
||||
"lease_generation.py",
|
||||
gated_root,
|
||||
GATED_WI_HEAD,
|
||||
generation_relative,
|
||||
GATED_GENERATION_SHA256,
|
||||
),
|
||||
(
|
||||
"p3_generation_broker.py",
|
||||
gate0_root,
|
||||
GATED_BROKER_HEAD,
|
||||
BROKER_RELATIVE_PATH,
|
||||
GATED_BROKER_SHA256,
|
||||
),
|
||||
)
|
||||
member_bytes: dict[str, bytes] = {}
|
||||
member_sources: dict[str, str] = {}
|
||||
for name, git_root, commit, relative_path, digest in members:
|
||||
data = git_object_bytes(git_root, commit, relative_path)
|
||||
if hashlib.sha256(data).hexdigest() != digest:
|
||||
raise RuntimeError(f"D4 precondition: {name} hash mismatch")
|
||||
try:
|
||||
member_sources[name] = data.decode("utf-8")
|
||||
except UnicodeDecodeError as error:
|
||||
raise RuntimeError(f"D4 precondition: pinned {name} is not UTF-8") from error
|
||||
member_bytes[name] = data
|
||||
closure_import_guard(member_sources)
|
||||
|
||||
pinned = root / "pinned"
|
||||
pinned.mkdir(mode=0o700)
|
||||
paths = {name: pinned / name for name, *_ in members}
|
||||
for name, path in paths.items():
|
||||
write_pinned_file(path, member_bytes[name])
|
||||
return PinnedClosure(
|
||||
launcher=paths["launch-runtime.py"],
|
||||
generation=paths["lease_generation.py"],
|
||||
broker=paths["p3_generation_broker.py"],
|
||||
)
|
||||
|
||||
|
||||
def gated_launcher_precondition(
|
||||
root: Path, socket_path: Path, environment: dict[str, str]
|
||||
) -> PinnedClosure:
|
||||
"""Verify and materialize the full WI-3/probe closure before execution."""
|
||||
|
||||
if environment.get("MOSAIC_LEASE_BROKER_SOCKET") != str(socket_path):
|
||||
raise RuntimeError("D4 precondition: lease broker socket is not this fixture")
|
||||
if environment.get("MOSAIC_LEASE_GENERATION_FILE"):
|
||||
raise RuntimeError("D4 precondition: inherited generation file is forbidden")
|
||||
fixture_path_vars = (
|
||||
"HOME",
|
||||
"XDG_CONFIG_HOME",
|
||||
"XDG_CACHE_HOME",
|
||||
"XDG_STATE_HOME",
|
||||
"XDG_RUNTIME_DIR",
|
||||
"TMPDIR",
|
||||
"D4_PI_LOG",
|
||||
"MOSAIC_AGENT_WORKDIR",
|
||||
"MOSAIC_HEARTBEAT_RUN_DIR",
|
||||
"MOSAIC_HOME",
|
||||
)
|
||||
if any(
|
||||
not (value := environment.get(name)) or not Path(value).is_relative_to(root)
|
||||
for name in fixture_path_vars
|
||||
):
|
||||
raise RuntimeError("D4 precondition: child write path escapes fixture root")
|
||||
if socket_path.parent != root or root.parent != Path(tempfile.gettempdir()):
|
||||
raise RuntimeError("D4 precondition: fixture socket is outside this run's temporary root")
|
||||
|
||||
gated_root = resolve_gated_wi_root()
|
||||
closure = materialize_closure(root, gated_root, repository_root())
|
||||
launcher_source = closure.launcher.read_text(encoding="utf-8")
|
||||
generation_source = closure.generation.read_text(encoding="utf-8")
|
||||
# Exact hashes in materialize_closure are the trust anchor. These marker
|
||||
# checks are belt-and-suspenders diagnostics only.
|
||||
behavior_markers = (
|
||||
'"action": "register_anchor"',
|
||||
"initialize_runtime_generation(generation_file, generation)",
|
||||
"execute(command[0], command, environment)",
|
||||
'source_environment["MOSAIC_LEASE_BROKER_SOCKET"]',
|
||||
'socket_path.parent / f"generation-{session_id}.state"',
|
||||
'environment["MOSAIC_LEASE_GENERATION_FILE"]',
|
||||
)
|
||||
if not all(marker in launcher_source for marker in behavior_markers) or not (
|
||||
"def read_runtime_generation" in generation_source
|
||||
and "def bump_runtime_generation" in generation_source
|
||||
):
|
||||
raise RuntimeError("D4 precondition: pinned launcher lacks file-generation markers")
|
||||
return closure
|
||||
|
||||
|
||||
def reject_pinned_bytecode(pinned_directory: Path) -> None:
|
||||
cache_directory = pinned_directory / "__pycache__"
|
||||
if cache_directory.exists() or any(pinned_directory.rglob("*.pyc")):
|
||||
raise RuntimeError("D4 precondition: pinned bytecode cache is forbidden")
|
||||
|
||||
|
||||
def launch_verified_pi(
|
||||
launcher: Path,
|
||||
workspace: Path,
|
||||
sessions: Path,
|
||||
extension: Path,
|
||||
environment: dict[str, str],
|
||||
) -> PiRpc:
|
||||
command = [
|
||||
sys.executable,
|
||||
# -s preserves sys.path[0]=pinned/ for the launcher's sibling helper.
|
||||
"-s",
|
||||
"-S",
|
||||
"-B",
|
||||
str(launcher),
|
||||
"--runtime",
|
||||
"pi",
|
||||
"--",
|
||||
"pi",
|
||||
"--mode",
|
||||
"rpc",
|
||||
"--session-dir",
|
||||
str(sessions),
|
||||
"--no-extensions",
|
||||
"--no-context-files",
|
||||
"--no-prompt-templates",
|
||||
"--model",
|
||||
"openai-codex/gpt-5.6-sol",
|
||||
"--thinking",
|
||||
"medium",
|
||||
"--extension",
|
||||
str(extension),
|
||||
]
|
||||
reject_pinned_bytecode(launcher.parent)
|
||||
# This is deliberately the statement immediately before Popen (inside
|
||||
# PiRpc): the fixture-pinned launcher bytes are re-hashed then executed.
|
||||
if hashlib.sha256(launcher.read_bytes()).hexdigest() != GATED_LAUNCHER_SHA256:
|
||||
raise RuntimeError("D4 precondition: adjacent launcher hash mismatch")
|
||||
return PiRpc(command, workspace, environment)
|
||||
|
||||
|
||||
def launch_verified_broker(
|
||||
broker_path: Path,
|
||||
generation_path: Path,
|
||||
socket_path: Path,
|
||||
log_path: Path,
|
||||
environment: dict[str, str],
|
||||
) -> subprocess.Popen[str]:
|
||||
command = [
|
||||
sys.executable,
|
||||
"-I",
|
||||
"-S",
|
||||
"-B",
|
||||
str(broker_path),
|
||||
"--socket",
|
||||
str(socket_path),
|
||||
"--log",
|
||||
str(log_path),
|
||||
"--generation-module",
|
||||
str(generation_path),
|
||||
]
|
||||
reject_pinned_bytecode(broker_path.parent)
|
||||
if hashlib.sha256(broker_path.read_bytes()).hexdigest() != GATED_BROKER_SHA256:
|
||||
raise RuntimeError("D4 precondition: pinned broker hash mismatch")
|
||||
# The final helper re-hash is immediately adjacent to the broker Popen.
|
||||
if hashlib.sha256(generation_path.read_bytes()).hexdigest() != GATED_GENERATION_SHA256:
|
||||
raise RuntimeError("D4 precondition: pinned helper hash mismatch")
|
||||
return subprocess.Popen(
|
||||
command,
|
||||
env=environment,
|
||||
text=True,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.STDOUT,
|
||||
)
|
||||
|
||||
|
||||
def assert_d4(records: list[dict[str, Any]]) -> dict[str, object]:
|
||||
def record_where(description: str, candidates: list[dict[str, Any]]) -> dict[str, Any]:
|
||||
if not candidates:
|
||||
raise AssertionError(f"missing D4 evidence record: {description}")
|
||||
return candidates[0]
|
||||
|
||||
fixture_listen = record_where(
|
||||
"fixture listen", [r for r in records if r.get("event") == "listen"]
|
||||
)
|
||||
fixture_root = Path(fixture_listen["socket"]).parent
|
||||
lifecycle = [record for record in records if record.get("event") == "runtime_generation_bump"]
|
||||
state_bumps = [record for record in records if record.get("event") == "generation_state_bumped"]
|
||||
promotion = record_where(
|
||||
"fixture promotion", [r for r in records if r.get("event") == "probe_lease_promoted"]
|
||||
)
|
||||
launcher_registration = record_where(
|
||||
"lease anchor", [r for r in records if r.get("event") == "lease_anchor_registered"]
|
||||
)
|
||||
reload_revoke = record_where(
|
||||
"reload shutdown",
|
||||
[
|
||||
r
|
||||
for r in lifecycle
|
||||
if r.get("reason") == "reload" and r.get("phase") == "shutdown"
|
||||
],
|
||||
)
|
||||
reload_start = record_where(
|
||||
"reload start",
|
||||
[
|
||||
r
|
||||
for r in lifecycle
|
||||
if r.get("reason") == "reload" and r.get("phase") == "start"
|
||||
],
|
||||
)
|
||||
authorization = [
|
||||
record for record in records if record.get("event") == "generation_authorization"
|
||||
]
|
||||
current_generation = reload_start["new_generation"]
|
||||
current_authorization = record_where(
|
||||
"current-generation authorization",
|
||||
[r for r in authorization if r.get("requested_generation") == current_generation],
|
||||
)
|
||||
superseded_authorization = record_where(
|
||||
"superseded-generation authorization",
|
||||
[r for r in authorization if r.get("requested_generation") == current_generation - 1],
|
||||
)
|
||||
|
||||
identities = {
|
||||
(record["peercred"]["pid"], record["starttime_ticks"])
|
||||
for record in [*lifecycle, *state_bumps, promotion, launcher_registration, *authorization]
|
||||
}
|
||||
generations = [record["new_generation"] for record in lifecycle]
|
||||
file_records = [*lifecycle, *state_bumps, promotion, *authorization]
|
||||
observed_reasons = {record.get("reason") for record in lifecycle}
|
||||
checks = {
|
||||
"same_pid_starttime": len(identities) == 1,
|
||||
"strictly_increasing_generation": all(
|
||||
previous < current for previous, current in zip(generations, generations[1:])
|
||||
),
|
||||
"state_file_drives_lifecycle": [record["generation"] for record in state_bumps]
|
||||
== generations[1:],
|
||||
"state_file_source": all(
|
||||
record.get("generation_source") == "state-file" for record in file_records
|
||||
),
|
||||
"state_file_in_fixture_root": all(
|
||||
Path(record["generation_file"]).parent == fixture_root for record in file_records
|
||||
)
|
||||
and Path(launcher_registration["generation_file"]).parent == fixture_root,
|
||||
"all_lifecycle_boundaries": {"startup", "reload", "fork", "new", "resume"}
|
||||
<= observed_reasons,
|
||||
"lease_anchor_fixture": launcher_registration.get("session_id_shape") == "hex-256",
|
||||
"verified_revoked_on_reload": reload_revoke.get("prior_lease") == "VERIFIED"
|
||||
and reload_revoke.get("prior_lease_revoked") is True,
|
||||
"new_generation_unverified": current_authorization.get("code") == "MUTATOR_UNVERIFIED",
|
||||
"prior_generation_stale": superseded_authorization.get("code") == "STALE_GENERATION",
|
||||
}
|
||||
failed = [name for name, passed in checks.items() if not passed]
|
||||
if failed:
|
||||
raise AssertionError(f"D4 checks failed: {', '.join(failed)}")
|
||||
passed = all(checks.values())
|
||||
if not passed:
|
||||
raise AssertionError("D4 PASS derivation failed")
|
||||
|
||||
return {
|
||||
"machine_assertions": "PASS" if passed else "FAIL",
|
||||
"checks": checks,
|
||||
"same_pid_starttime": next(iter(identities)),
|
||||
"generations": generations,
|
||||
"reload_revoke_verified": checks["verified_revoked_on_reload"],
|
||||
"lease_anchor_fixture": checks["lease_anchor_fixture"],
|
||||
"file_backed_generation": checks["state_file_drives_lifecycle"],
|
||||
"new_generation_code": current_authorization["code"],
|
||||
"superseded_generation_code": superseded_authorization["code"],
|
||||
}
|
||||
|
||||
|
||||
def isolated_environment(
|
||||
root: Path, index: int, workspace: Path, socket_path: Path, pi_log: Path
|
||||
) -> dict[str, str]:
|
||||
"""Build a write-confined child environment; no inherited path variable survives."""
|
||||
|
||||
fixture_home = root / "home"
|
||||
fixture_config = root / "config"
|
||||
fixture_cache = root / "cache"
|
||||
fixture_state = root / "state"
|
||||
fixture_runtime = root / "runtime"
|
||||
fixture_tmp = root / "tmp"
|
||||
fixture_heartbeat = root / "heartbeat"
|
||||
fixture_mosaic_home = root / "mosaic-home"
|
||||
for directory in (
|
||||
fixture_home,
|
||||
fixture_config,
|
||||
fixture_cache,
|
||||
fixture_state,
|
||||
fixture_runtime,
|
||||
fixture_tmp,
|
||||
fixture_heartbeat,
|
||||
fixture_mosaic_home,
|
||||
):
|
||||
directory.mkdir(mode=0o700)
|
||||
|
||||
# Authentication/settings are copied into fixture HOME so Pi never writes
|
||||
# under the operator's HOME. They are not emitted or modified in place.
|
||||
source_agent = Path.home() / ".pi" / "agent"
|
||||
target_agent = fixture_home / ".pi" / "agent"
|
||||
target_agent.mkdir(parents=True, mode=0o700)
|
||||
for name in ("settings.json", "auth.json", "bin/fd"):
|
||||
source = source_agent / name
|
||||
target = target_agent / name
|
||||
if source.is_file():
|
||||
target.parent.mkdir(parents=True, mode=0o700)
|
||||
shutil.copy2(source, target)
|
||||
|
||||
environment = {
|
||||
"HOME": str(fixture_home),
|
||||
"XDG_CONFIG_HOME": str(fixture_config),
|
||||
"XDG_CACHE_HOME": str(fixture_cache),
|
||||
"XDG_STATE_HOME": str(fixture_state),
|
||||
"XDG_RUNTIME_DIR": str(fixture_runtime),
|
||||
"TMPDIR": str(fixture_tmp),
|
||||
"PATH": os.environ.get("PATH", ""),
|
||||
"LANG": os.environ.get("LANG", "C.UTF-8"),
|
||||
"TERM": os.environ.get("TERM", "dumb"),
|
||||
"D4_GENERATION_SOCKET": str(socket_path),
|
||||
"MOSAIC_LEASE_BROKER_SOCKET": str(socket_path),
|
||||
"D4_PI_LOG": str(pi_log),
|
||||
"MOSAIC_AGENT_NAME": f"d4-fixture-{index}",
|
||||
"MOSAIC_AGENT_WORKDIR": str(workspace),
|
||||
"MOSAIC_HEARTBEAT_RUN_DIR": str(fixture_heartbeat),
|
||||
"MOSAIC_HOME": str(fixture_mosaic_home),
|
||||
"MOSAIC_PI_FORCE_SKILLS": "",
|
||||
"PI_SKIP_VERSION_CHECK": "1",
|
||||
"PYTHONDONTWRITEBYTECODE": "1",
|
||||
"PYTHONNOUSERSITE": "1",
|
||||
}
|
||||
if "PI_CODING_AGENT" in os.environ:
|
||||
environment["PI_CODING_AGENT"] = os.environ["PI_CODING_AGENT"]
|
||||
return environment
|
||||
|
||||
|
||||
def scrub_fixture_credentials(root: Path) -> None:
|
||||
"""Remove the copied Pi credential/config subtree before retaining evidence."""
|
||||
|
||||
copied_agent = root / "home" / ".pi" / "agent"
|
||||
if copied_agent.exists():
|
||||
shutil.rmtree(copied_agent)
|
||||
if copied_agent.exists():
|
||||
raise RuntimeError("D4 credential scrub failed")
|
||||
|
||||
|
||||
def run_once(index: int) -> Path:
|
||||
root = Path(tempfile.mkdtemp(prefix=f"gate0-d4-{index}-"))
|
||||
workspace = root / "workspace"
|
||||
sessions = root / "sessions"
|
||||
workspace.mkdir(mode=0o700)
|
||||
sessions.mkdir(mode=0o700)
|
||||
socket_path = root / "generation.sock"
|
||||
generation_log = root / "generation.jsonl"
|
||||
pi_log = root / "pi.jsonl"
|
||||
extension = root / "d4_extension.ts"
|
||||
write_extension(extension)
|
||||
broker: subprocess.Popen[str] | None = None
|
||||
pi: PiRpc | None = None
|
||||
|
||||
try:
|
||||
environment = isolated_environment(root, index, workspace, socket_path, pi_log)
|
||||
# Must run before the fixture broker or Pi process is launched. It proves
|
||||
# the launcher registers before exec and can only read this fixture socket.
|
||||
closure = gated_launcher_precondition(root, socket_path, environment)
|
||||
broker = launch_verified_broker(
|
||||
closure.broker, closure.generation, socket_path, generation_log, environment
|
||||
)
|
||||
wait_path(socket_path)
|
||||
pi = launch_verified_pi(closure.launcher, workspace, sessions, extension, environment)
|
||||
pi.send({"id": "state", "type": "get_state"})
|
||||
state = pi.response("state")
|
||||
original_session = state["data"]["sessionFile"]
|
||||
pi.prompt_and_settle(
|
||||
"fixture-promote",
|
||||
"Call d4_fixture_promote exactly once, then stop.",
|
||||
)
|
||||
pi.send({"id": "reload", "type": "prompt", "message": "/d4-reload"})
|
||||
reload_response = pi.response("reload")
|
||||
if not reload_response.get("success"):
|
||||
raise RuntimeError(f"reload failed: {reload_response}")
|
||||
for request_id, request_payload in [
|
||||
("clone", {"id": "clone", "type": "clone"}),
|
||||
("new", {"id": "new", "type": "new_session"}),
|
||||
(
|
||||
"resume",
|
||||
{"id": "resume", "type": "switch_session", "sessionPath": original_session},
|
||||
),
|
||||
]:
|
||||
pi.send(request_payload)
|
||||
response = pi.response(request_id)
|
||||
if not response.get("success") or response.get("data", {}).get("cancelled"):
|
||||
raise RuntimeError(f"{request_id} failed: {response}")
|
||||
results = assert_d4(jsonl(generation_log))
|
||||
verdict = results.get("machine_assertions")
|
||||
if verdict != "PASS":
|
||||
raise RuntimeError(f"D4 checks did not derive PASS: {verdict}")
|
||||
(root / "machine-assertions.json").write_text(
|
||||
json.dumps(results, sort_keys=True, indent=2) + "\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
print(f"run={index} evidence_dir={root}")
|
||||
print(f"machine_assertions={verdict}")
|
||||
print(json.dumps(results, sort_keys=True))
|
||||
except Exception as error:
|
||||
(root / "machine-assertions.json").write_text(
|
||||
json.dumps({"error": f"{type(error).__name__}: {error}"}, sort_keys=True, indent=2)
|
||||
+ "\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
print(f"run={index} evidence_dir={root}")
|
||||
print("machine_assertions=FAIL")
|
||||
print(f"error={type(error).__name__}: {error}")
|
||||
raise
|
||||
finally:
|
||||
try:
|
||||
try:
|
||||
if pi is not None:
|
||||
pi.close()
|
||||
finally:
|
||||
if broker is not None:
|
||||
try:
|
||||
request(socket_path, {"action": "shutdown-broker"})
|
||||
except OSError:
|
||||
pass
|
||||
try:
|
||||
broker.wait(timeout=5)
|
||||
except subprocess.TimeoutExpired:
|
||||
broker.kill()
|
||||
broker.wait()
|
||||
finally:
|
||||
scrub_fixture_credentials(root)
|
||||
return root
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("--runs", type=int, default=3, choices=(3,))
|
||||
args = parser.parse_args()
|
||||
roots: list[Path] = []
|
||||
for index in range(1, args.runs + 1):
|
||||
roots.append(run_once(index))
|
||||
print("d4_isolation_runs=" + ",".join(str(root) for root in roots))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,215 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""P3 broker prototype: peercred-keyed runtime_generation and lease revocation."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import importlib.util
|
||||
import json
|
||||
import os
|
||||
import secrets
|
||||
import socket
|
||||
import struct
|
||||
from collections.abc import Callable, Mapping
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
|
||||
def proc_starttime(pid: int) -> int:
|
||||
text = Path(f"/proc/{pid}/stat").read_text()
|
||||
close = text.rfind(")")
|
||||
return int(text[close + 2 :].split()[19])
|
||||
|
||||
|
||||
def emit(log: Path, value: dict[str, Any]) -> None:
|
||||
with log.open("a", encoding="utf-8") as out:
|
||||
out.write(json.dumps(value, sort_keys=True) + "\n")
|
||||
|
||||
|
||||
def load_generation_functions(
|
||||
path: Path,
|
||||
) -> tuple[Callable[[Mapping[str, str]], int], Callable[[Mapping[str, str]], int]]:
|
||||
spec = importlib.util.spec_from_file_location("d4_lease_generation", path)
|
||||
if spec is None or spec.loader is None:
|
||||
raise ValueError("generation module is unavailable")
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(module)
|
||||
reader = getattr(module, "read_runtime_generation", None)
|
||||
bumper = getattr(module, "bump_runtime_generation", None)
|
||||
if not callable(reader) or not callable(bumper):
|
||||
raise ValueError("generation module has no read/bump functions")
|
||||
return reader, bumper
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--socket", required=True)
|
||||
parser.add_argument("--log", required=True)
|
||||
parser.add_argument("--generation-module", required=True, type=Path)
|
||||
ns = parser.parse_args()
|
||||
socket_path = Path(ns.socket)
|
||||
log_path = Path(ns.log)
|
||||
read_runtime_generation, bump_runtime_generation = load_generation_functions(
|
||||
ns.generation_module
|
||||
)
|
||||
socket_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
os.chmod(socket_path.parent, 0o700)
|
||||
socket_path.unlink(missing_ok=True)
|
||||
log_path.unlink(missing_ok=True)
|
||||
|
||||
server = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
||||
server.bind(str(socket_path))
|
||||
os.chmod(socket_path, 0o600)
|
||||
server.listen(8)
|
||||
generations: dict[tuple[int, int], int] = {}
|
||||
lease_state: dict[tuple[int, int], str] = {}
|
||||
# The gated launcher registers its own exec-preserved PID here. This is
|
||||
# deliberately volatile fixture state; nothing is written outside root.
|
||||
launcher_sessions: dict[tuple[int, int], str] = {}
|
||||
generation_files: dict[tuple[int, int], Path] = {}
|
||||
|
||||
def generation_environment(identity: tuple[int, int]) -> dict[str, str]:
|
||||
state_path = generation_files.get(identity)
|
||||
if state_path is None or state_path.parent != socket_path.parent:
|
||||
raise ValueError("generation file is outside the fixture root")
|
||||
return {"MOSAIC_LEASE_GENERATION_FILE": str(state_path)}
|
||||
|
||||
def file_generation(identity: tuple[int, int]) -> int:
|
||||
return read_runtime_generation(generation_environment(identity))
|
||||
|
||||
emit(log_path, {"event": "listen", "pid": os.getpid(), "socket": str(socket_path)})
|
||||
|
||||
while True:
|
||||
conn, _ = server.accept()
|
||||
with conn:
|
||||
raw = conn.getsockopt(socket.SOL_SOCKET, socket.SO_PEERCRED, 12)
|
||||
pid, uid, gid = struct.unpack("3i", raw)
|
||||
starttime = proc_starttime(pid)
|
||||
request = json.loads(conn.makefile("r", encoding="utf-8").readline())
|
||||
if request.get("action") == "shutdown-broker":
|
||||
conn.sendall(b'{"ok":true}\n')
|
||||
break
|
||||
identity = (pid, starttime)
|
||||
if request.get("action") == "register_anchor":
|
||||
generation = request.get("runtime_generation")
|
||||
if type(generation) is not int or generation < 0:
|
||||
conn.sendall(b'{"ok":false,"code":"INVALID_GENERATION"}\n')
|
||||
continue
|
||||
session_id = launcher_sessions.setdefault(identity, secrets.token_hex(32))
|
||||
generation_file = socket_path.parent / f"generation-{session_id}.state"
|
||||
generation_files[identity] = generation_file
|
||||
record = {
|
||||
"event": "lease_anchor_registered",
|
||||
"peercred": {"pid": pid, "uid": uid, "gid": gid},
|
||||
"starttime_ticks": starttime,
|
||||
"runtime_generation": generation,
|
||||
"session_id_shape": "hex-256",
|
||||
"generation_file": str(generation_file),
|
||||
}
|
||||
emit(log_path, record)
|
||||
reply = {
|
||||
"ok": True,
|
||||
"session_id": session_id,
|
||||
"peer": {"pid": pid, "uid": uid, "gid": gid, "starttime": str(starttime)},
|
||||
}
|
||||
conn.sendall((json.dumps(reply, sort_keys=True) + "\n").encode())
|
||||
continue
|
||||
# The D4 extension requests this at each post-start lifecycle
|
||||
# boundary; the exact WI-3 helper mutates the launcher-created file.
|
||||
if request.get("action") == "bump-generation":
|
||||
generation = bump_runtime_generation(generation_environment(identity))
|
||||
record = {
|
||||
"event": "generation_state_bumped",
|
||||
"peercred": {"pid": pid, "uid": uid, "gid": gid},
|
||||
"starttime_ticks": starttime,
|
||||
"generation": generation,
|
||||
"generation_file": str(generation_files[identity]),
|
||||
"generation_source": "state-file",
|
||||
}
|
||||
emit(log_path, record)
|
||||
conn.sendall((json.dumps(record, sort_keys=True) + "\n").encode())
|
||||
continue
|
||||
if request.get("action") == "promote-probe":
|
||||
generation = file_generation(identity)
|
||||
lease_state[identity] = "VERIFIED"
|
||||
record = {
|
||||
"event": "probe_lease_promoted",
|
||||
"peercred": {"pid": pid, "uid": uid, "gid": gid},
|
||||
"starttime_ticks": starttime,
|
||||
"generation": generation,
|
||||
"generation_file": str(generation_files[identity]),
|
||||
"generation_source": "state-file",
|
||||
"new_lease_state": "VERIFIED",
|
||||
}
|
||||
emit(log_path, record)
|
||||
conn.sendall((json.dumps(record, sort_keys=True) + "\n").encode())
|
||||
continue
|
||||
# D4 fixture-only authorization observation. It exposes the broker's
|
||||
# current versus superseded generation disposition without changing it.
|
||||
if request.get("action") == "authorize-probe":
|
||||
generation = request.get("generation")
|
||||
if type(generation) is not int or generation < 0:
|
||||
conn.sendall(b'{"ok":false,"code":"INVALID_GENERATION"}\n')
|
||||
continue
|
||||
current_generation = file_generation(identity)
|
||||
current_lease = lease_state.get(identity, "NONE")
|
||||
if generation < current_generation:
|
||||
code = "STALE_GENERATION"
|
||||
elif generation > current_generation:
|
||||
code = "FUTURE_GENERATION"
|
||||
elif current_lease != "VERIFIED":
|
||||
code = "MUTATOR_UNVERIFIED"
|
||||
else:
|
||||
code = "ALLOW"
|
||||
record = {
|
||||
"event": "generation_authorization",
|
||||
"peercred": {"pid": pid, "uid": uid, "gid": gid},
|
||||
"starttime_ticks": starttime,
|
||||
"requested_generation": generation,
|
||||
"current_generation": current_generation,
|
||||
"generation_file": str(generation_files[identity]),
|
||||
"generation_source": "state-file",
|
||||
"lease_state": current_lease,
|
||||
"ok": code == "ALLOW",
|
||||
"code": code,
|
||||
}
|
||||
emit(log_path, record)
|
||||
conn.sendall((json.dumps(record, sort_keys=True) + "\n").encode())
|
||||
continue
|
||||
if request.get("action") != "lifecycle":
|
||||
conn.sendall(b'{"ok":false,"reason":"invalid-action"}\n')
|
||||
continue
|
||||
|
||||
old_generation = generations.get(identity, 0)
|
||||
old_lease = lease_state.get(identity, "NONE")
|
||||
new_generation = file_generation(identity)
|
||||
if new_generation <= old_generation:
|
||||
conn.sendall(b'{"ok":false,"code":"NON_MONOTONIC_STATE_FILE"}\n')
|
||||
continue
|
||||
generations[identity] = new_generation
|
||||
# Every lifecycle boundary revokes first. A start establishes a new
|
||||
# UNVERIFIED incarnation; it never inherits prior VERIFIED state.
|
||||
lease_state[identity] = "UNVERIFIED" if request.get("phase") == "start" else "REVOKED"
|
||||
record = {
|
||||
"event": "runtime_generation_bump",
|
||||
"peercred": {"pid": pid, "uid": uid, "gid": gid},
|
||||
"starttime_ticks": starttime,
|
||||
"phase": request.get("phase"),
|
||||
"reason": request.get("reason"),
|
||||
"old_generation": old_generation,
|
||||
"new_generation": new_generation,
|
||||
"generation_file": str(generation_files[identity]),
|
||||
"generation_source": "state-file",
|
||||
"prior_lease": old_lease,
|
||||
"prior_lease_revoked": True,
|
||||
"new_lease_state": lease_state[identity],
|
||||
}
|
||||
emit(log_path, record)
|
||||
conn.sendall((json.dumps(record, sort_keys=True) + "\n").encode())
|
||||
|
||||
server.close()
|
||||
socket_path.unlink(missing_ok=True)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,97 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Gate0 P4: exercise Linux SO_PEERCRED and correlate it to /proc."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import socket
|
||||
import stat
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def proc_identity(pid: int) -> dict[str, int | str]:
|
||||
stat_text = Path(f"/proc/{pid}/stat").read_text()
|
||||
close = stat_text.rfind(")")
|
||||
fields = stat_text[close + 2 :].split()
|
||||
# fields[0] is field 3 (state); ppid is field 4 and starttime is field 22.
|
||||
return {
|
||||
"pid": pid,
|
||||
"ppid": int(fields[1]),
|
||||
"starttime_ticks": int(fields[19]),
|
||||
"uid": int(Path(f"/proc/{pid}/status").read_text().split("Uid:", 1)[1].split()[0]),
|
||||
"exe": os.readlink(f"/proc/{pid}/exe"),
|
||||
}
|
||||
|
||||
|
||||
def main() -> None:
|
||||
with tempfile.TemporaryDirectory(prefix="gate0-p4-") as tmp:
|
||||
root = Path(tmp)
|
||||
os.chmod(root, 0o700)
|
||||
socket_path = root / "broker.sock"
|
||||
server = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
||||
server.bind(str(socket_path))
|
||||
os.chmod(socket_path, 0o600)
|
||||
server.listen(1)
|
||||
|
||||
child = os.fork()
|
||||
if child == 0:
|
||||
client = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
||||
client.connect(str(socket_path))
|
||||
identity = proc_identity(os.getpid())
|
||||
client.sendall((json.dumps(identity, sort_keys=True) + "\n").encode())
|
||||
# Keep /proc/<pid> alive until the server has correlated peercred.
|
||||
if client.recv(2) != b"OK":
|
||||
os._exit(2)
|
||||
client.close()
|
||||
os._exit(0)
|
||||
|
||||
conn, _ = server.accept()
|
||||
raw = conn.getsockopt(socket.SOL_SOCKET, socket.SO_PEERCRED, 12)
|
||||
peer_pid = int.from_bytes(raw[0:4], byteorder="little", signed=True)
|
||||
peer_uid = int.from_bytes(raw[4:8], byteorder="little", signed=True)
|
||||
peer_gid = int.from_bytes(raw[8:12], byteorder="little", signed=True)
|
||||
claimed = json.loads(conn.makefile("r", encoding="utf-8").readline())
|
||||
observed = proc_identity(peer_pid)
|
||||
conn.sendall(b"OK")
|
||||
_, status = os.waitpid(child, 0)
|
||||
|
||||
root_mode = stat.S_IMODE(root.stat().st_mode)
|
||||
socket_mode = stat.S_IMODE(socket_path.stat().st_mode)
|
||||
if not (
|
||||
peer_pid == claimed["pid"] == observed["pid"]
|
||||
and peer_uid == claimed["uid"] == observed["uid"]
|
||||
and claimed["starttime_ticks"] == observed["starttime_ticks"]
|
||||
and root_mode == 0o700
|
||||
and socket_mode == 0o600
|
||||
and os.waitstatus_to_exitcode(status) == 0
|
||||
):
|
||||
raise AssertionError("SO_PEERCRED, /proc identity, or socket-mode correlation failed")
|
||||
print("machine_assertions=PASS")
|
||||
print(f"server_pid={os.getpid()} server_uid={os.getuid()} server_gid={os.getgid()}")
|
||||
print(f"socket_path={socket_path}")
|
||||
print(f"directory_mode={root_mode:04o} socket_mode={socket_mode:04o}")
|
||||
print(f"SO_PEERCRED pid={peer_pid} uid={peer_uid} gid={peer_gid}")
|
||||
print("client_claim=" + json.dumps(claimed, sort_keys=True))
|
||||
print("proc_observed=" + json.dumps(observed, sort_keys=True))
|
||||
print(f"pid_match={peer_pid == claimed['pid'] == observed['pid']}")
|
||||
print(f"uid_match={peer_uid == claimed['uid'] == observed['uid']}")
|
||||
print(
|
||||
"starttime_match="
|
||||
+ str(claimed["starttime_ticks"] == observed["starttime_ticks"])
|
||||
)
|
||||
print(f"client_exit_status={os.waitstatus_to_exitcode(status)}")
|
||||
print("same_principal_socket=true")
|
||||
print(
|
||||
"posture=0700 parent + 0600 socket excludes other UIDs, but does not prevent "
|
||||
"the same UID from unlinking/rebinding; distinct-principal system service remains "
|
||||
"required for a claim stronger than T-C against same-UID counterfeit replacement"
|
||||
)
|
||||
|
||||
conn.close()
|
||||
server.close()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,54 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Claude SessionStart additionalContext producer for P6 observation."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
BLOCK = "\n".join(
|
||||
[
|
||||
"GATE0_CLAUDE_ATOMIC_BEGIN",
|
||||
"segment-01=alpha-2d11",
|
||||
"segment-02=middle-8e22",
|
||||
"segment-03=omega-4f33",
|
||||
"GATE0_CLAUDE_ATOMIC_END",
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
def starttime(pid: int) -> int:
|
||||
text = Path(f"/proc/{pid}/stat").read_text()
|
||||
return int(text[text.rfind(")") + 2 :].split()[19])
|
||||
|
||||
|
||||
def main() -> None:
|
||||
hook_input = json.load(sys.stdin)
|
||||
log = Path(os.environ["GATE0_CLAUDE_HOOK_LOG"])
|
||||
record = {
|
||||
"hook_event_name": hook_input.get("hook_event_name"),
|
||||
"pid": os.getpid(),
|
||||
"ppid": os.getppid(),
|
||||
"starttime_ticks": starttime(os.getpid()),
|
||||
"block_length": len(BLOCK.encode()),
|
||||
"block_sha256": hashlib.sha256(BLOCK.encode()).hexdigest(),
|
||||
"emission": "one hookSpecificOutput.additionalContext string field",
|
||||
}
|
||||
log.write_text(json.dumps(record, sort_keys=True) + "\n")
|
||||
print(
|
||||
json.dumps(
|
||||
{
|
||||
"hookSpecificOutput": {
|
||||
"hookEventName": "SessionStart",
|
||||
"additionalContext": BLOCK,
|
||||
}
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,145 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Run real Claude 2.1.x through `mosaic yolo` for P6 observation."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
HERE = Path(__file__).resolve().parent
|
||||
BLOCK = "\n".join(
|
||||
[
|
||||
"GATE0_CLAUDE_ATOMIC_BEGIN",
|
||||
"segment-01=alpha-2d11",
|
||||
"segment-02=middle-8e22",
|
||||
"segment-03=omega-4f33",
|
||||
"GATE0_CLAUDE_ATOMIC_END",
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
def strings(value: Any):
|
||||
if isinstance(value, str):
|
||||
yield value
|
||||
elif isinstance(value, list):
|
||||
for item in value:
|
||||
yield from strings(item)
|
||||
elif isinstance(value, dict):
|
||||
for item in value.values():
|
||||
yield from strings(item)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
with tempfile.TemporaryDirectory(prefix="gate0-p6-claude-") as temp:
|
||||
root = Path(temp)
|
||||
workspace = root / "workspace"
|
||||
workspace.mkdir()
|
||||
settings = root / "settings.json"
|
||||
hook_log = root / "hook.jsonl"
|
||||
settings.write_text(
|
||||
json.dumps(
|
||||
{
|
||||
"hooks": {
|
||||
"SessionStart": [
|
||||
{
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": f'python3 "{HERE / "p6_claude_hook.py"}"',
|
||||
"timeout": 20,
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
)
|
||||
)
|
||||
env = os.environ.copy()
|
||||
env["GATE0_CLAUDE_HOOK_LOG"] = str(hook_log)
|
||||
command = [
|
||||
"mosaic",
|
||||
"yolo",
|
||||
"claude",
|
||||
"--settings",
|
||||
str(settings),
|
||||
"--model",
|
||||
"haiku",
|
||||
"--print",
|
||||
"--output-format",
|
||||
"stream-json",
|
||||
"--verbose",
|
||||
"--include-hook-events",
|
||||
"--max-budget-usd",
|
||||
"0.10",
|
||||
"Return only the exact full GATE0_CLAUDE_ATOMIC_BEGIN through GATE0_CLAUDE_ATOMIC_END block injected by SessionStart, with no code fence or commentary.",
|
||||
]
|
||||
result = subprocess.run(
|
||||
command,
|
||||
cwd=workspace,
|
||||
env=env,
|
||||
stdin=subprocess.DEVNULL,
|
||||
text=True,
|
||||
capture_output=True,
|
||||
timeout=150,
|
||||
check=False,
|
||||
)
|
||||
events: list[dict[str, Any]] = []
|
||||
for line in result.stdout.splitlines():
|
||||
try:
|
||||
events.append(json.loads(line))
|
||||
except json.JSONDecodeError:
|
||||
continue
|
||||
hook_events = [
|
||||
event
|
||||
for event in events
|
||||
if event.get("type") == "system"
|
||||
and event.get("subtype") in {"hook_started", "hook_response"}
|
||||
]
|
||||
full_matches = [text for event in events for text in strings(event) if BLOCK in text]
|
||||
exact_matches = [text for event in events for text in strings(event) if text == BLOCK]
|
||||
assistant_texts: list[str] = []
|
||||
for event in events:
|
||||
if event.get("type") != "assistant":
|
||||
continue
|
||||
for text in strings(event.get("message", {})):
|
||||
if "GATE0_CLAUDE_ATOMIC_BEGIN" in text:
|
||||
assistant_texts.append(text)
|
||||
|
||||
if result.returncode != 0:
|
||||
raise AssertionError(f"Claude probe exited {result.returncode}")
|
||||
if not any(event.get("subtype") == "hook_response" and event.get("outcome") == "success" for event in hook_events):
|
||||
raise AssertionError("Claude SessionStart hook did not complete successfully")
|
||||
if BLOCK not in exact_matches:
|
||||
raise AssertionError("Claude did not return an exact full-block field")
|
||||
|
||||
print("$ python3 docs/compaction-refresh/probes/p6_claude_run.py")
|
||||
print("machine_assertions=PASS")
|
||||
print("command=mosaic yolo claude --settings <isolated> --model haiku --print --output-format stream-json --verbose --include-hook-events <prompt>")
|
||||
print("claude_version=" + subprocess.check_output(["claude", "--version"], text=True).strip())
|
||||
print("mosaic_version=" + subprocess.check_output(["mosaic", "--version"], text=True).strip())
|
||||
print(f"exit_code={result.returncode}")
|
||||
print("hook_process_log=" + hook_log.read_text().strip())
|
||||
for event in hook_events:
|
||||
print("hook_stream_event=" + json.dumps(event, sort_keys=True))
|
||||
print(f"block_length={len(BLOCK.encode())}")
|
||||
print(f"block_sha256={hashlib.sha256(BLOCK.encode()).hexdigest()}")
|
||||
print(f"stream_fields_containing_full_block={len(full_matches)}")
|
||||
print(f"stream_fields_exactly_equal_block={len(exact_matches)}")
|
||||
for text in assistant_texts:
|
||||
print(f"assistant_copy_length={len(text.encode())}")
|
||||
print(f"assistant_copy_sha256={hashlib.sha256(text.encode()).hexdigest()}")
|
||||
print(f"assistant_copy_exact={text == BLOCK}")
|
||||
print("assistant_copy=" + json.dumps(text))
|
||||
if result.stderr.strip():
|
||||
print("stderr_excerpt=" + json.dumps(result.stderr.splitlines()[:10]))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,350 +0,0 @@
|
||||
import type { ExtensionAPI } from '@earendil-works/pi-coding-agent';
|
||||
import { Type } from 'typebox';
|
||||
import { createHash, randomUUID } from 'node:crypto';
|
||||
import { readFileSync, appendFileSync, statSync } from 'node:fs';
|
||||
import net from 'node:net';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { resolve } from 'node:path';
|
||||
|
||||
const SELF = resolve(fileURLToPath(import.meta.url).split('?')[0]!);
|
||||
const LOG = process.env['GATE0_PI_LOG'];
|
||||
const CONTEXT_BLOCK =
|
||||
process.env['GATE0_PI_CONTEXT_BLOCK'] ??
|
||||
[
|
||||
'GATE0_PI_ATOMIC_BEGIN',
|
||||
'segment-01=alpha-7e31',
|
||||
'segment-02=middle-9c42',
|
||||
'segment-03=omega-5b83',
|
||||
'GATE0_PI_ATOMIC_END',
|
||||
].join('\n');
|
||||
|
||||
let sequence = 0;
|
||||
|
||||
function sha(value: string | Buffer): string {
|
||||
return createHash('sha256').update(value).digest('hex');
|
||||
}
|
||||
|
||||
function procStarttime(): number {
|
||||
const text = readFileSync(`/proc/${process.pid}/stat`, 'utf8');
|
||||
const close = text.lastIndexOf(')');
|
||||
const fields = text.slice(close + 2).trim().split(/\s+/);
|
||||
return Number(fields[19]);
|
||||
}
|
||||
|
||||
function log(event: string, details: Record<string, unknown> = {}): void {
|
||||
if (!LOG) return;
|
||||
sequence += 1;
|
||||
appendFileSync(
|
||||
LOG,
|
||||
`${JSON.stringify({ seq: sequence, event, pid: process.pid, starttime_ticks: procStarttime(), ...details })}\n`,
|
||||
);
|
||||
}
|
||||
|
||||
function argvExtensions(): string[] {
|
||||
const result: string[] = [];
|
||||
for (let i = 0; i < process.argv.length; i += 1) {
|
||||
if (process.argv[i] === '--extension' || process.argv[i] === '-e') {
|
||||
const candidate = process.argv[i + 1];
|
||||
if (candidate) result.push(resolve(candidate));
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
interface SourceValidation {
|
||||
ok: boolean;
|
||||
reason: string;
|
||||
fragment?: string;
|
||||
}
|
||||
|
||||
function validateSources(): SourceValidation {
|
||||
const manifestPath = process.env['GATE0_SOURCE_MANIFEST'];
|
||||
if (!manifestPath) return { ok: true, reason: 'no-manifest-probe-disabled' };
|
||||
try {
|
||||
const manifest = JSON.parse(readFileSync(manifestPath, 'utf8')) as {
|
||||
maxBytes: number;
|
||||
fragments: Array<{ path: string; sha256: string }>;
|
||||
};
|
||||
for (const fragment of manifest.fragments) {
|
||||
let fileStat;
|
||||
try {
|
||||
fileStat = statSync(fragment.path);
|
||||
} catch {
|
||||
return { ok: false, reason: 'missing', fragment: fragment.path };
|
||||
}
|
||||
if (!fileStat.isFile()) {
|
||||
return { ok: false, reason: 'not-regular-file', fragment: fragment.path };
|
||||
}
|
||||
if (fileStat.size > manifest.maxBytes) {
|
||||
return { ok: false, reason: 'oversize', fragment: fragment.path };
|
||||
}
|
||||
const bytes = readFileSync(fragment.path);
|
||||
if (sha(bytes) !== fragment.sha256) {
|
||||
return { ok: false, reason: 'hash-mismatch', fragment: fragment.path };
|
||||
}
|
||||
}
|
||||
return { ok: true, reason: 'all-fragments-valid' };
|
||||
} catch (error) {
|
||||
return { ok: false, reason: `manifest-error:${error instanceof Error ? error.name : 'unknown'}` };
|
||||
}
|
||||
}
|
||||
|
||||
function brokerRequest(payload: Record<string, unknown>): Promise<Record<string, unknown>> {
|
||||
const socketPath = process.env['GATE0_GENERATION_SOCKET'];
|
||||
if (!socketPath) return Promise.resolve({ skipped: true });
|
||||
return new Promise((resolvePromise, reject) => {
|
||||
const socket = net.createConnection(socketPath);
|
||||
let buffer = '';
|
||||
socket.setEncoding('utf8');
|
||||
socket.on('connect', () => socket.write(`${JSON.stringify(payload)}\n`));
|
||||
socket.on('data', (chunk) => {
|
||||
buffer += chunk;
|
||||
const newline = buffer.indexOf('\n');
|
||||
if (newline < 0) return;
|
||||
socket.end();
|
||||
resolvePromise(JSON.parse(buffer.slice(0, newline)) as Record<string, unknown>);
|
||||
});
|
||||
socket.on('error', reject);
|
||||
});
|
||||
}
|
||||
|
||||
function markerPaths(value: unknown, path = '$'): string[] {
|
||||
const matches: string[] = [];
|
||||
if (typeof value === 'string') {
|
||||
if (value.includes(CONTEXT_BLOCK)) matches.push(path);
|
||||
return matches;
|
||||
}
|
||||
if (Array.isArray(value)) {
|
||||
value.forEach((item, index) => matches.push(...markerPaths(item, `${path}[${index}]`)));
|
||||
return matches;
|
||||
}
|
||||
if (value && typeof value === 'object') {
|
||||
for (const [key, item] of Object.entries(value as Record<string, unknown>)) {
|
||||
matches.push(...markerPaths(item, `${path}.${key}`));
|
||||
}
|
||||
}
|
||||
return matches;
|
||||
}
|
||||
|
||||
function assistantToolIds(message: unknown): string[] {
|
||||
if (!message || typeof message !== 'object') return [];
|
||||
const candidate = message as { role?: string; content?: unknown };
|
||||
if (candidate.role !== 'assistant' || !Array.isArray(candidate.content)) return [];
|
||||
return candidate.content
|
||||
.filter(
|
||||
(block): block is { type: 'toolCall'; id: string } =>
|
||||
Boolean(
|
||||
block &&
|
||||
typeof block === 'object' &&
|
||||
(block as { type?: string }).type === 'toolCall' &&
|
||||
typeof (block as { id?: unknown }).id === 'string',
|
||||
),
|
||||
)
|
||||
.map((block) => block.id);
|
||||
}
|
||||
|
||||
function assistantText(message: unknown): string {
|
||||
if (!message || typeof message !== 'object') return '';
|
||||
const candidate = message as { role?: string; content?: unknown };
|
||||
if (candidate.role !== 'assistant' || !Array.isArray(candidate.content)) return '';
|
||||
return candidate.content
|
||||
.filter(
|
||||
(block): block is { type: 'text'; text: string } =>
|
||||
Boolean(
|
||||
block &&
|
||||
typeof block === 'object' &&
|
||||
(block as { type?: string }).type === 'text' &&
|
||||
typeof (block as { text?: unknown }).text === 'string',
|
||||
),
|
||||
)
|
||||
.map((block) => block.text)
|
||||
.join('');
|
||||
}
|
||||
|
||||
export default function register(pi: ExtensionAPI) {
|
||||
const localProviderUrl = process.env['GATE0_LOCAL_PROVIDER_URL'];
|
||||
if (localProviderUrl) {
|
||||
pi.registerProvider('gate0-local', {
|
||||
baseUrl: localProviderUrl,
|
||||
apiKey: 'gate0-probe-not-a-secret',
|
||||
api: 'openai-completions',
|
||||
models: [
|
||||
{
|
||||
id: 'gate0-model',
|
||||
name: 'Gate0 deterministic local model',
|
||||
reasoning: false,
|
||||
input: ['text'],
|
||||
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
||||
contextWindow: 32_000,
|
||||
maxTokens: 1_024,
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
const extensions = argvExtensions();
|
||||
const lastPosition = extensions.length > 0 && extensions.at(-1) === SELF;
|
||||
interface RequestCycle {
|
||||
nonce: string;
|
||||
verified: boolean;
|
||||
sourceReason: string;
|
||||
}
|
||||
let buildingCycle: RequestCycle | undefined;
|
||||
const inFlightCycles: RequestCycle[] = [];
|
||||
const toolNonce = new Map<string, { nonce: string; verified: boolean; sourceReason: string }>();
|
||||
|
||||
pi.on('session_start', async (event) => {
|
||||
const broker = await brokerRequest({ action: 'lifecycle', phase: 'start', reason: event.reason });
|
||||
log('session_start', {
|
||||
reason: event.reason,
|
||||
extensions,
|
||||
self: SELF,
|
||||
lastPosition,
|
||||
gateState: lastPosition ? 'UNVERIFIED_READY' : 'CLOSED_NOT_LAST',
|
||||
broker,
|
||||
});
|
||||
});
|
||||
|
||||
pi.on('session_shutdown', async (event) => {
|
||||
const broker = await brokerRequest({ action: 'lifecycle', phase: 'shutdown', reason: event.reason });
|
||||
log('session_shutdown', { reason: event.reason, broker });
|
||||
});
|
||||
|
||||
pi.on('context', async (event) => {
|
||||
const validation = validateSources();
|
||||
buildingCycle = {
|
||||
nonce: randomUUID(),
|
||||
sourceReason: validation.reason,
|
||||
verified: lastPosition && validation.ok,
|
||||
};
|
||||
const inputJson = JSON.stringify(event.messages);
|
||||
const injected = {
|
||||
role: 'custom' as const,
|
||||
customType: 'gate0-context',
|
||||
content: CONTEXT_BLOCK,
|
||||
display: false,
|
||||
timestamp: Date.now(),
|
||||
};
|
||||
const outputMessages = buildingCycle.verified
|
||||
? [...event.messages, injected]
|
||||
: [...event.messages];
|
||||
const outputPrefix = outputMessages.slice(0, event.messages.length);
|
||||
const sourceBroker = validation.ok
|
||||
? { action: 'none', reason: 'source-valid' }
|
||||
: await brokerRequest({ action: 'source-invalid', reason: validation.reason });
|
||||
log('context_return', {
|
||||
requestNonce: buildingCycle.nonce,
|
||||
sourceValidation: validation,
|
||||
sourceBroker,
|
||||
lastPosition,
|
||||
promotion: false,
|
||||
injectionDecision: buildingCycle.verified ? 'ONE_ATOMIC_AGENT_MESSAGE' : 'REFUSED',
|
||||
inputCount: event.messages.length,
|
||||
outputCount: outputMessages.length,
|
||||
prefixHashBefore: sha(inputJson),
|
||||
prefixHashAfter: sha(JSON.stringify(outputPrefix)),
|
||||
prefixPreservedByReturn: sha(inputJson) === sha(JSON.stringify(outputPrefix)),
|
||||
blockLength: CONTEXT_BLOCK.length,
|
||||
blockSha256: sha(CONTEXT_BLOCK),
|
||||
});
|
||||
return { messages: outputMessages };
|
||||
});
|
||||
|
||||
pi.on('before_provider_request', async (event) => {
|
||||
const paths = markerPaths(event.payload);
|
||||
const cycle = buildingCycle;
|
||||
buildingCycle = undefined;
|
||||
if (cycle) inFlightCycles.push(cycle);
|
||||
log('before_provider_request', {
|
||||
requestNonce: cycle?.nonce,
|
||||
inFlightDepth: inFlightCycles.length,
|
||||
markerOccurrences: paths.length,
|
||||
markerPaths: paths,
|
||||
finalPayloadValid: Boolean(cycle?.verified && paths.length === 1),
|
||||
});
|
||||
});
|
||||
|
||||
pi.on('after_provider_response', async (event) => {
|
||||
const cycle = inFlightCycles[0];
|
||||
log('after_provider_response', {
|
||||
requestNonce: cycle?.nonce,
|
||||
status: event.status,
|
||||
assistantContentAvailableAtThisHook: false,
|
||||
timing: 'headers/status before stream consumption',
|
||||
});
|
||||
});
|
||||
|
||||
pi.on('message_end', async (event) => {
|
||||
const role = (event.message as { role?: string }).role;
|
||||
const ids = assistantToolIds(event.message);
|
||||
const text = assistantText(event.message);
|
||||
const cycle = role === 'assistant' ? inFlightCycles.shift() : undefined;
|
||||
if (ids.length > 0 && cycle) {
|
||||
for (const id of ids) {
|
||||
toolNonce.set(id, {
|
||||
nonce: cycle.nonce,
|
||||
verified: cycle.verified,
|
||||
sourceReason: cycle.sourceReason,
|
||||
});
|
||||
}
|
||||
}
|
||||
log('message_end', {
|
||||
role,
|
||||
assistantContentObserved: role === 'assistant',
|
||||
requestNonce: cycle?.nonce,
|
||||
inFlightDepthAfter: inFlightCycles.length,
|
||||
toolCallIds: ids,
|
||||
nonceMappings: ids.map((id) => ({ toolCallId: id, requestNonce: cycle?.nonce })),
|
||||
exactContextBlockCopied: text.includes(CONTEXT_BLOCK),
|
||||
assistantTextSha256: text ? sha(text) : null,
|
||||
});
|
||||
});
|
||||
|
||||
pi.on('tool_call', async (event) => {
|
||||
const mapping = toolNonce.get(event.toolCallId);
|
||||
const allowed = Boolean(lastPosition && mapping?.verified);
|
||||
log('tool_call', {
|
||||
toolCallId: event.toolCallId,
|
||||
toolName: event.toolName,
|
||||
mapping: mapping ?? null,
|
||||
allowed,
|
||||
reason: !lastPosition
|
||||
? 'closed-not-last'
|
||||
: !mapping
|
||||
? 'unknown-tool-call-id'
|
||||
: !mapping.verified
|
||||
? `unverified-source:${mapping.sourceReason}`
|
||||
: 'exact-tool-call-id-mapped-to-verified-request-nonce',
|
||||
});
|
||||
if (!allowed) return { block: true, reason: 'Gate0 probe refused unverified tool batch' };
|
||||
});
|
||||
|
||||
pi.on('agent_settled', async () => {
|
||||
log('agent_settled', { retainedNonceMappingsBeforeClear: toolNonce.size });
|
||||
toolNonce.clear();
|
||||
});
|
||||
|
||||
pi.registerTool({
|
||||
name: 'gate0_nonce_probe',
|
||||
label: 'Gate0 Nonce Probe',
|
||||
description: 'Gate0-only harmless tool used to prove toolCallId to request-nonce correlation.',
|
||||
parameters: Type.Object({ label: Type.String() }),
|
||||
async execute(toolCallId, params) {
|
||||
const broker = await brokerRequest({ action: 'promote-probe' });
|
||||
log('tool_execute', { toolCallId, label: params.label, broker });
|
||||
return {
|
||||
content: [{ type: 'text', text: `gate0_nonce_probe executed for ${params.label}` }],
|
||||
details: { harmless: true },
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
pi.registerCommand('gate0-reload', {
|
||||
description: 'Trigger a real same-PID Pi extension/runtime reload.',
|
||||
handler: async (_args, ctx) => {
|
||||
log('reload_command_before');
|
||||
await ctx.reload();
|
||||
return;
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -1,450 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Drive real Pi 0.80.x RPC for P2/P3/P5/P6 runtime evidence."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import queue
|
||||
import shutil
|
||||
import signal
|
||||
import socket
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import threading
|
||||
import time
|
||||
from pathlib import Path
|
||||
from typing import Any, Callable
|
||||
|
||||
HERE = Path(__file__).resolve().parent
|
||||
BLOCK = "\n".join(
|
||||
[
|
||||
"GATE0_PI_ATOMIC_BEGIN",
|
||||
"segment-01=alpha-7e31",
|
||||
"segment-02=middle-9c42",
|
||||
"segment-03=omega-5b83",
|
||||
"GATE0_PI_ATOMIC_END",
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
def wait_path(path: Path, timeout: float = 20) -> None:
|
||||
deadline = time.monotonic() + timeout
|
||||
while time.monotonic() < deadline:
|
||||
if path.exists():
|
||||
return
|
||||
time.sleep(0.05)
|
||||
raise TimeoutError(f"timed out waiting for {path}")
|
||||
|
||||
|
||||
def socket_request(path: Path, payload: dict[str, object]) -> None:
|
||||
conn = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
||||
conn.connect(str(path))
|
||||
conn.sendall((json.dumps(payload) + "\n").encode())
|
||||
conn.makefile("r", encoding="utf-8").readline()
|
||||
conn.close()
|
||||
|
||||
|
||||
def jsonl(path: Path) -> list[dict[str, Any]]:
|
||||
if not path.exists():
|
||||
return []
|
||||
return [json.loads(line) for line in path.read_text().splitlines() if line]
|
||||
|
||||
|
||||
class PiRpc:
|
||||
def __init__(self, command: list[str], cwd: Path, env: dict[str, str]):
|
||||
self.process = subprocess.Popen(
|
||||
command,
|
||||
cwd=cwd,
|
||||
env=env,
|
||||
stdin=subprocess.PIPE,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
text=True,
|
||||
bufsize=1,
|
||||
start_new_session=True,
|
||||
)
|
||||
self.events: queue.Queue[dict[str, Any]] = queue.Queue()
|
||||
self.raw_lines: list[str] = []
|
||||
self.stderr_lines: list[str] = []
|
||||
threading.Thread(target=self._read_stdout, daemon=True).start()
|
||||
threading.Thread(target=self._read_stderr, daemon=True).start()
|
||||
|
||||
def _read_stdout(self) -> None:
|
||||
assert self.process.stdout is not None
|
||||
for line in self.process.stdout:
|
||||
stripped = line.rstrip("\n")
|
||||
self.raw_lines.append(stripped)
|
||||
try:
|
||||
event = json.loads(stripped)
|
||||
except json.JSONDecodeError:
|
||||
continue
|
||||
self.events.put(event)
|
||||
|
||||
def _read_stderr(self) -> None:
|
||||
assert self.process.stderr is not None
|
||||
for line in self.process.stderr:
|
||||
self.stderr_lines.append(line.rstrip("\n"))
|
||||
|
||||
def send(self, payload: dict[str, object]) -> None:
|
||||
assert self.process.stdin is not None
|
||||
self.process.stdin.write(json.dumps(payload) + "\n")
|
||||
self.process.stdin.flush()
|
||||
|
||||
def wait(self, predicate: Callable[[dict[str, Any]], bool], description: str, timeout: float = 180) -> dict[str, Any]:
|
||||
deadline = time.monotonic() + timeout
|
||||
while time.monotonic() < deadline:
|
||||
if self.process.poll() is not None and self.events.empty():
|
||||
raise RuntimeError(
|
||||
f"Pi exited {self.process.returncode} while waiting for {description}: "
|
||||
+ " | ".join(self.stderr_lines[-5:])
|
||||
)
|
||||
try:
|
||||
event = self.events.get(timeout=0.2)
|
||||
except queue.Empty:
|
||||
continue
|
||||
if predicate(event):
|
||||
return event
|
||||
raise TimeoutError(f"timed out waiting for {description}")
|
||||
|
||||
def response(self, request_id: str, timeout: float = 180) -> dict[str, Any]:
|
||||
return self.wait(
|
||||
lambda event: event.get("type") == "response" and event.get("id") == request_id,
|
||||
f"response {request_id}",
|
||||
timeout,
|
||||
)
|
||||
|
||||
def prompt_and_settle(self, request_id: str, message: str) -> None:
|
||||
self.send({"id": request_id, "type": "prompt", "message": message})
|
||||
response = self.response(request_id)
|
||||
if not response.get("success"):
|
||||
raise RuntimeError(f"prompt rejected: {response}")
|
||||
self.wait(lambda event: event.get("type") == "agent_settled", f"agent_settled {request_id}")
|
||||
|
||||
def close(self) -> None:
|
||||
if self.process.poll() is None:
|
||||
try:
|
||||
os.killpg(self.process.pid, signal.SIGTERM)
|
||||
except ProcessLookupError:
|
||||
pass
|
||||
try:
|
||||
self.process.wait(timeout=8)
|
||||
except subprocess.TimeoutExpired:
|
||||
os.killpg(self.process.pid, signal.SIGKILL)
|
||||
self.process.wait(timeout=5)
|
||||
|
||||
|
||||
def manifest(path: Path, fragment: Path, expected_hash: str, max_bytes: int = 64) -> None:
|
||||
path.write_text(
|
||||
json.dumps(
|
||||
{
|
||||
"maxBytes": max_bytes,
|
||||
"fragments": [{"path": str(fragment), "sha256": expected_hash}],
|
||||
},
|
||||
sort_keys=True,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def run_open(root: Path) -> tuple[list[dict[str, Any]], list[dict[str, Any]], list[str], list[str]]:
|
||||
workspace = root / "workspace"
|
||||
workspace.mkdir()
|
||||
session_dir = root / "sessions"
|
||||
session_dir.mkdir()
|
||||
pi_log = root / "pi-hooks.jsonl"
|
||||
generation_log = root / "generation.jsonl"
|
||||
generation_socket = root / "generation.sock"
|
||||
source_manifest = root / "manifest.json"
|
||||
valid_fragment = root / "fragment.md"
|
||||
valid_fragment.write_text("NORMATIVE-FRAGMENT-v1\n")
|
||||
expected = hashlib.sha256(valid_fragment.read_bytes()).hexdigest()
|
||||
manifest(source_manifest, valid_fragment, expected)
|
||||
|
||||
broker = subprocess.Popen(
|
||||
[
|
||||
sys.executable,
|
||||
str(HERE / "p3_generation_broker.py"),
|
||||
"--socket",
|
||||
str(generation_socket),
|
||||
"--log",
|
||||
str(generation_log),
|
||||
],
|
||||
text=True,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.STDOUT,
|
||||
)
|
||||
wait_path(generation_socket)
|
||||
env = os.environ.copy()
|
||||
env.update(
|
||||
{
|
||||
"GATE0_PI_LOG": str(pi_log),
|
||||
"GATE0_GENERATION_SOCKET": str(generation_socket),
|
||||
"GATE0_SOURCE_MANIFEST": str(source_manifest),
|
||||
"GATE0_PI_CONTEXT_BLOCK": BLOCK,
|
||||
"MOSAIC_PI_FORCE_SKILLS": "",
|
||||
"PI_SKIP_VERSION_CHECK": "1",
|
||||
}
|
||||
)
|
||||
command = [
|
||||
"mosaic",
|
||||
"yolo",
|
||||
"pi",
|
||||
"--mode",
|
||||
"rpc",
|
||||
"--session-dir",
|
||||
str(session_dir),
|
||||
"--no-extensions",
|
||||
"--no-context-files",
|
||||
"--no-prompt-templates",
|
||||
"--model",
|
||||
"openai-codex/gpt-5.6-sol",
|
||||
"--thinking",
|
||||
"medium",
|
||||
"--extension",
|
||||
str(HERE / "pi_gate0_extension.ts"),
|
||||
]
|
||||
pi = PiRpc(command, workspace, env)
|
||||
try:
|
||||
pi.send({"id": "state-0", "type": "get_state"})
|
||||
state0 = pi.response("state-0")
|
||||
original_session = state0["data"]["sessionFile"]
|
||||
|
||||
pi.prompt_and_settle(
|
||||
"p2",
|
||||
"Call gate0_nonce_probe exactly once with label p2. After the tool finishes, copy the exact full GATE0_PI_ATOMIC_BEGIN through GATE0_PI_ATOMIC_END block from context, with no commentary.",
|
||||
)
|
||||
|
||||
# P3 immediately follows the valid P2 promotion so reload must revoke a
|
||||
# genuinely VERIFIED prior generation, not an already-invalid source run.
|
||||
pi.send({"id": "reload", "type": "prompt", "message": "/gate0-reload"})
|
||||
reload_response = pi.response("reload")
|
||||
if not reload_response.get("success"):
|
||||
raise RuntimeError(f"reload command failed: {reload_response}")
|
||||
|
||||
pi.send({"id": "clone", "type": "clone"})
|
||||
clone_response = pi.response("clone")
|
||||
if not clone_response.get("success") or clone_response.get("data", {}).get("cancelled"):
|
||||
raise RuntimeError(f"clone failed: {clone_response}")
|
||||
|
||||
pi.send({"id": "new", "type": "new_session"})
|
||||
new_response = pi.response("new")
|
||||
if not new_response.get("success") or new_response.get("data", {}).get("cancelled"):
|
||||
raise RuntimeError(f"new session failed: {new_response}")
|
||||
|
||||
pi.send(
|
||||
{
|
||||
"id": "resume",
|
||||
"type": "switch_session",
|
||||
"sessionPath": original_session,
|
||||
}
|
||||
)
|
||||
resume_response = pi.response("resume")
|
||||
if not resume_response.get("success") or resume_response.get("data", {}).get("cancelled"):
|
||||
raise RuntimeError(f"resume failed: {resume_response}")
|
||||
|
||||
# P5 missing fragment: action-time source validation must revoke/refuse.
|
||||
manifest(source_manifest, root / "absent-fragment.md", expected)
|
||||
pi.prompt_and_settle(
|
||||
"p5-missing",
|
||||
"Call gate0_nonce_probe exactly once with label p5-missing, then stop.",
|
||||
)
|
||||
|
||||
# P5 oversize fragment: expected hash is correct, size limit is not.
|
||||
oversize = root / "oversize.md"
|
||||
oversize.write_text("X" * 65)
|
||||
manifest(source_manifest, oversize, hashlib.sha256(oversize.read_bytes()).hexdigest(), 64)
|
||||
pi.prompt_and_settle(
|
||||
"p5-oversize",
|
||||
"Call gate0_nonce_probe exactly once with label p5-oversize, then stop.",
|
||||
)
|
||||
|
||||
# P5 hash mismatch: size is valid but bytes differ from expected.
|
||||
mismatch = root / "mismatch.md"
|
||||
mismatch.write_text("tampered\n")
|
||||
manifest(source_manifest, mismatch, expected, 64)
|
||||
pi.prompt_and_settle(
|
||||
"p5-hash",
|
||||
"Call gate0_nonce_probe exactly once with label p5-hash-mismatch, then stop.",
|
||||
)
|
||||
|
||||
time.sleep(1)
|
||||
return jsonl(pi_log), jsonl(generation_log), list(pi.raw_lines), list(pi.stderr_lines)
|
||||
finally:
|
||||
pi.close()
|
||||
try:
|
||||
socket_request(generation_socket, {"action": "shutdown-broker"})
|
||||
except OSError:
|
||||
pass
|
||||
try:
|
||||
broker.wait(timeout=5)
|
||||
except subprocess.TimeoutExpired:
|
||||
broker.kill()
|
||||
broker.wait()
|
||||
|
||||
|
||||
def run_closed(root: Path) -> list[dict[str, Any]]:
|
||||
workspace = root / "closed-workspace"
|
||||
workspace.mkdir()
|
||||
pi_log = root / "closed-hooks.jsonl"
|
||||
env = os.environ.copy()
|
||||
env.update(
|
||||
{
|
||||
"GATE0_PI_LOG": str(pi_log),
|
||||
"MOSAIC_PI_FORCE_SKILLS": "",
|
||||
"PI_SKIP_VERSION_CHECK": "1",
|
||||
}
|
||||
)
|
||||
command = [
|
||||
"mosaic",
|
||||
"yolo",
|
||||
"pi",
|
||||
"--mode",
|
||||
"rpc",
|
||||
"--no-session",
|
||||
"--no-extensions",
|
||||
"--no-context-files",
|
||||
"--no-prompt-templates",
|
||||
"--extension",
|
||||
str(HERE / "pi_gate0_extension.ts"),
|
||||
"--extension",
|
||||
str(HERE / "pi_later_extension.ts"),
|
||||
]
|
||||
pi = PiRpc(command, workspace, env)
|
||||
try:
|
||||
pi.send({"id": "closed-state", "type": "get_state"})
|
||||
pi.response("closed-state")
|
||||
time.sleep(0.5)
|
||||
return jsonl(pi_log)
|
||||
finally:
|
||||
pi.close()
|
||||
|
||||
|
||||
def main() -> None:
|
||||
with tempfile.TemporaryDirectory(prefix="gate0-pi-") as temp:
|
||||
root = Path(temp)
|
||||
records, generations, rpc_lines, stderr_lines = run_open(root)
|
||||
closed = run_closed(root)
|
||||
|
||||
p2_message = next(
|
||||
r for r in records if r["event"] == "message_end" and r.get("nonceMappings")
|
||||
)
|
||||
p2_tool = next(r for r in records if r["event"] == "tool_call" and r.get("allowed"))
|
||||
mapped = p2_message["nonceMappings"][0]
|
||||
assert mapped["toolCallId"] == p2_tool["toolCallId"]
|
||||
assert mapped["requestNonce"] == p2_tool["mapping"]["nonce"]
|
||||
assert next(r for r in records if r["event"] == "session_start")["lastPosition"] is True
|
||||
assert next(r for r in closed if r["event"] == "session_start")["gateState"] == "CLOSED_NOT_LAST"
|
||||
reload_revoke = next(
|
||||
r
|
||||
for r in generations
|
||||
if r["event"] == "runtime_generation_bump"
|
||||
and r.get("reason") == "reload"
|
||||
and r.get("phase") == "shutdown"
|
||||
)
|
||||
assert reload_revoke["prior_lease"] == "VERIFIED"
|
||||
assert reload_revoke["prior_lease_revoked"] is True
|
||||
for reason in {"missing", "oversize", "hash-mismatch"}:
|
||||
assert any(
|
||||
r["event"] == "context_return"
|
||||
and r.get("sourceValidation", {}).get("reason") == reason
|
||||
and r.get("injectionDecision") == "REFUSED"
|
||||
and r.get("promotion") is False
|
||||
for r in records
|
||||
)
|
||||
assert any(
|
||||
r["event"] == "tool_call"
|
||||
and r.get("mapping", {}).get("sourceReason") == reason
|
||||
and r.get("allowed") is False
|
||||
for r in records
|
||||
)
|
||||
assert any(
|
||||
r["event"] == "message_end" and r.get("exactContextBlockCopied") is True
|
||||
for r in records
|
||||
)
|
||||
|
||||
print("$ python3 docs/compaction-refresh/probes/pi_gate0_run.py")
|
||||
print("machine_assertions=PASS")
|
||||
print("runtime_versions:")
|
||||
print(" " + subprocess.check_output(["pi", "--version"], text=True).strip())
|
||||
print(" " + subprocess.check_output(["mosaic", "--version"], text=True).strip())
|
||||
|
||||
print("\nP2_EVENT_ORDER_AND_NONCE_MAP:")
|
||||
for record in records:
|
||||
if record["seq"] <= 12 and record["event"] in {
|
||||
"after_provider_response",
|
||||
"message_end",
|
||||
"tool_call",
|
||||
"tool_execute",
|
||||
} and (
|
||||
record["event"] != "message_end"
|
||||
or record.get("role") == "assistant"
|
||||
):
|
||||
print(json.dumps(record, sort_keys=True))
|
||||
|
||||
print("\nP2_LAST_OR_CLOSED:")
|
||||
print(json.dumps(next(r for r in records if r["event"] == "session_start"), sort_keys=True))
|
||||
print(json.dumps(next(r for r in closed if r["event"] == "session_start"), sort_keys=True))
|
||||
|
||||
print("\nP3_GENERATION_BROKER:")
|
||||
for record in generations:
|
||||
if record["event"] in {"probe_lease_promoted", "runtime_generation_bump"}:
|
||||
print(json.dumps(record, sort_keys=True))
|
||||
|
||||
print("\nP5_SOURCE_INVALIDATION:")
|
||||
fault_reasons = {"missing", "oversize", "hash-mismatch"}
|
||||
emitted_context: set[str] = set()
|
||||
emitted_tool: set[str] = set()
|
||||
for record in records:
|
||||
source_reason = record.get("sourceValidation", {}).get("reason")
|
||||
if (
|
||||
record["event"] == "context_return"
|
||||
and source_reason in fault_reasons
|
||||
and source_reason not in emitted_context
|
||||
):
|
||||
print(json.dumps(record, sort_keys=True))
|
||||
emitted_context.add(source_reason)
|
||||
mapping_reason = record.get("mapping", {}).get("sourceReason")
|
||||
if (
|
||||
record["event"] == "tool_call"
|
||||
and not record.get("allowed")
|
||||
and mapping_reason in fault_reasons
|
||||
and mapping_reason not in emitted_tool
|
||||
):
|
||||
print(json.dumps(record, sort_keys=True))
|
||||
emitted_tool.add(mapping_reason)
|
||||
emitted_broker: set[str] = set()
|
||||
for record in generations:
|
||||
reason = record.get("source_reason")
|
||||
if record["event"] == "source_invalidation_revoke" and reason not in emitted_broker:
|
||||
print(json.dumps(record, sort_keys=True))
|
||||
emitted_broker.add(str(reason))
|
||||
|
||||
print("\nP6_PI_CONTEXT_ATOMIC_OBSERVATION:")
|
||||
for record in records:
|
||||
include = (
|
||||
(record["event"] == "context_return" and record.get("injectionDecision") == "ONE_ATOMIC_AGENT_MESSAGE")
|
||||
or (record["event"] == "before_provider_request" and record.get("finalPayloadValid"))
|
||||
or (record["event"] == "message_end" and record.get("exactContextBlockCopied"))
|
||||
)
|
||||
if include and record["seq"] <= 12:
|
||||
print(json.dumps(record, sort_keys=True))
|
||||
|
||||
print("\nRPC_EVENT_COUNTS:")
|
||||
counts: dict[str, int] = {}
|
||||
for line in rpc_lines:
|
||||
try:
|
||||
event = json.loads(line)
|
||||
except json.JSONDecodeError:
|
||||
continue
|
||||
key = str(event.get("type"))
|
||||
counts[key] = counts.get(key, 0) + 1
|
||||
print(json.dumps(counts, sort_keys=True))
|
||||
print("stderr_nonempty=" + str(bool(stderr_lines)))
|
||||
for line in stderr_lines[:10]:
|
||||
print("stderr: " + line[:500])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,8 +0,0 @@
|
||||
import type { ExtensionAPI } from '@earendil-works/pi-coding-agent';
|
||||
|
||||
// Deliberately loaded after pi_gate0_extension.ts. The Gate0 extension must
|
||||
// observe its argv position and remain CLOSED rather than claiming finality.
|
||||
export default function register(pi: ExtensionAPI) {
|
||||
pi.on('context', async (event) => ({ messages: [...event.messages] }));
|
||||
pi.on('before_provider_request', async () => undefined);
|
||||
}
|
||||
@@ -1,65 +0,0 @@
|
||||
# Gate0 Probe-3 (D4) Class-B — §3-Conformance Review v3
|
||||
|
||||
**Verdict: ✅ PASS**
|
||||
|
||||
## Pin (G1 — reviewed object, mandatory)
|
||||
|
||||
- **Reviewed object = `ace6066762c088f4b9729860da71b4c84451a7c3`** (harness commit, branch `feat/827-gate0-probe`, `mosaicstack/stack` @ git.mosaicstack.dev).
|
||||
- **Reviewed file:** `docs/compaction-refresh/probes/p3_d4_focused_run.py`
|
||||
- **Harness sha256 (pushed provider bytes, fetched `-o FILE`, FULL-40 ref, verified before trust):**
|
||||
`2f11c9391c0eef203f26b1206bee8bc4cd106e8c1192399c5e7b71f41a3f6b75` (17162 bytes; no not-found sentinel).
|
||||
- **§3 amendment authority read at pin:** `GATE0-PROBE3-EXEC-AMENDMENT.md` @ ref `571f239154c6793fb1a5eac0d1cd4182f286a3ac`,
|
||||
sha256 `9ac9ff873fad41a6e15763cc89cb94d0bc4a6b0cf9b6770561d1781b03f63276` (7699 bytes). MUST-HAVE/MUST-BE-ABSENT
|
||||
confirmed against the actual fetched §3 text, not a paraphrase.
|
||||
|
||||
## Independence (G2)
|
||||
|
||||
Distinct Opus §3-conformance reviewer (Gate-16 author≠reviewer). I did **not** build this harness (author =
|
||||
ms-rev-826); I am not Mos. This verdict is my own; the author did not author or edit it. Byte review only — **ran
|
||||
nothing** (no harness, no broker, no sockets/state). Reviewed across v1 (FAIL, live-broker launch path) → v2 (PASS,
|
||||
later found runtime-dead producer) → this v3 (closes the live-path review-gap).
|
||||
|
||||
## Why v3 (the review-gap closed)
|
||||
|
||||
v2 PASS @`839d156f` credited the static presence of `lease_anchor_registered` as isolation proof. At FIRE the
|
||||
producing path was **dead**: the harness drove the *released* `mosaic` binary, which launched Pi **ungated**, so
|
||||
`register_anchor` never ran. Static presence of an assertion ≠ its producing path executing. v3 requires the
|
||||
producing path to be **live at runtime**.
|
||||
|
||||
## Surface-by-surface
|
||||
|
||||
| Surface | Result | Evidence (file:line) |
|
||||
|---|---|---|
|
||||
| Pushed bytes fetched + sha-verified | ✅ | sha256==`2f11c939…`, 17162B, no sentinel |
|
||||
| (a) LIVE-PATH — drives the **gated** launcher, producer in the exec chain, NOT released `mosaic`/plain `execRuntime` | ✅ | launch = `python3 <GATED_LAUNCHER> --runtime pi -- pi …` :357-364; `GATED_LAUNCHER=…/launch-runtime.py` :32, pinned `GATED_WI_HEAD=abd2791f…` :31; `mosaic yolo`/`execRuntime` = 0 hits. `launch-runtime.py` unconditionally `register_anchor`s before `execvpe`, so the producer is in the invoked chain |
|
||||
| (b) Fail-closed precondition present + correct (gated + fixture-socket, refuses otherwise), invoked before all launches | ✅ | `gated_launcher_precondition` :229-251, called :342 **before** broker Popen :343 and Pi launch :357. Verifies (ii) `MOSAIC_LEASE_BROKER_SOCKET==fixture` :232 + fixture in tempdir :234; (i) launcher HEAD==`abd2791f` :243 + source has `register_anchor` **before** `execute(command[0]…)` and reads `MOSAIC_LEASE_BROKER_SOCKET` :246-250. Raises `RuntimeError` (no run) on any miss :233/:235/:242/:244/:250 |
|
||||
| (c) Fixture-socket isolation (no live/default broker reachable, single p3 fixture, non-destructive) | ✅ | `pop("MOSAIC_LEASE_BROKER_SOCKET")` :330 + set to fixture socket :334; harness invokes `launch-runtime.py` directly so it reads `MOSAIC_LEASE_BROKER_SOCKET`=fixture with **no** `defaultLeaseBrokerSocket`/XDG/`/run/user` fallback in the path; `register_anchor` served by the single p3 fixture; `p3_generation_broker.py` **zero diff** vs `839d156f` (in-memory volatile hex-256 session `secrets.token_hex(32)`, nothing durable outside tempdir) |
|
||||
| (d) Assertion INTACT (`lease_anchor_registered` + `session_id_shape=="hex-256"`, not softened/optional/repointed) | ✅ | :256-258 (event), :298 (`hex-256`), folded into single-PID/starttime identity set :286. Not Case C |
|
||||
| spawns ONLY p3_generation_broker.py | ✅ | broker Popen = `HERE/p3_generation_broker.py` :343-346; the pinned launcher is a register-before-exec launch wrapper, not a 2nd broker |
|
||||
| promotion = fixture-only (not P2-banked) | ✅ | `d4_fixture_promote` "not a P2 evidence-gathering authorization"; `promote-probe` in-memory; precondition target only; no P2 import |
|
||||
| D4 assertions complete | ✅ | same-PID+starttime persist (incl. launcher registration) :284-288; gen strictly increases :290-293; reload revokes genuinely-VERIFIED prior :299-300; new→`MUTATOR_UNVERIFIED` :301; prior→`STALE_GENERATION` :302; lifecycle boundaries :294-297 |
|
||||
| P5 / P6 / P2-bank / retry-launder / live-effect / mechanism-change / scope-widen ABSENT | ✅ | 0 hits: `source-invalid`/`run_open`/`atomic`/`p2_provider`/`p5`/`p6`/`pi_gate0_run`/`retry`; no real-broker path; the only mechanism change is the required launch-routing fix (release→pinned gated launcher), which narrows scope, not widens |
|
||||
| non-destructive | ✅ | per-run `tempfile.mkdtemp` fixtures; p3 in-memory + tempdir socket/log only; reads `/proc/<pid>/stat` (read-only) |
|
||||
| deterministic | ✅ | isolated tempdir per run; deterministic assertions; session-id randomness is **shape**-asserted only (`hex-256`), `setdefault` idempotent |
|
||||
| hidden exec-at-import | ✅ absent | only `if __name__ == "__main__": main()`; docstring: not executed until FIRE |
|
||||
|
||||
## Verdict
|
||||
|
||||
**PASS @ `ace60667`** — (a) LIVE-PATH, (b) fail-closed precondition, (c) fixture-socket isolation, and
|
||||
(d) intact assertion all hold, with zero out-of-scope surface. The v2 review-gap (runtime-dead producer via the
|
||||
released ungated `mosaic`) is structurally closed: the harness no longer invokes `mosaic` at all — it invokes the
|
||||
pinned `abd2791f` `launch-runtime.py` directly (register-before-exec), and refuses to launch unless the launcher is
|
||||
that pinned gated register-before-exec binary bound to this run's fixture socket.
|
||||
|
||||
**Findings: none.**
|
||||
|
||||
## Scope reminder (not a finding)
|
||||
|
||||
Per §3/§5 of the amendment, producing this evidence **executes** the Gate0 mechanism (launches processes, creates
|
||||
socket/state artifacts, exercises revocation). This review clears the **bytes**; **FIRE remains separately gated on
|
||||
Mos's explicit post-clear GO** and is not authorized by this review.
|
||||
|
||||
---
|
||||
|
||||
**Reviewer:** independent Opus §3-conformance reviewer (Gate-16 author≠reviewer). Byte review only; ran nothing.
|
||||
**Reviewed object (pin):** `ace6066762c088f4b9729860da71b4c84451a7c3` · harness sha256 `2f11c9391c0eef203f26b1206bee8bc4cd106e8c1192399c5e7b71f41a3f6b75`.
|
||||
@@ -1,95 +0,0 @@
|
||||
# Gate0 Probe-3 (D4) Observation-Fidelity — §3-Conformance Review v4
|
||||
|
||||
**Verdict: ✅ PASS**
|
||||
|
||||
## Pin (G1 — reviewed object, mandatory)
|
||||
|
||||
- **Reviewed object = `484849387006ab5561798506fd6042ddbd5617de`** (harness commit, branch `feat/827-gate0-probe`, `mosaicstack/stack` @ git.mosaicstack.dev).
|
||||
- **Reviewed file:** `docs/compaction-refresh/probes/p3_d4_focused_run.py` — sha256 (pushed provider bytes, `-o FILE`, FULL-40 ref, verified before trust): `9095eab7a4ddf11bb92bb5971d49e1facad12f4692ce2081665b0af47cbe5098` (23698 bytes, no not-found sentinel). Worktree bytes at `48484938` re-hashed identical.
|
||||
- **Co-reviewed fixture broker:** `docs/compaction-refresh/probes/p3_generation_broker.py` (sha256 `fd5327d2e9a2808282cbc9c4a4ccef42d2a806482b27cc48035191b9b11607c8`).
|
||||
- **Traceability anchor (independently recomputed):** `GATED_LAUNCHER_SHA256 = e950e4224e280f16979d90cabb89aa1896c5ee28bed2df957e14d018d43cda82` equals the sha256 of `packages/mosaic/framework/tools/lease-broker/launch-runtime.py` at WI-3 #830 reviewed head `f400830738998db105107a2a4c69c7f2a2a6fd5d` (computed from two independent worktrees).
|
||||
|
||||
## Independence (G2)
|
||||
|
||||
Distinct Opus §3-conformance reviewer (Gate-16 author≠reviewer). I did not build this harness (author = ms-rev-826);
|
||||
I am not Mos. This verdict is my own; the author did not author or edit it. Byte review only — **ran nothing** (no
|
||||
harness, no broker, no sockets/state). Prior v3 PASS (`ace60667`, pinned `abd2791f`) is correctly **superseded**:
|
||||
pinning a pre-`66b1e0a0` ancestor made D4 an in-memory socket simulation (hollow gate). v4 requires
|
||||
mechanism-fidelity within isolation (Jason R1).
|
||||
|
||||
## BAR1 — Traceability (evidence attests the SHIPPED WI-3 D4 mechanism)
|
||||
|
||||
| Check | Result | Evidence |
|
||||
|---|---|---|
|
||||
| `GATED_WI_HEAD == f4008307` (not `abd2791f`) | ✅ | :33 |
|
||||
| Launcher pinned by git-HEAD **and** sha256 | ✅ | precondition :285-288 (`head != GATED_WI_HEAD` raise; `sha256(launcher) != GATED_LAUNCHER_SHA256` raise); sha256 independently == f4008307's `launch-runtime.py` |
|
||||
| Launcher bytes contain the file-backed mechanism | ✅ | precondition requires `register_anchor` :290, `initialize_runtime_generation(generation_file, generation)` :291, `generation-{session_id}.state` :294, `MOSAIC_LEASE_GENERATION_FILE` :295, `read_runtime_generation`+`bump_runtime_generation` in `lease_generation.py` :296-299; order `register < initialize < execute` :302-303 |
|
||||
|
||||
## BAR2 — Fidelity (file-backed generation, not in-memory simulation)
|
||||
|
||||
| # | Requirement | Result | Evidence |
|
||||
|---|---|---|---|
|
||||
| i | Extension bumps `generation-{sid}.state` via the real helper, not in-mem | ✅ | ext `lifecycle()` calls `broker({action:'bump-generation'})` at every post-start boundary (harness :200-205); broker `bump-generation` → `bump_runtime_generation(generation_environment(identity))` (broker :119-120) |
|
||||
| ii | Broker reads generation via `read_runtime_generation`, not an in-mem counter | ✅ | broker loads the pinned module (`--generation-module`, :52-54); `file_generation()` = `read_runtime_generation(...)` (:77-78); authorize-probe reads `current_generation = file_generation(identity)` (:154); lifecycle `new_generation = file_generation(identity)` (:201). The in-mem `generations` dict is only an old-value cache for the monotonic guard (:199-204), never the authorization authority |
|
||||
| iii | `assert_d4` observes the FILE-BACKED transition | ✅ | `state_file_source` = all records `generation_source=="state-file"` (harness :370-372); `state_file_drives_lifecycle` = state-bumps == lifecycle generations[1:] (:368-369); `new_generation_unverified→MUTATOR_UNVERIFIED` :382; `prior_generation_stale→STALE_GENERATION` :383; `verified_revoked_on_reload` :380-381 — not a socket-only bump |
|
||||
| iv | `.state` bound to per-run fixture temp root | ✅ | broker `generation_environment` **raises if `state_path.parent != socket_path.parent`** (:73-74); `state_file_in_fixture_root` (:373-376); precondition forbids inherited `MOSAIC_LEASE_GENERATION_FILE` (:255-256) and requires socket in `gettempdir()` (:274-275); launcher/broker agree on `socket_path.parent / generation-{sid}.state` |
|
||||
|
||||
## BAR3 — Carry-over
|
||||
|
||||
| # | Result | Evidence |
|
||||
|---|---|---|
|
||||
| a LIVE-PATH (gated launcher @f4008307 at runtime, not released/plain execRuntime) | ✅ | Pi launched via `python3 <GATED_LAUNCHER> --runtime pi -- pi …` :502-505; `mosaic yolo`/`execRuntime` = 0 hits; register-before-exec producer in the invoked chain (precondition order gate) |
|
||||
| b Fail-closed precondition present+correct | ✅ | `gated_launcher_precondition` :250-306, invoked :485 **before** broker/Pi; raises on socket≠fixture / gen-file-inherited / write-path-escape / head-mismatch / hash-mismatch / not-register-before-exec-file-bound |
|
||||
| c Fixture-socket isolation, single p3 broker | ✅ | one broker Popen :486-500; `MOSAIC_LEASE_BROKER_SOCKET=socket_path` (fixture); direct launcher invocation ⇒ no default/XDG/`/run/user` fallback in path |
|
||||
| d `lease_anchor_registered` INTACT | ✅ | broker emits event + `session_id_shape=="hex-256"` (:102-107); `record_where` requires it (:324-326); `lease_anchor_fixture` check (:379) — not deleted/softened/optional/repointed (not Case-C) |
|
||||
|
||||
## BAR4 — Homelab Gate-B carry-forward findings
|
||||
|
||||
| # | Result | Evidence |
|
||||
|---|---|---|
|
||||
| b4-1 gated launcher @f4008307, not released/plain execRuntime | ✅ | :502-505; 0 `mosaic yolo`/`execRuntime` |
|
||||
| b4-2 **affirmative no-escape** (allow-list base, not deny-list) | ✅ | `isolated_environment` builds the child env from a **literal allow-list dict** (:442-461), NOT `os.environ.copy()`; only PATH/LANG/TERM/PI_CODING_AGENT (non-write-bearing) pass through; every write-bearing var (HOME/XDG*/TMPDIR/MOSAIC_AGENT_WORKDIR/HEARTBEAT_RUN_DIR/MOSAIC_HOME/D4_PI_LOG/socket) redirected under `root`; precondition double-checks each is `is_relative_to(root)` (:257-273). No unnamed/future inherited var survives |
|
||||
| b4-3 `--runs` exactly 3, fail-closed otherwise | ✅ | `add_argument("--runs", type=int, default=3, choices=(3,))` :591 (argparse rejects any other value) |
|
||||
| b4-4 cleanup try/finally spans the whole launch | ✅ | `broker=pi=None` :479-480; `try` opens **before** precondition/broker/PiRpc :482; nested `finally` always closes pi then broker+socket even on early `wait_path`/`PiRpc` failure :571-585 |
|
||||
| b4-5 `-O`-safe integrity + derived PASS | ✅ | load-bearing checks in a `checks` dict; `if failed: raise AssertionError` :385-387 and `if not passed: raise` :388-390 (NO bare `assert` anywhere — grep-confirmed); PASS = `"PASS" if passed else "FAIL"` derived from `all(checks.values())` :393, re-derived+checked in `run_once` :550-553 |
|
||||
|
||||
## MUST-BE-ABSENT sweep
|
||||
|
||||
`P5` / `P6` / `P2-bank` / `retry-launder` / `mosaic yolo` / `execRuntime` / `run_open` / `atomic-observation` /
|
||||
`pi_gate0_run` = **0 hits** (both files). Extension invokes only `bump-generation` / `lifecycle` /
|
||||
`authorize-probe` / `promote-probe`. No live/prod/real-broker path (single fixture broker; allow-list env; launcher
|
||||
pinned to fixture socket). No `.state`/gen-file path outside the fixture temp root (broker `generation_environment`
|
||||
raises otherwise). §4 live effect: none. No extra broker/socket beyond the single p3. No exec-at-import (both files
|
||||
`__main__`-guarded). Mechanism change is confined to the mandated R1 observation-fidelity deepening + BAR4 hardening;
|
||||
no scope-widen of what the probe touches.
|
||||
|
||||
## Observations (transparency — not findings)
|
||||
|
||||
1. The fixture broker retains a **dormant `source-invalid` action** (:179-194, P5-adjacent, in-mem). It is
|
||||
**never invoked** by the harness or its embedded extension (verified: extension actions are only
|
||||
bump/lifecycle/authorize/promote), and `assert_d4` never observes it — so the probe does **not** exercise or bank
|
||||
P5. Pre-existing shared-fixture code, unchanged. Surfaced so Mos may, if desired, apply a stricter
|
||||
purge-dormant-P5-from-the-fixture standard; under the "what the probe TOUCHES/does" framing it is not a violation.
|
||||
2. Fixture `HOME` receives a **read-only copy** of the operator's `~/.pi/agent` `settings.json`/`auth.json`/`bin/fd`
|
||||
(:430-440, `shutil.copy2` into the fixture) so real Pi can authenticate to the model provider. It reads operator
|
||||
state; it does not write/mutate operator HOME and does not emit/log credential material. Confined to the fixture.
|
||||
|
||||
## Verdict
|
||||
|
||||
**PASS @ `48484938`** — BAR1 (traceability to shipped f4008307 mechanism) + BAR2 (genuine file-backed generation,
|
||||
i–iv) + BAR3 (live-path / fail-closed precondition / isolation / intact assertion) + BAR4 (b4-1..b4-5) all hold,
|
||||
with zero out-of-scope surface exercised. The v3 hollow-gate (ancestor pin, in-mem simulation) is structurally
|
||||
closed: evidence now attests the shipped WI-3 D4 file-backed generation mechanism, launcher pinned by head+sha256,
|
||||
child env write-confined by allow-list, integrity `-O`-safe with a derived PASS.
|
||||
|
||||
**Findings: none.**
|
||||
|
||||
## Scope reminder (not a finding)
|
||||
|
||||
Per §3/§5 of the amendment, producing this evidence **executes** the Gate0 mechanism. This review clears the
|
||||
**bytes**; **FIRE remains separately gated on Mos's explicit post-clear GO** and is not authorized by this review.
|
||||
|
||||
---
|
||||
|
||||
**Reviewer:** independent Opus §3-conformance reviewer (Gate-16 author≠reviewer). Byte review only; ran nothing.
|
||||
**Reviewed object (pin):** `484849387006ab5561798506fd6042ddbd5617de` · harness sha256 `9095eab7a4ddf11bb92bb5971d49e1facad12f4692ce2081665b0af47cbe5098`.
|
||||
@@ -1,80 +0,0 @@
|
||||
# Gate0 Probe-3 (D4) Hygiene-Delta — §3-Conformance Review v5
|
||||
|
||||
**Verdict: ❌ FAIL** (hygiene delta (a)+(b) landed correctly and (c)+(d) hold, but homelab findings **NEW-5** and **NEW-6** are present in these bytes; both must close for PASS).
|
||||
|
||||
## Pin (G1 — reviewed object, mandatory)
|
||||
|
||||
- **Reviewed object = `7f975b95ad39096463a7548bd6be0dbb387cb61b`** (harness commit, branch `feat/827-gate0-probe`).
|
||||
- **Reviewed file:** `docs/compaction-refresh/probes/p3_d4_focused_run.py` — sha256 (pushed provider bytes, `-o FILE`, FULL-40 ref, verified before trust): `c3a09a342a4b367184d44472ec6fc11f8a3aabb7e90d5a72aa6b7044b1d9b91e` (24174 bytes, no not-found sentinel).
|
||||
- **Co-reviewed fixture broker:** `p3_generation_broker.py` sha256 `4db4fef1ac6658a8ca79ad5091cefc901d2aa26003265c3d6726c294cf895cad`.
|
||||
|
||||
## Reviewer identity / lane (independence — on the record)
|
||||
|
||||
This review is produced by a **distinct independent Opus §3-conformance / SECREV session** (Gate-16 author≠reviewer),
|
||||
**byte review only, ran nothing**, that **did not build** this harness (author = ms-rev-826) and **is not Mos**. The
|
||||
PROCESS/LANE separation (build lane ≠ review lane) holds and is attested here. Homelab's separate observation — that
|
||||
the published PASS commits and the repair commits share the `ms-lead-reviewer` **Git signer identity** — is a
|
||||
git-identity-signer question I do **not** self-resolve; per instruction it is routed to Mos. My lane attestation is
|
||||
independent of the git signer.
|
||||
|
||||
## Hygiene delta (v4 `48484938` → v5 `7f975b95`) — items (a)+(b): CLOSED
|
||||
|
||||
Full `diff` of both files shows the delta touches **only** these:
|
||||
|
||||
- **(a) Creds scrubbed — CLOSED.** New `scrub_fixture_credentials(root)` (:467-475): `shutil.rmtree(root/"home"/".pi"/"agent")` then a fail-closed re-check `if copied_agent.exists(): raise` (:473-474). Invoked in the **outermost `finally`** (:598), **after** pi/broker cleanup and **before** `return root` (:600) — so it runs on the **success path too**. The scrub target exactly matches the only copy site (`target_agent = fixture_home/".pi"/"agent"`, :433). `isolated_environment` was moved inside the `try` (:485) so the scrub finally also covers a partial env-setup failure. No copied agent credential/config subtree (incl. `auth.json`) survives in any retained root. ✅
|
||||
- **(b) `source-invalid` handler REMOVED — CLOSED.** `grep source-invalid / source_invalidation_revoke == 0` across **both** files. Broker delta is solely the deletion of the old `:179-194` block. ✅
|
||||
|
||||
## (c) Everything-else byte-stable vs `48484938` — HOLDS
|
||||
|
||||
Diff confirms **no other hunks**. Re-swept intact: traceability `GATED_WI_HEAD=f4008307` (:33) + `GATED_LAUNCHER_SHA256=e950e4224e…` (:35); fidelity `read_runtime_generation`/`bump_runtime_generation` + `generation_source=="state-file"` + `new→MUTATOR_UNVERIFIED`/`prior→STALE_GENERATION`; `lease_anchor_registered` + `hex-256`; `-O`-safe (0 bare `assert`, PASS derived `all(checks.values())`); allow-list literal env (0 `os.environ.copy`); `--runs choices=(3,)`; fail-closed precondition; single p3 broker. Blockers 1-4 (inherited env / `--runs`≠3 / cleanup-misses-setup-failure / `-O`-strips-checks) remain closed.
|
||||
|
||||
## (d) ABSENT sweep — 0
|
||||
|
||||
`P5`/`P6`/`P2-bank`/`retry-launder`/`mosaic yolo`/`execRuntime`/`run_open`/`atomic`/`pi_gate0` = 0 (both files);
|
||||
extension invokes only `bump-generation`/`lifecycle`/`authorize-probe`/`promote-probe`; no exec-at-import.
|
||||
|
||||
## Homelab carry-forward — NEW-5 / NEW-6: **PRESENT → FAIL**
|
||||
|
||||
### 🔴 NEW-6 — GATED_WI_ROOT resolves wrong / non-portably (CONFIRMED)
|
||||
`GATED_WI_ROOT = HERE.parents[3].parent / "stack-cr-wi3-revoke"` (:32). For the harness at
|
||||
`…/agent-work/stack-cr-wi0-gate0/docs/compaction-refresh/probes/`, this computes
|
||||
**`/home/hermes/stack-cr-wi3-revoke` — which does not exist**. The actual f4008307 worktree is
|
||||
`/home/hermes/agent-work/stack-cr-wi3-revoke` = `HERE.parents[3] / "stack-cr-wi3-revoke"`. The traversal is
|
||||
**off-by-one** (`.parents[3].parent` should be `.parents[3]`), and it additionally hardcodes the worktree name — a
|
||||
non-portable relative assumption. On this host the precondition therefore fail-closes ("gated WI launcher is
|
||||
unavailable") and the probe cannot locate/execute the pinned launcher at all. **Real resolution defect** (independently
|
||||
computed by path arithmetic; harness not run). **file:line — :32.**
|
||||
*Fix:* derive `GATED_WI_ROOT` from a portable, explicit anchor (e.g. an env-provided path validated to be the
|
||||
f4008307 worktree, or `HERE.parents[3] / "stack-cr-wi3-revoke"` with existence+HEAD assertion), not `.parents[3].parent`.
|
||||
|
||||
### 🔴 NEW-5 — launcher precondition is check-then-exec, not pinned-executed-bytes (CONFIRMED)
|
||||
The precondition hashes `launcher_bytes = GATED_LAUNCHER.read_bytes()` (:280) against `GATED_LAUNCHER_SHA256` (:287),
|
||||
but the launcher is **executed separately** via `PiRpc([sys.executable, str(GATED_LAUNCHER), …])` (:514-515), which
|
||||
opens and **re-reads the file at exec time**. There is **no fd-handoff and no exec-from-verified-copy**, so the
|
||||
verified snapshot does **not** bind the executed bytes. The window between check (:287) and exec (:514-515) spans the
|
||||
broker `Popen` + `wait_path` (≤20 s) — a genuine **check-then-exec TOCTOU / mutable-path trust**; the `git rev-parse
|
||||
HEAD` check (:285-286) is likewise on a mutable HEAD, not the executed bytes. Per the bar this is a real gap.
|
||||
**file:line — hash :280/:287 vs exec :514-515.**
|
||||
*Fix:* execute the exact verified bytes with no window — e.g. read once, verify, and exec from a fixture-private
|
||||
copy of the verified bytes (or `python3 /proc/self/fd/<verified-fd>`), so the hashed bytes == executed bytes.
|
||||
|
||||
## Verdict
|
||||
|
||||
**FAIL @ `7f975b95`.** The hygiene delta itself is correct — (a) creds scrub (fail-closed finally, success path,
|
||||
every retained root) and (b) `source-invalid` removal both landed cleanly, (c) everything else is byte-stable vs
|
||||
`48484938`, and (d) the absent sweep is 0. **However**, homelab's NEW-5 (check-then-exec launcher TOCTOU / not
|
||||
pinned-executed-bytes) and NEW-6 (GATED_WI_ROOT off-by-one/non-portable resolution) are **present in these bytes**;
|
||||
the addendum requires both **closed** for PASS. Not softened. Returns to author (ms-rev-826) — not to a builder
|
||||
re-review, no PASS-launder.
|
||||
|
||||
**Findings:** NEW-6 (`p3_d4_focused_run.py:32`); NEW-5 (`p3_d4_focused_run.py:280/:287` vs `:514-515`).
|
||||
|
||||
## Scope reminder (not a finding)
|
||||
|
||||
Producing this evidence **executes** the Gate0 mechanism (§3/§5). This review clears **bytes** only; FIRE remains
|
||||
separately gated on Mos's explicit post-clear GO — and is moot until this FAIL is remediated.
|
||||
|
||||
---
|
||||
|
||||
**Reviewer:** independent Opus §3-conformance/SECREV reviewer (Gate-16 author≠reviewer). Byte review only; ran nothing.
|
||||
**Reviewed object (pin):** `7f975b95ad39096463a7548bd6be0dbb387cb61b` · harness sha256 `c3a09a342a4b367184d44472ec6fc11f8a3aabb7e90d5a72aa6b7044b1d9b91e`.
|
||||
@@ -1,116 +0,0 @@
|
||||
# GATE0 Probe-3 (#827) — Mos byte-scope-verify CO-ATTESTATION (v-final)
|
||||
|
||||
**Principal:** Mos (orchestrator, merge authority for the mosaic-stack governance lane).
|
||||
**Committed under a DISTINCT git identity** (`mos-orchestrator@mosaic.local`) — deliberately NOT the
|
||||
`ms-lead-reviewer@mosaic.local` lane signer — so this record stands as a *distinct-identity*
|
||||
co-attestation, not a same-signer duplicate. See "Independence" below.
|
||||
|
||||
**Verify class:** independent provider-byte read (guarded `git show <full-40>:path | sha256sum` from a
|
||||
read-only clone of `mosaicstack/stack`). Not a re-build, not a re-run — a byte/scope/hygiene audit of
|
||||
the exact committed objects on the provider branch.
|
||||
|
||||
## Package under attestation
|
||||
|
||||
| Artifact | Ref |
|
||||
|---|---|
|
||||
| Branch | `feat/827-gate0-probe` |
|
||||
| Harness commit-40 | `2d54a9dd14cb924701b2ae4ed72dae4df760c4e3` |
|
||||
| Harness `p3_d4_focused_run.py` sha256 | `15a154df55273f51301763a984485fd63813f6d1f05d2728abb9fb8b9c040b1a` (27366 B) |
|
||||
| §3-review-v6 commit-40 | `23c0caca9b5d44002e6184cd7f2b6c837e8795b2` |
|
||||
| Review path | `docs/compaction-refresh/reviews/GATE0-PROBE3-NEW56-S3-REVIEW-v6.md` |
|
||||
|
||||
sha256 re-confirmed against the checked-out object at `HEAD:docs/compaction-refresh/probes/p3_d4_focused_run.py`
|
||||
(git object id `8c68cd07…`) — matches the relayed value byte-for-byte.
|
||||
|
||||
## Findings — VERDICT: byte-scope + mechanism + hygiene **PASS**
|
||||
|
||||
**Anchors.** Harness sha256 matches (27366 B). review-v6 (`23c0caca`) parent == harness commit
|
||||
`2d54a9dd`; review touches only the review `.md` (+82 lines, 1 file). Broker
|
||||
(`p3_generation_broker.py`) delta vs `48484938…` = **exactly** the 16-line `action=="source-invalid"`
|
||||
handler purge, byte-stable otherwise.
|
||||
|
||||
**NEW-6 (GATED_WI_ROOT off-by-one) — CLOSED.** `resolve_gated_wi_root()` selects the worktree by
|
||||
`git worktree list --porcelain` enumeration, requires a UNIQUE match on `HEAD==GATED_WI_HEAD`
|
||||
(`f400830738998db105107a2a4c69c7f2a2a6fd5d`) AND `branch==refs/heads/feat/830-compaction-revoke`,
|
||||
then fail-closed re-validates (`is-inside-work-tree==true`, `rev-parse HEAD==GATED_WI_HEAD`);
|
||||
`RuntimeError` on ambiguity/mismatch. The `HERE.parents[3].parent / "stack-cr-wi3-revoke"` off-by-one
|
||||
and the hardcoded `/home/hermes/...` literal are **gone** — portable, zero hardcoded path.
|
||||
|
||||
**NEW-5 (TOCTOU / pinned-executed-bytes) — CLOSED via approach (i), as mandated.** The `git`-object
|
||||
sha256 pin (`GATED_LAUNCHER_SHA256 = e950e422…`) is the trust anchor. Ordering/marker `.find()`
|
||||
heuristics are downgraded to explicitly diagnostic-only ("never a substitute for the pin"). In
|
||||
`launch_verified_pi()` the executed working-tree file is re-hashed against the pin **in the statement
|
||||
immediately before `Popen`** (no interleaved yield/IO), and the launcher is executed **in place at the
|
||||
pinned worktree path** — the higher-risk approach (ii) copy-to-fixture (previously at `7ff63cd5` /
|
||||
`6164dc07`) is **reverted** (the only remaining `shutil.copy2` is the legitimate credential copy, not a
|
||||
launcher copy). Residual sub-statement TOCTOU window on a local file inside a non-adversarial operator
|
||||
fixture is within this probe's threat model; the gross precondition→much-later-exec gap homelab flagged
|
||||
is closed.
|
||||
|
||||
**Hygiene — CLOSED.** `scrub_fixture_credentials(root)` removes the entire `.pi/agent` subtree in a
|
||||
`finally` block (nested try/finally, after `pi.close()` + broker shutdown, before `return root`) and
|
||||
`RuntimeError`s if the scrub fails — credentials are removed from retained evidence; logs retained.
|
||||
|
||||
**Invariants byte-stable (all INTACT):** assertion `lease_anchor_registered`; file-backed fidelity
|
||||
checks (`generation_source=="state-file"`, `state_file_in_fixture_root`, `MUTATOR_UNVERIFIED`,
|
||||
`STALE_GENERATION`); `-O`-safe (0 bare `assert`); allow-list env (0 `os.environ.copy`); single broker;
|
||||
`--runs choices=(3,)`; ABSENT-sweep (`yolo`/`execRuntime`/`p3_bank`/`promote_p2`/`retry`) = 0.
|
||||
|
||||
**R1 mechanism-fidelity boundary — HELD.** D4 exercises #830's real file-backed revocation
|
||||
(`generation-{sid}.state` / `MOSAIC_LEASE_GENERATION_FILE`, from `66b1e0a0`); `.state` stays inside the
|
||||
fixture temp root; fixture-socket / child-write-escape / gettempdir isolation preconditions preserved;
|
||||
launcher exec is in place at the pinned worktree (same surface as R1, not a new production/live
|
||||
surface). **No path escapes the fixture root.** BEYOND-R1 tripwire NOT tripped.
|
||||
|
||||
## Independence
|
||||
|
||||
- **Substantive principal-independence** of review-v6 is satisfied by an orchestrator-dispatched,
|
||||
builder-distinct Opus SECREV (`ms-secrev-828`, byte-only, non-builder) — that IS the substance of
|
||||
Gate-16.
|
||||
- The shared `ms-lead-reviewer` git signer on harness+review commits is **evidentiary, not
|
||||
substantive**. It is resolved by (1) this Mos co-attestation committed under a **distinct** identity
|
||||
(`mos-orchestrator`), and (2) a homelab third-principal verify under its own distinct identity —
|
||||
i.e. three distinct-identity principals of record.
|
||||
- The shared signer is a tracked **fleet-infra tooling-gap** (durable fix = per-lane distinct signers),
|
||||
**not a blocker**.
|
||||
|
||||
## Scope of this record — byte-clear, NOT fire-authorization
|
||||
|
||||
Producing probe evidence **executes** the Gate0 mechanism; a byte-clear is not a fire-authorization.
|
||||
This co-attestation clears the **bytes/scope/hygiene**. FIRE remains gated on: **homelab
|
||||
third-principal verify** + **Mos transparency-to-Jason** (real-Pi consumes operator model creds inside
|
||||
the isolated fixture, scrubbed post-run, never emitted) + **Mos explicit FIRE GO**. Until then: nothing
|
||||
banked, WI-3 #830 held at `f4008307` (unmoved), C-hatch armed (if 3× isolation still no-fire /
|
||||
wrong-value / isolation-FAIL → possible Case-C → STOP + escalate to Jason).
|
||||
|
||||
**Mos verdict: byte-scope + mechanism + hygiene PASS. Co-attestation of record — committed.**
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ SUPERSEDED — homelab third-principal FAIL raised a stricter bar (evidence-integrity note)
|
||||
|
||||
This co-attestation was **byte-clear on the v6 bar ONLY** and self-limited above to *"byte-clear
|
||||
ONLY, NOT fire-authorization; FIRE remains gated on homelab third-principal verify."* Homelab (the
|
||||
required third principal) subsequently returned **FAIL @2d54a9dd**, and Mos **UPHELD** it — so the v6
|
||||
byte-clear this document records is **SUPERSEDED** and does **NOT** authorize FIRE.
|
||||
|
||||
Homelab's substantively-correct deepening (accepted as gate-**strengthening**, not softening):
|
||||
1. `launch_verified_pi` hashes the launcher then `Popen`/execve **reopens the path** → statement
|
||||
adjacency shrinks but does not eliminate TOCTOU; hashed-snapshot ≠ executed-bytes.
|
||||
2. `lease_generation.py` helper is unpinned, loaded from the mutable worktree → HEAD + launcher-pin
|
||||
do not bind the helper bytes.
|
||||
3. `p3_generation_broker.py` executes from the mutable worktree unhashed → reviewed broker bytes need
|
||||
not be the evidence-producing bytes.
|
||||
|
||||
For a fail-closed DO-178C evidence gate, **hashed==executed must hold on the FULL executed closure**
|
||||
(launcher + helper + broker), which v6 (approach (i) adjacency) does not meet. Mos therefore
|
||||
**authorized approach (ii) full-closure materialization** (SHA-pin + materialize the full closure into
|
||||
a fixture-private 0700/0600 dir or held verified fds, exec from there, launcher+broker consume the same
|
||||
pinned helper; re-hash==f4008307 pins immediately before exec, fail-closed). This rides the existing R1
|
||||
authorization + Mos adjudication authority (it deepens isolation of already-authorized touch and stays
|
||||
inside the fixture temp root → R1 owner tripwire not tripped; no fresh owner window).
|
||||
|
||||
**Live target = v7** (materialized-closure harness, forthcoming). `2d54a9dd` / `23c0caca` / this
|
||||
co-attestation (`12914d8`) are **SUPERSEDED**. A fresh Mos co-attestation will be committed on v7
|
||||
byte-verify PASS. WI-3 #830 remains HELD at `f4008307`; nothing banked; C-hatch armed.
|
||||
|
||||
@@ -1,97 +0,0 @@
|
||||
# GATE0 Probe-3 (#827) — Mos byte-scope-verify CO-ATTESTATION (v10 no-site startup closure)
|
||||
|
||||
**Principal:** Mos (orchestrator, merge authority for the mosaic-stack governance lane).
|
||||
**Committed under a DISTINCT git identity** (`mos-orchestrator@mosaic.local`) — deliberately NOT the
|
||||
`ms-lead-reviewer@mosaic.local` lane signer that authored the harness and the §3 review — so this
|
||||
record stands as a *distinct-identity* co-attestation. See "Independence".
|
||||
|
||||
**Verify class:** independent provider-byte read (`git show <full-40>:path | sha256sum`) plus a
|
||||
git-diff byte-comparison of the v9→v10 delta and source inspection of the executed command lines. Not
|
||||
a re-build, not a re-run. This SUPERSEDES the v9 co-attestation `f320d075` (byte-clear on the v9
|
||||
env-iso + bytecode-pin bar), which was overtaken by homelab's 4th-round FAIL @1c34e3cb
|
||||
(site-startup-closure hole) and the resulting B9 bar.
|
||||
|
||||
## Package under attestation
|
||||
|
||||
| Artifact | Ref |
|
||||
|---|---|
|
||||
| Branch | `feat/827-gate0-probe` (mosaicstack/stack) |
|
||||
| Harness commit-40 | `ce5ba762051354338889959bfce2b0381f4a4e2a` |
|
||||
| Harness `p3_d4_focused_run.py` sha256 | `7e14ead89a7b2a297fcc17e7653291b3bcace1d2002a8f90a989db74f6985b6f` (32753 B) |
|
||||
| §3-review-v10 commit-40 | `ffc3b573a8258a86e3ece9ef92bac925fb5a7a5b` (parent == harness `ce5ba762`, byte-stable; adds only the review `.md`, +86; ms-secrev-828 authored) |
|
||||
| Review path | `docs/compaction-refresh/reviews/GATE0-PROBE3-V10-NOSITE-S3-REVIEW-v10.md` |
|
||||
|
||||
**Pinned closure (all 3 sha256 INDEPENDENTLY recomputed by Mos from git objects == pins == embedded harness constants; UNCHANGED from v9):**
|
||||
|
||||
| Member | Source (commit:path) | Pin sha256 | Size |
|
||||
|---|---|---|---|
|
||||
| launcher `launch-runtime.py` | `f4008307:packages/mosaic/framework/tools/lease-broker/launch-runtime.py` | `e950e422…` | 4237 B |
|
||||
| helper `lease_generation.py` | `f4008307:packages/mosaic/framework/tools/lease-broker/lease_generation.py` | `061625402f…` | 3769 B |
|
||||
| broker `p3_generation_broker.py` | `23c0caca:docs/compaction-refresh/probes/p3_generation_broker.py` | `4db4fef1…` | — |
|
||||
|
||||
> `f4008307` (held WI-3 #830 gated head) is **not on origin** — recomputed launcher+helper from the
|
||||
> local `stack-cr-wi3-revoke` worktree (HEAD == `f4008307`); broker from origin `23c0caca`.
|
||||
|
||||
## Findings — VERDICT: byte-scope + mechanism + hygiene **PASS** (v10 = v9 + B9 no-site)
|
||||
|
||||
The v9→v10 delta is **exactly 2 insertions / 0 deletions** (git-diff byte-compared), a `+28 B`
|
||||
harness growth **fully accounted** (2 × ` "-S",\n` = 14 B each). Every prior invariant is
|
||||
byte-stable — the diff touches nothing outside the two command lists.
|
||||
|
||||
**B9 — no-site startup closure (homelab 4th-round finding CLOSED).** Both child command lists now
|
||||
carry `-S`, which disables the `site` module entirely (no `.pth` executable lines, no
|
||||
`sitecustomize`/`usercustomize`) so no unpinned startup code runs before the exact launcher/broker or
|
||||
outside `closure_import_guard`:
|
||||
- launcher `launch_verified_pi` (`:515`): `-s` + **`-S`** + `-B`, **no `-I`** (`-I` appears exactly
|
||||
once in the harness, `:553`, the broker only).
|
||||
- broker `launch_verified_broker` (`:554`): `-I` + **`-S`** + `-B`.
|
||||
|
||||
Homelab's finding was **independently reproduced by Mos** and the fix **empirically verified** on host
|
||||
Py3.11.2 (throwaway, not the harness): `[-s -B]`→`no_site=0, site imported` (the v9 hole); `[-I -B]`→
|
||||
`no_site=0, site imported` (proves `-I` does **not** imply `-S`); `[-s -S -B]`→`no_site=1`, `site` not
|
||||
in `sys.modules`, **and the launcher's bare `from lease_generation import` still resolves** (`sys.path[0]`
|
||||
untouched by `-S` → no B6c regression); `[-I -S -B]`→`no_site=1`. The launcher deliberately omits `-I`
|
||||
(B6c: on 3.11+ `-I` implies `-P`, dropping the script dir from `sys.path[0]` → sibling import breaks);
|
||||
its env isolation comes from the `PiRpc` `env=` allow-list, not `-I`.
|
||||
|
||||
**All priors — byte-stable (outside the 2-line delta, re-confirmed from the v9 verify):**
|
||||
B5 conjunction (materialize-from-pin / `mkdir(0o700)`+`O_EXCL` no-writer-window / re-hash==pin
|
||||
immediately-before-exec); B6 (single pinned helper bound; `closure_import_guard` AST-reject); B6c
|
||||
(launcher no `-I`); B7 (broker `env=environment` strict allow-list `:570`); B8 (`reject_pinned_bytecode`
|
||||
fail-closed `:498-501` before each consumer `:535/:562` + `PYTHONDONTWRITEBYTECODE=1` `:729` + `-B` on
|
||||
both); fidelity asserts (`generation_source=='state-file'`, `state_file_in_fixture_root`,
|
||||
`MUTATOR_UNVERIFIED`, `STALE_GENERATION`); `lease_anchor_registered`; BAR1 `GATED_WI_HEAD==f4008307`
|
||||
(`:36`) + `merge-base --is-ancestor 66b1e0a0 f4008307` = **YES**; `--runs choices=(3,)`; `-O`-safe
|
||||
(0 bare `assert`); allow-list env (0 `os.environ.copy`); single broker (1 def + 1 call); the only
|
||||
`shutil.copy2` is the `.pi/agent` credential copy.
|
||||
|
||||
**Closure = exactly 3 files, materialized inside the fixture root.** No path escapes the fixture temp
|
||||
root; no live/default broker; `.state` fixture-bound. **R1 owner tripwire NOT tripped** — `-S`
|
||||
deepens startup-closure isolation of an already-authorized touch; it does not widen the touched surface.
|
||||
|
||||
## Independence
|
||||
|
||||
Substantive principal-independence of review-v10 is satisfied by an orchestrator-dispatched,
|
||||
builder-distinct Opus SECREV (`ms-secrev-828`, byte-only, non-builder, non-Mos). The shared
|
||||
`ms-lead-reviewer` git signer on harness+review commits is evidentiary, not substantive — resolved by
|
||||
(1) this Mos co-attestation under a **distinct** identity (`mos-orchestrator`) and (2) a homelab
|
||||
third-principal verify under its own distinct identity = three distinct-identity principals of record.
|
||||
The prior 2-of-3 (`ms-secrev-828` v9 §3 PASS + `f320d075`) does **not** carry — all three re-verify
|
||||
this v10 SHA. Shared signer = tracked fleet-infra tooling-gap, not a blocker.
|
||||
|
||||
## Scope of this record — byte-clear, NOT fire-authorization
|
||||
|
||||
Producing probe evidence **executes** the Gate0 mechanism; a byte-clear is not a fire-authorization.
|
||||
This clears **bytes / scope / mechanism / hygiene on the v10 (v9 + B9 no-site) bar**. FIRE remains
|
||||
gated on: **homelab third-principal re-verify** (5th round, own distinct identity) + **Mos
|
||||
transparency-to-Jason** + **Mos explicit FIRE GO**. The FIRE GO additionally carries an
|
||||
**execution-procedure constraint**: the 3× isolation dispatch must launch the runner under
|
||||
externally-enforced **`python -I -S -B p3_d4_focused_run.py`** — a self-reexec is too late, the
|
||||
harness's own `site` runs before it could re-add `-S` to itself. Until FIRE GO: nothing banked, WI-3
|
||||
#830 held at `f4008307` (unmoved), C-hatch armed (fired-rig only: no-fire / wrong-value /
|
||||
assertion-FAIL / isolation-FAIL → possible Case-C → STOP + escalate to Jason).
|
||||
|
||||
Prior v10-superseded set: `1c34e3cb` / `e1c9a468` / `f320d075` (and transitively the v7 chain).
|
||||
|
||||
**Mos verdict: v10 (v9 + B9 no-site) byte-scope + mechanism + hygiene PASS. Co-attestation of
|
||||
record — committed.**
|
||||
@@ -1,131 +0,0 @@
|
||||
# GATE0 Probe-3 (#827) — Mos byte-scope-verify CO-ATTESTATION (v7 full-closure)
|
||||
|
||||
**Principal:** Mos (orchestrator, merge authority for the mosaic-stack governance lane).
|
||||
**Committed under a DISTINCT git identity** (`mos-orchestrator@mosaic.local`) — deliberately NOT the
|
||||
`ms-lead-reviewer@mosaic.local` lane signer that authored both the harness and the §3 review — so this
|
||||
record stands as a *distinct-identity* co-attestation. See "Independence" below.
|
||||
|
||||
**Verify class:** independent provider-byte read (guarded `git show <full-40>:path | sha256sum`) plus
|
||||
source-level inspection of the executed mechanism. Not a re-build, not a re-run. This SUPERSEDES the v6
|
||||
co-attestation `12914d8` (and its SUPERSEDED-note `b6bd0cd`), which was byte-clear on the v6 bar only
|
||||
and was overtaken by homelab's third-principal FAIL @2d54a9dd + the resulting stricter full-closure bar.
|
||||
|
||||
## Package under attestation
|
||||
|
||||
| Artifact | Ref |
|
||||
|---|---|
|
||||
| Branch | `feat/827-gate0-probe` |
|
||||
| Harness commit-40 | `f609a44953f5ae61916805fcb45ca337de00b0b0` |
|
||||
| Harness `p3_d4_focused_run.py` sha256 | `0f1bd1b39399b32f243d901230e2d840794a2144edd723a095dab716833a7a9b` (32071 B) |
|
||||
| §3-review-v7 commit-40 | `2bba933f67c821899d320a938a9473a73a136422` (adds only the review `.md`; harness parent byte-stable) |
|
||||
| Review path | `docs/compaction-refresh/reviews/GATE0-PROBE3-V7-FULLCLOSURE-S3-REVIEW-v7.md` |
|
||||
|
||||
**Pinned closure (all 3 sha256 INDEPENDENTLY recomputed by Mos from git objects == pins):**
|
||||
|
||||
| Member | Source (commit:path) | Pin sha256 | Size |
|
||||
|---|---|---|---|
|
||||
| launcher `launch-runtime.py` | `f4008307:packages/mosaic/framework/tools/lease-broker/launch-runtime.py` | `e950e422…` | 4237 B |
|
||||
| helper `lease_generation.py` | `f4008307:packages/mosaic/framework/tools/lease-broker/lease_generation.py` | `061625402f…` | 3769 B |
|
||||
| broker `p3_generation_broker.py` | `23c0caca:docs/compaction-refresh/probes/p3_generation_broker.py` | `4db4fef1…` | — |
|
||||
|
||||
> Note: `f4008307` (the held WI-3 #830 gated head) is **not on origin** — it exists only as a local
|
||||
> `git worktree` on the build host. Mos recomputed the launcher+helper pins from that worktree
|
||||
> (`stack-cr-wi3-revoke`, HEAD == `f4008307`) rather than passing over a clone-completeness gap. The
|
||||
> broker pin was recomputed from origin `23c0caca`.
|
||||
|
||||
## Findings — VERDICT: byte-scope + mechanism + hygiene **PASS** (v7 full-closure bar)
|
||||
|
||||
Homelab's stricter bar — **hashed==executed on the FULL executed closure (launcher + helper + broker)**
|
||||
— is met. Verified at the source, not accepted on the review's assertion:
|
||||
|
||||
**B5 conjunction (the load-bearing repair) — HELD, all three legs:**
|
||||
- **(a) materialized from pinned git-object bytes, NOT the mutable worktree.** `materialize_closure`
|
||||
fetches each member via `git_object_bytes` (`git show {commit}:{path}`), then
|
||||
`sha256(data) == pin` **fail-closed** (`RuntimeError` on mismatch) before use.
|
||||
- **(b) no writable window hash→consume.** `pinned/` is `mkdir(mode=0o700)`; each file is written with
|
||||
`os.open(O_WRONLY|O_CREAT|O_EXCL|O_CLOEXEC, 0o600)` — `O_EXCL` refuses a pre-planted file. No `chmod`,
|
||||
no `os.rename/replace`, no `symlink`, and nothing re-opens a pinned file for write (grep = 0). The
|
||||
pinned bytes are immutable within the fixture threat model between hash and exec.
|
||||
- **(c) re-hash == pin IMMEDIATELY before each exec, no interleaved yield.** Launcher: re-hash then
|
||||
`return PiRpc(command,…)` whose `__init__` **first statement** is `subprocess.Popen(command,…)` —
|
||||
zero IO/yield/reopen between. Broker + helper: both re-hashed then `subprocess.Popen` on the next
|
||||
line. Adjacency-alone-without-materialization (the v6 defect) is **absent** — all three exec from
|
||||
`pinned/`.
|
||||
|
||||
**B6 — helper pinned AND bound to the SAME single copy.** The broker receives
|
||||
`--generation-module {closure.generation}` (the pinned helper); the launcher runs from `pinned/` so its
|
||||
`import lease_generation` resolves to the sibling pinned copy via `sys.path[0]`. `closure_import_guard`
|
||||
AST-parses every member and raises on any non-stdlib import other than the allowed `lease_generation`
|
||||
— proving the dependency closure is complete and no unpinned module can enter at runtime.
|
||||
|
||||
**Closure = exactly 3 files, materialized inside the fixture root** (`root / "pinned"`). No path escapes
|
||||
the fixture temp root; no live/default broker; `.state` remains fixture-bound. **R1 owner tripwire NOT
|
||||
tripped** — this deepened isolation of an already-authorized touch, it did not widen the touched surface.
|
||||
|
||||
**Invariants (all INTACT):** BAR1 `GATED_WI_HEAD == f4008307` and `merge-base --is-ancestor 66b1e0a0
|
||||
f4008307` = YES (file-backed `.state` revocation fidelity present); BAR2 `.state` =
|
||||
`socket_path.parent / generation-{sid}.state`, `state_file_in_fixture_root` + `generation_source ==
|
||||
"state-file"` checks present; BAR3 `lease_anchor_registered` / live-path / fixture-socket isolation
|
||||
intact. `-O`-safe (0 bare `assert`); ABSENT-sweep (`yolo`/`execRuntime`/`p3_bank`/`promote_p2`/`retry`)
|
||||
= 0; single broker (1 def + 1 call site); `--runs choices=(3,)`; allow-list env (0 `os.environ.copy`);
|
||||
the only `shutil.copy2` is the legitimate `.pi/agent` credential copy (settings/auth/fd), **not** a
|
||||
launcher/helper/broker copy — the v6 copy-to-fixture concern is gone.
|
||||
|
||||
## Independence
|
||||
|
||||
- **Substantive principal-independence** of review-v7 is satisfied by an orchestrator-dispatched,
|
||||
builder-distinct Opus SECREV (`ms-secrev-828`, byte-only, non-builder) — that IS the substance of
|
||||
Gate-16.
|
||||
- The shared `ms-lead-reviewer` git signer on both the harness (`ms-rev-826` build) and the review
|
||||
commit is **evidentiary, not substantive**. It is resolved by (1) this Mos co-attestation under a
|
||||
**distinct** identity (`mos-orchestrator`), and (2) a homelab third-principal verify under its own
|
||||
distinct identity — three distinct-identity principals of record. Tracked fleet-infra tooling-gap
|
||||
(durable fix = per-lane distinct signers), **not a blocker**.
|
||||
|
||||
## Scope of this record — byte-clear, NOT fire-authorization
|
||||
|
||||
Producing probe evidence **executes** the Gate0 mechanism; a byte-clear is not a fire-authorization.
|
||||
This co-attestation clears the **bytes / scope / mechanism / hygiene on the v7 full-closure bar**. FIRE
|
||||
remains gated on: **homelab third-principal re-verify** (under its own distinct identity) + **Mos
|
||||
transparency-to-Jason** + **Mos explicit FIRE GO**. Until then: nothing banked, WI-3 #830 held at
|
||||
`f4008307` (unmoved), C-hatch armed (if the materialized-closure rig still no-fire / wrong-value /
|
||||
assertion-FAIL / isolation-FAIL → possible Case-C → STOP + escalate to Jason).
|
||||
|
||||
**Mos verdict: v7 full-closure byte-scope + mechanism + hygiene PASS. Co-attestation of record —
|
||||
committed.**
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ SUPERSEDED — homelab v7 third-principal FAIL @f609a449 raised a stricter bar (evidence-integrity note)
|
||||
|
||||
This co-attestation was **byte-clear on the v7 full-closure bar ONLY** and self-limited above to
|
||||
*"byte-clear NOT fire-authorization; FIRE remains gated on homelab third-principal re-verify."*
|
||||
Homelab (the required third principal) subsequently returned **FAIL @f609a449** (static verify, no
|
||||
code run), and Mos **UPHELD** it after independently confirming both findings in source — so the v7
|
||||
byte-clear this document records is **SUPERSEDED** and does **NOT** authorize FIRE.
|
||||
|
||||
Two residual isolation/binding holes WITHIN the materialized closure (both independently reproduced
|
||||
by Mos in the harness source; accepted as gate-**strengthening**, not softening):
|
||||
1. **Broker child env not isolated.** `launch_verified_broker` (`:551`) calls `Popen` with **no
|
||||
`env=`** (only `PiRpc.__init__` `:50` passes an allow-listed `env`) → the broker child inherits
|
||||
ambient `os.environ` (PYTHONPATH/PYTHONHOME/PYTHONPYCACHEPREFIX). `closure_import_guard` is a
|
||||
static AST check and cannot bind the child's runtime stdlib resolution.
|
||||
2. **Executed bytecode-cache outside the pin.** No `PYTHONDONTWRITEBYTECODE`/`-I`/`-B`/`__pycache__`
|
||||
handling anywhere. `exec_module` on the pinned helper writes derived `.pyc` the pin never covers;
|
||||
only the `.py` is re-hashed → executed bytecode ≠ pinned-source-hash.
|
||||
|
||||
Both break "hashed==executed on the FULL executed closure" on **fidelity** grounds even in a
|
||||
non-adversarial fixture. Mos **authorized the bounded repair** (broker `Popen` with strict
|
||||
allow-listed `env=` + `-I` + `PYTHON*` stripped; bytecode-cache suppressed via
|
||||
`PYTHONDONTWRITEBYTECODE=1`/`-B` + reject stray `__pycache__`/`.pyc` fail-closed before each
|
||||
consumer; launcher sibling-import binding to the pinned helper preserved). This **rides the existing
|
||||
(ii)-full-closure authorization + R1 + Mos adjudication** — it deepens isolation/binding of an
|
||||
already-authorized touch, stays inside the fixture temp root, no fresh Jason owner-window. It is
|
||||
**NOT a Case-C escalation** (no probe fired, no evidence produced — a static pre-fire catch, exactly
|
||||
what the review gate is for). Added review bars **B7** (broker child env-isolated) and **B8**
|
||||
(executed bytecode pinned-or-suppressed) on top of B5+B6+all priors.
|
||||
|
||||
**Live target = v8** (env-isolated + bytecode-pinned harness, forthcoming). `f609a449` /
|
||||
`2bba933f` / this co-attestation (`e08ad03`) are **SUPERSEDED**. A fresh Mos co-attestation will be
|
||||
committed on v8 byte-verify PASS. WI-3 #830 remains HELD at `f4008307`; nothing banked; C-hatch
|
||||
armed; NO FIRE.
|
||||
@@ -1,145 +0,0 @@
|
||||
# GATE0 Probe-3 (#827) — Mos byte-scope-verify CO-ATTESTATION (v9 full-closure, env-isolated + bytecode-pinned)
|
||||
|
||||
**Principal:** Mos (orchestrator, merge authority for the mosaic-stack governance lane).
|
||||
**Committed under a DISTINCT git identity** (`mos-orchestrator@mosaic.local`) — deliberately NOT the
|
||||
`ms-lead-reviewer@mosaic.local` lane signer that authored the harness and the §3 review — so this
|
||||
record stands as a *distinct-identity* co-attestation. See "Independence".
|
||||
|
||||
**Verify class:** independent provider-byte read (`git show <full-40>:path | sha256sum`) plus
|
||||
source-level inspection of the executed mechanism and the v7→v9 delta. Not a re-build, not a re-run.
|
||||
This SUPERSEDES the v7 co-attestation `e08ad03` (and its SUPERSEDED note `2ae379e`), which was
|
||||
byte-clear on the v7 bar and was overtaken by homelab's third-principal FAIL @f609a449 (broker
|
||||
env-isolation + executed-bytecode-cache) and the resulting B7/B8 bar.
|
||||
|
||||
## Package under attestation
|
||||
|
||||
| Artifact | Ref |
|
||||
|---|---|
|
||||
| Branch | `feat/827-gate0-probe` |
|
||||
| Harness commit-40 | `1c34e3cb3172acdcd094e683e847d7c984afc96c` |
|
||||
| Harness `p3_d4_focused_run.py` sha256 | `29e5c7bfbe1911b52984bd94c79036bb1200ee82588318367b13c2b1053a0103` (32725 B) |
|
||||
| §3-review-v9 commit-40 | `e1c9a4682da2892ca5f5381012caffe1dd7b43a7` (parent == harness `1c34e3cb`, byte-stable; adds only the review `.md`; ms-secrev-828 authored) |
|
||||
| Review path | `docs/compaction-refresh/reviews/GATE0-PROBE3-V9-LAUNCHERFIX-S3-REVIEW-v9.md` |
|
||||
|
||||
**Pinned closure (all 3 sha256 INDEPENDENTLY recomputed by Mos from git objects == pins == embedded harness constants):**
|
||||
|
||||
| Member | Source (commit:path) | Pin sha256 | Size |
|
||||
|---|---|---|---|
|
||||
| launcher `launch-runtime.py` | `f4008307:packages/mosaic/framework/tools/lease-broker/launch-runtime.py` | `e950e422…` | 4237 B |
|
||||
| helper `lease_generation.py` | `f4008307:packages/mosaic/framework/tools/lease-broker/lease_generation.py` | `061625402f…` | 3769 B |
|
||||
| broker `p3_generation_broker.py` | `23c0caca:docs/compaction-refresh/probes/p3_generation_broker.py` | `4db4fef1…` | — |
|
||||
|
||||
> `f4008307` (held WI-3 #830 gated head) is **not on origin** — recomputed launcher+helper from the
|
||||
> local `stack-cr-wi3-revoke` worktree (HEAD == `f4008307`); broker from origin `23c0caca`.
|
||||
|
||||
## Findings — VERDICT: byte-scope + mechanism + hygiene **PASS** (v9 = v7 full-closure + B7 + B8)
|
||||
|
||||
The v7→v9 delta is **exactly 22 insertions / 2 deletions**, confined to the intended B7+B8+B6c
|
||||
surface; every prior invariant is byte-stable (outside the delta) from the v7 verify.
|
||||
|
||||
**B7 — broker child env-ISOLATED (homelab finding 1 CLOSED).** `launch_verified_broker` (`:568`) now
|
||||
passes `env=environment` (the strict allow-list, `:570`) — the ambient-`os.environ`-inheritance hole
|
||||
is gone — AND runs the broker with `-I` (`:552`, isolated: ignores `PYTHON*`/user-site) + `-B`
|
||||
(`:553`). Both children are env-controlled: the launcher was already `env=env` at `PiRpc` (`:53`).
|
||||
|
||||
**B8 — executed bytecode PINNED/SUPPRESSED (homelab finding 2 CLOSED).** `reject_pinned_bytecode`
|
||||
(`:498`) raises `RuntimeError` fail-closed if a `__pycache__` dir or any `*.pyc` exists in the pinned
|
||||
dir, and is called before **each** consumer (launcher `:535`, broker `:562`). Bytecode writes are
|
||||
disabled via `PYTHONDONTWRITEBYTECODE=1` (`:729`) in the allow-list env **and** `-B` on both command
|
||||
lines. No unpinned `.pyc` can be executed; only the pinned `.py` re-hash governs.
|
||||
|
||||
**B6c — launcher sibling-import PRESERVED (v8 regression FIXED).** v8 over-applied `-I` to the
|
||||
launcher; on Py3.11+ `-I` implies `-P`, dropping the script dir from `sys.path[0]`, so the pinned
|
||||
launcher's bare `from lease_generation import` (launch-runtime.py:15) would `ModuleNotFoundError`. v9
|
||||
uses `-s` (`:514`) + `-B` (`:515`) on the launcher (NO `-I`) — neither touches `sys.path[0]`, so the
|
||||
sibling import still resolves to `pinned/lease_generation.py`. **Mos empirically re-verified on host
|
||||
Py3.11.2** (throwaway, not the harness): `-I` launcher → `ModuleNotFoundError`; `-s`+`PYTHONNOUSERSITE`
|
||||
→ import OK. The launcher's env isolation comes from the `PiRpc` `env=` allow-list, NOT `-I`, so
|
||||
dropping `-I` does **not** reopen B7. My earlier constraint-(c) assumption ("`-I` does not strip the
|
||||
script dir") was FALSIFIED for 3.11+; the author≠reviewer gate (ms-secrev-828) caught it — recorded.
|
||||
|
||||
**B5 conjunction (load-bearing repair) — HELD, all three legs (byte-stable from v7):**
|
||||
(a) materialized from pinned git-object bytes via `materialize_closure`/`git_object_bytes`, `sha256==pin`
|
||||
fail-closed; (b) `pinned/` `mkdir(0o700)` + `O_EXCL|O_CLOEXEC` `0o600`, no writable window — now also
|
||||
`reject_pinned_bytecode` closes the `.pyc` side-channel; (c) re-hash == pin IMMEDIATELY before each
|
||||
exec, no interleaved yield: launcher re-hash (`:538`) → `return PiRpc(command,…)` whose `__init__`
|
||||
first statement is `Popen` (`:50`); broker re-hash (`:563`) + helper re-hash (`:566`) → `Popen`
|
||||
(`:568`) on the next line.
|
||||
|
||||
**B6 — single pinned helper, complete closure.** Broker gets `--generation-module {closure.generation}`;
|
||||
launcher resolves `import lease_generation` to the sibling pinned copy via `sys.path[0]`.
|
||||
`closure_import_guard` (`:351`, called `:433`) AST-rejects any non-stdlib import other than
|
||||
`lease_generation`. Single broker: `launch_verified_broker` 1 def (`:543`) + 1 call (`:766`).
|
||||
|
||||
**Invariants (all INTACT):** BAR1 `GATED_WI_HEAD == f4008307` (`:36`) and `merge-base --is-ancestor
|
||||
66b1e0a0 f4008307` = YES (file-backed `.state` revocation fidelity present); fidelity
|
||||
`generation_source=='state-file'` (`:639`), `state_file_in_fixture_root` (`:641`),
|
||||
`MUTATOR_UNVERIFIED` (`:650`), `STALE_GENERATION` (`:651`); `lease_anchor_registered` (`:593`);
|
||||
`-O`-safe (0 bare `assert`); `--runs choices=(3,)` (`:838`); allow-list env (0 `os.environ.copy`);
|
||||
ABSENT-sweep (`yolo`/`execRuntime`/`p3_bank`/`promote_p2`/`retry`) = 0; the only `shutil.copy2`
|
||||
(`:708`) is the `.pi/agent` credential copy, not a closure copy.
|
||||
|
||||
**Closure = exactly 3 files, materialized inside the fixture root.** No path escapes the fixture temp
|
||||
root; no live/default broker; `.state` fixture-bound. **R1 owner tripwire NOT tripped** — B7/B8
|
||||
deepen isolation/binding of an already-authorized touch, they do not widen the touched surface.
|
||||
|
||||
## Independence
|
||||
|
||||
Substantive principal-independence of review-v9 is satisfied by an orchestrator-dispatched,
|
||||
builder-distinct Opus SECREV (`ms-secrev-828`, byte-only, non-builder, non-Mos). The shared
|
||||
`ms-lead-reviewer` git signer on harness+review commits is evidentiary, not substantive — resolved by
|
||||
(1) this Mos co-attestation under a **distinct** identity (`mos-orchestrator`) and (2) a homelab
|
||||
third-principal verify under its own distinct identity = three distinct-identity principals of record.
|
||||
Shared signer = tracked fleet-infra tooling-gap (durable fix = per-lane distinct signers), not a blocker.
|
||||
|
||||
## Scope of this record — byte-clear, NOT fire-authorization
|
||||
|
||||
Producing probe evidence **executes** the Gate0 mechanism; a byte-clear is not a fire-authorization.
|
||||
This clears **bytes / scope / mechanism / hygiene on the v9 (full-closure + B7 + B8) bar**. FIRE
|
||||
remains gated on: **homelab third-principal re-verify** (4th round, own distinct identity) + **Mos
|
||||
transparency-to-Jason** + **Mos explicit FIRE GO**. Until then: nothing banked, WI-3 #830 held at
|
||||
`f4008307` (unmoved), C-hatch armed (materialized-closure rig still no-fire / wrong-value /
|
||||
assertion-FAIL / isolation-FAIL → possible Case-C → STOP + escalate to Jason).
|
||||
|
||||
**Mos verdict: v9 full-closure + B7 + B8 byte-scope + mechanism + hygiene PASS. Co-attestation of
|
||||
record — committed.**
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ SUPERSEDED — homelab v9 4th-round FAIL @1c34e3cb raised a stricter *startup-closure* bar
|
||||
|
||||
This co-attestation was **byte-clear on the v9 (env-iso + bytecode-pin) bar ONLY** and self-limited
|
||||
above to *"byte-clear NOT fire-authorization; FIRE remains gated on homelab 4th-round re-verify."*
|
||||
Homelab (the required third principal) returned **FAIL @1c34e3cb** (static, nothing executed), and Mos
|
||||
**UPHELD** it after independently confirming the finding in-source AND empirically on host Py3.11.2 —
|
||||
so the v9 byte-clear this document records is **SUPERSEDED** and does **NOT** authorize FIRE.
|
||||
|
||||
**Residual startup-closure hole (empirically reproduced by Mos; accepted as gate-STRENGTHENING):**
|
||||
neither child carries `-S`, so CPython imports the `site` module **before** the script runs. `-s`
|
||||
(launcher) suppresses only *user*-site; `-I` (broker) implies `-s -E -P` but **NOT** `-S`. Proven:
|
||||
|
||||
[-s -B ] no_site=0 site_imported=True ← v9 launcher: site runs
|
||||
[-I -B ] no_site=0 site_imported=True ← v9 broker: -I does NOT imply -S
|
||||
[-s -S -B] no_site=1 site_imported=False ← v10 launcher fix (sibling import STILL resolves)
|
||||
[-I -S -B] no_site=1 site_imported=False ← v10 broker fix (additive)
|
||||
|
||||
System-site executable `.pth` lines + sitecustomize/usercustomize can therefore run **unpinned startup
|
||||
code** before the exact launcher/broker and **outside** `closure_import_guard`, while every hash +
|
||||
`reject_pinned_bytecode` + import-guard still pass — defeating hashed==executed on the full *startup*
|
||||
closure (strictly wider than the module-import closure v9 cleared). A genuine fidelity hole for a
|
||||
fail-closed DO-178C evidence gate.
|
||||
|
||||
Mos **authorized the bounded v10 repair**: add `-S` to the **launcher** (keep `-s -B`, NOT `-I`) and
|
||||
to the **broker** (keep `-I -B`) — a minimal 2-line delta; no B6c regression (launcher `-s -S -B`
|
||||
sibling import empirically intact; `-S` does not touch `sys.path[0]`). Added review bar **B9**
|
||||
(no-site startup closure). This **rides the existing (ii)-full-closure authorization + R1 + Mos
|
||||
adjudication** (deepens startup-closure isolation of an already-authorized touch, inside the fixture
|
||||
temp root, no fresh Jason owner-window) and is **NOT a Case-C escalation** (static pre-fire catch, no
|
||||
probe fired). A separate **FIRE-time** constraint is captured: the 3× isolation dispatch must launch
|
||||
the runner under externally-enforced `python -I -S -B` (a self-reexec is too late).
|
||||
|
||||
**Live target = v10** (no-site harness, forthcoming). `1c34e3cb` / `e1c9a468` / this co-attestation
|
||||
(`f320d075`) are **SUPERSEDED**; the prior 2-of-3 (ms-secrev-828 v9 §3 PASS + `f320d075`) does NOT
|
||||
carry — all three distinct-identity principals re-verify the new v10 SHA. A fresh Mos co-attestation
|
||||
will be committed on v10 byte-verify PASS. WI-3 #830 remains HELD at `f4008307`; nothing banked;
|
||||
C-hatch armed (fired-rig only); NO FIRE.
|
||||
@@ -1,82 +0,0 @@
|
||||
# Gate0 Probe-3 (D4) NEW-5/NEW-6 Closure — §3-Conformance Review v6
|
||||
|
||||
**Verdict: ✅ PASS**
|
||||
|
||||
## Pin (G1 — reviewed object, mandatory)
|
||||
|
||||
- **Reviewed object = `2d54a9dd14cb924701b2ae4ed72dae4df760c4e3`** (harness commit, branch `feat/827-gate0-probe`, approach-**(i)** build; supersedes the reverted (ii) copy-to-fixture builds `7ff63cd5`/`6164dc07`, which were NOT reviewed to a verdict).
|
||||
- **Reviewed file:** `docs/compaction-refresh/probes/p3_d4_focused_run.py` — sha256 (pushed provider bytes, `-o FILE`, FULL-40 ref, verified before trust): `15a154df55273f51301763a984485fd63813f6d1f05d2728abb9fb8b9c040b1a` (27366 bytes, no not-found sentinel).
|
||||
- **Co-reviewed fixture broker:** `p3_generation_broker.py` sha256 `4db4fef1ac6658a8ca79ad5091cefc901d2aa26003265c3d6726c294cf895cad` — **byte-identical to v5** (unchanged by this delta).
|
||||
|
||||
## Principal-independence attestation (Mos independence ruling — process of record)
|
||||
|
||||
This review is produced by a **distinct Opus SECREV session, orchestrator-dispatched** — the **`ms-secrev-828`
|
||||
reviewer lane, dispatched by `mosaic-100`** — **byte review only, ran nothing**, and **did NOT build** this harness
|
||||
(builder = ms-rev-826). Author ≠ reviewer (Gate-16). This is one of three principals: **Mos commits his own
|
||||
distinct-identity byte-scope-verify co-attestation at v-final**, and **homelab's independent verify is the third
|
||||
principal**. (The `ms-lead-reviewer` **Git signer identity** shared across published review commits is a git-signer
|
||||
question routed to Mos; it does not bear on this lane's process/dispatch independence, attested here.)
|
||||
|
||||
## NEW-5 CLOSED — approach (i): exact-byte pin, adjacent re-hash, exec in place
|
||||
|
||||
- **Exact-byte sha256 is the trust anchor, NOT substring heuristics.** Launcher bytes are read from the immutable
|
||||
git object (`git show f4008307:<path>`) and gated on `sha256(launcher_bytes) == GATED_LAUNCHER_SHA256`
|
||||
(`e950e422…`) (:354). The `behavior_markers` `in`-checks (:364-372) are explicitly commented "the exact launcher
|
||||
digest above is the trust anchor. These marker checks are diagnostic belt-and-suspenders only, never a substitute
|
||||
for the pin" (:362-363). The old ordered `.find()` heuristic (`register < initialize < execute`, min<0) is **gone**.
|
||||
- **Final re-hash immediately adjacent to `Popen`, no interleaved yield.** `launch_verified_pi` assembles `command`,
|
||||
then — as the statement **immediately before** `return PiRpc(command, …)` (which performs the `Popen`) — re-hashes
|
||||
the launcher: `if hashlib.sha256(launcher.read_bytes()).hexdigest() != GATED_LAUNCHER_SHA256: raise` (:411-412),
|
||||
`return PiRpc(...)` (:413). **No harness-controlled step (no `wait_path`, no broker spawn) sits between the re-hash
|
||||
and the exec** — the broker `Popen` + `wait_path` occur *before* `launch_verified_pi` is called (:611-621). Window
|
||||
narrowed to the fork/exec itself.
|
||||
- **Exec stays IN PLACE at the pinned f4008307-worktree path.** The precondition returns the worktree paths
|
||||
`gated_root / launcher_relative`, `gated_root / generation_relative` (:378); Pi execs `str(launcher)` = that
|
||||
worktree launch-runtime.py (:390,:621), and the broker `--generation-module` = the worktree lease_generation.py
|
||||
(:614). The reverted (ii) machinery is **gone**: `grep pinned-lease-broker / PYTHONPATH / fixture_launcher /
|
||||
fixture_generation / write_bytes == 0`. Launcher import resolution and the file-backed fidelity surface are
|
||||
therefore **unperturbed** (this is the lower-risk approach Mos mandated over copy-to-fixture).
|
||||
|
||||
## NEW-6 CLOSED — portable, validated, off-by-one gone
|
||||
|
||||
`GATED_WI_ROOT` is no longer the off-by-one `HERE.parents[3].parent / "stack-cr-wi3-revoke"`. It is resolved by
|
||||
`resolve_gated_wi_root()` (:257-307): an explicit `GATED_WI_ROOT` env override, else **repo-relative** `git worktree
|
||||
list --porcelain` (from `repository_root()`, first parent containing `.git`) selecting the **unique** worktree whose
|
||||
`HEAD == f4008307` **and** `branch == refs/heads/feat/830-compaction-revoke` (raise if ambiguous/absent). It then
|
||||
**fail-closes** unless `gated_root.is_dir()`, `git rev-parse --is-inside-work-tree == "true"` (:304-305), and
|
||||
`HEAD == GATED_WI_HEAD` (:306-307). Independently recomputed on this host (git query, harness not run): it resolves
|
||||
to the real worktree **`/home/hermes/agent-work/stack-cr-wi3-revoke`**. Portable + validated; the off-by-one is gone.
|
||||
|
||||
## Full v4/v5 carry-over re-sweep (byte-stable vs `7f975b95` except the NEW-5/6 delta)
|
||||
|
||||
`diff 7f975b95 → 2d54a9dd` confines changes to launcher resolution (NEW-6) + adjacent-rehash-exec-in-place (NEW-5);
|
||||
nothing else moved. Re-swept intact: **creds-scrub** (`scrub_fixture_credentials` + outermost `finally`); **`source-invalid`
|
||||
ABSENT** (grep=0 both files); **fidelity file-backed** unperturbed (broker `read_runtime_generation`/`bump_runtime_generation`
|
||||
on the fixture `.state`; `assert_d4` `generation_source=="state-file"` + `state_file_drives_lifecycle` +
|
||||
`state_file_in_fixture_root` + `new→MUTATOR_UNVERIFIED`/`prior→STALE_GENERATION`); **`lease_anchor_registered`** INTACT
|
||||
(event + `hex-256`); **live-path** gated launcher; **fail-closed precondition**; **fixture-socket isolation**; **`-O`-safe**
|
||||
(0 bare `assert`, PASS derived); **allow-list env** (0 `os.environ.copy`); **`--runs choices=(3,)`**; **single p3 broker**
|
||||
(broker byte-identical to v5). **ABSENT sweep = 0** (P5/P6/P2-bank/retry-launder/mosaic-yolo/execRuntime/pi_gate0/run_open/
|
||||
atomic; extension actions only bump/lifecycle/authorize/promote; no exec-at-import). **Beyond-R1 tripwire: not tripped** —
|
||||
exec is in place, imports and the file-backed observation surface untouched; no isolation crossing.
|
||||
|
||||
## Verdict
|
||||
|
||||
**PASS @ `2d54a9dd`.** NEW-5 (approach (i): exact-byte sha256 pin as trust anchor; adjacent re-hash immediately
|
||||
before `Popen` with no interleaved yield; exec in place at the pinned f4008307-worktree path; (ii) copy-to-fixture/
|
||||
PYTHONPATH machinery reverted) and NEW-6 (portable, validated, off-by-one-gone root resolution) are both **closed**;
|
||||
the full v4/v5 carry-over holds byte-stable except the two intended surfaces; ABSENT sweep is 0; the R1 file-backed
|
||||
fidelity surface is unperturbed. Zero out-of-scope surface.
|
||||
|
||||
**Findings: none.**
|
||||
|
||||
## Scope reminder (not a finding)
|
||||
|
||||
Producing this evidence **executes** the Gate0 mechanism (§3/§5). This review clears **bytes** only; **FIRE remains
|
||||
separately gated on Mos's explicit post-clear GO** and is not authorized by this review.
|
||||
|
||||
---
|
||||
|
||||
**Reviewer:** distinct Opus SECREV session (`ms-secrev-828` lane, dispatched by `mosaic-100`), Gate-16 author≠reviewer,
|
||||
byte review only; ran nothing; did not build.
|
||||
**Reviewed object (pin):** `2d54a9dd14cb924701b2ae4ed72dae4df760c4e3` · harness sha256 `15a154df55273f51301763a984485fd63813f6d1f05d2728abb9fb8b9c040b1a`.
|
||||
@@ -1,86 +0,0 @@
|
||||
# Gate0 Probe-3 (D4) No-Site Startup Closure — §3-Conformance Review v10
|
||||
|
||||
**Verdict: ✅ PASS**
|
||||
|
||||
## Pin (GUARD 1 — reviewed object)
|
||||
|
||||
- **Reviewed object = `ce5ba762051354338889959bfce2b0381f4a4e2a`** (harness commit, branch `feat/827-gate0-probe`).
|
||||
- **Reviewed file:** `docs/compaction-refresh/probes/p3_d4_focused_run.py` — sha256 (pushed provider bytes, `-o FILE`, FULL-40 ref, verified before trust): `7e14ead89a7b2a297fcc17e7653291b3bcace1d2002a8f90a989db74f6985b6f` (32753 bytes, no not-found sentinel).
|
||||
- **Closure pins (unchanged):** launcher `e950e422…` @f4008307 · helper `061625402f08488eac47acd23272904e71fd1a71fd15b3bdab158632c801be4c` @f4008307 · broker `4db4fef1…` @23c0caca.
|
||||
|
||||
## Independence (GUARD 2 — principal-independence attestation)
|
||||
|
||||
Distinct Opus SECREV session, orchestrator-dispatched — the **`ms-secrev-828` reviewer lane, dispatched by
|
||||
`mosaic-100`** — **byte review only, ran nothing** (harness/broker not executed); did **not** build this harness
|
||||
(builder = ms-rev-826); is not Mos; distinct principal from both. This re-verifies from scratch on the v10 SHA after
|
||||
homelab's 4th-round FAIL @`1c34e3cb` (no `-S` → `site` startup-closure hole) superseded my v9 PASS + Mos's co-attest.
|
||||
The `-S`/`-s`/`-I` behavior checks below use a *throwaway* script to observe interpreter startup — not the harness.
|
||||
|
||||
## ★ B9 — No-site startup closure (the homelab 4th-round FAIL)
|
||||
|
||||
The delta vs `1c34e3cb` is **exactly two `-S` insertions**, byte-confirmed by `diff` (nothing else; +28 B fully
|
||||
accounted by the two ` "-S",\n` lines):
|
||||
|
||||
- **(i) Launcher command** (`launch_verified_pi`, :512-516): `sys.executable, "-s", "-S", "-B", str(launcher), …` —
|
||||
carries `-s` + **`-S`** + `-B`, and **no `-I`**.
|
||||
- **(ii) Broker command** (`launch_verified_broker`, :551-554): `sys.executable, "-I", "-S", "-B", str(broker_path), …`
|
||||
— carries `-I` + **`-S`** + `-B`.
|
||||
- **(iii) `site` not imported at child startup** — empirically confirmed (Python 3.11.2, throwaway script):
|
||||
`python3 -s -S -B main.py` → `sys.flags.no_site == 1`, `'site' in sys.modules == False`; `python3 -I -S -B main.py`
|
||||
→ `no_site == 1`. So system-site `.pth` executable lines and `sitecustomize`/`usercustomize` **cannot run unpinned
|
||||
startup code** before the pinned launcher/broker. (Contrast without `-S`: `python3 -s -B` → `no_site == 0`, `site`
|
||||
imported — the exact v9 hole this closes.)
|
||||
- **(iv)** No harness reliance on any site-injected path/hook (env is the constructed allow-list; children execute
|
||||
pinned bytes).
|
||||
- **(v) `-S` does not touch `sys.path[0]`** (unlike `-I`/`-P`) — empirically confirmed: `python3 -s -S -B main.py`
|
||||
keeps `sys.path[0]` = the script's directory, so the launcher's bare `from lease_generation import
|
||||
initialize_runtime_generation` (`f4008307:launch-runtime.py:15`) **still binds `pinned/lease_generation.py`** with
|
||||
`-S` present. The broker's explicit `--generation-module` import (via `importlib`) binds the pinned helper
|
||||
regardless of `sys.path`/site, so `-I -S` is correct there.
|
||||
- **(vi) Delta = exact 2-line `-S` only** vs `1c34e3cb` (git-diff/byte-compared, not accepted on assertion).
|
||||
|
||||
## All prior bars — byte-stable (delta was only the two `-S` lines)
|
||||
|
||||
- **B6(c):** launcher still carries no `-I`; sibling import binds `pinned/` (confirmed above with `-S` present). ✅
|
||||
- **B7:** broker `Popen` `env=environment` (allow-list, **not** `os.environ`; no `PYTHONPATH`/`PYTHONHOME`/
|
||||
`PYTHONPYCACHEPREFIX`) + `-I`. ✅
|
||||
- **B8:** `reject_pinned_bytecode` fail-closed before each consumer; `PYTHONDONTWRITEBYTECODE=1` + `PYTHONNOUSERSITE=1`
|
||||
in env; `-B` on both children. ✅
|
||||
- **B5:** 3-leg conjunction — materialize each of launcher/helper/broker from git-object bytes with `sha256==pin`
|
||||
fail-closed; `pinned/` `0o700` in fixture root, files `O_EXCL 0o600` (no writable window); re-hash `==pin`
|
||||
immediately before each `Popen`; launcher + broker consume the same single pinned helper. ✅
|
||||
- **B6:** `closure_import_guard` AST present; single pinned helper; broker `--generation-module = closure.generation`. ✅
|
||||
- **Fidelity:** extension bumps `generation-{sid}.state` via `MOSAIC_LEASE_GENERATION_FILE` (not in-mem); broker
|
||||
`read_runtime_generation`; `assert_d4` `generation_source=="state-file"` / `state_file_in_fixture_root` /
|
||||
`new→MUTATOR_UNVERIFIED` / `prior→STALE_GENERATION`; `.state` fixture-root-bound. ✅
|
||||
- **Traceability:** `GATED_WI_HEAD == f4008307` + `merge-base --is-ancestor 66b1e0a0 f4008307`. ✅
|
||||
- `lease_anchor_registered` + `hex-256` INTACT; LIVE-PATH (pinned gated launcher); single p3 broker;
|
||||
promotion=fixture-only; `--runs choices=(3,)`; `-O`-safe (0 bare `assert`); `copy2` = creds-only; allow-list env
|
||||
(0 `os.environ.copy`). ✅
|
||||
|
||||
## ABSENT sweep
|
||||
|
||||
P5/P6/P2-bank/retry-launder/mosaic-yolo/execRuntime = 0; `source-invalid` = 0; no live/real-broker path; no `.state`
|
||||
outside fixture root; no exec-at-import (`__main__`-guarded); no adjacency-only-exec-from-worktree; the only change is
|
||||
the authorized `-S` no-site isolation-deepening (no mechanism change, no scope-widen); `-O`-safe.
|
||||
|
||||
## Verdict
|
||||
|
||||
**PASS @ `ce5ba762`.** B9 (no-site startup closure) is closed — both children carry `-S`, `site` is not imported at
|
||||
startup (so system-site `.pth`/`sitecustomize` cannot execute unpinned code before the pinned launcher/broker),
|
||||
`-S` leaves `sys.path[0]` intact so the launcher sibling import and the broker explicit-path import both still bind
|
||||
the pinned helper, and the delta vs `1c34e3cb` is exactly the two `-S` insertions. All prior bars (B5/B6/B6c/B7/B8/
|
||||
fidelity/traceability/lease_anchor/live-path/single-broker/promotion/`--runs`/`-O`-safe) are byte-stable. Zero
|
||||
out-of-scope surface. **Findings: none.**
|
||||
|
||||
## Scope reminder (not a finding)
|
||||
|
||||
Producing this evidence **executes** the Gate0 mechanism (§3/§5). This review clears **bytes** only; **FIRE remains
|
||||
separately gated on Mos's explicit post-clear GO**, his byte-scope-verify co-attestation, and homelab's
|
||||
third-principal verify — not authorized by this review.
|
||||
|
||||
---
|
||||
|
||||
**Reviewer:** distinct Opus SECREV session (`ms-secrev-828` lane, dispatched by `mosaic-100`), Gate-16 author≠reviewer,
|
||||
byte review only; ran nothing.
|
||||
**Reviewed object (pin):** `ce5ba762051354338889959bfce2b0381f4a4e2a` · harness sha256 `7e14ead89a7b2a297fcc17e7653291b3bcace1d2002a8f90a989db74f6985b6f`.
|
||||
@@ -1,120 +0,0 @@
|
||||
> ⚠ SUPERSEDED by v8 (homelab 3rd-principal FAIL @f609a449: broker env-inherit + unpinned .pyc; PASS overtaken by stricter B7+B8 bar). This v7 PASS record is NOT fire-authorization.
|
||||
|
||||
# Gate0 Probe-3 (D4) Full-Closure Materialization — §3-Conformance Review v7
|
||||
|
||||
**Verdict: ✅ PASS** *(superseded — see banner above)*
|
||||
|
||||
## Pin (GUARD 1 — reviewed object)
|
||||
|
||||
- **Reviewed object = `f609a44953f5ae61916805fcb45ca337de00b0b0`** (harness commit, branch `feat/827-gate0-probe`).
|
||||
- **Reviewed file:** `docs/compaction-refresh/probes/p3_d4_focused_run.py` — sha256 (pushed provider bytes, `-o FILE`, FULL-40 ref, verified before trust): `0f1bd1b39399b32f243d901230e2d840794a2144edd723a095dab716833a7a9b` (32071 bytes, no not-found sentinel).
|
||||
- **Closure pins (independently recomputed from the git objects):**
|
||||
- launcher `packages/mosaic/framework/tools/lease-broker/launch-runtime.py` @ `f4008307` = `e950e4224e280f16979d90cabb89aa1896c5ee28bed2df957e14d018d43cda82` ✓
|
||||
- **helper** `packages/mosaic/framework/tools/lease-broker/lease_generation.py` @ `f4008307` = `061625402f08488eac47acd23272904e71fd1a71fd15b3bdab158632c801be4c` ✓
|
||||
- **broker** `docs/compaction-refresh/probes/p3_generation_broker.py` @ `23c0caca` = `4db4fef1ac6658a8ca79ad5091cefc901d2aa26003265c3d6726c294cf895cad` ✓
|
||||
|
||||
## Independence (GUARD 2 — principal-independence attestation)
|
||||
|
||||
This review is produced by a **distinct Opus SECREV session, orchestrator-dispatched — the `ms-secrev-828`
|
||||
reviewer lane, dispatched by `mosaic-100`** — **byte review only, ran nothing**, that **did NOT build** this harness
|
||||
(builder = ms-rev-826) and **is not Mos**. Three distinct principals: this reviewer, the builder, and Mos (whose
|
||||
own distinct-identity byte-scope-verify follows); homelab's independent verify is a further principal — it is
|
||||
homelab's third-principal FAIL @`2d54a9dd` (upheld by Mos) that correctly retired the approach-(i) adjacency
|
||||
re-hash and authorized this full-closure. v6/`2d54a9dd`/`23c0caca`/`12914d8` are superseded.
|
||||
|
||||
## Why v7 (the reopen-after-hash hole)
|
||||
|
||||
Approach (i) re-hashed the launcher then let `Popen` **reopen the worktree path** — hashed-snapshot ≠ executed-bytes
|
||||
(the worktree file is a shared, same-UID-mutable path). Statement adjacency alone did not bind. v7 closes it for the
|
||||
**full project-code closure** (launcher + `lease_generation.py` helper + `p3_generation_broker.py`).
|
||||
|
||||
## B5 — HASHED == EXECUTED on the full closure (binding conjunction, stated verbatim)
|
||||
|
||||
The reopen-after-hash shape is unavoidable for imported/exec'd files, so closure rests on the **conjunction of all
|
||||
three legs**, each byte-verified here:
|
||||
|
||||
> **(a)** bytes are materialized **from the pinned git-object @ `f4008307`** (helper/launcher) and **@ `23c0caca`**
|
||||
> (broker) — `git show <commit>:<path>`, the trusted immutable object, **never the mutable worktree file**; **AND**
|
||||
> **(b)** into a **fixture-private `0o700` dir with `0o600` files created via `O_CREAT|O_EXCL`** — no writer exists in
|
||||
> the threat model between hash and exec; **AND** **(c)** each member is **re-hashed == its pin IMMEDIATELY before
|
||||
> exec/import, fail-closed (`RuntimeError`)**.
|
||||
|
||||
Byte evidence:
|
||||
- **(a)** `git_object_bytes(git_root, commit, relative)` = `git show <commit>:<path>` (:322-327); `materialize_closure`
|
||||
reads all three members from git objects and asserts `sha256(data) == digest` else `RuntimeError` (:415-433). Worktree
|
||||
working-tree files are never read.
|
||||
- **(b)** `pinned = root / "pinned"; pinned.mkdir(mode=0o700)` (:435-436); `write_pinned_file` uses
|
||||
`os.open(path, O_WRONLY|O_CREAT|O_EXCL|O_CLOEXEC, 0o600)` (:379-382). **No `os.chmod`/`os.rename`/`shutil.move`
|
||||
anywhere** (grep=0); `O_EXCL` refuses a pre-planted file/symlink, so no symlink-follow or hijack gap; the dir is a
|
||||
fresh per-run `mkdtemp` child, owner-only. **No code re-opens the pinned files for write between materialize and
|
||||
consume** — there is no writable window.
|
||||
- **(c)** launcher re-hash `sha256(launcher.read_bytes()) == GATED_LAUNCHER_SHA256` is the statement immediately before
|
||||
`return PiRpc(command,…)` (:528-530); broker **and** helper re-hashes (`== GATED_BROKER_SHA256`,
|
||||
`== GATED_GENERATION_SHA256`) are the two statements immediately before `return subprocess.Popen(command,…)`
|
||||
(:546-551). No interleaved harness yield.
|
||||
|
||||
Adjacency-only exec-from-worktree is **absent** for every member (all three exec/import from `pinned/`; grep worktree-exec=0).
|
||||
|
||||
## B6 — helper + broker pinned and bound to execution (one shared helper)
|
||||
|
||||
`materialize_closure` writes exactly **one** `pinned/lease_generation.py` (:442). The broker executes the **pinned**
|
||||
broker with `--generation-module = closure.generation` = that pinned helper (`launch_verified_broker`, :533-551, called
|
||||
:746-747). The launcher executes the **pinned** launcher (`python3 pinned/launch-runtime.py`), whose
|
||||
`import lease_generation` resolves via `sys.path[0]` = the script's own `pinned/` dir to the **same** sibling
|
||||
`pinned/lease_generation.py`. Launcher-import and broker-`--generation-module` therefore resolve the **same single
|
||||
pinned helper copy**, not two copies and not the worktree. Worktree helper/broker are not re-read at runtime.
|
||||
|
||||
**Closure-import guard:** `closure_import_guard` AST-parses each member and refuses any non-stdlib import outside the
|
||||
allow-set `{"lease_generation"}` (and any relative import) → `RuntimeError` (:341-364). The 3-member closure is
|
||||
therefore provably complete — no unpinned project-code dependency can slip in.
|
||||
|
||||
## BAR1 — Traceability
|
||||
|
||||
`GATED_WI_HEAD == f4008307`; the precondition asserts `git merge-base --is-ancestor 66b1e0a0 f4008307` (:315-330),
|
||||
independently confirmed **YES** — the pinned launcher forward-contains the `66b1e0a0` file-backed generation mechanism.
|
||||
|
||||
## BAR2 — Fidelity file-backed, `.state` in fixture root, UNTOUCHED
|
||||
|
||||
`pinned/` holds **code bytes only** (launcher/helper/broker). The `.state` generation file is written by the launcher
|
||||
to `socket_path.parent` (the fixture root), **not** `pinned/`. The broker (pinned, byte-identical `4db4fef1`) still
|
||||
enforces `generation_environment` raising if `state_path.parent != socket_path.parent` (grep=2), and `assert_d4`
|
||||
still checks `state_file_source == "state-file"` / `state_file_drives_lifecycle` / `state_file_in_fixture_root` +
|
||||
`new→MUTATOR_UNVERIFIED` / `prior→STALE_GENERATION` (grep=4, unchanged). The v7 change did not move `.state` into
|
||||
`pinned/` or perturb these asserts.
|
||||
|
||||
## BAR3 — Carry-over
|
||||
|
||||
(a) **live-path:** Pi launched via `python3 pinned/launch-runtime.py --runtime pi -- pi …` (gated register-before-exec);
|
||||
`mosaic yolo`/`execRuntime` = 0. (b) **fail-closed precondition:** `gated_launcher_precondition` (resolve+materialize+
|
||||
verify) runs before any launch, fail-closed. (c) **fixture-socket isolation:** `MOSAIC_LEASE_BROKER_SOCKET` = per-run
|
||||
fixture socket; single pinned p3 broker serves `register_anchor`; no live/default broker reachable; non-destructive.
|
||||
(d) **`lease_anchor_registered` INTACT:** event + `session_id_shape=="hex-256"` unchanged (broker byte-identical);
|
||||
`assert_d4` folds it into the single-identity set — not deleted/softened/optional/repointed.
|
||||
|
||||
## Re-confirm + ABSENT sweep
|
||||
|
||||
Spawns ONLY the single pinned p3 broker; promotion=fixture-only; full D4 asserts; `--runs choices=(3,)`; allow-list
|
||||
env (0 `os.environ.copy`); **`-O`-safe** (all new checks `RuntimeError`, **0 bare `assert`**); creds-scrub intact;
|
||||
non-destructive (fixture tempdir only); deterministic (git objects + fixed pins); closure-import guard present.
|
||||
**ABSENT = 0:** P5/P6/P2-bank/retry-launder/mosaic-yolo/execRuntime/pi_gate0; `source-invalid` grep=0; no live/real-broker
|
||||
path; no `.state`/gen path outside the fixture root; no extra broker/socket; no exec-at-import (`__main__`-guarded); no
|
||||
adjacency-only exec-from-worktree for any member; the only mechanism change is materialization; no scope-widen.
|
||||
|
||||
## Verdict
|
||||
|
||||
**PASS @ `f609a449`.** B5 (full-closure hashed==executed via the (a)+(b)+(c) conjunction with no writable window),
|
||||
B6 (one shared pinned helper bound to both launcher-import and broker-`--generation-module`; complete closure), BAR1,
|
||||
BAR2 (fidelity `.state`-in-fixture-root untouched), and BAR3 all hold, with zero out-of-scope surface. **Findings: none.**
|
||||
|
||||
## Scope reminder (not a finding)
|
||||
|
||||
Producing this evidence **executes** the Gate0 mechanism (§3/§5). This review clears **bytes** only; **FIRE remains
|
||||
separately gated on Mos's explicit post-clear GO**, his v-final byte-scope-verify co-attestation, and homelab's
|
||||
third-principal verify — not authorized by this review.
|
||||
|
||||
---
|
||||
|
||||
**Reviewer:** distinct Opus SECREV session (`ms-secrev-828` lane, dispatched by `mosaic-100`), Gate-16 author≠reviewer,
|
||||
byte review only; ran nothing; did not build.
|
||||
**Reviewed object (pin):** `f609a44953f5ae61916805fcb45ca337de00b0b0` · harness sha256 `0f1bd1b39399b32f243d901230e2d840794a2144edd723a095dab716833a7a9b`.
|
||||
**Pinned closure:** launcher `e950e422…` @f4008307 · helper `06162540…be4c` @f4008307 · broker `4db4fef1…` @23c0caca.
|
||||
@@ -1,92 +0,0 @@
|
||||
> ⚠ SUPERSEDED by v9: the reviewed harness `a92ad090` is superseded by the narrow fix `1c34e3cb` (my v8 B6(c) FAIL — `-I` on the launcher — was remediated by `-I`→`-s` + `PYTHONNOUSERSITE=1`; re-review v9 = PASS). This v8 FAIL record pertains to the superseded commit.
|
||||
|
||||
# Gate0 Probe-3 (D4) Broker Env-Isolation + Bytecode Binding — §3-Conformance Review v8
|
||||
|
||||
**Verdict: ❌ FAIL** (B7 and B8 land correctly, but the same change breaks **B6(c)**: the launcher is run with `-I`, which strips the script directory from `sys.path` on Python 3.11+, so its bare `import lease_generation` cannot resolve the pinned helper — empirically confirmed).
|
||||
|
||||
## Pin (GUARD 1 — reviewed object)
|
||||
|
||||
- **Reviewed object = `a92ad090ae3828c643f961c7628d809b8521185f`** (harness commit, branch `feat/827-gate0-probe`).
|
||||
- **Reviewed file:** `docs/compaction-refresh/probes/p3_d4_focused_run.py` — sha256 (pushed provider bytes, `-o FILE`, FULL-40 ref, verified before trust): `915ebeb5aeab108cb60c5f629c1db520623ab4914eed427ca34ee66f9aa08390` (32614 bytes, no not-found sentinel).
|
||||
- **Closure pins (unchanged from v7):** launcher `e950e422…` @f4008307 · helper `061625402f08488eac47acd23272904e71fd1a71fd15b3bdab158632c801be4c` @f4008307 · broker `4db4fef1…` @23c0caca.
|
||||
|
||||
## Independence (GUARD 2 — principal-independence attestation)
|
||||
|
||||
Distinct Opus SECREV session, orchestrator-dispatched — the **`ms-secrev-828` reviewer lane, dispatched by
|
||||
`mosaic-100`** — **byte review only, ran nothing** (the harness/broker were not executed); did **not** build this
|
||||
harness (builder = ms-rev-826); is not Mos. This verdict is my own. (The `-I` semantics check below runs a *throwaway*
|
||||
two-line script to observe the interpreter's `sys.path` behavior — it does not run the harness, broker, or any part of
|
||||
the reviewed closure.)
|
||||
|
||||
## 🔴 BLOCKING FINDING — B6(c) broken: `-I` on the launcher strips the pinned-helper import path
|
||||
|
||||
**File:line — `p3_d4_focused_run.py:512`** (the `"-I"` added to `launch_verified_pi`'s launcher command).
|
||||
|
||||
The pinned launcher `launch-runtime.py` @`f4008307` imports its helper with a **bare top-level import**:
|
||||
`from lease_generation import initialize_runtime_generation` (launcher line 15) — no `sys.path` manipulation. Under
|
||||
v7 this bound because `python3 pinned/launch-runtime.py` put the script's directory (`pinned/`) at `sys.path[0]`, so
|
||||
the sibling `lease_generation` resolved to `pinned/lease_generation.py`.
|
||||
|
||||
v8 now runs the launcher as `python3 -I -B pinned/launch-runtime.py …` (:512-513). **`-I` implies `-P` (Python 3.11+),
|
||||
which does NOT prepend the script's directory to `sys.path`.** Empirically confirmed on this host (Python 3.11.2),
|
||||
using a throwaway script (not the harness):
|
||||
|
||||
```
|
||||
python3 -I -B main.py → sys.path[0] = '/usr/lib/python311.zip'
|
||||
import sibling → ModuleNotFoundError: No module named '…'
|
||||
python3 -B main.py → sys.path[0] = '<script dir>' → sibling import: OK
|
||||
```
|
||||
|
||||
Therefore, at FIRE on Python 3.11+, the launcher's line-15 `from lease_generation import …` raises
|
||||
`ModuleNotFoundError` at module load — the pinned helper does **not** resolve (neither pinned nor worktree; the import
|
||||
simply fails). **B6(c) — "launcher sibling-import to `pinned/` via `sys.path[0]` STILL BINDS" — does not hold.** The
|
||||
build report's assertion "`-I` keeps script dir" is false on 3.11+, and could not have been observed under the
|
||||
correct "never run" boundary.
|
||||
|
||||
Note: the env allow-list carries no `PYTHONPATH` (correct for B7), and `-I` ignores `PYTHON*` env regardless, so there
|
||||
is no alternate resolution path — the launcher import is unrecoverable under `-I`.
|
||||
|
||||
**Fix:** remove `-I` from the **launcher** command only (keep `-B` + the `env=` allow-list — the launcher's
|
||||
env-isolation is already provided by the constructed allow-list, which contains no `PYTHONPATH`/`PYTHONHOME`/
|
||||
`PYTHONPYCACHEPREFIX`, and it needs `pinned/` at `sys.path[0]` for the sibling import). Keep `-I` on the **broker**
|
||||
command (it loads the helper by explicit `--generation-module` path via `importlib`, so it never needs the script
|
||||
dir on `sys.path`). Alternatively, inject the pinned dir explicitly (e.g. `PYTHONPATH=pinned/` — but that reintroduces
|
||||
a `PYTHON*` passthrough B7 forbids, so dropping `-I` on the launcher is the clean fix).
|
||||
|
||||
## What DID land correctly (for the author's fast turnaround)
|
||||
|
||||
- **B7 — broker child env-isolated: correct.** `launch_verified_broker` now takes `environment` and passes
|
||||
`env=environment` (the constructed allow-list, **not** `os.environ`) to `Popen` (:566-568); the broker command
|
||||
includes `-I` (:551); the allow-list contains no `PYTHONPATH`/`PYTHONHOME`/`PYTHONPYCACHEPREFIX` passthrough. The
|
||||
broker child cannot inherit ambient env or resolve stdlib imports to ambient code. ✅
|
||||
- **B8 — bytecode pinned-or-suppressed: correct.** `PYTHONDONTWRITEBYTECODE=1` is in the allow-list env (:728) and
|
||||
`-B` is on **both** child commands (:512-513 launcher, :551-552 broker); `reject_pinned_bytecode` fails closed
|
||||
(`RuntimeError`) on any pre-existing `pinned/__pycache__` or `*.pyc` (:498-501) and is called **before each
|
||||
consumer** (:534 launcher, :561 broker). No unpinned `.pyc` can be executed. ✅
|
||||
- **B5 conjunction / B6 single-helper / closure-import-guard / BAR1 / BAR2 (`.state` fidelity untouched) / BAR3
|
||||
(live-path, fail-closed precondition, fixture-socket isolation, `lease_anchor_registered` + hex-256) / single p3
|
||||
broker / `-O`-safe / allow-list env / `--runs==(3,)` / ABSENT sweep:** all intact/unperturbed (the delta touches only
|
||||
the env/`-I`/`-B`/bytecode-reject surfaces). These are **not** the failing item.
|
||||
|
||||
## Verdict
|
||||
|
||||
**FAIL @ `a92ad090`.** B7 (broker env isolation) and B8 (bytecode pinned-or-suppressed) are correctly implemented,
|
||||
but the `-I` added to the **launcher** command breaks B6(c): the launcher's bare `from lease_generation import` at
|
||||
`f4008307:launch-runtime.py:15` cannot resolve the pinned helper because `-I`/`-P` strips `sys.path[0]` on Python
|
||||
3.11+ (empirically confirmed, 3.11.2 → `ModuleNotFoundError`). PASS requires **all** of B7+B8+B5+B6+BAR1/2/3; B6(c)
|
||||
does not hold. Not softened → returns to author (ms-rev-826). The fix is narrow: drop `-I` from the launcher command
|
||||
(retain `-B` + allow-list env), keep `-I` on the broker.
|
||||
|
||||
**Findings:** B6(c) — `p3_d4_focused_run.py:512` (`-I` on the launcher command; breaks the pinned-helper sibling
|
||||
import under Python 3.11+).
|
||||
|
||||
## Scope reminder (not a finding)
|
||||
|
||||
Producing this evidence **executes** the Gate0 mechanism (§3/§5). This review clears **bytes** only; FIRE remains
|
||||
separately gated on Mos's post-clear GO — moot until this FAIL is remediated.
|
||||
|
||||
---
|
||||
|
||||
**Reviewer:** distinct Opus SECREV session (`ms-secrev-828` lane, dispatched by `mosaic-100`), Gate-16 author≠reviewer,
|
||||
byte review only; ran nothing (harness/broker not executed).
|
||||
**Reviewed object (pin):** `a92ad090ae3828c643f961c7628d809b8521185f` · harness sha256 `915ebeb5aeab108cb60c5f629c1db520623ab4914eed427ca34ee66f9aa08390`.
|
||||
@@ -1,95 +0,0 @@
|
||||
> ⚠ SUPERSEDED: homelab 4th-round FAIL @`1c34e3cb` — no `-S` → Python imports `site` at startup, running unpinned system-site `.pth` executable lines + `sitecustomize`/`usercustomize` before the pinned launcher/broker (site startup-closure hole). This v9 PASS record is overtaken by the stricter B9 (no-site) bar and is NOT fire-authorization; superseded by v10.
|
||||
|
||||
# Gate0 Probe-3 (D4) Launcher-Import Fix — §3-Conformance Review v9
|
||||
|
||||
**Verdict: ✅ PASS** *(superseded — see banner above)*
|
||||
|
||||
## Pin (GUARD 1 — reviewed object)
|
||||
|
||||
- **Reviewed object = `1c34e3cb3172acdcd094e683e847d7c984afc96c`** (harness commit, branch `feat/827-gate0-probe`).
|
||||
- **Reviewed file:** `docs/compaction-refresh/probes/p3_d4_focused_run.py` — sha256 (pushed provider bytes, `-o FILE`, FULL-40 ref, verified before trust): `29e5c7bfbe1911b52984bd94c79036bb1200ee82588318367b13c2b1053a0103` (32725 bytes, no not-found sentinel).
|
||||
- **Closure pins (unchanged):** launcher `e950e422…` @f4008307 · helper `061625402f08488eac47acd23272904e71fd1a71fd15b3bdab158632c801be4c` @f4008307 · broker `4db4fef1…` @23c0caca.
|
||||
|
||||
## Independence (GUARD 2 — principal-independence attestation)
|
||||
|
||||
Distinct Opus SECREV session, orchestrator-dispatched — the **`ms-secrev-828` reviewer lane, dispatched by
|
||||
`mosaic-100`** — **byte review only, ran nothing** (harness/broker not executed); did **not** build this harness
|
||||
(builder = ms-rev-826); is not Mos. This is the re-review after **my own** v8 FAIL @`a92ad090` (B6(c): `-I` on the
|
||||
launcher broke the sibling import); the author applied the narrow fix and I verify it here. The `-s`/`-I` `sys.path`
|
||||
checks below use a *throwaway* two-line script to observe interpreter behavior — not the harness/broker/closure.
|
||||
|
||||
## ★ B6(c) — THE FIX (was the v8 FAIL): launcher `-I` dropped; sibling import binds to `pinned/`
|
||||
|
||||
The launcher command no longer carries `-I`; it now uses **`-s`** (`:514`, commented "`-s` preserves `sys.path[0]=pinned/`
|
||||
for the launcher's sibling helper") + `-B` (`:515`), and `PYTHONNOUSERSITE=1` is added to the allow-list env (`:730`).
|
||||
|
||||
`-s` and `PYTHONNOUSERSITE` disable **user site-packages only** — they do **not** strip the script's directory from
|
||||
`sys.path` (unlike `-I`/`-P`). Empirically confirmed on this host (Python 3.11.2), throwaway script:
|
||||
|
||||
```
|
||||
python3 -s -B main.py → sys.path[0] = '<script dir>' → sibling import: OK
|
||||
PYTHONNOUSERSITE=1 python3 -s -B main.py → sys.path[0] = '<script dir>' → sibling import: OK
|
||||
python3 -I -B main.py (the v8 FAIL form) → sys.path[0] = stdlib zip → ModuleNotFoundError
|
||||
```
|
||||
|
||||
Therefore `python3 -s -B pinned/launch-runtime.py …` puts `pinned/` at `sys.path[0]`, so the pinned launcher's bare
|
||||
top-level `from lease_generation import initialize_runtime_generation` (`f4008307:launch-runtime.py:15`, no `sys.path`
|
||||
manipulation) resolves to the **pinned** `pinned/lease_generation.py` — not the worktree, not a miss. **B6(c) holds.**
|
||||
|
||||
## B7 — Broker env-isolation (still holds)
|
||||
|
||||
`launch_verified_broker` passes `env=environment` (the constructed allow-list, **not** `os.environ`; contains no
|
||||
`PYTHONPATH`/`PYTHONHOME`/`PYTHONPYCACHEPREFIX`) to `Popen` (`:570`), and the broker command includes `-I` (`:552`).
|
||||
The broker imports the helper by explicit `--generation-module` path via `importlib`, so it never needs `sys.path[0]`
|
||||
— `-I` is correct there and does not affect it. (The env's `PYTHONNOUSERSITE`/`PYTHONDONTWRITEBYTECODE` are hardening
|
||||
flags, not path/home passthrough, and `-I` ignores all `PYTHON*` env anyway.)
|
||||
|
||||
## B8 — Bytecode pinned-or-suppressed (still holds)
|
||||
|
||||
`PYTHONDONTWRITEBYTECODE=1` (`:729`) and `PYTHONNOUSERSITE=1` (`:730`) in the allow-list env; `-B` on **both** child
|
||||
commands (`:515` launcher, `:553` broker); `reject_pinned_bytecode` fails closed (`RuntimeError`) on any pre-existing
|
||||
`pinned/__pycache__` or `*.pyc` (`:498-501`) and is called **before each consumer** (`:535` launcher, `:562` broker).
|
||||
No unpinned `.pyc` can be executed.
|
||||
|
||||
## B5 — 3-leg conjunction (still holds)
|
||||
|
||||
`materialize_closure` reads launcher+helper+broker from **git-object bytes** (`git show <commit>:<path>`) and asserts
|
||||
`sha256 == pin` for each, fail-closed; `pinned/` is a fixture-private `0o700` dir inside the per-run fixture temp root;
|
||||
files created `O_WRONLY|O_CREAT|O_EXCL|O_CLOEXEC 0o600` (no chmod/rename/symlink gap → no writable window); each member
|
||||
re-hashed `== pin` immediately before its `Popen` (launcher; broker + helper). Launcher and broker consume the **same
|
||||
single** pinned helper. `closure_import_guard` AST-rejects any unpinned non-stdlib import.
|
||||
|
||||
## Fidelity + traceability + carry-over (still hold)
|
||||
|
||||
`GATED_WI_HEAD == f4008307` + `merge-base --is-ancestor 66b1e0a0 f4008307` (forward-contains). Extension bumps
|
||||
`generation-{sid}.state` via `MOSAIC_LEASE_GENERATION_FILE` (not in-mem); broker reads via `read_runtime_generation`;
|
||||
`assert_d4` observes the file-backed transition (`generation_source=="state-file"`, `state_file_drives_lifecycle`,
|
||||
`state_file_in_fixture_root`, new→`MUTATOR_UNVERIFIED`, prior→`STALE_GENERATION`); `.state` stays in the fixture temp
|
||||
root. `lease_anchor_registered` INTACT (event + `session_id_shape=="hex-256"`). LIVE-PATH drives the pinned gated
|
||||
launcher (no released `mosaic`/`execRuntime`). Fail-closed precondition before any launch. Single pinned p3 broker.
|
||||
Promotion=fixture-only. `--runs choices=(3,)`. `copy2` = creds-only. Allow-list env (0 `os.environ.copy`).
|
||||
|
||||
## ABSENT sweep
|
||||
|
||||
P5/P6/P2-bank/retry-launder/mosaic-yolo/execRuntime = 0; `source-invalid` = 0; no live/real-broker path; no `.state`
|
||||
outside the fixture root; no exec-at-import (`__main__`-guarded); no adjacency-only-exec-from-worktree; the only change
|
||||
is the authorized launcher-flag isolation fix (no mechanism change, no scope-widen); **`-O`-safe** (0 bare `assert`).
|
||||
|
||||
## Verdict
|
||||
|
||||
**PASS @ `1c34e3cb`.** The v8 FAIL is remediated by the narrow fix (launcher `-I` → `-s` + `PYTHONNOUSERSITE=1`),
|
||||
empirically verified to preserve `sys.path[0]=pinned/` so the pinned launcher's sibling import binds to the pinned
|
||||
helper; the broker retains `-I` (explicit-path import). B7, B8, B5, B6-rest, fidelity, traceability, and all carry-over
|
||||
bars remain intact; zero out-of-scope surface. **Findings: none.**
|
||||
|
||||
## Scope reminder (not a finding)
|
||||
|
||||
Producing this evidence **executes** the Gate0 mechanism (§3/§5). This review clears **bytes** only; **FIRE remains
|
||||
separately gated on Mos's explicit post-clear GO**, his byte-scope-verify co-attestation, and homelab's
|
||||
third-principal verify — not authorized by this review.
|
||||
|
||||
---
|
||||
|
||||
**Reviewer:** distinct Opus SECREV session (`ms-secrev-828` lane, dispatched by `mosaic-100`), Gate-16 author≠reviewer,
|
||||
byte review only; ran nothing.
|
||||
**Reviewed object (pin):** `1c34e3cb3172acdcd094e683e847d7c984afc96c` · harness sha256 `29e5c7bfbe1911b52984bd94c79036bb1200ee82588318367b13c2b1053a0103`.
|
||||
36
docs/guides/lease-broker-operations.md
Normal file
36
docs/guides/lease-broker-operations.md
Normal file
@@ -0,0 +1,36 @@
|
||||
# Lease broker operations
|
||||
|
||||
Place the socket and state file in a dedicated directory with mode `0700`. Start the packaged daemon with:
|
||||
|
||||
```bash
|
||||
python3 "$MOSAIC_HOME/tools/lease-broker/daemon.py" \
|
||||
--socket /run/user/1000/mosaic-lease/broker.sock \
|
||||
--state /run/user/1000/mosaic-lease/state.json
|
||||
```
|
||||
|
||||
The broker refuses an existing parent directory whose mode is not exactly `0700`, an existing state file not at `0600`, corrupt/incompatible state, or an already-existing socket path. After bind it sets the socket to `0600`. It never silently unlinks a pre-existing socket. On normal termination it unlinks only the socket inode it created, so it does not remove a replacement path.
|
||||
|
||||
Before launching Claude, Claudex, or Pi, export the socket path; `mosaic` then runs the runtime through the packaged register-and-exec wrapper:
|
||||
|
||||
```bash
|
||||
export MOSAIC_LEASE_BROKER_SOCKET=/run/user/1000/mosaic-lease/broker.sock
|
||||
mosaic claude # or: mosaic claudex, mosaic yolo claudex, mosaic pi
|
||||
```
|
||||
|
||||
The wrapper obtains a broker-minted session ID and `exec`s the runtime without changing its PID/starttime anchor. The all-tools Claude `PreToolUse` hook and Pi `tool_call` handler inherit that identity. Claudex retains its isolated proxy environment and config directory; Mosaic merges the mandatory all-tools hook into that isolated `settings.json` before invoking the same wrapper. PRDY init/update, QA remediation, coord, orchestrator, and fleet launchers also converge on this boundary. Broker registration failure, unsafe isolated settings, or missing identity denies launch/tool execution fail-closed; broker timeout/unavailability and malformed replies also block tools.
|
||||
|
||||
Run the permanent launch inventory locally with:
|
||||
|
||||
```bash
|
||||
python3 packages/mosaic/framework/tools/lease-broker/check-runtime-launches.py --root .
|
||||
```
|
||||
|
||||
The same check runs in the Mosaic package test suite and therefore in root CI. Any direct Claude/Pi binary launch must be replaced with `launch-runtime.py`, `execLeaseGatedRuntime`, or the gated `mosaic` runtime command; do not add static allowlist exceptions.
|
||||
|
||||
Clients must complete the request boundary before waiting for a reply. After sending the single JSON object and its terminating newline, the client **MUST half-close the socket's write side** (`shutdown(SHUT_WR)` in POSIX clients; `socket.end()` in Node) and only then await the response. Merely calling `write()` and waiting is invalid: the broker waits for EOF to enforce the exact-one-frame contract and fails closed at its one-second deadline. Do not replace `end()` with `write()` in client helpers. A delayed second frame remains malformed and is rejected.
|
||||
|
||||
There is no automated recovery workflow yet. `mosaic_context_recover` is reserved as the only unverified mutator class, but its fixed payload/receipt implementation lands in a later WI. After a crash, preserve the protected state file and restart only after verifying that no broker owns the socket. Restart intentionally clears all volatile VERIFIED leases. A leftover socket requires an operator to verify the owning service is stopped and remove that exact socket deliberately. Corrupt, oversized, symlinked, or non-regular state fails closed; do not overwrite it. Preserve it for incident review and establish new state only through an explicit operational decision, which invalidates prior sessions and tokens.
|
||||
|
||||
## Security posture
|
||||
|
||||
Directory `0700` plus socket/state `0600` is built-in same-principal hardening only: it excludes other UIDs but does **not** stop the same UID from unlinking and counterfeiting the socket. It therefore does not close T-C same-UID replacement. WI-1 does not provide a distinct-principal boundary. A stronger distinct-principal deployment requires an external protected proxy, ACL, or service boundary that clients cannot unlink or rebind and that preserves the authenticated client identity required by the broker's `SO_PEERCRED` and ancestry checks. Server-side branch protection remains the irreducible backstop.
|
||||
86
docs/scratchpads/828-lease-broker.md
Normal file
86
docs/scratchpads/828-lease-broker.md
Normal file
@@ -0,0 +1,86 @@
|
||||
# WI-1 Scratchpad — Authenticated external lease broker
|
||||
|
||||
- **Issue:** Gitea #828
|
||||
- **Milestone:** 188 — Compaction-Refresh Mechanism (M1: Claude + Pi)
|
||||
- **Branch:** `feat/828-lease-broker`
|
||||
- **Starting HEAD:** `d801d6c4c8a984d6a95033c49714210018d3d9a8`
|
||||
- **Session role:** Orchestrator coordinating implementation; Mos retains merge authority.
|
||||
|
||||
## Objective
|
||||
|
||||
Implement the ratified WI-1 product lease broker under `packages/mosaic/`: Linux `SO_PEERCRED` identity, broker-minted logical session IDs, `(pid,starttime)` launcher anchors with per-hop `/proc` starttime revalidation, sibling-substitution rejection, same-PID runtime-generation revocation, crypto-RNG single-use token persistence, and protected Unix-socket posture.
|
||||
|
||||
## Authority verification
|
||||
|
||||
Verified before code on session start; all exact SHA-256 values matched:
|
||||
|
||||
- BUILD-BRIEF: `89fdbc27ed0e5050dc7b52f3ef2ddaea691edf17fd89d51b15e26fb5ed47171b`
|
||||
- SPEC-v5: `a6d07ade835758e8488ca10d3b0631caf0beb93ea3a6733631f151b0c2f01433`
|
||||
- Ratification: `bac58319c9c4028b5b40e1129e0033cdb5a6b7b02033c25f06f4cb77d7779c67`
|
||||
- P6 planner ruling: `b7bbb6ea6e8d9a5c3366993642ab4e4f65b961af04936dcac20bfbcdcbaf1a09`
|
||||
- WI-0 Gate0 evidence: `5d418306fcc597fd514e500bee40d1509f0bf467e46ee13fc5c280ed8274759d`
|
||||
|
||||
## Locked constraints
|
||||
|
||||
- Build against the ratified design; do not re-derive it.
|
||||
- Product code only in `packages/mosaic`; Gate0 Python probes are reference prototypes and are not shipped.
|
||||
- Caller-supplied/asserted `session_id` is refused.
|
||||
- Tokens use the operating-system CSPRNG via Python `secrets`; never `Math.random` or model output.
|
||||
- Socket parent directory mode `0700`, socket mode `0600` minimum; document distinct-principal deployment as the stronger T-C-closing posture.
|
||||
- Red-first TDD for six named cases; new-code coverage >=85%.
|
||||
- No merge. PR must say `closes #828`; exact 40-character head handed to Mos for Opus-SECREV and independent review.
|
||||
|
||||
## Plan
|
||||
|
||||
1. Load security/testing/docs guidance and inspect existing `packages/mosaic` architecture.
|
||||
2. Write the six required tests first and capture RED evidence.
|
||||
3. Implement minimal broker modules and CLI/runtime integration necessary for product use.
|
||||
4. Run focused tests with coverage, package gates, then full repository gates/suite.
|
||||
5. Run author-side review/remediation, commit `closes #828`, queue guard, push, and open PR through Mosaic wrappers.
|
||||
6. Send PR number + exact head SHA to `web1:mosaic-100`; stop without merging.
|
||||
|
||||
## Risks / boundaries
|
||||
|
||||
- Same-UID counterfeit socket replacement remains the disclosed T-C residual unless broker runs under a distinct principal; filesystem modes alone are minimum hardening, not a complete authenticity proof.
|
||||
- `.mosaic/orchestrator/mission.json` and `.mosaic/orchestrator/session.lock` were already modified at session start and must not be included in this PR.
|
||||
- Repository Woodpecker pipelines exist; CI is the canonical build path. No manual image build/deploy is in scope.
|
||||
|
||||
## Progress / evidence
|
||||
|
||||
- 2026-07-18 session start: mandatory mission files and orchestration guides loaded.
|
||||
- STEP 0: all four authority hashes matched; artifacts read in full.
|
||||
- Branch/HEAD confirmed; issue #828 open; Gate0 evidence hash confirmed.
|
||||
- Initial RED: focused Vitest acceptance suite failed 11/11 because the product daemon did not exist; the expected missing-product failure was observed before implementation.
|
||||
- Review-remediation RED: partial/zero-progress state writes, nested corrupt state, symlink state, canonical starttime, and duplicate-anchor generation behavior failed before their fixes. Real socket RED/GREEN runs were executed by the unrestricted parent harness because the delegated worker sandbox denies `AF_UNIX.bind()`.
|
||||
- Product implementation added at `packages/mosaic/framework/tools/lease-broker/daemon.py`; Gate0 probe scripts were read as references but not copied or shipped.
|
||||
- Independent Codex code review round 1 found 2 blockers + 1 should-fix (connection stall/crash, partial writes, packet-dependent framing); all were remediated with tests.
|
||||
- Independent Codex code review round 2 found 2 blockers + 1 relevant should-fix (half-close contract ambiguity, incomplete persisted-state validation, symlink/non-regular state); all were remediated with tests and documentation. Pre-existing `.mosaic/*` session dirt remains excluded from the PR.
|
||||
- Unrestricted focused situational suite: `35/35` GREEN.
|
||||
- New Python product module coverage: `90%` (`356` statements, `36` missed), above the user-required 85%.
|
||||
- Root typecheck: `42/42` Turbo tasks GREEN.
|
||||
- Root lint: `23/23` Turbo tasks GREEN.
|
||||
- Root format check: GREEN.
|
||||
- Package build + suite: `71/71` files and `1,369/1,369` tests GREEN, including framework shell tests.
|
||||
- Full root suite: `43/43` Turbo tasks GREEN after the oversized-frame production race fix.
|
||||
- Focused acceptance suite: `35/35` GREEN in three consecutive unrestricted runs; exact-head instrumented run also `35/35` GREEN.
|
||||
- Exact-head Python product coverage: `90%` (`365` statements, `37` missed), above the required 85%.
|
||||
- Review-triggered oversized-frame race was fixed in production by bounded drain-to-EOF; tests were not changed.
|
||||
- Commits banked in red/green cadence: `d61c5441` (RED contract), `deb11df7` (GREEN implementation/docs), `57770e34` (oversized-frame production fix).
|
||||
- Final-review blocker remediated: added a 256-token pending-state cap, deletion on consume/generation revocation, pre-open serialized-size enforcement, and request-wide in-memory rollback for every broker mutation/commit failure while retaining the v1 live-token schema.
|
||||
- Distinct-principal docs now state built-in `0700`/`0600` is same-principal only; WI-1 does not provide the external identity-preserving proxy/ACL/service boundary needed for the stronger deployment.
|
||||
- Exact Python unit suite: `8/8` GREEN. Unrestricted focused acceptance: `35/35` GREEN.
|
||||
- Exact-head package build/suite: `71/71` files and `1,369/1,369` tests GREEN.
|
||||
- Exact-head Python product coverage: `90%` (`376` statements, `36` missed), above required 85%.
|
||||
- Root typecheck: `42/42` GREEN. Root lint: `23/23` GREEN. Root format check and `git diff --check`: GREEN.
|
||||
- Final exact-head rereview found two persistence blockers: post-rename directory-fsync uncertainty and acceptance of impossible persisted token records. RED was captured as three invariant failures plus one missing fail-stop error; commits `a94b1220` (RED) and `d05465e5` (GREEN) remediate both without weakening tests.
|
||||
- Post-remediation evidence: Python unit suite `10/10`, focused real-socket acceptance `35/35`, full root suite `43/43` Turbo tasks, broker coverage `90%` (`395` statements, `38` missed), lint `23/23`, typecheck `42/42`, format check and `git diff --check` GREEN.
|
||||
- Independent Codex review of remediation commit `d05465e54736c4966294c4af8fbd6a4ad8fe81aa`: APPROVE, confidence `0.94`, zero findings. Reviewer sandbox could not allocate temp directories; unrestricted parent test evidence above is canonical.
|
||||
|
||||
- Remediation session: terra review comment `18072` reproduced a SERIAL-ACCEPT DoS; scope is RED regressions plus bounded concurrent connection handling on PR #836, preserving all existing broker security properties.
|
||||
- RED evidence against reviewed daemon: four silent peers delayed registration `3920 ms` beyond the `1500 ms` bound; 16 silent peers were not reaped within `2500 ms`. The first bounded implementation then exposed slot exhaustion by rejecting the valid queued caller with `EPIPE`; admission was corrected to wait for a reclaimed bounded slot. GREEN evidence: queued-peer test `211 ms`; strengthened cap/reap/reclaim test `1118 ms`; complete real-socket acceptance `37/37` and Python persistence suite `10/10`.
|
||||
|
||||
## Coordinator handoff requirements
|
||||
|
||||
1. Mandatory Opus-SECREV on the exact PR head; no GPT/terra substitute.
|
||||
2. Independent exact-head code review and exact-head RoR before Mos-authorized merge.
|
||||
3. Mos retains merge authority; this WI author stops after PR + full 40-character head handoff.
|
||||
130
docs/scratchpads/829-mutator-gate.md
Normal file
130
docs/scratchpads/829-mutator-gate.md
Normal file
@@ -0,0 +1,130 @@
|
||||
# WI-2 Scratchpad — Whole mutator-class gate
|
||||
|
||||
- **Issue:** Gitea #829
|
||||
- **Branch:** `feat/829-mutator-gate`
|
||||
- **Base HEAD:** `8ec67a1126adb0dcd4c3a2bf5525f3e239c0b201`
|
||||
- **Role:** sol author/build lane only; terra code review and Opus security review are coordinator-owned.
|
||||
|
||||
## Mission prompt
|
||||
|
||||
Implement BUILD-BRIEF Deliverable 2 as a framework-native whole mutator-class gate under `packages/mosaic/`, building against the merged WI-1 lease broker. No consequential mutator may succeed while UNVERIFIED after a compaction observer fires or after TTL. Carry the T-B compromised-tool acceptance criteria. Enforce revoke-first and promote-last structurally. A receipt is only a promotion prerequisite; the mutator-class gate remains the safety mechanism. M1 is Claude + Pi only.
|
||||
|
||||
## Authority verification
|
||||
|
||||
Verified exact SHA-256 before design/code:
|
||||
|
||||
- BUILD-BRIEF: `89fdbc27ed0e5050dc7b52f3ef2ddaea691edf17fd89d51b15e26fb5ed47171b`
|
||||
- SPEC-v5: `a6d07ade835758e8488ca10d3b0631caf0beb93ea3a6733631f151b0c2f01433`
|
||||
- Ratification: `bac58319c9c4028b5b40e1129e0033cdb5a6b7b02033c25f06f4cb77d7779c67`
|
||||
- sol final red-team: `3da326a4ea91767b731e128a93b13194e8002358101e30de3fcb8ca2f8f54faa`
|
||||
|
||||
Carried authority chain also verified/read for the locked T-B gate contract: SPEC-v4 `a5e9c261…`, v4 sol `1e76ee59…`, SPEC-v3 `e0830ba0…`, v3 sol `9f321ade…`.
|
||||
|
||||
## Plan
|
||||
|
||||
1. RED real-socket acceptance tests for default-deny whole classes, T-B raw-tool bypass, observer/TTL revocation, and structural revoke-first/promote-last.
|
||||
2. Extend the merged WI-1 broker as the sole lease authority; authenticate every transition through existing peercred/ancestry/session logic and consume WI-1 single-use cycle tokens atomically before promotion.
|
||||
3. Add one broker-backed runtime gate executable and wire it across all Claude `PreToolUse` tools and Pi `tool_call`; unknown/custom tools deny by default.
|
||||
4. Add proportional protocol/security/operations documentation and requirements-to-evidence mapping.
|
||||
5. Run focused coverage, package/full suites, lint/typecheck/format, then queue-guard, push, open an unmerged PR with `closes #829`, and hand off the exact head.
|
||||
|
||||
## Risks and bounds
|
||||
|
||||
- Receipt parsing/builders and compaction observers are later WIs; WI-2 exposes the promotion prerequisite boundary but does not treat a receipt as safety authority.
|
||||
- Broker restart intentionally loses volatile VERIFIED leases and therefore restarts UNVERIFIED; persistent WI-1 identity/token state remains unchanged.
|
||||
- The gate is whole-class and does not parse shell command strings. T-C extension/hook absence and same-UID broker replacement remain outside the client guarantee and server branch protection remains the backstop.
|
||||
- Initial lease TTL is capped at ratified 300 seconds; callers may only shorten it.
|
||||
- Working budget assumption: 35K tokens; reduce documentation/refactor breadth before touching locked scope if pressure rises.
|
||||
|
||||
## Progress and verification
|
||||
|
||||
- RED #1: all 5 initial real-socket contract tests failed on WI-1 with `UNKNOWN_ACTION` or missing adapter behavior.
|
||||
- RED #2: register-before-exec runtime test failed because `launch-runtime.py` did not exist.
|
||||
- GREEN: broker-owned volatile lease state, 300-second maximum monotonic TTL, WI-1 token-backed promotion, all-tools runtime gate, Claude/Pi wiring, and register-before-exec launcher delivered without changing WI-1 peercred/ancestry authority.
|
||||
- Focused broker + gate acceptance: `43/43` GREEN.
|
||||
- Instrumented Python coverage: `88%` total — daemon `89%`, register/exec launcher `86%`, runtime gate `86%`.
|
||||
- Full repository suite: `43/43` Turbo tasks GREEN; `@mosaicstack/mosaic` `72/72` files and `1,377/1,377` tests GREEN.
|
||||
- Root typecheck: `42/42`; lint: `23/23`; format check and `git diff --check`: GREEN.
|
||||
- No author self-review was run. Exact-head terra CODE and Opus SECREV remain coordinator-owned gates.
|
||||
|
||||
## Acceptance mapping
|
||||
|
||||
| Acceptance criterion | Evidence |
|
||||
| --- | --- |
|
||||
| No consequential mutator succeeds while UNVERIFIED after observer revoke or TTL | `observer revocation and monotonic TTL expiry deny the next mutator` real-socket acceptance test |
|
||||
| T-B compromised-tool bypass is covered by the whole gate | `T-B raw and custom mutator tools are default-denied without shell parsing` across Claude/Pi built-ins, raw Bash class, MCP/custom/unknown tools |
|
||||
| Revoke-first / promote-last is structural | `revoke-first and promote-last structurally bracket mutator authority`; direct promotion rejected, pending remains denied, token consumption commits before VERIFIED |
|
||||
| Consume WI-1 auth/lease substrate | All transitions and decisions traverse merged peercred/ancestry authentication; promotion consumes the exact WI-1 CSPRNG cycle token |
|
||||
| M1 Claude + Pi | Claude `.*` PreToolUse and Pi `tool_call` invoke the same broker gate; register-before-exec test proves broker-minted parent identity reaches runtime descendants |
|
||||
|
||||
## Locked discipline
|
||||
|
||||
- Re-verify and read the four authority artifacts before design or code.
|
||||
- RED-first tests must cover unverified mutation refusal, T-B compromised-tool refusal, and revoke-first/promote-last ordering.
|
||||
- Consume WI-1 VERIFIED-lease state; do not re-derive kernel identity, ancestry, sessions, or token authentication.
|
||||
- Minimum 85% new-code coverage; full suite, lint, typecheck, and format checks green.
|
||||
- Build only: no self-review and no merge. Open a PR containing `closes #829`, report its exact 40-character head, then exit.
|
||||
|
||||
## Remediation — terra CODE comment 18091
|
||||
|
||||
- Coordinator correction: terra returned REQUEST CHANGES at head `77b137ccc04b5be035cac5ca21bbbf3df8b94f97`; Opus SECREV was GO and CI green, but no evidence transfers to the remediated head.
|
||||
- BLOCKER 1 verified: first-class Claude/Pi route through `execLeaseGatedRuntime`, while the supported Claudex path preserves isolation but directly invokes `claude`; it therefore registers no anchor, injects no lease session, and the isolated config has no guaranteed all-tools gate hook.
|
||||
- BLOCKER 2 accepted: prior 88% was aggregate evidence. Remediation must produce independently measured branch coverage of at least 85% for each new executable (`launch-runtime.py`, `mutator-gate.py`, and daemon delta evidence), including successful exec-boundary collection and validation/error branches.
|
||||
- Remediation discipline: RED tests first; preserve the reviewed-good broker lock/state-transition ordering; update PR #837 on the same branch; no self-review or merge.
|
||||
|
||||
### Remediation evidence
|
||||
|
||||
- RED commit `046896c6`: both `mosaic claudex` and `mosaic yolo claudex` behavioral probes exited 1 because the direct path supplied neither a broker session nor the isolated all-tools hook; branch-focused Python tests failed on the absent injectable boundaries. Fresh WI-2 daemon-delta instrumentation also failed the ≥85% branch gate at 75%.
|
||||
- GREEN: Claudex now exposes only `execLeaseGated`, passes the preserved isolated proxy environment through the shared register-before-exec wrapper, and merges the exact `.*` mutator hook into isolated `settings.json` with mode `0600`. Missing broker/identity, malformed or symlinked settings, and an unverified consequential tool all fail closed. The broker transition/lock implementation was not changed.
|
||||
- Behavioral regression: normal and YOLO Claudex both receive a 64-hex broker session, retain their mode-specific arguments, observe the exact all-tools hook, and receive status 2 for unverified `Bash`.
|
||||
- Independent branch coverage: `launch-runtime.py` 16/18 = **89%** (statements 98%); `mutator-gate.py` 21/22 = **95%** (statements 99%); `daemon.py` WI-2 delta 35/40 = **88%** (whole-file branch 80%, statements 90%).
|
||||
- Fresh focused real-socket coverage run: WI-1 + WI-2 acceptance `46/46`; persistence `10/10`; branch unit suite `10/10`.
|
||||
- Fresh full repository suite: `43/43` Turbo tasks; `@mosaicstack/mosaic` `72/72` files and `1,381/1,381` tests.
|
||||
- Fresh root gates: typecheck `42/42`; lint `23/23`; format and `git diff --check` GREEN.
|
||||
- PR #837 remains open and unmerged. Terra CODE and Opus SECREV must both rerun from zero on the exact remediated head before coordinator-owned merge authorization.
|
||||
|
||||
## Remediation round 3 — terra CODE comment 18099 + binding upgrade
|
||||
|
||||
- Locked-good surfaces: Claudex gating and B2 per-executable coverage are verified; do not regress them. Broker state-transition/lock ordering remains untouched.
|
||||
- Mechanical repository sweep found direct executing Claude entries in PRDY init, PRDY update, QA remediation, and `@mosaicstack/coord` task launch. It also found a direct Claude command rendered into the QA report template and documentation examples. Existing Mosaic CLI Claude/Pi/Claudex, orchestrator session-run, and fleet starts already reach the gated boundary.
|
||||
- Elevated hard requirements: ship a permanent suite/CI guard that scans production source and fails on any direct Claude/Pi launch; route every executing entry through one common gated wrapper; add real-broker RED/GREEN tests for PRDY init/update and QA; preserve each environment and denial behavior; independently measure all new executable coverage at ≥85%.
|
||||
- Round-3 plan: first commit RED behavioral and scanner-contract tests; then add one framework `launch-runtime.sh` choke-point over `launch-runtime.py`, make Mosaic CLI and shell launchers use it, make coord route through `mosaic`, and wire the permanent guard into package tests. Update all discovered operator-facing direct-launch examples so the scanner inventory remains complete.
|
||||
|
||||
### Round-3 outcome
|
||||
|
||||
- RED commit `7f3418fa`: PRDY init, PRDY update, and QA remediation all reached the fake Claude binary without a broker session even when the configured socket did not exist; the permanent-guard contract initially failed because its executable was absent, then failed against the five discovered direct entries (four executing plus the QA command template).
|
||||
- Choke-point decision: a new shell layer was unnecessary. Every executing repository entry now converges directly or through `mosaic`/`execLeaseGatedRuntime` on the existing single `launch-runtime.py` register-then-exec wrapper. PRDY and QA preserve their working directories, prompts, flags, logging pipe, and environment. Coord rewrites direct Claude commands to `mosaic claude` and rejects unknown custom Claude launchers fail-closed.
|
||||
- Permanent guard: `packages/mosaic/framework/tools/lease-broker/check-runtime-launches.py`, invoked by `packages/mosaic/package.json` `test:framework-shell` and therefore root `pnpm test`/CI. It scans production code under `packages/`, `apps/`, `plugins/`, and `tools/` and rejects literal, absolute-path, process-API, command-array, and dynamic Claude/Pi launch forms. Synthetic bypass tests are permanent at `runtime_launch_guard_unittest.py`.
|
||||
- Mechanical inventory: **14 gated / 14 total** — coord 2, fleet 1, QA 2, orchestrator 3, PRDY 2, Mosaic Claude/Pi/Claudex adapter/boundary 4. No verification-layer fallback or follow-up issue is needed because the single code-level wrapper was achieved.
|
||||
- Real-socket behavioral evidence: PRDY init, PRDY update, and QA remediation each fail before runtime execution when the broker is absent; with the broker present they receive a broker-minted 64-hex session and the unverified `Bash` authorization exits 2. Claudex normal/YOLO and the broker state machine remain GREEN.
|
||||
- Fresh branch coverage: `launch-runtime.py` **18/18 = 100%**; `mutator-gate.py` **22/22 = 100%**; permanent guard **36/38 = 95%**; `daemon.py` WI-2 delta **35/40 = 87.5%**. All attributable executable statement coverage is at least 98%.
|
||||
- Fresh focused suites: broker + mutator real-socket acceptance `49/49`; persistence `10/10`; launcher/gate branch suite `13/13`; permanent guard suite `7/7`; coord `19/19`.
|
||||
- Fresh full repository suite: `43/43` Turbo tasks; `@mosaicstack/mosaic` `72/72` files and `1,384/1,384` tests. Root typecheck `42/42`, lint `23/23`, format, and diff checks GREEN.
|
||||
- PR #837 remains open and unmerged. Terra CODE and Opus SECREV must both rerun from zero on the exact round-3 head before coordinator-owned merge authorization.
|
||||
|
||||
## Remediation round 4 — terra CODE comment 18104
|
||||
|
||||
- Locked-good surfaces: the 14/14 launch inventory, single `launch-runtime.py` choke-point, real-socket launcher behavior, coverage, Claudex gating, and broker state machine must not change.
|
||||
- Reproduced RIDER E exactly at head `1792b7934dda7eff64a207b8b0edb9c460d4164b`: a temporary production file containing `exec claude --dangerously-skip-permissions "terra-r3" # launch-runtime.py` made the guard exit 0 and report `1 gated/1 total`.
|
||||
- Root cause: classification searched the unparsed physical line, and the broad gated regex treated any `launch-runtime.py` substring—including comments and inert arguments—as an invocation before the direct-launch finding was evaluated.
|
||||
- Round-4 plan: add permanent RED cases for the exact comment evasion plus string-argument, echo, and unrelated-variable marker evasions; tokenize/strip comments by launcher syntax; recognize only command-position wrapper invocations with `--runtime` and the gated command separator; retain 14/14 real inventory; rerun guard coverage and all gates fresh.
|
||||
- Mos Rider A/B decision: adopt **both** defenses. Command-position parsing remains necessary because a normal `claude -p` launch is consequential even without the dangerous flag. The primitive-location invariant is more mechanically robust for dangerous mode because it does not need to recognize a wrapper marker at all. Move the sole raw `--dangerously-skip-permissions` literal into `launch-runtime.py`; any occurrence in another production file is independently RED.
|
||||
- Rider-A RED matrix adds heredoc body, backslash continuation, non-first `;`/`&&`/pipe commands, command substitution, `eval`, and variable-execution indirection in addition to the six marker/comment evasions. Before the augmented implementation, primitive ownership, command substitution, `eval`, variable execution, and the preserved 14-site inventory all fail.
|
||||
- Round-4 GREEN uses both defenses. Quote-aware comment stripping removes shell/Python `#` and JS/TS line/block comments; shell command prefixes are segmented with `shlex`; validated wrappers require `launch-runtime.py` in command position, `--runtime`, and the `--` command separator; multiline TypeScript wrapper calls are validated as complete invocations. Direct command syntax wins over markers, while tracked runtime assignments plus `eval`/variable execution, command substitution, chained commands, heredocs, continuations, and `env`/`command`/`nohup` prefixes are rejected.
|
||||
- Primitive ownership is independently load-bearing: `launch-runtime.py` is the sole production owner of the raw Claude dangerous flag. Mosaic, Claudex, and PRDY request semantic `--dangerous`; the wrapper validates Claude and injects the primitive immediately before register/exec. This preserves actual YOLO argv behavior while making any raw primitive elsewhere fail without relying on wrapper-name recognition.
|
||||
- Permanent guard suite now has 10 tests and 31 direct-launch forms, including 18 new round-4 marker/comment/indirection/prefix evasions plus harmless-marker and multiline-wrapper controls. Terra's exact add-ungated source is exercised through the CLI effectiveness test. Repository inventory remains exactly **14 gated / 14 total**.
|
||||
- Fresh round-4 coverage: guard **97%** branch-aware aggregate (241 statements, 110 branches); `launch-runtime.py` **100%**; `mutator-gate.py` **100%**. The daemon is byte-unchanged from the round-3 head whose WI-2 delta is **87.5%**.
|
||||
- Fresh round-4 gates: real-socket acceptance **49/49**; persistence **10/10**; launcher/gate **14/14**; guard **10/10**; coord **19/19**; Mosaic **1384/1384**; root **43/43**; typecheck **42/42**; lint **23/23**; format and diff checks green.
|
||||
|
||||
## Remediation round 5 — terra 18116 + Opus 18114
|
||||
|
||||
- Both independent gates converged on one guard-only completeness gap at round-4 head `1eb77c17f3147d4fa9944f77f1826243135b9cc0`; all round-4 primitive anchoring, command-position parsing, 14/14 inventory, broker ordering, and coverage remain locked-good.
|
||||
- Reproduced exactly: a temporary production source containing `launcher=claude` followed by `exec "$launcher" -p x` exits 0 with `0 gated / 0 total`. The literal command resolver skips prefixes but cannot resolve a tracked variable; the variable resolver handles only bare/eval references and cannot skip prefixes.
|
||||
- Round-5 plan: add 10 permanent RED forms (quoted/unquoted `exec`, `command`, `nohup`, and `env` with assignment, each multiline and same-line), then unify shell command-position resolution so literal and tracked-variable terminal tokens traverse the same prefix parser. Retain an independent variable-reference backstop, the 14/14 inventory, and every round-4 regression.
|
||||
- Mos stopping-criterion augment: command parsing is explicitly best-effort rather than a complete shell interpreter. Add B1 proving a parser-exotic alias launch with the raw dangerous flag is still RED by primitive anchoring, and B2 proving a parser-missed non-dangerous alias launch reaches the global `.*` hook and fails closed with `GATE_UNAVAILABLE` when no lease session exists. Document A (realistic parser matrix) + B (robust residual backstops); fresh reviewers supply criterion C (no new non-overlapping finding).
|
||||
- RED commit `91a4a983`: all 10 prefix×variable cases failed as expected before the fix—quoted/unquoted `exec`, `command`, `nohup`, and `env A=1`, each in multiline and same-line assignment shapes.
|
||||
- GREEN structural resolution: `shell_command_tokens()` now owns command-position prefix skipping for both literal and variable callers, including nested `exec`/`command`/`nohup`/`env` ordering. `runtime_variables` is threaded into `is_shell_direct_invocation()` and the same terminal-token resolver backs `executes_runtime_variable()`; exact `$v` and `${v}` references are resolved after `shlex` removes quoting. Same-line runtime assignment delimiters include shell operators.
|
||||
- Residual backstops: B1 proves alias-indirected dangerous mode is classified `dangerous-primitive` even though the parser does not resolve the alias. B2 proves a non-dangerous alias residual remains parser-missed, then verifies the shipped global `.*` Claude hook and status-2 `GATE_UNAVAILABLE` denial for representative read, mutator, and custom/MCP tools without a lease session.
|
||||
- Stopping-criterion evidence A+B is committed in tests and architecture docs; C remains the fresh exact-head terra/Opus determination. Repository inventory remains exactly **14 gated / 14 total**.
|
||||
- Fresh round-5 coverage: permanent guard remains **97%** branch-aware aggregate (251 statements, 112 branches). Locked-good launcher and mutator-gate executables remain unchanged at their round-4 **100% / 100%** evidence.
|
||||
- Fresh round-5 gates: real-socket acceptance **50/50**; persistence **10/10**; launcher/gate **14/14**; guard **12/12**; coord **19/19**; Mosaic **1385/1385**; root **43/43**; typecheck **42/42**; lint **23/23**; format and diff checks green.
|
||||
44
docs/scratchpads/838-broker-acceptance-flake.md
Normal file
44
docs/scratchpads/838-broker-acceptance-flake.md
Normal file
@@ -0,0 +1,44 @@
|
||||
# Issue #838 — Broker acceptance socket flake
|
||||
|
||||
## Objective
|
||||
|
||||
Eliminate high-contention uncaught JSON parse failures in lease-broker and mutator-gate acceptance helpers without laundering malformed/empty replies into passing assertions.
|
||||
|
||||
## Constraints
|
||||
|
||||
- Branch: `fix/838-broker-acceptance-flake` in `/home/hermes/agent-work/stack-838-flakefix`.
|
||||
- Red-first TDD with a forced empty/truncated reply path and deterministic rejection or documented retry.
|
||||
- Read newline-framed replies completely; reject malformed broker replies with byte length/content context.
|
||||
- Determine RIDER2 branch before finalizing: test-harness-only `(a)` or daemon write truncation `(b)`.
|
||||
- If product-side, prove the real Claude/Pi adapter read path fails closed; fix any allow-risk.
|
||||
- Coverage >=85% per changed executable through real tests.
|
||||
- Full suite and repository gates green; independent exact-head review required.
|
||||
- Push and open a PR containing `closes #838`; do not merge.
|
||||
|
||||
## Progress
|
||||
|
||||
- 2026-07-17: Reclaimed from #824. Confirmed clean worktree on `fix/838-broker-acceptance-flake` at main base `abd2791f59b3f06f46dd08e55298ced72f6aa7c2`.
|
||||
- RED evidence: after dependency setup, `broker-test-client.spec.ts` failed to load the intentionally absent shared client module. Its contract forces empty-close retry, repeated truncated-close rejection with byte context, and newline-terminated malformed-reply rejection.
|
||||
- RIDER2 verdict: branch **(b)**. `daemon.py` starts one connection deadline before request reading, then may spend that budget waiting for `broker_lock`; after handling, `remaining <= 0` returns without writing. A timed `sendall` failure can likewise close after a partial write. A deterministic socketpair probe held the lock past `CONNECTION_DEADLINE_SECONDS` and observed `deadline_probe_reply_length=0`.
|
||||
- Adapter tripwire: real subprocess executions of `mutator-gate.py` for both `--runtime claude` and `--runtime pi` against actual Unix servers returning empty and truncated replies all exited 2 with `GATE_UNAVAILABLE`. Adapter fail-close is proven; there is no ALLOW risk.
|
||||
- Residual: production broker reply loss remains an availability-denial path under extreme contention, but cannot grant mutator authority. The acceptance-only client retries early closes and otherwise rejects with response byte length, escaped bytes, and hex; malformed newline-framed replies are never retried or converted to reply objects.
|
||||
- Shared client now owns newline framing and parsing for both acceptance suites. Focused suites pass 60 tests, including the real adapter tripwire.
|
||||
- Coverage gate: changed helper is 100% statements/lines/functions and 90% branches; existing `skill.ts` remains above 85% per-file thresholds.
|
||||
|
||||
## Scope corrections and bounded product repair
|
||||
|
||||
- Coordinator correction superseded the initial push/PR and retry language: this lane is BUILD-ONLY, and early-close retries are forbidden because they can mask a committed broker transaction. Nothing may be pushed or opened until explicitly cleared.
|
||||
- Revised RED evidence: the no-retry empty/truncated tests failed because the first failed exchange was retried into `{ ok: true }`; the daemon regression failed because a slow completed `broker.handle()` produced `b''` instead of a newline-framed reply.
|
||||
- Client repair: both former duplicated helpers use one shared reader. Empty, truncated, malformed, oversized, timed-out, and socket-error replies reject `BrokerTransportError` with a typed `kind`, attempt count fixed at one, response length, escaped bytes, and hex. No retry and no catch-to-reply conversion exists.
|
||||
- Product repair: `daemon.py` now has independent bounded read, broker-lock queue, and send budgets. Lock queue exhaustion returns explicit `BROKER_BUSY` before `broker.handle()` can mutate state. Once handling starts it finishes atomically, and its reply always receives a fresh send timeout instead of being skipped because read/lock/fsync consumed a shared deadline.
|
||||
- Product GREEN evidence: deterministic socketpair tests prove lock saturation returns framed `BROKER_BUSY` without invoking `handle()`, and a handle that completes after the former one-second shared deadline still returns its complete framed reply.
|
||||
- RIDER2b remains **fail-closed**: real Claude and Pi `mutator-gate.py` subprocesses against empty and truncated Unix-socket replies exit 2 with `GATE_UNAVAILABLE`; no malformed/default ALLOW was observed.
|
||||
- Residual/tripwire: an unavoidable peer disconnect or send failure can still lose acknowledgement after a valid transaction commits. The affected adapter call fails closed. A valid `promote_lease` may nevertheless remain VERIFIED after its acknowledgement is lost; that is authority-observability divergence requiring WI-3/Opus security review rather than expansion of #838. #838 does not add retries or attempt a protocol redesign.
|
||||
- Final package evidence: recursive Mosaic dependency build passed; 73 Vitest files / 1,392 tests passed; deadline unit tests 2/2, real runtime tool tests 15/15, launch guard tests 12/12, inventory 14/14, and shell regressions passed.
|
||||
- Final coverage: `broker-test-client.ts` 99.24% statements/lines, 86.11% branches, 100% functions under per-file >=85% thresholds. Repository typecheck 42/42, lint 23/23, and format check passed.
|
||||
- Independent Codex exact-head review requested one framing fix: a valid frame followed by trailing bytes in a later socket chunk could resolve before the garbage arrived. Security review also flagged complete token-bearing reply bodies in diagnostic properties/logs.
|
||||
- Review RED evidence: delayed cross-chunk garbage resolved `{ ok: true }` instead of rejecting, and a truncated `promotion_token` remained in the typed error. The tests use separate timed writes to prevent kernel/event-loop coalescing.
|
||||
- Review remediation: the shared client now accumulates through EOF, requires exactly one terminal newline, then parses inside a rejecting error boundary. Diagnostic bodies are capped at 256 bytes, sensitive broker fields are fully redacted, and a SHA-256 digest preserves correlation without credential disclosure.
|
||||
- Post-review coverage: `broker-test-client.ts` 99.35% statements/lines, 86.66% branches, 100% functions. Final full suite is 73 files / 1,394 tests plus all Python/shell gates; recursive build, typecheck, lint, and format are green.
|
||||
- Fresh exact-head Codex security review: risk `none`, no findings. Fresh code review found only that the real-adapter subprocess proof lacked a timeout; it now has a five-second bound and fails with runtime/wire context while closing the fake server. The focused Python suite remains 15/15 green.
|
||||
- Mandatory Opus SECREV remains coordinator-owned and pending before any push/PR decision; this build is intentionally local-only.
|
||||
41
packages/coord/src/__tests__/runtime-launch-gate.test.ts
Normal file
41
packages/coord/src/__tests__/runtime-launch-gate.test.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import { resolveLaunchCommand } from '../runner.js';
|
||||
|
||||
describe('coord consequential-runtime launch gate', () => {
|
||||
it('routes default and direct configured Claude commands through mosaic', () => {
|
||||
expect(resolveLaunchCommand('claude', 'continue', undefined)).toEqual([
|
||||
'mosaic',
|
||||
'claude',
|
||||
'-p',
|
||||
'continue',
|
||||
]);
|
||||
expect(resolveLaunchCommand('claude', 'continue', ['claude', '-p', '{prompt}'])).toEqual([
|
||||
'mosaic',
|
||||
'claude',
|
||||
'-p',
|
||||
'continue',
|
||||
]);
|
||||
});
|
||||
|
||||
it('preserves an already-gated Claude command and rejects unknown launchers', () => {
|
||||
expect(
|
||||
resolveLaunchCommand('claude', 'continue', ['mosaic', 'yolo', 'claude', '{prompt}']),
|
||||
).toEqual(['mosaic', 'yolo', 'claude', 'continue']);
|
||||
expect(() => resolveLaunchCommand('claude', 'continue', ['custom-launcher'])).toThrow(
|
||||
/must use `mosaic claude`/,
|
||||
);
|
||||
});
|
||||
|
||||
it('does not change the out-of-scope Codex command contract', () => {
|
||||
expect(resolveLaunchCommand('codex', 'continue', undefined)).toEqual([
|
||||
'codex',
|
||||
'-p',
|
||||
'continue',
|
||||
]);
|
||||
expect(resolveLaunchCommand('codex', 'continue', ['codex', '{prompt}'])).toEqual([
|
||||
'codex',
|
||||
'continue',
|
||||
]);
|
||||
});
|
||||
});
|
||||
@@ -179,32 +179,41 @@ function buildContinuationPrompt(params: {
|
||||
`3. Read \`${mission.scratchpadFile}\` for session history and decisions`,
|
||||
`4. Read \`${mission.tasksFile}\` for current task state`,
|
||||
'5. `git pull --rebase` to sync latest changes',
|
||||
`6. Launch runtime with \`${runtime} -p\``,
|
||||
`6. Launch runtime with \`mosaic ${runtime} -p\``,
|
||||
`7. Continue execution from task **${taskId}**`,
|
||||
'8. Follow Two-Phase Completion Protocol',
|
||||
`9. You are the SOLE writer of \`${mission.tasksFile}\``,
|
||||
].join('\n');
|
||||
}
|
||||
|
||||
function resolveLaunchCommand(
|
||||
export function resolveLaunchCommand(
|
||||
runtime: 'claude' | 'codex',
|
||||
prompt: string,
|
||||
configuredCommand: string[] | undefined,
|
||||
): string[] {
|
||||
if (configuredCommand === undefined || configuredCommand.length === 0) {
|
||||
return [runtime, '-p', prompt];
|
||||
return runtime === 'claude' ? ['mosaic', 'claude', '-p', prompt] : [runtime, '-p', prompt];
|
||||
}
|
||||
|
||||
const hasPromptPlaceholder = configuredCommand.some((value) => value === '{prompt}');
|
||||
const withInterpolation = configuredCommand.map((value) =>
|
||||
value === '{prompt}' ? prompt : value,
|
||||
);
|
||||
const command = hasPromptPlaceholder ? withInterpolation : [...withInterpolation, prompt];
|
||||
|
||||
if (hasPromptPlaceholder) {
|
||||
return withInterpolation;
|
||||
if (runtime !== 'claude') return command;
|
||||
if (
|
||||
command[0] === 'mosaic' &&
|
||||
(command[1] === 'claude' || (command[1] === 'yolo' && command[2] === 'claude'))
|
||||
) {
|
||||
return command;
|
||||
}
|
||||
|
||||
return [...withInterpolation, prompt];
|
||||
if (command[0] === 'claude') {
|
||||
return ['mosaic', 'claude', ...command.slice(1)];
|
||||
}
|
||||
throw new Error(
|
||||
'Custom Claude task commands must use `mosaic claude` so lease registration cannot be bypassed.',
|
||||
);
|
||||
}
|
||||
|
||||
async function writeAtomicJson(filePath: string, payload: unknown): Promise<void> {
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
When spawning workers, include skill loading in the kickstart:
|
||||
|
||||
```bash
|
||||
claude -p "Read ~/.config/mosaic/skills/nestjs-best-practices/SKILL.md then implement..."codex exec "Read ~/.config/mosaic/skills/nestjs-best-practices/SKILL.md then implement..."
|
||||
mosaic claude -p "Read ~/.config/mosaic/skills/nestjs-best-practices/SKILL.md then implement..."codex exec "Read ~/.config/mosaic/skills/nestjs-best-practices/SKILL.md then implement..."
|
||||
```
|
||||
|
||||
#### **MANDATORY**
|
||||
|
||||
@@ -2,6 +2,16 @@
|
||||
"model": "opus",
|
||||
"hooks": {
|
||||
"PreToolUse": [
|
||||
{
|
||||
"matcher": ".*",
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "python3 ~/.config/mosaic/tools/lease-broker/mutator-gate.py --runtime claude",
|
||||
"timeout": 3
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"matcher": "Write|Edit|MultiEdit",
|
||||
"hooks": [
|
||||
|
||||
@@ -28,6 +28,7 @@ import { execSync, spawnSync } from 'node:child_process';
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const MOSAIC_HOME = process.env['MOSAIC_HOME'] ?? join(homedir(), '.config', 'mosaic');
|
||||
const MUTATOR_GATE = join(MOSAIC_HOME, 'tools', 'lease-broker', 'mutator-gate.py');
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Helpers
|
||||
@@ -106,6 +107,23 @@ function nowIso(): string {
|
||||
return new Date().toISOString().replace(/\.\d{3}Z$/, 'Z');
|
||||
}
|
||||
|
||||
function checkPiMutatorGate(toolName: string): { block: true; reason: string } | undefined {
|
||||
const result = spawnSync('python3', [MUTATOR_GATE, '--runtime', 'pi'], {
|
||||
input: `${JSON.stringify({ tool_name: toolName })}\n`,
|
||||
encoding: 'utf8',
|
||||
timeout: 2_000,
|
||||
env: process.env,
|
||||
});
|
||||
if (result.status === 0) return undefined;
|
||||
const detail = String(result.stderr ?? '')
|
||||
.trim()
|
||||
.split('\n')[0];
|
||||
return {
|
||||
block: true,
|
||||
reason: detail || 'BLOCKED: Mosaic mutator gate is unavailable or the lease is UNVERIFIED.',
|
||||
};
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Mission detection
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -250,6 +268,11 @@ export default function register(pi: ExtensionAPI) {
|
||||
let hbModel: string | null = null;
|
||||
let hbTimer: ReturnType<typeof setInterval> | null = null;
|
||||
|
||||
// ── Whole mutator-class authorization gate ────────────────────────────
|
||||
// Every Pi tool, including unknown/custom tools, reaches the broker-backed
|
||||
// class gate before execution. Broker/script failure blocks fail-closed.
|
||||
pi.on('tool_call', async (event) => checkPiMutatorGate(event.toolName));
|
||||
|
||||
// ── Session Start ─────────────────────────────────────────────────────
|
||||
pi.on('session_start', async (_event, ctx) => {
|
||||
sessionCwd = process.cwd();
|
||||
|
||||
@@ -0,0 +1,436 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Fail CI when production code launches Claude/Pi outside the lease gate."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import re
|
||||
import shlex
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import Final, NamedTuple, Sequence
|
||||
|
||||
SCANNED_ROOTS: Final = ("packages", "apps", "plugins", "tools")
|
||||
SCANNED_SUFFIXES: Final = {
|
||||
".bash",
|
||||
".cjs",
|
||||
".js",
|
||||
".json",
|
||||
".mjs",
|
||||
".py",
|
||||
".sh",
|
||||
".ts",
|
||||
".tsx",
|
||||
".yaml",
|
||||
".yml",
|
||||
".zsh",
|
||||
}
|
||||
SKIPPED_DIRECTORIES: Final = {
|
||||
".git",
|
||||
".next",
|
||||
".turbo",
|
||||
"coverage",
|
||||
"dist",
|
||||
"node_modules",
|
||||
}
|
||||
DANGEROUS_PRIMITIVE: Final = "--dangerously-" + "skip-permissions"
|
||||
CHOKE_POINT_SUFFIX: Final = "framework/tools/lease-broker/launch-runtime.py"
|
||||
SHELL_SUFFIXES: Final = {".bash", ".sh", ".zsh"}
|
||||
|
||||
# A launch line is gated only when it CALLS the common wrapper in command
|
||||
# position, invokes the TypeScript adapter, or constructs/runs a `mosaic`
|
||||
# runtime command. A marker in a comment, string argument, echo, or unrelated
|
||||
# variable can never satisfy these invocation-shaped patterns.
|
||||
GATED_PATTERNS: Final = (
|
||||
re.compile(r"(?:^\s*|=>\s*)execLeaseGatedRuntime\s*\("),
|
||||
re.compile(
|
||||
r"\bexecRuntime\s*\(\s*[\"']python3[\"']\s*,\s*"
|
||||
r"\[\s*launcher\s*,.*[\"']--runtime[\"']\s*,\s*runtime\s*,\s*[\"']--[\"']"
|
||||
),
|
||||
re.compile(
|
||||
r"(?:^|[;&|]\s*|\bexec\s+|\b(?:LAUNCH_COMMAND|launch_cmd)\s*=\s*\(?|\becho\s+[\"'])"
|
||||
r"mosaic\s+(?:yolo\s+)?(?:claude|pi|claudex|[\"']?\$\{?runtime\}?[\"']?|[\"']?\$MOSAIC_AGENT_RUNTIME[\"']?)\b"
|
||||
),
|
||||
re.compile(r"\[\s*[\"']mosaic[\"']\s*,\s*(?:[\"']yolo[\"']\s*,\s*)?(?:runtime|[\"'](?:claude|pi|claudex)[\"'])"),
|
||||
)
|
||||
|
||||
DIRECT_PATTERNS: Final = (
|
||||
# Shell/process command forms, including here-doc command examples that an
|
||||
# operator could execute verbatim.
|
||||
re.compile(r"^\s*(?:claude|pi)(?:\s|$)"),
|
||||
re.compile(r"(?:^|[;&|]\s*|\bexec\s+|\bcommand\s+)(?:claude|pi)\s+(?:-p\b|--dangerously\b|--print\b)"),
|
||||
re.compile(r"\bexec\s+(?:claude|pi)(?:\s|$)"),
|
||||
re.compile(r"\bexec\s+(?:/[^\s/]+)+/(?:claude|pi)(?:\s|$)"),
|
||||
re.compile(r"\bexec\s+[\"']?\$(?:\{?runtime\}?|MOSAIC_AGENT_RUNTIME)\b"),
|
||||
# JS/TS and Python process APIs with a literal runtime binary.
|
||||
re.compile(
|
||||
r"\b(?:spawn|spawnSync|exec|execSync|execFile|execFileSync|execv|execvp|execvpe|Popen|run|call|system|check_call|check_output)\s*\(\s*(?:\[\s*)?[\"'](?:claude|pi)(?:[\"']|\s)"
|
||||
),
|
||||
re.compile(
|
||||
r"\b(?:spawn|spawnSync|exec|execSync|execFile|execFileSync|execv|execvp|execvpe|Popen|run|call|system|check_call|check_output)\s*\(\s*(?:\[\s*)?[\"'](?:/[^\"'/]+)+/(?:claude|pi)[\"']"
|
||||
),
|
||||
# Launch-command arrays and the prior @mosaicstack/coord dynamic default.
|
||||
re.compile(r"\b(?:spawn|spawnSync|exec|execSync|execFile|execFileSync)\s*\(\s*runtime\b"),
|
||||
re.compile(r"\b(?:command|launchCommand|LAUNCH_COMMAND)\s*=\s*(?:\(|\[)\s*[\"']?(?:claude|pi)\b"),
|
||||
re.compile(r"\b(?:command|launchCommand|LAUNCH_COMMAND)\s*=\s*(?:\(|\[)\s*[\"']?\$(?:\{?runtime\}?|MOSAIC_AGENT_RUNTIME)\b"),
|
||||
re.compile(r"\breturn\s*\[\s*runtime\s*,\s*[\"'](?:-p|--dangerously)"),
|
||||
re.compile(r"\$\(\s*(?:claude|pi)(?:\s|$)"),
|
||||
re.compile(r"\beval\s+[\"'](?:claude|pi)(?:\s|[\"'])"),
|
||||
)
|
||||
RUNTIME_ASSIGNMENT: Final = re.compile(
|
||||
r"^\s*([A-Za-z_][A-Za-z0-9_]*)\s*=\s*[\"']?(?:claude|pi)(?:\s|[\"']|[;&|]|$)"
|
||||
)
|
||||
TYPESCRIPT_WRAPPER_INVOCATION: Final = re.compile(
|
||||
r"(?m)^\s*execRuntime\s*\(\s*[\"']python3[\"']\s*,\s*"
|
||||
r"\[\s*launcher\s*,[^\]]*[\"']--runtime[\"']\s*,\s*runtime\s*,\s*"
|
||||
r"[\"']--[\"']\s*,\s*runtime(?:\s*,|\s*\])",
|
||||
re.DOTALL,
|
||||
)
|
||||
|
||||
|
||||
class LaunchSite(NamedTuple):
|
||||
path: Path
|
||||
line_number: int
|
||||
line: str
|
||||
classification: str
|
||||
|
||||
|
||||
def is_test_path(path: Path) -> bool:
|
||||
name = path.name.lower()
|
||||
return (
|
||||
"__tests__" in path.parts
|
||||
or ".spec." in name
|
||||
or ".test." in name
|
||||
or name.endswith("_unittest.py")
|
||||
or name.startswith("test-")
|
||||
or name.startswith("test_")
|
||||
)
|
||||
|
||||
|
||||
def strip_comments(path: Path, line: str, in_block_comment: bool = False) -> tuple[str, bool]:
|
||||
suffix = path.suffix.lower()
|
||||
hash_comments = suffix in {".bash", ".py", ".sh", ".yaml", ".yml", ".zsh"}
|
||||
slash_comments = suffix in {".cjs", ".js", ".mjs", ".ts", ".tsx"}
|
||||
output: list[str] = []
|
||||
quote: str | None = None
|
||||
escaped = False
|
||||
index = 0
|
||||
|
||||
while index < len(line):
|
||||
if in_block_comment:
|
||||
end = line.find("*/", index)
|
||||
if end < 0:
|
||||
return "".join(output), True
|
||||
in_block_comment = False
|
||||
index = end + 2
|
||||
continue
|
||||
|
||||
character = line[index]
|
||||
following = line[index + 1] if index + 1 < len(line) else ""
|
||||
if quote is not None:
|
||||
output.append(character)
|
||||
if escaped:
|
||||
escaped = False
|
||||
elif character == "\\":
|
||||
escaped = True
|
||||
elif character == quote:
|
||||
quote = None
|
||||
index += 1
|
||||
continue
|
||||
|
||||
if character in {"'", '"', "`"}:
|
||||
quote = character
|
||||
output.append(character)
|
||||
index += 1
|
||||
continue
|
||||
if slash_comments and character == "/" and following == "/":
|
||||
break
|
||||
if slash_comments and character == "/" and following == "*":
|
||||
in_block_comment = True
|
||||
index += 2
|
||||
continue
|
||||
if hash_comments and character == "#":
|
||||
if suffix == ".py" or index == 0 or line[index - 1].isspace():
|
||||
break
|
||||
output.append(character)
|
||||
index += 1
|
||||
|
||||
return "".join(output), in_block_comment
|
||||
|
||||
|
||||
def is_choke_point(path: Path) -> bool:
|
||||
return path.as_posix().endswith(CHOKE_POINT_SUFFIX)
|
||||
|
||||
|
||||
def shell_commands(line: str) -> list[list[str]]:
|
||||
candidate = line.rstrip().removesuffix("\\").rstrip()
|
||||
try:
|
||||
lexer = shlex.shlex(candidate, posix=True, punctuation_chars=";&|")
|
||||
lexer.whitespace_split = True
|
||||
lexer.commenters = ""
|
||||
tokens = list(lexer)
|
||||
except ValueError:
|
||||
return []
|
||||
|
||||
commands: list[list[str]] = []
|
||||
current: list[str] = []
|
||||
for token in tokens:
|
||||
if token and all(character in ";&|" for character in token):
|
||||
if current:
|
||||
commands.append(current)
|
||||
current = []
|
||||
else:
|
||||
current.append(token)
|
||||
if current:
|
||||
commands.append(current)
|
||||
return commands
|
||||
|
||||
|
||||
def is_wrapper_invocation(path: Path, line: str) -> bool:
|
||||
if path.suffix.lower() not in SHELL_SUFFIXES:
|
||||
return False
|
||||
separator = re.search(r"\s--(?:\s|$)", line)
|
||||
if separator is None:
|
||||
return False
|
||||
# Everything after the wrapper separator is opaque runtime argv and may
|
||||
# contain an open quote continued on later physical lines. Parse only the
|
||||
# complete command-position prefix through the separator.
|
||||
wrapper_prefix = line[: separator.end()]
|
||||
for command in shell_commands(wrapper_prefix):
|
||||
if command and command[0] == "exec":
|
||||
command = command[1:]
|
||||
if not command:
|
||||
continue
|
||||
if command[0] == "python3":
|
||||
if len(command) < 2 or not command[1].endswith("launch-runtime.py"):
|
||||
continue
|
||||
arguments = command[2:]
|
||||
elif command[0].endswith(("launch-runtime.py", "launch-runtime.sh")):
|
||||
arguments = command[1:]
|
||||
else:
|
||||
continue
|
||||
try:
|
||||
runtime_index = arguments.index("--runtime")
|
||||
separator_index = arguments.index("--")
|
||||
except ValueError:
|
||||
continue
|
||||
if runtime_index + 1 < len(arguments) and runtime_index < separator_index:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def is_gated_line(path: Path, line: str) -> bool:
|
||||
return is_wrapper_invocation(path, line) or any(
|
||||
pattern.search(line) for pattern in GATED_PATTERNS
|
||||
)
|
||||
|
||||
|
||||
def shell_command_tokens(path: Path, line: str) -> list[str]:
|
||||
if path.suffix.lower() not in SHELL_SUFFIXES:
|
||||
return []
|
||||
assignment = re.compile(r"^[A-Za-z_][A-Za-z0-9_]*=")
|
||||
case_arm = re.match(r"^\s*[^;&()]+\)\s*", line)
|
||||
command_source = line[case_arm.end() :] if case_arm is not None else line
|
||||
resolved: list[str] = []
|
||||
for command in shell_commands(command_source):
|
||||
index = 0
|
||||
while index < len(command) and assignment.match(command[index]):
|
||||
index += 1
|
||||
# Resolve command position once for every literal and variable caller.
|
||||
# Prefixes may be nested in either order (for example `exec env A=1`).
|
||||
while index < len(command):
|
||||
if command[index] in {"command", "exec", "nohup"}:
|
||||
index += 1
|
||||
continue
|
||||
if command[index] == "env":
|
||||
index += 1
|
||||
while index < len(command) and (
|
||||
command[index].startswith("-") or assignment.match(command[index])
|
||||
):
|
||||
index += 1
|
||||
continue
|
||||
break
|
||||
if index < len(command):
|
||||
resolved.append(command[index])
|
||||
return resolved
|
||||
|
||||
|
||||
def runtime_variable_reference(token: str, runtime_variables: set[str]) -> bool:
|
||||
match = re.fullmatch(r"\$(?:([A-Za-z_][A-Za-z0-9_]*)|\{([A-Za-z_][A-Za-z0-9_]*)\})", token)
|
||||
if match is None:
|
||||
return False
|
||||
return (match.group(1) or match.group(2)) in runtime_variables
|
||||
|
||||
|
||||
def is_shell_direct_invocation(
|
||||
path: Path, line: str, runtime_variables: set[str]
|
||||
) -> bool:
|
||||
return any(
|
||||
Path(token).name in {"claude", "pi"}
|
||||
or runtime_variable_reference(token, runtime_variables)
|
||||
for token in shell_command_tokens(path, line)
|
||||
)
|
||||
|
||||
|
||||
def is_direct_line(path: Path, line: str, runtime_variables: set[str]) -> bool:
|
||||
return is_shell_direct_invocation(path, line, runtime_variables) or any(
|
||||
pattern.search(line) for pattern in DIRECT_PATTERNS
|
||||
)
|
||||
|
||||
|
||||
def executes_runtime_variable(
|
||||
path: Path, line: str, runtime_variables: set[str]
|
||||
) -> bool:
|
||||
if any(
|
||||
runtime_variable_reference(token, runtime_variables)
|
||||
for token in shell_command_tokens(path, line)
|
||||
):
|
||||
return True
|
||||
for variable in runtime_variables:
|
||||
reference = rf"\$(?:{re.escape(variable)}|\{{{re.escape(variable)}\}})"
|
||||
if re.search(rf"\beval\s+[\"']?{reference}", line):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def typescript_wrapper_lines(path: Path, source: str) -> set[int]:
|
||||
if path.suffix.lower() not in {".js", ".mjs", ".ts", ".tsx"}:
|
||||
return set()
|
||||
return {
|
||||
source.count("\n", 0, match.start()) + 1
|
||||
for match in TYPESCRIPT_WRAPPER_INVOCATION.finditer(source)
|
||||
}
|
||||
|
||||
|
||||
def classify_text(path: Path, source: str) -> list[LaunchSite]:
|
||||
sites: list[LaunchSite] = []
|
||||
validated_typescript_wrappers = typescript_wrapper_lines(path, source)
|
||||
runtime_variables: set[str] = set()
|
||||
gated_continuation = False
|
||||
in_block_comment = False
|
||||
for line_number, physical_line in enumerate(source.splitlines(), start=1):
|
||||
line, in_block_comment = strip_comments(path, physical_line, in_block_comment)
|
||||
stripped = line.strip()
|
||||
if not stripped:
|
||||
gated_continuation = False
|
||||
continue
|
||||
|
||||
assignment = RUNTIME_ASSIGNMENT.match(line)
|
||||
if assignment is not None:
|
||||
runtime_variables.add(assignment.group(1))
|
||||
primitive_violation = DANGEROUS_PRIMITIVE in line and not is_choke_point(path)
|
||||
line_is_direct = is_direct_line(path, line, runtime_variables) or executes_runtime_variable(
|
||||
path, line, runtime_variables
|
||||
)
|
||||
line_is_gated = line_number in validated_typescript_wrappers or is_gated_line(path, line)
|
||||
|
||||
if primitive_violation:
|
||||
sites.append(
|
||||
LaunchSite(path, line_number, physical_line.rstrip(), "dangerous-primitive")
|
||||
)
|
||||
elif line_is_direct and not gated_continuation:
|
||||
# Direct syntax always wins over a same-line marker. Only the command
|
||||
# continuation of a previously validated wrapper may contain the raw
|
||||
# runtime binary itself.
|
||||
sites.append(LaunchSite(path, line_number, physical_line.rstrip(), "direct"))
|
||||
elif line_is_gated:
|
||||
sites.append(LaunchSite(path, line_number, physical_line.rstrip(), "gated"))
|
||||
|
||||
gated = line_is_gated or gated_continuation
|
||||
gated_continuation = gated and line.rstrip().endswith("\\")
|
||||
return sites
|
||||
|
||||
|
||||
def scan_text(path: Path, source: str) -> list[LaunchSite]:
|
||||
return [site for site in classify_text(path, source) if site.classification != "gated"]
|
||||
|
||||
|
||||
def source_files(root: Path):
|
||||
for relative_root in SCANNED_ROOTS:
|
||||
search_root = root / relative_root
|
||||
if not search_root.is_dir():
|
||||
continue
|
||||
for path in search_root.rglob("*"):
|
||||
if not path.is_file() or path.suffix.lower() not in SCANNED_SUFFIXES:
|
||||
continue
|
||||
if any(part in SKIPPED_DIRECTORIES for part in path.parts):
|
||||
continue
|
||||
if is_test_path(path):
|
||||
continue
|
||||
yield path
|
||||
|
||||
|
||||
def scan_repository(root: Path) -> list[LaunchSite]:
|
||||
violations: list[LaunchSite] = []
|
||||
for path in source_files(root):
|
||||
try:
|
||||
source = path.read_text(encoding="utf-8")
|
||||
except UnicodeDecodeError:
|
||||
violations.append(LaunchSite(path, 0, "non-UTF-8 source", "unscannable"))
|
||||
continue
|
||||
violations.extend(scan_text(path.relative_to(root), source))
|
||||
return violations
|
||||
|
||||
|
||||
def inventory_repository(root: Path) -> list[LaunchSite]:
|
||||
inventory: list[LaunchSite] = []
|
||||
for path in source_files(root):
|
||||
try:
|
||||
source = path.read_text(encoding="utf-8")
|
||||
except UnicodeDecodeError:
|
||||
inventory.append(LaunchSite(path.relative_to(root), 0, "non-UTF-8 source", "unscannable"))
|
||||
continue
|
||||
relative_path = path.relative_to(root)
|
||||
inventory.extend(classify_text(relative_path, source))
|
||||
return inventory
|
||||
|
||||
|
||||
def format_violation(violation: LaunchSite) -> str:
|
||||
return f"{violation.path}:{violation.line_number}: {violation.classification}: {violation.line.strip()}"
|
||||
|
||||
|
||||
def main(argv: Sequence[str] | None = None) -> int:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--root", type=Path, default=Path.cwd())
|
||||
parser.add_argument("--json", action="store_true")
|
||||
arguments = parser.parse_args(argv)
|
||||
root = arguments.root.resolve()
|
||||
inventory = inventory_repository(root)
|
||||
violations = [site for site in inventory if site.classification != "gated"]
|
||||
|
||||
if arguments.json:
|
||||
print(
|
||||
json.dumps(
|
||||
{
|
||||
"gated": sum(site.classification == "gated" for site in inventory),
|
||||
"total": len(inventory),
|
||||
"sites": [
|
||||
{
|
||||
"path": str(site.path),
|
||||
"line": site.line_number,
|
||||
"classification": site.classification,
|
||||
"source": site.line.strip(),
|
||||
}
|
||||
for site in inventory
|
||||
],
|
||||
},
|
||||
sort_keys=True,
|
||||
)
|
||||
)
|
||||
else:
|
||||
for site in inventory:
|
||||
print(format_violation(site))
|
||||
print(
|
||||
f"runtime launch inventory: "
|
||||
f"{len(inventory) - len(violations)} gated/{len(inventory)} total"
|
||||
)
|
||||
|
||||
if violations:
|
||||
print("ungated consequential runtime launch detected", file=sys.stderr)
|
||||
return 1
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
704
packages/mosaic/framework/tools/lease-broker/daemon.py
Normal file
704
packages/mosaic/framework/tools/lease-broker/daemon.py
Normal file
@@ -0,0 +1,704 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Mosaic external lease broker for Linux SO_PEERCRED authenticated clients."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import copy
|
||||
import errno
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
import json
|
||||
import os
|
||||
import secrets
|
||||
import signal
|
||||
import socket
|
||||
import stat
|
||||
import struct
|
||||
import sys
|
||||
import threading
|
||||
import time
|
||||
from pathlib import Path
|
||||
from typing import Final
|
||||
|
||||
MAX_FRAME: Final = 64 * 1024
|
||||
MAX_STATE: Final = 4 * 1024 * 1024
|
||||
MAX_PENDING_TOKENS: Final = 256
|
||||
MAX_IN_FLIGHT_CONNECTIONS: Final = 16
|
||||
MAX_LEASE_TTL_SECONDS: Final = 300
|
||||
STATE_VERSION: Final = 1
|
||||
READ_DEADLINE_SECONDS: Final = 1.0
|
||||
HANDLE_QUEUE_TIMEOUT_SECONDS: Final = 1.0
|
||||
SEND_TIMEOUT_SECONDS: Final = 1.0
|
||||
HEX_256_LENGTH: Final = 64
|
||||
LEASE_UNVERIFIED: Final = "UNVERIFIED"
|
||||
LEASE_PENDING: Final = "PENDING_VERIFICATION"
|
||||
LEASE_PENDING_PROMOTION: Final = "PENDING_PROMOTION"
|
||||
LEASE_VERIFIED: Final = "VERIFIED"
|
||||
READ_ONLY_TOOLS: Final = {
|
||||
"claude": frozenset({"Read", "Grep", "Glob", "Ls", "Find"}),
|
||||
"pi": frozenset({"read", "grep", "find", "ls"}),
|
||||
}
|
||||
RECOVERY_TOOL: Final = "mosaic_context_recover"
|
||||
|
||||
|
||||
class BrokerFailure(Exception):
|
||||
def __init__(self, code: str) -> None:
|
||||
super().__init__(code)
|
||||
self.code = code
|
||||
|
||||
|
||||
class StateCommitUncertain(RuntimeError):
|
||||
def __init__(self) -> None:
|
||||
super().__init__("STATE_COMMIT_UNCERTAIN")
|
||||
|
||||
|
||||
def is_non_negative_integer(value: object) -> bool:
|
||||
return type(value) is int and value >= 0
|
||||
|
||||
|
||||
def is_hex_256(value: object) -> bool:
|
||||
return (
|
||||
isinstance(value, str)
|
||||
and len(value) == HEX_256_LENGTH
|
||||
and all(character in "0123456789abcdef" for character in value)
|
||||
)
|
||||
|
||||
|
||||
def is_positive_decimal(value: object) -> bool:
|
||||
return (
|
||||
isinstance(value, str)
|
||||
and len(value) > 0
|
||||
and value[0] in "123456789"
|
||||
and all(character in "0123456789" for character in value)
|
||||
)
|
||||
|
||||
|
||||
def valid_binding(binding: object) -> bool:
|
||||
if not isinstance(binding, dict):
|
||||
return False
|
||||
required = {"compaction_epoch", "request_epoch", "h_source", "h_payload", "schema_version"}
|
||||
if set(binding) != required:
|
||||
return False
|
||||
if not all(
|
||||
is_non_negative_integer(binding[field])
|
||||
for field in ("compaction_epoch", "request_epoch", "schema_version")
|
||||
):
|
||||
return False
|
||||
return all(
|
||||
is_hex_256(binding[field])
|
||||
for field in ("h_source", "h_payload")
|
||||
)
|
||||
|
||||
|
||||
def validate_state(value: object) -> dict[str, object]:
|
||||
if not isinstance(value, dict) or set(value) != {"version", "sessions", "tokens"}:
|
||||
raise BrokerFailure("STATE_INTEGRITY")
|
||||
if type(value["version"]) is not int or value["version"] != STATE_VERSION:
|
||||
raise BrokerFailure("STATE_INTEGRITY")
|
||||
sessions = value["sessions"]
|
||||
tokens = value["tokens"]
|
||||
if not isinstance(sessions, dict) or not isinstance(tokens, dict):
|
||||
raise BrokerFailure("STATE_INTEGRITY")
|
||||
if len(tokens) > MAX_PENDING_TOKENS:
|
||||
raise BrokerFailure("STATE_INTEGRITY")
|
||||
|
||||
anchors: set[tuple[int, str]] = set()
|
||||
for session_id, session in sessions.items():
|
||||
if not is_hex_256(session_id) or not isinstance(session, dict):
|
||||
raise BrokerFailure("STATE_INTEGRITY")
|
||||
if set(session) != {"anchor_pid", "anchor_starttime", "runtime_generation"}:
|
||||
raise BrokerFailure("STATE_INTEGRITY")
|
||||
anchor_pid = session["anchor_pid"]
|
||||
anchor_starttime = session["anchor_starttime"]
|
||||
if type(anchor_pid) is not int or anchor_pid <= 0:
|
||||
raise BrokerFailure("STATE_INTEGRITY")
|
||||
if not is_positive_decimal(anchor_starttime):
|
||||
raise BrokerFailure("STATE_INTEGRITY")
|
||||
if not is_non_negative_integer(session["runtime_generation"]):
|
||||
raise BrokerFailure("STATE_INTEGRITY")
|
||||
anchor = (anchor_pid, anchor_starttime)
|
||||
if anchor in anchors:
|
||||
raise BrokerFailure("STATE_INTEGRITY")
|
||||
anchors.add(anchor)
|
||||
|
||||
for token_value, token in tokens.items():
|
||||
if not is_hex_256(token_value) or not isinstance(token, dict):
|
||||
raise BrokerFailure("STATE_INTEGRITY")
|
||||
if set(token) != {"session_id", "runtime_generation", "binding", "consumed"}:
|
||||
raise BrokerFailure("STATE_INTEGRITY")
|
||||
session_id = token["session_id"]
|
||||
generation = token["runtime_generation"]
|
||||
session = sessions.get(session_id) if isinstance(session_id, str) else None
|
||||
if not is_hex_256(session_id) or not isinstance(session, dict):
|
||||
raise BrokerFailure("STATE_INTEGRITY")
|
||||
if not is_non_negative_integer(generation):
|
||||
raise BrokerFailure("STATE_INTEGRITY")
|
||||
if generation != session["runtime_generation"]:
|
||||
raise BrokerFailure("STATE_INTEGRITY")
|
||||
if not valid_binding(token["binding"]) or token["consumed"] is not False:
|
||||
raise BrokerFailure("STATE_INTEGRITY")
|
||||
return value
|
||||
|
||||
|
||||
def proc_node(pid: int) -> dict[str, int | str]:
|
||||
try:
|
||||
text = Path(f"/proc/{pid}/stat").read_text(encoding="utf-8")
|
||||
except (FileNotFoundError, PermissionError, ProcessLookupError) as exc:
|
||||
raise BrokerFailure("PID_UNAVAILABLE") from exc
|
||||
close = text.rfind(")")
|
||||
fields = text[close + 2 :].split()
|
||||
if close < 0 or len(fields) < 20:
|
||||
raise BrokerFailure("PROC_STAT_INVALID")
|
||||
return {"pid": pid, "ppid": int(fields[1]), "starttime": fields[19]}
|
||||
|
||||
|
||||
def verified_ancestry(peer_pid: int, anchor_pid: int, anchor_starttime: str) -> bool:
|
||||
chain: list[dict[str, int | str]] = []
|
||||
seen: set[int] = set()
|
||||
current = peer_pid
|
||||
while current > 0 and current not in seen:
|
||||
seen.add(current)
|
||||
node = proc_node(current)
|
||||
chain.append(node)
|
||||
if current == anchor_pid:
|
||||
if node["starttime"] != anchor_starttime:
|
||||
return False
|
||||
break
|
||||
current = int(node["ppid"])
|
||||
else:
|
||||
return False
|
||||
if int(chain[-1]["pid"]) != anchor_pid:
|
||||
return False
|
||||
for original in chain:
|
||||
repeated = proc_node(int(original["pid"]))
|
||||
if repeated["starttime"] != original["starttime"]:
|
||||
raise BrokerFailure("PID_STARTTIME_RACE")
|
||||
return True
|
||||
|
||||
|
||||
def secure_parent(path: Path) -> None:
|
||||
parent = path.parent
|
||||
if not parent.is_dir() or stat.S_IMODE(parent.stat().st_mode) != 0o700:
|
||||
raise BrokerFailure("INSECURE_PARENT_MODE")
|
||||
|
||||
|
||||
class StateStore:
|
||||
def __init__(self, path: Path) -> None:
|
||||
self.path = path
|
||||
secure_parent(path)
|
||||
self.poisoned = False
|
||||
self.value: dict[str, object] = {"version": STATE_VERSION, "sessions": {}, "tokens": {}}
|
||||
flags = os.O_RDONLY | getattr(os, "O_CLOEXEC", 0) | getattr(os, "O_NOFOLLOW", 0)
|
||||
try:
|
||||
descriptor = os.open(path, flags)
|
||||
except FileNotFoundError:
|
||||
return
|
||||
except OSError as exc:
|
||||
raise BrokerFailure("STATE_INTEGRITY") from exc
|
||||
try:
|
||||
metadata = os.fstat(descriptor)
|
||||
if not stat.S_ISREG(metadata.st_mode):
|
||||
raise BrokerFailure("STATE_INTEGRITY")
|
||||
if stat.S_IMODE(metadata.st_mode) != 0o600:
|
||||
raise BrokerFailure("INSECURE_STATE_MODE")
|
||||
if metadata.st_size > MAX_STATE:
|
||||
raise BrokerFailure("STATE_INTEGRITY")
|
||||
chunks = bytearray()
|
||||
while len(chunks) <= MAX_STATE:
|
||||
chunk = os.read(descriptor, min(64 * 1024, MAX_STATE + 1 - len(chunks)))
|
||||
if not chunk:
|
||||
break
|
||||
chunks.extend(chunk)
|
||||
if len(chunks) > MAX_STATE:
|
||||
raise BrokerFailure("STATE_INTEGRITY")
|
||||
try:
|
||||
loaded = json.loads(chunks)
|
||||
except (json.JSONDecodeError, UnicodeDecodeError) as exc:
|
||||
raise BrokerFailure("STATE_INTEGRITY") from exc
|
||||
self.value = validate_state(loaded)
|
||||
except OSError as exc:
|
||||
raise BrokerFailure("STATE_INTEGRITY") from exc
|
||||
finally:
|
||||
os.close(descriptor)
|
||||
|
||||
def sessions(self) -> dict[str, dict[str, object]]:
|
||||
sessions = self.value.get("sessions")
|
||||
if not isinstance(sessions, dict):
|
||||
raise BrokerFailure("STATE_INTEGRITY")
|
||||
return sessions
|
||||
|
||||
def tokens(self) -> dict[str, dict[str, object]]:
|
||||
tokens = self.value.get("tokens")
|
||||
if not isinstance(tokens, dict):
|
||||
raise BrokerFailure("STATE_INTEGRITY")
|
||||
return tokens
|
||||
|
||||
def commit(self) -> None:
|
||||
if self.poisoned:
|
||||
raise StateCommitUncertain()
|
||||
payload = (
|
||||
json.dumps(self.value, sort_keys=True, separators=(",", ":")) + "\n"
|
||||
).encode()
|
||||
if len(payload) > MAX_STATE:
|
||||
raise BrokerFailure("STATE_TOO_LARGE")
|
||||
temporary = self.path.with_name(f".{self.path.name}.{os.getpid()}.tmp")
|
||||
descriptor = os.open(temporary, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600)
|
||||
replaced = False
|
||||
try:
|
||||
try:
|
||||
remaining = memoryview(payload)
|
||||
while remaining:
|
||||
written = os.write(descriptor, remaining)
|
||||
if written == 0:
|
||||
raise OSError(errno.EIO, "state write made no progress")
|
||||
remaining = remaining[written:]
|
||||
os.fsync(descriptor)
|
||||
finally:
|
||||
os.close(descriptor)
|
||||
os.replace(temporary, self.path)
|
||||
replaced = True
|
||||
try:
|
||||
directory = os.open(self.path.parent, os.O_RDONLY | os.O_DIRECTORY)
|
||||
try:
|
||||
os.fsync(directory)
|
||||
finally:
|
||||
os.close(directory)
|
||||
except OSError as exc:
|
||||
self.poisoned = True
|
||||
raise StateCommitUncertain() from exc
|
||||
finally:
|
||||
if not replaced:
|
||||
try:
|
||||
temporary.unlink()
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
|
||||
|
||||
class Broker:
|
||||
def __init__(self, store: StateStore) -> None:
|
||||
self.store = store
|
||||
# VERIFIED authority is deliberately volatile: broker restart revokes all
|
||||
# leases while preserving WI-1 identity and pending-token integrity.
|
||||
self.leases: dict[str, dict[str, object]] = {}
|
||||
|
||||
def authenticate(self, peer_pid: int, request: dict[str, object]) -> tuple[str, dict[str, object]]:
|
||||
session_id = request.get("session_id")
|
||||
generation = request.get("runtime_generation")
|
||||
if not isinstance(session_id, str) or not is_non_negative_integer(generation):
|
||||
raise BrokerFailure("INVALID_IDENTITY")
|
||||
session = self.store.sessions().get(session_id)
|
||||
if not isinstance(session, dict):
|
||||
raise BrokerFailure("UNKNOWN_SESSION")
|
||||
anchor_pid = session.get("anchor_pid")
|
||||
anchor_starttime = session.get("anchor_starttime")
|
||||
current_generation = session.get("runtime_generation")
|
||||
if (
|
||||
type(anchor_pid) is not int
|
||||
or not isinstance(anchor_starttime, str)
|
||||
or not is_non_negative_integer(current_generation)
|
||||
):
|
||||
raise BrokerFailure("STATE_INTEGRITY")
|
||||
if not verified_ancestry(peer_pid, anchor_pid, anchor_starttime):
|
||||
raise BrokerFailure("ANCESTRY_MISMATCH")
|
||||
if generation < current_generation:
|
||||
raise BrokerFailure("STALE_GENERATION")
|
||||
if generation > current_generation:
|
||||
session["runtime_generation"] = generation
|
||||
self.revoke_session_authority(session_id)
|
||||
return session_id, session
|
||||
|
||||
def session_for_anchor(
|
||||
self, anchor_pid: int, anchor_starttime: str
|
||||
) -> tuple[str, dict[str, object]] | None:
|
||||
for session_id, session in self.store.sessions().items():
|
||||
if (
|
||||
session["anchor_pid"] == anchor_pid
|
||||
and session["anchor_starttime"] == anchor_starttime
|
||||
):
|
||||
return session_id, session
|
||||
return None
|
||||
|
||||
def revoke_session_tokens(self, session_id: str) -> None:
|
||||
tokens = self.store.tokens()
|
||||
for token_value in [
|
||||
value for value, token in tokens.items() if token["session_id"] == session_id
|
||||
]:
|
||||
del tokens[token_value]
|
||||
|
||||
def revoke_session_authority(self, session_id: str) -> None:
|
||||
self.revoke_session_tokens(session_id)
|
||||
session = self.store.sessions().get(session_id)
|
||||
generation = session.get("runtime_generation") if isinstance(session, dict) else None
|
||||
self.leases[session_id] = {
|
||||
"state": LEASE_UNVERIFIED,
|
||||
"runtime_generation": generation,
|
||||
}
|
||||
|
||||
def mint_token(
|
||||
self,
|
||||
session_id: str,
|
||||
generation: int,
|
||||
binding: dict[str, object],
|
||||
) -> str:
|
||||
if len(self.store.tokens()) >= MAX_PENDING_TOKENS:
|
||||
raise BrokerFailure("TOKEN_CAPACITY")
|
||||
token = secrets.token_hex(32)
|
||||
self.store.tokens()[token] = {
|
||||
"session_id": session_id,
|
||||
"runtime_generation": generation,
|
||||
"binding": copy.deepcopy(binding),
|
||||
"consumed": False,
|
||||
}
|
||||
return token
|
||||
|
||||
def finish_promotion(self, session_id: str) -> None:
|
||||
lease = self.leases.get(session_id)
|
||||
if not isinstance(lease, dict) or lease.get("state") != LEASE_PENDING_PROMOTION:
|
||||
raise BrokerFailure("STATE_INTEGRITY")
|
||||
lease["state"] = LEASE_VERIFIED
|
||||
|
||||
def handle(self, peer: tuple[int, int, int], request: dict[str, object]) -> dict[str, object]:
|
||||
if self.store.poisoned:
|
||||
raise StateCommitUncertain()
|
||||
previous = copy.deepcopy(self.store.value)
|
||||
previous_leases = copy.deepcopy(self.leases)
|
||||
try:
|
||||
response = self._handle(peer, request)
|
||||
if self.store.value != previous:
|
||||
self.store.commit()
|
||||
if request.get("action") == "promote_lease":
|
||||
session_id = request.get("session_id")
|
||||
if not isinstance(session_id, str):
|
||||
raise BrokerFailure("INVALID_IDENTITY")
|
||||
self.finish_promotion(session_id)
|
||||
response["state"] = LEASE_VERIFIED
|
||||
return response
|
||||
except StateCommitUncertain:
|
||||
raise
|
||||
except Exception:
|
||||
self.store.value = previous
|
||||
self.leases = previous_leases
|
||||
raise
|
||||
|
||||
def _handle(self, peer: tuple[int, int, int], request: dict[str, object]) -> dict[str, object]:
|
||||
peer_pid, peer_uid, peer_gid = peer
|
||||
action = request.get("action")
|
||||
if action == "register_anchor":
|
||||
if "session_id" in request:
|
||||
raise BrokerFailure("CALLER_SESSION_ID_REFUSED")
|
||||
generation = request.get("runtime_generation")
|
||||
if not is_non_negative_integer(generation):
|
||||
raise BrokerFailure("INVALID_GENERATION")
|
||||
anchor = proc_node(peer_pid)
|
||||
anchor_starttime = str(anchor["starttime"])
|
||||
existing = self.session_for_anchor(peer_pid, anchor_starttime)
|
||||
if existing is None:
|
||||
session_id = secrets.token_hex(32)
|
||||
self.store.sessions()[session_id] = {
|
||||
"anchor_pid": peer_pid,
|
||||
"anchor_starttime": anchor_starttime,
|
||||
"runtime_generation": generation,
|
||||
}
|
||||
else:
|
||||
session_id, session = existing
|
||||
current_generation = session["runtime_generation"]
|
||||
if generation < current_generation:
|
||||
raise BrokerFailure("STALE_GENERATION")
|
||||
if generation > current_generation:
|
||||
session["runtime_generation"] = generation
|
||||
self.revoke_session_authority(session_id)
|
||||
return {"ok": True, "session_id": session_id, "peer": {"pid": peer_pid, "uid": peer_uid, "gid": peer_gid, "starttime": anchor["starttime"]}}
|
||||
if action == "authenticate":
|
||||
self.authenticate(peer_pid, request)
|
||||
return {"ok": True}
|
||||
if action == "mint_token":
|
||||
session_id, _ = self.authenticate(peer_pid, request)
|
||||
binding = request.get("binding")
|
||||
if not valid_binding(binding):
|
||||
raise BrokerFailure("INVALID_BINDING")
|
||||
token = self.mint_token(session_id, request["runtime_generation"], binding)
|
||||
return {"ok": True, "token": token}
|
||||
if action == "consume_token":
|
||||
session_id, _ = self.authenticate(peer_pid, request)
|
||||
token_value = request.get("token")
|
||||
token = self.store.tokens().get(token_value) if isinstance(token_value, str) else None
|
||||
if not isinstance(token, dict) or token.get("session_id") != session_id or token.get("runtime_generation") != request.get("runtime_generation") or token.get("consumed") is not False:
|
||||
raise BrokerFailure("TOKEN_REPLAY")
|
||||
del self.store.tokens()[token_value]
|
||||
return {"ok": True}
|
||||
if action == "begin_verification":
|
||||
session_id, _ = self.authenticate(peer_pid, request)
|
||||
runtime = request.get("runtime")
|
||||
binding = request.get("binding")
|
||||
ttl_seconds = request.get("ttl_seconds", MAX_LEASE_TTL_SECONDS)
|
||||
if runtime not in READ_ONLY_TOOLS:
|
||||
raise BrokerFailure("INVALID_RUNTIME")
|
||||
if not valid_binding(binding):
|
||||
raise BrokerFailure("INVALID_BINDING")
|
||||
if (
|
||||
type(ttl_seconds) is not int
|
||||
or ttl_seconds <= 0
|
||||
or ttl_seconds > MAX_LEASE_TTL_SECONDS
|
||||
):
|
||||
raise BrokerFailure("INVALID_LEASE_TTL")
|
||||
# Revoke-first is a broker operation, not advisory adapter order.
|
||||
self.revoke_session_authority(session_id)
|
||||
token = self.mint_token(session_id, request["runtime_generation"], binding)
|
||||
self.leases[session_id] = {
|
||||
"state": LEASE_PENDING,
|
||||
"runtime": runtime,
|
||||
"runtime_generation": request["runtime_generation"],
|
||||
"binding": copy.deepcopy(binding),
|
||||
"promotion_token": token,
|
||||
"ttl_seconds": ttl_seconds,
|
||||
}
|
||||
return {
|
||||
"ok": True,
|
||||
"state": LEASE_PENDING,
|
||||
"promotion_token": token,
|
||||
}
|
||||
if action == "promote_lease":
|
||||
session_id, _ = self.authenticate(peer_pid, request)
|
||||
promotion_token = request.get("promotion_token")
|
||||
lease = self.leases.get(session_id)
|
||||
if not isinstance(lease, dict) or lease.get("state") != LEASE_PENDING:
|
||||
raise BrokerFailure("INVALID_LEASE_TRANSITION")
|
||||
expected_token = lease.get("promotion_token")
|
||||
if (
|
||||
not isinstance(promotion_token, str)
|
||||
or not isinstance(expected_token, str)
|
||||
or not secrets.compare_digest(promotion_token, expected_token)
|
||||
):
|
||||
raise BrokerFailure("PROMOTION_TOKEN_MISMATCH")
|
||||
token = self.store.tokens().get(promotion_token)
|
||||
if (
|
||||
not isinstance(token, dict)
|
||||
or token.get("session_id") != session_id
|
||||
or token.get("runtime_generation") != request.get("runtime_generation")
|
||||
or token.get("binding") != lease.get("binding")
|
||||
or token.get("consumed") is not False
|
||||
):
|
||||
raise BrokerFailure("PROMOTION_TOKEN_INVALID")
|
||||
del self.store.tokens()[promotion_token]
|
||||
lease["state"] = LEASE_PENDING_PROMOTION
|
||||
lease["expires_at"] = time.monotonic() + int(lease["ttl_seconds"])
|
||||
# handle() commits token consumption before finish_promotion() makes
|
||||
# VERIFIED externally visible: promote-last by construction.
|
||||
return {"ok": True, "state": LEASE_PENDING_PROMOTION}
|
||||
if action == "revoke_lease":
|
||||
session_id, _ = self.authenticate(peer_pid, request)
|
||||
self.revoke_session_authority(session_id)
|
||||
return {"ok": True, "state": LEASE_UNVERIFIED}
|
||||
if action == "authorize_tool":
|
||||
session_id, _ = self.authenticate(peer_pid, request)
|
||||
runtime = request.get("runtime")
|
||||
tool_name = request.get("tool_name")
|
||||
if runtime not in READ_ONLY_TOOLS:
|
||||
raise BrokerFailure("INVALID_RUNTIME")
|
||||
if not isinstance(tool_name, str) or not tool_name or len(tool_name) > 256:
|
||||
raise BrokerFailure("INVALID_TOOL")
|
||||
lease = self.leases.get(session_id)
|
||||
state = lease.get("state") if isinstance(lease, dict) else LEASE_UNVERIFIED
|
||||
if tool_name in READ_ONLY_TOOLS[runtime] or tool_name == RECOVERY_TOOL:
|
||||
return {"ok": True, "decision": "allow", "state": state}
|
||||
if not isinstance(lease, dict) or lease.get("state") != LEASE_VERIFIED:
|
||||
return {
|
||||
"ok": False,
|
||||
"code": "MUTATOR_UNVERIFIED",
|
||||
"decision": "deny",
|
||||
"state": LEASE_UNVERIFIED,
|
||||
}
|
||||
if (
|
||||
lease.get("runtime") != runtime
|
||||
or lease.get("runtime_generation") != request.get("runtime_generation")
|
||||
):
|
||||
return {
|
||||
"ok": False,
|
||||
"code": "MUTATOR_UNVERIFIED",
|
||||
"decision": "deny",
|
||||
"state": LEASE_UNVERIFIED,
|
||||
}
|
||||
expires_at = lease.get("expires_at")
|
||||
if not isinstance(expires_at, (int, float)) or time.monotonic() >= expires_at:
|
||||
self.revoke_session_authority(session_id)
|
||||
return {
|
||||
"ok": False,
|
||||
"code": "LEASE_EXPIRED",
|
||||
"decision": "deny",
|
||||
"state": LEASE_UNVERIFIED,
|
||||
}
|
||||
return {
|
||||
"ok": True,
|
||||
"decision": "allow",
|
||||
"state": LEASE_VERIFIED,
|
||||
}
|
||||
raise BrokerFailure("UNKNOWN_ACTION")
|
||||
|
||||
|
||||
def read_frame(connection: socket.socket, deadline: float) -> dict[str, object]:
|
||||
data = bytearray()
|
||||
while len(data) <= MAX_FRAME:
|
||||
remaining = deadline - time.monotonic()
|
||||
if remaining <= 0:
|
||||
raise BrokerFailure("MALFORMED_REQUEST")
|
||||
connection.settimeout(remaining)
|
||||
chunk = connection.recv(min(4096, MAX_FRAME + 1 - len(data)))
|
||||
if not chunk:
|
||||
break
|
||||
data.extend(chunk)
|
||||
if len(data) > MAX_FRAME:
|
||||
while True:
|
||||
remaining = deadline - time.monotonic()
|
||||
if remaining <= 0:
|
||||
raise BrokerFailure("MALFORMED_REQUEST")
|
||||
connection.settimeout(remaining)
|
||||
if not connection.recv(4096):
|
||||
break
|
||||
raise BrokerFailure("MALFORMED_REQUEST")
|
||||
if not data.endswith(b"\n") or data.count(b"\n") != 1:
|
||||
raise BrokerFailure("MALFORMED_REQUEST")
|
||||
try:
|
||||
value = json.loads(data)
|
||||
except (json.JSONDecodeError, UnicodeDecodeError) as exc:
|
||||
raise BrokerFailure("MALFORMED_REQUEST") from exc
|
||||
if not isinstance(value, dict):
|
||||
raise BrokerFailure("MALFORMED_REQUEST")
|
||||
return value
|
||||
|
||||
|
||||
def handle_connection(
|
||||
connection: socket.socket,
|
||||
broker: Broker,
|
||||
broker_lock: threading.Lock,
|
||||
) -> None:
|
||||
with connection:
|
||||
read_deadline = time.monotonic() + READ_DEADLINE_SECONDS
|
||||
try:
|
||||
raw = connection.getsockopt(socket.SOL_SOCKET, socket.SO_PEERCRED, 12)
|
||||
peer = struct.unpack("3i", raw)
|
||||
request = read_frame(connection, read_deadline)
|
||||
except BrokerFailure as exc:
|
||||
reply = {"ok": False, "code": exc.code}
|
||||
except OSError:
|
||||
return
|
||||
else:
|
||||
# Queueing is bounded and fails closed before broker.handle mutates
|
||||
# state. Once handling starts it must finish atomically; its reply
|
||||
# then receives an independent send budget so a slow fsync cannot
|
||||
# consume the write opportunity and create client/broker ambiguity.
|
||||
acquired = broker_lock.acquire(timeout=HANDLE_QUEUE_TIMEOUT_SECONDS)
|
||||
if not acquired:
|
||||
reply = {"ok": False, "code": "BROKER_BUSY"}
|
||||
else:
|
||||
try:
|
||||
try:
|
||||
reply = broker.handle(peer, request)
|
||||
except BrokerFailure as exc:
|
||||
reply = {"ok": False, "code": exc.code}
|
||||
finally:
|
||||
broker_lock.release()
|
||||
try:
|
||||
connection.settimeout(SEND_TIMEOUT_SECONDS)
|
||||
connection.sendall((json.dumps(reply, separators=(",", ":")) + "\n").encode())
|
||||
except OSError:
|
||||
return
|
||||
|
||||
|
||||
def serve(socket_path: Path, state_path: Path) -> None:
|
||||
secure_parent(socket_path)
|
||||
if socket_path.exists() or socket_path.is_symlink():
|
||||
raise BrokerFailure("SOCKET_ALREADY_EXISTS")
|
||||
store = StateStore(state_path)
|
||||
broker = Broker(store)
|
||||
broker_lock = threading.Lock()
|
||||
slots = threading.BoundedSemaphore(MAX_IN_FLIGHT_CONNECTIONS)
|
||||
fatal_lock = threading.Lock()
|
||||
fatal_errors: list[Exception] = []
|
||||
executor = ThreadPoolExecutor(
|
||||
max_workers=MAX_IN_FLIGHT_CONNECTIONS,
|
||||
thread_name_prefix="mosaic-lease-broker",
|
||||
)
|
||||
server = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
||||
server.bind(str(socket_path))
|
||||
os.chmod(socket_path, 0o600)
|
||||
owned = (socket_path.stat().st_dev, socket_path.stat().st_ino)
|
||||
stopping = False
|
||||
|
||||
def stop(_signum: int, _frame: object) -> None:
|
||||
nonlocal stopping
|
||||
stopping = True
|
||||
server.close()
|
||||
|
||||
def process_connection(connection: socket.socket) -> None:
|
||||
try:
|
||||
handle_connection(connection, broker, broker_lock)
|
||||
except Exception as exc:
|
||||
with fatal_lock:
|
||||
if not fatal_errors:
|
||||
fatal_errors.append(exc)
|
||||
finally:
|
||||
slots.release()
|
||||
|
||||
def fatal_error() -> Exception | None:
|
||||
with fatal_lock:
|
||||
return fatal_errors[0] if fatal_errors else None
|
||||
|
||||
signal.signal(signal.SIGTERM, stop)
|
||||
signal.signal(signal.SIGINT, stop)
|
||||
server.listen(MAX_IN_FLIGHT_CONNECTIONS)
|
||||
server.settimeout(0.1)
|
||||
print("READY", flush=True)
|
||||
try:
|
||||
while not stopping:
|
||||
failure = fatal_error()
|
||||
if failure is not None:
|
||||
raise failure
|
||||
if not slots.acquire(timeout=0.1):
|
||||
continue
|
||||
try:
|
||||
connection, _ = server.accept()
|
||||
except socket.timeout:
|
||||
slots.release()
|
||||
continue
|
||||
except OSError:
|
||||
slots.release()
|
||||
failure = fatal_error()
|
||||
if failure is not None:
|
||||
raise failure
|
||||
if stopping:
|
||||
break
|
||||
raise
|
||||
try:
|
||||
executor.submit(process_connection, connection)
|
||||
except Exception:
|
||||
slots.release()
|
||||
connection.close()
|
||||
raise
|
||||
finally:
|
||||
server.close()
|
||||
executor.shutdown(wait=True)
|
||||
try:
|
||||
current = socket_path.stat()
|
||||
if (current.st_dev, current.st_ino) == owned:
|
||||
socket_path.unlink()
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--socket", required=True, type=Path)
|
||||
parser.add_argument("--state", required=True, type=Path)
|
||||
arguments = parser.parse_args()
|
||||
serve(arguments.socket, arguments.state)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
main()
|
||||
except BrokerFailure as failure:
|
||||
print(failure.code, file=sys.stderr)
|
||||
raise SystemExit(1)
|
||||
except StateCommitUncertain as failure:
|
||||
print(str(failure), file=sys.stderr)
|
||||
raise SystemExit(1)
|
||||
103
packages/mosaic/framework/tools/lease-broker/launch-runtime.py
Normal file
103
packages/mosaic/framework/tools/lease-broker/launch-runtime.py
Normal file
@@ -0,0 +1,103 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Register a runtime parent with the lease broker, then exec without changing PID."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import socket
|
||||
import sys
|
||||
from collections.abc import Callable, Mapping, Sequence
|
||||
from pathlib import Path
|
||||
from typing import Final
|
||||
|
||||
MAX_FRAME: Final = 64 * 1024
|
||||
BROKER_TIMEOUT_SECONDS: Final = 1.5
|
||||
CLAUDE_DANGEROUS_FLAG: Final = "--dangerously-skip-permissions"
|
||||
|
||||
|
||||
def broker_request(socket_path: Path, request: dict[str, object]) -> dict[str, object]:
|
||||
payload = (json.dumps(request, separators=(",", ":")) + "\n").encode()
|
||||
response = bytearray()
|
||||
with socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) as connection:
|
||||
connection.settimeout(BROKER_TIMEOUT_SECONDS)
|
||||
connection.connect(str(socket_path))
|
||||
connection.sendall(payload)
|
||||
connection.shutdown(socket.SHUT_WR)
|
||||
while len(response) <= MAX_FRAME:
|
||||
chunk = connection.recv(min(4096, MAX_FRAME + 1 - len(response)))
|
||||
if not chunk:
|
||||
break
|
||||
response.extend(chunk)
|
||||
if len(response) > MAX_FRAME or not response.endswith(b"\n"):
|
||||
raise ValueError("invalid broker reply")
|
||||
value = json.loads(response)
|
||||
if not isinstance(value, dict):
|
||||
raise ValueError("invalid broker reply")
|
||||
return value
|
||||
|
||||
|
||||
def main(
|
||||
argv: Sequence[str] | None = None,
|
||||
*,
|
||||
environ: Mapping[str, str] | None = None,
|
||||
request: Callable[[Path, dict[str, object]], dict[str, object]] = broker_request,
|
||||
execute: Callable[[str, list[str], dict[str, str]], object] = os.execvpe,
|
||||
) -> int:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--runtime", required=True, choices=("claude", "pi"))
|
||||
parser.add_argument("--dangerous", action="store_true")
|
||||
parser.add_argument("command", nargs=argparse.REMAINDER)
|
||||
arguments = parser.parse_args(argv)
|
||||
command = arguments.command
|
||||
if command and command[0] == "--":
|
||||
command = command[1:]
|
||||
if not command:
|
||||
print("lease-gated runtime command is required", file=sys.stderr)
|
||||
return 64
|
||||
if arguments.dangerous:
|
||||
if arguments.runtime != "claude" or Path(command[0]).name != "claude":
|
||||
print("dangerous mode is supported only for the Claude runtime", file=sys.stderr)
|
||||
return 64
|
||||
command = [command[0], CLAUDE_DANGEROUS_FLAG, *command[1:]]
|
||||
|
||||
source_environment = os.environ if environ is None else environ
|
||||
try:
|
||||
socket_path = Path(source_environment["MOSAIC_LEASE_BROKER_SOCKET"])
|
||||
generation = int(source_environment.get("MOSAIC_RUNTIME_GENERATION", "1"))
|
||||
if generation < 0:
|
||||
raise ValueError("invalid generation")
|
||||
reply = request(
|
||||
socket_path,
|
||||
{
|
||||
"action": "register_anchor",
|
||||
"runtime_generation": generation,
|
||||
},
|
||||
)
|
||||
session_id = reply.get("session_id")
|
||||
if (
|
||||
reply.get("ok") is not True
|
||||
or not isinstance(session_id, str)
|
||||
or len(session_id) != 64
|
||||
or any(character not in "0123456789abcdef" for character in session_id)
|
||||
):
|
||||
raise ValueError("registration refused")
|
||||
except (KeyError, ValueError, OSError, json.JSONDecodeError):
|
||||
print("Mosaic lease broker registration failed; runtime launch denied.", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
environment = dict(source_environment)
|
||||
environment["MOSAIC_LEASE_SESSION_ID"] = session_id
|
||||
environment["MOSAIC_RUNTIME_GENERATION"] = str(generation)
|
||||
environment["MOSAIC_LEASE_RUNTIME"] = arguments.runtime
|
||||
try:
|
||||
execute(command[0], command, environment)
|
||||
except OSError:
|
||||
print("Mosaic lease-gated runtime exec failed.", file=sys.stderr)
|
||||
return 1
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
100
packages/mosaic/framework/tools/lease-broker/mutator-gate.py
Normal file
100
packages/mosaic/framework/tools/lease-broker/mutator-gate.py
Normal file
@@ -0,0 +1,100 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Runtime-neutral whole mutator-class gate backed by the Mosaic lease broker."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import socket
|
||||
import sys
|
||||
from collections.abc import Callable, Mapping, Sequence
|
||||
from pathlib import Path
|
||||
from typing import BinaryIO, Final
|
||||
|
||||
MAX_FRAME: Final = 64 * 1024
|
||||
BROKER_TIMEOUT_SECONDS: Final = 1.5
|
||||
|
||||
|
||||
def deny(code: str) -> int:
|
||||
print(f"BLOCKED: Mosaic mutator gate denied this tool ({code}).", file=sys.stderr)
|
||||
return 2
|
||||
|
||||
|
||||
def read_tool_name(stream: BinaryIO | None = None) -> str:
|
||||
source = sys.stdin.buffer if stream is None else stream
|
||||
raw = source.read(MAX_FRAME + 1)
|
||||
if len(raw) > MAX_FRAME:
|
||||
raise ValueError("INVALID_GATE_INPUT")
|
||||
value = json.loads(raw)
|
||||
if not isinstance(value, dict):
|
||||
raise ValueError("INVALID_GATE_INPUT")
|
||||
tool_name = value.get("tool_name")
|
||||
if not isinstance(tool_name, str) or not tool_name or len(tool_name) > 256:
|
||||
raise ValueError("INVALID_GATE_INPUT")
|
||||
return tool_name
|
||||
|
||||
|
||||
def broker_request(socket_path: Path, request: dict[str, object]) -> dict[str, object]:
|
||||
payload = (json.dumps(request, separators=(",", ":")) + "\n").encode()
|
||||
if len(payload) > MAX_FRAME:
|
||||
raise ValueError("INVALID_GATE_INPUT")
|
||||
response = bytearray()
|
||||
with socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) as connection:
|
||||
connection.settimeout(BROKER_TIMEOUT_SECONDS)
|
||||
connection.connect(str(socket_path))
|
||||
connection.sendall(payload)
|
||||
connection.shutdown(socket.SHUT_WR)
|
||||
while len(response) <= MAX_FRAME:
|
||||
chunk = connection.recv(min(4096, MAX_FRAME + 1 - len(response)))
|
||||
if not chunk:
|
||||
break
|
||||
response.extend(chunk)
|
||||
if len(response) > MAX_FRAME or not response.endswith(b"\n"):
|
||||
raise ValueError("INVALID_BROKER_REPLY")
|
||||
value = json.loads(response)
|
||||
if not isinstance(value, dict):
|
||||
raise ValueError("INVALID_BROKER_REPLY")
|
||||
return value
|
||||
|
||||
|
||||
def main(
|
||||
argv: Sequence[str] | None = None,
|
||||
*,
|
||||
environ: Mapping[str, str] | None = None,
|
||||
stream: BinaryIO | None = None,
|
||||
request: Callable[[Path, dict[str, object]], dict[str, object]] = broker_request,
|
||||
) -> int:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--runtime", required=True, choices=("claude", "pi"))
|
||||
arguments = parser.parse_args(argv)
|
||||
source_environment = os.environ if environ is None else environ
|
||||
|
||||
try:
|
||||
tool_name = read_tool_name(stream)
|
||||
socket_value = source_environment["MOSAIC_LEASE_BROKER_SOCKET"]
|
||||
session_id = source_environment["MOSAIC_LEASE_SESSION_ID"]
|
||||
generation = int(source_environment["MOSAIC_RUNTIME_GENERATION"])
|
||||
if generation < 0:
|
||||
raise ValueError("INVALID_GENERATION")
|
||||
reply = request(
|
||||
Path(socket_value),
|
||||
{
|
||||
"action": "authorize_tool",
|
||||
"session_id": session_id,
|
||||
"runtime_generation": generation,
|
||||
"runtime": arguments.runtime,
|
||||
"tool_name": tool_name,
|
||||
},
|
||||
)
|
||||
except (KeyError, ValueError, OSError, json.JSONDecodeError):
|
||||
return deny("GATE_UNAVAILABLE")
|
||||
|
||||
if reply.get("ok") is True and reply.get("decision") == "allow":
|
||||
return 0
|
||||
code = reply.get("code")
|
||||
return deny(code if isinstance(code, str) else "MUTATOR_UNVERIFIED")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -32,7 +32,7 @@ Claude:
|
||||
```json
|
||||
{
|
||||
"worker": {
|
||||
"command_template": "claude -p \"Execute task {task_id}: {task_title}\""
|
||||
"command_template": "mosaic claude -p \"Execute task {task_id}: {task_title}\""
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
@@ -86,7 +86,8 @@ echo ""
|
||||
|
||||
cd "$PROJECT"
|
||||
if [[ "$RUNTIME_CMD" == "claude" ]]; then
|
||||
exec claude --dangerously-skip-permissions --append-system-prompt "$SYSTEM_PROMPT" "$KICKOFF"
|
||||
exec python3 "$SCRIPT_DIR/../lease-broker/launch-runtime.py" --dangerous --runtime claude -- \
|
||||
claude --append-system-prompt "$SYSTEM_PROMPT" "$KICKOFF"
|
||||
fi
|
||||
|
||||
if [[ "$RUNTIME_CMD" == "codex" ]]; then
|
||||
|
||||
@@ -74,7 +74,8 @@ echo ""
|
||||
|
||||
cd "$PROJECT"
|
||||
if [[ "$RUNTIME_CMD" == "claude" ]]; then
|
||||
exec claude --dangerously-skip-permissions --append-system-prompt "$SYSTEM_PROMPT" "$KICKOFF"
|
||||
exec python3 "$SCRIPT_DIR/../lease-broker/launch-runtime.py" --dangerous --runtime claude -- \
|
||||
claude --append-system-prompt "$SYSTEM_PROMPT" "$KICKOFF"
|
||||
fi
|
||||
|
||||
if [[ "$RUNTIME_CMD" == "codex" ]]; then
|
||||
|
||||
@@ -190,7 +190,7 @@ Pending QA validation
|
||||
This report was created by the QA automation hook.
|
||||
To process this report, run:
|
||||
\`\`\`bash
|
||||
claude -p "Use Task tool to launch universal-qa-agent for report: $REPORT_PATH"
|
||||
python3 ~/.config/mosaic/tools/lease-broker/launch-runtime.py --runtime claude -- claude -p "Use Task tool to launch universal-qa-agent for report: $REPORT_PATH"
|
||||
\`\`\`
|
||||
EOF
|
||||
|
||||
|
||||
@@ -58,8 +58,8 @@ ACTIONS_PATH="$IN_PROGRESS_DIR/$ACTIONS_FILE"
|
||||
|
||||
echo "[$(date '+%Y-%m-%d %H:%M:%S')] Starting remediation: $ACTIONS_PATH" | tee -a "$LOG_FILE"
|
||||
|
||||
# Trigger remediation agent
|
||||
claude -p "Use Task tool to launch auto-remediation-agent for:
|
||||
# Trigger remediation agent through the authenticated lease-broker choke-point.
|
||||
python3 "$(dirname "$0")/../lease-broker/launch-runtime.py" --runtime claude -- claude -p "Use Task tool to launch auto-remediation-agent for:
|
||||
- Remediation Report: $IN_PROGRESS_DIR/$(basename "$REPORT_FILE")
|
||||
- Actions File: $ACTIONS_PATH
|
||||
- Max Iterations: 5
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
"lint": "eslint src",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"test": "vitest run --passWithNoTests && pnpm run test:framework-shell",
|
||||
"test:framework-shell": "bash framework/tools/codex/test-pr-diff-context.sh"
|
||||
"test:framework-shell": "python3 src/lease-broker/daemon_deadline_unittest.py && python3 src/mutator-gate/runtime_tools_unittest.py && python3 src/mutator-gate/runtime_launch_guard_unittest.py && python3 framework/tools/lease-broker/check-runtime-launches.py --root ../.. && bash framework/tools/codex/test-pr-diff-context.sh"
|
||||
},
|
||||
"dependencies": {
|
||||
"@mosaicstack/brain": "workspace:*",
|
||||
|
||||
@@ -1,5 +1,13 @@
|
||||
import { describe, it, expect, vi } from 'vitest';
|
||||
import { mkdtempSync, mkdirSync, symlinkSync, rmSync, lstatSync, writeFileSync } from 'node:fs';
|
||||
import {
|
||||
lstatSync,
|
||||
mkdtempSync,
|
||||
mkdirSync,
|
||||
readFileSync,
|
||||
rmSync,
|
||||
symlinkSync,
|
||||
writeFileSync,
|
||||
} from 'node:fs';
|
||||
import { tmpdir, homedir } from 'node:os';
|
||||
import { join } from 'node:path';
|
||||
import {
|
||||
@@ -14,6 +22,7 @@ import {
|
||||
buildClaudexEnv,
|
||||
buildClaudexBanner,
|
||||
buildClaudexContractNote,
|
||||
ensureClaudexMutatorGateSettings,
|
||||
runClaudexProxyGate,
|
||||
launchClaudex,
|
||||
type ClaudexHarnessAdapter,
|
||||
@@ -36,12 +45,20 @@ function makeReport(overrides: Partial<PreflightReport> = {}): PreflightReport {
|
||||
};
|
||||
}
|
||||
|
||||
function okAdapter(overrides: Partial<ClaudexHarnessAdapter> = {}): ClaudexHarnessAdapter {
|
||||
type TestAdapterOverrides = Partial<ClaudexHarnessAdapter> & {
|
||||
exec?: (cmd: string, args: string[], env: NodeJS.ProcessEnv) => void;
|
||||
};
|
||||
|
||||
function okAdapter(overrides: TestAdapterOverrides = {}): ClaudexHarnessAdapter {
|
||||
const { exec, ...adapterOverrides } = overrides;
|
||||
return {
|
||||
harnessPreflight: () => {},
|
||||
composePrompt: () => '# Composed Claude contract',
|
||||
exec: () => {},
|
||||
...overrides,
|
||||
execLeaseGated:
|
||||
adapterOverrides.execLeaseGated ??
|
||||
((args, env, dangerous) =>
|
||||
exec?.('claude', dangerous ? ['--dangerously-skip-permissions', ...args] : args, env)),
|
||||
...adapterOverrides,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -561,11 +578,51 @@ describe('runClaudexProxyGate', () => {
|
||||
|
||||
// ─── launch orchestration (fail-closed ordering) ──────────────────────────────
|
||||
|
||||
describe('ensureClaudexMutatorGateSettings', () => {
|
||||
it('does not accept a lookalike hook and preserves isolated settings', () => {
|
||||
const root = mkdtempSync(join(tmpdir(), 'claudex-gate-settings-'));
|
||||
try {
|
||||
writeFileSync(
|
||||
join(root, 'settings.json'),
|
||||
JSON.stringify({
|
||||
preserved: true,
|
||||
hooks: {
|
||||
PreToolUse: [
|
||||
{
|
||||
matcher: '.*',
|
||||
hooks: [{ type: 'command', command: 'echo lease-broker/mutator-gate.py' }],
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
ensureClaudexMutatorGateSettings(root);
|
||||
|
||||
const settings = JSON.parse(readFileSync(join(root, 'settings.json'), 'utf8')) as {
|
||||
preserved: boolean;
|
||||
hooks: { PreToolUse: Array<{ hooks: Array<{ command: string }> }> };
|
||||
};
|
||||
const commands = settings.hooks.PreToolUse.flatMap((entry) =>
|
||||
entry.hooks.map((hook) => hook.command),
|
||||
);
|
||||
expect(settings.preserved).toBe(true);
|
||||
expect(commands).toContain(
|
||||
'python3 ~/.config/mosaic/tools/lease-broker/mutator-gate.py --runtime claude',
|
||||
);
|
||||
expect(lstatSync(join(root, 'settings.json')).mode & 0o777).toBe(0o600);
|
||||
} finally {
|
||||
rmSync(root, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('launchClaudex', () => {
|
||||
const baseDeps = {
|
||||
baseEnv: {},
|
||||
proxyGate: () => Promise.resolve({ ok: true, report: makeReport(), problems: [] }),
|
||||
resolveConfigDir: () => '/home/agent/.config/mosaic/claudex/home',
|
||||
prepareConfig: () => {},
|
||||
log: () => {},
|
||||
errorLog: () => {},
|
||||
fail: (() => {
|
||||
|
||||
@@ -27,7 +27,14 @@
|
||||
* unit-testable without spawning Claude Code or touching a real config dir.
|
||||
*/
|
||||
|
||||
import { lstatSync, mkdirSync, realpathSync } from 'node:fs';
|
||||
import {
|
||||
chmodSync,
|
||||
lstatSync,
|
||||
mkdirSync,
|
||||
readFileSync,
|
||||
realpathSync,
|
||||
writeFileSync,
|
||||
} from 'node:fs';
|
||||
import { homedir } from 'node:os';
|
||||
import { dirname, isAbsolute, join, relative, resolve } from 'node:path';
|
||||
import {
|
||||
@@ -278,6 +285,77 @@ export function buildClaudexEnv(
|
||||
return env;
|
||||
}
|
||||
|
||||
const CLAUDEX_MUTATOR_GATE_COMMAND =
|
||||
'python3 ~/.config/mosaic/tools/lease-broker/mutator-gate.py --runtime claude';
|
||||
|
||||
const CLAUDEX_MUTATOR_GATE_HOOK = {
|
||||
matcher: '.*',
|
||||
hooks: [
|
||||
{
|
||||
type: 'command',
|
||||
command: CLAUDEX_MUTATOR_GATE_COMMAND,
|
||||
timeout: 3,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Install the mandatory all-tools gate in Claudex's isolated config without
|
||||
* discarding any existing isolated settings. Malformed settings and symlinked
|
||||
* settings files fail closed rather than launching with uncertain hook state.
|
||||
*/
|
||||
export function ensureClaudexMutatorGateSettings(configDir: string): void {
|
||||
mkdirSync(configDir, { recursive: true, mode: 0o700 });
|
||||
const settingsPath = join(configDir, 'settings.json');
|
||||
let settings: Record<string, unknown> = {};
|
||||
|
||||
try {
|
||||
if (lstatSync(settingsPath).isSymbolicLink()) {
|
||||
throw new Error('claudex: isolated settings.json must not be a symlink (fail closed).');
|
||||
}
|
||||
const parsed: unknown = JSON.parse(readFileSync(settingsPath, 'utf8'));
|
||||
if (!isRecord(parsed)) {
|
||||
throw new Error('claudex: isolated settings.json must contain a JSON object (fail closed).');
|
||||
}
|
||||
settings = parsed;
|
||||
} catch (err) {
|
||||
if ((err as NodeJS.ErrnoException).code !== 'ENOENT') throw err;
|
||||
}
|
||||
|
||||
const hooksValue = settings['hooks'];
|
||||
if (hooksValue !== undefined && !isRecord(hooksValue)) {
|
||||
throw new Error('claudex: isolated settings hooks must be an object (fail closed).');
|
||||
}
|
||||
const hooks = hooksValue ?? {};
|
||||
const preToolUseValue = hooks['PreToolUse'];
|
||||
if (preToolUseValue !== undefined && !Array.isArray(preToolUseValue)) {
|
||||
throw new Error('claudex: isolated PreToolUse hooks must be an array (fail closed).');
|
||||
}
|
||||
const preToolUse = preToolUseValue ?? [];
|
||||
const gatePresent = preToolUse.some(
|
||||
(entry) =>
|
||||
isRecord(entry) &&
|
||||
entry['matcher'] === '.*' &&
|
||||
Array.isArray(entry['hooks']) &&
|
||||
entry['hooks'].some(
|
||||
(hook) =>
|
||||
isRecord(hook) &&
|
||||
hook['type'] === 'command' &&
|
||||
hook['command'] === CLAUDEX_MUTATOR_GATE_COMMAND,
|
||||
),
|
||||
);
|
||||
if (!gatePresent) preToolUse.unshift(CLAUDEX_MUTATOR_GATE_HOOK);
|
||||
|
||||
hooks['PreToolUse'] = preToolUse;
|
||||
settings['hooks'] = hooks;
|
||||
writeFileSync(settingsPath, `${JSON.stringify(settings, null, 2)}\n`, { mode: 0o600 });
|
||||
chmodSync(settingsPath, 0o600);
|
||||
}
|
||||
|
||||
// ─── EXPERIMENTAL classification (P4) ─────────────────────────────────────────
|
||||
|
||||
/** Console banner shown at launch. Contains no token material by construction. */
|
||||
@@ -396,8 +474,8 @@ export interface ClaudexHarnessAdapter {
|
||||
harnessPreflight: () => void;
|
||||
/** Compose the full Claude runtime contract (== `composeContract('claude')`). */
|
||||
composePrompt: () => string;
|
||||
/** Replace the current process with `claude` using the composed env. */
|
||||
exec: (cmd: string, args: string[], env: NodeJS.ProcessEnv) => void;
|
||||
/** Register the Claude parent with the broker, then exec with the same PID. */
|
||||
execLeaseGated: (args: string[], env: NodeJS.ProcessEnv, dangerous: boolean) => void;
|
||||
}
|
||||
|
||||
export interface LaunchClaudexDeps {
|
||||
@@ -406,6 +484,7 @@ export interface LaunchClaudexDeps {
|
||||
resolveConfigDir?: () => string;
|
||||
models?: () => ClaudexModels;
|
||||
buildEnv?: (base: NodeJS.ProcessEnv, opts: BuildClaudexEnvOptions) => NodeJS.ProcessEnv;
|
||||
prepareConfig?: (configDir: string) => void;
|
||||
log?: (message: string) => void;
|
||||
errorLog?: (message: string) => void;
|
||||
fail?: (code: number) => never;
|
||||
@@ -431,6 +510,7 @@ export async function launchClaudex(
|
||||
const resolveConfigDir = deps.resolveConfigDir ?? (() => resolveClaudexConfigDir(baseEnv));
|
||||
const models = deps.models ?? (() => resolveClaudexModels(baseEnv));
|
||||
const buildEnv = deps.buildEnv ?? buildClaudexEnv;
|
||||
const prepareConfig = deps.prepareConfig ?? ensureClaudexMutatorGateSettings;
|
||||
|
||||
try {
|
||||
// Harness readiness first (claude on PATH, mosaic home, sequential-thinking).
|
||||
@@ -447,14 +527,14 @@ export async function launchClaudex(
|
||||
// Compose the isolated launch env (guard throws → caught below, fail closed).
|
||||
const resolvedModels = models();
|
||||
const configDir = resolveConfigDir();
|
||||
prepareConfig(configDir);
|
||||
const env = buildEnv(baseEnv, { configDir, models: resolvedModels });
|
||||
const prompt = `${adapter.composePrompt()}\n\n${buildClaudexContractNote(resolvedModels)}`;
|
||||
|
||||
log(buildClaudexBanner(resolvedModels));
|
||||
|
||||
const cliArgs = yolo ? ['--dangerously-skip-permissions'] : [];
|
||||
cliArgs.push('--append-system-prompt', prompt, ...args);
|
||||
adapter.exec('claude', cliArgs, env);
|
||||
const cliArgs = ['--append-system-prompt', prompt, ...args];
|
||||
adapter.execLeaseGated(cliArgs, env, yolo);
|
||||
} catch (err) {
|
||||
errorLog(
|
||||
`[mosaic] claudex launch aborted: ${err instanceof Error ? err.message : String(err)}`,
|
||||
|
||||
@@ -115,7 +115,7 @@ function auditClaudeSettings(): SettingsAudit {
|
||||
// Check required hooks
|
||||
const hooks = settings['hooks'] as Record<string, unknown[]> | undefined;
|
||||
|
||||
const requiredPreToolUse = ['prevent-memory-write.sh'];
|
||||
const requiredPreToolUse = ['mutator-gate.py', 'prevent-memory-write.sh'];
|
||||
const requiredPostToolUse = ['qa-hook-stdin.sh', 'typecheck-hook.sh'];
|
||||
|
||||
const preHooks = (hooks?.['PreToolUse'] ?? []) as Array<Record<string, unknown>>;
|
||||
@@ -755,7 +755,7 @@ function launchRuntime(runtime: RuntimeName, args: string[], yolo: boolean): nev
|
||||
printSettingsWarnings(settingsAudit);
|
||||
|
||||
const prompt = buildRuntimePrompt('claude');
|
||||
const cliArgs = yolo ? ['--dangerously-skip-permissions'] : [];
|
||||
const cliArgs: string[] = [];
|
||||
cliArgs.push('--append-system-prompt', prompt);
|
||||
if (hasMissionNoArgs) {
|
||||
cliArgs.push(missionPrompt);
|
||||
@@ -763,7 +763,7 @@ function launchRuntime(runtime: RuntimeName, args: string[], yolo: boolean): nev
|
||||
cliArgs.push(...args);
|
||||
}
|
||||
console.log(`[mosaic] Launching ${label}${modeStr}${missionStr}...`);
|
||||
execRuntime('claude', cliArgs);
|
||||
execLeaseGatedRuntime('claude', cliArgs, process.env, yolo);
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -798,7 +798,7 @@ function launchRuntime(runtime: RuntimeName, args: string[], yolo: boolean): nev
|
||||
cliArgs.push(...args);
|
||||
}
|
||||
console.log(`[mosaic] Launching ${label}${modeStr}${missionStr}...`);
|
||||
execRuntime('pi', cliArgs);
|
||||
execLeaseGatedRuntime('pi', cliArgs);
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -806,6 +806,33 @@ function launchRuntime(runtime: RuntimeName, args: string[], yolo: boolean): nev
|
||||
process.exit(0); // Unreachable but satisfies never
|
||||
}
|
||||
|
||||
function defaultLeaseBrokerSocket(env: NodeJS.ProcessEnv = process.env): string {
|
||||
if (env['MOSAIC_LEASE_BROKER_SOCKET']) return env['MOSAIC_LEASE_BROKER_SOCKET'];
|
||||
const runtimeDir = env['XDG_RUNTIME_DIR'];
|
||||
if (runtimeDir) return join(runtimeDir, 'mosaic-lease', 'broker.sock');
|
||||
const uid = typeof process.getuid === 'function' ? process.getuid() : 0;
|
||||
return join('/run/user', String(uid), 'mosaic-lease', 'broker.sock');
|
||||
}
|
||||
|
||||
function execLeaseGatedRuntime(
|
||||
runtime: 'claude' | 'pi',
|
||||
args: string[],
|
||||
baseEnv: NodeJS.ProcessEnv = process.env,
|
||||
dangerous = false,
|
||||
): void {
|
||||
const launcher = resolveTool('lease-broker', 'launch-runtime.py');
|
||||
const dangerousArgs = dangerous ? ['--dangerous'] : [];
|
||||
execRuntime(
|
||||
'python3',
|
||||
[launcher, ...dangerousArgs, '--runtime', runtime, '--', runtime, ...args],
|
||||
{
|
||||
...baseEnv,
|
||||
MOSAIC_LEASE_BROKER_SOCKET: defaultLeaseBrokerSocket(baseEnv),
|
||||
MOSAIC_RUNTIME_GENERATION: baseEnv['MOSAIC_RUNTIME_GENERATION'] ?? '1',
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/** exec into the runtime, replacing the current process. */
|
||||
function execRuntime(cmd: string, args: string[], env: NodeJS.ProcessEnv = process.env): void {
|
||||
try {
|
||||
@@ -839,7 +866,8 @@ function launchClaudexProduction(args: string[], yolo: boolean): void {
|
||||
checkSequentialThinking('claude');
|
||||
},
|
||||
composePrompt: () => buildRuntimePrompt('claude'),
|
||||
exec: (cmd, cmdArgs, env) => execRuntime(cmd, cmdArgs, env),
|
||||
execLeaseGated: (cmdArgs, env, dangerous) =>
|
||||
execLeaseGatedRuntime('claude', cmdArgs, env, dangerous),
|
||||
};
|
||||
void launchClaudex(args, yolo, adapter);
|
||||
}
|
||||
|
||||
181
packages/mosaic/src/lease-broker/broker-test-client.spec.ts
Normal file
181
packages/mosaic/src/lease-broker/broker-test-client.spec.ts
Normal file
@@ -0,0 +1,181 @@
|
||||
import { mkdtemp, rm } from 'node:fs/promises';
|
||||
import { createServer, type Server, type Socket } from 'node:net';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { join } from 'node:path';
|
||||
|
||||
import { afterEach, describe, expect, test } from 'vitest';
|
||||
|
||||
import { BrokerTransportError, readBrokerReply, requestBrokerReply } from './broker-test-client.js';
|
||||
|
||||
const roots: string[] = [];
|
||||
const servers: Server[] = [];
|
||||
const sockets: Socket[] = [];
|
||||
|
||||
async function scriptedBroker(
|
||||
replies: ReadonlyArray<ReadonlyArray<Buffer> | 'hang'>,
|
||||
): Promise<{ socketPath: string; connections: () => number }> {
|
||||
const root = await mkdtemp(join(tmpdir(), 'mosaic-broker-client-'));
|
||||
roots.push(root);
|
||||
const socketPath = join(root, 'broker.sock');
|
||||
let connections = 0;
|
||||
const server = createServer({ allowHalfOpen: true }, (socket) => {
|
||||
sockets.push(socket);
|
||||
const chunks = replies[connections] ?? replies.at(-1) ?? [];
|
||||
connections += 1;
|
||||
socket.once('end', () => {
|
||||
if (chunks === 'hang') return;
|
||||
void (async () => {
|
||||
for (const chunk of chunks) {
|
||||
socket.write(chunk);
|
||||
await new Promise<void>((resolve) => setTimeout(resolve, 5));
|
||||
}
|
||||
socket.end();
|
||||
})();
|
||||
});
|
||||
socket.resume();
|
||||
});
|
||||
servers.push(server);
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
server.once('error', reject);
|
||||
server.listen(socketPath, resolve);
|
||||
});
|
||||
return { socketPath, connections: () => connections };
|
||||
}
|
||||
|
||||
afterEach(async () => {
|
||||
for (const socket of sockets.splice(0)) socket.destroy();
|
||||
await Promise.all(
|
||||
servers
|
||||
.splice(0)
|
||||
.map(
|
||||
(server) =>
|
||||
new Promise<void>((resolve, reject) =>
|
||||
server.close((error) => (error ? reject(error) : resolve())),
|
||||
),
|
||||
),
|
||||
);
|
||||
await Promise.all(roots.splice(0).map((root) => rm(root, { recursive: true, force: true })));
|
||||
});
|
||||
|
||||
describe('newline-framed broker test client', () => {
|
||||
test('rejects an empty early close without retrying into a false green', async () => {
|
||||
const broker = await scriptedBroker([[], [Buffer.from('{"ok":true}\n')]]);
|
||||
|
||||
const failure = await requestBrokerReply(broker.socketPath, { action: 'probe' }).catch(
|
||||
(error: unknown) => error,
|
||||
);
|
||||
|
||||
expect(failure).toBeInstanceOf(BrokerTransportError);
|
||||
expect(failure).toMatchObject({
|
||||
kind: 'early-close',
|
||||
attempts: 1,
|
||||
responseLength: 0,
|
||||
responseHex: '',
|
||||
});
|
||||
expect(failure).toHaveProperty(
|
||||
'message',
|
||||
expect.stringMatching(/closed before newline.*length=0.*hex=<empty>/i),
|
||||
);
|
||||
expect(broker.connections()).toBe(1);
|
||||
});
|
||||
|
||||
test('rejects repeated truncated early closes with response bytes and length', async () => {
|
||||
const truncated = Buffer.from('{"ok":');
|
||||
const broker = await scriptedBroker([[truncated], [Buffer.from('{"ok":true}\n')]]);
|
||||
|
||||
const failure = await requestBrokerReply(broker.socketPath, { action: 'probe' }).catch(
|
||||
(error: unknown) => error,
|
||||
);
|
||||
|
||||
expect(failure).toBeInstanceOf(BrokerTransportError);
|
||||
expect(failure).toMatchObject({
|
||||
kind: 'early-close',
|
||||
attempts: 1,
|
||||
responseLength: 6,
|
||||
responseHex: '7b226f6b223a',
|
||||
});
|
||||
expect(failure).toHaveProperty(
|
||||
'message',
|
||||
expect.stringMatching(/closed before newline.*length=6.*bytes=.*ok/i),
|
||||
);
|
||||
expect(broker.connections()).toBe(1);
|
||||
});
|
||||
|
||||
test('rejects a newline-terminated malformed broker reply without retrying', async () => {
|
||||
const broker = await scriptedBroker([[Buffer.from('{bad}\n')]]);
|
||||
|
||||
await expect(requestBrokerReply(broker.socketPath, { action: 'probe' })).rejects.toThrow(
|
||||
/malformed broker reply.*length=6.*bytes="\{bad\}\\n"/i,
|
||||
);
|
||||
expect(broker.connections()).toBe(1);
|
||||
});
|
||||
|
||||
test('rejects trailing bytes delivered after a complete frame in a later data event', async () => {
|
||||
const broker = await scriptedBroker([[Buffer.from('{"ok":true}\n'), Buffer.from('extra')]]);
|
||||
|
||||
const failure = await requestBrokerReply(broker.socketPath, { action: 'probe' }).catch(
|
||||
(error: unknown) => error,
|
||||
);
|
||||
|
||||
expect(failure).toBeInstanceOf(BrokerTransportError);
|
||||
expect(failure).toMatchObject({ kind: 'malformed-reply', responseLength: 17 });
|
||||
expect(failure).toHaveProperty(
|
||||
'message',
|
||||
expect.stringMatching(/bytes after the newline terminator/i),
|
||||
);
|
||||
});
|
||||
|
||||
test('redacts security tokens from typed transport diagnostics', async () => {
|
||||
const token = 'a'.repeat(64);
|
||||
const reply = Buffer.from(`{"ok":true,"promotion_token":"${token}`);
|
||||
const broker = await scriptedBroker([[reply]]);
|
||||
|
||||
const failure = await requestBrokerReply(broker.socketPath, { action: 'probe' }).catch(
|
||||
(error: unknown) => error,
|
||||
);
|
||||
|
||||
expect(failure).toBeInstanceOf(BrokerTransportError);
|
||||
expect(failure).toMatchObject({
|
||||
kind: 'early-close',
|
||||
responseLength: reply.length,
|
||||
responsePreview: '<redacted-sensitive-reply>',
|
||||
});
|
||||
expect(String((failure as Error).message)).not.toContain(token);
|
||||
expect(JSON.stringify(failure)).not.toContain(token);
|
||||
});
|
||||
|
||||
test.each([
|
||||
['trailing bytes', Buffer.from('{"ok":true}\nextra'), /bytes after the newline/i],
|
||||
['non-object JSON', Buffer.from('[]\n'), /JSON value is not an object/i],
|
||||
['oversized frame', Buffer.alloc(64 * 1024 + 1, 0x78), /exceeds 65536 bytes/i],
|
||||
])('rejects %s with deterministic framing context', async (_label, reply, message) => {
|
||||
const broker = await scriptedBroker([[reply as Buffer]]);
|
||||
|
||||
await expect(requestBrokerReply(broker.socketPath, { action: 'probe' })).rejects.toThrow(
|
||||
message as RegExp,
|
||||
);
|
||||
expect(broker.connections()).toBe(1);
|
||||
});
|
||||
|
||||
test('reports timeout, connection, and writer failures as promise rejections', async () => {
|
||||
const hanging = await scriptedBroker(['hang']);
|
||||
await expect(
|
||||
requestBrokerReply(hanging.socketPath, { action: 'probe' }, { timeoutMs: 10 }),
|
||||
).rejects.toThrow(/timed out before newline.*length=0.*hex=<empty>/i);
|
||||
|
||||
await expect(
|
||||
requestBrokerReply(join(rootForMissingSocket(), 'missing.sock'), {}),
|
||||
).rejects.toThrow(/socket error before newline.*length=0/i);
|
||||
|
||||
const writerFailure = await scriptedBroker(['hang']);
|
||||
await expect(
|
||||
readBrokerReply(writerFailure.socketPath, () => {
|
||||
throw new Error('writer failed');
|
||||
}),
|
||||
).rejects.toThrow('writer failed');
|
||||
});
|
||||
});
|
||||
|
||||
function rootForMissingSocket(): string {
|
||||
return join(tmpdir(), `mosaic-missing-broker-${process.pid}-${Date.now()}`);
|
||||
}
|
||||
187
packages/mosaic/src/lease-broker/broker-test-client.ts
Normal file
187
packages/mosaic/src/lease-broker/broker-test-client.ts
Normal file
@@ -0,0 +1,187 @@
|
||||
import { createHash } from 'node:crypto';
|
||||
import { createConnection, type Socket } from 'node:net';
|
||||
|
||||
const DEFAULT_TIMEOUT_MS = 3_000;
|
||||
const MAX_REPLY_BYTES = 64 * 1024;
|
||||
const MAX_DIAGNOSTIC_BYTES = 256;
|
||||
const SENSITIVE_REPLY_FIELD = /"(?:promotion_token|session_id|token)"\s*:/;
|
||||
|
||||
export interface BrokerTestClientOptions {
|
||||
timeoutMs?: number;
|
||||
}
|
||||
|
||||
export type BrokerTransportFailureKind =
|
||||
| 'early-close'
|
||||
| 'malformed-reply'
|
||||
| 'socket-error'
|
||||
| 'timeout';
|
||||
|
||||
export class BrokerTransportError extends Error {
|
||||
public readonly responseLength: number;
|
||||
public readonly responseBytes: string;
|
||||
public readonly responseHex: string;
|
||||
public readonly responsePreview: string;
|
||||
public readonly responseSha256: string;
|
||||
|
||||
public constructor(
|
||||
public readonly kind: BrokerTransportFailureKind,
|
||||
description: string,
|
||||
response: Buffer,
|
||||
public readonly attempts = 1,
|
||||
) {
|
||||
const diagnostics = responseDiagnostics(response);
|
||||
super(`${description}; attempts=${attempts}; ${responseContext(response, diagnostics)}`);
|
||||
this.name = 'BrokerTransportError';
|
||||
this.responseLength = response.length;
|
||||
this.responseBytes = diagnostics.preview;
|
||||
this.responseHex = diagnostics.hex;
|
||||
this.responsePreview = diagnostics.preview;
|
||||
this.responseSha256 = diagnostics.sha256;
|
||||
}
|
||||
}
|
||||
|
||||
interface ResponseDiagnostics {
|
||||
preview: string;
|
||||
hex: string;
|
||||
sha256: string;
|
||||
}
|
||||
|
||||
function responseDiagnostics(response: Buffer): ResponseDiagnostics {
|
||||
const fullText = response.toString('utf8');
|
||||
const sensitive = SENSITIVE_REPLY_FIELD.test(fullText);
|
||||
const bounded = response.subarray(0, MAX_DIAGNOSTIC_BYTES);
|
||||
const suffix = response.length > MAX_DIAGNOSTIC_BYTES ? '…' : '';
|
||||
return {
|
||||
preview:
|
||||
response.length === 0
|
||||
? '<empty>'
|
||||
: sensitive
|
||||
? '<redacted-sensitive-reply>'
|
||||
: `${bounded.toString('utf8')}${suffix}`,
|
||||
hex: sensitive ? '<redacted>' : `${bounded.toString('hex')}${suffix}`,
|
||||
sha256: createHash('sha256').update(response).digest('hex'),
|
||||
};
|
||||
}
|
||||
|
||||
function responseContext(response: Buffer, diagnostics = responseDiagnostics(response)): string {
|
||||
const hex = diagnostics.hex.length === 0 ? '<empty>' : diagnostics.hex;
|
||||
return `length=${response.length}; bytes=${JSON.stringify(diagnostics.preview)}; hex=${hex}; sha256=${diagnostics.sha256}`;
|
||||
}
|
||||
|
||||
function malformedReply(response: Buffer, reason: string): BrokerTransportError {
|
||||
return new BrokerTransportError('malformed-reply', `Malformed broker reply: ${reason}`, response);
|
||||
}
|
||||
|
||||
function readBrokerReplyAttempt<T extends object>(
|
||||
socketPath: string,
|
||||
write: (socket: Socket) => void,
|
||||
timeoutMs: number,
|
||||
): Promise<T> {
|
||||
return new Promise<T>((resolve, reject) => {
|
||||
const socket = createConnection(socketPath);
|
||||
let response = Buffer.alloc(0);
|
||||
let settled = false;
|
||||
|
||||
const settle = (callback: () => void): void => {
|
||||
if (settled) return;
|
||||
settled = true;
|
||||
clearTimeout(timer);
|
||||
callback();
|
||||
socket.destroy();
|
||||
};
|
||||
const fail = (error: Error): void => settle(() => reject(error));
|
||||
const timer = setTimeout(
|
||||
() =>
|
||||
fail(
|
||||
new BrokerTransportError(
|
||||
'timeout',
|
||||
`Broker reply timed out before newline after ${timeoutMs}ms`,
|
||||
response,
|
||||
),
|
||||
),
|
||||
timeoutMs,
|
||||
);
|
||||
|
||||
socket.once('error', (error) =>
|
||||
fail(
|
||||
new BrokerTransportError(
|
||||
'socket-error',
|
||||
`Broker reply socket error before newline: ${error.message}`,
|
||||
response,
|
||||
),
|
||||
),
|
||||
);
|
||||
socket.on('data', (chunk: Buffer) => {
|
||||
if (settled) return;
|
||||
response = Buffer.concat([response, chunk]);
|
||||
if (response.length > MAX_REPLY_BYTES) {
|
||||
fail(malformedReply(response, `exceeds ${MAX_REPLY_BYTES} bytes`));
|
||||
}
|
||||
});
|
||||
socket.once('end', () => {
|
||||
if (settled) return;
|
||||
const newline = response.indexOf(0x0a);
|
||||
if (response.length === 0 || newline < 0) {
|
||||
fail(
|
||||
new BrokerTransportError(
|
||||
'early-close',
|
||||
'Broker reply socket closed before newline',
|
||||
response,
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (newline !== response.length - 1) {
|
||||
fail(malformedReply(response, 'contains bytes after the newline terminator'));
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const parsed: unknown = JSON.parse(response.subarray(0, newline).toString('utf8'));
|
||||
if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
|
||||
fail(malformedReply(response, 'JSON value is not an object'));
|
||||
return;
|
||||
}
|
||||
settle(() => resolve(parsed as T));
|
||||
} catch (error: unknown) {
|
||||
fail(
|
||||
malformedReply(response, error instanceof Error ? error.message : 'JSON parsing failed'),
|
||||
);
|
||||
}
|
||||
});
|
||||
socket.once('connect', () => {
|
||||
try {
|
||||
write(socket);
|
||||
} catch (error: unknown) {
|
||||
fail(error instanceof Error ? error : new Error(String(error)));
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/** Read exactly one complete newline-framed JSON object; transport failures reject. */
|
||||
export async function readBrokerReply<T extends object>(
|
||||
socketPath: string,
|
||||
write: (socket: Socket) => void,
|
||||
options: BrokerTestClientOptions = {},
|
||||
): Promise<T> {
|
||||
return await readBrokerReplyAttempt<T>(
|
||||
socketPath,
|
||||
write,
|
||||
options.timeoutMs ?? DEFAULT_TIMEOUT_MS,
|
||||
);
|
||||
}
|
||||
|
||||
/** Send one newline-framed request and read its complete broker reply. */
|
||||
export async function requestBrokerReply<T extends object>(
|
||||
socketPath: string,
|
||||
requestValue: object,
|
||||
options?: BrokerTestClientOptions,
|
||||
): Promise<T> {
|
||||
return await readBrokerReply<T>(
|
||||
socketPath,
|
||||
(socket) => socket.end(`${JSON.stringify(requestValue)}\n`),
|
||||
options,
|
||||
);
|
||||
}
|
||||
103
packages/mosaic/src/lease-broker/daemon_deadline_unittest.py
Normal file
103
packages/mosaic/src/lease-broker/daemon_deadline_unittest.py
Normal file
@@ -0,0 +1,103 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Regression tests for bounded lease-broker read/handle/send deadlines."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib.util
|
||||
import json
|
||||
import socket
|
||||
import threading
|
||||
import time
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
DAEMON_PATH = Path(__file__).parents[2] / "framework/tools/lease-broker/daemon.py"
|
||||
SPEC = importlib.util.spec_from_file_location("lease_broker_deadline_daemon", DAEMON_PATH)
|
||||
if SPEC is None or SPEC.loader is None:
|
||||
raise RuntimeError("unable to load lease broker daemon")
|
||||
DAEMON = importlib.util.module_from_spec(SPEC)
|
||||
SPEC.loader.exec_module(DAEMON)
|
||||
|
||||
|
||||
def original_connection_budget() -> float:
|
||||
read_budget = getattr(DAEMON, "READ_DEADLINE_SECONDS", None)
|
||||
if isinstance(read_budget, (int, float)):
|
||||
return float(read_budget)
|
||||
return float(DAEMON.CONNECTION_DEADLINE_SECONDS)
|
||||
|
||||
|
||||
class SlowBroker:
|
||||
def __init__(self, delay: float) -> None:
|
||||
self.delay = delay
|
||||
self.calls = 0
|
||||
|
||||
def handle(self, _peer: tuple[int, int, int], request: dict[str, object]) -> dict[str, object]:
|
||||
self.calls += 1
|
||||
time.sleep(self.delay)
|
||||
return {"ok": True, "echo": request.get("action")}
|
||||
|
||||
|
||||
class BrokerDeadlineTest(unittest.TestCase):
|
||||
def test_lock_queue_timeout_returns_explicit_fail_closed_reply_without_handling(self) -> None:
|
||||
broker = SlowBroker(0)
|
||||
broker_lock = threading.Lock()
|
||||
broker_lock.acquire()
|
||||
server, client = socket.socketpair(socket.AF_UNIX, socket.SOCK_STREAM)
|
||||
client.settimeout(DAEMON.HANDLE_QUEUE_TIMEOUT_SECONDS + 2.0)
|
||||
worker = threading.Thread(
|
||||
target=DAEMON.handle_connection,
|
||||
args=(server, broker, broker_lock),
|
||||
daemon=True,
|
||||
)
|
||||
worker.start()
|
||||
client.sendall(b'{"action":"probe"}\n')
|
||||
client.shutdown(socket.SHUT_WR)
|
||||
|
||||
reply = bytearray()
|
||||
try:
|
||||
while True:
|
||||
chunk = client.recv(4096)
|
||||
if not chunk:
|
||||
break
|
||||
reply.extend(chunk)
|
||||
finally:
|
||||
broker_lock.release()
|
||||
worker.join(timeout=2.0)
|
||||
client.close()
|
||||
|
||||
self.assertFalse(worker.is_alive())
|
||||
self.assertEqual(broker.calls, 0)
|
||||
self.assertEqual(json.loads(reply), {"ok": False, "code": "BROKER_BUSY"})
|
||||
|
||||
def test_completed_slow_handle_gets_a_complete_framed_reply(self) -> None:
|
||||
broker = SlowBroker(original_connection_budget() + 0.1)
|
||||
broker_lock = threading.Lock()
|
||||
server, client = socket.socketpair(socket.AF_UNIX, socket.SOCK_STREAM)
|
||||
client.settimeout(original_connection_budget() + 2.0)
|
||||
worker = threading.Thread(
|
||||
target=DAEMON.handle_connection,
|
||||
args=(server, broker, broker_lock),
|
||||
daemon=True,
|
||||
)
|
||||
worker.start()
|
||||
client.sendall(b'{"action":"probe"}\n')
|
||||
client.shutdown(socket.SHUT_WR)
|
||||
|
||||
reply = bytearray()
|
||||
while True:
|
||||
chunk = client.recv(4096)
|
||||
if not chunk:
|
||||
break
|
||||
reply.extend(chunk)
|
||||
worker.join(timeout=2.0)
|
||||
client.close()
|
||||
|
||||
self.assertFalse(worker.is_alive())
|
||||
self.assertEqual(broker.calls, 1)
|
||||
self.assertTrue(reply.endswith(b"\n"), f"unframed reply: {bytes(reply)!r}")
|
||||
self.assertEqual(json.loads(reply), {"ok": True, "echo": "probe"})
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
629
packages/mosaic/src/lease-broker/lease-broker.acceptance.spec.ts
Normal file
629
packages/mosaic/src/lease-broker/lease-broker.acceptance.spec.ts
Normal file
@@ -0,0 +1,629 @@
|
||||
import { randomUUID } from 'node:crypto';
|
||||
import { chmod, mkdtemp, readFile, stat, symlink, writeFile } from 'node:fs/promises';
|
||||
import { createConnection, type Socket } from 'node:net';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { join } from 'node:path';
|
||||
import { spawn, spawnSync, type ChildProcess } from 'node:child_process';
|
||||
|
||||
import { afterEach, describe, expect, test, vi } from 'vitest';
|
||||
|
||||
import { readBrokerReply, requestBrokerReply } from './broker-test-client.js';
|
||||
|
||||
interface BrokerReply {
|
||||
ok: boolean;
|
||||
code?: string;
|
||||
session_id?: string;
|
||||
peer?: { pid: number; uid: number; gid: number; starttime: string };
|
||||
token?: string;
|
||||
}
|
||||
|
||||
const daemonPath = new URL('../../framework/tools/lease-broker/daemon.py', import.meta.url)
|
||||
.pathname;
|
||||
const children: ChildProcess[] = [];
|
||||
|
||||
async function withTimeout<T>(
|
||||
promise: Promise<T>,
|
||||
label: string,
|
||||
milliseconds = 3_000,
|
||||
): Promise<T> {
|
||||
let timer: NodeJS.Timeout | undefined;
|
||||
try {
|
||||
return await Promise.race([
|
||||
promise,
|
||||
new Promise<never>((_, reject) => {
|
||||
timer = setTimeout(() => reject(new Error(`${label} timed out`)), milliseconds);
|
||||
}),
|
||||
]);
|
||||
} finally {
|
||||
if (timer !== undefined) clearTimeout(timer);
|
||||
}
|
||||
}
|
||||
|
||||
async function rawRequest(
|
||||
socketPath: string,
|
||||
write: (socket: Socket) => void,
|
||||
): Promise<BrokerReply> {
|
||||
return await readBrokerReply<BrokerReply>(socketPath, write);
|
||||
}
|
||||
|
||||
async function request(socketPath: string, requestValue: object): Promise<BrokerReply> {
|
||||
return await requestBrokerReply<BrokerReply>(socketPath, requestValue);
|
||||
}
|
||||
|
||||
async function startBroker(
|
||||
parentMode = 0o700,
|
||||
): Promise<{ root: string; socket: string; state: string }> {
|
||||
const root = await mkdtemp(join(tmpdir(), 'mosaic-lease-broker-'));
|
||||
await chmod(root, parentMode);
|
||||
const socket = join(root, 'broker.sock');
|
||||
const state = join(root, 'state.json');
|
||||
const child = spawn('python3', [daemonPath, '--socket', socket, '--state', state], {
|
||||
stdio: ['ignore', 'pipe', 'pipe'],
|
||||
});
|
||||
children.push(child);
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
let stderr = '';
|
||||
child.stderr?.setEncoding('utf8');
|
||||
child.stderr?.on('data', (chunk: string) => (stderr += chunk));
|
||||
child.once('error', reject);
|
||||
child.once('exit', (code: number | null) =>
|
||||
reject(new Error(`broker exited ${code}: ${stderr}`)),
|
||||
);
|
||||
child.stdout?.once('data', () => resolve());
|
||||
});
|
||||
return { root, socket, state };
|
||||
}
|
||||
|
||||
async function startBrokerWithState(stateValue: string): Promise<string> {
|
||||
const root = await mkdtemp(join(tmpdir(), 'mosaic-lease-broker-'));
|
||||
await chmod(root, 0o700);
|
||||
const state = join(root, 'state.json');
|
||||
await writeFile(state, stateValue, { mode: 0o600 });
|
||||
const child = spawn('python3', [
|
||||
daemonPath,
|
||||
'--socket',
|
||||
join(root, 'broker.sock'),
|
||||
'--state',
|
||||
state,
|
||||
]);
|
||||
children.push(child);
|
||||
return await new Promise<string>((resolve) => {
|
||||
let raw = '';
|
||||
child.stderr?.on('data', (chunk: Buffer) => (raw += chunk.toString()));
|
||||
child.once('exit', () => resolve(raw));
|
||||
});
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
for (const child of children.splice(0)) child.kill('SIGTERM');
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
describe('authenticated external lease broker', () => {
|
||||
test('peercred returns true kernel (pid,starttime)', async () => {
|
||||
const getuid = process.getuid;
|
||||
const getgid = process.getgid;
|
||||
if (getuid === undefined || getgid === undefined) {
|
||||
throw new Error('Linux peer credentials require process.getuid() and process.getgid()');
|
||||
}
|
||||
|
||||
const { socket } = await startBroker();
|
||||
const reply = await request(socket, { action: 'register_anchor', runtime_generation: 1 });
|
||||
const statText = await readFile(`/proc/${process.pid}/stat`, 'utf8');
|
||||
const fields = statText.slice(statText.lastIndexOf(')') + 2).split(' ');
|
||||
expect(reply).toMatchObject({
|
||||
ok: true,
|
||||
peer: {
|
||||
pid: process.pid,
|
||||
uid: getuid(),
|
||||
gid: getgid(),
|
||||
starttime: fields[19],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test.each([null, '', 'chosen'])('caller-asserted session_id refused (%j)', async (session_id) => {
|
||||
const { socket } = await startBroker();
|
||||
const reply = await request(socket, {
|
||||
action: 'register_anchor',
|
||||
runtime_generation: 1,
|
||||
session_id,
|
||||
});
|
||||
expect(reply).toMatchObject({ ok: false, code: 'CALLER_SESSION_ID_REFUSED' });
|
||||
});
|
||||
|
||||
test('sibling-substitution rejected', async () => {
|
||||
const { socket } = await startBroker();
|
||||
const launcher = spawn(
|
||||
process.execPath,
|
||||
[
|
||||
'-e',
|
||||
`const n=require('net');const s=n.connect(${JSON.stringify(socket)},()=>s.end(JSON.stringify({action:'register_anchor',runtime_generation:1})+'\\n'));s.on('data',d=>{process.send(JSON.parse(d));setInterval(()=>{},1000)})`,
|
||||
],
|
||||
{ stdio: ['ignore', 'ignore', 'ignore', 'ipc'] },
|
||||
);
|
||||
children.push(launcher);
|
||||
const registration = await new Promise<BrokerReply>((resolve) =>
|
||||
launcher.once('message', (message) => resolve(message as BrokerReply)),
|
||||
);
|
||||
const attacker = spawn(
|
||||
process.execPath,
|
||||
[
|
||||
'-e',
|
||||
`const n=require('net');const s=n.connect(${JSON.stringify(socket)},()=>s.end(JSON.stringify({action:'authenticate',session_id:${JSON.stringify(registration.session_id)},runtime_generation:1})+'\\n'));s.pipe(process.stdout)`,
|
||||
],
|
||||
{ stdio: ['ignore', 'pipe', 'ignore'] },
|
||||
);
|
||||
children.push(attacker);
|
||||
let raw = '';
|
||||
attacker.stdout?.setEncoding('utf8');
|
||||
attacker.stdout?.on('data', (chunk: string) => (raw += chunk));
|
||||
await new Promise<void>((resolve) => attacker.once('exit', () => resolve()));
|
||||
expect(JSON.parse(raw)).toMatchObject({ ok: false, code: 'ANCESTRY_MISMATCH' });
|
||||
});
|
||||
|
||||
test('generation bump revokes prior incarnation', async () => {
|
||||
const { socket } = await startBroker();
|
||||
const registered = await request(socket, { action: 'register_anchor', runtime_generation: 1 });
|
||||
expect(
|
||||
await request(socket, {
|
||||
action: 'authenticate',
|
||||
session_id: registered.session_id,
|
||||
runtime_generation: 2,
|
||||
}),
|
||||
).toMatchObject({ ok: true });
|
||||
expect(
|
||||
await request(socket, {
|
||||
action: 'authenticate',
|
||||
session_id: registered.session_id,
|
||||
runtime_generation: 1,
|
||||
}),
|
||||
).toMatchObject({ ok: false, code: 'STALE_GENERATION' });
|
||||
});
|
||||
|
||||
test('same anchor re-registration reuses its session and revokes the prior incarnation', async () => {
|
||||
const { socket } = await startBroker();
|
||||
const first = await request(socket, { action: 'register_anchor', runtime_generation: 1 });
|
||||
const binding = {
|
||||
compaction_epoch: 2,
|
||||
request_epoch: 3,
|
||||
h_source: 'a'.repeat(64),
|
||||
h_payload: 'b'.repeat(64),
|
||||
schema_version: 1,
|
||||
};
|
||||
const minted = await request(socket, {
|
||||
action: 'mint_token',
|
||||
session_id: first.session_id,
|
||||
runtime_generation: 1,
|
||||
binding,
|
||||
});
|
||||
|
||||
const bumped = await request(socket, { action: 'register_anchor', runtime_generation: 2 });
|
||||
const repeated = await request(socket, { action: 'register_anchor', runtime_generation: 2 });
|
||||
const lower = await request(socket, { action: 'register_anchor', runtime_generation: 1 });
|
||||
|
||||
expect(bumped).toMatchObject({ ok: true, session_id: first.session_id });
|
||||
expect(repeated).toMatchObject({ ok: true, session_id: first.session_id });
|
||||
expect(lower).toMatchObject({ ok: false, code: 'STALE_GENERATION' });
|
||||
expect(
|
||||
await request(socket, {
|
||||
action: 'authenticate',
|
||||
session_id: first.session_id,
|
||||
runtime_generation: 1,
|
||||
}),
|
||||
).toMatchObject({ ok: false, code: 'STALE_GENERATION' });
|
||||
expect(
|
||||
await request(socket, {
|
||||
action: 'consume_token',
|
||||
session_id: first.session_id,
|
||||
runtime_generation: 2,
|
||||
token: minted.token,
|
||||
}),
|
||||
).toMatchObject({ ok: false, code: 'TOKEN_REPLAY' });
|
||||
});
|
||||
|
||||
test('crypto token path works when Math.random is poisoned', async () => {
|
||||
const { socket } = await startBroker();
|
||||
vi.spyOn(Math, 'random').mockImplementation(() => {
|
||||
throw new Error('Math.random forbidden');
|
||||
});
|
||||
const registered = await request(socket, { action: 'register_anchor', runtime_generation: 1 });
|
||||
const binding = {
|
||||
compaction_epoch: 2,
|
||||
request_epoch: 3,
|
||||
h_source: 'a'.repeat(64),
|
||||
h_payload: 'b'.repeat(64),
|
||||
schema_version: 1,
|
||||
};
|
||||
const first = await request(socket, {
|
||||
action: 'mint_token',
|
||||
session_id: registered.session_id,
|
||||
runtime_generation: 1,
|
||||
binding,
|
||||
});
|
||||
const second = await request(socket, {
|
||||
action: 'mint_token',
|
||||
session_id: registered.session_id,
|
||||
runtime_generation: 1,
|
||||
binding,
|
||||
});
|
||||
expect(first.token).toMatch(/^[a-f0-9]{64}$/);
|
||||
expect(second.token).not.toBe(first.token);
|
||||
expect(
|
||||
await request(socket, {
|
||||
action: 'consume_token',
|
||||
session_id: registered.session_id,
|
||||
runtime_generation: 1,
|
||||
token: first.token,
|
||||
}),
|
||||
).toMatchObject({ ok: true });
|
||||
expect(
|
||||
await request(socket, {
|
||||
action: 'consume_token',
|
||||
session_id: registered.session_id,
|
||||
runtime_generation: 1,
|
||||
token: first.token,
|
||||
}),
|
||||
).toMatchObject({ ok: false, code: 'TOKEN_REPLAY' });
|
||||
});
|
||||
|
||||
test('socket parent 0700 and socket 0600 enforced', async () => {
|
||||
const { root, socket, state } = await startBroker();
|
||||
await request(socket, { action: 'register_anchor', runtime_generation: 1 });
|
||||
expect((await stat(root)).mode & 0o777).toBe(0o700);
|
||||
expect((await stat(socket)).mode & 0o777).toBe(0o600);
|
||||
expect((await stat(state)).mode & 0o777).toBe(0o600);
|
||||
});
|
||||
|
||||
test('insecure existing posture refused', async () => {
|
||||
await expect(startBroker(0o755)).rejects.toThrow(/INSECURE_PARENT_MODE/);
|
||||
});
|
||||
|
||||
test('malformed and oversized frames fail closed without killing broker', async () => {
|
||||
const { socket } = await startBroker();
|
||||
const malformed = await new Promise<string>((resolve, reject) => {
|
||||
const connection = createConnection(socket, () => connection.end('{nope}\n'));
|
||||
let raw = '';
|
||||
connection.on('data', (chunk: Buffer) => (raw += chunk.toString()));
|
||||
connection.once('end', () => resolve(raw));
|
||||
connection.once('error', reject);
|
||||
});
|
||||
expect(JSON.parse(malformed)).toMatchObject({ ok: false, code: 'MALFORMED_REQUEST' });
|
||||
const registered = await request(socket, {
|
||||
action: 'register_anchor',
|
||||
runtime_generation: 1,
|
||||
nonce: randomUUID(),
|
||||
});
|
||||
expect(registered.ok).toBe(true);
|
||||
});
|
||||
|
||||
test('silent connection deadline cannot prevent the next valid registration', async () => {
|
||||
const { socket } = await startBroker();
|
||||
const silent = createConnection(socket);
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
silent.once('connect', resolve);
|
||||
silent.once('error', reject);
|
||||
});
|
||||
const registered = await withTimeout(
|
||||
request(socket, { action: 'register_anchor', runtime_generation: 1 }),
|
||||
'registration behind silent connection',
|
||||
);
|
||||
expect(registered.ok).toBe(true);
|
||||
silent.destroy();
|
||||
});
|
||||
|
||||
test('queued silent peers cannot serialize the next valid registration', async () => {
|
||||
const { socket } = await startBroker();
|
||||
const silentConnections = await Promise.all(
|
||||
Array.from(
|
||||
{ length: 4 },
|
||||
() =>
|
||||
new Promise<Socket>((resolve, reject) => {
|
||||
const connection = createConnection(socket);
|
||||
connection.once('connect', () => resolve(connection));
|
||||
connection.once('error', reject);
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
try {
|
||||
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||
const started = performance.now();
|
||||
const registered = await withTimeout(
|
||||
request(socket, { action: 'register_anchor', runtime_generation: 1 }),
|
||||
'registration behind queued silent connections',
|
||||
6_000,
|
||||
);
|
||||
const elapsed = performance.now() - started;
|
||||
|
||||
expect(registered.ok).toBe(true);
|
||||
expect(elapsed).toBeLessThan(1_500);
|
||||
} finally {
|
||||
for (const connection of silentConnections) connection.destroy();
|
||||
}
|
||||
});
|
||||
|
||||
test('silent peers are reaped at the concurrency bound and their slots are reclaimed', async () => {
|
||||
const { socket } = await startBroker();
|
||||
const concurrencyCap = 16;
|
||||
const peers = Array.from({ length: concurrencyCap }, () => {
|
||||
const connection = createConnection(socket);
|
||||
return {
|
||||
connection,
|
||||
connected: new Promise<void>((resolve, reject) => {
|
||||
connection.once('connect', resolve);
|
||||
connection.once('error', reject);
|
||||
}),
|
||||
closed: new Promise<void>((resolve) => connection.once('close', () => resolve())),
|
||||
};
|
||||
});
|
||||
|
||||
try {
|
||||
await Promise.all(peers.map(({ connected }) => connected));
|
||||
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||
const started = performance.now();
|
||||
const registration = withTimeout(
|
||||
request(socket, { action: 'register_anchor', runtime_generation: 1 }),
|
||||
'registration while silent peers hold the concurrency bound',
|
||||
2_500,
|
||||
);
|
||||
const reaping = withTimeout(
|
||||
Promise.all(peers.map(({ closed }) => closed)),
|
||||
'silent peer deadline reaping',
|
||||
2_500,
|
||||
);
|
||||
const [registered] = await Promise.all([registration, reaping]);
|
||||
const elapsed = performance.now() - started;
|
||||
|
||||
expect(registered.ok).toBe(true);
|
||||
expect(elapsed).toBeGreaterThan(500);
|
||||
expect(elapsed).toBeLessThan(2_500);
|
||||
} finally {
|
||||
for (const { connection } of peers) connection.destroy();
|
||||
}
|
||||
});
|
||||
|
||||
test('newline-only client without half-close gets no success and cannot block next request', async () => {
|
||||
const { socket } = await startBroker();
|
||||
const incomplete = createConnection(socket);
|
||||
let raw = '';
|
||||
incomplete.setEncoding('utf8');
|
||||
incomplete.on('data', (chunk: string) => (raw += chunk));
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
incomplete.once('connect', () => {
|
||||
incomplete.write(
|
||||
`${JSON.stringify({ action: 'register_anchor', runtime_generation: 1 })}\n`,
|
||||
);
|
||||
resolve();
|
||||
});
|
||||
incomplete.once('error', reject);
|
||||
});
|
||||
await new Promise((resolve) => setTimeout(resolve, 1_100));
|
||||
expect(raw).not.toContain('"ok":true');
|
||||
const registered = await withTimeout(
|
||||
request(socket, { action: 'register_anchor', runtime_generation: 1 }),
|
||||
'registration after non-half-closed client',
|
||||
);
|
||||
expect(registered.ok).toBe(true);
|
||||
incomplete.destroy();
|
||||
});
|
||||
|
||||
test('client disconnect cannot prevent the next valid authentication', async () => {
|
||||
const { socket } = await startBroker();
|
||||
const registered = await request(socket, { action: 'register_anchor', runtime_generation: 1 });
|
||||
const reset = createConnection(socket);
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
reset.once('connect', () => {
|
||||
reset.write(`${JSON.stringify({ action: 'register_anchor', runtime_generation: 1 })}\n`);
|
||||
reset.destroy();
|
||||
resolve();
|
||||
});
|
||||
reset.once('error', reject);
|
||||
});
|
||||
const authenticated = await withTimeout(
|
||||
request(socket, {
|
||||
action: 'authenticate',
|
||||
session_id: registered.session_id,
|
||||
runtime_generation: 1,
|
||||
}),
|
||||
'authentication after client disconnect',
|
||||
);
|
||||
expect(authenticated.ok).toBe(true);
|
||||
});
|
||||
|
||||
test('delayed second frame is rejected and the next request succeeds', async () => {
|
||||
const { socket } = await startBroker();
|
||||
const reply = await rawRequest(socket, (connection) => {
|
||||
connection.write(`${JSON.stringify({ action: 'register_anchor', runtime_generation: 1 })}\n`);
|
||||
setTimeout(() => connection.end('{}\n'), 50);
|
||||
});
|
||||
expect(reply).toMatchObject({ ok: false, code: 'MALFORMED_REQUEST' });
|
||||
expect(
|
||||
await request(socket, { action: 'register_anchor', runtime_generation: 1 }),
|
||||
).toMatchObject({
|
||||
ok: true,
|
||||
});
|
||||
});
|
||||
|
||||
test('unterminated frame is rejected and the next request succeeds', async () => {
|
||||
const { socket } = await startBroker();
|
||||
const reply = await rawRequest(socket, (connection) => connection.end('{}'));
|
||||
expect(reply).toMatchObject({ ok: false, code: 'MALFORMED_REQUEST' });
|
||||
expect(
|
||||
await request(socket, { action: 'register_anchor', runtime_generation: 1 }),
|
||||
).toMatchObject({
|
||||
ok: true,
|
||||
});
|
||||
});
|
||||
|
||||
test('genuinely oversized frame is rejected and the next request succeeds', async () => {
|
||||
const { socket } = await startBroker();
|
||||
const reply = await rawRequest(socket, (connection) =>
|
||||
connection.end(`${JSON.stringify({ padding: 'x'.repeat(64 * 1024) })}\n`),
|
||||
);
|
||||
expect(reply).toMatchObject({ ok: false, code: 'MALFORMED_REQUEST' });
|
||||
expect(
|
||||
await request(socket, { action: 'register_anchor', runtime_generation: 1 }),
|
||||
).toMatchObject({
|
||||
ok: true,
|
||||
});
|
||||
});
|
||||
|
||||
test('boolean runtime generations fail closed', async () => {
|
||||
const { socket } = await startBroker();
|
||||
expect(
|
||||
await request(socket, { action: 'register_anchor', runtime_generation: true }),
|
||||
).toMatchObject({ ok: false, code: 'INVALID_GENERATION' });
|
||||
const registered = await request(socket, { action: 'register_anchor', runtime_generation: 1 });
|
||||
expect(
|
||||
await request(socket, {
|
||||
action: 'authenticate',
|
||||
session_id: registered.session_id,
|
||||
runtime_generation: false,
|
||||
}),
|
||||
).toMatchObject({ ok: false, code: 'INVALID_IDENTITY' });
|
||||
});
|
||||
|
||||
test.each([
|
||||
{ compaction_epoch: true, request_epoch: 0, schema_version: 1 },
|
||||
{ compaction_epoch: 0, request_epoch: -1, schema_version: 1 },
|
||||
{ compaction_epoch: 0, request_epoch: 0, schema_version: false },
|
||||
{ compaction_epoch: 0, request_epoch: 0, schema_version: -1 },
|
||||
{ compaction_epoch: 0, request_epoch: 0, schema_version: 1, h_source: 'A'.repeat(64) },
|
||||
{ compaction_epoch: 0, request_epoch: 0, schema_version: 1, h_payload: 'a'.repeat(63) },
|
||||
])('invalid cycle binding fails closed without persisting a token (%j)', async (override) => {
|
||||
const { socket, state } = await startBroker();
|
||||
const registered = await request(socket, { action: 'register_anchor', runtime_generation: 1 });
|
||||
const binding = Object.assign(
|
||||
{
|
||||
compaction_epoch: 0,
|
||||
request_epoch: 0,
|
||||
h_source: 'a'.repeat(64),
|
||||
h_payload: 'b'.repeat(64),
|
||||
schema_version: 1,
|
||||
},
|
||||
override,
|
||||
);
|
||||
expect(
|
||||
await request(socket, {
|
||||
action: 'mint_token',
|
||||
session_id: registered.session_id,
|
||||
runtime_generation: 1,
|
||||
binding,
|
||||
}),
|
||||
).toMatchObject({ ok: false, code: 'INVALID_BINDING' });
|
||||
const persisted = JSON.parse(await readFile(state, 'utf8')) as { tokens: object };
|
||||
expect(persisted.tokens).toEqual({});
|
||||
});
|
||||
|
||||
test('StateStore write-all unit path handles partial writes and cleans failed temp files', () => {
|
||||
const result = spawnSync('python3', [join(import.meta.dirname, 'state_store_unittest.py')], {
|
||||
encoding: 'utf8',
|
||||
});
|
||||
expect(result.status, result.stderr).toBe(0);
|
||||
});
|
||||
|
||||
test('persistence integrity failure refuses startup', async () => {
|
||||
expect(await startBrokerWithState('{corrupt')).toContain('STATE_INTEGRITY');
|
||||
});
|
||||
|
||||
test.each([
|
||||
{ version: 1, sessions: {}, tokens: {}, unexpected: true },
|
||||
{ version: 1, sessions: { bad: {} }, tokens: {} },
|
||||
{
|
||||
version: 1,
|
||||
sessions: {
|
||||
['a'.repeat(64)]: { anchor_pid: true, anchor_starttime: '1', runtime_generation: 0 },
|
||||
},
|
||||
tokens: {},
|
||||
},
|
||||
{
|
||||
version: 1,
|
||||
sessions: {
|
||||
['a'.repeat(64)]: { anchor_pid: 1, anchor_starttime: '01', runtime_generation: 0 },
|
||||
},
|
||||
tokens: {},
|
||||
},
|
||||
{
|
||||
version: 1,
|
||||
sessions: {
|
||||
['a'.repeat(64)]: { anchor_pid: 1, anchor_starttime: '1', runtime_generation: 0 },
|
||||
['b'.repeat(64)]: { anchor_pid: 1, anchor_starttime: '1', runtime_generation: 1 },
|
||||
},
|
||||
tokens: {},
|
||||
},
|
||||
{
|
||||
version: 1,
|
||||
sessions: {
|
||||
['a'.repeat(64)]: { anchor_pid: 1, anchor_starttime: '1', runtime_generation: 0 },
|
||||
},
|
||||
tokens: {
|
||||
['b'.repeat(64)]: {
|
||||
session_id: 'c'.repeat(64),
|
||||
runtime_generation: 0,
|
||||
binding: {
|
||||
compaction_epoch: 0,
|
||||
request_epoch: 0,
|
||||
h_source: 'd'.repeat(64),
|
||||
h_payload: 'e'.repeat(64),
|
||||
schema_version: 1,
|
||||
},
|
||||
consumed: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
version: 1,
|
||||
sessions: {
|
||||
['a'.repeat(64)]: { anchor_pid: 1, anchor_starttime: '1', runtime_generation: 1 },
|
||||
},
|
||||
tokens: {
|
||||
['b'.repeat(64)]: {
|
||||
session_id: 'a'.repeat(64),
|
||||
runtime_generation: 2,
|
||||
binding: {
|
||||
compaction_epoch: 0,
|
||||
request_epoch: 0,
|
||||
h_source: 'd'.repeat(64),
|
||||
h_payload: 'e'.repeat(64),
|
||||
schema_version: 1,
|
||||
},
|
||||
consumed: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
])('nested corrupt state refuses startup (%#)', async (stateValue) => {
|
||||
expect(await startBrokerWithState(JSON.stringify(stateValue))).toContain('STATE_INTEGRITY');
|
||||
});
|
||||
|
||||
test('symlink state refuses startup', async () => {
|
||||
const root = await mkdtemp(join(tmpdir(), 'mosaic-lease-broker-'));
|
||||
await chmod(root, 0o700);
|
||||
const target = join(root, 'target.json');
|
||||
const state = join(root, 'state.json');
|
||||
await writeFile(target, JSON.stringify({ version: 1, sessions: {}, tokens: {} }), {
|
||||
mode: 0o600,
|
||||
});
|
||||
await symlink(target, state);
|
||||
const child = spawn('python3', [
|
||||
daemonPath,
|
||||
'--socket',
|
||||
join(root, 'broker.sock'),
|
||||
'--state',
|
||||
state,
|
||||
]);
|
||||
children.push(child);
|
||||
const stderr = await new Promise<string>((resolve) => {
|
||||
let raw = '';
|
||||
child.stderr?.on('data', (chunk: Buffer) => (raw += chunk.toString()));
|
||||
child.once('exit', () => resolve(raw));
|
||||
});
|
||||
expect(stderr).toContain('STATE_INTEGRITY');
|
||||
});
|
||||
|
||||
test('oversized state refuses startup', async () => {
|
||||
expect(await startBrokerWithState(' '.repeat(4 * 1024 * 1024 + 1))).toContain(
|
||||
'STATE_INTEGRITY',
|
||||
);
|
||||
});
|
||||
});
|
||||
333
packages/mosaic/src/lease-broker/state_store_unittest.py
Normal file
333
packages/mosaic/src/lease-broker/state_store_unittest.py
Normal file
@@ -0,0 +1,333 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Standard-library edge tests for lease-broker atomic state persistence."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import copy
|
||||
import importlib.util
|
||||
import json
|
||||
import os
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
from unittest.mock import patch
|
||||
|
||||
|
||||
DAEMON_PATH = Path(__file__).parents[2] / "framework/tools/lease-broker/daemon.py"
|
||||
SPEC = importlib.util.spec_from_file_location("lease_broker_daemon", DAEMON_PATH)
|
||||
if SPEC is None or SPEC.loader is None:
|
||||
raise RuntimeError("unable to load lease broker daemon")
|
||||
DAEMON = importlib.util.module_from_spec(SPEC)
|
||||
SPEC.loader.exec_module(DAEMON)
|
||||
|
||||
|
||||
class StateStoreCommitTest(unittest.TestCase):
|
||||
def make_store(self, root: Path):
|
||||
os.chmod(root, 0o700)
|
||||
store = DAEMON.StateStore(root / "state.json")
|
||||
store.value["marker"] = "partial-write-proof"
|
||||
return store
|
||||
|
||||
def test_partial_writes_persist_the_complete_payload(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
root = Path(directory)
|
||||
store = self.make_store(root)
|
||||
real_write = os.write
|
||||
|
||||
def partial_write(descriptor: int, payload: bytes) -> int:
|
||||
return real_write(descriptor, payload[: max(1, len(payload) // 3)])
|
||||
|
||||
with patch.object(DAEMON.os, "write", side_effect=partial_write):
|
||||
store.commit()
|
||||
|
||||
self.assertEqual(json.loads(store.path.read_text()), store.value)
|
||||
|
||||
def test_zero_progress_removes_owned_temporary_file(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
root = Path(directory)
|
||||
store = self.make_store(root)
|
||||
with patch.object(DAEMON.os, "write", return_value=0):
|
||||
with self.assertRaises(OSError):
|
||||
store.commit()
|
||||
|
||||
self.assertFalse(store.path.exists())
|
||||
self.assertEqual(list(root.glob(".*.tmp")), [])
|
||||
|
||||
def test_oversized_payload_is_refused_before_replacing_state(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
root = Path(directory)
|
||||
store = self.make_store(root)
|
||||
store.value.pop("marker")
|
||||
store.commit()
|
||||
durable = store.path.read_bytes()
|
||||
store.value["oversized"] = "x" * DAEMON.MAX_STATE
|
||||
|
||||
with patch.object(DAEMON.os, "open", wraps=os.open) as mocked_open:
|
||||
with self.assertRaisesRegex(DAEMON.BrokerFailure, "STATE_TOO_LARGE"):
|
||||
store.commit()
|
||||
|
||||
self.assertEqual(mocked_open.call_count, 0)
|
||||
self.assertEqual(store.path.read_bytes(), durable)
|
||||
self.assertEqual(list(root.glob(".*.tmp")), [])
|
||||
|
||||
|
||||
class StateStoreValidationTest(unittest.TestCase):
|
||||
@staticmethod
|
||||
def binding() -> dict[str, object]:
|
||||
return {
|
||||
"compaction_epoch": 0,
|
||||
"request_epoch": 0,
|
||||
"h_source": "a" * 64,
|
||||
"h_payload": "b" * 64,
|
||||
"schema_version": 1,
|
||||
}
|
||||
|
||||
def test_impossible_or_over_capacity_token_state_is_rejected(self) -> None:
|
||||
session_id = "1" * 64
|
||||
session = {
|
||||
"anchor_pid": 123,
|
||||
"anchor_starttime": "456",
|
||||
"runtime_generation": 2,
|
||||
}
|
||||
live_token = {
|
||||
"session_id": session_id,
|
||||
"runtime_generation": 2,
|
||||
"binding": self.binding(),
|
||||
"consumed": False,
|
||||
}
|
||||
cases = {
|
||||
"stale generation": {
|
||||
"2" * 64: {**live_token, "runtime_generation": 1},
|
||||
},
|
||||
"consumed token": {
|
||||
"2" * 64: {**live_token, "consumed": True},
|
||||
},
|
||||
"over capacity": {
|
||||
f"{index:064x}": copy.deepcopy(live_token)
|
||||
for index in range(DAEMON.MAX_PENDING_TOKENS + 1)
|
||||
},
|
||||
}
|
||||
|
||||
for label, tokens in cases.items():
|
||||
with self.subTest(label=label), tempfile.TemporaryDirectory() as directory:
|
||||
root = Path(directory)
|
||||
os.chmod(root, 0o700)
|
||||
state_path = root / "state.json"
|
||||
state_path.write_text(json.dumps({
|
||||
"version": 1,
|
||||
"sessions": {session_id: session},
|
||||
"tokens": tokens,
|
||||
}))
|
||||
os.chmod(state_path, 0o600)
|
||||
|
||||
with self.assertRaisesRegex(DAEMON.BrokerFailure, "STATE_INTEGRITY"):
|
||||
DAEMON.StateStore(state_path)
|
||||
|
||||
|
||||
class BrokerBehaviorTest(unittest.TestCase):
|
||||
def make_broker(self, root: Path):
|
||||
os.chmod(root, 0o700)
|
||||
return DAEMON.Broker(DAEMON.StateStore(root / "state.json"))
|
||||
|
||||
@staticmethod
|
||||
def binding() -> dict[str, object]:
|
||||
return {
|
||||
"compaction_epoch": 0,
|
||||
"request_epoch": 0,
|
||||
"h_source": "a" * 64,
|
||||
"h_payload": "b" * 64,
|
||||
"schema_version": 1,
|
||||
}
|
||||
|
||||
def register(self, broker, generation: int = 1) -> str:
|
||||
response = broker.handle((123, 1000, 1000), {
|
||||
"action": "register_anchor",
|
||||
"runtime_generation": generation,
|
||||
})
|
||||
return response["session_id"]
|
||||
|
||||
def mint(self, broker, session_id: str, generation: int = 1) -> str:
|
||||
response = broker.handle((123, 1000, 1000), {
|
||||
"action": "mint_token",
|
||||
"session_id": session_id,
|
||||
"runtime_generation": generation,
|
||||
"binding": self.binding(),
|
||||
})
|
||||
return response["token"]
|
||||
|
||||
def consume(self, broker, session_id: str, token: str, generation: int = 1):
|
||||
return broker.handle((123, 1000, 1000), {
|
||||
"action": "consume_token",
|
||||
"session_id": session_id,
|
||||
"runtime_generation": generation,
|
||||
"token": token,
|
||||
})
|
||||
|
||||
def test_anchor_generation_bump_reuses_session_and_revokes_token(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
root = Path(directory)
|
||||
broker = self.make_broker(root)
|
||||
with (
|
||||
patch.object(
|
||||
DAEMON,
|
||||
"proc_node",
|
||||
return_value={"pid": 123, "ppid": 1, "starttime": "456"},
|
||||
),
|
||||
patch.object(DAEMON, "verified_ancestry", return_value=True),
|
||||
):
|
||||
first = broker.handle((123, 1000, 1000), {
|
||||
"action": "register_anchor",
|
||||
"runtime_generation": 1,
|
||||
})
|
||||
minted = broker.handle((123, 1000, 1000), {
|
||||
"action": "mint_token",
|
||||
"session_id": first["session_id"],
|
||||
"runtime_generation": 1,
|
||||
"binding": self.binding(),
|
||||
})
|
||||
bumped = broker.handle((123, 1000, 1000), {
|
||||
"action": "register_anchor",
|
||||
"runtime_generation": 2,
|
||||
})
|
||||
repeated = broker.handle((123, 1000, 1000), {
|
||||
"action": "register_anchor",
|
||||
"runtime_generation": 2,
|
||||
})
|
||||
|
||||
self.assertEqual(bumped["session_id"], first["session_id"])
|
||||
self.assertEqual(repeated["session_id"], first["session_id"])
|
||||
self.assertNotIn(minted["token"], broker.store.tokens())
|
||||
restarted = self.make_broker(root)
|
||||
self.assertEqual(restarted.store.tokens(), {})
|
||||
self.assertEqual(
|
||||
restarted.store.sessions()[first["session_id"]]["runtime_generation"], 2
|
||||
)
|
||||
with self.assertRaisesRegex(DAEMON.BrokerFailure, "STALE_GENERATION"):
|
||||
with patch.object(DAEMON, "proc_node", return_value={"starttime": "456"}):
|
||||
broker.handle((123, 1000, 1000), {
|
||||
"action": "register_anchor",
|
||||
"runtime_generation": 1,
|
||||
})
|
||||
|
||||
def test_successful_consume_deletes_token_and_replay_is_refused(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as directory, (
|
||||
patch.object(DAEMON, "proc_node", return_value={"pid": 123, "ppid": 1, "starttime": "456"})
|
||||
), patch.object(DAEMON, "verified_ancestry", return_value=True):
|
||||
broker = self.make_broker(Path(directory))
|
||||
session_id = self.register(broker)
|
||||
token = self.mint(broker, session_id)
|
||||
|
||||
self.assertEqual(self.consume(broker, session_id, token), {"ok": True})
|
||||
self.assertNotIn(token, broker.store.tokens())
|
||||
with self.assertRaisesRegex(DAEMON.BrokerFailure, "TOKEN_REPLAY"):
|
||||
self.consume(broker, session_id, token)
|
||||
|
||||
def test_normal_cycles_remain_bounded_and_restartable(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as directory, (
|
||||
patch.object(DAEMON, "proc_node", return_value={"pid": 123, "ppid": 1, "starttime": "456"})
|
||||
), patch.object(DAEMON, "verified_ancestry", return_value=True):
|
||||
root = Path(directory)
|
||||
broker = self.make_broker(root)
|
||||
session_id = self.register(broker)
|
||||
|
||||
for _ in range(DAEMON.MAX_PENDING_TOKENS * 3):
|
||||
self.consume(broker, session_id, self.mint(broker, session_id))
|
||||
|
||||
self.assertEqual(broker.store.tokens(), {})
|
||||
self.assertLess((root / "state.json").stat().st_size, DAEMON.MAX_STATE)
|
||||
restarted = self.make_broker(root)
|
||||
self.assertEqual(restarted.store.tokens(), {})
|
||||
self.assertIn(session_id, restarted.store.sessions())
|
||||
|
||||
def test_pending_token_capacity_refusal_does_not_mutate_state(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as directory, (
|
||||
patch.object(DAEMON, "proc_node", return_value={"pid": 123, "ppid": 1, "starttime": "456"})
|
||||
), patch.object(DAEMON, "verified_ancestry", return_value=True):
|
||||
root = Path(directory)
|
||||
broker = self.make_broker(root)
|
||||
session_id = self.register(broker)
|
||||
for _ in range(DAEMON.MAX_PENDING_TOKENS):
|
||||
self.mint(broker, session_id)
|
||||
before = copy.deepcopy(broker.store.value)
|
||||
durable = broker.store.path.read_bytes()
|
||||
|
||||
with self.assertRaisesRegex(DAEMON.BrokerFailure, "TOKEN_CAPACITY"):
|
||||
self.mint(broker, session_id)
|
||||
|
||||
self.assertEqual(broker.store.value, before)
|
||||
self.assertEqual(broker.store.path.read_bytes(), durable)
|
||||
|
||||
def test_directory_fsync_failure_poisoned_store_cannot_continue(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as directory, patch.object(
|
||||
DAEMON,
|
||||
"proc_node",
|
||||
return_value={"pid": 123, "ppid": 1, "starttime": "456"},
|
||||
):
|
||||
root = Path(directory)
|
||||
broker = self.make_broker(root)
|
||||
real_fsync = os.fsync
|
||||
|
||||
def fail_directory_fsync(descriptor: int) -> None:
|
||||
if os.path.isdir(f"/proc/self/fd/{descriptor}"):
|
||||
raise OSError("directory fsync failed")
|
||||
real_fsync(descriptor)
|
||||
|
||||
with patch.object(DAEMON.os, "fsync", side_effect=fail_directory_fsync):
|
||||
with self.assertRaisesRegex(
|
||||
DAEMON.StateCommitUncertain, "STATE_COMMIT_UNCERTAIN"
|
||||
):
|
||||
self.register(broker)
|
||||
|
||||
durable = json.loads(broker.store.path.read_text())
|
||||
self.assertEqual(broker.store.value, durable)
|
||||
self.assertTrue(broker.store.poisoned)
|
||||
before = copy.deepcopy(broker.store.value)
|
||||
with self.assertRaisesRegex(
|
||||
DAEMON.StateCommitUncertain, "STATE_COMMIT_UNCERTAIN"
|
||||
):
|
||||
broker.handle((123, 1000, 1000), {
|
||||
"action": "register_anchor",
|
||||
"runtime_generation": 2,
|
||||
})
|
||||
self.assertEqual(broker.store.value, before)
|
||||
|
||||
def test_commit_failures_before_replace_roll_back_every_broker_mutation(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as directory, (
|
||||
patch.object(DAEMON, "proc_node", return_value={"pid": 123, "ppid": 1, "starttime": "456"})
|
||||
), patch.object(DAEMON, "verified_ancestry", return_value=True):
|
||||
root = Path(directory)
|
||||
broker = self.make_broker(root)
|
||||
session_id = self.register(broker)
|
||||
token = self.mint(broker, session_id)
|
||||
|
||||
def assert_rollback(request: dict[str, object]) -> None:
|
||||
before = copy.deepcopy(broker.store.value)
|
||||
durable = broker.store.path.read_bytes()
|
||||
with patch.object(broker.store, "commit", side_effect=OSError("fsync failed")):
|
||||
with self.assertRaisesRegex(OSError, "fsync failed"):
|
||||
broker.handle((123, 1000, 1000), request)
|
||||
self.assertEqual(broker.store.value, before)
|
||||
self.assertEqual(broker.store.path.read_bytes(), durable)
|
||||
|
||||
assert_rollback({"action": "register_anchor", "runtime_generation": 2})
|
||||
assert_rollback({
|
||||
"action": "mint_token", "session_id": session_id,
|
||||
"runtime_generation": 1, "binding": self.binding(),
|
||||
})
|
||||
assert_rollback({
|
||||
"action": "consume_token", "session_id": session_id,
|
||||
"runtime_generation": 1, "token": token,
|
||||
})
|
||||
|
||||
with tempfile.TemporaryDirectory() as second_directory:
|
||||
second = self.make_broker(Path(second_directory))
|
||||
with patch.object(second.store, "commit", side_effect=OSError("fsync failed")):
|
||||
with self.assertRaisesRegex(OSError, "fsync failed"):
|
||||
self.register(second)
|
||||
self.assertEqual(
|
||||
second.store.value, {"version": 1, "sessions": {}, "tokens": {}}
|
||||
)
|
||||
self.assertFalse(second.store.path.exists())
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
657
packages/mosaic/src/mutator-gate/mutator-gate.acceptance.spec.ts
Normal file
657
packages/mosaic/src/mutator-gate/mutator-gate.acceptance.spec.ts
Normal file
@@ -0,0 +1,657 @@
|
||||
import { chmod, mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { join } from 'node:path';
|
||||
import { spawn, spawnSync, type ChildProcess } from 'node:child_process';
|
||||
|
||||
import { afterEach, describe, expect, test } from 'vitest';
|
||||
|
||||
import { launchClaudex, type ClaudexHarnessAdapter } from '../commands/claudex.js';
|
||||
import { requestBrokerReply } from '../lease-broker/broker-test-client.js';
|
||||
|
||||
interface BrokerReply {
|
||||
ok: boolean;
|
||||
code?: string;
|
||||
decision?: 'allow' | 'deny';
|
||||
state?: 'UNVERIFIED' | 'PENDING_VERIFICATION' | 'VERIFIED';
|
||||
session_id?: string;
|
||||
promotion_token?: string;
|
||||
}
|
||||
|
||||
interface BrokerPaths {
|
||||
socket: string;
|
||||
}
|
||||
|
||||
const frameworkRoot = new URL('../../framework/', import.meta.url).pathname;
|
||||
const daemonPath = join(frameworkRoot, 'tools/lease-broker/daemon.py');
|
||||
const gatePath = join(frameworkRoot, 'tools/lease-broker/mutator-gate.py');
|
||||
const launchGuardPath = join(frameworkRoot, 'tools/lease-broker/check-runtime-launches.py');
|
||||
const launcherPath = join(frameworkRoot, 'tools/lease-broker/launch-runtime.py');
|
||||
const claudeSettingsPath = join(frameworkRoot, 'runtime/claude/settings.json');
|
||||
const piExtensionPath = join(frameworkRoot, 'runtime/pi/mosaic-extension.ts');
|
||||
const prdyInitPath = join(frameworkRoot, 'tools/prdy/prdy-init.sh');
|
||||
const prdyUpdatePath = join(frameworkRoot, 'tools/prdy/prdy-update.sh');
|
||||
const remediationHandlerPath = join(frameworkRoot, 'tools/qa/remediation-hook-handler.sh');
|
||||
const children: ChildProcess[] = [];
|
||||
const temporaryRoots: string[] = [];
|
||||
|
||||
const binding = (compaction_epoch = 1) => ({
|
||||
compaction_epoch,
|
||||
request_epoch: 0,
|
||||
h_source: 'a'.repeat(64),
|
||||
h_payload: 'b'.repeat(64),
|
||||
schema_version: 1,
|
||||
});
|
||||
|
||||
async function request(socketPath: string, requestValue: object): Promise<BrokerReply> {
|
||||
return await requestBrokerReply<BrokerReply>(socketPath, requestValue);
|
||||
}
|
||||
|
||||
async function startBroker(): Promise<BrokerPaths> {
|
||||
const root = await mkdtemp(join(tmpdir(), 'mosaic-mutator-gate-'));
|
||||
await chmod(root, 0o700);
|
||||
const socket = join(root, 'broker.sock');
|
||||
const child = spawn(
|
||||
'python3',
|
||||
[daemonPath, '--socket', socket, '--state', join(root, 'state.json')],
|
||||
{
|
||||
stdio: ['ignore', 'pipe', 'pipe'],
|
||||
},
|
||||
);
|
||||
children.push(child);
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
let stderr = '';
|
||||
child.stderr?.setEncoding('utf8');
|
||||
child.stderr?.on('data', (chunk: string) => (stderr += chunk));
|
||||
child.once('error', reject);
|
||||
child.once('exit', (code: number | null) =>
|
||||
reject(new Error(`broker exited ${code}: ${stderr}`)),
|
||||
);
|
||||
child.stdout?.once('data', () => resolve());
|
||||
});
|
||||
return { socket };
|
||||
}
|
||||
|
||||
interface RuntimeLaunchEntry {
|
||||
name: string;
|
||||
script: string;
|
||||
prepare(root: string): Promise<string[]>;
|
||||
}
|
||||
|
||||
const runtimeLaunchEntries: RuntimeLaunchEntry[] = [
|
||||
{
|
||||
name: 'prdy-init',
|
||||
script: prdyInitPath,
|
||||
prepare: async (root) => ['--project', root, '--name', 'Gate Test'],
|
||||
},
|
||||
{
|
||||
name: 'prdy-update',
|
||||
script: prdyUpdatePath,
|
||||
prepare: async (root) => {
|
||||
await mkdir(join(root, 'docs'), { recursive: true });
|
||||
await writeFile(join(root, 'docs/PRD.md'), '# Existing PRD\n');
|
||||
return ['--project', root];
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'qa-remediation',
|
||||
script: remediationHandlerPath,
|
||||
prepare: async (root) => {
|
||||
const pending = join(root, 'reports/pending');
|
||||
await mkdir(pending, { recursive: true });
|
||||
const report = join(pending, 'gate_remediation_needed.md');
|
||||
await writeFile(report, '# remediation\n');
|
||||
return [report];
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
async function runRuntimeLaunchEntry(entry: RuntimeLaunchEntry, socket: string) {
|
||||
const root = await mkdtemp(join(tmpdir(), `mosaic-${entry.name}-gate-`));
|
||||
temporaryRoots.push(root);
|
||||
const binDir = join(root, 'bin');
|
||||
await mkdir(binDir, { recursive: true });
|
||||
const fakeClaude = join(binDir, 'claude');
|
||||
await writeFile(
|
||||
fakeClaude,
|
||||
`#!/usr/bin/env python3
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
|
||||
session_id = os.environ.get("MOSAIC_LEASE_SESSION_ID", "")
|
||||
denied = subprocess.run(
|
||||
["python3", ${JSON.stringify(gatePath)}, "--runtime", "claude"],
|
||||
input=json.dumps({"tool_name": "Bash"}) + "\\n",
|
||||
text=True,
|
||||
capture_output=True,
|
||||
env=os.environ,
|
||||
).returncode == 2
|
||||
print("RUNTIME_PROBE=" + json.dumps({"session_id": session_id, "denied": denied}))
|
||||
raise SystemExit(0 if len(session_id) == 64 and denied else 1)
|
||||
`,
|
||||
{ mode: 0o700 },
|
||||
);
|
||||
await chmod(fakeClaude, 0o700);
|
||||
const args = await entry.prepare(root);
|
||||
return spawnSync('bash', [entry.script, ...args], {
|
||||
cwd: root,
|
||||
encoding: 'utf8',
|
||||
env: {
|
||||
...process.env,
|
||||
PATH: `${binDir}:${process.env.PATH ?? ''}`,
|
||||
MOSAIC_HOME: frameworkRoot,
|
||||
MOSAIC_PRDY_RUNTIME: 'claude',
|
||||
MOSAIC_LEASE_BROKER_SOCKET: socket,
|
||||
MOSAIC_RUNTIME_GENERATION: '1',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async function register(socket: string, runtime_generation = 1): Promise<string> {
|
||||
const reply = await request(socket, { action: 'register_anchor', runtime_generation });
|
||||
expect(reply.ok).toBe(true);
|
||||
expect(reply.session_id).toMatch(/^[a-f0-9]{64}$/);
|
||||
return reply.session_id!;
|
||||
}
|
||||
|
||||
async function beginVerification(
|
||||
socket: string,
|
||||
session_id: string,
|
||||
runtime: 'claude' | 'pi',
|
||||
runtime_generation = 1,
|
||||
ttl_seconds = 300,
|
||||
compactionEpoch = 1,
|
||||
): Promise<BrokerReply> {
|
||||
return await request(socket, {
|
||||
action: 'begin_verification',
|
||||
session_id,
|
||||
runtime_generation,
|
||||
runtime,
|
||||
ttl_seconds,
|
||||
binding: binding(compactionEpoch),
|
||||
});
|
||||
}
|
||||
|
||||
async function promote(
|
||||
socket: string,
|
||||
session_id: string,
|
||||
promotion_token: string,
|
||||
runtime_generation = 1,
|
||||
): Promise<BrokerReply> {
|
||||
return await request(socket, {
|
||||
action: 'promote_lease',
|
||||
session_id,
|
||||
runtime_generation,
|
||||
promotion_token,
|
||||
});
|
||||
}
|
||||
|
||||
async function authorize(
|
||||
socket: string,
|
||||
session_id: string,
|
||||
runtime: 'claude' | 'pi',
|
||||
tool_name: string,
|
||||
runtime_generation = 1,
|
||||
): Promise<BrokerReply> {
|
||||
return await request(socket, {
|
||||
action: 'authorize_tool',
|
||||
session_id,
|
||||
runtime_generation,
|
||||
runtime,
|
||||
tool_name,
|
||||
});
|
||||
}
|
||||
|
||||
function runRuntimeGate(
|
||||
socket: string,
|
||||
sessionId: string,
|
||||
runtime: 'claude' | 'pi',
|
||||
toolName: string,
|
||||
generation = 1,
|
||||
) {
|
||||
return spawnSync('python3', [gatePath, '--runtime', runtime], {
|
||||
input: `${JSON.stringify({ tool_name: toolName })}\n`,
|
||||
encoding: 'utf8',
|
||||
env: {
|
||||
...process.env,
|
||||
MOSAIC_LEASE_BROKER_SOCKET: socket,
|
||||
MOSAIC_LEASE_SESSION_ID: sessionId,
|
||||
MOSAIC_RUNTIME_GENERATION: String(generation),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
afterEach(async () => {
|
||||
for (const child of children.splice(0)) child.kill('SIGTERM');
|
||||
await Promise.all(
|
||||
temporaryRoots.splice(0).map((root) => rm(root, { recursive: true, force: true })),
|
||||
);
|
||||
});
|
||||
|
||||
describe('whole mutator-class lease gate', () => {
|
||||
test('revoke-first and promote-last structurally bracket mutator authority', async () => {
|
||||
const { socket } = await startBroker();
|
||||
const sessionId = await register(socket);
|
||||
|
||||
expect(await promote(socket, sessionId, 'c'.repeat(64))).toMatchObject({
|
||||
ok: false,
|
||||
code: 'INVALID_LEASE_TRANSITION',
|
||||
});
|
||||
expect(await authorize(socket, sessionId, 'claude', 'Bash')).toMatchObject({
|
||||
ok: false,
|
||||
code: 'MUTATOR_UNVERIFIED',
|
||||
decision: 'deny',
|
||||
});
|
||||
|
||||
const pending = await beginVerification(socket, sessionId, 'claude');
|
||||
expect(pending).toMatchObject({ ok: true, state: 'PENDING_VERIFICATION' });
|
||||
expect(pending.promotion_token).toMatch(/^[a-f0-9]{64}$/);
|
||||
expect(await authorize(socket, sessionId, 'claude', 'Write')).toMatchObject({
|
||||
ok: false,
|
||||
decision: 'deny',
|
||||
});
|
||||
|
||||
expect(await promote(socket, sessionId, pending.promotion_token!)).toMatchObject({
|
||||
ok: true,
|
||||
state: 'VERIFIED',
|
||||
});
|
||||
expect(await authorize(socket, sessionId, 'claude', 'Bash')).toMatchObject({
|
||||
ok: true,
|
||||
decision: 'allow',
|
||||
state: 'VERIFIED',
|
||||
});
|
||||
|
||||
const nextCycle = await beginVerification(socket, sessionId, 'claude', 1, 300, 2);
|
||||
expect(nextCycle).toMatchObject({ ok: true, state: 'PENDING_VERIFICATION' });
|
||||
expect(await authorize(socket, sessionId, 'claude', 'Edit')).toMatchObject({
|
||||
ok: false,
|
||||
decision: 'deny',
|
||||
});
|
||||
expect(await promote(socket, sessionId, pending.promotion_token!)).toMatchObject({
|
||||
ok: false,
|
||||
code: 'PROMOTION_TOKEN_MISMATCH',
|
||||
});
|
||||
});
|
||||
|
||||
test('non-dangerous parser residual is denied by the global all-tools hook without a lease', async () => {
|
||||
const root = await mkdtemp(join(tmpdir(), 'mosaic-parser-residual-'));
|
||||
temporaryRoots.push(root);
|
||||
const source = join(root, 'packages/probe/launch.sh');
|
||||
await mkdir(join(root, 'packages/probe'), { recursive: true });
|
||||
await writeFile(source, 'alias hidden_runtime=claude\nhidden_runtime -p x\n');
|
||||
|
||||
const parserResult = spawnSync('python3', [launchGuardPath, '--root', root, '--json'], {
|
||||
encoding: 'utf8',
|
||||
});
|
||||
expect(parserResult.status).toBe(0);
|
||||
expect(JSON.parse(parserResult.stdout)).toMatchObject({ gated: 0, total: 0 });
|
||||
|
||||
const settings = JSON.parse(await readFile(claudeSettingsPath, 'utf8')) as {
|
||||
hooks: { PreToolUse: Array<{ matcher: string; hooks: Array<{ command: string }> }> };
|
||||
};
|
||||
const allToolsHook = settings.hooks.PreToolUse.find((hook) => hook.matcher === '.*');
|
||||
expect(allToolsHook?.hooks[0]?.command).toContain('mutator-gate.py --runtime claude');
|
||||
|
||||
const environment = { ...process.env };
|
||||
delete environment['MOSAIC_LEASE_SESSION_ID'];
|
||||
delete environment['MOSAIC_LEASE_BROKER_SOCKET'];
|
||||
for (const toolName of ['Bash', 'Read', 'mcp__provider__custom']) {
|
||||
const gateResult = spawnSync('python3', [gatePath, '--runtime', 'claude'], {
|
||||
input: `${JSON.stringify({ tool_name: toolName })}\n`,
|
||||
encoding: 'utf8',
|
||||
env: environment,
|
||||
});
|
||||
expect(gateResult.status, toolName).toBe(2);
|
||||
expect(gateResult.stderr, toolName).toContain('GATE_UNAVAILABLE');
|
||||
}
|
||||
});
|
||||
|
||||
test('T-B raw and custom mutator tools are default-denied without shell parsing', async () => {
|
||||
const { socket } = await startBroker();
|
||||
const sessionId = await register(socket);
|
||||
const mutators: Array<['claude' | 'pi', string]> = [
|
||||
['claude', 'Bash'],
|
||||
['claude', 'Edit'],
|
||||
['claude', 'Write'],
|
||||
['claude', 'NotebookEdit'],
|
||||
['claude', 'mcp__provider__close_issue'],
|
||||
['pi', 'bash'],
|
||||
['pi', 'edit'],
|
||||
['pi', 'write'],
|
||||
['pi', 'deploy'],
|
||||
['pi', 'unknown_custom_tool'],
|
||||
];
|
||||
|
||||
for (const [runtime, toolName] of mutators) {
|
||||
expect(
|
||||
await authorize(socket, sessionId, runtime, toolName),
|
||||
`${runtime}:${toolName}`,
|
||||
).toMatchObject({
|
||||
ok: false,
|
||||
code: 'MUTATOR_UNVERIFIED',
|
||||
decision: 'deny',
|
||||
});
|
||||
}
|
||||
|
||||
for (const [runtime, toolName] of [
|
||||
['claude', 'Read'],
|
||||
['claude', 'Grep'],
|
||||
['pi', 'read'],
|
||||
['pi', 'grep'],
|
||||
['pi', 'mosaic_context_recover'],
|
||||
] as const) {
|
||||
expect(await authorize(socket, sessionId, runtime, toolName)).toMatchObject({
|
||||
ok: true,
|
||||
decision: 'allow',
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
test('lease and tool validation failures remain fail-closed at the broker boundary', async () => {
|
||||
const { socket } = await startBroker();
|
||||
const sessionId = await register(socket);
|
||||
const baseRequest = {
|
||||
action: 'begin_verification',
|
||||
session_id: sessionId,
|
||||
runtime_generation: 1,
|
||||
runtime: 'claude',
|
||||
ttl_seconds: 300,
|
||||
binding: binding(),
|
||||
};
|
||||
|
||||
expect(await request(socket, { ...baseRequest, runtime: 'codex' })).toMatchObject({
|
||||
ok: false,
|
||||
code: 'INVALID_RUNTIME',
|
||||
});
|
||||
expect(
|
||||
await request(socket, { ...baseRequest, binding: { ...binding(), h_source: 'bad' } }),
|
||||
).toMatchObject({
|
||||
ok: false,
|
||||
code: 'INVALID_BINDING',
|
||||
});
|
||||
expect(await request(socket, { ...baseRequest, ttl_seconds: 0 })).toMatchObject({
|
||||
ok: false,
|
||||
code: 'INVALID_LEASE_TTL',
|
||||
});
|
||||
expect(
|
||||
await request(socket, {
|
||||
action: 'authorize_tool',
|
||||
session_id: sessionId,
|
||||
runtime_generation: 1,
|
||||
runtime: 'codex',
|
||||
tool_name: 'Read',
|
||||
}),
|
||||
).toMatchObject({ ok: false, code: 'INVALID_RUNTIME' });
|
||||
expect(
|
||||
await request(socket, {
|
||||
action: 'authorize_tool',
|
||||
session_id: sessionId,
|
||||
runtime_generation: 1,
|
||||
runtime: 'claude',
|
||||
tool_name: 'x'.repeat(257),
|
||||
}),
|
||||
).toMatchObject({ ok: false, code: 'INVALID_TOOL' });
|
||||
});
|
||||
|
||||
test('observer revocation and monotonic TTL expiry deny the next mutator', async () => {
|
||||
const { socket } = await startBroker();
|
||||
const sessionId = await register(socket);
|
||||
const pending = await beginVerification(socket, sessionId, 'claude', 1, 1);
|
||||
await promote(socket, sessionId, pending.promotion_token!);
|
||||
|
||||
expect(await authorize(socket, sessionId, 'claude', 'Bash')).toMatchObject({
|
||||
ok: true,
|
||||
decision: 'allow',
|
||||
});
|
||||
await new Promise((resolve) => setTimeout(resolve, 1_100));
|
||||
expect(await authorize(socket, sessionId, 'claude', 'Bash')).toMatchObject({
|
||||
ok: false,
|
||||
code: 'LEASE_EXPIRED',
|
||||
decision: 'deny',
|
||||
});
|
||||
|
||||
const refreshed = await beginVerification(socket, sessionId, 'claude', 1, 300, 2);
|
||||
await promote(socket, sessionId, refreshed.promotion_token!);
|
||||
expect(
|
||||
await request(socket, {
|
||||
action: 'revoke_lease',
|
||||
session_id: sessionId,
|
||||
runtime_generation: 1,
|
||||
reason: 'compaction_observer',
|
||||
}),
|
||||
).toMatchObject({ ok: true, state: 'UNVERIFIED' });
|
||||
expect(await authorize(socket, sessionId, 'claude', 'Write')).toMatchObject({
|
||||
ok: false,
|
||||
code: 'MUTATOR_UNVERIFIED',
|
||||
decision: 'deny',
|
||||
});
|
||||
});
|
||||
|
||||
test('runtime-generation replacement cannot inherit a verified lease', async () => {
|
||||
const { socket } = await startBroker();
|
||||
const sessionId = await register(socket);
|
||||
const pending = await beginVerification(socket, sessionId, 'pi');
|
||||
await promote(socket, sessionId, pending.promotion_token!);
|
||||
|
||||
expect(await authorize(socket, sessionId, 'pi', 'bash', 2)).toMatchObject({
|
||||
ok: false,
|
||||
code: 'MUTATOR_UNVERIFIED',
|
||||
decision: 'deny',
|
||||
});
|
||||
expect(await authorize(socket, sessionId, 'pi', 'bash', 1)).toMatchObject({
|
||||
ok: false,
|
||||
code: 'STALE_GENERATION',
|
||||
});
|
||||
});
|
||||
|
||||
test('runtime launcher anchors broker identity before exec and fails closed without broker', async () => {
|
||||
const { socket } = await startBroker();
|
||||
const probe = [
|
||||
'import json,os,socket',
|
||||
's=socket.socket(socket.AF_UNIX,socket.SOCK_STREAM)',
|
||||
"s.connect(os.environ['MOSAIC_LEASE_BROKER_SOCKET'])",
|
||||
"request={'action':'authorize_tool','session_id':os.environ['MOSAIC_LEASE_SESSION_ID'],'runtime_generation':int(os.environ['MOSAIC_RUNTIME_GENERATION']),'runtime':'claude','tool_name':'Read'}",
|
||||
"s.sendall((json.dumps(request)+'\\n').encode())",
|
||||
's.shutdown(socket.SHUT_WR)',
|
||||
"print(json.dumps({'session_id':os.environ['MOSAIC_LEASE_SESSION_ID'],'reply':json.loads(s.recv(65536))}))",
|
||||
].join(';');
|
||||
const launched = spawnSync(
|
||||
'python3',
|
||||
[launcherPath, '--runtime', 'claude', '--', 'python3', '-c', probe],
|
||||
{
|
||||
encoding: 'utf8',
|
||||
env: {
|
||||
...process.env,
|
||||
MOSAIC_LEASE_BROKER_SOCKET: socket,
|
||||
MOSAIC_RUNTIME_GENERATION: '1',
|
||||
},
|
||||
},
|
||||
);
|
||||
expect(launched.status, launched.stderr).toBe(0);
|
||||
expect(JSON.parse(launched.stdout)).toMatchObject({
|
||||
session_id: expect.stringMatching(/^[a-f0-9]{64}$/),
|
||||
reply: { ok: true, decision: 'allow' },
|
||||
});
|
||||
|
||||
const unavailable = spawnSync(
|
||||
'python3',
|
||||
[launcherPath, '--runtime', 'claude', '--', 'python3', '-c', "print('EXECUTED')"],
|
||||
{
|
||||
encoding: 'utf8',
|
||||
env: {
|
||||
...process.env,
|
||||
MOSAIC_LEASE_BROKER_SOCKET: join(tmpdir(), 'missing-mosaic-broker.sock'),
|
||||
MOSAIC_RUNTIME_GENERATION: '1',
|
||||
},
|
||||
},
|
||||
);
|
||||
expect(unavailable.status).not.toBe(0);
|
||||
expect(unavailable.stdout).not.toContain('EXECUTED');
|
||||
});
|
||||
|
||||
test.each(runtimeLaunchEntries)(
|
||||
'$name registers before launch, denies an unverified mutator, and fails closed without broker',
|
||||
async (entry) => {
|
||||
const missingSocket = join(tmpdir(), `missing-${entry.name}-${process.pid}.sock`);
|
||||
const unavailable = await runRuntimeLaunchEntry(entry, missingSocket);
|
||||
expect(unavailable.status).not.toBe(0);
|
||||
expect(`${unavailable.stdout}${unavailable.stderr}`).not.toContain('RUNTIME_PROBE=');
|
||||
|
||||
const { socket } = await startBroker();
|
||||
const launched = await runRuntimeLaunchEntry(entry, socket);
|
||||
expect(launched.status, launched.stderr).toBe(0);
|
||||
const match = /RUNTIME_PROBE=(\{[^\n]+\})/.exec(`${launched.stdout}${launched.stderr}`);
|
||||
expect(match).not.toBeNull();
|
||||
expect(JSON.parse(match![1]!)).toEqual({
|
||||
session_id: expect.stringMatching(/^[a-f0-9]{64}$/),
|
||||
denied: true,
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
test.each([
|
||||
{ command: 'mosaic claudex', yolo: false },
|
||||
{ command: 'mosaic yolo claudex', yolo: true },
|
||||
])(
|
||||
'$command registers a broker anchor, installs the all-tools hook, and denies an unverified mutator',
|
||||
async ({ yolo }) => {
|
||||
const { socket } = await startBroker();
|
||||
const root = await mkdtemp(join(tmpdir(), 'mosaic-claudex-gate-'));
|
||||
temporaryRoots.push(root);
|
||||
const configDir = join(root, 'isolated-claude');
|
||||
const binDir = join(root, 'bin');
|
||||
await mkdir(configDir, { recursive: true });
|
||||
await mkdir(binDir, { recursive: true });
|
||||
|
||||
const fakeClaude = join(binDir, 'claude');
|
||||
const probe = `#!/usr/bin/env python3
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
session_id = os.environ.get("MOSAIC_LEASE_SESSION_ID", "")
|
||||
settings_path = Path(os.environ["CLAUDE_CONFIG_DIR"]) / "settings.json"
|
||||
try:
|
||||
settings = json.loads(settings_path.read_text())
|
||||
except (OSError, json.JSONDecodeError):
|
||||
settings = {}
|
||||
pre_tool = settings.get("hooks", {}).get("PreToolUse", [])
|
||||
hook_present = any(
|
||||
item.get("matcher") == ".*" and any("mutator-gate.py" in hook.get("command", "") for hook in item.get("hooks", []))
|
||||
for item in pre_tool
|
||||
)
|
||||
denied = subprocess.run(
|
||||
["python3", ${JSON.stringify(gatePath)}, "--runtime", "claude"],
|
||||
input=json.dumps({"tool_name": "Bash"}) + "\\n",
|
||||
text=True,
|
||||
capture_output=True,
|
||||
env=os.environ,
|
||||
).returncode == 2
|
||||
is_yolo = "--dangerously-skip-permissions" in sys.argv[1:]
|
||||
result = {
|
||||
"session_id": session_id,
|
||||
"hook_present": hook_present,
|
||||
"denied": denied,
|
||||
"is_yolo": is_yolo,
|
||||
}
|
||||
print(json.dumps(result))
|
||||
raise SystemExit(0 if len(session_id) == 64 and hook_present and denied else 1)
|
||||
`;
|
||||
await writeFile(fakeClaude, probe, { mode: 0o700 });
|
||||
await chmod(fakeClaude, 0o700);
|
||||
|
||||
let execution: ReturnType<typeof spawnSync> | undefined;
|
||||
const run = (cmd: string, args: string[], env: NodeJS.ProcessEnv) => {
|
||||
execution = spawnSync(cmd, args, { encoding: 'utf8', env });
|
||||
};
|
||||
const adapter = {
|
||||
harnessPreflight: () => {},
|
||||
composePrompt: () => '# composed Claude contract',
|
||||
// Claudex exposes only the shared register-before-exec boundary.
|
||||
execLeaseGated: (args: string[], env: NodeJS.ProcessEnv, dangerous: boolean) =>
|
||||
run(
|
||||
'python3',
|
||||
[
|
||||
launcherPath,
|
||||
...(dangerous ? ['--dangerous'] : []),
|
||||
'--runtime',
|
||||
'claude',
|
||||
'--',
|
||||
'claude',
|
||||
...args,
|
||||
],
|
||||
env,
|
||||
),
|
||||
} as unknown as ClaudexHarnessAdapter;
|
||||
|
||||
await launchClaudex([], yolo, adapter, {
|
||||
baseEnv: {
|
||||
...process.env,
|
||||
PATH: `${binDir}:${process.env.PATH ?? ''}`,
|
||||
MOSAIC_LEASE_BROKER_SOCKET: socket,
|
||||
MOSAIC_RUNTIME_GENERATION: '1',
|
||||
},
|
||||
proxyGate: () =>
|
||||
Promise.resolve({
|
||||
ok: true,
|
||||
report: {
|
||||
binaryPresent: true,
|
||||
binaryPath: '/test/claude-code-proxy',
|
||||
auth: { state: 'valid' },
|
||||
live: true,
|
||||
listenerVerdict: 'ok',
|
||||
needsReauth: false,
|
||||
ok: true,
|
||||
problems: [],
|
||||
},
|
||||
problems: [],
|
||||
}),
|
||||
resolveConfigDir: () => configDir,
|
||||
log: () => {},
|
||||
errorLog: () => {},
|
||||
fail: ((code: number) => {
|
||||
throw new Error(`exit ${code}`);
|
||||
}) as (code: number) => never,
|
||||
});
|
||||
|
||||
expect(execution).toBeDefined();
|
||||
expect(execution!.status, String(execution!.stderr)).toBe(0);
|
||||
expect(JSON.parse(String(execution!.stdout))).toEqual({
|
||||
session_id: expect.stringMatching(/^[a-f0-9]{64}$/),
|
||||
hook_present: true,
|
||||
denied: true,
|
||||
is_yolo: yolo,
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
test('Claude and Pi runtime adapters consult the broker for every tool class', async () => {
|
||||
const { socket } = await startBroker();
|
||||
const sessionId = await register(socket);
|
||||
|
||||
expect(runRuntimeGate(socket, sessionId, 'claude', 'Read').status).toBe(0);
|
||||
expect(runRuntimeGate(socket, sessionId, 'claude', 'Bash').status).toBe(2);
|
||||
expect(runRuntimeGate(socket, sessionId, 'pi', 'unknown_custom_tool').status).toBe(2);
|
||||
|
||||
const pending = await beginVerification(socket, sessionId, 'claude');
|
||||
await promote(socket, sessionId, pending.promotion_token!);
|
||||
expect(runRuntimeGate(socket, sessionId, 'claude', 'Bash').status).toBe(0);
|
||||
|
||||
const settings = JSON.parse(await readFile(claudeSettingsPath, 'utf8')) as {
|
||||
hooks: { PreToolUse: Array<{ matcher?: string; hooks: Array<{ command: string }> }> };
|
||||
};
|
||||
expect(
|
||||
settings.hooks.PreToolUse.some(
|
||||
(entry) =>
|
||||
entry.matcher === '.*' &&
|
||||
entry.hooks.some((hook) => hook.command.includes('mutator-gate.py')),
|
||||
),
|
||||
).toBe(true);
|
||||
|
||||
const piExtension = await readFile(piExtensionPath, 'utf8');
|
||||
expect(piExtension).toContain("pi.on('tool_call'");
|
||||
expect(piExtension).toContain('mutator-gate.py');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,211 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Contract tests for the permanent consequential-runtime launch guard."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib.util
|
||||
import io
|
||||
import json
|
||||
import runpy
|
||||
import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
from contextlib import redirect_stderr, redirect_stdout
|
||||
from pathlib import Path
|
||||
from unittest.mock import patch
|
||||
|
||||
|
||||
MOSAIC_ROOT = Path(__file__).parents[2]
|
||||
REPO_ROOT = Path(__file__).parents[4]
|
||||
GUARD_PATH = MOSAIC_ROOT / "framework/tools/lease-broker/check-runtime-launches.py"
|
||||
SPEC = importlib.util.spec_from_file_location("runtime_launch_guard", GUARD_PATH)
|
||||
if SPEC is None or SPEC.loader is None:
|
||||
raise RuntimeError("unable to load runtime launch guard")
|
||||
GUARD = importlib.util.module_from_spec(SPEC)
|
||||
SPEC.loader.exec_module(GUARD)
|
||||
|
||||
|
||||
class RuntimeLaunchGuardTest(unittest.TestCase):
|
||||
def test_detects_direct_shell_and_process_api_launches(self) -> None:
|
||||
cases = {
|
||||
"shell-exec.sh": 'exec claude --dangerously-skip-permissions "prompt"\n',
|
||||
"shell-print.sh": 'claude -p "prompt" | tee report.log\n',
|
||||
"typescript.ts": "spawn('pi', ['--print', prompt]);\n",
|
||||
"python.py": "subprocess.run(['claude', '-p', prompt])\n",
|
||||
"dynamic.ts": "return [runtime, '-p', prompt];\n",
|
||||
"plain-shell.sh": 'claude "$prompt"\n',
|
||||
"node-exec.ts": 'exec("claude --print hello");\n',
|
||||
"command-array.ts": "const launchCommand = ['pi', '--print', prompt];\n",
|
||||
"python-system.py": 'os.system("claude -p prompt")\n',
|
||||
"dynamic-shell.sh": 'exec "$runtime" "$prompt"\n',
|
||||
"dynamic-spawn.ts": 'spawn(runtime, args);\n',
|
||||
"dynamic-command.sh": 'LAUNCH_COMMAND=("$MOSAIC_AGENT_RUNTIME" --print)\n',
|
||||
"absolute-shell.sh": 'exec /usr/local/bin/claude -p prompt\n',
|
||||
"absolute-spawn.ts": "spawn('/opt/bin/pi', args);\n",
|
||||
"terra-comment.sh": 'exec claude --dangerously-skip-permissions "terra-r3" # launch-runtime.py\n',
|
||||
"python-comment.py": "subprocess.run(['claude', '-p', prompt]) # launch-runtime.py\n",
|
||||
"typescript-comment.ts": "spawn('pi', args); // launch-runtime.py\n",
|
||||
"marker-argument.sh": 'exec claude --dangerously-skip-permissions "launch-runtime.py"\n',
|
||||
"marker-echo.sh": 'exec claude --dangerously-skip-permissions "prompt"; echo launch-runtime.py\n',
|
||||
"marker-variable.sh": 'marker=launch-runtime.py; exec claude --dangerously-skip-permissions "prompt"\n',
|
||||
"heredoc.sh": "cat <<'EOF'\nexec claude --dangerously-skip-permissions prompt # launch-runtime.py\nEOF\n",
|
||||
"continued.sh": "exec \\\n claude --dangerously-skip-permissions prompt # launch-runtime.py\n",
|
||||
"chain-semicolon.sh": "true; claude -p prompt\n",
|
||||
"chain-and.sh": "true && claude -p prompt\n",
|
||||
"chain-pipe.sh": "printf input | claude -p prompt\n",
|
||||
"command-substitution.sh": "output=$(claude -p prompt)\n",
|
||||
"eval.sh": "launcher='claude -p prompt'\neval \"$launcher\"\n",
|
||||
"variable-exec.sh": "launcher=claude\n\"$launcher\" -p prompt\n",
|
||||
"env-prefix.sh": "env SAFE=1 claude --help\n",
|
||||
"command-prefix.sh": "command pi --help\n",
|
||||
"nohup-prefix.sh": "nohup claude --help &\n",
|
||||
}
|
||||
for filename, source in cases.items():
|
||||
with self.subTest(filename=filename):
|
||||
violations = GUARD.scan_text(Path(filename), source)
|
||||
self.assertNotEqual(violations, [], source)
|
||||
|
||||
def test_allows_only_explicit_gated_boundaries(self) -> None:
|
||||
cases = {
|
||||
"shell-helper.sh": 'exec "$GATED_RUNTIME" claude -- claude -p "prompt"\n',
|
||||
"mosaic.sh": 'exec mosaic yolo "$runtime" "prompt"\n',
|
||||
"launch.ts": "execLeaseGatedRuntime('claude', args);\n",
|
||||
"coord.ts": "return ['mosaic', runtime, '-p', prompt];\n",
|
||||
}
|
||||
for filename, source in cases.items():
|
||||
with self.subTest(filename=filename):
|
||||
self.assertEqual(GUARD.scan_text(Path(filename), source), [])
|
||||
|
||||
def test_detects_prefixed_tracked_runtime_variable_execution(self) -> None:
|
||||
multiline = {
|
||||
"exec-quoted": 'exec "$v" -p x',
|
||||
"exec-unquoted": "exec $v -p x",
|
||||
"command": 'command "$v" -p x',
|
||||
"nohup": 'nohup "$v" -p x',
|
||||
"env": 'env A=1 "$v" -p x',
|
||||
}
|
||||
cases = {
|
||||
**{f"multiline-{name}.sh": f"v=claude\n{command}\n" for name, command in multiline.items()},
|
||||
**{f"same-line-{name}.sh": f"v=claude; {command}\n" for name, command in multiline.items()},
|
||||
}
|
||||
for filename, source in cases.items():
|
||||
with self.subTest(filename=filename):
|
||||
self.assertNotEqual(GUARD.scan_text(Path(filename), source), [], source)
|
||||
|
||||
def test_accepts_only_validated_multiline_typescript_wrapper_invocation(self) -> None:
|
||||
source = """execRuntime(
|
||||
'python3',
|
||||
[launcher, ...dangerousArgs, '--runtime', runtime, '--', runtime, ...args],
|
||||
environment,
|
||||
);
|
||||
"""
|
||||
sites = GUARD.classify_text(Path("launch.ts"), source)
|
||||
self.assertEqual(len(sites), 1)
|
||||
self.assertEqual(sites[0].classification, "gated")
|
||||
|
||||
def test_marker_comments_strings_and_assignments_are_not_gated_sites(self) -> None:
|
||||
harmless_sources = {
|
||||
"comment.sh": "# launch-runtime.py --runtime claude --\n",
|
||||
"echo.sh": "echo 'launch-runtime.py --runtime claude --'\n",
|
||||
"assignment.sh": "marker='launch-runtime.py --runtime claude --'\n",
|
||||
"argument.sh": "printf '%s' 'launch-runtime.py --runtime claude --'\n",
|
||||
}
|
||||
for filename, source in harmless_sources.items():
|
||||
with self.subTest(filename=filename):
|
||||
self.assertEqual(GUARD.classify_text(Path(filename), source), [])
|
||||
|
||||
def test_dangerous_primitive_backstops_parser_exotic_alias_indirection(self) -> None:
|
||||
source = (
|
||||
"alias hidden_runtime=claude\n"
|
||||
"hidden_runtime --dangerously-skip-permissions -p x\n"
|
||||
)
|
||||
sites = GUARD.scan_text(Path("alias-launch.sh"), source)
|
||||
self.assertEqual(len(sites), 1)
|
||||
self.assertEqual(sites[0].classification, "dangerous-primitive")
|
||||
|
||||
def test_dangerous_primitive_is_owned_only_by_the_choke_point(self) -> None:
|
||||
primitive = "--dangerously-skip-permissions"
|
||||
self.assertNotEqual(GUARD.scan_text(Path("caller.ts"), f"args = ['{primitive}'];\n"), [])
|
||||
self.assertEqual(
|
||||
GUARD.scan_text(Path("framework/tools/lease-broker/launch-runtime.py"), f'FLAG = "{primitive}"\n'),
|
||||
[],
|
||||
)
|
||||
|
||||
def test_repository_has_no_ungated_consequential_runtime_launch(self) -> None:
|
||||
violations = GUARD.scan_repository(REPO_ROOT)
|
||||
self.assertEqual(
|
||||
violations,
|
||||
[],
|
||||
"\n".join(GUARD.format_violation(violation) for violation in violations),
|
||||
)
|
||||
inventory = GUARD.inventory_repository(REPO_ROOT)
|
||||
self.assertEqual(len(inventory), 14)
|
||||
self.assertTrue(all(site.classification == "gated" for site in inventory))
|
||||
|
||||
def test_repository_walk_skips_tests_build_outputs_and_reports_unscannable_source(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
root = Path(directory)
|
||||
production = root / "packages/example/src/launch.sh"
|
||||
production.parent.mkdir(parents=True)
|
||||
production.write_text("exec claude -p prompt\n")
|
||||
(production.parent / "launch.spec.ts").write_text("spawn('pi', [])\n")
|
||||
dist = root / "packages/example/dist/launch.js"
|
||||
dist.parent.mkdir(parents=True)
|
||||
dist.write_text("exec('claude -p prompt')\n")
|
||||
ignored_suffix = production.parent / "notes.txt"
|
||||
ignored_suffix.write_text("claude -p prompt\n")
|
||||
invalid = production.parent / "invalid.py"
|
||||
invalid.write_bytes(b"\xff\xfe")
|
||||
|
||||
violations = GUARD.scan_repository(root)
|
||||
formatted = [GUARD.format_violation(item) for item in violations]
|
||||
self.assertEqual(len(violations), 2)
|
||||
self.assertTrue(any("launch.sh:1: direct" in item for item in formatted))
|
||||
self.assertTrue(any("invalid.py:0: unscannable" in item for item in formatted))
|
||||
self.assertFalse(any("spec" in item or "dist" in item or "notes" in item for item in formatted))
|
||||
|
||||
inventory = GUARD.inventory_repository(root)
|
||||
self.assertEqual({item.classification for item in inventory}, {"direct", "unscannable"})
|
||||
|
||||
def test_main_emits_machine_inventory_and_fails_on_a_direct_site(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
root = Path(directory)
|
||||
source = root / "packages/example/launch.sh"
|
||||
source.parent.mkdir(parents=True)
|
||||
source.write_text(
|
||||
'exec claude --dangerously-skip-permissions "terra-r3" # launch-runtime.py\n'
|
||||
)
|
||||
stdout = io.StringIO()
|
||||
stderr = io.StringIO()
|
||||
with redirect_stdout(stdout), redirect_stderr(stderr):
|
||||
result = GUARD.main(["--root", str(root), "--json"])
|
||||
payload = json.loads(stdout.getvalue())
|
||||
self.assertEqual(result, 1)
|
||||
self.assertEqual(payload["gated"], 0)
|
||||
self.assertEqual(payload["total"], 1)
|
||||
self.assertIn("ungated consequential runtime", stderr.getvalue())
|
||||
|
||||
def test_main_text_mode_reports_a_green_gated_inventory(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
root = Path(directory)
|
||||
source = root / "packages/example/launch.sh"
|
||||
source.parent.mkdir(parents=True)
|
||||
source.write_text("exec mosaic yolo claude prompt\n")
|
||||
stdout = io.StringIO()
|
||||
with redirect_stdout(stdout):
|
||||
result = GUARD.main(["--root", str(root)])
|
||||
self.assertEqual(result, 0)
|
||||
self.assertIn("1 gated/1 total", stdout.getvalue())
|
||||
|
||||
def test_script_entrypoint_uses_current_directory_default(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
root = Path(directory)
|
||||
(root / "packages").mkdir()
|
||||
with patch.object(sys, "argv", [str(GUARD_PATH)]), patch("pathlib.Path.cwd", return_value=root):
|
||||
with redirect_stdout(io.StringIO()), self.assertRaises(SystemExit) as raised:
|
||||
runpy.run_path(str(GUARD_PATH), run_name="__main__")
|
||||
self.assertEqual(raised.exception.code, 0)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
436
packages/mosaic/src/mutator-gate/runtime_tools_unittest.py
Normal file
436
packages/mosaic/src/mutator-gate/runtime_tools_unittest.py
Normal file
@@ -0,0 +1,436 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Branch-focused tests for the lease-gated runtime executables."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib.util
|
||||
import io
|
||||
import json
|
||||
import os
|
||||
import runpy
|
||||
import socket
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import threading
|
||||
import unittest
|
||||
from contextlib import redirect_stderr
|
||||
from pathlib import Path
|
||||
from unittest.mock import patch
|
||||
|
||||
|
||||
TOOLS_DIR = Path(__file__).parents[2] / "framework/tools/lease-broker"
|
||||
|
||||
|
||||
def load_tool(module_name: str, filename: str):
|
||||
spec = importlib.util.spec_from_file_location(module_name, TOOLS_DIR / filename)
|
||||
if spec is None or spec.loader is None:
|
||||
raise RuntimeError(f"unable to load {filename}")
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(module)
|
||||
return module
|
||||
|
||||
|
||||
LAUNCHER = load_tool("lease_runtime_launcher", "launch-runtime.py")
|
||||
GATE = load_tool("lease_mutator_gate", "mutator-gate.py")
|
||||
|
||||
|
||||
class FakeSocket:
|
||||
def __init__(self, *chunks: bytes):
|
||||
self.chunks = list(chunks)
|
||||
self.timeout = None
|
||||
self.connected = None
|
||||
self.sent = b""
|
||||
self.shutdown_how = None
|
||||
|
||||
def __enter__(self):
|
||||
return self
|
||||
|
||||
def __exit__(self, *_args):
|
||||
return False
|
||||
|
||||
def settimeout(self, value: float) -> None:
|
||||
self.timeout = value
|
||||
|
||||
def connect(self, value: str) -> None:
|
||||
self.connected = value
|
||||
|
||||
def sendall(self, value: bytes) -> None:
|
||||
self.sent += value
|
||||
|
||||
def shutdown(self, how: int) -> None:
|
||||
self.shutdown_how = how
|
||||
|
||||
def recv(self, _size: int) -> bytes:
|
||||
return self.chunks.pop(0) if self.chunks else b""
|
||||
|
||||
|
||||
class LaunchRuntimeTest(unittest.TestCase):
|
||||
def test_success_registers_then_injects_session_before_exec(self) -> None:
|
||||
calls: dict[str, object] = {}
|
||||
session_id = "a" * 64
|
||||
|
||||
def request(path: Path, payload: dict[str, object]) -> dict[str, object]:
|
||||
calls["path"] = path
|
||||
calls["request"] = payload
|
||||
return {"ok": True, "session_id": session_id}
|
||||
|
||||
def execute(command: str, argv: list[str], environment: dict[str, str]) -> None:
|
||||
calls["execute"] = (command, argv, environment)
|
||||
|
||||
result = LAUNCHER.main(
|
||||
["--runtime", "claude", "--", "claude", "--print", "hello"],
|
||||
environ={
|
||||
"MOSAIC_LEASE_BROKER_SOCKET": "/run/test/broker.sock",
|
||||
"MOSAIC_RUNTIME_GENERATION": "7",
|
||||
"PRESERVED": "yes",
|
||||
},
|
||||
request=request,
|
||||
execute=execute,
|
||||
)
|
||||
|
||||
self.assertEqual(result, 0)
|
||||
self.assertEqual(calls["path"], Path("/run/test/broker.sock"))
|
||||
self.assertEqual(
|
||||
calls["request"],
|
||||
{"action": "register_anchor", "runtime_generation": 7},
|
||||
)
|
||||
command, argv, environment = calls["execute"]
|
||||
self.assertEqual(command, "claude")
|
||||
self.assertEqual(argv, ["claude", "--print", "hello"])
|
||||
self.assertEqual(environment["MOSAIC_LEASE_SESSION_ID"], session_id)
|
||||
self.assertEqual(environment["MOSAIC_RUNTIME_GENERATION"], "7")
|
||||
self.assertEqual(environment["MOSAIC_LEASE_RUNTIME"], "claude")
|
||||
self.assertEqual(environment["PRESERVED"], "yes")
|
||||
|
||||
def test_dangerous_claude_mode_is_owned_and_injected_by_the_wrapper(self) -> None:
|
||||
executed: list[tuple[str, list[str], dict[str, str]]] = []
|
||||
result = LAUNCHER.main(
|
||||
["--runtime", "claude", "--dangerous", "--", "claude", "-p", "hello"],
|
||||
environ={"MOSAIC_LEASE_BROKER_SOCKET": "/broker"},
|
||||
request=lambda *_args: {"ok": True, "session_id": "e" * 64},
|
||||
execute=lambda *args: executed.append(args),
|
||||
)
|
||||
self.assertEqual(result, 0)
|
||||
self.assertEqual(
|
||||
executed[0][1],
|
||||
["claude", "--dangerously-skip-permissions", "-p", "hello"],
|
||||
)
|
||||
|
||||
with redirect_stderr(io.StringIO()):
|
||||
self.assertEqual(
|
||||
LAUNCHER.main(
|
||||
["--runtime", "pi", "--dangerous", "--", "pi"],
|
||||
environ={"MOSAIC_LEASE_BROKER_SOCKET": "/broker"},
|
||||
request=lambda *_args: {"ok": True, "session_id": "e" * 64},
|
||||
execute=lambda *_args: self.fail("invalid dangerous runtime executed"),
|
||||
),
|
||||
64,
|
||||
)
|
||||
|
||||
def test_command_without_separator_is_forwarded_unchanged(self) -> None:
|
||||
executed: list[tuple[str, list[str], dict[str, str]]] = []
|
||||
result = LAUNCHER.main(
|
||||
["--runtime", "pi", "pi", "--print", "hello"],
|
||||
environ={"MOSAIC_LEASE_BROKER_SOCKET": "/broker"},
|
||||
request=lambda *_args: {"ok": True, "session_id": "f" * 64},
|
||||
execute=lambda *args: executed.append(args),
|
||||
)
|
||||
self.assertEqual(result, 0)
|
||||
self.assertEqual(executed[0][0:2], ("pi", ["pi", "--print", "hello"]))
|
||||
|
||||
def test_missing_command_is_usage_error(self) -> None:
|
||||
with redirect_stderr(io.StringIO()):
|
||||
self.assertEqual(
|
||||
LAUNCHER.main(
|
||||
["--runtime", "pi", "--"],
|
||||
environ={},
|
||||
request=lambda *_args: {},
|
||||
execute=lambda *_args: None,
|
||||
),
|
||||
64,
|
||||
)
|
||||
|
||||
def test_registration_validation_and_environment_fail_closed(self) -> None:
|
||||
good_session = "b" * 64
|
||||
cases = [
|
||||
({}, {"ok": True, "session_id": good_session}),
|
||||
({"MOSAIC_LEASE_BROKER_SOCKET": "/x", "MOSAIC_RUNTIME_GENERATION": "bad"}, {}),
|
||||
({"MOSAIC_LEASE_BROKER_SOCKET": "/x", "MOSAIC_RUNTIME_GENERATION": "-1"}, {}),
|
||||
({"MOSAIC_LEASE_BROKER_SOCKET": "/x"}, {"ok": False, "session_id": good_session}),
|
||||
({"MOSAIC_LEASE_BROKER_SOCKET": "/x"}, {"ok": True, "session_id": 4}),
|
||||
({"MOSAIC_LEASE_BROKER_SOCKET": "/x"}, {"ok": True, "session_id": "b" * 63}),
|
||||
({"MOSAIC_LEASE_BROKER_SOCKET": "/x"}, {"ok": True, "session_id": "z" * 64}),
|
||||
]
|
||||
for environment, reply in cases:
|
||||
with self.subTest(environment=environment, reply=reply), redirect_stderr(io.StringIO()):
|
||||
executed: list[object] = []
|
||||
result = LAUNCHER.main(
|
||||
["--runtime", "pi", "--", "pi"],
|
||||
environ=environment,
|
||||
request=lambda *_args, value=reply: value,
|
||||
execute=lambda *args: executed.append(args),
|
||||
)
|
||||
self.assertEqual(result, 1)
|
||||
self.assertEqual(executed, [])
|
||||
|
||||
def test_registration_exceptions_fail_closed(self) -> None:
|
||||
failures = [ValueError("bad"), OSError("down"), json.JSONDecodeError("bad", "x", 0)]
|
||||
for failure in failures:
|
||||
with self.subTest(failure=type(failure).__name__), redirect_stderr(io.StringIO()):
|
||||
def request(*_args, error=failure):
|
||||
raise error
|
||||
|
||||
self.assertEqual(
|
||||
LAUNCHER.main(
|
||||
["--runtime", "claude", "--", "claude"],
|
||||
environ={"MOSAIC_LEASE_BROKER_SOCKET": "/x"},
|
||||
request=request,
|
||||
execute=lambda *_args: self.fail("must not execute"),
|
||||
),
|
||||
1,
|
||||
)
|
||||
|
||||
def test_exec_failure_is_fail_closed(self) -> None:
|
||||
with redirect_stderr(io.StringIO()):
|
||||
self.assertEqual(
|
||||
LAUNCHER.main(
|
||||
["--runtime", "pi", "--", "pi"],
|
||||
environ={"MOSAIC_LEASE_BROKER_SOCKET": "/x"},
|
||||
request=lambda *_args: {"ok": True, "session_id": "c" * 64},
|
||||
execute=lambda *_args: (_ for _ in ()).throw(OSError("missing")),
|
||||
),
|
||||
1,
|
||||
)
|
||||
|
||||
def test_broker_reply_framing_and_shape_validation(self) -> None:
|
||||
replies = [
|
||||
(b'{"ok":true}\n', {"ok": True}),
|
||||
(b'{"ok":true}', ValueError),
|
||||
(b'[]\n', ValueError),
|
||||
(b"x" * (LAUNCHER.MAX_FRAME + 1), ValueError),
|
||||
]
|
||||
for wire_reply, expected in replies:
|
||||
with self.subTest(size=len(wire_reply)):
|
||||
fake = FakeSocket(wire_reply)
|
||||
with patch.object(LAUNCHER.socket, "socket", return_value=fake):
|
||||
if isinstance(expected, type) and issubclass(expected, Exception):
|
||||
with self.assertRaises(expected):
|
||||
LAUNCHER.broker_request(Path("/broker"), {"action": "register_anchor"})
|
||||
else:
|
||||
self.assertEqual(
|
||||
LAUNCHER.broker_request(Path("/broker"), {"action": "register_anchor"}),
|
||||
expected,
|
||||
)
|
||||
self.assertEqual(fake.timeout, LAUNCHER.BROKER_TIMEOUT_SECONDS)
|
||||
self.assertEqual(fake.connected, "/broker")
|
||||
self.assertEqual(fake.shutdown_how, socket.SHUT_WR)
|
||||
|
||||
|
||||
class ExecutableEntrypointTest(unittest.TestCase):
|
||||
def test_real_claude_and_pi_gates_fail_closed_on_empty_or_truncated_reply(self) -> None:
|
||||
for runtime in ("claude", "pi"):
|
||||
for wire_reply in (b"", b'{"ok":true'):
|
||||
with self.subTest(runtime=runtime, wire_reply=wire_reply), tempfile.TemporaryDirectory() as root:
|
||||
socket_path = Path(root) / "broker.sock"
|
||||
server = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
||||
server.bind(str(socket_path))
|
||||
server.listen(1)
|
||||
|
||||
def serve_reply() -> None:
|
||||
with server:
|
||||
connection, _ = server.accept()
|
||||
with connection:
|
||||
while connection.recv(4096):
|
||||
pass
|
||||
if wire_reply:
|
||||
connection.sendall(wire_reply)
|
||||
|
||||
thread = threading.Thread(target=serve_reply, daemon=True)
|
||||
thread.start()
|
||||
environment = {
|
||||
**os.environ,
|
||||
"MOSAIC_LEASE_BROKER_SOCKET": str(socket_path),
|
||||
"MOSAIC_LEASE_SESSION_ID": "d" * 64,
|
||||
"MOSAIC_RUNTIME_GENERATION": "1",
|
||||
}
|
||||
try:
|
||||
result = subprocess.run(
|
||||
[sys.executable, str(TOOLS_DIR / "mutator-gate.py"), "--runtime", runtime],
|
||||
input=b'{"tool_name":"Read"}\n',
|
||||
capture_output=True,
|
||||
env=environment,
|
||||
check=False,
|
||||
timeout=5,
|
||||
)
|
||||
except subprocess.TimeoutExpired as exc:
|
||||
server.close()
|
||||
thread.join(timeout=2)
|
||||
self.fail(
|
||||
f"{runtime} gate hung on wire reply {wire_reply!r}: {exc}"
|
||||
)
|
||||
thread.join(timeout=2)
|
||||
|
||||
self.assertFalse(thread.is_alive())
|
||||
self.assertEqual(result.returncode, 2)
|
||||
self.assertIn(b"GATE_UNAVAILABLE", result.stderr)
|
||||
|
||||
def test_launcher_entrypoint_returns_usage_without_a_command(self) -> None:
|
||||
with patch.object(
|
||||
sys,
|
||||
"argv",
|
||||
[str(TOOLS_DIR / "launch-runtime.py"), "--runtime", "claude"],
|
||||
), redirect_stderr(io.StringIO()), self.assertRaises(SystemExit) as raised:
|
||||
runpy.run_path(str(TOOLS_DIR / "launch-runtime.py"), run_name="__main__")
|
||||
self.assertEqual(raised.exception.code, 64)
|
||||
|
||||
def test_gate_entrypoint_denies_when_identity_environment_is_absent(self) -> None:
|
||||
class Stdin:
|
||||
buffer = io.BytesIO(b'{"tool_name":"Bash"}')
|
||||
|
||||
with patch.object(
|
||||
sys,
|
||||
"argv",
|
||||
[str(TOOLS_DIR / "mutator-gate.py"), "--runtime", "claude"],
|
||||
), patch.object(sys, "stdin", Stdin()), patch.dict(
|
||||
os.environ, {}, clear=True
|
||||
), redirect_stderr(io.StringIO()), self.assertRaises(SystemExit) as raised:
|
||||
runpy.run_path(str(TOOLS_DIR / "mutator-gate.py"), run_name="__main__")
|
||||
self.assertEqual(raised.exception.code, 2)
|
||||
|
||||
|
||||
class MutatorGateTest(unittest.TestCase):
|
||||
@staticmethod
|
||||
def environment() -> dict[str, str]:
|
||||
return {
|
||||
"MOSAIC_LEASE_BROKER_SOCKET": "/run/test/broker.sock",
|
||||
"MOSAIC_LEASE_SESSION_ID": "d" * 64,
|
||||
"MOSAIC_RUNTIME_GENERATION": "2",
|
||||
}
|
||||
|
||||
def run_main(self, *, tool: object = "Bash", reply: dict[str, object] | None = None):
|
||||
calls: list[tuple[Path, dict[str, object]]] = []
|
||||
|
||||
def request(path: Path, payload: dict[str, object]) -> dict[str, object]:
|
||||
calls.append((path, payload))
|
||||
return reply if reply is not None else {"ok": True, "decision": "allow"}
|
||||
|
||||
stderr = io.StringIO()
|
||||
with redirect_stderr(stderr):
|
||||
result = GATE.main(
|
||||
["--runtime", "claude"],
|
||||
environ=self.environment(),
|
||||
stream=io.BytesIO(json.dumps({"tool_name": tool}).encode()),
|
||||
request=request,
|
||||
)
|
||||
return result, stderr.getvalue(), calls
|
||||
|
||||
def test_allow_and_denial_decisions(self) -> None:
|
||||
allowed, allowed_stderr, calls = self.run_main()
|
||||
self.assertEqual(allowed, 0)
|
||||
self.assertEqual(allowed_stderr, "")
|
||||
self.assertEqual(calls[0][0], Path("/run/test/broker.sock"))
|
||||
self.assertEqual(
|
||||
calls[0][1],
|
||||
{
|
||||
"action": "authorize_tool",
|
||||
"session_id": "d" * 64,
|
||||
"runtime_generation": 2,
|
||||
"runtime": "claude",
|
||||
"tool_name": "Bash",
|
||||
},
|
||||
)
|
||||
|
||||
denied, denied_stderr, _ = self.run_main(reply={"ok": False, "code": "LEASE_EXPIRED"})
|
||||
self.assertEqual(denied, 2)
|
||||
self.assertIn("LEASE_EXPIRED", denied_stderr)
|
||||
|
||||
defaulted, defaulted_stderr, _ = self.run_main(reply={"ok": False, "code": 4})
|
||||
self.assertEqual(defaulted, 2)
|
||||
self.assertIn("MUTATOR_UNVERIFIED", defaulted_stderr)
|
||||
|
||||
def test_input_validation_fails_closed(self) -> None:
|
||||
payloads = [
|
||||
b"x" * (GATE.MAX_FRAME + 1),
|
||||
b"[]",
|
||||
b"{}",
|
||||
json.dumps({"tool_name": ""}).encode(),
|
||||
json.dumps({"tool_name": 4}).encode(),
|
||||
json.dumps({"tool_name": "x" * 257}).encode(),
|
||||
b"not-json",
|
||||
]
|
||||
for payload in payloads:
|
||||
with self.subTest(size=len(payload)), redirect_stderr(io.StringIO()):
|
||||
self.assertEqual(
|
||||
GATE.main(
|
||||
["--runtime", "pi"],
|
||||
environ=self.environment(),
|
||||
stream=io.BytesIO(payload),
|
||||
request=lambda *_args: self.fail("invalid input reached broker"),
|
||||
),
|
||||
2,
|
||||
)
|
||||
|
||||
def test_environment_generation_and_request_failures_deny(self) -> None:
|
||||
environments = [
|
||||
{},
|
||||
{**self.environment(), "MOSAIC_RUNTIME_GENERATION": "bad"},
|
||||
{**self.environment(), "MOSAIC_RUNTIME_GENERATION": "-1"},
|
||||
]
|
||||
for environment in environments:
|
||||
with self.subTest(environment=environment), redirect_stderr(io.StringIO()):
|
||||
self.assertEqual(
|
||||
GATE.main(
|
||||
["--runtime", "claude"],
|
||||
environ=environment,
|
||||
stream=io.BytesIO(b'{"tool_name":"Read"}'),
|
||||
request=lambda *_args: {},
|
||||
),
|
||||
2,
|
||||
)
|
||||
|
||||
failures = [ValueError("bad"), OSError("down"), json.JSONDecodeError("bad", "x", 0)]
|
||||
for failure in failures:
|
||||
with self.subTest(failure=type(failure).__name__), redirect_stderr(io.StringIO()):
|
||||
def request(*_args, error=failure):
|
||||
raise error
|
||||
|
||||
self.assertEqual(
|
||||
GATE.main(
|
||||
["--runtime", "claude"],
|
||||
environ=self.environment(),
|
||||
stream=io.BytesIO(b'{"tool_name":"Read"}'),
|
||||
request=request,
|
||||
),
|
||||
2,
|
||||
)
|
||||
|
||||
def test_broker_request_framing_payload_and_shape_validation(self) -> None:
|
||||
with self.assertRaises(ValueError):
|
||||
GATE.broker_request(Path("/broker"), {"session_id": "x" * GATE.MAX_FRAME})
|
||||
|
||||
replies = [
|
||||
(b'{"ok":true,"decision":"allow"}\n', {"ok": True, "decision": "allow"}),
|
||||
(b'{"ok":true}', ValueError),
|
||||
(b'[]\n', ValueError),
|
||||
(b"x" * (GATE.MAX_FRAME + 1), ValueError),
|
||||
]
|
||||
for wire_reply, expected in replies:
|
||||
with self.subTest(size=len(wire_reply)):
|
||||
fake = FakeSocket(wire_reply)
|
||||
with patch.object(GATE.socket, "socket", return_value=fake):
|
||||
if isinstance(expected, type) and issubclass(expected, Exception):
|
||||
with self.assertRaises(expected):
|
||||
GATE.broker_request(Path("/broker"), {"action": "authorize_tool"})
|
||||
else:
|
||||
self.assertEqual(
|
||||
GATE.broker_request(Path("/broker"), {"action": "authorize_tool"}),
|
||||
expected,
|
||||
)
|
||||
self.assertEqual(fake.timeout, GATE.BROKER_TIMEOUT_SECONDS)
|
||||
self.assertEqual(fake.connected, "/broker")
|
||||
self.assertEqual(fake.shutdown_how, socket.SHUT_WR)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -7,9 +7,10 @@ export default defineConfig({
|
||||
testTimeout: 30_000,
|
||||
coverage: {
|
||||
provider: 'v8',
|
||||
include: ['src/commands/skill.ts'],
|
||||
include: ['src/commands/skill.ts', 'src/lease-broker/broker-test-client.ts'],
|
||||
reporter: ['text', 'json-summary'],
|
||||
thresholds: {
|
||||
perFile: true,
|
||||
statements: 85,
|
||||
branches: 85,
|
||||
functions: 85,
|
||||
|
||||
Reference in New Issue
Block a user