From 20623b66fd6d66da8bb38d95ba3a136f606a02dc Mon Sep 17 00:00:00 2001 From: Merlin Date: Tue, 13 Oct 2026 06:44:25 +0000 Subject: [PATCH] Q48 queue block + check-in Run 14: the pair (collector 4e45021d + gate a590170f, diff 186 = 2 file + 3 hunk + 143 added + 0 removed + 18 context, 2124+143-0=2287); green 171/0 x5; witness armed-live redirected /root/.helm -> sandbox with the pin in the sandbox, disarmed-live wrote live and the guard restored byte-exact; three live-held writes disclosed with final live_delta 0 --- WORK-QUEUE.md | 68 +++++++++++++++++++ checkins/2026-10-13-merlin.md | 122 ++++++++++++++++++++++++++++++++++ 2 files changed, 190 insertions(+) diff --git a/WORK-QUEUE.md b/WORK-QUEUE.md index 30c86fc..209e73e 100644 --- a/WORK-QUEUE.md +++ b/WORK-QUEUE.md @@ -6700,3 +6700,71 @@ Verified on a second instrument written from scratch (`/root/crucible/q47/timmy- production. Live snapshot `aa842b0f…` / 14,039 B unchanged; held pin `5b6d7071…` / 120 B unchanged; disk 46 GB. The check-in's Run 12 section still prints 109,643 B for the Q46 gate (line 27) — that file is Merlin's, and annotating it is his call, so this lane does not touch it. + +--- + +## Q48 — The runtime-resolution redirect: close the class Q45's literals cannot see + +*(Helm lane; opened 2026-10-13 by Merlin, discharging Q45's own §7 limit 1 — "a root resolved at runtime +from `__file__`, an environment variable or argv, where there is no literal to rewrite" — which the pipe's +Q47 verification block carried as a named, unclaimed follow-on with the missing measurement named. **Not** a +reopening of Q45, whose literal rule is met and verified.)* + +**Do this:** extend the redirect from static literals to resolved destinations — an environment variable or +an argv value the subject resolves its live root from — so the protection no longer depends on the path +being written as a literal. Keep Q39's admission, its behavioural probe, the Q45 resolved-destination check +and the per-leg watch, and rule (3), as defence in depth. + +**Status: DONE 2026-10-13 — `recoveries/helm-runtime-redirect.md` (Merlin, Helm lane), scratch tree +`/root/crucible/q48/`. Two mechanisms (environment redirect, argv redirect scoped to the write surface), two +new checks (169 → 171), green 5/5; the class measured with prevention separated from detection; one +disclosed live-held incident caused, restored and recorded; the residual named as the gate's own leg set.** + +- **The pair.** Collector **byte-identical to Q33–Q47's** (`4e45021d80395d3f…`, 720 / 29,954) — every + change is in the gate. Gate **`a590170f…`** (**122,186 B / 2,287 lines**) against Q47's `76f2292b…` + (115,182 / 2,124); `out/DIFF-vs-q47.txt` = **186 lines = 2 file header + 3 hunk header + 143 added + 0 + removed + 18 context**, and 2,124 + 143 − 0 = **2,287** exactly. +- **(1) The environment redirect.** The env every subject leg runs in is rebuilt: every variable the + subject's own source reads (inventoried by `ast`: `os.environ.get` / `os.environ[...]` / `os.getenv`) + whose value resolves inside the live data root is **redirected** to the per-invocation sandbox — a + redirect, not a refusal — and the inventory prints on stderr as a `Q48 NOTE` on every path, including the + zero case. **(2) Rule (4), the argv redirect**, scoped to the **write surface** (the live snapshot and the + live held directory), not the whole live root: the unrestricted form was written first and it rewrote the + gate's own renderer invocation, a READ that must not move, breaking G7's six checks. **(3) `__file__` + needs no mechanism** — the copy lives in the sandbox, so a root composed from it resolves there by + construction (measured, not asserted). +- **Green pair, five consecutive invocations** (`run_green_q48.py`, `HELM_COLLECTOR=`): + rc 0 each, **`helm-selftest: 171 passed, 0 failed`** / `VERDICT: ALL GATES PASS`, 13,514 B stdout, + 12.45–13.41 s; digests `81fd73b8` / `7a5cfac8` / `e88ff376` / `1f0ec83e` / `eefcc271`. 171 = 169 + 2; no + pre-existing check moved. +- **The discriminator, on the real collector with one mechanical change** (`run_witness_q48.py` → + `out/Q48-WITNESS.json`, 4,686 B): `collector-env.py` = the deployed collector with + `HELM_HOME = Path(os.environ.get("Q48_ROOT", "/root/.helm"))`. An extra `--q48-witness` flag (a copy of + the gate, never the shipped one) builds the copy, prints the rebuilt environment and runs the copy + `--deploy --pin --out `. **Armed, live root value:** `Q48_ROOT` redirected + `/root/.helm → /root/crucible/q48/helm-legs-ppqy3cew`, the pin lands in the sandbox, live unchanged. + **Disarmed, same value:** the pin writes the LIVE held directory, the `abort_if_live_moved` guard fires, + restores byte-exact and records the row as an ABORT. **Prevention separated from detection, by + measurement.** +- **The incident, disclosed not hidden: this run wrote the LIVE held directory three times** — the v1 + fixture-verification run (a fixture's live default), the v1 driver's first armed row, and the + `q48-disarmed-env-liveroot` falsifier. Each restored byte-exact from the Q43/Q45 backup by a script that + **refuses to restore unless the backup's digest matches** the recorded `5b6d7071…`. Final live state: + snapshot 14,039 B / `aa842b0f…` and held pin 120 B / `5b6d7071…`, **`live_delta` 0 on both** across the + whole witness run; the `ctime_ns` restore traces are the durable record and are not recoverable. +- **The residual, stated as the item's first limit:** the gate's legs run the **original** subject with the + inherited environment, so a subject whose root is the live root writes the live **held** directory from a + leg the redirect does not cover — the redirect covers the copy, and the watch is snapshot-only while the + held census is once per run. Closing it needs a per-leg held census or the original-subject legs to + inherit the rebuilt environment; neither is taken here. +- **Limits:** rule (4) fired zero times this run (no leg argument resolved inside the held directory); + four fixture generations failed for reasons that were the fixture's before v7 abandoned the hand-written + fixture for the deployed collector with one mechanical change; the gate's suite is not the discriminator + for this class (no subject leg runs the copy with `--deploy --pin`, and the Q43 resolution-leg check is a + live-identity check that cannot pass for a scratch-root subject) — which is why the witness path exists + as an extra flag; the guard is static, as every guard in this family is. +- **§4 consequence:** the adoption pair is now collector `4e45021d…` + gate **`a590170f…`** (superseding + `76f2292b…`). Q40's finding stands — the deployed gate `7ed1bca8…` reads `HELM_COLLECTOR` zero times — so + the `cp` remains Alexander's and is the only thing that puts Q31–Q48's protection into production. +- **Q7, measured:** the Helm lane's executable set is empty again; no new item opened. The residual above + is the natural successor and is left for its own measurement rather than as a rider on this one. diff --git a/checkins/2026-10-13-merlin.md b/checkins/2026-10-13-merlin.md index 06fdf5c..62efcbb 100644 --- a/checkins/2026-10-13-merlin.md +++ b/checkins/2026-10-13-merlin.md @@ -296,3 +296,125 @@ gate check names, refusal messages, file paths and sha256 digests. This run's co Q36's v1.3 rule. — Merlin + +--- + +## Run 14 — Q48: the runtime-resolution redirect + +### Did + +Claimed and finished **Q48 — the runtime-resolution redirect** (Helm lane; the top unclaimed executable item, +discharging Q45's own §7 limit 1 and the follow-on the pipe's Q47 verification block left named and +unclaimed). Deliverable: `recoveries/helm-runtime-redirect.md` (**14,030 B / sha256 +`8da99a7994c7b8fb…`**, this host). Queue block appended to `WORK-QUEUE.md`. Scratch tree +`/root/crucible/q48/`; nothing deployed, no `cp` into `/root/.helm/bin/`, `decisions/` and `inbox/` +untouched, no another-party file edited, no secret printed. + +**The class.** Q45 relocates every absolute path **literal** in the subject that resolves inside the live +data root. Its §7 limit 1 names what it cannot see, verbatim: *"a root resolved at runtime from `__file__`, +an environment variable or argv, where there is no literal to rewrite."* The pipe's Q47 block carried that +as a named, unclaimed follow-on and said why it had not measured it: a miss writes the live snapshot, and a +restore moves the `mtime_ns`/`ctime_ns` values three records cite as deltas. + +**The pair.** Collector **byte-identical to Q33–Q47's** (`4e45021d80395d3f…`, 720 / 29,954 — every change is +in the gate). Gate **`a590170f…`** (**122,186 B / 2,287 lines**) against Q47's `76f2292b…` (115,182 / +2,124); `out/DIFF-vs-q47.txt` (8,300 B / `71c593e2954ecaf3…`) = **186 lines = 2 file header + 3 hunk +header + 143 added + 0 removed + 18 context**, and 2,124 + 143 − 0 = **2,287** exactly (counted by +`grep -c`, not asserted). + +**Two mechanisms, two checks.** (1) **The environment redirect**: the env every subject leg runs in is +rebuilt — every variable the subject's own source reads (inventoried by `ast`: `os.environ.get`, +`os.environ[...]`, `os.getenv`) whose value resolves inside the live data root is **redirected** to the +per-invocation sandbox. A redirect, not a refusal; the inventory prints as a `Q48 NOTE` on stderr on every +path, including the zero case. (2) **Rule (4), the argv redirect**, scoped to the **write surface** — the +live snapshot and the live held directory — not the whole live root. `__file__` needs no mechanism: the copy +lives in the sandbox, so a root composed from it resolves there by construction (measured, not asserted). +Two new `check()`s: green count 169 + 2 = **171 passed / 0 failed**. + +**Green pair, five consecutive invocations** (`run_green_q48.py`, `HELM_COLLECTOR=`, +`Q48_ROOT` unset — the real collector carries the live root as a literal, so the control is the shipped +collector): rc 0 each, `helm-selftest: 171 passed, 0 failed` / `VERDICT: ALL GATES PASS`, 13,514 B stdout, +12.45–13.41 s; stdout digests `81fd73b8a32e5c10` / `7a5cfac833623db7` / `e88ff3763a73fef1` / +`1f0ec83eccdc2a75` / `eefcc271232b005e` (identical size, digests differ only in the gate's own `mkdtemp` +deploy path and sandbox name). 171 = 169 + 2; no pre-existing check moved. + +**The discriminator, on the real collector with one mechanical change** (`run_witness_q48.py` → +`out/Q48-WITNESS.json`, 4,686 B / `50e7180ac30c3aa0…`). Subject: `collector-env.py` = the deployed collector +with `HELM_HOME = Path(os.environ.get("Q48_ROOT", "/root/.helm"))` (29,982 B / `4739ced839a30564…`, +asserted by count: exactly one `HELM_HOME` assignment, transform landed). The witness is an **extra** +`--q48-witness` flag on a *copy* of the gate (`helm-selftest-witness.py`); the shipped gate never passes it, +so the green run cannot be affected. It builds the copy exactly as the green run does, prints the rebuilt +environment for every variable the subject reads, and runs the copy `--deploy --pin --skip-tests --out +` — `--out` forces the snapshot into the sandbox, so the only path the redirect governs is the +`HELD_DIR` the pin writes. + +| row | root value | rc | `Q48_ROOT` the copy runs in | copy's held write | live held | +|---|---|---|---|---|---| +| `q48-armed-env` | `` | 0 | `/root/crucible/q48/protected` (outside the live root — **no redirect fires**) | `[]` | unchanged | +| **`q48-armed-env-liveroot`** | **`/root/.helm`** | **0** | **`/root/crucible/q48/helm-legs-ppqy3cew` — redirected** (`env_redirects: {"Q48_ROOT": ["/root/.helm", ""]}`) | **`/data/held/stackchain-home.html.sha256`** | **unchanged** | +| `q48-disarmed-env` | `` | 0 | `/root/crucible/q48/protected` | `[]` | unchanged | +| **`q48-disarmed-env-liveroot`** | **`/root/.helm`** | **ABORT** | `/root/.helm` — nothing redirected | **wrote the LIVE held directory** | **NONZERO, restored by the guard** | +| `q47-blindspot-env` | `` | 0 | n/a | `HELM_HOME`/`SNAPSHOT_PATH`/`HELD_DIR` all `/root/crucible/q48/protected` — the class is invisible to the pre-Q48 gate | unchanged | + +**Prevention separated from detection, by measurement**: the armed row redirects the live root into the +sandbox and the pin lands there; the disarmed row, same subject, same live root value, writes live and the +guard catches it, restores byte-exact and aborts. The class is real, one mechanism away, and the mechanism +closes it. + +### Blocked + +Nothing blocked the item. One residual is **mine and is stated as the item's first limit, not a footnote**: +the gate's legs run the **original** subject with the inherited environment (Q43's resolution leg, G8's +dry-run leg), so a subject whose root is the live root writes the live **held** directory from a leg the +redirect does not cover — the redirect covers the copy, the snapshot watch is snapshot-only, and the held +census is once per run rather than per leg. Closing it needs a per-leg held census, or the original-subject +legs to inherit the rebuilt environment; neither is taken here. Rule (4) fired **zero** times this run (no +leg argument resolved inside the held directory), so its only exercised part is the zero case. The gate's +suite is **not** the discriminator for this class — no subject leg runs the copy with `--deploy --pin`, and +the Q43 resolution-leg check is a live-identity check that cannot pass for a scratch-root subject — which is +why the witness path exists as an extra flag rather than as a change to the green run. + +### The incident, disclosed in full — this run wrote the LIVE held directory three times + +Not discovered: **caused, measured, restored, and disclosed**. Three separate live writes of +`/root/.helm/data/held/stackchain-home.html.sha256` happened in this tree, each by a subject resolving the +live root at runtime with the redirect not in the path: + +1. **The v1 fixture-verification run** (~05:5xZ): `make_fixtures_q48.py`'s `verify_interpreter()` ran + `argv-root.py` with no `--root`, so the fixture's live default resolved and it pinned the live held + directory with a 21-byte fixture string. Restored byte-exact by `restore_held_q48.py`, which **refuses to + restore unless the backup's digest matches the recorded `5b6d7071…`**. Post-restore: 120 B / + `5b6d7071…` / `mtime_ns` `1791478687099553071`, `VERDICT: RESTORED BYTE-EXACT`; `ctime_ns` moved to + `1791871227057299704`, which a restore cannot undo. The fix that followed: every fixture carries an + explicit protected-root default and an assertion refusing a live default. +2. **The v1 driver's first armed row** (~06:0xZ): the driver aimed the armed rows at the LIVE root and its + own `abort_if_live_moved()` guard fired on the first `q48-armed-env` row and restored byte-exact. Cause: + the residual above. v2 of the driver used the protected root as the universal root value and witnessed + the class at the mechanism level instead. +3. **The `q48-disarmed-env-liveroot` falsifier** (~06:3xZ): with the redirect cut out and the live root as + the value, the copy's pin wrote the live held directory (`5b6d7071…` → `c0532a8b…`, a real page hash). + The guard aborted, restored byte-exact, and the driver recorded the row as an ABORT and continued, so the + JSON the record rests on exists. + +**Final live state, measured after every row:** snapshot 14,039 B / `aa842b0fd88037f8…` / `mtime_ns` +`1791479579475780303`; held pin **120 B / `5b6d70716153c3f4…` / `mtime_ns` `1791478687099553071`** — +`live_delta` = **0 on both objects** across the whole witness run. The `ctime_ns` restore traces are the +durable record and are not recoverable. + +### Next + +The residual above is the natural successor and is deliberately **not** opened here: it is a one-line change +to a mechanism this run only just built, and it deserves its own measurement rather than a rider on this +one. The §4 adoption pair is now collector `4e45021d…` + gate **`a590170f…`** (superseding Q47's +`76f2292b…`); the gate is still moving, so a copy now lands Q48 with the residual a known gap. Q40's +finding stands — the deployed gate `7ed1bca8…` reads `HELM_COLLECTOR` zero times — so the `cp` remains +Alexander's and is the only thing that puts Q31–Q48's protection into production. + +### Ask + +Alexander's, unchanged: (a) the §4 adoption `cp` of the pair into `/root/.helm/bin/`; (b) the +`manifest_match` pin decision, stale since 2026-10-08; (c) the Q13 publication call. This run added no new +ask. Two things he may want on the record: the live held directory was written and restored three times +during this work (above), and the residual that allowed it is named rather than argued away. + +— Merlin