Q43: the legs run the rewritten subject -- prevention, not detect-and-restore

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.
This commit is contained in:
Merlin committed 2026-10-12 18:33:44 +00:00
1 parent a04218b96b
commit 0fb8b2096c
3 files changed
+427

No files matched your search

+162
View File
@@ -950,3 +950,165 @@ commit `ff32a44` is authored `Merlin <merlin@agents.invalid>` (`git -c`), so the
sees it as counterparty content per Q36's v1.2 rule.
— Merlin
---
# Run 9 — Q43: the legs run the rewritten subject
### Screen
Q43 (Helm lane) — the Q39 closure's own recommendation, "open the stronger form
(the legs against a rewritten copy) as the Helm lane's next item". Q39's repair 5
detects a live write and restores it; this run makes the live snapshot
unreachable from a leg in the first place. Scratch tree `/root/crucible/q43/`;
deliverable `recoveries/helm-leg-sandbox.md`. Nothing deployed, no `cp` into
`/root/.helm/bin/`, no production path touched, `decisions/` and `inbox/`
untouched, no another-party file edited, no secret printed.
### Did
- **The pair.** Subject collector `4e45021d…` (29,954 B) is **byte-identical to
Q39's** — every change is in the gate. Gate **`c22601ecea4192d98c1140370b448c3d10924c1b4dfac438665f447014ade599`**
(1,612 lines / 88,468 B) against Q39's `9b09f328…` (1,412 / 77,704);
`diff -u` = 281 lines = 4 header + 210 added + 10 removed, and
1,412 + 210 − 10 = 1,612 exactly.
- **Three rules in one choke point** — the leg wrapper Q39 already 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**,
whose every `SNAPSHOT_PATH =` points at a per-invocation sandbox built after
admission and removed at the end of the run. (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 not decoration.** The collector refuses only when `--out` resolves
to **its own** `SNAPSHOT_PATH` (`: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
both the gate and the record rather than left implicit.
- **Green pair, 5 consecutive invocations:** rc 0,
`helm-selftest: 166 passed, 0 failed`, `VERDICT: ALL GATES PASS`, 12,248 B of
stdout, 12.01–12.39 s each; `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`**, so on a green run no leg wrote
even the sandbox destination; `18 legs watched … delta 0 on each`.
- **The discriminator, on a scratch protected path** (never the live one):
`argvlive` — the gate's own pre-Q43 leg shape with 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 all unchanged**: the leg never
ran. `argvlive-norule3` — the same with 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, measured.** `pin_stackchain()` writes
`HELD_DIR / "stackchain-home.html.sha256"` (`:664-680`) and `HELD_DIR` is
composed from `HELM_HOME` (`:31`), so the rewrite does not touch it and Q39's
literal scan admits it. 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
(`pinned 5e5cd6d26a70…`), 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 --skip-tests`, both 20,570 B, scrubbed over the gate's full clock
set → **`diff_keys: []`**. That is what licenses "the legs run the same
collector at a different destination" as a measurement.
- **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 subject legs redirected` 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.
Evidence, all from this host: `run_negatives.py` → `out/Q43-NEGATIVES.json`
(17 invocations, each with rc, stdout bytes/sha, report-block-present, the live
snapshot's (bytes, sha256, mtime_ns) before and after, the fixture census before
and after, and leftover sandboxes); `run_residuals.py` →
`out/Q43-RESIDUALS.json`; `live_state.py` → `out/live-before.json`,
`out/live-after-green2.json`; `out/DIFF-vs-q39.txt`; per-run `.out`/`.err` logs.
**Live, across all 20 gate invocations: snapshot 14,039 B /
`aa842b0fd88037f8…` / mtime_ns `1791479579475780303`; held pin 120 B /
`5b6d70716153c3f4…` / mtime_ns `1791478687099553071` — delta 0 on every one.**
One side observation with its evidence: the scratch pin's digest
`5e5cd6d26a70…` is the **text-mode** hash of the live body, because `run()` calls
`subprocess.run(..., text=True)` (`:123`) and universal-newline translation
applies — the same normalisation my 2026-10-11 measurement recorded — while 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. The two
digests and the code path are Corroborated; this host cannot prove which revision
wrote the held file, so the drift reading rests on `pin_stackchain()` being its
only writer.
### Blocked
Nothing in my lane. The standing gates are unchanged and all Alexander's: the
§4 adoption `cp` of the Helm pair (now subject `4e45021d…` + gate
`c22601ec…`), Q9 (checksum handover), Q13 (the wording fixes), Q19 (wizard
onboarding). Host disk **46 GB free of 154 GB (71% used)** at 18:16Z, measured
(`df -h /`) — the WorkBench volume move holds, and I reclaimed, rotated, stopped
and reconfigured nothing from this lane.
### Next
1. **The pipe's verification of this run**, when it lands. Everything here is
re-runnable from `/root/crucible/q43/`: `run_negatives.py` and
`run_residuals.py` are self-contained, and `make_variants.py` regenerates the
two discriminators from the gate.
2. **Q42** stays open and offered to zephyrus — the non-magazine second pass,
whose value now sits in the fresh-bytes half (re-fetch the 35 anchors and the
30 entry pages, rebuild the bodies, and report whether the 42 offsets, the
35-anchor set and the 9 multi-occurrence rows survive drift).
3. **The Helm lane's executable set is empty again.** Q31's `cp` decision, the
Q34-adjacent gates and this adoption all wait on the same human call, so the
next Helm unit is a decision rather than a task. The one follow-on I would
name if the lane is re-opened: extend the redirect beyond `SNAPSHOT_PATH` to
every constant composed from the live data root, which is what would close
the `--pin` class structurally instead of by argv rule.
### Ask
**Alexander** — unchanged and consolidated: (a) rule on the §4 adoption `cp` for
the Helm pair (subject `4e45021d…` + gate `c22601ec…`), since Q40 measured that
the deployed gate `7ed1bca8…` has no subject-selection path at all and none of
Q31–Q43's protection is in production until it lands; (b) the
`stackchain-dashboard.service` 32-byte key repair or a stop — the unit reads
`ActiveState=failed` with `NRestarts=724,359` frozen at the pipe's 16:2xZ
reading, which I did not re-measure; (c) the Q13 publication call, where item
13's decided note (escalate, do not edit the live page) still controls over its
`proposed_action`.
**The pipe** — nothing owed from this run. One thing to check rather than accept:
`recoveries/helm-leg-sandbox.md` claims the `argvlive` variant's protected file
was untouched in **ctime** as well as sha and mtime, which is the whole
discriminator. The measurement is `run_negatives.py`'s
`protected_ctime_moved` field, and `argvlive-norule3` is the control that shows
ctime moving when the write does happen — so a re-run of that pair is the
falsifier, and it costs about 20 seconds.
Landing evidence, Run 9, appended after the commit and read back over the
anonymous raw path
(`https://forge.alexanderwhitestone.com/git/stackchain/troupe-standup/raw/branch/main/<path>`),
no credential — each **HTTP 200** and `cmp` **byte-identical** to disk:
| file | status / bytes | sha256 |
|---|---|---|
| `recoveries/helm-leg-sandbox.md` | 200 / 13,561 | `<filled after the push>` |
| `WORK-QUEUE.md` | 200 / 691,024 | `<filled after the push>` |
| `checkins/2026-10-12-merlin.md` | 200 / `<filled after the push>` — the committed revision, not a local one: this file's own row cites what the server serves after the push, because Run 8's row cited a transient local revision and the pipe returned that as D1 | `<filled after the push>` |
Both other files are `cmp` byte-identical to disk; this file's own row is read
back over HTTPS after the push rather than hashed from inside itself. Nothing was
deployed, published or rebuilt; `/root/.helm/bin/` was not touched; `decisions/`
and `inbox/` were not touched; no another-party file was edited; no secret is
printed in this check-in or in the files it lands — the long strings are paths,
URLs, article-free command output and sha256 digests. This run's commit is
authored `Merlin <merlin@agents.invalid>` (`git -c`), so the monitor sees it as
counterparty content per Q36's v1.2 rule.
— Merlin