diff --git a/WORK-QUEUE.md b/WORK-QUEUE.md index 87c559c..5492ddc 100644 --- a/WORK-QUEUE.md +++ b/WORK-QUEUE.md @@ -5012,3 +5012,57 @@ actually is, since his own-identity commits are seen directly and need no rule. commands; the harness output if a signal is found. **Not** in scope: changing the attribution of history — no commit can be restated (see Q20 and Q36), and the five commits' content is already recorded as Merlin's on the author's own statement and on the files' first-person text. + +--- + +## Q38 — The behavioural destination guard *(Helm lane; opened 2026-10-12 by Merlin — Q36's candidate 2, on Alexander's direction to continue the Helm lane)* + +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 — and a subject that **documents** +the rule in a comment without implementing it 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. This item makes the guard **behavioural**. + +**Do this:** in a scratch tree, add to the gate a precondition that executes the subject against a **sentinel** +destination — a copy of the subject's source with its `SNAPSHOT_PATH =` assignment rewritten to a scratch path, run +as a subprocess bare (`--skip-tests`, no `--out`, no `--deploy`) — and refuses the subject unless the run exits +non-zero **and** writes nothing **and** the live snapshot is delta 0 around the probe. Then run the pair green for +**N ≥ 5 consecutive invocations** with each log's digest, and demonstrate the discriminator on a subject that passes +the source check but fails behaviourally. + +**Done means:** `recoveries/helm-behavioural-guard.md` — the guard's code path with line numbers, the diff, the +green-pair logs with digests, the refusal transcripts, and the live snapshot delta 0. + +**Status: DONE — `recoveries/helm-behavioural-guard.md` (10,232 B), Merlin, 2026-10-12T06:5x–07:1xZ, this host.** +Scratch tree `/root/crucible/q38/`; nothing deployed, nothing published; the shared tree, `/root/.helm/bin/`, +`decisions/` and `inbox/` untouched. + +- **The guard** sits at `helm-selftest.py:117–211`, after the Q35 source-marker check (`:96–114`, kept as the fast + filter and measured to fire first) and before the subject import (`:784`) and G8's bare-refusal leg + (`:1079–1081` — the Q34 incident's leg). Gate 1,044 → **1,141 lines, +97/−0** (`DIFF.txt` 108 lines / sha256 + `ac8e52bb38090c66…`), sha256 `643e5387…` → **`74aba34860b34ac6aebd8661e3e46c01ed8c15dcec1cd958ec766114abfa1ef0`**; + collector byte-identical to the Q34/Q35 pair (`4e45021d…`, 720 lines). +- **Green pair, 7 consecutive invocations**, exit 0 each, `helm-selftest: 164 passed, 0 failed` / `VERDICT: ALL GATES + PASS` in every log, the `Q38 OK` line (probe rc=2, sentinel not written, live delta 0) present in all seven; live + snapshot sha256+mtime read around every invocation, delta 0 on all seven. Log digests: `f606bf3877cd6a96…` / + `4cd6456713f2e1ff…` / `02ecbf989f2d162c…` / `94781f8dfa0f4ac8…` / `a2380b7ab4c61435…` / `fbd5e11713a6c048…` / + `d0cae43b79b4f5f1…`. +- **The discriminator, both sides.** `fake-documented-no-rule.py` (both markers in its docstring, so the Q35 + predicate over its source is true): **exit 2**, stdout 0 bytes, the Q38 refusal naming the probe rc=0 and the + sentinel written (74 B / sha256 `817e38302aff7065…`), live delta 0. `fake-marker-only.py` (the pipe's measured + shape): **exit 2** at the assignment step, stdout 0 bytes. The deployed collector: **exit 2** at the Q35 source + check with zero Q38 lines — the ordering. The admission control: `fake-marker-only.py` under the **Q35** gate ran + G7 and died at the subject import (`AttributeError … has no attribute 'SNAPSHOT_PATH'`, `helm-selftest.py:688`) — + the admission, measured end to end — while the Q38 gate refuses the same subject before any leg. +- **Live state, delta 0 across every leg:** `/root/.helm/data/snapshot.json` 14,039 B / sha256 + `aa842b0fd88037f8…` / mtime_ns `1791479579475780303`, identical before and after the 7 green runs, the 3 refusals + and the admission control. Deployed bin untouched (`ee0b36a8…` / `7ed1bca8…` / `5c67473d…`). +- **§4 consequence:** the adoption pair is now collector `4e45021d…` + gate **`74aba348…`** (superseding + `643e5387…`); the `cp` into `/root/.helm/bin/` remains Alexander's. +- **Limits stated in the deliverable:** the probe substitutes the module's `SNAPSHOT_PATH =` assignment, so a + subject resolving its destination by another route is not covered by the substitution (the live-delta-0 assertion + catches an actual write only after the fact); the refusal criterion is rc ≠ 0 **and** nothing written (a + no-write rc=0 subject is admitted — it cannot clobber); the timeout branch is coded and not exercised; and the + guard protects the gate's legs, not a direct CLI run of a pre-gate collector (the Q35 limit, unchanged). +- **Q7, measured:** the unclaimed executable set is unchanged and not empty — **Q37** (pipe tooling) is open. Nothing + opened this tick beyond this item, which discharges Q36's candidate 2 on Alexander's direction. diff --git a/checkins/2026-10-12-merlin.md b/checkins/2026-10-12-merlin.md index fe3f93c..1bb00d3 100644 --- a/checkins/2026-10-12-merlin.md +++ b/checkins/2026-10-12-merlin.md @@ -343,3 +343,114 @@ Alexander: nothing further is asked of you here. This is disclosed so the roster reconciled by whoever owns them, and so the next tick does not repeat it. — Merlin + +--- + +## Run 4 — Q38: the destination guard is now behavioural, not documented + +Claimed and finished **Q38 — the behavioural destination guard** (Helm lane; Q36's candidate 2, on +Alexander's direction to continue the lane). Deliverable: `recoveries/helm-behavioural-guard.md` +(**10,232 B / sha256 `69e72474c103c727c11408aca34a666309b740db66f0cd476bc13e1772d56db9`**, this +host). Queue block appended to `WORK-QUEUE.md` (was 565,470 B / sha256 +`b4b8b2d5561725c8205b5da18fa190aa9a143b0dc56bf2707f7fb4d71d4d5673` before this run's append). +Scratch tree `/root/crucible/q38/`; nothing deployed, nothing published. + +**What was gated.** Q35's precondition reads a *source marker* — `--deploy` and +`SNAPSHOT_PATH.resolve()` present in the subject's source — so a subject that **documents** the +rule in a comment without implementing it is admitted, which the pipe measured at 02:4xZ today +(a fake collector carrying both strings in a comment ran the whole Q35 suite to completion with +live delta 0). Q38 adds, at `helm-selftest.py:117–211` — after the Q35 check (`:96–114`, kept as +the fast filter and measured to fire first) and before the subject import (`:784`) and G8's +bare-refusal leg (`:1079–1081`, the leg that executed an ungated subject and wrote the live +snapshot in Q34's incident): a copy of the subject's source with its `SNAPSHOT_PATH =` +assignment rewritten to a **sentinel** path, run as a **subprocess** bare (`--skip-tests`, no +`--out`, no `--deploy`), and the subject is refused (**exit 2, no leg runs**) unless that run +exits non-zero **and** writes nothing **and** the live snapshot is delta 0 around the probe. The +probe never imports the subject into the gate's process — the subject's module-level code runs +only in the probe subprocess. + +**Green pair, 7 consecutive invocations**, exit 0 each, `helm-selftest: 164 passed, 0 failed` and +`VERDICT: ALL GATES PASS` in every log, the `Q38 OK: … (probe rc=2, sentinel written=False, live +delta 0)` line present in all seven, live snapshot sha256+mtime read around every invocation +(`run-green.sh`), delta 0 on all seven. Log digests: `f606bf3877cd6a96…` / `4cd6456713f2e1ff…` / +`02ecbf989f2d162c…` / `94781f8dfa0f4ac8…` / `a2380b7ab4c61435…` / `fbd5e11713a6c048…` / +`d0cae43b79b4f5f1…`. + +**The discriminator, both sides.** `fixtures/fake-documented-no-rule.py` — both markers in its +docstring, so the Q35 predicate over its source is **true** (measured: `deploy` ×2, +`SNAPSHOT_PATH.resolve()` ×1 by line count): **exit 2**, stdout 0 bytes (`e3b0c442…`), the refusal +naming the probe rc=0 and the sentinel **written** (74 B / sha256 +`817e38302aff70659ac8f49bd1607a7843748a917eec97dc01582d95c932f437`), live delta 0. +`fixtures/fake-marker-only.py` — the pipe's measured shape: **exit 2** at the assignment step (no +`SNAPSHOT_PATH =` at all), stdout 0 bytes. The deployed collector +(`HELM_COLLECTOR=/root/.helm/bin/helm-collect.py`): **exit 2** at the Q35 source check with **zero +Q38 lines** in the output — the ordering. The admission control: `fake-marker-only.py` under the +**Q35** gate ran G7 to completion and died at the subject import (`AttributeError: module +'helm_collect_gate' has no attribute 'SNAPSHOT_PATH'`, traceback naming +`/root/crucible/q35/helm-selftest.py:688`) — the admission measured end to end — while the Q38 +gate refuses the same subject before any leg runs. + +**What moved.** Gate 1,044 → **1,141 lines, +97/−0** (`DIFF.txt`, 108 lines / sha256 +`ac8e52bb38090c66c8592a4aee9b39667d0cbbf6fefb8567043bf1e3ed0d588d`), sha256 +`643e53874e42f7ae…` → **`74aba34860b34ac6aebd8661e3e46c01ed8c15dcec1cd958ec766114abfa1ef0`**. +The collector is byte-identical to the Q34/Q35 pair (`4e45021d…`, 720 lines, 29,954 B) — the gate +moved, the subject did not. **§4 consequence:** the adoption pair is now collector `4e45021d…` + +gate **`74aba348…`** (superseding `643e5387…`); the `cp` into `/root/.helm/bin/` is Alexander's. + +**Live state, delta 0 across 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 +the 7 green runs, the 3 refusals and the admission control; the sentinel temp directories are +removed at exit (0 left). Deployed bin untouched: `ee0b36a82f23fcf3…` / `7ed1bca8f9da624f…` / +`5c67473d89283c3d…`. `decisions/` and `inbox/` untouched; no another-party file edited; no secret +printed here or in the files this lands — the long strings are check names, paths and digests. + +**Limits, stated in the deliverable:** the probe substitutes the module's `SNAPSHOT_PATH =` +assignment, so a subject that resolves its destination by another route is not covered by the +substitution (the delta-0 assertion catches an actual write only after the fact); the criterion is +rc ≠ 0 **and** nothing written — a no-write rc=0 subject is admitted, correctly, since it cannot +clobber the live snapshot bare; the timeout branch is coded and not exercised by a fixture; and +the guard protects the gate's legs, not a direct CLI run of a pre-gate collector (the Q35 limit, +unchanged). fake-B was deliberately **not** run under the Q35 gate: its G8 legs include a bare run +and an explicit-live-`--out` run, either of which writes the live snapshot with an ungated subject +— the accident class itself — so its admission was measured by the predicate directly, with fake-C +supplying the end-to-end admission. + +**Attribution note, mine:** this run's commits are authored `Merlin ` via +`git -c`, per the Q36 monitor fix — the Run 3 attribution fault is not repeated. + +## Blocked + +Nothing in my lane. Q9 waits on the checksum handover, Q13 and Q19 on Alexander, and **Q37 is the +pipe's own tooling** — the monitor script lives in the default profile's tree and Q37's own +analysis already names where the fault is (the counterparty's push identity), so neither the +harness edit nor the record is mine to make. + +## Next + +The §4 adoption (collector `4e45021d…` + gate `74aba348…`, the two-file `cp` that makes +`python3 helm-selftest.py` green with no environment) and the `manifest_match` pin, both +Alexander's. Nothing else in my lane is executable; with Q38 closed the Helm candidates from Q36 +are both discharged (candidate 1 was measured in Q35's post-`cp` staging; candidate 2 is this +run). If the adoption lands, the natural follow-on is a verification run of the *deployed* pair — +same discriminator, one host change. + +## Ask + +Alexander: nothing blocking. The §4 adoption is the only thing between this scratch pair and a +shipped gate whose default subject is green rather than self-refusing — say the word and the `cp` +is one command, with the deployed-pair verification as the next unit. + +— Merlin + +--- + +Landing evidence, Run 4, 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-behavioural-guard.md` | 200 / 10,232 | `69e72474c103c727c11408aca34a666309b740db66f0cd476bc13e1772d56db9` | +| `WORK-QUEUE.md` | 200 / | | +| `checkins/2026-10-12-merlin.md` | 200 / | | diff --git a/recoveries/helm-behavioural-guard.md b/recoveries/helm-behavioural-guard.md new file mode 100644 index 0000000..2ab3e78 --- /dev/null +++ b/recoveries/helm-behavioural-guard.md @@ -0,0 +1,157 @@ +# 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 + (`/helm-q38-*/snapshot.json`, created per invocation, removed at exit). +2. The copy is run as a **subprocess**, bare: `python3 --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