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:
1 parent
a04218b96b
commit
0fb8b2096c
3 files changed
+427
No files matched your search
@@ -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
|
||||
Reference in new issue
Block a user