Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -205,7 +205,7 @@ When a launch resolves `headroom` on, the launcher (synchronous end to end, righ

The front door, not the headroom supervisor, hosts the second server: the CONNECT surface OAuth launches point `HTTPS_PROXY` at. It terminates TLS for the intercept hosts, each with its own leaf signed by a local CA persisted under `<home>/frontdoor/ca/` (the key mode 0600): `api.anthropic.com`, whose `/v1/` paths run through the same ordered pipeline the provider listener serves, and `platform.claude.com`, the claude.ai control plane, which is never HTTP-parsed and is pumped byte for byte; every path on the API host the pipeline does not serve, and every host outside the intercept set, is piped untouched. The transparent surface, a second listener for traffic that ignores proxies, terminates redirected connections the same way, keyed on the TLS SNI name the client presents. None of that happens for a client that has not authenticated: the CONNECT request must carry a live launch's capability as its `Proxy-Authorization` credential (the launcher puts it in the `HTTPS_PROXY` URL, from which clients derive that header), checked against the session registry with the same constant-time comparison the pipeline's admission uses, or it is answered 407 before the target is parsed or dialled. A connection ledger inside the surface enforces `ConnectLimits`: a head deadline, a pending-head cap that evicts the oldest waiting connection, a cap on authenticated connections, and a periodic revalidation that closes every connection whose launch's capability has since been pruned. Authenticated tunnels deliberately have no idle timeout, since Remote Control streams may sit silent indefinitely. That shape is what keeps Claude Code's Remote Control and connectors working under compression: they refuse any `ANTHROPIC_BASE_URL` the child can be given, but honour the proxy layer, so the base URL stays pointing at the real API. All of the surface's effects (TCP listening, TLS termination, HTTP parsing, forwarding) sit behind a `ConnectEffects` port in the same style as `SupervisorPorts`, and its routing decisions are pure functions over strings; the TLS round-trip tests run the real server against certificates this module issues with only the upstream target redirected.

The same CA signs the front door's provider listener, which serves HTTPS with a leaf for `127.0.0.1` and `localhost`. Provider sessions send their credential and launch capability on every request, so the child must be able to tell the door from anything else holding the port; a TLS leaf whose CA key only this user can read is that proof, and every routed child trusts the CA through `NODE_EXTRA_CA_CERTS` (combined with any bundle the parent named, since Node reads only one file). The launcher checks the same thing before trusting a door it finds: `ensureFrontDoor` probes the listener's health endpoint over TLS against the CA, from a worker thread that the synchronous launcher waits on with `Atomics.wait`, and a listener that keeps failing is distrusted and replaced, or the launch refused, before any capability is written. This tool's own supervisor that misses its probes is retired first, so the replacement keeps the sticky ports, but only when no live session is registered against it: a miss is what a busy door looks like, so a door with live sessions is waited on until the launch's own deadline and reported, never signalled, and `frontdoor restart` is the deliberate way to replace it: an explicit replace retires the door even when it is not answering, asking it to exit and killing it if it has not exited after a grace period, because a door whose event loop is blocked never runs its signal handler. A launch planned inside the serving door itself (the usage refresh, the window opener, a job) registers its session and returns the door's own addresses without probing, because the thread that would wait on the probe is the one event loop that has to answer it. The door enters its process role (`src/processRole.ts`) before it binds anything, and the primitives that block the calling thread (the synchronous sleep and the synchronous probe) refuse to run while a role is held, so a path that would stall the door throws `BlockingInEventLoopError` at its first wait instead of freezing every session. The door also carries a watchdog on a thread of its own (`frontdoor/watchdog.ts`): the supervisor beats a shared counter every tick and the thread, whose event loop a blocked main loop cannot stall, ends the process when the counter has not moved for twice the longest a launcher waits on a door before calling it unresponsive, after recording the stall in the state file's `lastError`. It is started once the listeners are bound and stopped before any retirement begins, and it restarts its clock when it was itself descheduled for longer than its cadence, so a suspended or starved machine is not taken for a hung door. The next launch starts a fresh door on the same sticky ports with the session registry intact. `doctor` and `frontdoor status` ask the listeners themselves whether they accept (`frontdoor/listenerReadings.ts`): a TLS handshake against the CA on the provider listener and a plain TCP accept on the CONNECT surface, each timed, because a live supervisor pid cannot tell a door that serves from one whose event loop is blocked, and the two listeners carry different sessions (provider sessions on one, every plain OAuth session on the other). The direct listener headroom forwards back to stays plain HTTP, because headroom is what connects to it; what keeps that leg harmless is that no credential travels on it. The hop parks the provider's credential headers in an in-memory custody record and sends headroom placeholders and a hop id, and the direct listener's admission step redeems a live id (for the same provider, with the generation's hop secret) for the real headers before routing. Admission is a pipeline stage in its own right, returning the headers a request is routed with, so restoring credentials is admission's job and no route knows about it.
The same CA signs the front door's provider listener, which serves HTTPS with a leaf for `127.0.0.1` and `localhost`. Provider sessions send their credential and launch capability on every request, so the child must be able to tell the door from anything else holding the port; a TLS leaf whose CA key only this user can read is that proof, and every routed child trusts the CA through `NODE_EXTRA_CA_CERTS` (combined with any bundle the parent named, since Node reads only one file). The launcher checks the same thing before trusting a door it finds: `ensureFrontDoor` probes the listener's health endpoint over TLS against the CA, from a worker thread that the synchronous launcher waits on with `Atomics.wait`, and a listener that keeps failing is distrusted and replaced, or the launch refused, before any capability is written. This tool's own supervisor that misses its probes is retired first, so the replacement keeps the sticky ports, but only when no live session is registered against it: a miss is what a busy door looks like, so a door with live sessions is waited on until the launch's own deadline and reported, never signalled, and `frontdoor restart` is the deliberate way to replace it: an explicit replace retires the door even when it is not answering, asking it to exit and killing it if it has not exited after a grace period, because a door whose event loop is blocked never runs its signal handler. A launch planned inside the serving door itself (the usage refresh, the window opener, a job) registers its session and returns the door's own addresses without probing, because the thread that would wait on the probe is the one event loop that has to answer it. The door enters its process role (`src/processRole.ts`) before it binds anything, and the primitives that block the calling thread (the synchronous sleep and the synchronous probe) refuse to run while a role is held, so a path that would stall the door throws `BlockingInEventLoopError` at its first wait instead of freezing every session. The door also carries a watchdog on a thread of its own (`frontdoor/watchdog.ts`): the supervisor beats a shared counter every tick and the thread, whose event loop a blocked main loop cannot stall, ends the process when the counter has not moved for twice the longest a launcher waits on a door before calling it unresponsive, after recording the stall in the state file's `lastError`. It is started once the listeners are bound and stopped before any retirement begins, and it restarts its clock when it was itself descheduled for longer than its cadence, so a suspended or starved machine is not taken for a hung door. A stall always leaves a replacement behind, started while the old pid is still dying so it can bind the same sticky ports the moment they free (the session registry is on disk either way); the chain stops after a bounded run of consecutive replacements, because a stall that has recurred through that many doors in a row is the machine's, and the next launch's ensure is what starts the door after that. `doctor` and `frontdoor status` ask the listeners themselves whether they accept (`frontdoor/listenerReadings.ts`): a TLS handshake against the CA on the provider listener and a plain TCP accept on the CONNECT surface, each timed, because a live supervisor pid cannot tell a door that serves from one whose event loop is blocked, and the two listeners carry different sessions (provider sessions on one, every plain OAuth session on the other). The direct listener headroom forwards back to stays plain HTTP, because headroom is what connects to it; what keeps that leg harmless is that no credential travels on it. The hop parks the provider's credential headers in an in-memory custody record and sends headroom placeholders and a hop id, and the direct listener's admission step redeems a live id (for the same provider, with the generation's hop secret) for the real headers before routing. Admission is a pipeline stage in its own right, returning the headers a request is routed with, so restoring credentials is admission's job and no route knows about it.

The front door records the wire protocol it speaks (`FRONT_DOOR_PROTOCOL` in `src/frontdoor/state.ts`) in its state once it is serving. A launcher that finds a live door whose protocol is lower than its own (a door from before the field counts as 0) stops it, after its listener has authenticated as this tool's own, and spawns a replacement that rebinds the same sticky ports. That is safe because the capability registry is on disk and sessions already running keep reaching the addresses they froze at launch. A door of the same or a newer protocol is always joined, so an older launcher never downgrades a newer door. The rule for a bump: a door accepts every lower protocol's launches (protocol 1 accepts the former `x-claude-use-*` header names).

Expand Down
Loading
Loading