agent-shim 7.13.1 → 7.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (93) hide show
  1. package/dist/cli.cjs +95 -27
  2. package/dist/index.cjs +70 -7
  3. package/dist/index.mjs +70 -7
  4. package/dist/types/index.d.ts +2789 -40
  5. package/package.json +3 -1
  6. package/dist/types/checkReport.d.ts +0 -154
  7. package/dist/types/claudeShim.d.ts +0 -114
  8. package/dist/types/cli/bool.d.ts +0 -8
  9. package/dist/types/cli/commandDeps.d.ts +0 -39
  10. package/dist/types/cli/envBool.d.ts +0 -11
  11. package/dist/types/cli/pairs.d.ts +0 -19
  12. package/dist/types/cliError.d.ts +0 -27
  13. package/dist/types/codex/http.d.ts +0 -15
  14. package/dist/types/config/classify.d.ts +0 -49
  15. package/dist/types/config/load.d.ts +0 -42
  16. package/dist/types/config/schema.d.ts +0 -746
  17. package/dist/types/config/store.d.ts +0 -46
  18. package/dist/types/configProfilesStore.d.ts +0 -61
  19. package/dist/types/configure.d.ts +0 -170
  20. package/dist/types/credential.d.ts +0 -136
  21. package/dist/types/credentialCache.d.ts +0 -46
  22. package/dist/types/directoryRulesStore.d.ts +0 -43
  23. package/dist/types/doctorReport.d.ts +0 -117
  24. package/dist/types/frontdoor/capability.d.ts +0 -19
  25. package/dist/types/frontdoor/capture.d.ts +0 -58
  26. package/dist/types/frontdoor/connect.d.ts +0 -209
  27. package/dist/types/frontdoor/connectEffects.d.ts +0 -3
  28. package/dist/types/frontdoor/custody.d.ts +0 -32
  29. package/dist/types/frontdoor/ensure.d.ts +0 -36
  30. package/dist/types/frontdoor/headroomHop.d.ts +0 -22
  31. package/dist/types/frontdoor/pipeline.d.ts +0 -109
  32. package/dist/types/frontdoor/probe.d.ts +0 -13
  33. package/dist/types/frontdoor/realFrontDoorPort.d.ts +0 -5
  34. package/dist/types/frontdoor/route.d.ts +0 -112
  35. package/dist/types/frontdoor/server.d.ts +0 -44
  36. package/dist/types/frontdoor/state.d.ts +0 -57
  37. package/dist/types/frontdoor/supervisor.d.ts +0 -48
  38. package/dist/types/frontdoor/tap.d.ts +0 -9
  39. package/dist/types/frontdoor/trust.d.ts +0 -30
  40. package/dist/types/frontdoor/x509.d.ts +0 -11
  41. package/dist/types/headroom/ensure.d.ts +0 -31
  42. package/dist/types/headroom/headers.d.ts +0 -11
  43. package/dist/types/headroom/settings.d.ts +0 -13
  44. package/dist/types/headroom/source.d.ts +0 -8
  45. package/dist/types/headroom/state.d.ts +0 -77
  46. package/dist/types/headroom/supervisor.d.ts +0 -107
  47. package/dist/types/identityStore.d.ts +0 -85
  48. package/dist/types/launchWiring.d.ts +0 -49
  49. package/dist/types/launcher/argv.d.ts +0 -42
  50. package/dist/types/launcher/cascade.d.ts +0 -42
  51. package/dist/types/launcher/cliOverride.d.ts +0 -38
  52. package/dist/types/launcher/extraFlags.d.ts +0 -6
  53. package/dist/types/launcher/farm.d.ts +0 -168
  54. package/dist/types/launcher/flags.d.ts +0 -96
  55. package/dist/types/launcher/guard.d.ts +0 -57
  56. package/dist/types/launcher/identity.d.ts +0 -95
  57. package/dist/types/launcher/lock.d.ts +0 -41
  58. package/dist/types/launcher/pool.d.ts +0 -44
  59. package/dist/types/launcher/ports.d.ts +0 -135
  60. package/dist/types/launcher/spawn.d.ts +0 -20
  61. package/dist/types/launcher.d.ts +0 -98
  62. package/dist/types/legacy.d.ts +0 -29
  63. package/dist/types/pathNorm.d.ts +0 -14
  64. package/dist/types/paths.d.ts +0 -79
  65. package/dist/types/poolPickReport.d.ts +0 -44
  66. package/dist/types/poolStore.d.ts +0 -18
  67. package/dist/types/providersStore.d.ts +0 -137
  68. package/dist/types/realCredentialCache.d.ts +0 -11
  69. package/dist/types/realPorts.d.ts +0 -109
  70. package/dist/types/resolve/conditions.d.ts +0 -37
  71. package/dist/types/resolve/decide.d.ts +0 -45
  72. package/dist/types/resolve/extends.d.ts +0 -41
  73. package/dist/types/resolve/flatten.d.ts +0 -13
  74. package/dist/types/resolve/match.d.ts +0 -58
  75. package/dist/types/resolve/pipeline.d.ts +0 -33
  76. package/dist/types/resolve/plan.d.ts +0 -57
  77. package/dist/types/resolve/projects.d.ts +0 -61
  78. package/dist/types/resolve/reconcile.d.ts +0 -80
  79. package/dist/types/resolve/types.d.ts +0 -159
  80. package/dist/types/resolve/walk.d.ts +0 -85
  81. package/dist/types/resolve/walkPolicy.d.ts +0 -15
  82. package/dist/types/usage/account.d.ts +0 -20
  83. package/dist/types/usage/middleware.d.ts +0 -17
  84. package/dist/types/usage/pick.d.ts +0 -62
  85. package/dist/types/usage/plan.d.ts +0 -18
  86. package/dist/types/usage/poolPick.d.ts +0 -31
  87. package/dist/types/usage/preflight.d.ts +0 -20
  88. package/dist/types/usage/rateLimit.d.ts +0 -25
  89. package/dist/types/usage/read.d.ts +0 -70
  90. package/dist/types/usage/scan.d.ts +0 -24
  91. package/dist/types/usage/schema.d.ts +0 -318
  92. package/dist/types/usage/store.d.ts +0 -43
  93. package/dist/types/versionDiscovery.d.ts +0 -47
@@ -1,19 +0,0 @@
1
- /**
2
- * How a launch's capability token is presented and checked. The token reaches the child twice: as the `x-agent-shim-auth` header on every request it sends (see `AUTH_HEADER`), and as the password in the `HTTPS_PROXY` URL an OAuth launch gets, which every proxy-aware client turns into `Proxy-Authorization: Basic ...` on its CONNECT request. Both are checked against the same live registry with the same constant-time comparison.
3
- */
4
- /** The user name in the proxy URL. A label only: the surface checks the password, which is the capability, and ignores the user name, so nothing depends on how a client spells it. */
5
- export declare const CONNECT_PROXY_USER = "agent-shim";
6
- /** The realm the CONNECT surface's 407 challenge names, so a client that waits for a challenge before sending credentials knows which ones to send. */
7
- export declare const CONNECT_PROXY_REALM = "agent-shim front door";
8
- /**
9
- * The `HTTPS_PROXY` value for a launch: the CONNECT surface's loopback address with the capability as the URL's password. Both userinfo halves are percent-encoded, because a client percent-decodes userinfo before it builds the `Proxy-Authorization` value, so the surface receives exactly the token whatever characters it holds.
10
- */
11
- export declare function connectProxyUrl(port: number, token: string): string;
12
- /**
13
- * The capability a `Proxy-Authorization` header value presents: the password half of a Basic credential (everything after the first colon of the decoded user-pass, since RFC 7617 forbids a colon in the user name and permits one in the password). Undefined for any other scheme, a credential with no colon, or an empty password.
14
- */
15
- export declare function capabilityFromProxyAuthorization(value: string): string | undefined;
16
- /**
17
- * Whether `presented` is one of the live capabilities, compared in constant time: every live token is compared against the presented one in full, with no early exit on a match or a mismatching prefix, so the time a check takes reveals neither which token matched nor how much of a guess was right.
18
- */
19
- export declare function isLiveCapability(presented: string, live: Readonly<Iterable<string>>): boolean;
@@ -1,58 +0,0 @@
1
- import type { IncomingHttpHeaders } from "node:http";
2
- /**
3
- * How much of any one chunk is logged. A chunk excerpt only has to identify what the traffic is and carry the first frames of a stream, and 8 KiB is already the head budget this surface applies elsewhere (`MAX_CONNECT_HEAD_BYTES`), so one budget serves both notions of "a diagnostic excerpt".
4
- */
5
- export declare const CHUNK_LOG_CAP_BYTES = 8192;
6
- /**
7
- * How many body bytes each direction of one request may log in total. Enough to hold a full Remote Control handshake and a long run of its frames, while bounding the file against an unbounded SSE stream that could otherwise grow it without limit. Bytes past the cap are counted, never stored.
8
- */
9
- export declare const STREAM_LOG_CAP_BYTES = 262144;
10
- /** The request facts a capture sees, shaped so both a real `IncomingMessage` and a test's plain object satisfy it. */
11
- interface CapturedRequestShape {
12
- readonly method?: string;
13
- readonly url?: string;
14
- readonly headers: IncomingHttpHeaders;
15
- }
16
- /** The callbacks one piped request's forwarding drives, all optional callers except, so a fake or a partial observer fits the same seam. */
17
- export interface PassthroughObserver {
18
- readonly onRequestChunk: (chunk: Buffer) => void;
19
- readonly onResponse: (status: number, headers: IncomingHttpHeaders) => void;
20
- readonly onResponseChunk: (chunk: Buffer) => void;
21
- readonly onEnd: () => void;
22
- }
23
- /** The direction one tapped byte chunk flowed, named from the session's point of view. */
24
- type StreamDirection = "client-to-server" | "server-to-client";
25
- /** The byte-level tap for one terminated stream whose protocol the surface does not parse: every chunk both directions, then the end. */
26
- export interface StreamTap {
27
- readonly onChunk: (direction: StreamDirection, chunk: Readonly<Buffer>) => void;
28
- readonly onEnd: () => void;
29
- }
30
- /** The tap the connect surface drives. `undefined` everywhere means "not capturing"; one object covers the whole door process. */
31
- export interface ConnectCapture {
32
- /** Opens the byte-level tap for one host's terminated stream, whose chunks are recorded verbatim (text, or base64 when not valid UTF-8) under the same caps as piped bodies. Optional because a capture can be stream-silent by construction. */
33
- readonly tapStream?: (host: string) => StreamTap;
34
- /** Records one authenticated CONNECT target: the host and port only, since a blind tunnel's bytes are unreadable by construction. */
35
- readonly connect: (target: {
36
- readonly host: string;
37
- readonly port: number;
38
- }, intercepted: boolean) => void;
39
- /** Records one non-routed request's head and returns the observer its forwarding drives, so the capture correlates every later chunk with the request it belongs to. */
40
- readonly observePassthrough: (request: CapturedRequestShape) => PassthroughObserver;
41
- /** Records one HTTP upgrade request arriving on a terminated session. The surface destroys the socket afterwards either way, with or without a capture, matching Node's own no-listener behaviour exactly. */
42
- readonly upgrade: (request: CapturedRequestShape) => void;
43
- }
44
- /** Replaces every header value whose name looks like a credential with a marker, keeping every other header verbatim. Returns a fresh object. */
45
- export declare function redactHeaders(headers: Readonly<IncomingHttpHeaders>): IncomingHttpHeaders;
46
- /**
47
- * Redacts one body excerpt for the capture file. A body that parses as JSON is redacted structurally (key-named credentials go even when their shape matches no pattern); anything else gets the substring patterns, which still catch the bearer and JWT forms an OAuth refresh or a Remote Control frame carries. Invalid UTF-8 decodes with replacement characters, which is fine: the excerpt exists to be read, not to be re-encoded.
48
- */
49
- export declare function redactBody(chunk: Readonly<Buffer>): string;
50
- /**
51
- * The capture the door runs with, or undefined when the environment did not ask for one. Anything but `1` means off, the same exact-value vocabulary the other `AGENT_SHIM_*` diagnostics use, so a stray value never silently records traffic.
52
- */
53
- export declare function captureFromEnv(env: NodeJS.ProcessEnv, logsDir: string): ConnectCapture | undefined;
54
- /**
55
- * Builds the file-backed capture: one JSON object per line, appended to `file` (conventionally `logs/frontdoor-capture.jsonl`), never read back by the door itself. The directory is created up front rather than per record, so a capture started against a fresh state root cannot race its own first append.
56
- */
57
- export declare function createFileCapture(file: string, now?: () => Date): ConnectCapture;
58
- export {};
@@ -1,209 +0,0 @@
1
- import type { IncomingHttpHeaders, IncomingMessage, ServerResponse } from "node:http";
2
- import type * as net from "node:net";
3
- import type { Duplex } from "node:stream";
4
- import type { ConnectCapture, PassthroughObserver } from "./capture";
5
- /**
6
- * The front door's CONNECT surface, so an OAuth session can have agent-shim's routing AND Claude Code's Remote Control at once: Remote Control refuses any `ANTHROPIC_BASE_URL` other than the real API, but happily honours `HTTPS_PROXY`, so OAuth launches route at the proxy layer instead.
7
- *
8
- * Nothing is tunnelled or intercepted for a client that has not authenticated: every CONNECT request must present a live launch's capability as its proxy credential (`Proxy-Authorization: Basic`, which clients derive from the credential in the `HTTPS_PROXY` URL the launcher sets), or it is answered 407 before any target is dialled, so the port is no open proxy for other local processes. Pending and authenticated connections are bounded by `ConnectLimits`.
9
- *
10
- * Every authenticated CONNECT target is blind-tunnelled byte for byte EXCEPT the intercept hosts (Claude Code's own API and the claude.ai control plane), whose TLS this surface terminates with leaf certificates signed by a locally generated CA. On the API host's terminated session, the paths the front door routes (`/v1/`) are handed to the same ordered pipeline the provider listener serves (the client's Authorization header passes through untouched), and every other path there, and every path on the control plane's session, is piped by this surface to that session's own real upstream over TLS, so Remote Control's streaming, OAuth refreshes, and unknown endpoints bypass the pipeline entirely. When a `capture` is configured, those piped exchanges and every CONNECT target are recorded through it (see `./capture.ts`); the forwarding itself is identical either way.
11
- */
12
- /** The API host: Claude Code's own upstream, the one host whose `/v1/` paths the routed pipeline serves. */
13
- export declare const CONNECT_INTERCEPT_HOST = "api.anthropic.com";
14
- /**
15
- * Every CONNECT host whose TLS this surface terminates, each with its own leaf and its own HTTP parser: the API host above, and `platform.claude.com`, the claude.ai control plane where Remote Control's long-lived channel and its handshake live. Terminating the second one is what makes a Remote Control login rejection observable at all: its bytes cross this surface either way, and blind-tunnelling them hides exactly the exchange that failed. Only the API host's `/v1/` paths ever reach the routed pipeline; everything else on either session is piped to that session's own real upstream, so the pipeline's identification and authorisation are never asked to serve another host's traffic.
16
- */
17
- export declare const CONNECT_INTERCEPT_HOSTS: readonly string[];
18
- /**
19
- * The intercept hosts the surface never parses as HTTP: terminated with ALPN for HTTP/2 and HTTP/1.1 both offered, pumped byte for byte to the real host over TLS (offering upstream only the protocol the client negotiated), and teed into the capture's stream tap when capturing is on. The claude.ai control plane's channel mixes an HTTP/1.1-shaped request with binary HTTP/2 frames, so parsing it as either protocol would destroy or distort it; tapping preserves the channel exactly as a blind tunnel served it while making its bytes observable.
20
- */
21
- export declare const CONNECT_TAP_HOSTS: readonly string[];
22
- /** Paths under this prefix are what the routed pipeline serves; everything else on the terminated session goes to the real upstream. */
23
- export declare const ROUTED_PATH_PREFIX = "/v1/";
24
- /** The port HTTPS is served on, both the default when a CONNECT authority names none and the port non-headroom paths are piped to on the real upstream. */
25
- export declare const HTTPS_PORT = 443;
26
- /** What the forwarding effect answers with when the upstream is unreachable. */
27
- export declare const HTTP_BAD_GATEWAY = 502;
28
- /** Largest CONNECT request head accepted before the connection is refused: a real head is a request line and a few short headers (Host, the proxy credential, a User-Agent), so anything this size is a confused or hostile client. 8 KiB is also the per-header-line limit common HTTP servers apply by default (nginx's `large_client_header_buffers` buffer, Apache's `LimitRequestFieldSize` of 8190 bytes), so no client is built to need a longer line, and a CONNECT head is a few such lines at most. */
29
- export declare const MAX_CONNECT_HEAD_BYTES = 8192;
30
- /**
31
- * The surface's connection deadlines and caps. Production passes `CONNECT_LIMITS`; tests pass small values so a deadline or a cap is reached in milliseconds.
32
- *
33
- * A connection is pending from accept until its CONNECT head has arrived, and authenticated once that head presented a live launch's capability. Pending connections are bounded twice, by `headDeadlineMs` and by `maxPendingHeads` (reaching the cap evicts the oldest pending connection rather than refusing the newest, so a squatter holding open half-sent heads cannot lock live launches out: a real client's head arrives in its first segment and is authenticated long before that many newer connections could push it out). Authenticated connections are bounded by `maxTunnels` and revoked when their launch ends; they have no idle timeout (see `CONNECT_LIMITS`).
34
- */
35
- export interface ConnectLimits {
36
- /** How long a connection may take to deliver its whole CONNECT head before it is answered 408 and closed. */
37
- readonly headDeadlineMs: number;
38
- /** How many connections may be waiting for their head at once; one more evicts the oldest. */
39
- readonly maxPendingHeads: number;
40
- /** How many authenticated connections (blind tunnels and terminated sessions together) may be open at once; one more is answered 503 and closed. */
41
- readonly maxTunnels: number;
42
- /** How often every authenticated connection's capability is re-checked, closing the connections of any launch that has since ended. */
43
- readonly revalidateMs: number;
44
- }
45
- /**
46
- * The production limits, each derived from what the surface's real clients do.
47
- *
48
- * `headDeadlineMs`: every client checked (Claude Code, curl, Node's own proxy support, Python's urllib) sends its whole CONNECT head, credential included, the moment the TCP connection opens, without waiting for anything from the proxy, so on loopback the head arrives within milliseconds. The deadline only has to outlast a door process stalled by a loaded machine, and ten seconds is three orders of magnitude above any such stall while still ending a slowloris connection quickly.
49
- *
50
- * `maxPendingHeads`: since a real head arrives in the first segment, the pending set at any instant holds only connections opened in the same instant. The largest real burst is a package manager or similar running inside a session, which opens its parallel downloads through HTTPS_PROXY at once; 64 covers the defaults of npm (`maxsockets` 15) and pnpm (`network-concurrency` 16) several times over, and eviction means even a larger burst only costs a retry, never a lockout.
51
- *
52
- * `maxTunnels`: each authenticated connection holds two descriptors (the client's and the upstream's). Node raises its soft descriptor limit to the hard limit at start, and 4096 is the smallest hard limit common Linux distributions ship (macOS's is far higher), so 1024 tunnels keep the surface to half of it, leaving the provider and direct listeners room to serve rather than all three failing with EMFILE. It is still many times what concurrent sessions hold: each keeps a handful of tunnels plus whatever bursts its tools make.
53
- *
54
- * `revalidateMs`: the supervisor prunes dead launches from the registry once per `FRONTDOOR_POLL_MS`, so re-checking more often than that could never find anything newly dead.
55
- *
56
- * There is deliberately no idle timeout on an authenticated connection: Remote Control and other long-lived streams can sit silent for as long as the far end chooses between events, at an interval the surface cannot know, so any idle timeout would cut legitimate sessions. Authenticated connections are bounded instead by who can open them (only a live launch's process tree holds the capability), by `maxTunnels`, and by revocation: a launch that ends loses every connection it opened at the next revalidation.
57
- */
58
- export declare const CONNECT_LIMITS: ConnectLimits;
59
- /** One parsed CONNECT authority, the host lowercased and the port defaulted. */
60
- export interface ConnectTarget {
61
- readonly host: string;
62
- readonly port: number;
63
- }
64
- /** Parses a CONNECT authority (`host:port`, or a bare `host` meaning 443), or undefined when it names no host or a non-numeric port. */
65
- export declare function parseConnectTarget(authority: string): ConnectTarget | undefined;
66
- /** Whether a CONNECT target's host (port already stripped) is one whose TLS this proxy terminates. */
67
- export declare function isInterceptedHost(host: string, interceptHosts: readonly string[]): boolean;
68
- /** Whether a request path on the terminated session is one the routed pipeline serves. The query string stays part of the path: `/v1/messages?beta=true` is still the pipeline's. */
69
- export declare function servedByPipeline(path: string | undefined): boolean;
70
- /**
71
- * Headers a forwarded HTTP message keeps: everything except the hop-by-hop set, which belongs to whichever connection carried it and must not leak onto the next one. The `Connection` header's own value can name further hop-by-hop headers (RFC 9110 section 7.6.1), so those go too. Returns a fresh object; the input is the live parser's map and is never mutated.
72
- */
73
- export declare function forwardableHeaders(headers: Readonly<IncomingHttpHeaders>): IncomingHttpHeaders;
74
- /** A CA certificate and its private key, both as PEM. */
75
- export interface CaMaterial {
76
- readonly certPem: string;
77
- readonly keyPem: string;
78
- }
79
- /** A TLS leaf certificate and its private key, both as PEM. */
80
- export interface LeafCert {
81
- readonly certPem: string;
82
- readonly keyPem: string;
83
- }
84
- /** Generates the local CA: one RSA keypair, self-signed, CA-only by basicConstraints and keyUsage, with a random serial. Built on `node:crypto` alone (see `x509.ts`), no openssl, so the same code works on every platform agent-shim ships to. */
85
- export declare function generateCa(now: Readonly<Date>): CaMaterial;
86
- /** The names the front door's provider listener answers to: the loopback address every provider session's base URL names, plus the loopback hostname for a client that spells it that way. */
87
- export declare const LOOPBACK_LEAF_NAMES: readonly string[];
88
- /**
89
- * Mints a TLS leaf signed by the CA: its own RSA keypair, server-auth usage only, the first name as the subject's common name and every name in the SAN. A name that parses as an IP address goes in as an IP SAN, because TLS clients match an IP host only against IP SANs, never against a DNS SAN spelling the same digits.
90
- */
91
- export declare function mintLeaf(ca: CaMaterial, names: readonly string[], now: Readonly<Date>): LeafCert;
92
- /** Mints each host's leaf once from the CA and reuses it, because generating a 2048-bit RSA keypair costs noticeable time and nothing about the certificate changes between sessions. */
93
- export declare function createLeafCache(ca: CaMaterial, now: () => Date): (host: string) => LeafCert;
94
- /**
95
- * Persistence for the CA: generated once on the machine's first headroom start and reused forever after, because regenerating it would invalidate every child's `NODE_EXTRA_CA_CERTS` pointing at the old file. The real implementation writes the key with mode 0600; injected fakes keep it in memory.
96
- */
97
- export interface ConnectCertStore {
98
- /** The persisted CA, or undefined when none exists yet (or only half of it does: a cert without its key can sign nothing, so it counts as absent). */
99
- readonly loadCa: () => CaMaterial | undefined;
100
- /** Persists both halves of the CA, the key unreadable to other users. */
101
- readonly writeCa: (ca: CaMaterial) => void;
102
- }
103
- /**
104
- * Loads the persisted CA, generating and persisting one when no usable CA exists. A stored pair that `node:crypto` cannot parse is treated as absent and replaced: a truncated write is the one failure mode a regeneration can actually repair, and every child that trusted the old file needs a new `NODE_EXTRA_CA_CERTS` anyway once its CA stops parsing.
105
- */
106
- export declare function ensureCa(store: ConnectCertStore, generate: () => CaMaterial): CaMaterial;
107
- /** Where one forwarded request goes, and how: the connect surface's own forwards go to the real upstream over TLS, while a local hop over plain HTTP is what tests redirect to. */
108
- interface ConnectForwardTarget {
109
- readonly host: string;
110
- readonly port: number;
111
- readonly tls: boolean;
112
- /** The SNI name to present; defaults to `host`. Separated so a test can connect to a local fake while presenting the production hostname. */
113
- readonly servername?: string;
114
- /** Extra trust anchors for the target's certificate, when it is not system-trusted (a test's local fake upstream). */
115
- readonly ca?: readonly string[];
116
- /** Overrides certificate verification; production leaves it at the Node default (verify against system trust or `ca`). */
117
- readonly rejectUnauthorized?: boolean;
118
- }
119
- /** Handles one parsed HTTP request on a terminated TLS session. */
120
- type ConnectRequestHandler = (request: IncomingMessage, response: ServerResponse) => void;
121
- /** A bound loopback listener and how to stop it. */
122
- export interface ConnectListenerHandle {
123
- readonly port: number;
124
- /** Stops accepting, ends every live connection, and resolves once the port is released. */
125
- readonly close: () => Promise<void>;
126
- }
127
- /** Presents one leaf's server-side TLS on CONNECTed sockets. */
128
- interface TlsAcceptor {
129
- /** Starts the TLS handshake on a socket that has just received its CONNECT response, carrying `context` to the handshake's completion untouched: whatever the caller knows about this connection (its tunnel's capability) reaches `onSecure` without the acceptor interpreting it. */
130
- readonly accept: (socket: net.Socket, context: string) => void;
131
- /** Drops every session this acceptor is terminating. */
132
- readonly close: () => void;
133
- }
134
- /** Parses HTTP over terminated sockets and serves each request through the handler, keep-alive included. */
135
- interface HttpParserSession {
136
- readonly serve: (socket: net.Socket) => void;
137
- readonly close: () => void;
138
- }
139
- /**
140
- * Every effect the MITM proxy performs, injected so the routing decisions and lifecycle run against fakes in unit tests (the SupervisorPorts pattern): TCP listening, TLS termination, HTTP parsing, plain TCP tunnels, and request forwarding. The real implementation is `realConnectEffects`; the TLS round-trip test uses it with redirected targets.
141
- */
142
- export interface ConnectEffects {
143
- /** Binds a loopback TCP listener; `preferredPort` is tried first and any free port used when it is taken (bind, do not probe: check-then-bind races). Resolves with the bound port and a close handle. */
144
- readonly listenLoopback: (preferredPort: number | undefined, onSocket: (socket: net.Socket) => void) => Promise<ConnectListenerHandle>;
145
- /** Builds the TLS terminator for one leaf, handing each successfully handshaked session to `onSecure` together with the context its `accept` call carried. `alpnProtocols`, when given, is advertised during the handshake, which is how a tap host's session can offer HTTP/2 the way its real server does. */
146
- readonly createTlsAcceptor: (leaf: LeafCert, onSecure: (secure: net.Socket, context: string) => void, alpnProtocols?: readonly string[]) => TlsAcceptor;
147
- /** Opens a TLS connection to a real host, presenting its name as SNI, for a tap session's upstream half. `clientAlpn`, when given, is the protocol the client negotiated with the surface and the only one offered upstream, so the tap never changes the channel's protocol. */
148
- readonly connectTlsUpstream: (host: string, port: number, clientAlpn: string | undefined) => Promise<net.Socket>;
149
- /** Builds the HTTP parser bound to one request handler. `onUpgrade`, when given, owns every HTTP upgrade request arriving on a terminated session, socket and already-read bytes included; a session without one lets Node destroy the upgrade, its own no-listener behaviour. */
150
- readonly createHttpSession: (handler: ConnectRequestHandler, onUpgrade?: (request: IncomingMessage, socket: Duplex, head: Buffer) => void) => HttpParserSession;
151
- /** Opens a raw TCP connection for a blind tunnel. */
152
- readonly connectTcp: (host: string, port: number) => Promise<net.Socket>;
153
- /** Streams one request to `target` and the response back, never buffering a body. When `observer` is given, the forwarding drives it with additive listeners (request and response chunks arrive to both the pipe and the observer) so capturing never changes the forwarding's own flow control. */
154
- readonly forwardHttp: (target: ConnectForwardTarget, request: IncomingMessage, response: ServerResponse, observer?: PassthroughObserver) => void;
155
- }
156
- /** Everything the connect surface needs to route, resolved before it starts. */
157
- export interface ConnectServerConfig {
158
- /** The CONNECT hosts whose TLS get terminated, each with its own leaf, its own HTTP parser and its own upstream. */
159
- readonly interceptHosts: readonly string[];
160
- /**
161
- * The one intercept host whose `/v1/` paths `serveRouted` handles: the API host, whose pipeline identification, authorisation and headroom hop are about Anthropic sessions and no other host's traffic.
162
- */
163
- readonly routedHost: string;
164
- /**
165
- * Serves one routed path (`/v1/...`) from the terminated session: the same ordered pipeline the provider listener hands requests to, so an OAuth session and a provider session run identical identification, middleware and routing.
166
- */
167
- readonly serveRouted: ConnectRequestHandler;
168
- /** The leaf to terminate each intercept host with, minted and cached per host. */
169
- readonly leafFor: (host: string) => LeafCert;
170
- /** Where non-routed paths on one host's terminated session are piped: that host's own real upstream over TLS. */
171
- readonly upstreamFor: (host: string) => ConnectForwardTarget;
172
- /** Whether a capability presented as a CONNECT request's proxy credential belongs to a live launch: the same constant-time check against the same registry the routed pipeline's admission makes, read fresh on every call because launches come and go. */
173
- readonly isLiveCapability: (token: string) => boolean;
174
- /**
175
- * Binds a second listener that serves the intercept hosts' sessions to connections arriving pre-established, with no CONNECT handshake to authenticate: what an operating-system redirect (a pf rule sending the real hosts' address to this port) delivers. Only the API host's session is reachable this way, never a tap host's, because a redirect can name only addresses and the control plane shares the API host's address today; if that ever splits, the transparent listener must not follow it blindly. There is no capability to check on this surface: the connection carried no proxy credential, so every request rides the pipeline anonymously, admitted the way a tunnel's own header-less requests are (by nothing, which is exactly what they present), and the surface exists solely for clients that ignore proxies and would otherwise be invisible to the door entirely.
176
- */
177
- readonly transparentPort?: number;
178
- /**
179
- * The capability the transparent surface's sessions carry: a real, registered token minted by the door for its own pid, so header-less traffic that arrives by redirect is admitted and attributed like any launch's instead of being refused for a credential it cannot possibly present. Absent along with `transparentPort`.
180
- */
181
- readonly transparentCapability?: string;
182
- /** The intercept hosts that are never HTTP-parsed: terminated, tapped at the byte level, and pumped to their real host. */
183
- readonly tapHosts: readonly string[];
184
- /** The connection deadlines and caps; production passes `CONNECT_LIMITS`. */
185
- readonly limits: ConnectLimits;
186
- /** The diagnostic tap, when capturing is enabled; absent means nothing is recorded. */
187
- readonly capture?: ConnectCapture;
188
- }
189
- /** A running connect surface. */
190
- export interface ConnectServerHandle {
191
- readonly port: number;
192
- /** The transparent listener's port, when one was requested: what an operating-system redirect should point at. */
193
- readonly transparentPort?: number;
194
- /** Stops the listener, drops every live tunnel and terminated session, and resolves once the port is released. */
195
- readonly close: () => Promise<void>;
196
- }
197
- /**
198
- * Starts the connect surface: binds the listener, mints the intercept host's leaf, and wires the per-connection routing. Resolves once the listener is bound.
199
- *
200
- * The connection flow: read the CONNECT head within the deadline (never more of the stream than that, so an early ClientHello glued to the head is pushed back with `unshift` and still seen by whatever consumes the socket next), authenticate it, then either blind-tunnel the target or terminate its TLS and parse HTTP on the session. Every network effect flows through `effects`; nothing here touches the network or filesystem itself.
201
- */
202
- export declare function startConnectServer(config: ConnectServerConfig, effects: ConnectEffects, preferredPort: number | undefined): Promise<ConnectServerHandle>;
203
- /** The real `ConnectCertStore` on the filesystem: `ca.pem` world-readable (it is public material), `ca.key` mode 0600 exactly, chmod'ed after the write because a create's mode bits pass through the process umask. */
204
- export declare function realConnectCertStore(paths: {
205
- readonly frontdoorCaDir: string;
206
- readonly frontdoorCaCertFile: string;
207
- readonly frontdoorCaKeyFile: string;
208
- }): ConnectCertStore;
209
- export {};
@@ -1,3 +0,0 @@
1
- import { type ConnectEffects } from "./connect";
2
- /** The real `ConnectEffects` over node's own net, tls, and http. `connectTcp` opens real connections to the CONNECTed host, so tests that want a blind tunnel redirect it. */
3
- export declare function realConnectEffects(): ConnectEffects;
@@ -1,32 +0,0 @@
1
- import type { IncomingHttpHeaders } from "node:http";
2
- /** What stands in for a credential while its request crosses headroom: an opaque word that authenticates nothing anywhere. */
3
- export declare const SEQUESTERED_CREDENTIAL = "agent-shim-sequestered";
4
- /** The credential headers one custody record holds, exactly as the client sent them. */
5
- type HeldCredentials = Readonly<Record<string, string | readonly string[]>>;
6
- /** One request's headers with its credentials swapped out, and the id that redeems them. */
7
- interface Sequestered {
8
- readonly hopId: string;
9
- readonly headers: Record<string, string | string[] | undefined>;
10
- }
11
- /**
12
- * Holds a provider session's real credentials in this process's memory while its request takes the headroom hop, so the credential never crosses headroom or the plain-HTTP leg back to the door.
13
- *
14
- * The hop calls `sequester`, sends headroom the placeholder headers plus the hop id, and calls `release` once its response to the client has finished or the client has gone. The direct listener calls `redeem` with the hop id and the provider its request path names: a live id recorded for that same provider yields the real headers, anything else yields nothing and the request is refused. A process that binds the direct port therefore receives only placeholders and an id that is worthless once the hop ends, and nothing outside this process can mint an id.
15
- *
16
- * An id stays redeemable for the whole hop rather than exactly once, because headroom legitimately sends one client request upstream more than once: it retries 429, 529 and 5xx answers, and its memory-tool and CCR continuations call the upstream again inside the same client turn, all with the headers it was handed.
17
- */
18
- export interface CredentialCustody {
19
- /** Takes custody of `headers`' credential headers for one hop to `provider`, returning the headers to send headroom (placeholders where the credentials were) and the hop id that redeems them. */
20
- readonly sequester: (headers: Readonly<IncomingHttpHeaders>, provider: string) => Sequestered;
21
- /** Ends a hop: its id stops redeeming. Idempotent. */
22
- readonly release: (hopId: string) => void;
23
- /** The real credential headers for a live hop to `provider`, or undefined for an unknown, released or mismatched id. */
24
- readonly redeem: (hopId: string, provider: string) => HeldCredentials | undefined;
25
- }
26
- /** Creates an empty custody store. `randomId` must be unguessable (a v4 UUID or better): the id is the only thing that redeems a credential. */
27
- export declare function createCredentialCustody(randomId: () => string): CredentialCustody;
28
- /**
29
- * The headers a redeemed request is routed with: every credential header headroom forwarded (placeholders, or anything else headroom chose to send) is dropped, and the held originals take their place. Whatever the hop's request carried in those headers is never trusted; only what the client sent is.
30
- */
31
- export declare function restoreCredentials(headers: Readonly<IncomingHttpHeaders>, credentials: HeldCredentials): IncomingHttpHeaders;
32
- export {};
@@ -1,36 +0,0 @@
1
- import { CliError } from "../cliError";
2
- import type { LayoutPaths } from "../paths";
3
- import { type HeadroomFs } from "../headroom/state";
4
- import type { ListenerVerdict } from "./probe";
5
- /** Raised when the front door cannot be brought up in time or has recorded a fatal error: a routed launch has no other way to reach its provider, so it must fail rather than start a session that can never answer. */
6
- export declare class FrontDoorStartError extends CliError {
7
- constructor(message: string);
8
- }
9
- /** Every effect the ensure step performs, injected so it runs against fakes in tests. */
10
- export interface EnsureFrontDoorPorts {
11
- readonly fs: HeadroomFs;
12
- /** Zombie-aware liveness (see `realIsProcessRunning`). */
13
- readonly isRunning: (pid: number) => boolean;
14
- readonly now: () => number;
15
- readonly sleep: (ms: number) => void;
16
- /** Spawns the detached front-door supervisor, returning its pid. */
17
- readonly spawnSupervisor: (paths: LayoutPaths) => number;
18
- /** Authenticates the provider listener on `port`: a TLS handshake chaining to agent-shim's CA and a healthy answer, with no capability sent (see `probeFrontDoorSync`). */
19
- readonly verifyListener: (port: number) => ListenerVerdict;
20
- /** Asks a running front-door supervisor to exit, without waiting for it to: the caller polls `isRunning`. Only ever called for a pid whose recorded listener has just authenticated as agent-shim's own. */
21
- readonly stopSupervisor: (pid: number) => void;
22
- }
23
- /**
24
- * Brings the front door up for this launch, or finds it serving, and registers this launcher pid in its session registry: the fact that keeps the door from idling out while this session lives, and the record that holds this launch's capability token. The same lock-and-poll coordination as headroom's and the old codex daemon's ensure: the exclusive-create start lock decides which of several concurrent launches spawns the one supervisor, everyone waits on state.json, and a lock whose holder died is removed and retried. Ready means state names a live supervisor and both listeners' ports, which the supervisor writes only after both have bound (and the provider listener has answered its own health probe).
25
- *
26
- * A live pid in state proves nothing about who holds the port it names: the door may have crashed and left the port to anyone, or the pid may have been reused by an unrelated process. So before anything is registered or returned, the provider listener is authenticated over TLS against agent-shim's CA. A listener that fails (wrong certificate, nothing answering, a bad answer) is treated as a stale or hostile owner: its pid is distrusted for the rest of this call and a replacement supervisor is spawned, which binds a free port when the sticky one is held and is then verified in turn. If a supervisor this call spawned also fails verification, the launch is refused naming the reason and the daemon log; no capability is written and no address is handed to the child.
27
- */
28
- export declare function ensureFrontDoor(params: {
29
- readonly paths: LayoutPaths;
30
- readonly launcherPid: number;
31
- readonly ports: EnsureFrontDoorPorts;
32
- }): {
33
- readonly port: number;
34
- readonly connectPort: number;
35
- readonly token: string;
36
- };
@@ -1,22 +0,0 @@
1
- import type { CredentialCustody } from "./custody";
2
- import { type RoutedRequest, type RoutedResponse } from "./route";
3
- /** Everything the headroom hop depends on, injected so the pipeline's ordering tests can point it at a scripted daemon. */
4
- export interface HeadroomHopDeps {
5
- /**
6
- * The headroom daemon's loopback port, read per request rather than captured at start-up: the daemon can crash and restart on a different port while this door keeps listening, and the hop must follow it. Undefined while the daemon is down (the restart window).
7
- */
8
- readonly headroomPort: () => number | undefined;
9
- /**
10
- * The per-generation secret the direct listener requires on what the hop forwards back. Headroom passes non-x-headroom headers through untouched, so the secret survives the round trip and arrives where this process can check it, while no loopback process outside this door ever holds it.
11
- */
12
- readonly hopSecret: string;
13
- /** Holds a provider session's real credentials while its request crosses headroom; the direct listener redeems them by the hop id. */
14
- readonly custody: CredentialCustody;
15
- readonly log: (line: string) => void;
16
- }
17
- /**
18
- * Applies the headroom hop: forwards the request, still Anthropic-shaped, to the headroom daemon as a backend hop and streams its response back to the client. The hop ALWAYS sits before the route's own serving (a translator needs the request headroom has compressed, never the reverse), which is why this runs in the pipeline and not inside any route.
19
- *
20
- * `upstream` is what headroom is told to forward to once it has done its work (its per-request base URL header). When it is set it is always this door's direct listener (every provider route declares exactly that), and the provider credential is then taken into custody for the hop: headroom receives placeholders and a hop id, and the direct listener restores the real headers when headroom forwards the request back. Neither headroom nor anything that binds a port on that plain-HTTP round trip ever holds the credential. Undefined leaves headroom's default upstream (Claude Code's API) in charge, which is exactly what an OAuth session wants, and the OAuth bearer then passes through untouched: headroom forwards it straight to that API, never back here, and uses it for its subscription tracking. The session's project identity is re-set here, and only here: the identity step stripped it from everything that leaves the machine, and headroom is the one consumer that needs it.
21
- */
22
- export declare function applyHeadroomHop(request: RoutedRequest, response: RoutedResponse, upstream: string | undefined, deps: HeadroomHopDeps): Promise<void>;
@@ -1,109 +0,0 @@
1
- import type { IncomingHttpHeaders, IncomingMessage, ServerResponse } from "node:http";
2
- import { type HeadroomHopDeps } from "./headroomHop";
3
- import { type FrontDoorRoute, type RoutedRequest, type RoutedResponse, type SessionIdentity } from "./route";
4
- /** What one routed response looked like as it started back to the client: the typed event response middleware observes. */
5
- export interface RoutedResponseEvent {
6
- readonly session: SessionIdentity;
7
- /** The route that served the response. */
8
- readonly route: string;
9
- /** The request's method. */
10
- readonly method: string;
11
- /** The request target's path, without its query string. */
12
- readonly path: string;
13
- /** When the pipeline received the request, in epoch milliseconds from the injected clock. */
14
- readonly receivedAt: number;
15
- readonly status: number;
16
- readonly headers: Readonly<Record<string, string>>;
17
- }
18
- /** How a routed response's body finished: written to the end, or cut short (the client went away, or the route destroyed the response after an upstream failure). */
19
- export type ResponseOutcome = "completed" | "aborted";
20
- /**
21
- * Follows one response's body as the client receives it. `chunk` sees each chunk after it has been handed to the client's socket, so observing never holds a byte back; `end` is called exactly once, when the body has finished either way. Like the observer that returned it, a body observer that throws is logged and dropped for the rest of that response, never allowed to break it.
22
- */
23
- export interface ResponseBodyObserver {
24
- readonly chunk: (chunk: Uint8Array) => void;
25
- readonly end: (outcome: ResponseOutcome) => void;
26
- }
27
- /**
28
- * Response middleware: called once per routed response, in registration order, once the route's response head is known and before any of it is written to the client. It returns a body observer when it wants to follow the body, or undefined when the head was all it needed. Observing must be side-effect-only: an observer that throws is logged and skipped, never allowed to break routing, and an observer must not block the response.
29
- *
30
- * Usage tracking registers here (see `src/usage/middleware.ts`): the head carries the rate-limit headers, and the body carries the token usage.
31
- */
32
- export type ResponseObserver = (event: RoutedResponseEvent) => ResponseBodyObserver | undefined;
33
- /** The outcome of resolving a request's target to a route. */
34
- export type RouteResolution = {
35
- readonly ok: true;
36
- readonly route: FrontDoorRoute;
37
- } | {
38
- readonly ok: false;
39
- readonly status: number;
40
- readonly message: string;
41
- };
42
- /** Everything the ordered pipeline depends on, injected so it runs against fakes in tests. */
43
- export interface PipelineDeps {
44
- /** Resolves the route a request's target names, reading whatever configuration the route needs. */
45
- readonly resolveRoute: (request: RoutedRequest) => Promise<RouteResolution>;
46
- /** The response middleware chain, run in order at each response head. This is where usage capture registers. */
47
- readonly responseObservers: readonly ResponseObserver[];
48
- /**
49
- * The headroom hop, applied BEFORE a route whose declaration admits it whenever the session's launch resolved headroom on: headroom needs the Anthropic-shaped request, so it always sits in front of a translator, never behind one. Omitted by a listener that serves routes directly (the direct listener headroom forwards an in-process route back to), or when no headroom is wired.
50
- */
51
- readonly headroom?: HeadroomHopDeps;
52
- /** The clock response middleware times requests by, in epoch milliseconds. */
53
- readonly now: () => number;
54
- readonly log: (line: string) => void;
55
- /**
56
- * The listener's admission step, run after identification and before anything is routed: it decides whether the request may be routed at all and with which headers. A client-facing listener admits a request carrying the per-launch capability of a live registered session, unchanged; the direct listener admits only what this process's own headroom hop sent back (the generation's hop secret plus a live custody id for the provider the path names) and swaps the hop's placeholder credentials for the real ones. A refused request is answered 401 without ever reaching a route, so a loopback process that never launched through agent-shim cannot spend a session's credentials or quota.
57
- */
58
- readonly admit: (request: AdmissionRequest) => Admission;
59
- }
60
- /** What the admission step sees of one request. */
61
- interface AdmissionRequest {
62
- /** The request target exactly as received. */
63
- readonly url: string;
64
- /** Every header as received, internal ones included: the capability and hop headers live here. */
65
- readonly headers: Readonly<IncomingHttpHeaders>;
66
- /** The headers the identity step judged forwardable, internal ones stripped. */
67
- readonly forwardable: Readonly<IncomingHttpHeaders>;
68
- }
69
- /** The admission step's verdict: route with these headers, or refuse with this message. */
70
- type Admission = {
71
- readonly ok: true;
72
- readonly headers: IncomingHttpHeaders;
73
- } | {
74
- readonly ok: false;
75
- readonly message: string;
76
- };
77
- /** One request as a transport hands it to the pipeline, before anything has been identified or routed. */
78
- export interface PipelineRequest {
79
- readonly method: string;
80
- /** The request target exactly as received, path and query together. */
81
- readonly url: string;
82
- readonly headers: IncomingHttpHeaders;
83
- /** The request's body stream. */
84
- readonly body: IncomingMessage;
85
- /** Aborted the moment the client goes away. */
86
- readonly signal: AbortSignal;
87
- readonly response: ServerResponse;
88
- }
89
- /** What the response middleware learns about the request a routed response answers. */
90
- interface ObservedRequest {
91
- readonly method: string;
92
- /** The request target's path, without its query string. */
93
- readonly path: string;
94
- readonly receivedAt: number;
95
- }
96
- /**
97
- * Builds the response half a route writes through. The head is what runs the response middleware: `start` invokes every observer, in order, with the session, the request and the route's name before a byte reaches the client, and keeps the body observers they return. Each body chunk reaches those observers only after it has been handed to the client's socket, and their `end` runs once when the response ends, is destroyed, or the client goes away first, so following the body never holds a byte back and never misses how it finished. Body writes apply backpressure by waiting for the socket to drain, and resolve without writing once the client is gone, so a route streaming to a dead client finishes promptly rather than filling a buffer.
98
- */
99
- export declare function createRoutedResponse(response: ServerResponse, context: {
100
- readonly deps: PipelineDeps;
101
- readonly session: SessionIdentity;
102
- readonly route: string;
103
- readonly request: ObservedRequest;
104
- }): RoutedResponse;
105
- /**
106
- * The ordered pipeline every routed session's request runs through: identify the session (splitting the launcher-injected headers out of what may leave the machine), then route, with the response middleware running at each response head. A route that cannot be resolved is answered as an Anthropic-shaped error, and the observers see that error like any other response, since quota and rate-limit state lives in exactly those.
107
- */
108
- export declare function serveRouted(request: PipelineRequest, deps: PipelineDeps): Promise<void>;
109
- export {};
@@ -1,13 +0,0 @@
1
- /** The outcome of authenticating the listener a front-door state file names. */
2
- export type ListenerVerdict = {
3
- readonly ok: true;
4
- } | {
5
- readonly ok: false;
6
- readonly reason: string;
7
- };
8
- /**
9
- * Authenticates the front door's provider listener before a launch trusts it: a TLS handshake that must chain to agent-shim's CA (whose key only this user can read) for 127.0.0.1, then a healthy answer. A process that merely holds the port (a stale state file's port re-bound by someone else, or a reused pid's listener) cannot complete that handshake, so it fails here and the launch never hands its child that address.
10
- *
11
- * Synchronous because the launcher is synchronous right through to `spawnSync`: the HTTPS request runs on a worker thread and this thread blocks on a shared buffer until the worker posts its verdict.
12
- */
13
- export declare function probeFrontDoorSync(port: number, caPem: string): ListenerVerdict;
@@ -1,5 +0,0 @@
1
- import type { FrontDoorPort } from "../launcher/ports";
2
- import type { LayoutPaths } from "../paths";
3
- import { type DaemonSpawner } from "../realPorts";
4
- /** The real `FrontDoorPort` for one launch: `ensure` runs the lock-and-poll coordination, authenticates the listener and registers this launcher; `release` removes its registration. */
5
- export declare function realFrontDoorPort(paths: LayoutPaths, spawnDaemon: DaemonSpawner): FrontDoorPort;