@slashfi/agents-sdk 0.77.2 → 0.78.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -16,12 +16,13 @@
16
16
  * await adk.ref.call('notion', 'notion-search', { query: 'hello' });
17
17
  * ```
18
18
  */
19
+ import { AdkError } from "./adk-error.js";
20
+ import { decryptSecret, encryptSecret } from "./crypto.js";
19
21
  import { normalizeRef } from "./define-config.js";
22
+ import { buildOAuthAuthorizeUrl, discoverOAuthMetadata, dynamicClientRegistration, exchangeCodeForTokens, probeRegistryAuth, refreshAccessToken, } from "./mcp-client.js";
20
23
  import { createRegistryConsumer } from "./registry-consumer.js";
21
- import { decryptSecret, encryptSecret } from "./crypto.js";
22
- import { AdkError } from "./adk-error.js";
23
- import { discoverOAuthMetadata, dynamicClientRegistration, buildOAuthAuthorizeUrl, exchangeCodeForTokens, probeRegistryAuth, refreshAccessToken, } from "./mcp-client.js";
24
24
  const CONFIG_PATH = "consumer-config.json";
25
+ const REGISTRY_CACHE_PATH = "registry-cache.json";
25
26
  const SECRET_PREFIX = "secret:";
26
27
  // ============================================
27
28
  // Internal helpers
@@ -74,7 +75,9 @@ async function decryptConfigSecrets(obj, encryptionKey) {
74
75
  if (typeof value === "string" && value.startsWith(SECRET_PREFIX)) {
75
76
  result[key] = await decryptSecret(value.slice(SECRET_PREFIX.length), encryptionKey);
76
77
  }
77
- else if (value !== null && typeof value === "object" && !Array.isArray(value)) {
78
+ else if (value !== null &&
79
+ typeof value === "object" &&
80
+ !Array.isArray(value)) {
78
81
  result[key] = await decryptConfigSecrets(value, encryptionKey);
79
82
  }
80
83
  else {
@@ -92,7 +95,7 @@ async function decryptConfigSecrets(obj, encryptionKey) {
92
95
  * Fallback: _httpStatus from tool result body
93
96
  */
94
97
  function isUnauthorized(result) {
95
- if (!result || typeof result !== 'object')
98
+ if (!result || typeof result !== "object")
96
99
  return false;
97
100
  const r = result;
98
101
  // Primary: HTTP status forwarded by the registry and set by callRegistry
@@ -107,17 +110,21 @@ function isUnauthorized(result) {
107
110
  // ============================================
108
111
  // Local auth form HTML
109
112
  // ============================================
110
- const esc = (s) => s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
113
+ const esc = (s) => s
114
+ .replace(/&/g, "&amp;")
115
+ .replace(/</g, "&lt;")
116
+ .replace(/>/g, "&gt;")
117
+ .replace(/"/g, "&quot;");
111
118
  function renderCredentialForm(name, fields, error) {
112
- const fieldHtml = fields.map((f) => `
119
+ const fieldHtml = fields
120
+ .map((f) => `
113
121
  <div class="field">
114
122
  <label for="${esc(f.name)}">${esc(f.label)}</label>
115
123
  ${f.description ? `<p class="desc">${esc(f.description)}</p>` : ""}
116
124
  <input id="${esc(f.name)}" name="${esc(f.name)}" type="${f.secret ? "password" : "text"}" required autocomplete="off" spellcheck="false" />
117
- </div>`).join("");
118
- const errorHtml = error
119
- ? `<div class="error">${esc(error)}</div>`
120
- : "";
125
+ </div>`)
126
+ .join("");
127
+ const errorHtml = error ? `<div class="error">${esc(error)}</div>` : "";
121
128
  return `<!DOCTYPE html>
122
129
  <html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
123
130
  <title>Authenticate \u2014 ${esc(name)}</title>
@@ -180,6 +187,95 @@ export function createAdk(fs, options = {}) {
180
187
  async function writeConfig(config) {
181
188
  await fs.writeFile(CONFIG_PATH, JSON.stringify(config, null, 2));
182
189
  }
190
+ // -------------------------------------------------------------------------
191
+ // Registry cache helpers
192
+ //
193
+ // The cache is purely an internal optimization for the adk's read paths
194
+ // (`ref.list()`, `ref.get()`). Writes happen as side-effects of methods
195
+ // that already call the registry (`ref.add()`, `ref.inspect()`); the
196
+ // public surface never grows new methods. Cache failures (missing file,
197
+ // malformed JSON, fs errors during write) are swallowed so the registry
198
+ // cache can never break a registry operation.
199
+ // -------------------------------------------------------------------------
200
+ async function readRegistryCache() {
201
+ try {
202
+ const content = await fs.readFile(REGISTRY_CACHE_PATH);
203
+ if (!content)
204
+ return { refs: {} };
205
+ const parsed = JSON.parse(content);
206
+ return { refs: parsed.refs ?? {} };
207
+ }
208
+ catch {
209
+ return { refs: {} };
210
+ }
211
+ }
212
+ async function writeRegistryCache(cache) {
213
+ try {
214
+ await fs.writeFile(REGISTRY_CACHE_PATH, JSON.stringify(cache, null, 2));
215
+ }
216
+ catch {
217
+ // Best-effort. A failed cache write should never break the operation
218
+ // that triggered it.
219
+ }
220
+ }
221
+ /**
222
+ * Project an inspect/list response into the slim shape we cache. Drops
223
+ * `inputSchema` (too large) and `fullTokens` (registry-internal). Returns
224
+ * undefined if the response carries nothing worth caching.
225
+ */
226
+ function buildCacheEntry(ref, info) {
227
+ if (!info)
228
+ return undefined;
229
+ const toolSource = info.tools ?? info.toolSummaries;
230
+ const tools = toolSource?.map((t) => {
231
+ const slim = { name: t.name };
232
+ if (t.description !== undefined)
233
+ slim.description = t.description;
234
+ return slim;
235
+ });
236
+ if (info.description === undefined && (!tools || tools.length === 0)) {
237
+ return undefined;
238
+ }
239
+ const entry = {
240
+ ref,
241
+ fetchedAt: new Date().toISOString(),
242
+ };
243
+ if (info.description !== undefined)
244
+ entry.description = info.description;
245
+ if (tools && tools.length > 0)
246
+ entry.tools = tools;
247
+ return entry;
248
+ }
249
+ async function upsertRegistryCacheEntry(name, entry) {
250
+ if (!entry)
251
+ return;
252
+ const cache = await readRegistryCache();
253
+ cache.refs[name] = entry;
254
+ await writeRegistryCache(cache);
255
+ }
256
+ async function removeRegistryCacheEntry(name) {
257
+ const cache = await readRegistryCache();
258
+ if (!(name in cache.refs))
259
+ return;
260
+ delete cache.refs[name];
261
+ await writeRegistryCache(cache);
262
+ }
263
+ /**
264
+ * Hydrate a `ResolvedRef` with cached registry metadata when available.
265
+ * Pure: never mutates input. Leaves `description` / `tools` undefined when
266
+ * the cache has no entry, so callers can apply their own UX fallback.
267
+ */
268
+ function hydrateFromCache(ref, cache) {
269
+ const cached = cache.refs[ref.name];
270
+ if (!cached)
271
+ return ref;
272
+ const next = { ...ref };
273
+ if (cached.description !== undefined)
274
+ next.description = cached.description;
275
+ if (cached.tools !== undefined)
276
+ next.tools = cached.tools;
277
+ return next;
278
+ }
183
279
  /**
184
280
  * Store a secret value in a ref's config, encrypted if encryptionKey is set.
185
281
  * The value is stored inline as "secret:<encrypted>" in consumer-config.json.
@@ -208,6 +304,47 @@ export function createAdk(fs, options = {}) {
208
304
  }
209
305
  return value;
210
306
  }
307
+ /**
308
+ * Build a `tryResolve(field, oauthMetadata?)` function bound to a
309
+ * specific ref + entry + security context. Wraps the host-injected
310
+ * `resolveCredentials` callback (e.g. atlas's env/static/tenant chain
311
+ * for first-party agents). Errors propagate to the caller.
312
+ */
313
+ function makeTryResolve(ctx) {
314
+ return async (field, oauthMetadata) => {
315
+ const resolve = options.resolveCredentials;
316
+ if (!resolve)
317
+ return null;
318
+ return resolve({
319
+ ref: ctx.name,
320
+ field,
321
+ entry: ctx.entry,
322
+ security: ctx.security,
323
+ oauthMetadata,
324
+ });
325
+ };
326
+ }
327
+ /**
328
+ * Resolve OAuth client credentials (client_id + client_secret) for a
329
+ * ref. Walks: `resolveCredentials` callback → per-ref VCS storage.
330
+ * Used by both `auth` (initial OAuth flow) and `refreshToken` (token
331
+ * refresh) — must be a single function so the two paths can never
332
+ * disagree about where credentials live.
333
+ *
334
+ * Returns null when no client_id is available anywhere; caller decides
335
+ * whether to attempt dynamic registration (`auth`) or bail (`refresh`).
336
+ */
337
+ async function resolveOAuthClient(ctx) {
338
+ const tryResolve = makeTryResolve(ctx);
339
+ const clientId = (await tryResolve("client_id", ctx.metadata)) ??
340
+ (await readRefSecret(ctx.name, "client_id"));
341
+ if (!clientId)
342
+ return null;
343
+ const clientSecret = (await tryResolve("client_secret", ctx.metadata)) ??
344
+ (await readRefSecret(ctx.name, "client_secret")) ??
345
+ undefined;
346
+ return { clientId, ...(clientSecret && { clientSecret }) };
347
+ }
211
348
  const PENDING_OAUTH_PATH = "pending-oauth.json";
212
349
  async function readPendingOAuth() {
213
350
  const content = await fs.readFile(PENDING_OAUTH_PATH);
@@ -249,7 +386,10 @@ export function createAdk(fs, options = {}) {
249
386
  let reqId = 0;
250
387
  let sessionId;
251
388
  async function rpc(method, rpcParams) {
252
- const reqHeaders = { ...headers, ...(sessionId ? { "Mcp-Session-Id": sessionId } : {}) };
389
+ const reqHeaders = {
390
+ ...headers,
391
+ ...(sessionId ? { "Mcp-Session-Id": sessionId } : {}),
392
+ };
253
393
  const res = await globalThis.fetch(url, {
254
394
  method: "POST",
255
395
  headers: reqHeaders,
@@ -290,7 +430,7 @@ export function createAdk(fs, options = {}) {
290
430
  }
291
431
  return undefined;
292
432
  }
293
- const json = await res.json();
433
+ const json = (await res.json());
294
434
  if (json.error)
295
435
  throw new Error(`MCP RPC error: ${json.error.message}`);
296
436
  return json.result;
@@ -302,20 +442,32 @@ export function createAdk(fs, options = {}) {
302
442
  clientInfo: { name: "adk", version: "1.0.0" },
303
443
  });
304
444
  await rpc("notifications/initialized").catch(() => { });
305
- const result = await rpc("tools/call", { name: toolName, arguments: params });
445
+ const result = (await rpc("tools/call", {
446
+ name: toolName,
447
+ arguments: params,
448
+ }));
306
449
  const textContent = result?.content?.find((c) => c.type === "text");
307
450
  if (textContent?.text) {
308
451
  try {
309
- return { success: true, result: JSON.parse(textContent.text) };
452
+ return {
453
+ success: true,
454
+ result: JSON.parse(textContent.text),
455
+ };
310
456
  }
311
457
  catch {
312
- return { success: true, result: textContent.text };
458
+ return {
459
+ success: true,
460
+ result: textContent.text,
461
+ };
313
462
  }
314
463
  }
315
464
  return { success: true, result };
316
465
  }
317
466
  catch (err) {
318
- return { success: false, error: err instanceof Error ? err.message : String(err) };
467
+ return {
468
+ success: false,
469
+ error: err instanceof Error ? err.message : String(err),
470
+ };
319
471
  }
320
472
  }
321
473
  function callbackUrl() {
@@ -328,7 +480,7 @@ export function createAdk(fs, options = {}) {
328
480
  const res = await globalThis.fetch(url);
329
481
  if (!res.ok)
330
482
  return null;
331
- const data = await res.json();
483
+ const data = (await res.json());
332
484
  if (data.authorization_endpoint && data.token_endpoint) {
333
485
  return data;
334
486
  }
@@ -698,7 +850,9 @@ export function createAdk(fs, options = {}) {
698
850
  ...final,
699
851
  proxy: {
700
852
  mode: discovered.proxy.mode,
701
- ...(discovered.proxy.agent && { agent: discovered.proxy.agent }),
853
+ ...(discovered.proxy.agent && {
854
+ agent: discovered.proxy.agent,
855
+ }),
702
856
  },
703
857
  };
704
858
  }
@@ -825,7 +979,12 @@ export function createAdk(fs, options = {}) {
825
979
  }));
826
980
  return results.map((r) => r.status === "fulfilled"
827
981
  ? r.value
828
- : { name: "unknown", url: "unknown", status: "error", error: "unknown" });
982
+ : {
983
+ name: "unknown",
984
+ url: "unknown",
985
+ status: "error",
986
+ error: "unknown",
987
+ });
829
988
  },
830
989
  async auth(nameOrUrl, credential) {
831
990
  // Encrypt the secret value up-front so the write path is uniform;
@@ -1105,7 +1264,10 @@ export function createAdk(fs, options = {}) {
1105
1264
  entry = { ...entry, scheme: "registry" };
1106
1265
  }
1107
1266
  else if (entry.url) {
1108
- entry = { ...entry, scheme: entry.url.startsWith("http") ? "https" : "mcp" };
1267
+ entry = {
1268
+ ...entry,
1269
+ scheme: entry.url.startsWith("http") ? "https" : "mcp",
1270
+ };
1109
1271
  }
1110
1272
  else {
1111
1273
  throw new AdkError({
@@ -1133,6 +1295,7 @@ export function createAdk(fs, options = {}) {
1133
1295
  details: { ref: entry.ref, scheme: entry.scheme },
1134
1296
  });
1135
1297
  }
1298
+ let cacheEntry;
1136
1299
  if (hasRegistries || entry.sourceRegistry?.url) {
1137
1300
  try {
1138
1301
  const consumer = await buildConsumerForRef(entry);
@@ -1140,9 +1303,10 @@ export function createAdk(fs, options = {}) {
1140
1303
  const info = await consumer.inspect(agentToInspect);
1141
1304
  const requiresValidation = !!entry.sourceRegistry;
1142
1305
  if (requiresValidation) {
1143
- const hasContent = info && (info.description ||
1144
- (info.tools && info.tools.length > 0) ||
1145
- (info.toolSummaries && info.toolSummaries.length > 0));
1306
+ const hasContent = info &&
1307
+ (info.description ||
1308
+ (info.tools && info.tools.length > 0) ||
1309
+ (info.toolSummaries && info.toolSummaries.length > 0));
1146
1310
  if (!hasContent) {
1147
1311
  // Inspect returned empty — fall back to browse to check if agent exists
1148
1312
  const registryUrl = entry.sourceRegistry?.url;
@@ -1164,7 +1328,11 @@ export function createAdk(fs, options = {}) {
1164
1328
  code: "REF_NOT_FOUND",
1165
1329
  message: `Agent "${entry.ref}" not found on ${registryHint}`,
1166
1330
  hint: "Check available agents with: adk registry browse",
1167
- details: { ref: entry.ref, sourceRegistry: entry.sourceRegistry, scheme: entry.scheme },
1331
+ details: {
1332
+ ref: entry.ref,
1333
+ sourceRegistry: entry.sourceRegistry,
1334
+ scheme: entry.scheme,
1335
+ },
1168
1336
  });
1169
1337
  }
1170
1338
  }
@@ -1174,10 +1342,11 @@ export function createAdk(fs, options = {}) {
1174
1342
  const agentMode = info?.mode;
1175
1343
  if (agentMode)
1176
1344
  entry.mode = agentMode;
1177
- if (info?.upstream && !entry.url && agentMode !== 'api') {
1345
+ if (info?.upstream && !entry.url && agentMode !== "api") {
1178
1346
  entry.url = info.upstream;
1179
1347
  entry.scheme = entry.scheme ?? "mcp";
1180
1348
  }
1349
+ cacheEntry = buildCacheEntry(entry.ref, info);
1181
1350
  }
1182
1351
  catch (err) {
1183
1352
  if (err instanceof AdkError)
@@ -1186,13 +1355,17 @@ export function createAdk(fs, options = {}) {
1186
1355
  code: "REGISTRY_UNREACHABLE",
1187
1356
  message: `Could not reach registry to validate "${entry.ref}"`,
1188
1357
  hint: "Check your registry connection with: adk registry test",
1189
- details: { ref: entry.ref, error: err instanceof Error ? err.message : String(err) },
1358
+ details: {
1359
+ ref: entry.ref,
1360
+ error: err instanceof Error ? err.message : String(err),
1361
+ },
1190
1362
  cause: err,
1191
1363
  });
1192
1364
  }
1193
1365
  }
1194
1366
  const refs = [...(config.refs ?? []), entry];
1195
1367
  await writeConfig({ ...config, refs });
1368
+ await upsertRegistryCacheEntry(name, cacheEntry);
1196
1369
  return { security };
1197
1370
  },
1198
1371
  async remove(name) {
@@ -1204,15 +1377,27 @@ export function createAdk(fs, options = {}) {
1204
1377
  if (refs.length === before)
1205
1378
  return false;
1206
1379
  await writeConfig({ ...config, refs });
1380
+ await removeRegistryCacheEntry(name);
1207
1381
  return true;
1208
1382
  },
1209
1383
  async list() {
1210
- const config = await readConfig();
1211
- return (config.refs ?? []).map(normalizeRef);
1384
+ const [config, cache] = await Promise.all([
1385
+ readConfig(),
1386
+ readRegistryCache(),
1387
+ ]);
1388
+ return (config.refs ?? [])
1389
+ .map(normalizeRef)
1390
+ .map((r) => hydrateFromCache(r, cache));
1212
1391
  },
1213
1392
  async get(name) {
1214
- const config = await readConfig();
1215
- return findRef(config.refs ?? [], name) ?? null;
1393
+ const [config, cache] = await Promise.all([
1394
+ readConfig(),
1395
+ readRegistryCache(),
1396
+ ]);
1397
+ const found = findRef(config.refs ?? [], name);
1398
+ if (!found)
1399
+ return null;
1400
+ return hydrateFromCache(found, cache);
1216
1401
  },
1217
1402
  async update(name, updates) {
1218
1403
  const config = await readConfig();
@@ -1258,27 +1443,35 @@ export function createAdk(fs, options = {}) {
1258
1443
  if (!entry)
1259
1444
  throw new Error(`Ref "${name}" not found`);
1260
1445
  const consumer = await buildConsumerForRef(entry);
1261
- return consumer.inspect(entry.sourceRegistry?.agentPath ?? entry.ref, entry.sourceRegistry?.url, opts);
1446
+ const result = await consumer.inspect(entry.sourceRegistry?.agentPath ?? entry.ref, entry.sourceRegistry?.url, opts);
1447
+ // Side-effect: refresh the registry cache so subsequent ref.list()
1448
+ // / ref.get() calls see the latest description and tool summaries
1449
+ // without another network round-trip. Strips inputSchema (caller's
1450
+ // `result` is unaffected — it still carries the full data).
1451
+ await upsertRegistryCacheEntry(name, buildCacheEntry(entry.ref, result));
1452
+ return result;
1262
1453
  },
1263
1454
  async call(name, tool, params) {
1264
1455
  const config = await readConfig();
1265
1456
  const entry = findRef(config.refs ?? [], name);
1266
1457
  if (!entry)
1267
1458
  throw new Error(`Ref "${name}" not found`);
1268
- let accessToken = await readRefSecret(name, "access_token")
1269
- ?? await readRefSecret(name, "api_key")
1270
- ?? await readRefSecret(name, "token");
1459
+ const accessToken = (await readRefSecret(name, "access_token")) ??
1460
+ (await readRefSecret(name, "api_key")) ??
1461
+ (await readRefSecret(name, "token"));
1271
1462
  // Resolve custom headers from config (e.g. { "X-API-Key": "secret:..." })
1272
1463
  const refConfig = (entry.config ?? {});
1273
1464
  const rawHeaders = refConfig.headers;
1274
1465
  let resolvedHeaders;
1275
- if (rawHeaders && typeof rawHeaders === 'object') {
1466
+ if (rawHeaders && typeof rawHeaders === "object") {
1276
1467
  resolvedHeaders = {};
1277
1468
  for (const [k, v] of Object.entries(rawHeaders)) {
1278
- if (typeof v === 'string' && v.startsWith(SECRET_PREFIX) && options.encryptionKey) {
1469
+ if (typeof v === "string" &&
1470
+ v.startsWith(SECRET_PREFIX) &&
1471
+ options.encryptionKey) {
1279
1472
  resolvedHeaders[k] = await decryptSecret(v.slice(SECRET_PREFIX.length), options.encryptionKey);
1280
1473
  }
1281
- else if (typeof v === 'string') {
1474
+ else if (typeof v === "string") {
1282
1475
  resolvedHeaders[k] = v;
1283
1476
  }
1284
1477
  }
@@ -1286,8 +1479,8 @@ export function createAdk(fs, options = {}) {
1286
1479
  const doCall = async (token) => {
1287
1480
  // Direct MCP only for redirect/proxy agents with an MCP upstream.
1288
1481
  // API-mode agents must go through the registry (it does REST translation).
1289
- const agentMode = entry.mode ?? 'redirect';
1290
- if (token && entry.url && agentMode !== 'api') {
1482
+ const agentMode = entry.mode ?? "redirect";
1483
+ if (token && entry.url && agentMode !== "api") {
1291
1484
  return callMcpDirect(entry.url, tool, params ?? {}, token, resolvedHeaders);
1292
1485
  }
1293
1486
  const consumer = await buildConsumerForRef(entry);
@@ -1363,12 +1556,9 @@ export function createAdk(fs, options = {}) {
1363
1556
  return { name, security, complete: true, fields: {} };
1364
1557
  }
1365
1558
  const configKeys = Object.keys(entry.config ?? {});
1366
- const resolve = options.resolveCredentials;
1559
+ const tryResolveField = makeTryResolve({ name, entry, security });
1367
1560
  async function canResolve(field, oauthMetadata) {
1368
- if (!resolve || !entry)
1369
- return false;
1370
- const val = await resolve({ ref: name, field, entry, security, oauthMetadata });
1371
- return val !== null;
1561
+ return (await tryResolveField(field, oauthMetadata)) !== null;
1372
1562
  }
1373
1563
  const fields = {};
1374
1564
  if (security.type === "oauth2") {
@@ -1406,10 +1596,15 @@ export function createAdk(fs, options = {}) {
1406
1596
  }
1407
1597
  else if (security.type === "apiKey") {
1408
1598
  const apiKeySec = security;
1409
- const toStorageKey = (headerName) => headerName.toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_|_$/g, "");
1599
+ const toStorageKey = (headerName) => headerName
1600
+ .toLowerCase()
1601
+ .replace(/[^a-z0-9]+/g, "_")
1602
+ .replace(/^_|_$/g, "");
1410
1603
  // config.headers: { "Header-Name": "value" } — check by header name (case-insensitive)
1411
1604
  const configHeaders = entry?.config?.headers;
1412
- const configHeaderKeys = configHeaders ? Object.keys(configHeaders) : [];
1605
+ const configHeaderKeys = configHeaders
1606
+ ? Object.keys(configHeaders)
1607
+ : [];
1413
1608
  const hasConfigHeader = (name) => configHeaderKeys.some((k) => k.toLowerCase() === name.toLowerCase());
1414
1609
  // Collect all declared header names from the security scheme
1415
1610
  const declaredHeaders = apiKeySec.headers
@@ -1458,7 +1653,9 @@ export function createAdk(fs, options = {}) {
1458
1653
  // agent. The registry owns the client_id/secret and returns an authorize
1459
1654
  // URL pointing at the registry's OAuth callback domain, so the user
1460
1655
  // completes the flow against the registry instead of localhost.
1461
- const proxy = await resolveProxyForRef(entry, { preferLocal: opts?.preferLocal });
1656
+ const proxy = await resolveProxyForRef(entry, {
1657
+ preferLocal: opts?.preferLocal,
1658
+ });
1462
1659
  if (proxy) {
1463
1660
  const params = { name };
1464
1661
  if (opts?.apiKey !== undefined)
@@ -1473,23 +1670,24 @@ export function createAdk(fs, options = {}) {
1473
1670
  }
1474
1671
  const status = await ref.authStatus(name);
1475
1672
  const security = status.security;
1476
- const resolve = options.resolveCredentials;
1477
- async function tryResolve(field, oauthMetadata) {
1478
- if (!resolve)
1479
- return null;
1480
- return resolve({ ref: name, field, entry: entry, security, oauthMetadata });
1481
- }
1673
+ const tryResolve = makeTryResolve({ name, entry, security });
1482
1674
  if (!security || security.type === "none") {
1483
1675
  return { type: "none", complete: true };
1484
1676
  }
1485
1677
  if (security.type === "apiKey") {
1486
1678
  const apiKeySec = security;
1487
- const toStorageKey = (headerName) => headerName.toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_|_$/g, "");
1679
+ const toStorageKey = (headerName) => headerName
1680
+ .toLowerCase()
1681
+ .replace(/[^a-z0-9]+/g, "_")
1682
+ .replace(/^_|_$/g, "");
1488
1683
  // Check existing config.headers
1489
1684
  const existingHeaders = (entry.config ?? {}).headers;
1490
1685
  // Collect declared headers: from security.headers or security.name
1491
1686
  const declaredHeaders = apiKeySec.headers
1492
- ? Object.entries(apiKeySec.headers).map(([h, meta]) => ({ headerName: h, description: meta.description }))
1687
+ ? Object.entries(apiKeySec.headers).map(([h, meta]) => ({
1688
+ headerName: h,
1689
+ description: meta.description,
1690
+ }))
1493
1691
  : apiKeySec.name
1494
1692
  ? [{ headerName: apiKeySec.name }]
1495
1693
  : [];
@@ -1499,12 +1697,13 @@ export function createAdk(fs, options = {}) {
1499
1697
  for (const { headerName, description } of declaredHeaders) {
1500
1698
  const storageKey = toStorageKey(headerName);
1501
1699
  // Check: credentials param → existing config.headers → legacy config key → resolve callback
1502
- const value = opts?.credentials?.[storageKey]
1503
- ?? opts?.credentials?.[headerName]
1504
- ?? (existingHeaders && Object.entries(existingHeaders).find(([k]) => k.toLowerCase() === headerName.toLowerCase())?.[1])
1505
- ?? opts?.apiKey
1506
- ?? await readRefSecret(name, storageKey)
1507
- ?? await tryResolve(storageKey);
1700
+ const value = opts?.credentials?.[storageKey] ??
1701
+ opts?.credentials?.[headerName] ??
1702
+ (existingHeaders &&
1703
+ Object.entries(existingHeaders).find(([k]) => k.toLowerCase() === headerName.toLowerCase())?.[1]) ??
1704
+ opts?.apiKey ??
1705
+ (await readRefSecret(name, storageKey)) ??
1706
+ (await tryResolve(storageKey));
1508
1707
  if (value) {
1509
1708
  resolvedHeaders[headerName] = value;
1510
1709
  }
@@ -1524,22 +1723,28 @@ export function createAdk(fs, options = {}) {
1524
1723
  const encKey = options.encryptionKey;
1525
1724
  const headersToStore = {};
1526
1725
  for (const [h, v] of Object.entries(resolvedHeaders)) {
1527
- headersToStore[h] = encKey ? `${SECRET_PREFIX}${await encryptSecret(v, encKey)}` : v;
1726
+ headersToStore[h] = encKey
1727
+ ? `${SECRET_PREFIX}${await encryptSecret(v, encKey)}`
1728
+ : v;
1528
1729
  }
1529
1730
  await ref.update(name, { config: { headers: headersToStore } });
1530
1731
  return { type: "apiKey", complete: true };
1531
1732
  }
1532
1733
  // Fallback: no headers declared → generic api_key
1533
- const key = opts?.credentials?.["api_key"] ?? opts?.apiKey ?? await tryResolve("api_key");
1734
+ const key = opts?.credentials?.["api_key"] ??
1735
+ opts?.apiKey ??
1736
+ (await tryResolve("api_key"));
1534
1737
  if (!key) {
1535
1738
  return {
1536
1739
  type: "apiKey",
1537
1740
  complete: false,
1538
- fields: [{
1741
+ fields: [
1742
+ {
1539
1743
  name: "api_key",
1540
1744
  label: "API Key",
1541
1745
  secret: true,
1542
- }],
1746
+ },
1747
+ ],
1543
1748
  };
1544
1749
  }
1545
1750
  await storeRefSecret(name, "api_key", key);
@@ -1549,14 +1754,22 @@ export function createAdk(fs, options = {}) {
1549
1754
  const httpSec = security;
1550
1755
  const isBasic = httpSec.scheme === "basic";
1551
1756
  if (isBasic) {
1552
- const username = opts?.credentials?.["username"] ?? await tryResolve("username");
1553
- const password = opts?.credentials?.["password"] ?? await tryResolve("password");
1757
+ const username = opts?.credentials?.["username"] ?? (await tryResolve("username"));
1758
+ const password = opts?.credentials?.["password"] ?? (await tryResolve("password"));
1554
1759
  if (!username || !password) {
1555
1760
  const missingFields = [];
1556
1761
  if (!username)
1557
- missingFields.push({ name: "username", label: "Username", secret: false });
1762
+ missingFields.push({
1763
+ name: "username",
1764
+ label: "Username",
1765
+ secret: false,
1766
+ });
1558
1767
  if (!password)
1559
- missingFields.push({ name: "password", label: "Password", secret: true });
1768
+ missingFields.push({
1769
+ name: "password",
1770
+ label: "Password",
1771
+ secret: true,
1772
+ });
1560
1773
  return { type: "http", complete: false, fields: missingFields };
1561
1774
  }
1562
1775
  // Store as base64 encoded basic auth token
@@ -1565,7 +1778,9 @@ export function createAdk(fs, options = {}) {
1565
1778
  return { type: "http", complete: true };
1566
1779
  }
1567
1780
  // Bearer token
1568
- const token = opts?.credentials?.["token"] ?? opts?.apiKey ?? await tryResolve("token");
1781
+ const token = opts?.credentials?.["token"] ??
1782
+ opts?.apiKey ??
1783
+ (await tryResolve("token"));
1569
1784
  if (!token) {
1570
1785
  return {
1571
1786
  type: "http",
@@ -1610,17 +1825,21 @@ export function createAdk(fs, options = {}) {
1610
1825
  }
1611
1826
  const redirectUri = callbackUrl();
1612
1827
  // Resolve client credentials: callback → stored → dynamic registration
1613
- let clientId = await tryResolve("client_id", metadata)
1614
- ?? await readRefSecret(name, "client_id");
1615
- let clientSecret = await tryResolve("client_secret", metadata)
1616
- ?? await readRefSecret(name, "client_secret")
1617
- ?? undefined;
1828
+ const fromHelper = await resolveOAuthClient({
1829
+ name,
1830
+ entry,
1831
+ security,
1832
+ metadata,
1833
+ });
1834
+ let clientId = fromHelper?.clientId;
1835
+ let clientSecret = fromHelper?.clientSecret;
1618
1836
  if (!clientId && metadata.registration_endpoint) {
1619
1837
  const supportedAuthMethods = metadata.token_endpoint_auth_methods_supported ?? ["none"];
1620
1838
  const preferredMethod = supportedAuthMethods.includes("none")
1621
1839
  ? "none"
1622
- : supportedAuthMethods[0] ?? "client_secret_post";
1623
- const securityClientName = security.clientName;
1840
+ : (supportedAuthMethods[0] ?? "client_secret_post");
1841
+ const securityClientName = security
1842
+ .clientName;
1624
1843
  const reg = await dynamicClientRegistration(metadata.registration_endpoint, {
1625
1844
  clientName: securityClientName ?? options.oauthClientName ?? "adk",
1626
1845
  redirectUris: [redirectUri],
@@ -1638,10 +1857,18 @@ export function createAdk(fs, options = {}) {
1638
1857
  // Return fields telling the caller what OAuth credentials to provide
1639
1858
  const missingFields = [];
1640
1859
  if (!clientId) {
1641
- missingFields.push({ name: "client_id", label: "Client ID", secret: false });
1860
+ missingFields.push({
1861
+ name: "client_id",
1862
+ label: "Client ID",
1863
+ secret: false,
1864
+ });
1642
1865
  }
1643
1866
  // Always ask for client_secret alongside client_id — most providers need it
1644
- missingFields.push({ name: "client_secret", label: "Client Secret", secret: true });
1867
+ missingFields.push({
1868
+ name: "client_secret",
1869
+ label: "Client Secret",
1870
+ secret: true,
1871
+ });
1645
1872
  return { type: "oauth2", complete: false, fields: missingFields };
1646
1873
  }
1647
1874
  // State ties the callback back to this ref. Encode as base64 JSON
@@ -1663,7 +1890,9 @@ export function createAdk(fs, options = {}) {
1663
1890
  const scopes = agentScopes.length > 0
1664
1891
  ? [
1665
1892
  ...agentScopes,
1666
- ...(metadata.scopes_supported?.includes('openid') ? ['openid'] : []),
1893
+ ...(metadata.scopes_supported?.includes("openid")
1894
+ ? ["openid"]
1895
+ : []),
1667
1896
  ]
1668
1897
  : metadata.scopes_supported;
1669
1898
  // Read provider-specific authorization params from the agent's security section
@@ -1712,7 +1941,9 @@ export function createAdk(fs, options = {}) {
1712
1941
  // owns the credential store, so the user needs to submit via
1713
1942
  // whatever UI the registry exposes. Supporting this through the
1714
1943
  // proxy would need a remote form endpoint — out of scope here.
1715
- if (result.fields && result.fields.length > 0 && result.type !== "oauth2") {
1944
+ if (result.fields &&
1945
+ result.fields.length > 0 &&
1946
+ result.type !== "oauth2") {
1716
1947
  if (proxy) {
1717
1948
  throw new Error(`Ref "${name}" is sourced from a proxied registry; submit credentials through ${proxy.agent} instead of a local form.`);
1718
1949
  }
@@ -1834,22 +2065,28 @@ export function createAdk(fs, options = {}) {
1834
2065
  const refreshToken = await readRefSecret(name, "refresh_token");
1835
2066
  if (!refreshToken)
1836
2067
  return null;
1837
- // Read client credentials
1838
- const clientId = await readRefSecret(name, "client_id");
1839
- if (!clientId)
1840
- return null;
1841
- const clientSecret = await readRefSecret(name, "client_secret");
1842
- // Get the agent's token endpoint from its security metadata
2068
+ // Resolve token endpoint + OAuth client via the host's
2069
+ // `resolveCredentials` chain. Same chain `auth` uses (see
2070
+ // `resolveOAuthClient`) — kept symmetric so refresh works on every
2071
+ // ref `auth` works on, including first-party registry-hosted
2072
+ // clients whose creds live in env / tenant scope, not the user's
2073
+ // per-ref config.
1843
2074
  const entry = await ref.get(name);
1844
2075
  if (!entry)
1845
2076
  return null;
1846
- const info = await ref.inspect(name);
1847
- const security = info?.security;
1848
- const flows = security?.flows;
2077
+ const status = await ref.authStatus(name);
2078
+ const security = status.security;
2079
+ const flows = security && "flows" in security
2080
+ ? security.flows
2081
+ : undefined;
1849
2082
  const authCodeFlow = flows?.authorizationCode;
1850
- const tokenUrl = (authCodeFlow?.refreshUrl ?? authCodeFlow?.tokenUrl);
2083
+ const tokenUrl = authCodeFlow?.refreshUrl ?? authCodeFlow?.tokenUrl;
1851
2084
  if (!tokenUrl)
1852
2085
  return null;
2086
+ const oauthClient = await resolveOAuthClient({ name, entry, security });
2087
+ if (!oauthClient)
2088
+ return null;
2089
+ const { clientId, clientSecret } = oauthClient;
1853
2090
  // POST to the token endpoint with grant_type=refresh_token
1854
2091
  const body = new URLSearchParams({
1855
2092
  grant_type: "refresh_token",
@@ -1866,7 +2103,7 @@ export function createAdk(fs, options = {}) {
1866
2103
  });
1867
2104
  if (!res.ok)
1868
2105
  return null;
1869
- const data = await res.json();
2106
+ const data = (await res.json());
1870
2107
  const newAccessToken = data.access_token;
1871
2108
  if (!newAccessToken)
1872
2109
  return null;
@@ -1901,7 +2138,9 @@ export function createAdk(fs, options = {}) {
1901
2138
  try {
1902
2139
  stateContext = JSON.parse(atob(params.state));
1903
2140
  }
1904
- catch { /* state wasn't base64 JSON — legacy format */ }
2141
+ catch {
2142
+ /* state wasn't base64 JSON — legacy format */
2143
+ }
1905
2144
  return { refName: pending.refName, complete: true, stateContext };
1906
2145
  }
1907
2146
  return { registry, ref, readConfig, writeConfig, handleCallback };