claude-use 7.5.0 → 7.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -1887,10 +1887,6 @@ function recoveryDiagnostics(recovery, identity) {
1887
1887
  // src/frontdoor/connect.ts
1888
1888
  var import_node_crypto3 = require("node:crypto");
1889
1889
  var fs = __toESM(require("node:fs"), 1);
1890
- var http = __toESM(require("node:http"), 1);
1891
- var https = __toESM(require("node:https"), 1);
1892
- var net2 = __toESM(require("node:net"), 1);
1893
- var tls = __toESM(require("node:tls"), 1);
1894
1890
 
1895
1891
  // src/frontdoor/x509.ts
1896
1892
  var import_node_crypto2 = require("node:crypto");
@@ -2163,6 +2159,54 @@ function issueLeafCertificate(ca, names, notBefore, notAfter) {
2163
2159
  return { certPem: toPem("CERTIFICATE", der), keyPem: keyPem(privateKey) };
2164
2160
  }
2165
2161
 
2162
+ // src/frontdoor/tap.ts
2163
+ function pumpTapSession(secure, openUpstream, tap) {
2164
+ let upstream;
2165
+ let ended = false;
2166
+ const end = () => {
2167
+ if (!ended) {
2168
+ ended = true;
2169
+ tap?.onEnd();
2170
+ }
2171
+ };
2172
+ const drop = () => {
2173
+ end();
2174
+ secure.destroy();
2175
+ upstream?.destroy();
2176
+ };
2177
+ secure.once("close", () => {
2178
+ end();
2179
+ upstream?.destroy();
2180
+ });
2181
+ secure.once("error", drop);
2182
+ const clientAlpn = "alpnProtocol" in secure && typeof secure.alpnProtocol === "string" ? secure.alpnProtocol : void 0;
2183
+ void (async () => {
2184
+ try {
2185
+ upstream = await openUpstream(clientAlpn);
2186
+ } catch {
2187
+ drop();
2188
+ return;
2189
+ }
2190
+ if (secure.destroyed) {
2191
+ end();
2192
+ upstream.destroy();
2193
+ return;
2194
+ }
2195
+ if (tap !== void 0) {
2196
+ secure.on("data", (chunk) => {
2197
+ tap.onChunk("client-to-server", chunk);
2198
+ });
2199
+ upstream.on("data", (chunk) => {
2200
+ tap.onChunk("server-to-client", chunk);
2201
+ });
2202
+ }
2203
+ secure.pipe(upstream);
2204
+ upstream.pipe(secure);
2205
+ upstream.once("close", drop);
2206
+ upstream.once("error", drop);
2207
+ })();
2208
+ }
2209
+
2166
2210
  // src/frontdoor/capability.ts
2167
2211
  var CONNECT_PROXY_REALM = "agent-shim front door";
2168
2212
  var BASIC_SCHEME = "basic";
@@ -2184,8 +2228,13 @@ function capabilityFromProxyAuthorization(value) {
2184
2228
  // src/frontdoor/state.ts
2185
2229
  var import_node_path5 = __toESM(require("node:path"), 1);
2186
2230
  var import_zod3 = require("zod");
2231
+ var FRONT_DOOR_PROTOCOL = 1;
2187
2232
  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) });
2188
2233
  var FrontDoorStateSchema = import_zod3.z.strictObject({
2234
+ /**
2235
+ * 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`).
2236
+ */
2237
+ protocol: import_zod3.z.number().int().nonnegative().optional(),
2189
2238
  /** The supervisor process serving the front door. Alive means both listeners are up in that process. */
2190
2239
  supervisorPid: import_zod3.z.number().int().positive().optional(),
2191
2240
  /** 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. */
@@ -2307,23 +2356,23 @@ async function runFrontDoorSupervisor(idleShutdownMinutes, ports, options = {})
2307
2356
  }
2308
2357
  lastPort = http2.port;
2309
2358
  const previousStickyConnect = lastConnectPort;
2310
- let connect2;
2359
+ let connect3;
2311
2360
  try {
2312
- connect2 = await ports.startConnectListener(lastConnectPort);
2361
+ connect3 = await ports.startConnectListener(lastConnectPort);
2313
2362
  } catch (error) {
2314
2363
  await http2.close();
2315
2364
  return fail(`could not start the CONNECT surface: ${error instanceof Error ? error.message : String(error)}`);
2316
2365
  }
2317
- if (previousStickyConnect !== void 0 && connect2.port !== previousStickyConnect) {
2318
- ports.log(`agent-shim frontdoor supervisor: sticky CONNECT port ${String(previousStickyConnect)} was occupied; moving to ${String(connect2.port)} (OAuth sessions launched against the old port are stale until they relaunch)`);
2366
+ if (previousStickyConnect !== void 0 && connect3.port !== previousStickyConnect) {
2367
+ ports.log(`agent-shim frontdoor supervisor: sticky CONNECT port ${String(previousStickyConnect)} was occupied; moving to ${String(connect3.port)} (OAuth sessions launched against the old port are stale until they relaunch)`);
2319
2368
  }
2320
- lastConnectPort = connect2.port;
2369
+ lastConnectPort = connect3.port;
2321
2370
  const previousStickyDirect = lastDirectPort;
2322
2371
  let direct;
2323
2372
  try {
2324
2373
  direct = await ports.startDirectListener(lastDirectPort);
2325
2374
  } catch (error) {
2326
- await connect2.close();
2375
+ await connect3.close();
2327
2376
  await http2.close();
2328
2377
  return fail(`could not start the direct listener: ${error instanceof Error ? error.message : String(error)}`);
2329
2378
  }
@@ -2331,9 +2380,9 @@ async function runFrontDoorSupervisor(idleShutdownMinutes, ports, options = {})
2331
2380
  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)`);
2332
2381
  }
2333
2382
  lastDirectPort = direct.port;
2334
- writeFrontDoorState(fs3, paths.frontdoorStateFile, { supervisorPid: ports.ownPid, port: http2.port, connectPort: connect2.port, directPort: direct.port, ...sticky() });
2383
+ writeFrontDoorState(fs3, paths.frontdoorStateFile, { protocol: FRONT_DOOR_PROTOCOL, supervisorPid: ports.ownPid, port: http2.port, connectPort: connect3.port, directPort: direct.port, ...sticky() });
2335
2384
  fs3.removeRecursive(paths.frontdoorLockFile);
2336
- 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(connect2.port)}`);
2385
+ 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)}`);
2337
2386
  let idleSince;
2338
2387
  let ticks = 0;
2339
2388
  for (; ; ) {
@@ -2347,7 +2396,7 @@ async function runFrontDoorSupervisor(idleShutdownMinutes, ports, options = {})
2347
2396
  if (ports.now() - idleSince >= idleShutdownMinutes * MS_PER_MINUTE) {
2348
2397
  ports.log(`agent-shim frontdoor supervisor: no sessions for ${String(idleShutdownMinutes)} minute(s); closing the front door and exiting`);
2349
2398
  await direct.close();
2350
- await connect2.close();
2399
+ await connect3.close();
2351
2400
  await http2.close();
2352
2401
  writeFrontDoorState(fs3, paths.frontdoorStateFile, sticky());
2353
2402
  return 0;
@@ -2471,9 +2520,20 @@ function caParses(ca) {
2471
2520
  return false;
2472
2521
  }
2473
2522
  }
2523
+ var TAP_ALPN_PROTOCOLS = ["h2", "http/1.1"];
2474
2524
  async function startConnectServer(config, effects, preferredPort) {
2475
2525
  const onUpgrade = config.capture === void 0 ? void 0 : (request3) => config.capture?.upgrade(request3);
2476
2526
  const sessions = config.interceptHosts.map((host) => {
2527
+ if (config.tapHosts.includes(host)) {
2528
+ const tlsAcceptor2 = effects.createTlsAcceptor(
2529
+ config.leafFor(host),
2530
+ (secure) => {
2531
+ pumpTapSession(secure, async (clientAlpn) => await effects.connectTlsUpstream(host, HTTPS_PORT, clientAlpn), config.capture?.tapStream?.(host));
2532
+ },
2533
+ TAP_ALPN_PROTOCOLS
2534
+ );
2535
+ return { host, tlsAcceptor: tlsAcceptor2 };
2536
+ }
2477
2537
  const handler = (request3, response) => {
2478
2538
  if (host === config.routedHost && servedByPipeline(request3.url)) {
2479
2539
  config.serveRouted(request3, response);
@@ -2498,7 +2558,7 @@ async function startConnectServer(config, effects, preferredPort) {
2498
2558
  await listener.close();
2499
2559
  for (const { httpSession, tlsAcceptor } of sessions) {
2500
2560
  tlsAcceptor.close();
2501
- httpSession.close();
2561
+ httpSession?.close();
2502
2562
  }
2503
2563
  }
2504
2564
  };
@@ -2674,6 +2734,40 @@ async function blindTunnel(socket, target, effects) {
2674
2734
  socket.destroy();
2675
2735
  });
2676
2736
  }
2737
+ function isEnoent(error) {
2738
+ return typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT";
2739
+ }
2740
+ function realConnectCertStore(paths) {
2741
+ const read = (file) => {
2742
+ try {
2743
+ return fs.readFileSync(file, "utf8");
2744
+ } catch (error) {
2745
+ if (isEnoent(error)) {
2746
+ return void 0;
2747
+ }
2748
+ throw error;
2749
+ }
2750
+ };
2751
+ return {
2752
+ loadCa: () => {
2753
+ const certPem = read(paths.frontdoorCaCertFile);
2754
+ const keyPem2 = read(paths.frontdoorCaKeyFile);
2755
+ return certPem === void 0 || keyPem2 === void 0 ? void 0 : { certPem, keyPem: keyPem2 };
2756
+ },
2757
+ writeCa: (ca) => {
2758
+ fs.mkdirSync(paths.frontdoorCaDir, { recursive: true });
2759
+ fs.writeFileSync(paths.frontdoorCaCertFile, ca.certPem, "utf8");
2760
+ fs.writeFileSync(paths.frontdoorCaKeyFile, ca.keyPem, { encoding: "utf8", mode: CA_KEY_FILE_MODE });
2761
+ fs.chmodSync(paths.frontdoorCaKeyFile, CA_KEY_FILE_MODE);
2762
+ }
2763
+ };
2764
+ }
2765
+
2766
+ // src/frontdoor/connectEffects.ts
2767
+ var http = __toESM(require("node:http"), 1);
2768
+ var https = __toESM(require("node:https"), 1);
2769
+ var net2 = __toESM(require("node:net"), 1);
2770
+ var tls = __toESM(require("node:tls"), 1);
2677
2771
  function realConnectEffects() {
2678
2772
  const plainAgent = new http.Agent({ keepAlive: true });
2679
2773
  const tlsAgent = new https.Agent({ keepAlive: true });
@@ -2719,10 +2813,13 @@ function realConnectEffects() {
2719
2813
  }
2720
2814
  return await listenOnce(0, onSocket);
2721
2815
  },
2722
- createTlsAcceptor: (leaf, onSecure) => {
2723
- const server = tls.createServer({ key: leaf.keyPem, cert: leaf.certPem }, (secure) => {
2724
- onSecure(secure);
2725
- });
2816
+ createTlsAcceptor: (leaf, onSecure, alpnProtocols) => {
2817
+ const server = tls.createServer(
2818
+ { key: leaf.keyPem, cert: leaf.certPem, ...alpnProtocols === void 0 ? {} : { ALPNProtocols: [...alpnProtocols] } },
2819
+ (secure) => {
2820
+ onSecure(secure);
2821
+ }
2822
+ );
2726
2823
  server.on("clientError", (_error, socket) => {
2727
2824
  socket.destroy();
2728
2825
  });
@@ -2757,6 +2854,13 @@ function realConnectEffects() {
2757
2854
  }
2758
2855
  };
2759
2856
  },
2857
+ connectTlsUpstream: async (host, port, clientAlpn) => await new Promise((resolve, reject) => {
2858
+ const socket = tls.connect({ host, port, servername: host, ...clientAlpn === void 0 ? {} : { ALPNProtocols: [clientAlpn] } });
2859
+ socket.once("secureConnect", () => {
2860
+ resolve(socket);
2861
+ });
2862
+ socket.once("error", reject);
2863
+ }),
2760
2864
  connectTcp: async (host, port) => await new Promise((resolve, reject) => {
2761
2865
  const socket = net2.connect({ host, port });
2762
2866
  socket.once("connect", () => {
@@ -2820,34 +2924,6 @@ function realConnectEffects() {
2820
2924
  }
2821
2925
  };
2822
2926
  }
2823
- function isEnoent(error) {
2824
- return typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT";
2825
- }
2826
- function realConnectCertStore(paths) {
2827
- const read = (file) => {
2828
- try {
2829
- return fs.readFileSync(file, "utf8");
2830
- } catch (error) {
2831
- if (isEnoent(error)) {
2832
- return void 0;
2833
- }
2834
- throw error;
2835
- }
2836
- };
2837
- return {
2838
- loadCa: () => {
2839
- const certPem = read(paths.frontdoorCaCertFile);
2840
- const keyPem2 = read(paths.frontdoorCaKeyFile);
2841
- return certPem === void 0 || keyPem2 === void 0 ? void 0 : { certPem, keyPem: keyPem2 };
2842
- },
2843
- writeCa: (ca) => {
2844
- fs.mkdirSync(paths.frontdoorCaDir, { recursive: true });
2845
- fs.writeFileSync(paths.frontdoorCaCertFile, ca.certPem, "utf8");
2846
- fs.writeFileSync(paths.frontdoorCaKeyFile, ca.keyPem, { encoding: "utf8", mode: CA_KEY_FILE_MODE });
2847
- fs.chmodSync(paths.frontdoorCaKeyFile, CA_KEY_FILE_MODE);
2848
- }
2849
- };
2850
- }
2851
2927
 
2852
2928
  // src/frontdoor/ensure.ts
2853
2929
  var import_node_crypto5 = require("node:crypto");
@@ -2993,6 +3069,7 @@ function ensureFrontDoor(params) {
2993
3069
  const { paths, ports } = params;
2994
3070
  const deadline = ports.now() + FRONTDOOR_START_TIMEOUT_MS;
2995
3071
  let spawned = false;
3072
+ let replaced = false;
2996
3073
  const distrusted = /* @__PURE__ */ new Set();
2997
3074
  let lastRejection;
2998
3075
  for (; ; ) {
@@ -3001,6 +3078,17 @@ function ensureFrontDoor(params) {
3001
3078
  if (supervisorPid !== void 0 && !distrusted.has(supervisorPid) && ports.isRunning(supervisorPid)) {
3002
3079
  if (state?.port !== void 0 && state.connectPort !== void 0) {
3003
3080
  const verdict = ports.verifyListener(state.port);
3081
+ if (verdict.ok && (state.protocol ?? 0) < FRONT_DOOR_PROTOCOL && !replaced) {
3082
+ ports.stopSupervisor(supervisorPid);
3083
+ while (ports.isRunning(supervisorPid)) {
3084
+ if (ports.now() >= deadline) {
3085
+ 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}`);
3086
+ }
3087
+ ports.sleep(FRONTDOOR_LAUNCHER_POLL_MS);
3088
+ }
3089
+ replaced = true;
3090
+ continue;
3091
+ }
3004
3092
  if (verdict.ok) {
3005
3093
  const token = (0, import_node_crypto5.randomUUID)();
3006
3094
  writeFrontDoorSession(ports.fs, paths.frontdoorSessionsDir, { pid: params.launcherPid, startedAt: ports.now(), token });
package/dist/index.mjs CHANGED
@@ -1787,10 +1787,6 @@ function recoveryDiagnostics(recovery, identity) {
1787
1787
  // src/frontdoor/connect.ts
1788
1788
  import { X509Certificate as X509Certificate2, createPrivateKey as createPrivateKey2 } from "node:crypto";
1789
1789
  import * as fs from "node:fs";
1790
- import * as http from "node:http";
1791
- import * as https from "node:https";
1792
- import * as net2 from "node:net";
1793
- import * as tls from "node:tls";
1794
1790
 
1795
1791
  // src/frontdoor/x509.ts
1796
1792
  import { createHash, createPrivateKey, generateKeyPairSync, randomBytes, sign, X509Certificate } from "node:crypto";
@@ -2063,6 +2059,54 @@ function issueLeafCertificate(ca, names, notBefore, notAfter) {
2063
2059
  return { certPem: toPem("CERTIFICATE", der), keyPem: keyPem(privateKey) };
2064
2060
  }
2065
2061
 
2062
+ // src/frontdoor/tap.ts
2063
+ function pumpTapSession(secure, openUpstream, tap) {
2064
+ let upstream;
2065
+ let ended = false;
2066
+ const end = () => {
2067
+ if (!ended) {
2068
+ ended = true;
2069
+ tap?.onEnd();
2070
+ }
2071
+ };
2072
+ const drop = () => {
2073
+ end();
2074
+ secure.destroy();
2075
+ upstream?.destroy();
2076
+ };
2077
+ secure.once("close", () => {
2078
+ end();
2079
+ upstream?.destroy();
2080
+ });
2081
+ secure.once("error", drop);
2082
+ const clientAlpn = "alpnProtocol" in secure && typeof secure.alpnProtocol === "string" ? secure.alpnProtocol : void 0;
2083
+ void (async () => {
2084
+ try {
2085
+ upstream = await openUpstream(clientAlpn);
2086
+ } catch {
2087
+ drop();
2088
+ return;
2089
+ }
2090
+ if (secure.destroyed) {
2091
+ end();
2092
+ upstream.destroy();
2093
+ return;
2094
+ }
2095
+ if (tap !== void 0) {
2096
+ secure.on("data", (chunk) => {
2097
+ tap.onChunk("client-to-server", chunk);
2098
+ });
2099
+ upstream.on("data", (chunk) => {
2100
+ tap.onChunk("server-to-client", chunk);
2101
+ });
2102
+ }
2103
+ secure.pipe(upstream);
2104
+ upstream.pipe(secure);
2105
+ upstream.once("close", drop);
2106
+ upstream.once("error", drop);
2107
+ })();
2108
+ }
2109
+
2066
2110
  // src/frontdoor/capability.ts
2067
2111
  var CONNECT_PROXY_REALM = "agent-shim front door";
2068
2112
  var BASIC_SCHEME = "basic";
@@ -2084,8 +2128,13 @@ function capabilityFromProxyAuthorization(value) {
2084
2128
  // src/frontdoor/state.ts
2085
2129
  import path5 from "node:path";
2086
2130
  import { z as z3 } from "zod";
2131
+ var FRONT_DOOR_PROTOCOL = 1;
2087
2132
  var FrontDoorSessionSchema = z3.strictObject({ pid: z3.number().int().positive(), startedAt: z3.number(), token: z3.string().min(1) });
2088
2133
  var FrontDoorStateSchema = z3.strictObject({
2134
+ /**
2135
+ * 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`).
2136
+ */
2137
+ protocol: z3.number().int().nonnegative().optional(),
2089
2138
  /** The supervisor process serving the front door. Alive means both listeners are up in that process. */
2090
2139
  supervisorPid: z3.number().int().positive().optional(),
2091
2140
  /** 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. */
@@ -2207,23 +2256,23 @@ async function runFrontDoorSupervisor(idleShutdownMinutes, ports, options = {})
2207
2256
  }
2208
2257
  lastPort = http2.port;
2209
2258
  const previousStickyConnect = lastConnectPort;
2210
- let connect2;
2259
+ let connect3;
2211
2260
  try {
2212
- connect2 = await ports.startConnectListener(lastConnectPort);
2261
+ connect3 = await ports.startConnectListener(lastConnectPort);
2213
2262
  } catch (error) {
2214
2263
  await http2.close();
2215
2264
  return fail(`could not start the CONNECT surface: ${error instanceof Error ? error.message : String(error)}`);
2216
2265
  }
2217
- if (previousStickyConnect !== void 0 && connect2.port !== previousStickyConnect) {
2218
- ports.log(`agent-shim frontdoor supervisor: sticky CONNECT port ${String(previousStickyConnect)} was occupied; moving to ${String(connect2.port)} (OAuth sessions launched against the old port are stale until they relaunch)`);
2266
+ if (previousStickyConnect !== void 0 && connect3.port !== previousStickyConnect) {
2267
+ ports.log(`agent-shim frontdoor supervisor: sticky CONNECT port ${String(previousStickyConnect)} was occupied; moving to ${String(connect3.port)} (OAuth sessions launched against the old port are stale until they relaunch)`);
2219
2268
  }
2220
- lastConnectPort = connect2.port;
2269
+ lastConnectPort = connect3.port;
2221
2270
  const previousStickyDirect = lastDirectPort;
2222
2271
  let direct;
2223
2272
  try {
2224
2273
  direct = await ports.startDirectListener(lastDirectPort);
2225
2274
  } catch (error) {
2226
- await connect2.close();
2275
+ await connect3.close();
2227
2276
  await http2.close();
2228
2277
  return fail(`could not start the direct listener: ${error instanceof Error ? error.message : String(error)}`);
2229
2278
  }
@@ -2231,9 +2280,9 @@ async function runFrontDoorSupervisor(idleShutdownMinutes, ports, options = {})
2231
2280
  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)`);
2232
2281
  }
2233
2282
  lastDirectPort = direct.port;
2234
- writeFrontDoorState(fs3, paths.frontdoorStateFile, { supervisorPid: ports.ownPid, port: http2.port, connectPort: connect2.port, directPort: direct.port, ...sticky() });
2283
+ writeFrontDoorState(fs3, paths.frontdoorStateFile, { protocol: FRONT_DOOR_PROTOCOL, supervisorPid: ports.ownPid, port: http2.port, connectPort: connect3.port, directPort: direct.port, ...sticky() });
2235
2284
  fs3.removeRecursive(paths.frontdoorLockFile);
2236
- 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(connect2.port)}`);
2285
+ 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)}`);
2237
2286
  let idleSince;
2238
2287
  let ticks = 0;
2239
2288
  for (; ; ) {
@@ -2247,7 +2296,7 @@ async function runFrontDoorSupervisor(idleShutdownMinutes, ports, options = {})
2247
2296
  if (ports.now() - idleSince >= idleShutdownMinutes * MS_PER_MINUTE) {
2248
2297
  ports.log(`agent-shim frontdoor supervisor: no sessions for ${String(idleShutdownMinutes)} minute(s); closing the front door and exiting`);
2249
2298
  await direct.close();
2250
- await connect2.close();
2299
+ await connect3.close();
2251
2300
  await http2.close();
2252
2301
  writeFrontDoorState(fs3, paths.frontdoorStateFile, sticky());
2253
2302
  return 0;
@@ -2371,9 +2420,20 @@ function caParses(ca) {
2371
2420
  return false;
2372
2421
  }
2373
2422
  }
2423
+ var TAP_ALPN_PROTOCOLS = ["h2", "http/1.1"];
2374
2424
  async function startConnectServer(config, effects, preferredPort) {
2375
2425
  const onUpgrade = config.capture === void 0 ? void 0 : (request3) => config.capture?.upgrade(request3);
2376
2426
  const sessions = config.interceptHosts.map((host) => {
2427
+ if (config.tapHosts.includes(host)) {
2428
+ const tlsAcceptor2 = effects.createTlsAcceptor(
2429
+ config.leafFor(host),
2430
+ (secure) => {
2431
+ pumpTapSession(secure, async (clientAlpn) => await effects.connectTlsUpstream(host, HTTPS_PORT, clientAlpn), config.capture?.tapStream?.(host));
2432
+ },
2433
+ TAP_ALPN_PROTOCOLS
2434
+ );
2435
+ return { host, tlsAcceptor: tlsAcceptor2 };
2436
+ }
2377
2437
  const handler = (request3, response) => {
2378
2438
  if (host === config.routedHost && servedByPipeline(request3.url)) {
2379
2439
  config.serveRouted(request3, response);
@@ -2398,7 +2458,7 @@ async function startConnectServer(config, effects, preferredPort) {
2398
2458
  await listener.close();
2399
2459
  for (const { httpSession, tlsAcceptor } of sessions) {
2400
2460
  tlsAcceptor.close();
2401
- httpSession.close();
2461
+ httpSession?.close();
2402
2462
  }
2403
2463
  }
2404
2464
  };
@@ -2574,6 +2634,40 @@ async function blindTunnel(socket, target, effects) {
2574
2634
  socket.destroy();
2575
2635
  });
2576
2636
  }
2637
+ function isEnoent(error) {
2638
+ return typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT";
2639
+ }
2640
+ function realConnectCertStore(paths) {
2641
+ const read = (file) => {
2642
+ try {
2643
+ return fs.readFileSync(file, "utf8");
2644
+ } catch (error) {
2645
+ if (isEnoent(error)) {
2646
+ return void 0;
2647
+ }
2648
+ throw error;
2649
+ }
2650
+ };
2651
+ return {
2652
+ loadCa: () => {
2653
+ const certPem = read(paths.frontdoorCaCertFile);
2654
+ const keyPem2 = read(paths.frontdoorCaKeyFile);
2655
+ return certPem === void 0 || keyPem2 === void 0 ? void 0 : { certPem, keyPem: keyPem2 };
2656
+ },
2657
+ writeCa: (ca) => {
2658
+ fs.mkdirSync(paths.frontdoorCaDir, { recursive: true });
2659
+ fs.writeFileSync(paths.frontdoorCaCertFile, ca.certPem, "utf8");
2660
+ fs.writeFileSync(paths.frontdoorCaKeyFile, ca.keyPem, { encoding: "utf8", mode: CA_KEY_FILE_MODE });
2661
+ fs.chmodSync(paths.frontdoorCaKeyFile, CA_KEY_FILE_MODE);
2662
+ }
2663
+ };
2664
+ }
2665
+
2666
+ // src/frontdoor/connectEffects.ts
2667
+ import * as http from "node:http";
2668
+ import * as https from "node:https";
2669
+ import * as net2 from "node:net";
2670
+ import * as tls from "node:tls";
2577
2671
  function realConnectEffects() {
2578
2672
  const plainAgent = new http.Agent({ keepAlive: true });
2579
2673
  const tlsAgent = new https.Agent({ keepAlive: true });
@@ -2619,10 +2713,13 @@ function realConnectEffects() {
2619
2713
  }
2620
2714
  return await listenOnce(0, onSocket);
2621
2715
  },
2622
- createTlsAcceptor: (leaf, onSecure) => {
2623
- const server = tls.createServer({ key: leaf.keyPem, cert: leaf.certPem }, (secure) => {
2624
- onSecure(secure);
2625
- });
2716
+ createTlsAcceptor: (leaf, onSecure, alpnProtocols) => {
2717
+ const server = tls.createServer(
2718
+ { key: leaf.keyPem, cert: leaf.certPem, ...alpnProtocols === void 0 ? {} : { ALPNProtocols: [...alpnProtocols] } },
2719
+ (secure) => {
2720
+ onSecure(secure);
2721
+ }
2722
+ );
2626
2723
  server.on("clientError", (_error, socket) => {
2627
2724
  socket.destroy();
2628
2725
  });
@@ -2657,6 +2754,13 @@ function realConnectEffects() {
2657
2754
  }
2658
2755
  };
2659
2756
  },
2757
+ connectTlsUpstream: async (host, port, clientAlpn) => await new Promise((resolve, reject) => {
2758
+ const socket = tls.connect({ host, port, servername: host, ...clientAlpn === void 0 ? {} : { ALPNProtocols: [clientAlpn] } });
2759
+ socket.once("secureConnect", () => {
2760
+ resolve(socket);
2761
+ });
2762
+ socket.once("error", reject);
2763
+ }),
2660
2764
  connectTcp: async (host, port) => await new Promise((resolve, reject) => {
2661
2765
  const socket = net2.connect({ host, port });
2662
2766
  socket.once("connect", () => {
@@ -2720,34 +2824,6 @@ function realConnectEffects() {
2720
2824
  }
2721
2825
  };
2722
2826
  }
2723
- function isEnoent(error) {
2724
- return typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT";
2725
- }
2726
- function realConnectCertStore(paths) {
2727
- const read = (file) => {
2728
- try {
2729
- return fs.readFileSync(file, "utf8");
2730
- } catch (error) {
2731
- if (isEnoent(error)) {
2732
- return void 0;
2733
- }
2734
- throw error;
2735
- }
2736
- };
2737
- return {
2738
- loadCa: () => {
2739
- const certPem = read(paths.frontdoorCaCertFile);
2740
- const keyPem2 = read(paths.frontdoorCaKeyFile);
2741
- return certPem === void 0 || keyPem2 === void 0 ? void 0 : { certPem, keyPem: keyPem2 };
2742
- },
2743
- writeCa: (ca) => {
2744
- fs.mkdirSync(paths.frontdoorCaDir, { recursive: true });
2745
- fs.writeFileSync(paths.frontdoorCaCertFile, ca.certPem, "utf8");
2746
- fs.writeFileSync(paths.frontdoorCaKeyFile, ca.keyPem, { encoding: "utf8", mode: CA_KEY_FILE_MODE });
2747
- fs.chmodSync(paths.frontdoorCaKeyFile, CA_KEY_FILE_MODE);
2748
- }
2749
- };
2750
- }
2751
2827
 
2752
2828
  // src/frontdoor/ensure.ts
2753
2829
  import { randomUUID as randomUUID2 } from "node:crypto";
@@ -2893,6 +2969,7 @@ function ensureFrontDoor(params) {
2893
2969
  const { paths, ports } = params;
2894
2970
  const deadline = ports.now() + FRONTDOOR_START_TIMEOUT_MS;
2895
2971
  let spawned = false;
2972
+ let replaced = false;
2896
2973
  const distrusted = /* @__PURE__ */ new Set();
2897
2974
  let lastRejection;
2898
2975
  for (; ; ) {
@@ -2901,6 +2978,17 @@ function ensureFrontDoor(params) {
2901
2978
  if (supervisorPid !== void 0 && !distrusted.has(supervisorPid) && ports.isRunning(supervisorPid)) {
2902
2979
  if (state?.port !== void 0 && state.connectPort !== void 0) {
2903
2980
  const verdict = ports.verifyListener(state.port);
2981
+ if (verdict.ok && (state.protocol ?? 0) < FRONT_DOOR_PROTOCOL && !replaced) {
2982
+ ports.stopSupervisor(supervisorPid);
2983
+ while (ports.isRunning(supervisorPid)) {
2984
+ if (ports.now() >= deadline) {
2985
+ 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}`);
2986
+ }
2987
+ ports.sleep(FRONTDOOR_LAUNCHER_POLL_MS);
2988
+ }
2989
+ replaced = true;
2990
+ continue;
2991
+ }
2904
2992
  if (verdict.ok) {
2905
2993
  const token = randomUUID2();
2906
2994
  writeFrontDoorSession(ports.fs, paths.frontdoorSessionsDir, { pid: params.launcherPid, startedAt: ports.now(), token });
@@ -20,8 +20,17 @@ export interface PassthroughObserver {
20
20
  readonly onResponseChunk: (chunk: Buffer) => void;
21
21
  readonly onEnd: () => void;
22
22
  }
23
+ /** The direction one tapped byte chunk flowed, named from the session's point of view. */
24
+ type StreamDirection = "client-to-server" | "server-to-client";
25
+ /** The byte-level tap for one terminated stream whose protocol the surface does not parse: every chunk both directions, then the end. */
26
+ export interface StreamTap {
27
+ readonly onChunk: (direction: StreamDirection, chunk: Readonly<Buffer>) => void;
28
+ readonly onEnd: () => void;
29
+ }
23
30
  /** The tap the connect surface drives. `undefined` everywhere means "not capturing"; one object covers the whole door process. */
24
31
  export interface ConnectCapture {
32
+ /** Opens the byte-level tap for one host's terminated stream, whose chunks are recorded verbatim (text, or base64 when not valid UTF-8) under the same caps as piped bodies. Optional because a capture can be stream-silent by construction. */
33
+ readonly tapStream?: (host: string) => StreamTap;
25
34
  /** Records one authenticated CONNECT target: the host and port only, since a blind tunnel's bytes are unreadable by construction. */
26
35
  readonly connect: (target: {
27
36
  readonly host: string;
@@ -1,5 +1,5 @@
1
1
  import type { IncomingHttpHeaders, IncomingMessage, ServerResponse } from "node:http";
2
- import * as net from "node:net";
2
+ import type * as net from "node:net";
3
3
  import type { ConnectCapture, PassthroughObserver } from "./capture";
4
4
  /**
5
5
  * 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.
@@ -14,10 +14,16 @@ export declare const CONNECT_INTERCEPT_HOST = "api.anthropic.com";
14
14
  * Every CONNECT host whose TLS this surface terminates, each with its own leaf and its own HTTP parser: the API host above, and `platform.claude.com`, the claude.ai control plane where Remote Control's long-lived channel and its handshake live. Terminating the second one is what makes a Remote Control login rejection observable at all: its bytes cross this surface either way, and blind-tunnelling them hides exactly the exchange that failed. Only the API host's `/v1/` paths ever reach the routed pipeline; everything else on either session is piped to that session's own real upstream, so the pipeline's identification and authorisation are never asked to serve another host's traffic.
15
15
  */
16
16
  export declare const CONNECT_INTERCEPT_HOSTS: readonly string[];
17
+ /**
18
+ * The intercept hosts the surface never parses as HTTP: terminated with ALPN for HTTP/2 and HTTP/1.1 both offered, pumped byte for byte to the real host over TLS (offering upstream only the protocol the client negotiated), and teed into the capture's stream tap when capturing is on. The claude.ai control plane's channel mixes an HTTP/1.1-shaped request with binary HTTP/2 frames, so parsing it as either protocol would destroy or distort it; tapping preserves the channel exactly as a blind tunnel served it while making its bytes observable.
19
+ */
20
+ export declare const CONNECT_TAP_HOSTS: readonly string[];
17
21
  /** Paths under this prefix are what the routed pipeline serves; everything else on the terminated session goes to the real upstream. */
18
22
  export declare const ROUTED_PATH_PREFIX = "/v1/";
19
23
  /** The port HTTPS is served on, both the default when a CONNECT authority names none and the port non-headroom paths are piped to on the real upstream. */
20
24
  export declare const HTTPS_PORT = 443;
25
+ /** What the forwarding effect answers with when the upstream is unreachable. */
26
+ export declare const HTTP_BAD_GATEWAY = 502;
21
27
  /** Largest CONNECT request head accepted before the connection is refused: a real head is a request line and a few short headers (Host, the proxy credential, a User-Agent), so anything this size is a confused or hostile client. 8 KiB is also the per-header-line limit common HTTP servers apply by default (nginx's `large_client_header_buffers` buffer, Apache's `LimitRequestFieldSize` of 8190 bytes), so no client is built to need a longer line, and a CONNECT head is a few such lines at most. */
22
28
  export declare const MAX_CONNECT_HEAD_BYTES = 8192;
23
29
  /**
@@ -112,7 +118,7 @@ interface ConnectForwardTarget {
112
118
  /** Handles one parsed HTTP request on a terminated TLS session. */
113
119
  type ConnectRequestHandler = (request: IncomingMessage, response: ServerResponse) => void;
114
120
  /** A bound loopback listener and how to stop it. */
115
- interface ConnectListenerHandle {
121
+ export interface ConnectListenerHandle {
116
122
  readonly port: number;
117
123
  /** Stops accepting, ends every live connection, and resolves once the port is released. */
118
124
  readonly close: () => Promise<void>;
@@ -135,8 +141,10 @@ interface HttpParserSession {
135
141
  export interface ConnectEffects {
136
142
  /** 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. */
137
143
  readonly listenLoopback: (preferredPort: number | undefined, onSocket: (socket: net.Socket) => void) => Promise<ConnectListenerHandle>;
138
- /** Builds the TLS terminator for one leaf, handing each successfully handshaked session to `onSecure`. */
139
- readonly createTlsAcceptor: (leaf: LeafCert, onSecure: (secure: net.Socket) => void) => TlsAcceptor;
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;
146
+ /** 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
+ readonly connectTlsUpstream: (host: string, port: number, clientAlpn: string | undefined) => Promise<net.Socket>;
140
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. */
141
149
  readonly createHttpSession: (handler: ConnectRequestHandler, onUpgrade?: (request: IncomingMessage) => void) => HttpParserSession;
142
150
  /** Opens a raw TCP connection for a blind tunnel. */
@@ -162,6 +170,8 @@ export interface ConnectServerConfig {
162
170
  readonly upstreamFor: (host: string) => ConnectForwardTarget;
163
171
  /** Whether a capability presented as a CONNECT request's proxy credential belongs to a live launch: the same constant-time check against the same registry the routed pipeline's admission makes, read fresh on every call because launches come and go. */
164
172
  readonly isLiveCapability: (token: string) => boolean;
173
+ /** The intercept hosts that are never HTTP-parsed: terminated, tapped at the byte level, and pumped to their real host. */
174
+ readonly tapHosts: readonly string[];
165
175
  /** The connection deadlines and caps; production passes `CONNECT_LIMITS`. */
166
176
  readonly limits: ConnectLimits;
167
177
  /** The diagnostic tap, when capturing is enabled; absent means nothing is recorded. */
@@ -179,8 +189,6 @@ export interface ConnectServerHandle {
179
189
  * The connection flow: read the CONNECT head within the deadline (never more of the stream than that, so an early ClientHello glued to the head is pushed back with `unshift` and still seen by whatever consumes the socket next), authenticate it, then either blind-tunnel the target or terminate its TLS and parse HTTP on the session. Every network effect flows through `effects`; nothing here touches the network or filesystem itself.
180
190
  */
181
191
  export declare function startConnectServer(config: ConnectServerConfig, effects: ConnectEffects, preferredPort: number | undefined): Promise<ConnectServerHandle>;
182
- /** The real `ConnectEffects` over node's own net, tls, and http. `connectTcp` opens real connections to the CONNECTed host, so tests that want a blind tunnel redirect it. */
183
- export declare function realConnectEffects(): ConnectEffects;
184
192
  /** The real `ConnectCertStore` on the filesystem: `ca.pem` world-readable (it is public material), `ca.key` mode 0600 exactly, chmod'ed after the write because a create's mode bits pass through the process umask. */
185
193
  export declare function realConnectCertStore(paths: {
186
194
  readonly frontdoorCaDir: string;
@@ -0,0 +1,3 @@
1
+ import { type ConnectEffects } from "./connect";
2
+ /** The real `ConnectEffects` over node's own net, tls, and http. `connectTcp` opens real connections to the CONNECTed host, so tests that want a blind tunnel redirect it. */
3
+ export declare function realConnectEffects(): ConnectEffects;