claude-use 7.6.0 → 7.7.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.
- package/dist/cli.cjs +54 -12
- package/dist/index.cjs +53 -11
- package/dist/index.mjs +53 -11
- package/dist/types/frontdoor/connect.d.ts +7 -6
- package/dist/types/frontdoor/ensure.d.ts +2 -0
- package/dist/types/frontdoor/route.d.ts +112 -0
- package/dist/types/frontdoor/state.d.ts +7 -0
- package/dist/types/frontdoor/tap.d.ts +2 -1
- package/package.json +1 -1
package/dist/cli.cjs
CHANGED
|
@@ -61053,7 +61053,7 @@ function resolveLaunchIdentity(paths, cwd) {
|
|
|
61053
61053
|
// package.json
|
|
61054
61054
|
var package_default = {
|
|
61055
61055
|
name: "claude-use",
|
|
61056
|
-
version: "7.
|
|
61056
|
+
version: "7.7.0",
|
|
61057
61057
|
description: "A profile manager and launcher for Claude Code that lets one person run multiple logins from one machine while controlling what gets shared between them.",
|
|
61058
61058
|
license: "MIT",
|
|
61059
61059
|
author: "Joseph Mearman <joseph@mearman.co.uk>",
|
|
@@ -63791,8 +63791,13 @@ var import_node_path27 = __toESM(require("node:path"), 1);
|
|
|
63791
63791
|
|
|
63792
63792
|
// src/frontdoor/state.ts
|
|
63793
63793
|
var import_node_path26 = __toESM(require("node:path"), 1);
|
|
63794
|
+
var FRONT_DOOR_PROTOCOL = 1;
|
|
63794
63795
|
var FrontDoorSessionSchema = external_exports.strictObject({ pid: external_exports.number().int().positive(), startedAt: external_exports.number(), token: external_exports.string().min(1) });
|
|
63795
63796
|
var FrontDoorStateSchema = external_exports.strictObject({
|
|
63797
|
+
/**
|
|
63798
|
+
* The launcher-to-door wire protocol this supervisor speaks, written once it is serving; absent in state written by a release that predates the field, which `ensureFrontDoor` reads as protocol 0. A launcher replaces a door whose protocol is lower than its own (see `FRONT_DOOR_PROTOCOL`).
|
|
63799
|
+
*/
|
|
63800
|
+
protocol: external_exports.number().int().nonnegative().optional(),
|
|
63796
63801
|
/** The supervisor process serving the front door. Alive means both listeners are up in that process. */
|
|
63797
63802
|
supervisorPid: external_exports.number().int().positive().optional(),
|
|
63798
63803
|
/** 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. */
|
|
@@ -64803,7 +64808,7 @@ function issueLeafCertificate(ca, names, notBefore, notAfter) {
|
|
|
64803
64808
|
}
|
|
64804
64809
|
|
|
64805
64810
|
// src/frontdoor/tap.ts
|
|
64806
|
-
function pumpTapSession(secure, openUpstream, tap) {
|
|
64811
|
+
function pumpTapSession(secure, openUpstream, tap, prefix = Buffer.alloc(0)) {
|
|
64807
64812
|
let upstream;
|
|
64808
64813
|
let ended = false;
|
|
64809
64814
|
const end = () => {
|
|
@@ -64835,6 +64840,9 @@ function pumpTapSession(secure, openUpstream, tap) {
|
|
|
64835
64840
|
upstream.destroy();
|
|
64836
64841
|
return;
|
|
64837
64842
|
}
|
|
64843
|
+
if (prefix.length > 0) {
|
|
64844
|
+
upstream.write(prefix);
|
|
64845
|
+
}
|
|
64838
64846
|
if (tap !== void 0) {
|
|
64839
64847
|
secure.on("data", (chunk) => {
|
|
64840
64848
|
tap.onChunk("client-to-server", chunk);
|
|
@@ -64908,7 +64916,7 @@ async function runFrontDoorSupervisor(idleShutdownMinutes, ports, options = {})
|
|
|
64908
64916
|
ports.log(`agent-shim frontdoor supervisor: sticky direct port ${String(previousStickyDirect)} was occupied; moving to ${String(direct.port)} (headroom must be restarted to admit the new address)`);
|
|
64909
64917
|
}
|
|
64910
64918
|
lastDirectPort = direct.port;
|
|
64911
|
-
writeFrontDoorState(fs18, paths.frontdoorStateFile, { supervisorPid: ports.ownPid, port: http5.port, connectPort: connect3.port, directPort: direct.port, ...sticky() });
|
|
64919
|
+
writeFrontDoorState(fs18, paths.frontdoorStateFile, { protocol: FRONT_DOOR_PROTOCOL, supervisorPid: ports.ownPid, port: http5.port, connectPort: connect3.port, directPort: direct.port, ...sticky() });
|
|
64912
64920
|
fs18.removeRecursive(paths.frontdoorLockFile);
|
|
64913
64921
|
ports.log(`agent-shim frontdoor supervisor ${String(ports.ownPid)}: front door on 127.0.0.1:${String(http5.port)}, CONNECT surface on 127.0.0.1:${String(connect3.port)}`);
|
|
64914
64922
|
let idleSince;
|
|
@@ -65049,7 +65057,19 @@ function caParses(ca) {
|
|
|
65049
65057
|
}
|
|
65050
65058
|
var TAP_ALPN_PROTOCOLS = ["h2", "http/1.1"];
|
|
65051
65059
|
async function startConnectServer(config2, effects, preferredPort) {
|
|
65052
|
-
const
|
|
65060
|
+
const tunnelTokens = /* @__PURE__ */ new WeakMap();
|
|
65061
|
+
const onUpgrade = (host) => (request6, socket, head) => {
|
|
65062
|
+
config2.capture?.upgrade(request6);
|
|
65063
|
+
if (head.length > 0) {
|
|
65064
|
+
socket.pause();
|
|
65065
|
+
socket.unshift(head);
|
|
65066
|
+
}
|
|
65067
|
+
const headText = `${request6.method ?? "GET"} ${request6.url ?? "/"} HTTP/1.1\r
|
|
65068
|
+
${request6.rawHeaders.reduce((accumulated, value, index) => index % 2 === 0 ? `${accumulated}${value}: ` : `${accumulated}${value}\r
|
|
65069
|
+
`, "")}\r
|
|
65070
|
+
`;
|
|
65071
|
+
pumpTapSession(socket, async (clientAlpn) => await effects.connectTlsUpstream(host, HTTPS_PORT, clientAlpn), void 0, Buffer.from(headText, "utf8"));
|
|
65072
|
+
};
|
|
65053
65073
|
const sessions = config2.interceptHosts.map((host) => {
|
|
65054
65074
|
if (config2.tapHosts.includes(host)) {
|
|
65055
65075
|
const tlsAcceptor2 = effects.createTlsAcceptor(
|
|
@@ -65063,13 +65083,18 @@ async function startConnectServer(config2, effects, preferredPort) {
|
|
|
65063
65083
|
}
|
|
65064
65084
|
const handler = (request6, response) => {
|
|
65065
65085
|
if (host === config2.routedHost && servedByPipeline(request6.url)) {
|
|
65086
|
+
const tunnelToken = tunnelTokens.get(request6.socket);
|
|
65087
|
+
if (tunnelToken !== void 0 && request6.headers[AUTH_HEADER] === void 0) {
|
|
65088
|
+
request6.headers[AUTH_HEADER] = tunnelToken;
|
|
65089
|
+
}
|
|
65066
65090
|
config2.serveRouted(request6, response);
|
|
65067
65091
|
return;
|
|
65068
65092
|
}
|
|
65069
65093
|
effects.forwardHttp(config2.upstreamFor(host), request6, response, config2.capture?.observePassthrough(request6));
|
|
65070
65094
|
};
|
|
65071
|
-
const httpSession = effects.createHttpSession(handler, onUpgrade);
|
|
65072
|
-
const tlsAcceptor = effects.createTlsAcceptor(config2.leafFor(host), (secure) => {
|
|
65095
|
+
const httpSession = effects.createHttpSession(handler, onUpgrade(host));
|
|
65096
|
+
const tlsAcceptor = effects.createTlsAcceptor(config2.leafFor(host), (secure, context) => {
|
|
65097
|
+
tunnelTokens.set(secure, context);
|
|
65073
65098
|
httpSession.serve(secure);
|
|
65074
65099
|
});
|
|
65075
65100
|
return { host, httpSession, tlsAcceptor };
|
|
@@ -65210,6 +65235,7 @@ function routeConnect(socket, head, context) {
|
|
|
65210
65235
|
const credential = proxyAuthorizationOf(head);
|
|
65211
65236
|
const capability = credential === void 0 ? void 0 : capabilityFromProxyAuthorization(credential);
|
|
65212
65237
|
if (capability === void 0 || !config2.isLiveCapability(capability)) {
|
|
65238
|
+
console.error(`[upg-dbg] refused CONNECT to ${head.split("\r\n", 1)[0]?.split(" ")[1] ?? "?"} (credential ${credential === void 0 ? "absent" : "present but dead"})`);
|
|
65213
65239
|
refuse(socket, REFUSAL.proxyAuthenticationRequired, [`Proxy-Authenticate: Basic realm="${CONNECT_PROXY_REALM}"`]);
|
|
65214
65240
|
return;
|
|
65215
65241
|
}
|
|
@@ -65229,7 +65255,7 @@ function routeConnect(socket, head, context) {
|
|
|
65229
65255
|
config2.capture?.connect(target, session !== void 0);
|
|
65230
65256
|
socket.write(CONNECT_ESTABLISHED);
|
|
65231
65257
|
if (session !== void 0) {
|
|
65232
|
-
session.tlsAcceptor.accept(socket);
|
|
65258
|
+
session.tlsAcceptor.accept(socket, capability);
|
|
65233
65259
|
return;
|
|
65234
65260
|
}
|
|
65235
65261
|
void blindTunnel(socket, target, effects);
|
|
@@ -65341,10 +65367,11 @@ function realConnectEffects() {
|
|
|
65341
65367
|
return await listenOnce(0, onSocket);
|
|
65342
65368
|
},
|
|
65343
65369
|
createTlsAcceptor: (leaf, onSecure, alpnProtocols) => {
|
|
65370
|
+
let pendingContext = "";
|
|
65344
65371
|
const server = tls.createServer(
|
|
65345
65372
|
{ key: leaf.keyPem, cert: leaf.certPem, ...alpnProtocols === void 0 ? {} : { ALPNProtocols: [...alpnProtocols] } },
|
|
65346
65373
|
(secure) => {
|
|
65347
|
-
onSecure(secure);
|
|
65374
|
+
onSecure(secure, pendingContext);
|
|
65348
65375
|
}
|
|
65349
65376
|
);
|
|
65350
65377
|
server.on("clientError", (_error, socket) => {
|
|
@@ -65352,7 +65379,8 @@ function realConnectEffects() {
|
|
|
65352
65379
|
});
|
|
65353
65380
|
return {
|
|
65354
65381
|
// tls.Server is a net.Server whose connection listener wraps the raw socket in the TLS handshake, and emitting the event by hand is what runs that listener on a socket this process already owns (the CONNECT half of the connection) rather than one the server accepted itself.
|
|
65355
|
-
accept: (socket) => {
|
|
65382
|
+
accept: (socket, context) => {
|
|
65383
|
+
pendingContext = context;
|
|
65356
65384
|
server.emit("connection", socket);
|
|
65357
65385
|
},
|
|
65358
65386
|
close: () => {
|
|
@@ -65366,9 +65394,8 @@ function realConnectEffects() {
|
|
|
65366
65394
|
socket.destroy();
|
|
65367
65395
|
});
|
|
65368
65396
|
if (onUpgrade !== void 0) {
|
|
65369
|
-
server.on("upgrade", (request6, socket) => {
|
|
65370
|
-
onUpgrade(request6);
|
|
65371
|
-
socket.destroy();
|
|
65397
|
+
server.on("upgrade", (request6, socket, head) => {
|
|
65398
|
+
onUpgrade(request6, socket, head);
|
|
65372
65399
|
});
|
|
65373
65400
|
}
|
|
65374
65401
|
return {
|
|
@@ -65466,6 +65493,7 @@ function ensureFrontDoor(params) {
|
|
|
65466
65493
|
const { paths, ports } = params;
|
|
65467
65494
|
const deadline = ports.now() + FRONTDOOR_START_TIMEOUT_MS;
|
|
65468
65495
|
let spawned = false;
|
|
65496
|
+
let replaced = false;
|
|
65469
65497
|
const distrusted = /* @__PURE__ */ new Set();
|
|
65470
65498
|
let lastRejection;
|
|
65471
65499
|
for (; ; ) {
|
|
@@ -65474,6 +65502,17 @@ function ensureFrontDoor(params) {
|
|
|
65474
65502
|
if (supervisorPid !== void 0 && !distrusted.has(supervisorPid) && ports.isRunning(supervisorPid)) {
|
|
65475
65503
|
if (state?.port !== void 0 && state.connectPort !== void 0) {
|
|
65476
65504
|
const verdict = ports.verifyListener(state.port);
|
|
65505
|
+
if (verdict.ok && (state.protocol ?? 0) < FRONT_DOOR_PROTOCOL && !replaced) {
|
|
65506
|
+
ports.stopSupervisor(supervisorPid);
|
|
65507
|
+
while (ports.isRunning(supervisorPid)) {
|
|
65508
|
+
if (ports.now() >= deadline) {
|
|
65509
|
+
throw new FrontDoorStartError(`agent-shim: the front door (pid ${String(supervisorPid)}, protocol ${String(state.protocol ?? 0)}) is older than this launcher (protocol ${String(FRONT_DOOR_PROTOCOL)}) and did not exit when asked. Daemon log: ${paths.frontdoorLogPath}`);
|
|
65510
|
+
}
|
|
65511
|
+
ports.sleep(FRONTDOOR_LAUNCHER_POLL_MS);
|
|
65512
|
+
}
|
|
65513
|
+
replaced = true;
|
|
65514
|
+
continue;
|
|
65515
|
+
}
|
|
65477
65516
|
if (verdict.ok) {
|
|
65478
65517
|
const token = (0, import_node_crypto11.randomUUID)();
|
|
65479
65518
|
writeFrontDoorSession(ports.fs, paths.frontdoorSessionsDir, { pid: params.launcherPid, startedAt: ports.now(), token });
|
|
@@ -67152,6 +67191,9 @@ function realFrontDoorPort(paths) {
|
|
|
67152
67191
|
now: () => Date.now(),
|
|
67153
67192
|
sleep: realSleepSync,
|
|
67154
67193
|
spawnSupervisor: (layout) => spawnDetachedSupervisor(layout, "__frontdoor-supervisor", layout.frontdoorLogPath),
|
|
67194
|
+
stopSupervisor: (pid) => {
|
|
67195
|
+
process.kill(pid, "SIGTERM");
|
|
67196
|
+
},
|
|
67155
67197
|
// Read fresh for each probe: a replacement supervisor may have regenerated an unparseable CA, and the probe must trust exactly what the serving door's leaf chains to.
|
|
67156
67198
|
verifyListener: (port) => probeFrontDoorSync(port, import_node_fs11.default.readFileSync(paths.frontdoorCaCertFile, "utf8"))
|
|
67157
67199
|
}
|
package/dist/index.cjs
CHANGED
|
@@ -2160,7 +2160,7 @@ function issueLeafCertificate(ca, names, notBefore, notAfter) {
|
|
|
2160
2160
|
}
|
|
2161
2161
|
|
|
2162
2162
|
// src/frontdoor/tap.ts
|
|
2163
|
-
function pumpTapSession(secure, openUpstream, tap) {
|
|
2163
|
+
function pumpTapSession(secure, openUpstream, tap, prefix = Buffer.alloc(0)) {
|
|
2164
2164
|
let upstream;
|
|
2165
2165
|
let ended = false;
|
|
2166
2166
|
const end = () => {
|
|
@@ -2192,6 +2192,9 @@ function pumpTapSession(secure, openUpstream, tap) {
|
|
|
2192
2192
|
upstream.destroy();
|
|
2193
2193
|
return;
|
|
2194
2194
|
}
|
|
2195
|
+
if (prefix.length > 0) {
|
|
2196
|
+
upstream.write(prefix);
|
|
2197
|
+
}
|
|
2195
2198
|
if (tap !== void 0) {
|
|
2196
2199
|
secure.on("data", (chunk) => {
|
|
2197
2200
|
tap.onChunk("client-to-server", chunk);
|
|
@@ -2225,11 +2228,19 @@ function capabilityFromProxyAuthorization(value) {
|
|
|
2225
2228
|
return password === "" ? void 0 : password;
|
|
2226
2229
|
}
|
|
2227
2230
|
|
|
2231
|
+
// src/frontdoor/route.ts
|
|
2232
|
+
var AUTH_HEADER = "x-agent-shim-auth";
|
|
2233
|
+
|
|
2228
2234
|
// src/frontdoor/state.ts
|
|
2229
2235
|
var import_node_path5 = __toESM(require("node:path"), 1);
|
|
2230
2236
|
var import_zod3 = require("zod");
|
|
2237
|
+
var FRONT_DOOR_PROTOCOL = 1;
|
|
2231
2238
|
var FrontDoorSessionSchema = import_zod3.z.strictObject({ pid: import_zod3.z.number().int().positive(), startedAt: import_zod3.z.number(), token: import_zod3.z.string().min(1) });
|
|
2232
2239
|
var FrontDoorStateSchema = import_zod3.z.strictObject({
|
|
2240
|
+
/**
|
|
2241
|
+
* The launcher-to-door wire protocol this supervisor speaks, written once it is serving; absent in state written by a release that predates the field, which `ensureFrontDoor` reads as protocol 0. A launcher replaces a door whose protocol is lower than its own (see `FRONT_DOOR_PROTOCOL`).
|
|
2242
|
+
*/
|
|
2243
|
+
protocol: import_zod3.z.number().int().nonnegative().optional(),
|
|
2233
2244
|
/** The supervisor process serving the front door. Alive means both listeners are up in that process. */
|
|
2234
2245
|
supervisorPid: import_zod3.z.number().int().positive().optional(),
|
|
2235
2246
|
/** 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. */
|
|
@@ -2375,7 +2386,7 @@ async function runFrontDoorSupervisor(idleShutdownMinutes, ports, options = {})
|
|
|
2375
2386
|
ports.log(`agent-shim frontdoor supervisor: sticky direct port ${String(previousStickyDirect)} was occupied; moving to ${String(direct.port)} (headroom must be restarted to admit the new address)`);
|
|
2376
2387
|
}
|
|
2377
2388
|
lastDirectPort = direct.port;
|
|
2378
|
-
writeFrontDoorState(fs3, paths.frontdoorStateFile, { supervisorPid: ports.ownPid, port: http2.port, connectPort: connect3.port, directPort: direct.port, ...sticky() });
|
|
2389
|
+
writeFrontDoorState(fs3, paths.frontdoorStateFile, { protocol: FRONT_DOOR_PROTOCOL, supervisorPid: ports.ownPid, port: http2.port, connectPort: connect3.port, directPort: direct.port, ...sticky() });
|
|
2379
2390
|
fs3.removeRecursive(paths.frontdoorLockFile);
|
|
2380
2391
|
ports.log(`agent-shim frontdoor supervisor ${String(ports.ownPid)}: front door on 127.0.0.1:${String(http2.port)}, CONNECT surface on 127.0.0.1:${String(connect3.port)}`);
|
|
2381
2392
|
let idleSince;
|
|
@@ -2517,7 +2528,19 @@ function caParses(ca) {
|
|
|
2517
2528
|
}
|
|
2518
2529
|
var TAP_ALPN_PROTOCOLS = ["h2", "http/1.1"];
|
|
2519
2530
|
async function startConnectServer(config, effects, preferredPort) {
|
|
2520
|
-
const
|
|
2531
|
+
const tunnelTokens = /* @__PURE__ */ new WeakMap();
|
|
2532
|
+
const onUpgrade = (host) => (request3, socket, head) => {
|
|
2533
|
+
config.capture?.upgrade(request3);
|
|
2534
|
+
if (head.length > 0) {
|
|
2535
|
+
socket.pause();
|
|
2536
|
+
socket.unshift(head);
|
|
2537
|
+
}
|
|
2538
|
+
const headText = `${request3.method ?? "GET"} ${request3.url ?? "/"} HTTP/1.1\r
|
|
2539
|
+
${request3.rawHeaders.reduce((accumulated, value, index) => index % 2 === 0 ? `${accumulated}${value}: ` : `${accumulated}${value}\r
|
|
2540
|
+
`, "")}\r
|
|
2541
|
+
`;
|
|
2542
|
+
pumpTapSession(socket, async (clientAlpn) => await effects.connectTlsUpstream(host, HTTPS_PORT, clientAlpn), void 0, Buffer.from(headText, "utf8"));
|
|
2543
|
+
};
|
|
2521
2544
|
const sessions = config.interceptHosts.map((host) => {
|
|
2522
2545
|
if (config.tapHosts.includes(host)) {
|
|
2523
2546
|
const tlsAcceptor2 = effects.createTlsAcceptor(
|
|
@@ -2531,13 +2554,18 @@ async function startConnectServer(config, effects, preferredPort) {
|
|
|
2531
2554
|
}
|
|
2532
2555
|
const handler = (request3, response) => {
|
|
2533
2556
|
if (host === config.routedHost && servedByPipeline(request3.url)) {
|
|
2557
|
+
const tunnelToken = tunnelTokens.get(request3.socket);
|
|
2558
|
+
if (tunnelToken !== void 0 && request3.headers[AUTH_HEADER] === void 0) {
|
|
2559
|
+
request3.headers[AUTH_HEADER] = tunnelToken;
|
|
2560
|
+
}
|
|
2534
2561
|
config.serveRouted(request3, response);
|
|
2535
2562
|
return;
|
|
2536
2563
|
}
|
|
2537
2564
|
effects.forwardHttp(config.upstreamFor(host), request3, response, config.capture?.observePassthrough(request3));
|
|
2538
2565
|
};
|
|
2539
|
-
const httpSession = effects.createHttpSession(handler, onUpgrade);
|
|
2540
|
-
const tlsAcceptor = effects.createTlsAcceptor(config.leafFor(host), (secure) => {
|
|
2566
|
+
const httpSession = effects.createHttpSession(handler, onUpgrade(host));
|
|
2567
|
+
const tlsAcceptor = effects.createTlsAcceptor(config.leafFor(host), (secure, context) => {
|
|
2568
|
+
tunnelTokens.set(secure, context);
|
|
2541
2569
|
httpSession.serve(secure);
|
|
2542
2570
|
});
|
|
2543
2571
|
return { host, httpSession, tlsAcceptor };
|
|
@@ -2678,6 +2706,7 @@ function routeConnect(socket, head, context) {
|
|
|
2678
2706
|
const credential = proxyAuthorizationOf(head);
|
|
2679
2707
|
const capability = credential === void 0 ? void 0 : capabilityFromProxyAuthorization(credential);
|
|
2680
2708
|
if (capability === void 0 || !config.isLiveCapability(capability)) {
|
|
2709
|
+
console.error(`[upg-dbg] refused CONNECT to ${head.split("\r\n", 1)[0]?.split(" ")[1] ?? "?"} (credential ${credential === void 0 ? "absent" : "present but dead"})`);
|
|
2681
2710
|
refuse(socket, REFUSAL.proxyAuthenticationRequired, [`Proxy-Authenticate: Basic realm="${CONNECT_PROXY_REALM}"`]);
|
|
2682
2711
|
return;
|
|
2683
2712
|
}
|
|
@@ -2697,7 +2726,7 @@ function routeConnect(socket, head, context) {
|
|
|
2697
2726
|
config.capture?.connect(target, session !== void 0);
|
|
2698
2727
|
socket.write(CONNECT_ESTABLISHED);
|
|
2699
2728
|
if (session !== void 0) {
|
|
2700
|
-
session.tlsAcceptor.accept(socket);
|
|
2729
|
+
session.tlsAcceptor.accept(socket, capability);
|
|
2701
2730
|
return;
|
|
2702
2731
|
}
|
|
2703
2732
|
void blindTunnel(socket, target, effects);
|
|
@@ -2809,10 +2838,11 @@ function realConnectEffects() {
|
|
|
2809
2838
|
return await listenOnce(0, onSocket);
|
|
2810
2839
|
},
|
|
2811
2840
|
createTlsAcceptor: (leaf, onSecure, alpnProtocols) => {
|
|
2841
|
+
let pendingContext = "";
|
|
2812
2842
|
const server = tls.createServer(
|
|
2813
2843
|
{ key: leaf.keyPem, cert: leaf.certPem, ...alpnProtocols === void 0 ? {} : { ALPNProtocols: [...alpnProtocols] } },
|
|
2814
2844
|
(secure) => {
|
|
2815
|
-
onSecure(secure);
|
|
2845
|
+
onSecure(secure, pendingContext);
|
|
2816
2846
|
}
|
|
2817
2847
|
);
|
|
2818
2848
|
server.on("clientError", (_error, socket) => {
|
|
@@ -2820,7 +2850,8 @@ function realConnectEffects() {
|
|
|
2820
2850
|
});
|
|
2821
2851
|
return {
|
|
2822
2852
|
// tls.Server is a net.Server whose connection listener wraps the raw socket in the TLS handshake, and emitting the event by hand is what runs that listener on a socket this process already owns (the CONNECT half of the connection) rather than one the server accepted itself.
|
|
2823
|
-
accept: (socket) => {
|
|
2853
|
+
accept: (socket, context) => {
|
|
2854
|
+
pendingContext = context;
|
|
2824
2855
|
server.emit("connection", socket);
|
|
2825
2856
|
},
|
|
2826
2857
|
close: () => {
|
|
@@ -2834,9 +2865,8 @@ function realConnectEffects() {
|
|
|
2834
2865
|
socket.destroy();
|
|
2835
2866
|
});
|
|
2836
2867
|
if (onUpgrade !== void 0) {
|
|
2837
|
-
server.on("upgrade", (request3, socket) => {
|
|
2838
|
-
onUpgrade(request3);
|
|
2839
|
-
socket.destroy();
|
|
2868
|
+
server.on("upgrade", (request3, socket, head) => {
|
|
2869
|
+
onUpgrade(request3, socket, head);
|
|
2840
2870
|
});
|
|
2841
2871
|
}
|
|
2842
2872
|
return {
|
|
@@ -3064,6 +3094,7 @@ function ensureFrontDoor(params) {
|
|
|
3064
3094
|
const { paths, ports } = params;
|
|
3065
3095
|
const deadline = ports.now() + FRONTDOOR_START_TIMEOUT_MS;
|
|
3066
3096
|
let spawned = false;
|
|
3097
|
+
let replaced = false;
|
|
3067
3098
|
const distrusted = /* @__PURE__ */ new Set();
|
|
3068
3099
|
let lastRejection;
|
|
3069
3100
|
for (; ; ) {
|
|
@@ -3072,6 +3103,17 @@ function ensureFrontDoor(params) {
|
|
|
3072
3103
|
if (supervisorPid !== void 0 && !distrusted.has(supervisorPid) && ports.isRunning(supervisorPid)) {
|
|
3073
3104
|
if (state?.port !== void 0 && state.connectPort !== void 0) {
|
|
3074
3105
|
const verdict = ports.verifyListener(state.port);
|
|
3106
|
+
if (verdict.ok && (state.protocol ?? 0) < FRONT_DOOR_PROTOCOL && !replaced) {
|
|
3107
|
+
ports.stopSupervisor(supervisorPid);
|
|
3108
|
+
while (ports.isRunning(supervisorPid)) {
|
|
3109
|
+
if (ports.now() >= deadline) {
|
|
3110
|
+
throw new FrontDoorStartError(`agent-shim: the front door (pid ${String(supervisorPid)}, protocol ${String(state.protocol ?? 0)}) is older than this launcher (protocol ${String(FRONT_DOOR_PROTOCOL)}) and did not exit when asked. Daemon log: ${paths.frontdoorLogPath}`);
|
|
3111
|
+
}
|
|
3112
|
+
ports.sleep(FRONTDOOR_LAUNCHER_POLL_MS);
|
|
3113
|
+
}
|
|
3114
|
+
replaced = true;
|
|
3115
|
+
continue;
|
|
3116
|
+
}
|
|
3075
3117
|
if (verdict.ok) {
|
|
3076
3118
|
const token = (0, import_node_crypto5.randomUUID)();
|
|
3077
3119
|
writeFrontDoorSession(ports.fs, paths.frontdoorSessionsDir, { pid: params.launcherPid, startedAt: ports.now(), token });
|
package/dist/index.mjs
CHANGED
|
@@ -2060,7 +2060,7 @@ function issueLeafCertificate(ca, names, notBefore, notAfter) {
|
|
|
2060
2060
|
}
|
|
2061
2061
|
|
|
2062
2062
|
// src/frontdoor/tap.ts
|
|
2063
|
-
function pumpTapSession(secure, openUpstream, tap) {
|
|
2063
|
+
function pumpTapSession(secure, openUpstream, tap, prefix = Buffer.alloc(0)) {
|
|
2064
2064
|
let upstream;
|
|
2065
2065
|
let ended = false;
|
|
2066
2066
|
const end = () => {
|
|
@@ -2092,6 +2092,9 @@ function pumpTapSession(secure, openUpstream, tap) {
|
|
|
2092
2092
|
upstream.destroy();
|
|
2093
2093
|
return;
|
|
2094
2094
|
}
|
|
2095
|
+
if (prefix.length > 0) {
|
|
2096
|
+
upstream.write(prefix);
|
|
2097
|
+
}
|
|
2095
2098
|
if (tap !== void 0) {
|
|
2096
2099
|
secure.on("data", (chunk) => {
|
|
2097
2100
|
tap.onChunk("client-to-server", chunk);
|
|
@@ -2125,11 +2128,19 @@ function capabilityFromProxyAuthorization(value) {
|
|
|
2125
2128
|
return password === "" ? void 0 : password;
|
|
2126
2129
|
}
|
|
2127
2130
|
|
|
2131
|
+
// src/frontdoor/route.ts
|
|
2132
|
+
var AUTH_HEADER = "x-agent-shim-auth";
|
|
2133
|
+
|
|
2128
2134
|
// src/frontdoor/state.ts
|
|
2129
2135
|
import path5 from "node:path";
|
|
2130
2136
|
import { z as z3 } from "zod";
|
|
2137
|
+
var FRONT_DOOR_PROTOCOL = 1;
|
|
2131
2138
|
var FrontDoorSessionSchema = z3.strictObject({ pid: z3.number().int().positive(), startedAt: z3.number(), token: z3.string().min(1) });
|
|
2132
2139
|
var FrontDoorStateSchema = z3.strictObject({
|
|
2140
|
+
/**
|
|
2141
|
+
* The launcher-to-door wire protocol this supervisor speaks, written once it is serving; absent in state written by a release that predates the field, which `ensureFrontDoor` reads as protocol 0. A launcher replaces a door whose protocol is lower than its own (see `FRONT_DOOR_PROTOCOL`).
|
|
2142
|
+
*/
|
|
2143
|
+
protocol: z3.number().int().nonnegative().optional(),
|
|
2133
2144
|
/** The supervisor process serving the front door. Alive means both listeners are up in that process. */
|
|
2134
2145
|
supervisorPid: z3.number().int().positive().optional(),
|
|
2135
2146
|
/** 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. */
|
|
@@ -2275,7 +2286,7 @@ async function runFrontDoorSupervisor(idleShutdownMinutes, ports, options = {})
|
|
|
2275
2286
|
ports.log(`agent-shim frontdoor supervisor: sticky direct port ${String(previousStickyDirect)} was occupied; moving to ${String(direct.port)} (headroom must be restarted to admit the new address)`);
|
|
2276
2287
|
}
|
|
2277
2288
|
lastDirectPort = direct.port;
|
|
2278
|
-
writeFrontDoorState(fs3, paths.frontdoorStateFile, { supervisorPid: ports.ownPid, port: http2.port, connectPort: connect3.port, directPort: direct.port, ...sticky() });
|
|
2289
|
+
writeFrontDoorState(fs3, paths.frontdoorStateFile, { protocol: FRONT_DOOR_PROTOCOL, supervisorPid: ports.ownPid, port: http2.port, connectPort: connect3.port, directPort: direct.port, ...sticky() });
|
|
2279
2290
|
fs3.removeRecursive(paths.frontdoorLockFile);
|
|
2280
2291
|
ports.log(`agent-shim frontdoor supervisor ${String(ports.ownPid)}: front door on 127.0.0.1:${String(http2.port)}, CONNECT surface on 127.0.0.1:${String(connect3.port)}`);
|
|
2281
2292
|
let idleSince;
|
|
@@ -2417,7 +2428,19 @@ function caParses(ca) {
|
|
|
2417
2428
|
}
|
|
2418
2429
|
var TAP_ALPN_PROTOCOLS = ["h2", "http/1.1"];
|
|
2419
2430
|
async function startConnectServer(config, effects, preferredPort) {
|
|
2420
|
-
const
|
|
2431
|
+
const tunnelTokens = /* @__PURE__ */ new WeakMap();
|
|
2432
|
+
const onUpgrade = (host) => (request3, socket, head) => {
|
|
2433
|
+
config.capture?.upgrade(request3);
|
|
2434
|
+
if (head.length > 0) {
|
|
2435
|
+
socket.pause();
|
|
2436
|
+
socket.unshift(head);
|
|
2437
|
+
}
|
|
2438
|
+
const headText = `${request3.method ?? "GET"} ${request3.url ?? "/"} HTTP/1.1\r
|
|
2439
|
+
${request3.rawHeaders.reduce((accumulated, value, index) => index % 2 === 0 ? `${accumulated}${value}: ` : `${accumulated}${value}\r
|
|
2440
|
+
`, "")}\r
|
|
2441
|
+
`;
|
|
2442
|
+
pumpTapSession(socket, async (clientAlpn) => await effects.connectTlsUpstream(host, HTTPS_PORT, clientAlpn), void 0, Buffer.from(headText, "utf8"));
|
|
2443
|
+
};
|
|
2421
2444
|
const sessions = config.interceptHosts.map((host) => {
|
|
2422
2445
|
if (config.tapHosts.includes(host)) {
|
|
2423
2446
|
const tlsAcceptor2 = effects.createTlsAcceptor(
|
|
@@ -2431,13 +2454,18 @@ async function startConnectServer(config, effects, preferredPort) {
|
|
|
2431
2454
|
}
|
|
2432
2455
|
const handler = (request3, response) => {
|
|
2433
2456
|
if (host === config.routedHost && servedByPipeline(request3.url)) {
|
|
2457
|
+
const tunnelToken = tunnelTokens.get(request3.socket);
|
|
2458
|
+
if (tunnelToken !== void 0 && request3.headers[AUTH_HEADER] === void 0) {
|
|
2459
|
+
request3.headers[AUTH_HEADER] = tunnelToken;
|
|
2460
|
+
}
|
|
2434
2461
|
config.serveRouted(request3, response);
|
|
2435
2462
|
return;
|
|
2436
2463
|
}
|
|
2437
2464
|
effects.forwardHttp(config.upstreamFor(host), request3, response, config.capture?.observePassthrough(request3));
|
|
2438
2465
|
};
|
|
2439
|
-
const httpSession = effects.createHttpSession(handler, onUpgrade);
|
|
2440
|
-
const tlsAcceptor = effects.createTlsAcceptor(config.leafFor(host), (secure) => {
|
|
2466
|
+
const httpSession = effects.createHttpSession(handler, onUpgrade(host));
|
|
2467
|
+
const tlsAcceptor = effects.createTlsAcceptor(config.leafFor(host), (secure, context) => {
|
|
2468
|
+
tunnelTokens.set(secure, context);
|
|
2441
2469
|
httpSession.serve(secure);
|
|
2442
2470
|
});
|
|
2443
2471
|
return { host, httpSession, tlsAcceptor };
|
|
@@ -2578,6 +2606,7 @@ function routeConnect(socket, head, context) {
|
|
|
2578
2606
|
const credential = proxyAuthorizationOf(head);
|
|
2579
2607
|
const capability = credential === void 0 ? void 0 : capabilityFromProxyAuthorization(credential);
|
|
2580
2608
|
if (capability === void 0 || !config.isLiveCapability(capability)) {
|
|
2609
|
+
console.error(`[upg-dbg] refused CONNECT to ${head.split("\r\n", 1)[0]?.split(" ")[1] ?? "?"} (credential ${credential === void 0 ? "absent" : "present but dead"})`);
|
|
2581
2610
|
refuse(socket, REFUSAL.proxyAuthenticationRequired, [`Proxy-Authenticate: Basic realm="${CONNECT_PROXY_REALM}"`]);
|
|
2582
2611
|
return;
|
|
2583
2612
|
}
|
|
@@ -2597,7 +2626,7 @@ function routeConnect(socket, head, context) {
|
|
|
2597
2626
|
config.capture?.connect(target, session !== void 0);
|
|
2598
2627
|
socket.write(CONNECT_ESTABLISHED);
|
|
2599
2628
|
if (session !== void 0) {
|
|
2600
|
-
session.tlsAcceptor.accept(socket);
|
|
2629
|
+
session.tlsAcceptor.accept(socket, capability);
|
|
2601
2630
|
return;
|
|
2602
2631
|
}
|
|
2603
2632
|
void blindTunnel(socket, target, effects);
|
|
@@ -2709,10 +2738,11 @@ function realConnectEffects() {
|
|
|
2709
2738
|
return await listenOnce(0, onSocket);
|
|
2710
2739
|
},
|
|
2711
2740
|
createTlsAcceptor: (leaf, onSecure, alpnProtocols) => {
|
|
2741
|
+
let pendingContext = "";
|
|
2712
2742
|
const server = tls.createServer(
|
|
2713
2743
|
{ key: leaf.keyPem, cert: leaf.certPem, ...alpnProtocols === void 0 ? {} : { ALPNProtocols: [...alpnProtocols] } },
|
|
2714
2744
|
(secure) => {
|
|
2715
|
-
onSecure(secure);
|
|
2745
|
+
onSecure(secure, pendingContext);
|
|
2716
2746
|
}
|
|
2717
2747
|
);
|
|
2718
2748
|
server.on("clientError", (_error, socket) => {
|
|
@@ -2720,7 +2750,8 @@ function realConnectEffects() {
|
|
|
2720
2750
|
});
|
|
2721
2751
|
return {
|
|
2722
2752
|
// tls.Server is a net.Server whose connection listener wraps the raw socket in the TLS handshake, and emitting the event by hand is what runs that listener on a socket this process already owns (the CONNECT half of the connection) rather than one the server accepted itself.
|
|
2723
|
-
accept: (socket) => {
|
|
2753
|
+
accept: (socket, context) => {
|
|
2754
|
+
pendingContext = context;
|
|
2724
2755
|
server.emit("connection", socket);
|
|
2725
2756
|
},
|
|
2726
2757
|
close: () => {
|
|
@@ -2734,9 +2765,8 @@ function realConnectEffects() {
|
|
|
2734
2765
|
socket.destroy();
|
|
2735
2766
|
});
|
|
2736
2767
|
if (onUpgrade !== void 0) {
|
|
2737
|
-
server.on("upgrade", (request3, socket) => {
|
|
2738
|
-
onUpgrade(request3);
|
|
2739
|
-
socket.destroy();
|
|
2768
|
+
server.on("upgrade", (request3, socket, head) => {
|
|
2769
|
+
onUpgrade(request3, socket, head);
|
|
2740
2770
|
});
|
|
2741
2771
|
}
|
|
2742
2772
|
return {
|
|
@@ -2964,6 +2994,7 @@ function ensureFrontDoor(params) {
|
|
|
2964
2994
|
const { paths, ports } = params;
|
|
2965
2995
|
const deadline = ports.now() + FRONTDOOR_START_TIMEOUT_MS;
|
|
2966
2996
|
let spawned = false;
|
|
2997
|
+
let replaced = false;
|
|
2967
2998
|
const distrusted = /* @__PURE__ */ new Set();
|
|
2968
2999
|
let lastRejection;
|
|
2969
3000
|
for (; ; ) {
|
|
@@ -2972,6 +3003,17 @@ function ensureFrontDoor(params) {
|
|
|
2972
3003
|
if (supervisorPid !== void 0 && !distrusted.has(supervisorPid) && ports.isRunning(supervisorPid)) {
|
|
2973
3004
|
if (state?.port !== void 0 && state.connectPort !== void 0) {
|
|
2974
3005
|
const verdict = ports.verifyListener(state.port);
|
|
3006
|
+
if (verdict.ok && (state.protocol ?? 0) < FRONT_DOOR_PROTOCOL && !replaced) {
|
|
3007
|
+
ports.stopSupervisor(supervisorPid);
|
|
3008
|
+
while (ports.isRunning(supervisorPid)) {
|
|
3009
|
+
if (ports.now() >= deadline) {
|
|
3010
|
+
throw new FrontDoorStartError(`agent-shim: the front door (pid ${String(supervisorPid)}, protocol ${String(state.protocol ?? 0)}) is older than this launcher (protocol ${String(FRONT_DOOR_PROTOCOL)}) and did not exit when asked. Daemon log: ${paths.frontdoorLogPath}`);
|
|
3011
|
+
}
|
|
3012
|
+
ports.sleep(FRONTDOOR_LAUNCHER_POLL_MS);
|
|
3013
|
+
}
|
|
3014
|
+
replaced = true;
|
|
3015
|
+
continue;
|
|
3016
|
+
}
|
|
2975
3017
|
if (verdict.ok) {
|
|
2976
3018
|
const token = randomUUID2();
|
|
2977
3019
|
writeFrontDoorSession(ports.fs, paths.frontdoorSessionsDir, { pid: params.launcherPid, startedAt: ports.now(), token });
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { IncomingHttpHeaders, IncomingMessage, ServerResponse } from "node:http";
|
|
2
2
|
import type * as net from "node:net";
|
|
3
|
+
import type { Duplex } from "node:stream";
|
|
3
4
|
import type { ConnectCapture, PassthroughObserver } from "./capture";
|
|
4
5
|
/**
|
|
5
6
|
* The front door's CONNECT surface, so an OAuth session can have agent-shim's routing AND Claude Code's Remote Control at once: Remote Control refuses any `ANTHROPIC_BASE_URL` other than the real API, but happily honours `HTTPS_PROXY`, so OAuth launches route at the proxy layer instead.
|
|
@@ -125,8 +126,8 @@ export interface ConnectListenerHandle {
|
|
|
125
126
|
}
|
|
126
127
|
/** Presents one leaf's server-side TLS on CONNECTed sockets. */
|
|
127
128
|
interface TlsAcceptor {
|
|
128
|
-
/** Starts the TLS handshake on a socket that has just received its CONNECT response. */
|
|
129
|
-
readonly accept: (socket: net.Socket) => void;
|
|
129
|
+
/** Starts the TLS handshake on a socket that has just received its CONNECT response, carrying `context` to the handshake's completion untouched: whatever the caller knows about this connection (its tunnel's capability) reaches `onSecure` without the acceptor interpreting it. */
|
|
130
|
+
readonly accept: (socket: net.Socket, context: string) => void;
|
|
130
131
|
/** Drops every session this acceptor is terminating. */
|
|
131
132
|
readonly close: () => void;
|
|
132
133
|
}
|
|
@@ -141,12 +142,12 @@ interface HttpParserSession {
|
|
|
141
142
|
export interface ConnectEffects {
|
|
142
143
|
/** Binds a loopback TCP listener; `preferredPort` is tried first and any free port used when it is taken (bind, do not probe: check-then-bind races). Resolves with the bound port and a close handle. */
|
|
143
144
|
readonly listenLoopback: (preferredPort: number | undefined, onSocket: (socket: net.Socket) => void) => Promise<ConnectListenerHandle>;
|
|
144
|
-
/** Builds the TLS terminator for one leaf, handing each successfully handshaked session to `onSecure
|
|
145
|
-
readonly createTlsAcceptor: (leaf: LeafCert, onSecure: (secure: net.Socket) => void, alpnProtocols?: readonly string[]) => TlsAcceptor;
|
|
145
|
+
/** Builds the TLS terminator for one leaf, handing each successfully handshaked session to `onSecure` together with the context its `accept` call carried. `alpnProtocols`, when given, is advertised during the handshake, which is how a tap host's session can offer HTTP/2 the way its real server does. */
|
|
146
|
+
readonly createTlsAcceptor: (leaf: LeafCert, onSecure: (secure: net.Socket, context: string) => void, alpnProtocols?: readonly string[]) => TlsAcceptor;
|
|
146
147
|
/** Opens a TLS connection to a real host, presenting its name as SNI, for a tap session's upstream half. `clientAlpn`, when given, is the protocol the client negotiated with the surface and the only one offered upstream, so the tap never changes the channel's protocol. */
|
|
147
148
|
readonly connectTlsUpstream: (host: string, port: number, clientAlpn: string | undefined) => Promise<net.Socket>;
|
|
148
|
-
/** Builds the HTTP parser bound to one request handler. `onUpgrade`, when given,
|
|
149
|
-
readonly createHttpSession: (handler: ConnectRequestHandler, onUpgrade?: (request: IncomingMessage) => void) => HttpParserSession;
|
|
149
|
+
/** Builds the HTTP parser bound to one request handler. `onUpgrade`, when given, owns every HTTP upgrade request arriving on a terminated session, socket and already-read bytes included; a session without one lets Node destroy the upgrade, its own no-listener behaviour. */
|
|
150
|
+
readonly createHttpSession: (handler: ConnectRequestHandler, onUpgrade?: (request: IncomingMessage, socket: Duplex, head: Buffer) => void) => HttpParserSession;
|
|
150
151
|
/** Opens a raw TCP connection for a blind tunnel. */
|
|
151
152
|
readonly connectTcp: (host: string, port: number) => Promise<net.Socket>;
|
|
152
153
|
/** Streams one request to `target` and the response back, never buffering a body. When `observer` is given, the forwarding drives it with additive listeners (request and response chunks arrive to both the pipe and the observer) so capturing never changes the forwarding's own flow control. */
|
|
@@ -17,6 +17,8 @@ export interface EnsureFrontDoorPorts {
|
|
|
17
17
|
readonly spawnSupervisor: (paths: LayoutPaths) => number;
|
|
18
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
19
|
readonly verifyListener: (port: number) => ListenerVerdict;
|
|
20
|
+
/** Asks a running front-door supervisor to exit, without waiting for it to: the caller polls `isRunning`. Only ever called for a pid whose recorded listener has just authenticated as agent-shim's own. */
|
|
21
|
+
readonly stopSupervisor: (pid: number) => void;
|
|
20
22
|
}
|
|
21
23
|
/**
|
|
22
24
|
* Brings the front door up for this launch, or finds it serving, and registers this launcher pid in its session registry: the fact that keeps the door from idling out while this session lives, and the record that holds this launch's capability token. The same lock-and-poll coordination as headroom's and the old codex daemon's ensure: the exclusive-create start lock decides which of several concurrent launches spawns the one supervisor, everyone waits on state.json, and a lock whose holder died is removed and retried. Ready means state names a live supervisor and both listeners' ports, which the supervisor writes only after both have bound (and the provider listener has answered its own health probe).
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import type { IncomingHttpHeaders, IncomingMessage } from "node:http";
|
|
2
|
+
/**
|
|
3
|
+
* The agent-shim-internal headers the launcher injects through `ANTHROPIC_CUSTOM_HEADERS` so the front door can identify and route a request's session. None of them may ever leave the machine: the identity step strips every one before a route sees the request, so no upstream, and not even the headroom hop, learns what they say.
|
|
4
|
+
*/
|
|
5
|
+
export declare const INTERNAL_HEADER_NAMES: readonly ["x-agent-shim-identity", "x-agent-shim-session", "x-agent-shim-headroom", "x-agent-shim-auth", "x-agent-shim-hop", "x-agent-shim-hop-id", "x-headroom-project-id", "x-headroom-base-url"];
|
|
6
|
+
/** Carries the launching identity's name (the `@name` the launch resolved), set by the launcher and stripped at the door. */
|
|
7
|
+
export declare const IDENTITY_HEADER = "x-agent-shim-identity";
|
|
8
|
+
/** Carries one launch's session id, generated by the launcher and stripped at the door; response middleware keys its observations by it. */
|
|
9
|
+
export declare const SESSION_HEADER = "x-agent-shim-session";
|
|
10
|
+
/** Set to `1` when the launch resolved headroom on, so the front door knows this session's requests must take the headroom hop; stripped at the door. */
|
|
11
|
+
export declare const HEADROOM_FLAG_HEADER = "x-agent-shim-headroom";
|
|
12
|
+
/**
|
|
13
|
+
* Carries the project identity headroom scopes its memory state to (the git repository root of the launch directory). Also injected by the launcher and stripped at the door: the headroom hop re-sets it on its own request, so it reaches headroom and nowhere else.
|
|
14
|
+
*/
|
|
15
|
+
export declare const PROJECT_ID_HEADER = "x-headroom-project-id";
|
|
16
|
+
/**
|
|
17
|
+
* Headroom's per-request upstream selector, which the child used to carry and the door now owns: the door strips any inbound copy (a leftover from an older launcher's environment would otherwise redirect the daemon behind the door's back) and its hop sets it fresh when a route names an upstream.
|
|
18
|
+
*/
|
|
19
|
+
export declare const HEADROOM_BASE_URL_HEADER = "x-headroom-base-url";
|
|
20
|
+
/**
|
|
21
|
+
* The per-launch capability a client-facing listener requires before it routes anything: a random token the launcher generates, hands the child through `ANTHROPIC_CUSTOM_HEADERS`, and records in the door's session registry, so a loopback process that never launched through agent-shim cannot spend the session's credentials or quota. Stripped at the door like every internal header.
|
|
22
|
+
*/
|
|
23
|
+
export declare const AUTH_HEADER = "x-agent-shim-auth";
|
|
24
|
+
/**
|
|
25
|
+
* The capability the direct listener requires: a per-generation random secret held only in the door process's memory, set by the headroom hop on the requests it forwards back. A loopback process that finds the direct port still cannot use it, and nothing on disk ever holds the secret.
|
|
26
|
+
*/
|
|
27
|
+
export declare const HOP_SECRET_HEADER = "x-agent-shim-hop";
|
|
28
|
+
/**
|
|
29
|
+
* Names the credential custody record one headroom hop holds: a random id the hop sets on the request it hands headroom, alongside placeholders where the real credential headers were. The direct listener exchanges a live id (and only for the provider the record was made for) for the real headers before routing, so the credential itself never crosses headroom or the plain-HTTP leg back.
|
|
30
|
+
*/
|
|
31
|
+
export declare const HOP_ID_HEADER = "x-agent-shim-hop-id";
|
|
32
|
+
/** What the identity step learned about one request from the launcher-injected headers. */
|
|
33
|
+
export interface SessionIdentity {
|
|
34
|
+
/** The identity name the launch resolved, when it resolved one. */
|
|
35
|
+
readonly identity: string | undefined;
|
|
36
|
+
/** The per-launch session id, when the launcher injected one. */
|
|
37
|
+
readonly sessionId: string | undefined;
|
|
38
|
+
/** Whether this launch resolved headroom on, so its requests must pass through the headroom hop before their route. */
|
|
39
|
+
readonly headroom: boolean;
|
|
40
|
+
/** The project identity headroom scopes memory to, re-set on the headroom hop and sent nowhere else. */
|
|
41
|
+
readonly projectId: string | undefined;
|
|
42
|
+
}
|
|
43
|
+
/** The outcome of the pipeline's first step: what the session is, and the headers that may actually leave the machine. */
|
|
44
|
+
export interface IdentifiedRequest {
|
|
45
|
+
readonly session: SessionIdentity;
|
|
46
|
+
/**
|
|
47
|
+
* The request's headers with every internal header removed. A route forwards these, and only these, onwards: the identity and session headers are for the front door alone and must never reach an upstream, the headroom hop, or a translator's backend call.
|
|
48
|
+
*/
|
|
49
|
+
readonly forwardableHeaders: IncomingHttpHeaders;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* The identity step: splits a request's headers into what the front door may learn (the session) and what may leave the machine (everything else). Internal header names are matched case-insensitively, which is how the transport already normalises them.
|
|
53
|
+
*/
|
|
54
|
+
export declare function identifyRequest(headers: Readonly<IncomingHttpHeaders>): IdentifiedRequest;
|
|
55
|
+
/** The path prefix every provider-scoped request sits under: a routed session's base URL is the front door's address plus `/providers/<name>`, so one listener serves any number of providers, each with its own credential and translation settings. */
|
|
56
|
+
export declare const PROVIDER_PATH_PREFIX = "/providers/";
|
|
57
|
+
/** The base URL a provider's sessions are pointed at: HTTPS, because the child sends its provider credential on every request and only a listener presenting a leaf signed by agent-shim's CA may receive it. */
|
|
58
|
+
export declare function providerBaseUrl(port: number, provider: string): string;
|
|
59
|
+
/** The bare origin of a loopback listener, which is what a headroom hop is told to forward to: headroom appends the client's own request path to it. */
|
|
60
|
+
export declare function directOrigin(port: number): string;
|
|
61
|
+
/** Splits `/providers/<name>/<rest>` into the provider name and the rest, or undefined for any other path. */
|
|
62
|
+
export declare function parseProviderPath(path: string): {
|
|
63
|
+
readonly provider: string;
|
|
64
|
+
readonly rest: string;
|
|
65
|
+
} | undefined;
|
|
66
|
+
/** One request as a route sees it: the transport's own request and abort signal, plus what the identity step resolved. */
|
|
67
|
+
export interface RoutedRequest {
|
|
68
|
+
readonly method: string;
|
|
69
|
+
/** The request target exactly as received, path and query together. */
|
|
70
|
+
readonly url: string;
|
|
71
|
+
/** The request's headers with every internal header already stripped: exactly what a route may forward onwards. */
|
|
72
|
+
readonly headers: IncomingHttpHeaders;
|
|
73
|
+
/** The request's body stream: a route either pipes it onward untouched or reads it whole, never both. */
|
|
74
|
+
readonly body: IncomingMessage;
|
|
75
|
+
/** Aborted the moment the client goes away; a route aborts its own upstream work with it. */
|
|
76
|
+
readonly signal: AbortSignal;
|
|
77
|
+
readonly session: SessionIdentity;
|
|
78
|
+
}
|
|
79
|
+
/** The response half a route writes through, so the pipeline can run response middleware at the head and apply backpressure uniformly. */
|
|
80
|
+
export interface RoutedResponse {
|
|
81
|
+
/** Whether the head has already been written. */
|
|
82
|
+
readonly headersSent: boolean;
|
|
83
|
+
/** Writes the response head, running every response observer first. Must be called exactly once, before any write. */
|
|
84
|
+
readonly start: (status: number, headers: Readonly<Record<string, string>>) => void;
|
|
85
|
+
/** Sends the head on its way before any body chunk exists: a streaming response's client should see its headers the moment they are known. */
|
|
86
|
+
readonly flush: () => void;
|
|
87
|
+
/**
|
|
88
|
+
* Writes one body chunk, waiting for the socket to take it when the client is slow (backpressure: a slow client slows the route, and its upstream, instead of growing a buffer without bound). Resolves early, without writing, once the client is gone.
|
|
89
|
+
*/
|
|
90
|
+
readonly write: (chunk: string | Uint8Array) => Promise<void>;
|
|
91
|
+
/** Ends the response. */
|
|
92
|
+
readonly end: () => void;
|
|
93
|
+
/** Drops the connection: an upstream failure after the head went out, where no status can be sent any more. */
|
|
94
|
+
readonly destroy: () => void;
|
|
95
|
+
}
|
|
96
|
+
/** One pluggable destination the front door routes a request to. */
|
|
97
|
+
export interface FrontDoorRoute {
|
|
98
|
+
/** The route's name, for logs and for response middleware context. */
|
|
99
|
+
readonly name: string;
|
|
100
|
+
/**
|
|
101
|
+
* Whether the headroom hop may sit in front of this route. Headroom needs the Anthropic-shaped request, so a protocol translator built on an Anthropic-compatible surface opts in, and a route whose request shape headroom could not understand opts out.
|
|
102
|
+
*/
|
|
103
|
+
readonly headroomEligible: boolean;
|
|
104
|
+
/**
|
|
105
|
+
* The upstream headroom is told to use (its per-request base URL header) when the hop sits in front of this route. When defined it is always this door's direct listener, which is what lets the hop keep the route's credential in custody and the direct listener restore it; a route must never name any other address here. Undefined leaves headroom's own default upstream in charge, which is Claude Code's API.
|
|
106
|
+
*/
|
|
107
|
+
readonly headroomUpstream: string | undefined;
|
|
108
|
+
/**
|
|
109
|
+
* Serves the request directly: headroom has already been applied, or does not apply to this route. Writes the response through `response` and resolves once the response has ended.
|
|
110
|
+
*/
|
|
111
|
+
readonly serve: (request: RoutedRequest, response: RoutedResponse) => Promise<void>;
|
|
112
|
+
}
|
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import type { HeadroomFs } from "../headroom/state";
|
|
3
|
+
/**
|
|
4
|
+
* The launcher-to-door wire protocol this release speaks: the headers a launch's child presents, the capability registry's record format, and the routes. Bump it when a change would stop a door of this release serving launches made by an older one, or an older door serving this release's launches.
|
|
5
|
+
*
|
|
6
|
+
* The rule that makes a bump safe: a door accepts every lower protocol's launches (protocol 1 accepts the former `x-claude-use-*` header names), and a launcher replaces a door whose protocol is lower than its own, never one that is higher. Replacing is safe because restarts are what the door is designed for: the capability registry is on disk and the listeners keep their sticky ports, so launches already running reach the replacement on the addresses they froze at launch.
|
|
7
|
+
*/
|
|
8
|
+
export declare const FRONT_DOOR_PROTOCOL = 1;
|
|
3
9
|
/** 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
10
|
declare const FrontDoorSessionSchema: z.ZodObject<{
|
|
5
11
|
pid: z.ZodNumber;
|
|
@@ -11,6 +17,7 @@ export type FrontDoorSession = z.infer<typeof FrontDoorSessionSchema>;
|
|
|
11
17
|
* 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
18
|
*/
|
|
13
19
|
declare const FrontDoorStateSchema: z.ZodObject<{
|
|
20
|
+
protocol: z.ZodOptional<z.ZodNumber>;
|
|
14
21
|
supervisorPid: z.ZodOptional<z.ZodNumber>;
|
|
15
22
|
port: z.ZodOptional<z.ZodNumber>;
|
|
16
23
|
lastPort: z.ZodOptional<z.ZodNumber>;
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import type * as net from "node:net";
|
|
2
|
+
import type { Duplex } from "node:stream";
|
|
2
3
|
import type { StreamTap } from "./capture";
|
|
3
4
|
/**
|
|
4
5
|
* Pumps one already-TLS-terminated stream byte for byte to its real host over a fresh TLS connection, teeing every chunk in both directions into `tap` when one is given. This is the instrumentation for a host whose protocol the surface does not parse: the claude.ai control plane carries Remote Control on a channel that mixes shapes (observed under capture: an HTTP/1.1-shaped token request first, then binary HTTP/2 frames around it), so parsing it as either protocol would destroy or distort it. Pass everything through unchanged and record, never interpret: the frames are decoded offline, and a protocol the door does not understand keeps working exactly as the blind tunnel served it.
|
|
5
6
|
*
|
|
6
7
|
* Lifecycle mirrors `blindTunnel`: whoever goes away first takes the other side with it, so neither half of a tap session ever outlives its peer. The tap's listeners ride beside the pipes, additive like every other observer in this surface, so a recording failure fails loudly rather than silently stopping a diagnostic.
|
|
7
8
|
*/
|
|
8
|
-
export declare function pumpTapSession(secure:
|
|
9
|
+
export declare function pumpTapSession(secure: Duplex, openUpstream: (clientAlpn: string | undefined) => Promise<net.Socket>, tap: StreamTap | undefined, prefix?: Readonly<Buffer>): void;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-use",
|
|
3
|
-
"version": "7.
|
|
3
|
+
"version": "7.7.0",
|
|
4
4
|
"description": "A profile manager and launcher for Claude Code that lets one person run multiple logins from one machine while controlling what gets shared between them.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Joseph Mearman <joseph@mearman.co.uk>",
|