# M5-D7 — carried infra seams (JIT plan)

**Status:** CLOSED 2026-06-04 — D7c verified (run 26994211671, twohost green),
D7b shipped (`34278b3`), D7a shipped (meet module; see piece 3 for the
transcript-bind refinement). Upstream: D6 closed (`10e2dcc`, CI-green-FINAL — see
`M5-D6-PLAN.md`). No new requirement activations — D7 closes M4 deferral annotations
(M5-PLAN coverage map). Authoritative: M5-PLAN §D7, M4-D2f-PLAN Q1, the
`apply_brain_only` residual note (`spt-daemon/src/update.rs:214-220`),
`CI-SELFHOST-PLAN.md`, `docs/TWO-HOST-RUNBOOK.md`.

## Researched code map (explorer-verified 2026-06-05)

- **D7a token (exists):** `spt-net::pairing::rendezvous` —
  `rendezvous_token(subnet_name, totp_step) -> [u8;32]` (domain-separated,
  length-prefixed SHA-256) + `rendezvous_window` (±1 TOTP-step skew, 3 tokens).
  Q1 deferral (M4-D2f-PLAN:24, M4-D9-PLAN:127): the derivation shipped, the
  **routing consumer** did not — pairing today needs LAN mDNS or a known addr.
  `RelayPolicy { N0Default, SelfHosted, Disabled }` (spt-net endpoint.rs);
  daemon maps `RelayChoice::N0 => N0Default`. `spt-daemon::relay::Relay` is the
  *message* relay (poll+drain) — unrelated; no rendezvous hooks anywhere.
- **D7b apply (exists, uncalled):** `update.rs:222`
  `apply_brain_only(plan, outgoing, relaunch)` — guarded NotBrainOnly-first;
  `brain_swap.rs` proves the real swap E2E. The honest residual (update.rs:214):
  "the wiring from notif-ack → apply lands with the M5 daemon-lifecycle work."
  The consent notif body already promises the verb: "confirm to apply
  (`spt update apply`) or dismiss" — **`spt update apply` does not exist**
  (`NotifCmd` = List/Dismiss only).
- **D7c (mostly DONE already):** ci.yml test matrix already runs on
  `[self-hosted, Linux, gravity]` + `[self-hosted, Windows, hfenduleam]`;
  `twohost-a`/`twohost-b` cross-runner jobs exist, gated on `[twohost]` in the
  commit message (tailscale peer IPs, `SPT_TWO_HOST_*` env, 900s wait,
  `needs: test`); whole M4 ladder proven green 2026-06-04 (run 26958175812).
  `tests/twohost.rs` env-gated rig: role a = driver, b = seed-holder; identities
  + TOTP + addresses derived from the shared secret. Runbook = manual fallback.

## Decisions (locked)

- **D7c is a verification + gap-close, not a build.** The M5-PLAN acceptance
  ("cross-runner job goes green unattended") is met by the existing tag-gated
  jobs — fire one `[twohost]` run on the current head to prove the M4 ladder
  still passes post-M5-D1..D6, and record the run id in this plan. The M5 ladder
  *rungs* (remote suspend/wake, presence redirect, shell relink, notif→toast)
  are **D9a scope** — do not pull them forward. Workflow edits only if the
  verification run surfaces drift (timeouts, runner labels).
- **D7b = `spt update apply` + the daemon-side orchestration.** The ack IS the
  command the notif body names (ADR-0007: the harness's native confirm
  affordance answers by running it; dismiss = deny — already wired). The CLI
  verb: load the staged release (`ReleaseCache`), re-verify + `plan_update`
  against the running broker's ABI (`classify`), then:
  - `UpdateClass::BrainOnly` ⇒ drive the M3c swap machinery against the live
    daemon (the `brain_swap.rs`-proven path) — the daemon's brain relaunches,
    broker (and every PTY/QUIC resource) survives — REQ-UPD-3's whole point;
  - anything else ⇒ typed refusal naming the class (full-swap orchestration is
    not M5 scope; the staged artifact stays staged).
  Idempotent: nothing staged / already-current version ⇒ `NO_UPDATE` exit 0.
  Dismissing the consent notif marks it — `apply` after a dismiss still works
  (the user changed their mind; the gate was the *prompt*, not a lock).
- **D7a routes the rendezvous over the *iroh relay*, not a new transport:**
  the joiner and responder each derive `rendezvous_window(subnet, step)`; the
  token is used as a **deterministic rendezvous identity** — the responder
  binds/advertises reachability under it, the joiner dials it relay-first
  (`RelayPolicy::N0Default` carries traffic for unknown-NAT peers; that is the
  relay's whole job). Concretely: responder derives an ephemeral iroh keypair
  from the token (seeded — both sides can compute the *public* id), listens on
  it beside its real endpoint for the pairing window; joiner dials that derived
  node id via relay and runs the existing SPAKE2 handshake unchanged (the
  token gates *discovery only*; security stays with the PAKE — a derived-key
  eavesdropper still can't pass SPAKE2 without the TOTP secret). Window
  rotation = re-derive per step; skew = try all three. **Design risk:** iroh
  0.98.2 API must allow a second short-lived endpoint identity — if it fights
  this, fall back to: rendezvous token as an ALPN/stream-prefix on the
  responder's *real* endpoint advertised via a relay-reachable ticket exchanged
  out of band; surface to the user before building the fallback.
- **No new requirements.** Evidence rides existing tags: D7b tags
  `[impl->REQ-UPD-3]`/`[unit->REQ-UPD-3]` (the apply orchestration is its
  surface) and closes the update.rs residual note; D7a tags
  `[impl->REQ-NET-2]`-family (pairing) per the existing rendezvous.rs tags;
  D7c records the verification run id here.

## Pieces (build order)

1. **D7c — verification run.** Push the D7 plan docs commit with `[twohost]` in
   the message (docs-only + ladder run in one); record conclusion + run id
   here. Gap-close only on drift.
   **DONE (gap-closed):** the first run (26992532561 on `1ea2a35`) failed on
   the Linux leg — NOT ladder drift: the D6 presence test
   `dispatcher_surfaces_a_remote_won_notif_at_the_winning_node` asserted the
   seen-mark immediately after observing the spool delivery, but
   `first_fire_at` delivers BEFORE marking (send → mark_seen) — a test race,
   fixed by polling past the gap. Re-run on the fix commit `bc37f06`
   `[twohost]`: **run 26994211671 SUCCESS** — full matrix green including
   `twohost-a`/`twohost-b` (~2m23s each): the M4 ladder holds post-M5-D1..D6 +
   D7b. Acceptance met; no workflow drift.
2. **D7b — `spt update apply`.** CLI verb + daemon-side brain-swap
   orchestration; unit = staged→applied happy path (mock relaunch), class
   refusal, idempotent no-op; update the update.rs residual note to name the
   caller. Consent-notif body keeps its promise.
   **DONE (`34278b3`):** `spt-daemon::applyhost::apply_staged` — at-rest
   re-verify (fail-closed on unprovisioned keys) → classify → BrainOnly-only →
   binary step-aside/land/rollback → live brain handoff; `spt update apply`
   CLI verb (NO_UPDATE / APPLIED:v / typed refusals); update.rs residual note
   now names the caller.
3. **D7a — relay rendezvous routing.** Derived-identity listen + relay-first
   dial inside the pairing window; loopback int test (relay disabled ⇒ direct
   fallback still pairs; the derived-id path exercised hermetically as far as
   iroh allows); rig leg rides D9a. Fall back per the locked decision if the
   iroh API refuses.
   **DONE (refined, not the fallback):** `spt-net::pairing::meet` — the
   derived identity binds fine as a SECOND short-lived endpoint (iroh 0.98.2
   has no objection), but the **ceremony cannot ride the derived-id
   connection**: the transcript binds both real node pubkeys from the
   iroh-authenticated `remote_id` and the initiator trust-pins that TLS
   identity (REQ-HAZARD-PAIR-TRANSCRIPT-BIND) — on a derived-id conn that
   would bind/pin the token-derived key any (name, code-window) holder can
   compute. So the meet endpoint serves **discovery only**: joiner dials the
   derived id (window of 3), the listener answers one frame = the responder's
   real `EndpointAddr`, joiner re-dials the real endpoint on the ceremony
   ALPN — SPAKE2 unchanged, real pubkeys both ways. Security unchanged: a
   meet impostor redirects to a responder that fails the PAKE (the ADR-0006
   collision story). Hermetic int test proves derived-id listen + redirect +
   real-key ceremony conn; relay leg rides D9a.

## NOT in D7
- M5 twohost ladder rungs — D9a. PresenceChannel — deferred. Full-swap
  (broker) apply orchestration — post-M5 (BrainOnly is the M5 surface).

## Conventions (carried)
- NO `cargo fmt`; tag evidence in the same commit; `traceable-reqs check` before done.
- Linux clippy `-D warnings` + `--no-default-features` stay green.
- Push per slice → sha-pinned FINAL; never push over an in-flight run.
- Commit trailer: `Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>`.
