Files
troupe-standup/recoveries/helm-resolved-root-live-path.md
Merlin 26d4020ce2 Q49 landing evidence: the three post-push digests, read back from main
The record's deferred table and the check-in's deferred line are filled from the
anonymous raw read at main, taken after the push (a pre-push readback returns 404
for a new file), so the figures are stable for any outsider. All three HTTP 200
and byte-identical to disk.
2026-10-13 09:07:12 +00:00

15 KiB
Raw Permalink Blame History

Q49 — The runtime-resolved root's live write path: named, reproduced, and closed (with one residual)

Run 15, Merlin, Helm lane. 2026-10-13, this host. Scratch tree /root/crucible/q49/. Every figure below was measured on this host in this run. A claim I could not measure is marked Testimony only or unattributed — nothing here is asserted from the record.

Verdict: Q49 (a) is discharged by measurement — the invocation that moved the live held pin during Q48 is named with its argv and reproduced under a guard, twice, with the negative control. Q49 (b) is closed for the class the item describes (the --dry-run original-subject leg) by the DEFAULT redirect, green 5/5 at 174 passed / 0 failed with no pre-existing check moved. Two things this run found that the item did not predict: the G8 --deploy leg runs hcg.main() IN-PROCESS and was entirely unguarded (it wrote the live snapshot, 14,039 B → 137 B, reproduced and restored), and in the fully disarmed configuration the live pin still moves with no spawn the traced instrument can attribute — that residual is named, not closed.


0. My own defects, logged first

  1. The rebind NOTE counts the census, not the rebind. The Q49 NOTE line reports 2 path constant(s) rebound in the C-inproc-off and D-disarmed rows, where the rebind is disarmed. The counter is filled before the setattr is replaced by pass, so the note overstates what happened. No check depends on the count; the note is wrong and is recorded here rather than silently fixed in a re-run. (Affects rows C and D of §4.)
  2. A restore is not silent on ctime. q49_guard.restore() re-stamps mtime with os.utime, and utime moves ctime. So after any restore, ctime is the restore's own stamp and cannot evidence the write. Every restore record below therefore carries mtime_restamped_to (recoverable) and ctime_now_ns (the restore's, not the writer's).
  3. The first witness JSON was overwritten. out/Q49-WITNESS.json first held a 4-row run whose q49-armed-liveroot row carried the M2 measurement quoted in §4; the 2×2 re-run replaced it. The M2 evidence survives in that row's captured stdout (out/q49-armed-liveroot.out), in the restore records printed at the time, and in §4 below — not in the current JSON.
  4. The item's stated closure could not work as written. Q49 (b) offers "the original-subject legs inheriting the rebuilt environment (one line in _q39_run)". That line already exists in the Q48 gate — helm-selftest.py:1117, _kw["env"] = _q48_env — and it is a no-op for the incident's shape, because with the variable unset there is nothing to inherit. Measured, not argued: the shipped Q48 gate's own NOTE reads 0 redirected from the live data root into the sandbox: none while the pin moves (§1, row R2). What closes the class is redirecting the subject's default literal, which is what §2 implements.

1. (a) The attribution: the invocation, named and reproduced

The v1 fixture is rebuilt from q48/env-root.py by one mechanical change back: the two default literals /root/crucible/q48/protected → /root/.helm (asserted count 2). env-root-v1.py = 5,634 B / sha256 76b55b8ab0203c4f…. v7's own assertion says the v1 incident was "carries the live default"; the v1 driver's first armed row is recorded in q48/out/old/q48-armed-env.err firing the redirect from /root/.helm with Q48_ROOT unset, which is only possible when the subject's own resolution starts there.

Guard: q49_guard.py takes a digest-verified backup of both live objects before any run, compares (bytes, sha256, mtime_ns, ctime_ns), and refuses to restore unless the backup's digest matches the recorded value.

Live state at session start (unchanged at session end): /root/.helm/data/snapshot.json 14,039 B / aa842b0fd88037f8…; /root/.helm/data/held/stackchain-home.html.sha256 120 B / 5b6d70716153c3f4…. Runner: reproduce_q49.py → out/Q49-ATTRIBUTION.json (6,495 B / 89a827295a3d148d…).

# Invocation (exact) rc Live held pin Restored
R1a python3 env-root-v1.py --dry-run --skip-tests, Q48_ROOT unset 0 MOVED 120 → 14 B yes
R1b python3 env-root-v1.py --skip-tests, Q48_ROOT unset 2 MOVED yes
R1c python3 env-root-v1.py --deploy --skip-tests --out <scratch>, Q48_ROOT unset 0 MOVED yes
R2 HELM_COLLECTOR=env-root-v1.py python3 /root/crucible/q48/helm-selftest.py, Q48_ROOT unset 1 MOVED yes
N1a python3 env-root-v1.py --dry-run --skip-tests, Q48_ROOT=<scratch> 0 no —
N1b python3 env-root-v1.py --skip-tests, Q48_ROOT=<scratch> 2 no —
N1c HELM_COLLECTOR=env-root-v1.py Q48_ROOT=<scratch> python3 helm-selftest.py 1 no —

R1a is the fixture-verification shape (make_fixtures_q48.py v1's verify_interpreter(), ~05:5xZ): the held pin is written before the --dry-run early return, so a "writes nothing" flag writes it anyway. Its own stdout says so — "root": "/root/.helm", "held_dir": "/root/.helm/data/held" — with the banner q48-env: held write -> /root/.helm/data/held.

R2 is the driver's first armed row, and it is the one that settles the item's question. Q48_ROOT unset, the shipped Q48 gate prints Q48 NOTE: … 1 variable(s) the subject reads (Q48_ROOT), **0 redirected** … : none, and the pin moves. Which leg: the Q49 per-leg census in §4 row B names it verbatim — python3 …/env-root-v1.py --dry-run --skip-tests, the G8 leg whose own source comment reads "--dry-run at the live destination is NOT gated: it writes nothing" (helm-selftest.py:2274–2278). That premise is the deployed collector's contract, asserted for every subject the gate admits.

The written bytes: 14 B, sha256 784b9bded20e9e22…, content 'q48-env-pin\n' — recorded by a 5 ms polling watcher (watch_pin_q49.py) that copies the pin on every transition, so the content is measured rather than inferred.

So Q48's §6.2 attribution is upheld as a measurement, not withdrawn. The item allowed either outcome; this is the first: the gate's own --dry-run original-subject leg moved the live held pin, with the argv above, and the pipe's negative (live root named → redirect fires → delta 0) is reproduced exactly by N1c.


2. (b) The closure: three changes, three checks

patch_gate_q49.py → helm-selftest-q49.py, 128,948 B / 2,445 lines / sha256 085ddf4f42ae80ea… against Q48's helm-selftest.py 122,186 B / 2,287 lines (a590170f…). The shipped Q48 gate is untouched; nothing was copied into /root/.helm/bin/.

  1. The DEFAULT redirect (prevention). _q48_env_default_sites() inventories every os.environ.get("X", "<default>") pair by ast. A name whose ambient value is absent and whose default literal resolves inside the live data root is redirected to the per-invocation sandbox. Measured on the v1 fixture: Q49 NOTE: … 1 default literal(s) resolve inside the live data root and were redirected … Q48_ROOT default /root/.helm -> /root/crucible/q49/helm-legs-0idqju7v.
  2. The per-leg census of both live objects (detection). _q39_live_objs() reads (bytes, sha256, mtime_ns, ctime_ns) of the snapshot and the pin on both sides of every spawn and appends the leg that moved them.
  3. The in-process --deploy leg (prevention). §4.

Check census: check(" registrations go 120 → 123; the per-prefix diff is exactly +3 check("Q49" and nothing else — no pre-existing check moved, added or reworded.

Green pair, five consecutive invocations (run_green_q49.py, subject = the Q33–Q48 collector 4e45021d…, Q48_ROOT unset):

run rc verdict stdout sha256 (16) s
1 0 helm-selftest: 174 passed, 0 failed 13,863 B 13cf5cf75cd58b62 12.13
2 0 same 13,863 B d4666b9fd3d12676 12.64
3 0 same 13,863 B 0abd28cd50fcc406 12.70
4 0 same 13,863 B 5ed81c37caf679c4 12.57
5 0 same 13,863 B bbbd190164e3cbe6 12.39

174 = 171 + 3, matching the check census exactly. live_delta 0 on both objects across the set. Artifact: out/Q49-GREEN.json (5,169 B / bbbc1c84ea0a3f0a…). The control that makes this meaningful: the real collector reads no environment variable, so both Q49 mechanisms are inert for it (0 env names, 0 default redirects, 0 per-leg moves) — the green pair proves the additions did not move a result, not merely that the suite passes.


3. The 2×2 discriminator: prevention separated from detection

Same instrument that opened the class (the gate driven with HELM_COLLECTOR at a runtime-resolving subject), four rows. Predictions were written before the run (run_witness_q49.py, PREDICT). Artifact: out/Q49-WITNESS.json (13,785 B / 877138d09bbdac3e…).

row gate Q48_ROOT predicted measured
A helm-selftest-q49.py unset delta 0 delta 0 (no restore)
B -nodefault unset pin moves (M1) pin moved, 120 → 14 B, restored
C -inproc-off /root/.helm both move (M2) pin moved, restored
D -disarmed /root/.helm both move pin moved, restored

Every row is rc 1 with a named failing set, and the expected red is stated in advance: the Q43 check "the subject's own default destination resolves to the live snapshot the gate protects" cannot pass for a subject whose root is redirected into the sandbox — it is a live-identity check, not a redirect check. The witness is the live objects' location, not the check count.

A vs B is the closure of the class the item describes, by measurement. Identical configuration, one mechanism switched: armed → live_delta 0 on both objects; disarmed → the pin moves and the per-leg census names the leg: "leg": "…python3 …/env-root-v1.py --dry-run --skip-tests". Prevention and detection are therefore separate: the census does not prevent, the redirect does not report, and each is measured with the other switched off.

Live state after the whole witness run: snapshot 14,039 B / aa842b0fd880…, pin 120 B / 5b6d70716153… — byte-identical to session start.


4. A defect the item did not name: the in-process --deploy leg

G8's --deploy branch (helm-selftest.py:2280–2294) sets sys.argv = ["helm-collect.py", "--deploy", "--skip-tests"] and calls hcg.main() inside the gate's own process, having patched only hcg.SNAPSHOT_PATH.

Measured 2026-10-13 with HELM_COLLECTOR=<a live-default fixture> and Q48_ROOT=/root/.helm, against the shipped Q48 gate: the live snapshot went 14,039 B → 137 B (sha256 aec681402a3d03cb…, the fixture's own payload) and the live held pin 120 B → 14 B, both restored byte-exact by the digest-verified guard. The Q44 check caught the pin (out/q49-armed-liveroot.out: "the held directory changed: before … 120 / 5b6d7071…"); Q39's watch reported "14 legs watched … delta 0 on each" because no subprocess was spawned; and the per-leg census is silent for the same reason.

A subject that resolves its root at import time ignores the SNAPSHOT_PATH patch, so the leg wrote both live objects from inside the gate. The Q49 closure runs that leg with the rebuilt environment and rebinds every path-valued module constant of the imported subject that resolves inside the live data root into the sandbox, and censuses both live objects around the call. Measured on row A: 1 env name(s) rebuilt and 2 path constant(s) rebound … live objects it moved: none, and the new foot check passes. (The count in that note is the census of candidates — see §0.1.)


5. The residual, named and not closed

In the fully disarmed configuration (row D: default redirect off, in-process rebind off, Q48_ROOT=/root/.helm) the live pin moves — reproduced three times — and the instrument cannot attribute it:

  • The traced twin (env-root-v1-trace.py, prints argv/pid/ppid/parent cmdline before its held write) records exactly one fixture main() per run, and its banner reads q48-env: held write -> /root/crucible/q49/helm-legs-chn0zt3b/data/held — the sandbox.
  • grep -a -o "held write -> [^\\\"]*" over the run's full stdout and stderr returns one occurrence, that sandbox one. No spawn names the live path.
  • The Q49 per-leg census and the in-process census both report no move.
  • A 5 ms polling watcher still records the transition: 120 B / 5b6d7071… → 14 B / 784b9bde…, content 'q48-env-pin\n', at t+280 ms, and the guard restores byte-exact.

So the write is real, its content is the env fixture's own pin string, and its mechanism is unattributed. I will not guess at it. It is handed on as the next Helm unit: either a writer outside the subject's main() (a module-scope path in the fixture family, or a gate leg that writes the imported module's HELD_DIR directly), or a hole in the census window itself. Everything else in this run is measured.


6. Limits, and what is owed to Alexander

  • Nothing deployed. /root/.helm/bin/helm-selftest.py is still 7ed1bca8… and still reads HELM_COLLECTOR 0 times, so none of Q31–Q49 is in production. The adoption pair is now collector 4e45021d… + gate 085ddf4f… (Q48's a590170f… plus §2), and the cp remains Alexander's — §4 puts deploy outside a wizard's initiative and no approval artifact exists in this repo or in decisions/.
  • Live objects, whole session: snapshot aa842b0f… / 14,039 B and pin 5b6d7071… / 120 B, byte-identical at start and end. Six live writes occurred under guard (R1a/R1b/R1c/R2, rows B/C/D), every one restored from a digest-verified backup. ctime moved on each restore and is not recoverable (§0.2).
  • Suite ≠ discriminator for this class. No subject leg runs the copy with --deploy --pin, which is why the witness path exists; the discriminator is the mechanism itself, as in Q48.
  • The guard is static, as every guard in this family is: it covers env names and default literals the subject's own source contains, and a shape it cannot see is §5.
  • Q7, measured: the Helm lane's executable set is not empty — §5 is the successor. Q13's publication call and the manifest_match pin (stale since 2026-10-08) are unchanged and still Alexander's.

7. Landing evidence (computed after the commit, not before)

artifact bytes sha256 (16) readback
recoveries/helm-resolved-root-live-path.md 15,220 a64f26e740f61cd0 HTTP 200, byte-identical to disk
checkins/2026-10-13-merlin.md 36,515 cdfed86a1a0be688 HTTP 200, byte-identical to disk
WORK-QUEUE.md 823,541 d0bf9080f9ef6157 HTTP 200, byte-identical to disk

Note on the regress: a file cannot cite its own final digest — the queue block and the check-in defer to each other's figures, and these three rows are the only ones filled from the live read, taken after the push (a pre-push readback returns 404 for a new file). The three digests above are of the post-push revisions, so they are stable for any outsider reading main. This table is the last thing written, so recoveries/…'s own digest describes the revision before this table was filled; the queue block and the check-in carry the matching figures and their own digests are not self-cited.