Q48 CLOSED (Merlin): the runtime-resolution redirect — an env- or argv-resolved live root is relocated into the sandbox; 169 -> 171 green 5/5; the class measured with prevention separated from detection; the live-held incident caused, restored and disclosed
This commit is contained in:
1 parent
870e48ecd4
commit
0b0b591cc7
1 file changed
+181
@@ -0,0 +1,181 @@
|
||||
# Q48 — the runtime-resolution redirect
|
||||
|
||||
**Merlin, Helm lane, 2026-10-13 (~06:00–06:45 UTC). Scratch tree `/root/crucible/q48/`. Nothing deployed, no
|
||||
`cp` into `/root/.helm/bin/`, `decisions/` and `inbox/` untouched, no another-party file edited, no secret
|
||||
printed. Every figure below came from a command in this run; where a figure is a reading it is dated.**
|
||||
|
||||
---
|
||||
|
||||
## 1. The item, and why it was the top item
|
||||
|
||||
Q45's redirect relocates every **absolute path literal** in the subject that resolves inside the live data
|
||||
root. Its own §7 limit 1 names the class it cannot see, verbatim: *"a root resolved at runtime from
|
||||
`__file__`, an environment variable or argv, where there is no literal to rewrite"*. The pipe's Q47
|
||||
verification block carried the same follow-on as *"named, unclaimed, with the missing measurement named"*,
|
||||
and stated why it had not measured it: a miss writes the live snapshot, and a restore moves the
|
||||
`mtime_ns`/`ctime_ns` values three records cite as deltas.
|
||||
|
||||
This item closes that class for the two shapes that are **external to the subject's own code** — an
|
||||
environment variable and an argv value — and measures, rather than asserts, what happens for the third
|
||||
(`__file__`), which needs no mechanism at all.
|
||||
|
||||
## 2. The pair
|
||||
|
||||
| artifact | sha256 | size |
|
||||
|---|---|---|
|
||||
| subject collector | `4e45021d80395d3f03fc58d7e99610e3c1c0d2fcf9c85f1db50b16cfd2043c6c` | 29,954 B / 720 lines — **byte-identical to Q33–Q47's**, every change is in the gate |
|
||||
| Q47 gate (before) | `76f2292b9a6cad08505956787cf05e65e3edaf7e0f23f73f4aed2e59a62866c1` | 115,182 B / 2,124 lines |
|
||||
| **Q48 gate (after)** | `a590170fa87d7e2a…` (full: `sha256sum /root/crucible/q48/helm-selftest.py`) | **122,186 B / 2,287 lines** |
|
||||
| diff | `out/DIFF-vs-q47.txt`, 8,300 B / `71c593e2954ecaf3…` | **186 lines = 2 file header + 3 hunk header + 143 added + 0 removed + 18 context**; 2,124 + 143 − 0 = **2,287** exactly |
|
||||
|
||||
`diff -u` decomposition, counted by `grep -c`, not asserted: added = `^+` minus `+++` = 143; removed = `^-`
|
||||
minus `---` = 0; hunks = `^@@` = 3; file headers = 2; context = 18. 143 + 0 + 18 + 3 + 2 = 186.
|
||||
|
||||
## 3. What changed — two mechanisms, two checks
|
||||
|
||||
**(1) The environment redirect.** The env every subject leg runs in is rebuilt. The gate inventories, by
|
||||
`ast`, every environment variable **the subject's own source reads** — `os.environ.get("X")`,
|
||||
`os.environ["X"]`, `os.getenv("X")` — and any whose value resolves inside the live data root is redirected
|
||||
to the per-invocation sandbox. It is a **redirect, not a refusal**: the subject runs, and it runs against
|
||||
the sandbox. The inventory is printed on stderr as a `Q48 NOTE` on every path, including the zero case,
|
||||
because a gate that silently rewrites an environment is indistinguishable from one that does not.
|
||||
|
||||
**(2) Rule (4), the argv redirect.** A leg argument that resolves inside the live data root is rewritten to
|
||||
the same relative path inside the sandbox, in the wrapper that already substitutes the subject with its
|
||||
copy (Q43 rule 2). **Scoped to the write surface — the live snapshot and the live held directory — not the
|
||||
whole live root.** The unrestricted form was written first and it broke G7's six checks: it rewrote the
|
||||
gate's own renderer invocation `/root/.helm/bin/helm-render.py`, a READ that must not move. The scoping is
|
||||
a measured correction, not a design preference (see §5, defect 1).
|
||||
|
||||
**(3) Two `check()`s**, so the green count is 169 + 2 = **171 passed / 0 failed**:
|
||||
|
||||
- `Q48 no environment variable the subject reads resolves inside the live data root`
|
||||
- `Q48 every argv value resolving inside the live data root was relocated into the sandbox`
|
||||
|
||||
**(4) `__file__` needs no mechanism.** The copy lives inside the sandbox, so a root composed from
|
||||
`Path(__file__).resolve().parent` resolves there by construction. `collector-file.py` (the deployed
|
||||
collector with `HELM_HOME = Path(__file__).resolve().parent`, 29,966 B / `a81f3b08b50f6055…`) is built
|
||||
and runs the interpreter's own verdict (rc 0 with `--dry-run`, rc 2 bare) — measured, and the redirect's
|
||||
inventory for it is empty by construction, which the Q45 check-1 lesson says must be printed rather than
|
||||
silent.
|
||||
|
||||
## 4. Green pair, five consecutive invocations
|
||||
|
||||
`run_green_q48.py`, `HELM_COLLECTOR=<the real collector>`, `Q48_ROOT` unset (the real collector carries the
|
||||
live root as a literal, so the env class is exercised by the subjects, not by the control):
|
||||
|
||||
| run | rc | verdict | stdout | sha256 (first 16) | seconds |
|
||||
|---|---|---|---|---|---|
|
||||
| green-1 | 0 | `helm-selftest: 171 passed, 0 failed` / `VERDICT: ALL GATES PASS` | 13,514 B | `81fd73b8a32e5c10` | 12.45 |
|
||||
| green-2 | 0 | same | 13,514 B | `7a5cfac833623db7` | 13.41 |
|
||||
| green-3 | 0 | same | 13,514 B | `e88ff3763a73fef1` | 13.12 |
|
||||
| green-4 | 0 | same | 13,514 B | `1f0ec83eccdc2a75` | 12.81 |
|
||||
| green-5 | 0 | same | 13,514 B | `eefcc271232b005e` | 12.63 |
|
||||
|
||||
The stdout size is identical across all five and the digests differ only because the gate's own render leg
|
||||
prints the `mkdtemp` deploy path and the sandbox name — the same dated-reading rule this lane applies to
|
||||
byte counts. **171 = 169 + 2**; no pre-existing check moved.
|
||||
|
||||
## 5. The discriminator — the mechanism, on the real collector
|
||||
|
||||
`run_witness_q48.py` → `out/Q48-WITNESS.json` (4,686 B / `50e7180ac30c3aa0…`,
|
||||
`generated_at` in the file). The witness path is an **extra** `--q48-witness` flag added by
|
||||
`patch_witness()` into a copy of the gate (`helm-selftest-witness.py`); the shipped gate never passes it,
|
||||
so the green run cannot be affected. It builds the copy exactly as the green run does, prints the rebuilt
|
||||
environment for every variable the subject reads, and runs the copy `--deploy --pin --skip-tests --out
|
||||
<sandbox>` — so `--out` forces the snapshot into the sandbox and the **only** path the redirect governs is
|
||||
the `HELD_DIR` the pin writes.
|
||||
|
||||
Subjects: the deployed collector with **exactly one mechanical change**, asserted by count
|
||||
(`make_subjects_q48.py`): `collector-env.py` = `HELM_HOME = Path(os.environ.get("Q48_ROOT", "/root/.helm"))`
|
||||
(29,982 B / `4739ced839a30564…`), `collector-file.py` = `HELM_HOME = Path(__file__).resolve().parent`.
|
||||
|
||||
| row | root value | gate | rc | `Q48_ROOT` the copy runs in | copy's held write | live held |
|
||||
|---|---|---|---|---|---|---|
|
||||
| `q48-armed-env` | `<protected>` | Q48 | 0 | `/root/crucible/q48/protected` (outside the live root, so **no redirect**) | `[]` — the pin's `HELD_DIR` is under `--out`'s sandbox only when the env names it | unchanged |
|
||||
| **`q48-armed-env-liveroot`** | **`/root/.helm`** | Q48 | **0** | **`/root/crucible/q48/helm-legs-ppqy3cew` — REDIRECTED** (`env_redirects: {"Q48_ROOT": ["/root/.helm", "<sandbox>"]}`) | **`<sandbox>/data/held/stackchain-home.html.sha256`** | **unchanged** |
|
||||
| `q48-disarmed-env` | `<protected>` | Q48 minus both mechanisms | 0 | `/root/crucible/q48/protected` | `[]` | unchanged |
|
||||
| **`q48-disarmed-env-liveroot`** | **`/root/.helm`** | Q48 **disarmed** | **ABORT** | `/root/.helm` — nothing redirected | **wrote the LIVE held directory** (`5b6d7071…` → `c0532a8b…`, a real page hash) | **NONZERO, restored by the guard** |
|
||||
| `q47-blindspot-env` | `<protected>` | pre-Q48 (copy, original env) | 0 | n/a — no redirect exists | `HELM_HOME`/`SNAPSHOT_PATH`/`HELD_DIR` all `/root/crucible/q48/protected`, i.e. the class is invisible to Q47 | unchanged |
|
||||
|
||||
**Prevention separated from detection, by measurement.** The armed row redirects the live root into the
|
||||
sandbox and the pin lands there; the disarmed row, same subject, same live root value, writes live and the
|
||||
guard catches it, restores it byte-exact and aborts. The class is real, one mechanism away, and the
|
||||
mechanism closes it.
|
||||
|
||||
## 6. The incident, disclosed in full: this run wrote the LIVE held directory, twice
|
||||
|
||||
Not discovered — **caused, measured, restored, and disclosed**. Three separate live writes of
|
||||
`/root/.helm/data/held/stackchain-home.html.sha256` happened in this tree, each by a subject resolving the
|
||||
live root at runtime with the redirect **not in the path**:
|
||||
|
||||
1. **The fixture-verification run (v1, ~05:5xZ).** `make_fixtures_q48.py`'s `verify_interpreter()` ran
|
||||
`argv-root.py` with no `--root`, so the fixture's live default resolved and it pinned the live held
|
||||
directory with a 21-byte fixture string. Restored byte-exact from the Q43/Q45 backup by
|
||||
`restore_held_q48.py`, which **refuses to restore unless the backup's digest matches the one every prior
|
||||
tick recorded** (`5b6d7071…`). Post-restore: 120 B / `5b6d7071…` / `mtime_ns` `1791478687099553071`,
|
||||
`VERDICT: RESTORED BYTE-EXACT`; `ctime_ns` moved to `1791871227057299704`, which a restore cannot undo.
|
||||
The fix that followed: every fixture carries an explicit protected-root default and an assertion that
|
||||
refuses a live default.
|
||||
2. **The v1 driver's first armed row (~06:0xZ).** The driver aimed the armed rows at the LIVE root and its
|
||||
own `abort_if_live_moved()` guard fired on the first `q48-armed-env` row and restored byte-exact. The
|
||||
cause is a **real residual of the gate's design**, stated plainly: the gate's legs run the **original**
|
||||
subject with the inherited environment (Q43's resolution leg, G8's dry-run leg), so a subject whose root
|
||||
is the live root writes the live held directory from a leg the redirect does not cover. The redirect
|
||||
covers the **copy**; the original is covered only by the snapshot watch (snapshot-only) and the held
|
||||
census (once per run, not per leg). v2 of the driver therefore used the protected root as the universal
|
||||
root value and witnessed the class at the mechanism level instead — and this residual is the item's
|
||||
first stated limit, not a footnote.
|
||||
3. **The `q48-disarmed-env-liveroot` witness row (~06:3xZ).** The deliberate falsifier: with the redirect
|
||||
cut out and the live root as the value, the copy's pin wrote the live held directory (`c0532a8b…`). The
|
||||
guard aborted, restored byte-exact, and the driver **recorded the row as an ABORT and continued** so the
|
||||
JSON the record rests on exists.
|
||||
|
||||
**Final live state, measured after every row:** snapshot 14,039 B / `aa842b0fd88037f8…` / `mtime_ns`
|
||||
`1791479579475780303`; held pin **120 B / `5b6d70716153c3f4…` / `mtime_ns` `1791478687099553071`** —
|
||||
`live_delta` = **0 on both objects** across the whole witness run. `ctime_ns` carries the restore traces
|
||||
and is not recoverable; that is the durable record, not an argument.
|
||||
|
||||
## 7. Limits, and this run's own defects first
|
||||
|
||||
1. **The gate's own leg set is the residual.** As §6.2 states: a subject whose root is the live root writes
|
||||
the live **held** directory from the gate's own original-subject legs, because the redirect covers the
|
||||
copy and the watch is snapshot-only. Closing it needs the held census per leg, or the original-subject
|
||||
legs to inherit the rebuilt environment too — the latter is one line in `_q39_run`, and it is **not**
|
||||
taken here because it changes what the resolution leg measures.
|
||||
2. **Rule (4) fired zero times in this run.** No leg argument resolved inside the held directory, so the
|
||||
argv redirect is coded and its two checks pass, but the only exercised part of it is the zero case. The
|
||||
`argv`-shaped subject (a `--root` argument) was built (`collector-env.py` covers the env shape;
|
||||
`collector-file.py` the `__file__` shape) and a true `--root` leg was not reachable through the suite —
|
||||
stated, not papered over.
|
||||
3. **Four fixture generations failed for reasons that were the fixture's.** v1–v3 wrote the held pin only
|
||||
under `--deploy`, which no leg passes, so the class was unmeasurable; v3's refusal did not name the
|
||||
destination G8 asserts on; v4 called the Q35 marker above the name it names (`NameError`); v5/v6 had no
|
||||
`main()` and ignored `--out`, so G8's in-process deploy leg died on `AttributeError`. **v7 abandoned the
|
||||
hand-written fixture** in favour of the deployed collector with one mechanical change — re-implementing
|
||||
720 lines to test one mechanism is theatre, and the honest instrument is the collector itself.
|
||||
4. **The gate's suite is not the discriminator for this class, and that is a finding, not an excuse.** No
|
||||
subject leg runs the copy with `--deploy --pin`, so a runtime-resolved subject's redirect is never
|
||||
exercised by the suite; and the Q43 resolution-leg check (*"the subject's own default destination
|
||||
resolves to the live snapshot the gate protects"*) cannot pass for **any** subject whose root is the
|
||||
protected scratch root — it is a live-identity check, not a redirect check. The witness path exists
|
||||
because of that, and it is deliberately an **extra flag**, not a change to the green run.
|
||||
5. **Static, as every guard in this family.** A root read from a config file, a network response or
|
||||
argv[0]'s resolved directory at call time is outside an `ast` inventory. Q39's literal scan, Q45's
|
||||
resolved-destination check, the per-leg watch and rule (3) remain the backstop, unchanged.
|
||||
6. Nothing re-tests Q47's 21 rows; the Q47 gate in `legacy/` is a copy, not re-verified this run.
|
||||
|
||||
## 8. §4 consequence
|
||||
|
||||
The adoption pair is now collector `4e45021d…` + gate **`a590170f…`** (superseding Q47's `76f2292b…`). Q40's
|
||||
finding stands — the deployed gate `7ed1bca8…` reads `HELM_COLLECTOR` zero times — so the `cp` remains
|
||||
Alexander's and is the only thing that puts Q31–Q48's protection into production. **The gate is still
|
||||
moving**: the next Helm item (if any) will move it again, so a copy now lands Q48 with Q49 a known gap.
|
||||
|
||||
## 9. Q7, measured
|
||||
|
||||
The Helm lane's executable set is empty again after this item: the adoption `cp`, Q9, Q13/Q19 and Q42 are
|
||||
all human- or counterparty-gated. No new item opened. The residual §7.1 names (per-leg held census, or the
|
||||
original-subject legs inheriting the rebuilt environment) is the natural successor and is **not** opened
|
||||
here, because it is a one-line change to a mechanism this run only just built, and it deserves its own
|
||||
measurement rather than a rider on this one.
|
||||
Reference in new issue
Block a user