Files
troupe-standup/recoveries/helm-behavioural-guard.md
Merlin 0776480b51 Q38: the destination guard is behavioural, not documented (Helm lane, Q36 candidate 2)
Q35's precondition reads a source marker (--deploy and SNAPSHOT_PATH.resolve() in the subject's
source), so a subject that documents the rule in a comment without implementing it is admitted —
the pipe measured exactly that at 02:4xZ today. The gate now, at helm-selftest.py:117-211, after
the Q35 check and before the subject import (:784) and G8's bare-refusal leg (:1079-1081, the Q34
incident's leg), runs a copy of the subject's source with its SNAPSHOT_PATH = assignment rewritten
to a sentinel path as a subprocess bare, and refuses the subject (exit 2, no leg runs) unless that
run exits non-zero, writes nothing, and leaves the live snapshot delta 0. The probe never imports
the subject into the gate's process.

Measured this run (scratch tree /root/crucible/q38/, nothing deployed):
- 7 consecutive green invocations, exit 0 each, 164 passed / 0 failed and ALL GATES PASS in every
  log, the Q38 OK line present in all seven, live snapshot sha256+mtime read around every
  invocation with delta 0 throughout.
- Discriminator on both sides: fake-documented-no-rule.py (both markers in its docstring, so the
  Q35 predicate over its source is true) exits 2 with the refusal naming probe rc=0 and the
  sentinel written (74 B / 817e3830...); fake-marker-only.py (the pipe's measured shape) exits 2 at
  the assignment step; the deployed collector exits 2 at the Q35 source check with zero Q38 lines —
  the ordering. The admission control: fake-marker-only.py under the Q35 gate runs G7 and dies at
  the subject import (AttributeError, helm-selftest.py:688) — the admission measured end to end.
- Gate 1,044 -> 1,141 lines (+97/-0; DIFF.txt 108 lines / ac8e52bb...), sha256 643e5387... ->
  74aba348...; collector byte-identical to the Q34/Q35 pair (4e45021d..., 720 lines).
- Live snapshot /root/.helm/data/snapshot.json 14,039 B / aa842b0f... / mtime_ns
  1791479579475780303, delta 0 across every leg; deployed bin untouched (ee0b36a8 / 7ed1bca8 /
  5c67473d); decisions/ and inbox/ untouched; no another-party file edited.

Section 4 consequence: the adoption pair is now collector 4e45021d... + gate 74aba348...
(superseding 643e5387...); the cp into /root/.helm/bin/ remains Alexander's.
2026-10-12 07:06:41 +00:00

10 KiB
Raw Permalink Blame History

The behavioural destination guard — Q38 (Helm lane, 2026-10-12)

Q35 made the destination-rule guard a precondition, but the precondition reads a source marker: the strings --deploy and SNAPSHOT_PATH.resolve() present in the subject collector's source. A subject that documents the rule in a comment without implementing it passes that check and is admitted. The pipe measured exactly that on 2026-10-12T02:4xZ: a fake collector whose body is import sys; sys.exit(0) plus a comment carrying both strings ran the whole Q35 suite to completion with live delta 0. Q38 makes the guard behavioural: the subject is executed against a sentinel destination and must refuse.

Nothing is published, nothing is deployed. The shared build tree, /root/.helm/bin/, the live snapshot, decisions/ and inbox/ are untouched; every figure below came from a command run in this pass. Scratch tree /root/crucible/q38/.

The guard

At helm-selftest.py:117–211 — after the Q35 source-marker check (:96–114) and before the subject is imported (:784) and before G8's bare-refusal leg (:1079–1081, the leg that executed an ungated subject and wrote the live snapshot in Q34's incident):

  1. A copy of the subject's source is written to the gate's own scratch tree, with its SNAPSHOT_PATH = assignment rewritten to a sentinel path (<gate dir>/helm-q38-*/snapshot.json, created per invocation, removed at exit).
  2. The copy is run as a subprocess, bare: python3 <probe> --skip-tests — no --out, no --deploy, 180 s timeout. The subject's module-level code never executes inside the gate's process (the Q35 mechanism probe imported and rebound in-process; this one does not).
  3. The subject passes iff the run exits non-zero and writes nothing to the sentinel and the live snapshot is byte- and mtime-identical before and after. Otherwise the gate exits 2 with a stderr message naming the subject, the probe path, the probe rc, the sentinel's byte count and sha256, and the live snapshot's before/after readings — and no leg runs. A subject that exits 0 and writes nothing is admitted: it cannot clobber the live snapshot bare, which is the property under test.
  4. A subject carrying no SNAPSHOT_PATH = assignment at all is refused at step 1 — the destination the rule keys on does not exist, and such a subject crashes the gate at the import (:784) anyway.

The Q35 source check is kept as the fast first filter and fires first (measured below); the behavioural check is the authority. The precondition registers no check(), so the green pair still reports exactly 164 passed / 0 failed; the positive side prints one Q38 OK line on stderr in every gate log so it is visible rather than implicit.

Gate: 1,044 → 1,141 lines, +97/−0 (git diff --no-index --numstat; DIFF.txt 108 lines / sha256 ac8e52bb38090c66c8592a4aee9b39667d0cbbf6fefb8567043bf1e3ed0d588d), sha256 643e53874e42f7ae… → 74aba34860b34ac6aebd8661e3e46c01ed8c15dcec1cd958ec766114abfa1ef0. The collector is byte-identical to the Q34/Q35 pair: 4e45021d80395d3f…, 720 lines, 29,954 B. Both fixtures and the runner (run-green.sh) live in the same tree.

The subjects and the matrix

subject Q35 source check Q38 behavioural outcome
pair 4e45021d… (/root/crucible/q38/helm-collect.py) pass pass (probe rc=2, sentinel not written, live delta 0) admitted — 7× green
fixtures/fake-documented-no-rule.py pass (both markers in its docstring) fail (rc=0, sentinel written) exit 2, no leg
fixtures/fake-marker-only.py (the pipe's measured shape) pass fail (no SNAPSHOT_PATH = assignment) exit 2, no leg
deployed /root/.helm/bin/helm-collect.py (ee0b36a8…) fail (no markers) not reached exit 2 at Q35 — the ordering

Green pair — 7 consecutive invocations

HELM_COLLECTOR=/root/crucible/q38/helm-collect.py python3 helm-selftest.py, exit 0 each, helm-selftest: 164 passed, 0 failed and VERDICT: ALL GATES PASS in every log, and the Q38 OK: the subject refused its sentinel destination (probe rc=2, sentinel written=False, live delta 0) line present in all seven (run-green.sh reads the live snapshot's sha256 and mtime around every invocation; delta 0 on all seven). Log digests:

f606bf3877cd6a96… / 4cd6456713f2e1ff… / 02ecbf989f2d162c… / 94781f8dfa0f4ac8… / a2380b7ab4c61435… / fbd5e11713a6c048… / d0cae43b79b4f5f1…

The logs differ run to run in the temp paths only (the Q35 tick's C15 class); the verdict line is identical in all seven and is what the digests are quoted for.

fake-B — documents the rule, writes its default anyway

HELM_COLLECTOR=/root/crucible/q38/fixtures/fake-documented-no-rule.py python3 helm-selftest.py → exit 2, stdout 0 bytes (e3b0c442…, the empty-file digest — no helm-selftest: summary and no PASS line exists, which is how "no leg executed" is measured), stderr (q38-refuse-fakeB.err, sha256 38dcd0e40b05f9ba…):

Q38 REFUSAL: the subject collector … carries no WORKING destination rule — a bare run against the sentinel exited rc=0 (stderr: ); sentinel written=True (74 B / 817e38302aff70659ac8f49bd1607a7843748a917eec97dc01582d95c932f437); live delta 0 — so no leg is executed.

Its docstring carries both marker strings, so the Q35 predicate over its source is true (measured: deploy ×2, SNAPSHOT_PATH.resolve() ×1 by line count on the fixture) — it is admitted by the source check and refused by behaviour. Its unrewritten default destination is deliberately not the live snapshot: the unrewritten file is never executed by this gate, but a non-live default means even a false admission during measurement could not clobber live state. The real-world subject this models — the deployed collector — has the live snapshot as its default, and that is the shape Q34's incident ran.

fake-B was not run under the Q35 gate. Its G8 legs include a bare run (:982 in the Q35 file) and an explicit-live---out run (:994), either of which writes the live snapshot with an ungated subject — the accident class itself. The admission was measured by the predicate directly, and the end-to-end admission is shown by fake-C below.

fake-C — the pipe's measured shape, admitted by Q35, refused by Q38

HELM_COLLECTOR=/root/crucible/q38/fixtures/fake-marker-only.py python3 helm-selftest.py → exit 2, stdout 0 bytes, stderr (q38-refuse-fakeC.err, sha256 b3ab82a71d170998…): "carries no WORKING destination rule — the subject carries no SNAPSHOT_PATH = assignment, so its default destination is not the module constant the rule keys on — so no leg is executed."

The same subject under the Q35 gate (HELM_COLLECTOR=… python3 /root/crucible/q35/helm-selftest.py, q35-admit-fakeC.log, 841 B / sha256 691155d4b99dee0b…) ran G7 to completion and then died at the subject import — AttributeError: module 'helm_collect_gate' has no attribute 'SNAPSHOT_PATH' at helm-selftest.py:688 — which is the admission, measured: the Q35 precondition passed the subject and the gate proceeded past it. Under Q38 the same subject never reaches the import.

Ordering — the deployed collector is still refused at Q35

HELM_COLLECTOR=/root/.helm/bin/helm-collect.py python3 helm-selftest.py → exit 2, stdout 0 bytes, stderr (q38-refuse-deployed.err, sha256 2aaa36c1f8277e90…, byte-identical to the Q35 tick's own q35-refuse-deployed.err) carrying the Q35 REFUSAL line and zero Q38 lines: the source-marker check fires first, the behavioural check is not reached, live delta 0.

Live state, measured before and after every leg

/root/.helm/data/snapshot.json: 14,039 B / sha256 aa842b0fd88037f83115b4a54d617f27666f4de06ef84f9a64c8fd0a5f04656d / mtime 2026-10-08 17:12:59.475780303 +0000 (mtime_ns 1791479579475780303) — identical before and after each of the 7 green runs, each of the 3 refusals, and the Q35 admission control. The sentinel temp directories are removed at exit (0 left behind).

Deployed bin untouched: helm-collect.py ee0b36a82f23fcf3… (687 lines / 27,907 B), helm-selftest.py 7ed1bca8f9da624f… (609 / 33,647), helm-render.py 5c67473d89283c3d… (496 / 21,766). Nothing deployed, nothing rebuilt, nothing published; decisions/ and inbox/ untouched; no another-party file edited; no secret printed here or in the files this lands.

Limits, stated

  • The probe substitutes the module's SNAPSHOT_PATH = assignment. A subject that resolves its destination by another route — a hardcoded path, an environment variable — is not covered by the substitution; the live-delta-0 assertion would catch an actual write only after the fact. The pair keys on the module's own SNAPSHOT_PATH (Q31/Q35 measured that rebinding does not disable the rule), so the substitution is sound for this artifact family; it is not a general sandbox.
  • The refusal criterion is rc ≠ 0 and nothing written and delta 0. A subject that refuses with rc = 0 while writing nothing is admitted — correct, since it cannot clobber the live snapshot bare — but "refuses" is defined by the non-zero exit, not by a message.
  • The timeout branch (rc None) is coded and not exercised by a fixture.
  • The guard protects the gate's legs, not a direct CLI run of a pre-gate collector — the Q35 limit, unchanged.
  • The bare-run leg now admits only behaviourally gated subjects, but the shipped gate is still the pre-Q38 one: the §4 cp is Alexander's, and with this run the adoption pair becomes collector 4e45021d… + gate 74aba348… (superseding 643e5387…).

Artifacts

/root/crucible/q38/: helm-selftest.py (the patched gate), helm-collect.py (the pair, unchanged), fixtures/fake-documented-no-rule.py, fixtures/fake-marker-only.py, DIFF.txt, run-green.sh, q38-gate-run-1..7.log, q38-refuse-fakeB.{log,err}, q38-refuse-fakeC.{log,err}, q38-refuse-deployed.{log,err}, q35-admit-fakeC.log.

— Merlin, 2026-10-12