agent-shim 7.6.1 → 7.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -1
- package/dist/cli.cjs +3714 -3687
- package/dist/index.cjs +1087 -183
- package/dist/index.mjs +1017 -168
- package/dist/types/cli/bool.d.ts +8 -0
- package/dist/types/cli/parsers.d.ts +40 -0
- package/dist/types/configProfilesStore.d.ts +61 -0
- package/dist/types/credential.d.ts +136 -0
- package/dist/types/credentialCache.d.ts +46 -0
- package/dist/types/directoryRulesStore.d.ts +43 -0
- package/dist/types/frontdoor/connect.d.ts +7 -6
- package/dist/types/frontdoor/route.d.ts +112 -0
- package/dist/types/frontdoor/tap.d.ts +2 -1
- package/dist/types/headroom/headers.d.ts +11 -0
- package/dist/types/identityStore.d.ts +85 -0
- package/dist/types/index.d.ts +7 -0
- package/dist/types/launcher/flags.d.ts +96 -0
- package/dist/types/launcher/identity.d.ts +95 -0
- package/dist/types/poolStore.d.ts +18 -0
- package/dist/types/providersStore.d.ts +137 -0
- package/package.json +1 -1
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The one boolean vocabulary every agent-shim input shares, CLI flag values and environment variables alike: `true` and `1` mean true, `false` and `0` mean false, and nothing else is a boolean (no case folding, no `yes`/`no`).
|
|
3
|
+
*
|
|
4
|
+
* Lives in a module with no imports so both `src/cli/parsers.ts` (which raises on anything else) and `src/cliError.ts` (which must never raise while reporting another failure) can share it without an import cycle.
|
|
5
|
+
* @param input - The raw text.
|
|
6
|
+
* @returns The boolean it spells, or undefined when it spells none.
|
|
7
|
+
*/
|
|
8
|
+
export declare function parseBoolWord(input: string): boolean | undefined;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { UsageError } from "../cliError";
|
|
2
|
+
/** One `<key>=<value>` pair, as parsed by `parsePair`. */
|
|
3
|
+
export interface ParsedPair {
|
|
4
|
+
readonly key: string;
|
|
5
|
+
readonly value: string;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Splits `input` on its *first* `=` into a key and a value. The value may itself contain further `=` characters (they stay part of the value); only the first `=` is treated as the separator.
|
|
9
|
+
*
|
|
10
|
+
* Throws when there is no `=` at all, or when the key half is empty (`=true`, or a leading `=`).
|
|
11
|
+
*/
|
|
12
|
+
export declare function parsePair(input: string): ParsedPair;
|
|
13
|
+
/** Parses `input` as a boolean in the shared vocabulary (`true`/`1`, `false`/`0`); throws on anything else. */
|
|
14
|
+
export declare function parseBool(input: string): boolean;
|
|
15
|
+
/** Raised when a boolean environment variable holds something outside the shared vocabulary. */
|
|
16
|
+
export declare class InvalidEnvBoolError extends UsageError {
|
|
17
|
+
readonly variable: string;
|
|
18
|
+
readonly value: string;
|
|
19
|
+
constructor(variable: string, value: string);
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Reads a boolean environment variable with the same vocabulary a CLI flag value uses. Unset and the empty string both mean "not given" (undefined), consistent with how every other agent-shim environment variable treats the empty string; anything outside the vocabulary throws `InvalidEnvBoolError` rather than silently reading as false.
|
|
23
|
+
*/
|
|
24
|
+
export declare function parseEnvBool(variable: string, value: string | undefined): boolean | undefined;
|
|
25
|
+
/**
|
|
26
|
+
* Parses a comma-separated list of `<key>=<bool>` pairs into a plain object, e.g. `"history=true,knowledge=false"` becomes `{ history: true, knowledge: false }`. Used only for the list-valued environment variables; flags repeat instead.
|
|
27
|
+
*
|
|
28
|
+
* An empty string parses to `{}`. A key repeated within the same list is not an error: the later occurrence wins, matching how a plain object literal with a repeated key behaves.
|
|
29
|
+
*/
|
|
30
|
+
export declare function parseBoolPairList(input: string): Record<string, boolean>;
|
|
31
|
+
/**
|
|
32
|
+
* Commander repeatable-option collector for one `--flag <key>=<bool>` occurrence: parses `value` and merges it over `previous`, so `--category history=true --category knowledge=false` accumulates into one object, the later occurrence winning on key collision.
|
|
33
|
+
*/
|
|
34
|
+
export declare function collectBoolPair(value: string, previous?: Readonly<Record<string, boolean>>): Record<string, boolean>;
|
|
35
|
+
/** Commander repeatable-option collector for one `--flag <KEY>=<VALUE>` occurrence with a free-text value, merged over `previous` the same way `collectBoolPair` merges. */
|
|
36
|
+
export declare function collectStringPair(value: string, previous?: Readonly<Record<string, string>>): Record<string, string>;
|
|
37
|
+
/** Commander repeatable-option collector for a plain repeated value (`--extends a --extends b`), preserving the order given. */
|
|
38
|
+
export declare function collectRepeated(value: string, previous?: readonly string[]): string[];
|
|
39
|
+
/** Commander option parser for a duration such as `30m`, `5h` or `7d` (a count followed by ms, s, m, h, d or w), returning milliseconds. A malformed value is a usage error naming the option. */
|
|
40
|
+
export declare function parseDurationOption(value: string): number;
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { type ConfigProfile, type GlobalConfig, type LaunchFlags } from "./config/schema";
|
|
2
|
+
import { CliError } from "./cliError";
|
|
3
|
+
import type { LayoutPaths } from "./paths";
|
|
4
|
+
/** Raised by any operation that requires a configuration profile to already exist, when it does not. */
|
|
5
|
+
export declare class ProfileNotFoundError extends CliError {
|
|
6
|
+
readonly profileName: string;
|
|
7
|
+
constructor(profileName: string);
|
|
8
|
+
}
|
|
9
|
+
/** Raised by `createProfile` (`profile add`) when a profile with the given name already has a file. */
|
|
10
|
+
export declare class ProfileAlreadyExistsError extends CliError {
|
|
11
|
+
readonly profileName: string;
|
|
12
|
+
constructor(profileName: string);
|
|
13
|
+
}
|
|
14
|
+
/** Raised when a `--category` patch names something other than one of the four overridable categories (e.g. `secret`, or a typo). */
|
|
15
|
+
export declare class InvalidCategoryNameError extends CliError {
|
|
16
|
+
readonly categoryName: string;
|
|
17
|
+
constructor(categoryName: string);
|
|
18
|
+
}
|
|
19
|
+
/** True when a profile file exists for `name`, regardless of whether it validates. */
|
|
20
|
+
export declare function profileExists(paths: LayoutPaths, name: string): boolean;
|
|
21
|
+
export declare function requireProfileExists(paths: LayoutPaths, name: string): void;
|
|
22
|
+
/** Reads and validates one configuration profile, or undefined when it does not exist. */
|
|
23
|
+
export declare function readProfile(paths: LayoutPaths, name: string): ConfigProfile | undefined;
|
|
24
|
+
/**
|
|
25
|
+
* Creates a new configuration profile, empty apart from the optional `extends` list and description. Throws `ProfileAlreadyExistsError` if a profile with this name already has a file, and `ConfigValidationError` when the result fails `ConfigProfileSchema` (e.g. an empty `--extends ""` name), rather than letting the underlying `ZodError` escape as an unhandled crash.
|
|
26
|
+
*/
|
|
27
|
+
export declare function createProfile(paths: LayoutPaths, name: string, extendsList?: readonly string[], description?: string): ConfigProfile;
|
|
28
|
+
/** One profile as reported by `listProfiles`. */
|
|
29
|
+
export interface ProfileListEntry {
|
|
30
|
+
readonly name: string;
|
|
31
|
+
readonly profile: ConfigProfile;
|
|
32
|
+
}
|
|
33
|
+
/** Lists every configuration profile under `configProfilesDir` that has a valid `<name>.json`. */
|
|
34
|
+
export declare function listProfiles(paths: LayoutPaths): readonly ProfileListEntry[];
|
|
35
|
+
/** Reads the user-global `~/.agent-shim/config.json`, or undefined when it does not exist. */
|
|
36
|
+
export declare function readGlobalConfig(paths: LayoutPaths): GlobalConfig | undefined;
|
|
37
|
+
/** Sets the user-global default configuration profile in `~/.agent-shim/config.json`, creating the file if it doesn't exist yet. Whether the profile exists is the caller's concern: `profile use` checks it first. */
|
|
38
|
+
export declare function setGlobalDefaultProfile(paths: LayoutPaths, name: string): GlobalConfig;
|
|
39
|
+
/**
|
|
40
|
+
* Merges `patch` (from one or more `--category cat=bool` flags) into `profile`'s own `categories` object and writes it back.
|
|
41
|
+
*
|
|
42
|
+
* Throws `InvalidCategoryNameError` for any key that isn't one of the four overridable categories — `secret` can never be toggled by any configuration layer, this included.
|
|
43
|
+
*/
|
|
44
|
+
export declare function setProfileCategories(paths: LayoutPaths, name: string, patch: Readonly<Record<string, boolean>>): ConfigProfile;
|
|
45
|
+
/**
|
|
46
|
+
* Merges `patch` (from one or more `--entry "path"=bool` flags) into `profile`'s own `entries` object and writes it back.
|
|
47
|
+
*
|
|
48
|
+
* Key validity (the `<category>/<real-relative-path>` prefix requirement) is enforced by `ConfigProfileSchema`'s own `EntriesSchema` at write time — an invalid key surfaces as the usual `ConfigValidationError`.
|
|
49
|
+
*/
|
|
50
|
+
export declare function setProfileEntries(paths: LayoutPaths, name: string, patch: Readonly<Record<string, boolean>>): ConfigProfile;
|
|
51
|
+
/**
|
|
52
|
+
* Merges `patch` into `profile`'s own `launch` object and writes it back. A key present in `patch` with the value `undefined` removes that setting outright (a `--no-launch-provider`, say) rather than overwriting it, and a `launch` object left with no settings is removed too.
|
|
53
|
+
*/
|
|
54
|
+
export declare function setProfileLaunchFlags(paths: LayoutPaths, name: string, patch: Readonly<LaunchFlags>): ConfigProfile;
|
|
55
|
+
/** Replaces `profile`'s own `extends` list and/or description; an empty list or an undefined description removes the field. */
|
|
56
|
+
export declare function setProfileMetadata(paths: LayoutPaths, name: string, patch: Readonly<{
|
|
57
|
+
extends?: readonly string[];
|
|
58
|
+
description?: string | false;
|
|
59
|
+
}>): ConfigProfile;
|
|
60
|
+
/** Deletes a configuration profile's file. Throws `ProfileNotFoundError` when it does not exist. Nothing that references it by name is rewritten; `agent-shim doctor` reports any identity default, directory rule or `extends` left pointing at it. */
|
|
61
|
+
export declare function removeProfile(paths: LayoutPaths, name: string): void;
|
|
@@ -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;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { IncomingHttpHeaders, IncomingMessage, ServerResponse } from "node:http";
|
|
2
2
|
import type * as net from "node:net";
|
|
3
|
+
import type { Duplex } from "node:stream";
|
|
3
4
|
import type { ConnectCapture, PassthroughObserver } from "./capture";
|
|
4
5
|
/**
|
|
5
6
|
* The front door's CONNECT surface, so an OAuth session can have agent-shim's routing AND Claude Code's Remote Control at once: Remote Control refuses any `ANTHROPIC_BASE_URL` other than the real API, but happily honours `HTTPS_PROXY`, so OAuth launches route at the proxy layer instead.
|
|
@@ -125,8 +126,8 @@ export interface ConnectListenerHandle {
|
|
|
125
126
|
}
|
|
126
127
|
/** Presents one leaf's server-side TLS on CONNECTed sockets. */
|
|
127
128
|
interface TlsAcceptor {
|
|
128
|
-
/** Starts the TLS handshake on a socket that has just received its CONNECT response. */
|
|
129
|
-
readonly accept: (socket: net.Socket) => void;
|
|
129
|
+
/** Starts the TLS handshake on a socket that has just received its CONNECT response, carrying `context` to the handshake's completion untouched: whatever the caller knows about this connection (its tunnel's capability) reaches `onSecure` without the acceptor interpreting it. */
|
|
130
|
+
readonly accept: (socket: net.Socket, context: string) => void;
|
|
130
131
|
/** Drops every session this acceptor is terminating. */
|
|
131
132
|
readonly close: () => void;
|
|
132
133
|
}
|
|
@@ -141,12 +142,12 @@ interface HttpParserSession {
|
|
|
141
142
|
export interface ConnectEffects {
|
|
142
143
|
/** Binds a loopback TCP listener; `preferredPort` is tried first and any free port used when it is taken (bind, do not probe: check-then-bind races). Resolves with the bound port and a close handle. */
|
|
143
144
|
readonly listenLoopback: (preferredPort: number | undefined, onSocket: (socket: net.Socket) => void) => Promise<ConnectListenerHandle>;
|
|
144
|
-
/** Builds the TLS terminator for one leaf, handing each successfully handshaked session to `onSecure
|
|
145
|
-
readonly createTlsAcceptor: (leaf: LeafCert, onSecure: (secure: net.Socket) => void, alpnProtocols?: readonly string[]) => TlsAcceptor;
|
|
145
|
+
/** Builds the TLS terminator for one leaf, handing each successfully handshaked session to `onSecure` together with the context its `accept` call carried. `alpnProtocols`, when given, is advertised during the handshake, which is how a tap host's session can offer HTTP/2 the way its real server does. */
|
|
146
|
+
readonly createTlsAcceptor: (leaf: LeafCert, onSecure: (secure: net.Socket, context: string) => void, alpnProtocols?: readonly string[]) => TlsAcceptor;
|
|
146
147
|
/** Opens a TLS connection to a real host, presenting its name as SNI, for a tap session's upstream half. `clientAlpn`, when given, is the protocol the client negotiated with the surface and the only one offered upstream, so the tap never changes the channel's protocol. */
|
|
147
148
|
readonly connectTlsUpstream: (host: string, port: number, clientAlpn: string | undefined) => Promise<net.Socket>;
|
|
148
|
-
/** Builds the HTTP parser bound to one request handler. `onUpgrade`, when given,
|
|
149
|
-
readonly createHttpSession: (handler: ConnectRequestHandler, onUpgrade?: (request: IncomingMessage) => void) => HttpParserSession;
|
|
149
|
+
/** Builds the HTTP parser bound to one request handler. `onUpgrade`, when given, owns every HTTP upgrade request arriving on a terminated session, socket and already-read bytes included; a session without one lets Node destroy the upgrade, its own no-listener behaviour. */
|
|
150
|
+
readonly createHttpSession: (handler: ConnectRequestHandler, onUpgrade?: (request: IncomingMessage, socket: Duplex, head: Buffer) => void) => HttpParserSession;
|
|
150
151
|
/** Opens a raw TCP connection for a blind tunnel. */
|
|
151
152
|
readonly connectTcp: (host: string, port: number) => Promise<net.Socket>;
|
|
152
153
|
/** Streams one request to `target` and the response back, never buffering a body. When `observer` is given, the forwarding drives it with additive listeners (request and response chunks arrive to both the pipe and the observer) so capturing never changes the forwarding's own flow control. */
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import type { IncomingHttpHeaders, IncomingMessage } from "node:http";
|
|
2
|
+
/**
|
|
3
|
+
* The agent-shim-internal headers the launcher injects through `ANTHROPIC_CUSTOM_HEADERS` so the front door can identify and route a request's session. None of them may ever leave the machine: the identity step strips every one before a route sees the request, so no upstream, and not even the headroom hop, learns what they say.
|
|
4
|
+
*/
|
|
5
|
+
export declare const INTERNAL_HEADER_NAMES: readonly ["x-agent-shim-identity", "x-agent-shim-session", "x-agent-shim-headroom", "x-agent-shim-auth", "x-agent-shim-hop", "x-agent-shim-hop-id", "x-headroom-project-id", "x-headroom-base-url"];
|
|
6
|
+
/** Carries the launching identity's name (the `@name` the launch resolved), set by the launcher and stripped at the door. */
|
|
7
|
+
export declare const IDENTITY_HEADER = "x-agent-shim-identity";
|
|
8
|
+
/** Carries one launch's session id, generated by the launcher and stripped at the door; response middleware keys its observations by it. */
|
|
9
|
+
export declare const SESSION_HEADER = "x-agent-shim-session";
|
|
10
|
+
/** Set to `1` when the launch resolved headroom on, so the front door knows this session's requests must take the headroom hop; stripped at the door. */
|
|
11
|
+
export declare const HEADROOM_FLAG_HEADER = "x-agent-shim-headroom";
|
|
12
|
+
/**
|
|
13
|
+
* Carries the project identity headroom scopes its memory state to (the git repository root of the launch directory). Also injected by the launcher and stripped at the door: the headroom hop re-sets it on its own request, so it reaches headroom and nowhere else.
|
|
14
|
+
*/
|
|
15
|
+
export declare const PROJECT_ID_HEADER = "x-headroom-project-id";
|
|
16
|
+
/**
|
|
17
|
+
* Headroom's per-request upstream selector, which the child used to carry and the door now owns: the door strips any inbound copy (a leftover from an older launcher's environment would otherwise redirect the daemon behind the door's back) and its hop sets it fresh when a route names an upstream.
|
|
18
|
+
*/
|
|
19
|
+
export declare const HEADROOM_BASE_URL_HEADER = "x-headroom-base-url";
|
|
20
|
+
/**
|
|
21
|
+
* The per-launch capability a client-facing listener requires before it routes anything: a random token the launcher generates, hands the child through `ANTHROPIC_CUSTOM_HEADERS`, and records in the door's session registry, so a loopback process that never launched through agent-shim cannot spend the session's credentials or quota. Stripped at the door like every internal header.
|
|
22
|
+
*/
|
|
23
|
+
export declare const AUTH_HEADER = "x-agent-shim-auth";
|
|
24
|
+
/**
|
|
25
|
+
* The capability the direct listener requires: a per-generation random secret held only in the door process's memory, set by the headroom hop on the requests it forwards back. A loopback process that finds the direct port still cannot use it, and nothing on disk ever holds the secret.
|
|
26
|
+
*/
|
|
27
|
+
export declare const HOP_SECRET_HEADER = "x-agent-shim-hop";
|
|
28
|
+
/**
|
|
29
|
+
* Names the credential custody record one headroom hop holds: a random id the hop sets on the request it hands headroom, alongside placeholders where the real credential headers were. The direct listener exchanges a live id (and only for the provider the record was made for) for the real headers before routing, so the credential itself never crosses headroom or the plain-HTTP leg back.
|
|
30
|
+
*/
|
|
31
|
+
export declare const HOP_ID_HEADER = "x-agent-shim-hop-id";
|
|
32
|
+
/** What the identity step learned about one request from the launcher-injected headers. */
|
|
33
|
+
export interface SessionIdentity {
|
|
34
|
+
/** The identity name the launch resolved, when it resolved one. */
|
|
35
|
+
readonly identity: string | undefined;
|
|
36
|
+
/** The per-launch session id, when the launcher injected one. */
|
|
37
|
+
readonly sessionId: string | undefined;
|
|
38
|
+
/** Whether this launch resolved headroom on, so its requests must pass through the headroom hop before their route. */
|
|
39
|
+
readonly headroom: boolean;
|
|
40
|
+
/** The project identity headroom scopes memory to, re-set on the headroom hop and sent nowhere else. */
|
|
41
|
+
readonly projectId: string | undefined;
|
|
42
|
+
}
|
|
43
|
+
/** The outcome of the pipeline's first step: what the session is, and the headers that may actually leave the machine. */
|
|
44
|
+
export interface IdentifiedRequest {
|
|
45
|
+
readonly session: SessionIdentity;
|
|
46
|
+
/**
|
|
47
|
+
* The request's headers with every internal header removed. A route forwards these, and only these, onwards: the identity and session headers are for the front door alone and must never reach an upstream, the headroom hop, or a translator's backend call.
|
|
48
|
+
*/
|
|
49
|
+
readonly forwardableHeaders: IncomingHttpHeaders;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* The identity step: splits a request's headers into what the front door may learn (the session) and what may leave the machine (everything else). Internal header names are matched case-insensitively, which is how the transport already normalises them.
|
|
53
|
+
*/
|
|
54
|
+
export declare function identifyRequest(headers: Readonly<IncomingHttpHeaders>): IdentifiedRequest;
|
|
55
|
+
/** The path prefix every provider-scoped request sits under: a routed session's base URL is the front door's address plus `/providers/<name>`, so one listener serves any number of providers, each with its own credential and translation settings. */
|
|
56
|
+
export declare const PROVIDER_PATH_PREFIX = "/providers/";
|
|
57
|
+
/** The base URL a provider's sessions are pointed at: HTTPS, because the child sends its provider credential on every request and only a listener presenting a leaf signed by agent-shim's CA may receive it. */
|
|
58
|
+
export declare function providerBaseUrl(port: number, provider: string): string;
|
|
59
|
+
/** The bare origin of a loopback listener, which is what a headroom hop is told to forward to: headroom appends the client's own request path to it. */
|
|
60
|
+
export declare function directOrigin(port: number): string;
|
|
61
|
+
/** Splits `/providers/<name>/<rest>` into the provider name and the rest, or undefined for any other path. */
|
|
62
|
+
export declare function parseProviderPath(path: string): {
|
|
63
|
+
readonly provider: string;
|
|
64
|
+
readonly rest: string;
|
|
65
|
+
} | undefined;
|
|
66
|
+
/** One request as a route sees it: the transport's own request and abort signal, plus what the identity step resolved. */
|
|
67
|
+
export interface RoutedRequest {
|
|
68
|
+
readonly method: string;
|
|
69
|
+
/** The request target exactly as received, path and query together. */
|
|
70
|
+
readonly url: string;
|
|
71
|
+
/** The request's headers with every internal header already stripped: exactly what a route may forward onwards. */
|
|
72
|
+
readonly headers: IncomingHttpHeaders;
|
|
73
|
+
/** The request's body stream: a route either pipes it onward untouched or reads it whole, never both. */
|
|
74
|
+
readonly body: IncomingMessage;
|
|
75
|
+
/** Aborted the moment the client goes away; a route aborts its own upstream work with it. */
|
|
76
|
+
readonly signal: AbortSignal;
|
|
77
|
+
readonly session: SessionIdentity;
|
|
78
|
+
}
|
|
79
|
+
/** The response half a route writes through, so the pipeline can run response middleware at the head and apply backpressure uniformly. */
|
|
80
|
+
export interface RoutedResponse {
|
|
81
|
+
/** Whether the head has already been written. */
|
|
82
|
+
readonly headersSent: boolean;
|
|
83
|
+
/** Writes the response head, running every response observer first. Must be called exactly once, before any write. */
|
|
84
|
+
readonly start: (status: number, headers: Readonly<Record<string, string>>) => void;
|
|
85
|
+
/** Sends the head on its way before any body chunk exists: a streaming response's client should see its headers the moment they are known. */
|
|
86
|
+
readonly flush: () => void;
|
|
87
|
+
/**
|
|
88
|
+
* Writes one body chunk, waiting for the socket to take it when the client is slow (backpressure: a slow client slows the route, and its upstream, instead of growing a buffer without bound). Resolves early, without writing, once the client is gone.
|
|
89
|
+
*/
|
|
90
|
+
readonly write: (chunk: string | Uint8Array) => Promise<void>;
|
|
91
|
+
/** Ends the response. */
|
|
92
|
+
readonly end: () => void;
|
|
93
|
+
/** Drops the connection: an upstream failure after the head went out, where no status can be sent any more. */
|
|
94
|
+
readonly destroy: () => void;
|
|
95
|
+
}
|
|
96
|
+
/** One pluggable destination the front door routes a request to. */
|
|
97
|
+
export interface FrontDoorRoute {
|
|
98
|
+
/** The route's name, for logs and for response middleware context. */
|
|
99
|
+
readonly name: string;
|
|
100
|
+
/**
|
|
101
|
+
* Whether the headroom hop may sit in front of this route. Headroom needs the Anthropic-shaped request, so a protocol translator built on an Anthropic-compatible surface opts in, and a route whose request shape headroom could not understand opts out.
|
|
102
|
+
*/
|
|
103
|
+
readonly headroomEligible: boolean;
|
|
104
|
+
/**
|
|
105
|
+
* The upstream headroom is told to use (its per-request base URL header) when the hop sits in front of this route. When defined it is always this door's direct listener, which is what lets the hop keep the route's credential in custody and the direct listener restore it; a route must never name any other address here. Undefined leaves headroom's own default upstream in charge, which is Claude Code's API.
|
|
106
|
+
*/
|
|
107
|
+
readonly headroomUpstream: string | undefined;
|
|
108
|
+
/**
|
|
109
|
+
* Serves the request directly: headroom has already been applied, or does not apply to this route. Writes the response through `response` and resolves once the response has ended.
|
|
110
|
+
*/
|
|
111
|
+
readonly serve: (request: RoutedRequest, response: RoutedResponse) => Promise<void>;
|
|
112
|
+
}
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import type * as net from "node:net";
|
|
2
|
+
import type { Duplex } from "node:stream";
|
|
2
3
|
import type { StreamTap } from "./capture";
|
|
3
4
|
/**
|
|
4
5
|
* Pumps one already-TLS-terminated stream byte for byte to its real host over a fresh TLS connection, teeing every chunk in both directions into `tap` when one is given. This is the instrumentation for a host whose protocol the surface does not parse: the claude.ai control plane carries Remote Control on a channel that mixes shapes (observed under capture: an HTTP/1.1-shaped token request first, then binary HTTP/2 frames around it), so parsing it as either protocol would destroy or distort it. Pass everything through unchanged and record, never interpret: the frames are decoded offline, and a protocol the door does not understand keeps working exactly as the blind tunnel served it.
|
|
5
6
|
*
|
|
6
7
|
* Lifecycle mirrors `blindTunnel`: whoever goes away first takes the other side with it, so neither half of a tap session ever outlives its peer. The tap's listeners ride beside the pipes, additive like every other observer in this surface, so a recording failure fails loudly rather than silently stopping a diagnostic.
|
|
7
8
|
*/
|
|
8
|
-
export declare function pumpTapSession(secure:
|
|
9
|
+
export declare function pumpTapSession(secure: Duplex, openUpstream: (clientAlpn: string | undefined) => Promise<net.Socket>, tap: StreamTap | undefined, prefix?: Readonly<Buffer>): void;
|
|
@@ -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;
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { type CredentialCache, type CredentialSource, type CredentialTarget, type Identity } from "./config/schema";
|
|
2
|
+
import { CliError } from "./cliError";
|
|
3
|
+
import type { LayoutPaths } from "./paths";
|
|
4
|
+
/** Raised by any operation that requires an identity to already exist, when it does not. */
|
|
5
|
+
export declare class IdentityNotFoundError extends CliError {
|
|
6
|
+
readonly name: string;
|
|
7
|
+
constructor(name: string);
|
|
8
|
+
}
|
|
9
|
+
/** Raised by `addIdentity` when an identity with the given name already has an `identity.json`. */
|
|
10
|
+
export declare class IdentityAlreadyExistsError extends CliError {
|
|
11
|
+
readonly identityName: string;
|
|
12
|
+
constructor(identityName: string);
|
|
13
|
+
}
|
|
14
|
+
/** Raised by `addIdentity` when `name` fails `IdentitySchema`'s own naming rule — it must start with a letter or number and may then contain letters, numbers, dots, hyphens, underscores, and at signs, so an email address names an identity directly while a *leading* `@` stays invalid (it would collide with the `@name` selector syntax's first-`@` split). */
|
|
15
|
+
export declare class InvalidIdentityNameError extends CliError {
|
|
16
|
+
readonly attemptedName: string;
|
|
17
|
+
constructor(attemptedName: string);
|
|
18
|
+
}
|
|
19
|
+
export declare function identityExists(paths: LayoutPaths, name: string): boolean;
|
|
20
|
+
/** Reads and validates one identity's `identity.json`, or undefined when it does not exist. */
|
|
21
|
+
export declare function readIdentity(paths: LayoutPaths, name: string): Identity | undefined;
|
|
22
|
+
/**
|
|
23
|
+
* Creates a new identity: validates `name` against `IdentitySchema`'s own naming rule and writes a fresh `identity.json` with `allowAmbientCredential: false` and no `defaultConfigProfile`.
|
|
24
|
+
*
|
|
25
|
+
* Throws `IdentityAlreadyExistsError` if an identity with this name already has an `identity.json` — `add` never silently overwrites an existing identity. Throws `InvalidIdentityNameError` when `name` fails `IdentitySchema`'s naming rule, rather than letting the underlying `ZodError` escape as an unhandled crash.
|
|
26
|
+
*/
|
|
27
|
+
export declare function addIdentity(paths: LayoutPaths, name: string): Identity;
|
|
28
|
+
/**
|
|
29
|
+
* Persists `name` as the active identity, written atomically as plain text (not JSON — this file is read by `decideIdentity` in `src/launcher/identity.ts` via a simple UTF-8 read-and-trim, matching the README's documented `~/.agent-shim/active-identity` file).
|
|
30
|
+
*
|
|
31
|
+
* `name` may be a `pool:<name>` selector, which makes launches pick a member of that pool. Throws `IdentityNotFoundError` when no identity with this name exists yet, and `PoolNotFoundError` for a pool that is not defined: selecting either would silently persist a name nothing else can ever load.
|
|
32
|
+
*/
|
|
33
|
+
export declare function useIdentity(paths: LayoutPaths, name: string): void;
|
|
34
|
+
/** Reads the persisted active-identity file, or undefined when none is set. */
|
|
35
|
+
export declare function readActiveIdentity(paths: LayoutPaths): string | undefined;
|
|
36
|
+
/**
|
|
37
|
+
* Whether a directory name directly under `identitiesDir` names an actual identity, rather than one of agent-shim's own farm directories.
|
|
38
|
+
*
|
|
39
|
+
* `IdentitySchema` requires an identity name to start with a letter or digit, so a leading `.` can only be a resync's own bookkeeping: a `.<identity>.scratch.<suffix>` tree still being built, or a `.<identity>.previous.<suffix>` superseded farm retained for `agent-shim identity resolve-conflicts`. Neither is an identity, and neither should be reported as a broken one for lacking an `identity.json` a resync never put there.
|
|
40
|
+
*/
|
|
41
|
+
export declare function isIdentityDirectoryName(name: string): boolean;
|
|
42
|
+
/** One identity as reported by `listIdentities`, whose `identity.json` parsed and validated cleanly. */
|
|
43
|
+
export interface IdentityListEntry {
|
|
44
|
+
readonly name: string;
|
|
45
|
+
readonly identity: Identity;
|
|
46
|
+
readonly isActive: boolean;
|
|
47
|
+
readonly problem?: never;
|
|
48
|
+
}
|
|
49
|
+
/** One identity whose `identity.json` is present but unreadable — malformed JSON, or valid JSON this version's `IdentitySchema` rejects. `problem` carries the reason, already flattened onto a single line. */
|
|
50
|
+
export interface UnreadableIdentityListEntry {
|
|
51
|
+
readonly name: string;
|
|
52
|
+
readonly identity?: never;
|
|
53
|
+
readonly isActive: boolean;
|
|
54
|
+
readonly problem: string;
|
|
55
|
+
}
|
|
56
|
+
/** Either shape `listIdentities` can report, discriminated by which of `identity`/`problem` is present rather than by a tag field — the two are never simultaneously satisfiable. */
|
|
57
|
+
export type IdentityListing = IdentityListEntry | UnreadableIdentityListEntry;
|
|
58
|
+
/**
|
|
59
|
+
* Lists every identity under `identitiesDir`, marking which one (if any) is currently active.
|
|
60
|
+
*
|
|
61
|
+
* One identity whose `identity.json` cannot be read is reported as its own `UnreadableIdentityListEntry` rather than aborting the whole listing. A single bad file blocking `identity list` outright is exactly the failure mode that hides every *other* identity from view at the moment the user most needs to see them — and the file need not even be corrupt to land here, since a name written by a newer agent-shim whose naming rule has since widened is rejected outright by an older binary's own copy of `IdentitySchema`.
|
|
62
|
+
*/
|
|
63
|
+
export declare function listIdentities(paths: LayoutPaths): readonly IdentityListing[];
|
|
64
|
+
/**
|
|
65
|
+
* Sets `identity`'s `defaultConfigProfile` field, or clears it when `profileName` is undefined. Throws `IdentityNotFoundError` when the identity does not exist. Whether the profile exists is the caller's concern: `identity set --default-profile` checks it first.
|
|
66
|
+
*/
|
|
67
|
+
export declare function setDefaultConfigProfile(paths: LayoutPaths, identityName: string, profileName: string | undefined): Identity;
|
|
68
|
+
/**
|
|
69
|
+
* Patches `identity`'s `allowAmbientCredential` field. Throws `IdentityNotFoundError` when the identity does not exist.
|
|
70
|
+
*/
|
|
71
|
+
export declare function setAllowAmbientCredential(paths: LayoutPaths, identityName: string, allow: boolean): Identity;
|
|
72
|
+
/** The change `setIdentityCredential` makes: new sources and/or a new target for the credential block, or `false` to remove the block and return the identity to its stored login. */
|
|
73
|
+
export type IdentityCredentialChange = {
|
|
74
|
+
readonly sources?: readonly CredentialSource[];
|
|
75
|
+
readonly target?: CredentialTarget;
|
|
76
|
+
readonly cache?: CredentialCache | false;
|
|
77
|
+
} | false;
|
|
78
|
+
/**
|
|
79
|
+
* Sets, changes or removes `identityName`'s credential block. New `sources` replace the whole ordered list (the order is the meaning); a `target` alone keeps the existing sources, and so needs a credential block to exist already. Throws `IdentityNotFoundError` when the identity does not exist, and `UsageError` when only a target is given for an identity with no credential yet.
|
|
80
|
+
*/
|
|
81
|
+
export declare function setIdentityCredential(paths: LayoutPaths, identityName: string, change: IdentityCredentialChange): Identity;
|
|
82
|
+
/**
|
|
83
|
+
* Deletes identity `name` entirely: its directory under `identitiesDir` (the farm, its `identity.json`, and whatever that farm holds unshared, credentials included), its resync lock, any scratch or superseded farm a resync left behind, and the active-identity selection when it names this identity. Data shared into `~/.claude` stays there, since the farm only ever symlinks to it. Throws `IdentityNotFoundError` when the identity does not exist.
|
|
84
|
+
*/
|
|
85
|
+
export declare function removeIdentity(paths: LayoutPaths, name: string): void;
|