helm: the Q45 s9 follow-on closed -- the home-directory channel (collector 4e45021d unchanged; gate 78b290ecf2d93e0c3087e5ede8baef5c10ca52986706d5da69a13a619dbdd9f, 140,365 B / 2,677 lines / 126 registrations, on the Q51 gate e4967da9, diff 6 hunks + 122 - 3 = 2,677 lines; disarmed twin be7b306a one line different)

The last runtime-resolution route in Q45 s7-limit-1's family with neither a literal nor an ast-visible env read: HELM_HOME = Path.home() / ".helm" composes the live root because CPython's posixpath.expanduser reads HOME itself. The channel is live in the shipped subject -- the real collector calls Path.home() twice (its himalaya search). Mechanism: an ast inventory of home-database reads (_q52_home_sites, :786) and, inside Q50's one rebuild function (two callers, probe :977 / legs :1162), a redirect of HOME + USERPROFILE to <target>/q52-home -- a redirect, not a refusal -- with a conditional Q52 NOTE and one new check("Q52", :1204). A --q52-witness extra flag (:1261) runs the copy with --deploy --pin --skip-tests --out <sandbox dest>, the pin shape the suite itself refuses (Q44's rule), because the class's write path is unreachable from the green run.

Measured: green 5/5 at 176 passed / 0 failed (= 175 + 1), 14,044 B stdout, live delta {}; the real collector's own .home() read makes the check non-vacuous. Discriminator, five cells under the digest-verified guard: armed x collector-home.py (d62888c4) -- the copy resolves inside the sandbox and the pin lands at q52-home/.helm/data/held/stackchain-home.html.sha256 in the sandbox, live delta 0; armed x collector-expanduser.py (99c49d39) -- same; disarmed x home -- HOME stays /root, the copy resolves HELM_HOME=/root/.helm and the live pin moves to sha256 caff1f504855264a / mtime_ns 1791907100434371499, restored byte-exact; disarmed without the witness flag -- the foot's own check fails (FAIL Q52, home_reads=['.home()'] HOME='/root', 174 passed / 2 failed); direct invocation, ambient env -- the live pin moves (the class's floor). Regressions 11/11 hold; live delta 0 on bytes/sha256/mtime across all 21 invocations (snapshot 14,039 B / aa842b0f, held pin 120 B / 5b6d7071); ctime moved on both objects (1791907371554631194), the guard restores of the two deliberately-live rows -- disclosed. Final tree 0 sandboxes; deployed /root/.helm/bin untouched (7ed1bca8, grep -c HELM_COLLECTOR = 0 -- Q40 stands, so none of Q31-Q52 is in production); disk 46 GiB free of 154.

Five defects of mine logged first in the deliverable: the witness's missing local subprocess import; the witness anchored before the leg copy was written; the witness's live census reading the held directory (IsADirectoryError); the NOTE claiming a redirect that had not fired (the disarmed twin's own FAIL contradicted it -- the NOTE is now derived from the actual env); and the first matrix measuring nothing because the real collector pins only under --pin, which the suite refuses. Limits stated as the item's own: the original-subject legs inherit the ambient environment (Q48's residual); pwd.getpwuid().pw_dir bypasses HOME (the inventory names it, the redirect cannot relocate it); the inventory is static (Q46's limit).

Files: recoveries/helm-home-channel-guard.md (13,173 B / sha256 3f813ca23202590c675f7ac7587f0d5e7d8b0ff9ee9f4f0711996530a2d6d7db), WORK-QUEUE.md (897,771 B), checkins/2026-10-13-merlin.md (Run 18). Scratch tree /root/crucible/q52/ not committed. No deployment, decisions/ and inbox/ untouched, no another-party file edited, no secret printed. The Helm lane's executable set is empty again; the adoption cp (collector 4e45021d + gate 78b290ec, superseding e4967da9) remains Alexander's.
This commit is contained in:
Merlin committed 2026-10-13 16:08:17 +00:00
1 parent 0c190a4cec
commit c559d33131
3 files changed
+396

No files matched your search

+65
View File
@@ -7586,3 +7586,68 @@ claim of mine is corrected. 2026-10-13 15:37Z (control plane; `date -u` read at
**nothing** (no commit exists between `b7601a0` 14:15:22Z and `7d76bf2` 15:14:24Z), which is the v1.3 cost behaving as
its note describes. If Alexander would rather the pipe's own queue-only blocks did not re-arm the monitor at all, that is
a change to `troupe_pipe_monitor.py` and his call — not one this tick takes on its own.
---
---
## Q45 §9 follow-on — the home-directory channel *(Helm lane; Merlin)*
**The named follow-on, closed.** The item Merlin's Q45 §7/Q45 §9 follow-on line left open and every check-in
since has carried: *"resolve every path the subject declares **at runtime**, rather than rewriting the literals it
declares statically."* Q48 discharged the environment-value and argv halves; **the home-directory channel was the
remainder and is now closed.** Deliverable: `recoveries/helm-home-channel-guard.md` (**13,173 B / sha256
`3f813ca23202590c675f7ac7587f0d5e7d8b0ff9ee9f4f0711996530a2d6d7db`**, this host, measured after the write below**). Scratch tree `/root/crucible/q52/`; nothing deployed,
no `cp` into `/root/.helm/bin/`, `decisions/` and `inbox/` untouched, no another-party file edited, no secret printed.
**The class.** `HELM_HOME = Path.home() / ".helm"` — a root composed from the **user database**. CPython's
`posixpath.expanduser` reads `HOME` itself and `Path.home()` calls it, so the composed path is the live root
(`/root/.helm`) while Q45's literal scan sees only `".helm"` (not absolute), Q48's env inventory finds no
`os.environ` access in the subject's own AST, Q49's default redirect keys on that same inventory, and Q48's argv
rule sees no root argument. The channel is live in the shipped subject: the real collector calls `Path.home()`
**twice** (`/root/crucible/q48/helm-collect.py` lines 297–298, its himalaya config search), on the collector
byte-identical since Q33 (`4e45021d80395d3f…`, 720 / 29,954).
**The pair.** Collector untouched (measured by sha256). Gate **`78b290ecf2d93e0c…`** (**140,365 B / 2,677 lines /
126 registrations**) on the Q51 gate `e4967da9…` (134,427 / 2,558 / 125);
`out/DIFF-vs-q51.txt` = **169 lines = 6 hunks + 122 added − 3 removed**, and 2,558 + 122 − 3 = 2,677 exactly.
Disarmed twin `be7b306a…` (140,390 / 2,677 / 126) — one line different. **Mechanism:** an ast inventory of
home-database reads (`_q52_home_sites`, gate :786) and, inside Q50's one rebuild function (two callers, :977 probe /
:1162 legs), a redirect of `HOME` + `USERPROFILE` to `<target>/q52-home` — a redirect, not a refusal — with a
conditional `Q52 NOTE` on stderr and one new `check("Q52")` (:1204): when the subject reads the home database, the
legs' `HOME` must be the sandbox home; vacuous otherwise. A `--q52-witness` extra flag (:1261) runs the copy with
`--deploy --pin --skip-tests --out <sandbox dest>` — the pin shape the suite itself refuses (Q44's rule) — because
the class's write path is structurally unreachable from the green run.
**Measured, not asserted.** Green pair **5/5**: rc 0, **176 passed / 0 failed** (= 175 + 1), 14,044 B stdout /
1,216 B stderr, live delta `{}`, the real collector's own `.home()` read making the check non-vacuous
(`PASS Q52 …`; `Q52 NOTE: … HOME redirected to <sandbox>/q52-home (/root -> <sandbox>/q52-home)`).
**The discriminator, five cells under the digest-verified guard** (`out/Q52-MATRIX.json`): armed ×
`collector-home.py` (`HELM_HOME = Path.home()/".helm"`, `d62888c4…`) — the copy resolves inside the sandbox, the
pin lands at `q52-home/.helm/data/held/stackchain-home.html.sha256` **inside the sandbox**, live delta 0; armed ×
`collector-expanduser.py` (`99c49d39…`) — same; **disarmed × home** — `HOME` stays `/root`, the copy resolves
`HELM_HOME=/root/.helm`, the live pin moves to sha256 `caff1f504855264a…` / mtime_ns `1791907100434371499` and is
restored byte-exact by the guard; disarmed without the witness flag — the foot's own check fails
(`FAIL Q52 … home_reads=['.home()'] HOME='/root' expected='<sandbox>/q52-home'`, 174/2); direct invocation with
the ambient environment — the live pin moves (the class's floor). Regressions **11/11**: nine refusals rc 2,
`collector-latin1.py` / `collector-bom.py` green at 14,044 B, live delta `{}` on all eleven.
**Live state, delta 0 on bytes/sha256/mtime across all 21 invocations** (5 green + 5 matrix + 11 regressions):
snapshot **14,039 B / `aa842b0fd88037f8…` / mtime_ns `1791479579475780303`**; held pin **120 B /
`5b6d70716153c3f4…` / mtime_ns `1791478687099553071`**. **ctime moved on both objects**
(`1791907371554631194`) — the guard restores of the two deliberately-live rows; disclosed, not hidden. Final tree
0 sandboxes; deployed `/root/.helm/bin/` untouched (`7ed1bca8…`, `grep -c HELM_COLLECTOR` = **0** — Q40 stands, so
none of Q31–Q52 is in production); disk 46 GiB free of 154.
**Limits, stated in the record as the item's first limits:** the original-subject legs still inherit the ambient
environment (Q48's residual); a subject reading `pwd.getpwuid(...).pw_dir` directly bypasses `HOME` and the
redirect cannot see it (the inventory names it); the inventory is static (Q46's limit); `USERPROFILE` is
redirected alongside `HOME` as a no-op on this host, for portability.
**Consequence beside it (reply 36 §3, restated):** the run-wide Q44 instrument now fails on a *restore trace*
alone — ctime is the only stamp separating `delta {}` from "moved" in the run-wide record. That is the
conservative direction for a detector and is not reopened here.
**Open after this item, Helm lane:** the §4 adoption `cp` — now collector `4e45021d…` + gate **`78b290ec…`**
(superseding `e4967da9…`) — Q9 (checksum handover), Q13 (wording fixes), Q19 (wizard onboarding), Q42
(zephyrus's). **The Helm lane's executable set is empty again; the next Helm unit is a decision, not a task.**
+98
View File
@@ -796,3 +796,101 @@ paths and sha256 digests. This run's commit is authored `Merlin <merlin@agents.i
so the monitor sees it as counterparty content per Q36's v1.3 rule.
— Merlin
---
## Run 18 — the Q45 §9 follow-on: the home-directory channel
### Screen
Merlin, Helm lane, 2026-10-13, this host. Item: **the named Q45 §9 follow-on** — *resolve every path the subject
declares at runtime, rather than rewriting the literals it declares statically* — the one Helm unit the queue's own
open set carried as mine and unclaimed (the pipe's 15:37Z block lists it by name; Q52 is Mercury's and is not
touched here). Deliverable: `recoveries/helm-home-channel-guard.md` (**13,173 B / sha256
`3f813ca23202590c675f7ac7587f0d5e7d8b0ff9ee9f4f0711996530a2d6d7db`**, this host). Queue block appended to
`WORK-QUEUE.md` (897,771 B after the append). Scratch tree `/root/crucible/q52/`; nothing deployed, no `cp` into
`/root/.helm/bin/`, `decisions/` and `inbox/` untouched, no another-party file edited, no secret.
### Did
**The class, and why every existing mechanism missed it.** `HELM_HOME = Path.home() / ".helm"` composes the live
root at runtime from the user database: CPython's `posixpath.expanduser` reads `HOME` itself, so Q45's literal scan
sees only `".helm"` (not absolute), Q48's env inventory finds no `os.environ` access in the subject's own AST,
Q49's default redirect keys on that same inventory, and Q48's argv rule sees no root argument. The channel is live
in the shipped subject: the real collector calls `Path.home()` **twice** (lines 297–298 of
`/root/crucible/q48/helm-collect.py`, `4e45021d…`, 720 / 29,954) for its himalaya config search.
**The pair.** Collector byte-identical (measured by sha256) — every change is in the gate. Gate **`78b290ecf2d93e0c…`**
(**140,365 B / 2,677 lines / 126 registrations**) on the Q51 gate `e4967da9…` (134,427 / 2,558 / 125);
`out/DIFF-vs-q51.txt` = **169 lines = 6 hunks + 122 added − 3 removed**, 2,558 + 122 − 3 = 2,677 exactly.
Disarmed twin `be7b306a…` (140,390 / 2,677 / 126), one line different. **Mechanism:** an ast inventory of
home-database reads (`_q52_home_sites`, :786) and, inside Q50's one rebuild function (two callers — probe :977,
legs :1162), a redirect of `HOME` + `USERPROFILE` to `<target>/q52-home`; a conditional `Q52 NOTE` on stderr and
one new `check("Q52")` (:1204). A `--q52-witness` extra flag (:1261) runs the copy with `--deploy --pin
--skip-tests --out <sandbox dest>` — the pin shape the suite refuses (Q44's rule) — because the class's write path
is structurally unreachable from the green run.
**Fixtures, verified against the interpreter first** (`make_fixtures_q52.py`): `collector-home.py`
(`d62888c4c9470d30…`, 29,956 B) and `collector-expanduser.py` (`99c49d395467709d…`, 29,974 B), each
`py_compile`-clean and each resolving `HELM_HOME /tmp/<scratch>/.helm` under a scratch HOME — the class measured,
not asserted.
**Green pair, five consecutive invocations** (`run_green_q52.py`): rc 0 each, **176 passed / 0 failed** (= 175 + 1),
ALL GATES PASS, 14,044 B stdout / 1,216 B stderr, 12.34–12.73 s, live delta `{}`. The real collector's own
`.home()` read makes the check non-vacuous: `PASS Q52 the subject's home-directory reads resolve inside the sandbox
when the subject reads the home database`, `Q52 NOTE: … HOME redirected to <sandbox>/q52-home (/root -> <sandbox>/q52-home)`
(`out/q52-green-1.{out,err}`). Q44 and Q50 stay green on the same runs.
**The discriminator, five cells under the digest-verified guard** (`run_matrix_q52.py` → `out/Q52-MATRIX.json`):
armed × `collector-home.py` — the copy resolves `HELM_HOME=<sandbox>/q52-home/.helm`, the pin lands at
`q52-home/.helm/data/held/stackchain-home.html.sha256` **inside the sandbox**, live delta 0; armed ×
`collector-expanduser.py` — same, on the second channel; **disarmed × home** — `HOME` stays `/root`, the copy
resolves `HELM_HOME=/root/.helm` and the live pin moves to sha256 `caff1f504855264a…` / mtime_ns
`1791907100434371499`, restored byte-exact by the guard; **disarmed without the witness flag** — the foot's own
check fails (`FAIL Q52 … home_reads=['.home()'] HOME='/root' expected='<sandbox>/q52-home'`, 174 passed / 2
failed); **direct invocation, ambient env** — the live pin moves, guard restores (the class's floor). Prevention
and detection measured separately.
**Regressions 11/11** (`run_regressions_q52.py` → `out/Q52-REGRESSIONS.json`): nine refusals rc 2 (n1–n5, noparse,
dep-unparseable, undecodable, broken_dep); `collector-latin1.py` and `collector-bom.py` green at 14,044 B; live
delta `{}` on all eleven. `VERDICT: ALL REGRESSIONS HOLD`.
**Live state, delta 0 on bytes/sha256/mtime across all 21 invocations** (5 green + 5 matrix + 11 regressions):
snapshot **14,039 B / `aa842b0fd88037f83115b4a54d617f27666f4de06ef84f9a64c8fd0a5f04656d` / mtime_ns
`1791479579475780303`**; held pin **120 B /
`5b6d70716153c3f4bd82045d5d71edf88f793a240ce1275c3dfca5759d0ee297` / mtime_ns `1791478687099553071`**.
**ctime moved on both objects** (`1791907371554631194`) — the guard restores of the two deliberately-live rows;
disclosed, not hidden. Final tree 0 sandboxes; deployed `/root/.helm/bin/` untouched (`7ed1bca8…`, `grep -c
HELM_COLLECTOR` = **0** — Q40 stands); disk 46 GiB free of 154.
**Five defects of mine, logged first in the deliverable:** the witness's missing local `subprocess` import; the
witness anchored before the leg copy was written; the witness's live census reading the held *directory*
(`IsADirectoryError`); the NOTE claiming a redirect that had not fired (the disarmed twin's own FAIL contradicted
it — the NOTE is now derived from the actual environment); and the first matrix measuring nothing because the real
collector pins only under `--pin`, which the suite refuses — the witness flag exists because of that.
### Blocked
Nothing in my lane. Same standing gates, all Alexander's: the **§4 adoption `cp`** — now collector `4e45021d…` +
gate **`78b290ec…`** (superseding `e4967da9…`); Q40's finding stands (the deployed gate `7ed1bca8…` reads
`HELM_COLLECTOR` zero times), so none of Q31–Q52's protection is in production until it lands — Q9 (checksum
handover), Q13 (wording fixes), Q19 (wizard onboarding), Q42 (zephyrus's).
### Next
1. **The pipe's verification of this run**, when it lands. Everything re-runs from `/root/crucible/q52/`:
`make_fixtures_q52.py`, `patch_gate_q52.py`, `run_green_q52.py`, `run_matrix_q52.py`,
`run_regressions_q52.py`, `final_state.py`.
2. **The Helm lane's executable set is now empty.** The Q45 §9 follow-on is closed; the adoption, Q9, Q13/Q19 and
Q42 are human- or counterparty-gated. **No new item opened** — the next Helm unit is a decision, not a task.
The two limits the record states as the item's own — the original-subject legs' ambient environment (Q48's
residual) and `pwd.getpwuid()` bypassing `HOME` — are the only named candidates, and neither is a task until
someone says the class matters.
### Ask
Alexander: nothing blocking. The standing items, consolidated: (a) rule on the **§4 adoption `cp`** — collector
`4e45021d…` + gate **`78b290ec…`**, the pair that makes `python3 helm-selftest.py` green with no environment and
puts Q31–Q52's protection into production (the deployed gate today has no subject-selection path at all); (b) the
**`manifest_match` pin** decision (stale since 2026-10-08); (c) the **Q13 publication call** (item 13's decided
note — escalate, do not edit the live page — still controls over its `proposed_action`).
+233
View File
@@ -0,0 +1,233 @@
# Q52 — the home-directory channel
**Merlin, Helm lane, 2026-10-13.** The last route in Q45's §7-limit-1 family
that carries neither a literal nor an ast-visible environment read. Scratch
tree `/root/crucible/q52/`; nothing deployed, no `cp` into `/root/.helm/bin/`,
`decisions/` and `inbox/` untouched, no another-party file edited, no secret.
> Screen. Every claim below carries a path, a hash, a URL, or a command with
> its output, or the word `unverified`.
---
## 1. The class
The subject resolves its root from the **user database**:
```python
HELM_HOME = Path.home() / ".helm"
```
CPython's `posixpath.expanduser` reads `HOME` from `os.environ` **itself**,
and `Path.home()` calls it, so:
- **Q45's literal relocation** sees no absolute live-root literal — the only
string is `".helm"`, which does not resolve inside the live root on its
own;
- **Q48's environment inventory** walks the subject's own AST for
`os.environ` accesses and finds none — the read happens inside the
interpreter;
- **Q49's default redirect** keys on the same inventory;
- **Q48's argv rule** sees no root argument;
- **`__file__`** needs no mechanism (Q48's note) but is not this class;
yet the composed path is the live root `/root/.helm`, and the subject pins
the live held directory. The channel is live in the shipped subject, not
hypothetical: the real collector calls `Path.home()` **twice** — its himalaya
config search, `/root/crucible/q48/helm-collect.py` lines 297–298
(`grep -n 'Path.home()'` → 2 hits), on the collector byte-identical since Q33
(`4e45021d80395d3f…`, 720 lines / 29,954 B, sha256 measured this run).
## 2. The pair
**Collector untouched** — every change is in the gate, as the item requires
(`4e45021d…` re-measured by sha256).
| | bytes / lines | registrations | sha256 |
|---|---|---|---|
| Q51 gate (base) | 134,427 / 2,558 | 125 | `e4967da9bdc45378…` |
| **Q52 gate (shipped)** | **140,365 / 2,677** | **126** | **`78b290ecf2d93e0c…`** |
| Q52 disarmed twin | 140,390 / 2,677 | 126 | `be7b306afa7c5b3a3…` |
`out/DIFF-vs-q51.txt` = **169 lines = 6 hunks + 122 added − 3 removed**, and
2,558 + 122 − 3 = 2,677 exactly (counted by `grep -c`, not asserted).
Registration count `grep -cE '^[[:space:]]*check\('` = 126; the disarmed twin
differs from the shipped gate in exactly one line (the redirect cut), as in
Q50's and Q51's pairs.
**The mechanism, one function, two call sites, one check.**
1. **The inventory** — `_q52_home_sites()` (gate line 786): every
home-database read in the subject's own AST — `Path.home()`, a bare
`.home()`, `expanduser` (attribute or bare), `pwd.*`. Module-level
`_q52_home_reads` holds the result for both rebuild callers.
2. **The redirect** — inside `_q50_rebuild_env` (line 815), the same function
that already serves the admission probe (Q50) and the legs: when the
subject reads the home database, `HOME` **and** `USERPROFILE` are
redirected to `<target>/q52-home`. A redirect, not a refusal; the
collector's own `Path.home()` reads keep working, against the sandbox's
home. Probe caller at :977, legs caller at :1162 — both take the new
four-tuple, so the two paths cannot drift.
3. **The NOTE** — one `Q52 NOTE` on stderr, derived from the **actual**
environment, so it cannot claim a redirect that did not fire (see defect 4).
4. **The check** — `check("Q52", …)` at :1204, registered at the legs' site:
when the subject reads the home database, the legs' `HOME` must be the
sandbox home; with no home-database read it passes vacuously and the NOTE
says so.
5. **The witness** — `--q52-witness` (line 1261, after the leg-copy write):
runs the copy the way the suite never does — `--deploy --pin --skip-tests
--out <sandbox dest>` — under the rebuilt environment, prints one JSON
object and exits before the foot. The green run never passes the flag, so
the block cannot affect it. It exists because the suite itself refuses a
`--pin` leg (Q44's rule), so the pin shape is unreachable from the suite
(defect 5).
## 3. The fixtures, verified against the interpreter first
Two transforms of the real collector (`make_fixtures_q52.py`, asserted count
1 each):
| fixture | transform | bytes | sha256 |
|---|---|---|---|
| `collector-home.py` | `HELM_HOME = Path.home() / ".helm"` | 29,956 | `d62888c4c9470d30…` |
| `collector-expanduser.py` | `HELM_HOME = Path(os.path.expanduser("~")) / ".helm"` | 29,974 | `99c49d395467709d…` |
The interpreter's own verdict, measured by `make_fixtures_q52.py`'s probe
(`runpy` + a scratch `HOME`): both fixtures `rc 0` with
`HELM_HOME /tmp/<scratch>/.helm` and `HELD_DIR /tmp/<scratch>/.helm/data/held`
— **each resolves inside a scratch HOME**, and with the ambient HOME (`/root`)
the same composition is the live root. That is the class, measured rather
than asserted.
## 4. The green pair, five consecutive invocations
`run_green_q52.py` (subject `/root/crucible/q48/helm-collect.py`,
`Q48_ROOT` unset; logs `out/q52-green-{1..5}.{out,err}`, artifact
`out/Q52-GREEN.json`): **rc 0 each, 176 passed / 0 failed**, `VERDICT: ALL
GATES PASS`, **14,044 B stdout / 1,216 B stderr**, 12.34–12.73 s, live delta
`{}` on all five. 176 = 175 + 1; no pre-existing check moved.
The control that matters: the real collector **reads the home database**, so
the redirect fires on every green run and the check is non-vacuous —
```
PASS Q52 the subject's home-directory reads resolve inside the sandbox when the subject reads the home database
Q52 NOTE: the subject reads the home database at 1 site(s) (.home()); HOME redirected to
/root/crucible/q52/helm-legs-abis_h7c/q52-home (/root -> /root/crucible/q52/helm-legs-abis_h7c/q52-home)
```
(`out/q52-green-1.out`, `out/q52-green-1.err`). The himalaya source is
`UNREACHABLE` either way — the binary is absent from this host — so the
redirect moves an already-empty search, and `Q44`/`Q50` stay green:
`PASS Q50 the admission probe's spawn moved neither live object`, `PASS Q44
the live held directory is unchanged by every leg in the run`.
## 5. The discriminator, five cells, every one under the guard
`run_matrix_q52.py` → `out/Q52-MATRIX.json` (guard: `q52_guard.py`, refuses
to restore unless the backup digest matches the recorded `5b6d7071…`).
| row | gate | subject | rc | live moved | what the witness shows |
|---|---|---|---|---|---|
| `q52-armed-home` | Q52 `78b290ec…` | `collector-home.py` | 0 | **no** | `HOME_witness_env` = `<sandbox>/q52-home`; the copy's `HELM_HOME=<sandbox>/q52-home/.helm`, `HELD_DIR=<sandbox>/q52-home/.helm/data/held`; the pin landed at `q52-home/.helm/data/held/stackchain-home.html.sha256` **inside the sandbox**; live snapshot `14,039 B / aa842b0f…`, live pin `120 B / 5b6d7071…` — unchanged |
| `q52-armed-expand` | Q52 `78b290ec…` | `collector-expanduser.py` | 0 | **no** | same, on the `expanduser` channel (2 inventory sites: `.home()`, `expanduser` — the collector's own himalaya search plus the transform) |
| `q52-dis-home` | Q52 disarmed `be7b306a…` | `collector-home.py` | 0 | **YES** | `HOME_witness_env` = `/root`; the copy resolves `HELM_HOME=/root/.helm` — the LIVE root; the live pin moved to sha256 `caff1f504855264a…` / mtime_ns `1791907100434371499`, restored byte-exact by the guard |
| `q52-dis-check` | Q52 disarmed `be7b306a…` | `collector-home.py` | 1 | no | the foot's own check fails: `FAIL Q52 … home_reads=['.home()'] HOME='/root' expected='<sandbox>/q52-home'`, `174 passed, 2 failed` |
| `q52-raw-home` | none — direct invocation | `collector-home.py` | 0 | **YES** | the class with nothing in the path: `--deploy --pin --skip-tests --out <scratch>`, ambient env — the live pin moves, guard restores |
Prevention and detection are measured separately: the armed rows redirect
and the pin lands in the sandbox; the disarmed row, same subject, writes
live; the check fails when the redirect is cut. The class is real, one
mechanism away, and the mechanism closes it.
## 6. Regressions, 11/11
`run_regressions_q52.py` → `out/Q52-REGRESSIONS.json`: `n1–n5`, `noparse-with-markers.py`,
`dep-unparseable.py`, `collector-undecodable.py`, `broken_dep.py` all **rc 2**
(Q39 refusals / Q46 parse guard / repair 3 / Q47 read path / Q35 source
precondition); `collector-latin1.py` and `collector-bom.py` **green at 14,044
B** — the read-path fix's subjects unaffected. Live delta `{}` on all eleven.
`VERDICT: ALL REGRESSIONS HOLD`.
## 7. Live state
**Delta 0 on bytes / sha256 / mtime_ns across all 21 invocations** (5 green +
5 matrix + 11 regressions): snapshot `/root/.helm/data/snapshot.json`
**14,039 B / `aa842b0fd88037f83115b4a54d617f27666f4de06ef84f9a64c8fd0a5f04656d`
/ mtime_ns `1791479579475780303`**; held pin
`/root/.helm/data/held/stackchain-home.html.sha256` **120 B /
`5b6d70716153c3f4bd82045d5d71edf88f793a240ce1275c3dfca5759d0ee297` /
mtime_ns `1791478687099553071`**.
**ctime moved on both objects** — `1791907371554631194` — the in-gate and
guard restores of the `q52-dis-home` and `q52-raw-home` rows, which wrote
live by design. The stamp a restore cannot undo (Q43); disclosed, not hidden.
Final tree: **0 sandboxes** (`glob` for `helm-legs-*` / `helm-q39-*` → `[]`).
Deployed `/root/.helm/bin/` untouched (`helm-selftest.py` 33,647 B /
`7ed1bca8…` / 2026-10-11 11:13:31Z, `grep -c HELM_COLLECTOR` = **0** — Q40's
finding stands, so none of Q31–Q52 is in production). Disk **46 GiB free of
154 (71 % used)** (`df -h /`).
## 8. Five defects of mine, logged first
1. **The witness crashed on a missing import.** `subprocess` is imported at
module level *below* the witness block (repair 5), so the first witness
run died with `NameError: name 'subprocess' is not defined` after the
copy had already been built. Fixed with a local import in the block,
asserted in the patcher.
2. **The witness was anchored before the copy was written.** The first
placement ran `subject-legs.py` before `_q43_subject.write_text(...)`, so
it died on `FileNotFoundError`. Re-anchored on the copy write; the
patcher asserts the ordering (inventory < rebuild < callers < check <
copy write < witness).
3. **The witness's live census read a directory.** `live_held_after` used
`_q44_held_live.read_bytes()`, which is the held *directory* in this
lineage — `IsADirectoryError`. Fixed to `_q50_public(_q50_census())`, the
same both-objects census the window detector uses.
4. **The NOTE claimed a redirect that had not fired.** The first version
printed "HOME redirected to …" whenever the subject read the home
database — and the disarmed twin's own `FAIL Q52` line contradicted it.
The NOTE is now derived from the actual environment, so the disarmed twin
prints `HOME NOT redirected -- HOME in the legs' environment is '/root'`.
The same defect class as Q50's disarmed twin NOTE; caught here before the
pipe could.
5. **The first matrix measured nothing.** The real collector pins only under
`--pin`, and the suite refuses a `--pin` leg (Q44's rule), so the first
four rows — subjects run without the pin shape — moved nothing and
proved nothing. The `--q52-witness` flag exists because of this: the
class's write path is structurally unreachable from the green suite, the
same shape Q48's witness was built for.
## 9. Limits, stated as the item's first limits
- **The original-subject legs still inherit the ambient environment**
(Q43's resolution leg, G8's dry-run leg, G8's in-process deploy leg): a
subject whose root resolves at import time in the gate's own process
writes live from a leg the redirect does not cover. Q48's residual,
unchanged by this item. The witness covers the copy's environment only.
- **`pwd` reads bypass `HOME`.** `posixpath.expanduser` prefers `HOME` when
set (so the redirect covers it), but a subject calling
`pwd.getpwuid(os.getuid()).pw_dir` directly reads the user database and
the redirect cannot see it. The inventory *names* such a site; the
redirect does not relocate it. Stated, not fixed.
- **The inventory is static.** Home reads behind indirection — `getattr`,
aliasing, a helper module — are outside every static inventory's reach
(carried from Q46).
- **`USERPROFILE` is redirected alongside `HOME`** on this host although it
is a Windows variable and unset here; the redirect is a no-op for it, and
the pair keeps the mechanism portable rather than POSIX-specific.
## 10. Ask
Alexander's, unchanged: (a) the **§4 adoption `cp`** — collector
`4e45021d…` + gate **`78b290ec…`**, the pair that makes
`python3 helm-selftest.py` green with no environment and puts Q31–Q52's
protection into production (the deployed gate today has no
subject-selection path at all); (b) the **`manifest_match` pin** decision
(stale since 2026-10-08); (c) the **Q13 publication call** (item 13's
decided note — escalate, do not edit the live page — still controls over its
`proposed_action`). This run added no new ask, and wrote and restored the
live held directory twice, by design, under the guard (disclosed in §7).
— Merlin