Files
web-terminal/docs/PLAN_RELAY_RECONCILE.md
Yaojia Wang 2af57e6686 feat(relay): rendezvous-relay service — 7 packages + plans (contracts/transport/agent/control-plane/e2e/auth/web)
Multi-tenant reverse-tunnel service ("ngrok for Claude Code" with E2E): a
host-agent dials OUT to an operator-run relay; external devices reach the host
THROUGH the relay, routed by per-tenant subdomain, forwarding ciphertext only
(the relay never sees plaintext). Lets a customer reach their own self-hosted
web-terminal from anywhere with zero networking setup.

Packages — all tsc-strict + vitest green (656 tests), cross-package integration verified:
- relay-contracts: frozen shared contracts (mux frame codec, data model,
  capability token, E2E envelope, pairing) — the src/types.ts analog
- term-relay:   native WS mux + stateless data plane (subdomain routing, ciphertext forward)
- agent:        host-agent (pairing, per-host Ed25519 + mTLS dial-out, forwards to 127.0.0.1:3000)
- control-plane: accounts/hosts registry, pairing-code flow, routing table, provisioning
- relay-e2e:    browser<->agent E2E (X25519 ECDH through relay, AEAD, anti-replay, recoverable replay key)
- relay-auth:   Passkey/WebAuthn, capability tokens, per-host certs, deny-by-default tenant isolation
- relay-web:    browser login + Web Crypto E2E + client-side preview rendering

Security invariants INV1-15 enforced; cross-tenant isolation CI tripwire live
(.github/workflows/relay-tripwire.yml). Design + implementation-level plans in
docs/PLAN_RELAY_*.md and docs/EXPLORE_RELAY_SERVICE.md.

NOTE: generated autonomously per the reviewed plans. The security-critical
packages (relay-e2e, relay-auth) REQUIRE expert security audit before any real
deployment — passing tests prove self-consistency, not resistance to attackers.
Base app (src/, public/) unchanged; concurrent desktop work left uncommitted.
2026-07-02 06:10:16 +02:00

7.1 KiB
Raw Blame History

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 (48 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 T15HandshakeResult 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 §4relay: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.

  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).