@slashfi/agents-sdk 0.77.3 → 0.79.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,141 @@ 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
+ * Slim auth-field metadata cached so hosts can locally answer "is this
72
+ * ref ready to call?" without a registry round-trip. Mirrors the
73
+ * authoritative shape `auth-status` produces — same source of truth,
74
+ * just persisted.
75
+ *
76
+ * For each field name in the security scheme:
77
+ * - `required` — must end up satisfied for `ref.call` to work.
78
+ * - `automated` — adk fills this in itself (e.g. dynamic OAuth
79
+ * client registration). Doesn't need to be `present`
80
+ * in the user's config to count as satisfied.
81
+ */
82
+ export interface RegistryCacheAuthField {
83
+ required: boolean;
84
+ automated: boolean;
85
+ }
86
+
87
+ /**
88
+ * Per-ref cache entry. Updated as a side-effect of `ref.add()`,
89
+ * `ref.inspect()`, and `ref.authStatus()` whenever the registry
90
+ * response carries description / tool / security-scheme info.
91
+ * Identity-relative (lives next to the consumer-config that issued
92
+ * the registry call), so permission-filtered views stay consistent.
93
+ */
94
+ export interface RegistryCacheEntry {
95
+ /** Canonical agent path (e.g. `notion`). Stored for sanity/debug. */
96
+ ref: string;
97
+ description?: string;
98
+ tools?: RegistryCacheToolSummary[];
99
+ /**
100
+ * Auth field requirements derived from the registry's security
101
+ * scheme (extracted by `auth-status`). When present, hosts can
102
+ * compute "is this ref callable?" by intersecting these with the
103
+ * entry's `config` — no network round-trip needed. Absent when the
104
+ * scheme couldn't be fetched (e.g. registry was offline at add
105
+ * time); fall back to whatever heuristic the caller chooses.
106
+ *
107
+ * Note on proxy refs: when the entry is in `proxy` mode the
108
+ * security scheme is exposed by the *proxy*, and the answer to
109
+ * "is this callable?" lives server-side — `authFields` is omitted
110
+ * locally and hosts should treat proxy refs as authoritative
111
+ * regardless of entry-side fields.
112
+ */
113
+ authFields?: Record<string, RegistryCacheAuthField>;
114
+ /** ISO timestamp of the most recent registry round-trip that wrote this. */
115
+ fetchedAt: string;
116
+ }
117
+
118
+ /**
119
+ * On-disk shape of `registry-cache.json`. Keyed by `RefEntry.name` (local
120
+ * identifier) — the same key consumer-config uses, so hydration is a 1:1
121
+ * lookup.
122
+ */
123
+ export interface RegistryCache {
124
+ refs: Record<string, RegistryCacheEntry>;
125
+ }
126
+
127
+ /**
128
+ * "Is this ref ready to call?" answered locally using the cached
129
+ * security-scheme requirements. Mirrors the `complete` boolean
130
+ * `auth-status` returns, but doesn't need a network round-trip — the
131
+ * cached `authFields` capture what the registry said is required, and
132
+ * we evaluate satisfaction against the entry's current `config`.
133
+ *
134
+ * Behavior:
135
+ * - `mode: 'proxy'` refs → always true. Auth lives server-side; the
136
+ * proxy is the source of truth, no entry-side fields involved.
137
+ * - Cache miss (no `authFields` for this ref yet) → returns `null`,
138
+ * signaling "I don't know — caller should fall back to its own
139
+ * heuristic or call `auth-status` to populate the cache".
140
+ * - Cache hit → for every required, non-automated field, checks
141
+ * presence in `entry.config`. Mirrors the `present || resolvable`
142
+ * check in `auth-status` but evaluates against current config.
143
+ * `automated` fields (e.g. dynamic OAuth client_id) count as
144
+ * satisfied even when absent — adk supplies them at call time.
145
+ *
146
+ * Returning `null` for cache miss is intentional. A boolean would
147
+ * force callers to choose a default that's wrong half the time;
148
+ * `null` lets them branch explicitly.
149
+ */
150
+ export function isRefAuthComplete(
151
+ entry: RefEntry,
152
+ cacheEntry: RegistryCacheEntry | undefined,
153
+ ): boolean | null {
154
+ if (typeof entry === "string") return false;
155
+ if ((entry as { mode?: unknown }).mode === "proxy") return true;
156
+ const authFields = cacheEntry?.authFields;
157
+ if (!authFields) return null;
158
+ const config = entry.config ?? {};
159
+ for (const [field, info] of Object.entries(authFields)) {
160
+ if (!info.required) continue;
161
+ if (info.automated) continue;
162
+ if (!(field in config)) return false;
163
+ }
164
+ return true;
165
+ }
166
+
55
167
  // ============================================
56
168
  // Types
57
169
  // ============================================
@@ -128,7 +240,9 @@ export interface RegistryTestResult {
128
240
  }
129
241
 
130
242
  export interface AdkRegistryApi {
131
- add(entry: RegistryEntry): Promise<{ authRequirement?: RegistryAuthRequirement }>;
243
+ add(
244
+ entry: RegistryEntry,
245
+ ): Promise<{ authRequirement?: RegistryAuthRequirement }>;
132
246
  remove(nameOrUrl: string): Promise<boolean>;
133
247
  list(): Promise<RegistryEntry[]>;
134
248
  get(name: string): Promise<RegistryEntry | null>;
@@ -230,7 +344,7 @@ export interface AuthStartResult {
230
344
  * When populated, call() rejects unknown agent paths and tool names at compile time.
231
345
  */
232
346
  // eslint-disable-next-line @typescript-eslint/no-empty-interface
233
- export interface AdkAgentRegistry {}
347
+ export type AdkAgentRegistry = {};
234
348
 
235
349
  /** @internal Helper types for conditional call() signature */
236
350
  type AgentPath = keyof AdkAgentRegistry;
@@ -244,9 +358,17 @@ type ParamsOf<
244
358
 
245
359
  type AdkRefCallFn = keyof AdkAgentRegistry extends never
246
360
  ? // No registry — loose fallback
247
- (name: string, tool: string, params?: Record<string, unknown>) => Promise<CallAgentResponse>
361
+ (
362
+ name: string,
363
+ tool: string,
364
+ params?: Record<string, unknown>,
365
+ ) => Promise<CallAgentResponse>
248
366
  : // Registry populated — strict typed overload
249
- <A extends AgentPath, T extends ToolsOf<A>>(name: A, tool: T, params: ParamsOf<A, T>) => Promise<CallAgentResponse>;
367
+ <A extends AgentPath, T extends ToolsOf<A>>(
368
+ name: A,
369
+ tool: T,
370
+ params: ParamsOf<A, T>,
371
+ ) => Promise<CallAgentResponse>;
250
372
 
251
373
  export interface AdkRefApi {
252
374
  add(entry: RefAddInput): Promise<{ security: SecuritySchemeSummary | null }>;
@@ -254,7 +376,10 @@ export interface AdkRefApi {
254
376
  list(): Promise<ResolvedRef[]>;
255
377
  get(name: string): Promise<ResolvedRef | null>;
256
378
  update(name: string, updates: Partial<RefEntry>): Promise<boolean>;
257
- inspect(name: string, options?: { full?: boolean }): Promise<AgentInspection | null>;
379
+ inspect(
380
+ name: string,
381
+ options?: { full?: boolean },
382
+ ): Promise<AgentInspection | null>;
258
383
  call: AdkRefCallFn;
259
384
  resources(name: string): Promise<CallAgentResponse>;
260
385
  read(name: string, uris: string[]): Promise<CallAgentResponse>;
@@ -265,37 +390,43 @@ export interface AdkRefApi {
265
390
  * Call adk.handleCallback() when the callback arrives, or use
266
391
  * adk.ref.authLocal() to spin up a local server and block.
267
392
  */
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>;
393
+ auth(
394
+ name: string,
395
+ opts?: {
396
+ /** For API key / bearer auth: the key/token value (single-key shorthand) */
397
+ apiKey?: string;
398
+ /**
399
+ * Credentials map for multi-field auth. Keys match the `name` field
400
+ * from AuthChallengeField (e.g. { "api_key": "xxx", "app_key": "yyy" }).
401
+ * For single-key apiKey or http bearer, `apiKey` shorthand also works.
402
+ */
403
+ credentials?: Record<string, string>;
404
+ /** Extra context to encode in the OAuth state (e.g., tenant/user IDs for multi-tenant callbacks) */
405
+ stateContext?: Record<string, unknown>;
406
+ /** Additional scopes to request (e.g., optional scopes declared by the agent) */
407
+ scopes?: string[];
408
+ /**
409
+ * Opt out of proxy routing when the ref's source registry has
410
+ * `proxy: { mode: 'optional' }`. Ignored for `mode: 'required'`.
411
+ * Defaults to `false` — if a registry offers a proxy we use it.
412
+ */
413
+ preferLocal?: boolean;
414
+ },
415
+ ): Promise<AuthStartResult>;
288
416
  /**
289
417
  * Run the full OAuth flow locally: start auth, spin up a callback
290
418
  * server, open the browser, wait for the redirect, exchange tokens.
291
419
  * Resolves when auth is complete or times out.
292
420
  */
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 }>;
421
+ authLocal(
422
+ name: string,
423
+ opts?: {
424
+ /** Called with the authorize URL (e.g. to open in browser) */
425
+ onAuthorizeUrl?: (url: string) => void;
426
+ /** Timeout in ms (default 300_000 = 5 min) */
427
+ timeoutMs?: number;
428
+ },
429
+ ): Promise<{ complete: boolean }>;
299
430
  /**
300
431
  * Refresh an OAuth access token using a stored refresh_token.
301
432
  * Returns the new access_token, or null if refresh is not possible
@@ -317,7 +448,11 @@ export interface Adk {
317
448
  * Parse the callback query params and pass them here.
318
449
  * @returns the ref name and whether auth is complete
319
450
  */
320
- handleCallback(params: { code: string; state: string }): Promise<{ refName: string; complete: boolean; stateContext?: Record<string, unknown> }>;
451
+ handleCallback(params: { code: string; state: string }): Promise<{
452
+ refName: string;
453
+ complete: boolean;
454
+ stateContext?: Record<string, unknown>;
455
+ }>;
321
456
  }
322
457
 
323
458
  // ============================================
@@ -379,9 +514,19 @@ async function decryptConfigSecrets(
379
514
  const result: Record<string, unknown> = {};
380
515
  for (const [key, value] of Object.entries(obj)) {
381
516
  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);
517
+ result[key] = await decryptSecret(
518
+ value.slice(SECRET_PREFIX.length),
519
+ encryptionKey,
520
+ );
521
+ } else if (
522
+ value !== null &&
523
+ typeof value === "object" &&
524
+ !Array.isArray(value)
525
+ ) {
526
+ result[key] = await decryptConfigSecrets(
527
+ value as Record<string, unknown>,
528
+ encryptionKey,
529
+ );
385
530
  } else {
386
531
  result[key] = value;
387
532
  }
@@ -399,7 +544,7 @@ async function decryptConfigSecrets(
399
544
  * Fallback: _httpStatus from tool result body
400
545
  */
401
546
  function isUnauthorized(result: unknown): boolean {
402
- if (!result || typeof result !== 'object') return false;
547
+ if (!result || typeof result !== "object") return false;
403
548
  const r = result as Record<string, unknown>;
404
549
  // Primary: HTTP status forwarded by the registry and set by callRegistry
405
550
  if (r.httpStatus === 401) return true;
@@ -414,19 +559,29 @@ function isUnauthorized(result: unknown): boolean {
414
559
  // ============================================
415
560
 
416
561
  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) => `
562
+ s
563
+ .replace(/&/g, "&amp;")
564
+ .replace(/</g, "&lt;")
565
+ .replace(/>/g, "&gt;")
566
+ .replace(/"/g, "&quot;");
567
+
568
+ function renderCredentialForm(
569
+ name: string,
570
+ fields: AuthChallengeField[],
571
+ error?: string,
572
+ ): string {
573
+ const fieldHtml = fields
574
+ .map(
575
+ (f) => `
421
576
  <div class="field">
422
577
  <label for="${esc(f.name)}">${esc(f.label)}</label>
423
578
  ${f.description ? `<p class="desc">${esc(f.description)}</p>` : ""}
424
579
  <input id="${esc(f.name)}" name="${esc(f.name)}" type="${f.secret ? "password" : "text"}" required autocomplete="off" spellcheck="false" />
425
- </div>`).join("");
580
+ </div>`,
581
+ )
582
+ .join("");
426
583
 
427
- const errorHtml = error
428
- ? `<div class="error">${esc(error)}</div>`
429
- : "";
584
+ const errorHtml = error ? `<div class="error">${esc(error)}</div>` : "";
430
585
 
431
586
  return `<!DOCTYPE html>
432
587
  <html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
@@ -478,7 +633,6 @@ p{font-size:14px;color:#a3a3a3}
478
633
  }
479
634
 
480
635
  export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
481
-
482
636
  async function readConfig(): Promise<ConsumerConfig> {
483
637
  const content = await fs.readFile(CONFIG_PATH);
484
638
  if (!content) return {};
@@ -493,11 +647,138 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
493
647
  await fs.writeFile(CONFIG_PATH, JSON.stringify(config, null, 2));
494
648
  }
495
649
 
650
+ // -------------------------------------------------------------------------
651
+ // Registry cache helpers
652
+ //
653
+ // The cache is purely an internal optimization for the adk's read paths
654
+ // (`ref.list()`, `ref.get()`). Writes happen as side-effects of methods
655
+ // that already call the registry (`ref.add()`, `ref.inspect()`); the
656
+ // public surface never grows new methods. Cache failures (missing file,
657
+ // malformed JSON, fs errors during write) are swallowed so the registry
658
+ // cache can never break a registry operation.
659
+ // -------------------------------------------------------------------------
660
+
661
+ async function readRegistryCache(): Promise<RegistryCache> {
662
+ try {
663
+ const content = await fs.readFile(REGISTRY_CACHE_PATH);
664
+ if (!content) return { refs: {} };
665
+ const parsed = JSON.parse(content) as RegistryCache;
666
+ return { refs: parsed.refs ?? {} };
667
+ } catch {
668
+ return { refs: {} };
669
+ }
670
+ }
671
+
672
+ async function writeRegistryCache(cache: RegistryCache): Promise<void> {
673
+ try {
674
+ await fs.writeFile(REGISTRY_CACHE_PATH, JSON.stringify(cache, null, 2));
675
+ } catch {
676
+ // Best-effort. A failed cache write should never break the operation
677
+ // that triggered it.
678
+ }
679
+ }
680
+
681
+ /**
682
+ * Project an inspect/list response into the slim shape we cache. Drops
683
+ * `inputSchema` (too large) and `fullTokens` (registry-internal). Returns
684
+ * undefined if the response carries nothing worth caching.
685
+ */
686
+ function buildCacheEntry(
687
+ ref: string,
688
+ info:
689
+ | {
690
+ description?: string;
691
+ tools?: Array<{ name: string; description?: string }>;
692
+ toolSummaries?: Array<{ name: string; description?: string }>;
693
+ }
694
+ | null
695
+ | undefined,
696
+ ): RegistryCacheEntry | undefined {
697
+ if (!info) return undefined;
698
+ const toolSource = info.tools ?? info.toolSummaries;
699
+ const tools = toolSource?.map((t) => {
700
+ const slim: RegistryCacheToolSummary = { name: t.name };
701
+ if (t.description !== undefined) slim.description = t.description;
702
+ return slim;
703
+ });
704
+ if (info.description === undefined && (!tools || tools.length === 0)) {
705
+ return undefined;
706
+ }
707
+ const entry: RegistryCacheEntry = {
708
+ ref,
709
+ fetchedAt: new Date().toISOString(),
710
+ };
711
+ if (info.description !== undefined) entry.description = info.description;
712
+ if (tools && tools.length > 0) entry.tools = tools;
713
+ return entry;
714
+ }
715
+
716
+ async function upsertRegistryCacheEntry(
717
+ name: string,
718
+ entry: RegistryCacheEntry | undefined,
719
+ ): Promise<void> {
720
+ if (!entry) return;
721
+ const cache = await readRegistryCache();
722
+ cache.refs[name] = entry;
723
+ await writeRegistryCache(cache);
724
+ }
725
+
726
+ /**
727
+ * Merge `authFields` into an existing cache entry without clobbering
728
+ * description/tools, or create a minimal entry if one doesn't exist
729
+ * yet. Called from `authStatus` so the slim {required, automated}
730
+ * shape is always available for `isRefAuthComplete` to answer
731
+ * locally on subsequent calls.
732
+ */
733
+ async function upsertRegistryCacheAuthFields(
734
+ name: string,
735
+ ref: string,
736
+ authFields: Record<string, RegistryCacheAuthField>,
737
+ ): Promise<void> {
738
+ const cache = await readRegistryCache();
739
+ const existing = cache.refs[name];
740
+ cache.refs[name] = {
741
+ ...(existing ?? { ref, fetchedAt: new Date().toISOString() }),
742
+ authFields,
743
+ // Refresh fetchedAt so freshness telemetry stays accurate.
744
+ fetchedAt: new Date().toISOString(),
745
+ };
746
+ await writeRegistryCache(cache);
747
+ }
748
+
749
+ async function removeRegistryCacheEntry(name: string): Promise<void> {
750
+ const cache = await readRegistryCache();
751
+ if (!(name in cache.refs)) return;
752
+ delete cache.refs[name];
753
+ await writeRegistryCache(cache);
754
+ }
755
+
756
+ /**
757
+ * Hydrate a `ResolvedRef` with cached registry metadata when available.
758
+ * Pure: never mutates input. Leaves `description` / `tools` undefined when
759
+ * the cache has no entry, so callers can apply their own UX fallback.
760
+ */
761
+ function hydrateFromCache(
762
+ ref: ResolvedRef,
763
+ cache: RegistryCache,
764
+ ): ResolvedRef {
765
+ const cached = cache.refs[ref.name];
766
+ if (!cached) return ref;
767
+ const next: ResolvedRef = { ...ref };
768
+ if (cached.description !== undefined) next.description = cached.description;
769
+ if (cached.tools !== undefined) next.tools = cached.tools;
770
+ return next;
771
+ }
772
+
496
773
  /**
497
774
  * Store a secret value in a ref's config, encrypted if encryptionKey is set.
498
775
  * The value is stored inline as "secret:<encrypted>" in consumer-config.json.
499
776
  */
500
- async function storeRefSecret(name: string, key: string, value: string): Promise<void> {
777
+ async function storeRefSecret(
778
+ name: string,
779
+ key: string,
780
+ value: string,
781
+ ): Promise<void> {
501
782
  const stored = options.encryptionKey
502
783
  ? `${SECRET_PREFIX}${await encryptSecret(value, options.encryptionKey)}`
503
784
  : value;
@@ -510,13 +791,19 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
510
791
  await writeConfig({ ...config, refs });
511
792
  }
512
793
 
513
- async function readRefSecret(name: string, key: string): Promise<string | null> {
794
+ async function readRefSecret(
795
+ name: string,
796
+ key: string,
797
+ ): Promise<string | null> {
514
798
  const config = await readConfig();
515
799
  const entry = findRef(config.refs ?? [], name);
516
800
  const value = entry?.config?.[key];
517
801
  if (typeof value !== "string") return null;
518
802
  if (value.startsWith(SECRET_PREFIX) && options.encryptionKey) {
519
- return decryptSecret(value.slice(SECRET_PREFIX.length), options.encryptionKey);
803
+ return decryptSecret(
804
+ value.slice(SECRET_PREFIX.length),
805
+ options.encryptionKey,
806
+ );
520
807
  }
521
808
  return value;
522
809
  }
@@ -601,23 +888,36 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
601
888
  createdAt: number;
602
889
  }
603
890
 
604
- async function readPendingOAuth(): Promise<Record<string, PendingOAuthState>> {
891
+ async function readPendingOAuth(): Promise<
892
+ Record<string, PendingOAuthState>
893
+ > {
605
894
  const content = await fs.readFile(PENDING_OAUTH_PATH);
606
895
  if (!content) return {};
607
- try { return JSON.parse(content); } catch { return {}; }
896
+ try {
897
+ return JSON.parse(content);
898
+ } catch {
899
+ return {};
900
+ }
608
901
  }
609
902
 
610
- async function writePendingOAuth(pending: Record<string, PendingOAuthState>): Promise<void> {
903
+ async function writePendingOAuth(
904
+ pending: Record<string, PendingOAuthState>,
905
+ ): Promise<void> {
611
906
  await fs.writeFile(PENDING_OAUTH_PATH, JSON.stringify(pending, null, 2));
612
907
  }
613
908
 
614
- async function storePendingOAuth(state: string, data: PendingOAuthState): Promise<void> {
909
+ async function storePendingOAuth(
910
+ state: string,
911
+ data: PendingOAuthState,
912
+ ): Promise<void> {
615
913
  const pending = await readPendingOAuth();
616
914
  pending[state] = data;
617
915
  await writePendingOAuth(pending);
618
916
  }
619
917
 
620
- async function consumePendingOAuth(state: string): Promise<PendingOAuthState | null> {
918
+ async function consumePendingOAuth(
919
+ state: string,
920
+ ): Promise<PendingOAuthState | null> {
621
921
  const pending = await readPendingOAuth();
622
922
  const data = pending[state] ?? null;
623
923
  if (data) {
@@ -646,7 +946,10 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
646
946
  let reqId = 0;
647
947
  let sessionId: string | undefined;
648
948
  async function rpc(method: string, rpcParams?: Record<string, unknown>) {
649
- const reqHeaders = { ...headers, ...(sessionId ? { "Mcp-Session-Id": sessionId } : {}) };
949
+ const reqHeaders = {
950
+ ...headers,
951
+ ...(sessionId ? { "Mcp-Session-Id": sessionId } : {}),
952
+ };
650
953
  const res = await globalThis.fetch(url, {
651
954
  method: "POST",
652
955
  headers: reqHeaders,
@@ -658,7 +961,9 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
658
961
  }),
659
962
  });
660
963
  if (!res.ok) {
661
- throw new Error(`MCP ${method} failed (${res.status}): ${await res.text().catch(() => "unknown")}`);
964
+ throw new Error(
965
+ `MCP ${method} failed (${res.status}): ${await res.text().catch(() => "unknown")}`,
966
+ );
662
967
  }
663
968
 
664
969
  const contentType = res.headers.get("content-type") ?? "";
@@ -676,18 +981,23 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
676
981
  try {
677
982
  const json = JSON.parse(line.slice(6));
678
983
  if (json.id === reqId) {
679
- if (json.error) throw new Error(`MCP RPC error: ${json.error.message}`);
984
+ if (json.error)
985
+ throw new Error(`MCP RPC error: ${json.error.message}`);
680
986
  return json.result;
681
987
  }
682
988
  } catch (e) {
683
- if (e instanceof Error && e.message.startsWith("MCP RPC")) throw e;
989
+ if (e instanceof Error && e.message.startsWith("MCP RPC"))
990
+ throw e;
684
991
  }
685
992
  }
686
993
  }
687
994
  return undefined;
688
995
  }
689
996
 
690
- const json = await res.json() as { result?: unknown; error?: { message: string } };
997
+ const json = (await res.json()) as {
998
+ result?: unknown;
999
+ error?: { message: string };
1000
+ };
691
1001
  if (json.error) throw new Error(`MCP RPC error: ${json.error.message}`);
692
1002
  return json.result;
693
1003
  }
@@ -700,17 +1010,34 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
700
1010
  });
701
1011
  await rpc("notifications/initialized").catch(() => {});
702
1012
 
703
- const result = await rpc("tools/call", { name: toolName, arguments: params }) as
704
- { content?: Array<{ type: string; text?: string }>; isError?: boolean };
1013
+ const result = (await rpc("tools/call", {
1014
+ name: toolName,
1015
+ arguments: params,
1016
+ })) as {
1017
+ content?: Array<{ type: string; text?: string }>;
1018
+ isError?: boolean;
1019
+ };
705
1020
 
706
1021
  const textContent = result?.content?.find((c) => c.type === "text");
707
1022
  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; }
1023
+ try {
1024
+ return {
1025
+ success: true,
1026
+ result: JSON.parse(textContent.text),
1027
+ } as CallAgentResponse;
1028
+ } catch {
1029
+ return {
1030
+ success: true,
1031
+ result: textContent.text,
1032
+ } as CallAgentResponse;
1033
+ }
710
1034
  }
711
1035
  return { success: true, result } as CallAgentResponse;
712
1036
  } catch (err) {
713
- return { success: false, error: err instanceof Error ? err.message : String(err) } as CallAgentResponse;
1037
+ return {
1038
+ success: false,
1039
+ error: err instanceof Error ? err.message : String(err),
1040
+ } as CallAgentResponse;
714
1041
  }
715
1042
  }
716
1043
 
@@ -720,11 +1047,13 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
720
1047
  }
721
1048
 
722
1049
  /** 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> {
1050
+ async function tryFetchOAuthMetadata(
1051
+ url: string,
1052
+ ): Promise<import("./mcp-client.js").OAuthServerMetadata | null> {
724
1053
  try {
725
1054
  const res = await globalThis.fetch(url);
726
1055
  if (!res.ok) return null;
727
- const data = await res.json() as Record<string, unknown>;
1056
+ const data = (await res.json()) as Record<string, unknown>;
728
1057
  if (data.authorization_endpoint && data.token_endpoint) {
729
1058
  return data as unknown as import("./mcp-client.js").OAuthServerMetadata;
730
1059
  }
@@ -778,7 +1107,9 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
778
1107
  * Build a consumer that includes the ref's sourceRegistry if present.
779
1108
  * This ensures calls/inspect route to the correct registry endpoint.
780
1109
  */
781
- async function buildConsumerForRef(entry: RefEntry): Promise<RegistryConsumer> {
1110
+ async function buildConsumerForRef(
1111
+ entry: RefEntry,
1112
+ ): Promise<RegistryConsumer> {
782
1113
  const config = await readConfig();
783
1114
  let registries = config.registries ?? [];
784
1115
 
@@ -816,7 +1147,10 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
816
1147
  * Resolve the correct registry for a ref.
817
1148
  * If the ref has a sourceRegistry, use that; otherwise fall back to the first registry.
818
1149
  */
819
- function resolveRegistryForRef(consumer: RegistryConsumer, entry: RefEntry): ResolvedRegistry {
1150
+ function resolveRegistryForRef(
1151
+ consumer: RegistryConsumer,
1152
+ entry: RefEntry,
1153
+ ): ResolvedRegistry {
820
1154
  const regs = consumer.registries();
821
1155
  if (entry.sourceRegistry?.url) {
822
1156
  const match = regs.find((r) => r.url === entry.sourceRegistry!.url);
@@ -837,7 +1171,9 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
837
1171
  * the ref is sourced from a raw URL (no registry), in which case proxy routing
838
1172
  * does not apply.
839
1173
  */
840
- async function findRegistryEntryForRef(entry: RefEntry): Promise<RegistryEntry | null> {
1174
+ async function findRegistryEntryForRef(
1175
+ entry: RefEntry,
1176
+ ): Promise<RegistryEntry | null> {
841
1177
  const sourceUrl = entry.sourceRegistry?.url;
842
1178
  if (!sourceUrl) return null;
843
1179
  const config = await readConfig();
@@ -890,7 +1226,10 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
890
1226
  sourceRegistry: { url: reg.url, agentPath: agent },
891
1227
  });
892
1228
  const resolved = consumer.registries().find((r) => r.url === reg.url);
893
- if (!resolved) throw new Error(`Registry ${reg.url} not resolvable for proxy forwarding`);
1229
+ if (!resolved)
1230
+ throw new Error(
1231
+ `Registry ${reg.url} not resolvable for proxy forwarding`,
1232
+ );
894
1233
 
895
1234
  const response = await consumer.callRegistry(resolved, {
896
1235
  action: "execute_tool",
@@ -900,8 +1239,13 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
900
1239
  });
901
1240
 
902
1241
  if (!response.success) {
903
- const errResponse = response as { success: false; error?: string; code?: string };
904
- const msg = errResponse.error ?? `Proxy ${agent}.ref(${operation}) failed`;
1242
+ const errResponse = response as {
1243
+ success: false;
1244
+ error?: string;
1245
+ code?: string;
1246
+ };
1247
+ const msg =
1248
+ errResponse.error ?? `Proxy ${agent}.ref(${operation}) failed`;
905
1249
  throw new Error(msg);
906
1250
  }
907
1251
  return (response as { success: true; result: unknown }).result as T;
@@ -970,7 +1314,8 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
970
1314
  const rName = registryDisplayName(r);
971
1315
  if (rName !== nameOrUrl && registryUrl(r) !== nameOrUrl) return r;
972
1316
  found = true;
973
- const existing: RegistryEntry = typeof r === "string" ? { url: r } : { ...r };
1317
+ const existing: RegistryEntry =
1318
+ typeof r === "string" ? { url: r } : { ...r };
974
1319
  mutate(existing);
975
1320
  return existing;
976
1321
  });
@@ -983,11 +1328,16 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
983
1328
  * Decrypt a `secret:`-prefixed value if we hold the encryption key. Plaintext
984
1329
  * values pass through unchanged so dev configs keep working.
985
1330
  */
986
- async function revealSecret(value: string | undefined): Promise<string | undefined> {
1331
+ async function revealSecret(
1332
+ value: string | undefined,
1333
+ ): Promise<string | undefined> {
987
1334
  if (!value) return value;
988
1335
  if (!value.startsWith(SECRET_PREFIX)) return value;
989
1336
  if (!options.encryptionKey) return undefined;
990
- return decryptSecret(value.slice(SECRET_PREFIX.length), options.encryptionKey);
1337
+ return decryptSecret(
1338
+ value.slice(SECRET_PREFIX.length),
1339
+ options.encryptionKey,
1340
+ );
991
1341
  }
992
1342
 
993
1343
  /**
@@ -1002,7 +1352,8 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
1002
1352
  const target = findRegistry(config.registries ?? [], nameOrUrl);
1003
1353
  if (!target || typeof target === "string") return false;
1004
1354
  const oauth = target.oauth;
1005
- if (!oauth?.refreshToken || !oauth.tokenEndpoint || !oauth.clientId) return false;
1355
+ if (!oauth?.refreshToken || !oauth.tokenEndpoint || !oauth.clientId)
1356
+ return false;
1006
1357
 
1007
1358
  const refreshToken = await revealSecret(oauth.refreshToken);
1008
1359
  const clientSecret = await revealSecret(oauth.clientSecret);
@@ -1044,7 +1395,8 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
1044
1395
  try {
1045
1396
  return await fn();
1046
1397
  } catch (err) {
1047
- if (!(err instanceof AdkError) || err.code !== "registry_auth_required") throw err;
1398
+ if (!(err instanceof AdkError) || err.code !== "registry_auth_required")
1399
+ throw err;
1048
1400
  let refreshed = false;
1049
1401
  try {
1050
1402
  refreshed = await refreshRegistryToken(nameOrUrl);
@@ -1088,7 +1440,9 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
1088
1440
  }
1089
1441
 
1090
1442
  const registry: AdkRegistryApi = {
1091
- async add(entry: RegistryEntry): Promise<{ authRequirement?: RegistryAuthRequirement }> {
1443
+ async add(
1444
+ entry: RegistryEntry,
1445
+ ): Promise<{ authRequirement?: RegistryAuthRequirement }> {
1092
1446
  const config = await readConfig();
1093
1447
  const alias = entry.name ?? entry.url;
1094
1448
  const registries = (config.registries ?? []).filter(
@@ -1135,7 +1489,9 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
1135
1489
  ...final,
1136
1490
  proxy: {
1137
1491
  mode: discovered.proxy.mode,
1138
- ...(discovered.proxy.agent && { agent: discovered.proxy.agent }),
1492
+ ...(discovered.proxy.agent && {
1493
+ agent: discovered.proxy.agent,
1494
+ }),
1139
1495
  },
1140
1496
  };
1141
1497
  }
@@ -1156,7 +1512,8 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
1156
1512
  if (!config.registries?.length) return false;
1157
1513
  const before = config.registries.length;
1158
1514
  const registries = config.registries.filter(
1159
- (r) => registryDisplayName(r) !== nameOrUrl && registryUrl(r) !== nameOrUrl,
1515
+ (r) =>
1516
+ registryDisplayName(r) !== nameOrUrl && registryUrl(r) !== nameOrUrl,
1160
1517
  );
1161
1518
  if (registries.length === before) return false;
1162
1519
  await writeConfig({ ...config, registries });
@@ -1177,7 +1534,10 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
1177
1534
  return typeof target === "string" ? { url: target } : target;
1178
1535
  },
1179
1536
 
1180
- async update(name: string, updates: Partial<RegistryEntry>): Promise<boolean> {
1537
+ async update(
1538
+ name: string,
1539
+ updates: Partial<RegistryEntry>,
1540
+ ): Promise<boolean> {
1181
1541
  const config = await readConfig();
1182
1542
  if (!config.registries?.length) return false;
1183
1543
  let found = false;
@@ -1185,11 +1545,13 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
1185
1545
  const rName = registryDisplayName(r);
1186
1546
  if (rName !== name && registryUrl(r) !== name) return r;
1187
1547
  found = true;
1188
- const existing: RegistryEntry = typeof r === "string" ? { url: r } : { ...r };
1548
+ const existing: RegistryEntry =
1549
+ typeof r === "string" ? { url: r } : { ...r };
1189
1550
  if (updates.url) existing.url = updates.url;
1190
1551
  if (updates.name) existing.name = updates.name;
1191
1552
  if (updates.auth) existing.auth = updates.auth;
1192
- if (updates.headers) existing.headers = { ...existing.headers, ...updates.headers };
1553
+ if (updates.headers)
1554
+ existing.headers = { ...existing.headers, ...updates.headers };
1193
1555
  if (updates.proxy !== undefined) existing.proxy = updates.proxy;
1194
1556
  return existing;
1195
1557
  });
@@ -1201,7 +1563,8 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
1201
1563
  async browse(name: string, query?: string): Promise<AgentListEntry[]> {
1202
1564
  const config = await readConfig();
1203
1565
  const target = findRegistry(config.registries ?? [], name);
1204
- if (target && typeof target !== "string") assertRegistryAuthorized(target);
1566
+ if (target && typeof target !== "string")
1567
+ assertRegistryAuthorized(target);
1205
1568
  return callWithRefresh(name, async () => {
1206
1569
  const consumer = await buildConsumer(name);
1207
1570
  const url = target ? registryUrl(target) : name;
@@ -1212,7 +1575,8 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
1212
1575
  async inspect(name: string): Promise<RegistryConfiguration> {
1213
1576
  const config = await readConfig();
1214
1577
  const target = findRegistry(config.registries ?? [], name);
1215
- if (target && typeof target !== "string") assertRegistryAuthorized(target);
1578
+ if (target && typeof target !== "string")
1579
+ assertRegistryAuthorized(target);
1216
1580
  return callWithRefresh(name, async () => {
1217
1581
  const consumer = await buildConsumer(name);
1218
1582
  const url = target ? registryUrl(target) : name;
@@ -1224,7 +1588,9 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
1224
1588
  const config = await readConfig();
1225
1589
  const registries = config.registries ?? [];
1226
1590
  const targets = name
1227
- ? registries.filter((r) => registryDisplayName(r) === name || registryUrl(r) === name)
1591
+ ? registries.filter(
1592
+ (r) => registryDisplayName(r) === name || registryUrl(r) === name,
1593
+ )
1228
1594
  : registries;
1229
1595
 
1230
1596
  const results = await Promise.allSettled(
@@ -1265,7 +1631,12 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
1265
1631
  return results.map((r) =>
1266
1632
  r.status === "fulfilled"
1267
1633
  ? r.value
1268
- : { name: "unknown", url: "unknown", status: "error" as const, error: "unknown" },
1634
+ : {
1635
+ name: "unknown",
1636
+ url: "unknown",
1637
+ status: "error" as const,
1638
+ error: "unknown",
1639
+ },
1269
1640
  );
1270
1641
  },
1271
1642
 
@@ -1337,7 +1708,10 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
1337
1708
  });
1338
1709
  // Re-read so the flow below sees the fresh requirement.
1339
1710
  const refreshed = await readConfig();
1340
- const refreshedTarget = findRegistry(refreshed.registries ?? [], nameOrUrl);
1711
+ const refreshedTarget = findRegistry(
1712
+ refreshed.registries ?? [],
1713
+ nameOrUrl,
1714
+ );
1341
1715
  if (refreshedTarget && typeof refreshedTarget !== "string") {
1342
1716
  Object.assign(target, refreshedTarget);
1343
1717
  }
@@ -1403,17 +1777,21 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
1403
1777
  );
1404
1778
 
1405
1779
  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
- });
1780
+ const { url: authorizeUrl, codeVerifier } =
1781
+ await buildOAuthAuthorizeUrl({
1782
+ authorizationEndpoint: metadata.authorization_endpoint,
1783
+ clientId: registration.clientId,
1784
+ redirectUri,
1785
+ scopes: req.scopes,
1786
+ state,
1787
+ });
1413
1788
 
1414
1789
  return new Promise<{ complete: boolean }>((resolve, reject) => {
1415
1790
  const server = createServer(async (reqIn, resOut) => {
1416
- const reqUrl = new URL(reqIn.url ?? "/", `http://localhost:${port}`);
1791
+ const reqUrl = new URL(
1792
+ reqIn.url ?? "/",
1793
+ `http://localhost:${port}`,
1794
+ );
1417
1795
  if (reqUrl.pathname !== "/callback") {
1418
1796
  resOut.writeHead(404);
1419
1797
  resOut.end();
@@ -1423,7 +1801,8 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
1423
1801
  const code = reqUrl.searchParams.get("code");
1424
1802
  const returnedState = reqUrl.searchParams.get("state");
1425
1803
  if (!code || returnedState !== state) {
1426
- const error = reqUrl.searchParams.get("error") ?? "missing code/state";
1804
+ const error =
1805
+ reqUrl.searchParams.get("error") ?? "missing code/state";
1427
1806
  resOut.writeHead(400, { "Content-Type": "text/html" });
1428
1807
  resOut.end(`<h1>Error</h1><p>${esc(error)}</p>`);
1429
1808
  server.close();
@@ -1439,13 +1818,16 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
1439
1818
  }
1440
1819
 
1441
1820
  try {
1442
- const tokens = await exchangeCodeForTokens(metadata.token_endpoint, {
1443
- code,
1444
- codeVerifier,
1445
- clientId: registration.clientId,
1446
- clientSecret: registration.clientSecret,
1447
- redirectUri,
1448
- });
1821
+ const tokens = await exchangeCodeForTokens(
1822
+ metadata.token_endpoint,
1823
+ {
1824
+ code,
1825
+ codeVerifier,
1826
+ clientId: registration.clientId,
1827
+ clientSecret: registration.clientSecret,
1828
+ redirectUri,
1829
+ },
1830
+ );
1449
1831
  const expiresAt = tokens.expiresIn
1450
1832
  ? new Date(Date.now() + tokens.expiresIn * 1000).toISOString()
1451
1833
  : undefined;
@@ -1527,7 +1909,9 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
1527
1909
  const token = params.get("token");
1528
1910
  if (!token) {
1529
1911
  resOut.writeHead(200, { "Content-Type": "text/html" });
1530
- resOut.end(renderCredentialForm(displayName, fields, "Token is required."));
1912
+ resOut.end(
1913
+ renderCredentialForm(displayName, fields, "Token is required."),
1914
+ );
1531
1915
  return;
1532
1916
  }
1533
1917
  try {
@@ -1571,7 +1955,9 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
1571
1955
  // ==========================================
1572
1956
 
1573
1957
  const ref: AdkRefApi = {
1574
- async add(entryInput: RefAddInput): Promise<{ security: SecuritySchemeSummary | null }> {
1958
+ async add(
1959
+ entryInput: RefAddInput,
1960
+ ): Promise<{ security: SecuritySchemeSummary | null }> {
1575
1961
  let security: SecuritySchemeSummary | null = null;
1576
1962
 
1577
1963
  const config = await readConfig();
@@ -1593,7 +1979,10 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
1593
1979
  if (entry.sourceRegistry?.url) {
1594
1980
  entry = { ...entry, scheme: "registry" };
1595
1981
  } else if (entry.url) {
1596
- entry = { ...entry, scheme: entry.url.startsWith("http") ? "https" : "mcp" };
1982
+ entry = {
1983
+ ...entry,
1984
+ scheme: entry.url.startsWith("http") ? "https" : "mcp",
1985
+ };
1597
1986
  } else {
1598
1987
  throw new AdkError({
1599
1988
  code: "REF_INVALID",
@@ -1623,6 +2012,7 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
1623
2012
  });
1624
2013
  }
1625
2014
 
2015
+ let cacheEntry: RegistryCacheEntry | undefined;
1626
2016
  if (hasRegistries || entry.sourceRegistry?.url) {
1627
2017
  try {
1628
2018
  const consumer = await buildConsumerForRef(entry);
@@ -1631,11 +2021,11 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
1631
2021
 
1632
2022
  const requiresValidation = !!entry.sourceRegistry;
1633
2023
  if (requiresValidation) {
1634
- const hasContent = info && (
1635
- info.description ||
1636
- (info.tools && info.tools.length > 0) ||
1637
- (info.toolSummaries && info.toolSummaries.length > 0)
1638
- );
2024
+ const hasContent =
2025
+ info &&
2026
+ (info.description ||
2027
+ (info.tools && info.tools.length > 0) ||
2028
+ (info.toolSummaries && info.toolSummaries.length > 0));
1639
2029
  if (!hasContent) {
1640
2030
  // Inspect returned empty — fall back to browse to check if agent exists
1641
2031
  const registryUrl = entry.sourceRegistry?.url;
@@ -1644,7 +2034,9 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
1644
2034
  try {
1645
2035
  const agents = await consumer.browse(registryUrl);
1646
2036
  const stripAt = (s: string) => s.replace(/^@/, "");
1647
- const refKey = stripAt(entry.sourceRegistry?.agentPath ?? entry.ref);
2037
+ const refKey = stripAt(
2038
+ entry.sourceRegistry?.agentPath ?? entry.ref,
2039
+ );
1648
2040
  foundInBrowse = agents.some(
1649
2041
  (a) => a.path === entry.ref || stripAt(a.path) === refKey,
1650
2042
  );
@@ -1658,7 +2050,11 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
1658
2050
  code: "REF_NOT_FOUND",
1659
2051
  message: `Agent "${entry.ref}" not found on ${registryHint}`,
1660
2052
  hint: "Check available agents with: adk registry browse",
1661
- details: { ref: entry.ref, sourceRegistry: entry.sourceRegistry, scheme: entry.scheme },
2053
+ details: {
2054
+ ref: entry.ref,
2055
+ sourceRegistry: entry.sourceRegistry,
2056
+ scheme: entry.scheme,
2057
+ },
1662
2058
  });
1663
2059
  }
1664
2060
  }
@@ -1667,17 +2063,22 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
1667
2063
  if (info?.security) security = info.security;
1668
2064
  const agentMode = (info as any)?.mode;
1669
2065
  if (agentMode) (entry as any).mode = agentMode;
1670
- if (info?.upstream && !entry.url && agentMode !== 'api') {
2066
+ if (info?.upstream && !entry.url && agentMode !== "api") {
1671
2067
  entry.url = info.upstream as string;
1672
2068
  entry.scheme = entry.scheme ?? "mcp";
1673
2069
  }
2070
+
2071
+ cacheEntry = buildCacheEntry(entry.ref, info);
1674
2072
  } catch (err) {
1675
2073
  if (err instanceof AdkError) throw err;
1676
2074
  throw new AdkError({
1677
2075
  code: "REGISTRY_UNREACHABLE",
1678
2076
  message: `Could not reach registry to validate "${entry.ref}"`,
1679
2077
  hint: "Check your registry connection with: adk registry test",
1680
- details: { ref: entry.ref, error: err instanceof Error ? err.message : String(err) },
2078
+ details: {
2079
+ ref: entry.ref,
2080
+ error: err instanceof Error ? err.message : String(err),
2081
+ },
1681
2082
  cause: err,
1682
2083
  });
1683
2084
  }
@@ -1685,6 +2086,7 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
1685
2086
 
1686
2087
  const refs = [...(config.refs ?? []), entry];
1687
2088
  await writeConfig({ ...config, refs });
2089
+ await upsertRegistryCacheEntry(name, cacheEntry);
1688
2090
 
1689
2091
  return { security };
1690
2092
  },
@@ -1696,17 +2098,28 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
1696
2098
  const refs = config.refs.filter((r) => !refNameMatches(r, name));
1697
2099
  if (refs.length === before) return false;
1698
2100
  await writeConfig({ ...config, refs });
2101
+ await removeRegistryCacheEntry(name);
1699
2102
  return true;
1700
2103
  },
1701
2104
 
1702
2105
  async list(): Promise<ResolvedRef[]> {
1703
- const config = await readConfig();
1704
- return (config.refs ?? []).map(normalizeRef);
2106
+ const [config, cache] = await Promise.all([
2107
+ readConfig(),
2108
+ readRegistryCache(),
2109
+ ]);
2110
+ return (config.refs ?? [])
2111
+ .map(normalizeRef)
2112
+ .map((r) => hydrateFromCache(r, cache));
1705
2113
  },
1706
2114
 
1707
2115
  async get(name: string): Promise<ResolvedRef | null> {
1708
- const config = await readConfig();
1709
- return findRef(config.refs ?? [], name) ?? null;
2116
+ const [config, cache] = await Promise.all([
2117
+ readConfig(),
2118
+ readRegistryCache(),
2119
+ ]);
2120
+ const found = findRef(config.refs ?? [], name);
2121
+ if (!found) return null;
2122
+ return hydrateFromCache(found, cache);
1710
2123
  },
1711
2124
 
1712
2125
  async update(name: string, updates: Partial<RefEntry>): Promise<boolean> {
@@ -1735,8 +2148,10 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
1735
2148
  updated.name = updates.name;
1736
2149
  }
1737
2150
  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;
2151
+ if (updates.config)
2152
+ updated.config = { ...updated.config, ...updates.config };
2153
+ if (updates.sourceRegistry)
2154
+ updated.sourceRegistry = updates.sourceRegistry;
1740
2155
  return updated;
1741
2156
  });
1742
2157
  if (!found) return false;
@@ -1744,38 +2159,63 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
1744
2159
  return true;
1745
2160
  },
1746
2161
 
1747
- async inspect(name: string, opts?: { full?: boolean }): Promise<AgentInspection | null> {
2162
+ async inspect(
2163
+ name: string,
2164
+ opts?: { full?: boolean },
2165
+ ): Promise<AgentInspection | null> {
1748
2166
  const config = await readConfig();
1749
2167
  const entry = findRef(config.refs ?? [], name);
1750
2168
  if (!entry) throw new Error(`Ref "${name}" not found`);
1751
2169
 
1752
2170
  const consumer = await buildConsumerForRef(entry);
1753
- return consumer.inspect(
2171
+ const result = await consumer.inspect(
1754
2172
  entry.sourceRegistry?.agentPath ?? entry.ref,
1755
2173
  entry.sourceRegistry?.url,
1756
2174
  opts,
1757
2175
  );
2176
+
2177
+ // Side-effect: refresh the registry cache so subsequent ref.list()
2178
+ // / ref.get() calls see the latest description and tool summaries
2179
+ // without another network round-trip. Strips inputSchema (caller's
2180
+ // `result` is unaffected — it still carries the full data).
2181
+ await upsertRegistryCacheEntry(name, buildCacheEntry(entry.ref, result));
2182
+
2183
+ return result;
1758
2184
  },
1759
2185
 
1760
- async call(name: string, tool: string, params?: Record<string, unknown>): Promise<CallAgentResponse> {
2186
+ async call(
2187
+ name: string,
2188
+ tool: string,
2189
+ params?: Record<string, unknown>,
2190
+ ): Promise<CallAgentResponse> {
1761
2191
  const config = await readConfig();
1762
2192
  const entry = findRef(config.refs ?? [], name);
1763
2193
  if (!entry) throw new Error(`Ref "${name}" not found`);
1764
2194
 
1765
- let accessToken = await readRefSecret(name, "access_token")
1766
- ?? await readRefSecret(name, "api_key")
1767
- ?? await readRefSecret(name, "token");
2195
+ const accessToken =
2196
+ (await readRefSecret(name, "access_token")) ??
2197
+ (await readRefSecret(name, "api_key")) ??
2198
+ (await readRefSecret(name, "token"));
1768
2199
 
1769
2200
  // Resolve custom headers from config (e.g. { "X-API-Key": "secret:..." })
1770
2201
  const refConfig = (entry.config ?? {}) as Record<string, unknown>;
1771
- const rawHeaders = refConfig.headers as Record<string, string> | undefined;
2202
+ const rawHeaders = refConfig.headers as
2203
+ | Record<string, string>
2204
+ | undefined;
1772
2205
  let resolvedHeaders: Record<string, string> | undefined;
1773
- if (rawHeaders && typeof rawHeaders === 'object') {
2206
+ if (rawHeaders && typeof rawHeaders === "object") {
1774
2207
  resolvedHeaders = {};
1775
2208
  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') {
2209
+ if (
2210
+ typeof v === "string" &&
2211
+ v.startsWith(SECRET_PREFIX) &&
2212
+ options.encryptionKey
2213
+ ) {
2214
+ resolvedHeaders[k] = await decryptSecret(
2215
+ v.slice(SECRET_PREFIX.length),
2216
+ options.encryptionKey,
2217
+ );
2218
+ } else if (typeof v === "string") {
1779
2219
  resolvedHeaders[k] = v;
1780
2220
  }
1781
2221
  }
@@ -1784,9 +2224,15 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
1784
2224
  const doCall = async (token: string | null) => {
1785
2225
  // Direct MCP only for redirect/proxy agents with an MCP upstream.
1786
2226
  // 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);
2227
+ const agentMode = (entry as any).mode ?? "redirect";
2228
+ if (token && entry.url && agentMode !== "api") {
2229
+ return callMcpDirect(
2230
+ entry.url,
2231
+ tool,
2232
+ params ?? {},
2233
+ token,
2234
+ resolvedHeaders,
2235
+ );
1790
2236
  }
1791
2237
 
1792
2238
  const consumer = await buildConsumerForRef(entry);
@@ -1855,13 +2301,20 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
1855
2301
  // server-side so local inspection would always return "missing").
1856
2302
  const proxy = await resolveProxyForRef(entry);
1857
2303
  if (proxy) {
1858
- return forwardRefOpToProxy<RefAuthStatus>(proxy.reg, proxy.agent, "auth-status", { name });
2304
+ return forwardRefOpToProxy<RefAuthStatus>(
2305
+ proxy.reg,
2306
+ proxy.agent,
2307
+ "auth-status",
2308
+ { name },
2309
+ );
1859
2310
  }
1860
2311
 
1861
2312
  let security: SecuritySchemeSummary | null = null;
1862
2313
  try {
1863
2314
  const consumer = await buildConsumerForRef(entry);
1864
- const info = await consumer.inspect(entry.sourceRegistry?.agentPath ?? entry.ref);
2315
+ const info = await consumer.inspect(
2316
+ entry.sourceRegistry?.agentPath ?? entry.ref,
2317
+ );
1865
2318
  if (info?.security) security = info.security;
1866
2319
  } catch {
1867
2320
  // Can't reach registry
@@ -1889,12 +2342,15 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
1889
2342
  };
1890
2343
  const hasRegistration = !!securityExt.dynamicRegistration;
1891
2344
 
1892
- let oauthMetadata: import("./mcp-client.js").OAuthServerMetadata | null = null;
2345
+ let oauthMetadata:
2346
+ | import("./mcp-client.js").OAuthServerMetadata
2347
+ | null = null;
1893
2348
  let needsSecret = false;
1894
2349
  if (securityExt.discoveryUrl) {
1895
2350
  oauthMetadata = await tryFetchOAuthMetadata(securityExt.discoveryUrl);
1896
2351
  if (oauthMetadata) {
1897
- const authMethods = oauthMetadata.token_endpoint_auth_methods_supported ?? [];
2352
+ const authMethods =
2353
+ oauthMetadata.token_endpoint_auth_methods_supported ?? [];
1898
2354
  needsSecret = !authMethods.includes("none");
1899
2355
  }
1900
2356
  }
@@ -1921,15 +2377,22 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
1921
2377
  };
1922
2378
  } else if (security.type === "apiKey") {
1923
2379
  const apiKeySec = security as {
1924
- name?: string; headers?: Record<string, { description?: string }>;
2380
+ name?: string;
2381
+ headers?: Record<string, { description?: string }>;
1925
2382
  };
1926
2383
  const toStorageKey = (headerName: string) =>
1927
- headerName.toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_|_$/g, "");
2384
+ headerName
2385
+ .toLowerCase()
2386
+ .replace(/[^a-z0-9]+/g, "_")
2387
+ .replace(/^_|_$/g, "");
1928
2388
 
1929
2389
  // 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) : [];
2390
+ const configHeaders = (
2391
+ entry?.config as Record<string, unknown> | undefined
2392
+ )?.headers as Record<string, unknown> | undefined;
2393
+ const configHeaderKeys = configHeaders
2394
+ ? Object.keys(configHeaders)
2395
+ : [];
1933
2396
  const hasConfigHeader = (name: string) =>
1934
2397
  configHeaderKeys.some((k) => k.toLowerCase() === name.toLowerCase());
1935
2398
 
@@ -1943,7 +2406,8 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
1943
2406
  for (const headerName of declaredHeaders) {
1944
2407
  const storageKey = toStorageKey(headerName);
1945
2408
  const inConfigHeaders = hasConfigHeader(headerName);
1946
- const inLegacyKeys = configKeys.includes(storageKey) || configKeys.includes("api_key");
2409
+ const inLegacyKeys =
2410
+ configKeys.includes(storageKey) || configKeys.includes("api_key");
1947
2411
  fields[storageKey] = {
1948
2412
  required: true,
1949
2413
  automated: false,
@@ -1974,22 +2438,40 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
1974
2438
  (f) => !f.required || f.present || f.resolvable,
1975
2439
  );
1976
2440
 
2441
+ // Persist the slim {required, automated} per-field shape into the
2442
+ // registry cache so `isRefAuthComplete` can answer subsequent
2443
+ // host-side "is this ref ready?" checks without re-fetching the
2444
+ // security scheme. We deliberately omit `present`/`resolvable`
2445
+ // because those are computed against the current entry.config and
2446
+ // host environment — caching them would go stale immediately.
2447
+ const authFields: Record<string, RegistryCacheAuthField> = {};
2448
+ for (const [field, info] of Object.entries(fields)) {
2449
+ authFields[field] = {
2450
+ required: info.required,
2451
+ automated: info.automated,
2452
+ };
2453
+ }
2454
+ await upsertRegistryCacheAuthFields(name, entry.ref, authFields);
2455
+
1977
2456
  return { name, security, complete, fields };
1978
2457
  },
1979
2458
 
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> {
2459
+ async auth(
2460
+ name: string,
2461
+ opts?: {
2462
+ apiKey?: string;
2463
+ credentials?: Record<string, string>;
2464
+ /** Extra context to encode in the OAuth state (e.g., tenant/user IDs for multi-tenant callbacks) */
2465
+ stateContext?: Record<string, unknown>;
2466
+ /** Additional scopes to request (e.g., optional scopes declared by the agent) */
2467
+ scopes?: string[];
2468
+ /**
2469
+ * Opt out of proxy routing when the ref's source registry has
2470
+ * `proxy: { mode: 'optional' }`. Ignored for `mode: 'required'`.
2471
+ */
2472
+ preferLocal?: boolean;
2473
+ },
2474
+ ): Promise<AuthStartResult> {
1993
2475
  const config = await readConfig();
1994
2476
  const entry = findRef(config.refs ?? [], name);
1995
2477
  if (!entry) throw new Error(`Ref "${name}" not found`);
@@ -1998,14 +2480,21 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
1998
2480
  // agent. The registry owns the client_id/secret and returns an authorize
1999
2481
  // URL pointing at the registry's OAuth callback domain, so the user
2000
2482
  // completes the flow against the registry instead of localhost.
2001
- const proxy = await resolveProxyForRef(entry, { preferLocal: opts?.preferLocal });
2483
+ const proxy = await resolveProxyForRef(entry, {
2484
+ preferLocal: opts?.preferLocal,
2485
+ });
2002
2486
  if (proxy) {
2003
2487
  const params: Record<string, unknown> = { name };
2004
2488
  if (opts?.apiKey !== undefined) params.apiKey = opts.apiKey;
2005
2489
  if (opts?.credentials) params.credentials = opts.credentials;
2006
2490
  if (opts?.scopes) params.scopes = opts.scopes;
2007
2491
  if (opts?.stateContext) params.stateContext = opts.stateContext;
2008
- return forwardRefOpToProxy<AuthStartResult>(proxy.reg, proxy.agent, "auth", params);
2492
+ return forwardRefOpToProxy<AuthStartResult>(
2493
+ proxy.reg,
2494
+ proxy.agent,
2495
+ "auth",
2496
+ params,
2497
+ );
2009
2498
  }
2010
2499
 
2011
2500
  const status = await ref.authStatus(name);
@@ -2018,20 +2507,31 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
2018
2507
 
2019
2508
  if (security.type === "apiKey") {
2020
2509
  const apiKeySec = security as {
2021
- name?: string; prefix?: string;
2510
+ name?: string;
2511
+ prefix?: string;
2022
2512
  headers?: Record<string, { description?: string }>;
2023
2513
  };
2024
2514
 
2025
2515
  const toStorageKey = (headerName: string) =>
2026
- headerName.toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_|_$/g, "");
2516
+ headerName
2517
+ .toLowerCase()
2518
+ .replace(/[^a-z0-9]+/g, "_")
2519
+ .replace(/^_|_$/g, "");
2027
2520
 
2028
2521
  // Check existing config.headers
2029
- const existingHeaders = ((entry.config ?? {}) as Record<string, unknown>).headers as
2030
- Record<string, string> | undefined;
2522
+ const existingHeaders = (
2523
+ (entry.config ?? {}) as Record<string, unknown>
2524
+ ).headers as Record<string, string> | undefined;
2031
2525
 
2032
2526
  // 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 }))
2527
+ const declaredHeaders: Array<{
2528
+ headerName: string;
2529
+ description?: string;
2530
+ }> = apiKeySec.headers
2531
+ ? Object.entries(apiKeySec.headers).map(([h, meta]) => ({
2532
+ headerName: h,
2533
+ description: meta.description,
2534
+ }))
2035
2535
  : apiKeySec.name
2036
2536
  ? [{ headerName: apiKeySec.name }]
2037
2537
  : [];
@@ -2043,12 +2543,16 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
2043
2543
  for (const { headerName, description } of declaredHeaders) {
2044
2544
  const storageKey = toStorageKey(headerName);
2045
2545
  // 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);
2546
+ const value =
2547
+ opts?.credentials?.[storageKey] ??
2548
+ opts?.credentials?.[headerName] ??
2549
+ (existingHeaders &&
2550
+ Object.entries(existingHeaders).find(
2551
+ ([k]) => k.toLowerCase() === headerName.toLowerCase(),
2552
+ )?.[1]) ??
2553
+ opts?.apiKey ??
2554
+ (await readRefSecret(name, storageKey)) ??
2555
+ (await tryResolve(storageKey));
2052
2556
 
2053
2557
  if (value) {
2054
2558
  resolvedHeaders[headerName] = value;
@@ -2070,23 +2574,30 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
2070
2574
  const encKey = options.encryptionKey;
2071
2575
  const headersToStore: Record<string, string> = {};
2072
2576
  for (const [h, v] of Object.entries(resolvedHeaders)) {
2073
- headersToStore[h] = encKey ? `${SECRET_PREFIX}${await encryptSecret(v, encKey)}` : v;
2577
+ headersToStore[h] = encKey
2578
+ ? `${SECRET_PREFIX}${await encryptSecret(v, encKey)}`
2579
+ : v;
2074
2580
  }
2075
2581
  await ref.update(name, { config: { headers: headersToStore } });
2076
2582
  return { type: "apiKey", complete: true };
2077
2583
  }
2078
2584
 
2079
2585
  // Fallback: no headers declared → generic api_key
2080
- const key = opts?.credentials?.["api_key"] ?? opts?.apiKey ?? await tryResolve("api_key");
2586
+ const key =
2587
+ opts?.credentials?.["api_key"] ??
2588
+ opts?.apiKey ??
2589
+ (await tryResolve("api_key"));
2081
2590
  if (!key) {
2082
2591
  return {
2083
2592
  type: "apiKey",
2084
2593
  complete: false,
2085
- fields: [{
2086
- name: "api_key",
2087
- label: "API Key",
2088
- secret: true,
2089
- }],
2594
+ fields: [
2595
+ {
2596
+ name: "api_key",
2597
+ label: "API Key",
2598
+ secret: true,
2599
+ },
2600
+ ],
2090
2601
  };
2091
2602
  }
2092
2603
  await storeRefSecret(name, "api_key", key);
@@ -2098,12 +2609,24 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
2098
2609
  const isBasic = httpSec.scheme === "basic";
2099
2610
 
2100
2611
  if (isBasic) {
2101
- const username = opts?.credentials?.["username"] ?? await tryResolve("username");
2102
- const password = opts?.credentials?.["password"] ?? await tryResolve("password");
2612
+ const username =
2613
+ opts?.credentials?.["username"] ?? (await tryResolve("username"));
2614
+ const password =
2615
+ opts?.credentials?.["password"] ?? (await tryResolve("password"));
2103
2616
  if (!username || !password) {
2104
2617
  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 });
2618
+ if (!username)
2619
+ missingFields.push({
2620
+ name: "username",
2621
+ label: "Username",
2622
+ secret: false,
2623
+ });
2624
+ if (!password)
2625
+ missingFields.push({
2626
+ name: "password",
2627
+ label: "Password",
2628
+ secret: true,
2629
+ });
2107
2630
  return { type: "http", complete: false, fields: missingFields };
2108
2631
  }
2109
2632
  // Store as base64 encoded basic auth token
@@ -2113,7 +2636,10 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
2113
2636
  }
2114
2637
 
2115
2638
  // Bearer token
2116
- const token = opts?.credentials?.["token"] ?? opts?.apiKey ?? await tryResolve("token");
2639
+ const token =
2640
+ opts?.credentials?.["token"] ??
2641
+ opts?.apiKey ??
2642
+ (await tryResolve("token"));
2117
2643
  if (!token) {
2118
2644
  return {
2119
2645
  type: "http",
@@ -2126,7 +2652,16 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
2126
2652
  }
2127
2653
 
2128
2654
  if (security.type === "oauth2") {
2129
- const flows = (security as { flows?: { authorizationCode?: { authorizationUrl?: string; tokenUrl?: string } } }).flows;
2655
+ const flows = (
2656
+ security as {
2657
+ flows?: {
2658
+ authorizationCode?: {
2659
+ authorizationUrl?: string;
2660
+ tokenUrl?: string;
2661
+ };
2662
+ };
2663
+ }
2664
+ ).flows;
2130
2665
  const authCodeFlow = flows?.authorizationCode;
2131
2666
  if (!authCodeFlow?.authorizationUrl) {
2132
2667
  return {
@@ -2147,7 +2682,9 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
2147
2682
  }
2148
2683
  // Fallback: construct metadata from the security scheme's explicit URLs
2149
2684
  if (!metadata && authCodeFlow.tokenUrl) {
2150
- const flowScopes = (authCodeFlow as Record<string, unknown>).scopes as Record<string, string> | undefined;
2685
+ const flowScopes = (authCodeFlow as Record<string, unknown>).scopes as
2686
+ | Record<string, string>
2687
+ | undefined;
2151
2688
  metadata = {
2152
2689
  issuer: new URL(authUrl).origin,
2153
2690
  authorization_endpoint: authUrl,
@@ -2172,18 +2709,24 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
2172
2709
  let clientSecret: string | undefined = fromHelper?.clientSecret;
2173
2710
 
2174
2711
  if (!clientId && metadata.registration_endpoint) {
2175
- const supportedAuthMethods = metadata.token_endpoint_auth_methods_supported ?? ["none"];
2712
+ const supportedAuthMethods =
2713
+ metadata.token_endpoint_auth_methods_supported ?? ["none"];
2176
2714
  const preferredMethod = supportedAuthMethods.includes("none")
2177
2715
  ? "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
- });
2716
+ : (supportedAuthMethods[0] ?? "client_secret_post");
2717
+
2718
+ const securityClientName = (security as { clientName?: string })
2719
+ .clientName;
2720
+ const reg = await dynamicClientRegistration(
2721
+ metadata.registration_endpoint,
2722
+ {
2723
+ clientName:
2724
+ securityClientName ?? options.oauthClientName ?? "adk",
2725
+ redirectUris: [redirectUri],
2726
+ grantTypes: ["authorization_code"],
2727
+ tokenEndpointAuthMethod: preferredMethod,
2728
+ },
2729
+ );
2187
2730
  clientId = reg.clientId;
2188
2731
  clientSecret = reg.clientSecret;
2189
2732
  await storeRefSecret(name, "client_id", clientId);
@@ -2196,10 +2739,18 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
2196
2739
  // Return fields telling the caller what OAuth credentials to provide
2197
2740
  const missingFields: AuthChallengeField[] = [];
2198
2741
  if (!clientId) {
2199
- missingFields.push({ name: "client_id", label: "Client ID", secret: false });
2742
+ missingFields.push({
2743
+ name: "client_id",
2744
+ label: "Client ID",
2745
+ secret: false,
2746
+ });
2200
2747
  }
2201
2748
  // Always ask for client_secret alongside client_id — most providers need it
2202
- missingFields.push({ name: "client_secret", label: "Client Secret", secret: true });
2749
+ missingFields.push({
2750
+ name: "client_secret",
2751
+ label: "Client Secret",
2752
+ secret: true,
2753
+ });
2203
2754
  return { type: "oauth2", complete: false, fields: missingFields };
2204
2755
  }
2205
2756
 
@@ -2213,32 +2764,42 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
2213
2764
  };
2214
2765
  const state = btoa(JSON.stringify(statePayload));
2215
2766
 
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;
2767
+ const securityExt2 = security as {
2768
+ requiredScopes?: string[];
2769
+ optionalScopes?: string[];
2770
+ authorizationParams?: Record<string, string>;
2771
+ };
2772
+ const flowScopes = (authCodeFlow as Record<string, unknown>).scopes as
2773
+ | Record<string, string>
2774
+ | undefined;
2218
2775
  const agentScopes = [
2219
2776
  ...(securityExt2.requiredScopes ?? []),
2220
2777
  ...(flowScopes ? Object.keys(flowScopes) : []),
2221
2778
  ...(opts?.scopes ?? []),
2222
2779
  ].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;
2780
+ const scopes =
2781
+ agentScopes.length > 0
2782
+ ? [
2783
+ ...agentScopes,
2784
+ ...(metadata.scopes_supported?.includes("openid")
2785
+ ? ["openid"]
2786
+ : []),
2787
+ ]
2788
+ : metadata.scopes_supported;
2229
2789
 
2230
2790
  // Read provider-specific authorization params from the agent's security section
2231
2791
  // (e.g., { access_type: 'offline', prompt: 'consent' } for Google)
2232
2792
  const authorizationParams = securityExt2.authorizationParams;
2233
2793
 
2234
- const { url: authorizeUrl, codeVerifier } = await buildOAuthAuthorizeUrl({
2235
- authorizationEndpoint: metadata.authorization_endpoint,
2236
- clientId,
2237
- redirectUri,
2238
- scopes,
2239
- state,
2240
- extraParams: authorizationParams,
2241
- });
2794
+ const { url: authorizeUrl, codeVerifier } =
2795
+ await buildOAuthAuthorizeUrl({
2796
+ authorizationEndpoint: metadata.authorization_endpoint,
2797
+ clientId,
2798
+ redirectUri,
2799
+ scopes,
2800
+ state,
2801
+ extraParams: authorizationParams,
2802
+ });
2242
2803
 
2243
2804
  // Persist pending state so handleCallback works across processes
2244
2805
  await storePendingOAuth(state, {
@@ -2257,10 +2818,13 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
2257
2818
  return { type: security.type, complete: false };
2258
2819
  },
2259
2820
 
2260
- async authLocal(name: string, opts?: {
2261
- onAuthorizeUrl?: (url: string) => void;
2262
- timeoutMs?: number;
2263
- }): Promise<{ complete: boolean }> {
2821
+ async authLocal(
2822
+ name: string,
2823
+ opts?: {
2824
+ onAuthorizeUrl?: (url: string) => void;
2825
+ timeoutMs?: number;
2826
+ },
2827
+ ): Promise<{ complete: boolean }> {
2264
2828
  // `ref.auth` is already proxy-aware — for proxied refs it returns
2265
2829
  // the authorizeUrl that the registry minted against its own
2266
2830
  // callback domain. Everything below is identical for local and
@@ -2283,7 +2847,11 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
2283
2847
  // owns the credential store, so the user needs to submit via
2284
2848
  // whatever UI the registry exposes. Supporting this through the
2285
2849
  // proxy would need a remote form endpoint — out of scope here.
2286
- if (result.fields && result.fields.length > 0 && result.type !== "oauth2") {
2850
+ if (
2851
+ result.fields &&
2852
+ result.fields.length > 0 &&
2853
+ result.type !== "oauth2"
2854
+ ) {
2287
2855
  if (proxy) {
2288
2856
  throw new Error(
2289
2857
  `Ref "${name}" is sourced from a proxied registry; submit credentials through ${proxy.agent} instead of a local form.`,
@@ -2320,19 +2888,23 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
2320
2888
  resolve({ complete: true });
2321
2889
  } else {
2322
2890
  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
- ));
2891
+ res.end(
2892
+ renderCredentialForm(
2893
+ name,
2894
+ authResult.fields ?? result.fields!,
2895
+ "Some credentials were missing or invalid.",
2896
+ ),
2897
+ );
2328
2898
  }
2329
2899
  } catch (err) {
2330
2900
  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
- ));
2901
+ res.end(
2902
+ renderCredentialForm(
2903
+ name,
2904
+ result.fields!,
2905
+ err instanceof Error ? err.message : String(err),
2906
+ ),
2907
+ );
2336
2908
  }
2337
2909
  return;
2338
2910
  }
@@ -2379,7 +2951,8 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
2379
2951
  const state = reqUrl.searchParams.get("state");
2380
2952
 
2381
2953
  if (!code || !state) {
2382
- const error = reqUrl.searchParams.get("error") ?? "missing code/state";
2954
+ const error =
2955
+ reqUrl.searchParams.get("error") ?? "missing code/state";
2383
2956
  res.writeHead(400, { "Content-Type": "text/html" });
2384
2957
  res.end(`<h1>Error</h1><p>${error}</p>`);
2385
2958
  server.close();
@@ -2395,7 +2968,9 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
2395
2968
  resolve({ complete: cbResult.complete });
2396
2969
  } catch (err) {
2397
2970
  res.writeHead(500, { "Content-Type": "text/html" });
2398
- res.end(`<h1>Error</h1><p>${err instanceof Error ? err.message : String(err)}</p>`);
2971
+ res.end(
2972
+ `<h1>Error</h1><p>${err instanceof Error ? err.message : String(err)}</p>`,
2973
+ );
2399
2974
  server.close();
2400
2975
  reject(err);
2401
2976
  }
@@ -2440,9 +3015,17 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
2440
3015
 
2441
3016
  const status = await ref.authStatus(name);
2442
3017
  const security = status.security;
2443
- const flows = security && "flows" in security
2444
- ? (security as { flows?: Record<string, { tokenUrl?: string; refreshUrl?: string }> }).flows
2445
- : undefined;
3018
+ const flows =
3019
+ security && "flows" in security
3020
+ ? (
3021
+ security as {
3022
+ flows?: Record<
3023
+ string,
3024
+ { tokenUrl?: string; refreshUrl?: string }
3025
+ >;
3026
+ }
3027
+ ).flows
3028
+ : undefined;
2446
3029
  const authCodeFlow = flows?.authorizationCode;
2447
3030
  const tokenUrl = authCodeFlow?.refreshUrl ?? authCodeFlow?.tokenUrl;
2448
3031
  if (!tokenUrl) return null;
@@ -2469,7 +3052,7 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
2469
3052
 
2470
3053
  if (!res.ok) return null;
2471
3054
 
2472
- const data = await res.json() as Record<string, unknown>;
3055
+ const data = (await res.json()) as Record<string, unknown>;
2473
3056
  const newAccessToken = data.access_token as string | undefined;
2474
3057
  if (!newAccessToken) return null;
2475
3058
 
@@ -2487,7 +3070,14 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
2487
3070
  // Top-level callback handler
2488
3071
  // ==========================================
2489
3072
 
2490
- async function handleCallback(params: { code: string; state: string }): Promise<{ refName: string; complete: boolean; stateContext?: Record<string, unknown> }> {
3073
+ async function handleCallback(params: {
3074
+ code: string;
3075
+ state: string;
3076
+ }): Promise<{
3077
+ refName: string;
3078
+ complete: boolean;
3079
+ stateContext?: Record<string, unknown>;
3080
+ }> {
2491
3081
  const pending = await consumePendingOAuth(params.state);
2492
3082
  if (!pending) {
2493
3083
  throw new Error(`No pending OAuth flow for state "${params.state}".`);
@@ -2503,13 +3093,19 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
2503
3093
 
2504
3094
  await storeRefSecret(pending.refName, "access_token", tokens.accessToken);
2505
3095
  if (tokens.refreshToken) {
2506
- await storeRefSecret(pending.refName, "refresh_token", tokens.refreshToken);
3096
+ await storeRefSecret(
3097
+ pending.refName,
3098
+ "refresh_token",
3099
+ tokens.refreshToken,
3100
+ );
2507
3101
  }
2508
3102
 
2509
3103
  let stateContext: Record<string, unknown> | undefined;
2510
3104
  try {
2511
3105
  stateContext = JSON.parse(atob(params.state));
2512
- } catch { /* state wasn't base64 JSON — legacy format */ }
3106
+ } catch {
3107
+ /* state wasn't base64 JSON — legacy format */
3108
+ }
2513
3109
 
2514
3110
  return { refName: pending.refName, complete: true, stateContext };
2515
3111
  }