Q39's repair 5 is a net, not a fence: live bytes and mtime are read around every
subprocess and around the subject's in-process import, a change restores the
bytes and refuses the run, so the write has already happened when the watch
fires. Q39's own record names the residual and the Q39 closure asks for "the
stronger form (the legs against a rewritten copy)". This is that form, and it is
the Helm lane's next unit rather than a new invention.
Three rules in the one choke point the gate already has -- the leg wrapper
installed on subprocess.run, so every future leg inherits them:
1. a subject leg carrying --dry-run runs AS THE ORIGINAL, watched: it writes
nothing by the collector's own rule (helm-collect.py:698), and running the
original is what lets the resolution leg read the subject's own destination
out of its banner;
2. every other subject leg, and the in-process import, runs the REWRITTEN COPY
-- every SNAPSHOT_PATH = assignment pointing at a per-invocation sandbox
built after admission and removed at the end of the run -- so module-level
code can write at most into the sandbox;
3. a subject leg is REFUSED (exit 2, the leg never runs) if its argv names the
live snapshot or passes --pin.
Rule 3 is load-bearing, not decoration: the collector refuses only when --out
resolves to its OWN SNAPSHOT_PATH (helm-collect.py:702), so a copy whose
destination is the sandbox would write the live path if a leg named it. The
gate's own "explicit live --out is refused" leg therefore now names the copy's
own destination -- the Q30 discipline applied to the collector. The rule under
test is unchanged; it is measured on a path that is not live, and the semantic
change is stated in the gate and the record rather than left implicit.
Measured, 20 gate invocations, live delta 0 on every one (snapshot 14,039 B /
aa842b0f... / mtime_ns 1791479579475780303; held pin 120 B / 5b6d7071... /
mtime_ns 1791478687099553071):
- green pair, 5 consecutive runs: rc 0, 166 passed / 0 failed, ALL GATES PASS,
12,248 B stdout; 4 subject legs redirected, 3 --dry-run legs run as the
original, 0 refusals; the sandbox holds 3 paths (the copy and its bytecode
cache, no data/snapshot.json); 18 legs watched, delta 0 on each;
- the five Q39 negatives refuse under BOTH gates (rc 2, stdout 0 B, no report
block, live delta 0) -- but they are refused at Q39's admission, before the
Q43 sandbox exists, so they are a regression control and nothing in the
five-fixture suite exercises the redirect. Stated, not hidden;
- the discriminator, on a scratch protected path: argvlive (the gate's own
pre-Q43 leg shape, rule 3 armed) -> rc 2, the refusal naming the leg's argv,
the copy's destination and the rule at :702, and the protected file's sha,
mtime AND ctime unchanged -- the leg never ran. argvlive-norule3 (rule 3
disarmed) -> rc 2 via Q39's watch, protected ctime MOVED while sha and mtime
were restored -- the leg ran, the copy wrote a real snapshot over the
protected file, and only the restore put it back. ctime_ns is the
discriminator: the stamp a restore cannot undo;
- the --pin residual: pin_stackchain() writes HELD_DIR, composed from HELM_HOME
and untouched by the rewrite, so a --pin leg is refused (rc 2, naming --pin
and /root/.helm/data/held); a copy with HELD_DIR also redirected to scratch,
run --pin --deploy, writes the scratch held file, so the path is real; bare
--pin is refused by the destination gate before `if args.pin`, so it is one
flag combination away, not on the default path;
- the rewrite is behaviour-preserving: original vs copy, both --dry-run, both
20,570 B, scrubbed over the gate's full clock set -> diff_keys [].
Three defects of mine, logged first in the record: the redirect keyed on argv[0],
which is the interpreter (the subject is an argv element, and the first run
measured 0 redirects while its legs ran the original -- the Q40 shape); the
behaviour-preservation scrub omitted generated_at and duration_seconds; and the
first --pin measurement ran bare, so the held path was never reached.
Limits stated in the record: prevention is total for the SNAPSHOT_PATH class, not
for every class; the legs still execute the subject's code, and damage to
anything other than the snapshot is out of this gate's scope; the five-fixture
suite no longer reaches Q43's machinery, so the two variants and the pin residual
are the coverage.
Nothing deployed: no cp into /root/.helm/bin/, the deployed bin is unmoved
(ee0b36a8... / 7ed1bca8... / 5c67473d...), decisions/ and inbox/ untouched, no
another-party file edited, no secret printed. Adoption stays Alexander's, and Q40
measured that the deployed gate has no subject-selection path at all, so the cp
is the only thing that puts any of Q31-Q43's protection into production.
13 KiB
The leg sandbox: the gate's legs run the rewritten subject
Item: Q43 (Helm lane). Author: Merlin. Run: 9 of 2026-10-12.
Scratch tree: /root/crucible/q43/. Nothing was deployed: no cp into
/root/.helm/bin/, no production path touched, decisions/ and inbox/
untouched.
Identities
| artifact | lines | bytes | sha256 |
|---|---|---|---|
gate helm-selftest.py (Q43) |
1,612 | 88,468 | c22601ecea4192d98c1140370b448c3d10924c1b4dfac438665f447014ade599 |
subject helm-collect.py |
720 | 29,954 | 4e45021d80395d3f03fc58d7e99610e3c1c0d2fcf9c85f1db50b16cfd2043c6c |
| gate (Q39, the predecessor) | 1,412 | 77,704 | 9b09f3283008e695bb0634bc2d5492f2c24067b9eeb9ee2100099b7cbfcd1429 |
deployed helm-collect.py |
— | 27,907 | ee0b36a82f23fcf3c4742de95a205f824715b931fdb80947a897eb88999c4df6 |
deployed helm-selftest.py |
609 | 33,647 | 7ed1bca8f9da624f69b03313506097a0ce9b1130cbad9511908776e5015a0f46 |
The subject collector is byte-identical to Q39's (4e45021d…): every change
is in the gate. diff -u of the two gates is 281 lines = 4 header + **210 added
- 10 removed**, and 1,412 + 210 − 10 = 1,612 exactly (
out/DIFF-vs-q39.txt). Live snapshot at every reading in this record: 14,039 B /aa842b0fd88037f8…/ mtime_ns1791479579475780303; live held pin 120 B /5b6d70716153c3f4…/ mtime_ns1791478687099553071.
What Q39 left open, and what this does about it
Q39's repair 5 is detection: live bytes and mtime are read around every subprocess and around the subject's in-process import, a change restores the bytes and refuses the run. That is a net, not a fence — the write has already happened when the watch fires, and the file was wrong on disk for the length of the leg. Q39's own record says so ("the legs still run the ORIGINAL subject, so repair 5 detects-and-restores rather than prevents") and the Q39 closure asks for "the stronger form (the legs against a rewritten copy)". This is that form.
Three rules, applied in one choke point — the leg wrapper that Q39 already
installed on subprocess.run, so every future leg inherits them:
- A subject leg carrying
--dry-runruns as the ORIGINAL, watched.--dry-runwrites nothing by the collector's own rule (helm-collect.py:698), so there is nothing to prevent — and running the original is what lets the resolution leg read the subject's own destination out of its banner. - Any other subject leg runs the REWRITTEN COPY — the subject's own source
with every
SNAPSHOT_PATH =assignment pointing at a per-invocation sandbox, built after admission and removed at the end of the run. The most a leg can write through the module constant is a sandbox file. - A subject leg is REFUSED (exit 2, the leg never runs) if its argv names
the live snapshot or passes
--pin, because neither is redirectable.
Rule 3 exists because the rule under test is narrower than it looks. The
collector refuses when Path(args.out).resolve() == SNAPSHOT_PATH.resolve()
(helm-collect.py:702) — its own default destination, and nothing else. Move
that destination and the protection evaporates for an explicit --out: a copy
whose destination is the sandbox would happily write the live path if a leg
named it. So the gate's own "explicit live --out is refused" leg now names the
copy's own destination instead — the Q30 discipline (test the deploy branch
against a patched constant, never the real one) applied to the collector. The
rule under test is unchanged; it is measured on a path that is not live.
The in-process import (hcg = _load_collector(...)) loads the copy too, so
module-level code — which runs before any argv is parsed and which Q39 could
only watch — can write at most into the sandbox. LIVE_SNAPSHOT is the
deployed collector's destination, read before any subject code runs, and a
new resolution leg proves the subject's own default destination is that same
path.
Measurements
Every run below is one invocation of the gate with HELM_COLLECTOR naming its
subject. 20 gate invocations in this record; live delta 0 on all of them.
Side effects are detected from outside the run, never asserted
(run_negatives.py, run_residuals.py, live_state.py; logs and
Q43-NEGATIVES.json / Q43-RESIDUALS.json under out/).
The green pair — 5 consecutive invocations
HELM_COLLECTOR=/root/crucible/q43/helm-collect.py python3 /root/crucible/q43/helm-selftest.py:
| exit code | 0 on all five |
| report | helm-selftest: 166 passed, 0 failed / VERDICT: ALL GATES PASS |
| stdout | 12,248 B on all five (identical digest per run, in Q43-NEGATIVES.json) |
| Q39 line | 18 legs watched … delta 0 on each |
| Q43 line | 4 subject legs redirected … 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 |
| live | delta 0 (bytes, sha256, mtime_ns) on each |
| seconds | 12.01–12.39 |
166 = Q39's 164 + the two new Q43 checks. The sandbox census is the load-bearing
row: the sandbox holds the copy and its bytecode cache and nothing else — no
data/snapshot.json — so on a green run no leg wrote even the sandbox
destination. The four writing legs are --out <scratch> ×2 (they write their
own scratch path), the bare leg (refused by the copy's own rule) and the
explicit---out leg (refused likewise).
The five Q39 negatives — refused under BOTH gates
n1-incident-shape, n2-traceback, n3-decoy, n4-comment-only,
n5-live-literal, each as HELM_COLLECTOR:
| gate | rc | stdout | report block | live delta | fixture tree |
|---|---|---|---|---|---|
| Q43 | 2 | 0 B | absent | 0 | unchanged (census before = after) |
| Q39 | 2 | 0 B | absent | 0 | unchanged |
They are refused at Q39's admission, before the Q43 sandbox exists, so they are a regression control on the admission, not evidence about Q43. Stated plainly: nothing in the five-fixture suite exercises the redirect. That is what the two variants below are for.
The discriminator — rule 3, on a scratch protected path
Two variants of the gate, both protecting a scratch file
(/root/crucible/q43/protected/protected-snapshot.json) instead of the live
snapshot, so a false admission during measurement cannot clobber live state:
| variant | what it is | rc | report | protected sha | protected mtime | protected ctime |
|---|---|---|---|---|---|---|
argvlive |
the gate's own pre-Q43 leg shape: the explicit---out leg names the protected path, rule 3 armed |
2 | absent | unchanged | unchanged | unchanged |
argvlive-norule3 |
the same, with rule 3's live-argv refusal disabled | 2 | absent | unchanged | unchanged | moved |
- Armed: the refusal fires before the leg —
Q43 REFUSAL: a subject leg names the live snapshot in its argv (/root/crucible/q43/protected/protected-snapshot.json), and the leg runs the rewritten copy whose own destination is /root/crucible/q43/variants/helm-legs-uxm6uxze/data/snapshot.json, so the rule at helm-collect.py:702 would not refuse it and the leg would write live— naming the leg's argv, the copy's destination and the rule. The protected file's inode is untouched. - Disarmed: the leg runs and the copy writes the protected file, and only
Q39's watch stops it —
Q39 REFUSAL: a leg's subprocess (… --out /root/crucible/q43/protected/protected-snapshot.json --skip-tests) changed the live snapshot - sha256 aa77115031aa8546… before, sha256 c9f9401454b9fb89… after; the before-bytes were restored and the run is refused.
The discriminator is ctime_ns. The watch's restore rewrites the bytes and
re-stamps the mtime, so both are back where they started and neither can show
that a write occurred; the inode change time is the residue a restore cannot
undo. Armed: ctime unmoved, the write never happened. Disarmed: ctime moved, the
write happened and was repaired. That is the difference between prevention and
detection, measured rather than argued.
The --pin residual — a second live path the rewrite cannot redirect
pin_stackchain() writes HELD_DIR / "stackchain-home.html.sha256"
(helm-collect.py:664-680), and HELD_DIR = HELM_HOME / "data" / "held"
(:31) is composed from HELM_HOME, not from SNAPSHOT_PATH — so the
rewrite does not touch it, and Q39's absolute-literal scan admits it because
/root/.helm is not inside the data root. Measured:
| run | subject | rc | result |
|---|---|---|---|
| refusal | the real gate with a --pin leg |
2 | Q43 REFUSAL: a subject leg passes --pin, and --pin writes /root/.helm/data/held (HELD_DIR, composed from HELM_HOME) which the SNAPSHOT_PATH rewrite cannot redirect (helm-collect.py:664-680); report block absent; live snapshot and held pin unchanged |
| the write | a copy with HELD_DIR also redirected to scratch, --pin --deploy |
0 | pinned 5e5cd6d26a7004239c0ff8362b2f54c1697d848a602b35de7aa5580268c018eb -> /root/crucible/q43/residuals/held/stackchain-home.html.sha256; live held pin and live snapshot unchanged |
| the default path | the same copy, bare --pin |
2 | refusing to write the live snapshot … without --deploy — the destination gate at :702 fires before if args.pin (:707) |
So the second live path is one flag combination away, not on the default
path: --pin alone is refused by the destination gate, --pin --deploy
reaches it. Rule 3 refuses --pin on any subject leg regardless.
Side observation, evidence attached: the scratch pin's digest
5e5cd6d26a70… is the text-mode hash of the live body, because run() calls
subprocess.run(..., text=True) (helm-collect.py:123) and universal-newline
translation applies — the same normalisation my 2026-10-11 measurement recorded.
The live held pin on disk carries f27326e8b13d1dec…, pinned
2026-10-08T16:58:07.100Z. Both are the same code path's hash of the same URL, so
the live Stackchain body's normalised digest has changed since the pin was
taken. Corroborated for the two digests and the code path; this host cannot
prove which revision wrote the held file, so the drift reading rests on
pin_stackchain() being its only writer.
The rewrite is behaviour-preserving
The original and the rewritten copy, both --dry-run --skip-tests, both
20,570 B of stdout, scrubbed over the gate's own full clock set (observed_at,
rtt_seconds, age_seconds, last_active_at, last_checkin_at,
latest_active_path, newest_checkin_file, generated_at,
duration_seconds): diff_keys: []. That is what licenses "the legs run
the same collector at a different destination" as a measurement rather than an
assumption. Independently, all 166 checks pass with the copy as the subject,
including the two-run byte-agreement check and every Q33/Q34 in-process check.
Three defects of mine, logged first
- The redirect keyed on
argv[0], which is the interpreter. The gate's legs are[sys.executable, str(COLLECTOR), …], so the subject is an argv element. First run measured:0 subject legs redirected, the bare leg ran the ORIGINAL against the live default (refused by the collector's own rule, rc 2, live delta 0) and the explicit---outleg wrote the sandbox path through the original's argv handling. Fixed by_q43_subject_index(), which scans the argv. Both first-run effects were benign, and the run is the evidence: a gate that reports 0 redirects while its legs run the original is exactly the Q40 shape. - The scrub in my behaviour-preservation check omitted
generated_atandcollector.duration_seconds— the gate's own `TOP_SCRUB_KEYS — so the first reading reported the rewrite as behaviour-changing when the only differing key was a clock. Re-measured with the full set: identical. - The first
--pinmeasurement ran the collector bare, so the destination gate fired beforepin_stackchain()and the held path was never exercised. The finding (that--pinneeds--deployto be reachable) came out of fixing it.
Limits
- The static scan Q39 added is literal-based; a subject composing its live
destination from a constant other than
SNAPSHOT_PATHorHELM_HOMEis not covered by the redirect, and rule 3 covers only the two shapes measured here (--outat the live path,--pin). - Rule 2 redirects the module constant. A subject that writes live through a
path built at runtime from anything else is still caught only by Q39's watch —
prevention is total for the
SNAPSHOT_PATHclass, not for every class. - The legs still execute the subject's code; what changed is which destination that code can reach. A subject that damages something other than the snapshot is out of scope for this gate entirely.
- The five-fixture suite is not a census, and it no longer reaches Q43's machinery at all. The two variants and the pin residual are the coverage.
- 20 invocations on one host, one collector revision.
Adoption
The pair is subject 4e45021d… + gate c22601ec…. The deployed bin is unmoved
(ee0b36a8… / 7ed1bca8… / 5c67473d…), the live snapshot is unchanged, and
nothing was published. Adoption stays Alexander's — the cp is the only
thing that puts any of Q31–Q43's protection into production, and Q40 measured
that the deployed gate has no subject-selection path at all.