@qadanm/opencode-smart-router 0.0.0-stage → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. package/CHANGELOG.md +118 -0
  2. package/CONTRIBUTING.md +96 -0
  3. package/LICENSE +21 -0
  4. package/README.md +341 -2
  5. package/SECURITY.md +174 -0
  6. package/config.example.json +108 -0
  7. package/dist/allowance.js +487 -0
  8. package/dist/gui-experimental.cjs +1 -0
  9. package/dist/index.js +11111 -0
  10. package/dist/npm/index.js +40 -0
  11. package/dist/tui.js +587 -0
  12. package/docs/ARCHITECTURE.md +104 -0
  13. package/docs/DEBUGGING-GUI.md +70 -0
  14. package/docs/INSTALL.md +120 -0
  15. package/docs/LIMITATIONS.md +213 -0
  16. package/docs/MODELS.md +124 -0
  17. package/docs/OPENDESIGN-INTEGRATION.md +635 -0
  18. package/docs/QA.md +142 -0
  19. package/docs/SHARED-CORE.md +72 -0
  20. package/install +75 -0
  21. package/package.json +92 -4
  22. package/scripts/install.sh +324 -0
  23. package/scripts/uninstall.sh +91 -0
  24. package/src/allowance.ts +135 -0
  25. package/src/core/classify/classifier.ts +305 -0
  26. package/src/core/config-writer.ts +167 -0
  27. package/src/core/config.ts +1030 -0
  28. package/src/core/context/strategy.ts +188 -0
  29. package/src/core/context-health.ts +39 -0
  30. package/src/core/failures.ts +398 -0
  31. package/src/core/history/opencode.ts +173 -0
  32. package/src/core/models/protocol.ts +22 -0
  33. package/src/core/models/registry.ts +203 -0
  34. package/src/core/models/seed.ts +771 -0
  35. package/src/core/offline/mode.ts +321 -0
  36. package/src/core/plan-page.ts +390 -0
  37. package/src/core/plans/catalog.ts +481 -0
  38. package/src/core/policy/calibration.ts +259 -0
  39. package/src/core/policy/eligibility.ts +578 -0
  40. package/src/core/providers/enablement.ts +143 -0
  41. package/src/core/providers/opendesign-credits.ts +206 -0
  42. package/src/core/providers/opendesign-models.ts +628 -0
  43. package/src/core/providers/opendesign.ts +474 -0
  44. package/src/core/quota/calibration.ts +166 -0
  45. package/src/core/quota/commandcode.ts +471 -0
  46. package/src/core/quota/credentials.ts +490 -0
  47. package/src/core/quota/ledger.ts +1273 -0
  48. package/src/core/quota/opencode-go.ts +148 -0
  49. package/src/core/quota/opendesign.ts +1043 -0
  50. package/src/core/quota/reconcile.ts +155 -0
  51. package/src/core/quota/report.ts +261 -0
  52. package/src/core/quota/service.ts +273 -0
  53. package/src/core/reprompt.ts +48 -0
  54. package/src/core/routing/engine.ts +919 -0
  55. package/src/core/types.ts +825 -0
  56. package/src/core/ui.ts +236 -0
  57. package/src/core/util.ts +114 -0
  58. package/src/gui/index.tsx +1670 -0
  59. package/src/gui/viewmodel.ts +456 -0
  60. package/src/index.ts +48 -0
  61. package/src/rpc.ts +204 -0
  62. package/src/server/controller.ts +3281 -0
  63. package/src/server/route-tracker.ts +60 -0
  64. package/src/server/shared-core.ts +671 -0
  65. package/src/tui/index.tsx +168 -0
@@ -0,0 +1,490 @@
1
+ import { closeSync, constants, fstatSync, openSync, readFileSync } from "node:fs"
2
+ import { homedir } from "node:os"
3
+ import { join } from "node:path"
4
+ import { redact } from "../util.js"
5
+
6
+ /**
7
+ * Credential discovery from the local filesystem.
8
+ *
9
+ * ## Security model
10
+ *
11
+ * This module reads bearer credentials that belong to the user and that the
12
+ * user has already placed on disk for their own tools. It is therefore a
13
+ * *local trust* component, not an authentication component: it never grants
14
+ * access to anything the user could not already reach themselves.
15
+ *
16
+ * Even so, silently harvesting secrets from predictable paths is a pattern
17
+ * that deserves explicit, documented containment, because a router that
18
+ * "finds" credentials automatically is one bad path away from reading
19
+ * something it has no business reading. The rules below are therefore
20
+ * enforced in code and asserted in tests:
21
+ *
22
+ * 1. **Opt-in, never implicit.** Automatic filesystem discovery requires
23
+ * `credentialSource: "auto"` (or the `SMART_ROUTER_ALLOW_KEY_DISCOVERY`
24
+ * environment variable). The default is `"explicit"`, which reads only
25
+ * credentials the user named in configuration or the environment.
26
+ *
27
+ * 2. **No cross-application credential theft.** Only paths that belong to
28
+ * this router's own configuration directory or to the named provider are
29
+ * read. Another application's unified credential store (for example
30
+ * OpenCode's own `auth.json`, which holds OAuth refresh tokens for every
31
+ * provider the user has connected) is explicitly excluded. Reading it
32
+ * would mean handling tokens that are not ours, for providers we were
33
+ * never asked about.
34
+ *
35
+ * 3. **No symlinks, no TOCTOU.** Each file is opened once, with
36
+ * `O_NOFOLLOW`, and the permission and size checks are applied to the
37
+ * resulting file descriptor. The previous implementation checked
38
+ * `statSync(path)` and then read `path`, which allows the path to be
39
+ * swapped for a symlink between the two calls.
40
+ *
41
+ * 4. **Owner-only permissions.** A file readable by group or world is
42
+ * refused, because a shared machine could otherwise influence which
43
+ * credential the router uses.
44
+ *
45
+ * 5. **Bounded reads.** Files are size-capped before a read is attempted and
46
+ * the descriptor is always closed.
47
+ *
48
+ * 6. **Secrets never leave this module.** A discovered key is returned to the
49
+ * caller and placed in an `Authorization` header for the provider it
50
+ * belongs to. It is never logged, never written to the ledger, never
51
+ * embedded in a decision, a session title, a diagnostic bundle or an
52
+ * error message. `redact()` is applied to every error string.
53
+ */
54
+
55
+ export interface CredentialCandidate {
56
+ readonly source: string
57
+ readonly key: string
58
+ readonly path?: string
59
+ }
60
+
61
+ export interface CredentialDiscovery {
62
+ readonly candidates: CredentialCandidate[]
63
+ readonly warnings: string[]
64
+ }
65
+
66
+ /**
67
+ * Whether the router may look for credentials on disk without being told
68
+ * exactly where.
69
+ *
70
+ * - `explicit` (default) reads only configured paths and the environment.
71
+ * - `auto` also reads the provider's own well-known files.
72
+ * - `off` reads nothing from disk at all; the environment and configuration
73
+ * are the only sources.
74
+ */
75
+ export type CredentialSourcePolicy = "explicit" | "auto" | "off"
76
+
77
+ export const CREDENTIAL_DISCOVERY_ENV = "SMART_ROUTER_ALLOW_KEY_DISCOVERY"
78
+
79
+ /** Resolve the effective policy, allowing an explicit environment override. */
80
+ export const resolveCredentialSourcePolicy = (
81
+ configured: CredentialSourcePolicy | undefined,
82
+ env: Record<string, string | undefined> = process.env,
83
+ ): CredentialSourcePolicy => {
84
+ if (configured) return configured
85
+ const flag = env[CREDENTIAL_DISCOVERY_ENV]?.trim().toLowerCase()
86
+ if (flag === "1" || flag === "true" || flag === "yes") return "auto"
87
+ return "explicit"
88
+ }
89
+
90
+ const MAX_CREDENTIAL_FILE_BYTES = 64 * 1024
91
+
92
+ const isRecord = (value: unknown): value is Record<string, unknown> =>
93
+ typeof value === "object" && value !== null && !Array.isArray(value)
94
+
95
+ const pickString = (record: Record<string, unknown>, keys: ReadonlyArray<string>): string | undefined => {
96
+ for (const key of keys) {
97
+ const value = record[key]
98
+ if (typeof value === "string" && value.trim().length > 0) return value.trim()
99
+ }
100
+ return undefined
101
+ }
102
+
103
+ const plausibleKey = (value: string): boolean => {
104
+ const trimmed = value.trim()
105
+ if (trimmed.length < 16 || trimmed.length > 4096) return false
106
+ if (/\s/.test(trimmed)) return false
107
+ // Known provider credential prefixes are unambiguous even when they are
108
+ // entirely snake_case, which the placeholder test below would reject.
109
+ if (trimmed.startsWith("sk-") || trimmed.startsWith("vela_")) return true
110
+ if (/^[A-Za-z_][A-Za-z0-9_]*$/.test(trimmed) && !trimmed.startsWith("user_") && !trimmed.startsWith("cmd_")) {
111
+ return false
112
+ }
113
+ return true
114
+ }
115
+
116
+ const parseEnvFile = (text: string, envNames: ReadonlyArray<string>): string | undefined => {
117
+ for (const line of text.split(/\r?\n/)) {
118
+ const trimmed = line.trim()
119
+ if (!trimmed || trimmed.startsWith("#")) continue
120
+ const match = /^(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*)$/.exec(trimmed)
121
+ if (!match) continue
122
+ const [, name, rawValue] = match
123
+ if (!name || !envNames.includes(name)) continue
124
+ let value = (rawValue ?? "").trim()
125
+ if (
126
+ (value.startsWith('"') && value.endsWith('"')) ||
127
+ (value.startsWith("'") && value.endsWith("'"))
128
+ ) {
129
+ value = value.slice(1, -1)
130
+ }
131
+ if (plausibleKey(value)) return value
132
+ }
133
+ return undefined
134
+ }
135
+
136
+ const parseAuthJson = (value: unknown, outerKeys: ReadonlyArray<string>): string | undefined => {
137
+ if (!isRecord(value)) return undefined
138
+ const direct = pickString(value, ["apiKey", "key", "access", "token"])
139
+ if (direct && plausibleKey(direct)) return direct
140
+
141
+ for (const outer of outerKeys) {
142
+ const nested = value[outer]
143
+ if (isRecord(nested)) {
144
+ const key = pickString(nested, ["apiKey", "key", "access", "token"])
145
+ if (key && plausibleKey(key)) return key
146
+ }
147
+ }
148
+
149
+ for (const outer of ["providers", "auth", "credentials"]) {
150
+ const nested = value[outer]
151
+ if (isRecord(nested)) {
152
+ for (const inner of outerKeys) {
153
+ const entry = nested[inner]
154
+ if (isRecord(entry)) {
155
+ const key = pickString(entry, ["apiKey", "key", "access", "token"])
156
+ if (key && plausibleKey(key)) return key
157
+ }
158
+ }
159
+ }
160
+ }
161
+ return undefined
162
+ }
163
+
164
+ export interface CredentialFileOptions {
165
+ /** Environment variable names accepted inside an env-style file. */
166
+ readonly envNames: ReadonlyArray<string>
167
+ /** Top-level and nested object keys accepted inside a JSON file. */
168
+ readonly jsonKeys: ReadonlyArray<string>
169
+ /** Human-readable provider name used in warnings. */
170
+ readonly label: string
171
+ }
172
+
173
+ const COMMAND_CODE_FILE_OPTIONS: CredentialFileOptions = {
174
+ envNames: ["COMMAND_CODE_API_KEY", "COMMANDCODE_API_KEY"],
175
+ jsonKeys: ["commandcode", "command-code", "commandCode", "command_code"],
176
+ label: "Command Code",
177
+ }
178
+
179
+ /**
180
+ * Read a credential file safely.
181
+ *
182
+ * The file is opened once with `O_NOFOLLOW` and every check runs against the
183
+ * open descriptor, so a path cannot be swapped for a symlink between the
184
+ * permission check and the read. The descriptor is closed on every path out.
185
+ *
186
+ * `explicit` marks a path the operator named directly. Discovery applies a
187
+ * stricter rule than configuration does: when scanning well-known locations
188
+ * the router must not accept a file owned by another account, because it did
189
+ * not intend to read that specific file. When an operator names a path, that
190
+ * is already a deliberate trust decision, and refusing it on uid alone breaks
191
+ * containers and bind mounts where the process runs as a different user than
192
+ * the one that owns the mounted secret.
193
+ */
194
+ export const readCredentialFileWith = (
195
+ path: string,
196
+ warnings: string[],
197
+ options: CredentialFileOptions,
198
+ explicit = false,
199
+ ): CredentialCandidate | undefined => {
200
+ let fd: number | undefined
201
+ try {
202
+ // `O_NOFOLLOW` makes a symlink fail with ELOOP rather than being followed.
203
+ fd = openSync(path, constants.O_RDONLY | constants.O_NOFOLLOW)
204
+ const stat = fstatSync(fd)
205
+
206
+ if (!stat.isFile()) {
207
+ warnings.push(`${options.label} credential path is not a regular file: ${path}`)
208
+ return undefined
209
+ }
210
+ if (stat.size > MAX_CREDENTIAL_FILE_BYTES) {
211
+ warnings.push(`${options.label} credential file is unexpectedly large: ${path}`)
212
+ return undefined
213
+ }
214
+ // The key is a bearer credential. Refuse group/world-readable files
215
+ // unconditionally. Refuse a file owned by another user only when the path
216
+ // was discovered rather than named: on a shared machine another account's
217
+ // file must not be able to influence which credential we use, but an
218
+ // operator who named this exact path already made that decision.
219
+ if ((stat.mode & 0o077) !== 0) {
220
+ warnings.push(`Ignoring ${options.label} credential file with group/world permissions: ${path}`)
221
+ return undefined
222
+ }
223
+ if (!explicit && typeof process.getuid === "function" && stat.uid !== process.getuid()) {
224
+ warnings.push(`Ignoring ${options.label} credential file owned by another user: ${path}`)
225
+ return undefined
226
+ }
227
+ if (stat.size === 0) return undefined
228
+
229
+ // Bounded read from the descriptor that was actually checked.
230
+ const text = readFileSync(fd, "utf8").trim()
231
+ if (!text) return undefined
232
+
233
+ let key: string | undefined
234
+ if (text.startsWith("{")) {
235
+ try {
236
+ key = parseAuthJson(JSON.parse(text) as unknown, options.jsonKeys)
237
+ } catch (error) {
238
+ warnings.push(`Could not parse ${path}: ${redact(error instanceof Error ? error.message : String(error))}`)
239
+ }
240
+ } else if (text.includes("=")) {
241
+ key = parseEnvFile(text, options.envNames)
242
+ } else if (plausibleKey(text)) {
243
+ key = text
244
+ }
245
+
246
+ if (!key) {
247
+ warnings.push(`No ${options.label} API key found in ${path}`)
248
+ return undefined
249
+ }
250
+ return { source: path, key, path }
251
+ } catch (error) {
252
+ const code = (error as NodeJS.ErrnoException)?.code
253
+ if (code === "ENOENT") return undefined
254
+ if (code === "ELOOP") {
255
+ warnings.push(`Ignoring ${options.label} credential path because it is a symlink: ${path}`)
256
+ return undefined
257
+ }
258
+ warnings.push(`Could not read ${path}: ${redact(error instanceof Error ? error.message : String(error))}`)
259
+ return undefined
260
+ } finally {
261
+ if (fd !== undefined) {
262
+ try {
263
+ closeSync(fd)
264
+ } catch {
265
+ // Closing a descriptor we already failed to read is not actionable.
266
+ }
267
+ }
268
+ }
269
+ }
270
+
271
+ export const readCredentialFile = (
272
+ path: string,
273
+ warnings: string[],
274
+ ): CredentialCandidate | undefined => readCredentialFileWith(path, warnings, COMMAND_CODE_FILE_OPTIONS)
275
+
276
+ export interface DiscoverCommandCodeInput {
277
+ readonly home?: string
278
+ readonly env?: Record<string, string | undefined>
279
+ readonly explicitPath?: string
280
+ readonly openCodeIntegrationKey?: string
281
+ readonly providerSettingsKey?: string
282
+ /**
283
+ * Whether unwritten-on-disk discovery is permitted. Defaults to reading
284
+ * `SMART_ROUTER_ALLOW_KEY_DISCOVERY`; without it, only the environment and
285
+ * explicitly configured paths are used.
286
+ */
287
+ readonly credentialSource?: CredentialSourcePolicy
288
+ }
289
+
290
+ /**
291
+ * Command Code credential search order.
292
+ *
293
+ * The provider API key configured in OpenCode is deliberately last: it can
294
+ * serve inference but may not be authorized for the `/alpha` endpoints.
295
+ *
296
+ * Note what is *absent*: OpenCode's own `auth.json` is never read. That file is
297
+ * another application's unified credential store and contains OAuth material
298
+ * for every provider the user has connected. Those tokens belong to providers
299
+ * this router was never asked about.
300
+ */
301
+ export const discoverCommandCodeCredentials = (input: DiscoverCommandCodeInput = {}): CredentialDiscovery => {
302
+ const warnings: string[] = []
303
+ const candidates: CredentialCandidate[] = []
304
+ const seen = new Set<string>()
305
+ const home = input.home ?? homedir()
306
+ const env = input.env ?? process.env
307
+ const policy = resolveCredentialSourcePolicy(input.credentialSource, env)
308
+
309
+ const add = (candidate: CredentialCandidate | undefined) => {
310
+ if (!candidate || seen.has(candidate.key)) return
311
+ seen.add(candidate.key)
312
+ candidates.push(candidate)
313
+ }
314
+
315
+ const envKey = env.COMMAND_CODE_API_KEY?.trim() || env.COMMANDCODE_API_KEY?.trim()
316
+ if (envKey && plausibleKey(envKey)) add({ source: "environment", key: envKey })
317
+
318
+ if (input.explicitPath) add(readCredentialFileWith(input.explicitPath, warnings, COMMAND_CODE_FILE_OPTIONS, true))
319
+
320
+ if (policy === "off") {
321
+ if (candidates.length === 0) {
322
+ warnings.push(
323
+ "Credential discovery is disabled, so no Command Code key was found. Set COMMAND_CODE_API_KEY or configure an explicit path.",
324
+ )
325
+ }
326
+ } else if (policy === "auto") {
327
+ const defaultFiles = [
328
+ join(home, ".commandcode", "auth.json"),
329
+ join(home, ".config", "opencode", "commandcode.key"),
330
+ join(home, ".config", "opencode", "commandcode.env"),
331
+ join(home, ".config", "opencode", "commandcode.json"),
332
+ ]
333
+ for (const path of defaultFiles) add(readCredentialFile(path, warnings))
334
+ } else if (candidates.length === 0) {
335
+ warnings.push(
336
+ `No Command Code key was supplied. Automatic discovery is off by default; set ${CREDENTIAL_DISCOVERY_ENV}=1 to let the router read ${join(home, ".config", "opencode", "commandcode.key")}, or set COMMAND_CODE_API_KEY.`,
337
+ )
338
+ }
339
+
340
+ if (input.openCodeIntegrationKey && plausibleKey(input.openCodeIntegrationKey)) {
341
+ add({ source: "OpenCode integration", key: input.openCodeIntegrationKey })
342
+ }
343
+ if (input.providerSettingsKey && plausibleKey(input.providerSettingsKey)) {
344
+ add({ source: "OpenCode provider settings", key: input.providerSettingsKey })
345
+ }
346
+
347
+ return { candidates, warnings }
348
+ }
349
+
350
+ const OPENDESIGN_FILE_OPTIONS: CredentialFileOptions = {
351
+ envNames: ["OPENDESIGN_API_KEY", "OPEN_DESIGN_API_KEY"],
352
+ jsonKeys: ["opendesign", "open-design", "openDesign", "open_design"],
353
+ label: "OpenDesign",
354
+ }
355
+
356
+ /**
357
+ * An OpenDesign inference key is a bearer credential for the model gateway.
358
+ * It cannot read quota, and it is not a control key.
359
+ */
360
+ export const plausibleOpenDesignKey = (value: string): boolean => {
361
+ const trimmed = value.trim()
362
+ if (!plausibleKey(trimmed)) return false
363
+ return /^sk-[A-Za-z0-9._-]{8,}$/.test(trimmed)
364
+ }
365
+
366
+ export interface DiscoverOpenDesignInput {
367
+ readonly home?: string
368
+ readonly env?: Record<string, string | undefined>
369
+ readonly explicitPath?: string
370
+ readonly providerSettingsKey?: string
371
+ readonly credentialSource?: CredentialSourcePolicy
372
+ }
373
+
374
+ /**
375
+ * Discover the OpenDesign inference key.
376
+ *
377
+ * Only the OpenDesign hosts ever receive this value. The key is never written
378
+ * into a decision, a log, a session title, or a diagnostic bundle.
379
+ */
380
+ export const discoverOpenDesignCredentials = (input: DiscoverOpenDesignInput = {}): CredentialDiscovery => {
381
+ const warnings: string[] = []
382
+ const candidates: CredentialCandidate[] = []
383
+ const seen = new Set<string>()
384
+ const home = input.home ?? homedir()
385
+ const env = input.env ?? process.env
386
+ const policy = resolveCredentialSourcePolicy(input.credentialSource, env)
387
+
388
+ const add = (candidate: CredentialCandidate | undefined) => {
389
+ if (!candidate || seen.has(candidate.key)) return
390
+ if (!plausibleOpenDesignKey(candidate.key)) return
391
+ seen.add(candidate.key)
392
+ candidates.push(candidate)
393
+ }
394
+
395
+ const envKey = env.OPENDESIGN_API_KEY?.trim() || env.OPEN_DESIGN_API_KEY?.trim()
396
+ if (envKey) {
397
+ if (plausibleOpenDesignKey(envKey)) add({ source: "environment", key: envKey })
398
+ else warnings.push("The OpenDesign environment key does not look like an inference key and was ignored.")
399
+ }
400
+
401
+ if (input.explicitPath) add(readCredentialFileWith(input.explicitPath, warnings, OPENDESIGN_FILE_OPTIONS, true))
402
+
403
+ if (policy === "off") {
404
+ if (candidates.length === 0) {
405
+ warnings.push(
406
+ "Credential discovery is disabled, so no OpenDesign key was found. Set OPENDESIGN_API_KEY or configure an explicit path.",
407
+ )
408
+ }
409
+ } else if (policy === "auto") {
410
+ const defaultFiles = [
411
+ join(home, ".config", "opencode", "opendesign.key"),
412
+ join(home, ".config", "opencode", "opendesign.env"),
413
+ join(home, ".config", "opencode", "opendesign.json"),
414
+ ]
415
+ for (const path of defaultFiles) add(readCredentialFileWith(path, warnings, OPENDESIGN_FILE_OPTIONS))
416
+ } else if (candidates.length === 0) {
417
+ warnings.push(
418
+ `No OpenDesign key was supplied. Automatic discovery is off by default; set ${CREDENTIAL_DISCOVERY_ENV}=1 to let the router read ${join(home, ".config", "opencode", "opendesign.key")}, or set OPENDESIGN_API_KEY.`,
419
+ )
420
+ }
421
+
422
+ if (input.providerSettingsKey) add({ source: "OpenCode provider settings", key: input.providerSettingsKey })
423
+
424
+ return { candidates, warnings }
425
+ }
426
+
427
+ /**
428
+ * Discover an OpenDesign account-control key.
429
+ *
430
+ * The control key can read the account's coding-plan usage windows. It is a
431
+ * privileged credential, it is never discoverable from the environment, and
432
+ * the router never requests one. It is used only when the owner points at a
433
+ * file explicitly.
434
+ */
435
+ export const discoverOpenDesignControlKey = (input: {
436
+ readonly explicitPath?: string
437
+ } = {}): CredentialDiscovery => {
438
+ const warnings: string[] = []
439
+ const candidates: CredentialCandidate[] = []
440
+ if (!input.explicitPath) {
441
+ warnings.push(
442
+ "No OpenDesign control key path is configured, so quota is reported as an estimate rather than a verified balance.",
443
+ )
444
+ return { candidates, warnings }
445
+ }
446
+ const candidate = readCredentialFileWith(
447
+ input.explicitPath,
448
+ warnings,
449
+ {
450
+ envNames: ["OPENDESIGN_CONTROL_KEY"],
451
+ jsonKeys: ["opendesign-control", "opendesignControl", "control"],
452
+ label: "OpenDesign control",
453
+ },
454
+ true,
455
+ )
456
+ if (candidate) candidates.push(candidate)
457
+ return { candidates, warnings }
458
+ }
459
+
460
+ /**
461
+ * Describe where a resolved credential came from, without revealing it.
462
+ *
463
+ * Diagnostic output needs to answer "did the router find a key, and where from"
464
+ * without ever carrying the secret itself, and without leaking a path beyond
465
+ * the final segment. `source` is caller-supplied for the non-file cases, so it
466
+ * is reduced to a fixed vocabulary rather than echoed: an arbitrary string here
467
+ * would be an unbounded channel into a persisted snapshot note.
468
+ */
469
+ export const describeCredentialSource = (candidate: CredentialCandidate | undefined): string => {
470
+ if (!candidate) return "not found"
471
+ if (candidate.path) {
472
+ // Only the final path segment is useful for support and leaks no prefix.
473
+ // A trailing separator yields an empty segment rather than undefined, so
474
+ // filter empties before falling back.
475
+ const segments = candidate.path.split("/").filter((segment) => segment.length > 0)
476
+ const base = segments[segments.length - 1] ?? "file"
477
+ return `file (${base})`
478
+ }
479
+ switch (candidate.source) {
480
+ case "environment":
481
+ return "environment variable"
482
+ case "OpenCode integration":
483
+ return "OpenCode integration"
484
+ case "OpenCode provider settings":
485
+ return "OpenCode provider settings"
486
+ default:
487
+ // Never echo an unrecognized source string.
488
+ return "configured"
489
+ }
490
+ }