agent-shim 8.7.0 → 8.8.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/index.mjs CHANGED
@@ -3323,25 +3323,19 @@ function realConnectEffects() {
3323
3323
  }
3324
3324
  };
3325
3325
  },
3326
- connectTlsUpstream: async (host, port, clientAlpn) => await new Promise((resolve, reject) => {
3327
- const dial = (localPort) => {
3328
- const raw = net2.connect({ host, port, localPort, lookup: realAddressLookup });
3329
- return tls.connect({ socket: raw, servername: host, ...clientAlpn === void 0 ? {} : { ALPNProtocols: [clientAlpn] } });
3330
- };
3331
- const settle = (candidate) => {
3332
- candidate.once("secureConnect", () => {
3333
- resolve(candidate);
3334
- });
3335
- candidate.once("error", (error) => {
3336
- if ("code" in error && error.code === "EADDRINUSE") {
3337
- settle(dial(upstreamLocalPort()));
3338
- return;
3339
- }
3340
- reject(error);
3326
+ connectTlsUpstream: async (host, port, clientAlpn) => {
3327
+ const dialled = await reservedDial({ host, port });
3328
+ if (dialled.error !== null) {
3329
+ throw dialled.error;
3330
+ }
3331
+ return await new Promise((resolve, reject) => {
3332
+ const secure = tls.connect({ socket: dialled.socket, servername: host, ...clientAlpn === void 0 ? {} : { ALPNProtocols: [clientAlpn] } });
3333
+ secure.once("secureConnect", () => {
3334
+ resolve(secure);
3341
3335
  });
3342
- };
3343
- settle(dial(upstreamLocalPort()));
3344
- }),
3336
+ secure.once("error", reject);
3337
+ });
3338
+ },
3345
3339
  connectTcp: async (host, port) => await new Promise((resolve, reject) => {
3346
3340
  const socket = net2.connect({ host, port });
3347
3341
  socket.once("connect", () => {
@@ -4903,547 +4897,380 @@ function listUsageSnapshots(fs12, snapshotsDir) {
4903
4897
  }).sort((left, right) => left.identity.localeCompare(right.identity));
4904
4898
  }
4905
4899
 
4906
- // src/headroom/ensure.ts
4907
- var HEADROOM_START_TIMEOUT_MS = 12e4;
4908
- var HEADROOM_LAUNCHER_POLL_MS = 250;
4909
- var HeadroomStartError = class extends CliError {
4910
- constructor(message) {
4911
- super(message);
4912
- this.name = "HeadroomStartError";
4913
- }
4914
- };
4915
- function ensureHeadroom(params) {
4916
- const { paths, ports } = params;
4917
- const deadline = ports.now() + HEADROOM_START_TIMEOUT_MS;
4918
- let spawnedSupervisor = false;
4919
- let awaitingAllowlist = false;
4920
- for (; ; ) {
4921
- const state = readHeadroomState(ports.fs, paths.headroomStateFile);
4922
- if (state?.supervisorPid !== void 0 && ports.isRunning(state.supervisorPid)) {
4923
- const daemonUp = state.socketPath !== void 0 && state.headroomPid !== void 0 && ports.isRunning(state.headroomPid);
4924
- if (daemonUp && state.socketPath !== void 0) {
4925
- const required = params.requiredAllowlistHash?.();
4926
- if (required === void 0 || state.allowlistHash === required) {
4927
- writeSession(ports.fs, paths.headroomSessionsDir, { pid: params.launcherPid, startedAt: ports.now(), supervisorPid: state.supervisorPid });
4928
- return { socketPath: state.socketPath };
4929
- }
4930
- const holders = sessionsForSupervisor(ports.fs, paths.headroomSessionsDir, state.supervisorPid).filter((session) => ports.isRunning(session.pid));
4931
- if (holders.length > 0) {
4932
- throw new HeadroomStartError(
4933
- `the running headroom daemon was started before this launch's front door address and would refuse every request this launch sends through it. It restarts once the launches using it end (pid ${holders.map((session) => String(session.pid)).join(", ")}); end them and launch again, or launch with --no-headroom.`
4934
- );
4935
- }
4936
- awaitingAllowlist = true;
4937
- }
4938
- if (state.lastError !== void 0) {
4939
- throw new HeadroomStartError(
4940
- `the headroom daemon reported a fatal error and is not serving: ${state.lastError} (daemon log: ${paths.headroomLogPath})`
4941
- );
4942
- }
4943
- } else if (!spawnedSupervisor) {
4944
- ports.fs.mkdirp(paths.headroomDir);
4945
- const created = ports.fs.writeFileExclusive(
4946
- paths.headroomLockFile,
4947
- `${JSON.stringify({ pid: params.launcherPid, at: ports.now() })}
4948
- `
4949
- );
4950
- if (created) {
4951
- ports.spawnSupervisor(paths);
4952
- spawnedSupervisor = true;
4953
- } else {
4954
- const lock = readStartLock(ports.fs, paths.headroomLockFile);
4955
- if (lock === void 0 || !ports.isRunning(lock.pid)) {
4956
- ports.fs.removeRecursive(paths.headroomLockFile);
4957
- continue;
4958
- }
4959
- }
4960
- }
4961
- if (ports.now() >= deadline) {
4962
- const lastError = state?.lastError;
4963
- throw new HeadroomStartError(
4964
- `the headroom daemon did not become ready within ${String(HEADROOM_START_TIMEOUT_MS)}ms` + (lastError === void 0 ? "" : ` (last error: ${lastError})`) + (awaitingAllowlist ? " (the running daemon's allowlist does not admit this launch's front door, and it did not restart)" : "") + `. Daemon log: ${paths.headroomLogPath}`
4965
- );
4966
- }
4967
- ports.sleep(HEADROOM_LAUNCHER_POLL_MS);
4900
+ // src/usage/preflight.ts
4901
+ var MS_PER_SECOND2 = 1e3;
4902
+ var SECONDS_PER_MINUTE = 60;
4903
+ var MINUTES_PER_HOUR = 60;
4904
+ var HOURS_PER_DAY = 24;
4905
+ var WARNING_STATUSES = /* @__PURE__ */ new Set(["allowed_warning", "rejected"]);
4906
+ function formatAge(ms) {
4907
+ const minutes = Math.floor(ms / (MS_PER_SECOND2 * SECONDS_PER_MINUTE));
4908
+ if (minutes < MINUTES_PER_HOUR) {
4909
+ return `${String(Math.max(minutes, 0))}m`;
4968
4910
  }
4911
+ const hours = Math.floor(minutes / MINUTES_PER_HOUR);
4912
+ return hours < HOURS_PER_DAY ? `${String(hours)}h` : `${String(Math.floor(hours / HOURS_PER_DAY))}d`;
4969
4913
  }
4970
-
4971
- // src/headroom/settings.ts
4972
- import { createHash as createHash4 } from "node:crypto";
4973
- function settingsOf(config) {
4914
+ function effectiveWindow(window, nowMs) {
4915
+ const resetsAtMs = window.resetsAt === void 0 ? void 0 : Date.parse(window.resetsAt);
4916
+ if (resetsAtMs !== void 0 && resetsAtMs <= nowMs) {
4917
+ return { reset: true, utilization: 0 };
4918
+ }
4974
4919
  return {
4975
- ...config.mode === void 0 ? {} : { mode: config.mode },
4976
- ...config.targetRatio === void 0 ? {} : { targetRatio: config.targetRatio },
4977
- ...config.ccr === void 0 ? {} : { ccr: config.ccr },
4978
- ...config.rolloutChannel === void 0 ? {} : { rolloutChannel: config.rolloutChannel },
4979
- ...config.interceptToolResults === void 0 ? {} : { interceptToolResults: config.interceptToolResults },
4980
- ...config.readMaturation === void 0 ? {} : { readMaturation: config.readMaturation }
4920
+ reset: false,
4921
+ ...window.utilization === void 0 ? {} : { utilization: window.utilization },
4922
+ ...resetsAtMs === void 0 ? {} : { resetsAtMs },
4923
+ ...window.status === void 0 ? {} : { status: window.status }
4981
4924
  };
4982
4925
  }
4983
- function settingsArgs(settings) {
4984
- return [
4985
- ...settings.mode === void 0 ? [] : ["--mode", settings.mode],
4986
- ...settings.targetRatio === void 0 ? [] : ["--target-ratio", String(settings.targetRatio)],
4987
- ...settings.ccr === "lossless" ? ["--lossless"] : [],
4988
- ...settings.ccr === "none" ? ["--no-ccr"] : [],
4989
- ...settings.interceptToolResults === true ? ["--intercept-tool-results"] : [],
4990
- ...settings.readMaturation === true ? ["--read-maturation"] : []
4926
+ function quotaWarnings(snapshot, provider, nowMs) {
4927
+ const rateLimit = snapshot?.providers[provider]?.rateLimit;
4928
+ const unified = rateLimit?.unified;
4929
+ if (snapshot === void 0 || rateLimit === void 0 || unified === void 0) {
4930
+ return [];
4931
+ }
4932
+ const age = formatAge(nowMs - Date.parse(rateLimit.observedAt));
4933
+ const windows = [
4934
+ { name: "five-hour", window: unified.fiveHour },
4935
+ { name: "seven-day", window: unified.sevenDay }
4991
4936
  ];
4992
- }
4993
- function settingsEnv(settings) {
4994
- return settings.rolloutChannel === void 0 ? {} : { HEADROOM_ROLLOUT_CHANNEL: settings.rolloutChannel };
4995
- }
4996
- function hashSettings(settings) {
4997
- return createHash4("sha256").update(JSON.stringify([...settingsArgs(settings), JSON.stringify(settingsEnv(settings))])).digest("hex");
4937
+ return windows.flatMap(({ name, window }) => {
4938
+ const effective = window === void 0 ? void 0 : effectiveWindow(window, nowMs);
4939
+ if (window === void 0 || effective?.status === void 0 || !WARNING_STATUSES.has(effective.status)) {
4940
+ return [];
4941
+ }
4942
+ const reset = window.resetsAt === void 0 ? "" : `, resets ${window.resetsAt}`;
4943
+ const verdict = effective.status === "rejected" ? "exhausted" : "nearly used";
4944
+ return [`agent-shim: identity ${snapshot.identity}: the ${name} quota is ${verdict}${reset} (last seen ${age} ago)`];
4945
+ });
4998
4946
  }
4999
4947
 
5000
- // src/headroom/socket.ts
5001
- import path11 from "node:path";
5002
- var GROUP_OR_OTHER_BITS = 63;
5003
- var OCTAL_RADIX = 8;
5004
- var MODE_DIGITS = 4;
5005
- var HEADROOM_SOCKET_SUFFIX = ".sock";
5006
- var HEADROOM_SOCKET_NAME = /^(\d+)\.sock$/;
5007
- var UNIX_SOCKET_PATH_MAX_BYTES = 103;
5008
- var PLATFORMS_WITHOUT_UNIX_SOCKETS = ["win32"];
5009
- function headroomSocketPath(paths, supervisorPid) {
5010
- return path11.join(paths.headroomSocketDir, `${String(supervisorPid)}${HEADROOM_SOCKET_SUFFIX}`);
5011
- }
5012
- function platformRefusal(platform) {
5013
- return PLATFORMS_WITHOUT_UNIX_SOCKETS.includes(platform) ? `headroom routing needs a unix domain socket, which headroom cannot serve on ${platform}; agent-shim never falls back to a TCP port for this hop` : void 0;
5014
- }
5015
- function ownerOnlyRefusal(label, target, stat, expected, uid) {
5016
- if (stat === void 0) {
5017
- return `the headroom ${label} ${target} does not exist`;
5018
- }
5019
- if (stat.kind === "symlink") {
5020
- return `the headroom ${label} ${target} is a symlink; it must be the real ${expected === "dir" ? "directory" : "socket"}`;
5021
- }
5022
- if (stat.kind !== expected) {
5023
- return `the headroom ${label} ${target} is not a ${expected === "dir" ? "directory" : "socket"}`;
4948
+ // src/usage/plan.ts
4949
+ import { z as z6 } from "zod";
4950
+ var PlanClassSchema = z6.union([
4951
+ z6.strictObject({
4952
+ kind: z6.literal("subscription"),
4953
+ /** The plan's size as a multiple of the smallest subscription tier (Max 5x is 5, Max 20x is 20). */
4954
+ capacity: z6.number().positive(),
4955
+ /** False when the tier string named no multiplier and no tier this project knows, so `capacity` is the neutral 1 rather than a reading. */
4956
+ recognised: z6.boolean(),
4957
+ /** The tier string the classification rests on, when the account had one. */
4958
+ tier: z6.string().optional()
4959
+ }),
4960
+ z6.strictObject({
4961
+ kind: z6.literal("pay-per-use"),
4962
+ tier: z6.string().optional()
4963
+ })
4964
+ ]);
4965
+ var TIER_MULTIPLIER = /(?:^|_)(\d+)x$/;
4966
+ var ZERO_TIER = /(?:^|_)zero$/;
4967
+ var BASE_TIER = /(?:^|_)(?:pro|free)$/;
4968
+ function planOf(account) {
4969
+ const tier = account?.organizationRateLimitTier ?? account?.userRateLimitTier;
4970
+ const tierField = tier === void 0 ? {} : { tier };
4971
+ if (tier === void 0) {
4972
+ return { kind: "subscription", capacity: 1, recognised: false };
5024
4973
  }
5025
- if (stat.uid !== uid) {
5026
- return `the headroom ${label} ${target} is owned by uid ${String(stat.uid)}, not by this user (uid ${String(uid)})`;
4974
+ if (ZERO_TIER.test(tier)) {
4975
+ return { kind: "pay-per-use", ...tierField };
5027
4976
  }
5028
- if ((stat.mode & GROUP_OR_OTHER_BITS) !== 0) {
5029
- return `the headroom ${label} ${target} has mode ${stat.mode.toString(OCTAL_RADIX).padStart(MODE_DIGITS, "0")}, which lets other users reach it; it must be accessible to its owner only`;
4977
+ const multiplier = TIER_MULTIPLIER.exec(tier)?.[1];
4978
+ if (multiplier !== void 0) {
4979
+ return { kind: "subscription", capacity: Number(multiplier), recognised: true, ...tierField };
5030
4980
  }
5031
- return void 0;
4981
+ return { kind: "subscription", capacity: 1, recognised: BASE_TIER.test(tier), ...tierField };
5032
4982
  }
5033
- function prepareHeadroomSocket(fs12, ports, paths, supervisorPid) {
5034
- const unsupported = platformRefusal(ports.platform);
5035
- if (unsupported !== void 0) {
5036
- return { refused: unsupported };
5037
- }
5038
- const socketPath = headroomSocketPath(paths, supervisorPid);
5039
- const length = Buffer.byteLength(socketPath, "utf8");
5040
- if (length > UNIX_SOCKET_PATH_MAX_BYTES) {
5041
- return {
5042
- refused: `the headroom socket path ${socketPath} is ${String(length)} bytes, longer than the ${String(UNIX_SOCKET_PATH_MAX_BYTES)} a unix socket path can hold; set AGENT_SHIM_HOME to a shorter directory`
5043
- };
5044
- }
5045
- if (ports.lstat(paths.headroomSocketDir)?.kind === "symlink") {
5046
- return { refused: `the headroom socket directory ${paths.headroomSocketDir} is a symlink; it must be the real directory` };
4983
+
4984
+ // src/usage/rateLimit.ts
4985
+ var MS_PER_SECOND3 = 1e3;
4986
+ function epochSecondsToIso(value) {
4987
+ if (value === void 0) {
4988
+ return void 0;
5047
4989
  }
5048
- fs12.mkdirPrivate(paths.headroomSocketDir);
5049
- const refusal = ownerOnlyRefusal("socket directory", paths.headroomSocketDir, ports.lstat(paths.headroomSocketDir), "dir", ports.currentUid());
5050
- return refusal === void 0 ? { socketPath } : { refused: refusal };
4990
+ const seconds = Number(value);
4991
+ return Number.isFinite(seconds) && seconds > 0 ? new Date(seconds * MS_PER_SECOND3).toISOString() : void 0;
5051
4992
  }
5052
- function pruneStaleHeadroomSockets(fs12, paths, isRunning) {
5053
- const removed = [];
5054
- for (const name of fs12.readdir(paths.headroomSocketDir)) {
5055
- const pid = HEADROOM_SOCKET_NAME.exec(name)?.[1];
5056
- if (pid === void 0 || isRunning(Number.parseInt(pid, 10))) {
5057
- continue;
5058
- }
5059
- fs12.removeRecursive(path11.join(paths.headroomSocketDir, name));
5060
- removed.push(name);
4993
+ function nonNegativeNumber(value) {
4994
+ if (value === void 0 || value.trim() === "") {
4995
+ return void 0;
5061
4996
  }
5062
- return removed;
4997
+ const parsed = Number(value);
4998
+ return Number.isFinite(parsed) && parsed >= 0 ? parsed : void 0;
5063
4999
  }
5064
-
5065
- // src/headroom/source.ts
5066
- import { z as z6 } from "zod";
5067
- var GIT_SPEC_RE = /git\+[^@\s]+(?:@([^\s#]+))?/;
5068
- var COMMIT_SHA_RE = /^[0-9a-f]{40}$/i;
5069
- var DirectUrlSchema = z6.object({ vcs_info: z6.object({ commit_id: z6.string() }) });
5070
- function isMovingGitSource(spec) {
5071
- const match = GIT_SPEC_RE.exec(spec);
5072
- if (match === null) {
5073
- return false;
5000
+ var UNIFIED_PREFIX = "anthropic-ratelimit-unified-";
5001
+ function unifiedWindow(headers, window) {
5002
+ const utilization = nonNegativeNumber(headers[`${UNIFIED_PREFIX}${window}-utilization`]);
5003
+ const resetsAt = epochSecondsToIso(headers[`${UNIFIED_PREFIX}${window}-reset`]);
5004
+ const status = headers[`${UNIFIED_PREFIX}${window}-status`];
5005
+ if (utilization === void 0 && resetsAt === void 0 && status === void 0) {
5006
+ return void 0;
5074
5007
  }
5075
- const ref = match[1];
5076
- return ref === void 0 || !COMMIT_SHA_RE.test(ref);
5077
- }
5078
- function pinnedGitCommit(spec) {
5079
- const ref = GIT_SPEC_RE.exec(spec)?.[1];
5080
- return ref !== void 0 && COMMIT_SHA_RE.test(ref) ? ref.toLowerCase() : void 0;
5008
+ return { ...utilization === void 0 ? {} : { utilization }, ...resetsAt === void 0 ? {} : { resetsAt }, ...status === void 0 ? {} : { status } };
5081
5009
  }
5082
-
5083
- // src/headroom/supervisor.ts
5084
- function resolveSupervisorConfig(configured) {
5010
+ function parseUnifiedRateLimit(headers) {
5011
+ if (!Object.keys(headers).some((name) => name.startsWith(UNIFIED_PREFIX))) {
5012
+ return void 0;
5013
+ }
5014
+ const status = headers[`${UNIFIED_PREFIX}status`];
5015
+ const fiveHour = unifiedWindow(headers, "5h");
5016
+ const sevenDay = unifiedWindow(headers, "7d");
5017
+ const representativeClaim = headers[`${UNIFIED_PREFIX}representative-claim`];
5018
+ const resetAt = epochSecondsToIso(headers[`${UNIFIED_PREFIX}reset`]);
5019
+ const overageStatus = headers[`${UNIFIED_PREFIX}overage-status`];
5085
5020
  return {
5086
- source: configured.source ?? HEADROOM_DEFAULT_SOURCE,
5087
- idleShutdownMinutes: configured.idleShutdownMinutes ?? HEADROOM_DEFAULT_IDLE_SHUTDOWN_MINUTES,
5088
- settings: settingsOf(configured)
5021
+ ...status === void 0 ? {} : { status },
5022
+ ...fiveHour === void 0 ? {} : { fiveHour },
5023
+ ...sevenDay === void 0 ? {} : { sevenDay },
5024
+ ...representativeClaim === void 0 ? {} : { representativeClaim },
5025
+ ...resetAt === void 0 ? {} : { resetAt },
5026
+ ...overageStatus === void 0 ? {} : { overageStatus }
5089
5027
  };
5090
5028
  }
5091
- var HEADROOM_BACKOFF_BASE_MS = 500;
5092
- var HEADROOM_BACKOFF_CAP_MS = 3e4;
5093
- var HEADROOM_START_RETRY_BUDGET = 5;
5094
- var HEADROOM_READY_TIMEOUT_MS = 15e3;
5095
- var HEADROOM_POLL_MS = 1e3;
5096
- var MS_PER_MINUTE2 = 6e4;
5097
- function backoffForAttempt(attempt) {
5098
- return Math.min(HEADROOM_BACKOFF_CAP_MS, HEADROOM_BACKOFF_BASE_MS * 2 ** (attempt - 1));
5029
+
5030
+ // src/usage/scan.ts
5031
+ import { z as z7 } from "zod";
5032
+ var AnthropicUsageSchema = z7.looseObject({
5033
+ input_tokens: z7.number().int().nonnegative().nullish(),
5034
+ output_tokens: z7.number().int().nonnegative().nullish(),
5035
+ cache_creation_input_tokens: z7.number().int().nonnegative().nullish(),
5036
+ cache_read_input_tokens: z7.number().int().nonnegative().nullish()
5037
+ });
5038
+ var ErrorObjectSchema = z7.looseObject({
5039
+ type: z7.string().optional(),
5040
+ code: z7.union([z7.string(), z7.number()]).optional(),
5041
+ message: z7.string().optional()
5042
+ });
5043
+
5044
+ // src/usage/middleware.ts
5045
+ var ANTHROPIC_PROVIDER = "anthropic";
5046
+
5047
+ // src/usage/pick.ts
5048
+ var MS_PER_SECOND4 = 1e3;
5049
+ var MS_PER_HOUR = 36e5;
5050
+ var HOURS_PER_DAY2 = 24;
5051
+ var FIVE_HOURS = 5;
5052
+ var PROMPT_CACHE_TTL_HOURS = 1;
5053
+ var PERCENT = 100;
5054
+ var SEVEN_DAYS = 7;
5055
+ var FIVE_HOUR_WINDOW_MS = FIVE_HOURS * MS_PER_HOUR;
5056
+ var SEVEN_DAY_WINDOW_MS = SEVEN_DAYS * HOURS_PER_DAY2 * MS_PER_HOUR;
5057
+ var PROMPT_CACHE_TTL_MS = PROMPT_CACHE_TTL_HOURS * MS_PER_HOUR;
5058
+ var CLASS_ORDER = { scored: 0, unknown: 1, "pay-per-use": 2, ineligible: 3 };
5059
+ function unifiedOf(state) {
5060
+ return state?.rateLimit?.unified;
5099
5061
  }
5100
- function versionSatisfies(version, source) {
5101
- if (version === void 0) {
5102
- return false;
5103
- }
5104
- const specifier = /==|>=|<=|!=|>|<|~=/.exec(source);
5105
- if (specifier === null) {
5106
- return true;
5107
- }
5108
- const installed = parseVersion(version);
5109
- const wanted = parseVersion(source.slice(specifier.index + specifier[0].length));
5110
- if (installed === void 0 || wanted === void 0) {
5111
- return true;
5062
+ function limitBlockedUntil(state, nowMs) {
5063
+ const limit = state?.lastLimit;
5064
+ if (state === void 0 || limit === void 0) {
5065
+ return void 0;
5112
5066
  }
5113
- const cmp = compareVersions(installed, wanted);
5114
- switch (specifier[0]) {
5115
- case "==":
5116
- return cmp === 0;
5117
- case "!=":
5118
- return cmp !== 0;
5119
- case ">=":
5120
- return cmp >= 0;
5121
- case "<=":
5122
- return cmp <= 0;
5123
- case ">":
5124
- return cmp > 0;
5125
- case "<":
5126
- return cmp < 0;
5127
- case "~=":
5128
- return compareVersions(installed.slice(0, Math.max(0, wanted.length - 1)), wanted.slice(0, Math.max(0, wanted.length - 1))) === 0 && cmp >= 0;
5129
- default:
5130
- return true;
5067
+ if (Date.parse(state.lastRequestAt) > Date.parse(limit.observedAt)) {
5068
+ return void 0;
5131
5069
  }
5070
+ const until = limit.resetAt === void 0 ? limit.retryAfterSeconds === void 0 ? void 0 : Date.parse(limit.observedAt) + limit.retryAfterSeconds * MS_PER_SECOND4 : Date.parse(limit.resetAt);
5071
+ return until !== void 0 && until > nowMs ? until : void 0;
5132
5072
  }
5133
- function parseVersion(input) {
5134
- const match = /(\d+)(\.\d+)*/.exec(input);
5135
- if (match === null) {
5073
+ function windowBlockedUntil(windows) {
5074
+ const resets = windows.flatMap((window) => window.status === "rejected" && window.resetsAtMs !== void 0 ? [window.resetsAtMs] : []);
5075
+ return resets.length === 0 ? void 0 : Math.max(...resets);
5076
+ }
5077
+ function fiveHourBurn(records) {
5078
+ const points = [];
5079
+ for (const record of records) {
5080
+ const window = record.rateLimitHeaders === void 0 ? void 0 : parseUnifiedRateLimit(record.rateLimitHeaders)?.fiveHour;
5081
+ if (window?.utilization !== void 0 && window.resetsAt !== void 0) {
5082
+ points.push({ resetsAt: window.resetsAt, point: { atMs: Date.parse(record.at), utilization: window.utilization } });
5083
+ }
5084
+ }
5085
+ const latest = points.at(-1)?.resetsAt;
5086
+ const current = points.filter((entry) => entry.resetsAt === latest).map((entry) => entry.point);
5087
+ const first = current.at(0);
5088
+ const last = current.at(-1);
5089
+ if (first === void 0 || last === void 0 || last.atMs <= first.atMs || last.utilization <= first.utilization) {
5136
5090
  return void 0;
5137
5091
  }
5138
- return match[0].split(".").map((part) => Number.parseInt(part, 10));
5092
+ return (last.utilization - first.utilization) / (last.atMs - first.atMs);
5139
5093
  }
5140
- function compareVersions(a, b) {
5141
- for (let index = 0; index < Math.max(a.length, b.length); index += 1) {
5142
- const left = a[index] ?? 0;
5143
- const right = b[index] ?? 0;
5144
- if (left !== right) {
5145
- return left - right;
5146
- }
5094
+ function percent(fraction) {
5095
+ return `${String(Math.round(fraction * PERCENT))}%`;
5096
+ }
5097
+ function describeWindow(label, window, effective, nowMs) {
5098
+ if (effective.reset) {
5099
+ return `${label} window has reset`;
5147
5100
  }
5148
- return 0;
5101
+ const used = effective.utilization === void 0 ? "usage unknown" : `${percent(effective.utilization)} used`;
5102
+ const resets = effective.resetsAtMs === void 0 ? "" : `, resets in ${formatAge(effective.resetsAtMs - nowMs)}`;
5103
+ return `${label} ${used}${resets}${window.status === "allowed_warning" ? " (nearly used)" : ""}`;
5149
5104
  }
5150
- var HEADROOM_SUPERVISOR_STILL_RUNNING = -1;
5151
- async function runSupervisor(initialConfig, ports, options = {}) {
5152
- const { fs: fs12, paths } = ports;
5153
- let config = initialConfig;
5154
- const previousState = readHeadroomState(fs12, paths.headroomStateFile);
5155
- if (previousState?.supervisorPid !== void 0 && previousState.supervisorPid !== ports.ownPid && ports.isRunning(previousState.supervisorPid)) {
5156
- ports.log(`agent-shim headroom supervisor ${String(ports.ownPid)}: supervisor ${String(previousState.supervisorPid)} already owns the daemon; exiting`);
5157
- return 0;
5105
+ function capacityOf(plan) {
5106
+ return plan.kind === "subscription" ? plan.capacity : 1;
5107
+ }
5108
+ function assess(member, nowMs) {
5109
+ const plan = planOf(member.account);
5110
+ const planReasons = plan.kind === "pay-per-use" ? ["billed by use (no plan allowance)"] : plan.recognised ? [`${String(plan.capacity)}x plan`] : [`plan tier ${plan.tier === void 0 ? "not known" : `"${plan.tier}" not recognised`}, counted as 1x`];
5111
+ const base = { identity: member.identity, plan };
5112
+ if (member.readError !== void 0) {
5113
+ return { candidate: { ...base, class: "unknown", reasons: [`usage state unreadable: ${member.readError}`, ...planReasons] } };
5158
5114
  }
5159
- let installedSource = previousState?.installedSource;
5160
- const ownsState = () => {
5161
- const owner = readHeadroomState(fs12, paths.headroomStateFile)?.supervisorPid;
5162
- return owner === void 0 || owner === ports.ownPid || !ports.isRunning(owner);
5163
- };
5164
- const writeOwnState = (state) => {
5165
- if (!ownsState()) {
5166
- return false;
5167
- }
5168
- writeHeadroomState(fs12, paths.headroomStateFile, state);
5169
- return true;
5170
- };
5171
- const fail = (message) => {
5172
- if (writeOwnState({ lastError: message })) {
5173
- fs12.removeRecursive(paths.headroomLockFile);
5174
- }
5175
- ports.log(`agent-shim headroom supervisor: ${message}`);
5176
- return 1;
5177
- };
5178
- const ensureInstalled = () => {
5179
- const version2 = ports.headroomVersion();
5180
- const versionOk = version2 !== void 0 && versionSatisfies(version2, config.source);
5181
- const pinned = pinnedGitCommit(config.source);
5182
- const commitOk = pinned === void 0 || ports.installedCommit() === pinned;
5183
- if (versionOk && commitOk && (installedSource === void 0 || installedSource === config.source)) {
5184
- installedSource = config.source;
5185
- return true;
5186
- }
5187
- ports.log(`agent-shim headroom supervisor: installing headroom from ${config.source}`);
5188
- const result = ports.install(config.source);
5189
- if (!result.ok) {
5190
- return false;
5115
+ const state = member.snapshot?.providers[ANTHROPIC_PROVIDER];
5116
+ const unified = unifiedOf(state);
5117
+ if (member.snapshot === void 0 || state === void 0 || unified === void 0) {
5118
+ return { candidate: { ...base, class: "unknown", reasons: ["no usage recorded yet, so its quota is unknown (turn on trackUsage for it)", ...planReasons] } };
5119
+ }
5120
+ const observedAgo = state.rateLimit === void 0 ? "" : ` (last seen ${formatAge(nowMs - Date.parse(state.rateLimit.observedAt))} ago)`;
5121
+ const fiveHourWindow = unified.fiveHour;
5122
+ const sevenDayWindow = unified.sevenDay;
5123
+ const five = fiveHourWindow === void 0 ? void 0 : effectiveWindow(fiveHourWindow, nowMs);
5124
+ const seven = sevenDayWindow === void 0 ? void 0 : effectiveWindow(sevenDayWindow, nowMs);
5125
+ const windowReasons = [
5126
+ ...fiveHourWindow === void 0 || five === void 0 ? [] : [describeWindow("5h", fiveHourWindow, five, nowMs)],
5127
+ ...sevenDayWindow === void 0 || seven === void 0 ? [] : [describeWindow("7d", sevenDayWindow, seven, nowMs)]
5128
+ ];
5129
+ const blockedUntil = Math.max(
5130
+ windowBlockedUntil([five, seven].flatMap((window) => window === void 0 ? [] : [window])) ?? 0,
5131
+ limitBlockedUntil(state, nowMs) ?? 0
5132
+ );
5133
+ if (blockedUntil > nowMs) {
5134
+ if (unified.overageStatus === "allowed") {
5135
+ return { candidate: { ...base, class: "pay-per-use", reasons: [`plan exhausted until ${new Date(blockedUntil).toISOString()}, continues as extra usage`, ...windowReasons, ...planReasons] } };
5191
5136
  }
5192
- installedSource = config.source;
5193
- return true;
5194
- };
5195
- const socket = prepareHeadroomSocket(fs12, ports.socketTrust, paths, ports.ownPid);
5196
- if (socket.refused !== void 0) {
5197
- return fail(socket.refused);
5137
+ return { candidate: { ...base, class: "ineligible", blockedUntilMs: blockedUntil, reasons: [`refused until ${new Date(blockedUntil).toISOString()} (in ${formatAge(blockedUntil - nowMs)})`, ...windowReasons, ...planReasons] } };
5198
5138
  }
5199
- const { socketPath } = socket;
5200
- pruneStaleHeadroomSockets(fs12, paths, ports.isRunning);
5201
- if (!ensureInstalled()) {
5202
- return fail(`could not install headroom from ${config.source}: install failed (is uv installed and on PATH?)`);
5139
+ if (plan.kind === "pay-per-use" || fiveHourWindow === void 0 && sevenDayWindow === void 0) {
5140
+ return { candidate: { ...base, class: "pay-per-use", reasons: [...plan.kind === "pay-per-use" ? planReasons : ["no plan windows reported, so usage bills as extra usage"]] } };
5203
5141
  }
5204
- const version = ports.headroomVersion() ?? "unknown";
5205
- writeOwnState({ supervisorPid: ports.ownPid, version, installedSource });
5206
- ports.log(`agent-shim headroom supervisor ${String(ports.ownPid)}: managing headroom on allowlist [${allowlistOf(ports).join(", ")}]`);
5207
- const orphanPid = previousState?.headroomPid;
5208
- if (orphanPid !== void 0 && ports.isRunning(orphanPid)) {
5209
- ports.log(`agent-shim headroom supervisor: stopping daemon pid ${String(orphanPid)} left behind by the previous supervisor`);
5210
- ports.stopProcess(orphanPid);
5142
+ const basis = seven?.utilization !== void 0 ? { window: seven, windowMs: SEVEN_DAY_WINDOW_MS } : five?.utilization !== void 0 ? { window: five, windowMs: FIVE_HOUR_WINDOW_MS } : void 0;
5143
+ if (basis?.window.utilization === void 0) {
5144
+ return { candidate: { ...base, class: "unknown", reasons: ["quota windows reported no utilisation", ...windowReasons, ...planReasons] } };
5211
5145
  }
5212
- let headroomPid;
5213
- let runningHash;
5214
- let runningSettingsHash;
5215
- let consecutiveFailures = 0;
5216
- let idleSince;
5217
- let superseded = false;
5218
- let ticks = 0;
5219
- for (; ; ) {
5220
- if (options.tickLimit !== void 0 && ticks >= options.tickLimit) {
5221
- return HEADROOM_SUPERVISOR_STILL_RUNNING;
5146
+ const remaining = Math.max(0, 1 - basis.window.utilization);
5147
+ const untilResetMs = basis.window.resetsAtMs === void 0 ? basis.windowMs : Math.max(basis.window.resetsAtMs - nowMs, MS_PER_SECOND4);
5148
+ const score = remaining * plan.capacity / (untilResetMs / MS_PER_HOUR);
5149
+ const fiveRemaining = five?.utilization === void 0 ? void 0 : Math.max(0, 1 - five.utilization);
5150
+ const fiveUntil = five?.resetsAtMs === void 0 ? FIVE_HOUR_WINDOW_MS : Math.max(five.resetsAtMs - nowMs, MS_PER_SECOND4);
5151
+ const burn = fiveHourBurn(member.records);
5152
+ return {
5153
+ candidate: { ...base, class: "scored", score, reasons: [...windowReasons.map((reason, index) => index === windowReasons.length - 1 ? `${reason}${observedAgo}` : reason), ...planReasons] },
5154
+ ...fiveRemaining === void 0 ? {} : { fiveHour: { remaining: fiveRemaining, untilResetMs: fiveUntil } },
5155
+ ...burn === void 0 ? {} : { burn }
5156
+ };
5157
+ }
5158
+ function rankPool(input) {
5159
+ const { members, nowMs } = input;
5160
+ const assessments = members.map((member) => assess(member, nowMs));
5161
+ const demands = assessments.flatMap(({ candidate, burn }) => burn === void 0 ? [] : [burn * capacityOf(candidate.plan)]);
5162
+ const sharedDemand = demands.length === 0 ? void 0 : Math.max(...demands);
5163
+ const candidates = assessments.map((assessment) => {
5164
+ const { candidate, fiveHour, burn } = assessment;
5165
+ const pace = burn ?? (sharedDemand === void 0 ? void 0 : sharedDemand / capacityOf(candidate.plan));
5166
+ if (candidate.class !== "scored" || fiveHour === void 0 || pace === void 0) {
5167
+ return { ...candidate, feasible: true };
5222
5168
  }
5223
- ticks += 1;
5224
- config = ports.readConfig();
5225
- const settingsHash = hashSettings(config.settings);
5226
- const allowlist = allowlistOf(ports);
5227
- const allowlistHash = hashAllowlist(allowlist);
5228
- pruneDeadSessions(fs12, paths.headroomSessionsDir, ports.isRunning);
5229
- const ownsNow = ownsState();
5230
- if (!ownsNow && !superseded) {
5231
- ports.log("agent-shim headroom supervisor: another supervisor now owns the state file; serving this daemon's own sessions until they end");
5169
+ const runsDryInMs = fiveHour.remaining / pace;
5170
+ if (runsDryInMs >= fiveHour.untilResetMs) {
5171
+ return { ...candidate, feasible: true };
5232
5172
  }
5233
- superseded = !ownsNow;
5234
- const crashed = headroomPid !== void 0 && !ports.isRunning(headroomPid);
5235
- if (headroomPid === void 0 || crashed) {
5236
- if (crashed) {
5237
- ports.log(`agent-shim headroom supervisor: headroom pid ${String(headroomPid)} died`);
5238
- headroomPid = void 0;
5239
- runningHash = void 0;
5240
- runningSettingsHash = void 0;
5241
- writeOwnState({
5242
- supervisorPid: ports.ownPid,
5243
- version,
5244
- installedSource
5245
- });
5246
- }
5247
- if (consecutiveFailures >= HEADROOM_START_RETRY_BUDGET) {
5248
- return fail(`headroom failed to become ready ${String(consecutiveFailures)} times in a row; giving up`);
5249
- }
5250
- if (installedSource !== config.source && !ensureInstalled()) {
5251
- return fail(`could not install headroom from ${config.source}: install failed (is uv installed and on PATH?)`);
5252
- }
5253
- const pid = ports.spawnHeadroom(socketPath, allowlist, config.settings);
5254
- const ready = await waitUntilReady(ports, socketPath);
5255
- if (ready) {
5256
- consecutiveFailures = 0;
5257
- headroomPid = pid;
5258
- runningHash = allowlistHash;
5259
- runningSettingsHash = settingsHash;
5260
- const recorded = writeOwnState({
5261
- supervisorPid: ports.ownPid,
5262
- headroomPid: pid,
5263
- socketPath,
5264
- version,
5265
- allowlistHash,
5266
- settingsHash,
5267
- installedSource: config.source
5268
- });
5269
- if (recorded) {
5270
- fs12.removeRecursive(paths.headroomLockFile);
5271
- }
5272
- idleSince = void 0;
5273
- ports.log(`agent-shim headroom supervisor: headroom pid ${String(pid)} ready on unix socket ${socketPath}`);
5274
- } else {
5275
- ports.stopProcess(pid);
5276
- consecutiveFailures += 1;
5277
- ports.log(
5278
- `agent-shim headroom supervisor: headroom did not become ready on unix socket ${socketPath} (attempt ${String(consecutiveFailures)} of ${String(HEADROOM_START_RETRY_BUDGET)})`
5279
- );
5280
- await ports.sleep(backoffForAttempt(consecutiveFailures));
5281
- continue;
5282
- }
5283
- } else {
5284
- if (runningHash !== allowlistHash || runningSettingsHash !== settingsHash || installedSource !== config.source) {
5285
- if (sessionsForSupervisor(fs12, paths.headroomSessionsDir, ports.ownPid).length === 0) {
5286
- ports.log("agent-shim headroom supervisor: configuration drifted and no sessions are live; restarting headroom");
5287
- ports.stopProcess(headroomPid);
5288
- headroomPid = void 0;
5289
- runningHash = void 0;
5290
- runningSettingsHash = void 0;
5291
- continue;
5292
- }
5293
- }
5294
- const sessions = sessionsForSupervisor(fs12, paths.headroomSessionsDir, ports.ownPid);
5295
- if (sessions.length === 0) {
5296
- idleSince ??= ports.now();
5297
- const idleMs = ports.now() - idleSince;
5298
- if (idleMs >= config.idleShutdownMinutes * MS_PER_MINUTE2) {
5299
- ports.log(
5300
- `agent-shim headroom supervisor: no sessions for ${String(config.idleShutdownMinutes)} minute(s); stopping headroom and exiting`
5301
- );
5302
- ports.stopProcess(headroomPid);
5303
- writeOwnState({ version });
5304
- return 0;
5305
- }
5306
- } else {
5307
- idleSince = void 0;
5308
- }
5309
- }
5310
- await ports.sleep(HEADROOM_POLL_MS);
5311
- }
5312
- }
5313
- function currentHeadroomAllowlist(fs12, paths) {
5314
- const providers = readAllProviders(fs12, paths.providersDir).map((entry) => entry.provider);
5315
- return headroomAllowlist(headroomUpstreams(providers, frontDoorOrigin(readFrontDoorState(fs12, paths.frontdoorStateFile))));
5316
- }
5317
- function allowlistOf(ports) {
5318
- return currentHeadroomAllowlist(ports.fs, ports.paths);
5319
- }
5320
- async function waitUntilReady(ports, socketPath) {
5321
- const deadline = ports.now() + HEADROOM_READY_TIMEOUT_MS;
5322
- for (; ; ) {
5323
- if (await ports.ready(socketPath)) {
5324
- return true;
5325
- }
5326
- if (ports.now() >= deadline) {
5327
- return false;
5328
- }
5329
- await ports.sleep(HEADROOM_POLL_MS);
5330
- }
5331
- }
5332
-
5333
- // src/launcher/guard.ts
5334
- var AMBIENT_CREDENTIAL_VARS = [
5335
- "ANTHROPIC_API_KEY",
5336
- "ANTHROPIC_AUTH_TOKEN",
5337
- "CLAUDE_CODE_OAUTH_TOKEN",
5338
- "CLAUDE_CODE_USE_BEDROCK",
5339
- "CLAUDE_CODE_USE_VERTEX",
5340
- "CLAUDE_CODE_USE_FOUNDRY"
5341
- ];
5342
- function detectAmbientCredential(env, injected) {
5343
- for (const variable of AMBIENT_CREDENTIAL_VARS) {
5344
- const value = env[variable];
5345
- if (value === void 0 || value === "") {
5346
- continue;
5347
- }
5348
- if (injected?.variable === variable && value === injected.token) {
5349
- continue;
5173
+ return { ...candidate, feasible: false, reasons: [`5h window would run dry in ${formatAge(runsDryInMs)}, before it resets in ${formatAge(fiveHour.untilResetMs)}`, ...candidate.reasons] };
5174
+ });
5175
+ const ordered = [...candidates].sort(
5176
+ (left, right) => CLASS_ORDER[left.class] - CLASS_ORDER[right.class] || Number(right.feasible) - Number(left.feasible) || (right.score ?? 0) - (left.score ?? 0) || left.identity.localeCompare(right.identity)
5177
+ );
5178
+ const sticky = input.sticky;
5179
+ const stickyIndex = sticky === void 0 ? -1 : ordered.findIndex((candidate) => candidate.identity === sticky.identity);
5180
+ const stickyCandidate = ordered[stickyIndex];
5181
+ if (sticky !== void 0 && stickyCandidate?.class === "scored" && stickyCandidate.feasible) {
5182
+ const ageMs = nowMs - Date.parse(sticky.at);
5183
+ const warm = ageMs <= PROMPT_CACHE_TTL_MS;
5184
+ if (input.resuming || warm) {
5185
+ const why = warm ? `used here ${formatAge(ageMs)} ago, prompt cache still warm` : "resuming a conversation started on this account";
5186
+ ordered.splice(stickyIndex, 1);
5187
+ ordered.unshift({ ...stickyCandidate, reasons: [why, ...stickyCandidate.reasons] });
5350
5188
  }
5351
- return { variable };
5352
- }
5353
- return void 0;
5354
- }
5355
- function formatAmbientCredentialGuardMessage(variable, identityName) {
5356
- const identitySetCommand = identityName === void 0 ? "agent-shim identity set <name> --allow-ambient-credential" : `agent-shim identity set ${identityName} --allow-ambient-credential`;
5357
- return [
5358
- `error: ${variable} is set in the environment. This identity's isolated`,
5359
- "credential would be bypassed \u2014 every identity authenticates as this same key",
5360
- "while it's set. Unset it, or if this is deliberate, opt in per-launch with",
5361
- "AGENT_SHIM_ALLOW_AMBIENT_CREDENTIAL=1, or persistently for this identity with",
5362
- `\`${identitySetCommand}\`.`
5363
- ].join("\n");
5364
- }
5365
- function evaluateAmbientCredentialGuard(params) {
5366
- const detected = detectAmbientCredential(params.env, params.injectedCredential);
5367
- if (detected === void 0) {
5368
- return { ok: true };
5369
- }
5370
- if (params.allowAmbientCredentialOverride || params.allowAmbientCredential || params.providerSelected === true) {
5371
- return { ok: true };
5372
5189
  }
5190
+ const pick = ordered.find((candidate) => candidate.class !== "ineligible");
5191
+ const returns = ordered.flatMap((candidate) => candidate.blockedUntilMs === void 0 ? [] : [{ identity: candidate.identity, atMs: candidate.blockedUntilMs }]);
5192
+ const earliestReturn = returns.length === 0 ? void 0 : returns.reduce((soonest, entry) => entry.atMs < soonest.atMs ? entry : soonest);
5373
5193
  return {
5374
- ok: false,
5375
- variable: detected.variable,
5376
- message: formatAmbientCredentialGuardMessage(detected.variable, params.identityName)
5194
+ candidates: ordered,
5195
+ ...pick === void 0 ? { ...earliestReturn === void 0 ? {} : { earliestReturn } } : { pick }
5377
5196
  };
5378
5197
  }
5379
5198
 
5380
- // src/paths.ts
5381
- import fs3 from "node:fs";
5382
- import os from "node:os";
5383
- import path12 from "node:path";
5384
- function resolveAgentShimHome(env = process.env, home = os.homedir(), isDirectory = isExistingDirectory) {
5385
- const fromEnv = env.AGENT_SHIM_HOME;
5386
- if (fromEnv !== void 0 && fromEnv !== "") {
5387
- return fromEnv;
5388
- }
5389
- const current = path12.join(home, HOME_DIRNAME);
5390
- const legacy = path12.join(home, LEGACY_HOME_DIRNAME);
5391
- return !isDirectory(current) && isDirectory(legacy) ? legacy : current;
5199
+ // src/usage/poolPick.ts
5200
+ import path11 from "node:path";
5201
+ import { z as z8 } from "zod";
5202
+
5203
+ // src/usage/store.ts
5204
+ var DAY_MS2 = 864e5;
5205
+ var DAYS_IN_WEEK = 7;
5206
+ var USAGE_RETENTION_MS = DAYS_IN_WEEK * DAY_MS2;
5207
+
5208
+ // src/usage/poolPick.ts
5209
+ var SCHEMA_VERSION = 1;
5210
+ var PickEntrySchema = z8.strictObject({ identity: z8.string().min(1), at: z8.iso.datetime() });
5211
+ var PicksFileSchema = z8.strictObject({
5212
+ schemaVersion: z8.literal(SCHEMA_VERSION),
5213
+ picks: z8.record(z8.string().min(1), PickEntrySchema)
5214
+ });
5215
+ function errorMessage(error) {
5216
+ return error instanceof Error ? error.message : String(error);
5392
5217
  }
5393
- function isExistingDirectory(candidate) {
5394
- return fs3.statSync(candidate, { throwIfNoEntry: false })?.isDirectory() ?? false;
5218
+ function loadPoolMembers(fs12, paths, identities, nowMs) {
5219
+ const windowStartMs = nowMs - FIVE_HOUR_WINDOW_MS;
5220
+ const log = readUsageLog(fs12, paths.usageLogDir, { sinceMs: windowStartMs });
5221
+ return identities.map((identity) => {
5222
+ const records = log.records.filter((record) => record.identity === identity && record.provider === ANTHROPIC_PROVIDER && Date.parse(record.at) >= windowStartMs);
5223
+ try {
5224
+ const snapshot = readUsageSnapshot(fs12, paths.usageSnapshotsDir, identity);
5225
+ const account = readAccountMetadata(fs12, paths.identitiesDir, identity) ?? snapshot?.account;
5226
+ return { identity, records, ...snapshot === void 0 ? {} : { snapshot }, ...account === void 0 ? {} : { account } };
5227
+ } catch (error) {
5228
+ if (error instanceof UsageSnapshotError || error instanceof AccountMetadataError) {
5229
+ return { identity, records, readError: errorMessage(error) };
5230
+ }
5231
+ throw error;
5232
+ }
5233
+ });
5395
5234
  }
5396
- function resolveClaudeHome() {
5397
- const fromEnv = process.env.AGENT_SHIM_CLAUDE_HOME;
5398
- if (fromEnv !== void 0 && fromEnv !== "") {
5399
- return fromEnv;
5235
+ function rankPoolFromStore(params) {
5236
+ return rankPool({
5237
+ members: loadPoolMembers(params.fs, params.paths, params.identities, params.nowMs),
5238
+ nowMs: params.nowMs,
5239
+ resuming: params.resuming,
5240
+ ...params.sticky === void 0 ? {} : { sticky: params.sticky }
5241
+ });
5242
+ }
5243
+ function readPicksFile(fs12, file) {
5244
+ const empty = { schemaVersion: SCHEMA_VERSION, picks: {} };
5245
+ const raw = fs12.readFileUtf8(file);
5246
+ if (raw === void 0) {
5247
+ return { file: empty };
5248
+ }
5249
+ let parsed;
5250
+ try {
5251
+ parsed = JSON.parse(raw);
5252
+ } catch {
5253
+ return { file: empty, problem: `${file} is not valid JSON` };
5400
5254
  }
5401
- return path12.join(os.homedir(), ".claude");
5255
+ const result = PicksFileSchema.safeParse(parsed);
5256
+ return result.success ? { file: result.data } : { file: empty, problem: `${file} is not a picks file this agent-shim can read` };
5402
5257
  }
5403
- function buildLayoutPaths(root) {
5404
- return {
5405
- root,
5406
- identitiesDir: path12.join(root, "identities"),
5407
- configProfilesDir: path12.join(root, "config-profiles"),
5408
- providersDir: path12.join(root, "providers"),
5409
- directoryRulesFile: path12.join(root, "directory-rules.json"),
5410
- activeIdentityFile: path12.join(root, "active-identity"),
5411
- globalConfigFile: path12.join(root, "config.json"),
5412
- categoriesLocalFile: path12.join(root, "categories.local.json"),
5413
- claudeShimFile: path12.join(root, "claude-shim.json"),
5414
- headroomDir: path12.join(root, "headroom"),
5415
- headroomStateFile: path12.join(root, "headroom", `state.v${String(HEADROOM_STATE_SCHEMA_VERSION)}.json`),
5416
- headroomSocketDir: path12.join(root, "headroom", "run"),
5417
- headroomLockFile: path12.join(root, "headroom", "start.lock"),
5418
- headroomSessionsDir: path12.join(root, "headroom", "sessions"),
5419
- logsDir: path12.join(root, "logs"),
5420
- headroomLogPath: path12.join(root, "logs", "headroom.log"),
5421
- frontdoorDir: path12.join(root, "frontdoor"),
5422
- frontdoorCaDir: path12.join(root, "frontdoor", "ca"),
5423
- frontdoorCaCertFile: path12.join(root, "frontdoor", "ca", "ca.pem"),
5424
- frontdoorCaKeyFile: path12.join(root, "frontdoor", "ca", "ca.key"),
5425
- frontdoorCaBundlesDir: path12.join(root, "frontdoor", "ca", "bundles"),
5426
- frontdoorStateFile: path12.join(root, "frontdoor", "state.json"),
5427
- frontdoorLockFile: path12.join(root, "frontdoor", "start.lock"),
5428
- frontdoorControlTokenFile: path12.join(root, "frontdoor", "control-token"),
5429
- frontdoorSessionsDir: path12.join(root, "frontdoor", "sessions"),
5430
- frontdoorLogPath: path12.join(root, "logs", "frontdoor.log"),
5431
- usageDir: path12.join(root, "usage"),
5432
- usageLogDir: path12.join(root, "usage", "log"),
5433
- usageSnapshotsDir: path12.join(root, "usage", "snapshots"),
5434
- usagePicksFile: path12.join(root, "usage", "picks.json")
5435
- };
5258
+ function readStickyPick(fs12, picksFile, directory) {
5259
+ const read = readPicksFile(fs12, picksFile);
5260
+ const entry = read.file.picks[directory];
5261
+ return { ...entry === void 0 ? {} : { sticky: entry }, ...read.problem === void 0 ? {} : { problem: read.problem } };
5436
5262
  }
5437
- function resolveLayoutPaths() {
5438
- return buildLayoutPaths(resolveAgentShimHome());
5263
+ function recordStickyPick(fs12, picksFile, directory, identity, nowMs) {
5264
+ const current = readPicksFile(fs12, picksFile).file.picks;
5265
+ const kept = Object.entries(current).filter(([, entry]) => nowMs - Date.parse(entry.at) <= USAGE_RETENTION_MS);
5266
+ const next = { schemaVersion: SCHEMA_VERSION, picks: { ...Object.fromEntries(kept), [directory]: { identity, at: new Date(nowMs).toISOString() } } };
5267
+ fs12.mkdirPrivate(path11.dirname(picksFile));
5268
+ fs12.writeFilePrivate(picksFile, `${JSON.stringify(next, null, 2)}
5269
+ `);
5439
5270
  }
5440
5271
 
5441
- // src/identityStore.ts
5442
- import fs5 from "node:fs";
5443
- import path15 from "node:path";
5444
-
5445
5272
  // src/launcher/identity.ts
5446
- import path13 from "node:path";
5273
+ import path12 from "node:path";
5447
5274
  function poolNameOf(selector) {
5448
5275
  return selector.startsWith(POOL_SELECTOR_PREFIX) ? selector.slice(POOL_SELECTOR_PREFIX.length) : void 0;
5449
5276
  }
@@ -5492,1471 +5319,1682 @@ function decideConfigProfile(params) {
5492
5319
  return { source: "none" };
5493
5320
  }
5494
5321
  function loadIdentity(identitiesDir, name, fs12) {
5495
- return loadConfigFile(path13.join(identitiesDir, name, "identity.json"), IdentitySchema, fs12.readConfigFile);
5322
+ return loadConfigFile(path12.join(identitiesDir, name, "identity.json"), IdentitySchema, fs12.readConfigFile);
5496
5323
  }
5497
5324
 
5498
- // src/configProfilesStore.ts
5499
- import fs4 from "node:fs";
5500
- import path14 from "node:path";
5501
- var ProfileNotFoundError = class extends CliError {
5502
- constructor(profileName) {
5503
- super(`No configuration profile named "${profileName}". Run \`agent-shim profile add ${profileName}\` first.`);
5504
- this.profileName = profileName;
5505
- this.name = "ProfileNotFoundError";
5506
- }
5507
- profileName;
5508
- };
5509
- var ProfileAlreadyExistsError = class extends CliError {
5510
- constructor(profileName) {
5511
- super(`A configuration profile named "${profileName}" already exists.`);
5512
- this.profileName = profileName;
5513
- this.name = "ProfileAlreadyExistsError";
5514
- }
5515
- profileName;
5516
- };
5517
- var InvalidCategoryNameError = class extends CliError {
5518
- constructor(categoryName) {
5519
- super(`"${categoryName}" is not a category a configuration profile may toggle (runtime, history, knowledge, settings).`);
5520
- this.categoryName = categoryName;
5521
- this.name = "InvalidCategoryNameError";
5522
- }
5523
- categoryName;
5524
- };
5525
- function profileJsonPath(paths, name) {
5526
- return path14.join(paths.configProfilesDir, `${name}.json`);
5325
+ // src/launcher/pool.ts
5326
+ var RESUME_FLAGS = /* @__PURE__ */ new Set(["--continue", "-c", "--resume", "-r"]);
5327
+ var TERMINATOR = "--";
5328
+ var REASONS_LOGGED = 3;
5329
+ function splitMembers(pool, paths, fs12) {
5330
+ const exists = (name) => loadIdentity(paths.identitiesDir, name, fs12) !== void 0;
5331
+ return { present: pool.identities.filter(exists), missing: pool.identities.filter((name) => !exists(name)) };
5527
5332
  }
5528
- function profileExists(paths, name) {
5529
- return fs4.existsSync(profileJsonPath(paths, name));
5333
+ function isResuming(passthrough) {
5334
+ const end = passthrough.indexOf(TERMINATOR);
5335
+ return (end === -1 ? passthrough : passthrough.slice(0, end)).some((token) => RESUME_FLAGS.has(token));
5530
5336
  }
5531
- function requireProfileExists(paths, name) {
5532
- if (!profileExists(paths, name)) {
5533
- throw new ProfileNotFoundError(name);
5337
+ function resolvePoolLaunch(params) {
5338
+ const { poolName, log } = params;
5339
+ const pool = params.pools?.[poolName];
5340
+ if (pool === void 0) {
5341
+ return { ok: false, message: `agent-shim: no pool named "${poolName}" (selected via ${params.selectedVia}). Run \`agent-shim pool add ${poolName} --identity <name>...\` first.` };
5534
5342
  }
5535
- }
5536
- function readProfile(paths, name) {
5537
- return readJson(profileJsonPath(paths, name), ConfigProfileSchema);
5538
- }
5539
- function createProfile(paths, name, extendsList, description) {
5540
- if (profileExists(paths, name)) {
5541
- throw new ProfileAlreadyExistsError(name);
5343
+ const { present: identities, missing } = splitMembers(pool, params.paths, params.fs);
5344
+ for (const name of missing) {
5345
+ log.warn(`agent-shim: pool "${poolName}" names identity "${name}", which does not exist; skipping it`);
5542
5346
  }
5543
- const filePath = profileJsonPath(paths, name);
5544
- const parsed = ConfigProfileSchema.safeParse({
5545
- ...description === void 0 ? {} : { description },
5546
- ...extendsList !== void 0 && extendsList.length > 0 ? { extends: [...extendsList] } : {}
5547
- });
5548
- if (!parsed.success) {
5549
- throw new ConfigValidationError(filePath, parsed.error.issues);
5347
+ if (identities.length === 0) {
5348
+ return { ok: false, message: `agent-shim: no member of pool "${poolName}" is an existing identity.` };
5550
5349
  }
5551
- writeJsonAtomic(filePath, parsed.data);
5552
- return parsed.data;
5553
- }
5554
- function listProfiles(paths) {
5555
- if (!fs4.existsSync(paths.configProfilesDir)) {
5556
- return [];
5350
+ const resuming = isResuming(params.passthrough);
5351
+ const stickyRead = readStickyPick(params.usageFs, params.paths.usagePicksFile, params.cwd);
5352
+ if (stickyRead.problem !== void 0) {
5353
+ log.warn(`agent-shim: ${stickyRead.problem}; ignoring the last-pick record`);
5557
5354
  }
5558
- const names = fs4.readdirSync(paths.configProfilesDir, { withFileTypes: true }).filter((entry) => entry.isFile() && entry.name.endsWith(".json")).map((entry) => entry.name.slice(0, -".json".length)).sort();
5559
- const result = [];
5560
- for (const name of names) {
5561
- const profile = readProfile(paths, name);
5562
- if (profile !== void 0) {
5563
- result.push({ name, profile });
5355
+ for (; ; ) {
5356
+ const nowMs = params.now();
5357
+ const ranking = rankPoolFromStore({ fs: params.usageFs, paths: params.paths, identities, nowMs, resuming, ...stickyRead.sticky === void 0 ? {} : { sticky: stickyRead.sticky } });
5358
+ const { pick, earliestReturn } = ranking;
5359
+ if (pick !== void 0) {
5360
+ recordStickyPick(params.usageFs, params.paths.usagePicksFile, params.cwd, pick.identity, nowMs);
5361
+ return { ok: true, identity: pick.identity, explanation: `pool ${poolName}: ${pick.reasons.slice(0, REASONS_LOGGED).join("; ")}` };
5564
5362
  }
5565
- }
5566
- return result;
5567
- }
5568
- function globalConfigPath(paths) {
5569
- return paths.globalConfigFile;
5570
- }
5571
- function readGlobalConfig(paths) {
5572
- return readJson(globalConfigPath(paths), GlobalConfigSchema);
5573
- }
5574
- function setGlobalDefaultProfile(paths, name) {
5575
- return applyPatch(
5576
- globalConfigPath(paths),
5577
- GlobalConfigSchema,
5578
- { defaultConfigProfile: name },
5579
- { defaults: {} }
5580
- );
5581
- }
5582
- function validateCategoryNames(patch) {
5583
- for (const key of Object.keys(patch)) {
5584
- if (!isOverridableCategory(key)) {
5585
- throw new InvalidCategoryNameError(key);
5363
+ if (earliestReturn === void 0) {
5364
+ return { ok: false, message: `agent-shim: no member of pool "${poolName}" can be picked.` };
5586
5365
  }
5366
+ const until = new Date(earliestReturn.atMs).toISOString();
5367
+ if (!params.wait) {
5368
+ return {
5369
+ ok: false,
5370
+ message: `agent-shim: every member of pool "${poolName}" is refused right now; the earliest, ${earliestReturn.identity}, returns at ${until} (in ${formatAge(earliestReturn.atMs - nowMs)}). Pass --wait to sleep until then.`
5371
+ };
5372
+ }
5373
+ log.info(`agent-shim: every member of pool "${poolName}" is refused; waiting until ${until} for ${earliestReturn.identity}`);
5374
+ params.sleep(Math.max(earliestReturn.atMs - nowMs, 0));
5587
5375
  }
5588
5376
  }
5589
- function setProfileCategories(paths, name, patch) {
5590
- requireProfileExists(paths, name);
5591
- const expandedPatch = expandAllCategoryKey(patch);
5592
- validateCategoryNames(expandedPatch);
5593
- const existing = readProfile(paths, name) ?? {};
5594
- const mergedCategories = { ...existing.categories, ...expandedPatch };
5595
- return applyPatch(profileJsonPath(paths, name), ConfigProfileSchema, { categories: mergedCategories });
5596
- }
5597
- function setProfileEntries(paths, name, patch) {
5598
- requireProfileExists(paths, name);
5599
- const existing = readProfile(paths, name) ?? {};
5600
- const mergedEntries = { ...existing.entries, ...patch };
5601
- return applyPatch(profileJsonPath(paths, name), ConfigProfileSchema, { entries: mergedEntries });
5602
- }
5603
- function setProfileLaunchFlags(paths, name, patch) {
5604
- requireProfileExists(paths, name);
5605
- const existing = readProfile(paths, name) ?? {};
5606
- const mergedLaunch = { ...existing.launch, ...patch };
5607
- return applyPatch(profileJsonPath(paths, name), ConfigProfileSchema, {
5608
- // Read as `unknown` so the check sees the `undefined` a cleared key holds, which LaunchFlags' optional properties hide from the value type.
5609
- launch: Object.values(mergedLaunch).every((value) => value === void 0) ? void 0 : mergedLaunch
5610
- });
5377
+
5378
+ // src/poolPickReport.ts
5379
+ function candidateView(candidate) {
5380
+ return {
5381
+ identity: candidate.identity,
5382
+ class: candidate.class,
5383
+ ...candidate.score === void 0 ? {} : { score: candidate.score },
5384
+ feasible: candidate.feasible,
5385
+ ...candidate.blockedUntilMs === void 0 ? {} : { blockedUntil: new Date(candidate.blockedUntilMs).toISOString() },
5386
+ plan: candidate.plan,
5387
+ reasons: [...candidate.reasons]
5388
+ };
5611
5389
  }
5612
- function setProfileMetadata(paths, name, patch) {
5613
- requireProfileExists(paths, name);
5614
- return applyPatch(profileJsonPath(paths, name), ConfigProfileSchema, {
5615
- ...patch.extends === void 0 ? {} : { extends: patch.extends.length === 0 ? void 0 : [...patch.extends] },
5616
- ...patch.description === void 0 ? {} : { description: patch.description === false ? void 0 : patch.description }
5390
+ function collectPoolPick(params) {
5391
+ const { present, missing } = splitMembers(params.pool, params.paths, params.fs);
5392
+ const sticky = readStickyPick(params.usageFs, params.paths.usagePicksFile, params.directory);
5393
+ const ranking = rankPoolFromStore({
5394
+ fs: params.usageFs,
5395
+ paths: params.paths,
5396
+ identities: present,
5397
+ nowMs: params.nowMs,
5398
+ resuming: false,
5399
+ ...sticky.sticky === void 0 ? {} : { sticky: sticky.sticky }
5617
5400
  });
5618
- }
5619
- function removeProfile(paths, name) {
5620
- requireProfileExists(paths, name);
5621
- fs4.rmSync(profileJsonPath(paths, name));
5401
+ return {
5402
+ pool: params.poolName,
5403
+ directory: params.directory,
5404
+ ...ranking.pick === void 0 ? {} : { pick: ranking.pick.identity },
5405
+ candidates: ranking.candidates.map(candidateView),
5406
+ missing: [...missing],
5407
+ ...ranking.earliestReturn === void 0 ? {} : { earliestReturn: { identity: ranking.earliestReturn.identity, at: new Date(ranking.earliestReturn.atMs).toISOString() } },
5408
+ ...sticky.problem === void 0 ? {} : { stickyProblem: sticky.problem }
5409
+ };
5622
5410
  }
5623
5411
 
5624
- // src/poolStore.ts
5625
- var PoolNotFoundError = class extends CliError {
5626
- constructor(poolName) {
5627
- super(`No pool named "${poolName}". Run \`agent-shim pool add ${poolName} --identity <name>...\` first.`);
5628
- this.poolName = poolName;
5629
- this.name = "PoolNotFoundError";
5630
- }
5631
- poolName;
5632
- };
5633
- var PoolAlreadyExistsError = class extends CliError {
5634
- constructor(poolName) {
5635
- super(`A pool named "${poolName}" already exists. Use \`agent-shim pool set ${poolName}\` to change its members.`);
5636
- this.poolName = poolName;
5637
- this.name = "PoolAlreadyExistsError";
5638
- }
5639
- poolName;
5640
- };
5641
- var InvalidPoolNameError = class extends CliError {
5642
- constructor(attemptedName) {
5643
- super(`"${attemptedName}" is not a valid pool name: it must start with a letter or number and may then contain letters, numbers, dots, hyphens and underscores.`);
5644
- this.attemptedName = attemptedName;
5645
- this.name = "InvalidPoolNameError";
5646
- }
5647
- attemptedName;
5648
- };
5649
- function readPools(paths) {
5650
- return readGlobalConfig(paths)?.pools ?? {};
5651
- }
5652
- function requirePool(paths, name) {
5653
- const pool = readPools(paths)[name];
5654
- if (pool === void 0) {
5655
- throw new PoolNotFoundError(name);
5656
- }
5657
- return pool;
5658
- }
5659
- function writePools(paths, pools) {
5660
- applyPatch(paths.globalConfigFile, GlobalConfigSchema, { pools }, { defaults: {} });
5661
- }
5662
- function addPool(paths, name, identities) {
5663
- if (!PoolNameSchema.safeParse(name).success) {
5664
- throw new InvalidPoolNameError(name);
5665
- }
5666
- const pools = readPools(paths);
5667
- if (pools[name] !== void 0) {
5668
- throw new PoolAlreadyExistsError(name);
5669
- }
5670
- const pool = { identities: [...identities] };
5671
- writePools(paths, { ...pools, [name]: pool });
5672
- return pool;
5673
- }
5674
- function setPool(paths, name, identities) {
5675
- requirePool(paths, name);
5676
- const pool = { identities: [...identities] };
5677
- writePools(paths, { ...readPools(paths), [name]: pool });
5678
- return pool;
5679
- }
5680
- function removePool(paths, name) {
5681
- requirePool(paths, name);
5682
- const remaining = Object.fromEntries(Object.entries(readPools(paths)).filter(([existing]) => existing !== name));
5683
- writePools(paths, remaining);
5684
- }
5412
+ // src/usage/pickReportSchema.ts
5413
+ import { z as z9 } from "zod";
5414
+ var PoolPickCandidateViewSchema = z9.strictObject({
5415
+ identity: z9.string(),
5416
+ /** In pick order: `scored` has usable quota data, `unknown` has none, `pay-per-use` bills by use, `ineligible` is refused right now. */
5417
+ class: z9.enum(["scored", "unknown", "pay-per-use", "ineligible"]),
5418
+ /** Plan-size-weighted remaining quota per hour until its reset; present for `scored` only. */
5419
+ score: z9.number().optional(),
5420
+ /** False when the five-hour window would run dry, at the observed pace, before it resets. */
5421
+ feasible: z9.boolean(),
5422
+ /** When an `ineligible` member can be used again, as an ISO instant. */
5423
+ blockedUntil: z9.iso.datetime().optional(),
5424
+ plan: PlanClassSchema,
5425
+ /** The facts behind the class and score, in the order they matter. */
5426
+ reasons: z9.array(z9.string())
5427
+ });
5428
+ var PoolPickReportSchema = z9.strictObject({
5429
+ pool: z9.string(),
5430
+ directory: z9.string(),
5431
+ /** The member a launch would run as, absent when every member is refused. */
5432
+ pick: z9.string().optional(),
5433
+ candidates: z9.array(PoolPickCandidateViewSchema),
5434
+ /** Members that name an identity that does not exist; a launch skips them. */
5435
+ missing: z9.array(z9.string()),
5436
+ /** When nothing can be picked, the soonest any member returns. */
5437
+ earliestReturn: z9.strictObject({ identity: z9.string(), at: z9.iso.datetime() }).optional(),
5438
+ /** Why the last-pick record could not be read, when that is the case. */
5439
+ stickyProblem: z9.string().optional()
5440
+ });
5685
5441
 
5686
- // src/identityStore.ts
5687
- var IdentityNotFoundError = class extends CliError {
5688
- constructor(name) {
5689
- super(`No identity named "${name}". Run \`agent-shim identity add ${name}\` first.`);
5690
- this.name = name;
5691
- this.name = "IdentityNotFoundError";
5442
+ // src/headroom/ensure.ts
5443
+ var HEADROOM_START_TIMEOUT_MS = 12e4;
5444
+ var HEADROOM_LAUNCHER_POLL_MS = 250;
5445
+ var HeadroomStartError = class extends CliError {
5446
+ constructor(message) {
5447
+ super(message);
5448
+ this.name = "HeadroomStartError";
5692
5449
  }
5693
- name;
5694
5450
  };
5695
- var IdentityAlreadyExistsError = class extends CliError {
5696
- constructor(identityName) {
5697
- super(`An identity named "${identityName}" already exists.`);
5698
- this.identityName = identityName;
5699
- this.name = "IdentityAlreadyExistsError";
5700
- }
5701
- identityName;
5702
- };
5703
- var InvalidIdentityNameError = class extends CliError {
5704
- constructor(attemptedName) {
5705
- super(
5706
- `"${attemptedName}" is not a valid identity name \u2014 identity names must start with a letter or number and may then contain letters, numbers, dots, hyphens, underscores, and at signs.`
5707
- );
5708
- this.attemptedName = attemptedName;
5709
- this.name = "InvalidIdentityNameError";
5451
+ function ensureHeadroom(params) {
5452
+ const { paths, ports } = params;
5453
+ const deadline = ports.now() + HEADROOM_START_TIMEOUT_MS;
5454
+ let spawnedSupervisor = false;
5455
+ let awaitingAllowlist = false;
5456
+ for (; ; ) {
5457
+ const state = readHeadroomState(ports.fs, paths.headroomStateFile);
5458
+ if (state?.supervisorPid !== void 0 && ports.isRunning(state.supervisorPid)) {
5459
+ const daemonUp = state.socketPath !== void 0 && state.headroomPid !== void 0 && ports.isRunning(state.headroomPid);
5460
+ if (daemonUp && state.socketPath !== void 0) {
5461
+ const required = params.requiredAllowlistHash?.();
5462
+ if (required === void 0 || state.allowlistHash === required) {
5463
+ writeSession(ports.fs, paths.headroomSessionsDir, { pid: params.launcherPid, startedAt: ports.now(), supervisorPid: state.supervisorPid });
5464
+ return { socketPath: state.socketPath };
5465
+ }
5466
+ const holders = sessionsForSupervisor(ports.fs, paths.headroomSessionsDir, state.supervisorPid).filter((session) => ports.isRunning(session.pid));
5467
+ if (holders.length > 0) {
5468
+ throw new HeadroomStartError(
5469
+ `the running headroom daemon was started before this launch's front door address and would refuse every request this launch sends through it. It restarts once the launches using it end (pid ${holders.map((session) => String(session.pid)).join(", ")}); end them and launch again, or launch with --no-headroom.`
5470
+ );
5471
+ }
5472
+ awaitingAllowlist = true;
5473
+ }
5474
+ if (state.lastError !== void 0) {
5475
+ throw new HeadroomStartError(
5476
+ `the headroom daemon reported a fatal error and is not serving: ${state.lastError} (daemon log: ${paths.headroomLogPath})`
5477
+ );
5478
+ }
5479
+ } else if (!spawnedSupervisor) {
5480
+ ports.fs.mkdirp(paths.headroomDir);
5481
+ const created = ports.fs.writeFileExclusive(
5482
+ paths.headroomLockFile,
5483
+ `${JSON.stringify({ pid: params.launcherPid, at: ports.now() })}
5484
+ `
5485
+ );
5486
+ if (created) {
5487
+ ports.spawnSupervisor(paths);
5488
+ spawnedSupervisor = true;
5489
+ } else {
5490
+ const lock = readStartLock(ports.fs, paths.headroomLockFile);
5491
+ if (lock === void 0 || !ports.isRunning(lock.pid)) {
5492
+ ports.fs.removeRecursive(paths.headroomLockFile);
5493
+ continue;
5494
+ }
5495
+ }
5496
+ }
5497
+ if (ports.now() >= deadline) {
5498
+ const lastError = state?.lastError;
5499
+ throw new HeadroomStartError(
5500
+ `the headroom daemon did not become ready within ${String(HEADROOM_START_TIMEOUT_MS)}ms` + (lastError === void 0 ? "" : ` (last error: ${lastError})`) + (awaitingAllowlist ? " (the running daemon's allowlist does not admit this launch's front door, and it did not restart)" : "") + `. Daemon log: ${paths.headroomLogPath}`
5501
+ );
5502
+ }
5503
+ ports.sleep(HEADROOM_LAUNCHER_POLL_MS);
5710
5504
  }
5711
- attemptedName;
5712
- };
5713
- function identityJsonPath(paths, name) {
5714
- return path15.join(paths.identitiesDir, name, "identity.json");
5715
5505
  }
5716
- function identityExists(paths, name) {
5717
- return fs5.existsSync(identityJsonPath(paths, name));
5506
+
5507
+ // src/headroom/settings.ts
5508
+ import { createHash as createHash4 } from "node:crypto";
5509
+ function settingsOf(config) {
5510
+ return {
5511
+ ...config.mode === void 0 ? {} : { mode: config.mode },
5512
+ ...config.targetRatio === void 0 ? {} : { targetRatio: config.targetRatio },
5513
+ ...config.ccr === void 0 ? {} : { ccr: config.ccr },
5514
+ ...config.rolloutChannel === void 0 ? {} : { rolloutChannel: config.rolloutChannel },
5515
+ ...config.interceptToolResults === void 0 ? {} : { interceptToolResults: config.interceptToolResults },
5516
+ ...config.readMaturation === void 0 ? {} : { readMaturation: config.readMaturation }
5517
+ };
5718
5518
  }
5719
- function readIdentity(paths, name) {
5720
- return readJson(identityJsonPath(paths, name), IdentitySchema);
5519
+ function settingsArgs(settings) {
5520
+ return [
5521
+ ...settings.mode === void 0 ? [] : ["--mode", settings.mode],
5522
+ ...settings.targetRatio === void 0 ? [] : ["--target-ratio", String(settings.targetRatio)],
5523
+ ...settings.ccr === "lossless" ? ["--lossless"] : [],
5524
+ ...settings.ccr === "none" ? ["--no-ccr"] : [],
5525
+ ...settings.interceptToolResults === true ? ["--intercept-tool-results"] : [],
5526
+ ...settings.readMaturation === true ? ["--read-maturation"] : []
5527
+ ];
5721
5528
  }
5722
- function addIdentity(paths, name) {
5723
- if (identityExists(paths, name)) {
5724
- throw new IdentityAlreadyExistsError(name);
5725
- }
5726
- const parsed = IdentitySchema.safeParse({ name, allowAmbientCredential: false });
5727
- if (!parsed.success) {
5728
- throw new InvalidIdentityNameError(name);
5729
- }
5730
- writeJsonAtomic(identityJsonPath(paths, name), parsed.data);
5731
- return parsed.data;
5529
+ function settingsEnv(settings) {
5530
+ return settings.rolloutChannel === void 0 ? {} : { HEADROOM_ROLLOUT_CHANNEL: settings.rolloutChannel };
5732
5531
  }
5733
- function useIdentity(paths, name) {
5734
- const pool = poolNameOf(name);
5735
- if (pool !== void 0) {
5736
- requirePool(paths, pool);
5737
- } else if (!identityExists(paths, name)) {
5738
- throw new IdentityNotFoundError(name);
5739
- }
5740
- writeTextAtomic(paths.activeIdentityFile, `${name}
5741
- `);
5532
+ function hashSettings(settings) {
5533
+ return createHash4("sha256").update(JSON.stringify([...settingsArgs(settings), JSON.stringify(settingsEnv(settings))])).digest("hex");
5742
5534
  }
5743
- function readActiveIdentity(paths) {
5744
- if (!fs5.existsSync(paths.activeIdentityFile)) {
5745
- return void 0;
5746
- }
5747
- const raw = fs5.readFileSync(paths.activeIdentityFile, "utf8").trim();
5748
- return raw === "" ? void 0 : raw;
5535
+
5536
+ // src/headroom/socket.ts
5537
+ import path13 from "node:path";
5538
+ var GROUP_OR_OTHER_BITS = 63;
5539
+ var OCTAL_RADIX = 8;
5540
+ var MODE_DIGITS = 4;
5541
+ var HEADROOM_SOCKET_SUFFIX = ".sock";
5542
+ var HEADROOM_SOCKET_NAME = /^(\d+)\.sock$/;
5543
+ var UNIX_SOCKET_PATH_MAX_BYTES = 103;
5544
+ var PLATFORMS_WITHOUT_UNIX_SOCKETS = ["win32"];
5545
+ function headroomSocketPath(paths, supervisorPid) {
5546
+ return path13.join(paths.headroomSocketDir, `${String(supervisorPid)}${HEADROOM_SOCKET_SUFFIX}`);
5749
5547
  }
5750
- function isIdentityDirectoryName(name) {
5751
- return !name.startsWith(".");
5548
+ function platformRefusal(platform) {
5549
+ return PLATFORMS_WITHOUT_UNIX_SOCKETS.includes(platform) ? `headroom routing needs a unix domain socket, which headroom cannot serve on ${platform}; agent-shim never falls back to a TCP port for this hop` : void 0;
5752
5550
  }
5753
- function readIdentityForListing(paths, name) {
5754
- try {
5755
- return readIdentity(paths, name);
5756
- } catch (error) {
5757
- if (error instanceof ConfigValidationError || error instanceof SyntaxError) {
5758
- return { problem: error.message.replace(/\s*\n\s*/g, " ") };
5759
- }
5760
- throw error;
5551
+ function ownerOnlyRefusal(label, target, stat, expected, uid) {
5552
+ if (stat === void 0) {
5553
+ return `the headroom ${label} ${target} does not exist`;
5761
5554
  }
5762
- }
5763
- function listIdentities(paths) {
5764
- if (!fs5.existsSync(paths.identitiesDir)) {
5765
- return [];
5555
+ if (stat.kind === "symlink") {
5556
+ return `the headroom ${label} ${target} is a symlink; it must be the real ${expected === "dir" ? "directory" : "socket"}`;
5766
5557
  }
5767
- const active = readActiveIdentity(paths);
5768
- const names = fs5.readdirSync(paths.identitiesDir, { withFileTypes: true }).filter((entry) => entry.isDirectory() && isIdentityDirectoryName(entry.name)).map((entry) => entry.name).sort();
5769
- const result = [];
5770
- for (const name of names) {
5771
- const read = readIdentityForListing(paths, name);
5772
- if (read === void 0) {
5773
- continue;
5774
- }
5775
- const isActive = name === active;
5776
- result.push("problem" in read ? { name, isActive, problem: read.problem } : { name, identity: read, isActive });
5558
+ if (stat.kind !== expected) {
5559
+ return `the headroom ${label} ${target} is not a ${expected === "dir" ? "directory" : "socket"}`;
5777
5560
  }
5778
- return result;
5779
- }
5780
- function setDefaultConfigProfile(paths, identityName, profileName) {
5781
- if (!identityExists(paths, identityName)) {
5782
- throw new IdentityNotFoundError(identityName);
5561
+ if (stat.uid !== uid) {
5562
+ return `the headroom ${label} ${target} is owned by uid ${String(stat.uid)}, not by this user (uid ${String(uid)})`;
5783
5563
  }
5784
- return applyPatch(identityJsonPath(paths, identityName), IdentitySchema, {
5785
- defaultConfigProfile: profileName
5786
- });
5787
- }
5788
- function setAllowAmbientCredential(paths, identityName, allow) {
5789
- if (!identityExists(paths, identityName)) {
5790
- throw new IdentityNotFoundError(identityName);
5564
+ if ((stat.mode & GROUP_OR_OTHER_BITS) !== 0) {
5565
+ return `the headroom ${label} ${target} has mode ${stat.mode.toString(OCTAL_RADIX).padStart(MODE_DIGITS, "0")}, which lets other users reach it; it must be accessible to its owner only`;
5791
5566
  }
5792
- return applyPatch(identityJsonPath(paths, identityName), IdentitySchema, {
5793
- allowAmbientCredential: allow
5794
- });
5567
+ return void 0;
5795
5568
  }
5796
- function setIdentityCredential(paths, identityName, change) {
5797
- const existing = readIdentity(paths, identityName);
5798
- if (existing === void 0) {
5799
- throw new IdentityNotFoundError(identityName);
5569
+ function prepareHeadroomSocket(fs12, ports, paths, supervisorPid) {
5570
+ const unsupported = platformRefusal(ports.platform);
5571
+ if (unsupported !== void 0) {
5572
+ return { refused: unsupported };
5800
5573
  }
5801
- if (change === false) {
5802
- return applyPatch(identityJsonPath(paths, identityName), IdentitySchema, { credential: void 0 });
5574
+ const socketPath = headroomSocketPath(paths, supervisorPid);
5575
+ const length = Buffer.byteLength(socketPath, "utf8");
5576
+ if (length > UNIX_SOCKET_PATH_MAX_BYTES) {
5577
+ return {
5578
+ refused: `the headroom socket path ${socketPath} is ${String(length)} bytes, longer than the ${String(UNIX_SOCKET_PATH_MAX_BYTES)} a unix socket path can hold; set AGENT_SHIM_HOME to a shorter directory`
5579
+ };
5803
5580
  }
5804
- const sources = change.sources ?? existing.credential?.sources;
5805
- if (sources === void 0) {
5806
- throw new UsageError(`Identity "${identityName}" has no credential yet: pass --credential to give it one before choosing its target.`);
5581
+ if (ports.lstat(paths.headroomSocketDir)?.kind === "symlink") {
5582
+ return { refused: `the headroom socket directory ${paths.headroomSocketDir} is a symlink; it must be the real directory` };
5807
5583
  }
5808
- const target = change.target ?? existing.credential?.target;
5809
- const cache = change.cache === void 0 ? existing.credential?.cache : change.cache === false ? void 0 : change.cache;
5810
- return applyPatch(identityJsonPath(paths, identityName), IdentitySchema, {
5811
- credential: { sources: [...sources], ...target === void 0 ? {} : { target }, ...cache === void 0 ? {} : { cache } }
5812
- });
5813
- }
5814
- function belongsToIdentity(entry, name) {
5815
- return entry === name || entry === path15.basename(identityLockPath("", name)) || entry.startsWith(`.${name}.scratch.`) || entry.startsWith(`.${name}.previous.`);
5584
+ fs12.mkdirPrivate(paths.headroomSocketDir);
5585
+ const refusal = ownerOnlyRefusal("socket directory", paths.headroomSocketDir, ports.lstat(paths.headroomSocketDir), "dir", ports.currentUid());
5586
+ return refusal === void 0 ? { socketPath } : { refused: refusal };
5816
5587
  }
5817
- function removeIdentity(paths, name) {
5818
- if (!identityExists(paths, name)) {
5819
- throw new IdentityNotFoundError(name);
5820
- }
5821
- for (const entry of fs5.readdirSync(paths.identitiesDir)) {
5822
- if (belongsToIdentity(entry, name)) {
5823
- fs5.rmSync(path15.join(paths.identitiesDir, entry), { recursive: true, force: true });
5588
+ function pruneStaleHeadroomSockets(fs12, paths, isRunning) {
5589
+ const removed = [];
5590
+ for (const name of fs12.readdir(paths.headroomSocketDir)) {
5591
+ const pid = HEADROOM_SOCKET_NAME.exec(name)?.[1];
5592
+ if (pid === void 0 || isRunning(Number.parseInt(pid, 10))) {
5593
+ continue;
5824
5594
  }
5595
+ fs12.removeRecursive(path13.join(paths.headroomSocketDir, name));
5596
+ removed.push(name);
5825
5597
  }
5826
- if (readActiveIdentity(paths) === name) {
5827
- fs5.rmSync(paths.activeIdentityFile);
5828
- }
5598
+ return removed;
5829
5599
  }
5830
5600
 
5831
- // src/providersStore.ts
5832
- import fs6 from "node:fs";
5833
- import path16 from "node:path";
5834
-
5835
- // src/credentialCache.ts
5836
- import { z as z7 } from "zod";
5601
+ // src/headroom/source.ts
5602
+ import { z as z10 } from "zod";
5603
+ var GIT_SPEC_RE = /git\+[^@\s]+(?:@([^\s#]+))?/;
5604
+ var COMMIT_SHA_RE = /^[0-9a-f]{40}$/i;
5605
+ var DirectUrlSchema = z10.object({ vcs_info: z10.object({ commit_id: z10.string() }) });
5606
+ function isMovingGitSource(spec) {
5607
+ const match = GIT_SPEC_RE.exec(spec);
5608
+ if (match === null) {
5609
+ return false;
5610
+ }
5611
+ const ref = match[1];
5612
+ return ref === void 0 || !COMMIT_SHA_RE.test(ref);
5613
+ }
5614
+ function pinnedGitCommit(spec) {
5615
+ const ref = GIT_SPEC_RE.exec(spec)?.[1];
5616
+ return ref !== void 0 && COMMIT_SHA_RE.test(ref) ? ref.toLowerCase() : void 0;
5617
+ }
5837
5618
 
5838
- // src/usage/preflight.ts
5839
- var MS_PER_SECOND2 = 1e3;
5840
- var SECONDS_PER_MINUTE = 60;
5841
- var MINUTES_PER_HOUR = 60;
5842
- var HOURS_PER_DAY = 24;
5843
- var WARNING_STATUSES = /* @__PURE__ */ new Set(["allowed_warning", "rejected"]);
5844
- function formatAge(ms) {
5845
- const minutes = Math.floor(ms / (MS_PER_SECOND2 * SECONDS_PER_MINUTE));
5846
- if (minutes < MINUTES_PER_HOUR) {
5847
- return `${String(Math.max(minutes, 0))}m`;
5848
- }
5849
- const hours = Math.floor(minutes / MINUTES_PER_HOUR);
5850
- return hours < HOURS_PER_DAY ? `${String(hours)}h` : `${String(Math.floor(hours / HOURS_PER_DAY))}d`;
5851
- }
5852
- function effectiveWindow(window, nowMs) {
5853
- const resetsAtMs = window.resetsAt === void 0 ? void 0 : Date.parse(window.resetsAt);
5854
- if (resetsAtMs !== void 0 && resetsAtMs <= nowMs) {
5855
- return { reset: true, utilization: 0 };
5856
- }
5619
+ // src/headroom/supervisor.ts
5620
+ function resolveSupervisorConfig(configured) {
5857
5621
  return {
5858
- reset: false,
5859
- ...window.utilization === void 0 ? {} : { utilization: window.utilization },
5860
- ...resetsAtMs === void 0 ? {} : { resetsAtMs },
5861
- ...window.status === void 0 ? {} : { status: window.status }
5622
+ source: configured.source ?? HEADROOM_DEFAULT_SOURCE,
5623
+ idleShutdownMinutes: configured.idleShutdownMinutes ?? HEADROOM_DEFAULT_IDLE_SHUTDOWN_MINUTES,
5624
+ settings: settingsOf(configured)
5862
5625
  };
5863
5626
  }
5864
- function quotaWarnings(snapshot, provider, nowMs) {
5865
- const rateLimit = snapshot?.providers[provider]?.rateLimit;
5866
- const unified = rateLimit?.unified;
5867
- if (snapshot === void 0 || rateLimit === void 0 || unified === void 0) {
5868
- return [];
5869
- }
5870
- const age = formatAge(nowMs - Date.parse(rateLimit.observedAt));
5871
- const windows = [
5872
- { name: "five-hour", window: unified.fiveHour },
5873
- { name: "seven-day", window: unified.sevenDay }
5874
- ];
5875
- return windows.flatMap(({ name, window }) => {
5876
- const effective = window === void 0 ? void 0 : effectiveWindow(window, nowMs);
5877
- if (window === void 0 || effective?.status === void 0 || !WARNING_STATUSES.has(effective.status)) {
5878
- return [];
5879
- }
5880
- const reset = window.resetsAt === void 0 ? "" : `, resets ${window.resetsAt}`;
5881
- const verdict = effective.status === "rejected" ? "exhausted" : "nearly used";
5882
- return [`agent-shim: identity ${snapshot.identity}: the ${name} quota is ${verdict}${reset} (last seen ${age} ago)`];
5883
- });
5627
+ var HEADROOM_BACKOFF_BASE_MS = 500;
5628
+ var HEADROOM_BACKOFF_CAP_MS = 3e4;
5629
+ var HEADROOM_START_RETRY_BUDGET = 5;
5630
+ var HEADROOM_READY_TIMEOUT_MS = 15e3;
5631
+ var HEADROOM_POLL_MS = 1e3;
5632
+ var MS_PER_MINUTE2 = 6e4;
5633
+ function backoffForAttempt(attempt) {
5634
+ return Math.min(HEADROOM_BACKOFF_CAP_MS, HEADROOM_BACKOFF_BASE_MS * 2 ** (attempt - 1));
5884
5635
  }
5885
-
5886
- // src/credentialCache.ts
5887
- var MS_PER_UNIT = { s: 1e3, m: 6e4, h: 36e5, d: 864e5 };
5888
- var CachedCredentialSchema = z7.strictObject({
5889
- token: z7.string().min(1),
5890
- fetchedAt: z7.number().int().nonnegative(),
5891
- source: CredentialSourceSchema
5892
- });
5893
- function effectiveStore(cache, platform) {
5894
- return cache.store ?? (platform === "darwin" ? "keychain" : "file");
5636
+ function versionSatisfies(version, source) {
5637
+ if (version === void 0) {
5638
+ return false;
5639
+ }
5640
+ const specifier = /==|>=|<=|!=|>|<|~=/.exec(source);
5641
+ if (specifier === null) {
5642
+ return true;
5643
+ }
5644
+ const installed = parseVersion(version);
5645
+ const wanted = parseVersion(source.slice(specifier.index + specifier[0].length));
5646
+ if (installed === void 0 || wanted === void 0) {
5647
+ return true;
5648
+ }
5649
+ const cmp = compareVersions(installed, wanted);
5650
+ switch (specifier[0]) {
5651
+ case "==":
5652
+ return cmp === 0;
5653
+ case "!=":
5654
+ return cmp !== 0;
5655
+ case ">=":
5656
+ return cmp >= 0;
5657
+ case "<=":
5658
+ return cmp <= 0;
5659
+ case ">":
5660
+ return cmp > 0;
5661
+ case "<":
5662
+ return cmp < 0;
5663
+ case "~=":
5664
+ return compareVersions(installed.slice(0, Math.max(0, wanted.length - 1)), wanted.slice(0, Math.max(0, wanted.length - 1))) === 0 && cmp >= 0;
5665
+ default:
5666
+ return true;
5667
+ }
5895
5668
  }
5896
- function ttlMs(cache) {
5897
- if (cache.ttl === void 0) {
5669
+ function parseVersion(input) {
5670
+ const match = /(\d+)(\.\d+)*/.exec(input);
5671
+ if (match === null) {
5898
5672
  return void 0;
5899
5673
  }
5900
- const multiplier = MS_PER_UNIT[cache.ttl.slice(-1)];
5901
- if (multiplier === void 0) {
5902
- throw new Error(`credential cache ttl "${cache.ttl}" has no recognised unit`);
5674
+ return match[0].split(".").map((part) => Number.parseInt(part, 10));
5675
+ }
5676
+ function compareVersions(a, b) {
5677
+ for (let index = 0; index < Math.max(a.length, b.length); index += 1) {
5678
+ const left = a[index] ?? 0;
5679
+ const right = b[index] ?? 0;
5680
+ if (left !== right) {
5681
+ return left - right;
5682
+ }
5903
5683
  }
5904
- return Number(cache.ttl.slice(0, -1)) * multiplier;
5684
+ return 0;
5905
5685
  }
5906
- function isFresh(entry, cache, now) {
5907
- const limit = ttlMs(cache);
5908
- if (entry.fetchedAt > now) {
5909
- return false;
5686
+ var HEADROOM_SUPERVISOR_STILL_RUNNING = -1;
5687
+ async function runSupervisor(initialConfig, ports, options = {}) {
5688
+ const { fs: fs12, paths } = ports;
5689
+ let config = initialConfig;
5690
+ const previousState = readHeadroomState(fs12, paths.headroomStateFile);
5691
+ if (previousState?.supervisorPid !== void 0 && previousState.supervisorPid !== ports.ownPid && ports.isRunning(previousState.supervisorPid)) {
5692
+ ports.log(`agent-shim headroom supervisor ${String(ports.ownPid)}: supervisor ${String(previousState.supervisorPid)} already owns the daemon; exiting`);
5693
+ return 0;
5694
+ }
5695
+ let installedSource = previousState?.installedSource;
5696
+ const ownsState = () => {
5697
+ const owner = readHeadroomState(fs12, paths.headroomStateFile)?.supervisorPid;
5698
+ return owner === void 0 || owner === ports.ownPid || !ports.isRunning(owner);
5699
+ };
5700
+ const writeOwnState = (state) => {
5701
+ if (!ownsState()) {
5702
+ return false;
5703
+ }
5704
+ writeHeadroomState(fs12, paths.headroomStateFile, state);
5705
+ return true;
5706
+ };
5707
+ const fail = (message) => {
5708
+ if (writeOwnState({ lastError: message })) {
5709
+ fs12.removeRecursive(paths.headroomLockFile);
5710
+ }
5711
+ ports.log(`agent-shim headroom supervisor: ${message}`);
5712
+ return 1;
5713
+ };
5714
+ const ensureInstalled = () => {
5715
+ const version2 = ports.headroomVersion();
5716
+ const versionOk = version2 !== void 0 && versionSatisfies(version2, config.source);
5717
+ const pinned = pinnedGitCommit(config.source);
5718
+ const commitOk = pinned === void 0 || ports.installedCommit() === pinned;
5719
+ if (versionOk && commitOk && (installedSource === void 0 || installedSource === config.source)) {
5720
+ installedSource = config.source;
5721
+ return true;
5722
+ }
5723
+ ports.log(`agent-shim headroom supervisor: installing headroom from ${config.source}`);
5724
+ const result = ports.install(config.source);
5725
+ if (!result.ok) {
5726
+ return false;
5727
+ }
5728
+ installedSource = config.source;
5729
+ return true;
5730
+ };
5731
+ const socket = prepareHeadroomSocket(fs12, ports.socketTrust, paths, ports.ownPid);
5732
+ if (socket.refused !== void 0) {
5733
+ return fail(socket.refused);
5734
+ }
5735
+ const { socketPath } = socket;
5736
+ pruneStaleHeadroomSockets(fs12, paths, ports.isRunning);
5737
+ if (!ensureInstalled()) {
5738
+ return fail(`could not install headroom from ${config.source}: install failed (is uv installed and on PATH?)`);
5739
+ }
5740
+ const version = ports.headroomVersion() ?? "unknown";
5741
+ writeOwnState({ supervisorPid: ports.ownPid, version, installedSource });
5742
+ ports.log(`agent-shim headroom supervisor ${String(ports.ownPid)}: managing headroom on allowlist [${allowlistOf(ports).join(", ")}]`);
5743
+ const orphanPid = previousState?.headroomPid;
5744
+ if (orphanPid !== void 0 && ports.isRunning(orphanPid)) {
5745
+ ports.log(`agent-shim headroom supervisor: stopping daemon pid ${String(orphanPid)} left behind by the previous supervisor`);
5746
+ ports.stopProcess(orphanPid);
5747
+ }
5748
+ let headroomPid;
5749
+ let runningHash;
5750
+ let runningSettingsHash;
5751
+ let consecutiveFailures = 0;
5752
+ let idleSince;
5753
+ let superseded = false;
5754
+ let ticks = 0;
5755
+ for (; ; ) {
5756
+ if (options.tickLimit !== void 0 && ticks >= options.tickLimit) {
5757
+ return HEADROOM_SUPERVISOR_STILL_RUNNING;
5758
+ }
5759
+ ticks += 1;
5760
+ config = ports.readConfig();
5761
+ const settingsHash = hashSettings(config.settings);
5762
+ const allowlist = allowlistOf(ports);
5763
+ const allowlistHash = hashAllowlist(allowlist);
5764
+ pruneDeadSessions(fs12, paths.headroomSessionsDir, ports.isRunning);
5765
+ const ownsNow = ownsState();
5766
+ if (!ownsNow && !superseded) {
5767
+ ports.log("agent-shim headroom supervisor: another supervisor now owns the state file; serving this daemon's own sessions until they end");
5768
+ }
5769
+ superseded = !ownsNow;
5770
+ const crashed = headroomPid !== void 0 && !ports.isRunning(headroomPid);
5771
+ if (headroomPid === void 0 || crashed) {
5772
+ if (crashed) {
5773
+ ports.log(`agent-shim headroom supervisor: headroom pid ${String(headroomPid)} died`);
5774
+ headroomPid = void 0;
5775
+ runningHash = void 0;
5776
+ runningSettingsHash = void 0;
5777
+ writeOwnState({
5778
+ supervisorPid: ports.ownPid,
5779
+ version,
5780
+ installedSource
5781
+ });
5782
+ }
5783
+ if (consecutiveFailures >= HEADROOM_START_RETRY_BUDGET) {
5784
+ return fail(`headroom failed to become ready ${String(consecutiveFailures)} times in a row; giving up`);
5785
+ }
5786
+ if (installedSource !== config.source && !ensureInstalled()) {
5787
+ return fail(`could not install headroom from ${config.source}: install failed (is uv installed and on PATH?)`);
5788
+ }
5789
+ const pid = ports.spawnHeadroom(socketPath, allowlist, config.settings);
5790
+ const ready = await waitUntilReady(ports, socketPath);
5791
+ if (ready) {
5792
+ consecutiveFailures = 0;
5793
+ headroomPid = pid;
5794
+ runningHash = allowlistHash;
5795
+ runningSettingsHash = settingsHash;
5796
+ const recorded = writeOwnState({
5797
+ supervisorPid: ports.ownPid,
5798
+ headroomPid: pid,
5799
+ socketPath,
5800
+ version,
5801
+ allowlistHash,
5802
+ settingsHash,
5803
+ installedSource: config.source
5804
+ });
5805
+ if (recorded) {
5806
+ fs12.removeRecursive(paths.headroomLockFile);
5807
+ }
5808
+ idleSince = void 0;
5809
+ ports.log(`agent-shim headroom supervisor: headroom pid ${String(pid)} ready on unix socket ${socketPath}`);
5810
+ } else {
5811
+ ports.stopProcess(pid);
5812
+ consecutiveFailures += 1;
5813
+ ports.log(
5814
+ `agent-shim headroom supervisor: headroom did not become ready on unix socket ${socketPath} (attempt ${String(consecutiveFailures)} of ${String(HEADROOM_START_RETRY_BUDGET)})`
5815
+ );
5816
+ await ports.sleep(backoffForAttempt(consecutiveFailures));
5817
+ continue;
5818
+ }
5819
+ } else {
5820
+ if (runningHash !== allowlistHash || runningSettingsHash !== settingsHash || installedSource !== config.source) {
5821
+ if (sessionsForSupervisor(fs12, paths.headroomSessionsDir, ports.ownPid).length === 0) {
5822
+ ports.log("agent-shim headroom supervisor: configuration drifted and no sessions are live; restarting headroom");
5823
+ ports.stopProcess(headroomPid);
5824
+ headroomPid = void 0;
5825
+ runningHash = void 0;
5826
+ runningSettingsHash = void 0;
5827
+ continue;
5828
+ }
5829
+ }
5830
+ const sessions = sessionsForSupervisor(fs12, paths.headroomSessionsDir, ports.ownPid);
5831
+ if (sessions.length === 0) {
5832
+ idleSince ??= ports.now();
5833
+ const idleMs = ports.now() - idleSince;
5834
+ if (idleMs >= config.idleShutdownMinutes * MS_PER_MINUTE2) {
5835
+ ports.log(
5836
+ `agent-shim headroom supervisor: no sessions for ${String(config.idleShutdownMinutes)} minute(s); stopping headroom and exiting`
5837
+ );
5838
+ ports.stopProcess(headroomPid);
5839
+ writeOwnState({ version });
5840
+ return 0;
5841
+ }
5842
+ } else {
5843
+ idleSince = void 0;
5844
+ }
5845
+ }
5846
+ await ports.sleep(HEADROOM_POLL_MS);
5910
5847
  }
5911
- return limit === void 0 || now - entry.fetchedAt < limit;
5912
5848
  }
5913
- function cacheAgeMs(entry, now) {
5914
- return Math.max(0, now - entry.fetchedAt);
5849
+ function currentHeadroomAllowlist(fs12, paths) {
5850
+ const providers = readAllProviders(fs12, paths.providersDir).map((entry) => entry.provider);
5851
+ return headroomAllowlist(headroomUpstreams(providers, frontDoorOrigin(readFrontDoorState(fs12, paths.frontdoorStateFile))));
5915
5852
  }
5916
- function describeCachedCredential(params) {
5917
- const store = effectiveStore(params.cache, params.platform);
5918
- let entry;
5919
- try {
5920
- entry = params.port.read(store, params.owner);
5921
- } catch (error) {
5922
- return { store, status: "unreadable", reason: error instanceof Error ? error.message : String(error) };
5923
- }
5924
- if (entry === void 0) {
5925
- return { store, status: "empty" };
5926
- }
5927
- const limit = ttlMs(params.cache);
5928
- const ageMs = cacheAgeMs(entry, params.nowMs);
5929
- return {
5930
- store,
5931
- status: isFresh(entry, params.cache, params.nowMs) ? "fresh" : "expired",
5932
- ageMs,
5933
- ...limit === void 0 ? {} : { expiresInMs: limit - ageMs }
5934
- };
5853
+ function allowlistOf(ports) {
5854
+ return currentHeadroomAllowlist(ports.fs, ports.paths);
5935
5855
  }
5936
- function formatCachedCredentialState(state) {
5937
- if (state.status === "empty") {
5938
- return `no cached entry in the ${state.store} store yet`;
5939
- }
5940
- if (state.status === "unreadable") {
5941
- return `the ${state.store} store cannot be read here: ${state.reason}`;
5942
- }
5943
- if (state.status === "expired" && (state.expiresInMs === void 0 || state.expiresInMs > 0)) {
5944
- return "cached with a timestamp in the future, treated as expired";
5945
- }
5946
- const age = `cached ${formatAge(state.ageMs)} ago`;
5947
- if (state.expiresInMs === void 0) {
5948
- return `${age}, no expiry`;
5856
+ async function waitUntilReady(ports, socketPath) {
5857
+ const deadline = ports.now() + HEADROOM_READY_TIMEOUT_MS;
5858
+ for (; ; ) {
5859
+ if (await ports.ready(socketPath)) {
5860
+ return true;
5861
+ }
5862
+ if (ports.now() >= deadline) {
5863
+ return false;
5864
+ }
5865
+ await ports.sleep(HEADROOM_POLL_MS);
5949
5866
  }
5950
- return state.status === "fresh" ? `${age}, expires in ${formatAge(state.expiresInMs)}` : `${age}, expired ${formatAge(-state.expiresInMs)} ago`;
5951
5867
  }
5952
5868
 
5953
- // src/credential.ts
5954
- var CREDENTIAL_UNAVAILABLE_EXIT = 64;
5955
- var CREDENTIAL_COMMAND_TIMEOUT_MS = 1e4;
5956
- var CREDENTIAL_INTERACTIVE_TIMEOUT_MS = 12e4;
5957
- var GROUP_OTHER_PERMISSION_BITS = 63;
5958
- var PERMISSION_BITS = 511;
5959
- var OCTAL_RADIX2 = 8;
5960
- function commandArgv(source) {
5961
- if ("command" in source) {
5962
- return source.command;
5963
- }
5964
- if ("op" in source) {
5965
- return ["op", "read", source.op];
5869
+ // src/launcher/guard.ts
5870
+ var AMBIENT_CREDENTIAL_VARS = [
5871
+ "ANTHROPIC_API_KEY",
5872
+ "ANTHROPIC_AUTH_TOKEN",
5873
+ "CLAUDE_CODE_OAUTH_TOKEN",
5874
+ "CLAUDE_CODE_USE_BEDROCK",
5875
+ "CLAUDE_CODE_USE_VERTEX",
5876
+ "CLAUDE_CODE_USE_FOUNDRY"
5877
+ ];
5878
+ function detectAmbientCredential(env, injected) {
5879
+ for (const variable of AMBIENT_CREDENTIAL_VARS) {
5880
+ const value = env[variable];
5881
+ if (value === void 0 || value === "") {
5882
+ continue;
5883
+ }
5884
+ if (injected?.variable === variable && value === injected.token) {
5885
+ continue;
5886
+ }
5887
+ return { variable };
5966
5888
  }
5967
- const account = source.keychain.account;
5968
- return ["security", "find-generic-password", "-s", source.keychain.service, ...account === void 0 ? [] : ["-a", account], "-w"];
5889
+ return void 0;
5969
5890
  }
5970
- function isInteractiveSource(source, env) {
5971
- if ("command" in source || "keychain" in source) {
5972
- return source.interactive ?? false;
5973
- }
5974
- if ("op" in source) {
5975
- return source.interactive ?? (env.OP_SERVICE_ACCOUNT_TOKEN ?? "") === "";
5976
- }
5977
- return false;
5891
+ function formatAmbientCredentialGuardMessage(variable, identityName) {
5892
+ const identitySetCommand = identityName === void 0 ? "agent-shim identity set <name> --allow-ambient-credential" : `agent-shim identity set ${identityName} --allow-ambient-credential`;
5893
+ return [
5894
+ `error: ${variable} is set in the environment. This identity's isolated`,
5895
+ "credential would be bypassed \u2014 every identity authenticates as this same key",
5896
+ "while it's set. Unset it, or if this is deliberate, opt in per-launch with",
5897
+ "AGENT_SHIM_ALLOW_AMBIENT_CREDENTIAL=1, or persistently for this identity with",
5898
+ `\`${identitySetCommand}\`.`
5899
+ ].join("\n");
5978
5900
  }
5979
- function summariseSource(source) {
5980
- if ("env" in source) {
5981
- return { kind: "env", variable: source.env };
5982
- }
5983
- if ("file" in source) {
5984
- return { kind: "file", path: source.file };
5985
- }
5986
- if ("command" in source) {
5987
- return { kind: "command", program: source.command[0] };
5988
- }
5989
- if ("op" in source) {
5990
- return { kind: "op", reference: source.op };
5901
+ function evaluateAmbientCredentialGuard(params) {
5902
+ const detected = detectAmbientCredential(params.env, params.injectedCredential);
5903
+ if (detected === void 0) {
5904
+ return { ok: true };
5991
5905
  }
5992
- if ("keychain" in source) {
5993
- return {
5994
- kind: "keychain",
5995
- service: source.keychain.service,
5996
- ...source.keychain.account === void 0 ? {} : { account: source.keychain.account }
5997
- };
5906
+ if (params.allowAmbientCredentialOverride || params.allowAmbientCredential || params.providerSelected === true) {
5907
+ return { ok: true };
5998
5908
  }
5999
- return { kind: "literal" };
6000
- }
6001
- function summariseCredential(credential) {
6002
5909
  return {
6003
- target: credential.target ?? "bearer",
6004
- sources: credential.sources.map(summariseSource),
6005
- ...credential.cache === void 0 ? {} : { cache: credential.cache }
5910
+ ok: false,
5911
+ variable: detected.variable,
5912
+ message: formatAmbientCredentialGuardMessage(detected.variable, params.identityName)
6006
5913
  };
6007
5914
  }
6008
- function formatSourceSummary(summary) {
6009
- switch (summary.kind) {
6010
- case "env":
6011
- return `env ${summary.variable}`;
6012
- case "file":
6013
- return `file ${summary.path}`;
6014
- case "command":
6015
- return `command ${summary.program}`;
6016
- case "op":
6017
- return `op ${summary.reference}`;
6018
- case "keychain":
6019
- return `keychain ${summary.service}${summary.account === void 0 ? "" : ` (account ${summary.account})`}`;
6020
- case "literal":
6021
- return "literal (non-secret placeholder)";
6022
- default:
6023
- return summary;
5915
+
5916
+ // src/paths.ts
5917
+ import fs3 from "node:fs";
5918
+ import os from "node:os";
5919
+ import path14 from "node:path";
5920
+ function resolveAgentShimHome(env = process.env, home = os.homedir(), isDirectory = isExistingDirectory) {
5921
+ const fromEnv = env.AGENT_SHIM_HOME;
5922
+ if (fromEnv !== void 0 && fromEnv !== "") {
5923
+ return fromEnv;
6024
5924
  }
5925
+ const current = path14.join(home, HOME_DIRNAME);
5926
+ const legacy = path14.join(home, LEGACY_HOME_DIRNAME);
5927
+ return !isDirectory(current) && isDirectory(legacy) ? legacy : current;
6025
5928
  }
6026
- function describeSource(source) {
6027
- return formatSourceSummary(summariseSource(source));
5929
+ function isExistingDirectory(candidate) {
5930
+ return fs3.statSync(candidate, { throwIfNoEntry: false })?.isDirectory() ?? false;
6028
5931
  }
6029
- function formatCredentialSummary(summary) {
6030
- const cache = summary.cache === void 0 ? "" : `, cached ${summary.cache.ttl === void 0 ? "with no expiry" : `for ${summary.cache.ttl}`}${summary.cache.store === void 0 ? "" : ` in the ${summary.cache.store} store`}`;
6031
- return `${summary.target} from ${summary.sources.map(formatSourceSummary).join(", then ")}${cache}`;
5932
+ function resolveClaudeHome() {
5933
+ const fromEnv = process.env.AGENT_SHIM_CLAUDE_HOME;
5934
+ if (fromEnv !== void 0 && fromEnv !== "") {
5935
+ return fromEnv;
5936
+ }
5937
+ return path14.join(os.homedir(), ".claude");
6032
5938
  }
6033
- function describeCredential(credential) {
6034
- return formatCredentialSummary(summariseCredential(credential));
5939
+ function buildLayoutPaths(root) {
5940
+ return {
5941
+ root,
5942
+ identitiesDir: path14.join(root, "identities"),
5943
+ configProfilesDir: path14.join(root, "config-profiles"),
5944
+ providersDir: path14.join(root, "providers"),
5945
+ directoryRulesFile: path14.join(root, "directory-rules.json"),
5946
+ activeIdentityFile: path14.join(root, "active-identity"),
5947
+ globalConfigFile: path14.join(root, "config.json"),
5948
+ categoriesLocalFile: path14.join(root, "categories.local.json"),
5949
+ claudeShimFile: path14.join(root, "claude-shim.json"),
5950
+ headroomDir: path14.join(root, "headroom"),
5951
+ headroomStateFile: path14.join(root, "headroom", `state.v${String(HEADROOM_STATE_SCHEMA_VERSION)}.json`),
5952
+ headroomSocketDir: path14.join(root, "headroom", "run"),
5953
+ headroomLockFile: path14.join(root, "headroom", "start.lock"),
5954
+ headroomSessionsDir: path14.join(root, "headroom", "sessions"),
5955
+ logsDir: path14.join(root, "logs"),
5956
+ headroomLogPath: path14.join(root, "logs", "headroom.log"),
5957
+ frontdoorDir: path14.join(root, "frontdoor"),
5958
+ frontdoorCaDir: path14.join(root, "frontdoor", "ca"),
5959
+ frontdoorCaCertFile: path14.join(root, "frontdoor", "ca", "ca.pem"),
5960
+ frontdoorCaKeyFile: path14.join(root, "frontdoor", "ca", "ca.key"),
5961
+ frontdoorCaBundlesDir: path14.join(root, "frontdoor", "ca", "bundles"),
5962
+ frontdoorStateFile: path14.join(root, "frontdoor", "state.json"),
5963
+ frontdoorLockFile: path14.join(root, "frontdoor", "start.lock"),
5964
+ frontdoorControlTokenFile: path14.join(root, "frontdoor", "control-token"),
5965
+ frontdoorSessionsDir: path14.join(root, "frontdoor", "sessions"),
5966
+ frontdoorLogPath: path14.join(root, "logs", "frontdoor.log"),
5967
+ usageDir: path14.join(root, "usage"),
5968
+ usageLogDir: path14.join(root, "usage", "log"),
5969
+ usageSnapshotsDir: path14.join(root, "usage", "snapshots"),
5970
+ usagePicksFile: path14.join(root, "usage", "picks.json")
5971
+ };
6035
5972
  }
6036
- function trySource(source, params) {
6037
- const none = (reason, warn = false) => ({ ok: false, reason, warn });
6038
- if ("env" in source) {
6039
- const value = params.env[source.env];
6040
- return value === void 0 || value === "" ? none("is unset or empty") : { ok: true, token: value };
6041
- }
6042
- if ("literal" in source) {
6043
- return { ok: true, token: source.literal };
6044
- }
6045
- if ("file" in source) {
6046
- const read = params.port.readSecretFile(source.file);
6047
- if (!read.found) {
6048
- return none("does not exist");
6049
- }
6050
- if (read.mode !== void 0 && (read.mode & GROUP_OTHER_PERMISSION_BITS) !== 0) {
6051
- return none(
6052
- `is readable or writable by group or others (mode ${(read.mode & PERMISSION_BITS).toString(OCTAL_RADIX2)}); run \`chmod 600 ${source.file}\``,
6053
- true
6054
- );
6055
- }
6056
- const token2 = read.content.trim();
6057
- return token2 === "" ? none("is empty") : { ok: true, token: token2 };
5973
+ function resolveLayoutPaths() {
5974
+ return buildLayoutPaths(resolveAgentShimHome());
5975
+ }
5976
+
5977
+ // src/identityStore.ts
5978
+ import fs5 from "node:fs";
5979
+ import path16 from "node:path";
5980
+
5981
+ // src/configProfilesStore.ts
5982
+ import fs4 from "node:fs";
5983
+ import path15 from "node:path";
5984
+ var ProfileNotFoundError = class extends CliError {
5985
+ constructor(profileName) {
5986
+ super(`No configuration profile named "${profileName}". Run \`agent-shim profile add ${profileName}\` first.`);
5987
+ this.profileName = profileName;
5988
+ this.name = "ProfileNotFoundError";
6058
5989
  }
6059
- const argv = commandArgv(source);
6060
- const interactive = isInteractiveSource(source, params.env);
6061
- if (interactive && !params.port.personPresent()) {
6062
- return none("needs a person to approve it, but there is no terminal or desktop session");
5990
+ profileName;
5991
+ };
5992
+ var ProfileAlreadyExistsError = class extends CliError {
5993
+ constructor(profileName) {
5994
+ super(`A configuration profile named "${profileName}" already exists.`);
5995
+ this.profileName = profileName;
5996
+ this.name = "ProfileAlreadyExistsError";
6063
5997
  }
6064
- const timeoutMs = source.timeoutMs ?? (interactive ? CREDENTIAL_INTERACTIVE_TIMEOUT_MS : CREDENTIAL_COMMAND_TIMEOUT_MS);
6065
- const result = params.port.runCommand(argv, { timeoutMs, interactive });
6066
- if (result.timedOut) {
6067
- return none(`timed out after ${String(timeoutMs)}ms`);
5998
+ profileName;
5999
+ };
6000
+ var InvalidCategoryNameError = class extends CliError {
6001
+ constructor(categoryName) {
6002
+ super(`"${categoryName}" is not a category a configuration profile may toggle (runtime, history, knowledge, settings).`);
6003
+ this.categoryName = categoryName;
6004
+ this.name = "InvalidCategoryNameError";
6068
6005
  }
6069
- if (result.status !== 0) {
6070
- const stderr = result.stderr.trim();
6071
- const outcome = result.status === null ? "could not be run or was killed by a signal" : `exited with status ${String(result.status)}`;
6072
- return none(`${outcome}${stderr === "" ? "" : `: ${stderr}`}`);
6006
+ categoryName;
6007
+ };
6008
+ function profileJsonPath(paths, name) {
6009
+ return path15.join(paths.configProfilesDir, `${name}.json`);
6010
+ }
6011
+ function profileExists(paths, name) {
6012
+ return fs4.existsSync(profileJsonPath(paths, name));
6013
+ }
6014
+ function requireProfileExists(paths, name) {
6015
+ if (!profileExists(paths, name)) {
6016
+ throw new ProfileNotFoundError(name);
6073
6017
  }
6074
- const token = result.stdout.trim();
6075
- return token === "" ? none("printed no token") : { ok: true, token };
6076
6018
  }
6077
- function warmCommandFor(subject) {
6078
- const [kind, name] = subject.split(" ");
6079
- return kind === "provider" ? `agent-shim credential warm --provider ${name ?? ""}` : `agent-shim credential warm ${name ?? ""}`;
6019
+ function readProfile(paths, name) {
6020
+ return readJson(profileJsonPath(paths, name), ConfigProfileSchema);
6080
6021
  }
6081
- function resolveCredential(params) {
6082
- const target = params.credential.target ?? "bearer";
6083
- const cacheBlock = params.credential.cache;
6084
- const cache = cacheBlock === void 0 ? void 0 : params.port.cache;
6085
- const store = cacheBlock === void 0 || cache === void 0 ? void 0 : effectiveStore(cacheBlock, cache.platform);
6086
- if (cacheBlock !== void 0 && cache !== void 0 && store !== void 0 && params.refresh !== true) {
6087
- const entry = cache.port.read(store, params.subject);
6088
- if (entry !== void 0 && isFresh(entry, cacheBlock, cache.now())) {
6089
- return { ok: true, credential: { target, token: entry.token, source: entry.source, warnings: [], cachedAt: entry.fetchedAt } };
6090
- }
6022
+ function createProfile(paths, name, extendsList, description) {
6023
+ if (profileExists(paths, name)) {
6024
+ throw new ProfileAlreadyExistsError(name);
6091
6025
  }
6092
- const attempts = [];
6093
- const warnings = [];
6094
- for (const source of params.credential.sources) {
6095
- const outcome = trySource(source, params);
6096
- if (outcome.ok) {
6097
- if (cache !== void 0 && store !== void 0) {
6098
- cache.port.write(store, params.subject, { token: outcome.token, fetchedAt: cache.now(), source });
6099
- }
6100
- return { ok: true, credential: { target, token: outcome.token, source, warnings } };
6026
+ const filePath = profileJsonPath(paths, name);
6027
+ const parsed = ConfigProfileSchema.safeParse({
6028
+ ...description === void 0 ? {} : { description },
6029
+ ...extendsList !== void 0 && extendsList.length > 0 ? { extends: [...extendsList] } : {}
6030
+ });
6031
+ if (!parsed.success) {
6032
+ throw new ConfigValidationError(filePath, parsed.error.issues);
6033
+ }
6034
+ writeJsonAtomic(filePath, parsed.data);
6035
+ return parsed.data;
6036
+ }
6037
+ function listProfiles(paths) {
6038
+ if (!fs4.existsSync(paths.configProfilesDir)) {
6039
+ return [];
6040
+ }
6041
+ const names = fs4.readdirSync(paths.configProfilesDir, { withFileTypes: true }).filter((entry) => entry.isFile() && entry.name.endsWith(".json")).map((entry) => entry.name.slice(0, -".json".length)).sort();
6042
+ const result = [];
6043
+ for (const name of names) {
6044
+ const profile = readProfile(paths, name);
6045
+ if (profile !== void 0) {
6046
+ result.push({ name, profile });
6101
6047
  }
6102
- const line = `${describeSource(source)} ${outcome.reason}`;
6103
- attempts.push(line);
6104
- if (outcome.warn) {
6105
- warnings.push(`agent-shim: ${params.subject}: skipped ${line}`);
6048
+ }
6049
+ return result;
6050
+ }
6051
+ function globalConfigPath(paths) {
6052
+ return paths.globalConfigFile;
6053
+ }
6054
+ function readGlobalConfig(paths) {
6055
+ return readJson(globalConfigPath(paths), GlobalConfigSchema);
6056
+ }
6057
+ function setGlobalDefaultProfile(paths, name) {
6058
+ return applyPatch(
6059
+ globalConfigPath(paths),
6060
+ GlobalConfigSchema,
6061
+ { defaultConfigProfile: name },
6062
+ { defaults: {} }
6063
+ );
6064
+ }
6065
+ function validateCategoryNames(patch) {
6066
+ for (const key of Object.keys(patch)) {
6067
+ if (!isOverridableCategory(key)) {
6068
+ throw new InvalidCategoryNameError(key);
6106
6069
  }
6107
6070
  }
6108
- const hint = cacheBlock === void 0 ? "" : `; fill the cache from a terminal with \`${warmCommandFor(params.subject)}\``;
6109
- return { ok: false, message: `agent-shim: ${params.subject} has no usable credential: ${attempts.join("; ")}${hint}` };
6110
6071
  }
6111
- function credentialVariables(target, token) {
6112
- return {
6113
- ANTHROPIC_AUTH_TOKEN: target === "bearer" ? token : void 0,
6114
- ANTHROPIC_API_KEY: target === "apiKey" ? token : void 0,
6115
- CLAUDE_CODE_OAUTH_TOKEN: target === "oauthToken" ? token : void 0
6116
- };
6072
+ function setProfileCategories(paths, name, patch) {
6073
+ requireProfileExists(paths, name);
6074
+ const expandedPatch = expandAllCategoryKey(patch);
6075
+ validateCategoryNames(expandedPatch);
6076
+ const existing = readProfile(paths, name) ?? {};
6077
+ const mergedCategories = { ...existing.categories, ...expandedPatch };
6078
+ return applyPatch(profileJsonPath(paths, name), ConfigProfileSchema, { categories: mergedCategories });
6079
+ }
6080
+ function setProfileEntries(paths, name, patch) {
6081
+ requireProfileExists(paths, name);
6082
+ const existing = readProfile(paths, name) ?? {};
6083
+ const mergedEntries = { ...existing.entries, ...patch };
6084
+ return applyPatch(profileJsonPath(paths, name), ConfigProfileSchema, { entries: mergedEntries });
6085
+ }
6086
+ function setProfileLaunchFlags(paths, name, patch) {
6087
+ requireProfileExists(paths, name);
6088
+ const existing = readProfile(paths, name) ?? {};
6089
+ const mergedLaunch = { ...existing.launch, ...patch };
6090
+ return applyPatch(profileJsonPath(paths, name), ConfigProfileSchema, {
6091
+ // Read as `unknown` so the check sees the `undefined` a cleared key holds, which LaunchFlags' optional properties hide from the value type.
6092
+ launch: Object.values(mergedLaunch).every((value) => value === void 0) ? void 0 : mergedLaunch
6093
+ });
6094
+ }
6095
+ function setProfileMetadata(paths, name, patch) {
6096
+ requireProfileExists(paths, name);
6097
+ return applyPatch(profileJsonPath(paths, name), ConfigProfileSchema, {
6098
+ ...patch.extends === void 0 ? {} : { extends: patch.extends.length === 0 ? void 0 : [...patch.extends] },
6099
+ ...patch.description === void 0 ? {} : { description: patch.description === false ? void 0 : patch.description }
6100
+ });
6101
+ }
6102
+ function removeProfile(paths, name) {
6103
+ requireProfileExists(paths, name);
6104
+ fs4.rmSync(profileJsonPath(paths, name));
6117
6105
  }
6118
6106
 
6119
- // src/providersStore.ts
6120
- var ProviderNotFoundError = class extends CliError {
6121
- constructor(name) {
6122
- super(`No provider named "${name}". Run \`agent-shim provider add ${name}\` first.`);
6123
- this.name = name;
6124
- this.name = "ProviderNotFoundError";
6107
+ // src/poolStore.ts
6108
+ var PoolNotFoundError = class extends CliError {
6109
+ constructor(poolName) {
6110
+ super(`No pool named "${poolName}". Run \`agent-shim pool add ${poolName} --identity <name>...\` first.`);
6111
+ this.poolName = poolName;
6112
+ this.name = "PoolNotFoundError";
6125
6113
  }
6126
- name;
6114
+ poolName;
6127
6115
  };
6128
- var ProviderAlreadyExistsError = class extends CliError {
6129
- constructor(providerName) {
6130
- super(`A provider named "${providerName}" already exists.`);
6131
- this.providerName = providerName;
6132
- this.name = "ProviderAlreadyExistsError";
6116
+ var PoolAlreadyExistsError = class extends CliError {
6117
+ constructor(poolName) {
6118
+ super(`A pool named "${poolName}" already exists. Use \`agent-shim pool set ${poolName}\` to change its members.`);
6119
+ this.poolName = poolName;
6120
+ this.name = "PoolAlreadyExistsError";
6133
6121
  }
6134
- providerName;
6122
+ poolName;
6135
6123
  };
6136
- var InvalidProviderNameError = class extends CliError {
6124
+ var InvalidPoolNameError = class extends CliError {
6137
6125
  constructor(attemptedName) {
6138
- super(
6139
- `"${attemptedName}" is not a valid provider name. Provider names must start with a letter or number and may then contain letters, numbers, dots, hyphens, underscores, and at signs.`
6140
- );
6126
+ super(`"${attemptedName}" is not a valid pool name: it must start with a letter or number and may then contain letters, numbers, dots, hyphens and underscores.`);
6141
6127
  this.attemptedName = attemptedName;
6142
- this.name = "InvalidProviderNameError";
6128
+ this.name = "InvalidPoolNameError";
6143
6129
  }
6144
6130
  attemptedName;
6145
6131
  };
6146
- var PROVIDER_NAME_RE = /^[A-Za-z0-9][A-Za-z0-9._@-]*$/;
6147
- function providerJsonPath(paths, name) {
6148
- return path16.join(paths.providersDir, `${name}.json`);
6132
+ function readPools(paths) {
6133
+ return readGlobalConfig(paths)?.pools ?? {};
6149
6134
  }
6150
- function providerExists(paths, name) {
6151
- return fs6.existsSync(providerJsonPath(paths, name));
6135
+ function requirePool(paths, name) {
6136
+ const pool = readPools(paths)[name];
6137
+ if (pool === void 0) {
6138
+ throw new PoolNotFoundError(name);
6139
+ }
6140
+ return pool;
6152
6141
  }
6153
- var LEGACY_PROVIDER_FIELDS = ["tokenEnv", "tokenCommand", "authScheme"];
6154
- var LEGACY_ENV_CREDENTIAL_KEYS = Object.values(CREDENTIAL_TARGET_VARS);
6155
- var LEGACY_LITERAL_PLACEHOLDER = "<the value of env.ANTHROPIC_AUTH_TOKEN>";
6156
- function isRecord3(value) {
6157
- return typeof value === "object" && value !== null && !Array.isArray(value);
6142
+ function writePools(paths, pools) {
6143
+ applyPatch(paths.globalConfigFile, GlobalConfigSchema, { pools }, { defaults: {} });
6158
6144
  }
6159
- function legacyProviderConversion(raw) {
6160
- if (!isRecord3(raw) || "credential" in raw) {
6161
- return void 0;
6162
- }
6163
- const env = isRecord3(raw.env) ? raw.env : void 0;
6164
- const legacyEnvKeys = env === void 0 ? [] : Object.keys(env).filter((key) => LEGACY_ENV_CREDENTIAL_KEYS.includes(key));
6165
- const fields = [...LEGACY_PROVIDER_FIELDS.filter((field) => field in raw), ...legacyEnvKeys.map((key) => `env.${key}`)];
6166
- if (fields.length === 0) {
6167
- return void 0;
6145
+ function addPool(paths, name, identities) {
6146
+ if (!PoolNameSchema.safeParse(name).success) {
6147
+ throw new InvalidPoolNameError(name);
6168
6148
  }
6169
- const sources = [];
6170
- if (typeof raw.tokenEnv === "string") {
6171
- sources.push({ env: raw.tokenEnv });
6149
+ const pools = readPools(paths);
6150
+ if (pools[name] !== void 0) {
6151
+ throw new PoolAlreadyExistsError(name);
6172
6152
  }
6173
- if (Array.isArray(raw.tokenCommand)) {
6174
- sources.push({ command: raw.tokenCommand });
6153
+ const pool = { identities: [...identities] };
6154
+ writePools(paths, { ...pools, [name]: pool });
6155
+ return pool;
6156
+ }
6157
+ function setPool(paths, name, identities) {
6158
+ requirePool(paths, name);
6159
+ const pool = { identities: [...identities] };
6160
+ writePools(paths, { ...readPools(paths), [name]: pool });
6161
+ return pool;
6162
+ }
6163
+ function removePool(paths, name) {
6164
+ requirePool(paths, name);
6165
+ const remaining = Object.fromEntries(Object.entries(readPools(paths)).filter(([existing]) => existing !== name));
6166
+ writePools(paths, remaining);
6167
+ }
6168
+
6169
+ // src/identityStore.ts
6170
+ var IdentityNotFoundError = class extends CliError {
6171
+ constructor(name) {
6172
+ super(`No identity named "${name}". Run \`agent-shim identity add ${name}\` first.`);
6173
+ this.name = name;
6174
+ this.name = "IdentityNotFoundError";
6175
6175
  }
6176
- if (typeof env?.ANTHROPIC_AUTH_TOKEN === "string" && env.ANTHROPIC_AUTH_TOKEN !== "") {
6177
- sources.push({ literal: LEGACY_LITERAL_PLACEHOLDER });
6176
+ name;
6177
+ };
6178
+ var IdentityAlreadyExistsError = class extends CliError {
6179
+ constructor(identityName) {
6180
+ super(`An identity named "${identityName}" already exists.`);
6181
+ this.identityName = identityName;
6182
+ this.name = "IdentityAlreadyExistsError";
6178
6183
  }
6179
- const remainingEnv = env === void 0 ? void 0 : Object.fromEntries(Object.entries(env).filter(([key]) => !LEGACY_ENV_CREDENTIAL_KEYS.includes(key)));
6180
- const replacedKeys = [...LEGACY_PROVIDER_FIELDS, "env"];
6181
- const rest = Object.fromEntries(Object.entries(raw).filter(([key]) => !replacedKeys.includes(key)));
6182
- return {
6183
- fields,
6184
- replacement: {
6185
- ...rest,
6186
- credential: { sources, ...raw.authScheme === "apiKey" ? { target: "apiKey" } : {} },
6187
- ...remainingEnv === void 0 || Object.keys(remainingEnv).length === 0 ? {} : { env: remainingEnv }
6188
- }
6189
- };
6190
- }
6191
- var LegacyProviderFileError = class extends CliError {
6192
- constructor(filePath, conversion) {
6184
+ identityName;
6185
+ };
6186
+ var InvalidIdentityNameError = class extends CliError {
6187
+ constructor(attemptedName) {
6193
6188
  super(
6194
- `${filePath} uses ${conversion.fields.join(", ")}, which a credential block replaced. Rewrite it as:
6195
- ` + JSON.stringify(conversion.replacement, null, 2)
6189
+ `"${attemptedName}" is not a valid identity name \u2014 identity names must start with a letter or number and may then contain letters, numbers, dots, hyphens, underscores, and at signs.`
6196
6190
  );
6197
- this.filePath = filePath;
6198
- this.conversion = conversion;
6199
- this.name = "LegacyProviderFileError";
6200
- }
6201
- filePath;
6202
- conversion;
6203
- };
6204
- function parseProviderFile(filePath, raw) {
6205
- const conversion = legacyProviderConversion(raw);
6206
- if (conversion !== void 0) {
6207
- throw new LegacyProviderFileError(filePath, conversion);
6191
+ this.attemptedName = attemptedName;
6192
+ this.name = "InvalidIdentityNameError";
6208
6193
  }
6209
- const parsed = ProviderSchema.safeParse(raw);
6194
+ attemptedName;
6195
+ };
6196
+ function identityJsonPath(paths, name) {
6197
+ return path16.join(paths.identitiesDir, name, "identity.json");
6198
+ }
6199
+ function identityExists(paths, name) {
6200
+ return fs5.existsSync(identityJsonPath(paths, name));
6201
+ }
6202
+ function readIdentity(paths, name) {
6203
+ return readJson(identityJsonPath(paths, name), IdentitySchema);
6204
+ }
6205
+ function addIdentity(paths, name) {
6206
+ if (identityExists(paths, name)) {
6207
+ throw new IdentityAlreadyExistsError(name);
6208
+ }
6209
+ const parsed = IdentitySchema.safeParse({ name, allowAmbientCredential: false });
6210
6210
  if (!parsed.success) {
6211
- throw new ConfigValidationError(filePath, parsed.error.issues);
6211
+ throw new InvalidIdentityNameError(name);
6212
6212
  }
6213
+ writeJsonAtomic(identityJsonPath(paths, name), parsed.data);
6213
6214
  return parsed.data;
6214
6215
  }
6215
- function readProvider(paths, name) {
6216
- const filePath = providerJsonPath(paths, name);
6217
- if (!fs6.existsSync(filePath)) {
6216
+ function useIdentity(paths, name) {
6217
+ const pool = poolNameOf(name);
6218
+ if (pool !== void 0) {
6219
+ requirePool(paths, pool);
6220
+ } else if (!identityExists(paths, name)) {
6221
+ throw new IdentityNotFoundError(name);
6222
+ }
6223
+ writeTextAtomic(paths.activeIdentityFile, `${name}
6224
+ `);
6225
+ }
6226
+ function readActiveIdentity(paths) {
6227
+ if (!fs5.existsSync(paths.activeIdentityFile)) {
6218
6228
  return void 0;
6219
6229
  }
6220
- const raw = JSON.parse(fs6.readFileSync(filePath, "utf8"));
6221
- return parseProviderFile(filePath, raw);
6230
+ const raw = fs5.readFileSync(paths.activeIdentityFile, "utf8").trim();
6231
+ return raw === "" ? void 0 : raw;
6222
6232
  }
6223
- function loadProvider(providersDir, name, port) {
6224
- const filePath = path16.join(providersDir, `${name}.json`);
6225
- const raw = port.readConfigFile(filePath);
6226
- return raw === void 0 ? void 0 : parseProviderFile(filePath, raw);
6233
+ function isIdentityDirectoryName(name) {
6234
+ return !name.startsWith(".");
6227
6235
  }
6228
- function listProviders(paths) {
6229
- const names = listProviderNames(
6230
- (dir) => fs6.readdirSync(dir),
6231
- paths.providersDir
6232
- );
6236
+ function readIdentityForListing(paths, name) {
6237
+ try {
6238
+ return readIdentity(paths, name);
6239
+ } catch (error) {
6240
+ if (error instanceof ConfigValidationError || error instanceof SyntaxError) {
6241
+ return { problem: error.message.replace(/\s*\n\s*/g, " ") };
6242
+ }
6243
+ throw error;
6244
+ }
6245
+ }
6246
+ function listIdentities(paths) {
6247
+ if (!fs5.existsSync(paths.identitiesDir)) {
6248
+ return [];
6249
+ }
6250
+ const active = readActiveIdentity(paths);
6251
+ const names = fs5.readdirSync(paths.identitiesDir, { withFileTypes: true }).filter((entry) => entry.isDirectory() && isIdentityDirectoryName(entry.name)).map((entry) => entry.name).sort();
6233
6252
  const result = [];
6234
6253
  for (const name of names) {
6235
- let provider;
6236
- try {
6237
- provider = readProvider(paths, name);
6238
- } catch (error) {
6239
- if (!(error instanceof ConfigValidationError || error instanceof LegacyProviderFileError)) {
6240
- throw error;
6241
- }
6242
- console.error(`agent-shim: providers/${name}.json is invalid and was skipped: ${error.message}`);
6254
+ const read = readIdentityForListing(paths, name);
6255
+ if (read === void 0) {
6243
6256
  continue;
6244
6257
  }
6245
- if (provider !== void 0) {
6246
- result.push({ name, provider });
6247
- }
6258
+ const isActive = name === active;
6259
+ result.push("problem" in read ? { name, isActive, problem: read.problem } : { name, identity: read, isActive });
6248
6260
  }
6249
6261
  return result;
6250
6262
  }
6251
- function listProviderNames(readdir, providersDir) {
6252
- let entries;
6253
- try {
6254
- entries = readdir(providersDir);
6255
- } catch {
6256
- return [];
6263
+ function setDefaultConfigProfile(paths, identityName, profileName) {
6264
+ if (!identityExists(paths, identityName)) {
6265
+ throw new IdentityNotFoundError(identityName);
6257
6266
  }
6258
- return entries.filter((entry) => entry.endsWith(".json")).map((entry) => entry.slice(0, -".json".length)).sort();
6259
- }
6260
- function describeProviderEndpoint(provider) {
6261
- return isCodexProvider(provider) ? "codex translation daemon" : provider.baseUrl;
6267
+ return applyPatch(identityJsonPath(paths, identityName), IdentitySchema, {
6268
+ defaultConfigProfile: profileName
6269
+ });
6262
6270
  }
6263
- var ProviderKindMismatchError = class extends UsageError {
6264
- constructor(message) {
6265
- super(message);
6266
- this.name = "ProviderKindMismatchError";
6271
+ function setAllowAmbientCredential(paths, identityName, allow) {
6272
+ if (!identityExists(paths, identityName)) {
6273
+ throw new IdentityNotFoundError(identityName);
6267
6274
  }
6268
- };
6269
- function isEmptyCodexConfig(config) {
6270
- return config === void 0 || config.defaultModel === void 0 && config.effort === void 0 && (config.models === void 0 || Object.keys(config.models).length === 0);
6275
+ return applyPatch(identityJsonPath(paths, identityName), IdentitySchema, {
6276
+ allowAmbientCredential: allow
6277
+ });
6271
6278
  }
6272
- function addProvider(paths, name, input) {
6273
- if (providerExists(paths, name)) {
6274
- throw new ProviderAlreadyExistsError(name);
6275
- }
6276
- if (!PROVIDER_NAME_RE.test(name)) {
6277
- throw new InvalidProviderNameError(name);
6278
- }
6279
- const kind = input.kind ?? "http";
6280
- if (kind === "codex" && input.baseUrl !== void 0) {
6281
- throw new ProviderKindMismatchError("A codex provider has no base URL: the launcher points its sessions at the front-door daemon. Drop --base-url.");
6282
- }
6283
- if (kind === "http" && input.baseUrl === void 0) {
6284
- throw new ProviderKindMismatchError("An http provider needs --base-url.");
6279
+ function setIdentityCredential(paths, identityName, change) {
6280
+ const existing = readIdentity(paths, identityName);
6281
+ if (existing === void 0) {
6282
+ throw new IdentityNotFoundError(identityName);
6285
6283
  }
6286
- if (kind === "http" && !isEmptyCodexConfig(input.codex)) {
6287
- throw new ProviderKindMismatchError("Codex settings apply only to a codex provider (--kind codex).");
6284
+ if (change === false) {
6285
+ return applyPatch(identityJsonPath(paths, identityName), IdentitySchema, { credential: void 0 });
6288
6286
  }
6289
- const shared = {
6290
- displayName: input.displayName,
6291
- credential: { sources: input.sources, ...input.target === void 0 ? {} : { target: input.target } },
6292
- ...input.env === void 0 || Object.keys(input.env).length === 0 ? {} : { env: input.env }
6293
- };
6294
- const parsed = ProviderSchema.safeParse(
6295
- kind === "codex" ? { kind, ...shared, ...isEmptyCodexConfig(input.codex) ? {} : { codex: input.codex } } : { ...shared, baseUrl: input.baseUrl }
6296
- );
6297
- if (!parsed.success) {
6298
- throw new ConfigValidationError(providerJsonPath(paths, name), parsed.error.issues);
6287
+ const sources = change.sources ?? existing.credential?.sources;
6288
+ if (sources === void 0) {
6289
+ throw new UsageError(`Identity "${identityName}" has no credential yet: pass --credential to give it one before choosing its target.`);
6299
6290
  }
6300
- writeJsonAtomic(providerJsonPath(paths, name), parsed.data);
6301
- return parsed.data;
6291
+ const target = change.target ?? existing.credential?.target;
6292
+ const cache = change.cache === void 0 ? existing.credential?.cache : change.cache === false ? void 0 : change.cache;
6293
+ return applyPatch(identityJsonPath(paths, identityName), IdentitySchema, {
6294
+ credential: { sources: [...sources], ...target === void 0 ? {} : { target }, ...cache === void 0 ? {} : { cache } }
6295
+ });
6302
6296
  }
6303
- function updateProvider(paths, name, input) {
6304
- const existing = readProvider(paths, name);
6305
- if (existing === void 0) {
6306
- throw new ProviderNotFoundError(name);
6297
+ function belongsToIdentity(entry, name) {
6298
+ return entry === name || entry === path16.basename(identityLockPath("", name)) || entry.startsWith(`.${name}.scratch.`) || entry.startsWith(`.${name}.previous.`);
6299
+ }
6300
+ function removeIdentity(paths, name) {
6301
+ if (!identityExists(paths, name)) {
6302
+ throw new IdentityNotFoundError(name);
6307
6303
  }
6308
- const unset = new Set(input.unsetEnv);
6309
- const env = Object.fromEntries(Object.entries({ ...existing.env, ...input.env }).filter(([key]) => !unset.has(key)));
6310
- const target = input.target ?? existing.credential.target;
6311
- const cache = input.cache === void 0 ? existing.credential.cache : input.cache === false ? void 0 : input.cache;
6312
- if (isCodexProvider(existing) && input.baseUrl !== void 0) {
6313
- throw new ProviderKindMismatchError(`Provider "${name}" is a codex provider, which has no base URL.`);
6304
+ for (const entry of fs5.readdirSync(paths.identitiesDir)) {
6305
+ if (belongsToIdentity(entry, name)) {
6306
+ fs5.rmSync(path16.join(paths.identitiesDir, entry), { recursive: true, force: true });
6307
+ }
6314
6308
  }
6315
- if (!isCodexProvider(existing) && !isEmptyCodexConfig(input.codex)) {
6316
- throw new ProviderKindMismatchError(`Provider "${name}" is an http provider; codex settings apply only to a codex provider.`);
6309
+ if (readActiveIdentity(paths) === name) {
6310
+ fs5.rmSync(paths.activeIdentityFile);
6317
6311
  }
6318
- const shared = {
6319
- ...input.displayName === void 0 ? {} : { displayName: input.displayName },
6320
- credential: {
6321
- sources: input.sources ?? existing.credential.sources,
6322
- ...target === void 0 ? {} : { target },
6323
- ...cache === void 0 ? {} : { cache }
6324
- },
6325
- env: Object.keys(env).length === 0 ? void 0 : env
6326
- };
6327
- let candidate;
6328
- if (isCodexProvider(existing)) {
6329
- const codex = {
6330
- ...existing.codex,
6331
- ...input.codex,
6332
- ...existing.codex?.models === void 0 && input.codex?.models === void 0 ? {} : { models: { ...existing.codex?.models, ...input.codex?.models } }
6333
- };
6334
- candidate = { ...existing, ...shared, codex: isEmptyCodexConfig(codex) ? void 0 : codex };
6335
- } else {
6336
- candidate = { ...existing, ...shared, ...input.baseUrl === void 0 ? {} : { baseUrl: input.baseUrl } };
6312
+ }
6313
+
6314
+ // src/providersStore.ts
6315
+ import fs6 from "node:fs";
6316
+ import path17 from "node:path";
6317
+
6318
+ // src/credentialCache.ts
6319
+ import { z as z11 } from "zod";
6320
+ var MS_PER_UNIT = { s: 1e3, m: 6e4, h: 36e5, d: 864e5 };
6321
+ var CachedCredentialSchema = z11.strictObject({
6322
+ token: z11.string().min(1),
6323
+ fetchedAt: z11.number().int().nonnegative(),
6324
+ source: CredentialSourceSchema
6325
+ });
6326
+ function effectiveStore(cache, platform) {
6327
+ return cache.store ?? (platform === "darwin" ? "keychain" : "file");
6328
+ }
6329
+ function ttlMs(cache) {
6330
+ if (cache.ttl === void 0) {
6331
+ return void 0;
6337
6332
  }
6338
- const parsed = ProviderSchema.safeParse(candidate);
6339
- if (!parsed.success) {
6340
- throw new ConfigValidationError(providerJsonPath(paths, name), parsed.error.issues);
6333
+ const multiplier = MS_PER_UNIT[cache.ttl.slice(-1)];
6334
+ if (multiplier === void 0) {
6335
+ throw new Error(`credential cache ttl "${cache.ttl}" has no recognised unit`);
6341
6336
  }
6342
- writeJsonAtomic(providerJsonPath(paths, name), parsed.data);
6343
- return parsed.data;
6337
+ return Number(cache.ttl.slice(0, -1)) * multiplier;
6344
6338
  }
6345
- function removeProvider(paths, name) {
6346
- if (!providerExists(paths, name)) {
6347
- throw new ProviderNotFoundError(name);
6339
+ function isFresh(entry, cache, now) {
6340
+ const limit = ttlMs(cache);
6341
+ if (entry.fetchedAt > now) {
6342
+ return false;
6348
6343
  }
6349
- fs6.rmSync(providerJsonPath(paths, name));
6344
+ return limit === void 0 || now - entry.fetchedAt < limit;
6350
6345
  }
6351
- function cascadeProviderName(cascade) {
6352
- let name;
6353
- for (const layer of assembleCascade(cascade).layers) {
6354
- if (layer.launch?.provider !== void 0) {
6355
- name = layer.launch.provider;
6356
- }
6357
- }
6358
- return name;
6346
+ function cacheAgeMs(entry, now) {
6347
+ return Math.max(0, now - entry.fetchedAt);
6359
6348
  }
6360
- function resolveProvider(params) {
6361
- const cliName = params.cliProvider !== void 0 && params.cliProvider !== "" ? params.cliProvider : void 0;
6362
- const name = cliName ?? (params.cascade === void 0 ? void 0 : cascadeProviderName(params.cascade));
6363
- if (name === void 0) {
6364
- return void 0;
6349
+ function describeCachedCredential(params) {
6350
+ const store = effectiveStore(params.cache, params.platform);
6351
+ let entry;
6352
+ try {
6353
+ entry = params.port.read(store, params.owner);
6354
+ } catch (error) {
6355
+ return { store, status: "unreadable", reason: error instanceof Error ? error.message : String(error) };
6356
+ }
6357
+ if (entry === void 0) {
6358
+ return { store, status: "empty" };
6359
+ }
6360
+ const limit = ttlMs(params.cache);
6361
+ const ageMs = cacheAgeMs(entry, params.nowMs);
6362
+ return {
6363
+ store,
6364
+ status: isFresh(entry, params.cache, params.nowMs) ? "fresh" : "expired",
6365
+ ageMs,
6366
+ ...limit === void 0 ? {} : { expiresInMs: limit - ageMs }
6367
+ };
6368
+ }
6369
+ function formatCachedCredentialState(state) {
6370
+ if (state.status === "empty") {
6371
+ return `no cached entry in the ${state.store} store yet`;
6365
6372
  }
6366
- const definition = loadProvider(params.paths.providersDir, name, params.port);
6367
- if (definition === void 0) {
6368
- const known = listProviderNames(params.port.readdir, params.paths.providersDir);
6369
- return {
6370
- ok: false,
6371
- status: 1,
6372
- message: `agent-shim: no provider named "${name}". ` + (known.length > 0 ? `Known providers: ${known.join(", ")}.` : "No providers are defined yet; run `agent-shim provider add`.")
6373
- };
6373
+ if (state.status === "unreadable") {
6374
+ return `the ${state.store} store cannot be read here: ${state.reason}`;
6374
6375
  }
6375
- if (params.credentials === void 0) {
6376
- return { ok: false, status: 1, message: `agent-shim: provider ${name} needs its credential resolved, but this launcher has no credential port wired` };
6376
+ if (state.status === "expired" && (state.expiresInMs === void 0 || state.expiresInMs > 0)) {
6377
+ return "cached with a timestamp in the future, treated as expired";
6377
6378
  }
6378
- const resolution = resolveCredential({ credential: definition.credential, env: params.env, port: params.credentials, subject: `provider ${name}` });
6379
- if (!resolution.ok) {
6380
- return { ok: false, status: CREDENTIAL_UNAVAILABLE_EXIT, message: resolution.message };
6379
+ const age = `cached ${formatAge(state.ageMs)} ago`;
6380
+ if (state.expiresInMs === void 0) {
6381
+ return `${age}, no expiry`;
6381
6382
  }
6382
- return { ok: true, provider: { name, definition, credential: resolution.credential }, warnings: resolution.credential.warnings };
6383
+ return state.status === "fresh" ? `${age}, expires in ${formatAge(state.expiresInMs)}` : `${age}, expired ${formatAge(-state.expiresInMs)} ago`;
6383
6384
  }
6384
6385
 
6385
- // src/directoryRulesStore.ts
6386
- var DirectoryRuleNotFoundError = class extends CliError {
6387
- constructor(rulePath) {
6388
- super(`No directory rule found for path "${rulePath}".`);
6389
- this.rulePath = rulePath;
6390
- this.name = "DirectoryRuleNotFoundError";
6391
- }
6392
- rulePath;
6393
- };
6394
- var DirectoryRuleMissingTargetError = class extends CliError {
6395
- constructor() {
6396
- super("A directory rule must set at least one of --config-profile or --identity.");
6397
- this.name = "DirectoryRuleMissingTargetError";
6386
+ // src/credential.ts
6387
+ var CREDENTIAL_UNAVAILABLE_EXIT = 64;
6388
+ var CREDENTIAL_COMMAND_TIMEOUT_MS = 1e4;
6389
+ var CREDENTIAL_INTERACTIVE_TIMEOUT_MS = 12e4;
6390
+ var GROUP_OTHER_PERMISSION_BITS = 63;
6391
+ var PERMISSION_BITS = 511;
6392
+ var OCTAL_RADIX2 = 8;
6393
+ function commandArgv(source) {
6394
+ if ("command" in source) {
6395
+ return source.command;
6398
6396
  }
6399
- };
6400
- var DirectoryRuleAlreadyExistsError = class extends CliError {
6401
- constructor(rulePath) {
6402
- super(`A directory rule for path "${rulePath}" already exists. Use \`agent-shim rule set\` to change it.`);
6403
- this.rulePath = rulePath;
6404
- this.name = "DirectoryRuleAlreadyExistsError";
6397
+ if ("op" in source) {
6398
+ return ["op", "read", source.op];
6405
6399
  }
6406
- rulePath;
6407
- };
6408
- function readDirectoryRules(paths) {
6409
- return readJson(paths.directoryRulesFile, DirectoryRulesSchema) ?? { rules: [] };
6400
+ const account = source.keychain.account;
6401
+ return ["security", "find-generic-password", "-s", source.keychain.service, ...account === void 0 ? [] : ["-a", account], "-w"];
6410
6402
  }
6411
- function writeDirectoryRules(paths, rules) {
6412
- const parsed = DirectoryRulesSchema.safeParse(rules);
6413
- if (!parsed.success) {
6414
- throw new ConfigValidationError(paths.directoryRulesFile, parsed.error.issues);
6403
+ function isInteractiveSource(source, env) {
6404
+ if ("command" in source || "keychain" in source) {
6405
+ return source.interactive ?? false;
6415
6406
  }
6416
- writeJsonAtomic(paths.directoryRulesFile, parsed.data);
6417
- }
6418
- function listDirectoryRules(paths) {
6419
- return readDirectoryRules(paths).rules;
6407
+ if ("op" in source) {
6408
+ return source.interactive ?? (env.OP_SERVICE_ACCOUNT_TOKEN ?? "") === "";
6409
+ }
6410
+ return false;
6420
6411
  }
6421
- function addDirectoryRule(paths, rulePath, options) {
6422
- if (options.configProfile === void 0 && options.identity === void 0) {
6423
- throw new DirectoryRuleMissingTargetError();
6412
+ function summariseSource(source) {
6413
+ if ("env" in source) {
6414
+ return { kind: "env", variable: source.env };
6424
6415
  }
6425
- const current = readDirectoryRules(paths);
6426
- if (current.rules.some((rule) => rule.path === rulePath)) {
6427
- throw new DirectoryRuleAlreadyExistsError(rulePath);
6416
+ if ("file" in source) {
6417
+ return { kind: "file", path: source.file };
6428
6418
  }
6429
- const created = buildNewRule(rulePath, options);
6430
- writeDirectoryRules(paths, { ...current, rules: [...current.rules, created] });
6431
- return created;
6432
- }
6433
- function ruleHasEffect(rule) {
6434
- return rule.configProfile !== void 0 || rule.identity !== void 0 || rule.categories !== void 0 || rule.entries !== void 0 || rule.launch !== void 0;
6435
- }
6436
- function updateDirectoryRule(paths, rulePath, options) {
6437
- const current = readDirectoryRules(paths);
6438
- const index = current.rules.findIndex((rule) => rule.path === rulePath);
6439
- const existing = index === -1 ? void 0 : current.rules[index];
6440
- if (existing === void 0) {
6441
- throw new DirectoryRuleNotFoundError(rulePath);
6419
+ if ("command" in source) {
6420
+ return { kind: "command", program: source.command[0] };
6442
6421
  }
6443
- const { configProfile, identity, ...rest } = existing;
6444
- const nextConfigProfile = options.configProfile === void 0 ? configProfile : options.configProfile === false ? void 0 : options.configProfile;
6445
- const nextIdentity = options.identity === void 0 ? identity : options.identity === false ? void 0 : options.identity;
6446
- const updated = {
6447
- ...rest,
6448
- ...nextConfigProfile === void 0 ? {} : { configProfile: nextConfigProfile },
6449
- ...nextIdentity === void 0 ? {} : { identity: nextIdentity }
6450
- };
6451
- if (!ruleHasEffect(updated)) {
6452
- throw new DirectoryRuleMissingTargetError();
6422
+ if ("op" in source) {
6423
+ return { kind: "op", reference: source.op };
6453
6424
  }
6454
- const nextRules = [...current.rules];
6455
- nextRules[index] = updated;
6456
- writeDirectoryRules(paths, { ...current, rules: nextRules });
6457
- return updated;
6425
+ if ("keychain" in source) {
6426
+ return {
6427
+ kind: "keychain",
6428
+ service: source.keychain.service,
6429
+ ...source.keychain.account === void 0 ? {} : { account: source.keychain.account }
6430
+ };
6431
+ }
6432
+ return { kind: "literal" };
6458
6433
  }
6459
- function buildNewRule(rulePath, options) {
6434
+ function summariseCredential(credential) {
6460
6435
  return {
6461
- path: rulePath,
6462
- ...options.configProfile !== void 0 ? { configProfile: options.configProfile } : {},
6463
- ...options.identity !== void 0 ? { identity: options.identity } : {}
6436
+ target: credential.target ?? "bearer",
6437
+ sources: credential.sources.map(summariseSource),
6438
+ ...credential.cache === void 0 ? {} : { cache: credential.cache }
6464
6439
  };
6465
6440
  }
6466
- function removeDirectoryRule(paths, rulePath) {
6467
- const current = readDirectoryRules(paths);
6468
- const nextRules = current.rules.filter((rule) => rule.path !== rulePath);
6469
- if (nextRules.length === current.rules.length) {
6470
- throw new DirectoryRuleNotFoundError(rulePath);
6441
+ function formatSourceSummary(summary) {
6442
+ switch (summary.kind) {
6443
+ case "env":
6444
+ return `env ${summary.variable}`;
6445
+ case "file":
6446
+ return `file ${summary.path}`;
6447
+ case "command":
6448
+ return `command ${summary.program}`;
6449
+ case "op":
6450
+ return `op ${summary.reference}`;
6451
+ case "keychain":
6452
+ return `keychain ${summary.service}${summary.account === void 0 ? "" : ` (account ${summary.account})`}`;
6453
+ case "literal":
6454
+ return "literal (non-secret placeholder)";
6455
+ default:
6456
+ return summary;
6471
6457
  }
6472
- writeDirectoryRules(paths, { ...current, rules: nextRules });
6473
6458
  }
6474
-
6475
- // src/checkReport.ts
6476
- import os4 from "node:os";
6477
-
6478
- // src/launcher/cascade.ts
6479
- import path18 from "node:path";
6480
-
6481
- // src/pathNorm.ts
6482
- import path17 from "node:path";
6483
- function expandTilde(value, home) {
6484
- if (value === "~") {
6485
- return home;
6486
- }
6487
- if (value.startsWith("~/")) {
6488
- return path17.join(home, value.slice(2));
6489
- }
6490
- return value;
6459
+ function describeSource(source) {
6460
+ return formatSourceSummary(summariseSource(source));
6491
6461
  }
6492
- function normaliseRulePath(rulePath, home) {
6493
- return path17.resolve(expandTilde(rulePath, home));
6462
+ function formatCredentialSummary(summary) {
6463
+ const cache = summary.cache === void 0 ? "" : `, cached ${summary.cache.ttl === void 0 ? "with no expiry" : `for ${summary.cache.ttl}`}${summary.cache.store === void 0 ? "" : ` in the ${summary.cache.store} store`}`;
6464
+ return `${summary.target} from ${summary.sources.map(formatSourceSummary).join(", then ")}${cache}`;
6494
6465
  }
6495
-
6496
- // src/launcher/cascade.ts
6497
- var PORTABLE_CONFIG_FILENAME = ".agent-shim.json";
6498
- var PORTABLE_LOCAL_CONFIG_FILENAME = ".agent-shim.local.json";
6499
- function loadCascadeInput(params) {
6500
- const globalConfig = loadConfigFile(params.paths.globalConfigFile, GlobalConfigSchema, params.read);
6501
- const directoryRules = loadConfigFile(params.paths.directoryRulesFile, DirectoryRulesSchema, params.read);
6502
- const loadProfile = (name) => {
6503
- const filepath = path18.join(params.paths.configProfilesDir, `${name}.json`);
6504
- const loaded = loadConfigFile(filepath, ConfigProfileSchema, params.read);
6505
- if (loaded === void 0) {
6506
- return void 0;
6466
+ function describeCredential(credential) {
6467
+ return formatCredentialSummary(summariseCredential(credential));
6468
+ }
6469
+ function trySource(source, params) {
6470
+ const none = (reason, warn = false) => ({ ok: false, reason, warn });
6471
+ if ("env" in source) {
6472
+ const value = params.env[source.env];
6473
+ return value === void 0 || value === "" ? none("is unset or empty") : { ok: true, token: value };
6474
+ }
6475
+ if ("literal" in source) {
6476
+ return { ok: true, token: source.literal };
6477
+ }
6478
+ if ("file" in source) {
6479
+ const read = params.port.readSecretFile(source.file);
6480
+ if (!read.found) {
6481
+ return none("does not exist");
6507
6482
  }
6508
- return { name, profile: loaded.config, entryOrder: loaded.entryOrder, filepath: loaded.filepath };
6509
- };
6510
- const limit = globalConfig?.config.walkUpLimit;
6511
- const dirs = walkDirectoryAncestors(params.cwd, {
6512
- home: params.home,
6513
- ...limit === void 0 ? {} : { limit: expandTilde(limit, params.home) },
6514
- ...params.isReadable === void 0 ? {} : { isReadable: params.isReadable }
6515
- });
6516
- const levels = [];
6517
- for (const dir of dirs) {
6518
- const portable = loadConfigFile(path18.join(dir, PORTABLE_CONFIG_FILENAME), PortableConfigSchema, params.read) ?? loadConfigFile(path18.join(dir, LEGACY_PORTABLE_CONFIG_FILENAME), PortableConfigSchema, params.read);
6519
- const portableLocal = loadConfigFile(path18.join(dir, PORTABLE_LOCAL_CONFIG_FILENAME), PortableConfigSchema, params.read) ?? loadConfigFile(path18.join(dir, LEGACY_PORTABLE_LOCAL_CONFIG_FILENAME), PortableConfigSchema, params.read);
6520
- const rules = (directoryRules?.config.rules ?? []).map((rule, index) => ({ rule, index })).filter(({ rule }) => normaliseRulePath(rule.path, params.home) === dir).map(({ rule, index }) => {
6521
- const entryOrder = directoryRules?.ruleEntryOrders[index];
6522
- return {
6523
- rule,
6524
- ...entryOrder === void 0 ? {} : { entryOrder },
6525
- filepath: params.paths.directoryRulesFile
6526
- };
6527
- });
6528
- if (portable === void 0 && portableLocal === void 0 && rules.length === 0) {
6529
- continue;
6483
+ if (read.mode !== void 0 && (read.mode & GROUP_OTHER_PERMISSION_BITS) !== 0) {
6484
+ return none(
6485
+ `is readable or writable by group or others (mode ${(read.mode & PERMISSION_BITS).toString(OCTAL_RADIX2)}); run \`chmod 600 ${source.file}\``,
6486
+ true
6487
+ );
6530
6488
  }
6531
- levels.push({
6532
- dir,
6533
- ...portable === void 0 ? {} : { portable: { config: portable.config, entryOrder: portable.entryOrder, filepath: portable.filepath } },
6534
- ...rules.length === 0 ? {} : { rules },
6535
- ...portableLocal === void 0 ? {} : { portableLocal: { config: portableLocal.config, entryOrder: portableLocal.entryOrder, filepath: portableLocal.filepath } }
6536
- });
6489
+ const token2 = read.content.trim();
6490
+ return token2 === "" ? none("is empty") : { ok: true, token: token2 };
6491
+ }
6492
+ const argv = commandArgv(source);
6493
+ const interactive = isInteractiveSource(source, params.env);
6494
+ if (interactive && !params.port.personPresent()) {
6495
+ return none("needs a person to approve it, but there is no terminal or desktop session");
6496
+ }
6497
+ const timeoutMs = source.timeoutMs ?? (interactive ? CREDENTIAL_INTERACTIVE_TIMEOUT_MS : CREDENTIAL_COMMAND_TIMEOUT_MS);
6498
+ const result = params.port.runCommand(argv, { timeoutMs, interactive });
6499
+ if (result.timedOut) {
6500
+ return none(`timed out after ${String(timeoutMs)}ms`);
6501
+ }
6502
+ if (result.status !== 0) {
6503
+ const stderr = result.stderr.trim();
6504
+ const outcome = result.status === null ? "could not be run or was killed by a signal" : `exited with status ${String(result.status)}`;
6505
+ return none(`${outcome}${stderr === "" ? "" : `: ${stderr}`}`);
6537
6506
  }
6538
- const input = {
6539
- home: params.home,
6540
- ...globalConfig === void 0 ? {} : { globalConfig: { config: globalConfig.config, entryOrder: globalConfig.entryOrder, filepath: globalConfig.filepath } },
6541
- ...params.baseConfigProfile === void 0 ? {} : { baseConfigProfile: params.baseConfigProfile },
6542
- loadProfile,
6543
- levels,
6544
- ...params.cliOverride === void 0 ? {} : { cliOverride: params.cliOverride }
6545
- };
6546
- return { input, ...globalConfig === void 0 ? {} : { globalConfig: globalConfig.config } };
6507
+ const token = result.stdout.trim();
6508
+ return token === "" ? none("printed no token") : { ok: true, token };
6547
6509
  }
6548
- function readDirectorySelections(loaded) {
6549
- let identity;
6550
- let configProfile;
6551
- for (const level of loaded.input.levels ?? []) {
6552
- for (const source of [level.portable?.config, ...(level.rules ?? []).map((entry) => entry.rule), level.portableLocal?.config]) {
6553
- if (source === void 0) {
6554
- continue;
6555
- }
6556
- if (source.identity !== void 0) {
6557
- identity = source.identity;
6558
- }
6559
- if (source.configProfile !== void 0) {
6560
- configProfile = source.configProfile;
6510
+ function warmCommandFor(subject) {
6511
+ const [kind, name] = subject.split(" ");
6512
+ return kind === "provider" ? `agent-shim credential warm --provider ${name ?? ""}` : `agent-shim credential warm ${name ?? ""}`;
6513
+ }
6514
+ function resolveCredential(params) {
6515
+ const target = params.credential.target ?? "bearer";
6516
+ const cacheBlock = params.credential.cache;
6517
+ const cache = cacheBlock === void 0 ? void 0 : params.port.cache;
6518
+ const store = cacheBlock === void 0 || cache === void 0 ? void 0 : effectiveStore(cacheBlock, cache.platform);
6519
+ if (cacheBlock !== void 0 && cache !== void 0 && store !== void 0 && params.refresh !== true) {
6520
+ const entry = cache.port.read(store, params.subject);
6521
+ if (entry !== void 0 && isFresh(entry, cacheBlock, cache.now())) {
6522
+ return { ok: true, credential: { target, token: entry.token, source: entry.source, warnings: [], cachedAt: entry.fetchedAt } };
6523
+ }
6524
+ }
6525
+ const attempts = [];
6526
+ const warnings = [];
6527
+ for (const source of params.credential.sources) {
6528
+ const outcome = trySource(source, params);
6529
+ if (outcome.ok) {
6530
+ if (cache !== void 0 && store !== void 0) {
6531
+ cache.port.write(store, params.subject, { token: outcome.token, fetchedAt: cache.now(), source });
6561
6532
  }
6533
+ return { ok: true, credential: { target, token: outcome.token, source, warnings } };
6534
+ }
6535
+ const line = `${describeSource(source)} ${outcome.reason}`;
6536
+ attempts.push(line);
6537
+ if (outcome.warn) {
6538
+ warnings.push(`agent-shim: ${params.subject}: skipped ${line}`);
6562
6539
  }
6563
6540
  }
6564
- return { ...identity === void 0 ? {} : { identity }, ...configProfile === void 0 ? {} : { configProfile } };
6541
+ const hint = cacheBlock === void 0 ? "" : `; fill the cache from a terminal with \`${warmCommandFor(params.subject)}\``;
6542
+ return { ok: false, message: `agent-shim: ${params.subject} has no usable credential: ${attempts.join("; ")}${hint}` };
6543
+ }
6544
+ function credentialVariables(target, token) {
6545
+ return {
6546
+ ANTHROPIC_AUTH_TOKEN: target === "bearer" ? token : void 0,
6547
+ ANTHROPIC_API_KEY: target === "apiKey" ? token : void 0,
6548
+ CLAUDE_CODE_OAUTH_TOKEN: target === "oauthToken" ? token : void 0
6549
+ };
6565
6550
  }
6566
6551
 
6567
- // src/usage/poolPick.ts
6568
- import path19 from "node:path";
6569
- import { z as z9 } from "zod";
6570
-
6571
- // src/usage/plan.ts
6572
- var TIER_MULTIPLIER = /(?:^|_)(\d+)x$/;
6573
- var ZERO_TIER = /(?:^|_)zero$/;
6574
- var BASE_TIER = /(?:^|_)(?:pro|free)$/;
6575
- function planOf(account) {
6576
- const tier = account?.organizationRateLimitTier ?? account?.userRateLimitTier;
6577
- const tierField = tier === void 0 ? {} : { tier };
6578
- if (tier === void 0) {
6579
- return { kind: "subscription", capacity: 1, recognised: false };
6552
+ // src/providersStore.ts
6553
+ var ProviderNotFoundError = class extends CliError {
6554
+ constructor(name) {
6555
+ super(`No provider named "${name}". Run \`agent-shim provider add ${name}\` first.`);
6556
+ this.name = name;
6557
+ this.name = "ProviderNotFoundError";
6580
6558
  }
6581
- if (ZERO_TIER.test(tier)) {
6582
- return { kind: "pay-per-use", ...tierField };
6559
+ name;
6560
+ };
6561
+ var ProviderAlreadyExistsError = class extends CliError {
6562
+ constructor(providerName) {
6563
+ super(`A provider named "${providerName}" already exists.`);
6564
+ this.providerName = providerName;
6565
+ this.name = "ProviderAlreadyExistsError";
6583
6566
  }
6584
- const multiplier = TIER_MULTIPLIER.exec(tier)?.[1];
6585
- if (multiplier !== void 0) {
6586
- return { kind: "subscription", capacity: Number(multiplier), recognised: true, ...tierField };
6567
+ providerName;
6568
+ };
6569
+ var InvalidProviderNameError = class extends CliError {
6570
+ constructor(attemptedName) {
6571
+ super(
6572
+ `"${attemptedName}" is not a valid provider name. Provider names must start with a letter or number and may then contain letters, numbers, dots, hyphens, underscores, and at signs.`
6573
+ );
6574
+ this.attemptedName = attemptedName;
6575
+ this.name = "InvalidProviderNameError";
6587
6576
  }
6588
- return { kind: "subscription", capacity: 1, recognised: BASE_TIER.test(tier), ...tierField };
6577
+ attemptedName;
6578
+ };
6579
+ var PROVIDER_NAME_RE = /^[A-Za-z0-9][A-Za-z0-9._@-]*$/;
6580
+ function providerJsonPath(paths, name) {
6581
+ return path17.join(paths.providersDir, `${name}.json`);
6589
6582
  }
6590
-
6591
- // src/usage/rateLimit.ts
6592
- var MS_PER_SECOND3 = 1e3;
6593
- function epochSecondsToIso(value) {
6594
- if (value === void 0) {
6595
- return void 0;
6596
- }
6597
- const seconds = Number(value);
6598
- return Number.isFinite(seconds) && seconds > 0 ? new Date(seconds * MS_PER_SECOND3).toISOString() : void 0;
6583
+ function providerExists(paths, name) {
6584
+ return fs6.existsSync(providerJsonPath(paths, name));
6599
6585
  }
6600
- function nonNegativeNumber(value) {
6601
- if (value === void 0 || value.trim() === "") {
6602
- return void 0;
6603
- }
6604
- const parsed = Number(value);
6605
- return Number.isFinite(parsed) && parsed >= 0 ? parsed : void 0;
6586
+ var LEGACY_PROVIDER_FIELDS = ["tokenEnv", "tokenCommand", "authScheme"];
6587
+ var LEGACY_ENV_CREDENTIAL_KEYS = Object.values(CREDENTIAL_TARGET_VARS);
6588
+ var LEGACY_LITERAL_PLACEHOLDER = "<the value of env.ANTHROPIC_AUTH_TOKEN>";
6589
+ function isRecord3(value) {
6590
+ return typeof value === "object" && value !== null && !Array.isArray(value);
6606
6591
  }
6607
- var UNIFIED_PREFIX = "anthropic-ratelimit-unified-";
6608
- function unifiedWindow(headers, window) {
6609
- const utilization = nonNegativeNumber(headers[`${UNIFIED_PREFIX}${window}-utilization`]);
6610
- const resetsAt = epochSecondsToIso(headers[`${UNIFIED_PREFIX}${window}-reset`]);
6611
- const status = headers[`${UNIFIED_PREFIX}${window}-status`];
6612
- if (utilization === void 0 && resetsAt === void 0 && status === void 0) {
6592
+ function legacyProviderConversion(raw) {
6593
+ if (!isRecord3(raw) || "credential" in raw) {
6613
6594
  return void 0;
6614
6595
  }
6615
- return { ...utilization === void 0 ? {} : { utilization }, ...resetsAt === void 0 ? {} : { resetsAt }, ...status === void 0 ? {} : { status } };
6616
- }
6617
- function parseUnifiedRateLimit(headers) {
6618
- if (!Object.keys(headers).some((name) => name.startsWith(UNIFIED_PREFIX))) {
6596
+ const env = isRecord3(raw.env) ? raw.env : void 0;
6597
+ const legacyEnvKeys = env === void 0 ? [] : Object.keys(env).filter((key) => LEGACY_ENV_CREDENTIAL_KEYS.includes(key));
6598
+ const fields = [...LEGACY_PROVIDER_FIELDS.filter((field) => field in raw), ...legacyEnvKeys.map((key) => `env.${key}`)];
6599
+ if (fields.length === 0) {
6619
6600
  return void 0;
6620
6601
  }
6621
- const status = headers[`${UNIFIED_PREFIX}status`];
6622
- const fiveHour = unifiedWindow(headers, "5h");
6623
- const sevenDay = unifiedWindow(headers, "7d");
6624
- const representativeClaim = headers[`${UNIFIED_PREFIX}representative-claim`];
6625
- const resetAt = epochSecondsToIso(headers[`${UNIFIED_PREFIX}reset`]);
6626
- const overageStatus = headers[`${UNIFIED_PREFIX}overage-status`];
6602
+ const sources = [];
6603
+ if (typeof raw.tokenEnv === "string") {
6604
+ sources.push({ env: raw.tokenEnv });
6605
+ }
6606
+ if (Array.isArray(raw.tokenCommand)) {
6607
+ sources.push({ command: raw.tokenCommand });
6608
+ }
6609
+ if (typeof env?.ANTHROPIC_AUTH_TOKEN === "string" && env.ANTHROPIC_AUTH_TOKEN !== "") {
6610
+ sources.push({ literal: LEGACY_LITERAL_PLACEHOLDER });
6611
+ }
6612
+ const remainingEnv = env === void 0 ? void 0 : Object.fromEntries(Object.entries(env).filter(([key]) => !LEGACY_ENV_CREDENTIAL_KEYS.includes(key)));
6613
+ const replacedKeys = [...LEGACY_PROVIDER_FIELDS, "env"];
6614
+ const rest = Object.fromEntries(Object.entries(raw).filter(([key]) => !replacedKeys.includes(key)));
6627
6615
  return {
6628
- ...status === void 0 ? {} : { status },
6629
- ...fiveHour === void 0 ? {} : { fiveHour },
6630
- ...sevenDay === void 0 ? {} : { sevenDay },
6631
- ...representativeClaim === void 0 ? {} : { representativeClaim },
6632
- ...resetAt === void 0 ? {} : { resetAt },
6633
- ...overageStatus === void 0 ? {} : { overageStatus }
6616
+ fields,
6617
+ replacement: {
6618
+ ...rest,
6619
+ credential: { sources, ...raw.authScheme === "apiKey" ? { target: "apiKey" } : {} },
6620
+ ...remainingEnv === void 0 || Object.keys(remainingEnv).length === 0 ? {} : { env: remainingEnv }
6621
+ }
6634
6622
  };
6635
6623
  }
6636
-
6637
- // src/usage/scan.ts
6638
- import { z as z8 } from "zod";
6639
- var AnthropicUsageSchema = z8.looseObject({
6640
- input_tokens: z8.number().int().nonnegative().nullish(),
6641
- output_tokens: z8.number().int().nonnegative().nullish(),
6642
- cache_creation_input_tokens: z8.number().int().nonnegative().nullish(),
6643
- cache_read_input_tokens: z8.number().int().nonnegative().nullish()
6644
- });
6645
- var ErrorObjectSchema = z8.looseObject({
6646
- type: z8.string().optional(),
6647
- code: z8.union([z8.string(), z8.number()]).optional(),
6648
- message: z8.string().optional()
6649
- });
6650
-
6651
- // src/usage/middleware.ts
6652
- var ANTHROPIC_PROVIDER = "anthropic";
6653
-
6654
- // src/usage/pick.ts
6655
- var MS_PER_SECOND4 = 1e3;
6656
- var MS_PER_HOUR = 36e5;
6657
- var HOURS_PER_DAY2 = 24;
6658
- var FIVE_HOURS = 5;
6659
- var PROMPT_CACHE_TTL_HOURS = 1;
6660
- var PERCENT = 100;
6661
- var SEVEN_DAYS = 7;
6662
- var FIVE_HOUR_WINDOW_MS = FIVE_HOURS * MS_PER_HOUR;
6663
- var SEVEN_DAY_WINDOW_MS = SEVEN_DAYS * HOURS_PER_DAY2 * MS_PER_HOUR;
6664
- var PROMPT_CACHE_TTL_MS = PROMPT_CACHE_TTL_HOURS * MS_PER_HOUR;
6665
- var CLASS_ORDER = { scored: 0, unknown: 1, "pay-per-use": 2, ineligible: 3 };
6666
- function unifiedOf(state) {
6667
- return state?.rateLimit?.unified;
6668
- }
6669
- function limitBlockedUntil(state, nowMs) {
6670
- const limit = state?.lastLimit;
6671
- if (state === void 0 || limit === void 0) {
6672
- return void 0;
6624
+ var LegacyProviderFileError = class extends CliError {
6625
+ constructor(filePath, conversion) {
6626
+ super(
6627
+ `${filePath} uses ${conversion.fields.join(", ")}, which a credential block replaced. Rewrite it as:
6628
+ ` + JSON.stringify(conversion.replacement, null, 2)
6629
+ );
6630
+ this.filePath = filePath;
6631
+ this.conversion = conversion;
6632
+ this.name = "LegacyProviderFileError";
6673
6633
  }
6674
- if (Date.parse(state.lastRequestAt) > Date.parse(limit.observedAt)) {
6634
+ filePath;
6635
+ conversion;
6636
+ };
6637
+ function parseProviderFile(filePath, raw) {
6638
+ const conversion = legacyProviderConversion(raw);
6639
+ if (conversion !== void 0) {
6640
+ throw new LegacyProviderFileError(filePath, conversion);
6641
+ }
6642
+ const parsed = ProviderSchema.safeParse(raw);
6643
+ if (!parsed.success) {
6644
+ throw new ConfigValidationError(filePath, parsed.error.issues);
6645
+ }
6646
+ return parsed.data;
6647
+ }
6648
+ function readProvider(paths, name) {
6649
+ const filePath = providerJsonPath(paths, name);
6650
+ if (!fs6.existsSync(filePath)) {
6675
6651
  return void 0;
6676
6652
  }
6677
- const until = limit.resetAt === void 0 ? limit.retryAfterSeconds === void 0 ? void 0 : Date.parse(limit.observedAt) + limit.retryAfterSeconds * MS_PER_SECOND4 : Date.parse(limit.resetAt);
6678
- return until !== void 0 && until > nowMs ? until : void 0;
6653
+ const raw = JSON.parse(fs6.readFileSync(filePath, "utf8"));
6654
+ return parseProviderFile(filePath, raw);
6679
6655
  }
6680
- function windowBlockedUntil(windows) {
6681
- const resets = windows.flatMap((window) => window.status === "rejected" && window.resetsAtMs !== void 0 ? [window.resetsAtMs] : []);
6682
- return resets.length === 0 ? void 0 : Math.max(...resets);
6656
+ function loadProvider(providersDir, name, port) {
6657
+ const filePath = path17.join(providersDir, `${name}.json`);
6658
+ const raw = port.readConfigFile(filePath);
6659
+ return raw === void 0 ? void 0 : parseProviderFile(filePath, raw);
6683
6660
  }
6684
- function fiveHourBurn(records) {
6685
- const points = [];
6686
- for (const record of records) {
6687
- const window = record.rateLimitHeaders === void 0 ? void 0 : parseUnifiedRateLimit(record.rateLimitHeaders)?.fiveHour;
6688
- if (window?.utilization !== void 0 && window.resetsAt !== void 0) {
6689
- points.push({ resetsAt: window.resetsAt, point: { atMs: Date.parse(record.at), utilization: window.utilization } });
6661
+ function listProviders(paths) {
6662
+ const names = listProviderNames(
6663
+ (dir) => fs6.readdirSync(dir),
6664
+ paths.providersDir
6665
+ );
6666
+ const result = [];
6667
+ for (const name of names) {
6668
+ let provider;
6669
+ try {
6670
+ provider = readProvider(paths, name);
6671
+ } catch (error) {
6672
+ if (!(error instanceof ConfigValidationError || error instanceof LegacyProviderFileError)) {
6673
+ throw error;
6674
+ }
6675
+ console.error(`agent-shim: providers/${name}.json is invalid and was skipped: ${error.message}`);
6676
+ continue;
6677
+ }
6678
+ if (provider !== void 0) {
6679
+ result.push({ name, provider });
6690
6680
  }
6691
6681
  }
6692
- const latest = points.at(-1)?.resetsAt;
6693
- const current = points.filter((entry) => entry.resetsAt === latest).map((entry) => entry.point);
6694
- const first = current.at(0);
6695
- const last = current.at(-1);
6696
- if (first === void 0 || last === void 0 || last.atMs <= first.atMs || last.utilization <= first.utilization) {
6697
- return void 0;
6682
+ return result;
6683
+ }
6684
+ function listProviderNames(readdir, providersDir) {
6685
+ let entries;
6686
+ try {
6687
+ entries = readdir(providersDir);
6688
+ } catch {
6689
+ return [];
6698
6690
  }
6699
- return (last.utilization - first.utilization) / (last.atMs - first.atMs);
6691
+ return entries.filter((entry) => entry.endsWith(".json")).map((entry) => entry.slice(0, -".json".length)).sort();
6700
6692
  }
6701
- function percent(fraction) {
6702
- return `${String(Math.round(fraction * PERCENT))}%`;
6693
+ function describeProviderEndpoint(provider) {
6694
+ return isCodexProvider(provider) ? "codex translation daemon" : provider.baseUrl;
6703
6695
  }
6704
- function describeWindow(label, window, effective, nowMs) {
6705
- if (effective.reset) {
6706
- return `${label} window has reset`;
6696
+ var ProviderKindMismatchError = class extends UsageError {
6697
+ constructor(message) {
6698
+ super(message);
6699
+ this.name = "ProviderKindMismatchError";
6707
6700
  }
6708
- const used = effective.utilization === void 0 ? "usage unknown" : `${percent(effective.utilization)} used`;
6709
- const resets = effective.resetsAtMs === void 0 ? "" : `, resets in ${formatAge(effective.resetsAtMs - nowMs)}`;
6710
- return `${label} ${used}${resets}${window.status === "allowed_warning" ? " (nearly used)" : ""}`;
6711
- }
6712
- function capacityOf(plan) {
6713
- return plan.kind === "subscription" ? plan.capacity : 1;
6701
+ };
6702
+ function isEmptyCodexConfig(config) {
6703
+ return config === void 0 || config.defaultModel === void 0 && config.effort === void 0 && (config.models === void 0 || Object.keys(config.models).length === 0);
6714
6704
  }
6715
- function assess(member, nowMs) {
6716
- const plan = planOf(member.account);
6717
- const planReasons = plan.kind === "pay-per-use" ? ["billed by use (no plan allowance)"] : plan.recognised ? [`${String(plan.capacity)}x plan`] : [`plan tier ${plan.tier === void 0 ? "not known" : `"${plan.tier}" not recognised`}, counted as 1x`];
6718
- const base = { identity: member.identity, plan };
6719
- if (member.readError !== void 0) {
6720
- return { candidate: { ...base, class: "unknown", reasons: [`usage state unreadable: ${member.readError}`, ...planReasons] } };
6705
+ function addProvider(paths, name, input) {
6706
+ if (providerExists(paths, name)) {
6707
+ throw new ProviderAlreadyExistsError(name);
6721
6708
  }
6722
- const state = member.snapshot?.providers[ANTHROPIC_PROVIDER];
6723
- const unified = unifiedOf(state);
6724
- if (member.snapshot === void 0 || state === void 0 || unified === void 0) {
6725
- return { candidate: { ...base, class: "unknown", reasons: ["no usage recorded yet, so its quota is unknown (turn on trackUsage for it)", ...planReasons] } };
6709
+ if (!PROVIDER_NAME_RE.test(name)) {
6710
+ throw new InvalidProviderNameError(name);
6726
6711
  }
6727
- const observedAgo = state.rateLimit === void 0 ? "" : ` (last seen ${formatAge(nowMs - Date.parse(state.rateLimit.observedAt))} ago)`;
6728
- const fiveHourWindow = unified.fiveHour;
6729
- const sevenDayWindow = unified.sevenDay;
6730
- const five = fiveHourWindow === void 0 ? void 0 : effectiveWindow(fiveHourWindow, nowMs);
6731
- const seven = sevenDayWindow === void 0 ? void 0 : effectiveWindow(sevenDayWindow, nowMs);
6732
- const windowReasons = [
6733
- ...fiveHourWindow === void 0 || five === void 0 ? [] : [describeWindow("5h", fiveHourWindow, five, nowMs)],
6734
- ...sevenDayWindow === void 0 || seven === void 0 ? [] : [describeWindow("7d", sevenDayWindow, seven, nowMs)]
6735
- ];
6736
- const blockedUntil = Math.max(
6737
- windowBlockedUntil([five, seven].flatMap((window) => window === void 0 ? [] : [window])) ?? 0,
6738
- limitBlockedUntil(state, nowMs) ?? 0
6739
- );
6740
- if (blockedUntil > nowMs) {
6741
- if (unified.overageStatus === "allowed") {
6742
- return { candidate: { ...base, class: "pay-per-use", reasons: [`plan exhausted until ${new Date(blockedUntil).toISOString()}, continues as extra usage`, ...windowReasons, ...planReasons] } };
6743
- }
6744
- return { candidate: { ...base, class: "ineligible", blockedUntilMs: blockedUntil, reasons: [`refused until ${new Date(blockedUntil).toISOString()} (in ${formatAge(blockedUntil - nowMs)})`, ...windowReasons, ...planReasons] } };
6712
+ const kind = input.kind ?? "http";
6713
+ if (kind === "codex" && input.baseUrl !== void 0) {
6714
+ throw new ProviderKindMismatchError("A codex provider has no base URL: the launcher points its sessions at the front-door daemon. Drop --base-url.");
6745
6715
  }
6746
- if (plan.kind === "pay-per-use" || fiveHourWindow === void 0 && sevenDayWindow === void 0) {
6747
- return { candidate: { ...base, class: "pay-per-use", reasons: [...plan.kind === "pay-per-use" ? planReasons : ["no plan windows reported, so usage bills as extra usage"]] } };
6716
+ if (kind === "http" && input.baseUrl === void 0) {
6717
+ throw new ProviderKindMismatchError("An http provider needs --base-url.");
6748
6718
  }
6749
- const basis = seven?.utilization !== void 0 ? { window: seven, windowMs: SEVEN_DAY_WINDOW_MS } : five?.utilization !== void 0 ? { window: five, windowMs: FIVE_HOUR_WINDOW_MS } : void 0;
6750
- if (basis?.window.utilization === void 0) {
6751
- return { candidate: { ...base, class: "unknown", reasons: ["quota windows reported no utilisation", ...windowReasons, ...planReasons] } };
6719
+ if (kind === "http" && !isEmptyCodexConfig(input.codex)) {
6720
+ throw new ProviderKindMismatchError("Codex settings apply only to a codex provider (--kind codex).");
6752
6721
  }
6753
- const remaining = Math.max(0, 1 - basis.window.utilization);
6754
- const untilResetMs = basis.window.resetsAtMs === void 0 ? basis.windowMs : Math.max(basis.window.resetsAtMs - nowMs, MS_PER_SECOND4);
6755
- const score = remaining * plan.capacity / (untilResetMs / MS_PER_HOUR);
6756
- const fiveRemaining = five?.utilization === void 0 ? void 0 : Math.max(0, 1 - five.utilization);
6757
- const fiveUntil = five?.resetsAtMs === void 0 ? FIVE_HOUR_WINDOW_MS : Math.max(five.resetsAtMs - nowMs, MS_PER_SECOND4);
6758
- const burn = fiveHourBurn(member.records);
6759
- return {
6760
- candidate: { ...base, class: "scored", score, reasons: [...windowReasons.map((reason, index) => index === windowReasons.length - 1 ? `${reason}${observedAgo}` : reason), ...planReasons] },
6761
- ...fiveRemaining === void 0 ? {} : { fiveHour: { remaining: fiveRemaining, untilResetMs: fiveUntil } },
6762
- ...burn === void 0 ? {} : { burn }
6722
+ const shared = {
6723
+ displayName: input.displayName,
6724
+ credential: { sources: input.sources, ...input.target === void 0 ? {} : { target: input.target } },
6725
+ ...input.env === void 0 || Object.keys(input.env).length === 0 ? {} : { env: input.env }
6763
6726
  };
6764
- }
6765
- function rankPool(input) {
6766
- const { members, nowMs } = input;
6767
- const assessments = members.map((member) => assess(member, nowMs));
6768
- const demands = assessments.flatMap(({ candidate, burn }) => burn === void 0 ? [] : [burn * capacityOf(candidate.plan)]);
6769
- const sharedDemand = demands.length === 0 ? void 0 : Math.max(...demands);
6770
- const candidates = assessments.map((assessment) => {
6771
- const { candidate, fiveHour, burn } = assessment;
6772
- const pace = burn ?? (sharedDemand === void 0 ? void 0 : sharedDemand / capacityOf(candidate.plan));
6773
- if (candidate.class !== "scored" || fiveHour === void 0 || pace === void 0) {
6774
- return { ...candidate, feasible: true };
6775
- }
6776
- const runsDryInMs = fiveHour.remaining / pace;
6777
- if (runsDryInMs >= fiveHour.untilResetMs) {
6778
- return { ...candidate, feasible: true };
6779
- }
6780
- return { ...candidate, feasible: false, reasons: [`5h window would run dry in ${formatAge(runsDryInMs)}, before it resets in ${formatAge(fiveHour.untilResetMs)}`, ...candidate.reasons] };
6781
- });
6782
- const ordered = [...candidates].sort(
6783
- (left, right) => CLASS_ORDER[left.class] - CLASS_ORDER[right.class] || Number(right.feasible) - Number(left.feasible) || (right.score ?? 0) - (left.score ?? 0) || left.identity.localeCompare(right.identity)
6727
+ const parsed = ProviderSchema.safeParse(
6728
+ kind === "codex" ? { kind, ...shared, ...isEmptyCodexConfig(input.codex) ? {} : { codex: input.codex } } : { ...shared, baseUrl: input.baseUrl }
6784
6729
  );
6785
- const sticky = input.sticky;
6786
- const stickyIndex = sticky === void 0 ? -1 : ordered.findIndex((candidate) => candidate.identity === sticky.identity);
6787
- const stickyCandidate = ordered[stickyIndex];
6788
- if (sticky !== void 0 && stickyCandidate?.class === "scored" && stickyCandidate.feasible) {
6789
- const ageMs = nowMs - Date.parse(sticky.at);
6790
- const warm = ageMs <= PROMPT_CACHE_TTL_MS;
6791
- if (input.resuming || warm) {
6792
- const why = warm ? `used here ${formatAge(ageMs)} ago, prompt cache still warm` : "resuming a conversation started on this account";
6793
- ordered.splice(stickyIndex, 1);
6794
- ordered.unshift({ ...stickyCandidate, reasons: [why, ...stickyCandidate.reasons] });
6795
- }
6730
+ if (!parsed.success) {
6731
+ throw new ConfigValidationError(providerJsonPath(paths, name), parsed.error.issues);
6796
6732
  }
6797
- const pick = ordered.find((candidate) => candidate.class !== "ineligible");
6798
- const returns = ordered.flatMap((candidate) => candidate.blockedUntilMs === void 0 ? [] : [{ identity: candidate.identity, atMs: candidate.blockedUntilMs }]);
6799
- const earliestReturn = returns.length === 0 ? void 0 : returns.reduce((soonest, entry) => entry.atMs < soonest.atMs ? entry : soonest);
6800
- return {
6801
- candidates: ordered,
6802
- ...pick === void 0 ? { ...earliestReturn === void 0 ? {} : { earliestReturn } } : { pick }
6733
+ writeJsonAtomic(providerJsonPath(paths, name), parsed.data);
6734
+ return parsed.data;
6735
+ }
6736
+ function updateProvider(paths, name, input) {
6737
+ const existing = readProvider(paths, name);
6738
+ if (existing === void 0) {
6739
+ throw new ProviderNotFoundError(name);
6740
+ }
6741
+ const unset = new Set(input.unsetEnv);
6742
+ const env = Object.fromEntries(Object.entries({ ...existing.env, ...input.env }).filter(([key]) => !unset.has(key)));
6743
+ const target = input.target ?? existing.credential.target;
6744
+ const cache = input.cache === void 0 ? existing.credential.cache : input.cache === false ? void 0 : input.cache;
6745
+ if (isCodexProvider(existing) && input.baseUrl !== void 0) {
6746
+ throw new ProviderKindMismatchError(`Provider "${name}" is a codex provider, which has no base URL.`);
6747
+ }
6748
+ if (!isCodexProvider(existing) && !isEmptyCodexConfig(input.codex)) {
6749
+ throw new ProviderKindMismatchError(`Provider "${name}" is an http provider; codex settings apply only to a codex provider.`);
6750
+ }
6751
+ const shared = {
6752
+ ...input.displayName === void 0 ? {} : { displayName: input.displayName },
6753
+ credential: {
6754
+ sources: input.sources ?? existing.credential.sources,
6755
+ ...target === void 0 ? {} : { target },
6756
+ ...cache === void 0 ? {} : { cache }
6757
+ },
6758
+ env: Object.keys(env).length === 0 ? void 0 : env
6803
6759
  };
6760
+ let candidate;
6761
+ if (isCodexProvider(existing)) {
6762
+ const codex = {
6763
+ ...existing.codex,
6764
+ ...input.codex,
6765
+ ...existing.codex?.models === void 0 && input.codex?.models === void 0 ? {} : { models: { ...existing.codex?.models, ...input.codex?.models } }
6766
+ };
6767
+ candidate = { ...existing, ...shared, codex: isEmptyCodexConfig(codex) ? void 0 : codex };
6768
+ } else {
6769
+ candidate = { ...existing, ...shared, ...input.baseUrl === void 0 ? {} : { baseUrl: input.baseUrl } };
6770
+ }
6771
+ const parsed = ProviderSchema.safeParse(candidate);
6772
+ if (!parsed.success) {
6773
+ throw new ConfigValidationError(providerJsonPath(paths, name), parsed.error.issues);
6774
+ }
6775
+ writeJsonAtomic(providerJsonPath(paths, name), parsed.data);
6776
+ return parsed.data;
6804
6777
  }
6805
-
6806
- // src/usage/store.ts
6807
- var DAY_MS2 = 864e5;
6808
- var DAYS_IN_WEEK = 7;
6809
- var USAGE_RETENTION_MS = DAYS_IN_WEEK * DAY_MS2;
6810
-
6811
- // src/usage/poolPick.ts
6812
- var SCHEMA_VERSION = 1;
6813
- var PickEntrySchema = z9.strictObject({ identity: z9.string().min(1), at: z9.iso.datetime() });
6814
- var PicksFileSchema = z9.strictObject({
6815
- schemaVersion: z9.literal(SCHEMA_VERSION),
6816
- picks: z9.record(z9.string().min(1), PickEntrySchema)
6817
- });
6818
- function errorMessage(error) {
6819
- return error instanceof Error ? error.message : String(error);
6778
+ function removeProvider(paths, name) {
6779
+ if (!providerExists(paths, name)) {
6780
+ throw new ProviderNotFoundError(name);
6781
+ }
6782
+ fs6.rmSync(providerJsonPath(paths, name));
6820
6783
  }
6821
- function loadPoolMembers(fs12, paths, identities, nowMs) {
6822
- const windowStartMs = nowMs - FIVE_HOUR_WINDOW_MS;
6823
- const log = readUsageLog(fs12, paths.usageLogDir, { sinceMs: windowStartMs });
6824
- return identities.map((identity) => {
6825
- const records = log.records.filter((record) => record.identity === identity && record.provider === ANTHROPIC_PROVIDER && Date.parse(record.at) >= windowStartMs);
6826
- try {
6827
- const snapshot = readUsageSnapshot(fs12, paths.usageSnapshotsDir, identity);
6828
- const account = readAccountMetadata(fs12, paths.identitiesDir, identity) ?? snapshot?.account;
6829
- return { identity, records, ...snapshot === void 0 ? {} : { snapshot }, ...account === void 0 ? {} : { account } };
6830
- } catch (error) {
6831
- if (error instanceof UsageSnapshotError || error instanceof AccountMetadataError) {
6832
- return { identity, records, readError: errorMessage(error) };
6833
- }
6834
- throw error;
6784
+ function cascadeProviderName(cascade) {
6785
+ let name;
6786
+ for (const layer of assembleCascade(cascade).layers) {
6787
+ if (layer.launch?.provider !== void 0) {
6788
+ name = layer.launch.provider;
6835
6789
  }
6836
- });
6790
+ }
6791
+ return name;
6837
6792
  }
6838
- function rankPoolFromStore(params) {
6839
- return rankPool({
6840
- members: loadPoolMembers(params.fs, params.paths, params.identities, params.nowMs),
6841
- nowMs: params.nowMs,
6842
- resuming: params.resuming,
6843
- ...params.sticky === void 0 ? {} : { sticky: params.sticky }
6844
- });
6793
+ function resolveProvider(params) {
6794
+ const cliName = params.cliProvider !== void 0 && params.cliProvider !== "" ? params.cliProvider : void 0;
6795
+ const name = cliName ?? (params.cascade === void 0 ? void 0 : cascadeProviderName(params.cascade));
6796
+ if (name === void 0) {
6797
+ return void 0;
6798
+ }
6799
+ const definition = loadProvider(params.paths.providersDir, name, params.port);
6800
+ if (definition === void 0) {
6801
+ const known = listProviderNames(params.port.readdir, params.paths.providersDir);
6802
+ return {
6803
+ ok: false,
6804
+ status: 1,
6805
+ message: `agent-shim: no provider named "${name}". ` + (known.length > 0 ? `Known providers: ${known.join(", ")}.` : "No providers are defined yet; run `agent-shim provider add`.")
6806
+ };
6807
+ }
6808
+ if (params.credentials === void 0) {
6809
+ return { ok: false, status: 1, message: `agent-shim: provider ${name} needs its credential resolved, but this launcher has no credential port wired` };
6810
+ }
6811
+ const resolution = resolveCredential({ credential: definition.credential, env: params.env, port: params.credentials, subject: `provider ${name}` });
6812
+ if (!resolution.ok) {
6813
+ return { ok: false, status: CREDENTIAL_UNAVAILABLE_EXIT, message: resolution.message };
6814
+ }
6815
+ return { ok: true, provider: { name, definition, credential: resolution.credential }, warnings: resolution.credential.warnings };
6845
6816
  }
6846
- function readPicksFile(fs12, file) {
6847
- const empty = { schemaVersion: SCHEMA_VERSION, picks: {} };
6848
- const raw = fs12.readFileUtf8(file);
6849
- if (raw === void 0) {
6850
- return { file: empty };
6817
+
6818
+ // src/directoryRulesStore.ts
6819
+ var DirectoryRuleNotFoundError = class extends CliError {
6820
+ constructor(rulePath) {
6821
+ super(`No directory rule found for path "${rulePath}".`);
6822
+ this.rulePath = rulePath;
6823
+ this.name = "DirectoryRuleNotFoundError";
6851
6824
  }
6852
- let parsed;
6853
- try {
6854
- parsed = JSON.parse(raw);
6855
- } catch {
6856
- return { file: empty, problem: `${file} is not valid JSON` };
6825
+ rulePath;
6826
+ };
6827
+ var DirectoryRuleMissingTargetError = class extends CliError {
6828
+ constructor() {
6829
+ super("A directory rule must set at least one of --config-profile or --identity.");
6830
+ this.name = "DirectoryRuleMissingTargetError";
6857
6831
  }
6858
- const result = PicksFileSchema.safeParse(parsed);
6859
- return result.success ? { file: result.data } : { file: empty, problem: `${file} is not a picks file this agent-shim can read` };
6832
+ };
6833
+ var DirectoryRuleAlreadyExistsError = class extends CliError {
6834
+ constructor(rulePath) {
6835
+ super(`A directory rule for path "${rulePath}" already exists. Use \`agent-shim rule set\` to change it.`);
6836
+ this.rulePath = rulePath;
6837
+ this.name = "DirectoryRuleAlreadyExistsError";
6838
+ }
6839
+ rulePath;
6840
+ };
6841
+ function readDirectoryRules(paths) {
6842
+ return readJson(paths.directoryRulesFile, DirectoryRulesSchema) ?? { rules: [] };
6860
6843
  }
6861
- function readStickyPick(fs12, picksFile, directory) {
6862
- const read = readPicksFile(fs12, picksFile);
6863
- const entry = read.file.picks[directory];
6864
- return { ...entry === void 0 ? {} : { sticky: entry }, ...read.problem === void 0 ? {} : { problem: read.problem } };
6844
+ function writeDirectoryRules(paths, rules) {
6845
+ const parsed = DirectoryRulesSchema.safeParse(rules);
6846
+ if (!parsed.success) {
6847
+ throw new ConfigValidationError(paths.directoryRulesFile, parsed.error.issues);
6848
+ }
6849
+ writeJsonAtomic(paths.directoryRulesFile, parsed.data);
6865
6850
  }
6866
- function recordStickyPick(fs12, picksFile, directory, identity, nowMs) {
6867
- const current = readPicksFile(fs12, picksFile).file.picks;
6868
- const kept = Object.entries(current).filter(([, entry]) => nowMs - Date.parse(entry.at) <= USAGE_RETENTION_MS);
6869
- const next = { schemaVersion: SCHEMA_VERSION, picks: { ...Object.fromEntries(kept), [directory]: { identity, at: new Date(nowMs).toISOString() } } };
6870
- fs12.mkdirPrivate(path19.dirname(picksFile));
6871
- fs12.writeFilePrivate(picksFile, `${JSON.stringify(next, null, 2)}
6872
- `);
6851
+ function listDirectoryRules(paths) {
6852
+ return readDirectoryRules(paths).rules;
6873
6853
  }
6874
-
6875
- // src/launcher/pool.ts
6876
- var RESUME_FLAGS = /* @__PURE__ */ new Set(["--continue", "-c", "--resume", "-r"]);
6877
- var TERMINATOR = "--";
6878
- var REASONS_LOGGED = 3;
6879
- function splitMembers(pool, paths, fs12) {
6880
- const exists = (name) => loadIdentity(paths.identitiesDir, name, fs12) !== void 0;
6881
- return { present: pool.identities.filter(exists), missing: pool.identities.filter((name) => !exists(name)) };
6854
+ function addDirectoryRule(paths, rulePath, options) {
6855
+ if (options.configProfile === void 0 && options.identity === void 0) {
6856
+ throw new DirectoryRuleMissingTargetError();
6857
+ }
6858
+ const current = readDirectoryRules(paths);
6859
+ if (current.rules.some((rule) => rule.path === rulePath)) {
6860
+ throw new DirectoryRuleAlreadyExistsError(rulePath);
6861
+ }
6862
+ const created = buildNewRule(rulePath, options);
6863
+ writeDirectoryRules(paths, { ...current, rules: [...current.rules, created] });
6864
+ return created;
6882
6865
  }
6883
- function isResuming(passthrough) {
6884
- const end = passthrough.indexOf(TERMINATOR);
6885
- return (end === -1 ? passthrough : passthrough.slice(0, end)).some((token) => RESUME_FLAGS.has(token));
6866
+ function ruleHasEffect(rule) {
6867
+ return rule.configProfile !== void 0 || rule.identity !== void 0 || rule.categories !== void 0 || rule.entries !== void 0 || rule.launch !== void 0;
6886
6868
  }
6887
- function resolvePoolLaunch(params) {
6888
- const { poolName, log } = params;
6889
- const pool = params.pools?.[poolName];
6890
- if (pool === void 0) {
6891
- return { ok: false, message: `agent-shim: no pool named "${poolName}" (selected via ${params.selectedVia}). Run \`agent-shim pool add ${poolName} --identity <name>...\` first.` };
6869
+ function updateDirectoryRule(paths, rulePath, options) {
6870
+ const current = readDirectoryRules(paths);
6871
+ const index = current.rules.findIndex((rule) => rule.path === rulePath);
6872
+ const existing = index === -1 ? void 0 : current.rules[index];
6873
+ if (existing === void 0) {
6874
+ throw new DirectoryRuleNotFoundError(rulePath);
6892
6875
  }
6893
- const { present: identities, missing } = splitMembers(pool, params.paths, params.fs);
6894
- for (const name of missing) {
6895
- log.warn(`agent-shim: pool "${poolName}" names identity "${name}", which does not exist; skipping it`);
6876
+ const { configProfile, identity, ...rest } = existing;
6877
+ const nextConfigProfile = options.configProfile === void 0 ? configProfile : options.configProfile === false ? void 0 : options.configProfile;
6878
+ const nextIdentity = options.identity === void 0 ? identity : options.identity === false ? void 0 : options.identity;
6879
+ const updated = {
6880
+ ...rest,
6881
+ ...nextConfigProfile === void 0 ? {} : { configProfile: nextConfigProfile },
6882
+ ...nextIdentity === void 0 ? {} : { identity: nextIdentity }
6883
+ };
6884
+ if (!ruleHasEffect(updated)) {
6885
+ throw new DirectoryRuleMissingTargetError();
6896
6886
  }
6897
- if (identities.length === 0) {
6898
- return { ok: false, message: `agent-shim: no member of pool "${poolName}" is an existing identity.` };
6887
+ const nextRules = [...current.rules];
6888
+ nextRules[index] = updated;
6889
+ writeDirectoryRules(paths, { ...current, rules: nextRules });
6890
+ return updated;
6891
+ }
6892
+ function buildNewRule(rulePath, options) {
6893
+ return {
6894
+ path: rulePath,
6895
+ ...options.configProfile !== void 0 ? { configProfile: options.configProfile } : {},
6896
+ ...options.identity !== void 0 ? { identity: options.identity } : {}
6897
+ };
6898
+ }
6899
+ function removeDirectoryRule(paths, rulePath) {
6900
+ const current = readDirectoryRules(paths);
6901
+ const nextRules = current.rules.filter((rule) => rule.path !== rulePath);
6902
+ if (nextRules.length === current.rules.length) {
6903
+ throw new DirectoryRuleNotFoundError(rulePath);
6899
6904
  }
6900
- const resuming = isResuming(params.passthrough);
6901
- const stickyRead = readStickyPick(params.usageFs, params.paths.usagePicksFile, params.cwd);
6902
- if (stickyRead.problem !== void 0) {
6903
- log.warn(`agent-shim: ${stickyRead.problem}; ignoring the last-pick record`);
6905
+ writeDirectoryRules(paths, { ...current, rules: nextRules });
6906
+ }
6907
+
6908
+ // src/checkReport.ts
6909
+ import os4 from "node:os";
6910
+
6911
+ // src/launcher/cascade.ts
6912
+ import path19 from "node:path";
6913
+
6914
+ // src/pathNorm.ts
6915
+ import path18 from "node:path";
6916
+ function expandTilde(value, home) {
6917
+ if (value === "~") {
6918
+ return home;
6904
6919
  }
6905
- for (; ; ) {
6906
- const nowMs = params.now();
6907
- const ranking = rankPoolFromStore({ fs: params.usageFs, paths: params.paths, identities, nowMs, resuming, ...stickyRead.sticky === void 0 ? {} : { sticky: stickyRead.sticky } });
6908
- const { pick, earliestReturn } = ranking;
6909
- if (pick !== void 0) {
6910
- recordStickyPick(params.usageFs, params.paths.usagePicksFile, params.cwd, pick.identity, nowMs);
6911
- return { ok: true, identity: pick.identity, explanation: `pool ${poolName}: ${pick.reasons.slice(0, REASONS_LOGGED).join("; ")}` };
6912
- }
6913
- if (earliestReturn === void 0) {
6914
- return { ok: false, message: `agent-shim: no member of pool "${poolName}" can be picked.` };
6920
+ if (value.startsWith("~/")) {
6921
+ return path18.join(home, value.slice(2));
6922
+ }
6923
+ return value;
6924
+ }
6925
+ function normaliseRulePath(rulePath, home) {
6926
+ return path18.resolve(expandTilde(rulePath, home));
6927
+ }
6928
+
6929
+ // src/launcher/cascade.ts
6930
+ var PORTABLE_CONFIG_FILENAME = ".agent-shim.json";
6931
+ var PORTABLE_LOCAL_CONFIG_FILENAME = ".agent-shim.local.json";
6932
+ function loadCascadeInput(params) {
6933
+ const globalConfig = loadConfigFile(params.paths.globalConfigFile, GlobalConfigSchema, params.read);
6934
+ const directoryRules = loadConfigFile(params.paths.directoryRulesFile, DirectoryRulesSchema, params.read);
6935
+ const loadProfile = (name) => {
6936
+ const filepath = path19.join(params.paths.configProfilesDir, `${name}.json`);
6937
+ const loaded = loadConfigFile(filepath, ConfigProfileSchema, params.read);
6938
+ if (loaded === void 0) {
6939
+ return void 0;
6915
6940
  }
6916
- const until = new Date(earliestReturn.atMs).toISOString();
6917
- if (!params.wait) {
6941
+ return { name, profile: loaded.config, entryOrder: loaded.entryOrder, filepath: loaded.filepath };
6942
+ };
6943
+ const limit = globalConfig?.config.walkUpLimit;
6944
+ const dirs = walkDirectoryAncestors(params.cwd, {
6945
+ home: params.home,
6946
+ ...limit === void 0 ? {} : { limit: expandTilde(limit, params.home) },
6947
+ ...params.isReadable === void 0 ? {} : { isReadable: params.isReadable }
6948
+ });
6949
+ const levels = [];
6950
+ for (const dir of dirs) {
6951
+ const portable = loadConfigFile(path19.join(dir, PORTABLE_CONFIG_FILENAME), PortableConfigSchema, params.read) ?? loadConfigFile(path19.join(dir, LEGACY_PORTABLE_CONFIG_FILENAME), PortableConfigSchema, params.read);
6952
+ const portableLocal = loadConfigFile(path19.join(dir, PORTABLE_LOCAL_CONFIG_FILENAME), PortableConfigSchema, params.read) ?? loadConfigFile(path19.join(dir, LEGACY_PORTABLE_LOCAL_CONFIG_FILENAME), PortableConfigSchema, params.read);
6953
+ const rules = (directoryRules?.config.rules ?? []).map((rule, index) => ({ rule, index })).filter(({ rule }) => normaliseRulePath(rule.path, params.home) === dir).map(({ rule, index }) => {
6954
+ const entryOrder = directoryRules?.ruleEntryOrders[index];
6918
6955
  return {
6919
- ok: false,
6920
- message: `agent-shim: every member of pool "${poolName}" is refused right now; the earliest, ${earliestReturn.identity}, returns at ${until} (in ${formatAge(earliestReturn.atMs - nowMs)}). Pass --wait to sleep until then.`
6956
+ rule,
6957
+ ...entryOrder === void 0 ? {} : { entryOrder },
6958
+ filepath: params.paths.directoryRulesFile
6921
6959
  };
6960
+ });
6961
+ if (portable === void 0 && portableLocal === void 0 && rules.length === 0) {
6962
+ continue;
6922
6963
  }
6923
- log.info(`agent-shim: every member of pool "${poolName}" is refused; waiting until ${until} for ${earliestReturn.identity}`);
6924
- params.sleep(Math.max(earliestReturn.atMs - nowMs, 0));
6964
+ levels.push({
6965
+ dir,
6966
+ ...portable === void 0 ? {} : { portable: { config: portable.config, entryOrder: portable.entryOrder, filepath: portable.filepath } },
6967
+ ...rules.length === 0 ? {} : { rules },
6968
+ ...portableLocal === void 0 ? {} : { portableLocal: { config: portableLocal.config, entryOrder: portableLocal.entryOrder, filepath: portableLocal.filepath } }
6969
+ });
6925
6970
  }
6926
- }
6927
-
6928
- // src/poolPickReport.ts
6929
- function candidateView(candidate) {
6930
- return {
6931
- identity: candidate.identity,
6932
- class: candidate.class,
6933
- ...candidate.score === void 0 ? {} : { score: candidate.score },
6934
- feasible: candidate.feasible,
6935
- ...candidate.blockedUntilMs === void 0 ? {} : { blockedUntil: new Date(candidate.blockedUntilMs).toISOString() },
6936
- plan: candidate.plan,
6937
- reasons: candidate.reasons
6971
+ const input = {
6972
+ home: params.home,
6973
+ ...globalConfig === void 0 ? {} : { globalConfig: { config: globalConfig.config, entryOrder: globalConfig.entryOrder, filepath: globalConfig.filepath } },
6974
+ ...params.baseConfigProfile === void 0 ? {} : { baseConfigProfile: params.baseConfigProfile },
6975
+ loadProfile,
6976
+ levels,
6977
+ ...params.cliOverride === void 0 ? {} : { cliOverride: params.cliOverride }
6938
6978
  };
6979
+ return { input, ...globalConfig === void 0 ? {} : { globalConfig: globalConfig.config } };
6939
6980
  }
6940
- function collectPoolPick(params) {
6941
- const { present, missing } = splitMembers(params.pool, params.paths, params.fs);
6942
- const sticky = readStickyPick(params.usageFs, params.paths.usagePicksFile, params.directory);
6943
- const ranking = rankPoolFromStore({
6944
- fs: params.usageFs,
6945
- paths: params.paths,
6946
- identities: present,
6947
- nowMs: params.nowMs,
6948
- resuming: false,
6949
- ...sticky.sticky === void 0 ? {} : { sticky: sticky.sticky }
6950
- });
6951
- return {
6952
- pool: params.poolName,
6953
- directory: params.directory,
6954
- ...ranking.pick === void 0 ? {} : { pick: ranking.pick.identity },
6955
- candidates: ranking.candidates.map(candidateView),
6956
- missing,
6957
- ...ranking.earliestReturn === void 0 ? {} : { earliestReturn: { identity: ranking.earliestReturn.identity, at: new Date(ranking.earliestReturn.atMs).toISOString() } },
6958
- ...sticky.problem === void 0 ? {} : { stickyProblem: sticky.problem }
6959
- };
6981
+ function readDirectorySelections(loaded) {
6982
+ let identity;
6983
+ let configProfile;
6984
+ for (const level of loaded.input.levels ?? []) {
6985
+ for (const source of [level.portable?.config, ...(level.rules ?? []).map((entry) => entry.rule), level.portableLocal?.config]) {
6986
+ if (source === void 0) {
6987
+ continue;
6988
+ }
6989
+ if (source.identity !== void 0) {
6990
+ identity = source.identity;
6991
+ }
6992
+ if (source.configProfile !== void 0) {
6993
+ configProfile = source.configProfile;
6994
+ }
6995
+ }
6996
+ }
6997
+ return { ...identity === void 0 ? {} : { identity }, ...configProfile === void 0 ? {} : { configProfile } };
6960
6998
  }
6961
6999
 
6962
7000
  // src/realCredentialCache.ts
@@ -7519,7 +7557,7 @@ function realResolveClaudeBinary(isOwnBinary) {
7519
7557
 
7520
7558
  // src/checkReport.ts
7521
7559
  import path23 from "node:path";
7522
- import { z as z10 } from "zod";
7560
+ import { z as z12 } from "zod";
7523
7561
 
7524
7562
  // src/cli/bool.ts
7525
7563
  function parseBoolWord(input) {
@@ -7634,9 +7672,9 @@ function lookupKeychainService(run, farmRoot) {
7634
7672
  note: serviceName === void 0 ? `A Keychain entry was found for account "${farmRoot}" but its service name could not be parsed from security's output.` : `Keychain service name for this identity: "${serviceName}".`
7635
7673
  };
7636
7674
  }
7637
- var SettingsSecretsShapeSchema = z10.looseObject({
7638
- env: z10.record(z10.string(), z10.unknown()).optional(),
7639
- hooks: z10.record(z10.string(), z10.array(z10.unknown())).optional()
7675
+ var SettingsSecretsShapeSchema = z12.looseObject({
7676
+ env: z12.record(z12.string(), z12.unknown()).optional(),
7677
+ hooks: z12.record(z12.string(), z12.array(z12.unknown())).optional()
7640
7678
  });
7641
7679
  function hookCommandCountOf(group) {
7642
7680
  if (typeof group !== "object" || group === null) {
@@ -7930,11 +7968,11 @@ function collectCheckReport(params) {
7930
7968
  // src/claudeShim.ts
7931
7969
  import fs9 from "node:fs";
7932
7970
  import path24 from "node:path";
7933
- import { z as z11 } from "zod";
7934
- var ClaudeShimStateSchema = z11.strictObject({
7935
- targetPath: z11.string().min(1),
7936
- method: z11.enum(["hardlink", "copy"]),
7937
- installedAtMs: z11.number()
7971
+ import { z as z13 } from "zod";
7972
+ var ClaudeShimStateSchema = z13.strictObject({
7973
+ targetPath: z13.string().min(1),
7974
+ method: z13.enum(["hardlink", "copy"]),
7975
+ installedAtMs: z13.number()
7938
7976
  });
7939
7977
  function commandFilename(ownExecutablePath, commandName) {
7940
7978
  return ownExecutablePath.toLowerCase().endsWith(".exe") ? `${commandName}.exe` : commandName;
@@ -9389,8 +9427,11 @@ export {
9389
9427
  InvalidProviderNameError,
9390
9428
  LaunchRefusedError,
9391
9429
  LegacyProviderFileError,
9430
+ PROMPT_CACHE_TTL_MS,
9431
+ PlanClassSchema,
9392
9432
  PoolNameSchema,
9393
9433
  PoolNotFoundError,
9434
+ PoolPickReportSchema,
9394
9435
  PoolSchema,
9395
9436
  PortableConfigSchema,
9396
9437
  ProfileAlreadyExistsError,
@@ -9427,16 +9468,19 @@ export {
9427
9468
  checkReportToJson,
9428
9469
  collectCheckReport,
9429
9470
  collectDoctorReport,
9471
+ collectPoolPick,
9430
9472
  createLeafCache,
9431
9473
  createProfile,
9432
9474
  createRcControlHandler,
9433
9475
  createRcSessionTracker,
9434
9476
  describeProviderEndpoint,
9435
9477
  detectAmbientCredential,
9478
+ effectiveWindow,
9436
9479
  ensureCa,
9437
9480
  ensureFrontDoor,
9438
9481
  ensureHeadroom,
9439
9482
  evaluateAmbientCredentialGuard,
9483
+ evaluateWhen,
9440
9484
  formatAmbientCredentialGuardMessage,
9441
9485
  formatCheckReport,
9442
9486
  formatDoctorReport,
@@ -9454,13 +9498,16 @@ export {
9454
9498
  listProfiles,
9455
9499
  listProviders,
9456
9500
  listUsageSnapshots,
9501
+ matchBranch,
9457
9502
  mintLeaf,
9458
9503
  observingRoutedRoute,
9459
9504
  parseConnectTarget,
9505
+ planOf,
9460
9506
  prepareClaudeLaunch,
9461
9507
  prepareLaunch,
9462
9508
  profileExists,
9463
9509
  providerExists,
9510
+ rankPool,
9464
9511
  rcEventWriteResultFromAnswer,
9465
9512
  rcObserverAsPassthrough,
9466
9513
  readActiveIdentity,