Files
web-terminal/docs/PLAN_RELAY_RUN_PHASE0.md
Yaojia Wang 3d17bed322 docs(relay): Phase 0 runnable-relay build spec (PLAN_RELAY_RUN_PHASE0.md)
File-level plan for a single-host `npm start` relay: new relay-run/ package wiring
the real P4/P5 + in-memory control-plane + self-signed TLS + agent + relay-web.
Seam files to read, 9-step build order, verify steps. Executable spec for a fresh
build session; reuses the e2e/harness wiring.
2026-07-04 13:49:09 +02:00

53 lines
5.6 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Phase 0 — single-host runnable relay (build spec)
> Goal: a single `npm start` that boots an **end-to-end relay you can click through locally** —
> real P4(crypto)+P5(auth) wiring, in-memory stores, self-signed TLS, agent dialing `localhost`,
> `relay-web` served. No Postgres/Redis/KMS. This is the executable spec for the Phase 0 milestone
> in [DEPLOY_RELAY.md](./DEPLOY_RELAY.md) §4. Reuses the wiring proven in `e2e/harness/`.
>
> **Run this build in a FRESH session** (recommend `/compact` first). Read the seam files named below
> before writing — the exact interface shapes must be confirmed against the code, not assumed.
## Deliverable
A new package `relay-run/` (top-level, symlink node_modules like `e2e/`) with one entry
`relay-run/src/main.ts` that `npm start` runs, plus small wiring modules. Result: browser at
`https://localhost:8443/` → authenticates → live terminal to a shell on the same machine, spliced
through the real relay-node, E2E-encrypted (relay sees only ciphertext).
## Seam files to read FIRST (confirm exact shapes)
- `term-relay/data-plane/ws-like.ts` — the `WebSocketLike` interface the relay-node consumes.
- `term-relay/data-plane/agent-listener.ts``AgentListener`/`AgentTunnel` shapes (how an agent mux attaches).
- `term-relay/data-plane/relay-node.ts` + `upgrade.ts` + `authz-port.ts``createRelayNode`, `authorizeUpgrade`, the `Authorizer` port (`onUpgrade`/`onReattach`).
- `term-relay/mux/mux-session.ts``createMuxSession`, `MuxStreamHandle` (frame the agent side).
- `control-plane/src/main.ts``buildControlPlane`/overrides (`stores`, `verifier`, `bus`, `kmsResolver`), `createMemoryStores`, `inProcessCaSigner`.
- `agent/src/cli.ts` + `agent/src/e2e/hostEndpoint.ts` + `transport/dial.ts` — how the agent dials + runs the E2E host endpoint + loopback splice.
- `e2e/harness/world.ts` — the REFERENCE wiring: it already composes real P5 `onUpgrade` + P4 handshake + fakes. Lift the `EnforceDeps`/registry/bucket/audit construction from here (and `e2e/harness/fakes.ts`).
## Build steps (order)
1. **Scaffold `relay-run/`**: `package.json` (type module, `"start":"tsx src/main.ts"`, dep on `tsx`, `ws`), `tsconfig.json`, and `node_modules` symlinks to all relay packages + `ws`/`tsx` (mirror `e2e/`s manual-symlink approach — avoids `npm install`). Add `relay-run/node_modules` to `.gitignore` coverage (root already ignores `node_modules/`).
2. **Self-signed TLS**: generate a localhost cert at boot (node `selfsigned` or a checked-in dev cert) for the browser WSS listener; a throwaway CA for agent mTLS.
3. **Control-plane (in-memory)**: boot `buildControlPlane` with `overrides = { stores: createMemoryStores(), verifier: <real P5 verifyCapabilityToken bound to the dev P5 pubkey>, bus: in-memory, kmsResolver: inProcessCaSigner }`. Listen on `:8080`. Seed one account + issue a pairing code programmatically at boot.
4. **P5 EnforceDeps**: build the real `EnforceDeps` (hosts/sessions/revocation/buckets/audit + `stepUpPolicyFor: () => NO_STEPUP_POLICY`) backed by the in-memory control-plane registries — copy the construction from `e2e/harness`. Configure the P5 verify key (`configureVerifyKey`).
5. **`WebSocketLike` adapter**: wrap `ws` `WebSocket` → the `WebSocketLike` interface (send/close/onmessage/onclose per ws-like.ts). Browser WSS server on `:8443`; on upgrade, build the `UpgradeRequest` (extract token from subprotocol/cookie, Origin, DPoP header) and call `authorizeUpgrade` (→ real `onUpgrade`). **Close F2 here**: pass a resolved principal when the host policy requires step-up (Phase 0 default policy = not-required, so `null` is fine).
6. **`AgentListener` adapter**: mTLS `tls.Server` on `:8444`; verify the agent client cert against the dev CA (`verifyAgentCert`), attach each as an `AgentTunnel` + `createMuxSession`. Wire `createRelayNode({ config, agentListener, authorizer })` and the opaque splice.
7. **Agent**: build the CLI (`dist/cli.js`) or run `agent` in-process; enroll against control-plane `POST /enroll` with the seeded pairing code (→ SPIFFE cert + `hostContentSecret` + subdomain); dial `wss://localhost:8444` (mTLS) and run the E2E host endpoint splicing to a local shell (`LOCAL_TARGET_URL` / node-pty).
8. **relay-web**: `npm --prefix relay-web run build`; serve the bundle from the browser listener (same origin as WSS so Origin/CSWSH passes). Wire the login → P5 capability-token mint (Phase 0: a dev endpoint that mints a token after a stub human-auth, or reuse `issueCapabilityToken` directly).
9. **`main.ts`**: bring up 3→5→6→7→8 in order; print the URL + a ready-to-use token/subdomain.
## Verify
- `npm start` in `relay-run/` boots without error; console prints the local URL + subdomain.
- Browser: open `https://localhost:8443/?...`, authenticate, see a live shell; type `whoami` → output.
- Kill/restart the agent → session survives per design (reconnect); `revoke()` → stream tears down.
- Add a smoke test (Vitest) that boots the world in-process and round-trips one command E2E through
the real relay-node (extend the `e2e/harness` pattern with the real `WebSocketLike` adapter).
## Explicitly NOT in Phase 0
Postgres/Redis (in-memory only), real KMS (dev in-process signer), wildcard TLS/multi-tenant subdomain
routing, horizontal scaling, F6 replay-epoch transport (preview stays inert), metering. Those are
Phase 1/2 in DEPLOY_RELAY.md.
## Security notes (keep even in dev)
- Real Origin/CSWSH check on the WSS upgrade (dont stub it off for localhost — set allowedOrigins to
the dev origin). Real capability-token verify + DPoP (no bypass). Self-signed certs are dev-only;
never reuse the dev CA anywhere real. hostContentSecret 0600, never logged.