agent-shim 0.0.0 → 7.1.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 (44) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +233 -0
  3. package/dist/cli.cjs +69549 -0
  4. package/dist/index.cjs +3620 -0
  5. package/dist/index.mjs +3545 -0
  6. package/dist/types/cliError.d.ts +27 -0
  7. package/dist/types/config/classify.d.ts +49 -0
  8. package/dist/types/config/load.d.ts +42 -0
  9. package/dist/types/config/schema.d.ts +746 -0
  10. package/dist/types/config/store.d.ts +46 -0
  11. package/dist/types/frontdoor/capability.d.ts +19 -0
  12. package/dist/types/frontdoor/connect.d.ts +179 -0
  13. package/dist/types/frontdoor/ensure.d.ts +34 -0
  14. package/dist/types/frontdoor/probe.d.ts +13 -0
  15. package/dist/types/frontdoor/state.d.ts +50 -0
  16. package/dist/types/frontdoor/supervisor.d.ts +48 -0
  17. package/dist/types/frontdoor/x509.d.ts +11 -0
  18. package/dist/types/headroom/ensure.d.ts +31 -0
  19. package/dist/types/headroom/settings.d.ts +13 -0
  20. package/dist/types/headroom/source.d.ts +8 -0
  21. package/dist/types/headroom/state.d.ts +77 -0
  22. package/dist/types/headroom/supervisor.d.ts +107 -0
  23. package/dist/types/index.d.ts +21 -0
  24. package/dist/types/launcher/farm.d.ts +165 -0
  25. package/dist/types/launcher/guard.d.ts +57 -0
  26. package/dist/types/launcher/lock.d.ts +41 -0
  27. package/dist/types/launcher/ports.d.ts +135 -0
  28. package/dist/types/legacy.d.ts +24 -0
  29. package/dist/types/paths.d.ts +79 -0
  30. package/dist/types/resolve/conditions.d.ts +37 -0
  31. package/dist/types/resolve/decide.d.ts +45 -0
  32. package/dist/types/resolve/extends.d.ts +41 -0
  33. package/dist/types/resolve/flatten.d.ts +13 -0
  34. package/dist/types/resolve/match.d.ts +58 -0
  35. package/dist/types/resolve/pipeline.d.ts +30 -0
  36. package/dist/types/resolve/plan.d.ts +57 -0
  37. package/dist/types/resolve/projects.d.ts +61 -0
  38. package/dist/types/resolve/reconcile.d.ts +80 -0
  39. package/dist/types/resolve/types.d.ts +159 -0
  40. package/dist/types/resolve/walk.d.ts +85 -0
  41. package/dist/types/usage/account.d.ts +20 -0
  42. package/dist/types/usage/read.d.ts +70 -0
  43. package/dist/types/usage/schema.d.ts +318 -0
  44. package/package.json +91 -2
@@ -0,0 +1,46 @@
1
+ import type { z } from "zod";
2
+ /**
3
+ * Filesystem primitives `store.ts` needs, injected so tests can simulate a crash between the temp-file write and the rename without touching real process semantics (e.g. actually killing the process mid-write). The default implementation (`nodeStoreFs`) is real `node:fs`, used by every CLI adapter in normal operation and by this module's own tests when exercising the real atomic-rename behaviour end to end.
4
+ */
5
+ export interface StoreFs {
6
+ /** Reads a file's contents as UTF-8 text, or returns undefined when it does not exist. */
7
+ readonly readFileUtf8: (filePath: string) => string | undefined;
8
+ /** Writes a file's full contents as UTF-8 text, creating or truncating it. */
9
+ readonly writeFileUtf8: (filePath: string, contents: string) => void;
10
+ /** Atomically renames `from` to `to` (same filesystem — both are always in the same directory in this module's own usage). */
11
+ readonly renameSync: (from: string, to: string) => void;
12
+ /** Creates a directory (and any missing parents), succeeding silently if it already exists. */
13
+ readonly mkdirSync: (dirPath: string) => void;
14
+ /** Removes a file, succeeding silently if it does not exist — used to clean up a stray temp file after a failed rename. */
15
+ readonly unlinkSync: (filePath: string) => void;
16
+ }
17
+ /** The real `node:fs`-backed implementation of `StoreFs`, used by every CLI adapter in normal operation. */
18
+ export declare const nodeStoreFs: StoreFs;
19
+ /**
20
+ * Reads and validates one JSON config file against `schema`.
21
+ *
22
+ * Returns undefined when the file does not exist. Throws `ConfigValidationError` (the same error type `src/config/load.ts` throws for every other config file in this project) when it exists but fails validation.
23
+ */
24
+ export declare function readJson<S extends z.ZodType>(filePath: string, schema: S, storeFs?: StoreFs): z.infer<S> | undefined;
25
+ /**
26
+ * Writes `contents` as UTF-8 text to `filePath` atomically: writes to a temp file in the *same directory* first, then renames it into place. A crash (or thrown error) between the write and the rename leaves the original file at `filePath` completely untouched — the failure mode this exists to prevent is a config file left half-written after an interrupted process.
27
+ *
28
+ * The temp file's name includes the destination's own basename, the process id, and a random UUID, so two concurrent writers (even to the same file, even from two processes) never collide on the same temp path.
29
+ */
30
+ export declare function writeTextAtomic(filePath: string, contents: string, storeFs?: StoreFs): void;
31
+ /** Writes `data` as pretty-printed JSON to `filePath`, via `writeTextAtomic`. */
32
+ export declare function writeJsonAtomic(filePath: string, data: unknown, storeFs?: StoreFs): void;
33
+ /** Inputs to `applyPatch`. */
34
+ export interface ApplyPatchOptions<T> {
35
+ /** Used as the base to merge `patch` into when `filePath` does not exist yet. Required for a file that may not exist yet (e.g. a config profile's own first `set` before it was ever written by `create`); omit only when the file is guaranteed to already exist. */
36
+ readonly defaults?: T;
37
+ readonly storeFs?: StoreFs;
38
+ }
39
+ /**
40
+ * Reads `filePath` (or falls back to `options.defaults` when it does not exist), shallow-merges `patch` over the top-level keys of the result, re-validates the merged object against `schema`, and writes it back atomically.
41
+ *
42
+ * The merge is deliberately shallow — only top-level keys of the parsed config are replaced wholesale by the corresponding key in `patch`. A caller that wants to add one key to a nested object (e.g. one more category inside `categories`) must read the existing nested object itself and pass the already-merged nested object as part of `patch`; `applyPatch` does not recurse into nested objects on its own.
43
+ *
44
+ * Throws when the file does not exist and no `defaults` were given, and re-throws `ConfigValidationError` (via the underlying `schema.parse`) when the merged result does not validate.
45
+ */
46
+ export declare function applyPatch<S extends z.ZodType>(filePath: string, schema: S, patch: Readonly<Partial<z.infer<S>>>, options?: ApplyPatchOptions<z.infer<S>>): z.infer<S>;
@@ -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-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;
@@ -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 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.
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. Built on `node:crypto` alone (see `x509.ts`), no openssl, so the same code works on every platform agent-shim 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 generating a 2048-bit RSA keypair costs noticeable time 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: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.
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 agent-shim'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 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.
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 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;
@@ -0,0 +1,50 @@
1
+ import { z } from "zod";
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>;
10
+ /**
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.
12
+ */
13
+ declare const FrontDoorStateSchema: z.ZodObject<{
14
+ supervisorPid: z.ZodOptional<z.ZodNumber>;
15
+ port: z.ZodOptional<z.ZodNumber>;
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>;
21
+ lastError: z.ZodOptional<z.ZodString>;
22
+ }, z.core.$strict>;
23
+ export type FrontDoorState = z.infer<typeof FrontDoorStateSchema>;
24
+ /** Reads state.json, or undefined when absent or malformed: a malformed file is overwritten by the next supervisor rather than failing every launch over one bad write. */
25
+ export declare function readFrontDoorState(fs: HeadroomFs, stateFile: string): FrontDoorState | undefined;
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. */
27
+ export declare function writeFrontDoorState(fs: HeadroomFs, stateFile: string, state: Readonly<FrontDoorState>): void;
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
+ */
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>;
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 agent-shim'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 {};
@@ -0,0 +1,11 @@
1
+ /** An IP address as the 4 or 16 octets an iPAddress GeneralName carries. */
2
+ export declare function ipBytes(address: string): Buffer;
3
+ /** A key pair in the PEM forms agent-shim persists. */
4
+ export interface PemKeyPair {
5
+ readonly certPem: string;
6
+ readonly keyPem: string;
7
+ }
8
+ /** Generates a self-signed CA certificate, CA-only by basicConstraints and keyUsage, with a random serial. */
9
+ export declare function issueCaCertificate(commonName: string, notBefore: Readonly<Date>, notAfter: Readonly<Date>): PemKeyPair;
10
+ /** Issues a server-auth leaf under a CA: its own RSA key, the first name as the common name, every name in the subjectAltName (an IP address as an iPAddress entry, because TLS clients match an IP host only against those). */
11
+ export declare function issueLeafCertificate(ca: PemKeyPair, names: readonly string[], notBefore: Readonly<Date>, notAfter: Readonly<Date>): PemKeyPair;
@@ -0,0 +1,31 @@
1
+ import { CliError } from "../cliError";
2
+ import type { LayoutPaths } from "../paths";
3
+ import { type HeadroomFs } from "./state";
4
+ /** Raised when the daemon cannot be brought up in time, or reports a fatal `lastError`: a launch that asked for headroom must never silently fall back to a direct connection. */
5
+ export declare class HeadroomStartError extends CliError {
6
+ constructor(message: string);
7
+ }
8
+ /** Every effect the ensure step performs, injected so it runs against fakes in tests. */
9
+ export interface EnsureHeadroomPorts {
10
+ readonly fs: HeadroomFs;
11
+ /** Zombie-aware liveness: a supervisor or daemon that exited without being reaped still answers signal 0 as alive, but will never serve a request or write state, so it must read as dead here (see `realIsProcessRunning`). */
12
+ readonly isRunning: (pid: number) => boolean;
13
+ readonly now: () => number;
14
+ readonly sleep: (ms: number) => void;
15
+ /** Spawns the detached supervisor process that owns the headroom daemon, returning its pid. */
16
+ readonly spawnSupervisor: (paths: LayoutPaths) => number;
17
+ }
18
+ /**
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
+ *
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
+ *
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
+ */
25
+ export declare function ensureHeadroom(params: {
26
+ readonly paths: LayoutPaths;
27
+ readonly launcherPid: number;
28
+ readonly ports: EnsureHeadroomPorts;
29
+ }): {
30
+ readonly port: number;
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;
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Whether an install spec resolves through a git ref that can move underneath it. A full commit SHA never moves; a branch name, a tag a maintainer can re-point, or no ref at all (the remote's default branch) can, and a rebased, renamed or deleted branch changes or breaks every fresh install. A registry requirement is not a git source.
3
+ */
4
+ export declare function isMovingGitSource(spec: string): boolean;
5
+ /** The full commit SHA an install spec pins, lowercased, or undefined when the spec is a registry requirement or follows a moving ref. */
6
+ export declare function pinnedGitCommit(spec: string): string | undefined;
7
+ /** The commit an install was built from, read from the text of its `direct_url.json`, or undefined when the file records no VCS commit (a registry install) or is not valid JSON. */
8
+ export declare function parseInstalledCommit(directUrlJson: string): string | undefined;
@@ -0,0 +1,77 @@
1
+ import { z } from "zod";
2
+ import { type Provider } from "../config/schema";
3
+ import type { FarmFs } from "../launcher/ports";
4
+ /** The upstream headroom serves when no provider's base URL claims the request: Claude Code's own API. */
5
+ export declare const HEADROOM_ANTHROPIC_UPSTREAM = "https://api.anthropic.com";
6
+ /**
7
+ * The headroom supervisor's state.json under `<home>/headroom/`. Every field is optional because the file exists in stages: a fresh supervisor writes only `supervisorPid` before the daemon is up, and a shut-down daemon leaves the file with everything cleared except any `lastError` worth surfacing.
8
+ */
9
+ export declare const HeadroomStateSchema: z.ZodObject<{
10
+ supervisorPid: z.ZodOptional<z.ZodNumber>;
11
+ headroomPid: z.ZodOptional<z.ZodNumber>;
12
+ port: z.ZodOptional<z.ZodNumber>;
13
+ lastPort: z.ZodOptional<z.ZodNumber>;
14
+ version: z.ZodOptional<z.ZodString>;
15
+ allowlistHash: z.ZodOptional<z.ZodString>;
16
+ settingsHash: z.ZodOptional<z.ZodString>;
17
+ installedSource: z.ZodOptional<z.ZodString>;
18
+ lastError: z.ZodOptional<z.ZodString>;
19
+ }, z.core.$strict>;
20
+ export type HeadroomState = z.infer<typeof HeadroomStateSchema>;
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. */
22
+ export type HeadroomFs = Pick<FarmFs, "mkdirp" | "mkdirPrivate" | "readFileUtf8" | "writeFileUtf8" | "writeFilePrivate" | "writeFileExclusive" | "readdir" | "removeRecursive">;
23
+ /**
24
+ * The record held in one session-registry file. `supervisorPid` names the supervisor whose daemon the launch connected to: several supervisors can be alive at once (a superseded one keeps serving the sessions it already has), and each must judge idle shutdown and drift restarts by its own sessions only, or none would ever retire.
25
+ */
26
+ declare const HeadroomSessionSchema: z.ZodObject<{
27
+ pid: z.ZodNumber;
28
+ startedAt: z.ZodNumber;
29
+ supervisorPid: z.ZodNumber;
30
+ }, z.core.$strict>;
31
+ export type HeadroomSession = z.infer<typeof HeadroomSessionSchema>;
32
+ /** The exclusive-create start lock's recorded holder. */
33
+ declare const HeadroomLockSchema: z.ZodObject<{
34
+ pid: z.ZodNumber;
35
+ at: z.ZodNumber;
36
+ }, z.core.$strict>;
37
+ export type HeadroomLock = z.infer<typeof HeadroomLockSchema>;
38
+ /** Reads state.json, or undefined when absent. A malformed file is treated as absent: the lock-and-spawn flow below overwrites it with a fresh supervisor's state rather than crashing every future launch over one bad write. */
39
+ export declare function readHeadroomState(fs: HeadroomFs, stateFile: string): HeadroomState | undefined;
40
+ /**
41
+ * Writes state.json atomically (a temporary sibling renamed into place), so a concurrent reader never sees a partial file. A reader treats a malformed file as absent, and a launcher that finds no supervisor in state spawns one, so a torn write read mid-flight started a redundant supervisor and daemon.
42
+ */
43
+ export declare function writeHeadroomState(fs: HeadroomFs, stateFile: string, state: Readonly<HeadroomState>): void;
44
+ /** The allowlist the daemon is started with: every provider's base URL plus Claude Code's own API, so a session with no provider selected reaches the real Anthropic upstream through the same proxy. */
45
+ export declare function headroomAllowlist(providers: readonly {
46
+ readonly baseUrl: string;
47
+ }[]): readonly string[];
48
+ /**
49
+ * 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.
50
+ */
51
+ export declare function headroomUpstreams(providers: readonly Provider[], frontDoorOrigin: string | undefined): readonly {
52
+ readonly baseUrl: string;
53
+ }[];
54
+ /** A stable hash of an allowlist, so drift detection compares one short string instead of recomputing set equality against a running process it cannot interrogate. */
55
+ export declare function hashAllowlist(allowlist: readonly string[]): string;
56
+ /** Every provider definition under `providersDir`, best-effort: unreadable files are skipped, because the allowlist's job is to open every upstream a launch might use, not to fail a launch over one broken file. */
57
+ export declare function readAllProviders(fs: HeadroomFs, providersDir: string): readonly {
58
+ name: string;
59
+ provider: Provider;
60
+ }[];
61
+ /** One live session-registry entry, or undefined when the launcher pid has no file. */
62
+ export declare function readSession(fs: HeadroomFs, sessionsDir: string, pid: number): HeadroomSession | undefined;
63
+ /** Registers this launch in the session registry: the fact the daemon must keep serving, and the fact whose disappearance starts the idle shutdown clock. */
64
+ export declare function writeSession(fs: HeadroomFs, sessionsDir: string, session: Readonly<HeadroomSession>): void;
65
+ /** Removes a launcher's registry entry. Idempotent: an absent entry is not an error, because the supervisor's own pruning may have removed it first. */
66
+ export declare function removeSession(fs: HeadroomFs, sessionsDir: string, pid: number): void;
67
+ /** Every registry entry on disk, unreadable files skipped, sorted by pid: what `headroom status` prints and what the supervisor's idle and drift decisions read. */
68
+ export declare function listSessions(fs: HeadroomFs, sessionsDir: string): readonly HeadroomSession[];
69
+ /** The registry entries recorded against one supervisor: the sessions whose daemon that supervisor is responsible for keeping up. */
70
+ export declare function sessionsForSupervisor(fs: HeadroomFs, sessionsDir: string, supervisorPid: number): readonly HeadroomSession[];
71
+ /**
72
+ * Removes every registry entry whose pid is no longer running: a launcher that died without releasing its entry must not keep the daemon awake or block a drift restart forever. The predicate must be zombie-aware (see `realIsProcessRunning`): a launcher that exited but was never reaped still answers signal 0 as alive, which would keep its session registered indefinitely.
73
+ */
74
+ export declare function pruneDeadSessions(fs: HeadroomFs, sessionsDir: string, isRunning: (pid: number) => boolean): readonly number[];
75
+ /** Reads the start lock, or undefined when absent or unreadable. */
76
+ export declare function readStartLock(fs: HeadroomFs, lockFile: string): HeadroomLock | undefined;
77
+ export {};