@slashfi/agents-sdk 0.77.3 → 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,13 +695,19 @@ 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
  }
@@ -601,23 +792,36 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
601
792
  createdAt: number;
602
793
  }
603
794
 
604
- async function readPendingOAuth(): Promise<Record<string, PendingOAuthState>> {
795
+ async function readPendingOAuth(): Promise<
796
+ Record<string, PendingOAuthState>
797
+ > {
605
798
  const content = await fs.readFile(PENDING_OAUTH_PATH);
606
799
  if (!content) return {};
607
- try { return JSON.parse(content); } catch { return {}; }
800
+ try {
801
+ return JSON.parse(content);
802
+ } catch {
803
+ return {};
804
+ }
608
805
  }
609
806
 
610
- async function writePendingOAuth(pending: Record<string, PendingOAuthState>): Promise<void> {
807
+ async function writePendingOAuth(
808
+ pending: Record<string, PendingOAuthState>,
809
+ ): Promise<void> {
611
810
  await fs.writeFile(PENDING_OAUTH_PATH, JSON.stringify(pending, null, 2));
612
811
  }
613
812
 
614
- async function storePendingOAuth(state: string, data: PendingOAuthState): Promise<void> {
813
+ async function storePendingOAuth(
814
+ state: string,
815
+ data: PendingOAuthState,
816
+ ): Promise<void> {
615
817
  const pending = await readPendingOAuth();
616
818
  pending[state] = data;
617
819
  await writePendingOAuth(pending);
618
820
  }
619
821
 
620
- async function consumePendingOAuth(state: string): Promise<PendingOAuthState | null> {
822
+ async function consumePendingOAuth(
823
+ state: string,
824
+ ): Promise<PendingOAuthState | null> {
621
825
  const pending = await readPendingOAuth();
622
826
  const data = pending[state] ?? null;
623
827
  if (data) {
@@ -646,7 +850,10 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
646
850
  let reqId = 0;
647
851
  let sessionId: string | undefined;
648
852
  async function rpc(method: string, rpcParams?: Record<string, unknown>) {
649
- const reqHeaders = { ...headers, ...(sessionId ? { "Mcp-Session-Id": sessionId } : {}) };
853
+ const reqHeaders = {
854
+ ...headers,
855
+ ...(sessionId ? { "Mcp-Session-Id": sessionId } : {}),
856
+ };
650
857
  const res = await globalThis.fetch(url, {
651
858
  method: "POST",
652
859
  headers: reqHeaders,
@@ -658,7 +865,9 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
658
865
  }),
659
866
  });
660
867
  if (!res.ok) {
661
- 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
+ );
662
871
  }
663
872
 
664
873
  const contentType = res.headers.get("content-type") ?? "";
@@ -676,18 +885,23 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
676
885
  try {
677
886
  const json = JSON.parse(line.slice(6));
678
887
  if (json.id === reqId) {
679
- 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}`);
680
890
  return json.result;
681
891
  }
682
892
  } catch (e) {
683
- if (e instanceof Error && e.message.startsWith("MCP RPC")) throw e;
893
+ if (e instanceof Error && e.message.startsWith("MCP RPC"))
894
+ throw e;
684
895
  }
685
896
  }
686
897
  }
687
898
  return undefined;
688
899
  }
689
900
 
690
- 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
+ };
691
905
  if (json.error) throw new Error(`MCP RPC error: ${json.error.message}`);
692
906
  return json.result;
693
907
  }
@@ -700,17 +914,34 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
700
914
  });
701
915
  await rpc("notifications/initialized").catch(() => {});
702
916
 
703
- const result = await rpc("tools/call", { name: toolName, arguments: params }) as
704
- { 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
+ };
705
924
 
706
925
  const textContent = result?.content?.find((c) => c.type === "text");
707
926
  if (textContent?.text) {
708
- try { return { success: true, result: JSON.parse(textContent.text) } as CallAgentResponse; }
709
- 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
+ }
710
938
  }
711
939
  return { success: true, result } as CallAgentResponse;
712
940
  } catch (err) {
713
- 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;
714
945
  }
715
946
  }
716
947
 
@@ -720,11 +951,13 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
720
951
  }
721
952
 
722
953
  /** Try fetching a URL directly as OAuth metadata (it may already be a discovery URL). */
723
- 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> {
724
957
  try {
725
958
  const res = await globalThis.fetch(url);
726
959
  if (!res.ok) return null;
727
- const data = await res.json() as Record<string, unknown>;
960
+ const data = (await res.json()) as Record<string, unknown>;
728
961
  if (data.authorization_endpoint && data.token_endpoint) {
729
962
  return data as unknown as import("./mcp-client.js").OAuthServerMetadata;
730
963
  }
@@ -778,7 +1011,9 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
778
1011
  * Build a consumer that includes the ref's sourceRegistry if present.
779
1012
  * This ensures calls/inspect route to the correct registry endpoint.
780
1013
  */
781
- async function buildConsumerForRef(entry: RefEntry): Promise<RegistryConsumer> {
1014
+ async function buildConsumerForRef(
1015
+ entry: RefEntry,
1016
+ ): Promise<RegistryConsumer> {
782
1017
  const config = await readConfig();
783
1018
  let registries = config.registries ?? [];
784
1019
 
@@ -816,7 +1051,10 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
816
1051
  * Resolve the correct registry for a ref.
817
1052
  * If the ref has a sourceRegistry, use that; otherwise fall back to the first registry.
818
1053
  */
819
- function resolveRegistryForRef(consumer: RegistryConsumer, entry: RefEntry): ResolvedRegistry {
1054
+ function resolveRegistryForRef(
1055
+ consumer: RegistryConsumer,
1056
+ entry: RefEntry,
1057
+ ): ResolvedRegistry {
820
1058
  const regs = consumer.registries();
821
1059
  if (entry.sourceRegistry?.url) {
822
1060
  const match = regs.find((r) => r.url === entry.sourceRegistry!.url);
@@ -837,7 +1075,9 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
837
1075
  * the ref is sourced from a raw URL (no registry), in which case proxy routing
838
1076
  * does not apply.
839
1077
  */
840
- async function findRegistryEntryForRef(entry: RefEntry): Promise<RegistryEntry | null> {
1078
+ async function findRegistryEntryForRef(
1079
+ entry: RefEntry,
1080
+ ): Promise<RegistryEntry | null> {
841
1081
  const sourceUrl = entry.sourceRegistry?.url;
842
1082
  if (!sourceUrl) return null;
843
1083
  const config = await readConfig();
@@ -890,7 +1130,10 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
890
1130
  sourceRegistry: { url: reg.url, agentPath: agent },
891
1131
  });
892
1132
  const resolved = consumer.registries().find((r) => r.url === reg.url);
893
- 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
+ );
894
1137
 
895
1138
  const response = await consumer.callRegistry(resolved, {
896
1139
  action: "execute_tool",
@@ -900,8 +1143,13 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
900
1143
  });
901
1144
 
902
1145
  if (!response.success) {
903
- const errResponse = response as { success: false; error?: string; code?: string };
904
- 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`;
905
1153
  throw new Error(msg);
906
1154
  }
907
1155
  return (response as { success: true; result: unknown }).result as T;
@@ -970,7 +1218,8 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
970
1218
  const rName = registryDisplayName(r);
971
1219
  if (rName !== nameOrUrl && registryUrl(r) !== nameOrUrl) return r;
972
1220
  found = true;
973
- const existing: RegistryEntry = typeof r === "string" ? { url: r } : { ...r };
1221
+ const existing: RegistryEntry =
1222
+ typeof r === "string" ? { url: r } : { ...r };
974
1223
  mutate(existing);
975
1224
  return existing;
976
1225
  });
@@ -983,11 +1232,16 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
983
1232
  * Decrypt a `secret:`-prefixed value if we hold the encryption key. Plaintext
984
1233
  * values pass through unchanged so dev configs keep working.
985
1234
  */
986
- async function revealSecret(value: string | undefined): Promise<string | undefined> {
1235
+ async function revealSecret(
1236
+ value: string | undefined,
1237
+ ): Promise<string | undefined> {
987
1238
  if (!value) return value;
988
1239
  if (!value.startsWith(SECRET_PREFIX)) return value;
989
1240
  if (!options.encryptionKey) return undefined;
990
- return decryptSecret(value.slice(SECRET_PREFIX.length), options.encryptionKey);
1241
+ return decryptSecret(
1242
+ value.slice(SECRET_PREFIX.length),
1243
+ options.encryptionKey,
1244
+ );
991
1245
  }
992
1246
 
993
1247
  /**
@@ -1002,7 +1256,8 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
1002
1256
  const target = findRegistry(config.registries ?? [], nameOrUrl);
1003
1257
  if (!target || typeof target === "string") return false;
1004
1258
  const oauth = target.oauth;
1005
- if (!oauth?.refreshToken || !oauth.tokenEndpoint || !oauth.clientId) return false;
1259
+ if (!oauth?.refreshToken || !oauth.tokenEndpoint || !oauth.clientId)
1260
+ return false;
1006
1261
 
1007
1262
  const refreshToken = await revealSecret(oauth.refreshToken);
1008
1263
  const clientSecret = await revealSecret(oauth.clientSecret);
@@ -1044,7 +1299,8 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
1044
1299
  try {
1045
1300
  return await fn();
1046
1301
  } catch (err) {
1047
- if (!(err instanceof AdkError) || err.code !== "registry_auth_required") throw err;
1302
+ if (!(err instanceof AdkError) || err.code !== "registry_auth_required")
1303
+ throw err;
1048
1304
  let refreshed = false;
1049
1305
  try {
1050
1306
  refreshed = await refreshRegistryToken(nameOrUrl);
@@ -1088,7 +1344,9 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
1088
1344
  }
1089
1345
 
1090
1346
  const registry: AdkRegistryApi = {
1091
- async add(entry: RegistryEntry): Promise<{ authRequirement?: RegistryAuthRequirement }> {
1347
+ async add(
1348
+ entry: RegistryEntry,
1349
+ ): Promise<{ authRequirement?: RegistryAuthRequirement }> {
1092
1350
  const config = await readConfig();
1093
1351
  const alias = entry.name ?? entry.url;
1094
1352
  const registries = (config.registries ?? []).filter(
@@ -1135,7 +1393,9 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
1135
1393
  ...final,
1136
1394
  proxy: {
1137
1395
  mode: discovered.proxy.mode,
1138
- ...(discovered.proxy.agent && { agent: discovered.proxy.agent }),
1396
+ ...(discovered.proxy.agent && {
1397
+ agent: discovered.proxy.agent,
1398
+ }),
1139
1399
  },
1140
1400
  };
1141
1401
  }
@@ -1156,7 +1416,8 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
1156
1416
  if (!config.registries?.length) return false;
1157
1417
  const before = config.registries.length;
1158
1418
  const registries = config.registries.filter(
1159
- (r) => registryDisplayName(r) !== nameOrUrl && registryUrl(r) !== nameOrUrl,
1419
+ (r) =>
1420
+ registryDisplayName(r) !== nameOrUrl && registryUrl(r) !== nameOrUrl,
1160
1421
  );
1161
1422
  if (registries.length === before) return false;
1162
1423
  await writeConfig({ ...config, registries });
@@ -1177,7 +1438,10 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
1177
1438
  return typeof target === "string" ? { url: target } : target;
1178
1439
  },
1179
1440
 
1180
- async update(name: string, updates: Partial<RegistryEntry>): Promise<boolean> {
1441
+ async update(
1442
+ name: string,
1443
+ updates: Partial<RegistryEntry>,
1444
+ ): Promise<boolean> {
1181
1445
  const config = await readConfig();
1182
1446
  if (!config.registries?.length) return false;
1183
1447
  let found = false;
@@ -1185,11 +1449,13 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
1185
1449
  const rName = registryDisplayName(r);
1186
1450
  if (rName !== name && registryUrl(r) !== name) return r;
1187
1451
  found = true;
1188
- const existing: RegistryEntry = typeof r === "string" ? { url: r } : { ...r };
1452
+ const existing: RegistryEntry =
1453
+ typeof r === "string" ? { url: r } : { ...r };
1189
1454
  if (updates.url) existing.url = updates.url;
1190
1455
  if (updates.name) existing.name = updates.name;
1191
1456
  if (updates.auth) existing.auth = updates.auth;
1192
- if (updates.headers) existing.headers = { ...existing.headers, ...updates.headers };
1457
+ if (updates.headers)
1458
+ existing.headers = { ...existing.headers, ...updates.headers };
1193
1459
  if (updates.proxy !== undefined) existing.proxy = updates.proxy;
1194
1460
  return existing;
1195
1461
  });
@@ -1201,7 +1467,8 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
1201
1467
  async browse(name: string, query?: string): Promise<AgentListEntry[]> {
1202
1468
  const config = await readConfig();
1203
1469
  const target = findRegistry(config.registries ?? [], name);
1204
- if (target && typeof target !== "string") assertRegistryAuthorized(target);
1470
+ if (target && typeof target !== "string")
1471
+ assertRegistryAuthorized(target);
1205
1472
  return callWithRefresh(name, async () => {
1206
1473
  const consumer = await buildConsumer(name);
1207
1474
  const url = target ? registryUrl(target) : name;
@@ -1212,7 +1479,8 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
1212
1479
  async inspect(name: string): Promise<RegistryConfiguration> {
1213
1480
  const config = await readConfig();
1214
1481
  const target = findRegistry(config.registries ?? [], name);
1215
- if (target && typeof target !== "string") assertRegistryAuthorized(target);
1482
+ if (target && typeof target !== "string")
1483
+ assertRegistryAuthorized(target);
1216
1484
  return callWithRefresh(name, async () => {
1217
1485
  const consumer = await buildConsumer(name);
1218
1486
  const url = target ? registryUrl(target) : name;
@@ -1224,7 +1492,9 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
1224
1492
  const config = await readConfig();
1225
1493
  const registries = config.registries ?? [];
1226
1494
  const targets = name
1227
- ? registries.filter((r) => registryDisplayName(r) === name || registryUrl(r) === name)
1495
+ ? registries.filter(
1496
+ (r) => registryDisplayName(r) === name || registryUrl(r) === name,
1497
+ )
1228
1498
  : registries;
1229
1499
 
1230
1500
  const results = await Promise.allSettled(
@@ -1265,7 +1535,12 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
1265
1535
  return results.map((r) =>
1266
1536
  r.status === "fulfilled"
1267
1537
  ? r.value
1268
- : { 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
+ },
1269
1544
  );
1270
1545
  },
1271
1546
 
@@ -1337,7 +1612,10 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
1337
1612
  });
1338
1613
  // Re-read so the flow below sees the fresh requirement.
1339
1614
  const refreshed = await readConfig();
1340
- const refreshedTarget = findRegistry(refreshed.registries ?? [], nameOrUrl);
1615
+ const refreshedTarget = findRegistry(
1616
+ refreshed.registries ?? [],
1617
+ nameOrUrl,
1618
+ );
1341
1619
  if (refreshedTarget && typeof refreshedTarget !== "string") {
1342
1620
  Object.assign(target, refreshedTarget);
1343
1621
  }
@@ -1403,17 +1681,21 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
1403
1681
  );
1404
1682
 
1405
1683
  const state = crypto.randomUUID();
1406
- const { url: authorizeUrl, codeVerifier } = await buildOAuthAuthorizeUrl({
1407
- authorizationEndpoint: metadata.authorization_endpoint,
1408
- clientId: registration.clientId,
1409
- redirectUri,
1410
- scopes: req.scopes,
1411
- state,
1412
- });
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
+ });
1413
1692
 
1414
1693
  return new Promise<{ complete: boolean }>((resolve, reject) => {
1415
1694
  const server = createServer(async (reqIn, resOut) => {
1416
- const reqUrl = new URL(reqIn.url ?? "/", `http://localhost:${port}`);
1695
+ const reqUrl = new URL(
1696
+ reqIn.url ?? "/",
1697
+ `http://localhost:${port}`,
1698
+ );
1417
1699
  if (reqUrl.pathname !== "/callback") {
1418
1700
  resOut.writeHead(404);
1419
1701
  resOut.end();
@@ -1423,7 +1705,8 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
1423
1705
  const code = reqUrl.searchParams.get("code");
1424
1706
  const returnedState = reqUrl.searchParams.get("state");
1425
1707
  if (!code || returnedState !== state) {
1426
- const error = reqUrl.searchParams.get("error") ?? "missing code/state";
1708
+ const error =
1709
+ reqUrl.searchParams.get("error") ?? "missing code/state";
1427
1710
  resOut.writeHead(400, { "Content-Type": "text/html" });
1428
1711
  resOut.end(`<h1>Error</h1><p>${esc(error)}</p>`);
1429
1712
  server.close();
@@ -1439,13 +1722,16 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
1439
1722
  }
1440
1723
 
1441
1724
  try {
1442
- const tokens = await exchangeCodeForTokens(metadata.token_endpoint, {
1443
- code,
1444
- codeVerifier,
1445
- clientId: registration.clientId,
1446
- clientSecret: registration.clientSecret,
1447
- redirectUri,
1448
- });
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
+ );
1449
1735
  const expiresAt = tokens.expiresIn
1450
1736
  ? new Date(Date.now() + tokens.expiresIn * 1000).toISOString()
1451
1737
  : undefined;
@@ -1527,7 +1813,9 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
1527
1813
  const token = params.get("token");
1528
1814
  if (!token) {
1529
1815
  resOut.writeHead(200, { "Content-Type": "text/html" });
1530
- resOut.end(renderCredentialForm(displayName, fields, "Token is required."));
1816
+ resOut.end(
1817
+ renderCredentialForm(displayName, fields, "Token is required."),
1818
+ );
1531
1819
  return;
1532
1820
  }
1533
1821
  try {
@@ -1571,7 +1859,9 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
1571
1859
  // ==========================================
1572
1860
 
1573
1861
  const ref: AdkRefApi = {
1574
- async add(entryInput: RefAddInput): Promise<{ security: SecuritySchemeSummary | null }> {
1862
+ async add(
1863
+ entryInput: RefAddInput,
1864
+ ): Promise<{ security: SecuritySchemeSummary | null }> {
1575
1865
  let security: SecuritySchemeSummary | null = null;
1576
1866
 
1577
1867
  const config = await readConfig();
@@ -1593,7 +1883,10 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
1593
1883
  if (entry.sourceRegistry?.url) {
1594
1884
  entry = { ...entry, scheme: "registry" };
1595
1885
  } else if (entry.url) {
1596
- entry = { ...entry, scheme: entry.url.startsWith("http") ? "https" : "mcp" };
1886
+ entry = {
1887
+ ...entry,
1888
+ scheme: entry.url.startsWith("http") ? "https" : "mcp",
1889
+ };
1597
1890
  } else {
1598
1891
  throw new AdkError({
1599
1892
  code: "REF_INVALID",
@@ -1623,6 +1916,7 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
1623
1916
  });
1624
1917
  }
1625
1918
 
1919
+ let cacheEntry: RegistryCacheEntry | undefined;
1626
1920
  if (hasRegistries || entry.sourceRegistry?.url) {
1627
1921
  try {
1628
1922
  const consumer = await buildConsumerForRef(entry);
@@ -1631,11 +1925,11 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
1631
1925
 
1632
1926
  const requiresValidation = !!entry.sourceRegistry;
1633
1927
  if (requiresValidation) {
1634
- const hasContent = info && (
1635
- info.description ||
1636
- (info.tools && info.tools.length > 0) ||
1637
- (info.toolSummaries && info.toolSummaries.length > 0)
1638
- );
1928
+ const hasContent =
1929
+ info &&
1930
+ (info.description ||
1931
+ (info.tools && info.tools.length > 0) ||
1932
+ (info.toolSummaries && info.toolSummaries.length > 0));
1639
1933
  if (!hasContent) {
1640
1934
  // Inspect returned empty — fall back to browse to check if agent exists
1641
1935
  const registryUrl = entry.sourceRegistry?.url;
@@ -1644,7 +1938,9 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
1644
1938
  try {
1645
1939
  const agents = await consumer.browse(registryUrl);
1646
1940
  const stripAt = (s: string) => s.replace(/^@/, "");
1647
- const refKey = stripAt(entry.sourceRegistry?.agentPath ?? entry.ref);
1941
+ const refKey = stripAt(
1942
+ entry.sourceRegistry?.agentPath ?? entry.ref,
1943
+ );
1648
1944
  foundInBrowse = agents.some(
1649
1945
  (a) => a.path === entry.ref || stripAt(a.path) === refKey,
1650
1946
  );
@@ -1658,7 +1954,11 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
1658
1954
  code: "REF_NOT_FOUND",
1659
1955
  message: `Agent "${entry.ref}" not found on ${registryHint}`,
1660
1956
  hint: "Check available agents with: adk registry browse",
1661
- 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
+ },
1662
1962
  });
1663
1963
  }
1664
1964
  }
@@ -1667,17 +1967,22 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
1667
1967
  if (info?.security) security = info.security;
1668
1968
  const agentMode = (info as any)?.mode;
1669
1969
  if (agentMode) (entry as any).mode = agentMode;
1670
- if (info?.upstream && !entry.url && agentMode !== 'api') {
1970
+ if (info?.upstream && !entry.url && agentMode !== "api") {
1671
1971
  entry.url = info.upstream as string;
1672
1972
  entry.scheme = entry.scheme ?? "mcp";
1673
1973
  }
1974
+
1975
+ cacheEntry = buildCacheEntry(entry.ref, info);
1674
1976
  } catch (err) {
1675
1977
  if (err instanceof AdkError) throw err;
1676
1978
  throw new AdkError({
1677
1979
  code: "REGISTRY_UNREACHABLE",
1678
1980
  message: `Could not reach registry to validate "${entry.ref}"`,
1679
1981
  hint: "Check your registry connection with: adk registry test",
1680
- 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
+ },
1681
1986
  cause: err,
1682
1987
  });
1683
1988
  }
@@ -1685,6 +1990,7 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
1685
1990
 
1686
1991
  const refs = [...(config.refs ?? []), entry];
1687
1992
  await writeConfig({ ...config, refs });
1993
+ await upsertRegistryCacheEntry(name, cacheEntry);
1688
1994
 
1689
1995
  return { security };
1690
1996
  },
@@ -1696,17 +2002,28 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
1696
2002
  const refs = config.refs.filter((r) => !refNameMatches(r, name));
1697
2003
  if (refs.length === before) return false;
1698
2004
  await writeConfig({ ...config, refs });
2005
+ await removeRegistryCacheEntry(name);
1699
2006
  return true;
1700
2007
  },
1701
2008
 
1702
2009
  async list(): Promise<ResolvedRef[]> {
1703
- const config = await readConfig();
1704
- 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));
1705
2017
  },
1706
2018
 
1707
2019
  async get(name: string): Promise<ResolvedRef | null> {
1708
- const config = await readConfig();
1709
- 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);
1710
2027
  },
1711
2028
 
1712
2029
  async update(name: string, updates: Partial<RefEntry>): Promise<boolean> {
@@ -1735,8 +2052,10 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
1735
2052
  updated.name = updates.name;
1736
2053
  }
1737
2054
  if (updates.scheme) updated.scheme = updates.scheme;
1738
- if (updates.config) updated.config = { ...updated.config, ...updates.config };
1739
- 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;
1740
2059
  return updated;
1741
2060
  });
1742
2061
  if (!found) return false;
@@ -1744,38 +2063,63 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
1744
2063
  return true;
1745
2064
  },
1746
2065
 
1747
- async inspect(name: string, opts?: { full?: boolean }): Promise<AgentInspection | null> {
2066
+ async inspect(
2067
+ name: string,
2068
+ opts?: { full?: boolean },
2069
+ ): Promise<AgentInspection | null> {
1748
2070
  const config = await readConfig();
1749
2071
  const entry = findRef(config.refs ?? [], name);
1750
2072
  if (!entry) throw new Error(`Ref "${name}" not found`);
1751
2073
 
1752
2074
  const consumer = await buildConsumerForRef(entry);
1753
- return consumer.inspect(
2075
+ const result = await consumer.inspect(
1754
2076
  entry.sourceRegistry?.agentPath ?? entry.ref,
1755
2077
  entry.sourceRegistry?.url,
1756
2078
  opts,
1757
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;
1758
2088
  },
1759
2089
 
1760
- 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> {
1761
2095
  const config = await readConfig();
1762
2096
  const entry = findRef(config.refs ?? [], name);
1763
2097
  if (!entry) throw new Error(`Ref "${name}" not found`);
1764
2098
 
1765
- let accessToken = await readRefSecret(name, "access_token")
1766
- ?? await readRefSecret(name, "api_key")
1767
- ?? await readRefSecret(name, "token");
2099
+ const accessToken =
2100
+ (await readRefSecret(name, "access_token")) ??
2101
+ (await readRefSecret(name, "api_key")) ??
2102
+ (await readRefSecret(name, "token"));
1768
2103
 
1769
2104
  // Resolve custom headers from config (e.g. { "X-API-Key": "secret:..." })
1770
2105
  const refConfig = (entry.config ?? {}) as Record<string, unknown>;
1771
- const rawHeaders = refConfig.headers as Record<string, string> | undefined;
2106
+ const rawHeaders = refConfig.headers as
2107
+ | Record<string, string>
2108
+ | undefined;
1772
2109
  let resolvedHeaders: Record<string, string> | undefined;
1773
- if (rawHeaders && typeof rawHeaders === 'object') {
2110
+ if (rawHeaders && typeof rawHeaders === "object") {
1774
2111
  resolvedHeaders = {};
1775
2112
  for (const [k, v] of Object.entries(rawHeaders)) {
1776
- if (typeof v === 'string' && v.startsWith(SECRET_PREFIX) && options.encryptionKey) {
1777
- resolvedHeaders[k] = await decryptSecret(v.slice(SECRET_PREFIX.length), options.encryptionKey);
1778
- } 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") {
1779
2123
  resolvedHeaders[k] = v;
1780
2124
  }
1781
2125
  }
@@ -1784,9 +2128,15 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
1784
2128
  const doCall = async (token: string | null) => {
1785
2129
  // Direct MCP only for redirect/proxy agents with an MCP upstream.
1786
2130
  // API-mode agents must go through the registry (it does REST translation).
1787
- const agentMode = (entry as any).mode ?? 'redirect';
1788
- if (token && entry.url && agentMode !== 'api') {
1789
- 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
+ );
1790
2140
  }
1791
2141
 
1792
2142
  const consumer = await buildConsumerForRef(entry);
@@ -1855,13 +2205,20 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
1855
2205
  // server-side so local inspection would always return "missing").
1856
2206
  const proxy = await resolveProxyForRef(entry);
1857
2207
  if (proxy) {
1858
- 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
+ );
1859
2214
  }
1860
2215
 
1861
2216
  let security: SecuritySchemeSummary | null = null;
1862
2217
  try {
1863
2218
  const consumer = await buildConsumerForRef(entry);
1864
- const info = await consumer.inspect(entry.sourceRegistry?.agentPath ?? entry.ref);
2219
+ const info = await consumer.inspect(
2220
+ entry.sourceRegistry?.agentPath ?? entry.ref,
2221
+ );
1865
2222
  if (info?.security) security = info.security;
1866
2223
  } catch {
1867
2224
  // Can't reach registry
@@ -1889,12 +2246,15 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
1889
2246
  };
1890
2247
  const hasRegistration = !!securityExt.dynamicRegistration;
1891
2248
 
1892
- let oauthMetadata: import("./mcp-client.js").OAuthServerMetadata | null = null;
2249
+ let oauthMetadata:
2250
+ | import("./mcp-client.js").OAuthServerMetadata
2251
+ | null = null;
1893
2252
  let needsSecret = false;
1894
2253
  if (securityExt.discoveryUrl) {
1895
2254
  oauthMetadata = await tryFetchOAuthMetadata(securityExt.discoveryUrl);
1896
2255
  if (oauthMetadata) {
1897
- const authMethods = oauthMetadata.token_endpoint_auth_methods_supported ?? [];
2256
+ const authMethods =
2257
+ oauthMetadata.token_endpoint_auth_methods_supported ?? [];
1898
2258
  needsSecret = !authMethods.includes("none");
1899
2259
  }
1900
2260
  }
@@ -1921,15 +2281,22 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
1921
2281
  };
1922
2282
  } else if (security.type === "apiKey") {
1923
2283
  const apiKeySec = security as {
1924
- name?: string; headers?: Record<string, { description?: string }>;
2284
+ name?: string;
2285
+ headers?: Record<string, { description?: string }>;
1925
2286
  };
1926
2287
  const toStorageKey = (headerName: string) =>
1927
- headerName.toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_|_$/g, "");
2288
+ headerName
2289
+ .toLowerCase()
2290
+ .replace(/[^a-z0-9]+/g, "_")
2291
+ .replace(/^_|_$/g, "");
1928
2292
 
1929
2293
  // config.headers: { "Header-Name": "value" } — check by header name (case-insensitive)
1930
- const configHeaders = (entry?.config as Record<string, unknown> | undefined)?.headers as
1931
- Record<string, unknown> | undefined;
1932
- 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
+ : [];
1933
2300
  const hasConfigHeader = (name: string) =>
1934
2301
  configHeaderKeys.some((k) => k.toLowerCase() === name.toLowerCase());
1935
2302
 
@@ -1943,7 +2310,8 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
1943
2310
  for (const headerName of declaredHeaders) {
1944
2311
  const storageKey = toStorageKey(headerName);
1945
2312
  const inConfigHeaders = hasConfigHeader(headerName);
1946
- const inLegacyKeys = configKeys.includes(storageKey) || configKeys.includes("api_key");
2313
+ const inLegacyKeys =
2314
+ configKeys.includes(storageKey) || configKeys.includes("api_key");
1947
2315
  fields[storageKey] = {
1948
2316
  required: true,
1949
2317
  automated: false,
@@ -1977,19 +2345,22 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
1977
2345
  return { name, security, complete, fields };
1978
2346
  },
1979
2347
 
1980
- async auth(name: string, opts?: {
1981
- apiKey?: string;
1982
- credentials?: Record<string, string>;
1983
- /** Extra context to encode in the OAuth state (e.g., tenant/user IDs for multi-tenant callbacks) */
1984
- stateContext?: Record<string, unknown>;
1985
- /** Additional scopes to request (e.g., optional scopes declared by the agent) */
1986
- scopes?: string[];
1987
- /**
1988
- * Opt out of proxy routing when the ref's source registry has
1989
- * `proxy: { mode: 'optional' }`. Ignored for `mode: 'required'`.
1990
- */
1991
- preferLocal?: boolean;
1992
- }): 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> {
1993
2364
  const config = await readConfig();
1994
2365
  const entry = findRef(config.refs ?? [], name);
1995
2366
  if (!entry) throw new Error(`Ref "${name}" not found`);
@@ -1998,14 +2369,21 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
1998
2369
  // agent. The registry owns the client_id/secret and returns an authorize
1999
2370
  // URL pointing at the registry's OAuth callback domain, so the user
2000
2371
  // completes the flow against the registry instead of localhost.
2001
- const proxy = await resolveProxyForRef(entry, { preferLocal: opts?.preferLocal });
2372
+ const proxy = await resolveProxyForRef(entry, {
2373
+ preferLocal: opts?.preferLocal,
2374
+ });
2002
2375
  if (proxy) {
2003
2376
  const params: Record<string, unknown> = { name };
2004
2377
  if (opts?.apiKey !== undefined) params.apiKey = opts.apiKey;
2005
2378
  if (opts?.credentials) params.credentials = opts.credentials;
2006
2379
  if (opts?.scopes) params.scopes = opts.scopes;
2007
2380
  if (opts?.stateContext) params.stateContext = opts.stateContext;
2008
- return forwardRefOpToProxy<AuthStartResult>(proxy.reg, proxy.agent, "auth", params);
2381
+ return forwardRefOpToProxy<AuthStartResult>(
2382
+ proxy.reg,
2383
+ proxy.agent,
2384
+ "auth",
2385
+ params,
2386
+ );
2009
2387
  }
2010
2388
 
2011
2389
  const status = await ref.authStatus(name);
@@ -2018,20 +2396,31 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
2018
2396
 
2019
2397
  if (security.type === "apiKey") {
2020
2398
  const apiKeySec = security as {
2021
- name?: string; prefix?: string;
2399
+ name?: string;
2400
+ prefix?: string;
2022
2401
  headers?: Record<string, { description?: string }>;
2023
2402
  };
2024
2403
 
2025
2404
  const toStorageKey = (headerName: string) =>
2026
- headerName.toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_|_$/g, "");
2405
+ headerName
2406
+ .toLowerCase()
2407
+ .replace(/[^a-z0-9]+/g, "_")
2408
+ .replace(/^_|_$/g, "");
2027
2409
 
2028
2410
  // Check existing config.headers
2029
- const existingHeaders = ((entry.config ?? {}) as Record<string, unknown>).headers as
2030
- Record<string, string> | undefined;
2411
+ const existingHeaders = (
2412
+ (entry.config ?? {}) as Record<string, unknown>
2413
+ ).headers as Record<string, string> | undefined;
2031
2414
 
2032
2415
  // Collect declared headers: from security.headers or security.name
2033
- const declaredHeaders: Array<{ headerName: string; description?: string }> = apiKeySec.headers
2034
- ? 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
+ }))
2035
2424
  : apiKeySec.name
2036
2425
  ? [{ headerName: apiKeySec.name }]
2037
2426
  : [];
@@ -2043,12 +2432,16 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
2043
2432
  for (const { headerName, description } of declaredHeaders) {
2044
2433
  const storageKey = toStorageKey(headerName);
2045
2434
  // Check: credentials param → existing config.headers → legacy config key → resolve callback
2046
- const value = opts?.credentials?.[storageKey]
2047
- ?? opts?.credentials?.[headerName]
2048
- ?? (existingHeaders && Object.entries(existingHeaders).find(([k]) => k.toLowerCase() === headerName.toLowerCase())?.[1])
2049
- ?? opts?.apiKey
2050
- ?? await readRefSecret(name, storageKey)
2051
- ?? 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));
2052
2445
 
2053
2446
  if (value) {
2054
2447
  resolvedHeaders[headerName] = value;
@@ -2070,23 +2463,30 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
2070
2463
  const encKey = options.encryptionKey;
2071
2464
  const headersToStore: Record<string, string> = {};
2072
2465
  for (const [h, v] of Object.entries(resolvedHeaders)) {
2073
- headersToStore[h] = encKey ? `${SECRET_PREFIX}${await encryptSecret(v, encKey)}` : v;
2466
+ headersToStore[h] = encKey
2467
+ ? `${SECRET_PREFIX}${await encryptSecret(v, encKey)}`
2468
+ : v;
2074
2469
  }
2075
2470
  await ref.update(name, { config: { headers: headersToStore } });
2076
2471
  return { type: "apiKey", complete: true };
2077
2472
  }
2078
2473
 
2079
2474
  // Fallback: no headers declared → generic api_key
2080
- 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"));
2081
2479
  if (!key) {
2082
2480
  return {
2083
2481
  type: "apiKey",
2084
2482
  complete: false,
2085
- fields: [{
2086
- name: "api_key",
2087
- label: "API Key",
2088
- secret: true,
2089
- }],
2483
+ fields: [
2484
+ {
2485
+ name: "api_key",
2486
+ label: "API Key",
2487
+ secret: true,
2488
+ },
2489
+ ],
2090
2490
  };
2091
2491
  }
2092
2492
  await storeRefSecret(name, "api_key", key);
@@ -2098,12 +2498,24 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
2098
2498
  const isBasic = httpSec.scheme === "basic";
2099
2499
 
2100
2500
  if (isBasic) {
2101
- const username = opts?.credentials?.["username"] ?? await tryResolve("username");
2102
- 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"));
2103
2505
  if (!username || !password) {
2104
2506
  const missingFields: AuthChallengeField[] = [];
2105
- if (!username) missingFields.push({ name: "username", label: "Username", secret: false });
2106
- 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
+ });
2107
2519
  return { type: "http", complete: false, fields: missingFields };
2108
2520
  }
2109
2521
  // Store as base64 encoded basic auth token
@@ -2113,7 +2525,10 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
2113
2525
  }
2114
2526
 
2115
2527
  // Bearer token
2116
- const token = opts?.credentials?.["token"] ?? opts?.apiKey ?? await tryResolve("token");
2528
+ const token =
2529
+ opts?.credentials?.["token"] ??
2530
+ opts?.apiKey ??
2531
+ (await tryResolve("token"));
2117
2532
  if (!token) {
2118
2533
  return {
2119
2534
  type: "http",
@@ -2126,7 +2541,16 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
2126
2541
  }
2127
2542
 
2128
2543
  if (security.type === "oauth2") {
2129
- 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;
2130
2554
  const authCodeFlow = flows?.authorizationCode;
2131
2555
  if (!authCodeFlow?.authorizationUrl) {
2132
2556
  return {
@@ -2147,7 +2571,9 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
2147
2571
  }
2148
2572
  // Fallback: construct metadata from the security scheme's explicit URLs
2149
2573
  if (!metadata && authCodeFlow.tokenUrl) {
2150
- 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;
2151
2577
  metadata = {
2152
2578
  issuer: new URL(authUrl).origin,
2153
2579
  authorization_endpoint: authUrl,
@@ -2172,18 +2598,24 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
2172
2598
  let clientSecret: string | undefined = fromHelper?.clientSecret;
2173
2599
 
2174
2600
  if (!clientId && metadata.registration_endpoint) {
2175
- const supportedAuthMethods = metadata.token_endpoint_auth_methods_supported ?? ["none"];
2601
+ const supportedAuthMethods =
2602
+ metadata.token_endpoint_auth_methods_supported ?? ["none"];
2176
2603
  const preferredMethod = supportedAuthMethods.includes("none")
2177
2604
  ? "none"
2178
- : supportedAuthMethods[0] ?? "client_secret_post";
2179
-
2180
- const securityClientName = (security as { clientName?: string }).clientName;
2181
- const reg = await dynamicClientRegistration(metadata.registration_endpoint, {
2182
- clientName: securityClientName ?? options.oauthClientName ?? "adk",
2183
- redirectUris: [redirectUri],
2184
- grantTypes: ["authorization_code"],
2185
- tokenEndpointAuthMethod: preferredMethod,
2186
- });
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
+ );
2187
2619
  clientId = reg.clientId;
2188
2620
  clientSecret = reg.clientSecret;
2189
2621
  await storeRefSecret(name, "client_id", clientId);
@@ -2196,10 +2628,18 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
2196
2628
  // Return fields telling the caller what OAuth credentials to provide
2197
2629
  const missingFields: AuthChallengeField[] = [];
2198
2630
  if (!clientId) {
2199
- 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
+ });
2200
2636
  }
2201
2637
  // Always ask for client_secret alongside client_id — most providers need it
2202
- 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
+ });
2203
2643
  return { type: "oauth2", complete: false, fields: missingFields };
2204
2644
  }
2205
2645
 
@@ -2213,32 +2653,42 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
2213
2653
  };
2214
2654
  const state = btoa(JSON.stringify(statePayload));
2215
2655
 
2216
- const securityExt2 = security as { requiredScopes?: string[]; optionalScopes?: string[]; authorizationParams?: Record<string, string> };
2217
- 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;
2218
2664
  const agentScopes = [
2219
2665
  ...(securityExt2.requiredScopes ?? []),
2220
2666
  ...(flowScopes ? Object.keys(flowScopes) : []),
2221
2667
  ...(opts?.scopes ?? []),
2222
2668
  ].filter((v, i, a) => a.indexOf(v) === i);
2223
- const scopes = agentScopes.length > 0
2224
- ? [
2225
- ...agentScopes,
2226
- ...(metadata.scopes_supported?.includes('openid') ? ['openid'] : []),
2227
- ]
2228
- : 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;
2229
2678
 
2230
2679
  // Read provider-specific authorization params from the agent's security section
2231
2680
  // (e.g., { access_type: 'offline', prompt: 'consent' } for Google)
2232
2681
  const authorizationParams = securityExt2.authorizationParams;
2233
2682
 
2234
- const { url: authorizeUrl, codeVerifier } = await buildOAuthAuthorizeUrl({
2235
- authorizationEndpoint: metadata.authorization_endpoint,
2236
- clientId,
2237
- redirectUri,
2238
- scopes,
2239
- state,
2240
- extraParams: authorizationParams,
2241
- });
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
+ });
2242
2692
 
2243
2693
  // Persist pending state so handleCallback works across processes
2244
2694
  await storePendingOAuth(state, {
@@ -2257,10 +2707,13 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
2257
2707
  return { type: security.type, complete: false };
2258
2708
  },
2259
2709
 
2260
- async authLocal(name: string, opts?: {
2261
- onAuthorizeUrl?: (url: string) => void;
2262
- timeoutMs?: number;
2263
- }): Promise<{ complete: boolean }> {
2710
+ async authLocal(
2711
+ name: string,
2712
+ opts?: {
2713
+ onAuthorizeUrl?: (url: string) => void;
2714
+ timeoutMs?: number;
2715
+ },
2716
+ ): Promise<{ complete: boolean }> {
2264
2717
  // `ref.auth` is already proxy-aware — for proxied refs it returns
2265
2718
  // the authorizeUrl that the registry minted against its own
2266
2719
  // callback domain. Everything below is identical for local and
@@ -2283,7 +2736,11 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
2283
2736
  // owns the credential store, so the user needs to submit via
2284
2737
  // whatever UI the registry exposes. Supporting this through the
2285
2738
  // proxy would need a remote form endpoint — out of scope here.
2286
- 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
+ ) {
2287
2744
  if (proxy) {
2288
2745
  throw new Error(
2289
2746
  `Ref "${name}" is sourced from a proxied registry; submit credentials through ${proxy.agent} instead of a local form.`,
@@ -2320,19 +2777,23 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
2320
2777
  resolve({ complete: true });
2321
2778
  } else {
2322
2779
  res.writeHead(200, { "Content-Type": "text/html" });
2323
- res.end(renderCredentialForm(
2324
- name,
2325
- authResult.fields ?? result.fields!,
2326
- "Some credentials were missing or invalid.",
2327
- ));
2780
+ res.end(
2781
+ renderCredentialForm(
2782
+ name,
2783
+ authResult.fields ?? result.fields!,
2784
+ "Some credentials were missing or invalid.",
2785
+ ),
2786
+ );
2328
2787
  }
2329
2788
  } catch (err) {
2330
2789
  res.writeHead(500, { "Content-Type": "text/html" });
2331
- res.end(renderCredentialForm(
2332
- name,
2333
- result.fields!,
2334
- err instanceof Error ? err.message : String(err),
2335
- ));
2790
+ res.end(
2791
+ renderCredentialForm(
2792
+ name,
2793
+ result.fields!,
2794
+ err instanceof Error ? err.message : String(err),
2795
+ ),
2796
+ );
2336
2797
  }
2337
2798
  return;
2338
2799
  }
@@ -2379,7 +2840,8 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
2379
2840
  const state = reqUrl.searchParams.get("state");
2380
2841
 
2381
2842
  if (!code || !state) {
2382
- const error = reqUrl.searchParams.get("error") ?? "missing code/state";
2843
+ const error =
2844
+ reqUrl.searchParams.get("error") ?? "missing code/state";
2383
2845
  res.writeHead(400, { "Content-Type": "text/html" });
2384
2846
  res.end(`<h1>Error</h1><p>${error}</p>`);
2385
2847
  server.close();
@@ -2395,7 +2857,9 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
2395
2857
  resolve({ complete: cbResult.complete });
2396
2858
  } catch (err) {
2397
2859
  res.writeHead(500, { "Content-Type": "text/html" });
2398
- 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
+ );
2399
2863
  server.close();
2400
2864
  reject(err);
2401
2865
  }
@@ -2440,9 +2904,17 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
2440
2904
 
2441
2905
  const status = await ref.authStatus(name);
2442
2906
  const security = status.security;
2443
- const flows = security && "flows" in security
2444
- ? (security as { flows?: Record<string, { tokenUrl?: string; refreshUrl?: string }> }).flows
2445
- : undefined;
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;
2446
2918
  const authCodeFlow = flows?.authorizationCode;
2447
2919
  const tokenUrl = authCodeFlow?.refreshUrl ?? authCodeFlow?.tokenUrl;
2448
2920
  if (!tokenUrl) return null;
@@ -2469,7 +2941,7 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
2469
2941
 
2470
2942
  if (!res.ok) return null;
2471
2943
 
2472
- const data = await res.json() as Record<string, unknown>;
2944
+ const data = (await res.json()) as Record<string, unknown>;
2473
2945
  const newAccessToken = data.access_token as string | undefined;
2474
2946
  if (!newAccessToken) return null;
2475
2947
 
@@ -2487,7 +2959,14 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
2487
2959
  // Top-level callback handler
2488
2960
  // ==========================================
2489
2961
 
2490
- 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
+ }> {
2491
2970
  const pending = await consumePendingOAuth(params.state);
2492
2971
  if (!pending) {
2493
2972
  throw new Error(`No pending OAuth flow for state "${params.state}".`);
@@ -2503,13 +2982,19 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
2503
2982
 
2504
2983
  await storeRefSecret(pending.refName, "access_token", tokens.accessToken);
2505
2984
  if (tokens.refreshToken) {
2506
- await storeRefSecret(pending.refName, "refresh_token", tokens.refreshToken);
2985
+ await storeRefSecret(
2986
+ pending.refName,
2987
+ "refresh_token",
2988
+ tokens.refreshToken,
2989
+ );
2507
2990
  }
2508
2991
 
2509
2992
  let stateContext: Record<string, unknown> | undefined;
2510
2993
  try {
2511
2994
  stateContext = JSON.parse(atob(params.state));
2512
- } catch { /* state wasn't base64 JSON — legacy format */ }
2995
+ } catch {
2996
+ /* state wasn't base64 JSON — legacy format */
2997
+ }
2513
2998
 
2514
2999
  return { refName: pending.refName, complete: true, stateContext };
2515
3000
  }