@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.
- package/dist/adk-tools.d.ts.map +1 -1
- package/dist/adk-tools.js +104 -253
- package/dist/adk-tools.js.map +1 -1
- package/dist/call-agent-schema.d.ts +12 -12
- package/dist/cjs/adk-tools.js +104 -253
- package/dist/cjs/adk-tools.js.map +1 -1
- package/dist/cjs/config-store.js +333 -94
- package/dist/cjs/config-store.js.map +1 -1
- package/dist/cjs/define-config.js.map +1 -1
- package/dist/cjs/index.js.map +1 -1
- package/dist/cjs/registry.js +2 -33
- package/dist/cjs/registry.js.map +1 -1
- package/dist/cjs/types.js.map +1 -1
- package/dist/config-store.d.ts +34 -4
- package/dist/config-store.d.ts.map +1 -1
- package/dist/config-store.js +333 -94
- package/dist/config-store.js.map +1 -1
- package/dist/define-config.d.ts +28 -4
- package/dist/define-config.d.ts.map +1 -1
- package/dist/define-config.js.map +1 -1
- package/dist/index.d.ts +5 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/registry.d.ts.map +1 -1
- package/dist/registry.js +2 -33
- package/dist/registry.js.map +1 -1
- package/dist/types.d.ts +3 -5
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/package.json +1 -1
- package/src/adk-tools.ts +113 -282
- package/src/config-store.test.ts +345 -28
- package/src/config-store.ts +829 -274
- package/src/define-config.ts +47 -21
- package/src/index.ts +16 -13
- package/src/ref-naming.test.ts +1 -49
- package/src/registry.ts +2 -40
- package/src/types.ts +6 -21
package/dist/config-store.js
CHANGED
|
@@ -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 &&
|
|
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 !==
|
|
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
|
|
113
|
+
const esc = (s) => s
|
|
114
|
+
.replace(/&/g, "&")
|
|
115
|
+
.replace(/</g, "<")
|
|
116
|
+
.replace(/>/g, ">")
|
|
117
|
+
.replace(/"/g, """);
|
|
111
118
|
function renderCredentialForm(name, fields, error) {
|
|
112
|
-
const fieldHtml = fields
|
|
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>`)
|
|
118
|
-
|
|
119
|
-
|
|
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 = {
|
|
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", {
|
|
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 {
|
|
452
|
+
return {
|
|
453
|
+
success: true,
|
|
454
|
+
result: JSON.parse(textContent.text),
|
|
455
|
+
};
|
|
310
456
|
}
|
|
311
457
|
catch {
|
|
312
|
-
return {
|
|
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 {
|
|
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 && {
|
|
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
|
-
: {
|
|
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 = {
|
|
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 &&
|
|
1144
|
-
(info.
|
|
1145
|
-
|
|
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: {
|
|
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 !==
|
|
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: {
|
|
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
|
|
1211
|
-
|
|
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
|
|
1215
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
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 ===
|
|
1466
|
+
if (rawHeaders && typeof rawHeaders === "object") {
|
|
1276
1467
|
resolvedHeaders = {};
|
|
1277
1468
|
for (const [k, v] of Object.entries(rawHeaders)) {
|
|
1278
|
-
if (typeof v ===
|
|
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 ===
|
|
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 ??
|
|
1290
|
-
if (token && entry.url && agentMode !==
|
|
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
|
|
1559
|
+
const tryResolveField = makeTryResolve({ name, entry, security });
|
|
1367
1560
|
async function canResolve(field, oauthMetadata) {
|
|
1368
|
-
|
|
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
|
|
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
|
|
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, {
|
|
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
|
|
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
|
|
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]) => ({
|
|
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
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
??
|
|
1507
|
-
|
|
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
|
|
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"] ??
|
|
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({
|
|
1762
|
+
missingFields.push({
|
|
1763
|
+
name: "username",
|
|
1764
|
+
label: "Username",
|
|
1765
|
+
secret: false,
|
|
1766
|
+
});
|
|
1558
1767
|
if (!password)
|
|
1559
|
-
missingFields.push({
|
|
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"] ??
|
|
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
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
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
|
|
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({
|
|
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({
|
|
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(
|
|
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 &&
|
|
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
|
-
//
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
//
|
|
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
|
|
1847
|
-
const security =
|
|
1848
|
-
const flows = security
|
|
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 =
|
|
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 {
|
|
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 };
|