# WAX-SEAL W2 — TOTP ceremony build JIT (todlando, 2026-08-23)

Lane: `build/wax-seal-w2` off `build/wax-seal-w1` @0244c5d2, worktree `.worktrees/wax-seal-w1`
(kept per dispatch), pool re-claimed label `wax-seal-w2`. W1 GATE PASSED (doyle, 2026-08-23);
PR #156 parks for the golden chain. Dispatch of record: WAX-SEAL-21-JIT.md W2 + design-of-record
block; template: ACCESS-CONTROL-JIT.md FF-T2 + ER bring-up gate (broker seat resolver).

## Design (recon-grounded; both recon reports 2026-08-23)

**Write-through-daemon resolves doyle's W1 interim finding (1).** The ceremony code rides UP to
the daemon; verify + attempt ledger + `mint_seal` + save all run in the daemon process, save
under the same `SEAL_APPLY` mutex as the replication apply arm (`dispatch.rs:1487` — its own doc
names the cross-process race this closes). The client ships the PRESENTED CODE only, never a
verdict — bring-up precedent (`brain.rs:549`, `broker.rs:1959`: an agent can speak the broker
IPC, a client-side check is decorative). No file lock needed; the CLI process never writes
`trust/seals.json`.

**Transport = the attach stream, one code path for local and remote.** New ADDITIVE
`AttachRecord` variants (spt-net `attach.rs`, serde-tagged, N-1 discipline):
- down `SealCeremony { ceremony_id, content_b64, subnet }` — opens the overlay
- up `SealCeremonyCode { ceremony_id, code: Option<String> }` — `None` = Esc-cancel
- down `SealCeremonyResult { ceremony_id, outcome, detail }` — admitted / refused{failures} /
  throttled{retry_in_ms} / cancelled
N-1 closed by a **capability bit**: `Request`/`SubscribeReq` grow
`#[serde(default)] seal_ceremony: bool`; the broker records it on `ControllerSink` and never
pushes a ceremony record to a controller that did not declare it (refusal names the too-old
controller). Old daemons never receive ceremony records (daemon initiates).

**Flow.** Internal mint entry = new daemon IPC verb (`KIND_SEAL_CEREMONY` req/reply, `Brain::`
method; W3's `spt seal mint`/`spt send --seal` call it; W2 tests drive it directly).
Broker dispatch arm:
1. Resolve minter endpoint's session; seat check via the log-lock `has_controller()` — NEVER the
   perch stamp (`info.rs:510` KH 7.15 stamp-lag; `msg.rs:1218` controller_by=None ambiguity).
   No session / no controller / non-capable controller → fast refusal `SEAL_NO_CEREMONY_SURFACE`
   + attach-and-retry detail. No pending-mint state, no timeout queue.
2. Push `SealCeremony` down the controller sink (new `CtrlMsg` arm alongside Output/Sync/Exit —
   rides the writer thread, ordered).
3. Requester conn WAITS (human-scale); conn drop or controller detach mid-ceremony aborts the
   ceremony and answers refused. State lives only while the requester waits.
4. On `SealCeremonyCode`: `classify_attempt` on the SEAL ledger (throttle evaluated FIRST,
   `Presented::Absent`/cancel spends nothing), verify member-or-admin of the BINDING subnet —
   both seeds tried unconditionally via `code_matches_window`, combined bitwise (broker.rs:4134
   pattern; which key admitted is not distinguished — nothing hinges on it here). Refused counts
   and re-prompts (overlay stays up, human retries or Escs — each presentation = one counted
   guess); loud past third (notif, alert-shape reuse). Admitted → compose fully-qualified minter
   `subnet:endpoint@node`, `mint_seal(ceremony_kind="totp")`, save under `SEAL_APPLY`, result
   down + reply to requester.

**Ledger** = same `GateLedger`/`classify_attempt`/`apply`/`backoff_ms` types, NEW file
`trust/seal-ceremony-gate.json` (`perch.rs` sibling of `engine_room_gate_file`). SEPARATE ledger
per the empower precedent's denial-of-governance rationale (api/engineroom.rs:324): failed seal
guesses must not shut the ER bring-up gate a human needs, and vice versa.

**Overlay (rc.rs, client-side byte-emit).** Ceremony mode in `pump`: stdin folds through the
SHARED `fold_code_key` grammar (Esc/ctrl-c cancel, Enter submits non-empty, digits only, len<12)
instead of riding `Input`; overlay paints the TOP HALF (DECSC/abs-position/DECRC byte-emit,
StatusRow + reconnect_banner precedents), shows the sealed content VERBATIM + names the binding
subnet, re-painted after each output frame write (per-frame sticky — the approach the
STATUS_ROW_ENABLED comment reserves for when a grid exists; here the repaint cost is bounded by
ceremony duration). On cancel: clear region, resume normal pumping, session repaints itself
(cosmetic residue accepted — Size-cue class).

## Flags to doyle (novel contract arms, not solo-ruled)

- (a) **Content overflow**: content larger than the overlay surface — CONTENT-SHOWN's consent
  rationale says the human must see ALL bytes they seal. Proposed: client refuses the ceremony
  with a named reason (`content exceeds ceremony surface`) rather than truncating; requester
  gets a distinct refusal. Doyle rules.
- (b) **Blocked requester**: the mint IPC conn blocks for the ceremony duration; ctrl-c/conn
  drop = clean abort. No timeout (dispatch: no timeout queue). Stated, not asked.
- (c) **Wire shape**: 3 additive AttachRecord variants + capability bit (above). FYI.
- (d) **Retry-within-overlay**: multiple presentations per ceremony, each counted. Stated.

## REQs (mint registry-first, doc/impl/unit active, int at gate)

REQ-SEAL-CEREMONY-TOTP · REQ-SEAL-CEREMONY-CONTENT-SHOWN · REQ-SEAL-CEREMONY-ESC-CANCEL
(satisfies releases#19 — tag there) · REQ-SEAL-NO-CEREMONY-SURFACE · REQ-SEAL-CEREMONY-RC-CLIENT.
House form per W1 block traceable-reqs.toml:7120 (banner + single-line title + Gate: clause +
activation comment).

## Task order

T0 registry mints → T1 spt-store (ledger path + any pure classifier) → T2 wire types (attach.rs
variants, msg.rs kinds/payloads, capability bit) → T3 daemon orchestration (dispatch arm, seat
check, ceremony push, verify+ledger+mint under SEAL_APPLY, abort paths) → T4 rc overlay →
T5 units per REQ + clippy + treqs, capture-first (.raw + .exit), report shape unchanged.

Scope guard: NO public mint verb, NO `E` FIDO2 shortcut, no W3 entry points/envelope work.
W3 carries the two-node int leg + int-activation of W1 store REQs.

## Doyle rulings received in-lane (2026-08-23, after JIT authoring)

- Write-through-daemon APPROVED. (b) blocked-requester + (d) retry-within-overlay ACCEPTED as
  stated. Capability arm PINNED at his ask: an attached N-1 controller WITHOUT the bit is NOT a
  surface — refuses SEAL_NO_CEREMONY_SURFACE, never blocks pushing undecodable records (already
  in the REQ title). (a) overflow refuse-arm PROVISIONAL — operator grill question out; a flip
  to scrolling lands as a CONTENT-SHOWN title amendment only.
- REQs MINTED in `traceable-reqs.toml` after REQ-SEAL-VERIFY-CONTENT-BOUND (banner + 5 entries,
  activation comments "delivering commit W2"); they ride the delivering commit with evidence so
  treqs never reds mid-lane. ESC-CANCEL satisfies releases#19 — record on that issue at ship.

## Seam reference (condensed from 2026-08-23 recon; all paths in the worktree)

**W1 seal (this branch):** `spt-store/src/seal.rs` — `mint_seal(store,&content,minter,ceremony_kind,minted_at)->Result<SealRecord,SealMintError>` :251 (in-memory only; persistence is caller's); `SealStore::{load,load_from,save,save_to,find,merge_record}`; `content_hash_hex` :103; `seals_file()` perch.rs:368. Daemon apply arm: `dispatch.rs:1487` `static SEAL_APPLY: Mutex<()>`, `serve_seal_feed` :1495-1546 (lock → load → apply_seal_feed → save-if-inserted). SEAL_APPLY doc names the cross-process race W2 closes. `sealsync.rs`: `apply_seal_feed` :76, `SealApplyVerdict` :36. Pump: `pump/mod.rs` seals path :276-301, SealWorker :639.

**Attempt bound (reuse verbatim):** `spt-store/src/engineroom.rs` — `GateLedger{failures:u32,last_failure_ms:u64}` :453 (+load/load_from/save/save_to :466-484, pretty-JSON 2 fields); `Presented::{Absent,Code{verified}}` :548; `GateOutcome::{Admitted,Refused{failures,backoff_ms,loud},Throttled{retry_in_ms},Uncounted}` :510; `classify_attempt(ledger,now_ms,presented)` :584 (throttle FIRST → Absent=Uncounted → verify); `apply(ledger,outcome,now_ms)` :612 (Admitted resets, Refused counts, Throttled/Uncounted no-op); `backoff_ms` :498 (1s<<n cap 1h); LOUD_AFTER_FAILURES=3 :55. Ledger paths: bring-up `perch.rs:280 engine_room_gate_file()` = trust/engine-room-gate.json; empower SEPARATE `api/engineroom.rs:704 EMPOWER_LEDGER_FILE="engine-room-empower-gate.json"` + rationale :324-330 (denial-of-governance). W2 adds `seal_ceremony_gate_file()` beside :280 → trust/seal-ceremony-gate.json.

**TOTP verify:** `spt-net/src/net/pairing/totp.rs` — `code_matches_window(seed:&[u8;20],presented,now_secs)->bool` :183 (constant-time whole ±1 window, `|=`, trims); `ceremony_now_secs()` ntp.rs:102. Seeds: `subnet.rs` `seed_bytes` :185 / `admin_seed_bytes` :207 (only-read-path warning — consumers are local verifiers, never display). Both-seeds pattern: `broker.rs:4134 bringup_code_verifies` — member|admin each `is_some_and(code_matches_window)`, combined via `classify_cred` :4104 table (seal arm needs only bool: member|admin bitwise, no cred distinction). Loud alert: `broker.rs:4157 raise_engine_room_alert` (node-scoped notif, detached thread off log lock, stderr fallback) — mint a seal-flavored sibling.

**IPC verb precedent (bring_up):** `brain.rs:563 Brain::bring_up(endpoint,code)` — send KIND + loop read_event until typed reply. `msg.rs`: KIND_BRING_UP :301, `BringUpReq{endpoint,code:Option}` :1152 (serde default/skip_none), `BroughtUpReply{outcome:String,detail,session_id,admit}` — outcome = stable token not prose. W2 mints sibling: KIND_SEAL_CEREMONY + SealCeremonyReq{endpoint,subnet,content_b64}/Reply{outcome,detail,token:Option} (shape at impl). Codec: 4-byte BE + JSON Envelope, MAX_FRAME 16MiB codec.rs:19.

**Attach wire:** `spt-net/src/net/attach.rs` — `AttachRecord` serde tag="kind" snake_case :71: Request{session_id,from_seq,intent,endpoint_id,gen,code} :86 / Output :119 / Input{data_b64,op_id} :124 / Resize :128 / Size :132 / Exit :134 / Notice{text} :151 (the non-frame broker-authored news precedent + why-not-forged-Output rationale :138) / Displaced :155. `AttachDecoder=NdjsonDecoder<AttachRecord>` :168, `encode_line` :160. W2 adds SealCeremony/SealCeremonyCode/SealCeremonyResult variants + `#[serde(default)] seal_ceremony: bool` on Request AND msg.rs `SubscribeReq` :471 (already has code:Option precedent).

**Daemon serve/broker:** `spt-daemon/src/attach.rs` — `serve_attach` :444 (two-conn split :474: `brain` reads session events, `wire` forwards to operator stream); `handle_attach_request` :343; Input handler :613-642 (role-gated: Controller→send_effect_no_ack, None→pending_input, Viewer→drop); `send_attach_input` :875; Notice emit :670-673 (from Subscribed.note — seat-resolution-time only today). Broker seat truth: `broker.rs` OutputLog :833 — `controller:Option<ControllerSink>` :666 (has `by`, tx:SyncSender<CtrlMsg>), `has_controller()` :1428 (log-lock snapshot), `is_controller(sub)` :3409 (resize-exclusivity precedent); `CtrlMsg::{Output,Sync,Exit}` :3539 — W2 adds a Ceremony arm riding controller_writer :3638; `become_controller` :2382. NEVER read perch stamp for liveness: info.rs:510 (terminal_normalize lag warning), msg.rs:1218 (controller_by None ambiguous), KH 7.15. ER refusal carrier precedent: `engine_room_refusal` broker.rs:1461 → SeatGate::Refused(BusyControlled{by:reason},label) :1503; `resolve_subscribe_gated` :2742, gate-first :2866; dispatch_resize :7242 (controller-exclusive verb precedent). BusyControlled reason never reaches the client (FIN → PumpEnd) — ceremony refusals must ride the CEREMONY reply instead.

**rc client:** `rc.rs` — pump :2622 (40ms POLL_SLICE loop: stdin drain :2708 StdinMsg::{Bytes,Detach} → send_attach_input; read_event_until :2725; arms Output :2748 / Exit / Displaced / Notice :2788 / Size :2796 / `_=>{}` :2809). `fold_code_key` :1559 + `CodePromptStep` :1549 (shared kernel — reuse for overlay); `prompt_digits` :1643 (raw-mode, stderr, pre-attach — NOT the overlay path). Overlay byte-emit precedents: StatusRow :227 (DECSTBM reserve + DECSC/abs/DECRC repaint bytes :241-277, ReassertScanner re-assert after output :2768; STATUS_ROW_ENABLED=false :208 — machinery intact); reconnect_banner_bytes :2512 (centered full-paint). REQ-RC-DISPLAY-SOLE-WRITER :2076 — never stderr while attached; obstap breadcrumb for observability. Stdin reader is a separate thread (events reader on Windows :1454, bytes :1420) — ceremony mode diverts BYTES at the pump, folding key events client-side; note bytes-mode stdin delivers raw bytes not KeyEvents → fold from bytes needs a byte-grammar arm (Esc=0x1b, Enter=\r/\n, digits) or Windows events path; handle both.

**No daemon→client request/response exists today** — the ceremony triple is the first; capability bit is what keeps N-1 clients out of it.

## Immediate next steps (post-clear resume point)

T1 spt-store: `seal_ceremony_gate_file()` (perch.rs, beside :280, [impl->REQ-SEAL-CEREMONY-TOTP]).
T2 wire: attach.rs 3 variants + Request.seal_ceremony; msg.rs SubscribeReq.seal_ceremony + KIND_SEAL_CEREMONY req/reply payloads; Brain::seal_ceremony method.
T3 daemon: broker dispatch arm (seat check per NO-CEREMONY-SURFACE title), ControllerSink.seal_ceremony capability, CtrlMsg ceremony push, code-arrival path (attach.rs serve → broker), verify fn (both-seeds bool), ledger+classify+apply, mint under SEAL_APPLY, result down + reply, abort-on-detach/conn-drop.
T4 rc: ceremony mode in pump (divert stdin, overlay paint top-half w/ verbatim content + subnet, per-frame re-paint, oversize refuse-arm, Esc/Enter via shared grammar), capability bit set on Request.
T5 units per the five REQ Gate clauses + CONTEXT.md doc tags at the ceremony clauses (check whether W1 tagged CONTEXT.md and mirror); clippy --workspace; traceable-reqs check; captures .raw+.exit per leg. Then report to doyle: conditions named separately, capture-first.
