# Provisioning the isolated home so the LAN-bootstrap gate can admit it

hertz, 2026-09-13. **PROPOSAL ONLY.** Nothing here is executed: no host query, no field run, no
signing, no apply, no build, no daemon launch. `fp-driver-d2.sh` (e093e80d) and `d2_capture.ps1`
(69b8506a) stay frozen. Product source is read-only for me (todlando's lane).

## 1. The refusal, mechanically — and why it lands before the firewall

The D2 setup command is `env SPT_HOME=$H $EXE serve lan --bootstrap --port $PORT`, run elevated.
Traced through todlando's lane at `.worktrees/304-w2-repr`:

    serveverb.rs:190   ensure_running()                  -> a daemon for $H, spawned from the
                                                            INVOKED exe (daemon.rs:874 current_exe)
    serveverb.rs:194   servehost::call(LanBootstrap)
    lanhost.rs:665     start_at_inner -> gate_with_policy(home, CANONICAL_EXE, production)
    lanhost.rs:249-263 reads the exe bytes, opens $H/releases, runs set_gate
    lanhost.rs:184-213 FOUR CONJUNCTS (below)
    on Err           -> ServeResult::LanRefused -> serveverb.rs:260 eprintln + exit 1
    on Ok            -> LanUp -> serveverb.rs:247 report_lan_admission
                              -> request_lan_firewall -> elevated `serve lan-firewall`
                              -> bootstrap_firewall::reconcile(binder, port)  <- THE OWNED PAIR

**`report_lan_admission` is only reached on the Ok arm.** A refusal therefore produces no listener,
no firewall admission and no rule pair — the D2 experiment's subject never comes into existence.
Doyle's statement is exact, and this is the code path that makes it exact.

The four conjuncts (`set_gate`, lanhost.rs:184-213):

1. `staged` is `StagedUpdate::Set(signed)` — a legacy SINGLE is refused `REFUSED_UNSIGNED_EXE`
   (it carries no per-triple identity);
2. `verify_update_set_metadata(&signed, policy)` passes under `VerifyPolicy::production(0, 0)`;
3. the host triple's `artifact_sha256` equals the sha256 of **the running daemon's own executable**
   (`CANONICAL_EXE`, captured once at daemon start, lanhost.rs:90-110) — else `REFUSED_SHA_MISMATCH`;
4. `applied_state()` is `AppliedRecord::Applied { version }` **at that set's version** — a staged
   but unapplied set, an `AppliedPending` trial or a `RolledBack` record all give
   `REFUSED_SET_NOT_APPLIED`.

A fresh isolated home has an empty `$H/releases`, so it fails at conjunct 1. **Provisioning is the
phase that makes all four true honestly.** Nothing below removes, relaxes or reorders a conjunct.

## 2. Two ways to satisfy the gate, and they differ in what the experiment's SUBJECT is

The gate anchors on **the bytes the daemon is running**. So the question "how do we provision the
home" is really "**which binary is D2 about**", and that is the one decision this proposal cannot
take on its own.

### A — the rig runs a PUBLISHED artifact

Stage and apply a published signed set vN into `$H` with the product's own verbs
(`spt update fetch` → `spt update apply`, cli.rs:11065 stages, applyhost/brainproc promote), with
the rig exe being that release's own `spt.exe`. Every conjunct passes under
`BUILTIN_RELEASE_KEYS` with no key material minted anywhere.

**Cost: the D2 subject stops being the W2 worktree build.** The current driver pins
`EXE_SHA=72d30901…` at `.worktrees/304-w2-repr/target/release/spt.exe` — an unsigned local build
that is not an artifact of any signed set and **can never satisfy conjunct 3 under the builtin
keys.** Under A, D2 measures the firewall-pair behaviour of a published release, not of the
worktree subject.

### B — the isolated home carries its own trust root (recommended)

`VerifyPolicy::production` loads `identity_dir()/release-keys.json` (release.rs:359-364), and
`identity_dir()` is **`spt_home()/identity`** (`spt-store/src/perch.rs:245`) — home-scoped, so an
isolated `SPT_HOME` has its own. `load_from` starts from `BUILTIN_RELEASE_KEYS` and the **file keys
ADD** (release.rs:377-400). So an intentionally provisioned home can trust one extra, rig-scoped
release key, and a set signed with it — whose host artifact is the **worktree build's exact bytes** —
satisfies conjuncts 1-3 with no product change at all.

**This is the documented mechanism used as documented**, not a bypass: a per-node overlay that exists
so a node's trusted set can be widened or narrowed without a rebuild. It weakens nothing for any
other home, because no other home has the file.

**What B costs, stated plainly:** the rig key is a *real* release-trust key for any process whose
`SPT_HOME` is that home. Containment is the whole discipline:

- the key file exists **only** at `$H/identity/release-keys.json`, under a run-bound disposable root;
- `identity_dir()` is home-scoped, so the fleet home (`~/.spt`) cannot pick it up — that is a
  property of the product, not a convention I am asking anyone to remember;
- the signing secret never enters the repo, the fleet home, or a shared scratch dir, and dies with
  the run root;
- the file **adds a key and revokes nothing** — a `revoked` entry would void a builtin id and that
  IS a weakening;
- signing authority is doyle's to grant. I am proposing the shape, not minting anything.

**Recommendation: B**, because D2 exists to measure the W2 build's firewall behaviour and A changes
the subject out from under it. If doyle prefers A, everything below still applies except §2B and the
signing step — and the finding must then say, in the record, that it is about a published release.

## 3. Provisioning is a separate admitted phase

It runs **before GO**, on the preparation clock, and it is not the experiment:

- it makes no firewall mutation and asks for no elevation (staging, applying and starting a daemon
  in an isolated home are unelevated; only the later `serve lan --bootstrap` leg is elevated);
- it **refuses to start** rather than truncating — the `prep_check` shape already in the driver
  (exit 8: nothing dispatched, nothing created, nothing to tear down);
- a failed provisioning means the experimental window never opens.

## 4. The dedicated paths (todlando's constraint)

    RIG_ROOT   <run-root>/rig                     run-specific, refused if it already exists
    RIG_EXE    $RIG_ROOT/bin/spt.exe              THE ONE executable path
    H          $RIG_ROOT/home                     SPT_HOME
    trust      $H/identity/release-keys.json      the rig key (B only)
    relcache   $H/releases                        staged set + artifacts
    rollout    $H/releases                        applied_state / last_outcome live in the same cache

**One path, shared by CLI, supervisor and brain — and that is structural, not a convention.**
`spawn_detached` spawns the daemon from `std::env::current_exe()` (daemon.rs:874) and the brain is
spawned by that supervisor, so invoking **only** `$RIG_EXE` makes all three the same path by
construction. The rig exe is a **copy**, taken once into `$RIG_ROOT/bin` and hashed there.

**NEVER apply from the preserved worktree subject or the fleet installation.** `spt update apply`
replaces the executable it is running; pointed at `$W/target/release/spt.exe` it would overwrite
the preserved subject, and pointed at the fleet install it would overwrite the fleet's binary. The
copy is what makes the apply disposable. The provisioning phase refuses to run if `$RIG_EXE`
resolves (canonically) to either of those two paths.

## 5. Provenance — how an intentionally provisioned home is told apart from any other

Doyle's constraint: **do not remove the fresh-home refusal; preserve refusal of unrelated or
previously-used homes.** Under this proposal **the product refusal is not touched at all** — the
provisioning phase satisfies the gate rather than loosening it, so an unrelated or previously-used
home still fails conjunct 1, 3 or 4 exactly as it does today.

The provenance checks are the RIG's, and they run **before** provisioning writes anything:

| # | check | refusal |
|---|---|---|
| P1 | `$RIG_ROOT` does not exist | **refuse an existing destination rather than clearing it** — the standing rule; a pre-existing root is a previously-used home by definition |
| P2 | `$H` holds no `daemon.pid`, no `releases/`, no `identity/` | a home with any of these has been used; refuse, do not reuse |
| P3 | `$RIG_EXE` canonically equals neither `$W/target/release/spt.exe` nor the fleet install path | refuse — §4 |
| P4 | the copied `$RIG_EXE` hashes to the sha the run pinned before copying | refuse — a copy that is not the subject is a different subject |
| P5 | after provisioning, `$H/provenance.json` exists and names: RUN_ID, `$RIG_EXE` + its sha, the set version, the rig `key_id` (B), and the UTC instant of each step | its absence, or a RUN_ID that is not this run's, means this home belongs to another run |
| P6 | the staged set's host `artifact_sha256` equals P4's sha | the home was provisioned for a different binary |
| P7 | `applied_state()` is `Applied{version}` at the staged set's version | the promotion did not complete; §7 |

P5 is what makes "intentionally provisioned" a **measurable** property rather than an assertion, and
P1/P2 are what keep it from being satisfied by a leftover.

## 6. Admission — the artifact hash AND the running executable path

Both, as doyle requires, and they answer different questions:

- **H1 — the bytes.** `sha256($RIG_EXE)` equals the host triple's `artifact_sha256` in the staged
  signed metadata, measured **after** the apply. This is the same conjunct the gate takes, taken
  independently by the rig so that a gate pass is corroborated rather than trusted.
- **H2 — the running path.** The supervisor pid from `$H/daemon.pid`, its `ExecutablePath` and every
  child of it from `Win32_Process`, compared to `$RIG_EXE` with **`binder_path_canon`/
  `binder_path_verdict`** — the r2 comparison already frozen in the driver (:350-440) and covered by
  `binder-path-fixtures-r2.sh` (24/24, plus r1 retained as its negative control). `DIFFERENT`
  refuses; `UNMEASURED` refuses — an instrument failure is not a pass.
- **H3 — the listener's binder.** The product reports the listener's binder itself
  (`ServeResult::LanUp { binder }`, serveverb.rs:247); it must canonically equal `$RIG_EXE` too.
  The driver already runs this comparison for the 29470 listener at :2389.

H1 without H2 admits the right bytes at a path nothing is running; H2 without H1 admits the right
path holding different bytes. Neither is sufficient, which is why doyle asked for both.

## 7. Bounded apply and promotion

`spt update apply` is not one step: it records `AppliedPending`, swaps the executable, and the
promotion to `Applied` happens when the next generation comes up healthy (applyhost.rs:280,
brainproc.rs:702) — with `RolledBack` as the other outcome. It can therefore **hang, or settle in a
state the gate refuses** (conjunct 4 refuses `AppliedPending` explicitly).

- Provisioning runs the apply under a bound of the F5 `bounded()` shape — a deadline, the child
  registered at launch, the termination outcome **measured** rather than assumed (§F5 r2.1 §4a/§6).
- The promotion is **observed, never forced**: `applied_state()` is polled to the deadline. If it
  is not `Applied{version}` at expiry, provisioning **REFUSES** and the window never opens. It does
  not hand-write the applied record: a record written to make a check pass is exactly the class of
  workaround that manufactures a green.
- `AppliedPending` at expiry is reported as itself — a trial in flight, distinct from `RolledBack`,
  distinct from an unreadable cache.

## 8. Residue disposition

Everything provisioning creates is **inside `$RIG_ROOT`**, and that is the whole reason for §4:

| residue | disposition |
|---|---|
| `$H` (identity incl. the rig key, releases cache, applied record, daemon.pid) | dies with `$RIG_ROOT`; the key file is named explicitly in the teardown record so its destruction is a measured step, not an implication |
| `$RIG_EXE` and any `spt.old-N` the apply leaves beside it | enumerated and hashed before removal; a leftover `.old-N` is evidence the apply swapped, and is reported before it is reaped |
| the daemon/brain processes | **not killed by provisioning.** They are registered identities (F5 §4a) and go through the same measured, marker-plus-measurement cleanup gates as everything else; UNREADABLE is never CLEAR |
| a FAILED provisioning attempt | **preserved, not cleared** — new run-specific directory next time, refuse an existing destination, and a failed attempt is often the only evidence a defect was real |

Nothing outside `$RIG_ROOT` is removed by this phase, and no `target/` directory is involved at all.

## 9. One platform caveat, stated because it does not fire here

`CANONICAL_EXE` is captured at daemon start and the gate hashes the file at that path when the
request arrives. On **Windows** — this rig — `current_exe()` is path-tracking, so a gate running
after the apply hashes the **new** bytes, which is what makes "provision, then serve" work in one
daemon lifetime. On **Linux** the same sequence would hash the OLD inode through
`/proc/self/exe` (the KH 6.11 mechanism this capture exists to avoid, named in lanhost.rs:87-92) and
fabricate `sha-mismatch`. **This proposal is Windows-only as written**; a Linux rig must restart the
daemon between the apply and the serve, and that is a different sequence, not the same one.

## 10. What needs doyle's ruling, and what I am not proposing

**The decision:** A or B — whether D2's subject stays the W2 worktree build (B, and a rig-scoped
release key has to be minted and signed by someone with the authority) or becomes a published
release (A, no key material, different subject). Everything else in this document is the same under
both.

**Not proposed:** no change to `lanhost.rs`, `set_gate`, the refusal codes or any conjunct; no
removal or relaxation of the fresh-home refusal; no `gate_with_policy` test seam in a production
path; no `SPT_POOL_*`-style override; no automatic termination of anything; and no execution of any
kind until this is ruled on.
