@agentproto/runtime 3.1.0 → 3.2.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.
@@ -0,0 +1,161 @@
1
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ import { ProfileProvisionDeps, CreatedAuthProfile } from '@agentproto/auth';
3
+
4
+ /**
5
+ * Read-only DISCOVERY SCANNER — `auth_discover_credentials`.
6
+ *
7
+ * Probes the well-known local locations where the CLIs and gateways this host
8
+ * knows about write their credentials (Claude Code's OAuth item, Codex/Gemini
9
+ * login files, `~/.hermes/config.yaml`, and the provider API-key env vars) and
10
+ * reports what it FINDS — so onboarding can offer to import a credential the
11
+ * user already has, instead of making them paste it again.
12
+ *
13
+ * It is a scanner, not a resolver. Two invariants make it money-safe, and the
14
+ * suite in `__tests__/credential-discovery.test.ts` is written against both:
15
+ *
16
+ * 1. NEVER return (or log) a discovered secret's VALUE. Every probe answers a
17
+ * BOOLEAN "is a non-empty credential present here?" via {@link jsonFieldPresent}
18
+ * / a presence check — the value is never bound into a result, only a
19
+ * non-secret `hint` locator ("OPENROUTER_API_KEY in ~/.hermes/config.yaml").
20
+ * This mirrors `verifyLocalLoginPresent`'s discard-the-value discipline in
21
+ * `claude-code-oauth-source.ts`, taken one step further: we never hold the
22
+ * plaintext at all.
23
+ *
24
+ * 2. NEVER throw on a malformed/unreadable source. A single corrupt file must
25
+ * not sink the whole scan — each probe is wrapped so a parse error / bad
26
+ * permission becomes a per-source `warn`+skip (same "malformed → empty, not
27
+ * fatal" spirit as `loadProviders`). A MISSING file is normal "not found",
28
+ * not even a warning.
29
+ *
30
+ * The source recipes (Keychain service name, file paths, jsonPaths) mirror the
31
+ * builtin provision recipes in
32
+ * `@agentproto/secrets/provision/recipe` (`claudeCodeOauthRecipe`,
33
+ * `codexRecipe`, `geminiRecipe`) so discovery and provisioning agree on where a
34
+ * credential lives.
35
+ *
36
+ * PHASE 1 (this file) is pure discovery: it reports EVERYTHING it finds, with
37
+ * provenance. Cross-referencing against already-imported profiles (the
38
+ * "found-but-not-*imported*" filter, keyed on a future AuthProfile `origin`
39
+ * field) is Phase 2 and is deliberately not done here.
40
+ */
41
+
42
+ /** Where a discovered credential came from. Stable, non-secret provenance. */
43
+ type CredentialOrigin = "claude-code" | "hermes-config" | "env" | "codex" | "gemini";
44
+ /** One found-but-not-imported credential. Carries provenance and a non-secret
45
+ * locator only — NEVER the credential value. */
46
+ interface DiscoveredCredential {
47
+ /** Billing endpoint / vendor the credential is for (anthropic, openai, …). */
48
+ endpoint: string;
49
+ /** Auth method the credential installs as. */
50
+ method: "oauth-bearer" | "api-key";
51
+ /** Where it was found. */
52
+ origin: CredentialOrigin;
53
+ /** Non-secret human locator — e.g. "OPENROUTER_API_KEY in ~/.hermes/config.yaml".
54
+ * Says WHERE the secret is, never WHAT it is. */
55
+ hint: string;
56
+ }
57
+ /**
58
+ * Injectable I/O so the scanner is unit-testable without touching the real
59
+ * home dir / Keychain. Defaults reach for the real host.
60
+ */
61
+ interface CredentialDiscoveryDeps {
62
+ /** Home directory (`~` expands to this). Default: `os.homedir()`. */
63
+ homeDir?: string;
64
+ /** Process env to probe. Default: `process.env`. */
65
+ env?: Record<string, string | undefined>;
66
+ /** Read a file's text. MUST throw a Node-style error with `.code === "ENOENT"`
67
+ * for a missing file (the default `readFileSync` does). Default: real fs. */
68
+ readFile?: (path: string) => string;
69
+ /** Look up a macOS Keychain generic-password by service, returning its raw
70
+ * value; MUST throw when the item is absent. Default: `security find-generic-password`
71
+ * on darwin, always-throws elsewhere. */
72
+ keychainLookup?: (service: string) => string;
73
+ /** Platform gate for the Keychain probe. Default: `process.platform`. */
74
+ platform?: NodeJS.Platform;
75
+ /** Non-secret warning sink for a malformed/unreadable source. Default: no-op. */
76
+ warn?: (message: string) => void;
77
+ }
78
+ /**
79
+ * Is there a non-empty STRING at `path` in this JSON text? Returns a boolean —
80
+ * it deliberately NEVER returns the value, so a discovered secret can't leak
81
+ * through it. Throws only if the text isn't parseable JSON (a malformed
82
+ * source), which the caller turns into a warn+skip.
83
+ */
84
+ declare function jsonFieldPresent(raw: string, path: string): boolean;
85
+ /**
86
+ * Extract a `KEY: <value>` value from `~/.hermes/config.yaml` text — a minimal,
87
+ * dependency-free reader (no YAML parser pulled in): the first top-levelish
88
+ * `key:` line, quotes stripped. Returns `undefined` when the key is absent or
89
+ * its value is blank/empty-quotes.
90
+ *
91
+ * Used TWO ways: the discovery scanner only asks {@link yamlKeyPresent}
92
+ * (boolean, never the value); the import path calls this directly to COPY the
93
+ * value into the keychain. The value it returns is a SECRET — callers must
94
+ * pass it straight to the credential store and never echo it.
95
+ */
96
+ declare function readYamlKeyValue(raw: string, key: string): string | undefined;
97
+ /**
98
+ * Does `~/.hermes/config.yaml` carry a non-empty value for `key`? Presence
99
+ * only — returns a boolean, NEVER the value (the discovery invariant).
100
+ */
101
+ declare function yamlKeyPresent(raw: string, key: string): boolean;
102
+ /**
103
+ * Scan the known local credential locations and report what's present, with
104
+ * provenance and a non-secret locator. Read-only, and safe against a corrupt
105
+ * or unreadable source (per-source warn+skip). NEVER returns a secret value.
106
+ */
107
+ declare function discoverCredentials(deps?: CredentialDiscoveryDeps): DiscoveredCredential[];
108
+ /** Raised when an import can't proceed (unknown origin/endpoint, or nothing to
109
+ * import). Distinct type so the MCP surface can map it to a clean error. */
110
+ declare class CredentialImportError extends Error {
111
+ constructor(message: string);
112
+ }
113
+ /** How a chosen `{ origin, endpoint }` becomes a profile. Discriminated: a
114
+ * `source`-backed oauth-bearer, or an api-key COPIED from a located value. */
115
+ type ImportMaterialization = {
116
+ method: "oauth-bearer";
117
+ endpoint: string;
118
+ source: string;
119
+ } | {
120
+ method: "api-key";
121
+ endpoint: string;
122
+ copy: {
123
+ via: "env" | "hermes";
124
+ key: string;
125
+ };
126
+ };
127
+ /**
128
+ * Decide how to materialize a profile for a discovered `{ origin, endpoint }`.
129
+ * PURE — no I/O, no secret. Throws {@link CredentialImportError} for an unknown
130
+ * origin, or an endpoint that origin doesn't serve. The method is fixed by
131
+ * origin here, which is what forbids a bearer↔api-key mix.
132
+ */
133
+ declare function planCredentialImport(origin: string, endpoint: string): ImportMaterialization;
134
+ interface ImportCredentialArgs {
135
+ /** Discovery origin to import from. */
136
+ origin: string;
137
+ /** Billing endpoint to import (must be one the origin serves). */
138
+ endpoint: string;
139
+ /** Optional explicit profile id; defaults to `<origin>-<endpoint>`. */
140
+ id?: string;
141
+ /** Optional human label. */
142
+ label?: string;
143
+ }
144
+ /**
145
+ * Import a discovered credential into a named auth profile end-to-end.
146
+ *
147
+ * Guards against fabricating a profile: it re-runs discovery and refuses unless
148
+ * the requested `{ origin, endpoint }` is ACTUALLY present right now — discovery
149
+ * having once located a value is not license to import it. Then materializes via
150
+ * the single validated create path ({@link createAuthProfile}), stamping
151
+ * `origin` so the UI can badge it. Returns the non-secret {@link
152
+ * CreatedAuthProfile} (fingerprint, never the credential).
153
+ *
154
+ * `provisionDeps` is the profile/credential storage (a real keychain in prod, a
155
+ * `MemoryStore` in tests); `ioDeps` is the discovery I/O (env / files /
156
+ * keychain-read), injectable for the same reason.
157
+ */
158
+ declare function importDiscoveredCredential(args: ImportCredentialArgs, provisionDeps: ProfileProvisionDeps, ioDeps?: CredentialDiscoveryDeps): Promise<CreatedAuthProfile>;
159
+ declare function registerCredentialDiscoveryTools(server: McpServer): void;
160
+
161
+ export { type CredentialDiscoveryDeps, CredentialImportError, type CredentialOrigin, type DiscoveredCredential, type ImportCredentialArgs, type ImportMaterialization, discoverCredentials, importDiscoveredCredential, jsonFieldPresent, planCredentialImport, readYamlKeyValue, registerCredentialDiscoveryTools, yamlKeyPresent };
@@ -0,0 +1,305 @@
1
+ import { execFileSync } from 'child_process';
2
+ import { readFileSync } from 'fs';
3
+ import { homedir } from 'os';
4
+ import { createAuthProfile } from '@agentproto/auth';
5
+
6
+ /**
7
+ * @agentproto/runtime v0.1.0-alpha
8
+ * Long-running gateway: MCP server + HTTP transport + HEARTBEAT autonomy + conversation persistence over a workspace dir.
9
+ */
10
+
11
+ function isMissingFile(err) {
12
+ return typeof err === "object" && err !== null && "code" in err && err.code === "ENOENT";
13
+ }
14
+ function expandHome(homeDir, p) {
15
+ if (p === "~") return homeDir;
16
+ if (p.startsWith("~/")) return homeDir.replace(/\/$/, "") + "/" + p.slice(2);
17
+ return p;
18
+ }
19
+ function jsonFieldPresent(raw, path) {
20
+ let cur = JSON.parse(raw);
21
+ for (const part of path.split(".")) {
22
+ if (cur == null || typeof cur !== "object") return false;
23
+ cur = cur[part];
24
+ }
25
+ return typeof cur === "string" && cur.length > 0;
26
+ }
27
+ function readYamlKeyValue(raw, key) {
28
+ const escaped = key.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
29
+ const re = new RegExp(`^\\s*${escaped}\\s*:\\s*(.*)$`, "m");
30
+ const m = re.exec(raw);
31
+ if (!m) return void 0;
32
+ let value = (m[1] ?? "").trim();
33
+ if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
34
+ value = value.slice(1, -1);
35
+ }
36
+ return value.length > 0 ? value : void 0;
37
+ }
38
+ function yamlKeyPresent(raw, key) {
39
+ return readYamlKeyValue(raw, key) !== void 0;
40
+ }
41
+ var CLAUDE_CODE_KEYCHAIN = "Claude Code-credentials";
42
+ var CLAUDE_CODE_JSON_PATH = "claudeAiOauth.accessToken";
43
+ var CLAUDE_CODE_FILE = "~/.claude/.credentials.json";
44
+ var FILE_OAUTH_PROBES = [
45
+ { origin: "codex", endpoint: "openai", file: "~/.codex/auth.json", jsonPath: "tokens.access_token" },
46
+ { origin: "gemini", endpoint: "google", file: "~/.gemini/oauth_creds.json", jsonPath: "access_token" }
47
+ ];
48
+ var HERMES_FILE = "~/.hermes/config.yaml";
49
+ var HERMES_KEYS = [
50
+ { key: "OPENROUTER_API_KEY", endpoint: "openrouter" },
51
+ { key: "OPENAI_API_KEY", endpoint: "openai" }
52
+ ];
53
+ var ENV_KEYS = [
54
+ { env: "OPENROUTER_API_KEY", endpoint: "openrouter" },
55
+ { env: "MOONSHOT_API_KEY", endpoint: "moonshot" },
56
+ { env: "OPENAI_API_KEY", endpoint: "openai" },
57
+ { env: "ANTHROPIC_API_KEY", endpoint: "anthropic" },
58
+ { env: "DEEPSEEK_API_KEY", endpoint: "deepseek" },
59
+ { env: "XAI_API_KEY", endpoint: "xai" }
60
+ ];
61
+ function defaultKeychainLookup(service) {
62
+ return execFileSync("security", ["find-generic-password", "-w", "-s", service], {
63
+ encoding: "utf8"
64
+ }).trim();
65
+ }
66
+ function defaultReadFile(path) {
67
+ return readFileSync(path, "utf8");
68
+ }
69
+ function resolveDeps(deps) {
70
+ const platform = deps.platform ?? process.platform;
71
+ return {
72
+ homeDir: deps.homeDir ?? homedir(),
73
+ env: deps.env ?? process.env,
74
+ readFile: deps.readFile ?? defaultReadFile,
75
+ keychainLookup: deps.keychainLookup ?? (platform === "darwin" ? defaultKeychainLookup : () => {
76
+ throw new Error("keychain is macOS-only");
77
+ }),
78
+ platform,
79
+ warn: deps.warn ?? (() => {
80
+ })
81
+ };
82
+ }
83
+ function discoverCredentials(deps = {}) {
84
+ const d = resolveDeps(deps);
85
+ const found = [];
86
+ probeClaudeCode(d, found);
87
+ for (const probe of FILE_OAUTH_PROBES) probeFileOauth(d, probe, found);
88
+ probeHermes(d, found);
89
+ probeEnv(d, found);
90
+ return found;
91
+ }
92
+ function probeClaudeCode(d, out) {
93
+ if (d.platform === "darwin") {
94
+ try {
95
+ const raw = d.keychainLookup(CLAUDE_CODE_KEYCHAIN);
96
+ if (raw && jsonFieldPresent(raw, CLAUDE_CODE_JSON_PATH)) {
97
+ out.push({
98
+ endpoint: "anthropic",
99
+ method: "oauth-bearer",
100
+ origin: "claude-code",
101
+ hint: `Claude Code OAuth token in macOS Keychain ("${CLAUDE_CODE_KEYCHAIN}")`
102
+ });
103
+ return;
104
+ }
105
+ } catch (err) {
106
+ }
107
+ }
108
+ try {
109
+ const raw = d.readFile(expandHome(d.homeDir, CLAUDE_CODE_FILE));
110
+ if (jsonFieldPresent(raw, CLAUDE_CODE_JSON_PATH)) {
111
+ out.push({
112
+ endpoint: "anthropic",
113
+ method: "oauth-bearer",
114
+ origin: "claude-code",
115
+ hint: `Claude Code OAuth token in ${CLAUDE_CODE_FILE}`
116
+ });
117
+ }
118
+ } catch (err) {
119
+ if (!isMissingFile(err)) {
120
+ d.warn(`skipping ${CLAUDE_CODE_FILE}: ${errText(err)}`);
121
+ }
122
+ }
123
+ }
124
+ function probeFileOauth(d, probe, out) {
125
+ try {
126
+ const raw = d.readFile(expandHome(d.homeDir, probe.file));
127
+ if (jsonFieldPresent(raw, probe.jsonPath)) {
128
+ out.push({
129
+ endpoint: probe.endpoint,
130
+ method: "oauth-bearer",
131
+ origin: probe.origin,
132
+ hint: `${probe.origin} OAuth token in ${probe.file}`
133
+ });
134
+ }
135
+ } catch (err) {
136
+ if (!isMissingFile(err)) {
137
+ d.warn(`skipping ${probe.file}: ${errText(err)}`);
138
+ }
139
+ }
140
+ }
141
+ function probeHermes(d, out) {
142
+ let raw;
143
+ try {
144
+ raw = d.readFile(expandHome(d.homeDir, HERMES_FILE));
145
+ } catch (err) {
146
+ if (!isMissingFile(err)) d.warn(`skipping ${HERMES_FILE}: ${errText(err)}`);
147
+ return;
148
+ }
149
+ for (const { key, endpoint } of HERMES_KEYS) {
150
+ try {
151
+ if (yamlKeyPresent(raw, key)) {
152
+ out.push({
153
+ endpoint,
154
+ method: "api-key",
155
+ origin: "hermes-config",
156
+ hint: `${key} in ${HERMES_FILE}`
157
+ });
158
+ }
159
+ } catch (err) {
160
+ d.warn(`skipping ${key} in ${HERMES_FILE}: ${errText(err)}`);
161
+ }
162
+ }
163
+ }
164
+ function probeEnv(d, out) {
165
+ for (const { env, endpoint } of ENV_KEYS) {
166
+ const value = d.env[env];
167
+ if (value && value.trim().length > 0) {
168
+ out.push({
169
+ endpoint,
170
+ method: "api-key",
171
+ origin: "env",
172
+ hint: `${env} in the environment`
173
+ });
174
+ }
175
+ }
176
+ }
177
+ function errText(err) {
178
+ return err instanceof Error ? err.message : String(err);
179
+ }
180
+ var CredentialImportError = class extends Error {
181
+ constructor(message) {
182
+ super(message);
183
+ this.name = "CredentialImportError";
184
+ }
185
+ };
186
+ var SOURCE_BACKED_ORIGINS = {
187
+ "claude-code": { source: "claude-code-oauth", endpoint: "anthropic" },
188
+ codex: { source: "codex", endpoint: "openai" },
189
+ gemini: { source: "gemini", endpoint: "google" }
190
+ };
191
+ function planCredentialImport(origin, endpoint) {
192
+ const wanted = endpoint.trim();
193
+ const sourceBacked = SOURCE_BACKED_ORIGINS[origin];
194
+ if (sourceBacked) {
195
+ if (wanted && wanted !== sourceBacked.endpoint) {
196
+ throw new CredentialImportError(
197
+ `origin "${origin}" authenticates ${sourceBacked.endpoint}, not "${wanted}"`
198
+ );
199
+ }
200
+ return { method: "oauth-bearer", endpoint: sourceBacked.endpoint, source: sourceBacked.source };
201
+ }
202
+ if (origin === "env") {
203
+ const entry = ENV_KEYS.find((e) => e.endpoint === wanted);
204
+ if (!entry) {
205
+ throw new CredentialImportError(
206
+ `no known env var maps to endpoint "${wanted}" for origin "env"`
207
+ );
208
+ }
209
+ return { method: "api-key", endpoint: wanted, copy: { via: "env", key: entry.env } };
210
+ }
211
+ if (origin === "hermes-config") {
212
+ const entry = HERMES_KEYS.find((h) => h.endpoint === wanted);
213
+ if (!entry) {
214
+ throw new CredentialImportError(
215
+ `no known ~/.hermes/config.yaml key maps to endpoint "${wanted}"`
216
+ );
217
+ }
218
+ return { method: "api-key", endpoint: wanted, copy: { via: "hermes", key: entry.key } };
219
+ }
220
+ throw new CredentialImportError(`unknown credential origin "${origin}"`);
221
+ }
222
+ function resolveCopyValue(copy, d) {
223
+ if (copy.via === "env") {
224
+ const value2 = d.env[copy.key]?.trim();
225
+ if (!value2) {
226
+ throw new CredentialImportError(`no ${copy.key} in the environment to import`);
227
+ }
228
+ return value2;
229
+ }
230
+ let raw;
231
+ try {
232
+ raw = d.readFile(expandHome(d.homeDir, HERMES_FILE));
233
+ } catch (err) {
234
+ throw new CredentialImportError(`cannot read ${HERMES_FILE} to import ${copy.key}: ${errText(err)}`);
235
+ }
236
+ const value = readYamlKeyValue(raw, copy.key);
237
+ if (!value) {
238
+ throw new CredentialImportError(`no ${copy.key} value in ${HERMES_FILE} to import`);
239
+ }
240
+ return value;
241
+ }
242
+ function defaultImportId(origin, endpoint) {
243
+ return `${origin}-${endpoint}`;
244
+ }
245
+ async function importDiscoveredCredential(args, provisionDeps, ioDeps = {}) {
246
+ const plan = planCredentialImport(args.origin, args.endpoint);
247
+ const present = discoverCredentials(ioDeps).some(
248
+ (c) => c.origin === args.origin && c.endpoint === plan.endpoint
249
+ );
250
+ if (!present) {
251
+ throw new CredentialImportError(
252
+ `nothing to import \u2014 no ${args.origin} credential for ${plan.endpoint} was discovered`
253
+ );
254
+ }
255
+ const id = args.id?.trim() || defaultImportId(args.origin, plan.endpoint);
256
+ const label = args.label?.trim();
257
+ const base = {
258
+ id,
259
+ endpoint: plan.endpoint,
260
+ method: plan.method,
261
+ origin: args.origin,
262
+ ...label ? { label } : {}
263
+ };
264
+ if (plan.method === "oauth-bearer") {
265
+ return createAuthProfile({ ...base, source: plan.source }, provisionDeps);
266
+ }
267
+ const credential = resolveCopyValue(plan.copy, resolveDeps(ioDeps));
268
+ return createAuthProfile({ ...base, credential }, provisionDeps);
269
+ }
270
+ function text(value) {
271
+ return {
272
+ content: [
273
+ {
274
+ type: "text",
275
+ text: typeof value === "string" ? value : JSON.stringify(value)
276
+ }
277
+ ]
278
+ };
279
+ }
280
+ function errorText(message) {
281
+ return { content: [{ type: "text", text: message }], isError: true };
282
+ }
283
+ function registerCredentialDiscoveryTools(server) {
284
+ server.tool(
285
+ "auth_discover_credentials",
286
+ "Read-only scan of the known local credential locations (Claude Code's Keychain/OAuth file, Codex/Gemini login files, `~/.hermes/config.yaml`, and the provider API-key env vars). Reports which credentials are PRESENT and where they came from, so onboarding can offer to import one you already have. Returns only non-secret provenance \u2014 `{ endpoint, method, origin, hint }` \u2014 and NEVER the credential value; the `hint` is a locator, not the secret. A malformed or unreadable source is skipped, never fatal; a missing file is a normal 'not found'.",
287
+ {},
288
+ async () => {
289
+ try {
290
+ const warnings = [];
291
+ const credentials = discoverCredentials({ warn: (m) => warnings.push(m) });
292
+ return text({
293
+ credentials,
294
+ ...warnings.length ? { warnings } : {}
295
+ });
296
+ } catch (err) {
297
+ return errorText(`auth_discover_credentials failed: ${errText(err)}`);
298
+ }
299
+ }
300
+ );
301
+ }
302
+
303
+ export { CredentialImportError, discoverCredentials, importDiscoveredCredential, jsonFieldPresent, planCredentialImport, readYamlKeyValue, registerCredentialDiscoveryTools, yamlKeyPresent };
304
+ //# sourceMappingURL=credential-discovery.mjs.map
305
+ //# sourceMappingURL=credential-discovery.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/credential-discovery.ts"],"names":["value"],"mappings":"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","file":"credential-discovery.mjs","sourcesContent":["/**\n * Read-only DISCOVERY SCANNER — `auth_discover_credentials`.\n *\n * Probes the well-known local locations where the CLIs and gateways this host\n * knows about write their credentials (Claude Code's OAuth item, Codex/Gemini\n * login files, `~/.hermes/config.yaml`, and the provider API-key env vars) and\n * reports what it FINDS — so onboarding can offer to import a credential the\n * user already has, instead of making them paste it again.\n *\n * It is a scanner, not a resolver. Two invariants make it money-safe, and the\n * suite in `__tests__/credential-discovery.test.ts` is written against both:\n *\n * 1. NEVER return (or log) a discovered secret's VALUE. Every probe answers a\n * BOOLEAN \"is a non-empty credential present here?\" via {@link jsonFieldPresent}\n * / a presence check — the value is never bound into a result, only a\n * non-secret `hint` locator (\"OPENROUTER_API_KEY in ~/.hermes/config.yaml\").\n * This mirrors `verifyLocalLoginPresent`'s discard-the-value discipline in\n * `claude-code-oauth-source.ts`, taken one step further: we never hold the\n * plaintext at all.\n *\n * 2. NEVER throw on a malformed/unreadable source. A single corrupt file must\n * not sink the whole scan — each probe is wrapped so a parse error / bad\n * permission becomes a per-source `warn`+skip (same \"malformed → empty, not\n * fatal\" spirit as `loadProviders`). A MISSING file is normal \"not found\",\n * not even a warning.\n *\n * The source recipes (Keychain service name, file paths, jsonPaths) mirror the\n * builtin provision recipes in\n * `@agentproto/secrets/provision/recipe` (`claudeCodeOauthRecipe`,\n * `codexRecipe`, `geminiRecipe`) so discovery and provisioning agree on where a\n * credential lives.\n *\n * PHASE 1 (this file) is pure discovery: it reports EVERYTHING it finds, with\n * provenance. Cross-referencing against already-imported profiles (the\n * \"found-but-not-*imported*\" filter, keyed on a future AuthProfile `origin`\n * field) is Phase 2 and is deliberately not done here.\n */\n\nimport { execFileSync } from \"node:child_process\"\nimport { readFileSync } from \"node:fs\"\nimport { homedir } from \"node:os\"\nimport type { McpServer } from \"@modelcontextprotocol/sdk/server/mcp.js\"\nimport {\n createAuthProfile,\n type CreateAuthProfileInput,\n type CreatedAuthProfile,\n type ProfileProvisionDeps,\n} from \"@agentproto/auth\"\n\n/** Where a discovered credential came from. Stable, non-secret provenance. */\nexport type CredentialOrigin =\n | \"claude-code\"\n | \"hermes-config\"\n | \"env\"\n | \"codex\"\n | \"gemini\"\n\n/** One found-but-not-imported credential. Carries provenance and a non-secret\n * locator only — NEVER the credential value. */\nexport interface DiscoveredCredential {\n /** Billing endpoint / vendor the credential is for (anthropic, openai, …). */\n endpoint: string\n /** Auth method the credential installs as. */\n method: \"oauth-bearer\" | \"api-key\"\n /** Where it was found. */\n origin: CredentialOrigin\n /** Non-secret human locator — e.g. \"OPENROUTER_API_KEY in ~/.hermes/config.yaml\".\n * Says WHERE the secret is, never WHAT it is. */\n hint: string\n}\n\n/**\n * Injectable I/O so the scanner is unit-testable without touching the real\n * home dir / Keychain. Defaults reach for the real host.\n */\nexport interface CredentialDiscoveryDeps {\n /** Home directory (`~` expands to this). Default: `os.homedir()`. */\n homeDir?: string\n /** Process env to probe. Default: `process.env`. */\n env?: Record<string, string | undefined>\n /** Read a file's text. MUST throw a Node-style error with `.code === \"ENOENT\"`\n * for a missing file (the default `readFileSync` does). Default: real fs. */\n readFile?: (path: string) => string\n /** Look up a macOS Keychain generic-password by service, returning its raw\n * value; MUST throw when the item is absent. Default: `security find-generic-password`\n * on darwin, always-throws elsewhere. */\n keychainLookup?: (service: string) => string\n /** Platform gate for the Keychain probe. Default: `process.platform`. */\n platform?: NodeJS.Platform\n /** Non-secret warning sink for a malformed/unreadable source. Default: no-op. */\n warn?: (message: string) => void\n}\n\ninterface ResolvedDeps {\n homeDir: string\n env: Record<string, string | undefined>\n readFile: (path: string) => string\n keychainLookup: (service: string) => string\n platform: NodeJS.Platform\n warn: (message: string) => void\n}\n\n/** A Node fs error for a file that does not exist — a normal \"not found\", not a\n * malformed-source warning. */\nfunction isMissingFile(err: unknown): boolean {\n return (\n typeof err === \"object\" &&\n err !== null &&\n \"code\" in err &&\n (err as { code?: unknown }).code === \"ENOENT\"\n )\n}\n\nfunction expandHome(homeDir: string, p: string): string {\n if (p === \"~\") return homeDir\n if (p.startsWith(\"~/\")) return homeDir.replace(/\\/$/, \"\") + \"/\" + p.slice(2)\n return p\n}\n\n/**\n * Is there a non-empty STRING at `path` in this JSON text? Returns a boolean —\n * it deliberately NEVER returns the value, so a discovered secret can't leak\n * through it. Throws only if the text isn't parseable JSON (a malformed\n * source), which the caller turns into a warn+skip.\n */\nexport function jsonFieldPresent(raw: string, path: string): boolean {\n let cur: unknown = JSON.parse(raw)\n for (const part of path.split(\".\")) {\n if (cur == null || typeof cur !== \"object\") return false\n cur = (cur as Record<string, unknown>)[part]\n }\n return typeof cur === \"string\" && cur.length > 0\n}\n\n/**\n * Extract a `KEY: <value>` value from `~/.hermes/config.yaml` text — a minimal,\n * dependency-free reader (no YAML parser pulled in): the first top-levelish\n * `key:` line, quotes stripped. Returns `undefined` when the key is absent or\n * its value is blank/empty-quotes.\n *\n * Used TWO ways: the discovery scanner only asks {@link yamlKeyPresent}\n * (boolean, never the value); the import path calls this directly to COPY the\n * value into the keychain. The value it returns is a SECRET — callers must\n * pass it straight to the credential store and never echo it.\n */\nexport function readYamlKeyValue(raw: string, key: string): string | undefined {\n const escaped = key.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\")\n const re = new RegExp(`^\\\\s*${escaped}\\\\s*:\\\\s*(.*)$`, \"m\")\n const m = re.exec(raw)\n if (!m) return undefined\n // Strip surrounding quotes; an empty remainder = no value.\n let value = (m[1] ?? \"\").trim()\n if (\n (value.startsWith('\"') && value.endsWith('\"')) ||\n (value.startsWith(\"'\") && value.endsWith(\"'\"))\n ) {\n value = value.slice(1, -1)\n }\n return value.length > 0 ? value : undefined\n}\n\n/**\n * Does `~/.hermes/config.yaml` carry a non-empty value for `key`? Presence\n * only — returns a boolean, NEVER the value (the discovery invariant).\n */\nexport function yamlKeyPresent(raw: string, key: string): boolean {\n return readYamlKeyValue(raw, key) !== undefined\n}\n\n/** claude-code OAuth item, and its file fallback — mirrors `claudeCodeOauthRecipe`. */\nconst CLAUDE_CODE_KEYCHAIN = \"Claude Code-credentials\"\nconst CLAUDE_CODE_JSON_PATH = \"claudeAiOauth.accessToken\"\nconst CLAUDE_CODE_FILE = \"~/.claude/.credentials.json\"\n\n/** File-based OAuth logins — mirror `codexRecipe` / `geminiRecipe`. */\nconst FILE_OAUTH_PROBES: ReadonlyArray<{\n origin: CredentialOrigin\n endpoint: string\n file: string\n jsonPath: string\n}> = [\n { origin: \"codex\", endpoint: \"openai\", file: \"~/.codex/auth.json\", jsonPath: \"tokens.access_token\" },\n { origin: \"gemini\", endpoint: \"google\", file: \"~/.gemini/oauth_creds.json\", jsonPath: \"access_token\" },\n]\n\n/** `~/.hermes/config.yaml` gateway keys — presence only. */\nconst HERMES_FILE = \"~/.hermes/config.yaml\"\nconst HERMES_KEYS: ReadonlyArray<{ key: string; endpoint: string }> = [\n { key: \"OPENROUTER_API_KEY\", endpoint: \"openrouter\" },\n { key: \"OPENAI_API_KEY\", endpoint: \"openai\" },\n]\n\n/** Provider API-key env vars — presence only. */\nconst ENV_KEYS: ReadonlyArray<{ env: string; endpoint: string }> = [\n { env: \"OPENROUTER_API_KEY\", endpoint: \"openrouter\" },\n { env: \"MOONSHOT_API_KEY\", endpoint: \"moonshot\" },\n { env: \"OPENAI_API_KEY\", endpoint: \"openai\" },\n { env: \"ANTHROPIC_API_KEY\", endpoint: \"anthropic\" },\n { env: \"DEEPSEEK_API_KEY\", endpoint: \"deepseek\" },\n { env: \"XAI_API_KEY\", endpoint: \"xai\" },\n]\n\nfunction defaultKeychainLookup(service: string): string {\n return execFileSync(\"security\", [\"find-generic-password\", \"-w\", \"-s\", service], {\n encoding: \"utf8\",\n }).trim()\n}\n\nfunction defaultReadFile(path: string): string {\n return readFileSync(path, \"utf8\")\n}\n\nfunction resolveDeps(deps: CredentialDiscoveryDeps): ResolvedDeps {\n const platform = deps.platform ?? process.platform\n return {\n homeDir: deps.homeDir ?? homedir(),\n env: deps.env ?? process.env,\n readFile: deps.readFile ?? defaultReadFile,\n keychainLookup:\n deps.keychainLookup ??\n (platform === \"darwin\"\n ? defaultKeychainLookup\n : () => {\n throw new Error(\"keychain is macOS-only\")\n }),\n platform,\n warn: deps.warn ?? (() => {}),\n }\n}\n\n/**\n * Scan the known local credential locations and report what's present, with\n * provenance and a non-secret locator. Read-only, and safe against a corrupt\n * or unreadable source (per-source warn+skip). NEVER returns a secret value.\n */\nexport function discoverCredentials(\n deps: CredentialDiscoveryDeps = {},\n): DiscoveredCredential[] {\n const d = resolveDeps(deps)\n const found: DiscoveredCredential[] = []\n\n probeClaudeCode(d, found)\n for (const probe of FILE_OAUTH_PROBES) probeFileOauth(d, probe, found)\n probeHermes(d, found)\n probeEnv(d, found)\n\n return found\n}\n\nfunction probeClaudeCode(d: ResolvedDeps, out: DiscoveredCredential[]): void {\n // Keychain first (macOS), then the file fallback — first hit wins, reported\n // once. Mirrors the recipe's ordered source chain.\n if (d.platform === \"darwin\") {\n try {\n const raw = d.keychainLookup(CLAUDE_CODE_KEYCHAIN)\n if (raw && jsonFieldPresent(raw, CLAUDE_CODE_JSON_PATH)) {\n out.push({\n endpoint: \"anthropic\",\n method: \"oauth-bearer\",\n origin: \"claude-code\",\n hint: `Claude Code OAuth token in macOS Keychain (\"${CLAUDE_CODE_KEYCHAIN}\")`,\n })\n return\n }\n } catch (err) {\n // A missing Keychain item throws too — that's a normal \"not logged in\",\n // not a malformed source, so don't warn on it. Fall through to the file.\n void err\n }\n }\n try {\n const raw = d.readFile(expandHome(d.homeDir, CLAUDE_CODE_FILE))\n if (jsonFieldPresent(raw, CLAUDE_CODE_JSON_PATH)) {\n out.push({\n endpoint: \"anthropic\",\n method: \"oauth-bearer\",\n origin: \"claude-code\",\n hint: `Claude Code OAuth token in ${CLAUDE_CODE_FILE}`,\n })\n }\n } catch (err) {\n if (!isMissingFile(err)) {\n d.warn(`skipping ${CLAUDE_CODE_FILE}: ${errText(err)}`)\n }\n }\n}\n\nfunction probeFileOauth(\n d: ResolvedDeps,\n probe: { origin: CredentialOrigin; endpoint: string; file: string; jsonPath: string },\n out: DiscoveredCredential[],\n): void {\n try {\n const raw = d.readFile(expandHome(d.homeDir, probe.file))\n if (jsonFieldPresent(raw, probe.jsonPath)) {\n out.push({\n endpoint: probe.endpoint,\n method: \"oauth-bearer\",\n origin: probe.origin,\n hint: `${probe.origin} OAuth token in ${probe.file}`,\n })\n }\n } catch (err) {\n if (!isMissingFile(err)) {\n d.warn(`skipping ${probe.file}: ${errText(err)}`)\n }\n }\n}\n\nfunction probeHermes(d: ResolvedDeps, out: DiscoveredCredential[]): void {\n let raw: string\n try {\n raw = d.readFile(expandHome(d.homeDir, HERMES_FILE))\n } catch (err) {\n if (!isMissingFile(err)) d.warn(`skipping ${HERMES_FILE}: ${errText(err)}`)\n return\n }\n for (const { key, endpoint } of HERMES_KEYS) {\n try {\n if (yamlKeyPresent(raw, key)) {\n out.push({\n endpoint,\n method: \"api-key\",\n origin: \"hermes-config\",\n hint: `${key} in ${HERMES_FILE}`,\n })\n }\n } catch (err) {\n d.warn(`skipping ${key} in ${HERMES_FILE}: ${errText(err)}`)\n }\n }\n}\n\nfunction probeEnv(d: ResolvedDeps, out: DiscoveredCredential[]): void {\n for (const { env, endpoint } of ENV_KEYS) {\n const value = d.env[env]\n if (value && value.trim().length > 0) {\n out.push({\n endpoint,\n method: \"api-key\",\n origin: \"env\",\n hint: `${env} in the environment`,\n })\n }\n }\n}\n\nfunction errText(err: unknown): string {\n return err instanceof Error ? err.message : String(err)\n}\n\n// ─────────────────────────────────────────────────────────────────────────\n// IMPORT — materialize a discovered credential into a named auth profile.\n//\n// Discovery only LOCATES a credential; import COPIES it into agentproto's own\n// storage (or wires a self-refreshing source). The two invariants that keep it\n// money-safe:\n//\n// - The method is bound by ORIGIN, never by caller input, so a subscription\n// bearer can never be materialized as an api-key (and vice-versa) — \"never\n// mix a bearer into an api-key env\". `createAuthProfile` also enforces the\n// method/source/credential exclusivity a second time.\n// - The COPY path reads the plaintext at the daemon ONLY to hand it straight\n// to `createAuthProfile`, which stores it and returns a fingerprint. The\n// secret is never placed in a return value or a log — fail loud (throw)\n// rather than proceed on anything ambiguous.\n// ─────────────────────────────────────────────────────────────────────────\n\n/** Raised when an import can't proceed (unknown origin/endpoint, or nothing to\n * import). Distinct type so the MCP surface can map it to a clean error. */\nexport class CredentialImportError extends Error {\n constructor(message: string) {\n super(message)\n this.name = \"CredentialImportError\"\n }\n}\n\n/** The three source-backed origins and the spawn-valid `source` + endpoint each\n * materializes as. `claude-code-oauth` resolves a fresh bearer at spawn;\n * `codex`/`gemini` are EXTERNAL file-based logins the CLI reads itself — their\n * source is verified (`verifyLocalLoginPresent`) but injects no bearer\n * (session-spawn.ts external branch). */\nconst SOURCE_BACKED_ORIGINS: Record<string, { source: string; endpoint: string }> = {\n \"claude-code\": { source: \"claude-code-oauth\", endpoint: \"anthropic\" },\n codex: { source: \"codex\", endpoint: \"openai\" },\n gemini: { source: \"gemini\", endpoint: \"google\" },\n}\n\n/** How a chosen `{ origin, endpoint }` becomes a profile. Discriminated: a\n * `source`-backed oauth-bearer, or an api-key COPIED from a located value. */\nexport type ImportMaterialization =\n | { method: \"oauth-bearer\"; endpoint: string; source: string }\n | {\n method: \"api-key\"\n endpoint: string\n copy: { via: \"env\" | \"hermes\"; key: string }\n }\n\n/**\n * Decide how to materialize a profile for a discovered `{ origin, endpoint }`.\n * PURE — no I/O, no secret. Throws {@link CredentialImportError} for an unknown\n * origin, or an endpoint that origin doesn't serve. The method is fixed by\n * origin here, which is what forbids a bearer↔api-key mix.\n */\nexport function planCredentialImport(\n origin: string,\n endpoint: string,\n): ImportMaterialization {\n const wanted = endpoint.trim()\n const sourceBacked = SOURCE_BACKED_ORIGINS[origin]\n if (sourceBacked) {\n if (wanted && wanted !== sourceBacked.endpoint) {\n throw new CredentialImportError(\n `origin \"${origin}\" authenticates ${sourceBacked.endpoint}, not \"${wanted}\"`,\n )\n }\n return { method: \"oauth-bearer\", endpoint: sourceBacked.endpoint, source: sourceBacked.source }\n }\n if (origin === \"env\") {\n const entry = ENV_KEYS.find((e) => e.endpoint === wanted)\n if (!entry) {\n throw new CredentialImportError(\n `no known env var maps to endpoint \"${wanted}\" for origin \"env\"`,\n )\n }\n return { method: \"api-key\", endpoint: wanted, copy: { via: \"env\", key: entry.env } }\n }\n if (origin === \"hermes-config\") {\n const entry = HERMES_KEYS.find((h) => h.endpoint === wanted)\n if (!entry) {\n throw new CredentialImportError(\n `no known ~/.hermes/config.yaml key maps to endpoint \"${wanted}\"`,\n )\n }\n return { method: \"api-key\", endpoint: wanted, copy: { via: \"hermes\", key: entry.key } }\n }\n throw new CredentialImportError(`unknown credential origin \"${origin}\"`)\n}\n\n/**\n * Resolve the plaintext for an api-key COPY. Returns a SECRET — the caller\n * hands it straight to `createAuthProfile` and never echoes it. Fails LOUD\n * ({@link CredentialImportError}) when the located value is absent/blank rather\n * than importing an empty credential.\n */\nfunction resolveCopyValue(\n copy: { via: \"env\" | \"hermes\"; key: string },\n d: ResolvedDeps,\n): string {\n if (copy.via === \"env\") {\n const value = d.env[copy.key]?.trim()\n if (!value) {\n throw new CredentialImportError(`no ${copy.key} in the environment to import`)\n }\n return value\n }\n let raw: string\n try {\n raw = d.readFile(expandHome(d.homeDir, HERMES_FILE))\n } catch (err) {\n throw new CredentialImportError(`cannot read ${HERMES_FILE} to import ${copy.key}: ${errText(err)}`)\n }\n const value = readYamlKeyValue(raw, copy.key)\n if (!value) {\n throw new CredentialImportError(`no ${copy.key} value in ${HERMES_FILE} to import`)\n }\n return value\n}\n\n/** Default profile id for an import — `<origin>-<endpoint>` (both are\n * slug-safe already). A caller may override. */\nfunction defaultImportId(origin: string, endpoint: string): string {\n return `${origin}-${endpoint}`\n}\n\nexport interface ImportCredentialArgs {\n /** Discovery origin to import from. */\n origin: string\n /** Billing endpoint to import (must be one the origin serves). */\n endpoint: string\n /** Optional explicit profile id; defaults to `<origin>-<endpoint>`. */\n id?: string\n /** Optional human label. */\n label?: string\n}\n\n/**\n * Import a discovered credential into a named auth profile end-to-end.\n *\n * Guards against fabricating a profile: it re-runs discovery and refuses unless\n * the requested `{ origin, endpoint }` is ACTUALLY present right now — discovery\n * having once located a value is not license to import it. Then materializes via\n * the single validated create path ({@link createAuthProfile}), stamping\n * `origin` so the UI can badge it. Returns the non-secret {@link\n * CreatedAuthProfile} (fingerprint, never the credential).\n *\n * `provisionDeps` is the profile/credential storage (a real keychain in prod, a\n * `MemoryStore` in tests); `ioDeps` is the discovery I/O (env / files /\n * keychain-read), injectable for the same reason.\n */\nexport async function importDiscoveredCredential(\n args: ImportCredentialArgs,\n provisionDeps: ProfileProvisionDeps,\n ioDeps: CredentialDiscoveryDeps = {},\n): Promise<CreatedAuthProfile> {\n const plan = planCredentialImport(args.origin, args.endpoint)\n\n // Only import what discovery currently sees — never fabricate a profile.\n const present = discoverCredentials(ioDeps).some(\n (c) => c.origin === args.origin && c.endpoint === plan.endpoint,\n )\n if (!present) {\n throw new CredentialImportError(\n `nothing to import — no ${args.origin} credential for ${plan.endpoint} was discovered`,\n )\n }\n\n const id = args.id?.trim() || defaultImportId(args.origin, plan.endpoint)\n const label = args.label?.trim()\n const base: CreateAuthProfileInput = {\n id,\n endpoint: plan.endpoint,\n method: plan.method,\n origin: args.origin,\n ...(label ? { label } : {}),\n }\n\n if (plan.method === \"oauth-bearer\") {\n return createAuthProfile({ ...base, source: plan.source }, provisionDeps)\n }\n // api-key COPY — resolve the plaintext at the daemon, hand it straight to the\n // store, never return it.\n const credential = resolveCopyValue(plan.copy, resolveDeps(ioDeps))\n return createAuthProfile({ ...base, credential }, provisionDeps)\n}\n\nfunction text(value: string | object): {\n content: Array<{ type: \"text\"; text: string }>\n} {\n return {\n content: [\n {\n type: \"text\",\n text: typeof value === \"string\" ? value : JSON.stringify(value),\n },\n ],\n }\n}\n\nfunction errorText(message: string): {\n content: Array<{ type: \"text\"; text: string }>\n isError: true\n} {\n return { content: [{ type: \"text\", text: message }], isError: true }\n}\n\nexport function registerCredentialDiscoveryTools(server: McpServer): void {\n // ── auth_discover_credentials ─────────────────────────────────\n server.tool(\n \"auth_discover_credentials\",\n \"Read-only scan of the known local credential locations (Claude Code's \" +\n \"Keychain/OAuth file, Codex/Gemini login files, `~/.hermes/config.yaml`, \" +\n \"and the provider API-key env vars). Reports which credentials are \" +\n \"PRESENT and where they came from, so onboarding can offer to import one \" +\n \"you already have. Returns only non-secret provenance — `{ endpoint, \" +\n \"method, origin, hint }` — and NEVER the credential value; the `hint` is \" +\n \"a locator, not the secret. A malformed or unreadable source is skipped, \" +\n \"never fatal; a missing file is a normal 'not found'.\",\n {},\n async () => {\n try {\n const warnings: string[] = []\n const credentials = discoverCredentials({ warn: (m) => warnings.push(m) })\n return text({\n credentials,\n ...(warnings.length ? { warnings } : {}),\n })\n } catch (err) {\n return errorText(`auth_discover_credentials failed: ${errText(err)}`)\n }\n },\n )\n}\n"]}