# fp-driver v9 — stub control-flow review: handoff receipt

**Author:** hertz · **Gater/DRI:** doyle · **Closed:** 2026-09-12 · **Host:** HFENDULEAM

**Subject:** `v9-as-delivered/fp-driver-v9.sh`, sha256 `a9510e1e…`, **never edited**. The helper
`run-v9.sh` (`cbc0c734…`) and `exec-runexactly.sh` (`38d41cdc…`) were likewise never edited. Every
run below modified the driver only through its two declared config knobs (`SP=`, `EXE=`), and each
run's diff against the delivered file was verified by me rather than taken from the runner's own
"config-only" line.

## What this receipt does and does not cover

Covered: **stub control-flow only** — the driver's own ordering, state marking, handoff request and
receipt handling, cleanup sequencing, and signal handling, as exercised against stubs.

**Not covered, and no claim is made about any of it:** real provenance (the staged artifact is a
synthetic fixture; the three-way agreement at S5 holds *by construction* and tests nothing about
signing or the product), helper correctness, real firewall behaviour, and field acceptance. No
build, live probe, product execution, or elevation occurred in any run.

## Pinned hashes (`v9-exercises/stubcases/RIG-PIN.sha256`, pin 4, 2026-09-12T10:02:40Z)

| file | sha256 |
|---|---|
| `v9-as-delivered/fp-driver-v9.sh` (**subject, untouched**) | `a9510e1ec86a71266b7f421b90a6a02c5c0f7586f96803c6843133b5b2b0bb12` |
| `run-v9.sh` (**helper, untouched**) | `cbc0c734cd2579568aa58951a5a392ebfe3cbeb4be9cb2de1842e9767a10a28a` |
| `exec-runexactly.sh` (**helper, untouched**) | `38d41cdc688b80f1ed224bef6c3c2a3863ae814fcf531f565c6b9bea187e2105` |
| `make-stubs.sh` (rig generator, final) | `aa356ac19cfa97f9571dee0eded8ca3c3e5b00242bc37e09d96da79d875cb136` |
| `spt-stub.sh` | `04744188fe2d82ec47f1514c04dbebf134f8650a72a3717a8d7994d07a6fa2f6` |
| `port-isolation-check.sh` | `e844abfb685b6d3379bf2ed335c544cc245a3bba5f149c967db71daeb1d19513` |
| `face-assert.sh` | `132ee846406aecfea2b190d8dc3f21cca630357a7c90a26bb308f7ae06e32187` |
| `face-assert-controls.sh` | `1caee26076d7b3d70d14c9370e98b021e1b14e846294ce21848a48769ab4c444` |
| `signal-case.sh` | `f1e23a47891c296a2d3bd3b6c3d4d2de70a280738eed22d7509084f95603e7a8` |
| `signal-assert.sh` | `57b98735142778ac34bccd91b4d7383198e51c33fdcf773e7ab9b45c9c336506` |
| `late-signal-case.sh` | `cc042e0a1ad9f147166982e4d3dd2981f69022712c6f1f4b8d6359f3330ba459` |

Superseded generators are pinned too and **must not be deleted** — each is a live negative control:
`.pre-argv` `2b099c2e…` · `.pre-portfix` `75a1fdc5…` · `.pre-ownership` `6598200f…` ·
`.pre-signalrig` `acce8af1…` · `.pre-driverpid` `ebafe4a0…`.

## Case receipts

| # | case | driver exit | receipt | evidence |
|---|---|---|---|---|
| 1 | generated-command gate (spaces in EXECUTABLE + HOME + CAPTURE) | 1 | **16/16 PASS** over 2 requests | `gate-evidence-094108/` |
| 2 | arm2 setup rejection | 3 | **6 PASS / 1 FAIL** (kept as-is) | `arm2-evidence-094635/` |
| 3 | early-signal (TERM at first product invocation) | 130 | run-v9 4/4; mine **5 PASS / 2 FAIL** (kept as-is) | `signal-evidence/` |
| 4 | late-signal (TERM with a teardown owed) | 130 | **13/13 PASS** | `late-signal-evidence/` |

**Case 1.** Both handoffs executed through `exec-runexactly`; per request: probe stdout and stderr
bytes landed in the *declared* capture files, the receipt carried the command's actual exit and it
was the probe's known exit (7), argv count as received matched the command text, `SPT_HOME` arrived
whole, `SPT_INSTALL_NO_FIREWALL` was stripped by `env -u`, and no argv entry was split at a space.

**Case 2.** Exit 3 with Arm B held; setup non-acceptance recorded; D1 ran on the rejection and before
any teardown request; `TEARDOWN_VERIFIED` on a clean teardown; cleanup entered exactly once.

**Case 4 — the required window, from the driver's own ordering, not from intent:**

```
50  STATE_MARKED fw_mutation_may_have_occurred
55  SETUP FACE=enforcement            <- injection gate; TERM sent here
67  SIGNAL TERM — entering state-aware cleanup once; NO trial will resume
68  CLEANUP_BEGIN states=[daemon_may_run fw_mutation_may_have_occurred listener_may_exist pool_acquired]
70  HANDOFF_REQUESTED teardown seq=002
72  TEARDOWN_CHECK receipt_exit=0 census_exit=0 clean_verdict=1 named_rules_absent=1
73  TEARDOWN_VERIFIED
75  CLEANUP_END steps_failed=0 original_rc=130
```

Handled after mutation became possible (50 < 67) and before cleanup began (67 < 68). Receipt
processed under ABORT and verified rather than accepted; cleanup entered once and completed once;
no trials resumed; foreign 5470 listener preserved with its owner intact and the test home's 29470
cleared. **Signal-send to last dispatched action: 9.141 s** — *not* handler latency; it is the span
from sending TERM to the last action the driver dispatched, and it includes the operator loop's own
3 s poll. Nothing in this rig records when the driver entered its handler.

## Replay adjudications (original failing tallies kept; corrections appended, never substituted)

**FACE — `arm2-evidence-094635/FACE-REPLAY.txt`.** run-v9 requires exactly one
`SETUP FACE=enforcement` occurrence and finds four, so case 2 fails. The face value is a multi-line
blob that `ARM_B_SETUP`, `ARM_B_SETUP_NOT_ACCEPTED` and `CLEANUP_END` each interpolate verbatim: the
count tracks summaries, not outcomes. `face-assert.sh` scopes the record to a standalone
`SETUP FACE=` line preceded by a standalone `SETUP FACE_COUNTS` line, and rejects missing, duplicate
and conflicting records distinctly. Controls **6/6**, including the discriminator — *standalone
removed, every quoted copy kept* → `REJECT-MISSING`, where run-v9's counter still sees three.
Replay verdict: **1 standalone record, value `enforcement`, PASS**. run-v9 is unedited; this stands
as an appended adjudication only.

**Signal assertions — `signal-evidence/SIGNAL-REPLAY.txt`, 11/11.** Case 3's two failures were both
mine: `c=$(grep -c X f || echo 0)` yields the two-line value `0\n0` (grep prints 0 *and* exits 1),
and the expectation itself was wrong — v9 gates the firewall teardown on
`fw_mutation_may_have_occurred` (`fp-driver-v9.sh:438`, skip recorded `:468`, marked `:1060`), and an
early signal lands before any elevated reconcile is requested, so a teardown there would have been
the defect. The driver recorded the decision rather than going quiet:
`CLEANUP_SKIP firewall — no fw_mutation_may_have_occurred state was ever marked`.

## Rig defects found — all mine, none a v9 finding

1. `--stage-dir` extracted with `[^ ]*`, cut at the first space: the fixture staged into a directory
   literally named `sp`, and the driver correctly halted on the missing artifact. Same class at the
   python stub's `--out`. Fixed with argv walks over `"$@"`.
2. `actions.log` renders argv space-joined, so it cannot distinguish one space-bearing argument from
   two — argv assertions were never assertable from it. Added `argv.log`, one argument per line plus
   environment.
3. One global `listener-bound` flag read by portfields regardless of port: the driver bootstraps on
   **29470** while its guard reads **5470**, so the bootstrap flipped the simulated 5470 listener and
   the driver halted on `GUARD_CHANGED post-setup` — a rig artefact wearing a v9 finding. Fixed:
   port-scoped.
4. `--stop` cleared **every** port (doyle's catch), modelling a stop that takes an unrelated 5470
   listener down with the test daemon; with a NONE baseline no arm could have caught it. Fixed: each
   flag records its owning `SPT_HOME` and a stop clears only its own.
5. The two case-3 assertion defects above.

Coexistence proof (`port-isolation-check.sh`, **12/12**) carries a negative control in both parts:
the superseded generator is required to *fail* that part's key arm. Part B seeds a 5470 listener
owned by another home, so the baseline is READ and there is something to preserve; the stop record
reads `cleared=[29470] kept=[5470]`, against `stop cleared every port` pre-fix.

## Remaining limitations and open items

1. **run-v9's FACE assertion still counts quotations.** The scoped parser is a separate instrument;
   run-v9 is unedited. Doyle's to rule.
2. **The gate's `declared capture files now present:` summary line printed EMPTY** while all four
   files are on disk and the same `ls`, run afterwards by hand with the same variable form, lists all
   four. **Unresolved and deliberately unchased** — rerunning would overwrite the evidence.
3. **Case 2's cleanup evidence is bounded.** It ran before the ownership correction, so every
   `GUARD_UNCHANGED` line in it is consistent with an *absent* 5470 listener and says nothing about
   preserving a present one. Stated inside the artifact as `LIMITATION.txt`.
4. **A discarded run is kept, not hidden:** `arm2-CONTAMINATED-preportfix/` ran on the pre-port-fix
   rig and claims nothing about v9; `WHY-DISCARDED.txt` says so.
5. **Case 4's vehicle was run-v9's arm2 arm**, so its console prints arm2's own expectations meeting
   a different case (`driver exits 3 FAIL (want 3, got 130)`). Meaningless here; `VEHICLE-NOTE.txt`
   records it inside the evidence directory.
6. **The staged artifact is a synthetic fixture.** S5's three-way provenance agreement holds by
   construction and proves nothing about real provenance, signing, or the product.

---

# Addendum — runner-census analyzer classification repair (accepted 2026-09-12)

Separate assignment from the stub-gate work above, closed the same day. **Accepted helper:**
`v9-exercises/census-fix/runner-census-r3.ps1`, sha256
`63d0508b93a8d15fa2bc9940708c7a13ac4abda1f8ca9c9a5d32a63b1dcf3e8a`.

**The defect.** `v9-as-delivered/fp-bin/runner-census.ps1:79` classifies analyzers by process image
name (`$c.Name -like 'rust-analyzer*'`). rustup installs its proxies as copies of `rustup.exe`, so a
running rust-analyzer proxy reports `Name=rustup.exe` while its command resolves to
`…\.cargo\bin\rust-analyzer.exe`. The name test misses, the producer regex finds no `\b` word in
that path, and the process lands in `shim_unresolved` — failing the box closed on an IDE service.
Two such processes were live on HFENDULEAM. **Measured by todlando** (not by me) at 10:05:50Z with
the frozen helper: `analyzer=4 shim_unresolved=2`, `valid=YES`, exit 0 — the instrument is healthy
and it is the *gate verdict* that refuses.

**The fix.** Analyzer classification is made against a resolved executable token: argv[0],
corroborated against `ExecutablePath`. Fails closed — never `analyzer`, never a silent drop — when
the command line is absent with no exepath, when argv[0] is unparseable, and when the two readable
leaves disagree. Runner ancestry precedence, the build-class name list, the producer regex, the
summary field names and counts, the positive control and the exit codes are all unchanged.

**Two revisions, both preserved.** r2 (`08943db4…`) promised to refuse an ambiguous command and then
fell back to `ExecutablePath` when a command line was *present but unparseable* — a fail-open doyle
caught by direct read. **r2's own ambiguous fixture could not catch it: that row carried an empty
`ExecutablePath`, so the fallback was never reached — the setup suppressed the failing arm.** r3
refuses a present-but-unparseable command line before any fallback, and distinguishes it from a
genuinely absent one, which still resolves. The reason token carries the distinction into the row.

**Receipts.** r2 fixtures 18/18 (`census-fixtures.ps1`, `b5ee8696…`); r3 fixtures **21/21**
(`census-fixtures-r3.ps1`, `2e4625a0…`), whose load-bearing arm is r2 as a **negative control** on
the new row — r2 answers `analyzer`, r3 answers `shim_unresolved`, so the fixture demonstrably
discriminates the correction. One read-only live census was taken with r2 (`live-census.txt`,
`a51ad529…`): `analyzer=6 shim_unresolved=0`, `valid=YES`; all six analyzers descend from agent
sessions on this box. Pin: `CENSUS-PIN.sha256`.

**r3 live validation — measured by todlando, not by me. Read the scope of it carefully.**
I validated r3 by fixtures only, as directed, so every live arm here is his. What his work
ESTABLISHES, and what it does NOT, are different things, and an earlier revision of this
paragraph ran them together:

*Established.* He identified the file by **hash-hunting 78 candidates rather than trusting the
filename**, verified its hash before and after a binary copy, and ran r3 **directly** against a
live process table, read-only:

    tag=preflight-r3 mode=live procs=582 self_seen=True candidates=6
    cargo_build=0 runner_descended=0 analyzer=6 shim_unresolved=0 valid=YES   (exit 0)

Pids 39260
and 43280 — the two that refused before — read `class=analyzer resolved=rust-analyzer.exe
reason=argv0+exepath-agree`. So the bare-argv[0] case is measured on a live process table, not
merely replayed. Neither analyzer was reaped, so it ran against the population the repair was
written for.

*Also established, and it retired a risk I had understated.* r3 ADDS a `mode=` field to the
summary line, and I described the summary as unchanged on the grounds that no EXISTING field name
or count moved. An added field can still break a positional parse: "I only added" and "nothing
broke" are different claims and I reported them as one. He settled it with controls rather than by
reading the regex, against the parser EXTRACTED from the driver: box_gate reports ok on the live
row (exit 0), DIEs naming `shim_unresolved=1` when that field is mutated (exit 1), and DIEs with
`could not read shim_unresolved` when the field is deleted (exit 1). The mutate and delete arms
are what show the parser reads the field rather than merely tolerating the line.

*NOT established, and this is the open blocker.* None of the above shows that the STAGED DRIVER
would load r3 at runtime. doyle's source read found the driver's hardcoded helper path still
pointing at the ORIGINAL helper, so a bundle can contain r3, and the extracted parser can accept
r3's row, while the driver as it runs still executes the old one. Running a helper directly and
exercising a parser lifted out of the driver are both weaker than the driver loading it.
**Runtime bundle wiring is the admission blocker**; todlando is correcting that
configuration-only execution copy. Nothing in this receipt should be read as clearing it.

**Limitations.** The frozen helper was never run by me for a side-by-side — one census was granted
and spent on the revision; todlando's measurement supplies that arm. Ancestry naming was
deliberately not touched: todlando measured that `Get-Process` reports the parent as
`claude.exe.old.1789154286817` (an update renamed the image in place) while `Win32_Process.Name`
reports `claude.exe`, and `Get-Chain` builds from the latter — so a rule written against an elevated
`Get-Process` read would not match what this helper sees. Nothing in r2 or r3 depends on that
difference. The frozen helper itself remains byte-unchanged at `0d8b67ed…`, corroborated by two
independent reads.
