diff --git a/WORK-QUEUE.md b/WORK-QUEUE.md index 23ccf87..64cb2c3 100644 --- a/WORK-QUEUE.md +++ b/WORK-QUEUE.md @@ -8031,3 +8031,66 @@ append. Live and deployed unchanged (snapshot `aa842b0f…` / 14,039, pin `5b6d7 **Convention, from this tick:** every new opening record carries a forward pointer to its closure line or revision, and a claim check reads **this ledger** before taking any item whose opening line reads open or unclaimed. The eight historical records above are **left exactly as written** — corrections are logged, never deleted, and inserting pointers into them would shift every line-number citation in the archive, Merlin's Run-21 cites (`:6847`, `:6952`, `:7106`, `:7918–7920`, `:7940`) among them. **Why it earns a ledger:** Q49's stale line (`:6847`) is in the Helm lane and reads claimable; Merlin reports it nearly cost Run 21 a duplicate run (**Testimony only** for the near-miss; the stale line itself is **Documented**). + +--- + +**Q45 §9 follow-on, Run 22 — the pwd channel closed: a home-database read the redirect cannot reach is +now refused at admission (Merlin, Helm lane, 2026-10-14 ~01:1x–01:3xZ; clock read at 01:29:06Z).** +Deliverable: `recoveries/helm-pwdir-channel-refusal.md`. Scratch tree `/root/crucible/q55/`; nothing +deployed, no `cp` into `/root/.helm/bin/`, `decisions/` and `inbox/` untouched, no another-party file +edited, no secret printed. + +- **The class, measured on the shipped Q52 gate before the change.** `collector-pwdir-root.py` (the + real collector, root composed from the user database: `ROOT = Path(pwd.getpwuid(os.getuid()).pw_dir) + / ".helm"`, sha256 `8b368eadb857…`, 29,979 B) resolves `ROOT=/root/.helm` and + `HELD_DIR=/root/.helm/data/held` under a **scratch HOME** (rc 0 — the interpreter's own verdict, both + pwd fixtures). Under the shipped gate: default run 175/1 (Q45 FAIL, `escapes={"ROOT": + "/root/.helm", "HELD_DIR": "/root/.helm/data/held"}`) while **Q52 PASSES** — the check asserts the + legs' HOME is redirected, and `pwd.getpwuid` never reads HOME. Under the gate's own `--q52-witness` + flag: rc 0, `copy_pin_files_anywhere_in_sandbox` **[]**, and the **live held pin moves** + (`5b6d7071…` → `fdd45f74…`, mtime_ns `1791478687099553071` → `1791940472306970986`), restored + byte-exact by the guard. The named-constant fixture writes live through the same path + (`302bfda5…`). The defect does not depend on the constant's name; the rename only decides which check + catches it — and a foot check reports, while only a precondition refuses (Q35's own lesson, in the + one place the Q35 fix did not reach). +- **The fix.** Gate **`cd43284ff009d1cceb3aaa6a65b701d1076eaf801e32b705672601b4122b746a`** (143,385 B / + 2,735 lines / **126** `check("` registrations — unchanged) on the Q52 gate `78b290ec…` (140,365 / + 2,677 / 126): one edit, **+3,020 B / +58 lines** (2,677 + 58 = 2,735 exact). A precondition after + Q46's parse guard and the ast import, **before the Q39 sandbox, the admission probe and any leg**: a + subject whose own AST reads the `pwd` module is refused — exit 2, dated message naming the subject, + every `pwd` site with its line, and the live snapshot before/after. Disarmed twin `8adad99bdc872…` + (143,416 / 2,735 / 126), one line different. No `check()` registered: the green pair still reports + exactly 176 / 0. The gate's internal label is **`Q45S9`**, not a queue number (the lane's own + collision lesson, applied to a fresh artifact; the rename happened before any readback or push, so + these are the only digests this record carries). +- **The matrix, 25 cells under the final digest, all guarded.** armed × pwr / pwr-witness / pwn → rc 2 + refusal, stdout **0 B**, live delta `{}`. **disarmed × pwr-witness → the escape reproduces** (probe + `HELD_DIR=/root/.helm/data/held`, pin in sandbox `[]`, live pin `5b6d7071…` → `ced810a9…` / + `1791941106156792591`, restored byte-exact). disarmed × pwr → 175/1, Q52 PASS, Q45 FAIL (the shipped + behaviour). **Green pair ×5: 176/0**, 14,044 B stdout, live delta `{}` each. **Regressions 11/11** + (nine refusals rc 2; latin1 and bom 176/0). Covered shapes are not over-refused: armed × + `collector-home.py --q52-witness` puts the pin **inside the sandbox** (`q52-home/.helm/data/held/…`) + with the live pin unmoved; and the covered fixtures fail **Q43 identically under the shipped gate** + (175/1 both, PASS/FAIL lists identical modulo sandbox names — measured side by side, so the fix + introduces no behaviour change for them). +- **Live, delta 0 on bytes/sha256/mtime across all 25 cells** (`compare final2`, `"moved": false`): + snapshot 14,039 B / `aa842b0fd88037f8…` / mtime_ns `1791479579475780303`; pin 120 B / + `5b6d70716153c3f4…` / `1791478687099553071`. **ctime moved on the pin** across the deliberately-live + witness cells (guard restores) — disclosed. **Sandboxes: q55 tree 0/0** (the refusal exits before the + sandbox is created); the E14 leak re-measured at **26 across /root/crucible and all 26 removed** + (842,401 B reclaimed, after-count 0 both ways, `out/Q55-CLEANUP.json`); the q55 `__pycache__` (the + C32 class) removed in the same pass. Deployed `/root/.helm/bin/` untouched (five digests re-read), + `HELM_COLLECTOR` count **0** — Q40 stands. Disk 45 G avail of 154. +- **Adoption:** the §4 `cp` is collector `4e45021d…` + gate **`cd43284ff009…`**, superseding + `78b290ec…` / `e4967da9…` / `a951dbe8…`. Alexander's, unchanged. +- **Limits and defects are in the record.** The refusal refuses innocent `pwd` readers too (the + conservative direction, same trade as the Q35 marker); the inventory is static (Q46's limit); the + general class — any home channel the redirect cannot reach — stays open. Five defects of mine are + logged first in the deliverable: part 1's void d/e rows (fixtures not yet copied into the tree), its + prefix-vs-full `restored` comparison, its broken Q45 filter, my assumed d/e expectation (the + shipped-gate baseline, taken after, was the correction), and the gate's first-written `Q55` label. +- **Open after this tick, Helm lane:** nothing executable. The §4 `cp`, the stale `manifest_match` pin, + Q13, Q9, Q19 are Alexander's; Q42 zephyrus's; Q52 Mercury's; Q53 the career lane's. **No new item + opened** — the next Helm unit is a decision, not a task. + +— Merlin (Run 22) diff --git a/checkins/2026-10-14-merlin.md b/checkins/2026-10-14-merlin.md new file mode 100644 index 0000000..8ba1171 --- /dev/null +++ b/checkins/2026-10-14-merlin.md @@ -0,0 +1,131 @@ +# 2026-10-14 — merlin + +> Screen. Every claim below carries a path, a hash, a URL, or a command with its output, or the word +> `unverified`. Nothing secret is printed anywhere in this check-in or in the files it lands. + +--- + +## Run 22 — Q45 §9: the pwd channel + +### Did + +Claimed and finished **the Q45 §9 follow-on — the pwd channel** (Helm lane; the lane's own named +residual, the limit the Q45 §9 record carried: *"a subject reading `pwd.getpwuid(...).pw_dir` directly +bypasses HOME and the redirect cannot see it"*). Deliverable: +`recoveries/helm-pwdir-channel-refusal.md` (**14,808 B / sha256 +`d4c381f713e9fe95beefbd14a93220ab4049625f280c116e656419f3e11b7b89`**, this host). Queue block appended +at the foot of `WORK-QUEUE.md` (**951,756 B / sha256 +`d8f54086edbcbbc96e39533d7b6dcf0e9effff108b596ed18685b03d1045e1db`**, +5,419 B / 63 lines, trailing +newline asserted before and after). Scratch tree `/root/crucible/q55/`; nothing deployed, no `cp` into +`/root/.helm/bin/`, `decisions/` and `inbox/` untouched, no another-party file edited, no secret. + +**The class, measured on the shipped gate before the change.** The base gate is +`/root/crucible/q52/helm-selftest-q52.py` = `78b290ecf2d93e0c…` (140,365 B / 2,677 lines / 126 +registrations), copied into `q55/` and re-verified by `sha256sum`. Two fixtures transform the real +collector (`4e45021d…`, 720 / 29,954, byte-identical to Q45 §9's) into a subject whose root comes from +the user database — `collector-pwdir-named.py` (`4d62da91…`, 29,994 B) and `collector-pwdir-root.py` +(`8b368ead…`, 29,979 B, the constant renamed `HELM_HOME`→`ROOT`). Run under a **scratch HOME**, both +resolve `ROOT=/root/.helm` and `HELD_DIR=/root/.helm/data/held` with rc 0 — the interpreter's own +verdict of the class. Under the shipped gate: the default run is 175/1 (Q45 FAIL, `escapes={"ROOT": +"/root/.helm", "HELD_DIR": "/root/.helm/data/held"}`) while **Q52 PASSES** — the check asserts the +legs' HOME is redirected, and `pwd.getpwuid` never reads HOME. Under the gate's own `--q52-witness` +flag — the flag that exists to measure this class's write path: rc 0, +`copy_pin_files_anywhere_in_sandbox` **[]**, and the **live held pin moves** +(`5b6d7071…`→`fdd45f74…`, mtime_ns `1791478687099553071`→`1791940472306970986`; guard +`q55_guard.py compare after-pwr-witness`: `"moved": true`), restored byte-exact. The named-constant +fixture writes live through the same path (`302bfda5…`). A foot check reports after the legs; only a +precondition refuses before them. + +**The fix.** Gate **`cd43284ff009d1cceb3aaa6a65b701d1076eaf801e32b705672601b4122b746a`** (143,385 B / +2,735 lines / **126** registrations — unchanged; one edit, +3,020 B / +58 lines, 2,677 + 58 = 2,735 +exactly) on the Q52 gate: a precondition after Q46's parse guard and the ast import, **before the Q39 +sandbox, the admission probe and any leg** — a subject whose own AST reads the `pwd` module is refused +(exit 2, dated, naming the subject, every `pwd` site with its line, the live snapshot before/after). No +`check()` registered, so the green pair still reports exactly 176 / 0. Disarmed twin +`8adad99bdc872ecd09ddea88ae55b5fc55a3886571ffaa5f49a053b52430586d` (143,416 / 2,735 / 126), one line +different, 31 B **larger** (the Q52-twin pattern). The internal label is `Q45S9`, not a queue number — +the lane's own collision lesson applied to a fresh artifact before any readback or push, so these are +the only digests this run carries. + +**The matrix, 25 cells, all guarded** (`run_matrix_q55.py` → `out/Q55-MATRIX.json`, 12; +`run_matrix2_q55.py` → `out/Q55-MATRIX2.json`, 13). Armed × pwr / pwr-witness / pwn → rc 2 refusal, +stdout **0 B** (`e3b0c442…`), live delta `{}`. **Disarmed × pwr-witness → the escape reproduces** +(probe `HELD_DIR=/root/.helm/data/held`, pin in sandbox `[]`, live pin `5b6d7071…`→`ced810a9…` / +`1791941106156792591`, restored byte-exact) — the refusal is load-bearing. Disarmed × pwr → 175/1, +Q52 PASS, Q45 FAIL (the shipped behaviour). **Green pair ×5: 176/0**, 14,044 B stdout / 1,216 B stderr, +live delta `{}` each, 20.5–24.1 s. **Regressions 11/11** (n1–n5, noparse, dep-unparseable, +undecodable, broken_dep rc 2; latin1 and bom 176/0). Covered shapes are not over-refused and stay +protected: armed × `collector-home.py --q52-witness` puts the pin **inside the sandbox** +(`q52-home/.helm/data/held/…`) with the live pin unmoved; and the covered fixtures fail **Q43 +identically under the shipped gate** (175/1 both, PASS/FAIL lists identical modulo sandbox names — +measured side by side, so the fix introduces no behaviour change for them). + +**Live state, sandboxes, deployed.** Delta 0 on bytes/sha256/mtime across all 25 cells +(`q55_guard.py compare final2`, `"moved": false`): snapshot 14,039 B / `aa842b0fd88037f8…` / +mtime_ns `1791479579475780303`; pin 120 B / `5b6d70716153c3f4…` / `1791478687099553071`. **ctime moved +on the pin** across the deliberately-live witness cells (guard restores) — disclosed. Sandboxes: the +q55 tree **0/0** both ways (the refusal exits before the sandbox is created); the E14 leak re-measured +at **26 across /root/crucible and all 26 removed** (842,401 B reclaimed, after-count 0 both ways, +`out/Q55-CLEANUP.json`), plus the q55 `__pycache__` (the C32 class). Deployed `/root/.helm/bin/` +untouched (five digests re-read: `ee0b36a8…` / `5c67473d…` / `7ed1bca8…` / `12570786…` / `73869998…`), +`grep -c HELM_COLLECTOR` = **0** — Q40 stands, so none of Q31–Q45 §9 is in production. Disk 45 G avail +of 154 (71 %). + +**Five defects of mine, logged first in the deliverable:** part 1's d/e rows pointed at fixtures not +yet copied into the tree (a Q35 refusal against a nonexistent path — void rows, re-run in part 2, kept +with the note); part 1's `restored` field compared a 12-char prefix to a full digest; part 1's Q45 +filter matched the one-space rendering and missed the row it existed to find; my d/e expectation of +176/0 was assumed and the shipped-gate baseline taken after was the correction; and the gate's +first-written `Q55` label. + +### Blocked + +Nothing blocks the lane. The standing human gates are unchanged: the **§4 adoption `cp` — now +collector `4e45021d…` + gate `cd43284ff009…`** (superseding `78b290ec…` / `e4967da9…` / `a951dbe8…`), +the **`manifest_match` pin** (stale since 2026-10-08), **Q13's publication call** (item 13's decided +note controls over its `proposed_action`), **Q9's checksum handover**, **Q19's onboarding**. Q42 is +zephyrus-sparkborn's, Q52 is Mercury's, Q53 is the career lane held for Alexander. No counterparty +check-in or reply has landed since the 2026-10-13 23:18Z hygiene ledger (`git log --oneline +--since=2026-10-13T20:00`: nothing after `8dbaf23`; `checkins/` has no 2026-10-14 file but this one). + +### Next + +1. **The pipe's verification of this run**, when it lands. Everything re-runs from + `/root/crucible/q55/`: `make_fixtures_q55.py`, `patch_gate_q55.py`, `run_matrix_q55.py`, + `run_matrix2_q55.py`, `q55_guard.py`, `census_q55.py` and `cleanup_sandboxes.py` are + self-contained; `out/` holds every cell's stdout and stderr plus `Q55-MATRIX.json`, + `Q55-MATRIX2.json`, `Q55-CENSUS.json` and `Q55-CLEANUP.json` (host-local — the repo carries 0 paths + under `out/`, E13's rule). +2. **No new item opened.** The Helm lane's executable set is empty and a task invented to fill it would + be invented work; the next Helm unit is the `cp` decision, not a task. + +### Ask + +Alexander's, unchanged: **(a)** the §4 adoption `cp` — collector `4e45021d…` + gate +`cd43284ff009…` — the only thing between Q31–Q45 §9 and production protection in `/root/.helm`; +**(b)** the `manifest_match` pin, stale since 2026-10-08; **(c)** the Q13 publication call. No ask of +the pipe: this run returned nothing to the pipe's own queue (the hygiene ledger's Q52 is Mercury's), +and the lane's own limits are discharged by measurement above. + +— Merlin (Run 22) + +### Run 22 landing evidence + +Read back over the anonymous raw path +(`https://forge.alexanderwhitestone.com/git/stackchain/troupe-standup/raw/branch/main/`, no +credential) by `python3 /root/crucible/q55/readback_q55.py `; artifact +`out/Q55-READBACK.txt` (host-local — the repo carries 0 paths under `out/`, E13's rule). All rows +**HTTP 200, served = disk = `git cat-file -p :`**: + +| file | HTTP / bytes | sha256 (served = disk = git) | +|---|---|---| +| `recoveries/helm-pwdir-channel-refusal.md` | 200 / 14,808 | `d4c381f713e9fe95beefbd14a93220ab4049625f280c116e656419f3e11b7b89` | +| `WORK-QUEUE.md` | 200 / 951,756 | `d8f54086edbcbbc96e39533d7b6dcf0e9effff108b596ed18685b03d1045e1db` | +| `checkins/2026-10-14-merlin.md` | 200 / (this file at the pushed revision — a file cannot hash itself, so this row cites the revision it was written against; this append lands as its own commit) | — | + +Live state at readback, unchanged: snapshot 14,039 B / `aa842b0f…` / mtime_ns `1791479579475780303`; +held pin 120 B / `5b6d7071…` / mtime_ns `1791478687099553071`; deployed `/root/.helm/bin/` untouched, +`HELM_COLLECTOR` count **0**. Nothing deployed, no another-party file edited, `decisions/` and `inbox/` +untouched, no secret printed. + +— Merlin (Run 22, landing) diff --git a/recoveries/helm-pwdir-channel-refusal.md b/recoveries/helm-pwdir-channel-refusal.md new file mode 100644 index 0000000..8c246fc --- /dev/null +++ b/recoveries/helm-pwdir-channel-refusal.md @@ -0,0 +1,205 @@ +# Q45 §9 follow-on — the pwd channel: a home-directory read the redirect cannot reach + +> Screen. Every claim below carries a path, a hash, a URL, or a command with its output, or the word +> `unverified`. Nothing secret is printed anywhere in this record or in the files it lands. + +Merlin, Helm lane, 2026-10-14 ~01:1x–01:3xZ (clock read at 01:29:06Z immediately before this write). +Scratch tree `/root/crucible/q55/`; nothing deployed, no `cp` into `/root/.helm/bin/`, `decisions/` and +`inbox/` untouched, no another-party file edited, no secret printed. + +## The class + +Q52 redirected HOME and USERPROFILE inside the one rebuild function that already served the admission +probe and the legs, and registered one check: *when the subject reads the home database, the legs' HOME +must be the sandbox home*. The redirect covers `Path.home()` and `expanduser` because CPython reads +HOME first. It cannot reach `pwd.getpwuid(os.getuid()).pw_dir`, whose answer comes from the **user +database**. A subject composing its root from a `pw_dir` read therefore passes the Q52 check — the check +asserts a property of the *gate's redirect*, not of the *subject's resolution* — on a subject whose root +is the live root. + +The Q45 §9 record named this as its first limit ("a subject reading `pwd.getpwuid(...).pw_dir` directly +bypasses HOME and the redirect cannot see it"). This run measures what that limit costs and closes it. + +## The pair + +Base gate: `/root/crucible/q52/helm-selftest-q52.py`, sha256 +`78b290ecf2d93e0cc3087e5ede8baef5c10ca52986706d5da69a13a619dbdd9f` (140,365 B / 2,677 lines / **126** +`check("` registrations), copied into `q55/` and re-verified by `sha256sum` before any run. + +- Collector: **byte-identical to Q45 §9's** (`4e45021d80395d3f…`, 720 / 29,954, at + `/root/crucible/q48/helm-collect.py`) — every change is in the gate. +- Gate **`cd43284ff009d1cceb3aaa6a65b701d1076eaf801e32b705672601b4122b746a`** (143,385 B / 2,735 lines / + **126** registrations — unchanged) against the Q52 base: one edit, a 3,043-char block minus its + 23-char anchor = **+3,020 B / +58 lines**, and 2,677 + 58 = 2,735 exactly. +- Disarmed twin **`8adad99bdc872ecd09ddea88ae55b5fc55a3886571ffaa5f49a053b52430586d`** (143,416 B / 2,735 + lines / 126) — one line different (`if _q45S9_pwd_sites:` → `if False: # Q45S9 disarmed: the pwd + refusal is cut`), and **31 B larger** than the gate it disarms, the Q52-twin pattern (a comment where + a condition was). +- The gate's internal label is **`Q45S9`**, not a queue number: the lane's own logged lesson (Q45 §9 Run + 19) is that an ad-hoc item label must not collide with a Q number another lane can open. The scratch + tree keeps its build-order name `q55`; the refusal prefix, the twin comment and the block docstring + carry the section id. The rename happened **before** any readback or push, so the digests above are + the only ones this record carries. + +## The fixtures, and the interpreter's verdict + +Two transforms of the real collector (`make_fixtures_q55.py`, each asserted to land exactly once): + +| fixture | root assignment | sha256 | bytes | +|---|---|---|---| +| `collector-pwdir-named.py` | `HELM_HOME = Path(pwd.getpwuid(os.getuid()).pw_dir) / ".helm"` | `4d62da91d94fc010a99d6c157b28b14bef86333afc91336d6b8d7319f3fb14ec` | 29,994 | +| `collector-pwdir-root.py` | `ROOT = Path(pwd.getpwuid(os.getuid()).pw_dir) / ".helm"` (every `HELM_HOME` identifier renamed) | `8b368eadb857dbaf82bb1d2c795778a04b243c1e5ee2527b903ccec508687b3b` | 29,979 | + +Run under a **scratch HOME** (`python3 ` via `runpy`, `sys.argv = ['x', '--skip-tests']`), both +resolve `ROOT /root/.helm` and `HELD_DIR /root/.helm/data/held` — the live root — with rc 0. The class +is measured against the interpreter, not asserted: *ALL PWD FIXTURES RESOLVE THE LIVE ROOT DESPITE A +SCRATCH HOME*. + +The second fixture removes the one thing that would have caught the class in a default run: Q45's +foot check ("no module-level constant of the copy resolves inside the live data root") fails on +`ROOT` either way — but a foot check reports after the legs; only a precondition refuses before them. +Q35's own lesson, in the one place the Q35 fix did not reach. + +## The defect, measured on the shipped gate before the change + +**`collector-pwdir-root.py` under the shipped Q52 gate, default run** (`out-pwr.txt`): rc 1, +**175 passed / 1 failed** — the FAIL is Q45 (`values={"ROOT": "/root/.helm", "SNAPSHOT_PATH": +"/data/snapshot.json", "HELD_DIR": "/root/.helm/data/held", …}`, `escapes={"ROOT": +"/root/.helm", "HELD_DIR": "/root/.helm/data/held"}`), while **Q52 PASSES**: the stderr NOTE reads +*"the subject reads the home database at 2 site(s) (.home(), pwd.getpwuid); HOME redirected to +/q52-home"* — the shipped collector's own `Path.home()` himalaya search plus the injected +`pwd.getpwuid`, and the check is satisfied by a mechanism the `pw_dir` read never uses. + +**The same subject under the gate's own `--q52-witness` flag** — the flag that exists to measure this +class's write path (`out-pwr-wit.txt`): rc **0**. The copy's probe prints `HELM_HOME=None`, +`HELD_DIR=/root/.helm/data/held`; `copy_held_files` is `[]` and +`copy_pin_files_anywhere_in_sandbox` is `[]` — the pin is not in the sandbox because **it is in the live +tree**: `/root/.helm/data/held/stackchain-home.html.sha256` moved from +`5b6d70716153c3f4bd82045d5d71edf88f793a240ce1275c3dfca5759d0ee297` / mtime_ns `1791478687099553071` to +`fdd45f74f59dc7de869d290205e8ed3dc19139e3fbbae9e88f8c6b295ca25dca` / mtime_ns `1791940472306970986` +(guard `q55_guard.py compare after-pwr-witness`: `"moved": true`), restored byte-exact by +`q55_guard.py restore` (rc 0). + +**`collector-pwdir-named.py` under the same witness** (`out-pwn-wit.txt`): rc 0, the same shape — the +live pin moves to `302bfda56b2d858c907345b6c9e1afaee8ac3849d3e45ca7d68ae24428dbe21d` / mtime_ns +`1791940506504841121`, restored byte-exact (rc 0). The defect does not depend on the constant's name; +the rename only decides which check catches it. + +## The fix + +A **precondition**, not a check: after Q46's parse guard (so an unparseable subject is still refused by +Q46 first) and after the `import ast as _q39_ast` line, **before the Q39 sandbox is created, before the +admission probe, and before any leg**. If the subject's own AST reads the `pwd` module, the run is +refused: exit 2, a dated message naming the subject, every `pwd` site with its line number, why the +redirect cannot reach it, and the live snapshot read before and after. No `check()` is registered — a +refusal fails the RUN, not a suite result, so the green pair still reports exactly 176 passed / 0 failed. + +The refusal text (cell a, verbatim opening): *"Q45S9 REFUSAL: the subject collector +/root/crucible/q55/collector-pwdir-root.py reads the home directory from the pwd module (pwd.getpwuid +at line 30), a channel the Q52 HOME/USERPROFILE redirect cannot reach: pwd.getpwuid answers from the +user database, not from HOME, so the Q52 home-channel check is satisfied by a mechanism the subject +never uses and the composed root can be the live root (measured: the --q52-witness run writes the live +held pin from an admitted pwd subject). Refused at 2026-10-14T01:24:23.743346+00:00, after the Q46 +parse guard, before the Q39 sandbox is created, before the admission probe, and before any leg."* + +## The matrix, all under the final gate digest + +25 cells (`run_matrix_q55.py` → `out/Q55-MATRIX.json`, 12 cells; `run_matrix2_q55.py` → +`out/Q55-MATRIX2.json`, 13 cells; every cell run with the live-object guard around it, compare before +and after, restore byte-exact on a move): + +| cell | gate × subject | rc | result | +|---|---|---|---| +| a | armed × `collector-pwdir-root.py` | 2 | refusal, stdout **0 B** (`e3b0c442…`), live delta `{}` | +| b | armed × `collector-pwdir-root.py --q52-witness` | 2 | the same refusal — the witness never runs | +| c | armed × `collector-pwdir-named.py` | 2 | refusal (`pwd.getpwuid at line 30`), live delta `{}` | +| d | armed × `collector-home.py` | 1 | 175/1, Q43 FAIL — **identical to the shipped gate on the same fixture** (see below) | +| e | armed × `collector-expanduser.py` | 1 | 175/1, Q43 FAIL — identical | +| f | **disarmed** × `collector-pwdir-root.py --q52-witness` | 0 | the escape reproduces: probe `HELD_DIR=/root/.helm/data/held`, pin in sandbox `[]`, live pin `5b6d7071…` → `ced810a9ca9f47eabcc3d0429bd903b35dfb9f4cde43a8cca4bd73698545d437` / mtime_ns `1791941106156792591`, restored byte-exact | +| g | **disarmed** × `collector-pwdir-root.py` | 1 | 175/1, Q52 **PASS**, Q45 FAIL — the shipped behaviour, reproduced | +| h | armed × the real collector ×5 | 0 | **176 passed / 0 failed** each, 14,044 B stdout / 1,216 B stderr, live delta `{}` each, 20.5–24.1 s | +| r01–r09 | the nine refusal fixtures (n1–n5, noparse, dep-unparseable, undecodable, broken_dep) | 2 | refused each, live delta `{}` | +| r10–r11 | `collector-latin1.py`, `collector-bom.py` | 0 | 176/0 at 14,044 B each, live delta `{}` | + +**Regressions 11/11; the green pair is 176/0 — the Q52 count exactly, no check added.** + +Cells d/e are the honest result, not a defect of the fix: the two covered-shape fixtures fail **Q43** +("the subject's own default destination resolves to the live snapshot the gate protects") in a default +run, and they fail it **identically under the shipped Q52 gate** — measured side by side this run +(`out/shipped-home.txt` vs `out/fixed-home.txt`, rc 1 / 175/1 both, the PASS/FAIL lists identical +modulo the per-invocation sandbox directory names). Their green path is the witness run, which the fix +does not touch: **armed × `collector-home.py --q52-witness` under the final gate** puts the pin at +`q52-home/.helm/data/held/stackchain-home.html.sha256` **inside the sandbox** +(`copy_pin_files_anywhere_in_sandbox` non-empty) with the live pin unmoved (`5b6d7071…`, mtime_ns +`1791478687099553071`). The covered shapes are not over-refused and stay protected; the pwd shapes are +refused before anything runs. + +## Live state, sandboxes, cleanup + +**Live, delta 0 on bytes/sha256/mtime across all 25 cells** (`q55_guard.py compare final2`, `"moved": +false`): snapshot `/root/.helm/data/snapshot.json` 14,039 B / +`aa842b0fd88037f83115b4a54d617f27666f4de06ef84f9a64c8fd0a5f04656d` / mtime_ns `1791479579475780303`; +held pin 120 B / `5b6d70716153c3f4bd82045d5d71edf88f793a240ce1275c3dfca5759d0ee297` / mtime_ns +`1791478687099553071`. **ctime moved on the pin** across the two deliberately-live witness cells (the +guard restores of cells f and the shipped-gate witness runs) — the durable trace a restore cannot undo, +disclosed, not hidden. + +**Sandboxes, both ways per row** (`census_q55.py` → `out/Q55-CENSUS.json`): the `q55` tree **0 / 0** +(shallow glob / recursive `rglob`) — the refusal exits before the sandbox is created, so it leaves +nothing. Across `/root/crucible` the pre-existing E14 leak was re-measured at **26** (19 `helm-legs-*` ++ 7 `helm-q39-*`, the same distribution as Run 20's census) and **all 26 were removed** by +`cleanup_sandboxes.py` (`out/Q55-CLEANUP.json`: 26 removed, 0 failed, **842,401 bytes** reclaimed, +after-count 0 both ways). The `q55` tree's own `__pycache__` (the C32 class) was removed in the same +pass. These are per-invocation sandboxes from crashed or early-exited runs; no gate run needs them and +none sat in a live path. + +**Deployed `/root/.helm/bin/` untouched** (measured after the matrix): `helm-collect.py ee0b36a82f23…`, +`helm-render.py 5c67473d8928…`, `helm-selftest.py 7ed1bca8f9da…`, `helm-validate.py 12570786ba70…`, +`summarize.py 738699986778…`; `grep -c HELM_COLLECTOR` on the deployed gate = **0** — Q40 stands, so +none of Q31–Q45 §9 is in production. Disk 45 G avail of 154 (71 % used). + +## Adoption + +The §4 `cp` is **collector `4e45021d80395d3f…` + gate +`cd43284ff009d1cceb3aaa6a65b701d1076eaf801e32b705672601b4122b746a`**, superseding `78b290ec…` +(`e4967da9…`, `a951dbe8…`). Alexander's, unchanged. + +## Limits + +- The refusal is a refusal, not coverage: a subject that reads `pwd.getpwuid` for an innocent reason + (a username lookup, say) is refused with the covered shapes. The conservative direction, and correct + for a detector — the same trade the Q35 marker check makes. +- The inventory is static (Q46's limit): a subject that computes its home at runtime from a source the + AST cannot see — reading `/etc/passwd` directly, `os.stat`-ing a crafted path — is outside it. +- The covered set is exactly the redirect's reach: `.home()`, `expanduser` (both forms). +- The general shape stays open: **any** home channel the redirect cannot reach is the class. The pwd + instance is closed by this precondition; the class is not. + +## Defects of mine, logged first + +1. **Matrix part 1's d/e rows were void.** They pointed `HELM_COLLECTOR` at + `collector-home.py` / `collector-expanduser.py` **before those fixtures were copied into the `q55` + tree**, so both rows are a Q35 refusal against a path that did not exist — 0.2 s, rc 2, "carries no + destination rule". Re-run correctly in part 2 after the `cp`; the part-1 rows are kept in + `out/Q55-MATRIX.json` with this note rather than deleted. +2. **Part 1's `restored` field compared a 12-character sha prefix against the full digest**, so it + printed `false` on every restored row including the unmoved ones. Part 2 compares full digests + (`restored_ok: true` on all 13 cells) and the durable evidence is the final `compare final2` + (`"moved": false`). +3. **Part 1's Q45 filter was wrong**: it matched `startswith("FAIL Q45")` against the two-space + rendering `FAIL Q45 …`, so it missed the one row it existed to find. The FAIL line is in + `out/g-disarmed-pwr.out` and is quoted above. +4. **My d/e expectation of 176/0 was assumed, not measured.** I expected the covered shapes to run + green under the fixed gate; the shipped-gate baseline I should have taken first shows they fail Q43 + identically. The measurement was taken after, side by side, and the expectation was wrong — logged, + not smoothed. +5. **The gate label was first written `Q55`** and renamed to `Q45S9` before any readback or push — the + lane's own collision lesson, applied to my own fresh artifact rather than discovered later. The + digests above are post-rename; the matrix logs were re-run in full under them. + +## What this owes + +Nothing from any counterparty. The next Helm unit remains a decision, not a task: the §4 adoption `cp` +above, the `manifest_match` pin (stale since 2026-10-08), Q13's publication call, Q9's checksum +handover, Q19's onboarding — all Alexander's. Q42 is zephyrus's; Q52 is Mercury's; Q53 is the career +lane, held for Alexander. A task invented to fill the lane would be invented work.