agent-shim 0.0.0 → 7.0.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.
Files changed (44) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +233 -0
  3. package/dist/cli.cjs +69549 -0
  4. package/dist/index.cjs +3620 -0
  5. package/dist/index.mjs +3545 -0
  6. package/dist/types/cliError.d.ts +27 -0
  7. package/dist/types/config/classify.d.ts +49 -0
  8. package/dist/types/config/load.d.ts +42 -0
  9. package/dist/types/config/schema.d.ts +746 -0
  10. package/dist/types/config/store.d.ts +46 -0
  11. package/dist/types/frontdoor/capability.d.ts +19 -0
  12. package/dist/types/frontdoor/connect.d.ts +179 -0
  13. package/dist/types/frontdoor/ensure.d.ts +34 -0
  14. package/dist/types/frontdoor/probe.d.ts +13 -0
  15. package/dist/types/frontdoor/state.d.ts +50 -0
  16. package/dist/types/frontdoor/supervisor.d.ts +48 -0
  17. package/dist/types/frontdoor/x509.d.ts +11 -0
  18. package/dist/types/headroom/ensure.d.ts +31 -0
  19. package/dist/types/headroom/settings.d.ts +13 -0
  20. package/dist/types/headroom/source.d.ts +8 -0
  21. package/dist/types/headroom/state.d.ts +77 -0
  22. package/dist/types/headroom/supervisor.d.ts +107 -0
  23. package/dist/types/index.d.ts +21 -0
  24. package/dist/types/launcher/farm.d.ts +165 -0
  25. package/dist/types/launcher/guard.d.ts +57 -0
  26. package/dist/types/launcher/lock.d.ts +41 -0
  27. package/dist/types/launcher/ports.d.ts +135 -0
  28. package/dist/types/legacy.d.ts +24 -0
  29. package/dist/types/paths.d.ts +79 -0
  30. package/dist/types/resolve/conditions.d.ts +37 -0
  31. package/dist/types/resolve/decide.d.ts +45 -0
  32. package/dist/types/resolve/extends.d.ts +41 -0
  33. package/dist/types/resolve/flatten.d.ts +13 -0
  34. package/dist/types/resolve/match.d.ts +58 -0
  35. package/dist/types/resolve/pipeline.d.ts +30 -0
  36. package/dist/types/resolve/plan.d.ts +57 -0
  37. package/dist/types/resolve/projects.d.ts +61 -0
  38. package/dist/types/resolve/reconcile.d.ts +80 -0
  39. package/dist/types/resolve/types.d.ts +159 -0
  40. package/dist/types/resolve/walk.d.ts +85 -0
  41. package/dist/types/usage/account.d.ts +20 -0
  42. package/dist/types/usage/read.d.ts +70 -0
  43. package/dist/types/usage/schema.d.ts +318 -0
  44. package/package.json +91 -2
@@ -0,0 +1,107 @@
1
+ import { type HeadroomGlobalConfig } from "../config/schema";
2
+ import type { LayoutPaths } from "../paths";
3
+ import { type HeadroomSettings } from "./settings";
4
+ import { type HeadroomFs } from "./state";
5
+ /** Everything the supervisor needs to run the daemon, resolved from the global config and the filesystem before it starts. */
6
+ export interface HeadroomSupervisorConfig {
7
+ /** The install spec as configured (`headroom.source`), already defaulted. */
8
+ readonly source: string;
9
+ /** `headroom.idleShutdownMinutes`, already defaulted. */
10
+ readonly idleShutdownMinutes: number;
11
+ /** The token-saving settings (`mode`, `targetRatio`, `ccr`, the rollout channel and its opt-ins), as configured; unset fields leave headroom on its own defaults. */
12
+ readonly settings: HeadroomSettings;
13
+ }
14
+ /** How the supervisor resolves the configured source against its defaults. */
15
+ export declare function resolveSupervisorConfig(configured: Readonly<HeadroomGlobalConfig>): HeadroomSupervisorConfig;
16
+ /**
17
+ * Every effect the supervisor performs, injected so the whole lifecycle is testable against fakes: no real process, port, clock, or HTTP call ever happens in a unit test.
18
+ */
19
+ export interface SupervisorPorts {
20
+ readonly fs: HeadroomFs;
21
+ readonly paths: LayoutPaths;
22
+ readonly ownPid: number;
23
+ readonly now: () => number;
24
+ /**
25
+ * Waits `ms`, yielding to the event loop while it does. Deliberately NOT a synchronous Atomics.wait-style sleep: the real supervisor learns of its child's death through the ChildProcess exit event, and a blocking sleep would stop the event loop from ever delivering it (the exact failure that left an unreaped zombie answering liveness checks as alive).
26
+ */
27
+ readonly sleep: (ms: number) => Promise<void>;
28
+ /**
29
+ * Zombie-aware liveness, and for a daemon this supervisor spawned, exit-event-backed: the implementation keeps the spawned ChildProcess handle, consumes its exit event (which is also what reaps it), and reports the pid as not running from that moment. A bare signal-0 check is not enough, because a defunct process still answers it.
30
+ */
31
+ readonly isRunning: (pid: number) => boolean;
32
+ /** Returns a free loopback port. Async because the only reliable way to reserve one is to bind and release a socket. */
33
+ readonly freePort: () => Promise<number>;
34
+ /** Whether nothing is currently listening on one specific loopback port: the check that decides whether the sticky `lastPort` can be reused. The same bind-and-release technique `freePort` uses, applied to a number the caller already cares about. */
35
+ readonly isPortFree: (port: number) => Promise<boolean>;
36
+ /**
37
+ * Starts `headroom proxy` bound to `port` with the given allowlist, returning its pid. Output goes to the daemon log. The implementation must keep the ChildProcess handle and attach an exit listener (detaching the process is fine; unref'ing a child you still need events from is not, since without the listener nothing reaps it and it lingers as a zombie).
38
+ */
39
+ readonly spawnHeadroom: (port: number, allowlist: readonly string[], settings: Readonly<HeadroomSettings>) => number;
40
+ /** Reads the headroom config block as it stands on disk now. The supervisor calls it every tick, so a changed `source` or setting is noticed while the daemon runs and applied by the same quiet-registry restart an allowlist change gets. */
41
+ readonly readConfig: () => HeadroomSupervisorConfig;
42
+ /** Stops a process the supervisor owns, escalating SIGTERM to SIGKILL on a bounded timeout (see `stopSupervisedProcess`). */
43
+ readonly stopProcess: (pid: number) => void;
44
+ /** True once `GET /readyz` on the port succeeds. */
45
+ readonly ready: (port: number) => Promise<boolean>;
46
+ /** Runs `uv tool install <spec>`, logging output to the daemon log. Returns ok=false with the error when `uv` is absent or the install fails. */
47
+ readonly install: (spec: string) => {
48
+ readonly ok: boolean;
49
+ readonly error?: string;
50
+ };
51
+ /** The installed `headroom --version` output, or undefined when the binary is not on PATH. */
52
+ readonly headroomVersion: () => string | undefined;
53
+ /** The commit the installed headroom was built from, as the install itself records it (`direct_url.json`), or undefined when it was not installed from a git source. */
54
+ readonly installedCommit: () => string | undefined;
55
+ readonly log: (line: string) => void;
56
+ }
57
+ /** First retry delay after a failed start. Half a second keeps a transient blip invisible to the next launch. */
58
+ export declare const HEADROOM_BACKOFF_BASE_MS = 500;
59
+ /** Ceiling for the retry delay: with no live sessions there is no user waiting, so half a minute between attempts is the most the daemon log should grow. */
60
+ export declare const HEADROOM_BACKOFF_CAP_MS = 30000;
61
+ /** Consecutive failed starts before the supervisor gives up, records `lastError`, and exits: a proxy that cannot come up five times in a row is broken, not unlucky. */
62
+ export declare const HEADROOM_START_RETRY_BUDGET = 5;
63
+ /** The supervisor's own tick rate for crash detection, drift checks, and idle shutdown. */
64
+ export declare const HEADROOM_POLL_MS = 1000;
65
+ /** The process-control primitives the escalating stop needs, injected so the escalation itself is unit-testable against a process that ignores SIGTERM. */
66
+ export interface StopProcessPrimitives {
67
+ /** Delivers a signal; throws when the target is already gone. */
68
+ readonly signal: (pid: number, signal: NodeJS.Signals) => void;
69
+ /** Zombie-aware liveness: a defunct process is not running. */
70
+ readonly isRunning: (pid: number) => boolean;
71
+ /** Synchronous wait between liveness polls; blocking is correct here, since nothing productive can happen until the target is gone or the grace expires. */
72
+ readonly waitMs: (ms: number) => void;
73
+ readonly now: () => number;
74
+ }
75
+ /** How `stopSupervisedProcess` ended. `still-running` means even SIGKILL did not clear the pid within its grace (an uninterruptible process); callers log it loudly. */
76
+ export type StopOutcome = "exited-on-term" | "killed" | "still-running";
77
+ /** Grace after SIGTERM before escalating to SIGKILL: enough for a proxy to drain an in-flight request, short enough that a restart is not visibly stalled. This proxy's server has been observed to ignore SIGTERM outright, so the escalation is not optional. */
78
+ export declare const TERM_GRACE_MS = 1500;
79
+ /** Grace after SIGKILL before declaring the process unkillable. SIGKILL is immediate at the kernel level; this only bounds the wait for the scheduler to reap it. */
80
+ export declare const KILL_GRACE_MS = 1500;
81
+ /**
82
+ * Stops a process the supervisor owns: SIGTERM, wait up to `TERM_GRACE_MS`; SIGKILL, wait up to `KILL_GRACE_MS`; then report. Liveness during the waits must be zombie-aware: an unreaped target still answers signal 0, which would read as "survived SIGKILL".
83
+ */
84
+ export declare function stopSupervisedProcess(pid: number, primitives: StopProcessPrimitives): StopOutcome;
85
+ /** The backoff for attempt `attempt` (1-based): base doubled per failure, capped. */
86
+ export declare function backoffForAttempt(attempt: number): number;
87
+ /**
88
+ * Whether an installed `headroom --version` string satisfies the configured source. A source carrying a PEP 440 specifier (`headroom>=0.39`, `headroom==0.39.1`) is checked against the version; anything else (a bare git URL, the default) carries no version to check and is satisfied by any install, with drift handled by comparing `installedSource` instead.
89
+ */
90
+ export declare function versionSatisfies(version: string | undefined, source: string): boolean;
91
+ /** Options for `runSupervisor`. */
92
+ export interface RunSupervisorOptions {
93
+ /**
94
+ * Bounds the supervisor loop's iterations. The real supervisor runs until it exits on its own (idle shutdown or fatal error); a bounded run is how tests observe steady-state behaviour (drift deferral, restart-on-crash, pruning) without an infinite loop, and such a run returns `HEADROOM_SUPERVISOR_STILL_RUNNING` instead of an exit code.
95
+ */
96
+ readonly tickLimit?: number;
97
+ }
98
+ /** Returned by a tick-bounded `runSupervisor` that reached its limit while still managing the daemon: not an exit code, and never produced without `tickLimit`. */
99
+ export declare const HEADROOM_SUPERVISOR_STILL_RUNNING = -1;
100
+ /**
101
+ * Runs the headroom supervisor loop: install the daemon, start it, keep it alive (for crash or drift), and stop it after the configured idle period with an empty session registry. Returns the process exit code (0 for an idle shutdown, 1 for a fatal setup failure).
102
+ *
103
+ * Every effect flows through `ports`, so the whole loop is unit-testable with a fake clock, filesystem, and processes. The loop awaits its sleeps, which in the real implementation run on a timer: between ticks the event loop turns, the spawned child's exit event is delivered (and the child thereby reaped), and the next tick's liveness check reads the truth.
104
+ */
105
+ export declare function runSupervisor(initialConfig: HeadroomSupervisorConfig, ports: SupervisorPorts, options?: RunSupervisorOptions): Promise<number>;
106
+ /** The allowlist as it stands right now: every provider's upstream (the front door's origin for a codex provider, which headroom forwards codex traffic back to) plus Claude Code's own API. */
107
+ export declare function currentHeadroomAllowlist(fs: HeadroomFs, paths: LayoutPaths): readonly string[];
@@ -0,0 +1,21 @@
1
+ /**
2
+ * The library surface of agent-shim: the pure, port-injected parts other tools can call in-process instead of shelling out to the CLI. Four groups, none of which imports interactive prompting or argument parsing:
3
+ *
4
+ * - identity and configuration-profile resolution and farm sync (`resolveDecisions`, `resyncFarm`, `buildEntryFacts`, ...), pure over facts and ports passed in;
5
+ * - routing: the front door's Remote-Control-preserving, capability-authenticated CONNECT surface and its certificate authority, the front-door supervisor and ensure lifecycle, and the headroom supervisor and ensure lifecycle, each pure over its injected ports;
6
+ * - the ambient-credential guard;
7
+ * - the per-identity usage snapshot a statusline or launcher reads: its schema and the reader over an injected filesystem.
8
+ *
9
+ * Everything the CLI alone needs (commander wiring, prompts, `doctor`, `check`) is deliberately not exported here.
10
+ */
11
+ export { resolveDecisions, topLevelNames, type ResolveDecisionsInput, type ResolvedState } from "./resolve/pipeline";
12
+ export type { CompiledRule, Decision, Diagnostic, EliminatedRule, EntryFact, EntryFacts, FlattenedCascade, Layer, LayerId } from "./resolve/types";
13
+ export { buildEntryFacts, carryOver, FARM_MANIFEST_FILENAME, readFarmManifest, recoverFarm, recoveryDiagnostics, resyncFarm, type BuildEntryFactsParams, type CarryOverParams, type CarryOverResult, type RecoverFarmParams, type RecoveryResult, type ResyncFarmParams, type ResyncFarmResult, } from "./launcher/farm";
14
+ export { CONNECT_INTERCEPT_HOST, CONNECT_LIMITS, createLeafCache, ensureCa, forwardableHeaders, generateCa, isInterceptedHost, mintLeaf, parseConnectTarget, realConnectCertStore, realConnectEffects, ROUTED_PATH_PREFIX, servedByPipeline, startConnectServer, type CaMaterial, type ConnectCertStore, type ConnectEffects, type ConnectLimits, type ConnectServerConfig, type ConnectServerHandle, type ConnectTarget, type LeafCert, } from "./frontdoor/connect";
15
+ export { ensureFrontDoor, FrontDoorStartError, type EnsureFrontDoorPorts } from "./frontdoor/ensure";
16
+ export { runFrontDoorSupervisor, type FrontDoorSupervisorPorts, type RunFrontDoorSupervisorOptions } from "./frontdoor/supervisor";
17
+ export { listUsageSnapshots, readUsageSnapshot, snapshotPath, UsageSnapshotError, type UsageReadFs } from "./usage/read";
18
+ export { UsageSnapshotSchema, type UsageSnapshot } from "./usage/schema";
19
+ export { ensureHeadroom, HeadroomStartError, type EnsureHeadroomPorts } from "./headroom/ensure";
20
+ export { resolveSupervisorConfig, runSupervisor, type HeadroomSupervisorConfig, type RunSupervisorOptions, type SupervisorPorts } from "./headroom/supervisor";
21
+ export { AMBIENT_CREDENTIAL_VARS, detectAmbientCredential, evaluateAmbientCredentialGuard, formatAmbientCredentialGuardMessage, type AmbientCredentialDetection, type AmbientCredentialGuardResult, type AmbientCredentialVar, type EvaluateAmbientCredentialGuardParams, type InjectedCredential, } from "./launcher/guard";
@@ -0,0 +1,165 @@
1
+ import type { CategoryClassification, CategoryClassificationOverlay } from "../config/schema";
2
+ import { type FarmManifest } from "../resolve/reconcile";
3
+ import { type ResolvedState } from "../resolve/pipeline";
4
+ import type { CascadeInput } from "../resolve/walk";
5
+ import type { Diagnostic, EntryFacts } from "../resolve/types";
6
+ import type { FarmFs } from "./ports";
7
+ /** The manifest file each farm carries at its own root, recording what that resync built. Named distinctly from anything Claude Code itself writes into a config directory. A farm built under the former name carries `LEGACY_FARM_MANIFEST_FILENAME` instead, which is read as a fallback and never carried into the next farm, whose resync writes this name. */
8
+ export declare const FARM_MANIFEST_FILENAME = ".agent-shim-farm.json";
9
+ /** Reads and validates the manifest at a farm root, or undefined when it is missing, unparseable, or does not validate. */
10
+ export declare function readFarmManifest(fs: FarmFs, farmRoot: string): FarmManifest | undefined;
11
+ /** Everything `buildEntryFacts` needs. Everything except the tree itself is injected, so the fact manifest is reproducible from a fake filesystem, a fake clock, and a fake branch. */
12
+ export interface BuildEntryFactsParams {
13
+ readonly fs: FarmFs;
14
+ readonly claudeHome: string;
15
+ readonly home: string;
16
+ readonly cwd: string;
17
+ readonly nowMs: number;
18
+ readonly env: Readonly<Record<string, string | undefined>>;
19
+ readonly branch?: string;
20
+ readonly branchDetached?: boolean;
21
+ }
22
+ /**
23
+ * Walks the canonical `~/.claude` and builds the `EntryFacts` manifest the pure resolver consumes.
24
+ *
25
+ * Two properties matter more than the walk itself. Symlinks are never followed: a directory symlink escaping the tree is one entry, not a subtree, which keeps the walk finite and matches how the farm links such an entry back. And every directory's `latestMtimeMs`/`totalSizeBytes` aggregate over its whole subtree rather than reporting its own inode, because a directory-scoped `newerThan` or `maxSizeBytes` condition asks about the data inside the directory — a directory's own mtime does not move when a file three levels down is rewritten.
26
+ */
27
+ export declare function buildEntryFacts(params: BuildEntryFactsParams): EntryFacts;
28
+ /** Inputs to `carryOver`. */
29
+ export interface CarryOverParams {
30
+ readonly fs: FarmFs;
31
+ /** The superseded farm, already renamed aside. */
32
+ readonly previousRoot: string;
33
+ /** The new farm, already swapped into place. */
34
+ readonly farmRoot: string;
35
+ /**
36
+ * When given, a colliding name classified as `runtime` is resolved automatically rather than reported — see `CarryOverResult.autoResolved` for why that is always safe. Optional because a caller with no classification loaded (there is none, today) should get the old, fully-manual behaviour rather than a crash; every real call site passes it.
37
+ */
38
+ readonly classification?: {
39
+ readonly defaults: CategoryClassification;
40
+ readonly overlay?: CategoryClassificationOverlay;
41
+ };
42
+ }
43
+ /** What `carryOver` moved, resolved on its own, and could not. */
44
+ export interface CarryOverResult {
45
+ /** Top-level names moved from the superseded farm into the new one. */
46
+ readonly carried: readonly string[];
47
+ /** Top-level names that collided but were resolved automatically because their category is `runtime` — see the doc comment on `carryOver` for why discarding the old copy is always safe here. */
48
+ readonly autoResolved: readonly string[];
49
+ /** Top-level names left behind because the new farm has its own entry of that name, and resolving them needs a human — everything that collided but was not classified `runtime`. */
50
+ readonly collided: readonly string[];
51
+ }
52
+ /**
53
+ * Moves an identity's own locally-written state out of a superseded farm and into the new one.
54
+ *
55
+ * This is what makes a whole-directory swap safe at all. The farm root *is* `CLAUDE_CONFIG_DIR`, so it holds more than symlinks: the identity's own `identity.json`, the credentials file Claude Code writes on Linux and Windows, its `.claude.json`, its daemon and runtime state. None of that is shared, none of it is rebuildable, and a swap that simply replaced the directory would destroy all of it.
56
+ *
57
+ * Anything the previous resync built itself is skipped rather than carried: a symlink is a view of the canonical tree with no data of its own, and a directory the manifest records as materialised had its real children adopted into `~/.claude` before the swap ever started. Everything else is real local data and moves across by rename, so a credential file is relocated rather than duplicated — never briefly existing as two copies on disk.
58
+ *
59
+ * A name that exists in both is genuinely ambiguous in general — overwriting the new farm's own entry would discard whatever the resync just decided; overwriting the old one would discard data — so by default it is reported rather than resolved here, and the caller keeps the superseded farm on disk in that case.
60
+ *
61
+ * One category is not ambiguous, though: `runtime`'s own definition (see `config/categories.default.json`'s category table in the README) is specifically "live per-process or per-machine artifacts" — daemon locks, an MCP auth-needed cache, an update-check result — that make no sense being preserved across a swap at all, let alone fought over. When `classification` is given, a colliding name whose category resolves to `runtime` is discarded from the superseded copy and left exactly as the new farm already has it, with no data ever moved: `keep-new` is not a judgement call for this category, it is what the category already means. This needs only the name's *static* classification, never the resolved shared/not-shared decision for the current directory — a `runtime` entry is disposable whether or not this identity currently chooses to share it, so no cascade resolution is needed to make the call.
62
+ *
63
+ * Exported so `launcher/farmResolve.ts` can reuse this exact collision detection (and the same `runtime` auto-resolution) for `agent-shim identity resolve-conflicts`'s interactive pass, rather than a second implementation that could drift from this one.
64
+ */
65
+ export declare function carryOver(params: CarryOverParams): CarryOverResult;
66
+ /** What recovery found and did. */
67
+ export interface RecoveryResult {
68
+ /** Abandoned scratch trees removed. */
69
+ readonly removedScratch: readonly string[];
70
+ /** Set when the farm root was missing and a superseded farm was restored into its place. */
71
+ readonly restoredFrom?: string;
72
+ /** Superseded farms whose carry-over was completed and which were then discarded. */
73
+ readonly completed: readonly string[];
74
+ /** Top-level names, across every superseded farm processed, resolved automatically because their category is `runtime` — see `carryOver`'s own doc comment for why that needs no human decision. */
75
+ readonly autoResolved: readonly string[];
76
+ /** Superseded farms left on disk because they still held colliding data a human still needs to decide. */
77
+ readonly retained: readonly string[];
78
+ /** True when recovery changed anything, in which case the farm cannot be assumed to match its own manifest. */
79
+ readonly recovered: boolean;
80
+ }
81
+ /** Inputs to `recoverFarm`: the lock-taking wrapper around `recoverInterruptedSwap`. */
82
+ export interface RecoverFarmParams {
83
+ readonly fs: FarmFs;
84
+ readonly identitiesDir: string;
85
+ readonly identity: string;
86
+ readonly now: () => number;
87
+ readonly lock: ResyncFarmParams["lock"];
88
+ readonly classification?: {
89
+ readonly defaults: CategoryClassification;
90
+ readonly overlay?: CategoryClassificationOverlay;
91
+ };
92
+ }
93
+ /**
94
+ * Runs `recoverInterruptedSwap` under the identity's own lock.
95
+ *
96
+ * This exists as its own entry point because recovery has to happen *before* the identity's `identity.json` is read, not merely before the farm is rebuilt: that file lives inside the farm root, so a crash between the swap's two renames leaves it sitting in a superseded directory where nothing will find it. Reading the identity first would silently launch with its `defaultConfigProfile` and `allowAmbientCredential` unset — the first of which decides what the farm is about to share.
97
+ *
98
+ * `resyncFarm` calls `recoverInterruptedSwap` directly rather than calling this, since it already holds the lock; taking it a second time from the same process would block against itself.
99
+ */
100
+ export declare function recoverFarm(params: RecoverFarmParams): RecoveryResult;
101
+ /** Inputs to `resyncFarm`. */
102
+ export interface ResyncFarmParams {
103
+ readonly fs: FarmFs;
104
+ /** `~/.agent-shim/identities` — the farm root's parent, and where the lock, scratch trees, and superseded farms all live. */
105
+ readonly identitiesDir: string;
106
+ readonly identity: string;
107
+ /** The configuration profile this launch resolved to, recorded in the manifest for `agent-shim check`. */
108
+ readonly configProfile?: string;
109
+ /** The canonical `~/.claude` every farm symlink points back into. */
110
+ readonly claudeHome: string;
111
+ readonly home: string;
112
+ readonly cwd: string;
113
+ readonly env: Readonly<Record<string, string | undefined>>;
114
+ readonly branch?: string;
115
+ readonly branchDetached?: boolean;
116
+ readonly cascade: CascadeInput;
117
+ readonly classification: {
118
+ readonly defaults: CategoryClassification;
119
+ readonly overlay?: CategoryClassificationOverlay;
120
+ };
121
+ /** Reads the current time. Called repeatedly while waiting on the lock, then once more to stamp the build. */
122
+ readonly now: () => number;
123
+ /** Distinguishes this resync's scratch and superseded directory names from any other process's. `${pid}.${uuid}` in real use; a fixed string in tests. */
124
+ readonly uniqueSuffix: string;
125
+ readonly lock: {
126
+ readonly pid: number;
127
+ readonly isRunning: (pid: number) => boolean;
128
+ readonly sleep: (ms: number) => void;
129
+ readonly staleAfterMs?: number;
130
+ readonly retryDelayMs?: number;
131
+ readonly maxAttempts?: number;
132
+ };
133
+ }
134
+ /** The outcome of one resync. */
135
+ export interface ResyncFarmResult {
136
+ readonly farmRoot: string;
137
+ /** True when the resolved decision already matched what the farm's own manifest recorded, so nothing was written. */
138
+ readonly noOp: boolean;
139
+ /** The manifest the farm now carries — the newly written one, or the existing one on a no-op. */
140
+ readonly manifest: FarmManifest;
141
+ /** The fully resolved state, including the flattened cascade's launch flags, which the launcher goes on to use. */
142
+ readonly resolved: ResolvedState;
143
+ readonly adopted: readonly string[];
144
+ readonly recovery: RecoveryResult;
145
+ readonly diagnostics: readonly Diagnostic[];
146
+ readonly retainedPrevious?: string;
147
+ }
148
+ /**
149
+ * Brings one identity's farm into line with the cascade resolved for the current directory, and hands back everything the launcher needs to go on and spawn the real binary.
150
+ *
151
+ * The order below is fixed and each step depends on the one before it:
152
+ *
153
+ * 1. Take the per-identity lock, so two sessions under one identity in two directories cannot rewrite the same farm toward two different states at once.
154
+ * 2. Finish or undo any swap a previous launch was killed in the middle of, so everything after this point sees a coherent farm.
155
+ * 3. Read the previous manifest and, guided by it, list the parts of the still-live farm reconciliation is allowed to look at. This step only reads the farm; nothing mutates it.
156
+ * 4. Adopt anything Claude Code wrote into a materialised directory back into the canonical `~/.claude`.
157
+ * 5. **Then** build the fact manifest, from a canonical tree that now includes everything step 4 adopted. Building facts before adoption would make newly adopted data invisible to this resync and only visible on the next launch — the single most important ordering constraint in this module.
158
+ * 6. Resolve the cascade and plan the farm, both of them pure functions over those facts.
159
+ * 7. If the plan matches what the farm's manifest already records and step 4 adopted nothing, stop: no filesystem write happens at all. Launching repeatedly from the same directory with no configuration change is the overwhelmingly common case and it must stay cheap.
160
+ * 8. Otherwise build a scratch tree, write its manifest, and swap it into place.
161
+ * 9. Release the lock — before the real binary is spawned, never held across it.
162
+ */
163
+ export declare function resyncFarm(params: ResyncFarmParams): ResyncFarmResult;
164
+ /** Turns a recovery result into the one diagnostic worth reporting about it, or nothing when there was nothing to recover. */
165
+ export declare function recoveryDiagnostics(recovery: RecoveryResult, identity: string): Diagnostic[];
@@ -0,0 +1,57 @@
1
+ import type { CredentialTargetVar } from "../config/schema";
2
+ /**
3
+ * The environment variables that authenticate Claude Code directly from the process environment, ahead of any stored identity credential. If any of these is set, every identity would silently authenticate as the same key/token/backend while it's set — defeating the entire premise of separate identities. Order here is also lookup order: `detectAmbientCredential` reports the first one found.
4
+ */
5
+ export declare const AMBIENT_CREDENTIAL_VARS: readonly ["ANTHROPIC_API_KEY", "ANTHROPIC_AUTH_TOKEN", "CLAUDE_CODE_OAUTH_TOKEN", "CLAUDE_CODE_USE_BEDROCK", "CLAUDE_CODE_USE_VERTEX", "CLAUDE_CODE_USE_FOUNDRY"];
6
+ export type AmbientCredentialVar = (typeof AMBIENT_CREDENTIAL_VARS)[number];
7
+ /** Which guarded variable was found set, and to what. */
8
+ export interface AmbientCredentialDetection {
9
+ readonly variable: AmbientCredentialVar;
10
+ }
11
+ /** A credential this launch itself exports to the child: the variable it lands in and its value. */
12
+ export interface InjectedCredential {
13
+ readonly variable: CredentialTargetVar;
14
+ readonly token: string;
15
+ }
16
+ /**
17
+ * Detects whether any of `AMBIENT_CREDENTIAL_VARS` is set to a non-empty value in `env`, returning the first one found in declared order, or undefined when none are set.
18
+ *
19
+ * An empty string counts as unset, not set — confirmed load-bearing: one of Joe's real wrapper scripts (`o`, running Claude Code against OpenRouter) does `export ANTHROPIC_API_KEY=""` specifically to *clear* it so `ANTHROPIC_AUTH_TOKEN` takes effect instead, and this must never trip the guard.
20
+ *
21
+ * `injected`, when given, is the credential this launch exports for its own identity. Its variable holding exactly that value is not ambient: it is what a agent-shim launch of the same identity left in the environment of the session this one starts from (a `claude @work` run inside a `claude @work` session). The same variable holding any other value still counts.
22
+ */
23
+ export declare function detectAmbientCredential(env: Readonly<Record<string, string | undefined>>, injected?: InjectedCredential): AmbientCredentialDetection | undefined;
24
+ /**
25
+ * Builds the exact refusal message: which variable was found, why it matters, and the two ways to opt in (a one-off env var, or a persistent per-identity setting). Mirrors the message documented in the project's README. `identityName` is included in the persistent-opt-in command when known; when no identity has been resolved yet (e.g. the `CLAUDE_CONFIG_DIR`-already-set escape hatch, or no identity resolved at all), a generic `<name>` placeholder is used instead, matching the README's own generic wording.
26
+ */
27
+ export declare function formatAmbientCredentialGuardMessage(variable: AmbientCredentialVar, identityName?: string): string;
28
+ /** Inputs to the ambient-credential guard evaluation. */
29
+ export interface EvaluateAmbientCredentialGuardParams {
30
+ readonly env: Readonly<Record<string, string | undefined>>;
31
+ /** The active identity's own `allowAmbientCredential` setting from its `identity.json`, or false when no identity is known. */
32
+ readonly allowAmbientCredential: boolean;
33
+ /** True when `AGENT_SHIM_ALLOW_AMBIENT_CREDENTIAL=1` is set for this one invocation. */
34
+ readonly allowAmbientCredentialOverride: boolean;
35
+ /** The active identity's name, for the message's persistent-opt-in command. Undefined when no identity is known. */
36
+ readonly identityName?: string;
37
+ /**
38
+ * True when this launch routes through an API provider. The guard inspects the PARENT environment, before `buildEnv` runs; a provider launch has its `ANTHROPIC_AUTH_TOKEN` injected and its `ANTHROPIC_API_KEY` cleared by `buildEnv` itself, so an ambient credential in the parent environment never reaches the child and there is nothing left for this guard to protect against. Everything else about the guard (including `IdentitySchema.allowAmbientCredential`) is unchanged by this flag.
39
+ */
40
+ readonly providerSelected?: boolean;
41
+ /** The launching identity's own credential, when its credential block resolved: see `detectAmbientCredential`. */
42
+ readonly injectedCredential?: InjectedCredential;
43
+ }
44
+ /** The result of one guard evaluation: either launch may proceed, or it must be refused with an explanatory message. */
45
+ export type AmbientCredentialGuardResult = {
46
+ readonly ok: true;
47
+ } | {
48
+ readonly ok: false;
49
+ readonly variable: AmbientCredentialVar;
50
+ readonly message: string;
51
+ };
52
+ /**
53
+ * Evaluates the ambient-credential guard: refuses unless no guarded variable is set, or the active identity opted in (`allowAmbientCredential: true`), or this one invocation opted in (`AGENT_SHIM_ALLOW_AMBIENT_CREDENTIAL=1`), or a provider takes over the child's credential (see `providerSelected`).
54
+ *
55
+ * This guard is about credential isolation, not identity/config-dir selection — it must still run even when the `CLAUDE_CONFIG_DIR`-already-set escape hatch applies (callers pass `allowAmbientCredential: false` and no `identityName` in that case, since there is no active identity to consult).
56
+ */
57
+ export declare function evaluateAmbientCredentialGuard(params: EvaluateAmbientCredentialGuardParams): AmbientCredentialGuardResult;
@@ -0,0 +1,41 @@
1
+ import { CliError } from "../cliError";
2
+ import type { FarmFs } from "./ports";
3
+ /** A held identity lock. Releasing is idempotent and never removes a lock some other process has since taken. */
4
+ export interface IdentityLock {
5
+ readonly path: string;
6
+ readonly token: string;
7
+ readonly release: () => void;
8
+ }
9
+ /** Raised when another process holds the identity's lock and did not release it within the retry budget. */
10
+ export declare class IdentityLockBusyError extends CliError {
11
+ readonly identity: string;
12
+ readonly lockPath: string;
13
+ readonly holderPid: number | undefined;
14
+ constructor(identity: string, lockPath: string, holderPid: number | undefined);
15
+ }
16
+ /** Everything `acquireIdentityLock` needs, all of it injected so a test never sleeps for real, never reads a real pid table, and never writes outside its own fake filesystem. */
17
+ export interface AcquireIdentityLockParams {
18
+ readonly identity: string;
19
+ /** The directory the lock file lives in — `~/.agent-shim/identities`, alongside (never inside) the farm the lock protects, so a farm swap can rename the farm root out from under itself without disturbing the lock. */
20
+ readonly dir: string;
21
+ readonly fs: FarmFs;
22
+ readonly nowMs: () => number;
23
+ readonly pid: number;
24
+ /** Answers whether a process is still running, so a lock left behind by a crash is recognised rather than waited out for the full staleness window. Zombie-aware: a holder that exited without being reaped still answers signal 0 as alive, but will never release the lock itself. */
25
+ readonly isRunning: (pid: number) => boolean;
26
+ /** Blocks for the given number of milliseconds. Synchronous by necessity: the whole launcher is synchronous, right through to `spawnSync`. */
27
+ readonly sleep: (ms: number) => void;
28
+ readonly staleAfterMs?: number;
29
+ readonly retryDelayMs?: number;
30
+ readonly maxAttempts?: number;
31
+ }
32
+ /** The path of one identity's lock file. */
33
+ export declare function identityLockPath(dir: string, identity: string): string;
34
+ /**
35
+ * Takes the per-identity resync lock, held for the whole of a resync and released before the real `claude` binary is spawned.
36
+ *
37
+ * Two terminals in two different client directories under one identity is exactly the pattern directory rules exist to support, and it is exactly the pattern that races: both launches resolve their own cascade and both want to rewrite the same identity's farm toward two different states. This serialises them.
38
+ *
39
+ * Mutual exclusion rests on one exclusive-create — a read-then-write pair would leave a window where both processes see no lock. A lock whose holder is no longer running, or whose record is older than the staleness window, or whose contents cannot be parsed at all, is removed and retaken: a machine that crashed mid-resync must not need manual cleanup before `claude` works again. A lock genuinely held by a live process is waited on, and only after the full retry budget does this give up — loudly, with the holder's pid, rather than proceeding unsynchronised.
40
+ */
41
+ export declare function acquireIdentityLock(params: AcquireIdentityLockParams): IdentityLock;
@@ -0,0 +1,135 @@
1
+ import type { ConfigFileReader } from "../config/load";
2
+ /**
3
+ * Filesystem operations the launcher needs, injected so no test in this project ever touches a real filesystem — consistent with the resolver core's own injected-facts approach in `src/resolve/types.ts`.
4
+ */
5
+ export interface FsPort {
6
+ /** Reads a file's contents as UTF-8 text, or returns undefined when it does not exist. Used for plain-text files like the persisted active-identity file. */
7
+ readonly readFileUtf8: (filePath: string) => string | undefined;
8
+ /** Reads and parses a config file (JSON/YAML/JS via cosmiconfig in the real implementation), returning undefined when it does not exist. Mirrors `src/config/load.ts`'s own injected reader shape so `loadIdentity` can compose directly with `loadConfigFile`. */
9
+ readonly readConfigFile: ConfigFileReader;
10
+ /** Lists one directory's immediate child names, or an empty list when it does not exist. Used to name the known providers in an unknown-provider refusal. */
11
+ readonly readdir: (dir: string) => readonly string[];
12
+ }
13
+ /** What one node of a tree is, as reported by an `lstat` that never follows symlinks. */
14
+ type FarmNodeKind = "dir" | "file" | "symlink";
15
+ /** The subset of stat information the farm builder and the fact builder actually read. */
16
+ export interface FarmStat {
17
+ readonly kind: FarmNodeKind;
18
+ readonly mtimeMs: number;
19
+ readonly sizeBytes: number;
20
+ }
21
+ /**
22
+ * Every filesystem operation the farm resync performs, injected so the whole of `src/launcher/farm.ts` — symlink creation, materialisation, adoption copies, and the atomic swap itself — runs against an in-memory fake in tests and never against a real `~/.claude` or `~/.agent-shim`.
23
+ *
24
+ * `lstat` never follows symlinks: a `~/.claude` that contains a directory symlink escaping the tree (a skills directory linked out to a separate dotfiles repository, say) must be recorded as one symlink entry rather than recursed into, both to keep the fact manifest finite and because the farm links such an entry at `<claudeHome>/<rel>` and lets the OS resolve the remaining hops.
25
+ */
26
+ export interface FarmFs {
27
+ /** Stats one path without following symlinks, or returns undefined when nothing is there. */
28
+ readonly lstat: (path: string) => FarmStat | undefined;
29
+ /** Lists one directory's immediate child names, or an empty list when it does not exist. */
30
+ readonly readdir: (path: string) => readonly string[];
31
+ /** Creates a directory and any missing parents, succeeding silently when it already exists. */
32
+ readonly mkdirp: (path: string) => void;
33
+ /** Creates a symbolic link at `linkPath` pointing at `target`. */
34
+ readonly symlink: (target: string, linkPath: string) => void;
35
+ /** Renames `from` to `to`. Both are always within one directory tree on one filesystem in this module's own usage, so this is the atomic primitive the farm swap is built on. */
36
+ readonly rename: (from: string, to: string) => void;
37
+ /** Removes a path and everything beneath it, succeeding silently when it does not exist. */
38
+ readonly removeRecursive: (path: string) => void;
39
+ /** Copies a file, symlink, or whole directory tree from `from` to `to`. */
40
+ readonly copyRecursive: (from: string, to: string) => void;
41
+ /** Reads a file's contents as UTF-8 text, or undefined when it does not exist. */
42
+ readonly readFileUtf8: (path: string) => string | undefined;
43
+ /** Writes a file's full contents as UTF-8 text, creating or truncating it. */
44
+ readonly writeFileUtf8: (path: string, contents: string) => void;
45
+ /** Creates a directory (and any missing parents) readable only by its owner: mode 0700, enforced on the leaf even when it already existed with a wider mode. For a directory holding capabilities another local account must never read. */
46
+ readonly mkdirPrivate: (path: string) => void;
47
+ /** Writes a file's full contents as UTF-8 text readable only by its owner (mode 0600), atomically: a temporary sibling is written at that mode and renamed into place, so no reader ever sees a partial file or a wider mode. */
48
+ readonly writeFilePrivate: (path: string, contents: string) => void;
49
+ /** Appends UTF-8 text to a file readable only by its owner (mode 0600), creating it at that mode when it is missing and narrowing an existing wider mode. The file is opened in append mode, so the text always lands at the end however the file grew; a reader running at the same moment can still see a trailing partial write. It is the primitive an append-only log with one writer per file is built on. */
50
+ readonly appendFilePrivate: (path: string, contents: string) => void;
51
+ /** Creates a file only if it does not already exist, returning false when it does. This is the exclusive-create the identity lock depends on for its mutual exclusion — a read-then-write pair would race. */
52
+ readonly writeFileExclusive: (path: string, contents: string) => boolean;
53
+ /** A stable content hash of one file, or undefined when it is missing or is not a regular file. Used only to recognise farm data that has already been adopted into the canonical tree. */
54
+ readonly hashFile: (path: string) => string | undefined;
55
+ }
56
+ /** The outcome of one `spawnSync` call, mirroring Node's own `SpawnSyncReturns` shape narrowly to what the launcher actually reads. */
57
+ export interface SpawnResult {
58
+ /** The child's exit code, or null when it was terminated by a signal instead of exiting normally. */
59
+ readonly status: number | null;
60
+ /** The signal that terminated the child, or null when it exited normally. */
61
+ readonly signal: NodeJS.Signals | null;
62
+ /** Set when the child process could not even be spawned (e.g. the binary does not exist). */
63
+ readonly error?: Error;
64
+ }
65
+ /** Runs the real `claude` binary synchronously, injected so no test ever spawns a real process. The Node equivalent of the legacy bash tool's `exec` is `spawnSync` with `stdio: "inherit"` followed by propagating the child's own exit code — see `src/launcher/spawn.ts`. */
66
+ export interface SpawnPort {
67
+ readonly spawnSync: (command: string, args: readonly string[], options: {
68
+ readonly stdio: "inherit";
69
+ readonly env: Readonly<Record<string, string | undefined>>;
70
+ }) => SpawnResult;
71
+ }
72
+ /** The outcome of one synchronous external-command run (distinct from `SpawnPort`, which is specifically for replacing this process with the real `claude` binary — `RunPort` is for auxiliary commands like `git rev-parse` for branch detection, or `security find-generic-password` for the macOS Keychain diagnostic in `check.ts`, whose output this process needs to keep running and read). */
73
+ interface RunResult {
74
+ readonly status: number | null;
75
+ readonly stdout: string;
76
+ readonly stderr: string;
77
+ }
78
+ /** Runs an external command and captures its output, injected so no test ever shells out for real. Not used by this phase's own modules yet — declared here so every later module (branch-conditioned `when` evaluation, `check.ts`'s Keychain lookup) shares one injectable shape rather than each inventing its own. */
79
+ export interface RunPort {
80
+ readonly run: (command: string, args: readonly string[]) => RunResult;
81
+ }
82
+ /**
83
+ * The running process itself: its environment, its logical argv, and the ability to terminate with an exit code.
84
+ *
85
+ * `argv` is the *logical* CLI arguments only — whatever the user typed after `claude`/`agent-shim`, already stripped of the Node executable path and the script path (`process.argv.slice(2)` in the real implementation). `exit` mirrors `process.exit`'s own `never` return type: a real process never returns from it, and a test fake enforces the same shape by throwing a marker so control flow unwinds the same way in both cases.
86
+ */
87
+ export interface ProcPort {
88
+ readonly env: Readonly<Record<string, string | undefined>>;
89
+ readonly argv: readonly string[];
90
+ readonly exit: (code: number) => never;
91
+ }
92
+ /** Where the launcher reports what it decided and why — refusals, warnings about an identity that could not be loaded, and (per the "strictly-better than the legacy script" goal) which identity/profile ended up selected. */
93
+ export interface LogPort {
94
+ readonly info: (message: string) => void;
95
+ readonly warn: (message: string) => void;
96
+ readonly error: (message: string) => void;
97
+ }
98
+ /** The running headroom daemon one launch routed through, as `HeadroomPort.ensure` reports it. The front door owns the session's routing; this names the daemon the door's headroom hop forwards to, and the project identity headroom scopes memory to. */
99
+ export interface HeadroomUp {
100
+ /** The loopback port the headroom daemon listens on: what the door's headroom hop reads live and `HEADROOM_PROXY_URL` names for anything else that wants the daemon. */
101
+ readonly port: number;
102
+ /** The project identity headroom scopes memory state to: the git repo root of the launch directory, or the directory itself outside a repository. */
103
+ readonly projectId: string;
104
+ }
105
+ /**
106
+ * Everything the launcher needs from the headroom daemon, injected so its daemon-supervision side effects stay behind a port and the launcher's own logic stays pure over it (`src/realPorts.ts` wires the real one; tests wire fakes).
107
+ *
108
+ * `ensure` brings the daemon up (spawning the detached supervisor when nothing healthy is running) and registers this launch in its session registry; `release` removes that registration when the spawned `claude` exits.
109
+ */
110
+ export interface HeadroomPort {
111
+ readonly ensure: () => HeadroomUp;
112
+ readonly release: () => void;
113
+ }
114
+ /**
115
+ * Everything the launcher needs from the front-door daemon, injected like `HeadroomPort`: `ensure` brings the door up (spawning its detached supervisor when nothing authenticated is serving), registers this launch in its session registry and returns both listeners' loopback ports plus the CA bundle the child must trust; `release` removes that registration when the spawned `claude` exits.
116
+ */
117
+ export interface FrontDoorPort {
118
+ /** `inheritedExtraCaCerts` is the parent environment's own `NODE_EXTRA_CA_CERTS`, which the returned trust bundle keeps (see `resolveTrustBundle`). */
119
+ readonly ensure: (inheritedExtraCaCerts: string | undefined) => FrontDoorUp;
120
+ readonly release: () => void;
121
+ }
122
+ /** The running front door one launch routed through, as `FrontDoorPort.ensure` reports it. */
123
+ export interface FrontDoorUp {
124
+ /** The loopback port the provider listener serves HTTPS on: the base URL every provider session points at. */
125
+ readonly port: number;
126
+ /** The loopback port the CONNECT surface listens on: what an OAuth launch points HTTPS_PROXY at. */
127
+ readonly connectPort: number;
128
+ /** The CA bundle every routed child points NODE_EXTRA_CA_CERTS at: agent-shim's CA (which signs both the provider listener's leaf and the CONNECT surface's), plus whatever the parent environment already trusted that way. */
129
+ readonly trustBundlePath: string;
130
+ /** Why the trust bundle dropped something the parent environment named, when it did; the launcher logs it. */
131
+ readonly trustWarning?: string;
132
+ /** This launch's capability token, injected alongside the session headers and required by the door before it routes anything. */
133
+ readonly sessionToken: string;
134
+ }
135
+ export {};
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Everything agent-shim still honours from its former name, `claude-use`, so an installation, a committed project config or an exported environment written for the old name keeps working. Nothing here is consulted when the current name's equivalent is present: a current name always wins, and the legacy form is only a fallback that `agent-shim doctor` reports so it can be retired.
3
+ */
4
+ /** Prefix of every environment variable agent-shim reads. */
5
+ export declare const ENV_PREFIX = "AGENT_SHIM_";
6
+ /** The former prefix, still accepted wherever `ENV_PREFIX` is. */
7
+ export declare const LEGACY_ENV_PREFIX = "CLAUDE_USE_";
8
+ /** The state root directory name under the home directory. */
9
+ export declare const HOME_DIRNAME = ".agent-shim";
10
+ /**
11
+ * The former state root directory name. It is adopted in place rather than moved: macOS Claude Code keys each identity's Keychain login on the exact `CLAUDE_CONFIG_DIR` string (`<root>/identities/<name>`), so relocating the root would sign every identity out.
12
+ */
13
+ export declare const LEGACY_HOME_DIRNAME = ".claude-use";
14
+ /** Former committed and per-clone portable config file names. */
15
+ export declare const LEGACY_PORTABLE_CONFIG_FILENAME = ".claude-use.json";
16
+ export declare const LEGACY_PORTABLE_LOCAL_CONFIG_FILENAME = ".claude-use.local.json";
17
+ /** The former farm manifest file name, read when a farm built by an older release has no current one. */
18
+ export declare const LEGACY_FARM_MANIFEST_FILENAME = ".claude-use-farm.json";
19
+ /** The former executable name, still installed beside the current one and still dispatched to the same program. */
20
+ export declare const LEGACY_COMMAND_NAME = "claude-use";
21
+ /**
22
+ * Copies every `CLAUDE_USE_*` variable in `env` to its `AGENT_SHIM_*` name unless that name is already set, mutating `env`. Returns the legacy names that supplied a value, in sorted order. An empty current value counts as set, so exporting `AGENT_SHIM_X=` still overrides a stale `CLAUDE_USE_X`.
23
+ */
24
+ export declare function aliasLegacyEnv(env: NodeJS.ProcessEnv): string[];