The subject collector named by HELM_COLLECTOR is checked for the rule (--deploy and SNAPSHOT_PATH.resolve() in its source) at helm-selftest.py:96-114 BEFORE the subject is imported (:679) and before G1, and refused with exit 2, a stderr message naming the subject and the missing rule, and the live snapshot read before/after the refusal. Gate +61/-2 (985 -> 1044 lines, 45e53187 -> 643e5387); collector byte-identical to the Q34 pair (4e45021d); the precondition registers no check() so the suite count is unchanged at 164 and the old foot-of-suite source check is kept at :1032. Seven consecutive green invocations (164 passed / 0 failed each, log digests in the deliverable), live snapshot delta 0 across every leg (aa842b0f / 14039 B / mtime_ns 1791479579475780303). Discriminator both sides in one run pair: deployed pre-gate collector as subject -> exit 2, stdout 0 bytes (e3b0c442, no summary and no PASS line, which is how "no leg executed" is measured), live delta 0; missing subject refused the same way, not crashed. Mechanism probe from this tree (q35-mechanism.py, mech-probe.log): default destination rebound to scratch, bare main() -- deployed ee0b36a8 rc=0 no refusal, 18523 B written (9331b218, schema helm.snapshot/1); pair 4e45021d rc=2, refusal naming the path and --deploy, nothing written. Limits stated: the gate run bare now refuses itself (default subject is the pre-gate deployed collector; the section-4 cp is what makes the shipped gate green, Alexander's); the precondition reads a source marker, not behavioral proof; the rule protects the gate's legs, not a direct CLI run. Nothing deployed; decisions/ and inbox/ untouched.
8.1 KiB
The gate refuses an ungated subject before any leg runs
Q35 — Helm-lane gate precondition. Author: Merlin (merlin@agents.invalid). Clock: 2026-10-12T02:10–02:36Z, this host.
Scratch tree: /root/crucible/q35/. Nothing deployed; the §4 adoption cp into /root/.helm/bin/ is out of scope and untouched.
The twice-fired class, made mechanical at the gate. Two live-snapshot writes this day came from the same
shape — a negative-control leg of the gate running a subject collector that carries no destination rule
of its own, so the leg's bare --skip-tests call executed it and it wrote its default destination
(2026-10-11T13:28Z Q31; 2026-10-12T00:26:01.933369921Z Q34, restored byte-exact). The rule was in
the skill and in the gate's own source check, but that check ("the collector the gate runs carries the
destination rule (source guard)") sat at the foot of the suite — after every leg that could execute
the collector. It red-flagged the subject after the damage. Q35 hoists the rule to a precondition:
the subject named by HELM_COLLECTOR is checked before it is imported and before G1, and refused
with exit 2 and a message naming the subject and the missing rule. No leg runs.
What moved, and where the guard now lives
The collector did not move: /root/crucible/q35/helm-collect.py is byte-identical to the Q34 pair's
(sha256 4e45021d80395d3f03fc58d7e99610e3c1c0d2fcf9c85f1db50b16cfd2043c6c, 720 lines). The gate moved
+61 / −2 (git diff --no-index --numstat helm-selftest-q34-before.py helm-selftest.py → 61 2),
985 → 1,044 lines, sha256 45e53187c01398df7a0edcbf24a08b7d91255eda0cfe93b9cb01e8398f0d7091 →
643e53874e42f7ae58160c06c47eb07846725eae397104d9265964506bfcd0c9.
The precondition's code path, in helm-selftest.py:
| line | what |
|---|---|
:96 |
COLLECTOR = Path(os.environ.get("HELM_COLLECTOR", "/root/.helm/bin/helm-collect.py")) — resolved once, at the top |
:97–99 |
live snapshot path from the deployed module (hc.SNAPSHOT_PATH, the module the gate has always imported at :42), source text read |
:100–114 |
the rule check: "--deploy" in src and "SNAPSHOT_PATH.resolve()" in src; on failure the live snapshot is read before and after, the refusal (subject, missing rule, both readings, delta) is written to stderr, and sys.exit(2) |
:679 |
hcg = _load_collector(COLLECTOR) — the subject is imported only after the precondition has passed |
:1032 |
the old foot-of-suite check is kept as the in-suite statement of the same rule (PASS G8 the collector the gate runs carries the destination rule (source guard), measured at q35-gate-run-7.log:176). On any green run it is implied by the precondition having passed |
The precondition registers no check(): a refusal fails the run, not a suite result, so the green
pair still reports exactly 164 passed / 0 failed — unchanged from Q34, Q34's four pointer checks
included (measured at q35-gate-run-7.log:163-166).
The mechanism probe — the rule measured on both subjects, not read
q35-mechanism.py (sha256 31df78f6e9bcf755000cd7ed47bf92e6f097e5aef7c63a2aaf2a005ad53dca11), run from
this tree (mech-probe.log, sha256 c4714d743a388ad3e1a52049a3347af8e939aca67db9f363bbef83125ab26dba):
each collector is imported, its SNAPSHOT_PATH (the default destination) rebound to scratch, and
main() called bare with --skip-tests. Rebinding does not disable the rule — the refusal keys on the
module's own SNAPSHOT_PATH, whatever path it holds — it only removes the blast radius.
| subject | rc | scratch default written | stderr | live snapshot delta |
|---|---|---|---|---|
deployed, pre-gate (/root/.helm/bin/helm-collect.py, ee0b36a8…, --deploy ×0, SNAPSHOT_PATH.resolve() ×0) |
0 | yes — 18,523 B, sha256 9331b21860e5c9a481e8a6b8e6b6edbb3040832262a717a3349acff7b2f44211, schema helm.snapshot/1 |
none — wrote … on stdout |
0 (aa842b0f…, 14,039 B) |
pair, Q33/Q34 (helm-collect.py, 4e45021d…) |
2 | no | refusing to write the live snapshot … without --deploy; pass an explicit --out <scratch path> instead |
0 |
The discriminator reproduces: the deployed collector writes its default with no refusal; the pair refuses its default. That is the behavioral half of the source marker the precondition reads.
The green pair — 7 consecutive invocations
HELM_COLLECTOR=/root/crucible/q35/helm-collect.py python3 helm-selftest.py, exit 0 every time,
helm-selftest: 164 passed, 0 failed / VERDICT: ALL GATES PASS:
| run | log | sha256 of log |
|---|---|---|
| 1 | q35-gate-run-1.log |
32faa0e91a8da5a4bf6a3be8a6be60e9ac92aa8750dbbbb636fe02d3b2c0e0a1 |
| 2 | q35-gate-run-2.log |
bfa6d09e7c510d562d1b0d370e30092144e25de215a7fb50ebb9bc05dc2aadf3 |
| 3 | q35-gate-run-3.log |
8bd47387cba01b5f3429637ea48eaa8b6cc3fefc72e49a65570beb071f14eeb1 |
| 4 | q35-gate-run-4.log |
627561761ccb34a1d737eb2fb6dcc88dc160984eafe9db72bcd51b3d8cebf33c |
| 5 | q35-gate-run-5.log |
1c9639efc502d4a7ccb4e0621bcaa5fe3ff763c5f588fb3f553eaa49d40e6bb2 |
| 6 | q35-gate-run-6.log |
f99929816ae3b28126bef11b8a9f29e5e35dc6f72a3a03439ced6b168d2d1317 |
| 7 | q35-gate-run-7.log |
e5a2c5fd484e874d954feee1fecb22da2ed6eafd2ff05be0bfa402fda6b911ca |
Log digests differ run to run only by clocks and durations; the verdict line is identical in all seven.
The refusal side of the discriminator — both subjects, one run pair
HELM_COLLECTOR=/root/.helm/bin/helm-collect.py python3 helm-selftest.py
(q35-refuse-deployed.err, sha256 2aaa36c1f8277e900268ff4c5ff1d8ebda8b5fce16d9dd15eaa9521e1c66ca96):
- exit 2, and stdout is 0 bytes (
q35-refuse-deployed.log, sha256e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855— the empty-file digest). Nohelm-selftest: N passedsummary and noPASSline exists, which is what "no leg executed" is measured by: the report block is at the foot of the suite, so a run that reaches it prints. - stderr, verbatim:
Q35 REFUSAL: the subject collector /root/.helm/bin/helm-collect.py carries no destination rule (missing '--deploy' and/or 'SNAPSHOT_PATH.resolve()'), so no leg is executed.plus the live-snapshot readings line. - Live snapshot across the refusal: sha256
aa842b0fd88037f83115b4a54d617f27666f4de06ef84f9a64c8fd0a5f04656d/ mtime_ns1791479579475780303before and after — delta 0, the same reading held across all seven green runs and both probe legs (14,039 B throughout).
A missing subject is refused, not crashed: HELM_COLLECTOR=/root/crucible/q35/no-such-collector.py
→ exit 2, same refusal shape (q35-refuse-missing.err, sha256
900386166ac630ce957afb6c805b797170963572fd4ebc64841d8d0b955a9f88), stdout 0 bytes, live delta 0.
Limits, stated
- The gate run bare now refuses itself. The default
HELM_COLLECTORis the deployed collector, which is pre-gate — sopython3 helm-selftest.pywith no environment exits 2. That is the item working as specified, and it is why the green runs above name the subject explicitly. The §4cp(collector + this gate as a pair) would make the shipped gate green; that move is Alexander's and is unchanged by this run. - The precondition reads a source marker, not a proof of behavior.
"--deploy"and"SNAPSHOT_PATH.resolve()"are strong markers — the second is the rule's own comparison — but a subject carrying both strings without the rule would pass the check. The mechanism probe above is the behavioral half; making the marker itself behavioral (import in a sandbox, measure the refusal) is not attempted here because importing an untrusted subject is the very thing the precondition exists to prevent. - The rule protects the gate's legs, not a direct CLI run. A human or script running a pre-gate collector bare still overwrites the live snapshot. The gate refuses to use such a subject; nothing in this change stops a collector from being executed outside the gate.
- The live-snapshot readings above are this host's, at the clocks stated; every figure was re-measured in the same session as the runs it describes.