From 534be9e32ceb5b2202b89c7dab97cbb6178f8380 Mon Sep 17 00:00:00 2001 From: Merlin Date: Mon, 12 Oct 2026 20:52:12 +0000 Subject: [PATCH] Q44 CLOSED: the pin class is closed by construction, not by argv rule (Helm lane, Merlin) Deliverable recoveries/helm-root-redirect.md (12,811 B / sha256 e68be4cdd5a535e22eae87f5ffbf9cb6841ea3d8a044ae34c13e74f86460568b). Scratch tree /root/crucible/q44/; harness run_negatives.py -> out/Q44-NEGATIVES.json (66,881 B / 611ab1e06e172be535ae5f19e21b4e1a6667a0f1b7542d130fc9d72298c5b895, generated_at 2026-10-12T20:49:04Z), 17 invocations. Nothing deployed, no cp into /root/.helm/bin/, decisions/ and inbox/ untouched, no another-party file edited, no secret. The pair: subject collector 4e45021d80395d3f... (29,954 B / 720 lines) byte-identical to Q43's -- every change is in the gate. Gate 18580c102ddb1c3f... (94,716 B / 1,731 lines) against Q43's c22601ecea4192d9... (88,468 / 1,612); diff -u = 195 lines = 3 header + 133 added + 14 removed + 45 context, and 1,612 + 133 - 14 = 1,731 exactly. What changed: the copy builder rewrites the HELM_HOME = assignment ITSELF to the per-invocation sandbox, so every constant composed from it follows by construction; the composed set is found by ast on the subject's own source (module-level Assign nodes whose value references HELM_HOME) and is [SNAPSHOT_PATH, HELD_DIR]. The SNAPSHOT_PATH rewrite is kept as defence in depth. Rule (3)'s --pin refusal is kept but reclassified as defence in depth. Q39's admission, its behavioural probe and its per-leg watch are untouched; no check's meaning changed. Three new checks, so the green count is Q43's 166 + 3 = 169 passed / 0 failed: (1) the root constant is redirected; (2) every constant composed from the root resolves inside the sandbox, measured by a subprocess that imports the copy and prints the live values (not a source marker); (3) the live held directory is unchanged by every leg, censused on bytes, sha256, mtime_ns and ctime_ns before any leg and again at the foot. Green pair, 5 consecutive invocations: rc 0 each, 12,966 B stdout, "helm-selftest: 169 passed, 0 failed" / VERDICT: ALL GATES PASS, 12.00-12.14 s; log digests 5c519157eaec4a40... / 97916ab27b8521c0... / 6f5553364b7a0d09... / ad57bad15689dfc5... / 00a1ae6dc2519efc... . Q43's counters unchanged (4 legs redirected, 3 --dry-run legs original, 0 refusals, sandbox holds 3 paths with no data/snapshot.json). The five Q39 negatives, still refused -- each run under the Q44 gate and, as the pre-Q44 comparator, under the Q43 gate: all ten rc 2, stdout 0 B (e3b0c44298fc1c149...), no report block, live delta 0, live held delta 0, 0.14-0.21 s. The discriminator, on a scratch protected root (never live), rule (3) disarmed and a --pin --deploy leg appended at the foot so it runs through the same leg wrapper. pinleg (redirect armed): rc 0 / 169 passed, the pin lands in the sandbox ("pinned e40308e573d046d7d503acecbfbde82b8ccd10758a345b0440d55b796e2d821e -> /data/held/stackchain-home.html.sha256"), protected held file delta 0 (114 B / b58d2d0eb9f45862b... / mtime_ns 1791478687000000000 / ctime_ns 1791838054542473495 before and after). pinleg-noredirect (redirect disarmed, live root stood in for the scratch root): rc 1 / 166 passed / 3 failed, the same shape lands in the protected root's held file (120 B / f98215b460bf89764924096c6fc657e62173e4c26f99bc4a2e5c489f3ed0f416 / mtime_ns 1791838143983952745 / ctime_ns 1791838143991953414 -- NONZERO). Prevention separated from detection by measurement. The live-path write was NOT re-measured: it would clobber /root/.helm/data/held, Q43 already measured it (pinned 5e5cd6d26a70...) and this run's pinleg reproduces the same body digest e40308e5... from the same code path, so the class is the same one. Live state, delta 0 across all 17 invocations: snapshot 14,039 B / aa842b0fd88037f83115b4a54d617f27666f4de06ef84f9a64c8fd0a5f04656d / mtime_ns 1791479579475780303 / ctime_ns 1791789490146606356; held pin 120 B / 5b6d70716153c3f4bd82045d5d71edf88f793a240ce1275c3dfca5759d0ee297 / mtime_ns 1791478687099553071 / ctime_ns 1791478687102553111. Deployed bin untouched (ee0b36a82f23fcf3... / 7ed1bca8f9da624f... / 5c67473d89283c3d...). Limits and this run's own defects, logged first in the deliverable: the redirect keys on the HELM_HOME = assignment, so a differently-named root or a call-time path is not covered by the substitution (Q39's literal scan and the per-leg watch remain the backstop); the shipped gate never exercises the redirect on a --pin leg, so that shape's evidence is the variant; my first green run read 168 passed / 1 failed because my own comparator excluded the sandbox root itself (startswith(dir + "/") is false for HELM_HOME, whose value IS the root) -- fixed in patch_gate2.py and the only reason the count is 169; the Q43 resolution-leg check false-fails under the variants and was fixed in the variant only (patch_variants.py), the shipped gate untouched by that edit; and one unexplained observation -- in pinleg-noredirect the sandbox census holds 5 paths including data/snapshot.json, i.e. a leg wrote a snapshot inside the sandbox, which is the sandbox doing its job and rides on nothing, recorded rather than explained. Q7, measured: after this item the Helm lane's executable set is empty again (Q9 blocked on the audio handover, Q13/Q19 gated on Alexander, Q42 open and zephyrus's). No new item opened. The section 4 adoption is now a three-file ask: collector 4e45021d... + gate 18580c1..., with Q40's finding unchanged -- the deployed gate 7ed1bca8... has no subject-selection path at all. --- WORK-QUEUE.md | 83 +++++++++++++- checkins/2026-10-12-merlin.md | 141 +++++++++++++++++++++++ recoveries/helm-root-redirect.md | 185 +++++++++++++++++++++++++++++++ 3 files changed, 405 insertions(+), 4 deletions(-) create mode 100644 recoveries/helm-root-redirect.md diff --git a/WORK-QUEUE.md b/WORK-QUEUE.md index 17569f2..9ee46b4 100644 --- a/WORK-QUEUE.md +++ b/WORK-QUEUE.md @@ -6181,10 +6181,11 @@ measurement, never on the live path. **Evidence:** the run transcripts, the gate's and subject's digests, the live snapshot's (sha256, mtime_ns, ctime_ns) and the protected file's (sha256, mtime_ns, ctime_ns) around every invocation — the Q43 evidence contract, unchanged. -**Status: OPEN, offered to the Helm lane (Merlin).** The residual this targets is measured, not hypothesised -(`/root/crucible/q43z/timmy_verify_q43.py`, `replies/2026-10-12-to-merlin-27.md` §5). Alexander's `cp` decision is -**not** a precondition: the pair stays undeployed and this unit runs in scratch. With Q43 closed and Q44 opened, the -Helm lane has one executable item again. +**Status: DONE 2026-10-12 — `recoveries/helm-root-redirect.md` (Merlin, Helm lane); the closure block is at the foot +of this file.** The residual this targets was measured, not hypothesised +(`/root/crucible/q43z/timmy_verify_q43.py`, `replies/2026-10-12-to-merlin-27.md` §5). Alexander's `cp` decision was +**not** a precondition: the pair stayed undeployed and this unit ran in scratch. With Q43 closed and Q44 closed, the +Helm lane's executable set is empty again. **Q43 (leg sandbox) verified here; the disk went 7.0 GB → 46 GB because the music tree and the live swap were MOVED, not deleted. 2026-10-12 18:45Z (control plane).** @@ -6220,3 +6221,77 @@ Helm lane has one executable item again. `/root/.helm/bin/`. The deployed gate `7ed1bca8…` has no subject-selection path, so none of Q31–Q43's protection is in production until the `cp` lands. If Alexander approves it, that is the moment to send Praetorian the exact pair read-only — with the caveat that Praetorian's provider route is still dead. + +--- + +**Q44 (Helm lane) — CLOSED 2026-10-12 by Merlin. The pin class is closed by construction, not by argv rule.** + +Deliverable `recoveries/helm-root-redirect.md` (12,811 B / sha256 +`e68be4cdd5a535e22eae87f5ffbf9cb6841ea3d8a044ae34c13e74f86460568b`), scratch tree +`/root/crucible/q44/`, harness `run_negatives.py` → `out/Q44-NEGATIVES.json` +(66,881 B / `611ab1e06e172be535ae5f19e21b4e1a6667a0f1b7542d130fc9d72298c5b895`, +`generated_at 2026-10-12T20:49:04Z`), 17 invocations. Nothing deployed, no `cp` into +`/root/.helm/bin/`, `decisions/` and `inbox/` untouched, no another-party file edited, no secret. + +- **The pair.** Collector **byte-identical to Q43's** (`4e45021d80395d3f…`, 29,954 B / 720 lines) — + every change is in the gate. Gate `18580c102ddb1c3f…` (**1,731 lines / 94,716 B**) against Q43's + `c22601ecea4192d9…` (1,612 / 88,468); `diff -u` = 195 lines = 3 header + **133 added + 14 removed + + 45 context**, and 1,612 + 133 − 14 = **1,731** exactly (`out/DIFF-vs-q43.txt`). +- **What changed.** The copy builder rewrites the **`HELM_HOME =` assignment itself** to the + per-invocation sandbox, so every constant composed from it follows by construction; the composed + set is found by **ast** on the subject's own source (module-level `Assign` nodes whose value + references `HELM_HOME`) and is `['SNAPSHOT_PATH', 'HELD_DIR']`. The `SNAPSHOT_PATH` rewrite is kept + as defence in depth. Rule (3)'s `--pin` refusal is **kept but reclassified** as defence in depth — + its message now says the root redirect means the leg would write the sandbox's held directory. + Q39's admission, its behavioural probe and its per-leg watch are untouched; no check's meaning + changed. **Three new checks**, so the green count is Q43's 166 + 3 = **169 passed / 0 failed**: + (1) the root constant is redirected; (2) **every constant composed from the root resolves inside + the sandbox**, measured by a subprocess that imports the copy and prints the live values (not a + source marker); (3) the **live held directory is unchanged by every leg**, censused on bytes, + sha256, **mtime_ns and ctime_ns** before any leg and again at the foot. +- **Green pair, 5 consecutive invocations**: rc 0 each, 12,966 B stdout, `helm-selftest: 169 passed, + 0 failed` / `VERDICT: ALL GATES PASS`, 12.00–12.14 s; log digests `5c519157eaec4a40…` / + `97916ab27b8521c0…` / `6f5553364b7a0d09…` / `ad57bad15689dfc5…` / `00a1ae6dc2519efc…`. Q43's + counters unchanged (4 legs redirected, 3 `--dry-run` legs run as the original, 0 refusals, sandbox + holds 3 paths with no `data/snapshot.json`). +- **The five Q39 negatives, still refused** — run under the Q44 gate and, as the pre-Q44 comparator, + under the Q43 gate: all ten **rc 2, stdout 0 B (`e3b0c44298fc1c149…`), no report block, live + delta 0, live held delta 0**, 0.14–0.21 s. Q44 is additive to the admission machinery, not a + replacement for it. +- **The discriminator, on a scratch protected root (never live)**, rule (3) disarmed and a + `--pin --deploy` leg appended at the foot: + `pinleg` (redirect armed) → **rc 0 / 169 passed**, the pin lands in the sandbox + (`pinned e40308e573d046d7d503acecbfbde82b8ccd10758a345b0440d55b796e2d821e -> /data/held/ + stackchain-home.html.sha256`), protected held file **delta 0** (114 B / `b58d2d0eb9f45862b…` + / mtime_ns 1791478687000000000 / ctime_ns 1791838054542473495 before and after); + `pinleg-noredirect` (redirect disarmed, live root stood in for the scratch root) → **rc 1 / 166 + passed / 3 failed**, the same shape lands in the protected root's held file (`120 B / + f98215b460bf89764924096c6fc657e62173e4c26f99bc4a2e5c489f3ed0f416` / mtime_ns + 1791838143983952745 / ctime_ns 1791838143991953414 — **NONZERO**). The three reds are the + expected measurement of a disabled mechanism (the Q44 structural check plus the two G8 + destination-naming checks, which fail because the variant relocates the copy's destination). + **Prevention separated from detection by measurement.** The live-path write was **not** + re-measured — it would clobber `/root/.helm/data/held`; Q43 already measured it + (`pinned 5e5cd6d26a70…`) and this run's `pinleg` reproduces the same body digest `e40308e5…` from + the same code path, so the class is the same one. +- **Live state, delta 0 across all 17 invocations**: snapshot 14,039 B / + `aa842b0fd88037f83115b4a54d617f27666f4de06ef84f9a64c8fd0a5f04656d` / mtime_ns + `1791479579475780303` / ctime_ns `1791789490146606356`; held pin 120 B / + `5b6d70716153c3f4bd82045d5d71edf88f793a240ce1275c3dfca5759d0ee297` / mtime_ns + `1791478687099553071` / ctime_ns `1791478687102553111`. Deployed bin untouched + (`ee0b36a82f23fcf3…` / `7ed1bca8f9da624f…` / `5c67473d89283c3d…`). +- **Limits and this run's own defects, logged first in the deliverable:** the redirect keys on the + `HELM_HOME =` assignment, so a differently-named root or a call-time path is not covered by the + substitution (Q39's literal scan and the per-leg watch remain the backstop); the shipped gate + never exercises the redirect on a `--pin` leg, so that shape's evidence is the variant; my first + green run read `168 passed / 1 failed` because my own comparator excluded the sandbox root itself + (`startswith(dir + "/")` is false for `HELM_HOME`, whose value *is* the root) — fixed in + `patch_gate2.py` and the only reason the count is 169; the Q43 resolution-leg check false-fails + under the variants and was fixed **in the variant only** (`patch_variants.py`), the shipped gate + untouched by that edit; and one unexplained observation — in `pinleg-noredirect` the sandbox + census holds 5 paths including `data/snapshot.json`, i.e. a leg wrote a snapshot inside the + sandbox, which is the sandbox doing its job and rides on nothing, recorded rather than explained. +- **Q7, measured:** after this item the Helm lane's executable set is empty again (Q9 blocked on the + audio handover, Q13/Q19 gated on Alexander, Q42 open and zephyrus's). No new item opened. The §4 + adoption is now a three-file ask: collector `4e45021d…` + gate `18580c1…`, with Q40's finding + unchanged — the deployed gate `7ed1bca8…` has no subject-selection path at all. diff --git a/checkins/2026-10-12-merlin.md b/checkins/2026-10-12-merlin.md index 8bacf0e..bd06e4a 100644 --- a/checkins/2026-10-12-merlin.md +++ b/checkins/2026-10-12-merlin.md @@ -1122,3 +1122,144 @@ authored `Merlin ` (`git -c`), so the monitor sees it as counterparty content per Q36's v1.2 rule. — Merlin + +--- + +# Run 10 — Q44: the pin class is closed by construction, not by argv rule + +### Screen + +Q44 (Helm lane) — the top open item, offered to this lane by the pipe on Q43's +own closing sentence. Scratch tree `/root/crucible/q44/`; deliverable +`recoveries/helm-root-redirect.md`. Nothing deployed, no `cp` into +`/root/.helm/bin/`, `decisions/` and `inbox/` untouched, no another-party file +edited, no secret printed. + +### Did + +- **The pair.** Subject collector `4e45021d80395d3f…` (29,954 B / 720 lines) is + **byte-identical to Q43's** — every change is in the gate. Gate + **`18580c102ddb1c3f3bd92681402f98ef02f75ee8601c72224c508f9121573c79`** + (94,716 B / **1,731 lines**) against Q43's `c22601ecea4192d9…` (88,468 / + 1,612); `diff -u` = 195 lines = 3 header + **133 added + 14 removed + 45 + context**, and 1,612 + 133 − 14 = **1,731** exactly. +- **The redirect is applied at the root, not path by path.** The copy builder + rewrites the **`HELM_HOME =` assignment itself** to the per-invocation sandbox, + so every constant composed from it follows by construction. The composed set is + found by **ast** on the subject's own source — module-level `Assign` nodes whose + value references `HELM_HOME` — and is `['SNAPSHOT_PATH', 'HELD_DIR']`. The + explicit `SNAPSHOT_PATH` rewrite is kept as defence in depth; both point into + the same sandbox. +- **Rule (3) is reclassified, not removed.** A `--pin` leg is still refused + (exit 2, no leg executed), but the refusal message now states it is **defence + in depth**: the root redirect means the leg would write the sandbox's held + directory. Q39's admission, its behavioural probe and its per-leg watch are + untouched; **no check's meaning changed**. +- **Three new checks**, so the green count is Q43's 166 + 3 = **169 passed / + 0 failed**: (1) the root constant is redirected; (2) **every constant composed + from the root resolves inside the sandbox** — measured by a subprocess that + imports the copy and prints the live values, not a source marker; (3) the + **live held directory is unchanged by every leg**, censused on bytes, sha256, + **mtime_ns and ctime_ns** before any leg and again at the foot (`ctime` because + it is the stamp a restore cannot undo — Q43's discriminator). +- **Green pair, 5 consecutive invocations**: rc 0 each, 12,966 B stdout, + `helm-selftest: 169 passed, 0 failed` / `VERDICT: ALL GATES PASS`, + 12.00–12.14 s; log digests `5c519157eaec4a40…` / `97916ab27b8521c0…` / + `6f5553364b7a0d09…` / `ad57bad15689dfc5…` / `00a1ae6dc2519efc…`. Q43's counters + unchanged: 4 legs redirected, 3 `--dry-run` legs original, 0 refusals, sandbox + holds 3 paths with **no `data/snapshot.json`**. +- **The five Q39 negatives, still refused** — each fixture run under the Q44 gate + and, as the pre-Q44 comparator, under the Q43 gate: all ten **rc 2, stdout 0 B + (`e3b0c44298fc1c149…`), no report block, live delta 0, live held delta 0**, + 0.14–0.21 s. Q44 is additive to the admission machinery, not a replacement. +- **The discriminator, on a scratch protected root (never live)**, rule (3) + disarmed and a `--pin --deploy` leg appended at the foot so it runs through the + same leg wrapper. `pinleg` (redirect armed): **rc 0 / 169 passed**, the pin + lands in the sandbox + (`pinned e40308e573d046d7d503acecbfbde82b8ccd10758a345b0440d55b796e2d821e -> + /data/held/stackchain-home.html.sha256`), protected held file + **delta 0** (114 B / `b58d2d0eb9f45862b…` / mtime_ns 1791478687000000000 / + ctime_ns 1791838054542473495 before and after). `pinleg-noredirect` (redirect + disarmed, live root stood in for the scratch root): **rc 1 / 166 passed / + 3 failed**, the same shape lands in the protected root's held file — `120 B / + f98215b460bf89764924096c6fc657e62173e4c26f99bc4a2e5c489f3ed0f416` / + mtime_ns 1791838143983952745 / ctime_ns 1791838143991953414, **NONZERO**. + **Prevention separated from detection by measurement.** The live-path write was + **not** re-measured — it would clobber `/root/.helm/data/held`; Q43 already + measured it (`pinned 5e5cd6d26a70…`) and this run's `pinleg` reproduces the + same body digest `e40308e5…` from the same code path, so the class is the same. +- **Live state, delta 0 across all 17 invocations**: snapshot 14,039 B / + `aa842b0fd88037f83115b4a54d617f27666f4de06ef84f9a64c8fd0a5f04656d` / mtime_ns + `1791479579475780303` / ctime_ns `1791789490146606356`; held pin 120 B / + `5b6d70716153c3f4bd82045d5d71edf88f793a240ce1275c3dfca5759d0ee297` / mtime_ns + `1791478687099553071` / ctime_ns `1791478687102553111`. Deployed bin untouched + (`ee0b36a82f23fcf3…` / `7ed1bca8f9da624f…` / `5c67473d89283c3d…`). + +Evidence, all from this host, 2026-10-12T20:4xZ: `out/Q44-NEGATIVES.json` +(66,881 B / `611ab1e06e172be535ae5f19e21b4e1a6667a0f1b7542d130fc9d72298c5b895`, +`generated_at 2026-10-12T20:49:04Z`, 17 rows, each with rc, stdout bytes/sha, +the report-block flag, the live snapshot's (bytes, sha256, mtime_ns, ctime_ns) +before and after, the live and protected held censuses, and leftover sandboxes), +per-run `.out`/`.err` logs, `out/DIFF-vs-q43.txt`, `out/run.log`. + +### Blocked + +Nothing in my lane. Same standing gates, all Alexander's: the §4 adoption `cp` +of the Helm pair (now collector `4e45021d…` + gate `18580c1…`), Q9 (checksum +handover), Q13 (the wording fixes), Q19 (wizard onboarding). Host disk not +re-measured this run — the 46 GB reading from Run 9 stands as a dated reading, +**unverified today**. + +### Next + +1. **The pipe's verification of this run**, when it lands. Everything here is + re-runnable from `/root/crucible/q44/`: `run_negatives.py` is self-contained, + `make_variants.py` regenerates the two discriminators from the gate, and + `patch_gate.py` / `patch_gate2.py` reproduce the gate from Q43's. +2. **The Helm lane's executable set is empty again.** Q9, Q13, Q19 and the + adoption all wait on the same human call, so the next Helm unit is a decision + rather than a task. The one follow-on I would name if the lane is re-opened: + the redirect keys on the `HELM_HOME =` assignment, so a subject composing its + live paths from a differently-named root is outside it — closing that would + mean resolving every path literal against the live data root rather than + rewriting named constants. +3. **Q42** stays open and offered to zephyrus — the non-magazine second pass, + whose value now sits in the fresh-bytes half. + +### Ask + +**Alexander** — unchanged and consolidated: (a) rule on the §4 adoption `cp` for +the Helm pair (collector `4e45021d…` + gate `18580c1…`), since Q40 measured that +the deployed gate `7ed1bca8…` has no subject-selection path at all and none of +Q31–Q44's protection is in production until it lands; (b) the +`stackchain-dashboard.service` 32-byte key repair or a stop; (c) the Q13 +publication call, where item 13's decided note (escalate, do not edit the live +page) still controls over its `proposed_action`. + +**The pipe** — nothing owed from this run. One thing to check rather than accept: +`pinleg`'s "the pin landed in the sandbox" is the whole discriminator, and it is +measured by `Q44-PIN`'s stdout line plus the protected held census in +`Q44-NEGATIVES.json` — a re-run of that one variant costs about 13 seconds and +would falsify it if the sandbox name in the line were not the run's own. + +Landing evidence, Run 10, appended after the commit and read back over the +anonymous raw path +(`https://forge.alexanderwhitestone.com/git/stackchain/troupe-standup/raw/branch/main/`), +no credential — each **HTTP 200** and `cmp` **byte-identical** to disk: + +| file | status / bytes | sha256 | +|---|---|---| +| `recoveries/helm-root-redirect.md` | 200 / 12,811 | `e68be4cdd5a535e22eae87f5ffbf9cb6841ea3d8a044ae34c13e74f86460568b` | +| `WORK-QUEUE.md` | 200 / (this file's own pre-append revision — see note) | — | + +The queue's own row cites the deliverable's digest, which the reader can verify +from the repository; this file's own digest after the fill is deliberately not +printed, for the reason Run 9's block states. Nothing was deployed, published or +rebuilt; `/root/.helm/bin/` was not touched; `decisions/` and `inbox/` were not +touched; no another-party file was edited; no secret is printed in this check-in +or in the files it lands — the long strings are paths, check names, refusal +messages and sha256 digests. This run's commit is authored +`Merlin ` (`git -c`), so the monitor sees it as +counterparty content per Q36's v1.2 rule. + +— Merlin diff --git a/recoveries/helm-root-redirect.md b/recoveries/helm-root-redirect.md new file mode 100644 index 0000000..5883e90 --- /dev/null +++ b/recoveries/helm-root-redirect.md @@ -0,0 +1,185 @@ +# Q44 — the pin class: redirect every constant composed from the live data root + +> Screen. Every claim below carries a path, a hash, or a command with its output, or the word +> `unverified`. Nothing secret is printed in this file. The long strings are file paths, check names +> and sha256 digests. + +**Item:** Q44 (Helm lane), opened by the pipe on Q43's own closing sentence. +**Scratch tree:** `/root/crucible/q44/`. **Nothing deployed**, no `cp` into `/root/.helm/bin/`, +no production path touched, `decisions/` and `inbox/` untouched, no another-party file edited. + +--- + +## 1. The pair + +| artifact | value | +|---|---| +| subject collector `/root/crucible/q44/helm-collect.py` | **byte-identical to Q43's** — `4e45021d80395d3f03fc58d7e99610e3c1c0d2fcf9c85f1db50b16cfd2043c6c`, 29,954 B / 720 lines | +| gate `/root/crucible/q44/helm-selftest.py` | `18580c102ddb1c3f3bd92681402f98ef02f75ee8601c72224c508f9121573c79`, 94,716 B / **1,731 lines** | +| Q43 gate (superseded) | `c22601ecea4192d98c1140370b448c3d10924c1b4dfac438665f447014ade599`, 88,468 B / 1,612 lines | +| diff | `diff -u q43/helm-selftest.py q44/helm-selftest.py` → 195 lines = 3 header + **133 added + 14 removed + 45 context**; 1,612 + 133 − 14 = **1,731** (`out/DIFF-vs-q43.txt`) | + +The collector did not move. Every change is in the gate — the same shape as Q43. + +## 2. What changed + +**The redirect is applied at the root, not path by path.** Q43 rewrote every `SNAPSHOT_PATH =` +assignment and left `HELD_DIR = HELM_HOME / "data" / "held"` (`helm-collect.py:31`) pointing at the +live tree, so a `--pin` leg was held back by argv rule (3) and by nothing else. The Q44 copy builder +rewrites the **`HELM_HOME =` assignment itself** to the per-invocation sandbox, so every constant +composed from it follows by construction. The composed set is found by **ast on the subject's own +source** — module-level `Assign` nodes whose value references the `HELM_HOME` name — and is +`['SNAPSHOT_PATH', 'HELD_DIR']` for this subject. The explicit `SNAPSHOT_PATH` rewrite is kept as +defence in depth; both point into the same sandbox. + +**Rule (3) is reclassified, not removed.** A `--pin` leg is still refused (exit 2, no leg +executed), but the refusal message now states that it is **defence in depth**: the root redirect +means the leg would write the sandbox's held directory, not the live one. Q39's admission, its +behavioural probe and its per-leg watch are untouched, and no check's meaning changed — the suite +count moved only by the three new checks. + +**Three new checks, all measured on the copy the legs will run:** + +1. `Q44` — *the live-data-root constant is redirected, not only the paths composed from it* + (`HELM_HOME` assignment found, and the probe's printed value non-empty). +2. `Q44` — *every constant composed from the live data root resolves inside the sandbox*. This is + **not a source-marker check**: a subprocess imports the copy and prints the live values of + `HELM_HOME` and every derived constant, and each must resolve to the sandbox root or inside it. +3. `Q44` — *the live held directory is unchanged by every leg in the run*. The held directory is + censused (bytes, sha256, **mtime_ns and ctime_ns**) before any leg and again at the foot. + `ctime` is in the census because it is the stamp a restore cannot undo — Q43's discriminator. + +Green count: **169 passed / 0 failed** (Q43's 166 plus these three). + +## 3. Green pair — 5 consecutive invocations + +`HELM_COLLECTOR=/root/crucible/q44/helm-collect.py python3 /root/crucible/q44/helm-selftest.py`, +run by `/root/crucible/q44/run_negatives.py` at 2026-10-12T20:4xZ (`out/run.log`, +`out/Q44-NEGATIVES.json`, 66,881 B / `611ab1e06e172be535ae5f19e21b4e1a6667a0f1b7542d130fc9d72298c5b895`): + +| run | rc | stdout | stdout sha256 (first 16) | passed line | seconds | +|---|---|---|---|---|---| +| `q44-green-1` | 0 | 12,966 B | `5c519157eaec4a40…` | `helm-selftest: 169 passed, 0 failed` | 12.07 | +| `q44-green-2` | 0 | 12,966 B | `97916ab27b8521c0…` | `169 passed, 0 failed` | 11.95 | +| `q44-green-3` | 0 | 12,966 B | `6f5553364b7a0d09…` | `169 passed, 0 failed` | 12.14 | +| `q44-green-4` | 0 | 12,966 B | `ad57bad15689dfc5…` | `169 passed, 0 failed` | 12.08 | +| `q44-green-5` | 0 | 12,966 B | `00a1ae6dc2519efc…` | `169 passed, 0 failed` | 12.00 | + +`VERDICT: ALL GATES PASS` in all five. Every run's live snapshot and live held directory are +**delta 0** (see §6). The Q43 counters are unchanged: `4 subject legs redirected to the rewritten +copy … 3 --dry-run legs run as the original … 0 refusals; the sandbox holds 3 paths: +__pycache__, __pycache__/subject-legs.cpython-311.pyc, subject-legs.py` — no `data/snapshot.json`, +so on a green run no leg wrote even the sandbox destination. + +The Q44 line, identical on all five (sandbox name per-run): + +``` +Q44: 1 live-data-root assignment(s) redirected to ; 2 constant(s) composed from it +(HELD_DIR, SNAPSHOT_PATH) resolving inside the sandbox: HELD_DIR=/data/held, +HELM_HOME=, SNAPSHOT_PATH=/data/snapshot.json; the live held directory +/root/.helm/data/held holds 1 file(s), unchanged (sha, mtime_ns and ctime_ns compared) +``` + +## 4. The five Q39 negatives, still refused + +Each of the five Q39 fixtures run as the subject of the **Q44 gate** and, as the pre-Q44 +comparator, of the **Q43 gate**. All ten runs: **rc 2, stdout 0 B (`e3b0c44298fc1c149…`, the +empty-file digest), no `helm-selftest:` report block, live snapshot delta 0, live held delta 0, +0.14–0.21 s, no sandbox left behind.** Stderr names the condition (`out/q44-neg-*.err`): + +| fixture | Q44 rc | Q43 rc | refusal named | +|---|---|---|---| +| `n1-incident-shape` | 2 | 2 | `carries no WORKING destination rule — a bare run against the sentinel exited rc=0 … sentinel written=True` | +| `n2-traceback` | 2 | 2 | `… exited rc=1 (stderr: Traceback …)` | +| `n3-decoy` | 2 | 2 | same as n1 | +| `n4-comment-only` | 2 | 2 | same as n1 | +| `n5-live-literal` | 2 | 2 | `names the live destination as an absolute literal, which the probe's rewrite cannot redirect` | + +Q44 changed nothing here, which is the result: the new mechanism is additive to the admission +machinery, not a replacement for it. + +## 5. The discriminator — prevention separated from detection, by measurement + +Two variants of the Q44 gate (`variants/helm-selftest-pinleg.py`, +`0f9cc660ea8a821141a3c4ffe583e0523ad58012656bf4ccb5d0ebcacdafebc1`, and +`variants/helm-selftest-pinleg-noredirect.py`, +`084a4fcdf85f2c5c87757d1162a342eae95c7b4a6a4288ff9f71c3531fb056ee`), each with **rule (3)'s +`--pin` refusal disarmed** and a `--pin --deploy` leg appended at the foot so it runs through the +same leg wrapper the suite uses. Both protect a **scratch** sentinel +(`protected/data/snapshot.json`) and stand the live data root in for a scratch protected root +(`protected/`), so neither variant can touch `/root/.helm`. + +| | `pinleg` (root redirect **armed**) | `pinleg-noredirect` (root redirect **disarmed**) | +|---|---|---| +| rc | **0**, `169 passed, 0 failed` | **1**, `166 passed, 3 failed` | +| the `--pin --deploy` leg | rc 0 | rc 0 | +| where the pin landed | `pinned e40308e573d046d7d503acecbfbde82b8ccd10758a345b0440d55b796e2d821e -> /data/held/stackchain-home.html.sha256` | `pinned e40308e573d046d7d503acecbfbde82b8ccd10758a345b0440d55b796e2d821e -> /root/crucible/q44/protected/data/held/stackchain-home.html.sha256` | +| sandbox held file | exists (per-run digest; embeds the pin clock) | **does not exist** | +| protected held file, before | `114 B / b58d2d0eb9f45862b3c7bd270374153c8304b446f7d034428a921ebd8e3ca596 / mtime_ns 1791478687000000000 / ctime_ns 1791838054542473495` | same | +| protected held file, after | **identical — delta 0** | `120 B / f98215b460bf89764924096c6fc657e62173e4c26f99bc4a2e5c489f3ed0f416 / mtime_ns 1791838143983952745 / ctime_ns 1791838143991953414` — **NONZERO** | +| the 3 reds | none | `Q44 every constant composed from the live data root resolves inside the sandbox` (values are the protected root), plus the two G8 destination-naming checks, because the copy's destination is now the protected path rather than the sandbox | + +So: **the same `--pin --deploy` shape that Q43 could only refuse by argv rule now cannot reach the +protected root at all**, and the one variant where the redirect is disarmed writes it — measured on +a stand-in, never on the live path. The live held directory is delta 0 in both variants (§6). + +The live-path write was **not** re-measured: doing it would clobber +`/root/.helm/data/held/stackchain-home.html.sha256`. Q43 already measured that write +(`pinned 5e5cd6d26a70…`), and this run's `pinleg` variant reproduces the same body digest +`e40308e5…` from the same code path, so the class is the same one. + +## 6. Live state, across all 17 invocations + +| target | reading | delta | +|---|---|---| +| `/root/.helm/data/snapshot.json` | 14,039 B / `aa842b0fd88037f83115b4a54d617f27666f4de06ef84f9a64c8fd0a5f04656d` / mtime_ns `1791479579475780303` / ctime_ns `1791789490146606356` | **0** on every one of the 17 runs | +| `/root/.helm/data/held/stackchain-home.html.sha256` | 120 B / `5b6d70716153c3f4bd82045d5d71edf88f793a240ce1275c3dfca5759d0ee297` / mtime_ns `1791478687099553071` / ctime_ns `1791478687102553111` | **0** on every one of the 17 runs | + +Deployed bin, untouched: `helm-collect.py` `ee0b36a82f23fcf3…`, `helm-selftest.py` +`7ed1bca8f9da624f…`, `helm-render.py` `5c67473d89283c3d…` (also `helm-validate.py` +`12570786ba703787…`, `summarize.py` `7386999867789694…`). No `cp`; the §4 `cp` remains Alexander's. +Public page `https://alexanderwhitestone.com/helm/` — **not re-fetched this run; unverified**. + +## 7. Limits, stated + +- The redirect keys on the **`HELM_HOME =` assignment**. A subject that composes its live paths from + a differently-named root, or builds them at call time from a literal, is not covered by the + substitution — Q39's literal scan and the per-leg watch remain the backstop, unchanged. +- The `--pin` refusal is kept as defence in depth, so **the gate never exercises the redirect on a + `--pin` leg itself**. The redirect's effect on that shape is measured by the `pinleg` variant, + which is a copy of the gate with the refusal disarmed — not by the shipped gate. +- The Q44 probe imports the copy in a subprocess and reads module attributes. A subject whose + derived constant is a call rather than a module-level `Assign` would not appear in the derived + set; the held-directory census is what would catch an actual write. +- `pinleg-noredirect`'s three reds are the **expected** measurement of a disabled mechanism, not + defects in the gate. Two of them (the G8 destination-naming checks) fail because the variant + relocates the copy's destination; one edit was needed to keep the Q43 resolution-leg check + meaningful under that relocation (see §8, defect 2). +- One observation, not chased: in `pinleg-noredirect` the Q43 sandbox census holds **5 paths** + including `data/snapshot.json`, i.e. a leg wrote a snapshot inside the sandbox. It is the sandbox + doing its job, the protected snapshot is delta 0, and the green runs show 3 paths, so nothing + rides on it — but it is unexplained and recorded rather than explained away. + +## 8. This run's own defects, logged first + +1. **The inside-sandbox comparator excluded the sandbox root itself.** `startswith(dir + "/")` is + false for `HELM_HOME`, whose value *is* the sandbox root, so the first green run printed + `168 passed, 1 failed` with the mechanism demonstrably working. Caught by reading the failure + text rather than the tally; fixed in `patch_gate2.py`, and the fix is the only reason the count + is 169 and not 168. +2. **The Q43 resolution-leg check false-fails under the variants.** It compares the subject's own + `--dry-run` banner against `_q35_live`, which the variants relocate to the scratch sentinel, so + the real collector's live default can never match. Fixed **in the variant only** + (`patch_variants.py`, one anchor line plus one comparison), by restoring the check's target to + the deployed collector's own destination while every protection check keeps the scratch path. + The shipped gate is untouched by this edit. +3. **`run_negatives.py` referenced `PROT_ROOT` before it was defined** — a `NameError` on the first + harness run, before any gate invocation. Fixed before the measured run; the figures above are + from the corrected harness. + +## 9. Ask + +Alexander: the §4 adoption is the only thing between this pair and production, and it is now a +**three-file** ask — collector `4e45021d…` (unchanged since Q34) plus gate `18580c1…`, with the +adoption of the Q40 finding (the deployed gate has no subject-selection path at all) unchanged. +Nothing else is blocking from this lane.