agent-shim 7.7.0 → 7.9.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 (42) hide show
  1. package/README.md +4 -2
  2. package/dist/cli.cjs +3983 -3955
  3. package/dist/index.cjs +3214 -173
  4. package/dist/index.mjs +3136 -158
  5. package/dist/types/checkReport.d.ts +154 -0
  6. package/dist/types/claudeShim.d.ts +114 -0
  7. package/dist/types/cli/bool.d.ts +8 -0
  8. package/dist/types/cli/commandDeps.d.ts +28 -0
  9. package/dist/types/cli/envBool.d.ts +11 -0
  10. package/dist/types/codex/http.d.ts +15 -0
  11. package/dist/types/configProfilesStore.d.ts +61 -0
  12. package/dist/types/configure.d.ts +170 -0
  13. package/dist/types/credential.d.ts +136 -0
  14. package/dist/types/credentialCache.d.ts +46 -0
  15. package/dist/types/directoryRulesStore.d.ts +43 -0
  16. package/dist/types/doctorReport.d.ts +117 -0
  17. package/dist/types/frontdoor/custody.d.ts +32 -0
  18. package/dist/types/frontdoor/headroomHop.d.ts +22 -0
  19. package/dist/types/frontdoor/pipeline.d.ts +109 -0
  20. package/dist/types/frontdoor/server.d.ts +44 -0
  21. package/dist/types/headroom/headers.d.ts +11 -0
  22. package/dist/types/identityStore.d.ts +85 -0
  23. package/dist/types/index.d.ts +12 -1
  24. package/dist/types/launcher/cascade.d.ts +42 -0
  25. package/dist/types/launcher/flags.d.ts +96 -0
  26. package/dist/types/launcher/identity.d.ts +95 -0
  27. package/dist/types/launcher/pool.d.ts +44 -0
  28. package/dist/types/pathNorm.d.ts +14 -0
  29. package/dist/types/poolPickReport.d.ts +44 -0
  30. package/dist/types/poolStore.d.ts +18 -0
  31. package/dist/types/providersStore.d.ts +137 -0
  32. package/dist/types/realPorts.d.ts +102 -0
  33. package/dist/types/usage/middleware.d.ts +17 -0
  34. package/dist/types/usage/pick.d.ts +62 -0
  35. package/dist/types/usage/plan.d.ts +18 -0
  36. package/dist/types/usage/poolPick.d.ts +31 -0
  37. package/dist/types/usage/preflight.d.ts +20 -0
  38. package/dist/types/usage/rateLimit.d.ts +25 -0
  39. package/dist/types/usage/scan.d.ts +24 -0
  40. package/dist/types/usage/store.d.ts +43 -0
  41. package/dist/types/versionDiscovery.d.ts +47 -0
  42. package/package.json +1 -1
@@ -0,0 +1,136 @@
1
+ import type { Credential, CredentialSource, CredentialTarget, CredentialTargetVar } from "./config/schema";
2
+ import { type CredentialCachePort } from "./credentialCache";
3
+ /** The exit status for a launch whose selected provider or identity has a credential block none of whose sources yields a token: 64, the conventional `EX_USAGE`, because the invocation asked for a credential the environment cannot supply. */
4
+ export declare const CREDENTIAL_UNAVAILABLE_EXIT = 64;
5
+ /** How long a non-interactive credential command may run. Secret-store CLIs answer from a local cache or agent well within a second, so this leaves a wide margin while still failing a wedged command quickly instead of stalling the launch. */
6
+ export declare const CREDENTIAL_COMMAND_TIMEOUT_MS = 10000;
7
+ /** How long an interactive credential command may run: long enough for a person to notice and approve a biometric or password prompt, short enough that a prompt nobody is watching fails the launch rather than hanging it. */
8
+ export declare const CREDENTIAL_INTERACTIVE_TIMEOUT_MS = 120000;
9
+ /** What reading one `file` source found: nothing at that path, or its contents and permission bits. `mode` is undefined on a platform with no POSIX permission bits (Windows), where the looseness check cannot apply. */
10
+ type SecretFileRead = {
11
+ readonly found: false;
12
+ } | {
13
+ readonly found: true;
14
+ readonly content: string;
15
+ readonly mode: number | undefined;
16
+ };
17
+ /** The outcome of one credential command. `stderr` is empty for an interactive run, whose stderr goes to the terminal so a person sees any prompt. */
18
+ export interface CredentialCommandResult {
19
+ readonly status: number | null;
20
+ readonly stdout: string;
21
+ readonly stderr: string;
22
+ readonly timedOut: boolean;
23
+ }
24
+ /**
25
+ * Everything resolving a credential needs from the outside world, injected so the resolver (and every launcher test) runs without `op`, the Keychain, real secret files or a terminal. `src/realPorts.ts` wires the real one; tests wire a fake.
26
+ */
27
+ export interface CredentialPort {
28
+ /** Reads a `file` source's path (absolute or `~`-rooted). */
29
+ readonly readSecretFile: (filePath: string) => SecretFileRead;
30
+ /** Runs a command source's argv directly (no shell) under a timeout, capturing stdout. An interactive run inherits the terminal's stdin and stderr so a person can answer a prompt. */
31
+ readonly runCommand: (argv: readonly [string, ...string[]], options: {
32
+ readonly timeoutMs: number;
33
+ readonly interactive: boolean;
34
+ }) => CredentialCommandResult;
35
+ /** Whether a person can answer a prompt now: standard input is a terminal, or there is a desktop session to show a dialog in. */
36
+ readonly personPresent: () => boolean;
37
+ /** Where cached credentials live, used only for a credential block that asks for caching. Omit to resolve every launch from the sources. */
38
+ readonly cache?: CredentialCacheEnv;
39
+ }
40
+ /** The sources that run a command: `command` itself and the two presets that compile to one. */
41
+ type CommandBackedSource = Extract<CredentialSource, {
42
+ readonly command: unknown;
43
+ } | {
44
+ readonly op: unknown;
45
+ } | {
46
+ readonly keychain: unknown;
47
+ }>;
48
+ /**
49
+ * The argv a command-backed source runs, with the `op` and `keychain` presets compiled to theirs: `op read <ref>` and `security find-generic-password -s <service> [-a <account>] -w`.
50
+ */
51
+ export declare function commandArgv(source: CommandBackedSource): readonly [string, ...string[]];
52
+ /**
53
+ * Whether a source needs a person present. An explicit `interactive` always wins. Otherwise only `op` defaults to interactive, because the desktop app's integration asks for approval, unless `OP_SERVICE_ACCOUNT_TOKEN` is set, in which case `op` authenticates as the service account with nobody asked. A plain `command` and a `keychain` read default to non-interactive: a Keychain item whose access list already trusts `security` reads silently, and one that does not fails at once with "User interaction is not allowed" rather than hanging.
54
+ */
55
+ export declare function isInteractiveSource(source: CredentialSource, env: Readonly<Record<string, string | undefined>>): boolean;
56
+ /** One source as `check`, `doctor` and every `--json` output report it: its kind plus the same non-secret identifying detail `describeSource` prints. */
57
+ type CredentialSourceSummary = {
58
+ readonly kind: "env";
59
+ readonly variable: string;
60
+ } | {
61
+ readonly kind: "file";
62
+ readonly path: string;
63
+ } | {
64
+ readonly kind: "command";
65
+ readonly program: string;
66
+ } | {
67
+ readonly kind: "op";
68
+ readonly reference: string;
69
+ } | {
70
+ readonly kind: "keychain";
71
+ readonly service: string;
72
+ readonly account?: string;
73
+ } | {
74
+ readonly kind: "literal";
75
+ };
76
+ /** A credential block as it is reported: its effective target and each source's summary, in order. */
77
+ export interface CredentialSummary {
78
+ readonly target: CredentialTarget;
79
+ readonly sources: readonly CredentialSourceSummary[];
80
+ /** The block's cache setting when it asks for caching: how long a token is kept (absent for no expiry) and where. */
81
+ readonly cache?: {
82
+ readonly ttl?: string;
83
+ readonly store?: string;
84
+ };
85
+ }
86
+ /** Summarises a credential block for reporting, with `target` resolved to its default. */
87
+ export declare function summariseCredential(credential: Credential): CredentialSummary;
88
+ /** Renders a source for a message, as `formatSourceSummary` does its summary. */
89
+ export declare function describeSource(source: CredentialSource): string;
90
+ /** Renders a credential summary on one line for human-readable output: `bearer from env Z_API_TOKEN, then op op://...`. */
91
+ export declare function formatCredentialSummary(summary: CredentialSummary): string;
92
+ /** Renders a credential block on one line, as `formatCredentialSummary` does its summary. */
93
+ export declare function describeCredential(credential: Credential): string;
94
+ /** Inputs to `resolveCredential`. */
95
+ export interface ResolveCredentialParams {
96
+ readonly credential: Credential;
97
+ /** The launching environment: where `env` sources are read, and what decides whether `op` is interactive. */
98
+ readonly env: Readonly<Record<string, string | undefined>>;
99
+ readonly port: CredentialPort;
100
+ /** What the credential belongs to, for messages and as its cache key: `provider z` or `identity work`. */
101
+ readonly subject: string;
102
+ /** True to skip a cached token and fetch from the sources, replacing the cached copy: what `credential warm` does. */
103
+ readonly refresh?: boolean;
104
+ }
105
+ /** What caching a credential needs beyond the block's own `cache` setting: the port holding entries, the platform that picks the default store, and the clock the TTL is measured on. */
106
+ export interface CredentialCacheEnv {
107
+ readonly port: CredentialCachePort;
108
+ readonly platform: NodeJS.Platform;
109
+ readonly now: () => number;
110
+ }
111
+ /** A resolved credential: the token, where it goes, and which source produced it. `warnings` carries any source refused on the way for a reason worth reporting (a secret file with loose permissions). */
112
+ export interface ResolvedCredential {
113
+ readonly target: CredentialTarget;
114
+ readonly token: string;
115
+ readonly source: CredentialSource;
116
+ readonly warnings: readonly string[];
117
+ /** When the token was fetched, if it came from the cache rather than a source; undefined for a live fetch. */
118
+ readonly cachedAt?: number;
119
+ }
120
+ /** The outcome of `resolveCredential`: a resolved credential, or a message naming every source and why none yielded a token. */
121
+ export type CredentialResolution = {
122
+ readonly ok: true;
123
+ readonly credential: ResolvedCredential;
124
+ } | {
125
+ readonly ok: false;
126
+ readonly message: string;
127
+ };
128
+ /**
129
+ * Resolves a credential block: a fresh cached token if the block caches one, else tries each source in order and returns the first non-empty token, with the block's target, storing it when the block caches. When none yields one, the message names every source and why, and never a value; for a caching block it also names the command that fills the cache from a terminal. Pure over its injected ports.
130
+ */
131
+ export declare function resolveCredential(params: ResolveCredentialParams): CredentialResolution;
132
+ /**
133
+ * The credential variables a child environment gets for a resolved token: the target's variable set to the token, and the other two set to undefined so that merging this over an environment removes them, and an ambient one inherited from the parent can neither outrank nor sit alongside the credential agent-shim chose. Removing a variable (rather than setting it to the empty string) leaves nothing for Claude Code to interpret.
134
+ */
135
+ export declare function credentialVariables(target: CredentialTarget, token: string): Record<CredentialTargetVar, string | undefined>;
136
+ export {};
@@ -0,0 +1,46 @@
1
+ import { z } from "zod";
2
+ import { type CredentialCache, type CredentialCacheStore } from "./config/schema";
3
+ /** What a cache stores for one credential: the token, when it was fetched, and the source that produced it (a non-secret descriptor, kept so `check` and the launch log can still say where the token came from). */
4
+ export declare const CachedCredentialSchema: z.ZodObject<{
5
+ token: z.ZodString;
6
+ fetchedAt: z.ZodNumber;
7
+ source: z.ZodUnion<readonly [z.ZodObject<{
8
+ env: z.ZodString;
9
+ }, z.core.$strict>, z.ZodObject<{
10
+ file: z.ZodString;
11
+ }, z.core.$strict>, z.ZodObject<{
12
+ interactive: z.ZodOptional<z.ZodBoolean>;
13
+ timeoutMs: z.ZodOptional<z.ZodInt>;
14
+ command: z.ZodTuple<[z.ZodString], z.ZodString>;
15
+ }, z.core.$strict>, z.ZodObject<{
16
+ interactive: z.ZodOptional<z.ZodBoolean>;
17
+ timeoutMs: z.ZodOptional<z.ZodInt>;
18
+ op: z.ZodString;
19
+ }, z.core.$strict>, z.ZodObject<{
20
+ interactive: z.ZodOptional<z.ZodBoolean>;
21
+ timeoutMs: z.ZodOptional<z.ZodInt>;
22
+ keychain: z.ZodObject<{
23
+ service: z.ZodString;
24
+ account: z.ZodOptional<z.ZodString>;
25
+ }, z.core.$strict>;
26
+ }, z.core.$strict>, z.ZodObject<{
27
+ literal: z.ZodString;
28
+ }, z.core.$strict>]>;
29
+ }, z.core.$strict>;
30
+ export type CachedCredential = z.infer<typeof CachedCredentialSchema>;
31
+ /**
32
+ * Where cached credentials live, injected so resolution and its tests need neither the Keychain nor a real directory. `owner` names what the credential belongs to (`identity work`, `provider z`) and is the entry's key; `store` picks the backing store. `read` returns undefined for a missing or unreadable entry, since an entry that cannot be used is the same as no entry.
33
+ */
34
+ export interface CredentialCachePort {
35
+ readonly read: (store: CredentialCacheStore, owner: string) => CachedCredential | undefined;
36
+ readonly write: (store: CredentialCacheStore, owner: string, entry: CachedCredential) => void;
37
+ readonly remove: (store: CredentialCacheStore, owner: string) => void;
38
+ }
39
+ /** The store a block uses: its own choice, else the Keychain on macOS and a file elsewhere. */
40
+ export declare function effectiveStore(cache: Readonly<CredentialCache>, platform: NodeJS.Platform): CredentialCacheStore;
41
+ /** A `ttl` such as `12h` in milliseconds, or undefined for no expiry. */
42
+ export declare function ttlMs(cache: Readonly<CredentialCache>): number | undefined;
43
+ /** Whether a cached entry is still within its TTL at `now`. A cache with no TTL never expires; an entry stamped in the future (a clock that moved back) is treated as expired. */
44
+ export declare function isFresh(entry: CachedCredential, cache: Readonly<CredentialCache>, now: number): boolean;
45
+ /** How old a cached entry is, for `check` and `doctor` to report without the value. */
46
+ export declare function cacheAgeMs(entry: CachedCredential, now: number): number;
@@ -0,0 +1,43 @@
1
+ import { type DirectoryRule, type DirectoryRules } from "./config/schema";
2
+ import { CliError } from "./cliError";
3
+ import type { LayoutPaths } from "./paths";
4
+ /** Raised by `removeDirectoryRule`, `updateDirectoryRule` and `rule show` when no rule matches the given path exactly. */
5
+ export declare class DirectoryRuleNotFoundError extends CliError {
6
+ readonly rulePath: string;
7
+ constructor(rulePath: string);
8
+ }
9
+ /** Raised by `addDirectoryRule` when neither `--config-profile` nor `--identity` is given, and by `updateDirectoryRule` when an update would leave a rule that sets nothing at all: a rule that pins and overrides nothing would do nothing. */
10
+ export declare class DirectoryRuleMissingTargetError extends CliError {
11
+ constructor();
12
+ }
13
+ /** Raised by `addDirectoryRule` when a rule for the exact path already exists: `rule add` creates, `rule set` updates. */
14
+ export declare class DirectoryRuleAlreadyExistsError extends CliError {
15
+ readonly rulePath: string;
16
+ constructor(rulePath: string);
17
+ }
18
+ /** Reads `~/.agent-shim/directory-rules.json`, or an empty rule set when the file does not exist yet. */
19
+ export declare function readDirectoryRules(paths: LayoutPaths): DirectoryRules;
20
+ /** Validates and writes the whole `~/.agent-shim/directory-rules.json` file. Exported so `src/configure.ts` can update a single rule's `categories`/`entries` in place without duplicating this validate-then-write step. Throws `ConfigValidationError` when `rules` fails `DirectoryRulesSchema`, rather than letting the underlying `ZodError` escape as an unhandled crash. */
21
+ export declare function writeDirectoryRules(paths: LayoutPaths, rules: DirectoryRules): void;
22
+ /** Lists every directory rule, in file order. */
23
+ export declare function listDirectoryRules(paths: LayoutPaths): readonly DirectoryRule[];
24
+ /** Inputs to `addDirectoryRule` beyond the path itself. */
25
+ export interface AddDirectoryRuleOptions {
26
+ readonly configProfile?: string;
27
+ readonly identity?: string;
28
+ }
29
+ /**
30
+ * Adds a new directory rule for `rulePath`. Throws `DirectoryRuleAlreadyExistsError` when a rule for that exact path already exists (`updateDirectoryRule` changes one), and `DirectoryRuleMissingTargetError` when neither `configProfile` nor `identity` is given, since a rule that pins neither would do nothing.
31
+ */
32
+ export declare function addDirectoryRule(paths: LayoutPaths, rulePath: string, options: AddDirectoryRuleOptions): DirectoryRule;
33
+ /** The fields `updateDirectoryRule` changes. A value of `false` removes that field from the rule. */
34
+ export interface UpdateDirectoryRuleOptions {
35
+ readonly configProfile?: string | false;
36
+ readonly identity?: string | false;
37
+ }
38
+ /**
39
+ * Updates the existing directory rule for `rulePath` in place, keeping its position in the file and every field not named. Throws `DirectoryRuleNotFoundError` when no rule matches that exact path, and `DirectoryRuleMissingTargetError` when the update would leave a rule that does nothing (remove it with `removeDirectoryRule` instead).
40
+ */
41
+ export declare function updateDirectoryRule(paths: LayoutPaths, rulePath: string, options: UpdateDirectoryRuleOptions): DirectoryRule;
42
+ /** Removes the directory rule for `rulePath`. Throws `DirectoryRuleNotFoundError` when no rule matches that exact path. */
43
+ export declare function removeDirectoryRule(paths: LayoutPaths, rulePath: string): void;
@@ -0,0 +1,117 @@
1
+ import { type ClaudeShimState, type PathShadowStatus } from "./claudeShim";
2
+ import type { LayoutPaths } from "./paths";
3
+ import type { RunPort } from "./launcher/ports";
4
+ import type { DiscoveredClaudeBinary } from "./versionDiscovery";
5
+ type DoctorSeverity = "pass" | "warn" | "fail";
6
+ type DoctorSection = "ambient-credential" | "binary-discovery" | "claude-shim" | "path-resolution" | "legacy-name" | "config-profile" | "identity" | "pool" | "provider" | "keychain" | "directory-rules" | "global-config" | "categories-local" | "active-identity" | "headroom";
7
+ /** One line of `agent-shim doctor`'s report. `subject` names the identity/profile/rule the finding is about, when the section has more than one of those. */
8
+ interface DoctorFinding {
9
+ readonly section: DoctorSection;
10
+ readonly subject?: string;
11
+ readonly severity: DoctorSeverity;
12
+ readonly message: string;
13
+ }
14
+ /** The full result of `runDoctor`. `ok` is false iff any finding is a `fail` — a `warn` never fails the report on its own. */
15
+ export interface DoctorReport {
16
+ readonly findings: readonly DoctorFinding[];
17
+ readonly ok: boolean;
18
+ }
19
+ /** One identity's raw `identity.json`, unparsed — `runDoctor` does its own JSON.parse/schema validation so one malformed file never aborts the rest of the report. */
20
+ export interface DoctorIdentityInput {
21
+ readonly name: string;
22
+ readonly path: string;
23
+ readonly raw: string | undefined;
24
+ /** The identity's own farm root (`identitiesDir/<name>`) — the account name the Keychain lookup uses. */
25
+ readonly farmRoot: string;
26
+ }
27
+ /** One configuration profile's raw `<name>.json`, unparsed. */
28
+ export interface DoctorConfigProfileInput {
29
+ readonly name: string;
30
+ readonly path: string;
31
+ readonly raw: string | undefined;
32
+ }
33
+ /** One provider's raw `<name>.json`, unparsed. */
34
+ export interface DoctorProviderInput {
35
+ readonly name: string;
36
+ readonly path: string;
37
+ readonly raw: string | undefined;
38
+ }
39
+ /** A single optional top-level file `runDoctor` validates against a schema when present — absent is a legitimate, unconfigured state, not a failure. */
40
+ interface DoctorFileInput {
41
+ readonly path: string;
42
+ readonly raw: string | undefined;
43
+ }
44
+ /** The outcome of resolving the real Claude Code binary, pre-resolved by the wiring layer since `discoverClaudeBinary` is not itself a parse-shaped pure operation and already has its own dedicated test coverage. */
45
+ type DoctorBinaryDiscovery = {
46
+ readonly ok: true;
47
+ readonly binary: DiscoveredClaudeBinary;
48
+ } | {
49
+ readonly ok: false;
50
+ readonly message: string;
51
+ };
52
+ /**
53
+ * Where a bare command name resolves for the two names this tool owns.
54
+ *
55
+ * `ownExecutablePath` is this process's own PATH-visible location (`realOwnExecutablePath()`); `agentShim` is `findPathShadow`'s verdict for a bare `agent-shim` against the directory that executable lives in. `claude` is only populated when a shim is actually enabled — without one, a `claude` on PATH is Claude Code's own binary, which is not a shadow of anything.
56
+ */
57
+ interface DoctorPathResolution {
58
+ readonly ownExecutablePath: string;
59
+ readonly agentShim: PathShadowStatus;
60
+ readonly claude?: PathShadowStatus;
61
+ }
62
+ /**
63
+ * Collapses a `shadowed` verdict back to `ok` when the shadowing entry and this executable are literally the same file reached by two names — `findPathShadow` compares *directories*, so a symlink on PATH pointing at the running executable's own real location otherwise reads as a shadow of itself.
64
+ *
65
+ * `realpath` must resolve symlinks, and must return its argument unchanged rather than throwing when the path cannot be resolved (a broken symlink, a race with an uninstall), so an unresolvable path simply stays unequal and the shadow verdict stands.
66
+ */
67
+ export declare function refinePathShadow(status: PathShadowStatus, ownExecutablePath: string, realpath: (target: string) => string): PathShadowStatus;
68
+ /** Everything `runDoctor` needs, all of it already loaded/injected — nothing in `runDoctor` itself reads a file, shells out, or touches the farm. */
69
+ export interface RunDoctorParams {
70
+ readonly env: Readonly<Record<string, string | undefined>>;
71
+ readonly identities: readonly DoctorIdentityInput[];
72
+ readonly configProfiles: readonly DoctorConfigProfileInput[];
73
+ readonly providers: readonly DoctorProviderInput[];
74
+ readonly directoryRules: DoctorFileInput;
75
+ readonly globalConfig: DoctorFileInput;
76
+ readonly categoriesLocal: DoctorFileInput;
77
+ readonly activeIdentity: DoctorFileInput;
78
+ readonly binaryDiscovery: DoctorBinaryDiscovery;
79
+ /** Whether `agent-shim shim enable` has been run, and whether its recorded target still exists on disk — pre-resolved by the wiring layer, since checking a file's existence is real I/O, not a parse-shaped pure operation. */
80
+ readonly claudeShim: {
81
+ readonly state: ClaudeShimState | undefined;
82
+ readonly targetExists: boolean;
83
+ };
84
+ /** Which executables a bare `agent-shim` (and, when the shim is enabled, a bare `claude`) would actually run — pre-resolved by the wiring layer, since scanning PATH is real I/O. */
85
+ readonly pathResolution: DoctorPathResolution;
86
+ /** The resolved state root (`paths.root`), whose directory name says whether this installation still lives under the former `.claude-use` name. */
87
+ readonly rootPath: string;
88
+ /** Runs `security find-generic-password` for the per-identity Keychain check. Omit to skip that check entirely (e.g. off macOS). */
89
+ readonly run?: RunPort;
90
+ /** `process.platform` in real use; the Keychain check only ever runs when this is `"darwin"`. */
91
+ readonly platform: string;
92
+ /** The headroom daemon's state.json plus a zombie-aware liveness predicate for the pids it names (a defunct daemon holds no port but still answers signal 0). Omit the raw text when the daemon has never run; that is a pass, not a failure. */
93
+ readonly headroom: {
94
+ readonly state: DoctorFileInput;
95
+ readonly isRunning: (pid: number) => boolean;
96
+ };
97
+ }
98
+ /**
99
+ * Audits the whole `~/.agent-shim` config graph for internal consistency: every identity, every configuration profile's own `extends` chain, every provider, `directory-rules.json`, `config.json`, `categories.local.json`, `active-identity`, plus real Claude Code binary discoverability and ambient-credential exposure.
100
+ *
101
+ * Deliberately identity/directory-agnostic, unlike `runCheck` — there is no single cascade to resolve `doctor` against, so it never touches settings-exposure (which only means anything relative to one resolved cascade).
102
+ *
103
+ * Every check aggregates rather than throws: a malformed file becomes one `fail` finding for that file, not an aborted report. This is the one place in the codebase that deliberately breaks the "throw a validation error and let it propagate" convention every other command relies on — `doctor`'s whole purpose is to survive a broken file and keep auditing everything else.
104
+ */
105
+ export declare function runDoctor(params: RunDoctorParams): DoctorReport;
106
+ /** Renders a full `DoctorReport` as plain text lines, one section header at a time, in the order `agent-shim doctor` prints them. */
107
+ export declare function formatDoctorReport(report: DoctorReport): string[];
108
+ /** What `collectDoctorReport` needs from its caller. */
109
+ export interface CollectDoctorReportParams {
110
+ readonly paths: LayoutPaths;
111
+ readonly env: NodeJS.ProcessEnv;
112
+ }
113
+ /**
114
+ * Audits the whole state root against this machine: the edge adapter that enumerates every identity, configuration profile and provider on disk, reads every top-level config file as raw text (never pre-parsing, so one malformed file cannot abort the report), discovers the real Claude Code binary and resolves where a bare command name runs from, and hands the facts to the pure `runDoctor`.
115
+ */
116
+ export declare function collectDoctorReport(params: CollectDoctorReportParams): DoctorReport;
117
+ export {};
@@ -0,0 +1,32 @@
1
+ import type { IncomingHttpHeaders } from "node:http";
2
+ /** What stands in for a credential while its request crosses headroom: an opaque word that authenticates nothing anywhere. */
3
+ export declare const SEQUESTERED_CREDENTIAL = "agent-shim-sequestered";
4
+ /** The credential headers one custody record holds, exactly as the client sent them. */
5
+ type HeldCredentials = Readonly<Record<string, string | readonly string[]>>;
6
+ /** One request's headers with its credentials swapped out, and the id that redeems them. */
7
+ interface Sequestered {
8
+ readonly hopId: string;
9
+ readonly headers: Record<string, string | string[] | undefined>;
10
+ }
11
+ /**
12
+ * Holds a provider session's real credentials in this process's memory while its request takes the headroom hop, so the credential never crosses headroom or the plain-HTTP leg back to the door.
13
+ *
14
+ * The hop calls `sequester`, sends headroom the placeholder headers plus the hop id, and calls `release` once its response to the client has finished or the client has gone. The direct listener calls `redeem` with the hop id and the provider its request path names: a live id recorded for that same provider yields the real headers, anything else yields nothing and the request is refused. A process that binds the direct port therefore receives only placeholders and an id that is worthless once the hop ends, and nothing outside this process can mint an id.
15
+ *
16
+ * An id stays redeemable for the whole hop rather than exactly once, because headroom legitimately sends one client request upstream more than once: it retries 429, 529 and 5xx answers, and its memory-tool and CCR continuations call the upstream again inside the same client turn, all with the headers it was handed.
17
+ */
18
+ export interface CredentialCustody {
19
+ /** Takes custody of `headers`' credential headers for one hop to `provider`, returning the headers to send headroom (placeholders where the credentials were) and the hop id that redeems them. */
20
+ readonly sequester: (headers: Readonly<IncomingHttpHeaders>, provider: string) => Sequestered;
21
+ /** Ends a hop: its id stops redeeming. Idempotent. */
22
+ readonly release: (hopId: string) => void;
23
+ /** The real credential headers for a live hop to `provider`, or undefined for an unknown, released or mismatched id. */
24
+ readonly redeem: (hopId: string, provider: string) => HeldCredentials | undefined;
25
+ }
26
+ /** Creates an empty custody store. `randomId` must be unguessable (a v4 UUID or better): the id is the only thing that redeems a credential. */
27
+ export declare function createCredentialCustody(randomId: () => string): CredentialCustody;
28
+ /**
29
+ * The headers a redeemed request is routed with: every credential header headroom forwarded (placeholders, or anything else headroom chose to send) is dropped, and the held originals take their place. Whatever the hop's request carried in those headers is never trusted; only what the client sent is.
30
+ */
31
+ export declare function restoreCredentials(headers: Readonly<IncomingHttpHeaders>, credentials: HeldCredentials): IncomingHttpHeaders;
32
+ export {};
@@ -0,0 +1,22 @@
1
+ import type { CredentialCustody } from "./custody";
2
+ import { type RoutedRequest, type RoutedResponse } from "./route";
3
+ /** Everything the headroom hop depends on, injected so the pipeline's ordering tests can point it at a scripted daemon. */
4
+ export interface HeadroomHopDeps {
5
+ /**
6
+ * The headroom daemon's loopback port, read per request rather than captured at start-up: the daemon can crash and restart on a different port while this door keeps listening, and the hop must follow it. Undefined while the daemon is down (the restart window).
7
+ */
8
+ readonly headroomPort: () => number | undefined;
9
+ /**
10
+ * The per-generation secret the direct listener requires on what the hop forwards back. Headroom passes non-x-headroom headers through untouched, so the secret survives the round trip and arrives where this process can check it, while no loopback process outside this door ever holds it.
11
+ */
12
+ readonly hopSecret: string;
13
+ /** Holds a provider session's real credentials while its request crosses headroom; the direct listener redeems them by the hop id. */
14
+ readonly custody: CredentialCustody;
15
+ readonly log: (line: string) => void;
16
+ }
17
+ /**
18
+ * Applies the headroom hop: forwards the request, still Anthropic-shaped, to the headroom daemon as a backend hop and streams its response back to the client. The hop ALWAYS sits before the route's own serving (a translator needs the request headroom has compressed, never the reverse), which is why this runs in the pipeline and not inside any route.
19
+ *
20
+ * `upstream` is what headroom is told to forward to once it has done its work (its per-request base URL header). When it is set it is always this door's direct listener (every provider route declares exactly that), and the provider credential is then taken into custody for the hop: headroom receives placeholders and a hop id, and the direct listener restores the real headers when headroom forwards the request back. Neither headroom nor anything that binds a port on that plain-HTTP round trip ever holds the credential. Undefined leaves headroom's default upstream (Claude Code's API) in charge, which is exactly what an OAuth session wants, and the OAuth bearer then passes through untouched: headroom forwards it straight to that API, never back here, and uses it for its subscription tracking. The session's project identity is re-set here, and only here: the identity step stripped it from everything that leaves the machine, and headroom is the one consumer that needs it.
21
+ */
22
+ export declare function applyHeadroomHop(request: RoutedRequest, response: RoutedResponse, upstream: string | undefined, deps: HeadroomHopDeps): Promise<void>;
@@ -0,0 +1,109 @@
1
+ import type { IncomingHttpHeaders, IncomingMessage, ServerResponse } from "node:http";
2
+ import { type HeadroomHopDeps } from "./headroomHop";
3
+ import { type FrontDoorRoute, type RoutedRequest, type RoutedResponse, type SessionIdentity } from "./route";
4
+ /** What one routed response looked like as it started back to the client: the typed event response middleware observes. */
5
+ export interface RoutedResponseEvent {
6
+ readonly session: SessionIdentity;
7
+ /** The route that served the response. */
8
+ readonly route: string;
9
+ /** The request's method. */
10
+ readonly method: string;
11
+ /** The request target's path, without its query string. */
12
+ readonly path: string;
13
+ /** When the pipeline received the request, in epoch milliseconds from the injected clock. */
14
+ readonly receivedAt: number;
15
+ readonly status: number;
16
+ readonly headers: Readonly<Record<string, string>>;
17
+ }
18
+ /** How a routed response's body finished: written to the end, or cut short (the client went away, or the route destroyed the response after an upstream failure). */
19
+ export type ResponseOutcome = "completed" | "aborted";
20
+ /**
21
+ * Follows one response's body as the client receives it. `chunk` sees each chunk after it has been handed to the client's socket, so observing never holds a byte back; `end` is called exactly once, when the body has finished either way. Like the observer that returned it, a body observer that throws is logged and dropped for the rest of that response, never allowed to break it.
22
+ */
23
+ export interface ResponseBodyObserver {
24
+ readonly chunk: (chunk: Uint8Array) => void;
25
+ readonly end: (outcome: ResponseOutcome) => void;
26
+ }
27
+ /**
28
+ * Response middleware: called once per routed response, in registration order, once the route's response head is known and before any of it is written to the client. It returns a body observer when it wants to follow the body, or undefined when the head was all it needed. Observing must be side-effect-only: an observer that throws is logged and skipped, never allowed to break routing, and an observer must not block the response.
29
+ *
30
+ * Usage tracking registers here (see `src/usage/middleware.ts`): the head carries the rate-limit headers, and the body carries the token usage.
31
+ */
32
+ export type ResponseObserver = (event: RoutedResponseEvent) => ResponseBodyObserver | undefined;
33
+ /** The outcome of resolving a request's target to a route. */
34
+ export type RouteResolution = {
35
+ readonly ok: true;
36
+ readonly route: FrontDoorRoute;
37
+ } | {
38
+ readonly ok: false;
39
+ readonly status: number;
40
+ readonly message: string;
41
+ };
42
+ /** Everything the ordered pipeline depends on, injected so it runs against fakes in tests. */
43
+ export interface PipelineDeps {
44
+ /** Resolves the route a request's target names, reading whatever configuration the route needs. */
45
+ readonly resolveRoute: (request: RoutedRequest) => Promise<RouteResolution>;
46
+ /** The response middleware chain, run in order at each response head. This is where usage capture registers. */
47
+ readonly responseObservers: readonly ResponseObserver[];
48
+ /**
49
+ * The headroom hop, applied BEFORE a route whose declaration admits it whenever the session's launch resolved headroom on: headroom needs the Anthropic-shaped request, so it always sits in front of a translator, never behind one. Omitted by a listener that serves routes directly (the direct listener headroom forwards an in-process route back to), or when no headroom is wired.
50
+ */
51
+ readonly headroom?: HeadroomHopDeps;
52
+ /** The clock response middleware times requests by, in epoch milliseconds. */
53
+ readonly now: () => number;
54
+ readonly log: (line: string) => void;
55
+ /**
56
+ * The listener's admission step, run after identification and before anything is routed: it decides whether the request may be routed at all and with which headers. A client-facing listener admits a request carrying the per-launch capability of a live registered session, unchanged; the direct listener admits only what this process's own headroom hop sent back (the generation's hop secret plus a live custody id for the provider the path names) and swaps the hop's placeholder credentials for the real ones. A refused request is answered 401 without ever reaching a route, so a loopback process that never launched through agent-shim cannot spend a session's credentials or quota.
57
+ */
58
+ readonly admit: (request: AdmissionRequest) => Admission;
59
+ }
60
+ /** What the admission step sees of one request. */
61
+ interface AdmissionRequest {
62
+ /** The request target exactly as received. */
63
+ readonly url: string;
64
+ /** Every header as received, internal ones included: the capability and hop headers live here. */
65
+ readonly headers: Readonly<IncomingHttpHeaders>;
66
+ /** The headers the identity step judged forwardable, internal ones stripped. */
67
+ readonly forwardable: Readonly<IncomingHttpHeaders>;
68
+ }
69
+ /** The admission step's verdict: route with these headers, or refuse with this message. */
70
+ type Admission = {
71
+ readonly ok: true;
72
+ readonly headers: IncomingHttpHeaders;
73
+ } | {
74
+ readonly ok: false;
75
+ readonly message: string;
76
+ };
77
+ /** One request as a transport hands it to the pipeline, before anything has been identified or routed. */
78
+ export interface PipelineRequest {
79
+ readonly method: string;
80
+ /** The request target exactly as received, path and query together. */
81
+ readonly url: string;
82
+ readonly headers: IncomingHttpHeaders;
83
+ /** The request's body stream. */
84
+ readonly body: IncomingMessage;
85
+ /** Aborted the moment the client goes away. */
86
+ readonly signal: AbortSignal;
87
+ readonly response: ServerResponse;
88
+ }
89
+ /** What the response middleware learns about the request a routed response answers. */
90
+ interface ObservedRequest {
91
+ readonly method: string;
92
+ /** The request target's path, without its query string. */
93
+ readonly path: string;
94
+ readonly receivedAt: number;
95
+ }
96
+ /**
97
+ * Builds the response half a route writes through. The head is what runs the response middleware: `start` invokes every observer, in order, with the session, the request and the route's name before a byte reaches the client, and keeps the body observers they return. Each body chunk reaches those observers only after it has been handed to the client's socket, and their `end` runs once when the response ends, is destroyed, or the client goes away first, so following the body never holds a byte back and never misses how it finished. Body writes apply backpressure by waiting for the socket to drain, and resolve without writing once the client is gone, so a route streaming to a dead client finishes promptly rather than filling a buffer.
98
+ */
99
+ export declare function createRoutedResponse(response: ServerResponse, context: {
100
+ readonly deps: PipelineDeps;
101
+ readonly session: SessionIdentity;
102
+ readonly route: string;
103
+ readonly request: ObservedRequest;
104
+ }): RoutedResponse;
105
+ /**
106
+ * The ordered pipeline every routed session's request runs through: identify the session (splitting the launcher-injected headers out of what may leave the machine), then route, with the response middleware running at each response head. A route that cannot be resolved is answered as an Anthropic-shaped error, and the observers see that error like any other response, since quota and rate-limit state lives in exactly those.
107
+ */
108
+ export declare function serveRouted(request: PipelineRequest, deps: PipelineDeps): Promise<void>;
109
+ export {};
@@ -0,0 +1,44 @@
1
+ import http from "node:http";
2
+ import type { IncomingMessage } from "node:http";
3
+ import https from "node:https";
4
+ import type { LeafCert } from "./connect";
5
+ import type { PipelineRequest } from "./pipeline";
6
+ /** A front-door listener: plain HTTP for the direct listener only headroom reaches, HTTPS for the provider listener every provider session's child connects to. */
7
+ export type FrontDoorServer = http.Server | https.Server;
8
+ /**
9
+ * A front-door listener: the transport a routed session's requests arrive on. It turns each Node request into a `PipelineRequest` (the raw request plus an abort signal that fires the moment the client goes away before the response finished) and hands it to the pipeline; the pipeline owns identification, admission, middleware and routing.
10
+ *
11
+ * With `tls`, the listener serves HTTPS with that leaf, which is how the provider listener authenticates itself to the child: the leaf is signed by agent-shim's CA, whose key only the owning user can read, and the only CA the child trusts for a 127.0.0.1 certificate is that one (no public CA issues certificates for a loopback address), so a process that merely binds the port cannot complete a handshake the child accepts and never receives the request (credentials and capability included). Without `tls` it serves plain HTTP, which only the direct listener does: nothing that reaches it carries a real credential (see the credential custody).
12
+ *
13
+ * The disconnect is read from the response's `close` event, not the request's: the request emits `close` as soon as its body has been read, long before the response ends. `GET /healthz` is answered here, before the pipeline, because a readiness probe is not a routed session and carries no session headers or capability.
14
+ */
15
+ export declare function createFrontDoorServer(pipeline: (request: PipelineRequest) => Promise<void>, log: (line: string) => void, tls?: LeafCert): FrontDoorServer;
16
+ /**
17
+ * Probes a listener's health endpoint once: the check the supervisor's start and the tests use to know the door is actually serving, not merely bound. With `ca`, the probe speaks HTTPS and accepts only a certificate chaining to that CA for 127.0.0.1, so a healthy answer also proves who is answering; without it, plain HTTP. No capability is ever sent: the health endpoint needs none.
18
+ */
19
+ export declare function frontDoorHealthy(port: number, ca?: string): Promise<boolean>;
20
+ /** How a listener is bound. */
21
+ interface ListenOptions {
22
+ /** The sticky port to try first; any free port is used when it is taken or absent. */
23
+ readonly preferredPort?: number;
24
+ /** Receives the actual port the moment the bind succeeds, before the health probe. */
25
+ readonly onBound?: (port: number) => void;
26
+ /** The CA a TLS listener's leaf chains to, so its own start-up probe verifies the handshake a child will make; omitted for a plain-HTTP listener. */
27
+ readonly ca?: string;
28
+ /**
29
+ * Called for any error the listener emits once it is bound and serving. Attached only after the bind has settled: a server emits the sticky port's EADDRINUSE as an ordinary `error` event, and a handler attached before the bind would treat the fallback the bind is about to make as fatal.
30
+ */
31
+ readonly onError?: (error: Error) => void;
32
+ }
33
+ /**
34
+ * Binds the listener on `preferredPort` when it is free and any free port otherwise (bind, do not probe: check-then-bind races), probing its own health endpoint before resolving.
35
+ */
36
+ export declare function listenFrontDoor(server: FrontDoorServer, options?: Readonly<ListenOptions>): Promise<{
37
+ readonly port: number;
38
+ readonly close: () => Promise<void>;
39
+ }>;
40
+ /**
41
+ * Iterates a forwarded response's body as typed chunks. Node types the async iteration of an `IncomingMessage` as `any`, so every consumer narrows here once rather than each carrying its own assertion.
42
+ */
43
+ export declare function upstreamChunks(stream: IncomingMessage): AsyncGenerator<Uint8Array>;
44
+ export {};
@@ -0,0 +1,11 @@
1
+ /** One `ANTHROPIC_CUSTOM_HEADERS` entry as the child environment carries it. */
2
+ export interface CustomHeaderEntry {
3
+ readonly name: string;
4
+ readonly value: string;
5
+ }
6
+ /**
7
+ * Merges `ANTHROPIC_CUSTOM_HEADERS` blocks: every string in `existing` (as Claude Code writes them, `Name: Value` entries separated by newlines) parsed in order, then every entry of `additions` set on top, so a later block replaces an earlier block's value for the same header name while leaving every other entry untouched. The result is re-serialised in first-seen order with additions updated in place.
8
+ *
9
+ * Blank lines are skipped on parse; a header name keeps its own case exactly as written, since header names are case-insensitive on the wire but a statusline or log reading this value back should see what was set.
10
+ */
11
+ export declare function mergeAnthropicCustomHeaders(existing: readonly (string | undefined)[], additions: readonly CustomHeaderEntry[]): string;