@slashfi/agents-sdk 0.77.2 → 0.78.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.
@@ -17,7 +17,9 @@
17
17
  * ```
18
18
  */
19
19
 
20
+ import { AdkError } from "./adk-error.js";
20
21
  import type { FsStore } from "./agent-definitions/config.js";
22
+ import { decryptSecret, encryptSecret } from "./crypto.js";
21
23
  import type {
22
24
  ConsumerConfig,
23
25
  RefAddInput,
@@ -27,31 +29,68 @@ import type {
27
29
  ResolvedRegistry,
28
30
  } from "./define-config.js";
29
31
  import { normalizeRef } from "./define-config.js";
32
+ import type { RegistryAuthRequirement } from "./define-config.js";
30
33
  import type { FetchFn } from "./fetch-types.js";
31
34
  import type { Logger } from "./logger.js";
32
- import { createRegistryConsumer } from "./registry-consumer.js";
33
- import type {
34
- AgentListEntry,
35
- AgentInspection,
36
- RegistryConfiguration,
37
- RegistryConsumer,
38
- } from "./registry-consumer.js";
39
- import type { CallAgentResponse, SecuritySchemeSummary } from "./types.js";
40
- import { decryptSecret, encryptSecret } from "./crypto.js";
41
- import { AdkError } from "./adk-error.js";
42
35
  import {
36
+ buildOAuthAuthorizeUrl,
43
37
  discoverOAuthMetadata,
44
38
  dynamicClientRegistration,
45
- buildOAuthAuthorizeUrl,
46
39
  exchangeCodeForTokens,
47
40
  probeRegistryAuth,
48
41
  refreshAccessToken,
49
42
  } from "./mcp-client.js";
50
- import type { RegistryAuthRequirement } from "./define-config.js";
43
+ import { createRegistryConsumer } from "./registry-consumer.js";
44
+ import type {
45
+ AgentInspection,
46
+ AgentListEntry,
47
+ RegistryConfiguration,
48
+ RegistryConsumer,
49
+ } from "./registry-consumer.js";
50
+ import type { CallAgentResponse, SecuritySchemeSummary } from "./types.js";
51
51
 
52
52
  const CONFIG_PATH = "consumer-config.json";
53
+ const REGISTRY_CACHE_PATH = "registry-cache.json";
53
54
  const SECRET_PREFIX = "secret:";
54
55
 
56
+ // ============================================
57
+ // Registry cache types
58
+ // ============================================
59
+
60
+ /**
61
+ * Slim tool summary stored in the registry cache. Mirrors the shape returned
62
+ * by `consumer.inspect()` (sans `inputSchema` and `fullTokens`) so the LLM
63
+ * can discover an agent's surface without a network round-trip.
64
+ */
65
+ export interface RegistryCacheToolSummary {
66
+ name: string;
67
+ description?: string;
68
+ }
69
+
70
+ /**
71
+ * Per-ref cache entry. Updated as a side-effect of `ref.add()` and
72
+ * `ref.inspect()` whenever the registry response carries description or tool
73
+ * information. Identity-relative (lives next to the consumer-config that
74
+ * issued the registry call), so permission-filtered views stay consistent.
75
+ */
76
+ export interface RegistryCacheEntry {
77
+ /** Canonical agent path (e.g. `notion`). Stored for sanity/debug. */
78
+ ref: string;
79
+ description?: string;
80
+ tools?: RegistryCacheToolSummary[];
81
+ /** ISO timestamp of the most recent registry round-trip that wrote this. */
82
+ fetchedAt: string;
83
+ }
84
+
85
+ /**
86
+ * On-disk shape of `registry-cache.json`. Keyed by `RefEntry.name` (local
87
+ * identifier) — the same key consumer-config uses, so hydration is a 1:1
88
+ * lookup.
89
+ */
90
+ export interface RegistryCache {
91
+ refs: Record<string, RegistryCacheEntry>;
92
+ }
93
+
55
94
  // ============================================
56
95
  // Types
57
96
  // ============================================
@@ -128,7 +167,9 @@ export interface RegistryTestResult {
128
167
  }
129
168
 
130
169
  export interface AdkRegistryApi {
131
- add(entry: RegistryEntry): Promise<{ authRequirement?: RegistryAuthRequirement }>;
170
+ add(
171
+ entry: RegistryEntry,
172
+ ): Promise<{ authRequirement?: RegistryAuthRequirement }>;
132
173
  remove(nameOrUrl: string): Promise<boolean>;
133
174
  list(): Promise<RegistryEntry[]>;
134
175
  get(name: string): Promise<RegistryEntry | null>;
@@ -230,7 +271,7 @@ export interface AuthStartResult {
230
271
  * When populated, call() rejects unknown agent paths and tool names at compile time.
231
272
  */
232
273
  // eslint-disable-next-line @typescript-eslint/no-empty-interface
233
- export interface AdkAgentRegistry {}
274
+ export type AdkAgentRegistry = {};
234
275
 
235
276
  /** @internal Helper types for conditional call() signature */
236
277
  type AgentPath = keyof AdkAgentRegistry;
@@ -244,9 +285,17 @@ type ParamsOf<
244
285
 
245
286
  type AdkRefCallFn = keyof AdkAgentRegistry extends never
246
287
  ? // No registry — loose fallback
247
- (name: string, tool: string, params?: Record<string, unknown>) => Promise<CallAgentResponse>
288
+ (
289
+ name: string,
290
+ tool: string,
291
+ params?: Record<string, unknown>,
292
+ ) => Promise<CallAgentResponse>
248
293
  : // Registry populated — strict typed overload
249
- <A extends AgentPath, T extends ToolsOf<A>>(name: A, tool: T, params: ParamsOf<A, T>) => Promise<CallAgentResponse>;
294
+ <A extends AgentPath, T extends ToolsOf<A>>(
295
+ name: A,
296
+ tool: T,
297
+ params: ParamsOf<A, T>,
298
+ ) => Promise<CallAgentResponse>;
250
299
 
251
300
  export interface AdkRefApi {
252
301
  add(entry: RefAddInput): Promise<{ security: SecuritySchemeSummary | null }>;
@@ -254,7 +303,10 @@ export interface AdkRefApi {
254
303
  list(): Promise<ResolvedRef[]>;
255
304
  get(name: string): Promise<ResolvedRef | null>;
256
305
  update(name: string, updates: Partial<RefEntry>): Promise<boolean>;
257
- inspect(name: string, options?: { full?: boolean }): Promise<AgentInspection | null>;
306
+ inspect(
307
+ name: string,
308
+ options?: { full?: boolean },
309
+ ): Promise<AgentInspection | null>;
258
310
  call: AdkRefCallFn;
259
311
  resources(name: string): Promise<CallAgentResponse>;
260
312
  read(name: string, uris: string[]): Promise<CallAgentResponse>;
@@ -265,37 +317,43 @@ export interface AdkRefApi {
265
317
  * Call adk.handleCallback() when the callback arrives, or use
266
318
  * adk.ref.authLocal() to spin up a local server and block.
267
319
  */
268
- auth(name: string, opts?: {
269
- /** For API key / bearer auth: the key/token value (single-key shorthand) */
270
- apiKey?: string;
271
- /**
272
- * Credentials map for multi-field auth. Keys match the `name` field
273
- * from AuthChallengeField (e.g. { "api_key": "xxx", "app_key": "yyy" }).
274
- * For single-key apiKey or http bearer, `apiKey` shorthand also works.
275
- */
276
- credentials?: Record<string, string>;
277
- /** Extra context to encode in the OAuth state (e.g., tenant/user IDs for multi-tenant callbacks) */
278
- stateContext?: Record<string, unknown>;
279
- /** Additional scopes to request (e.g., optional scopes declared by the agent) */
280
- scopes?: string[];
281
- /**
282
- * Opt out of proxy routing when the ref's source registry has
283
- * `proxy: { mode: 'optional' }`. Ignored for `mode: 'required'`.
284
- * Defaults to `false` if a registry offers a proxy we use it.
285
- */
286
- preferLocal?: boolean;
287
- }): Promise<AuthStartResult>;
320
+ auth(
321
+ name: string,
322
+ opts?: {
323
+ /** For API key / bearer auth: the key/token value (single-key shorthand) */
324
+ apiKey?: string;
325
+ /**
326
+ * Credentials map for multi-field auth. Keys match the `name` field
327
+ * from AuthChallengeField (e.g. { "api_key": "xxx", "app_key": "yyy" }).
328
+ * For single-key apiKey or http bearer, `apiKey` shorthand also works.
329
+ */
330
+ credentials?: Record<string, string>;
331
+ /** Extra context to encode in the OAuth state (e.g., tenant/user IDs for multi-tenant callbacks) */
332
+ stateContext?: Record<string, unknown>;
333
+ /** Additional scopes to request (e.g., optional scopes declared by the agent) */
334
+ scopes?: string[];
335
+ /**
336
+ * Opt out of proxy routing when the ref's source registry has
337
+ * `proxy: { mode: 'optional' }`. Ignored for `mode: 'required'`.
338
+ * Defaults to `false` — if a registry offers a proxy we use it.
339
+ */
340
+ preferLocal?: boolean;
341
+ },
342
+ ): Promise<AuthStartResult>;
288
343
  /**
289
344
  * Run the full OAuth flow locally: start auth, spin up a callback
290
345
  * server, open the browser, wait for the redirect, exchange tokens.
291
346
  * Resolves when auth is complete or times out.
292
347
  */
293
- authLocal(name: string, opts?: {
294
- /** Called with the authorize URL (e.g. to open in browser) */
295
- onAuthorizeUrl?: (url: string) => void;
296
- /** Timeout in ms (default 300_000 = 5 min) */
297
- timeoutMs?: number;
298
- }): Promise<{ complete: boolean }>;
348
+ authLocal(
349
+ name: string,
350
+ opts?: {
351
+ /** Called with the authorize URL (e.g. to open in browser) */
352
+ onAuthorizeUrl?: (url: string) => void;
353
+ /** Timeout in ms (default 300_000 = 5 min) */
354
+ timeoutMs?: number;
355
+ },
356
+ ): Promise<{ complete: boolean }>;
299
357
  /**
300
358
  * Refresh an OAuth access token using a stored refresh_token.
301
359
  * Returns the new access_token, or null if refresh is not possible
@@ -317,7 +375,11 @@ export interface Adk {
317
375
  * Parse the callback query params and pass them here.
318
376
  * @returns the ref name and whether auth is complete
319
377
  */
320
- handleCallback(params: { code: string; state: string }): Promise<{ refName: string; complete: boolean; stateContext?: Record<string, unknown> }>;
378
+ handleCallback(params: { code: string; state: string }): Promise<{
379
+ refName: string;
380
+ complete: boolean;
381
+ stateContext?: Record<string, unknown>;
382
+ }>;
321
383
  }
322
384
 
323
385
  // ============================================
@@ -379,9 +441,19 @@ async function decryptConfigSecrets(
379
441
  const result: Record<string, unknown> = {};
380
442
  for (const [key, value] of Object.entries(obj)) {
381
443
  if (typeof value === "string" && value.startsWith(SECRET_PREFIX)) {
382
- result[key] = await decryptSecret(value.slice(SECRET_PREFIX.length), encryptionKey);
383
- } else if (value !== null && typeof value === "object" && !Array.isArray(value)) {
384
- result[key] = await decryptConfigSecrets(value as Record<string, unknown>, encryptionKey);
444
+ result[key] = await decryptSecret(
445
+ value.slice(SECRET_PREFIX.length),
446
+ encryptionKey,
447
+ );
448
+ } else if (
449
+ value !== null &&
450
+ typeof value === "object" &&
451
+ !Array.isArray(value)
452
+ ) {
453
+ result[key] = await decryptConfigSecrets(
454
+ value as Record<string, unknown>,
455
+ encryptionKey,
456
+ );
385
457
  } else {
386
458
  result[key] = value;
387
459
  }
@@ -399,7 +471,7 @@ async function decryptConfigSecrets(
399
471
  * Fallback: _httpStatus from tool result body
400
472
  */
401
473
  function isUnauthorized(result: unknown): boolean {
402
- if (!result || typeof result !== 'object') return false;
474
+ if (!result || typeof result !== "object") return false;
403
475
  const r = result as Record<string, unknown>;
404
476
  // Primary: HTTP status forwarded by the registry and set by callRegistry
405
477
  if (r.httpStatus === 401) return true;
@@ -414,19 +486,29 @@ function isUnauthorized(result: unknown): boolean {
414
486
  // ============================================
415
487
 
416
488
  const esc = (s: string) =>
417
- s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
418
-
419
- function renderCredentialForm(name: string, fields: AuthChallengeField[], error?: string): string {
420
- const fieldHtml = fields.map((f) => `
489
+ s
490
+ .replace(/&/g, "&amp;")
491
+ .replace(/</g, "&lt;")
492
+ .replace(/>/g, "&gt;")
493
+ .replace(/"/g, "&quot;");
494
+
495
+ function renderCredentialForm(
496
+ name: string,
497
+ fields: AuthChallengeField[],
498
+ error?: string,
499
+ ): string {
500
+ const fieldHtml = fields
501
+ .map(
502
+ (f) => `
421
503
  <div class="field">
422
504
  <label for="${esc(f.name)}">${esc(f.label)}</label>
423
505
  ${f.description ? `<p class="desc">${esc(f.description)}</p>` : ""}
424
506
  <input id="${esc(f.name)}" name="${esc(f.name)}" type="${f.secret ? "password" : "text"}" required autocomplete="off" spellcheck="false" />
425
- </div>`).join("");
507
+ </div>`,
508
+ )
509
+ .join("");
426
510
 
427
- const errorHtml = error
428
- ? `<div class="error">${esc(error)}</div>`
429
- : "";
511
+ const errorHtml = error ? `<div class="error">${esc(error)}</div>` : "";
430
512
 
431
513
  return `<!DOCTYPE html>
432
514
  <html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
@@ -478,7 +560,6 @@ p{font-size:14px;color:#a3a3a3}
478
560
  }
479
561
 
480
562
  export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
481
-
482
563
  async function readConfig(): Promise<ConsumerConfig> {
483
564
  const content = await fs.readFile(CONFIG_PATH);
484
565
  if (!content) return {};
@@ -493,11 +574,115 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
493
574
  await fs.writeFile(CONFIG_PATH, JSON.stringify(config, null, 2));
494
575
  }
495
576
 
577
+ // -------------------------------------------------------------------------
578
+ // Registry cache helpers
579
+ //
580
+ // The cache is purely an internal optimization for the adk's read paths
581
+ // (`ref.list()`, `ref.get()`). Writes happen as side-effects of methods
582
+ // that already call the registry (`ref.add()`, `ref.inspect()`); the
583
+ // public surface never grows new methods. Cache failures (missing file,
584
+ // malformed JSON, fs errors during write) are swallowed so the registry
585
+ // cache can never break a registry operation.
586
+ // -------------------------------------------------------------------------
587
+
588
+ async function readRegistryCache(): Promise<RegistryCache> {
589
+ try {
590
+ const content = await fs.readFile(REGISTRY_CACHE_PATH);
591
+ if (!content) return { refs: {} };
592
+ const parsed = JSON.parse(content) as RegistryCache;
593
+ return { refs: parsed.refs ?? {} };
594
+ } catch {
595
+ return { refs: {} };
596
+ }
597
+ }
598
+
599
+ async function writeRegistryCache(cache: RegistryCache): Promise<void> {
600
+ try {
601
+ await fs.writeFile(REGISTRY_CACHE_PATH, JSON.stringify(cache, null, 2));
602
+ } catch {
603
+ // Best-effort. A failed cache write should never break the operation
604
+ // that triggered it.
605
+ }
606
+ }
607
+
608
+ /**
609
+ * Project an inspect/list response into the slim shape we cache. Drops
610
+ * `inputSchema` (too large) and `fullTokens` (registry-internal). Returns
611
+ * undefined if the response carries nothing worth caching.
612
+ */
613
+ function buildCacheEntry(
614
+ ref: string,
615
+ info:
616
+ | {
617
+ description?: string;
618
+ tools?: Array<{ name: string; description?: string }>;
619
+ toolSummaries?: Array<{ name: string; description?: string }>;
620
+ }
621
+ | null
622
+ | undefined,
623
+ ): RegistryCacheEntry | undefined {
624
+ if (!info) return undefined;
625
+ const toolSource = info.tools ?? info.toolSummaries;
626
+ const tools = toolSource?.map((t) => {
627
+ const slim: RegistryCacheToolSummary = { name: t.name };
628
+ if (t.description !== undefined) slim.description = t.description;
629
+ return slim;
630
+ });
631
+ if (info.description === undefined && (!tools || tools.length === 0)) {
632
+ return undefined;
633
+ }
634
+ const entry: RegistryCacheEntry = {
635
+ ref,
636
+ fetchedAt: new Date().toISOString(),
637
+ };
638
+ if (info.description !== undefined) entry.description = info.description;
639
+ if (tools && tools.length > 0) entry.tools = tools;
640
+ return entry;
641
+ }
642
+
643
+ async function upsertRegistryCacheEntry(
644
+ name: string,
645
+ entry: RegistryCacheEntry | undefined,
646
+ ): Promise<void> {
647
+ if (!entry) return;
648
+ const cache = await readRegistryCache();
649
+ cache.refs[name] = entry;
650
+ await writeRegistryCache(cache);
651
+ }
652
+
653
+ async function removeRegistryCacheEntry(name: string): Promise<void> {
654
+ const cache = await readRegistryCache();
655
+ if (!(name in cache.refs)) return;
656
+ delete cache.refs[name];
657
+ await writeRegistryCache(cache);
658
+ }
659
+
660
+ /**
661
+ * Hydrate a `ResolvedRef` with cached registry metadata when available.
662
+ * Pure: never mutates input. Leaves `description` / `tools` undefined when
663
+ * the cache has no entry, so callers can apply their own UX fallback.
664
+ */
665
+ function hydrateFromCache(
666
+ ref: ResolvedRef,
667
+ cache: RegistryCache,
668
+ ): ResolvedRef {
669
+ const cached = cache.refs[ref.name];
670
+ if (!cached) return ref;
671
+ const next: ResolvedRef = { ...ref };
672
+ if (cached.description !== undefined) next.description = cached.description;
673
+ if (cached.tools !== undefined) next.tools = cached.tools;
674
+ return next;
675
+ }
676
+
496
677
  /**
497
678
  * Store a secret value in a ref's config, encrypted if encryptionKey is set.
498
679
  * The value is stored inline as "secret:<encrypted>" in consumer-config.json.
499
680
  */
500
- async function storeRefSecret(name: string, key: string, value: string): Promise<void> {
681
+ async function storeRefSecret(
682
+ name: string,
683
+ key: string,
684
+ value: string,
685
+ ): Promise<void> {
501
686
  const stored = options.encryptionKey
502
687
  ? `${SECRET_PREFIX}${await encryptSecret(value, options.encryptionKey)}`
503
688
  : value;
@@ -510,18 +695,90 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
510
695
  await writeConfig({ ...config, refs });
511
696
  }
512
697
 
513
- async function readRefSecret(name: string, key: string): Promise<string | null> {
698
+ async function readRefSecret(
699
+ name: string,
700
+ key: string,
701
+ ): Promise<string | null> {
514
702
  const config = await readConfig();
515
703
  const entry = findRef(config.refs ?? [], name);
516
704
  const value = entry?.config?.[key];
517
705
  if (typeof value !== "string") return null;
518
706
  if (value.startsWith(SECRET_PREFIX) && options.encryptionKey) {
519
- return decryptSecret(value.slice(SECRET_PREFIX.length), options.encryptionKey);
707
+ return decryptSecret(
708
+ value.slice(SECRET_PREFIX.length),
709
+ options.encryptionKey,
710
+ );
520
711
  }
521
712
  return value;
522
713
  }
523
714
 
715
+ // ─────────────────────────────────────────────────────────────────────
716
+ // Credential resolution helpers
717
+ //
718
+ // Three callsites used to inline a `tryResolve`/`canResolve` closure
719
+ // (auth, authStatus, refreshToken) and two of them duplicated the
720
+ // client_id/client_secret lookup chain verbatim. Those chains MUST stay
721
+ // symmetric — if `auth` accepts a credential source, `refreshToken` has
722
+ // to read from the same source or refresh silently no-ops on every ref
723
+ // `auth` succeeded against. Centralising it here removes that drift
724
+ // risk.
725
+ // ─────────────────────────────────────────────────────────────────────
726
+
727
+ type OAuthServerMetadata = import("./mcp-client.js").OAuthServerMetadata;
728
+
729
+ interface CredentialResolverContext {
730
+ name: string;
731
+ entry: RefEntry;
732
+ security: SecuritySchemeSummary | null;
733
+ }
734
+
735
+ /**
736
+ * Build a `tryResolve(field, oauthMetadata?)` function bound to a
737
+ * specific ref + entry + security context. Wraps the host-injected
738
+ * `resolveCredentials` callback (e.g. atlas's env/static/tenant chain
739
+ * for first-party agents). Errors propagate to the caller.
740
+ */
741
+ function makeTryResolve(ctx: CredentialResolverContext) {
742
+ return async (
743
+ field: string,
744
+ oauthMetadata?: OAuthServerMetadata | null,
745
+ ): Promise<string | null> => {
746
+ const resolve = options.resolveCredentials;
747
+ if (!resolve) return null;
748
+ return resolve({
749
+ ref: ctx.name,
750
+ field,
751
+ entry: ctx.entry,
752
+ security: ctx.security,
753
+ oauthMetadata,
754
+ });
755
+ };
756
+ }
524
757
 
758
+ /**
759
+ * Resolve OAuth client credentials (client_id + client_secret) for a
760
+ * ref. Walks: `resolveCredentials` callback → per-ref VCS storage.
761
+ * Used by both `auth` (initial OAuth flow) and `refreshToken` (token
762
+ * refresh) — must be a single function so the two paths can never
763
+ * disagree about where credentials live.
764
+ *
765
+ * Returns null when no client_id is available anywhere; caller decides
766
+ * whether to attempt dynamic registration (`auth`) or bail (`refresh`).
767
+ */
768
+ async function resolveOAuthClient(
769
+ ctx: CredentialResolverContext & { metadata?: OAuthServerMetadata | null },
770
+ ): Promise<{ clientId: string; clientSecret?: string } | null> {
771
+ const tryResolve = makeTryResolve(ctx);
772
+ const clientId =
773
+ (await tryResolve("client_id", ctx.metadata)) ??
774
+ (await readRefSecret(ctx.name, "client_id"));
775
+ if (!clientId) return null;
776
+ const clientSecret =
777
+ (await tryResolve("client_secret", ctx.metadata)) ??
778
+ (await readRefSecret(ctx.name, "client_secret")) ??
779
+ undefined;
780
+ return { clientId, ...(clientSecret && { clientSecret }) };
781
+ }
525
782
 
526
783
  const PENDING_OAUTH_PATH = "pending-oauth.json";
527
784
 
@@ -535,23 +792,36 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
535
792
  createdAt: number;
536
793
  }
537
794
 
538
- async function readPendingOAuth(): Promise<Record<string, PendingOAuthState>> {
795
+ async function readPendingOAuth(): Promise<
796
+ Record<string, PendingOAuthState>
797
+ > {
539
798
  const content = await fs.readFile(PENDING_OAUTH_PATH);
540
799
  if (!content) return {};
541
- try { return JSON.parse(content); } catch { return {}; }
800
+ try {
801
+ return JSON.parse(content);
802
+ } catch {
803
+ return {};
804
+ }
542
805
  }
543
806
 
544
- async function writePendingOAuth(pending: Record<string, PendingOAuthState>): Promise<void> {
807
+ async function writePendingOAuth(
808
+ pending: Record<string, PendingOAuthState>,
809
+ ): Promise<void> {
545
810
  await fs.writeFile(PENDING_OAUTH_PATH, JSON.stringify(pending, null, 2));
546
811
  }
547
812
 
548
- async function storePendingOAuth(state: string, data: PendingOAuthState): Promise<void> {
813
+ async function storePendingOAuth(
814
+ state: string,
815
+ data: PendingOAuthState,
816
+ ): Promise<void> {
549
817
  const pending = await readPendingOAuth();
550
818
  pending[state] = data;
551
819
  await writePendingOAuth(pending);
552
820
  }
553
821
 
554
- async function consumePendingOAuth(state: string): Promise<PendingOAuthState | null> {
822
+ async function consumePendingOAuth(
823
+ state: string,
824
+ ): Promise<PendingOAuthState | null> {
555
825
  const pending = await readPendingOAuth();
556
826
  const data = pending[state] ?? null;
557
827
  if (data) {
@@ -580,7 +850,10 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
580
850
  let reqId = 0;
581
851
  let sessionId: string | undefined;
582
852
  async function rpc(method: string, rpcParams?: Record<string, unknown>) {
583
- const reqHeaders = { ...headers, ...(sessionId ? { "Mcp-Session-Id": sessionId } : {}) };
853
+ const reqHeaders = {
854
+ ...headers,
855
+ ...(sessionId ? { "Mcp-Session-Id": sessionId } : {}),
856
+ };
584
857
  const res = await globalThis.fetch(url, {
585
858
  method: "POST",
586
859
  headers: reqHeaders,
@@ -592,7 +865,9 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
592
865
  }),
593
866
  });
594
867
  if (!res.ok) {
595
- throw new Error(`MCP ${method} failed (${res.status}): ${await res.text().catch(() => "unknown")}`);
868
+ throw new Error(
869
+ `MCP ${method} failed (${res.status}): ${await res.text().catch(() => "unknown")}`,
870
+ );
596
871
  }
597
872
 
598
873
  const contentType = res.headers.get("content-type") ?? "";
@@ -610,18 +885,23 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
610
885
  try {
611
886
  const json = JSON.parse(line.slice(6));
612
887
  if (json.id === reqId) {
613
- if (json.error) throw new Error(`MCP RPC error: ${json.error.message}`);
888
+ if (json.error)
889
+ throw new Error(`MCP RPC error: ${json.error.message}`);
614
890
  return json.result;
615
891
  }
616
892
  } catch (e) {
617
- if (e instanceof Error && e.message.startsWith("MCP RPC")) throw e;
893
+ if (e instanceof Error && e.message.startsWith("MCP RPC"))
894
+ throw e;
618
895
  }
619
896
  }
620
897
  }
621
898
  return undefined;
622
899
  }
623
900
 
624
- const json = await res.json() as { result?: unknown; error?: { message: string } };
901
+ const json = (await res.json()) as {
902
+ result?: unknown;
903
+ error?: { message: string };
904
+ };
625
905
  if (json.error) throw new Error(`MCP RPC error: ${json.error.message}`);
626
906
  return json.result;
627
907
  }
@@ -634,17 +914,34 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
634
914
  });
635
915
  await rpc("notifications/initialized").catch(() => {});
636
916
 
637
- const result = await rpc("tools/call", { name: toolName, arguments: params }) as
638
- { content?: Array<{ type: string; text?: string }>; isError?: boolean };
917
+ const result = (await rpc("tools/call", {
918
+ name: toolName,
919
+ arguments: params,
920
+ })) as {
921
+ content?: Array<{ type: string; text?: string }>;
922
+ isError?: boolean;
923
+ };
639
924
 
640
925
  const textContent = result?.content?.find((c) => c.type === "text");
641
926
  if (textContent?.text) {
642
- try { return { success: true, result: JSON.parse(textContent.text) } as CallAgentResponse; }
643
- catch { return { success: true, result: textContent.text } as CallAgentResponse; }
927
+ try {
928
+ return {
929
+ success: true,
930
+ result: JSON.parse(textContent.text),
931
+ } as CallAgentResponse;
932
+ } catch {
933
+ return {
934
+ success: true,
935
+ result: textContent.text,
936
+ } as CallAgentResponse;
937
+ }
644
938
  }
645
939
  return { success: true, result } as CallAgentResponse;
646
940
  } catch (err) {
647
- return { success: false, error: err instanceof Error ? err.message : String(err) } as CallAgentResponse;
941
+ return {
942
+ success: false,
943
+ error: err instanceof Error ? err.message : String(err),
944
+ } as CallAgentResponse;
648
945
  }
649
946
  }
650
947
 
@@ -654,11 +951,13 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
654
951
  }
655
952
 
656
953
  /** Try fetching a URL directly as OAuth metadata (it may already be a discovery URL). */
657
- async function tryFetchOAuthMetadata(url: string): Promise<import("./mcp-client.js").OAuthServerMetadata | null> {
954
+ async function tryFetchOAuthMetadata(
955
+ url: string,
956
+ ): Promise<import("./mcp-client.js").OAuthServerMetadata | null> {
658
957
  try {
659
958
  const res = await globalThis.fetch(url);
660
959
  if (!res.ok) return null;
661
- const data = await res.json() as Record<string, unknown>;
960
+ const data = (await res.json()) as Record<string, unknown>;
662
961
  if (data.authorization_endpoint && data.token_endpoint) {
663
962
  return data as unknown as import("./mcp-client.js").OAuthServerMetadata;
664
963
  }
@@ -712,7 +1011,9 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
712
1011
  * Build a consumer that includes the ref's sourceRegistry if present.
713
1012
  * This ensures calls/inspect route to the correct registry endpoint.
714
1013
  */
715
- async function buildConsumerForRef(entry: RefEntry): Promise<RegistryConsumer> {
1014
+ async function buildConsumerForRef(
1015
+ entry: RefEntry,
1016
+ ): Promise<RegistryConsumer> {
716
1017
  const config = await readConfig();
717
1018
  let registries = config.registries ?? [];
718
1019
 
@@ -750,7 +1051,10 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
750
1051
  * Resolve the correct registry for a ref.
751
1052
  * If the ref has a sourceRegistry, use that; otherwise fall back to the first registry.
752
1053
  */
753
- function resolveRegistryForRef(consumer: RegistryConsumer, entry: RefEntry): ResolvedRegistry {
1054
+ function resolveRegistryForRef(
1055
+ consumer: RegistryConsumer,
1056
+ entry: RefEntry,
1057
+ ): ResolvedRegistry {
754
1058
  const regs = consumer.registries();
755
1059
  if (entry.sourceRegistry?.url) {
756
1060
  const match = regs.find((r) => r.url === entry.sourceRegistry!.url);
@@ -771,7 +1075,9 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
771
1075
  * the ref is sourced from a raw URL (no registry), in which case proxy routing
772
1076
  * does not apply.
773
1077
  */
774
- async function findRegistryEntryForRef(entry: RefEntry): Promise<RegistryEntry | null> {
1078
+ async function findRegistryEntryForRef(
1079
+ entry: RefEntry,
1080
+ ): Promise<RegistryEntry | null> {
775
1081
  const sourceUrl = entry.sourceRegistry?.url;
776
1082
  if (!sourceUrl) return null;
777
1083
  const config = await readConfig();
@@ -824,7 +1130,10 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
824
1130
  sourceRegistry: { url: reg.url, agentPath: agent },
825
1131
  });
826
1132
  const resolved = consumer.registries().find((r) => r.url === reg.url);
827
- if (!resolved) throw new Error(`Registry ${reg.url} not resolvable for proxy forwarding`);
1133
+ if (!resolved)
1134
+ throw new Error(
1135
+ `Registry ${reg.url} not resolvable for proxy forwarding`,
1136
+ );
828
1137
 
829
1138
  const response = await consumer.callRegistry(resolved, {
830
1139
  action: "execute_tool",
@@ -834,8 +1143,13 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
834
1143
  });
835
1144
 
836
1145
  if (!response.success) {
837
- const errResponse = response as { success: false; error?: string; code?: string };
838
- const msg = errResponse.error ?? `Proxy ${agent}.ref(${operation}) failed`;
1146
+ const errResponse = response as {
1147
+ success: false;
1148
+ error?: string;
1149
+ code?: string;
1150
+ };
1151
+ const msg =
1152
+ errResponse.error ?? `Proxy ${agent}.ref(${operation}) failed`;
839
1153
  throw new Error(msg);
840
1154
  }
841
1155
  return (response as { success: true; result: unknown }).result as T;
@@ -904,7 +1218,8 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
904
1218
  const rName = registryDisplayName(r);
905
1219
  if (rName !== nameOrUrl && registryUrl(r) !== nameOrUrl) return r;
906
1220
  found = true;
907
- const existing: RegistryEntry = typeof r === "string" ? { url: r } : { ...r };
1221
+ const existing: RegistryEntry =
1222
+ typeof r === "string" ? { url: r } : { ...r };
908
1223
  mutate(existing);
909
1224
  return existing;
910
1225
  });
@@ -917,11 +1232,16 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
917
1232
  * Decrypt a `secret:`-prefixed value if we hold the encryption key. Plaintext
918
1233
  * values pass through unchanged so dev configs keep working.
919
1234
  */
920
- async function revealSecret(value: string | undefined): Promise<string | undefined> {
1235
+ async function revealSecret(
1236
+ value: string | undefined,
1237
+ ): Promise<string | undefined> {
921
1238
  if (!value) return value;
922
1239
  if (!value.startsWith(SECRET_PREFIX)) return value;
923
1240
  if (!options.encryptionKey) return undefined;
924
- return decryptSecret(value.slice(SECRET_PREFIX.length), options.encryptionKey);
1241
+ return decryptSecret(
1242
+ value.slice(SECRET_PREFIX.length),
1243
+ options.encryptionKey,
1244
+ );
925
1245
  }
926
1246
 
927
1247
  /**
@@ -936,7 +1256,8 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
936
1256
  const target = findRegistry(config.registries ?? [], nameOrUrl);
937
1257
  if (!target || typeof target === "string") return false;
938
1258
  const oauth = target.oauth;
939
- if (!oauth?.refreshToken || !oauth.tokenEndpoint || !oauth.clientId) return false;
1259
+ if (!oauth?.refreshToken || !oauth.tokenEndpoint || !oauth.clientId)
1260
+ return false;
940
1261
 
941
1262
  const refreshToken = await revealSecret(oauth.refreshToken);
942
1263
  const clientSecret = await revealSecret(oauth.clientSecret);
@@ -978,7 +1299,8 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
978
1299
  try {
979
1300
  return await fn();
980
1301
  } catch (err) {
981
- if (!(err instanceof AdkError) || err.code !== "registry_auth_required") throw err;
1302
+ if (!(err instanceof AdkError) || err.code !== "registry_auth_required")
1303
+ throw err;
982
1304
  let refreshed = false;
983
1305
  try {
984
1306
  refreshed = await refreshRegistryToken(nameOrUrl);
@@ -1022,7 +1344,9 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
1022
1344
  }
1023
1345
 
1024
1346
  const registry: AdkRegistryApi = {
1025
- async add(entry: RegistryEntry): Promise<{ authRequirement?: RegistryAuthRequirement }> {
1347
+ async add(
1348
+ entry: RegistryEntry,
1349
+ ): Promise<{ authRequirement?: RegistryAuthRequirement }> {
1026
1350
  const config = await readConfig();
1027
1351
  const alias = entry.name ?? entry.url;
1028
1352
  const registries = (config.registries ?? []).filter(
@@ -1069,7 +1393,9 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
1069
1393
  ...final,
1070
1394
  proxy: {
1071
1395
  mode: discovered.proxy.mode,
1072
- ...(discovered.proxy.agent && { agent: discovered.proxy.agent }),
1396
+ ...(discovered.proxy.agent && {
1397
+ agent: discovered.proxy.agent,
1398
+ }),
1073
1399
  },
1074
1400
  };
1075
1401
  }
@@ -1090,7 +1416,8 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
1090
1416
  if (!config.registries?.length) return false;
1091
1417
  const before = config.registries.length;
1092
1418
  const registries = config.registries.filter(
1093
- (r) => registryDisplayName(r) !== nameOrUrl && registryUrl(r) !== nameOrUrl,
1419
+ (r) =>
1420
+ registryDisplayName(r) !== nameOrUrl && registryUrl(r) !== nameOrUrl,
1094
1421
  );
1095
1422
  if (registries.length === before) return false;
1096
1423
  await writeConfig({ ...config, registries });
@@ -1111,7 +1438,10 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
1111
1438
  return typeof target === "string" ? { url: target } : target;
1112
1439
  },
1113
1440
 
1114
- async update(name: string, updates: Partial<RegistryEntry>): Promise<boolean> {
1441
+ async update(
1442
+ name: string,
1443
+ updates: Partial<RegistryEntry>,
1444
+ ): Promise<boolean> {
1115
1445
  const config = await readConfig();
1116
1446
  if (!config.registries?.length) return false;
1117
1447
  let found = false;
@@ -1119,11 +1449,13 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
1119
1449
  const rName = registryDisplayName(r);
1120
1450
  if (rName !== name && registryUrl(r) !== name) return r;
1121
1451
  found = true;
1122
- const existing: RegistryEntry = typeof r === "string" ? { url: r } : { ...r };
1452
+ const existing: RegistryEntry =
1453
+ typeof r === "string" ? { url: r } : { ...r };
1123
1454
  if (updates.url) existing.url = updates.url;
1124
1455
  if (updates.name) existing.name = updates.name;
1125
1456
  if (updates.auth) existing.auth = updates.auth;
1126
- if (updates.headers) existing.headers = { ...existing.headers, ...updates.headers };
1457
+ if (updates.headers)
1458
+ existing.headers = { ...existing.headers, ...updates.headers };
1127
1459
  if (updates.proxy !== undefined) existing.proxy = updates.proxy;
1128
1460
  return existing;
1129
1461
  });
@@ -1135,7 +1467,8 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
1135
1467
  async browse(name: string, query?: string): Promise<AgentListEntry[]> {
1136
1468
  const config = await readConfig();
1137
1469
  const target = findRegistry(config.registries ?? [], name);
1138
- if (target && typeof target !== "string") assertRegistryAuthorized(target);
1470
+ if (target && typeof target !== "string")
1471
+ assertRegistryAuthorized(target);
1139
1472
  return callWithRefresh(name, async () => {
1140
1473
  const consumer = await buildConsumer(name);
1141
1474
  const url = target ? registryUrl(target) : name;
@@ -1146,7 +1479,8 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
1146
1479
  async inspect(name: string): Promise<RegistryConfiguration> {
1147
1480
  const config = await readConfig();
1148
1481
  const target = findRegistry(config.registries ?? [], name);
1149
- if (target && typeof target !== "string") assertRegistryAuthorized(target);
1482
+ if (target && typeof target !== "string")
1483
+ assertRegistryAuthorized(target);
1150
1484
  return callWithRefresh(name, async () => {
1151
1485
  const consumer = await buildConsumer(name);
1152
1486
  const url = target ? registryUrl(target) : name;
@@ -1158,7 +1492,9 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
1158
1492
  const config = await readConfig();
1159
1493
  const registries = config.registries ?? [];
1160
1494
  const targets = name
1161
- ? registries.filter((r) => registryDisplayName(r) === name || registryUrl(r) === name)
1495
+ ? registries.filter(
1496
+ (r) => registryDisplayName(r) === name || registryUrl(r) === name,
1497
+ )
1162
1498
  : registries;
1163
1499
 
1164
1500
  const results = await Promise.allSettled(
@@ -1199,7 +1535,12 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
1199
1535
  return results.map((r) =>
1200
1536
  r.status === "fulfilled"
1201
1537
  ? r.value
1202
- : { name: "unknown", url: "unknown", status: "error" as const, error: "unknown" },
1538
+ : {
1539
+ name: "unknown",
1540
+ url: "unknown",
1541
+ status: "error" as const,
1542
+ error: "unknown",
1543
+ },
1203
1544
  );
1204
1545
  },
1205
1546
 
@@ -1271,7 +1612,10 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
1271
1612
  });
1272
1613
  // Re-read so the flow below sees the fresh requirement.
1273
1614
  const refreshed = await readConfig();
1274
- const refreshedTarget = findRegistry(refreshed.registries ?? [], nameOrUrl);
1615
+ const refreshedTarget = findRegistry(
1616
+ refreshed.registries ?? [],
1617
+ nameOrUrl,
1618
+ );
1275
1619
  if (refreshedTarget && typeof refreshedTarget !== "string") {
1276
1620
  Object.assign(target, refreshedTarget);
1277
1621
  }
@@ -1337,17 +1681,21 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
1337
1681
  );
1338
1682
 
1339
1683
  const state = crypto.randomUUID();
1340
- const { url: authorizeUrl, codeVerifier } = await buildOAuthAuthorizeUrl({
1341
- authorizationEndpoint: metadata.authorization_endpoint,
1342
- clientId: registration.clientId,
1343
- redirectUri,
1344
- scopes: req.scopes,
1345
- state,
1346
- });
1684
+ const { url: authorizeUrl, codeVerifier } =
1685
+ await buildOAuthAuthorizeUrl({
1686
+ authorizationEndpoint: metadata.authorization_endpoint,
1687
+ clientId: registration.clientId,
1688
+ redirectUri,
1689
+ scopes: req.scopes,
1690
+ state,
1691
+ });
1347
1692
 
1348
1693
  return new Promise<{ complete: boolean }>((resolve, reject) => {
1349
1694
  const server = createServer(async (reqIn, resOut) => {
1350
- const reqUrl = new URL(reqIn.url ?? "/", `http://localhost:${port}`);
1695
+ const reqUrl = new URL(
1696
+ reqIn.url ?? "/",
1697
+ `http://localhost:${port}`,
1698
+ );
1351
1699
  if (reqUrl.pathname !== "/callback") {
1352
1700
  resOut.writeHead(404);
1353
1701
  resOut.end();
@@ -1357,7 +1705,8 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
1357
1705
  const code = reqUrl.searchParams.get("code");
1358
1706
  const returnedState = reqUrl.searchParams.get("state");
1359
1707
  if (!code || returnedState !== state) {
1360
- const error = reqUrl.searchParams.get("error") ?? "missing code/state";
1708
+ const error =
1709
+ reqUrl.searchParams.get("error") ?? "missing code/state";
1361
1710
  resOut.writeHead(400, { "Content-Type": "text/html" });
1362
1711
  resOut.end(`<h1>Error</h1><p>${esc(error)}</p>`);
1363
1712
  server.close();
@@ -1373,13 +1722,16 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
1373
1722
  }
1374
1723
 
1375
1724
  try {
1376
- const tokens = await exchangeCodeForTokens(metadata.token_endpoint, {
1377
- code,
1378
- codeVerifier,
1379
- clientId: registration.clientId,
1380
- clientSecret: registration.clientSecret,
1381
- redirectUri,
1382
- });
1725
+ const tokens = await exchangeCodeForTokens(
1726
+ metadata.token_endpoint,
1727
+ {
1728
+ code,
1729
+ codeVerifier,
1730
+ clientId: registration.clientId,
1731
+ clientSecret: registration.clientSecret,
1732
+ redirectUri,
1733
+ },
1734
+ );
1383
1735
  const expiresAt = tokens.expiresIn
1384
1736
  ? new Date(Date.now() + tokens.expiresIn * 1000).toISOString()
1385
1737
  : undefined;
@@ -1461,7 +1813,9 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
1461
1813
  const token = params.get("token");
1462
1814
  if (!token) {
1463
1815
  resOut.writeHead(200, { "Content-Type": "text/html" });
1464
- resOut.end(renderCredentialForm(displayName, fields, "Token is required."));
1816
+ resOut.end(
1817
+ renderCredentialForm(displayName, fields, "Token is required."),
1818
+ );
1465
1819
  return;
1466
1820
  }
1467
1821
  try {
@@ -1505,7 +1859,9 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
1505
1859
  // ==========================================
1506
1860
 
1507
1861
  const ref: AdkRefApi = {
1508
- async add(entryInput: RefAddInput): Promise<{ security: SecuritySchemeSummary | null }> {
1862
+ async add(
1863
+ entryInput: RefAddInput,
1864
+ ): Promise<{ security: SecuritySchemeSummary | null }> {
1509
1865
  let security: SecuritySchemeSummary | null = null;
1510
1866
 
1511
1867
  const config = await readConfig();
@@ -1527,7 +1883,10 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
1527
1883
  if (entry.sourceRegistry?.url) {
1528
1884
  entry = { ...entry, scheme: "registry" };
1529
1885
  } else if (entry.url) {
1530
- entry = { ...entry, scheme: entry.url.startsWith("http") ? "https" : "mcp" };
1886
+ entry = {
1887
+ ...entry,
1888
+ scheme: entry.url.startsWith("http") ? "https" : "mcp",
1889
+ };
1531
1890
  } else {
1532
1891
  throw new AdkError({
1533
1892
  code: "REF_INVALID",
@@ -1557,6 +1916,7 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
1557
1916
  });
1558
1917
  }
1559
1918
 
1919
+ let cacheEntry: RegistryCacheEntry | undefined;
1560
1920
  if (hasRegistries || entry.sourceRegistry?.url) {
1561
1921
  try {
1562
1922
  const consumer = await buildConsumerForRef(entry);
@@ -1565,11 +1925,11 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
1565
1925
 
1566
1926
  const requiresValidation = !!entry.sourceRegistry;
1567
1927
  if (requiresValidation) {
1568
- const hasContent = info && (
1569
- info.description ||
1570
- (info.tools && info.tools.length > 0) ||
1571
- (info.toolSummaries && info.toolSummaries.length > 0)
1572
- );
1928
+ const hasContent =
1929
+ info &&
1930
+ (info.description ||
1931
+ (info.tools && info.tools.length > 0) ||
1932
+ (info.toolSummaries && info.toolSummaries.length > 0));
1573
1933
  if (!hasContent) {
1574
1934
  // Inspect returned empty — fall back to browse to check if agent exists
1575
1935
  const registryUrl = entry.sourceRegistry?.url;
@@ -1578,7 +1938,9 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
1578
1938
  try {
1579
1939
  const agents = await consumer.browse(registryUrl);
1580
1940
  const stripAt = (s: string) => s.replace(/^@/, "");
1581
- const refKey = stripAt(entry.sourceRegistry?.agentPath ?? entry.ref);
1941
+ const refKey = stripAt(
1942
+ entry.sourceRegistry?.agentPath ?? entry.ref,
1943
+ );
1582
1944
  foundInBrowse = agents.some(
1583
1945
  (a) => a.path === entry.ref || stripAt(a.path) === refKey,
1584
1946
  );
@@ -1592,7 +1954,11 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
1592
1954
  code: "REF_NOT_FOUND",
1593
1955
  message: `Agent "${entry.ref}" not found on ${registryHint}`,
1594
1956
  hint: "Check available agents with: adk registry browse",
1595
- details: { ref: entry.ref, sourceRegistry: entry.sourceRegistry, scheme: entry.scheme },
1957
+ details: {
1958
+ ref: entry.ref,
1959
+ sourceRegistry: entry.sourceRegistry,
1960
+ scheme: entry.scheme,
1961
+ },
1596
1962
  });
1597
1963
  }
1598
1964
  }
@@ -1601,17 +1967,22 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
1601
1967
  if (info?.security) security = info.security;
1602
1968
  const agentMode = (info as any)?.mode;
1603
1969
  if (agentMode) (entry as any).mode = agentMode;
1604
- if (info?.upstream && !entry.url && agentMode !== 'api') {
1970
+ if (info?.upstream && !entry.url && agentMode !== "api") {
1605
1971
  entry.url = info.upstream as string;
1606
1972
  entry.scheme = entry.scheme ?? "mcp";
1607
1973
  }
1974
+
1975
+ cacheEntry = buildCacheEntry(entry.ref, info);
1608
1976
  } catch (err) {
1609
1977
  if (err instanceof AdkError) throw err;
1610
1978
  throw new AdkError({
1611
1979
  code: "REGISTRY_UNREACHABLE",
1612
1980
  message: `Could not reach registry to validate "${entry.ref}"`,
1613
1981
  hint: "Check your registry connection with: adk registry test",
1614
- details: { ref: entry.ref, error: err instanceof Error ? err.message : String(err) },
1982
+ details: {
1983
+ ref: entry.ref,
1984
+ error: err instanceof Error ? err.message : String(err),
1985
+ },
1615
1986
  cause: err,
1616
1987
  });
1617
1988
  }
@@ -1619,6 +1990,7 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
1619
1990
 
1620
1991
  const refs = [...(config.refs ?? []), entry];
1621
1992
  await writeConfig({ ...config, refs });
1993
+ await upsertRegistryCacheEntry(name, cacheEntry);
1622
1994
 
1623
1995
  return { security };
1624
1996
  },
@@ -1630,17 +2002,28 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
1630
2002
  const refs = config.refs.filter((r) => !refNameMatches(r, name));
1631
2003
  if (refs.length === before) return false;
1632
2004
  await writeConfig({ ...config, refs });
2005
+ await removeRegistryCacheEntry(name);
1633
2006
  return true;
1634
2007
  },
1635
2008
 
1636
2009
  async list(): Promise<ResolvedRef[]> {
1637
- const config = await readConfig();
1638
- return (config.refs ?? []).map(normalizeRef);
2010
+ const [config, cache] = await Promise.all([
2011
+ readConfig(),
2012
+ readRegistryCache(),
2013
+ ]);
2014
+ return (config.refs ?? [])
2015
+ .map(normalizeRef)
2016
+ .map((r) => hydrateFromCache(r, cache));
1639
2017
  },
1640
2018
 
1641
2019
  async get(name: string): Promise<ResolvedRef | null> {
1642
- const config = await readConfig();
1643
- return findRef(config.refs ?? [], name) ?? null;
2020
+ const [config, cache] = await Promise.all([
2021
+ readConfig(),
2022
+ readRegistryCache(),
2023
+ ]);
2024
+ const found = findRef(config.refs ?? [], name);
2025
+ if (!found) return null;
2026
+ return hydrateFromCache(found, cache);
1644
2027
  },
1645
2028
 
1646
2029
  async update(name: string, updates: Partial<RefEntry>): Promise<boolean> {
@@ -1669,8 +2052,10 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
1669
2052
  updated.name = updates.name;
1670
2053
  }
1671
2054
  if (updates.scheme) updated.scheme = updates.scheme;
1672
- if (updates.config) updated.config = { ...updated.config, ...updates.config };
1673
- if (updates.sourceRegistry) updated.sourceRegistry = updates.sourceRegistry;
2055
+ if (updates.config)
2056
+ updated.config = { ...updated.config, ...updates.config };
2057
+ if (updates.sourceRegistry)
2058
+ updated.sourceRegistry = updates.sourceRegistry;
1674
2059
  return updated;
1675
2060
  });
1676
2061
  if (!found) return false;
@@ -1678,38 +2063,63 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
1678
2063
  return true;
1679
2064
  },
1680
2065
 
1681
- async inspect(name: string, opts?: { full?: boolean }): Promise<AgentInspection | null> {
2066
+ async inspect(
2067
+ name: string,
2068
+ opts?: { full?: boolean },
2069
+ ): Promise<AgentInspection | null> {
1682
2070
  const config = await readConfig();
1683
2071
  const entry = findRef(config.refs ?? [], name);
1684
2072
  if (!entry) throw new Error(`Ref "${name}" not found`);
1685
2073
 
1686
2074
  const consumer = await buildConsumerForRef(entry);
1687
- return consumer.inspect(
2075
+ const result = await consumer.inspect(
1688
2076
  entry.sourceRegistry?.agentPath ?? entry.ref,
1689
2077
  entry.sourceRegistry?.url,
1690
2078
  opts,
1691
2079
  );
2080
+
2081
+ // Side-effect: refresh the registry cache so subsequent ref.list()
2082
+ // / ref.get() calls see the latest description and tool summaries
2083
+ // without another network round-trip. Strips inputSchema (caller's
2084
+ // `result` is unaffected — it still carries the full data).
2085
+ await upsertRegistryCacheEntry(name, buildCacheEntry(entry.ref, result));
2086
+
2087
+ return result;
1692
2088
  },
1693
2089
 
1694
- async call(name: string, tool: string, params?: Record<string, unknown>): Promise<CallAgentResponse> {
2090
+ async call(
2091
+ name: string,
2092
+ tool: string,
2093
+ params?: Record<string, unknown>,
2094
+ ): Promise<CallAgentResponse> {
1695
2095
  const config = await readConfig();
1696
2096
  const entry = findRef(config.refs ?? [], name);
1697
2097
  if (!entry) throw new Error(`Ref "${name}" not found`);
1698
2098
 
1699
- let accessToken = await readRefSecret(name, "access_token")
1700
- ?? await readRefSecret(name, "api_key")
1701
- ?? await readRefSecret(name, "token");
2099
+ const accessToken =
2100
+ (await readRefSecret(name, "access_token")) ??
2101
+ (await readRefSecret(name, "api_key")) ??
2102
+ (await readRefSecret(name, "token"));
1702
2103
 
1703
2104
  // Resolve custom headers from config (e.g. { "X-API-Key": "secret:..." })
1704
2105
  const refConfig = (entry.config ?? {}) as Record<string, unknown>;
1705
- const rawHeaders = refConfig.headers as Record<string, string> | undefined;
2106
+ const rawHeaders = refConfig.headers as
2107
+ | Record<string, string>
2108
+ | undefined;
1706
2109
  let resolvedHeaders: Record<string, string> | undefined;
1707
- if (rawHeaders && typeof rawHeaders === 'object') {
2110
+ if (rawHeaders && typeof rawHeaders === "object") {
1708
2111
  resolvedHeaders = {};
1709
2112
  for (const [k, v] of Object.entries(rawHeaders)) {
1710
- if (typeof v === 'string' && v.startsWith(SECRET_PREFIX) && options.encryptionKey) {
1711
- resolvedHeaders[k] = await decryptSecret(v.slice(SECRET_PREFIX.length), options.encryptionKey);
1712
- } else if (typeof v === 'string') {
2113
+ if (
2114
+ typeof v === "string" &&
2115
+ v.startsWith(SECRET_PREFIX) &&
2116
+ options.encryptionKey
2117
+ ) {
2118
+ resolvedHeaders[k] = await decryptSecret(
2119
+ v.slice(SECRET_PREFIX.length),
2120
+ options.encryptionKey,
2121
+ );
2122
+ } else if (typeof v === "string") {
1713
2123
  resolvedHeaders[k] = v;
1714
2124
  }
1715
2125
  }
@@ -1718,9 +2128,15 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
1718
2128
  const doCall = async (token: string | null) => {
1719
2129
  // Direct MCP only for redirect/proxy agents with an MCP upstream.
1720
2130
  // API-mode agents must go through the registry (it does REST translation).
1721
- const agentMode = (entry as any).mode ?? 'redirect';
1722
- if (token && entry.url && agentMode !== 'api') {
1723
- return callMcpDirect(entry.url, tool, params ?? {}, token, resolvedHeaders);
2131
+ const agentMode = (entry as any).mode ?? "redirect";
2132
+ if (token && entry.url && agentMode !== "api") {
2133
+ return callMcpDirect(
2134
+ entry.url,
2135
+ tool,
2136
+ params ?? {},
2137
+ token,
2138
+ resolvedHeaders,
2139
+ );
1724
2140
  }
1725
2141
 
1726
2142
  const consumer = await buildConsumerForRef(entry);
@@ -1789,13 +2205,20 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
1789
2205
  // server-side so local inspection would always return "missing").
1790
2206
  const proxy = await resolveProxyForRef(entry);
1791
2207
  if (proxy) {
1792
- return forwardRefOpToProxy<RefAuthStatus>(proxy.reg, proxy.agent, "auth-status", { name });
2208
+ return forwardRefOpToProxy<RefAuthStatus>(
2209
+ proxy.reg,
2210
+ proxy.agent,
2211
+ "auth-status",
2212
+ { name },
2213
+ );
1793
2214
  }
1794
2215
 
1795
2216
  let security: SecuritySchemeSummary | null = null;
1796
2217
  try {
1797
2218
  const consumer = await buildConsumerForRef(entry);
1798
- const info = await consumer.inspect(entry.sourceRegistry?.agentPath ?? entry.ref);
2219
+ const info = await consumer.inspect(
2220
+ entry.sourceRegistry?.agentPath ?? entry.ref,
2221
+ );
1799
2222
  if (info?.security) security = info.security;
1800
2223
  } catch {
1801
2224
  // Can't reach registry
@@ -1806,12 +2229,12 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
1806
2229
  }
1807
2230
 
1808
2231
  const configKeys = Object.keys(entry.config ?? {});
1809
- const resolve = options.resolveCredentials;
1810
-
1811
- async function canResolve(field: string, oauthMetadata?: import("./mcp-client.js").OAuthServerMetadata | null): Promise<boolean> {
1812
- if (!resolve || !entry) return false;
1813
- const val = await resolve({ ref: name, field, entry, security, oauthMetadata });
1814
- return val !== null;
2232
+ const tryResolveField = makeTryResolve({ name, entry, security });
2233
+ async function canResolve(
2234
+ field: string,
2235
+ oauthMetadata?: OAuthServerMetadata | null,
2236
+ ): Promise<boolean> {
2237
+ return (await tryResolveField(field, oauthMetadata)) !== null;
1815
2238
  }
1816
2239
 
1817
2240
  const fields: Record<string, CredentialField> = {};
@@ -1823,12 +2246,15 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
1823
2246
  };
1824
2247
  const hasRegistration = !!securityExt.dynamicRegistration;
1825
2248
 
1826
- let oauthMetadata: import("./mcp-client.js").OAuthServerMetadata | null = null;
2249
+ let oauthMetadata:
2250
+ | import("./mcp-client.js").OAuthServerMetadata
2251
+ | null = null;
1827
2252
  let needsSecret = false;
1828
2253
  if (securityExt.discoveryUrl) {
1829
2254
  oauthMetadata = await tryFetchOAuthMetadata(securityExt.discoveryUrl);
1830
2255
  if (oauthMetadata) {
1831
- const authMethods = oauthMetadata.token_endpoint_auth_methods_supported ?? [];
2256
+ const authMethods =
2257
+ oauthMetadata.token_endpoint_auth_methods_supported ?? [];
1832
2258
  needsSecret = !authMethods.includes("none");
1833
2259
  }
1834
2260
  }
@@ -1855,15 +2281,22 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
1855
2281
  };
1856
2282
  } else if (security.type === "apiKey") {
1857
2283
  const apiKeySec = security as {
1858
- name?: string; headers?: Record<string, { description?: string }>;
2284
+ name?: string;
2285
+ headers?: Record<string, { description?: string }>;
1859
2286
  };
1860
2287
  const toStorageKey = (headerName: string) =>
1861
- headerName.toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_|_$/g, "");
2288
+ headerName
2289
+ .toLowerCase()
2290
+ .replace(/[^a-z0-9]+/g, "_")
2291
+ .replace(/^_|_$/g, "");
1862
2292
 
1863
2293
  // config.headers: { "Header-Name": "value" } — check by header name (case-insensitive)
1864
- const configHeaders = (entry?.config as Record<string, unknown> | undefined)?.headers as
1865
- Record<string, unknown> | undefined;
1866
- const configHeaderKeys = configHeaders ? Object.keys(configHeaders) : [];
2294
+ const configHeaders = (
2295
+ entry?.config as Record<string, unknown> | undefined
2296
+ )?.headers as Record<string, unknown> | undefined;
2297
+ const configHeaderKeys = configHeaders
2298
+ ? Object.keys(configHeaders)
2299
+ : [];
1867
2300
  const hasConfigHeader = (name: string) =>
1868
2301
  configHeaderKeys.some((k) => k.toLowerCase() === name.toLowerCase());
1869
2302
 
@@ -1877,7 +2310,8 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
1877
2310
  for (const headerName of declaredHeaders) {
1878
2311
  const storageKey = toStorageKey(headerName);
1879
2312
  const inConfigHeaders = hasConfigHeader(headerName);
1880
- const inLegacyKeys = configKeys.includes(storageKey) || configKeys.includes("api_key");
2313
+ const inLegacyKeys =
2314
+ configKeys.includes(storageKey) || configKeys.includes("api_key");
1881
2315
  fields[storageKey] = {
1882
2316
  required: true,
1883
2317
  automated: false,
@@ -1911,19 +2345,22 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
1911
2345
  return { name, security, complete, fields };
1912
2346
  },
1913
2347
 
1914
- async auth(name: string, opts?: {
1915
- apiKey?: string;
1916
- credentials?: Record<string, string>;
1917
- /** Extra context to encode in the OAuth state (e.g., tenant/user IDs for multi-tenant callbacks) */
1918
- stateContext?: Record<string, unknown>;
1919
- /** Additional scopes to request (e.g., optional scopes declared by the agent) */
1920
- scopes?: string[];
1921
- /**
1922
- * Opt out of proxy routing when the ref's source registry has
1923
- * `proxy: { mode: 'optional' }`. Ignored for `mode: 'required'`.
1924
- */
1925
- preferLocal?: boolean;
1926
- }): Promise<AuthStartResult> {
2348
+ async auth(
2349
+ name: string,
2350
+ opts?: {
2351
+ apiKey?: string;
2352
+ credentials?: Record<string, string>;
2353
+ /** Extra context to encode in the OAuth state (e.g., tenant/user IDs for multi-tenant callbacks) */
2354
+ stateContext?: Record<string, unknown>;
2355
+ /** Additional scopes to request (e.g., optional scopes declared by the agent) */
2356
+ scopes?: string[];
2357
+ /**
2358
+ * Opt out of proxy routing when the ref's source registry has
2359
+ * `proxy: { mode: 'optional' }`. Ignored for `mode: 'required'`.
2360
+ */
2361
+ preferLocal?: boolean;
2362
+ },
2363
+ ): Promise<AuthStartResult> {
1927
2364
  const config = await readConfig();
1928
2365
  const entry = findRef(config.refs ?? [], name);
1929
2366
  if (!entry) throw new Error(`Ref "${name}" not found`);
@@ -1932,24 +2369,26 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
1932
2369
  // agent. The registry owns the client_id/secret and returns an authorize
1933
2370
  // URL pointing at the registry's OAuth callback domain, so the user
1934
2371
  // completes the flow against the registry instead of localhost.
1935
- const proxy = await resolveProxyForRef(entry, { preferLocal: opts?.preferLocal });
2372
+ const proxy = await resolveProxyForRef(entry, {
2373
+ preferLocal: opts?.preferLocal,
2374
+ });
1936
2375
  if (proxy) {
1937
2376
  const params: Record<string, unknown> = { name };
1938
2377
  if (opts?.apiKey !== undefined) params.apiKey = opts.apiKey;
1939
2378
  if (opts?.credentials) params.credentials = opts.credentials;
1940
2379
  if (opts?.scopes) params.scopes = opts.scopes;
1941
2380
  if (opts?.stateContext) params.stateContext = opts.stateContext;
1942
- return forwardRefOpToProxy<AuthStartResult>(proxy.reg, proxy.agent, "auth", params);
2381
+ return forwardRefOpToProxy<AuthStartResult>(
2382
+ proxy.reg,
2383
+ proxy.agent,
2384
+ "auth",
2385
+ params,
2386
+ );
1943
2387
  }
1944
2388
 
1945
2389
  const status = await ref.authStatus(name);
1946
2390
  const security = status.security;
1947
- const resolve = options.resolveCredentials;
1948
-
1949
- async function tryResolve(field: string, oauthMetadata?: import("./mcp-client.js").OAuthServerMetadata | null): Promise<string | null> {
1950
- if (!resolve) return null;
1951
- return resolve({ ref: name, field, entry: entry!, security, oauthMetadata });
1952
- }
2391
+ const tryResolve = makeTryResolve({ name, entry, security });
1953
2392
 
1954
2393
  if (!security || security.type === "none") {
1955
2394
  return { type: "none", complete: true };
@@ -1957,20 +2396,31 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
1957
2396
 
1958
2397
  if (security.type === "apiKey") {
1959
2398
  const apiKeySec = security as {
1960
- name?: string; prefix?: string;
2399
+ name?: string;
2400
+ prefix?: string;
1961
2401
  headers?: Record<string, { description?: string }>;
1962
2402
  };
1963
2403
 
1964
2404
  const toStorageKey = (headerName: string) =>
1965
- headerName.toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_|_$/g, "");
2405
+ headerName
2406
+ .toLowerCase()
2407
+ .replace(/[^a-z0-9]+/g, "_")
2408
+ .replace(/^_|_$/g, "");
1966
2409
 
1967
2410
  // Check existing config.headers
1968
- const existingHeaders = ((entry.config ?? {}) as Record<string, unknown>).headers as
1969
- Record<string, string> | undefined;
2411
+ const existingHeaders = (
2412
+ (entry.config ?? {}) as Record<string, unknown>
2413
+ ).headers as Record<string, string> | undefined;
1970
2414
 
1971
2415
  // Collect declared headers: from security.headers or security.name
1972
- const declaredHeaders: Array<{ headerName: string; description?: string }> = apiKeySec.headers
1973
- ? Object.entries(apiKeySec.headers).map(([h, meta]) => ({ headerName: h, description: meta.description }))
2416
+ const declaredHeaders: Array<{
2417
+ headerName: string;
2418
+ description?: string;
2419
+ }> = apiKeySec.headers
2420
+ ? Object.entries(apiKeySec.headers).map(([h, meta]) => ({
2421
+ headerName: h,
2422
+ description: meta.description,
2423
+ }))
1974
2424
  : apiKeySec.name
1975
2425
  ? [{ headerName: apiKeySec.name }]
1976
2426
  : [];
@@ -1982,12 +2432,16 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
1982
2432
  for (const { headerName, description } of declaredHeaders) {
1983
2433
  const storageKey = toStorageKey(headerName);
1984
2434
  // Check: credentials param → existing config.headers → legacy config key → resolve callback
1985
- const value = opts?.credentials?.[storageKey]
1986
- ?? opts?.credentials?.[headerName]
1987
- ?? (existingHeaders && Object.entries(existingHeaders).find(([k]) => k.toLowerCase() === headerName.toLowerCase())?.[1])
1988
- ?? opts?.apiKey
1989
- ?? await readRefSecret(name, storageKey)
1990
- ?? await tryResolve(storageKey);
2435
+ const value =
2436
+ opts?.credentials?.[storageKey] ??
2437
+ opts?.credentials?.[headerName] ??
2438
+ (existingHeaders &&
2439
+ Object.entries(existingHeaders).find(
2440
+ ([k]) => k.toLowerCase() === headerName.toLowerCase(),
2441
+ )?.[1]) ??
2442
+ opts?.apiKey ??
2443
+ (await readRefSecret(name, storageKey)) ??
2444
+ (await tryResolve(storageKey));
1991
2445
 
1992
2446
  if (value) {
1993
2447
  resolvedHeaders[headerName] = value;
@@ -2009,23 +2463,30 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
2009
2463
  const encKey = options.encryptionKey;
2010
2464
  const headersToStore: Record<string, string> = {};
2011
2465
  for (const [h, v] of Object.entries(resolvedHeaders)) {
2012
- headersToStore[h] = encKey ? `${SECRET_PREFIX}${await encryptSecret(v, encKey)}` : v;
2466
+ headersToStore[h] = encKey
2467
+ ? `${SECRET_PREFIX}${await encryptSecret(v, encKey)}`
2468
+ : v;
2013
2469
  }
2014
2470
  await ref.update(name, { config: { headers: headersToStore } });
2015
2471
  return { type: "apiKey", complete: true };
2016
2472
  }
2017
2473
 
2018
2474
  // Fallback: no headers declared → generic api_key
2019
- const key = opts?.credentials?.["api_key"] ?? opts?.apiKey ?? await tryResolve("api_key");
2475
+ const key =
2476
+ opts?.credentials?.["api_key"] ??
2477
+ opts?.apiKey ??
2478
+ (await tryResolve("api_key"));
2020
2479
  if (!key) {
2021
2480
  return {
2022
2481
  type: "apiKey",
2023
2482
  complete: false,
2024
- fields: [{
2025
- name: "api_key",
2026
- label: "API Key",
2027
- secret: true,
2028
- }],
2483
+ fields: [
2484
+ {
2485
+ name: "api_key",
2486
+ label: "API Key",
2487
+ secret: true,
2488
+ },
2489
+ ],
2029
2490
  };
2030
2491
  }
2031
2492
  await storeRefSecret(name, "api_key", key);
@@ -2037,12 +2498,24 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
2037
2498
  const isBasic = httpSec.scheme === "basic";
2038
2499
 
2039
2500
  if (isBasic) {
2040
- const username = opts?.credentials?.["username"] ?? await tryResolve("username");
2041
- const password = opts?.credentials?.["password"] ?? await tryResolve("password");
2501
+ const username =
2502
+ opts?.credentials?.["username"] ?? (await tryResolve("username"));
2503
+ const password =
2504
+ opts?.credentials?.["password"] ?? (await tryResolve("password"));
2042
2505
  if (!username || !password) {
2043
2506
  const missingFields: AuthChallengeField[] = [];
2044
- if (!username) missingFields.push({ name: "username", label: "Username", secret: false });
2045
- if (!password) missingFields.push({ name: "password", label: "Password", secret: true });
2507
+ if (!username)
2508
+ missingFields.push({
2509
+ name: "username",
2510
+ label: "Username",
2511
+ secret: false,
2512
+ });
2513
+ if (!password)
2514
+ missingFields.push({
2515
+ name: "password",
2516
+ label: "Password",
2517
+ secret: true,
2518
+ });
2046
2519
  return { type: "http", complete: false, fields: missingFields };
2047
2520
  }
2048
2521
  // Store as base64 encoded basic auth token
@@ -2052,7 +2525,10 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
2052
2525
  }
2053
2526
 
2054
2527
  // Bearer token
2055
- const token = opts?.credentials?.["token"] ?? opts?.apiKey ?? await tryResolve("token");
2528
+ const token =
2529
+ opts?.credentials?.["token"] ??
2530
+ opts?.apiKey ??
2531
+ (await tryResolve("token"));
2056
2532
  if (!token) {
2057
2533
  return {
2058
2534
  type: "http",
@@ -2065,7 +2541,16 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
2065
2541
  }
2066
2542
 
2067
2543
  if (security.type === "oauth2") {
2068
- const flows = (security as { flows?: { authorizationCode?: { authorizationUrl?: string; tokenUrl?: string } } }).flows;
2544
+ const flows = (
2545
+ security as {
2546
+ flows?: {
2547
+ authorizationCode?: {
2548
+ authorizationUrl?: string;
2549
+ tokenUrl?: string;
2550
+ };
2551
+ };
2552
+ }
2553
+ ).flows;
2069
2554
  const authCodeFlow = flows?.authorizationCode;
2070
2555
  if (!authCodeFlow?.authorizationUrl) {
2071
2556
  return {
@@ -2086,7 +2571,9 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
2086
2571
  }
2087
2572
  // Fallback: construct metadata from the security scheme's explicit URLs
2088
2573
  if (!metadata && authCodeFlow.tokenUrl) {
2089
- const flowScopes = (authCodeFlow as Record<string, unknown>).scopes as Record<string, string> | undefined;
2574
+ const flowScopes = (authCodeFlow as Record<string, unknown>).scopes as
2575
+ | Record<string, string>
2576
+ | undefined;
2090
2577
  metadata = {
2091
2578
  issuer: new URL(authUrl).origin,
2092
2579
  authorization_endpoint: authUrl,
@@ -2101,25 +2588,34 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
2101
2588
  const redirectUri = callbackUrl();
2102
2589
 
2103
2590
  // Resolve client credentials: callback → stored → dynamic registration
2104
- let clientId = await tryResolve("client_id", metadata)
2105
- ?? await readRefSecret(name, "client_id");
2106
- let clientSecret = await tryResolve("client_secret", metadata)
2107
- ?? await readRefSecret(name, "client_secret")
2108
- ?? undefined;
2591
+ const fromHelper = await resolveOAuthClient({
2592
+ name,
2593
+ entry,
2594
+ security,
2595
+ metadata,
2596
+ });
2597
+ let clientId: string | undefined = fromHelper?.clientId;
2598
+ let clientSecret: string | undefined = fromHelper?.clientSecret;
2109
2599
 
2110
2600
  if (!clientId && metadata.registration_endpoint) {
2111
- const supportedAuthMethods = metadata.token_endpoint_auth_methods_supported ?? ["none"];
2601
+ const supportedAuthMethods =
2602
+ metadata.token_endpoint_auth_methods_supported ?? ["none"];
2112
2603
  const preferredMethod = supportedAuthMethods.includes("none")
2113
2604
  ? "none"
2114
- : supportedAuthMethods[0] ?? "client_secret_post";
2115
-
2116
- const securityClientName = (security as { clientName?: string }).clientName;
2117
- const reg = await dynamicClientRegistration(metadata.registration_endpoint, {
2118
- clientName: securityClientName ?? options.oauthClientName ?? "adk",
2119
- redirectUris: [redirectUri],
2120
- grantTypes: ["authorization_code"],
2121
- tokenEndpointAuthMethod: preferredMethod,
2122
- });
2605
+ : (supportedAuthMethods[0] ?? "client_secret_post");
2606
+
2607
+ const securityClientName = (security as { clientName?: string })
2608
+ .clientName;
2609
+ const reg = await dynamicClientRegistration(
2610
+ metadata.registration_endpoint,
2611
+ {
2612
+ clientName:
2613
+ securityClientName ?? options.oauthClientName ?? "adk",
2614
+ redirectUris: [redirectUri],
2615
+ grantTypes: ["authorization_code"],
2616
+ tokenEndpointAuthMethod: preferredMethod,
2617
+ },
2618
+ );
2123
2619
  clientId = reg.clientId;
2124
2620
  clientSecret = reg.clientSecret;
2125
2621
  await storeRefSecret(name, "client_id", clientId);
@@ -2132,10 +2628,18 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
2132
2628
  // Return fields telling the caller what OAuth credentials to provide
2133
2629
  const missingFields: AuthChallengeField[] = [];
2134
2630
  if (!clientId) {
2135
- missingFields.push({ name: "client_id", label: "Client ID", secret: false });
2631
+ missingFields.push({
2632
+ name: "client_id",
2633
+ label: "Client ID",
2634
+ secret: false,
2635
+ });
2136
2636
  }
2137
2637
  // Always ask for client_secret alongside client_id — most providers need it
2138
- missingFields.push({ name: "client_secret", label: "Client Secret", secret: true });
2638
+ missingFields.push({
2639
+ name: "client_secret",
2640
+ label: "Client Secret",
2641
+ secret: true,
2642
+ });
2139
2643
  return { type: "oauth2", complete: false, fields: missingFields };
2140
2644
  }
2141
2645
 
@@ -2149,32 +2653,42 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
2149
2653
  };
2150
2654
  const state = btoa(JSON.stringify(statePayload));
2151
2655
 
2152
- const securityExt2 = security as { requiredScopes?: string[]; optionalScopes?: string[]; authorizationParams?: Record<string, string> };
2153
- const flowScopes = (authCodeFlow as Record<string, unknown>).scopes as Record<string, string> | undefined;
2656
+ const securityExt2 = security as {
2657
+ requiredScopes?: string[];
2658
+ optionalScopes?: string[];
2659
+ authorizationParams?: Record<string, string>;
2660
+ };
2661
+ const flowScopes = (authCodeFlow as Record<string, unknown>).scopes as
2662
+ | Record<string, string>
2663
+ | undefined;
2154
2664
  const agentScopes = [
2155
2665
  ...(securityExt2.requiredScopes ?? []),
2156
2666
  ...(flowScopes ? Object.keys(flowScopes) : []),
2157
2667
  ...(opts?.scopes ?? []),
2158
2668
  ].filter((v, i, a) => a.indexOf(v) === i);
2159
- const scopes = agentScopes.length > 0
2160
- ? [
2161
- ...agentScopes,
2162
- ...(metadata.scopes_supported?.includes('openid') ? ['openid'] : []),
2163
- ]
2164
- : metadata.scopes_supported;
2669
+ const scopes =
2670
+ agentScopes.length > 0
2671
+ ? [
2672
+ ...agentScopes,
2673
+ ...(metadata.scopes_supported?.includes("openid")
2674
+ ? ["openid"]
2675
+ : []),
2676
+ ]
2677
+ : metadata.scopes_supported;
2165
2678
 
2166
2679
  // Read provider-specific authorization params from the agent's security section
2167
2680
  // (e.g., { access_type: 'offline', prompt: 'consent' } for Google)
2168
2681
  const authorizationParams = securityExt2.authorizationParams;
2169
2682
 
2170
- const { url: authorizeUrl, codeVerifier } = await buildOAuthAuthorizeUrl({
2171
- authorizationEndpoint: metadata.authorization_endpoint,
2172
- clientId,
2173
- redirectUri,
2174
- scopes,
2175
- state,
2176
- extraParams: authorizationParams,
2177
- });
2683
+ const { url: authorizeUrl, codeVerifier } =
2684
+ await buildOAuthAuthorizeUrl({
2685
+ authorizationEndpoint: metadata.authorization_endpoint,
2686
+ clientId,
2687
+ redirectUri,
2688
+ scopes,
2689
+ state,
2690
+ extraParams: authorizationParams,
2691
+ });
2178
2692
 
2179
2693
  // Persist pending state so handleCallback works across processes
2180
2694
  await storePendingOAuth(state, {
@@ -2193,10 +2707,13 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
2193
2707
  return { type: security.type, complete: false };
2194
2708
  },
2195
2709
 
2196
- async authLocal(name: string, opts?: {
2197
- onAuthorizeUrl?: (url: string) => void;
2198
- timeoutMs?: number;
2199
- }): Promise<{ complete: boolean }> {
2710
+ async authLocal(
2711
+ name: string,
2712
+ opts?: {
2713
+ onAuthorizeUrl?: (url: string) => void;
2714
+ timeoutMs?: number;
2715
+ },
2716
+ ): Promise<{ complete: boolean }> {
2200
2717
  // `ref.auth` is already proxy-aware — for proxied refs it returns
2201
2718
  // the authorizeUrl that the registry minted against its own
2202
2719
  // callback domain. Everything below is identical for local and
@@ -2219,7 +2736,11 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
2219
2736
  // owns the credential store, so the user needs to submit via
2220
2737
  // whatever UI the registry exposes. Supporting this through the
2221
2738
  // proxy would need a remote form endpoint — out of scope here.
2222
- if (result.fields && result.fields.length > 0 && result.type !== "oauth2") {
2739
+ if (
2740
+ result.fields &&
2741
+ result.fields.length > 0 &&
2742
+ result.type !== "oauth2"
2743
+ ) {
2223
2744
  if (proxy) {
2224
2745
  throw new Error(
2225
2746
  `Ref "${name}" is sourced from a proxied registry; submit credentials through ${proxy.agent} instead of a local form.`,
@@ -2256,19 +2777,23 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
2256
2777
  resolve({ complete: true });
2257
2778
  } else {
2258
2779
  res.writeHead(200, { "Content-Type": "text/html" });
2259
- res.end(renderCredentialForm(
2260
- name,
2261
- authResult.fields ?? result.fields!,
2262
- "Some credentials were missing or invalid.",
2263
- ));
2780
+ res.end(
2781
+ renderCredentialForm(
2782
+ name,
2783
+ authResult.fields ?? result.fields!,
2784
+ "Some credentials were missing or invalid.",
2785
+ ),
2786
+ );
2264
2787
  }
2265
2788
  } catch (err) {
2266
2789
  res.writeHead(500, { "Content-Type": "text/html" });
2267
- res.end(renderCredentialForm(
2268
- name,
2269
- result.fields!,
2270
- err instanceof Error ? err.message : String(err),
2271
- ));
2790
+ res.end(
2791
+ renderCredentialForm(
2792
+ name,
2793
+ result.fields!,
2794
+ err instanceof Error ? err.message : String(err),
2795
+ ),
2796
+ );
2272
2797
  }
2273
2798
  return;
2274
2799
  }
@@ -2315,7 +2840,8 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
2315
2840
  const state = reqUrl.searchParams.get("state");
2316
2841
 
2317
2842
  if (!code || !state) {
2318
- const error = reqUrl.searchParams.get("error") ?? "missing code/state";
2843
+ const error =
2844
+ reqUrl.searchParams.get("error") ?? "missing code/state";
2319
2845
  res.writeHead(400, { "Content-Type": "text/html" });
2320
2846
  res.end(`<h1>Error</h1><p>${error}</p>`);
2321
2847
  server.close();
@@ -2331,7 +2857,9 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
2331
2857
  resolve({ complete: cbResult.complete });
2332
2858
  } catch (err) {
2333
2859
  res.writeHead(500, { "Content-Type": "text/html" });
2334
- res.end(`<h1>Error</h1><p>${err instanceof Error ? err.message : String(err)}</p>`);
2860
+ res.end(
2861
+ `<h1>Error</h1><p>${err instanceof Error ? err.message : String(err)}</p>`,
2862
+ );
2335
2863
  server.close();
2336
2864
  reject(err);
2337
2865
  }
@@ -2365,22 +2893,36 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
2365
2893
  const refreshToken = await readRefSecret(name, "refresh_token");
2366
2894
  if (!refreshToken) return null;
2367
2895
 
2368
- // Read client credentials
2369
- const clientId = await readRefSecret(name, "client_id");
2370
- if (!clientId) return null;
2371
- const clientSecret = await readRefSecret(name, "client_secret");
2372
-
2373
- // Get the agent's token endpoint from its security metadata
2896
+ // Resolve token endpoint + OAuth client via the host's
2897
+ // `resolveCredentials` chain. Same chain `auth` uses (see
2898
+ // `resolveOAuthClient`) kept symmetric so refresh works on every
2899
+ // ref `auth` works on, including first-party registry-hosted
2900
+ // clients whose creds live in env / tenant scope, not the user's
2901
+ // per-ref config.
2374
2902
  const entry = await ref.get(name);
2375
2903
  if (!entry) return null;
2376
2904
 
2377
- const info = await ref.inspect(name);
2378
- const security = (info as any)?.security as Record<string, unknown> | undefined;
2379
- const flows = (security?.flows as Record<string, Record<string, unknown>> | undefined);
2905
+ const status = await ref.authStatus(name);
2906
+ const security = status.security;
2907
+ const flows =
2908
+ security && "flows" in security
2909
+ ? (
2910
+ security as {
2911
+ flows?: Record<
2912
+ string,
2913
+ { tokenUrl?: string; refreshUrl?: string }
2914
+ >;
2915
+ }
2916
+ ).flows
2917
+ : undefined;
2380
2918
  const authCodeFlow = flows?.authorizationCode;
2381
- const tokenUrl = (authCodeFlow?.refreshUrl ?? authCodeFlow?.tokenUrl) as string | undefined;
2919
+ const tokenUrl = authCodeFlow?.refreshUrl ?? authCodeFlow?.tokenUrl;
2382
2920
  if (!tokenUrl) return null;
2383
2921
 
2922
+ const oauthClient = await resolveOAuthClient({ name, entry, security });
2923
+ if (!oauthClient) return null;
2924
+ const { clientId, clientSecret } = oauthClient;
2925
+
2384
2926
  // POST to the token endpoint with grant_type=refresh_token
2385
2927
  const body = new URLSearchParams({
2386
2928
  grant_type: "refresh_token",
@@ -2399,7 +2941,7 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
2399
2941
 
2400
2942
  if (!res.ok) return null;
2401
2943
 
2402
- const data = await res.json() as Record<string, unknown>;
2944
+ const data = (await res.json()) as Record<string, unknown>;
2403
2945
  const newAccessToken = data.access_token as string | undefined;
2404
2946
  if (!newAccessToken) return null;
2405
2947
 
@@ -2417,7 +2959,14 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
2417
2959
  // Top-level callback handler
2418
2960
  // ==========================================
2419
2961
 
2420
- async function handleCallback(params: { code: string; state: string }): Promise<{ refName: string; complete: boolean; stateContext?: Record<string, unknown> }> {
2962
+ async function handleCallback(params: {
2963
+ code: string;
2964
+ state: string;
2965
+ }): Promise<{
2966
+ refName: string;
2967
+ complete: boolean;
2968
+ stateContext?: Record<string, unknown>;
2969
+ }> {
2421
2970
  const pending = await consumePendingOAuth(params.state);
2422
2971
  if (!pending) {
2423
2972
  throw new Error(`No pending OAuth flow for state "${params.state}".`);
@@ -2433,13 +2982,19 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
2433
2982
 
2434
2983
  await storeRefSecret(pending.refName, "access_token", tokens.accessToken);
2435
2984
  if (tokens.refreshToken) {
2436
- await storeRefSecret(pending.refName, "refresh_token", tokens.refreshToken);
2985
+ await storeRefSecret(
2986
+ pending.refName,
2987
+ "refresh_token",
2988
+ tokens.refreshToken,
2989
+ );
2437
2990
  }
2438
2991
 
2439
2992
  let stateContext: Record<string, unknown> | undefined;
2440
2993
  try {
2441
2994
  stateContext = JSON.parse(atob(params.state));
2442
- } catch { /* state wasn't base64 JSON — legacy format */ }
2995
+ } catch {
2996
+ /* state wasn't base64 JSON — legacy format */
2997
+ }
2443
2998
 
2444
2999
  return { refName: pending.refName, complete: true, stateContext };
2445
3000
  }