@alpacakit/agents-conventions 0.1.0-beta.36 → 0.1.0-beta.39

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,59 +1,20 @@
1
1
  /**
2
- * AlpacaLoop-family conventions for `@alpacakit/agents` machine-local
3
- * config. This lives OUTSIDE the neutral `@alpacakit/agents` package on
4
- * purpose: it encodes AlpacaLoop-specific paths, keyring service names, and
5
- * the managed-home layout that no general OSS consumer needs. It builds on
6
- * the agents package's PUBLIC API only, so it is a plain consumer — the
7
- * same shape any third party would write for their own family convention.
2
+ * AlpacaKit-family conventions for `@alpacakit/agents` machine-local config.
3
+ * This lives OUTSIDE the neutral `@alpacakit/agents` package on purpose: it
4
+ * encodes the family's shared home, keyring service names, managed-home
5
+ * layout, and the ORDER and FAILURE POLICY of registering an agent — none of
6
+ * which a general OSS consumer needs. It builds on the agents package's
7
+ * PUBLIC API only, so it is a plain consumer: the same shape any third party
8
+ * would write for their own family convention.
8
9
  *
9
- * Not published (`private`): the public source doubles as the worked
10
- * example, without adding org policy to the npm surface.
10
+ * Products call the procedures here and print the result; they never re-decide
11
+ * the order or the failure policy. The `agent` command surface that mounts
12
+ * them lives on `./cli` (data only), so importing the procedures never pulls
13
+ * in `@alpacakit/channels`.
11
14
  */
12
- import path from "node:path";
13
- import { keyringResolver } from "@alpacakit/agents/keyring";
14
- export const ALPACA_LOOP_AGENTS_CONFIG_ENV_VAR = "ALPACA_AGENTS_HOME";
15
- export const ALPACA_LOOP_AGENTS_CONFIG_DIRECTORY = ".alpaca/agents";
16
- export const ALPACA_LOOP_AGENTS_CONFIG_KEYRING_SERVICE = "alpacaloop.agents-home";
17
- export const ALPACA_LOOP_AGENTS_CONFIG_KEYRING_RESOLVER = "agents-home-keyring";
18
- const ALPACA_LOOP_AGENTS_CONFIG_MANAGED_HOMES_DIRECTORY = "managed-homes";
19
- const ALPACA_LOOP_AGENTS_CONFIG_KEYRING_ACCOUNT = {
20
- prefix: "profile",
21
- apiKeySuffix: "api-key",
22
- separator: ":",
23
- };
24
- export function resolveAgentsConfigOptions(input) {
25
- return {
26
- rootDir: resolveAgentsConfigRootDir(input),
27
- secretResolvers: [createKeyringResolverEntry()],
28
- };
29
- }
30
- export function createKeyringSecretRef(profileId) {
31
- return {
32
- resolver: ALPACA_LOOP_AGENTS_CONFIG_KEYRING_RESOLVER,
33
- id: [
34
- ALPACA_LOOP_AGENTS_CONFIG_KEYRING_ACCOUNT.prefix,
35
- profileId,
36
- ALPACA_LOOP_AGENTS_CONFIG_KEYRING_ACCOUNT.apiKeySuffix,
37
- ].join(ALPACA_LOOP_AGENTS_CONFIG_KEYRING_ACCOUNT.separator),
38
- };
39
- }
40
- export function resolveManagedHomePath(input) {
41
- return path.join(input.rootDir, ALPACA_LOOP_AGENTS_CONFIG_MANAGED_HOMES_DIRECTORY, input.profileId);
42
- }
43
- function resolveAgentsConfigRootDir(input) {
44
- return (readNonBlankEnvValue(input.env, ALPACA_LOOP_AGENTS_CONFIG_ENV_VAR) ??
45
- path.join(input.homeDir, ALPACA_LOOP_AGENTS_CONFIG_DIRECTORY));
46
- }
47
- function createKeyringResolverEntry() {
48
- return {
49
- name: ALPACA_LOOP_AGENTS_CONFIG_KEYRING_RESOLVER,
50
- resolver: keyringResolver({
51
- service: ALPACA_LOOP_AGENTS_CONFIG_KEYRING_SERVICE,
52
- }),
53
- };
54
- }
55
- /** Inlined from the agents package's internal env helper (not exported). */
56
- function readNonBlankEnvValue(env, name) {
57
- const value = env[name];
58
- return typeof value === "string" && value.trim().length > 0 ? value : null;
59
- }
15
+ export { createRuntimeFactory, listAgentsHome, openAgentsHome, } from "./home.js";
16
+ export { AGENTS_HOME_RECORD_NAME_MAX_LENGTH, AGENTS_HOME_RECORD_NAME_PATTERN, AGENTS_HOME_RECORD_NAME_RULE, INVALID_NAME_RESULT_KIND, isAgentsHomeRecordName, rejectInvalidName, } from "./names.js";
17
+ export { createKeyringSecretRef, isManagedHomePath, resolveAgentsConfigOptions, resolveAgentsHome, resolveManagedHomePath, } from "./paths.js";
18
+ export { ADD_AGENT_CONFIG_RESULT, ADD_API_KEY_PROFILE_RESULT, addAgentConfig, addApiKeyProfile, LOGIN_MANAGED_HOME_RESULT, loginManagedHome, } from "./register.js";
19
+ export { REMOVE_AGENTS_HOME_ENTRY_RESULT, removeAgentsHomeEntry, } from "./remove.js";
20
+ export { AGENT_ADD_KIND, AGENTS_HOME_DIRECTORY, AGENTS_HOME_ENV_VAR, AGENTS_KEYRING_ACCOUNT, AGENTS_KEYRING_RESOLVER, AGENTS_KEYRING_SERVICE, AGENTS_MANAGED_HOMES_DIRECTORY, } from "./values.js";
@@ -0,0 +1,38 @@
1
+ /**
2
+ * The grammar of a record name in the shared home — profile ids and agent
3
+ * config ids — spelled ONCE.
4
+ *
5
+ * A record name is not free text: it becomes a path segment under
6
+ * `managed-homes/` and a segment of a keychain account
7
+ * (`profile:<name>:api-key`). An unconstrained name therefore decides where
8
+ * this package writes and what it deletes — `../../../escape` would put a
9
+ * login home outside the shared home and make `agent remove` delete a tree
10
+ * that is not ours. Constraining the vocabulary is the fix; checking for
11
+ * `..` at each use site is not, because the next use site forgets.
12
+ *
13
+ * This narrows, and so satisfies, the agents package's own id rule
14
+ * (non-empty, non-blank).
15
+ */
16
+ /** Lowercase, starts with a letter or digit, no separators of any kind. */
17
+ export declare const AGENTS_HOME_RECORD_NAME_PATTERN: RegExp;
18
+ export declare const AGENTS_HOME_RECORD_NAME_MAX_LENGTH = 64;
19
+ /**
20
+ * The rule in words, carried on the rejection so a product can tell the
21
+ * operator what to type without restating the grammar.
22
+ */
23
+ export declare const AGENTS_HOME_RECORD_NAME_RULE = "A name must be 1 to 64 characters of lowercase letters, digits, \".\", \"-\" or \"_\", and must start with a letter or digit.";
24
+ export declare const INVALID_NAME_RESULT_KIND: "invalid_name";
25
+ /** Shared by every procedure that takes a record name (I4: expected, not a fault). */
26
+ export type InvalidNameResult = {
27
+ readonly kind: typeof INVALID_NAME_RESULT_KIND;
28
+ readonly name: string;
29
+ readonly rule: string;
30
+ };
31
+ export declare function isAgentsHomeRecordName(value: string): boolean;
32
+ /**
33
+ * `null` when the name is usable. Procedures call this at their entry so a
34
+ * caller that reaches them without going through the CLI codec gets the same
35
+ * answer the CLI would have given.
36
+ */
37
+ export declare function rejectInvalidName(name: string): InvalidNameResult | null;
38
+ //# sourceMappingURL=names.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"names.d.ts","sourceRoot":"","sources":["../src/names.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,2EAA2E;AAC3E,eAAO,MAAM,+BAA+B,QAAiC,CAAC;AAE9E,eAAO,MAAM,kCAAkC,KAAK,CAAC;AAErD;;;GAGG;AACH,eAAO,MAAM,4BAA4B,kIAA+J,CAAC;AAEzM,eAAO,MAAM,wBAAwB,EAAG,cAAuB,CAAC;AAEhE,sFAAsF;AACtF,MAAM,MAAM,iBAAiB,GAAG;IAC9B,QAAQ,CAAC,IAAI,EAAE,OAAO,wBAAwB,CAAC;IAC/C,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;CACvB,CAAC;AAEF,wBAAgB,sBAAsB,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAE7D;AAED;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,GAAG,iBAAiB,GAAG,IAAI,CAQxE"}
package/dist/names.js ADDED
@@ -0,0 +1,41 @@
1
+ /**
2
+ * The grammar of a record name in the shared home — profile ids and agent
3
+ * config ids — spelled ONCE.
4
+ *
5
+ * A record name is not free text: it becomes a path segment under
6
+ * `managed-homes/` and a segment of a keychain account
7
+ * (`profile:<name>:api-key`). An unconstrained name therefore decides where
8
+ * this package writes and what it deletes — `../../../escape` would put a
9
+ * login home outside the shared home and make `agent remove` delete a tree
10
+ * that is not ours. Constraining the vocabulary is the fix; checking for
11
+ * `..` at each use site is not, because the next use site forgets.
12
+ *
13
+ * This narrows, and so satisfies, the agents package's own id rule
14
+ * (non-empty, non-blank).
15
+ */
16
+ /** Lowercase, starts with a letter or digit, no separators of any kind. */
17
+ export const AGENTS_HOME_RECORD_NAME_PATTERN = /^[a-z0-9][a-z0-9._-]{0,63}$/u;
18
+ export const AGENTS_HOME_RECORD_NAME_MAX_LENGTH = 64;
19
+ /**
20
+ * The rule in words, carried on the rejection so a product can tell the
21
+ * operator what to type without restating the grammar.
22
+ */
23
+ export const AGENTS_HOME_RECORD_NAME_RULE = `A name must be 1 to ${AGENTS_HOME_RECORD_NAME_MAX_LENGTH} characters of lowercase letters, digits, ".", "-" or "_", and must start with a letter or digit.`;
24
+ export const INVALID_NAME_RESULT_KIND = "invalid_name";
25
+ export function isAgentsHomeRecordName(value) {
26
+ return AGENTS_HOME_RECORD_NAME_PATTERN.test(value);
27
+ }
28
+ /**
29
+ * `null` when the name is usable. Procedures call this at their entry so a
30
+ * caller that reaches them without going through the CLI codec gets the same
31
+ * answer the CLI would have given.
32
+ */
33
+ export function rejectInvalidName(name) {
34
+ return isAgentsHomeRecordName(name)
35
+ ? null
36
+ : {
37
+ kind: INVALID_NAME_RESULT_KIND,
38
+ name,
39
+ rule: AGENTS_HOME_RECORD_NAME_RULE,
40
+ };
41
+ }
@@ -0,0 +1,41 @@
1
+ import type { SecretRef } from "@alpacakit/agents";
2
+ import type { OpenAgentsConfigOptions } from "@alpacakit/agents/config";
3
+ /**
4
+ * What every family procedure starts from: the machine's home directory and
5
+ * its environment. Nothing here reads `os.homedir()` or `process.env` — the
6
+ * caller owns both, so a test can point the whole family at a temp dir.
7
+ */
8
+ export type AgentsConfigOptionsInput = {
9
+ readonly homeDir: string;
10
+ readonly env: Readonly<Record<string, string | undefined>>;
11
+ };
12
+ export type ManagedHomePathInput = {
13
+ readonly rootDir: string;
14
+ readonly profileId: string;
15
+ };
16
+ export declare function resolveAgentsConfigOptions(input: AgentsConfigOptionsInput): OpenAgentsConfigOptions;
17
+ /** The shared home every family product opens. */
18
+ export declare function resolveAgentsHome(input: AgentsConfigOptionsInput): string;
19
+ export declare function createKeyringSecretRef(profileId: string): SecretRef;
20
+ /**
21
+ * The ONE place a managed-home path is built, and therefore the one place the
22
+ * invariant belongs: the result is always a direct child of
23
+ * `<agents home>/managed-homes`. Names are validated at every procedure's
24
+ * entry (`rejectInvalidName`), so a name that could escape means a caller
25
+ * bypassed that — a programming fault, not an operator's mistake, and it
26
+ * throws rather than quietly writing outside the shared home.
27
+ */
28
+ export declare function resolveManagedHomePath(input: ManagedHomePathInput): string;
29
+ /**
30
+ * Whether `path` is a login home this package created under the shared home,
31
+ * and may therefore delete. A home the operator pointed at themselves is
32
+ * theirs, so `agent remove` leaves it alone — and so is anything a name that
33
+ * cannot form a managed-home path could point at, which is why this answers
34
+ * `false` where `resolveManagedHomePath` throws.
35
+ */
36
+ export declare function isManagedHomePath(input: {
37
+ readonly rootDir: string;
38
+ readonly profileId: string;
39
+ readonly path: string;
40
+ }): boolean;
41
+ //# sourceMappingURL=paths.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"paths.d.ts","sourceRoot":"","sources":["../src/paths.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,mBAAmB,CAAC;AACnD,OAAO,KAAK,EAEV,uBAAuB,EACxB,MAAM,0BAA0B,CAAC;AAelC;;;;GAIG;AACH,MAAM,MAAM,wBAAwB,GAAG;IACrC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,GAAG,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAAC,CAAC;CAC5D,CAAC;AAEF,MAAM,MAAM,oBAAoB,GAAG;IACjC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;CAC5B,CAAC;AAEF,wBAAgB,0BAA0B,CACxC,KAAK,EAAE,wBAAwB,GAC9B,uBAAuB,CAKzB;AAED,kDAAkD;AAClD,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,wBAAwB,GAAG,MAAM,CAKzE;AAED,wBAAgB,sBAAsB,CAAC,SAAS,EAAE,MAAM,GAAG,SAAS,CASnE;AAED;;;;;;;GAOG;AACH,wBAAgB,sBAAsB,CAAC,KAAK,EAAE,oBAAoB,GAAG,MAAM,CAQ1E;AAED;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,EAAE;IACvC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;CACvB,GAAG,OAAO,CAGV"}
package/dist/paths.js ADDED
@@ -0,0 +1,72 @@
1
+ import path from "node:path";
2
+ import { keyringResolver } from "@alpacakit/core/keyring";
3
+ import { AGENTS_HOME_RECORD_NAME_RULE, isAgentsHomeRecordName, } from "./names.js";
4
+ import { AGENTS_HOME_DIRECTORY, AGENTS_HOME_ENV_VAR, AGENTS_KEYRING_ACCOUNT, AGENTS_KEYRING_RESOLVER, AGENTS_KEYRING_SERVICE, AGENTS_MANAGED_HOMES_DIRECTORY, } from "./values.js";
5
+ export function resolveAgentsConfigOptions(input) {
6
+ return {
7
+ rootDir: resolveAgentsHome(input),
8
+ secretResolvers: [createKeyringResolverEntry()],
9
+ };
10
+ }
11
+ /** The shared home every family product opens. */
12
+ export function resolveAgentsHome(input) {
13
+ return (readNonBlankEnvValue(input.env, AGENTS_HOME_ENV_VAR) ??
14
+ path.join(input.homeDir, AGENTS_HOME_DIRECTORY));
15
+ }
16
+ export function createKeyringSecretRef(profileId) {
17
+ return {
18
+ resolver: AGENTS_KEYRING_RESOLVER,
19
+ id: [
20
+ AGENTS_KEYRING_ACCOUNT.prefix,
21
+ profileId,
22
+ AGENTS_KEYRING_ACCOUNT.apiKeySuffix,
23
+ ].join(AGENTS_KEYRING_ACCOUNT.separator),
24
+ };
25
+ }
26
+ /**
27
+ * The ONE place a managed-home path is built, and therefore the one place the
28
+ * invariant belongs: the result is always a direct child of
29
+ * `<agents home>/managed-homes`. Names are validated at every procedure's
30
+ * entry (`rejectInvalidName`), so a name that could escape means a caller
31
+ * bypassed that — a programming fault, not an operator's mistake, and it
32
+ * throws rather than quietly writing outside the shared home.
33
+ */
34
+ export function resolveManagedHomePath(input) {
35
+ const resolved = managedHomePathOrNull(input);
36
+ if (resolved === null) {
37
+ throw new Error(`"${input.profileId}" is not a usable managed-home name: ${AGENTS_HOME_RECORD_NAME_RULE}`);
38
+ }
39
+ return resolved;
40
+ }
41
+ /**
42
+ * Whether `path` is a login home this package created under the shared home,
43
+ * and may therefore delete. A home the operator pointed at themselves is
44
+ * theirs, so `agent remove` leaves it alone — and so is anything a name that
45
+ * cannot form a managed-home path could point at, which is why this answers
46
+ * `false` where `resolveManagedHomePath` throws.
47
+ */
48
+ export function isManagedHomePath(input) {
49
+ const managed = managedHomePathOrNull(input);
50
+ return managed !== null && path.resolve(input.path) === managed;
51
+ }
52
+ /** `null` when the name would not land directly under `managed-homes`. */
53
+ function managedHomePathOrNull(input) {
54
+ const parent = path.resolve(input.rootDir, AGENTS_MANAGED_HOMES_DIRECTORY);
55
+ const resolved = path.resolve(parent, input.profileId);
56
+ const containedSegment = path.dirname(resolved) === parent &&
57
+ path.basename(resolved) === input.profileId;
58
+ return isAgentsHomeRecordName(input.profileId) && containedSegment
59
+ ? resolved
60
+ : null;
61
+ }
62
+ function createKeyringResolverEntry() {
63
+ return {
64
+ name: AGENTS_KEYRING_RESOLVER,
65
+ resolver: keyringResolver({ service: AGENTS_KEYRING_SERVICE }),
66
+ };
67
+ }
68
+ /** Inlined from the agents package's internal env helper (not exported). */
69
+ function readNonBlankEnvValue(env, name) {
70
+ const value = env[name];
71
+ return typeof value === "string" && value.trim().length > 0 ? value : null;
72
+ }
@@ -0,0 +1,122 @@
1
+ /**
2
+ * The three ways an agent enters the shared home — a browser sign-in into a
3
+ * managed home, a pasted credential, or a plain OpenAI-compatible endpoint.
4
+ * Each owns its ORDER and its FAILURE POLICY, which is the whole reason these
5
+ * live here and not in an app: a half-registered credential must not depend
6
+ * on which product ran the command.
7
+ */
8
+ import { type LoginEvent, type LoginMethod } from "@alpacakit/agents";
9
+ import type { AgentConfig, OpenAiCompatibleEngineKind, ProfileRecord } from "@alpacakit/agents/config";
10
+ import { type AgentsHomeInput } from "./home.js";
11
+ import { type InvalidNameResult } from "./names.js";
12
+ import { AGENT_ADD_KIND } from "./values.js";
13
+ export declare const LOGIN_MANAGED_HOME_RESULT: {
14
+ readonly registered: "registered";
15
+ readonly profileExists: "profile_exists";
16
+ readonly unknownAgent: "unknown_agent";
17
+ readonly unsupported: "unsupported";
18
+ readonly loginFailed: "login_failed";
19
+ };
20
+ export declare const ADD_API_KEY_PROFILE_RESULT: {
21
+ readonly registered: "registered";
22
+ readonly profileExists: "profile_exists";
23
+ readonly emptyKey: "empty_key";
24
+ };
25
+ export declare const ADD_AGENT_CONFIG_RESULT: {
26
+ readonly added: "added";
27
+ readonly exists: "exists";
28
+ };
29
+ export type LoginManagedHomeInput = AgentsHomeInput & {
30
+ readonly profileId: string;
31
+ readonly agentId: string;
32
+ readonly method: LoginMethod;
33
+ readonly signal?: AbortSignal;
34
+ readonly onEvent?: (event: LoginEvent) => void;
35
+ };
36
+ export type LoginManagedHomeResult = {
37
+ readonly kind: typeof LOGIN_MANAGED_HOME_RESULT.registered;
38
+ readonly profile: ProfileRecord;
39
+ readonly managedHome: string;
40
+ } | {
41
+ readonly kind: typeof LOGIN_MANAGED_HOME_RESULT.profileExists;
42
+ readonly profileId: string;
43
+ } | {
44
+ /** No agent config in this home projects that id — nothing to sign in to. */
45
+ readonly kind: typeof LOGIN_MANAGED_HOME_RESULT.unknownAgent;
46
+ readonly agentId: string;
47
+ readonly known: readonly string[];
48
+ } | {
49
+ /** The agent is here; it just does not offer that sign-in method. */
50
+ readonly kind: typeof LOGIN_MANAGED_HOME_RESULT.unsupported;
51
+ readonly agentId: string;
52
+ readonly method: LoginMethod;
53
+ readonly supported: readonly LoginMethod[];
54
+ } | {
55
+ readonly kind: typeof LOGIN_MANAGED_HOME_RESULT.loginFailed;
56
+ readonly detail: string;
57
+ readonly managedHome: string;
58
+ } | InvalidNameResult;
59
+ export type AddApiKeyProfileInput = AgentsHomeInput & {
60
+ readonly profileId: string;
61
+ readonly agentId: string;
62
+ readonly apiKey: string;
63
+ };
64
+ export type AddApiKeyProfileResult = {
65
+ readonly kind: typeof ADD_API_KEY_PROFILE_RESULT.registered;
66
+ readonly profile: ProfileRecord;
67
+ } | {
68
+ readonly kind: typeof ADD_API_KEY_PROFILE_RESULT.profileExists;
69
+ readonly profileId: string;
70
+ } | {
71
+ readonly kind: typeof ADD_API_KEY_PROFILE_RESULT.emptyKey;
72
+ readonly profileId: string;
73
+ } | InvalidNameResult;
74
+ /**
75
+ * One request shape per addable kind, discriminated by `kind` — the same
76
+ * discriminator `agent add --kind` carries, so the CLI projection IS this
77
+ * input. A new kind is a new member and a new row in the builder record.
78
+ */
79
+ export type OpenAiCompatibleAgentConfigRequest = {
80
+ readonly kind: typeof AGENT_ADD_KIND.openaiCompatible;
81
+ readonly id: string;
82
+ readonly baseUrl: string;
83
+ readonly model: string;
84
+ /** Env var holding the key; stored as a `${VAR}` template, never the secret (A9). */
85
+ readonly apiKeyEnv?: string;
86
+ readonly engine?: OpenAiCompatibleEngineKind;
87
+ };
88
+ export type AgentConfigRequest = OpenAiCompatibleAgentConfigRequest;
89
+ export type AddAgentConfigInput = AgentsHomeInput & AgentConfigRequest;
90
+ export type AddAgentConfigResult = {
91
+ readonly kind: typeof ADD_AGENT_CONFIG_RESULT.added;
92
+ readonly agentConfig: AgentConfig;
93
+ } | {
94
+ readonly kind: typeof ADD_AGENT_CONFIG_RESULT.exists;
95
+ readonly id: string;
96
+ } | InvalidNameResult;
97
+ /**
98
+ * Sign in with the agent's own CLI into a home this package owns, then record
99
+ * it. A failed sign-in leaves the directory in place — a device flow that
100
+ * timed out has already written state the next attempt continues from — but
101
+ * NEVER writes the profile, so the home never points at a home that cannot
102
+ * authenticate. A sign-in that succeeds and then cannot be recorded is a
103
+ * fault, not a result: silently discarding a completed browser flow would
104
+ * leave a live credential nobody knows about.
105
+ */
106
+ export declare function loginManagedHome(input: LoginManagedHomeInput): Promise<LoginManagedHomeResult>;
107
+ /**
108
+ * Store the key in the family keychain, then reference it from the home. If
109
+ * the reference cannot be written the stored secret is removed again, so a
110
+ * failed `credential add` never leaves an orphaned key in the operator's
111
+ * keychain that no command can ever name.
112
+ *
113
+ * A blank key is rejected before the keychain is touched: it is the operator's
114
+ * mistake (an empty paste, an empty pipe), not a keychain failure, so it comes
115
+ * back as a result and leaves nothing behind. The key is stored trimmed —
116
+ * `echo "sk-…" | … --stdin` otherwise saves a trailing newline that every
117
+ * later request sends to the provider.
118
+ */
119
+ export declare function addApiKeyProfile(input: AddApiKeyProfileInput): Promise<AddApiKeyProfileResult>;
120
+ /** Add an agent of the requested kind. `input.kind` is the discriminator. */
121
+ export declare function addAgentConfig(input: AddAgentConfigInput): Promise<AddAgentConfigResult>;
122
+ //# sourceMappingURL=register.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"register.d.ts","sourceRoot":"","sources":["../src/register.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,212 @@
1
+ /**
2
+ * The three ways an agent enters the shared home — a browser sign-in into a
3
+ * managed home, a pasted credential, or a plain OpenAI-compatible endpoint.
4
+ * Each owns its ORDER and its FAILURE POLICY, which is the whole reason these
5
+ * live here and not in an app: a half-registered credential must not depend
6
+ * on which product ran the command.
7
+ */
8
+ import { mkdir } from "node:fs/promises";
9
+ import { isAgentError, } from "@alpacakit/agents";
10
+ import { openAiCompatibleAgentConfig } from "@alpacakit/agents/config";
11
+ import { createRuntimeFactory, openAgentsHome, } from "./home.js";
12
+ import { rejectInvalidName } from "./names.js";
13
+ import { createKeyringSecretRef, resolveManagedHomePath } from "./paths.js";
14
+ import { AGENT_ADD_KIND } from "./values.js";
15
+ const PRIVATE_DIRECTORY_MODE = 0o700;
16
+ const REGISTER_FAULT_MESSAGE = {
17
+ loginNotRegistered: (profileId, managedHome) => `Sign-in succeeded into "${managedHome}" but profile "${profileId}" could not be registered. The credential is on disk and unreferenced; fix the agents home and run the same login again.`,
18
+ };
19
+ export const LOGIN_MANAGED_HOME_RESULT = {
20
+ registered: "registered",
21
+ profileExists: "profile_exists",
22
+ unknownAgent: "unknown_agent",
23
+ unsupported: "unsupported",
24
+ loginFailed: "login_failed",
25
+ };
26
+ export const ADD_API_KEY_PROFILE_RESULT = {
27
+ registered: "registered",
28
+ profileExists: "profile_exists",
29
+ emptyKey: "empty_key",
30
+ };
31
+ export const ADD_AGENT_CONFIG_RESULT = {
32
+ added: "added",
33
+ exists: "exists",
34
+ };
35
+ /**
36
+ * Sign in with the agent's own CLI into a home this package owns, then record
37
+ * it. A failed sign-in leaves the directory in place — a device flow that
38
+ * timed out has already written state the next attempt continues from — but
39
+ * NEVER writes the profile, so the home never points at a home that cannot
40
+ * authenticate. A sign-in that succeeds and then cannot be recorded is a
41
+ * fault, not a result: silently discarding a completed browser flow would
42
+ * leave a live credential nobody knows about.
43
+ */
44
+ export async function loginManagedHome(input) {
45
+ const invalidName = rejectInvalidName(input.profileId);
46
+ if (invalidName !== null) {
47
+ return invalidName;
48
+ }
49
+ const home = openAgentsHome(input);
50
+ if (await findProfile(home.config, input.profileId)) {
51
+ return {
52
+ kind: LOGIN_MANAGED_HOME_RESULT.profileExists,
53
+ profileId: input.profileId,
54
+ };
55
+ }
56
+ const runtimes = createRuntimeFactory(home, await home.config.loadRuntimeConfig());
57
+ const agents = runtimes.ambient.agents();
58
+ const agent = agents.find((candidate) => candidate.id === input.agentId);
59
+ // Two different problems with two different fixes: an id this home does not
60
+ // have (add or enable the agent config) versus a method this agent's CLI
61
+ // does not offer (use another method). Collapsing them would send the
62
+ // operator looking in the wrong place.
63
+ if (agent === undefined) {
64
+ return {
65
+ kind: LOGIN_MANAGED_HOME_RESULT.unknownAgent,
66
+ agentId: input.agentId,
67
+ known: agents.map((candidate) => candidate.id),
68
+ };
69
+ }
70
+ const login = agent.login;
71
+ if (login === null || !login.supports(input.method)) {
72
+ return {
73
+ kind: LOGIN_MANAGED_HOME_RESULT.unsupported,
74
+ agentId: input.agentId,
75
+ method: input.method,
76
+ supported: login?.methods() ?? [],
77
+ };
78
+ }
79
+ const managedHome = resolveManagedHomePath({
80
+ rootDir: home.agentsHome,
81
+ profileId: input.profileId,
82
+ });
83
+ await mkdir(managedHome, { recursive: true, mode: PRIVATE_DIRECTORY_MODE });
84
+ const request = {
85
+ method: input.method,
86
+ homeDir: managedHome,
87
+ ...(input.signal === undefined ? {} : { signal: input.signal }),
88
+ };
89
+ const handle = login.start(request, home.location);
90
+ const drained = drainLoginEvents(handle.events, input.onEvent);
91
+ try {
92
+ await handle.result;
93
+ }
94
+ catch (thrown) {
95
+ await drained;
96
+ if (!isAgentError(thrown)) {
97
+ throw thrown;
98
+ }
99
+ return {
100
+ kind: LOGIN_MANAGED_HOME_RESULT.loginFailed,
101
+ detail: thrown.message,
102
+ managedHome,
103
+ };
104
+ }
105
+ await drained;
106
+ let profile;
107
+ try {
108
+ profile = await home.config.profiles.addHomeDir({
109
+ id: input.profileId,
110
+ agentId: input.agentId,
111
+ path: managedHome,
112
+ });
113
+ }
114
+ catch (cause) {
115
+ throw new Error(REGISTER_FAULT_MESSAGE.loginNotRegistered(input.profileId, managedHome), { cause });
116
+ }
117
+ return { kind: LOGIN_MANAGED_HOME_RESULT.registered, profile, managedHome };
118
+ }
119
+ /**
120
+ * Store the key in the family keychain, then reference it from the home. If
121
+ * the reference cannot be written the stored secret is removed again, so a
122
+ * failed `credential add` never leaves an orphaned key in the operator's
123
+ * keychain that no command can ever name.
124
+ *
125
+ * A blank key is rejected before the keychain is touched: it is the operator's
126
+ * mistake (an empty paste, an empty pipe), not a keychain failure, so it comes
127
+ * back as a result and leaves nothing behind. The key is stored trimmed —
128
+ * `echo "sk-…" | … --stdin` otherwise saves a trailing newline that every
129
+ * later request sends to the provider.
130
+ */
131
+ export async function addApiKeyProfile(input) {
132
+ const invalidName = rejectInvalidName(input.profileId);
133
+ if (invalidName !== null) {
134
+ return invalidName;
135
+ }
136
+ const apiKey = input.apiKey.trim();
137
+ if (apiKey === "") {
138
+ return {
139
+ kind: ADD_API_KEY_PROFILE_RESULT.emptyKey,
140
+ profileId: input.profileId,
141
+ };
142
+ }
143
+ const home = openAgentsHome(input);
144
+ if (await findProfile(home.config, input.profileId)) {
145
+ return {
146
+ kind: ADD_API_KEY_PROFILE_RESULT.profileExists,
147
+ profileId: input.profileId,
148
+ };
149
+ }
150
+ const secret = createKeyringSecretRef(input.profileId);
151
+ await home.keyring.set(secret.id, apiKey);
152
+ try {
153
+ const profile = await home.config.profiles.addApiKey({
154
+ id: input.profileId,
155
+ agentId: input.agentId,
156
+ secret,
157
+ });
158
+ return { kind: ADD_API_KEY_PROFILE_RESULT.registered, profile };
159
+ }
160
+ catch (thrown) {
161
+ await home.keyring.delete(secret.id);
162
+ throw thrown;
163
+ }
164
+ }
165
+ /**
166
+ * One record per addable kind (design principle 7): the key IS the support
167
+ * declaration, so adding a kind is adding a row rather than a branch.
168
+ */
169
+ const AGENT_CONFIG_BUILDER = {
170
+ [AGENT_ADD_KIND.openaiCompatible]: (request) => openAiCompatibleAgentConfig({
171
+ id: request.id,
172
+ baseUrl: request.baseUrl,
173
+ model: request.model,
174
+ ...(request.apiKeyEnv === undefined
175
+ ? {}
176
+ : { apiKey: envTemplate(request.apiKeyEnv) }),
177
+ ...(request.engine === undefined ? {} : { engine: request.engine }),
178
+ }),
179
+ };
180
+ /** Add an agent of the requested kind. `input.kind` is the discriminator. */
181
+ export async function addAgentConfig(input) {
182
+ const invalidName = rejectInvalidName(input.id);
183
+ if (invalidName !== null) {
184
+ return invalidName;
185
+ }
186
+ const home = openAgentsHome(input);
187
+ const existing = await home.config.agentConfigs.list();
188
+ if (existing.some((agentConfig) => agentConfig.id === input.id)) {
189
+ return { kind: ADD_AGENT_CONFIG_RESULT.exists, id: input.id };
190
+ }
191
+ const agentConfig = await home.config.agentConfigs.add(AGENT_CONFIG_BUILDER[input.kind](input));
192
+ return { kind: ADD_AGENT_CONFIG_RESULT.added, agentConfig };
193
+ }
194
+ function envTemplate(name) {
195
+ return `\${${name}}`;
196
+ }
197
+ async function findProfile(config, profileId) {
198
+ return (await config.profiles.list()).some((profile) => profile.id === profileId);
199
+ }
200
+ /**
201
+ * Events are informational; a consumer that does not want them must not stall
202
+ * the sign-in, and a consumer that does must see them all before the result is
203
+ * reported. Awaiting the drain after the result gives both.
204
+ */
205
+ async function drainLoginEvents(events, onEvent) {
206
+ if (onEvent === undefined) {
207
+ return;
208
+ }
209
+ for await (const event of events) {
210
+ onEvent(event);
211
+ }
212
+ }
@@ -0,0 +1,40 @@
1
+ /**
2
+ * The one way an agent leaves the shared home. `name` is a record name, the
3
+ * same position `connection login <name>` uses, and profiles win over agent
4
+ * configs because a profile is the more specific thing an operator names.
5
+ */
6
+ import type { AgentConfig, ProfileRecord } from "@alpacakit/agents/config";
7
+ import { type AgentsHomeInput } from "./home.js";
8
+ import { type InvalidNameResult } from "./names.js";
9
+ export declare const REMOVE_AGENTS_HOME_ENTRY_RESULT: {
10
+ readonly removedProfile: "removed_profile";
11
+ readonly removedAgentConfig: "removed_agent_config";
12
+ readonly notFound: "not_found";
13
+ };
14
+ export type RemoveAgentsHomeEntryInput = AgentsHomeInput & {
15
+ readonly name: string;
16
+ };
17
+ export type RemoveAgentsHomeEntryResult = {
18
+ readonly kind: typeof REMOVE_AGENTS_HOME_ENTRY_RESULT.removedProfile;
19
+ readonly profile: ProfileRecord;
20
+ /** The login home deleted, or `null` when the path was not ours to delete. */
21
+ readonly removedManagedHome: string | null;
22
+ readonly removedSecret: boolean;
23
+ } | {
24
+ readonly kind: typeof REMOVE_AGENTS_HOME_ENTRY_RESULT.removedAgentConfig;
25
+ readonly agentConfig: AgentConfig;
26
+ } | {
27
+ readonly kind: typeof REMOVE_AGENTS_HOME_ENTRY_RESULT.notFound;
28
+ readonly name: string;
29
+ } | InvalidNameResult;
30
+ /**
31
+ * The record goes first, then the credential it pointed at: if deleting the
32
+ * credential fails the operator is left with an inert directory or an orphaned
33
+ * keychain entry and a loud error — never with a live record pointing at a
34
+ * credential that is already gone.
35
+ *
36
+ * Only artifacts this package created are deleted. A home the operator pointed
37
+ * at themselves, or a secret behind someone else's resolver, is theirs.
38
+ */
39
+ export declare function removeAgentsHomeEntry(input: RemoveAgentsHomeEntryInput): Promise<RemoveAgentsHomeEntryResult>;
40
+ //# sourceMappingURL=remove.d.ts.map