# Relay Plans — Cross-Plan Reconciliation TODO

The 6 relay plans + `PLAN_RELAY_INDEX.md` were generated and hardened in parallel
(32-agent workflow: draft → security+architecture review → fix → re-review → global
consistency). Each plan passed its own review (4–8 blocking findings fixed each). The
**global consistency review then found 11 CRITICAL/HIGH cross-plan integration issues** —
all one root cause: **plans drifted their SHARED contracts locally during hardening
instead of routing changes through the frozen INDEX §4** (violating INDEX §2 rule 1).

These must be reconciled BEFORE any plan's implementation is dispatched, or the plans
will not compose (they'd type-mismatch / fail at integration). The fix is a
**reconciliation pass**: freeze the drifted/missing contracts at the INDEX coordination
point, then sync the consumer plans. Below, the 11 findings collapse into 6 concrete fixes.

---

## FIX 1 — `relay-contracts/` (W0) has no owning task  *(GLOBAL CRITICAL)*
Every plan imports `relay-contracts/` read-only and INDEX says it's "frozen before any
plan's W1", but **no plan contains a task to AUTHOR it** (Zod schemas + TS types + the
mux binary/CBOR codec). It's also where several still-unfrozen shapes must land.
- **Do:** add an INDEX-owned **W0 task** that builds `relay-contracts/`: §4 Zod schemas +
  inferred types, `encodeMuxFrame`/`decodeMuxFrame` (+ CBOR), `GoAwayReason`/`decodeGoAwayReason`,
  the amended §4.4 shapes (Fix 2), account/session/pairing TS types, INV8 version-table DDL.
  Freeze before dispatching any W1.

## FIX 2 — §4.4 E2E contract drift (single `sessionKey: CryptoKey` → evolved shape)  *(E2E HIGH, GLOBAL HIGH, FRONTEND HIGH)*
P4 (E2E) evolved to: `AeadKey` (not `CryptoKey`), **synchronous** `sealFrame`/`openFrame`,
`aad = directionLabel‖seq`, **direction-split `DirectionalKeys` (`c2h`/`h2c`)** wrapped in a
stateful `E2ESession` (`.seal()`/`.open()`/`.rederive()`), handshake via
`buildClientHandshake(ctx, hostId, aeadOffer)`. But INDEX §4.4, **P2 T15 `makeHostHello`
(returns `sessionKey: CryptoKey`)**, and **P6 T8 (raw SubtleCrypto AES-GCM, single key)**
still use the ORIGINAL shape → they can't type-check against what P4 exports.
- **Do:** land P4 §9 Q4's amendments into **INDEX §4.4 / relay-contracts** first
  (`AeadKey`, sync sigs, `DirectionalKeys` c2h/h2c, `aad=directionLabel‖seq`, `ReplayKeyParams`).
  Then update **P2 T15** → `HandshakeResult` carrying `DirectionalKeys`; **P6 T8/T9** →
  consume `E2ESession`/`buildClientHandshake`/`AeadKey`. Closes P6 §8 Q4 by construction.

## FIX 3 — recoverable content-key (`K_content`/`hostContentSecret`) has no wiring or delivery  *(E2E HIGH, GLOBAL HIGH)*
The LOCKED "ciphertext ring-buffer survives reconnect" decision (EXPLORE §0 dec.2 / §4c)
needs a **recoverable** key: live frames use the ephemeral `h2c` key, but replay/preview
frames must use `K_content` derived from a host-scoped `hostContentSecret`. Problems:
(a) P2 never calls `sealReplayFrame`; P6 T9 never calls `openReplayCiphertext` → T10/T11
ship as unconsumed library code; (b) `hostContentSecret` has **no delivery** — P2 T4 and
P3 T8 `EnrollResult` return only `{hostId, subdomain, cert, caChain}`.
- **Do:** add `hostContentSecret` (wrapped to the agent's identity) to the **§4.5 enrollment
  contract + `EnrollResult`**; assign minting/wrapping to **P3 or P5 at BIND**, receipt/keystore
  to **P2**. Add consumer tasks: **P2** seals every replay-bound host→client output with
  `sealReplayFrame`; **P6 T9** decrypts via `openReplayCiphertext`/`deriveContentKey`. Extend
  T10/T11's merge gate to include this wiring.

## FIX 4 — INV12 revocation teardown has 3 incompatible descriptions + no P1 subscriber  *(AUTH HIGH, GLOBAL CRITICAL)*
P5 T10 publishes `KillSignal` on Redis pub/sub `relay:revocations` and asserts "P1
subscribes and injects §4.1 RST/GOAWAY". P3 T13 pushes over an unowned "control→node
channel (OQ4)". P1 T10/T11 imply P5 calls it directly. **`relay:revocations` + `KillSignal`
are NOT in INDEX §4.2** (only `route:{host_id}` / `revoked:{jti}` are), and **P1 has no
subscriber task**; `KillSignal` is defined P5-locally (would force P1→P5 import, against DAG).
- **Do:** freeze ONE control→data-plane teardown contract in **INDEX §4** — `relay:revocations`
  pub/sub + `KillSignal` shape, promoted into `relay-contracts`. Assign **P1** an explicit task
  that subscribes and turns `KillSignal` → §4.1 CLOSE+RST/GOAWAY via `closeStream`/`closeTunnel`.
  Reconcile **P3 OQ4** drain channel to the same named channel.

## FIX 5 — capability-token WS subprotocol wire format diverges  *(FRONTEND HIGH)*
P6 T4: `new WebSocket(url, ["relay.capability.v1", token])` (raw token). P1
`extractCapabilityToken`: `['term.relay.v1', 'term.token.' + base64url(token)]` (prefixed +
encoded, relay strips before verify; must echo only the app subprotocol). Neither is in
INDEX §4.3 → invented independently and diverged → real upgrade 401s while isolated tests pass.
- **Do:** freeze the exact app-subprotocol string + `term.token.` prefix + base64url encoding +
  "accepted subprotocol must be the app one, never the token" rule in **INDEX §4.3**. Update
  **P6 T4/T8** verbatim + assert `ws.protocol === 'term.relay.v1'` after open (tear down otherwise).

## FIX 6 — two more single-owner violations  *(GLOBAL HIGH ×2)*
- **6a. WS-upgrade auth implemented twice.** P1 T8 `authorizeUpgrade` re-implements the
  decision (Origin+token+cross-tenant+rights) while P5 T12 `onUpgrade` is the real gate
  (adds DPoP PoP, single-use burn, pre-auth throttle, step-up, per-tenant rate, audit).
  → Make **P1 T8 a thin adapter that DELEGATES to P5 `onUpgrade`/`onReattach`**; add DPoP +
  `activeSessionCount` to P1's `UpgradeRequest`. Authorization DECISION has exactly one owner (P5).
- **6b. `deviceAuthProof` verifier has 3 signatures + ambiguous home.** P5 T2 owns
  `verifyDeviceAuthProof(proof, expectedAccountId, transcriptHash, now)`; P4 T8 consumes an
  injected `verifyDeviceProof(proof, {clientEphPub, clientNonce})`; P2 T15 imports it from
  `relay-e2e` (wrong package). → **P5 owns** issuance+verification (binding to
  `{clientEphPub, clientNonce}` per P4 Q3); **P4 consumes as injected dep**; **P2 gets it via
  P4's host-handshake wiring** (fix the import). Unify the binding param across P2/P4/P5.

---

## Reconciliation pass shape (recommended)
1. **Amend `PLAN_RELAY_INDEX.md`** (single agent) — land Fixes 1,2,3,4,5,6 contract decisions
   into §4 (+ the W0 relay-contracts task). This is the coordination point; do it first.
2. **Sync consumer plans** (parallel, file-disjoint): P1 TRANSPORT (Fix 4 subscriber, 6a
   delegate, 5 confirm), P2 AGENT (Fix 2 DirectionalKeys, 3 seal+receive secret, 6b import),
   P3 CONTROLPLANE (Fix 3 EnrollResult, 4 OQ4 reconcile), P6 FRONTEND (Fix 2 T8/T9, 3 T9, 5 T4/T8).
   P4/P5 mostly already correct — minor confirmations.
3. **Re-run the global consistency review** to confirm zero residual contract drift.

Effort: ~7 agents (1 index + ~5 consumer syncs + 1 re-review) — far smaller than the
generation workflow. Do it as its own step, ideally in a fresh conversation (read this file
+ the 7 plan docs).
