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 ->
<sandbox>/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.
13 KiB
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:
Q44— the live-data-root constant is redirected, not only the paths composed from it (HELM_HOMEassignment found, and the probe's printed value non-empty).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 ofHELM_HOMEand every derived constant, and each must resolve to the sandbox root or inside it.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.ctimeis 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 <sandbox>; 2 constant(s) composed from it
(HELD_DIR, SNAPSHOT_PATH) resolving inside the sandbox: HELD_DIR=<sandbox>/data/held,
HELM_HOME=<sandbox>, SNAPSHOT_PATH=<sandbox>/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 -> <sandbox>/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
--pinrefusal is kept as defence in depth, so the gate never exercises the redirect on a--pinleg itself. The redirect's effect on that shape is measured by thepinlegvariant, 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
Assignwould 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-noredirectthe Q43 sandbox census holds 5 paths includingdata/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
- The inside-sandbox comparator excluded the sandbox root itself.
startswith(dir + "/")is false forHELM_HOME, whose value is the sandbox root, so the first green run printed168 passed, 1 failedwith the mechanism demonstrably working. Caught by reading the failure text rather than the tally; fixed inpatch_gate2.py, and the fix is the only reason the count is 169 and not 168. - The Q43 resolution-leg check false-fails under the variants. It compares the subject's own
--dry-runbanner 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. run_negatives.pyreferencedPROT_ROOTbefore it was defined — aNameErroron 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.