claude-use 4.5.0 → 5.0.1
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.
- package/README.md +1 -1
- package/dist/cli.cjs +3455 -2598
- package/dist/index.cjs +540 -162
- package/dist/index.mjs +530 -156
- package/dist/types/config/schema.d.ts +39 -0
- package/dist/types/frontdoor/capability.d.ts +19 -0
- package/dist/types/frontdoor/connect.d.ts +179 -0
- package/dist/types/frontdoor/ensure.d.ts +34 -0
- package/dist/types/frontdoor/probe.d.ts +13 -0
- package/dist/types/frontdoor/state.d.ts +32 -1
- package/dist/types/frontdoor/supervisor.d.ts +48 -0
- package/dist/types/headroom/ensure.d.ts +1 -2
- package/dist/types/headroom/settings.d.ts +13 -0
- package/dist/types/headroom/state.d.ts +3 -4
- package/dist/types/headroom/supervisor.d.ts +10 -12
- package/dist/types/index.d.ts +4 -2
- package/dist/types/launcher/ports.d.ts +22 -10
- package/dist/types/paths.d.ts +8 -6
- package/package.json +1 -1
- package/dist/types/headroom/mitm.d.ts +0 -138
|
@@ -559,6 +559,28 @@ export declare const PortableConfigSchema: z.ZodObject<{
|
|
|
559
559
|
}, z.core.$strict>>;
|
|
560
560
|
}, z.core.$strict>;
|
|
561
561
|
export type PortableConfig = z.infer<typeof PortableConfigSchema>;
|
|
562
|
+
declare const HeadroomGlobalConfigSchema: z.ZodObject<{
|
|
563
|
+
source: z.ZodOptional<z.ZodString>;
|
|
564
|
+
idleShutdownMinutes: z.ZodOptional<z.ZodNumber>;
|
|
565
|
+
mode: z.ZodOptional<z.ZodEnum<{
|
|
566
|
+
cache: "cache";
|
|
567
|
+
token: "token";
|
|
568
|
+
}>>;
|
|
569
|
+
targetRatio: z.ZodOptional<z.ZodNumber>;
|
|
570
|
+
ccr: z.ZodOptional<z.ZodEnum<{
|
|
571
|
+
default: "default";
|
|
572
|
+
none: "none";
|
|
573
|
+
lossless: "lossless";
|
|
574
|
+
}>>;
|
|
575
|
+
rolloutChannel: z.ZodOptional<z.ZodEnum<{
|
|
576
|
+
beta: "beta";
|
|
577
|
+
canary: "canary";
|
|
578
|
+
dev: "dev";
|
|
579
|
+
}>>;
|
|
580
|
+
interceptToolResults: z.ZodOptional<z.ZodBoolean>;
|
|
581
|
+
readMaturation: z.ZodOptional<z.ZodBoolean>;
|
|
582
|
+
}, z.core.$strict>;
|
|
583
|
+
export type HeadroomGlobalConfig = z.infer<typeof HeadroomGlobalConfigSchema>;
|
|
562
584
|
/**
|
|
563
585
|
* The default `headroom.source` install spec: the ExaDev headroom repository, with the `proxy` extra that provides the `headroom proxy` entry point. Pinned to a full commit SHA (the fork's `exadev-per-session-savings-1` tag), because a branch name can be rebased or deleted and would change or break every fresh install; bump it deliberately when the fork's per-session savings work changes. Kept here rather than in the supervisor because it is the schema's own documented default, referenced by `HeadroomGlobalConfigSchema`'s field docs.
|
|
564
586
|
*/
|
|
@@ -608,6 +630,23 @@ export declare const GlobalConfigSchema: z.ZodObject<{
|
|
|
608
630
|
headroom: z.ZodOptional<z.ZodObject<{
|
|
609
631
|
source: z.ZodOptional<z.ZodString>;
|
|
610
632
|
idleShutdownMinutes: z.ZodOptional<z.ZodNumber>;
|
|
633
|
+
mode: z.ZodOptional<z.ZodEnum<{
|
|
634
|
+
cache: "cache";
|
|
635
|
+
token: "token";
|
|
636
|
+
}>>;
|
|
637
|
+
targetRatio: z.ZodOptional<z.ZodNumber>;
|
|
638
|
+
ccr: z.ZodOptional<z.ZodEnum<{
|
|
639
|
+
default: "default";
|
|
640
|
+
none: "none";
|
|
641
|
+
lossless: "lossless";
|
|
642
|
+
}>>;
|
|
643
|
+
rolloutChannel: z.ZodOptional<z.ZodEnum<{
|
|
644
|
+
beta: "beta";
|
|
645
|
+
canary: "canary";
|
|
646
|
+
dev: "dev";
|
|
647
|
+
}>>;
|
|
648
|
+
interceptToolResults: z.ZodOptional<z.ZodBoolean>;
|
|
649
|
+
readMaturation: z.ZodOptional<z.ZodBoolean>;
|
|
611
650
|
}, z.core.$strict>>;
|
|
612
651
|
frontdoor: z.ZodOptional<z.ZodObject<{
|
|
613
652
|
idleShutdownMinutes: z.ZodOptional<z.ZodNumber>;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How a launch's capability token is presented and checked. The token reaches the child twice: as the `x-claude-use-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 = "claude-use";
|
|
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 = "claude-use 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;
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import type { IncomingHttpHeaders, IncomingMessage, ServerResponse } from "node:http";
|
|
2
|
+
import * as net from "node:net";
|
|
3
|
+
/**
|
|
4
|
+
* The front door's CONNECT surface, so an OAuth session can have claude-use'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.
|
|
5
|
+
*
|
|
6
|
+
* 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`.
|
|
7
|
+
*
|
|
8
|
+
* Every authenticated CONNECT target is blind-tunnelled byte for byte EXCEPT the intercept host (Claude Code's own API), whose TLS this surface terminates with a leaf certificate signed by a locally generated CA. On the 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 is piped by this surface to the real upstream over TLS, so Remote Control's streaming, OAuth refreshes, and unknown endpoints bypass the pipeline entirely.
|
|
9
|
+
*/
|
|
10
|
+
/** The one CONNECT host whose TLS gets terminated: Claude Code's own API, the upstream every OAuth session is really talking to. */
|
|
11
|
+
export declare const CONNECT_INTERCEPT_HOST = "api.anthropic.com";
|
|
12
|
+
/** Paths under this prefix are what the routed pipeline serves; everything else on the terminated session goes to the real upstream. */
|
|
13
|
+
export declare const ROUTED_PATH_PREFIX = "/v1/";
|
|
14
|
+
/** 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. */
|
|
15
|
+
export declare const HTTPS_PORT = 443;
|
|
16
|
+
/** 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. */
|
|
17
|
+
export declare const MAX_CONNECT_HEAD_BYTES = 8192;
|
|
18
|
+
/**
|
|
19
|
+
* 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.
|
|
20
|
+
*
|
|
21
|
+
* 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`).
|
|
22
|
+
*/
|
|
23
|
+
export interface ConnectLimits {
|
|
24
|
+
/** How long a connection may take to deliver its whole CONNECT head before it is answered 408 and closed. */
|
|
25
|
+
readonly headDeadlineMs: number;
|
|
26
|
+
/** How many connections may be waiting for their head at once; one more evicts the oldest. */
|
|
27
|
+
readonly maxPendingHeads: number;
|
|
28
|
+
/** How many authenticated connections (blind tunnels and terminated sessions together) may be open at once; one more is answered 503 and closed. */
|
|
29
|
+
readonly maxTunnels: number;
|
|
30
|
+
/** How often every authenticated connection's capability is re-checked, closing the connections of any launch that has since ended. */
|
|
31
|
+
readonly revalidateMs: number;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* The production limits, each derived from what the surface's real clients do.
|
|
35
|
+
*
|
|
36
|
+
* `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.
|
|
37
|
+
*
|
|
38
|
+
* `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.
|
|
39
|
+
*
|
|
40
|
+
* `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.
|
|
41
|
+
*
|
|
42
|
+
* `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.
|
|
43
|
+
*
|
|
44
|
+
* 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.
|
|
45
|
+
*/
|
|
46
|
+
export declare const CONNECT_LIMITS: ConnectLimits;
|
|
47
|
+
/** One parsed CONNECT authority, the host lowercased and the port defaulted. */
|
|
48
|
+
export interface ConnectTarget {
|
|
49
|
+
readonly host: string;
|
|
50
|
+
readonly port: number;
|
|
51
|
+
}
|
|
52
|
+
/** Parses a CONNECT authority (`host:port`, or a bare `host` meaning 443), or undefined when it names no host or a non-numeric port. */
|
|
53
|
+
export declare function parseConnectTarget(authority: string): ConnectTarget | undefined;
|
|
54
|
+
/** Whether a CONNECT target's host (port already stripped) is the one whose TLS this proxy terminates. */
|
|
55
|
+
export declare function isInterceptedHost(host: string, interceptHost: string): boolean;
|
|
56
|
+
/** 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. */
|
|
57
|
+
export declare function servedByPipeline(path: string | undefined): boolean;
|
|
58
|
+
/**
|
|
59
|
+
* 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.
|
|
60
|
+
*/
|
|
61
|
+
export declare function forwardableHeaders(headers: Readonly<IncomingHttpHeaders>): IncomingHttpHeaders;
|
|
62
|
+
/** A CA certificate and its private key, both as PEM. */
|
|
63
|
+
export interface CaMaterial {
|
|
64
|
+
readonly certPem: string;
|
|
65
|
+
readonly keyPem: string;
|
|
66
|
+
}
|
|
67
|
+
/** A TLS leaf certificate and its private key, both as PEM. */
|
|
68
|
+
export interface LeafCert {
|
|
69
|
+
readonly certPem: string;
|
|
70
|
+
readonly keyPem: string;
|
|
71
|
+
}
|
|
72
|
+
/** Generates the local CA: one RSA keypair, self-signed, CA-only by basicConstraints and keyUsage, with a random serial. Pure node-forge, no openssl, so the same code works on every platform claude-use ships to. */
|
|
73
|
+
export declare function generateCa(now: Readonly<Date>): CaMaterial;
|
|
74
|
+
/** 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. */
|
|
75
|
+
export declare const LOOPBACK_LEAF_NAMES: readonly string[];
|
|
76
|
+
/**
|
|
77
|
+
* 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.
|
|
78
|
+
*/
|
|
79
|
+
export declare function mintLeaf(ca: CaMaterial, names: readonly string[], now: Readonly<Date>): LeafCert;
|
|
80
|
+
/** Mints each host's leaf once from the CA and reuses it, because a 2048-bit keypair in pure JS costs seconds and nothing about the certificate changes between sessions. */
|
|
81
|
+
export declare function createLeafCache(ca: CaMaterial, now: () => Date): (host: string) => LeafCert;
|
|
82
|
+
/**
|
|
83
|
+
* 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.
|
|
84
|
+
*/
|
|
85
|
+
export interface ConnectCertStore {
|
|
86
|
+
/** 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). */
|
|
87
|
+
readonly loadCa: () => CaMaterial | undefined;
|
|
88
|
+
/** Persists both halves of the CA, the key unreadable to other users. */
|
|
89
|
+
readonly writeCa: (ca: CaMaterial) => void;
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Loads the persisted CA, generating and persisting one when no usable CA exists. A stored pair that node-forge 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.
|
|
93
|
+
*/
|
|
94
|
+
export declare function ensureCa(store: ConnectCertStore, generate: () => CaMaterial): CaMaterial;
|
|
95
|
+
/** 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. */
|
|
96
|
+
interface ConnectForwardTarget {
|
|
97
|
+
readonly host: string;
|
|
98
|
+
readonly port: number;
|
|
99
|
+
readonly tls: boolean;
|
|
100
|
+
/** The SNI name to present; defaults to `host`. Separated so a test can connect to a local fake while presenting the production hostname. */
|
|
101
|
+
readonly servername?: string;
|
|
102
|
+
/** Extra trust anchors for the target's certificate, when it is not system-trusted (a test's local fake upstream). */
|
|
103
|
+
readonly ca?: readonly string[];
|
|
104
|
+
/** Overrides certificate verification; production leaves it at the Node default (verify against system trust or `ca`). */
|
|
105
|
+
readonly rejectUnauthorized?: boolean;
|
|
106
|
+
}
|
|
107
|
+
/** Handles one parsed HTTP request on a terminated TLS session. */
|
|
108
|
+
type ConnectRequestHandler = (request: IncomingMessage, response: ServerResponse) => void;
|
|
109
|
+
/** A bound loopback listener and how to stop it. */
|
|
110
|
+
interface ConnectListenerHandle {
|
|
111
|
+
readonly port: number;
|
|
112
|
+
/** Stops accepting, ends every live connection, and resolves once the port is released. */
|
|
113
|
+
readonly close: () => Promise<void>;
|
|
114
|
+
}
|
|
115
|
+
/** Presents one leaf's server-side TLS on CONNECTed sockets. */
|
|
116
|
+
interface TlsAcceptor {
|
|
117
|
+
/** Starts the TLS handshake on a socket that has just received its CONNECT response. */
|
|
118
|
+
readonly accept: (socket: net.Socket) => void;
|
|
119
|
+
/** Drops every session this acceptor is terminating. */
|
|
120
|
+
readonly close: () => void;
|
|
121
|
+
}
|
|
122
|
+
/** Parses HTTP over terminated sockets and serves each request through the handler, keep-alive included. */
|
|
123
|
+
interface HttpParserSession {
|
|
124
|
+
readonly serve: (socket: net.Socket) => void;
|
|
125
|
+
readonly close: () => void;
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* 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.
|
|
129
|
+
*/
|
|
130
|
+
export interface ConnectEffects {
|
|
131
|
+
/** 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. */
|
|
132
|
+
readonly listenLoopback: (preferredPort: number | undefined, onSocket: (socket: net.Socket) => void) => Promise<ConnectListenerHandle>;
|
|
133
|
+
/** Builds the TLS terminator for one leaf, handing each successfully handshaked session to `onSecure`. */
|
|
134
|
+
readonly createTlsAcceptor: (leaf: LeafCert, onSecure: (secure: net.Socket) => void) => TlsAcceptor;
|
|
135
|
+
/** Builds the HTTP parser bound to one request handler. */
|
|
136
|
+
readonly createHttpSession: (handler: ConnectRequestHandler) => HttpParserSession;
|
|
137
|
+
/** Opens a raw TCP connection for a blind tunnel. */
|
|
138
|
+
readonly connectTcp: (host: string, port: number) => Promise<net.Socket>;
|
|
139
|
+
/** Streams one request to `target` and the response back, never buffering a body. */
|
|
140
|
+
readonly forwardHttp: (target: ConnectForwardTarget, request: IncomingMessage, response: ServerResponse) => void;
|
|
141
|
+
}
|
|
142
|
+
/** Everything the connect surface needs to route, resolved before it starts. */
|
|
143
|
+
export interface ConnectServerConfig {
|
|
144
|
+
/** The CONNECT host whose TLS gets terminated. */
|
|
145
|
+
readonly interceptHost: string;
|
|
146
|
+
/**
|
|
147
|
+
* 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.
|
|
148
|
+
*/
|
|
149
|
+
readonly serveRouted: ConnectRequestHandler;
|
|
150
|
+
/** The leaf to terminate `interceptHost` with, minted and cached per host. */
|
|
151
|
+
readonly leafFor: (host: string) => LeafCert;
|
|
152
|
+
/** Where non-routed paths on the terminated session are piped: the real upstream over TLS. */
|
|
153
|
+
readonly upstream: ConnectForwardTarget;
|
|
154
|
+
/** 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. */
|
|
155
|
+
readonly isLiveCapability: (token: string) => boolean;
|
|
156
|
+
/** The connection deadlines and caps; production passes `CONNECT_LIMITS`. */
|
|
157
|
+
readonly limits: ConnectLimits;
|
|
158
|
+
}
|
|
159
|
+
/** A running connect surface. */
|
|
160
|
+
export interface ConnectServerHandle {
|
|
161
|
+
readonly port: number;
|
|
162
|
+
/** Stops the listener, drops every live tunnel and terminated session, and resolves once the port is released. */
|
|
163
|
+
readonly close: () => Promise<void>;
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* 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.
|
|
167
|
+
*
|
|
168
|
+
* 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.
|
|
169
|
+
*/
|
|
170
|
+
export declare function startConnectServer(config: ConnectServerConfig, effects: ConnectEffects, preferredPort: number | undefined): Promise<ConnectServerHandle>;
|
|
171
|
+
/** 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. */
|
|
172
|
+
export declare function realConnectEffects(): ConnectEffects;
|
|
173
|
+
/** 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. */
|
|
174
|
+
export declare function realConnectCertStore(paths: {
|
|
175
|
+
readonly frontdoorCaDir: string;
|
|
176
|
+
readonly frontdoorCaCertFile: string;
|
|
177
|
+
readonly frontdoorCaKeyFile: string;
|
|
178
|
+
}): ConnectCertStore;
|
|
179
|
+
export {};
|
|
@@ -0,0 +1,34 @@
|
|
|
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 claude-use's CA and a healthy answer, with no capability sent (see `probeFrontDoorSync`). */
|
|
19
|
+
readonly verifyListener: (port: number) => ListenerVerdict;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* 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).
|
|
23
|
+
*
|
|
24
|
+
* 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 claude-use'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.
|
|
25
|
+
*/
|
|
26
|
+
export declare function ensureFrontDoor(params: {
|
|
27
|
+
readonly paths: LayoutPaths;
|
|
28
|
+
readonly launcherPid: number;
|
|
29
|
+
readonly ports: EnsureFrontDoorPorts;
|
|
30
|
+
}): {
|
|
31
|
+
readonly port: number;
|
|
32
|
+
readonly connectPort: number;
|
|
33
|
+
readonly token: string;
|
|
34
|
+
};
|
|
@@ -0,0 +1,13 @@
|
|
|
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 claude-use'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 +1,12 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import type { HeadroomFs } from "../headroom/state";
|
|
3
|
+
/** The record held in one front-door session-registry file: the launcher's pid plus the per-launch capability token its child presents on every request. */
|
|
4
|
+
declare const FrontDoorSessionSchema: z.ZodObject<{
|
|
5
|
+
pid: z.ZodNumber;
|
|
6
|
+
startedAt: z.ZodNumber;
|
|
7
|
+
token: z.ZodString;
|
|
8
|
+
}, z.core.$strict>;
|
|
9
|
+
export type FrontDoorSession = z.infer<typeof FrontDoorSessionSchema>;
|
|
3
10
|
/**
|
|
4
11
|
* The front-door supervisor's state.json under `<home>/frontdoor/`. Every field is optional because the file exists in stages, exactly like headroom's and the old codex daemon's: a fresh supervisor writes its own pid before the listener is up, and a shut-down front door leaves only the sticky `lastPort` and any `lastError` worth surfacing.
|
|
5
12
|
*/
|
|
@@ -7,6 +14,10 @@ declare const FrontDoorStateSchema: z.ZodObject<{
|
|
|
7
14
|
supervisorPid: z.ZodOptional<z.ZodNumber>;
|
|
8
15
|
port: z.ZodOptional<z.ZodNumber>;
|
|
9
16
|
lastPort: z.ZodOptional<z.ZodNumber>;
|
|
17
|
+
connectPort: z.ZodOptional<z.ZodNumber>;
|
|
18
|
+
lastConnectPort: z.ZodOptional<z.ZodNumber>;
|
|
19
|
+
directPort: z.ZodOptional<z.ZodNumber>;
|
|
20
|
+
lastDirectPort: z.ZodOptional<z.ZodNumber>;
|
|
10
21
|
lastError: z.ZodOptional<z.ZodString>;
|
|
11
22
|
}, z.core.$strict>;
|
|
12
23
|
export type FrontDoorState = z.infer<typeof FrontDoorStateSchema>;
|
|
@@ -14,6 +25,26 @@ export type FrontDoorState = z.infer<typeof FrontDoorStateSchema>;
|
|
|
14
25
|
export declare function readFrontDoorState(fs: HeadroomFs, stateFile: string): FrontDoorState | undefined;
|
|
15
26
|
/** Writes state.json. A plain write, for the reason headroom's is: the file is advisory coordination read tolerantly, and the exclusive-create start lock is the actual mutual exclusion. */
|
|
16
27
|
export declare function writeFrontDoorState(fs: HeadroomFs, stateFile: string, state: Readonly<FrontDoorState>): void;
|
|
17
|
-
/**
|
|
28
|
+
/**
|
|
29
|
+
* The origin the headroom hop forwards routed traffic back to (the direct listener's), or undefined before the front door has ever served: what the headroom allowlist must admit for a provider routed through headroom, since headroom reaches the door's routes at exactly this address.
|
|
30
|
+
*/
|
|
18
31
|
export declare function frontDoorOrigin(state: FrontDoorState | undefined): string | undefined;
|
|
32
|
+
/**
|
|
33
|
+
* Writes one launch's session record: the registry entry that both keeps the door from idling out and holds the token that launch's requests must present. The token is a bearer capability, so the directory is created owner-only (0700) and the record written owner-only (0600): on a multi-user host another local account must be able neither to list nor to read it, since presenting it to a listener would spend the victim's provider credential or Codex login.
|
|
34
|
+
*/
|
|
35
|
+
export declare function writeFrontDoorSession(fs: HeadroomFs, sessionsDir: string, session: Readonly<FrontDoorSession>): void;
|
|
36
|
+
/** A registered launch as status and the idle decision see it: the pid and start time, never the token, so nothing that prints or serialises a session can leak the capability. */
|
|
37
|
+
export type FrontDoorSessionSummary = Pick<FrontDoorSession, "pid" | "startedAt">;
|
|
38
|
+
/**
|
|
39
|
+
* Every registered launch, unreadable or malformed files skipped, sorted by pid. Read through `FrontDoorSessionSchema`, which carries the token: headroom's token-less `listSessions` is strict and rejects every front-door record, which would make a live session invisible to the supervisor's idle decision and close the listeners (releasing the sticky ports) under a running child.
|
|
40
|
+
*/
|
|
41
|
+
export declare function listFrontDoorSessions(fs: HeadroomFs, sessionsDir: string): readonly FrontDoorSessionSummary[];
|
|
42
|
+
/** Removes one launcher's registry entry, and with it that launch's capability. Idempotent. */
|
|
43
|
+
export declare function removeFrontDoorSession(fs: HeadroomFs, sessionsDir: string, pid: number): void;
|
|
44
|
+
/** Removes every entry whose launcher is no longer running, so a dead launch's capability stops being accepted and cannot keep the door awake. The predicate must be zombie-aware, as for headroom's. */
|
|
45
|
+
export declare function pruneDeadFrontDoorSessions(fs: HeadroomFs, sessionsDir: string, isRunning: (pid: number) => boolean): readonly number[];
|
|
46
|
+
/**
|
|
47
|
+
* Every live session's token, read fresh: what a client-facing listener checks a request's capability against. Malformed files are skipped for the same reason `readFrontDoorState` tolerates them.
|
|
48
|
+
*/
|
|
49
|
+
export declare function liveSessionTokens(fs: HeadroomFs, sessionsDir: string): ReadonlySet<string>;
|
|
19
50
|
export {};
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { HeadroomFs } from "../headroom/state";
|
|
2
|
+
import type { LayoutPaths } from "../paths";
|
|
3
|
+
/** A bound front-door listener in this process, and how to stop it. */
|
|
4
|
+
interface FrontDoorListenerHandle {
|
|
5
|
+
readonly port: number;
|
|
6
|
+
/** Stops accepting, ends every live connection, and resolves once the port is released. */
|
|
7
|
+
readonly close: () => Promise<void>;
|
|
8
|
+
}
|
|
9
|
+
/** Every effect the front-door supervisor performs, injected so its whole lifecycle runs against fakes: no real process, port, clock or listener in a unit test. */
|
|
10
|
+
export interface FrontDoorSupervisorPorts {
|
|
11
|
+
readonly fs: HeadroomFs;
|
|
12
|
+
readonly paths: LayoutPaths;
|
|
13
|
+
readonly ownPid: number;
|
|
14
|
+
readonly now: () => number;
|
|
15
|
+
/** An event-loop-yielding wait, for the same reason headroom's is: nothing else would ever run between ticks. */
|
|
16
|
+
readonly sleep: (ms: number) => Promise<void>;
|
|
17
|
+
/** Zombie-aware liveness, the notion every coordination decision in this project uses. */
|
|
18
|
+
readonly isRunning: (pid: number) => boolean;
|
|
19
|
+
/**
|
|
20
|
+
* Starts the provider listener in this process on `preferredPort` (falling back to any free port when the sticky one is taken, decided at bind time rather than by a racy check-then-bind probe) and resolves once it has bound and answered its own health probe over TLS. This is the HTTPS listener every provider session's base URL points at, serving a loopback leaf signed by claude-use's CA.
|
|
21
|
+
*/
|
|
22
|
+
readonly startProviderListener: (preferredPort: number | undefined) => Promise<FrontDoorListenerHandle>;
|
|
23
|
+
/**
|
|
24
|
+
* Starts the CONNECT surface in this process on `preferredPort` (same bind-time fallback): the TLS-terminating listener an OAuth session's HTTPS_PROXY points at. Bound before any state names it, so an OAuth launch never finds one listener up without the other.
|
|
25
|
+
*/
|
|
26
|
+
readonly startConnectListener: (preferredPort: number | undefined) => Promise<FrontDoorListenerHandle>;
|
|
27
|
+
/**
|
|
28
|
+
* Starts the direct listener on `preferredPort` (same bind-time fallback): the same routes as the provider listener, minus the headroom hop, over plain HTTP because headroom is what connects to it. Headroom forwards routed traffic back here, which is what keeps the hop from looping, and the port is sticky like the others because headroom's allowlist admits its exact origin. It admits only the hop's own requests and holds no credential until it redeems one from the hop's custody.
|
|
29
|
+
*/
|
|
30
|
+
readonly startDirectListener: (preferredPort: number | undefined) => Promise<FrontDoorListenerHandle>;
|
|
31
|
+
readonly log: (line: string) => void;
|
|
32
|
+
}
|
|
33
|
+
/** Options for `runFrontDoorSupervisor`. */
|
|
34
|
+
export interface RunFrontDoorSupervisorOptions {
|
|
35
|
+
/** Bounds the loop's iterations so a test can observe steady state; such a run returns `FRONTDOOR_SUPERVISOR_STILL_RUNNING`. */
|
|
36
|
+
readonly tickLimit?: number;
|
|
37
|
+
}
|
|
38
|
+
/** Returned by a tick-bounded run that reached its limit while still serving. Never an exit code. */
|
|
39
|
+
export declare const FRONTDOOR_SUPERVISOR_STILL_RUNNING = -1;
|
|
40
|
+
/** The supervisor's tick rate for the idle clock. */
|
|
41
|
+
export declare const FRONTDOOR_POLL_MS = 1000;
|
|
42
|
+
/**
|
|
43
|
+
* Runs the front-door supervisor: binds all three listeners (the HTTPS provider listener, the CONNECT surface and the plain-HTTP direct listener, each on its sticky port), records the serving state, and shuts down after `idleShutdownMinutes` with an empty session registry. Returns the exit code: 0 for an idle shutdown, 1 for a fatal start failure.
|
|
44
|
+
*
|
|
45
|
+
* The listeners live in this process, so there is no child to keep alive: a crash of this process is a crash of every routed session's door at once, and recovery is the next launch's ensure spawning a replacement on the same sticky ports, which is exactly the failure model the issue asks to settle with tests. Keeping the headroom daemon alive is the headroom supervisor's job, not this one's; the two daemons idle out independently.
|
|
46
|
+
*/
|
|
47
|
+
export declare function runFrontDoorSupervisor(idleShutdownMinutes: number, ports: FrontDoorSupervisorPorts, options?: RunFrontDoorSupervisorOptions): Promise<number>;
|
|
48
|
+
export {};
|
|
@@ -18,7 +18,7 @@ export interface EnsureHeadroomPorts {
|
|
|
18
18
|
/**
|
|
19
19
|
* Brings the headroom daemon up for this launch, or finds it already running, and registers this launcher pid in its session registry (the fact that keeps the daemon alive and defers drift restarts until this launch is done).
|
|
20
20
|
*
|
|
21
|
-
* The exclusive-create start lock decides which of several concurrent launches spawns the one supervisor: its holder spawns and keeps waiting like everyone else, everyone else waits on state.json, and a lock whose holder has died is removed and retried. Readiness is "state names a live supervisor, a live headroom pid,
|
|
21
|
+
* The exclusive-create start lock decides which of several concurrent launches spawns the one supervisor: its holder spawns and keeps waiting like everyone else, everyone else waits on state.json, and a lock whose holder has died is removed and retried. Readiness is "state names a live supervisor, a live headroom pid, and a port", which the supervisor only writes after its own `/readyz` probe has passed, so polling state alone never mistakes a bound-but-not-ready port for a usable daemon.
|
|
22
22
|
*
|
|
23
23
|
* Throws `HeadroomStartError` (naming the daemon log path and any recorded `lastError`) when the daemon is not up within `HEADROOM_START_TIMEOUT_MS`, or when a live supervisor has recorded a fatal error.
|
|
24
24
|
*/
|
|
@@ -28,5 +28,4 @@ export declare function ensureHeadroom(params: {
|
|
|
28
28
|
readonly ports: EnsureHeadroomPorts;
|
|
29
29
|
}): {
|
|
30
30
|
readonly port: number;
|
|
31
|
-
readonly mitmPort: number;
|
|
32
31
|
};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { HeadroomGlobalConfig } from "../config/schema";
|
|
2
|
+
/** The token-saving settings of the `headroom` config block, without its install and lifecycle fields (`source`, `idleShutdownMinutes`). */
|
|
3
|
+
export type HeadroomSettings = Pick<HeadroomGlobalConfig, "mode" | "targetRatio" | "ccr" | "rolloutChannel" | "interceptToolResults" | "readMaturation">;
|
|
4
|
+
/** Picks the settings out of a headroom config block, leaving unset fields unset. */
|
|
5
|
+
export declare function settingsOf(config: Readonly<HeadroomGlobalConfig>): HeadroomSettings;
|
|
6
|
+
/**
|
|
7
|
+
* The `headroom proxy` arguments the settings ask for, after the supervisor's own `--host` and `--port`. Only settings that are set produce a flag, so an empty settings object leaves headroom on its own defaults. `ccr: "default"` is headroom's default and likewise adds nothing.
|
|
8
|
+
*/
|
|
9
|
+
export declare function settingsArgs(settings: Readonly<HeadroomSettings>): string[];
|
|
10
|
+
/** The environment the settings add to the daemon's: only the rollout channel, which headroom reads from `HEADROOM_ROLLOUT_CHANNEL` and has no flag for. */
|
|
11
|
+
export declare function settingsEnv(settings: Readonly<HeadroomSettings>): Record<string, string>;
|
|
12
|
+
/** A stable hash of the settings, so drift detection compares one short string against what the running daemon was started with. The same settings hash the same whatever order the config file wrote its keys in. */
|
|
13
|
+
export declare function hashSettings(settings: Readonly<HeadroomSettings>): string;
|
|
@@ -10,17 +10,16 @@ export declare const HeadroomStateSchema: z.ZodObject<{
|
|
|
10
10
|
supervisorPid: z.ZodOptional<z.ZodNumber>;
|
|
11
11
|
headroomPid: z.ZodOptional<z.ZodNumber>;
|
|
12
12
|
port: z.ZodOptional<z.ZodNumber>;
|
|
13
|
-
mitmPort: z.ZodOptional<z.ZodNumber>;
|
|
14
|
-
lastMitmPort: z.ZodOptional<z.ZodNumber>;
|
|
15
13
|
lastPort: z.ZodOptional<z.ZodNumber>;
|
|
16
14
|
version: z.ZodOptional<z.ZodString>;
|
|
17
15
|
allowlistHash: z.ZodOptional<z.ZodString>;
|
|
16
|
+
settingsHash: z.ZodOptional<z.ZodString>;
|
|
18
17
|
installedSource: z.ZodOptional<z.ZodString>;
|
|
19
18
|
lastError: z.ZodOptional<z.ZodString>;
|
|
20
19
|
}, z.core.$strict>;
|
|
21
20
|
export type HeadroomState = z.infer<typeof HeadroomStateSchema>;
|
|
22
21
|
/** The filesystem operations headroom coordination needs: the same shape as `FarmFs`, injected so every decision over state, sessions, and the start lock runs against an in-memory fake in tests. */
|
|
23
|
-
export type HeadroomFs = Pick<FarmFs, "mkdirp" | "readFileUtf8" | "writeFileUtf8" | "writeFileExclusive" | "readdir" | "removeRecursive">;
|
|
22
|
+
export type HeadroomFs = Pick<FarmFs, "mkdirp" | "mkdirPrivate" | "readFileUtf8" | "writeFileUtf8" | "writeFilePrivate" | "writeFileExclusive" | "readdir" | "removeRecursive">;
|
|
24
23
|
/** The record held in one session-registry file. */
|
|
25
24
|
declare const HeadroomSessionSchema: z.ZodObject<{
|
|
26
25
|
pid: z.ZodNumber;
|
|
@@ -44,7 +43,7 @@ export declare function headroomAllowlist(providers: readonly {
|
|
|
44
43
|
readonly baseUrl: string;
|
|
45
44
|
}[]): readonly string[];
|
|
46
45
|
/**
|
|
47
|
-
* The upstreams a set of providers routes to through headroom: every `http` provider's base URL, plus the front door's origin
|
|
46
|
+
* The upstreams a set of providers routes to through headroom: every `http` provider's base URL, plus the front door's direct origin once the door has ever served. A provider session routed through headroom is forwarded by the door's hop back through its own direct listener (whatever route serves there, the in-process translator or the pass-through), so headroom must admit that address like any provider's; the direct port is sticky, so its origin stays stable across restarts. Before the front door has ever served there is no address to admit, and the first routed launch starts it before bringing headroom up, so a freshly started headroom daemon already sees it.
|
|
48
47
|
*/
|
|
49
48
|
export declare function headroomUpstreams(providers: readonly Provider[], frontDoorOrigin: string | undefined): readonly {
|
|
50
49
|
readonly baseUrl: string;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
+
import { type HeadroomGlobalConfig } from "../config/schema";
|
|
1
2
|
import type { LayoutPaths } from "../paths";
|
|
2
|
-
import type
|
|
3
|
+
import { type HeadroomSettings } from "./settings";
|
|
3
4
|
import { type HeadroomFs } from "./state";
|
|
4
5
|
/** Everything the supervisor needs to run the daemon, resolved from the global config and the filesystem before it starts. */
|
|
5
6
|
export interface HeadroomSupervisorConfig {
|
|
@@ -7,12 +8,11 @@ export interface HeadroomSupervisorConfig {
|
|
|
7
8
|
readonly source: string;
|
|
8
9
|
/** `headroom.idleShutdownMinutes`, already defaulted. */
|
|
9
10
|
readonly idleShutdownMinutes: number;
|
|
11
|
+
/** The token-saving settings (`mode`, `targetRatio`, `ccr`, the rollout channel and its opt-ins), as configured; unset fields leave headroom on its own defaults. */
|
|
12
|
+
readonly settings: HeadroomSettings;
|
|
10
13
|
}
|
|
11
14
|
/** How the supervisor resolves the configured source against its defaults. */
|
|
12
|
-
export declare function resolveSupervisorConfig(configured:
|
|
13
|
-
readonly source?: string;
|
|
14
|
-
readonly idleShutdownMinutes?: number;
|
|
15
|
-
}): HeadroomSupervisorConfig;
|
|
15
|
+
export declare function resolveSupervisorConfig(configured: Readonly<HeadroomGlobalConfig>): HeadroomSupervisorConfig;
|
|
16
16
|
/**
|
|
17
17
|
* Every effect the supervisor performs, injected so the whole lifecycle is testable against fakes: no real process, port, clock, or HTTP call ever happens in a unit test.
|
|
18
18
|
*/
|
|
@@ -36,7 +36,9 @@ export interface SupervisorPorts {
|
|
|
36
36
|
/**
|
|
37
37
|
* Starts `headroom proxy` bound to `port` with the given allowlist, returning its pid. Output goes to the daemon log. The implementation must keep the ChildProcess handle and attach an exit listener (detaching the process is fine; unref'ing a child you still need events from is not, since without the listener nothing reaps it and it lingers as a zombie).
|
|
38
38
|
*/
|
|
39
|
-
readonly spawnHeadroom: (port: number, allowlist: readonly string[]) => number;
|
|
39
|
+
readonly spawnHeadroom: (port: number, allowlist: readonly string[], settings: Readonly<HeadroomSettings>) => number;
|
|
40
|
+
/** Reads the headroom config block as it stands on disk now. The supervisor calls it every tick, so a changed `source` or setting is noticed while the daemon runs and applied by the same quiet-registry restart an allowlist change gets. */
|
|
41
|
+
readonly readConfig: () => HeadroomSupervisorConfig;
|
|
40
42
|
/** Stops a process the supervisor owns, escalating SIGTERM to SIGKILL on a bounded timeout (see `stopSupervisedProcess`). */
|
|
41
43
|
readonly stopProcess: (pid: number) => void;
|
|
42
44
|
/** True once `GET /readyz` on the port succeeds. */
|
|
@@ -48,10 +50,6 @@ export interface SupervisorPorts {
|
|
|
48
50
|
};
|
|
49
51
|
/** The installed `headroom --version` output, or undefined when the binary is not on PATH. */
|
|
50
52
|
readonly headroomVersion: () => string | undefined;
|
|
51
|
-
/**
|
|
52
|
-
* Starts this supervisor's second server: the in-process MITM CONNECT proxy on a loopback port, binding `preferredPort` when it is free and any free port otherwise. The proxy forwards the intercept host's headroom-served paths to `headroomPort()` (read live, because the daemon can crash and restart on a different port while the proxy keeps listening) and tunnels everything else untouched. Resolves once listening; rejects when no port can be bound.
|
|
53
|
-
*/
|
|
54
|
-
readonly startMitm: (preferredPort: number | undefined, headroomPort: () => number | undefined) => Promise<MitmServerHandle>;
|
|
55
53
|
readonly log: (line: string) => void;
|
|
56
54
|
}
|
|
57
55
|
/** First retry delay after a failed start. Half a second keeps a transient blip invisible to the next launch. */
|
|
@@ -98,10 +96,10 @@ export interface RunSupervisorOptions {
|
|
|
98
96
|
/** Returned by a tick-bounded `runSupervisor` that reached its limit while still managing the daemon: not an exit code, and never produced without `tickLimit`. */
|
|
99
97
|
export declare const HEADROOM_SUPERVISOR_STILL_RUNNING = -1;
|
|
100
98
|
/**
|
|
101
|
-
* Runs the headroom supervisor loop: install
|
|
99
|
+
* Runs the headroom supervisor loop: install the daemon, start it, keep it alive (for crash or drift), and stop it after the configured idle period with an empty session registry. Returns the process exit code (0 for an idle shutdown, 1 for a fatal setup failure).
|
|
102
100
|
*
|
|
103
101
|
* Every effect flows through `ports`, so the whole loop is unit-testable with a fake clock, filesystem, and processes. The loop awaits its sleeps, which in the real implementation run on a timer: between ticks the event loop turns, the spawned child's exit event is delivered (and the child thereby reaped), and the next tick's liveness check reads the truth.
|
|
104
102
|
*/
|
|
105
|
-
export declare function runSupervisor(
|
|
103
|
+
export declare function runSupervisor(initialConfig: HeadroomSupervisorConfig, ports: SupervisorPorts, options?: RunSupervisorOptions): Promise<number>;
|
|
106
104
|
/** The allowlist as it stands right now: every provider's upstream (the front door's origin for a codex provider, which headroom forwards codex traffic back to) plus Claude Code's own API. */
|
|
107
105
|
export declare function currentHeadroomAllowlist(fs: HeadroomFs, paths: LayoutPaths): readonly string[];
|
package/dist/types/index.d.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* The library surface of claude-use: the pure, port-injected parts other tools can call in-process instead of shelling out to the CLI. Three groups, none of which imports interactive prompting or argument parsing:
|
|
3
3
|
*
|
|
4
4
|
* - identity and configuration-profile resolution and farm sync (`resolveDecisions`, `resyncFarm`, `buildEntryFacts`, ...), pure over facts and ports passed in;
|
|
5
|
-
* -
|
|
5
|
+
* - routing: the front door's Remote-Control-preserving, capability-authenticated CONNECT surface and its certificate authority, the front-door supervisor and ensure lifecycle, and the headroom supervisor and ensure lifecycle, each pure over its injected ports;
|
|
6
6
|
* - the ambient-credential guard.
|
|
7
7
|
*
|
|
8
8
|
* Everything the CLI alone needs (commander wiring, prompts, `doctor`, `check`) is deliberately not exported here.
|
|
@@ -10,7 +10,9 @@
|
|
|
10
10
|
export { resolveDecisions, topLevelNames, type ResolveDecisionsInput, type ResolvedState } from "./resolve/pipeline";
|
|
11
11
|
export type { CompiledRule, Decision, Diagnostic, EliminatedRule, EntryFact, EntryFacts, FlattenedCascade, Layer, LayerId } from "./resolve/types";
|
|
12
12
|
export { buildEntryFacts, carryOver, FARM_MANIFEST_FILENAME, readFarmManifest, recoverFarm, recoveryDiagnostics, resyncFarm, type BuildEntryFactsParams, type CarryOverParams, type CarryOverResult, type RecoverFarmParams, type RecoveryResult, type ResyncFarmParams, type ResyncFarmResult, } from "./launcher/farm";
|
|
13
|
-
export { createLeafCache, ensureCa, forwardableHeaders, generateCa,
|
|
13
|
+
export { CONNECT_INTERCEPT_HOST, CONNECT_LIMITS, createLeafCache, ensureCa, forwardableHeaders, generateCa, isInterceptedHost, mintLeaf, parseConnectTarget, realConnectCertStore, realConnectEffects, ROUTED_PATH_PREFIX, servedByPipeline, startConnectServer, type CaMaterial, type ConnectCertStore, type ConnectEffects, type ConnectLimits, type ConnectServerConfig, type ConnectServerHandle, type ConnectTarget, type LeafCert, } from "./frontdoor/connect";
|
|
14
|
+
export { ensureFrontDoor, FrontDoorStartError, type EnsureFrontDoorPorts } from "./frontdoor/ensure";
|
|
15
|
+
export { runFrontDoorSupervisor, type FrontDoorSupervisorPorts, type RunFrontDoorSupervisorOptions } from "./frontdoor/supervisor";
|
|
14
16
|
export { ensureHeadroom, HeadroomStartError, type EnsureHeadroomPorts } from "./headroom/ensure";
|
|
15
17
|
export { resolveSupervisorConfig, runSupervisor, type HeadroomSupervisorConfig, type RunSupervisorOptions, type SupervisorPorts } from "./headroom/supervisor";
|
|
16
18
|
export { AMBIENT_CREDENTIAL_VARS, detectAmbientCredential, evaluateAmbientCredentialGuard, formatAmbientCredentialGuardMessage, type AmbientCredentialDetection, type AmbientCredentialGuardResult, type AmbientCredentialVar, type EvaluateAmbientCredentialGuardParams, type InjectedCredential, } from "./launcher/guard";
|