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
package/dist/index.mjs
CHANGED
|
@@ -155,11 +155,36 @@ var PortableConfigSchema = ConfigProfileSchema.omit({ description: true }).exten
|
|
|
155
155
|
identity: z.string().min(1).optional(),
|
|
156
156
|
when: WhenSchema.optional()
|
|
157
157
|
});
|
|
158
|
-
var
|
|
158
|
+
var HEADROOM_MODES = ["cache", "token"];
|
|
159
|
+
var HEADROOM_CCR_MODES = ["default", "lossless", "none"];
|
|
160
|
+
var HEADROOM_ROLLOUT_CHANNELS = ["beta", "canary", "dev"];
|
|
161
|
+
var INTERCEPT_CHANNELS = ["canary", "dev"];
|
|
162
|
+
var READ_MATURATION_CHANNELS = ["beta", "dev"];
|
|
163
|
+
var HeadroomGlobalConfigObjectSchema = z.strictObject({
|
|
159
164
|
/** Install spec handed to `uv tool install`. Defaults to HEADROOM_DEFAULT_SOURCE. */
|
|
160
165
|
source: z.string().min(1).optional(),
|
|
161
166
|
/** How long the daemon may sit with no registered sessions before the supervisor stops it. Defaults to HEADROOM_DEFAULT_IDLE_SHUTDOWN_MINUTES. */
|
|
162
|
-
idleShutdownMinutes: z.number().int().positive().optional()
|
|
167
|
+
idleShutdownMinutes: z.number().int().positive().optional(),
|
|
168
|
+
/** Optimisation mode, passed as `--mode`. Unset leaves headroom's own default (`cache`). */
|
|
169
|
+
mode: z.enum(HEADROOM_MODES).optional(),
|
|
170
|
+
/** Keep-ratio for prose and code compression, passed as `--target-ratio`: lower is more aggressive. Unset lets headroom decide. */
|
|
171
|
+
targetRatio: z.number().gt(0).max(1).optional(),
|
|
172
|
+
/** How compressed tool output stays recoverable. Unset leaves headroom's own default (`default`). */
|
|
173
|
+
ccr: z.enum(HEADROOM_CCR_MODES).optional(),
|
|
174
|
+
/** Headroom's experimental release channel, exported as `HEADROOM_ROLLOUT_CHANNEL`. Required by `interceptToolResults` and `readMaturation`, which are refused without a channel that unlocks them. */
|
|
175
|
+
rolloutChannel: z.enum(HEADROOM_ROLLOUT_CHANNELS).optional(),
|
|
176
|
+
/** Opt in to headroom's tool-result interceptors (`--intercept-tool-results`). Needs `rolloutChannel` canary or dev. */
|
|
177
|
+
interceptToolResults: z.boolean().optional(),
|
|
178
|
+
/** Opt in to headroom's experimental read maturation (`--read-maturation`). Needs `rolloutChannel` beta or dev. */
|
|
179
|
+
readMaturation: z.boolean().optional()
|
|
180
|
+
});
|
|
181
|
+
var HeadroomGlobalConfigSchema = HeadroomGlobalConfigObjectSchema.superRefine((config, context) => {
|
|
182
|
+
if (config.interceptToolResults === true && !INTERCEPT_CHANNELS.includes(config.rolloutChannel ?? "")) {
|
|
183
|
+
context.addIssue({ code: "custom", path: ["interceptToolResults"], message: "interceptToolResults needs rolloutChannel canary or dev" });
|
|
184
|
+
}
|
|
185
|
+
if (config.readMaturation === true && !READ_MATURATION_CHANNELS.includes(config.rolloutChannel ?? "")) {
|
|
186
|
+
context.addIssue({ code: "custom", path: ["readMaturation"], message: "readMaturation needs rolloutChannel beta or dev" });
|
|
187
|
+
}
|
|
163
188
|
});
|
|
164
189
|
var HEADROOM_DEFAULT_SOURCE = "headroom-ai[proxy] @ git+https://github.com/ExaDev/headroom@eb02fa4126450c9905a49394fdec19ae2e7c9c30";
|
|
165
190
|
var HEADROOM_DEFAULT_IDLE_SHUTDOWN_MINUTES = 15;
|
|
@@ -1733,15 +1758,213 @@ function recoveryDiagnostics(recovery, identity) {
|
|
|
1733
1758
|
];
|
|
1734
1759
|
}
|
|
1735
1760
|
|
|
1736
|
-
// src/
|
|
1761
|
+
// src/frontdoor/connect.ts
|
|
1737
1762
|
import * as fs from "node:fs";
|
|
1738
1763
|
import * as http from "node:http";
|
|
1739
1764
|
import * as https from "node:https";
|
|
1740
1765
|
import * as net from "node:net";
|
|
1741
1766
|
import * as tls from "node:tls";
|
|
1742
1767
|
import forge from "node-forge";
|
|
1743
|
-
|
|
1744
|
-
|
|
1768
|
+
|
|
1769
|
+
// src/frontdoor/capability.ts
|
|
1770
|
+
var CONNECT_PROXY_REALM = "claude-use front door";
|
|
1771
|
+
var BASIC_SCHEME = "basic";
|
|
1772
|
+
function capabilityFromProxyAuthorization(value) {
|
|
1773
|
+
const trimmed = value.trim();
|
|
1774
|
+
const space = trimmed.indexOf(" ");
|
|
1775
|
+
if (space === -1 || trimmed.slice(0, space).toLowerCase() !== BASIC_SCHEME) {
|
|
1776
|
+
return void 0;
|
|
1777
|
+
}
|
|
1778
|
+
const userPass = Buffer.from(trimmed.slice(space + 1).trim(), "base64").toString("utf8");
|
|
1779
|
+
const colon = userPass.indexOf(":");
|
|
1780
|
+
if (colon === -1) {
|
|
1781
|
+
return void 0;
|
|
1782
|
+
}
|
|
1783
|
+
const password = userPass.slice(colon + 1);
|
|
1784
|
+
return password === "" ? void 0 : password;
|
|
1785
|
+
}
|
|
1786
|
+
|
|
1787
|
+
// src/frontdoor/state.ts
|
|
1788
|
+
import path5 from "node:path";
|
|
1789
|
+
import { z as z3 } from "zod";
|
|
1790
|
+
var FrontDoorSessionSchema = z3.strictObject({ pid: z3.number().int().positive(), startedAt: z3.number(), token: z3.string().min(1) });
|
|
1791
|
+
var FrontDoorStateSchema = z3.strictObject({
|
|
1792
|
+
/** The supervisor process serving the front door. Alive means both listeners are up in that process. */
|
|
1793
|
+
supervisorPid: z3.number().int().positive().optional(),
|
|
1794
|
+
/** The loopback port the HTTPS provider listener serves on. Absent until the listener has bound and answered its health probe, so "port is set" is itself the ready signal a launcher polls for. */
|
|
1795
|
+
port: z3.number().int().positive().optional(),
|
|
1796
|
+
/**
|
|
1797
|
+
* The sticky port preference for the provider listener: kept across crashes, restarts and idle shutdowns so every provider session, whose base URL was frozen at launch, keeps finding the front door at the same address.
|
|
1798
|
+
*/
|
|
1799
|
+
lastPort: z3.number().int().positive().optional(),
|
|
1800
|
+
/**
|
|
1801
|
+
* The loopback port the CONNECT surface listens on. Absent whenever no supervisor is serving it, so "connectPort is set" is the ready signal an OAuth launch polls for alongside `port`.
|
|
1802
|
+
*/
|
|
1803
|
+
connectPort: z3.number().int().positive().optional(),
|
|
1804
|
+
/**
|
|
1805
|
+
* The sticky port preference for the CONNECT surface, the exact analogue of `lastPort` for the second listener: it survives every shutdown, because every OAuth session's environment was frozen at launch with HTTPS_PROXY pointing at this address and a restart that moves strands them.
|
|
1806
|
+
*/
|
|
1807
|
+
lastConnectPort: z3.number().int().positive().optional(),
|
|
1808
|
+
/**
|
|
1809
|
+
* The loopback port the direct listener serves on: the listener a headroom hop tells the daemon to forward routed traffic back to. Sticky like the others, because headroom's allowlist admits its exact origin and an ephemeral port would invalidate the allowlist on every restart.
|
|
1810
|
+
*/
|
|
1811
|
+
directPort: z3.number().int().positive().optional(),
|
|
1812
|
+
/** The sticky port preference for the direct listener, for the reason `directPort` gives. */
|
|
1813
|
+
lastDirectPort: z3.number().int().positive().optional(),
|
|
1814
|
+
/** The last fatal error, kept across shutdowns so `frontdoor status` can explain a front door that is not running. */
|
|
1815
|
+
lastError: z3.string().optional()
|
|
1816
|
+
});
|
|
1817
|
+
function readFrontDoorState(fs2, stateFile) {
|
|
1818
|
+
let raw;
|
|
1819
|
+
try {
|
|
1820
|
+
raw = fs2.readFileUtf8(stateFile);
|
|
1821
|
+
} catch {
|
|
1822
|
+
return void 0;
|
|
1823
|
+
}
|
|
1824
|
+
if (raw === void 0 || raw.trim() === "") {
|
|
1825
|
+
return void 0;
|
|
1826
|
+
}
|
|
1827
|
+
try {
|
|
1828
|
+
const parsed = FrontDoorStateSchema.safeParse(JSON.parse(raw));
|
|
1829
|
+
return parsed.success ? parsed.data : void 0;
|
|
1830
|
+
} catch {
|
|
1831
|
+
return void 0;
|
|
1832
|
+
}
|
|
1833
|
+
}
|
|
1834
|
+
function writeFrontDoorState(fs2, stateFile, state) {
|
|
1835
|
+
fs2.mkdirp(path5.dirname(stateFile));
|
|
1836
|
+
fs2.writeFileUtf8(stateFile, `${JSON.stringify(state, null, 2)}
|
|
1837
|
+
`);
|
|
1838
|
+
}
|
|
1839
|
+
function frontDoorOrigin(state) {
|
|
1840
|
+
const port = state?.directPort ?? state?.lastDirectPort;
|
|
1841
|
+
return port === void 0 ? void 0 : `http://127.0.0.1:${String(port)}`;
|
|
1842
|
+
}
|
|
1843
|
+
function writeFrontDoorSession(fs2, sessionsDir, session) {
|
|
1844
|
+
fs2.mkdirPrivate(sessionsDir);
|
|
1845
|
+
fs2.writeFilePrivate(path5.join(sessionsDir, `${String(session.pid)}.json`), `${JSON.stringify(session, null, 2)}
|
|
1846
|
+
`);
|
|
1847
|
+
}
|
|
1848
|
+
function listFrontDoorSessions(fs2, sessionsDir) {
|
|
1849
|
+
const sessions = [];
|
|
1850
|
+
for (const name of fs2.readdir(sessionsDir)) {
|
|
1851
|
+
if (!name.endsWith(".json")) {
|
|
1852
|
+
continue;
|
|
1853
|
+
}
|
|
1854
|
+
try {
|
|
1855
|
+
const parsed = FrontDoorSessionSchema.safeParse(JSON.parse(fs2.readFileUtf8(path5.join(sessionsDir, name)) ?? ""));
|
|
1856
|
+
if (parsed.success) {
|
|
1857
|
+
sessions.push({ pid: parsed.data.pid, startedAt: parsed.data.startedAt });
|
|
1858
|
+
}
|
|
1859
|
+
} catch {
|
|
1860
|
+
continue;
|
|
1861
|
+
}
|
|
1862
|
+
}
|
|
1863
|
+
return sessions.sort((a, b) => a.pid - b.pid);
|
|
1864
|
+
}
|
|
1865
|
+
function removeFrontDoorSession(fs2, sessionsDir, pid) {
|
|
1866
|
+
fs2.removeRecursive(path5.join(sessionsDir, `${String(pid)}.json`));
|
|
1867
|
+
}
|
|
1868
|
+
function pruneDeadFrontDoorSessions(fs2, sessionsDir, isRunning) {
|
|
1869
|
+
const removed = [];
|
|
1870
|
+
for (const session of listFrontDoorSessions(fs2, sessionsDir)) {
|
|
1871
|
+
if (!isRunning(session.pid)) {
|
|
1872
|
+
removeFrontDoorSession(fs2, sessionsDir, session.pid);
|
|
1873
|
+
removed.push(session.pid);
|
|
1874
|
+
}
|
|
1875
|
+
}
|
|
1876
|
+
return removed;
|
|
1877
|
+
}
|
|
1878
|
+
|
|
1879
|
+
// src/frontdoor/supervisor.ts
|
|
1880
|
+
var FRONTDOOR_SUPERVISOR_STILL_RUNNING = -1;
|
|
1881
|
+
var FRONTDOOR_POLL_MS = 1e3;
|
|
1882
|
+
var MS_PER_MINUTE = 6e4;
|
|
1883
|
+
async function runFrontDoorSupervisor(idleShutdownMinutes, ports, options = {}) {
|
|
1884
|
+
const { fs: fs2, paths } = ports;
|
|
1885
|
+
const previous = readFrontDoorState(fs2, paths.frontdoorStateFile);
|
|
1886
|
+
let lastPort = previous?.lastPort;
|
|
1887
|
+
let lastConnectPort = previous?.lastConnectPort;
|
|
1888
|
+
let lastDirectPort = previous?.lastDirectPort;
|
|
1889
|
+
const sticky = () => ({
|
|
1890
|
+
...lastPort === void 0 ? {} : { lastPort },
|
|
1891
|
+
...lastConnectPort === void 0 ? {} : { lastConnectPort },
|
|
1892
|
+
...lastDirectPort === void 0 ? {} : { lastDirectPort }
|
|
1893
|
+
});
|
|
1894
|
+
const fail = (message) => {
|
|
1895
|
+
writeFrontDoorState(fs2, paths.frontdoorStateFile, { lastError: message, ...sticky() });
|
|
1896
|
+
fs2.removeRecursive(paths.frontdoorLockFile);
|
|
1897
|
+
ports.log(`claude-use frontdoor supervisor: ${message}`);
|
|
1898
|
+
return 1;
|
|
1899
|
+
};
|
|
1900
|
+
writeFrontDoorState(fs2, paths.frontdoorStateFile, { supervisorPid: ports.ownPid, ...sticky() });
|
|
1901
|
+
const previousSticky = lastPort;
|
|
1902
|
+
let http2;
|
|
1903
|
+
try {
|
|
1904
|
+
http2 = await ports.startProviderListener(lastPort);
|
|
1905
|
+
} catch (error) {
|
|
1906
|
+
return fail(`could not start the provider listener: ${error instanceof Error ? error.message : String(error)}`);
|
|
1907
|
+
}
|
|
1908
|
+
if (previousSticky !== void 0 && http2.port !== previousSticky) {
|
|
1909
|
+
ports.log(`claude-use frontdoor supervisor: sticky port ${String(previousSticky)} was occupied; moving to ${String(http2.port)} (sessions launched against the old port are stale until they relaunch)`);
|
|
1910
|
+
}
|
|
1911
|
+
lastPort = http2.port;
|
|
1912
|
+
const previousStickyConnect = lastConnectPort;
|
|
1913
|
+
let connect2;
|
|
1914
|
+
try {
|
|
1915
|
+
connect2 = await ports.startConnectListener(lastConnectPort);
|
|
1916
|
+
} catch (error) {
|
|
1917
|
+
await http2.close();
|
|
1918
|
+
return fail(`could not start the CONNECT surface: ${error instanceof Error ? error.message : String(error)}`);
|
|
1919
|
+
}
|
|
1920
|
+
if (previousStickyConnect !== void 0 && connect2.port !== previousStickyConnect) {
|
|
1921
|
+
ports.log(`claude-use frontdoor supervisor: sticky CONNECT port ${String(previousStickyConnect)} was occupied; moving to ${String(connect2.port)} (OAuth sessions launched against the old port are stale until they relaunch)`);
|
|
1922
|
+
}
|
|
1923
|
+
lastConnectPort = connect2.port;
|
|
1924
|
+
const previousStickyDirect = lastDirectPort;
|
|
1925
|
+
let direct;
|
|
1926
|
+
try {
|
|
1927
|
+
direct = await ports.startDirectListener(lastDirectPort);
|
|
1928
|
+
} catch (error) {
|
|
1929
|
+
await connect2.close();
|
|
1930
|
+
await http2.close();
|
|
1931
|
+
return fail(`could not start the direct listener: ${error instanceof Error ? error.message : String(error)}`);
|
|
1932
|
+
}
|
|
1933
|
+
if (previousStickyDirect !== void 0 && direct.port !== previousStickyDirect) {
|
|
1934
|
+
ports.log(`claude-use frontdoor supervisor: sticky direct port ${String(previousStickyDirect)} was occupied; moving to ${String(direct.port)} (headroom must be restarted to admit the new address)`);
|
|
1935
|
+
}
|
|
1936
|
+
lastDirectPort = direct.port;
|
|
1937
|
+
writeFrontDoorState(fs2, paths.frontdoorStateFile, { supervisorPid: ports.ownPid, port: http2.port, connectPort: connect2.port, directPort: direct.port, ...sticky() });
|
|
1938
|
+
fs2.removeRecursive(paths.frontdoorLockFile);
|
|
1939
|
+
ports.log(`claude-use frontdoor supervisor ${String(ports.ownPid)}: front door on 127.0.0.1:${String(http2.port)}, CONNECT surface on 127.0.0.1:${String(connect2.port)}`);
|
|
1940
|
+
let idleSince;
|
|
1941
|
+
let ticks = 0;
|
|
1942
|
+
for (; ; ) {
|
|
1943
|
+
if (options.tickLimit !== void 0 && ticks >= options.tickLimit) {
|
|
1944
|
+
return FRONTDOOR_SUPERVISOR_STILL_RUNNING;
|
|
1945
|
+
}
|
|
1946
|
+
ticks += 1;
|
|
1947
|
+
pruneDeadFrontDoorSessions(fs2, paths.frontdoorSessionsDir, ports.isRunning);
|
|
1948
|
+
if (listFrontDoorSessions(fs2, paths.frontdoorSessionsDir).length === 0) {
|
|
1949
|
+
idleSince ??= ports.now();
|
|
1950
|
+
if (ports.now() - idleSince >= idleShutdownMinutes * MS_PER_MINUTE) {
|
|
1951
|
+
ports.log(`claude-use frontdoor supervisor: no sessions for ${String(idleShutdownMinutes)} minute(s); closing the front door and exiting`);
|
|
1952
|
+
await direct.close();
|
|
1953
|
+
await connect2.close();
|
|
1954
|
+
await http2.close();
|
|
1955
|
+
writeFrontDoorState(fs2, paths.frontdoorStateFile, sticky());
|
|
1956
|
+
return 0;
|
|
1957
|
+
}
|
|
1958
|
+
} else {
|
|
1959
|
+
idleSince = void 0;
|
|
1960
|
+
}
|
|
1961
|
+
await ports.sleep(FRONTDOOR_POLL_MS);
|
|
1962
|
+
}
|
|
1963
|
+
}
|
|
1964
|
+
|
|
1965
|
+
// src/frontdoor/connect.ts
|
|
1966
|
+
var CONNECT_INTERCEPT_HOST = "api.anthropic.com";
|
|
1967
|
+
var ROUTED_PATH_PREFIX = "/v1/";
|
|
1745
1968
|
var HTTPS_PORT = 443;
|
|
1746
1969
|
var MAX_PORT = 65535;
|
|
1747
1970
|
var SERIAL_NUMBER_BYTES = 16;
|
|
@@ -1751,9 +1974,22 @@ var CONNECT_LINE_TOKENS = 3;
|
|
|
1751
1974
|
var HEAD_TERMINATOR = "\r\n\r\n";
|
|
1752
1975
|
var MAX_CONNECT_HEAD_BYTES = 8192;
|
|
1753
1976
|
var CONNECT_ESTABLISHED = "HTTP/1.1 200 Connection Established\r\n\r\n";
|
|
1977
|
+
var REFUSAL = {
|
|
1978
|
+
badRequest: "400 Bad Request",
|
|
1979
|
+
proxyAuthenticationRequired: "407 Proxy Authentication Required",
|
|
1980
|
+
requestTimeout: "408 Request Timeout",
|
|
1981
|
+
headTooLarge: "431 Request Header Fields Too Large",
|
|
1982
|
+
serviceUnavailable: "503 Service Unavailable"
|
|
1983
|
+
};
|
|
1984
|
+
var CONNECT_LIMITS = {
|
|
1985
|
+
headDeadlineMs: 1e4,
|
|
1986
|
+
maxPendingHeads: 64,
|
|
1987
|
+
maxTunnels: 1024,
|
|
1988
|
+
revalidateMs: FRONTDOOR_POLL_MS
|
|
1989
|
+
};
|
|
1754
1990
|
var CA_VALIDITY_DAYS = 3650;
|
|
1755
1991
|
var LEAF_VALIDITY_DAYS = 397;
|
|
1756
|
-
var CA_SUBJECT_COMMON_NAME = "claude-use
|
|
1992
|
+
var CA_SUBJECT_COMMON_NAME = "claude-use front door CA";
|
|
1757
1993
|
var MS_PER_DAY = 864e5;
|
|
1758
1994
|
function parseConnectTarget(authority) {
|
|
1759
1995
|
const colon = authority.lastIndexOf(":");
|
|
@@ -1771,8 +2007,8 @@ function parseConnectTarget(authority) {
|
|
|
1771
2007
|
function isInterceptedHost(host, interceptHost) {
|
|
1772
2008
|
return host === interceptHost;
|
|
1773
2009
|
}
|
|
1774
|
-
function
|
|
1775
|
-
return
|
|
2010
|
+
function servedByPipeline(path7) {
|
|
2011
|
+
return path7?.startsWith(ROUTED_PATH_PREFIX) === true;
|
|
1776
2012
|
}
|
|
1777
2013
|
var HOP_BY_HOP_HEADERS = /* @__PURE__ */ new Set([
|
|
1778
2014
|
"connection",
|
|
@@ -1820,7 +2056,13 @@ function generateCa(now) {
|
|
|
1820
2056
|
cert.sign(keys.privateKey, forge.md.sha256.create());
|
|
1821
2057
|
return { certPem: forge.pki.certificateToPem(cert), keyPem: forge.pki.privateKeyToPem(keys.privateKey) };
|
|
1822
2058
|
}
|
|
1823
|
-
|
|
2059
|
+
var SAN_TYPE_DNS = 2;
|
|
2060
|
+
var SAN_TYPE_IP = 7;
|
|
2061
|
+
function mintLeaf(ca, names, now) {
|
|
2062
|
+
const [commonName] = names;
|
|
2063
|
+
if (commonName === void 0) {
|
|
2064
|
+
throw new Error("a leaf certificate needs at least one name");
|
|
2065
|
+
}
|
|
1824
2066
|
const caCert = forge.pki.certificateFromPem(ca.certPem);
|
|
1825
2067
|
const caKey = forge.pki.privateKeyFromPem(ca.keyPem);
|
|
1826
2068
|
const keys = forge.pki.rsa.generateKeyPair({ bits: 2048 });
|
|
@@ -1829,13 +2071,13 @@ function mintLeaf(ca, host, now) {
|
|
|
1829
2071
|
cert.serialNumber = forge.util.bytesToHex(forge.random.getBytesSync(SERIAL_NUMBER_BYTES));
|
|
1830
2072
|
cert.validity.notBefore = new Date(now.getTime() - MS_PER_DAY);
|
|
1831
2073
|
cert.validity.notAfter = new Date(now.getTime() + LEAF_VALIDITY_DAYS * MS_PER_DAY);
|
|
1832
|
-
cert.setSubject([{ name: "commonName", value:
|
|
2074
|
+
cert.setSubject([{ name: "commonName", value: commonName }]);
|
|
1833
2075
|
cert.setIssuer(caCert.subject.attributes);
|
|
1834
2076
|
cert.setExtensions([
|
|
1835
2077
|
{ name: "basicConstraints", cA: false, critical: true },
|
|
1836
2078
|
{ name: "keyUsage", digitalSignature: true, keyEncipherment: true, critical: true },
|
|
1837
2079
|
{ name: "extKeyUsage", serverAuth: true },
|
|
1838
|
-
{ name: "subjectAltName", altNames:
|
|
2080
|
+
{ name: "subjectAltName", altNames: names.map((name) => net.isIP(name) === 0 ? { type: SAN_TYPE_DNS, value: name } : { type: SAN_TYPE_IP, ip: name }) },
|
|
1839
2081
|
{ name: "subjectKeyIdentifier" },
|
|
1840
2082
|
{ name: "authorityKeyIdentifier", keyLocatorFields: [] }
|
|
1841
2083
|
]);
|
|
@@ -1849,7 +2091,7 @@ function createLeafCache(ca, now) {
|
|
|
1849
2091
|
if (existing !== void 0) {
|
|
1850
2092
|
return existing;
|
|
1851
2093
|
}
|
|
1852
|
-
const minted = mintLeaf(ca, host, now());
|
|
2094
|
+
const minted = mintLeaf(ca, [host], now());
|
|
1853
2095
|
cache.set(host, minted);
|
|
1854
2096
|
return minted;
|
|
1855
2097
|
};
|
|
@@ -1872,17 +2114,10 @@ function caParses(ca) {
|
|
|
1872
2114
|
return false;
|
|
1873
2115
|
}
|
|
1874
2116
|
}
|
|
1875
|
-
async function
|
|
2117
|
+
async function startConnectServer(config, effects, preferredPort) {
|
|
1876
2118
|
const handler = (request3, response) => {
|
|
1877
|
-
if (
|
|
1878
|
-
|
|
1879
|
-
if (daemonPort === void 0) {
|
|
1880
|
-
const body = "claude-use headroom: the daemon is restarting";
|
|
1881
|
-
response.writeHead(HTTP_BAD_GATEWAY, { "content-type": "text/plain", "content-length": String(body.length) });
|
|
1882
|
-
response.end(body);
|
|
1883
|
-
return;
|
|
1884
|
-
}
|
|
1885
|
-
effects.forwardHttp({ host: "127.0.0.1", port: daemonPort, tls: false }, request3, response);
|
|
2119
|
+
if (servedByPipeline(request3.url)) {
|
|
2120
|
+
config.serveRouted(request3, response);
|
|
1886
2121
|
return;
|
|
1887
2122
|
}
|
|
1888
2123
|
effects.forwardHttp(config.upstream, request3, response);
|
|
@@ -1891,50 +2126,153 @@ async function startMitmServer(config, effects, preferredPort) {
|
|
|
1891
2126
|
const tlsAcceptor = effects.createTlsAcceptor(config.leafFor(config.interceptHost), (secure) => {
|
|
1892
2127
|
httpSession.serve(secure);
|
|
1893
2128
|
});
|
|
2129
|
+
const ledger = createConnectionLedger(config.limits, config.isLiveCapability);
|
|
1894
2130
|
const listener = await effects.listenLoopback(preferredPort, (socket) => {
|
|
1895
|
-
handleConnect(socket, config, effects, tlsAcceptor);
|
|
2131
|
+
handleConnect(socket, { config, effects, tlsAcceptor, ledger });
|
|
1896
2132
|
});
|
|
1897
2133
|
return {
|
|
1898
2134
|
port: listener.port,
|
|
1899
2135
|
close: async () => {
|
|
2136
|
+
ledger.close();
|
|
1900
2137
|
await listener.close();
|
|
1901
2138
|
tlsAcceptor.close();
|
|
1902
2139
|
httpSession.close();
|
|
1903
2140
|
}
|
|
1904
2141
|
};
|
|
1905
2142
|
}
|
|
1906
|
-
function
|
|
2143
|
+
function createConnectionLedger(limits, isLiveCapability) {
|
|
2144
|
+
const pending = /* @__PURE__ */ new Map();
|
|
2145
|
+
const authenticated = /* @__PURE__ */ new Map();
|
|
2146
|
+
let tunnels = 0;
|
|
2147
|
+
const leavePending = (socket) => {
|
|
2148
|
+
const deadline = pending.get(socket);
|
|
2149
|
+
if (deadline !== void 0) {
|
|
2150
|
+
clearTimeout(deadline);
|
|
2151
|
+
pending.delete(socket);
|
|
2152
|
+
}
|
|
2153
|
+
};
|
|
2154
|
+
const revalidation = setInterval(() => {
|
|
2155
|
+
for (const [capability, sockets] of authenticated) {
|
|
2156
|
+
if (!isLiveCapability(capability)) {
|
|
2157
|
+
for (const socket of sockets) {
|
|
2158
|
+
socket.destroy();
|
|
2159
|
+
}
|
|
2160
|
+
}
|
|
2161
|
+
}
|
|
2162
|
+
}, limits.revalidateMs);
|
|
2163
|
+
revalidation.unref();
|
|
2164
|
+
return {
|
|
2165
|
+
accept: (socket) => {
|
|
2166
|
+
const [oldest] = pending.keys();
|
|
2167
|
+
if (pending.size >= limits.maxPendingHeads && oldest !== void 0) {
|
|
2168
|
+
leavePending(oldest);
|
|
2169
|
+
refuse(oldest, REFUSAL.requestTimeout);
|
|
2170
|
+
}
|
|
2171
|
+
pending.set(
|
|
2172
|
+
socket,
|
|
2173
|
+
setTimeout(() => {
|
|
2174
|
+
leavePending(socket);
|
|
2175
|
+
refuse(socket, REFUSAL.requestTimeout);
|
|
2176
|
+
}, limits.headDeadlineMs)
|
|
2177
|
+
);
|
|
2178
|
+
socket.once("close", () => {
|
|
2179
|
+
leavePending(socket);
|
|
2180
|
+
});
|
|
2181
|
+
},
|
|
2182
|
+
headReceived: leavePending,
|
|
2183
|
+
establish: (socket, capability) => {
|
|
2184
|
+
if (tunnels >= limits.maxTunnels) {
|
|
2185
|
+
return false;
|
|
2186
|
+
}
|
|
2187
|
+
tunnels += 1;
|
|
2188
|
+
const sockets = authenticated.get(capability) ?? /* @__PURE__ */ new Set();
|
|
2189
|
+
sockets.add(socket);
|
|
2190
|
+
authenticated.set(capability, sockets);
|
|
2191
|
+
socket.once("close", () => {
|
|
2192
|
+
tunnels -= 1;
|
|
2193
|
+
sockets.delete(socket);
|
|
2194
|
+
if (sockets.size === 0 && authenticated.get(capability) === sockets) {
|
|
2195
|
+
authenticated.delete(capability);
|
|
2196
|
+
}
|
|
2197
|
+
});
|
|
2198
|
+
return true;
|
|
2199
|
+
},
|
|
2200
|
+
close: () => {
|
|
2201
|
+
clearInterval(revalidation);
|
|
2202
|
+
for (const deadline of pending.values()) {
|
|
2203
|
+
clearTimeout(deadline);
|
|
2204
|
+
}
|
|
2205
|
+
pending.clear();
|
|
2206
|
+
}
|
|
2207
|
+
};
|
|
2208
|
+
}
|
|
2209
|
+
function refuse(socket, status, headers = []) {
|
|
2210
|
+
socket.removeAllListeners("data");
|
|
2211
|
+
socket.pause();
|
|
2212
|
+
if (socket.destroyed || socket.writableEnded) {
|
|
2213
|
+
return;
|
|
2214
|
+
}
|
|
2215
|
+
socket.end(`HTTP/1.1 ${status}\r
|
|
2216
|
+
${headers.map((header) => `${header}\r
|
|
2217
|
+
`).join("")}content-length: 0\r
|
|
2218
|
+
connection: close\r
|
|
2219
|
+
\r
|
|
2220
|
+
`, () => {
|
|
2221
|
+
socket.destroy();
|
|
2222
|
+
});
|
|
2223
|
+
}
|
|
2224
|
+
function handleConnect(socket, context) {
|
|
2225
|
+
socket.on("error", () => {
|
|
2226
|
+
socket.destroy();
|
|
2227
|
+
});
|
|
2228
|
+
context.ledger.accept(socket);
|
|
1907
2229
|
let buffer = Buffer.alloc(0);
|
|
1908
2230
|
const onData = (chunk) => {
|
|
1909
2231
|
buffer = Buffer.concat([buffer, chunk]);
|
|
1910
2232
|
const headEnd = buffer.indexOf(HEAD_TERMINATOR);
|
|
2233
|
+
if (headEnd === -1 ? buffer.length > MAX_CONNECT_HEAD_BYTES : headEnd > MAX_CONNECT_HEAD_BYTES) {
|
|
2234
|
+
context.ledger.headReceived(socket);
|
|
2235
|
+
refuse(socket, REFUSAL.headTooLarge);
|
|
2236
|
+
return;
|
|
2237
|
+
}
|
|
1911
2238
|
if (headEnd === -1) {
|
|
1912
|
-
if (buffer.length > MAX_CONNECT_HEAD_BYTES) {
|
|
1913
|
-
socket.removeListener("data", onData);
|
|
1914
|
-
socket.pause();
|
|
1915
|
-
socket.destroy();
|
|
1916
|
-
}
|
|
1917
2239
|
return;
|
|
1918
2240
|
}
|
|
1919
2241
|
socket.removeListener("data", onData);
|
|
1920
2242
|
socket.pause();
|
|
2243
|
+
context.ledger.headReceived(socket);
|
|
1921
2244
|
const head = buffer.subarray(0, headEnd).toString("utf8");
|
|
1922
2245
|
socket.unshift(buffer.subarray(headEnd + HEAD_TERMINATOR.length));
|
|
1923
|
-
routeConnect(socket, head,
|
|
2246
|
+
routeConnect(socket, head, context);
|
|
1924
2247
|
};
|
|
1925
2248
|
socket.on("data", onData);
|
|
1926
|
-
socket.on("error", () => {
|
|
1927
|
-
socket.destroy();
|
|
1928
|
-
});
|
|
1929
2249
|
}
|
|
1930
|
-
function
|
|
2250
|
+
function proxyAuthorizationOf(head) {
|
|
2251
|
+
const values = head.split("\r\n").slice(1).flatMap((line) => {
|
|
2252
|
+
const colon = line.indexOf(":");
|
|
2253
|
+
return colon > 0 && line.slice(0, colon).trim().toLowerCase() === "proxy-authorization" ? [line.slice(colon + 1)] : [];
|
|
2254
|
+
});
|
|
2255
|
+
const [value] = values;
|
|
2256
|
+
return values.length === 1 ? value : void 0;
|
|
2257
|
+
}
|
|
2258
|
+
function routeConnect(socket, head, context) {
|
|
2259
|
+
const { config, effects, tlsAcceptor, ledger } = context;
|
|
2260
|
+
const credential = proxyAuthorizationOf(head);
|
|
2261
|
+
const capability = credential === void 0 ? void 0 : capabilityFromProxyAuthorization(credential);
|
|
2262
|
+
if (capability === void 0 || !config.isLiveCapability(capability)) {
|
|
2263
|
+
refuse(socket, REFUSAL.proxyAuthenticationRequired, [`Proxy-Authenticate: Basic realm="${CONNECT_PROXY_REALM}"`]);
|
|
2264
|
+
return;
|
|
2265
|
+
}
|
|
1931
2266
|
const requestLine = head.split("\r\n", 1)[0] ?? "";
|
|
1932
2267
|
const parts = requestLine.split(" ");
|
|
1933
2268
|
const authority = parts.length === CONNECT_LINE_TOKENS && parts[0]?.toUpperCase() === "CONNECT" ? parts[1] ?? "" : void 0;
|
|
1934
2269
|
const target = authority === void 0 ? void 0 : parseConnectTarget(authority);
|
|
1935
2270
|
if (target === void 0) {
|
|
1936
|
-
socket.
|
|
1937
|
-
|
|
2271
|
+
refuse(socket, REFUSAL.badRequest);
|
|
2272
|
+
return;
|
|
2273
|
+
}
|
|
2274
|
+
if (!ledger.establish(socket, capability)) {
|
|
2275
|
+
refuse(socket, REFUSAL.serviceUnavailable);
|
|
1938
2276
|
return;
|
|
1939
2277
|
}
|
|
1940
2278
|
socket.write(CONNECT_ESTABLISHED);
|
|
@@ -1952,6 +2290,10 @@ async function blindTunnel(socket, target, effects) {
|
|
|
1952
2290
|
socket.destroy();
|
|
1953
2291
|
return;
|
|
1954
2292
|
}
|
|
2293
|
+
if (socket.destroyed) {
|
|
2294
|
+
upstream.destroy();
|
|
2295
|
+
return;
|
|
2296
|
+
}
|
|
1955
2297
|
socket.pipe(upstream);
|
|
1956
2298
|
upstream.pipe(socket);
|
|
1957
2299
|
const drop = () => {
|
|
@@ -1967,7 +2309,7 @@ async function blindTunnel(socket, target, effects) {
|
|
|
1967
2309
|
socket.destroy();
|
|
1968
2310
|
});
|
|
1969
2311
|
}
|
|
1970
|
-
function
|
|
2312
|
+
function realConnectEffects() {
|
|
1971
2313
|
const plainAgent = new http.Agent({ keepAlive: true });
|
|
1972
2314
|
const tlsAgent = new https.Agent({ keepAlive: true });
|
|
1973
2315
|
const listenOnce = async (port, onSocket) => await new Promise((resolve, reject) => {
|
|
@@ -2096,7 +2438,7 @@ function realMitmEffects() {
|
|
|
2096
2438
|
function isEnoent(error) {
|
|
2097
2439
|
return typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT";
|
|
2098
2440
|
}
|
|
2099
|
-
function
|
|
2441
|
+
function realConnectCertStore(paths) {
|
|
2100
2442
|
const read = (file) => {
|
|
2101
2443
|
try {
|
|
2102
2444
|
return fs.readFileSync(file, "utf8");
|
|
@@ -2109,54 +2451,51 @@ function realMitmCertStore(paths) {
|
|
|
2109
2451
|
};
|
|
2110
2452
|
return {
|
|
2111
2453
|
loadCa: () => {
|
|
2112
|
-
const certPem = read(paths.
|
|
2113
|
-
const keyPem = read(paths.
|
|
2454
|
+
const certPem = read(paths.frontdoorCaCertFile);
|
|
2455
|
+
const keyPem = read(paths.frontdoorCaKeyFile);
|
|
2114
2456
|
return certPem === void 0 || keyPem === void 0 ? void 0 : { certPem, keyPem };
|
|
2115
2457
|
},
|
|
2116
2458
|
writeCa: (ca) => {
|
|
2117
|
-
fs.mkdirSync(paths.
|
|
2118
|
-
fs.writeFileSync(paths.
|
|
2119
|
-
fs.writeFileSync(paths.
|
|
2120
|
-
fs.chmodSync(paths.
|
|
2459
|
+
fs.mkdirSync(paths.frontdoorCaDir, { recursive: true });
|
|
2460
|
+
fs.writeFileSync(paths.frontdoorCaCertFile, ca.certPem, "utf8");
|
|
2461
|
+
fs.writeFileSync(paths.frontdoorCaKeyFile, ca.keyPem, { encoding: "utf8", mode: CA_KEY_FILE_MODE });
|
|
2462
|
+
fs.chmodSync(paths.frontdoorCaKeyFile, CA_KEY_FILE_MODE);
|
|
2121
2463
|
}
|
|
2122
2464
|
};
|
|
2123
2465
|
}
|
|
2124
2466
|
|
|
2467
|
+
// src/frontdoor/ensure.ts
|
|
2468
|
+
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
2469
|
+
|
|
2125
2470
|
// src/headroom/state.ts
|
|
2126
2471
|
import { createHash } from "node:crypto";
|
|
2127
|
-
import
|
|
2128
|
-
import { z as
|
|
2472
|
+
import path6 from "node:path";
|
|
2473
|
+
import { z as z4 } from "zod";
|
|
2129
2474
|
var HEADROOM_ANTHROPIC_UPSTREAM = "https://api.anthropic.com";
|
|
2130
|
-
var HeadroomStateSchema =
|
|
2475
|
+
var HeadroomStateSchema = z4.strictObject({
|
|
2131
2476
|
/** The supervisor process owning the daemon. Alive means someone is keeping headroom running. */
|
|
2132
|
-
supervisorPid:
|
|
2477
|
+
supervisorPid: z4.number().int().positive().optional(),
|
|
2133
2478
|
/** The headroom proxy process itself. Absent while the supervisor is between restarts. */
|
|
2134
|
-
headroomPid:
|
|
2479
|
+
headroomPid: z4.number().int().positive().optional(),
|
|
2135
2480
|
/** The loopback port the proxy listens on. Absent until the proxy has passed its readiness check, so "port is set" is itself the ready signal a launcher polls for. */
|
|
2136
|
-
port:
|
|
2137
|
-
/**
|
|
2138
|
-
* The loopback port the supervisor's in-process MITM CONNECT proxy listens on. Absent whenever no supervisor is serving it, so "mitmPort is set" is the ready signal an OAuth launch polls for alongside `port`.
|
|
2139
|
-
*/
|
|
2140
|
-
mitmPort: z3.number().int().positive().optional(),
|
|
2141
|
-
/**
|
|
2142
|
-
* The sticky MITM port preference, the exact analogue of `lastPort` for the second server: it survives every shutdown, because every OAuth session's environment was frozen at launch with HTTPS_PROXY pointing at this address and a restart that moves strands them.
|
|
2143
|
-
*/
|
|
2144
|
-
lastMitmPort: z3.number().int().positive().optional(),
|
|
2481
|
+
port: z4.number().int().positive().optional(),
|
|
2145
2482
|
/**
|
|
2146
|
-
* The sticky port preference: the address the daemon last served on, kept across crashes, restarts, and idle shutdowns so the next start reuses it. Distinct from `port` on purpose: `port` is the ready signal (absent whenever nothing is serving), while `lastPort` survives every shutdown, because
|
|
2483
|
+
* The sticky port preference: the address the daemon last served on, kept across crashes, restarts, and idle shutdowns so the next start reuses it. Distinct from `port` on purpose: `port` is the ready signal (absent whenever nothing is serving), while `lastPort` survives every shutdown, because the front door (which routes sessions through this daemon) reads it live and follows a restart to whichever address it lands on.
|
|
2147
2484
|
*/
|
|
2148
|
-
lastPort:
|
|
2485
|
+
lastPort: z4.number().int().positive().optional(),
|
|
2149
2486
|
/** The `headroom --version` output of the running install. */
|
|
2150
|
-
version:
|
|
2487
|
+
version: z4.string().optional(),
|
|
2151
2488
|
/** Hash of the allowlist the running proxy was started with, so a provider-file change is detected as drift. */
|
|
2152
|
-
allowlistHash:
|
|
2489
|
+
allowlistHash: z4.string().optional(),
|
|
2490
|
+
/** Hash of the token-saving settings the running proxy was started with, so a settings change is detected as drift. */
|
|
2491
|
+
settingsHash: z4.string().optional(),
|
|
2153
2492
|
/** The install spec the running install was last installed from, so a `source` config change is detected as drift. */
|
|
2154
|
-
installedSource:
|
|
2493
|
+
installedSource: z4.string().optional(),
|
|
2155
2494
|
/** The last fatal error, kept across shutdowns so `headroom status` can explain a daemon that is not running. */
|
|
2156
|
-
lastError:
|
|
2495
|
+
lastError: z4.string().optional()
|
|
2157
2496
|
});
|
|
2158
|
-
var HeadroomSessionSchema =
|
|
2159
|
-
var HeadroomLockSchema =
|
|
2497
|
+
var HeadroomSessionSchema = z4.strictObject({ pid: z4.number().int().positive(), startedAt: z4.number() });
|
|
2498
|
+
var HeadroomLockSchema = z4.strictObject({ pid: z4.number().int().positive(), at: z4.number() });
|
|
2160
2499
|
function parseJson(raw, schema) {
|
|
2161
2500
|
if (raw === void 0 || raw.trim() === "") {
|
|
2162
2501
|
return void 0;
|
|
@@ -2172,7 +2511,7 @@ function readHeadroomState(fs2, stateFile) {
|
|
|
2172
2511
|
}
|
|
2173
2512
|
}
|
|
2174
2513
|
function writeHeadroomState(fs2, stateFile, state) {
|
|
2175
|
-
fs2.mkdirp(
|
|
2514
|
+
fs2.mkdirp(path6.dirname(stateFile));
|
|
2176
2515
|
fs2.writeFileUtf8(stateFile, `${JSON.stringify(state, null, 2)}
|
|
2177
2516
|
`);
|
|
2178
2517
|
}
|
|
@@ -2184,16 +2523,8 @@ function headroomAllowlist(providers) {
|
|
|
2184
2523
|
return [...urls].sort();
|
|
2185
2524
|
}
|
|
2186
2525
|
function headroomUpstreams(providers, frontDoorOrigin2) {
|
|
2187
|
-
const upstreams =
|
|
2188
|
-
|
|
2189
|
-
for (const provider of providers) {
|
|
2190
|
-
if (isCodexProvider(provider)) {
|
|
2191
|
-
hasCodex = true;
|
|
2192
|
-
} else {
|
|
2193
|
-
upstreams.push({ baseUrl: provider.baseUrl });
|
|
2194
|
-
}
|
|
2195
|
-
}
|
|
2196
|
-
if (hasCodex && frontDoorOrigin2 !== void 0) {
|
|
2526
|
+
const upstreams = providers.filter((provider) => !isCodexProvider(provider)).map((provider) => ({ baseUrl: provider.baseUrl }));
|
|
2527
|
+
if (providers.length > 0 && frontDoorOrigin2 !== void 0) {
|
|
2197
2528
|
upstreams.push({ baseUrl: frontDoorOrigin2 });
|
|
2198
2529
|
}
|
|
2199
2530
|
return upstreams;
|
|
@@ -2208,7 +2539,7 @@ function readAllProviders(fs2, providersDir) {
|
|
|
2208
2539
|
continue;
|
|
2209
2540
|
}
|
|
2210
2541
|
try {
|
|
2211
|
-
const parsed = parseJson(fs2.readFileUtf8(
|
|
2542
|
+
const parsed = parseJson(fs2.readFileUtf8(path6.join(providersDir, name)), ProviderSchema);
|
|
2212
2543
|
if (parsed !== void 0) {
|
|
2213
2544
|
result.push({ name: name.slice(0, -".json".length), provider: parsed });
|
|
2214
2545
|
}
|
|
@@ -2220,11 +2551,11 @@ function readAllProviders(fs2, providersDir) {
|
|
|
2220
2551
|
}
|
|
2221
2552
|
function writeSession(fs2, sessionsDir, session) {
|
|
2222
2553
|
fs2.mkdirp(sessionsDir);
|
|
2223
|
-
fs2.writeFileUtf8(
|
|
2554
|
+
fs2.writeFileUtf8(path6.join(sessionsDir, `${String(session.pid)}.json`), `${JSON.stringify(session, null, 2)}
|
|
2224
2555
|
`);
|
|
2225
2556
|
}
|
|
2226
2557
|
function removeSession(fs2, sessionsDir, pid) {
|
|
2227
|
-
fs2.removeRecursive(
|
|
2558
|
+
fs2.removeRecursive(path6.join(sessionsDir, `${String(pid)}.json`));
|
|
2228
2559
|
}
|
|
2229
2560
|
function listSessions(fs2, sessionsDir) {
|
|
2230
2561
|
const sessions = [];
|
|
@@ -2233,7 +2564,7 @@ function listSessions(fs2, sessionsDir) {
|
|
|
2233
2564
|
continue;
|
|
2234
2565
|
}
|
|
2235
2566
|
try {
|
|
2236
|
-
const parsed = parseJson(fs2.readFileUtf8(
|
|
2567
|
+
const parsed = parseJson(fs2.readFileUtf8(path6.join(sessionsDir, name)), HeadroomSessionSchema);
|
|
2237
2568
|
if (parsed !== void 0) {
|
|
2238
2569
|
sessions.push(parsed);
|
|
2239
2570
|
}
|
|
@@ -2261,6 +2592,67 @@ function readStartLock(fs2, lockFile) {
|
|
|
2261
2592
|
}
|
|
2262
2593
|
}
|
|
2263
2594
|
|
|
2595
|
+
// src/frontdoor/ensure.ts
|
|
2596
|
+
var FRONTDOOR_START_TIMEOUT_MS = 3e4;
|
|
2597
|
+
var FRONTDOOR_LAUNCHER_POLL_MS = 100;
|
|
2598
|
+
var FrontDoorStartError = class extends CliError {
|
|
2599
|
+
constructor(message) {
|
|
2600
|
+
super(message);
|
|
2601
|
+
this.name = "FrontDoorStartError";
|
|
2602
|
+
}
|
|
2603
|
+
};
|
|
2604
|
+
function ensureFrontDoor(params) {
|
|
2605
|
+
const { paths, ports } = params;
|
|
2606
|
+
const deadline = ports.now() + FRONTDOOR_START_TIMEOUT_MS;
|
|
2607
|
+
let spawned = false;
|
|
2608
|
+
const distrusted = /* @__PURE__ */ new Set();
|
|
2609
|
+
let lastRejection;
|
|
2610
|
+
for (; ; ) {
|
|
2611
|
+
const state = readFrontDoorState(ports.fs, paths.frontdoorStateFile);
|
|
2612
|
+
const supervisorPid = state?.supervisorPid;
|
|
2613
|
+
if (supervisorPid !== void 0 && !distrusted.has(supervisorPid) && ports.isRunning(supervisorPid)) {
|
|
2614
|
+
if (state?.port !== void 0 && state.connectPort !== void 0) {
|
|
2615
|
+
const verdict = ports.verifyListener(state.port);
|
|
2616
|
+
if (verdict.ok) {
|
|
2617
|
+
const token = randomUUID2();
|
|
2618
|
+
writeFrontDoorSession(ports.fs, paths.frontdoorSessionsDir, { pid: params.launcherPid, startedAt: ports.now(), token });
|
|
2619
|
+
return { port: state.port, connectPort: state.connectPort, token };
|
|
2620
|
+
}
|
|
2621
|
+
lastRejection = `the listener on 127.0.0.1:${String(state.port)} (recorded for pid ${String(supervisorPid)}) failed authentication: ${verdict.reason}`;
|
|
2622
|
+
if (spawned) {
|
|
2623
|
+
throw new FrontDoorStartError(`claude-use: ${lastRejection}; refusing to send this session's credentials to it. Daemon log: ${paths.frontdoorLogPath}`);
|
|
2624
|
+
}
|
|
2625
|
+
distrusted.add(supervisorPid);
|
|
2626
|
+
continue;
|
|
2627
|
+
}
|
|
2628
|
+
if (state?.lastError !== void 0) {
|
|
2629
|
+
throw new FrontDoorStartError(`claude-use: the front door reported a fatal error and is not serving: ${state.lastError} (daemon log: ${paths.frontdoorLogPath})`);
|
|
2630
|
+
}
|
|
2631
|
+
} else if (!spawned) {
|
|
2632
|
+
ports.fs.mkdirp(paths.frontdoorDir);
|
|
2633
|
+
const created = ports.fs.writeFileExclusive(paths.frontdoorLockFile, `${JSON.stringify({ pid: params.launcherPid, at: ports.now() })}
|
|
2634
|
+
`);
|
|
2635
|
+
if (created) {
|
|
2636
|
+
ports.spawnSupervisor(paths);
|
|
2637
|
+
spawned = true;
|
|
2638
|
+
} else {
|
|
2639
|
+
const lock = readStartLock(ports.fs, paths.frontdoorLockFile);
|
|
2640
|
+
if (lock === void 0 || !ports.isRunning(lock.pid)) {
|
|
2641
|
+
ports.fs.removeRecursive(paths.frontdoorLockFile);
|
|
2642
|
+
continue;
|
|
2643
|
+
}
|
|
2644
|
+
}
|
|
2645
|
+
}
|
|
2646
|
+
if (ports.now() >= deadline) {
|
|
2647
|
+
const lastError = state?.lastError;
|
|
2648
|
+
throw new FrontDoorStartError(
|
|
2649
|
+
`claude-use: the front door did not become ready within ${String(FRONTDOOR_START_TIMEOUT_MS)}ms` + (lastError === void 0 ? "" : ` (last error: ${lastError})`) + (lastRejection === void 0 ? "" : ` (${lastRejection})`) + `. Daemon log: ${paths.frontdoorLogPath}`
|
|
2650
|
+
);
|
|
2651
|
+
}
|
|
2652
|
+
ports.sleep(FRONTDOOR_LAUNCHER_POLL_MS);
|
|
2653
|
+
}
|
|
2654
|
+
}
|
|
2655
|
+
|
|
2264
2656
|
// src/headroom/ensure.ts
|
|
2265
2657
|
var HEADROOM_START_TIMEOUT_MS = 12e4;
|
|
2266
2658
|
var HEADROOM_LAUNCHER_POLL_MS = 250;
|
|
@@ -2277,10 +2669,10 @@ function ensureHeadroom(params) {
|
|
|
2277
2669
|
for (; ; ) {
|
|
2278
2670
|
const state = readHeadroomState(ports.fs, paths.headroomStateFile);
|
|
2279
2671
|
if (state?.supervisorPid !== void 0 && ports.isRunning(state.supervisorPid)) {
|
|
2280
|
-
const daemonUp = state.port !== void 0 && state.
|
|
2281
|
-
if (daemonUp && state.port !== void 0
|
|
2672
|
+
const daemonUp = state.port !== void 0 && state.headroomPid !== void 0 && ports.isRunning(state.headroomPid);
|
|
2673
|
+
if (daemonUp && state.port !== void 0) {
|
|
2282
2674
|
writeSession(ports.fs, paths.headroomSessionsDir, { pid: params.launcherPid, startedAt: ports.now() });
|
|
2283
|
-
return { port: state.port
|
|
2675
|
+
return { port: state.port };
|
|
2284
2676
|
}
|
|
2285
2677
|
if (state.lastError !== void 0) {
|
|
2286
2678
|
throw new HeadroomStartError(
|
|
@@ -2315,47 +2707,41 @@ function ensureHeadroom(params) {
|
|
|
2315
2707
|
}
|
|
2316
2708
|
}
|
|
2317
2709
|
|
|
2318
|
-
// src/
|
|
2319
|
-
import {
|
|
2320
|
-
|
|
2321
|
-
|
|
2322
|
-
|
|
2323
|
-
|
|
2324
|
-
|
|
2325
|
-
|
|
2326
|
-
|
|
2327
|
-
|
|
2328
|
-
|
|
2329
|
-
/** The last fatal error, kept across shutdowns so `frontdoor status` can explain a front door that is not running. */
|
|
2330
|
-
lastError: z4.string().optional()
|
|
2331
|
-
});
|
|
2332
|
-
function readFrontDoorState(fs2, stateFile) {
|
|
2333
|
-
let raw;
|
|
2334
|
-
try {
|
|
2335
|
-
raw = fs2.readFileUtf8(stateFile);
|
|
2336
|
-
} catch {
|
|
2337
|
-
return void 0;
|
|
2338
|
-
}
|
|
2339
|
-
if (raw === void 0 || raw.trim() === "") {
|
|
2340
|
-
return void 0;
|
|
2341
|
-
}
|
|
2342
|
-
try {
|
|
2343
|
-
const parsed = FrontDoorStateSchema.safeParse(JSON.parse(raw));
|
|
2344
|
-
return parsed.success ? parsed.data : void 0;
|
|
2345
|
-
} catch {
|
|
2346
|
-
return void 0;
|
|
2347
|
-
}
|
|
2710
|
+
// src/headroom/settings.ts
|
|
2711
|
+
import { createHash as createHash2 } from "node:crypto";
|
|
2712
|
+
function settingsOf(config) {
|
|
2713
|
+
return {
|
|
2714
|
+
...config.mode === void 0 ? {} : { mode: config.mode },
|
|
2715
|
+
...config.targetRatio === void 0 ? {} : { targetRatio: config.targetRatio },
|
|
2716
|
+
...config.ccr === void 0 ? {} : { ccr: config.ccr },
|
|
2717
|
+
...config.rolloutChannel === void 0 ? {} : { rolloutChannel: config.rolloutChannel },
|
|
2718
|
+
...config.interceptToolResults === void 0 ? {} : { interceptToolResults: config.interceptToolResults },
|
|
2719
|
+
...config.readMaturation === void 0 ? {} : { readMaturation: config.readMaturation }
|
|
2720
|
+
};
|
|
2348
2721
|
}
|
|
2349
|
-
function
|
|
2350
|
-
|
|
2351
|
-
|
|
2722
|
+
function settingsArgs(settings) {
|
|
2723
|
+
return [
|
|
2724
|
+
...settings.mode === void 0 ? [] : ["--mode", settings.mode],
|
|
2725
|
+
...settings.targetRatio === void 0 ? [] : ["--target-ratio", String(settings.targetRatio)],
|
|
2726
|
+
...settings.ccr === "lossless" ? ["--lossless"] : [],
|
|
2727
|
+
...settings.ccr === "none" ? ["--no-ccr"] : [],
|
|
2728
|
+
...settings.interceptToolResults === true ? ["--intercept-tool-results"] : [],
|
|
2729
|
+
...settings.readMaturation === true ? ["--read-maturation"] : []
|
|
2730
|
+
];
|
|
2731
|
+
}
|
|
2732
|
+
function settingsEnv(settings) {
|
|
2733
|
+
return settings.rolloutChannel === void 0 ? {} : { HEADROOM_ROLLOUT_CHANNEL: settings.rolloutChannel };
|
|
2734
|
+
}
|
|
2735
|
+
function hashSettings(settings) {
|
|
2736
|
+
return createHash2("sha256").update(JSON.stringify([...settingsArgs(settings), JSON.stringify(settingsEnv(settings))])).digest("hex");
|
|
2352
2737
|
}
|
|
2353
2738
|
|
|
2354
2739
|
// src/headroom/supervisor.ts
|
|
2355
2740
|
function resolveSupervisorConfig(configured) {
|
|
2356
2741
|
return {
|
|
2357
2742
|
source: configured.source ?? HEADROOM_DEFAULT_SOURCE,
|
|
2358
|
-
idleShutdownMinutes: configured.idleShutdownMinutes ?? HEADROOM_DEFAULT_IDLE_SHUTDOWN_MINUTES
|
|
2743
|
+
idleShutdownMinutes: configured.idleShutdownMinutes ?? HEADROOM_DEFAULT_IDLE_SHUTDOWN_MINUTES,
|
|
2744
|
+
settings: settingsOf(configured)
|
|
2359
2745
|
};
|
|
2360
2746
|
}
|
|
2361
2747
|
var HEADROOM_BACKOFF_BASE_MS = 500;
|
|
@@ -2363,7 +2749,7 @@ var HEADROOM_BACKOFF_CAP_MS = 3e4;
|
|
|
2363
2749
|
var HEADROOM_START_RETRY_BUDGET = 5;
|
|
2364
2750
|
var HEADROOM_READY_TIMEOUT_MS = 15e3;
|
|
2365
2751
|
var HEADROOM_POLL_MS = 1e3;
|
|
2366
|
-
var
|
|
2752
|
+
var MS_PER_MINUTE2 = 6e4;
|
|
2367
2753
|
function backoffForAttempt(attempt) {
|
|
2368
2754
|
return Math.min(HEADROOM_BACKOFF_CAP_MS, HEADROOM_BACKOFF_BASE_MS * 2 ** (attempt - 1));
|
|
2369
2755
|
}
|
|
@@ -2418,18 +2804,13 @@ function compareVersions(a, b) {
|
|
|
2418
2804
|
return 0;
|
|
2419
2805
|
}
|
|
2420
2806
|
var HEADROOM_SUPERVISOR_STILL_RUNNING = -1;
|
|
2421
|
-
async function runSupervisor(
|
|
2807
|
+
async function runSupervisor(initialConfig, ports, options = {}) {
|
|
2422
2808
|
const { fs: fs2, paths } = ports;
|
|
2809
|
+
let config = initialConfig;
|
|
2423
2810
|
const previousState = readHeadroomState(fs2, paths.headroomStateFile);
|
|
2424
2811
|
let installedSource = previousState?.installedSource;
|
|
2425
2812
|
let lastPort = previousState?.lastPort;
|
|
2426
|
-
|
|
2427
|
-
let daemonPort;
|
|
2428
|
-
const sticky = () => ({
|
|
2429
|
-
...lastPort === void 0 ? {} : { lastPort },
|
|
2430
|
-
...lastMitmPort === void 0 ? {} : { lastMitmPort }
|
|
2431
|
-
});
|
|
2432
|
-
let mitm;
|
|
2813
|
+
const sticky = () => lastPort === void 0 ? {} : { lastPort };
|
|
2433
2814
|
const fail = (message) => {
|
|
2434
2815
|
writeHeadroomState(fs2, paths.headroomStateFile, {
|
|
2435
2816
|
lastError: message,
|
|
@@ -2458,18 +2839,7 @@ async function runSupervisor(config, ports, options = {}) {
|
|
|
2458
2839
|
return fail(`could not install headroom from ${config.source}: install failed (is uv installed and on PATH?)`);
|
|
2459
2840
|
}
|
|
2460
2841
|
const version = ports.headroomVersion() ?? "unknown";
|
|
2461
|
-
|
|
2462
|
-
mitm = await ports.startMitm(lastMitmPort, () => daemonPort);
|
|
2463
|
-
} catch (error) {
|
|
2464
|
-
return fail(`could not start the MITM proxy: ${error instanceof Error ? error.message : String(error)}`);
|
|
2465
|
-
}
|
|
2466
|
-
lastMitmPort = mitm.port;
|
|
2467
|
-
if (previousState?.lastMitmPort !== void 0 && mitm.port !== previousState.lastMitmPort) {
|
|
2468
|
-
ports.log(
|
|
2469
|
-
`claude-use headroom supervisor: sticky MITM port ${String(previousState.lastMitmPort)} was occupied; moving to ${String(mitm.port)} (OAuth sessions launched against the old port are stale until they relaunch)`
|
|
2470
|
-
);
|
|
2471
|
-
}
|
|
2472
|
-
writeHeadroomState(fs2, paths.headroomStateFile, { supervisorPid: ports.ownPid, version, installedSource, mitmPort: mitm.port, ...sticky() });
|
|
2842
|
+
writeHeadroomState(fs2, paths.headroomStateFile, { supervisorPid: ports.ownPid, version, installedSource, ...sticky() });
|
|
2473
2843
|
ports.log(`claude-use headroom supervisor ${String(ports.ownPid)}: managing headroom on allowlist [${allowlistOf(ports).join(", ")}]`);
|
|
2474
2844
|
const orphanPid = previousState?.headroomPid;
|
|
2475
2845
|
if (orphanPid !== void 0 && ports.isRunning(orphanPid)) {
|
|
@@ -2478,6 +2848,7 @@ async function runSupervisor(config, ports, options = {}) {
|
|
|
2478
2848
|
}
|
|
2479
2849
|
let headroomPid;
|
|
2480
2850
|
let runningHash;
|
|
2851
|
+
let runningSettingsHash;
|
|
2481
2852
|
let consecutiveFailures = 0;
|
|
2482
2853
|
let idleSince;
|
|
2483
2854
|
let ticks = 0;
|
|
@@ -2486,6 +2857,8 @@ async function runSupervisor(config, ports, options = {}) {
|
|
|
2486
2857
|
return HEADROOM_SUPERVISOR_STILL_RUNNING;
|
|
2487
2858
|
}
|
|
2488
2859
|
ticks += 1;
|
|
2860
|
+
config = ports.readConfig();
|
|
2861
|
+
const settingsHash = hashSettings(config.settings);
|
|
2489
2862
|
const allowlist = allowlistOf(ports);
|
|
2490
2863
|
const allowlistHash = hashAllowlist(allowlist);
|
|
2491
2864
|
pruneDeadSessions(fs2, paths.headroomSessionsDir, ports.isRunning);
|
|
@@ -2494,13 +2867,12 @@ async function runSupervisor(config, ports, options = {}) {
|
|
|
2494
2867
|
if (crashed) {
|
|
2495
2868
|
ports.log(`claude-use headroom supervisor: headroom pid ${String(headroomPid)} died`);
|
|
2496
2869
|
headroomPid = void 0;
|
|
2497
|
-
daemonPort = void 0;
|
|
2498
2870
|
runningHash = void 0;
|
|
2871
|
+
runningSettingsHash = void 0;
|
|
2499
2872
|
writeHeadroomState(fs2, paths.headroomStateFile, {
|
|
2500
2873
|
supervisorPid: ports.ownPid,
|
|
2501
2874
|
version,
|
|
2502
2875
|
installedSource,
|
|
2503
|
-
mitmPort: mitm.port,
|
|
2504
2876
|
...sticky()
|
|
2505
2877
|
});
|
|
2506
2878
|
}
|
|
@@ -2512,13 +2884,13 @@ async function runSupervisor(config, ports, options = {}) {
|
|
|
2512
2884
|
}
|
|
2513
2885
|
const previousSticky = lastPort;
|
|
2514
2886
|
const port = lastPort !== void 0 && await ports.isPortFree(lastPort) ? lastPort : await ports.freePort();
|
|
2515
|
-
const pid = ports.spawnHeadroom(port, allowlist);
|
|
2887
|
+
const pid = ports.spawnHeadroom(port, allowlist, config.settings);
|
|
2516
2888
|
const ready = await waitUntilReady(ports, port);
|
|
2517
2889
|
if (ready) {
|
|
2518
2890
|
consecutiveFailures = 0;
|
|
2519
2891
|
headroomPid = pid;
|
|
2520
|
-
daemonPort = port;
|
|
2521
2892
|
runningHash = allowlistHash;
|
|
2893
|
+
runningSettingsHash = settingsHash;
|
|
2522
2894
|
lastPort = port;
|
|
2523
2895
|
if (previousSticky !== void 0 && port !== previousSticky) {
|
|
2524
2896
|
ports.log(
|
|
@@ -2529,9 +2901,9 @@ async function runSupervisor(config, ports, options = {}) {
|
|
|
2529
2901
|
supervisorPid: ports.ownPid,
|
|
2530
2902
|
headroomPid: pid,
|
|
2531
2903
|
port,
|
|
2532
|
-
mitmPort: mitm.port,
|
|
2533
2904
|
version,
|
|
2534
2905
|
allowlistHash,
|
|
2906
|
+
settingsHash,
|
|
2535
2907
|
installedSource: config.source,
|
|
2536
2908
|
...sticky()
|
|
2537
2909
|
});
|
|
@@ -2548,13 +2920,13 @@ async function runSupervisor(config, ports, options = {}) {
|
|
|
2548
2920
|
continue;
|
|
2549
2921
|
}
|
|
2550
2922
|
} else {
|
|
2551
|
-
if (runningHash !== allowlistHash || installedSource !== config.source) {
|
|
2923
|
+
if (runningHash !== allowlistHash || runningSettingsHash !== settingsHash || installedSource !== config.source) {
|
|
2552
2924
|
if (listSessions(fs2, paths.headroomSessionsDir).length === 0) {
|
|
2553
2925
|
ports.log("claude-use headroom supervisor: configuration drifted and no sessions are live; restarting headroom");
|
|
2554
2926
|
ports.stopProcess(headroomPid);
|
|
2555
2927
|
headroomPid = void 0;
|
|
2556
|
-
daemonPort = void 0;
|
|
2557
2928
|
runningHash = void 0;
|
|
2929
|
+
runningSettingsHash = void 0;
|
|
2558
2930
|
continue;
|
|
2559
2931
|
}
|
|
2560
2932
|
}
|
|
@@ -2562,13 +2934,11 @@ async function runSupervisor(config, ports, options = {}) {
|
|
|
2562
2934
|
if (sessions.length === 0) {
|
|
2563
2935
|
idleSince ??= ports.now();
|
|
2564
2936
|
const idleMs = ports.now() - idleSince;
|
|
2565
|
-
if (idleMs >= config.idleShutdownMinutes *
|
|
2937
|
+
if (idleMs >= config.idleShutdownMinutes * MS_PER_MINUTE2) {
|
|
2566
2938
|
ports.log(
|
|
2567
|
-
`claude-use headroom supervisor: no sessions for ${String(config.idleShutdownMinutes)} minute(s); stopping headroom and
|
|
2939
|
+
`claude-use headroom supervisor: no sessions for ${String(config.idleShutdownMinutes)} minute(s); stopping headroom and exiting`
|
|
2568
2940
|
);
|
|
2569
2941
|
ports.stopProcess(headroomPid);
|
|
2570
|
-
daemonPort = void 0;
|
|
2571
|
-
await mitm.close();
|
|
2572
2942
|
writeHeadroomState(fs2, paths.headroomStateFile, {
|
|
2573
2943
|
version,
|
|
2574
2944
|
...sticky()
|
|
@@ -2650,15 +3020,18 @@ function evaluateAmbientCredentialGuard(params) {
|
|
|
2650
3020
|
}
|
|
2651
3021
|
export {
|
|
2652
3022
|
AMBIENT_CREDENTIAL_VARS,
|
|
3023
|
+
CONNECT_INTERCEPT_HOST,
|
|
3024
|
+
CONNECT_LIMITS,
|
|
2653
3025
|
FARM_MANIFEST_FILENAME,
|
|
2654
|
-
|
|
3026
|
+
FrontDoorStartError,
|
|
2655
3027
|
HeadroomStartError,
|
|
2656
|
-
|
|
3028
|
+
ROUTED_PATH_PREFIX,
|
|
2657
3029
|
buildEntryFacts,
|
|
2658
3030
|
carryOver,
|
|
2659
3031
|
createLeafCache,
|
|
2660
3032
|
detectAmbientCredential,
|
|
2661
3033
|
ensureCa,
|
|
3034
|
+
ensureFrontDoor,
|
|
2662
3035
|
ensureHeadroom,
|
|
2663
3036
|
evaluateAmbientCredentialGuard,
|
|
2664
3037
|
formatAmbientCredentialGuardMessage,
|
|
@@ -2668,15 +3041,16 @@ export {
|
|
|
2668
3041
|
mintLeaf,
|
|
2669
3042
|
parseConnectTarget,
|
|
2670
3043
|
readFarmManifest,
|
|
2671
|
-
|
|
2672
|
-
|
|
3044
|
+
realConnectCertStore,
|
|
3045
|
+
realConnectEffects,
|
|
2673
3046
|
recoverFarm,
|
|
2674
3047
|
recoveryDiagnostics,
|
|
2675
3048
|
resolveDecisions,
|
|
2676
3049
|
resolveSupervisorConfig,
|
|
2677
3050
|
resyncFarm,
|
|
3051
|
+
runFrontDoorSupervisor,
|
|
2678
3052
|
runSupervisor,
|
|
2679
|
-
|
|
2680
|
-
|
|
3053
|
+
servedByPipeline,
|
|
3054
|
+
startConnectServer,
|
|
2681
3055
|
topLevelNames
|
|
2682
3056
|
};
|