agent-shim 8.0.0 → 8.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.cjs CHANGED
@@ -61039,7 +61039,7 @@ function registerIdentityCommand(program2, deps) {
61039
61039
  // package.json
61040
61040
  var package_default = {
61041
61041
  name: "agent-shim",
61042
- version: "8.0.0",
61042
+ version: "8.0.1",
61043
61043
  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.",
61044
61044
  license: "MIT",
61045
61045
  author: "Joseph Mearman <joseph@mearman.co.uk>",
@@ -68037,13 +68037,231 @@ function registerDoctorCommand(program2, deps) {
68037
68037
  }
68038
68038
 
68039
68039
  // src/headroom/commands.ts
68040
- var import_node_fs15 = __toESM(require("node:fs"), 1);
68040
+ var import_node_fs16 = __toESM(require("node:fs"), 1);
68041
68041
  var import_node_http2 = __toESM(require("node:http"), 1);
68042
- var import_node_path35 = __toESM(require("node:path"), 1);
68042
+ var import_node_path36 = __toESM(require("node:path"), 1);
68043
68043
  var import_node_child_process5 = require("node:child_process");
68044
68044
 
68045
- // src/headroom/settings.ts
68045
+ // src/frontdoor/realFrontDoorPort.ts
68046
+ var import_node_fs15 = __toESM(require("node:fs"), 1);
68047
+
68048
+ // src/frontdoor/ensure.ts
68046
68049
  var import_node_crypto12 = require("node:crypto");
68050
+ var FRONTDOOR_START_TIMEOUT_MS = 3e4;
68051
+ var FRONTDOOR_LAUNCHER_POLL_MS = 100;
68052
+ var FrontDoorStartError = class extends CliError {
68053
+ constructor(message) {
68054
+ super(message);
68055
+ this.name = "FrontDoorStartError";
68056
+ }
68057
+ };
68058
+ function ensureFrontDoor(params) {
68059
+ const { paths, ports } = params;
68060
+ const deadline = ports.now() + FRONTDOOR_START_TIMEOUT_MS;
68061
+ let spawned = false;
68062
+ let replaced = false;
68063
+ const distrusted = /* @__PURE__ */ new Set();
68064
+ let lastRejection;
68065
+ for (; ; ) {
68066
+ const state = readFrontDoorState(ports.fs, paths.frontdoorStateFile);
68067
+ const supervisorPid = state?.supervisorPid;
68068
+ if (supervisorPid !== void 0 && !distrusted.has(supervisorPid) && ports.isRunning(supervisorPid)) {
68069
+ if (state?.port !== void 0 && state.connectPort !== void 0) {
68070
+ const verdict = ports.verifyListener(state.port);
68071
+ if (verdict.ok && (state.protocol ?? 0) < FRONT_DOOR_PROTOCOL && !replaced) {
68072
+ ports.stopSupervisor(supervisorPid);
68073
+ while (ports.isRunning(supervisorPid)) {
68074
+ if (ports.now() >= deadline) {
68075
+ 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}`);
68076
+ }
68077
+ ports.sleep(FRONTDOOR_LAUNCHER_POLL_MS);
68078
+ }
68079
+ replaced = true;
68080
+ continue;
68081
+ }
68082
+ if (verdict.ok) {
68083
+ const token = (0, import_node_crypto12.randomUUID)();
68084
+ writeFrontDoorSession(ports.fs, paths.frontdoorSessionsDir, { pid: params.launcherPid, startedAt: ports.now(), token });
68085
+ return { port: state.port, connectPort: state.connectPort, token };
68086
+ }
68087
+ lastRejection = `the listener on 127.0.0.1:${String(state.port)} (recorded for pid ${String(supervisorPid)}) failed authentication: ${verdict.reason}`;
68088
+ if (spawned) {
68089
+ throw new FrontDoorStartError(`agent-shim: ${lastRejection}; refusing to send this session's credentials to it. Daemon log: ${paths.frontdoorLogPath}`);
68090
+ }
68091
+ distrusted.add(supervisorPid);
68092
+ continue;
68093
+ }
68094
+ if (state?.lastError !== void 0) {
68095
+ throw new FrontDoorStartError(`agent-shim: the front door reported a fatal error and is not serving: ${state.lastError} (daemon log: ${paths.frontdoorLogPath})`);
68096
+ }
68097
+ } else if (!spawned) {
68098
+ ports.fs.mkdirp(paths.frontdoorDir);
68099
+ const created = ports.fs.writeFileExclusive(paths.frontdoorLockFile, `${JSON.stringify({ pid: params.launcherPid, at: ports.now() })}
68100
+ `);
68101
+ if (created) {
68102
+ ports.spawnSupervisor(paths);
68103
+ spawned = true;
68104
+ } else {
68105
+ const lock = readStartLock(ports.fs, paths.frontdoorLockFile);
68106
+ if (lock === void 0 || !ports.isRunning(lock.pid)) {
68107
+ ports.fs.removeRecursive(paths.frontdoorLockFile);
68108
+ continue;
68109
+ }
68110
+ }
68111
+ }
68112
+ if (ports.now() >= deadline) {
68113
+ const lastError = state?.lastError;
68114
+ throw new FrontDoorStartError(
68115
+ `agent-shim: the front door did not become ready within ${String(FRONTDOOR_START_TIMEOUT_MS)}ms` + (lastError === void 0 ? "" : ` (last error: ${lastError})`) + (lastRejection === void 0 ? "" : ` (${lastRejection})`) + `. Daemon log: ${paths.frontdoorLogPath}`
68116
+ );
68117
+ }
68118
+ ports.sleep(FRONTDOOR_LAUNCHER_POLL_MS);
68119
+ }
68120
+ }
68121
+
68122
+ // src/frontdoor/probe.ts
68123
+ var import_node_worker_threads = require("node:worker_threads");
68124
+ var PROBE_TIMEOUT_MS = 2e3;
68125
+ var PROBE_WAIT_MARGIN_MS = 1e3;
68126
+ var REASON_BYTES = 1024;
68127
+ var HEADER_SLOTS = 2;
68128
+ var INT32_BYTES = 4;
68129
+ var VERDICT_PENDING = 0;
68130
+ var VERDICT_OK = 1;
68131
+ var VERDICT_FAILED = 2;
68132
+ var PROBE_SOURCE = `
68133
+ const { workerData } = require("node:worker_threads");
68134
+ const https = require("node:https");
68135
+ const { port, ca, timeoutMs, shared, ok, failed, headerSlots } = workerData;
68136
+ const header = new Int32Array(shared, 0, headerSlots);
68137
+ const reason = new Uint8Array(shared, headerSlots * 4);
68138
+ let done = false;
68139
+ const finish = (verdict, message) => {
68140
+ if (done) return;
68141
+ done = true;
68142
+ const bytes = Buffer.from(String(message), "utf8").subarray(0, reason.length);
68143
+ reason.set(bytes);
68144
+ Atomics.store(header, 1, bytes.length);
68145
+ Atomics.store(header, 0, verdict);
68146
+ Atomics.notify(header, 0);
68147
+ };
68148
+ const request = https.get({ host: "127.0.0.1", port, path: "/healthz", ca: [ca], agent: false }, (response) => {
68149
+ let body = "";
68150
+ response.setEncoding("utf8");
68151
+ response.on("data", (chunk) => { body += chunk; });
68152
+ response.on("end", () => {
68153
+ if (response.statusCode === 200 && body === "ok") finish(ok, "");
68154
+ else finish(failed, "the listener answered its health probe with HTTP " + String(response.statusCode));
68155
+ });
68156
+ response.on("error", (error) => finish(failed, error.message));
68157
+ });
68158
+ // A hard deadline over the whole exchange, handshake included: a listener that accepts the connection and then says nothing must still fail the probe.
68159
+ setTimeout(() => { request.destroy(new Error("no answer within " + String(timeoutMs) + "ms")); }, timeoutMs);
68160
+ request.on("error", (error) => finish(failed, (error.code ? error.code + ": " : "") + error.message));
68161
+ `;
68162
+ function probeFrontDoorSync(port, caPem) {
68163
+ const shared = new SharedArrayBuffer(HEADER_SLOTS * INT32_BYTES + REASON_BYTES);
68164
+ const header = new Int32Array(shared, 0, HEADER_SLOTS);
68165
+ const worker = new import_node_worker_threads.Worker(PROBE_SOURCE, {
68166
+ eval: true,
68167
+ workerData: { port, ca: caPem, timeoutMs: PROBE_TIMEOUT_MS, shared, ok: VERDICT_OK, failed: VERDICT_FAILED, headerSlots: HEADER_SLOTS }
68168
+ });
68169
+ worker.unref();
68170
+ try {
68171
+ const waited = Atomics.wait(header, 0, VERDICT_PENDING, PROBE_TIMEOUT_MS + PROBE_WAIT_MARGIN_MS);
68172
+ const verdict = Atomics.load(header, 0);
68173
+ if (waited === "timed-out" || verdict === VERDICT_PENDING) {
68174
+ return { ok: false, reason: `the probe did not finish within ${String(PROBE_TIMEOUT_MS + PROBE_WAIT_MARGIN_MS)}ms` };
68175
+ }
68176
+ if (verdict === VERDICT_OK) {
68177
+ return { ok: true };
68178
+ }
68179
+ const length = Atomics.load(header, 1);
68180
+ return { ok: false, reason: Buffer.from(new Uint8Array(shared, HEADER_SLOTS * INT32_BYTES, length)).toString("utf8") };
68181
+ } finally {
68182
+ void worker.terminate();
68183
+ }
68184
+ }
68185
+
68186
+ // src/frontdoor/trust.ts
68187
+ var import_node_crypto13 = require("node:crypto");
68188
+ var import_node_path35 = __toESM(require("node:path"), 1);
68189
+ var BUNDLE_NAME_HEX_DIGITS = 16;
68190
+ function resolveTrustBundle(params) {
68191
+ const { caCertFile, bundlesDir, variable, inherited, fs: fs19 } = params;
68192
+ if (inherited === void 0 || inherited === "" || import_node_path35.default.resolve(inherited) === import_node_path35.default.resolve(caCertFile)) {
68193
+ return { path: caCertFile };
68194
+ }
68195
+ const caPem = fs19.readFileUtf8(caCertFile);
68196
+ let inheritedPem;
68197
+ try {
68198
+ inheritedPem = fs19.readFileUtf8(inherited);
68199
+ } catch (error62) {
68200
+ return {
68201
+ path: caCertFile,
68202
+ warning: `agent-shim: ${variable} names ${inherited}, which could not be read (${error62 instanceof Error ? error62.message : String(error62)}); the child trusts agent-shim's front-door CA on top of the system store, and nothing from that file`
68203
+ };
68204
+ }
68205
+ if (inheritedPem.includes(caPem.trim())) {
68206
+ return { path: inherited };
68207
+ }
68208
+ const combined = `${inheritedPem.trimEnd()}
68209
+ ${caPem}`;
68210
+ const bundle = import_node_path35.default.join(bundlesDir, `${(0, import_node_crypto13.createHash)("sha256").update(combined).digest("hex").slice(0, BUNDLE_NAME_HEX_DIGITS)}.pem`);
68211
+ if (!fs19.exists(bundle)) {
68212
+ fs19.mkdirp(bundlesDir);
68213
+ fs19.writeFileAtomic(bundle, combined);
68214
+ }
68215
+ return { path: bundle };
68216
+ }
68217
+
68218
+ // src/frontdoor/realFrontDoorPort.ts
68219
+ var realTrustBundleFs = {
68220
+ readFileUtf8: (file2) => import_node_fs15.default.readFileSync(file2, "utf8"),
68221
+ exists: (file2) => import_node_fs15.default.existsSync(file2),
68222
+ mkdirp: (dir) => {
68223
+ import_node_fs15.default.mkdirSync(dir, { recursive: true });
68224
+ },
68225
+ writeFileAtomic: (file2, contents) => {
68226
+ realFarmFs.writeFilePrivate(file2, contents);
68227
+ }
68228
+ };
68229
+ function realFrontDoorPort(paths, spawnDaemon) {
68230
+ return {
68231
+ ensure: (inheritedExtraCaCerts) => {
68232
+ const up = ensureFrontDoor({
68233
+ paths,
68234
+ launcherPid: process.pid,
68235
+ ports: {
68236
+ fs: realFarmFs,
68237
+ isRunning: realIsProcessRunning,
68238
+ now: () => Date.now(),
68239
+ sleep: realSleepSync,
68240
+ spawnSupervisor: (layout) => spawnDaemon(layout, "__frontdoor-supervisor", layout.frontdoorLogPath),
68241
+ stopSupervisor: (pid) => {
68242
+ process.kill(pid, "SIGTERM");
68243
+ },
68244
+ // 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.
68245
+ verifyListener: (port) => probeFrontDoorSync(port, import_node_fs15.default.readFileSync(paths.frontdoorCaCertFile, "utf8"))
68246
+ }
68247
+ });
68248
+ const trust = resolveTrustBundle({ caCertFile: paths.frontdoorCaCertFile, bundlesDir: paths.frontdoorCaBundlesDir, variable: "NODE_EXTRA_CA_CERTS", inherited: inheritedExtraCaCerts, fs: realTrustBundleFs });
68249
+ return {
68250
+ port: up.port,
68251
+ connectPort: up.connectPort,
68252
+ trustBundlePath: trust.path,
68253
+ ...trust.warning === void 0 ? {} : { trustWarning: trust.warning },
68254
+ sessionToken: up.token
68255
+ };
68256
+ },
68257
+ release: () => {
68258
+ removeFrontDoorSession(realFarmFs, paths.frontdoorSessionsDir, process.pid);
68259
+ }
68260
+ };
68261
+ }
68262
+
68263
+ // src/headroom/settings.ts
68264
+ var import_node_crypto14 = require("node:crypto");
68047
68265
  function settingsOf(config2) {
68048
68266
  return {
68049
68267
  ...config2.mode === void 0 ? {} : { mode: config2.mode },
@@ -68068,7 +68286,7 @@ function settingsEnv(settings2) {
68068
68286
  return settings2.rolloutChannel === void 0 ? {} : { HEADROOM_ROLLOUT_CHANNEL: settings2.rolloutChannel };
68069
68287
  }
68070
68288
  function hashSettings(settings2) {
68071
- return (0, import_node_crypto12.createHash)("sha256").update(JSON.stringify([...settingsArgs(settings2), JSON.stringify(settingsEnv(settings2))])).digest("hex");
68289
+ return (0, import_node_crypto14.createHash)("sha256").update(JSON.stringify([...settingsArgs(settings2), JSON.stringify(settingsEnv(settings2))])).digest("hex");
68072
68290
  }
68073
68291
 
68074
68292
  // src/headroom/supervisor.ts
@@ -68413,7 +68631,14 @@ var READY_FETCH_TIMEOUT_MS = 2e3;
68413
68631
  function headroomPidRunning(pid) {
68414
68632
  return !exitedHeadroom.has(pid) && realIsProcessRunning(pid);
68415
68633
  }
68416
- function headroomSpawnEnv(parentEnv, allowlist, settings2, caBundle) {
68634
+ var HEADROOM_CA_BUNDLE = "HEADROOM_CA_BUNDLE";
68635
+ function resolveHeadroomTrustBundle(params) {
68636
+ if (!params.fs.exists(params.caCertFile)) {
68637
+ return void 0;
68638
+ }
68639
+ return resolveTrustBundle({ ...params, variable: HEADROOM_CA_BUNDLE });
68640
+ }
68641
+ function headroomSpawnEnv(parentEnv, allowlist, settings2, trustBundlePath) {
68417
68642
  const inherited = { ...parentEnv };
68418
68643
  delete inherited.HEADROOM_HOST;
68419
68644
  delete inherited.HEADROOM_PORT;
@@ -68421,8 +68646,8 @@ function headroomSpawnEnv(parentEnv, allowlist, settings2, caBundle) {
68421
68646
  ...inherited,
68422
68647
  HEADROOM_ALLOWED_BASE_URLS: allowlist.join(","),
68423
68648
  HEADROOM_HTTP2: parentEnv.HEADROOM_HTTP2 ?? "0",
68424
- // A transparent-interception deployment redirects the API host's address machine-wide, so headroom's own Python dials reach the front door's leaf instead of the origin's public certificate, and Python trusts only the system store: point its two trust variables (urllib3/httpx read both) at the door's combined bundle, which carries the system-equivalent anchors alongside the door's CA. An explicit value in the parent environment always wins, exactly like HEADROOM_HTTP2.
68425
- ...caBundle === void 0 || parentEnv.REQUESTS_CA_BUNDLE !== void 0 || parentEnv.SSL_CERT_FILE !== void 0 ? {} : { REQUESTS_CA_BUNDLE: caBundle, SSL_CERT_FILE: caBundle },
68649
+ // The bundle already holds any inherited HEADROOM_CA_BUNDLE's certificates, so replacing the variable loses nothing. An inherited SSL_CERT_FILE or REQUESTS_CA_BUNDLE passes through untouched and, by headroom's own precedence, still replaces everything: an explicit choice in the parent environment wins.
68650
+ ...trustBundlePath === void 0 ? {} : { [HEADROOM_CA_BUNDLE]: trustBundlePath },
68426
68651
  ...settingsEnv(settings2)
68427
68652
  };
68428
68653
  }
@@ -68449,12 +68674,12 @@ function readInstalledHeadroomCommit() {
68449
68674
  if (toolDir.status !== 0) {
68450
68675
  return void 0;
68451
68676
  }
68452
- const libDir = import_node_path35.default.join(toolDir.stdout.trim(), HEADROOM_TOOL_NAME, "lib");
68677
+ const libDir = import_node_path36.default.join(toolDir.stdout.trim(), HEADROOM_TOOL_NAME, "lib");
68453
68678
  for (const python of realFarmFs.readdir(libDir)) {
68454
- const sitePackages = import_node_path35.default.join(libDir, python, "site-packages");
68679
+ const sitePackages = import_node_path36.default.join(libDir, python, "site-packages");
68455
68680
  for (const entry of realFarmFs.readdir(sitePackages)) {
68456
68681
  if (entry.startsWith("headroom_ai-") && entry.endsWith(".dist-info")) {
68457
- const directUrl = realFarmFs.readFileUtf8(import_node_path35.default.join(sitePackages, entry, "direct_url.json"));
68682
+ const directUrl = realFarmFs.readFileUtf8(import_node_path36.default.join(sitePackages, entry, "direct_url.json"));
68458
68683
  return directUrl === void 0 ? void 0 : parseInstalledCommit(directUrl);
68459
68684
  }
68460
68685
  }
@@ -68477,14 +68702,18 @@ function realSupervisorPorts(paths) {
68477
68702
  socketTrust: realHeadroomSocketTrust,
68478
68703
  readConfig: () => resolveSupervisorConfig(readGlobalConfig(paths)?.headroom ?? {}),
68479
68704
  spawnHeadroom: (socketPath, allowlist, settings2) => {
68480
- const caBundle = import_node_fs15.default.existsSync(paths.frontdoorCaCertFile) ? import_node_fs15.default.readFileSync(paths.frontdoorCaCertFile, "utf8") : void 0;
68481
- import_node_fs15.default.mkdirSync(paths.logsDir, { recursive: true });
68482
- const logFd = import_node_fs15.default.openSync(paths.headroomLogPath, "a");
68705
+ const trust = resolveHeadroomTrustBundle({ caCertFile: paths.frontdoorCaCertFile, bundlesDir: paths.frontdoorCaBundlesDir, inherited: process.env[HEADROOM_CA_BUNDLE], fs: realTrustBundleFs });
68706
+ import_node_fs16.default.mkdirSync(paths.logsDir, { recursive: true });
68707
+ if (trust?.warning !== void 0) {
68708
+ import_node_fs16.default.appendFileSync(paths.headroomLogPath, `${(/* @__PURE__ */ new Date()).toISOString()} ${trust.warning}
68709
+ `);
68710
+ }
68711
+ const logFd = import_node_fs16.default.openSync(paths.headroomLogPath, "a");
68483
68712
  try {
68484
68713
  const child = (0, import_node_child_process5.spawn)("headroom", headroomProxyArgs(socketPath, settings2), {
68485
68714
  detached: true,
68486
68715
  stdio: ["ignore", logFd, logFd],
68487
- env: headroomSpawnEnv(process.env, allowlist, settings2, caBundle)
68716
+ env: headroomSpawnEnv(process.env, allowlist, settings2, trust?.path)
68488
68717
  });
68489
68718
  if (child.pid === void 0) {
68490
68719
  throw new Error("spawning headroom returned no pid");
@@ -68494,12 +68723,12 @@ function realSupervisorPorts(paths) {
68494
68723
  child.once("exit", (code, signal) => {
68495
68724
  ownedHeadroom.delete(pid);
68496
68725
  exitedHeadroom.add(pid);
68497
- import_node_fs15.default.appendFileSync(paths.headroomLogPath, `${(/* @__PURE__ */ new Date()).toISOString()} agent-shim headroom supervisor: headroom exited (${signal ?? `code ${String(code)}`})
68726
+ import_node_fs16.default.appendFileSync(paths.headroomLogPath, `${(/* @__PURE__ */ new Date()).toISOString()} agent-shim headroom supervisor: headroom exited (${signal ?? `code ${String(code)}`})
68498
68727
  `);
68499
68728
  });
68500
68729
  return pid;
68501
68730
  } finally {
68502
- import_node_fs15.default.closeSync(logFd);
68731
+ import_node_fs16.default.closeSync(logFd);
68503
68732
  }
68504
68733
  },
68505
68734
  stopProcess: (pid) => {
@@ -68512,7 +68741,7 @@ function realSupervisorPorts(paths) {
68512
68741
  now: () => Date.now()
68513
68742
  });
68514
68743
  if (outcome === "still-running") {
68515
- import_node_fs15.default.appendFileSync(
68744
+ import_node_fs16.default.appendFileSync(
68516
68745
  paths.headroomLogPath,
68517
68746
  `${(/* @__PURE__ */ new Date()).toISOString()} agent-shim headroom supervisor: pid ${String(pid)} survived SIGKILL within its grace; it is stuck uninterruptibly
68518
68747
  `
@@ -68526,10 +68755,10 @@ function realSupervisorPorts(paths) {
68526
68755
  return { ok: false, error: `uv is not installed or not on PATH (${result.error.message})` };
68527
68756
  }
68528
68757
  if (result.stdout !== "") {
68529
- import_node_fs15.default.appendFileSync(paths.headroomLogPath, result.stdout);
68758
+ import_node_fs16.default.appendFileSync(paths.headroomLogPath, result.stdout);
68530
68759
  }
68531
68760
  if (result.stderr !== "") {
68532
- import_node_fs15.default.appendFileSync(paths.headroomLogPath, result.stderr);
68761
+ import_node_fs16.default.appendFileSync(paths.headroomLogPath, result.stderr);
68533
68762
  }
68534
68763
  return result.status === 0 ? { ok: true } : { ok: false, error: `uv tool install exited ${String(result.status)}` };
68535
68764
  },
@@ -68539,8 +68768,8 @@ function realSupervisorPorts(paths) {
68539
68768
  },
68540
68769
  installedCommit: readInstalledHeadroomCommit,
68541
68770
  log: (line) => {
68542
- import_node_fs15.default.mkdirSync(paths.logsDir, { recursive: true });
68543
- import_node_fs15.default.appendFileSync(paths.headroomLogPath, `${(/* @__PURE__ */ new Date()).toISOString()} ${line}
68771
+ import_node_fs16.default.mkdirSync(paths.logsDir, { recursive: true });
68772
+ import_node_fs16.default.appendFileSync(paths.headroomLogPath, `${(/* @__PURE__ */ new Date()).toISOString()} ${line}
68544
68773
  `);
68545
68774
  }
68546
68775
  };
@@ -69253,224 +69482,6 @@ function buildProgram(deps) {
69253
69482
  var import_node_os8 = __toESM(require("node:os"), 1);
69254
69483
  var import_node_crypto16 = require("node:crypto");
69255
69484
 
69256
- // src/frontdoor/realFrontDoorPort.ts
69257
- var import_node_fs16 = __toESM(require("node:fs"), 1);
69258
-
69259
- // src/frontdoor/ensure.ts
69260
- var import_node_crypto13 = require("node:crypto");
69261
- var FRONTDOOR_START_TIMEOUT_MS = 3e4;
69262
- var FRONTDOOR_LAUNCHER_POLL_MS = 100;
69263
- var FrontDoorStartError = class extends CliError {
69264
- constructor(message) {
69265
- super(message);
69266
- this.name = "FrontDoorStartError";
69267
- }
69268
- };
69269
- function ensureFrontDoor(params) {
69270
- const { paths, ports } = params;
69271
- const deadline = ports.now() + FRONTDOOR_START_TIMEOUT_MS;
69272
- let spawned = false;
69273
- let replaced = false;
69274
- const distrusted = /* @__PURE__ */ new Set();
69275
- let lastRejection;
69276
- for (; ; ) {
69277
- const state = readFrontDoorState(ports.fs, paths.frontdoorStateFile);
69278
- const supervisorPid = state?.supervisorPid;
69279
- if (supervisorPid !== void 0 && !distrusted.has(supervisorPid) && ports.isRunning(supervisorPid)) {
69280
- if (state?.port !== void 0 && state.connectPort !== void 0) {
69281
- const verdict = ports.verifyListener(state.port);
69282
- if (verdict.ok && (state.protocol ?? 0) < FRONT_DOOR_PROTOCOL && !replaced) {
69283
- ports.stopSupervisor(supervisorPid);
69284
- while (ports.isRunning(supervisorPid)) {
69285
- if (ports.now() >= deadline) {
69286
- 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}`);
69287
- }
69288
- ports.sleep(FRONTDOOR_LAUNCHER_POLL_MS);
69289
- }
69290
- replaced = true;
69291
- continue;
69292
- }
69293
- if (verdict.ok) {
69294
- const token = (0, import_node_crypto13.randomUUID)();
69295
- writeFrontDoorSession(ports.fs, paths.frontdoorSessionsDir, { pid: params.launcherPid, startedAt: ports.now(), token });
69296
- return { port: state.port, connectPort: state.connectPort, token };
69297
- }
69298
- lastRejection = `the listener on 127.0.0.1:${String(state.port)} (recorded for pid ${String(supervisorPid)}) failed authentication: ${verdict.reason}`;
69299
- if (spawned) {
69300
- throw new FrontDoorStartError(`agent-shim: ${lastRejection}; refusing to send this session's credentials to it. Daemon log: ${paths.frontdoorLogPath}`);
69301
- }
69302
- distrusted.add(supervisorPid);
69303
- continue;
69304
- }
69305
- if (state?.lastError !== void 0) {
69306
- throw new FrontDoorStartError(`agent-shim: the front door reported a fatal error and is not serving: ${state.lastError} (daemon log: ${paths.frontdoorLogPath})`);
69307
- }
69308
- } else if (!spawned) {
69309
- ports.fs.mkdirp(paths.frontdoorDir);
69310
- const created = ports.fs.writeFileExclusive(paths.frontdoorLockFile, `${JSON.stringify({ pid: params.launcherPid, at: ports.now() })}
69311
- `);
69312
- if (created) {
69313
- ports.spawnSupervisor(paths);
69314
- spawned = true;
69315
- } else {
69316
- const lock = readStartLock(ports.fs, paths.frontdoorLockFile);
69317
- if (lock === void 0 || !ports.isRunning(lock.pid)) {
69318
- ports.fs.removeRecursive(paths.frontdoorLockFile);
69319
- continue;
69320
- }
69321
- }
69322
- }
69323
- if (ports.now() >= deadline) {
69324
- const lastError = state?.lastError;
69325
- throw new FrontDoorStartError(
69326
- `agent-shim: the front door did not become ready within ${String(FRONTDOOR_START_TIMEOUT_MS)}ms` + (lastError === void 0 ? "" : ` (last error: ${lastError})`) + (lastRejection === void 0 ? "" : ` (${lastRejection})`) + `. Daemon log: ${paths.frontdoorLogPath}`
69327
- );
69328
- }
69329
- ports.sleep(FRONTDOOR_LAUNCHER_POLL_MS);
69330
- }
69331
- }
69332
-
69333
- // src/frontdoor/probe.ts
69334
- var import_node_worker_threads = require("node:worker_threads");
69335
- var PROBE_TIMEOUT_MS = 2e3;
69336
- var PROBE_WAIT_MARGIN_MS = 1e3;
69337
- var REASON_BYTES = 1024;
69338
- var HEADER_SLOTS = 2;
69339
- var INT32_BYTES = 4;
69340
- var VERDICT_PENDING = 0;
69341
- var VERDICT_OK = 1;
69342
- var VERDICT_FAILED = 2;
69343
- var PROBE_SOURCE = `
69344
- const { workerData } = require("node:worker_threads");
69345
- const https = require("node:https");
69346
- const { port, ca, timeoutMs, shared, ok, failed, headerSlots } = workerData;
69347
- const header = new Int32Array(shared, 0, headerSlots);
69348
- const reason = new Uint8Array(shared, headerSlots * 4);
69349
- let done = false;
69350
- const finish = (verdict, message) => {
69351
- if (done) return;
69352
- done = true;
69353
- const bytes = Buffer.from(String(message), "utf8").subarray(0, reason.length);
69354
- reason.set(bytes);
69355
- Atomics.store(header, 1, bytes.length);
69356
- Atomics.store(header, 0, verdict);
69357
- Atomics.notify(header, 0);
69358
- };
69359
- const request = https.get({ host: "127.0.0.1", port, path: "/healthz", ca: [ca], agent: false }, (response) => {
69360
- let body = "";
69361
- response.setEncoding("utf8");
69362
- response.on("data", (chunk) => { body += chunk; });
69363
- response.on("end", () => {
69364
- if (response.statusCode === 200 && body === "ok") finish(ok, "");
69365
- else finish(failed, "the listener answered its health probe with HTTP " + String(response.statusCode));
69366
- });
69367
- response.on("error", (error) => finish(failed, error.message));
69368
- });
69369
- // A hard deadline over the whole exchange, handshake included: a listener that accepts the connection and then says nothing must still fail the probe.
69370
- setTimeout(() => { request.destroy(new Error("no answer within " + String(timeoutMs) + "ms")); }, timeoutMs);
69371
- request.on("error", (error) => finish(failed, (error.code ? error.code + ": " : "") + error.message));
69372
- `;
69373
- function probeFrontDoorSync(port, caPem) {
69374
- const shared = new SharedArrayBuffer(HEADER_SLOTS * INT32_BYTES + REASON_BYTES);
69375
- const header = new Int32Array(shared, 0, HEADER_SLOTS);
69376
- const worker = new import_node_worker_threads.Worker(PROBE_SOURCE, {
69377
- eval: true,
69378
- workerData: { port, ca: caPem, timeoutMs: PROBE_TIMEOUT_MS, shared, ok: VERDICT_OK, failed: VERDICT_FAILED, headerSlots: HEADER_SLOTS }
69379
- });
69380
- worker.unref();
69381
- try {
69382
- const waited = Atomics.wait(header, 0, VERDICT_PENDING, PROBE_TIMEOUT_MS + PROBE_WAIT_MARGIN_MS);
69383
- const verdict = Atomics.load(header, 0);
69384
- if (waited === "timed-out" || verdict === VERDICT_PENDING) {
69385
- return { ok: false, reason: `the probe did not finish within ${String(PROBE_TIMEOUT_MS + PROBE_WAIT_MARGIN_MS)}ms` };
69386
- }
69387
- if (verdict === VERDICT_OK) {
69388
- return { ok: true };
69389
- }
69390
- const length = Atomics.load(header, 1);
69391
- return { ok: false, reason: Buffer.from(new Uint8Array(shared, HEADER_SLOTS * INT32_BYTES, length)).toString("utf8") };
69392
- } finally {
69393
- void worker.terminate();
69394
- }
69395
- }
69396
-
69397
- // src/frontdoor/trust.ts
69398
- var import_node_crypto14 = require("node:crypto");
69399
- var import_node_path36 = __toESM(require("node:path"), 1);
69400
- var BUNDLE_NAME_HEX_DIGITS = 16;
69401
- function resolveTrustBundle(params) {
69402
- const { caCertFile, bundlesDir, inherited, fs: fs19 } = params;
69403
- if (inherited === void 0 || inherited === "" || import_node_path36.default.resolve(inherited) === import_node_path36.default.resolve(caCertFile)) {
69404
- return { path: caCertFile };
69405
- }
69406
- const caPem = fs19.readFileUtf8(caCertFile);
69407
- let inheritedPem;
69408
- try {
69409
- inheritedPem = fs19.readFileUtf8(inherited);
69410
- } catch (error62) {
69411
- return {
69412
- path: caCertFile,
69413
- warning: `agent-shim: NODE_EXTRA_CA_CERTS names ${inherited}, which could not be read (${error62 instanceof Error ? error62.message : String(error62)}); the child trusts agent-shim's front-door CA on top of the system store, and nothing from that file`
69414
- };
69415
- }
69416
- if (inheritedPem.includes(caPem.trim())) {
69417
- return { path: inherited };
69418
- }
69419
- const combined = `${inheritedPem.trimEnd()}
69420
- ${caPem}`;
69421
- const bundle = import_node_path36.default.join(bundlesDir, `${(0, import_node_crypto14.createHash)("sha256").update(combined).digest("hex").slice(0, BUNDLE_NAME_HEX_DIGITS)}.pem`);
69422
- if (!fs19.exists(bundle)) {
69423
- fs19.mkdirp(bundlesDir);
69424
- fs19.writeFileAtomic(bundle, combined);
69425
- }
69426
- return { path: bundle };
69427
- }
69428
-
69429
- // src/frontdoor/realFrontDoorPort.ts
69430
- var realTrustBundleFs = {
69431
- readFileUtf8: (file2) => import_node_fs16.default.readFileSync(file2, "utf8"),
69432
- exists: (file2) => import_node_fs16.default.existsSync(file2),
69433
- mkdirp: (dir) => {
69434
- import_node_fs16.default.mkdirSync(dir, { recursive: true });
69435
- },
69436
- writeFileAtomic: (file2, contents) => {
69437
- realFarmFs.writeFilePrivate(file2, contents);
69438
- }
69439
- };
69440
- function realFrontDoorPort(paths, spawnDaemon) {
69441
- return {
69442
- ensure: (inheritedExtraCaCerts) => {
69443
- const up = ensureFrontDoor({
69444
- paths,
69445
- launcherPid: process.pid,
69446
- ports: {
69447
- fs: realFarmFs,
69448
- isRunning: realIsProcessRunning,
69449
- now: () => Date.now(),
69450
- sleep: realSleepSync,
69451
- spawnSupervisor: (layout) => spawnDaemon(layout, "__frontdoor-supervisor", layout.frontdoorLogPath),
69452
- stopSupervisor: (pid) => {
69453
- process.kill(pid, "SIGTERM");
69454
- },
69455
- // 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.
69456
- verifyListener: (port) => probeFrontDoorSync(port, import_node_fs16.default.readFileSync(paths.frontdoorCaCertFile, "utf8"))
69457
- }
69458
- });
69459
- const trust = resolveTrustBundle({ caCertFile: paths.frontdoorCaCertFile, bundlesDir: paths.frontdoorCaBundlesDir, inherited: inheritedExtraCaCerts, fs: realTrustBundleFs });
69460
- return {
69461
- port: up.port,
69462
- connectPort: up.connectPort,
69463
- trustBundlePath: trust.path,
69464
- ...trust.warning === void 0 ? {} : { trustWarning: trust.warning },
69465
- sessionToken: up.token
69466
- };
69467
- },
69468
- release: () => {
69469
- removeFrontDoorSession(realFarmFs, paths.frontdoorSessionsDir, process.pid);
69470
- }
69471
- };
69472
- }
69473
-
69474
69485
  // src/launcher.ts
69475
69486
  var import_node_crypto15 = require("node:crypto");
69476
69487
  var import_node_path38 = __toESM(require("node:path"), 1);
package/dist/index.cjs CHANGED
@@ -8142,7 +8142,7 @@ var import_node_crypto10 = require("node:crypto");
8142
8142
  var import_node_path28 = __toESM(require("node:path"), 1);
8143
8143
  var BUNDLE_NAME_HEX_DIGITS = 16;
8144
8144
  function resolveTrustBundle(params) {
8145
- const { caCertFile, bundlesDir, inherited, fs: fs12 } = params;
8145
+ const { caCertFile, bundlesDir, variable, inherited, fs: fs12 } = params;
8146
8146
  if (inherited === void 0 || inherited === "" || import_node_path28.default.resolve(inherited) === import_node_path28.default.resolve(caCertFile)) {
8147
8147
  return { path: caCertFile };
8148
8148
  }
@@ -8153,7 +8153,7 @@ function resolveTrustBundle(params) {
8153
8153
  } catch (error) {
8154
8154
  return {
8155
8155
  path: caCertFile,
8156
- warning: `agent-shim: NODE_EXTRA_CA_CERTS names ${inherited}, which could not be read (${error instanceof Error ? error.message : String(error)}); the child trusts agent-shim's front-door CA on top of the system store, and nothing from that file`
8156
+ warning: `agent-shim: ${variable} names ${inherited}, which could not be read (${error instanceof Error ? error.message : String(error)}); the child trusts agent-shim's front-door CA on top of the system store, and nothing from that file`
8157
8157
  };
8158
8158
  }
8159
8159
  if (inheritedPem.includes(caPem.trim())) {
@@ -8199,7 +8199,7 @@ function realFrontDoorPort(paths, spawnDaemon) {
8199
8199
  verifyListener: (port) => probeFrontDoorSync(port, import_node_fs10.default.readFileSync(paths.frontdoorCaCertFile, "utf8"))
8200
8200
  }
8201
8201
  });
8202
- const trust = resolveTrustBundle({ caCertFile: paths.frontdoorCaCertFile, bundlesDir: paths.frontdoorCaBundlesDir, inherited: inheritedExtraCaCerts, fs: realTrustBundleFs });
8202
+ const trust = resolveTrustBundle({ caCertFile: paths.frontdoorCaCertFile, bundlesDir: paths.frontdoorCaBundlesDir, variable: "NODE_EXTRA_CA_CERTS", inherited: inheritedExtraCaCerts, fs: realTrustBundleFs });
8203
8203
  return {
8204
8204
  port: up.port,
8205
8205
  connectPort: up.connectPort,
package/dist/index.mjs CHANGED
@@ -7976,7 +7976,7 @@ import { createHash as createHash5 } from "node:crypto";
7976
7976
  import path28 from "node:path";
7977
7977
  var BUNDLE_NAME_HEX_DIGITS = 16;
7978
7978
  function resolveTrustBundle(params) {
7979
- const { caCertFile, bundlesDir, inherited, fs: fs12 } = params;
7979
+ const { caCertFile, bundlesDir, variable, inherited, fs: fs12 } = params;
7980
7980
  if (inherited === void 0 || inherited === "" || path28.resolve(inherited) === path28.resolve(caCertFile)) {
7981
7981
  return { path: caCertFile };
7982
7982
  }
@@ -7987,7 +7987,7 @@ function resolveTrustBundle(params) {
7987
7987
  } catch (error) {
7988
7988
  return {
7989
7989
  path: caCertFile,
7990
- warning: `agent-shim: NODE_EXTRA_CA_CERTS names ${inherited}, which could not be read (${error instanceof Error ? error.message : String(error)}); the child trusts agent-shim's front-door CA on top of the system store, and nothing from that file`
7990
+ warning: `agent-shim: ${variable} names ${inherited}, which could not be read (${error instanceof Error ? error.message : String(error)}); the child trusts agent-shim's front-door CA on top of the system store, and nothing from that file`
7991
7991
  };
7992
7992
  }
7993
7993
  if (inheritedPem.includes(caPem.trim())) {
@@ -8033,7 +8033,7 @@ function realFrontDoorPort(paths, spawnDaemon) {
8033
8033
  verifyListener: (port) => probeFrontDoorSync(port, fs11.readFileSync(paths.frontdoorCaCertFile, "utf8"))
8034
8034
  }
8035
8035
  });
8036
- const trust = resolveTrustBundle({ caCertFile: paths.frontdoorCaCertFile, bundlesDir: paths.frontdoorCaBundlesDir, inherited: inheritedExtraCaCerts, fs: realTrustBundleFs });
8036
+ const trust = resolveTrustBundle({ caCertFile: paths.frontdoorCaCertFile, bundlesDir: paths.frontdoorCaBundlesDir, variable: "NODE_EXTRA_CA_CERTS", inherited: inheritedExtraCaCerts, fs: realTrustBundleFs });
8037
8037
  return {
8038
8038
  port: up.port,
8039
8039
  connectPort: up.connectPort,
@@ -686,7 +686,7 @@ interface LayoutPaths {
686
686
  readonly frontdoorCaCertFile: string;
687
687
  /** Path to the front door's CA private key, mode 0600: whoever can read it can impersonate the door to every routed child. */
688
688
  readonly frontdoorCaKeyFile: string;
689
- /** Directory of combined CA bundles: a parent environment's own NODE_EXTRA_CA_CERTS file plus the front door's CA, one file per distinct combination, for children that must keep trusting both. */
689
+ /** Directory of combined CA bundles: a parent environment's own NODE_EXTRA_CA_CERTS (for a routed child) or HEADROOM_CA_BUNDLE (for the headroom daemon) file plus the front door's CA, one file per distinct combination, for processes that must keep trusting both. */
690
690
  readonly frontdoorCaBundlesDir: string;
691
691
  /** Path to the front door's state.json: its supervisor pid, its port, and the sticky port it last served on. */
692
692
  readonly frontdoorStateFile: string;
@@ -1470,7 +1470,7 @@ declare function mintLeaf(ca: CaMaterial, names: readonly string[], now: Readonl
1470
1470
  /** Mints each host's leaf once from the CA and reuses it, because generating a 2048-bit RSA keypair costs noticeable time and nothing about the certificate changes between sessions. */
1471
1471
  declare function createLeafCache(ca: CaMaterial, now: () => Date): (host: string) => LeafCert;
1472
1472
  /**
1473
- * Persistence for the CA: generated once on the machine's first headroom start and reused forever after, because regenerating it would invalidate every child's `NODE_EXTRA_CA_CERTS` pointing at the old file. The real implementation writes the key with mode 0600; injected fakes keep it in memory.
1473
+ * Persistence for the CA: generated once on the machine's first front-door start and reused forever after, because regenerating it would invalidate every child's `NODE_EXTRA_CA_CERTS` pointing at the old file. The real implementation writes the key with mode 0600; injected fakes keep it in memory.
1474
1474
  */
1475
1475
  interface ConnectCertStore {
1476
1476
  /** The persisted CA, or undefined when none exists yet (or only half of it does: a cert without its key can sign nothing, so it counts as absent). */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-shim",
3
- "version": "8.0.0",
3
+ "version": "8.0.1",
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>",