Q47 CLOSED (Merlin): the read path — a subject the interpreter can run neither crashes the gate nor is falsely refused
The subject's source is now read the way CPython reads it: bytes through tokenize.detect_encoding (BOM stripped, PEP 263 cookie honoured), with a dated exit-2 refusal for a file that cannot be read that way, at Q46's guard point — after Q35's precondition, before the admission probe, before any leg, before any sandbox. All four subject-source reads route through the one reader; one stderr NOTE when the encoding is not plain utf-8; the Q46 parse guard, the Q45 redirect, every check() and the 169 count untouched. Deliverable recoveries/helm-read-path-guard.md (12,821 B), scratch tree /root/crucible/q47/, evidence out/Q47-NEGATIVES.json (21 rows), out/Q47-VERDICT.json, out/Q47-GREEN.json, out/DIFF-vs-q46.txt. Measured, not asserted: latin-1 cookie subject rc 0 / 169 passed / ALL GATES PASS under the shipped gate (Q39-Q45 crash with UnicodeDecodeError); BOM'd collector rc 0 / 169 passed (Q43/Q45 SyntaxError at the sandbox copy, Q44 AttributeError, sandboxes left); genuinely undecodable subject rc 2 with the dated Q47 REFUSAL and no sandbox. Q39 runs the BOM subject green with 164 passed — the read-path class is the text-compiling gates (Q43-Q46), not subject handling generally. Green pair 5x rc 0, 169/0, 13,312 B stdout / 425 B stderr, live and held delta 0 on all 26 invocations. Adoption pair moves to collector 4e45021d + gate 76f2292b. Nothing deployed, no cp into /root/.helm/bin/, decisions/ and inbox/ untouched, no another-party file edited, no secret. Run 13 in the check-in; queue block appended.
This commit is contained in:
1 parent
c104fbbc58
commit
8cb941fc26
3 files changed
+313
No files matched your search
@@ -136,4 +136,137 @@ in the files it lands — the long strings are gate check names, refusal message
|
||||
digests. This run's commits are authored `Merlin <merlin@agents.invalid>` (`git -c`), so the monitor
|
||||
sees them as counterparty content per Q36's v1.3 rule.
|
||||
|
||||
—
|
||||
---
|
||||
|
||||
## Run 13 — Q47: the read path
|
||||
|
||||
### Did
|
||||
|
||||
Claimed and finished **Q47 — the read path** (Helm lane; opened by the pipe on reply 30's C1, its
|
||||
new `Documented` finding). Deliverable: `recoveries/helm-read-path-guard.md` (**12,821 B / sha256
|
||||
`5bf9ba115071a97f07a4d6e09bd4d4add78646680724ff63e32f2d101cdc1516`**, this host). Queue block
|
||||
appended to `WORK-QUEUE.md`. Scratch tree `/root/crucible/q47/`; nothing deployed, no `cp` into
|
||||
`/root/.helm/bin/`, `decisions/` and `inbox/` untouched, no another-party file edited, no secret.
|
||||
|
||||
**The defect, both directions, one cause.** A subject with `# -*- coding: latin-1 -*-` and one
|
||||
latin-1 byte — bytes-compile OK, `python3 <fixture>` rc 0, so the **interpreter runs it** — crashed
|
||||
the gate at `COLLECTOR.read_text()`: UTF-8 decode raises `UnicodeDecodeError`, rc 1, a traceback, no
|
||||
dated refusal. The real collector with a UTF-8 BOM — CPython strips a BOM when it reads a source
|
||||
file — was **falsely refused** by the Q46 guard: the decoded text still carries U+FEFF and
|
||||
`compile()` calls it an invalid non-printable character. The gate reads a decoded `str`; the
|
||||
interpreter compiles the file's **bytes**.
|
||||
|
||||
**The pair.** Collector **byte-identical to Q46's** (`4e45021d80395d3f…`, 720 / 29,954, measured by
|
||||
sha256) — every change is in the gate. Gate **`76f2292b9a6cad08…`** (**115,182 B / 2,124 lines**)
|
||||
against Q46's `ae69d5dd…` (110,509 / 2,032); `out/DIFF-vs-q46.txt` = **130 lines = 2 file + 4 hunk +
|
||||
96 added + 4 removed + 24 context**, and 2,032 + 96 − 4 = 2,124 exactly (the lane decomposition
|
||||
rule: added = `^+` minus `+++`, removed = `^-` minus `---`).
|
||||
|
||||
**What changed — one reader, four read sites, one refusal.** (1) `_q47_subject_text()` reads the
|
||||
subject the CPython way: bytes through `tokenize.detect_encoding` (BOM first, then the PEP 263
|
||||
cookie, then utf-8), decoding with `utf-8-sig` when a BOM is present so it is stripped exactly as
|
||||
the interpreter strips it. (2) A **dated exit-2 refusal** at Q46's guard point — after Q35's source
|
||||
precondition, before the admission probe, before any leg, before any sandbox — with the live
|
||||
snapshot read before and after. (3) All four subject-source reads routed through the one reader
|
||||
(Q35's `_q35_src`, the admission's `_q39_src_text`, the G8 source read, the G8 source-guard check).
|
||||
(4) One `Q47 NOTE` on **stderr**, never stdout, and only when the subject's encoding is not plain
|
||||
utf-8, so a green pair's stdout shape is unchanged and the positive side is visible. The parse
|
||||
guard, the redirect, every `check()` and the 169 count are untouched.
|
||||
|
||||
**The interpreter's own verdict, measured not assumed** (`make_fixtures.py`, fixtures built from
|
||||
the real collector with asserted counts): `collector-latin1.py` (30,028 B / `6ce8bf90292d83f1…`) →
|
||||
`python3 <fixture> --skip-tests` **rc 2**, the destination rule refusing — the file **runs**;
|
||||
`collector-bom.py` (29,957 B / `d14906c868e24997…`) → **rc 2**, runs; `collector-undecodable.py`
|
||||
(30,006 B / `47fc3580fdfe8308…`) → **rc 1**, `SyntaxError: Non-UTF-8 code starting with '\xe9'`.
|
||||
|
||||
**The discriminator, fifteen invocations, five gates** (`run_negatives_q47.py` →
|
||||
`out/Q47-NEGATIVES.json`, 57,247 B, 21 rows, `generated_at 2026-10-13T03:46:55.002706+00:00`;
|
||||
legacy gates digest-verified `9b09f328…` / `c22601ec…` / `18580c10…` / `2bbca218…`):
|
||||
|
||||
| fixture | Q39 | Q43 | Q44 | Q45 | **Q47 (shipped)** |
|
||||
|---|---|---|---|---|---|
|
||||
| `collector-latin1.py` | rc 1 — `UnicodeDecodeError … position 30026` (0.16 s) | rc 1 — same (0.13 s) | rc 1 — same (0.15 s) | rc 1 — same (0.14 s) | **rc 0, 169 passed / 0 failed, ALL GATES PASS, 13,312 B, 12.44 s**, `Q47 NOTE … (iso-8859-1)`, no leftover |
|
||||
| `collector-bom.py` | **rc 0, 164 passed, ALL GATES PASS, 11,398 B, 10.12 s** (see the finding below) | rc 1 — `SyntaxError: invalid non-printable character U+FEFF` at the sandbox copy's import, 575 B, **`legacy/q43/helm-legs-_sja2yko` left** (0.61 s) | rc 1 — `AttributeError: 'NoneType' object has no attribute 'body'`, 0 B, **`legacy/q44/helm-legs-sl1v6yx5` left** (0.22 s) | rc 1 — `SyntaxError … U+FEFF`, 575 B, **`legacy/q45/helm-legs-8qmmr11u` left** (0.66 s) | **rc 0, 169 passed / 0 failed, ALL GATES PASS, 13,312 B, 12.22 s**, `Q47 NOTE … (utf-8-sig)`, no leftover |
|
||||
| `collector-undecodable.py` | rc 1 — `UnicodeDecodeError … position 30004` (0.14 s) | rc 1 — same (0.14 s) | rc 1 — same (0.19 s) | rc 1 — same (0.14 s) | **rc 2, stdout 0 B (`e3b0c44298fc1c149…`)**, dated `Q47 REFUSAL: … cannot be read the way CPython reads a source file - 'utf-8' codec can't decode byte 0xe9 in position 30004: invalid continuation byte - refused at 2026-10-13T03:47:32.685519+00:00, before the admission probe, before any leg, and before any sandbox is created.` — no traceback, no leftover (0.13 s) |
|
||||
|
||||
The item's done-criteria, measured: the latin-1 subject proceeds past the guard and runs the whole
|
||||
suite green; the BOM subject is not refused and runs green; a genuinely undecodable subject is a
|
||||
dated exit-2 refusal on the shipped gate (the four pre-Q46 gates stay frozen controls at rc 1,
|
||||
measured, not patched); green still 169 passed / 0 failed; no sandbox left; **`live_delta` = 0 and
|
||||
`live_held_delta` = 0 on all 21 rows**.
|
||||
|
||||
**The Q39-bom row is a finding, not noise.** The oldest gate runs the BOM subject **green with 164
|
||||
passed** while Q43/Q44/Q45 crash on it. Q39's own report says why — `parse line -: the subject does
|
||||
not parse: invalid non-printable character U+FEFF` — and continues: Q39 never compiles the
|
||||
subject's text; it imports the file (where CPython strips the BOM) and runs the probe copy as a
|
||||
subprocess whose SyntaxError the crash detector cannot see. The read-path class is a property of
|
||||
the gates that compile the subject's **text** (Q43/Q45's sandbox-copy import, Q44's `ast`), not of
|
||||
subject handling generally.
|
||||
|
||||
**Regression, all still refused.** `q47-q46reg-noparse`: rc 2, the dated `Q46 REFUSAL` unchanged —
|
||||
the read-path fix did not loosen the parse guard. `q47-neg-n1…n5`: all five Q39 negatives rc 2,
|
||||
live delta 0.
|
||||
|
||||
**Green pair, five consecutive invocations** (`run_green_q47.py` → `out/Q47-GREEN.json`, 2,725 B;
|
||||
logs `out/q47-green-{1..5}.{out,err}`): rc 0 each, **169 passed / 0 failed**, ALL GATES PASS,
|
||||
**13,312 B stdout / 425 B stderr**, 12.03–12.58 s, live delta 0, no leftover; stdout digests
|
||||
`21920260cade855a…` / `580021754ed55d3d…` / `49025d65cc3082e5…` / `c8155f076d214d24…` /
|
||||
`1d2c7d69a8531d39…`. `out/q47-green-1.out` vs Q46's green log: **184 lines each, exactly 4 differ,
|
||||
all four randomized names** (the `/tmp` render dest, the deploy temp dir, the two sandbox-path
|
||||
lines) — the read path changed nothing the suite prints.
|
||||
|
||||
**Live state, delta 0 across all 26 invocations** (21 harness rows + 5 green runs), censused on
|
||||
(bytes, sha256, mtime_ns, ctime_ns): snapshot **14,039 B / `aa842b0fd88037f8…`** / mtime_ns
|
||||
`1791479579475780303` / ctime_ns `1791789490146606356`; held pin **120 B / `5b6d70716153c3f4…`** /
|
||||
mtime_ns `1791478687099553071` / ctime_ns `1791846636217687791` (the Q45 restore's trace).
|
||||
Deployed bin untouched (`ee0b36a82f23fcf3…` / `7ed1bca8f9da624f…` / `5c67473d89283c3d…`, plus
|
||||
`helm-validate.py` `12570786ba703787…` and `summarize.py` `7386999867789694…`). Final tree:
|
||||
**0 sandboxes**.
|
||||
|
||||
**Four defects of mine, logged first in the deliverable.** (1) The harness's verdict block passed
|
||||
fixture names **with** `.py` into a row-name builder that strips it, so the verdict raised
|
||||
`StopIteration` → `RuntimeError` **after** the rows JSON was written; the verdict was recomputed
|
||||
from the rows by `recompute_verdict.py` and `run_negatives_q47.py` is patched (the strip) — no gate
|
||||
invocation was re-run. (2) The negative fixtures' destination literals still name the **q46** tree
|
||||
(inherited by `cp -a`), so this run's fixture-dest census was vacuous; evidence no cross-tree write
|
||||
happened: `ls /root/crucible/q46/fixtures` shows no `n*-dest`/`dep-dest` directories, and each
|
||||
negative row's stderr shows the leg refusing its own destination. (3) The copies this gate writes
|
||||
are UTF-8 by construction (their header lines push the subject's cookie past PEP 263's line-2
|
||||
limit), so non-ASCII in **executable strings** under a cookie would be re-encoded — the fixture
|
||||
carries its byte in a comment only; recorded as a limit, not fixed. (4) The guard stays static —
|
||||
runtime-generated code is out of every static guard's reach (carried from Q46).
|
||||
|
||||
### Blocked
|
||||
|
||||
Nothing in my lane. Same standing gates, all Alexander's: the **§4 adoption `cp`** — now collector
|
||||
`4e45021d…` + gate **`76f2292b…`** (superseding `ae69d5dd…`); Q40's finding stands (the deployed gate
|
||||
`7ed1bca8…` reads `HELM_COLLECTOR` zero times), so none of Q31–Q47'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 is re-runnable from
|
||||
`/root/crucible/q47/`: `run_negatives_q47.py`, `run_green_q47.py`, `recompute_verdict.py`,
|
||||
`figures_q47.py` and `show_rows_q47.py` are self-contained.
|
||||
2. **Merlin's Q45 §9 follow-on stays open and unclaimed:** resolve every path the subject declares
|
||||
**at runtime**, rather than rewriting the literals it declares statically. Beside the adoption
|
||||
decision, that is the one Helm-side follow-on I would name.
|
||||
3. **Q7, measured: the Helm lane's executable set is empty again.** 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.
|
||||
|
||||
### Ask
|
||||
|
||||
Alexander: nothing blocking. The standing items, consolidated: (a) rule on the **§4 adoption `cp`**
|
||||
— collector `4e45021d…` + gate **`76f2292b…`**, the pair that makes `python3 helm-selftest.py` green
|
||||
with no environment and puts Q31–Q47'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`).
|
||||
|
||||
Landing evidence for this run lands as its own commit after the push, over the anonymous raw path,
|
||||
the same way Run 12's did.
|
||||
|
||||
— Merlin
|
||||
Reference in new issue
Block a user