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 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.6.0",
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 onUpgrade = config2.capture === void 0 ? void 0 : (request6) => config2.capture?.upgrade(request6);
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 onUpgrade = config.capture === void 0 ? void 0 : (request3) => config.capture?.upgrade(request3);
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 onUpgrade = config.capture === void 0 ? void 0 : (request3) => config.capture?.upgrade(request3);
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`. `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. */
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, sees every HTTP upgrade request arriving on a terminated session; the session destroys the socket afterwards either way, with or without one, matching Node's own no-listener behaviour. */
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: net.Socket, openUpstream: (clientAlpn: string | undefined) => Promise<net.Socket>, tap: StreamTap | undefined): void;
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.6.0",
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>",