@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.
- package/dist/call-agent-schema.d.ts +12 -12
- package/dist/cjs/config-store.js +340 -68
- package/dist/cjs/config-store.js.map +1 -1
- package/dist/cjs/define-config.js.map +1 -1
- package/dist/cjs/index.js +3 -1
- package/dist/cjs/index.js.map +1 -1
- package/dist/config-store.d.ts +90 -4
- package/dist/config-store.d.ts.map +1 -1
- package/dist/config-store.js +339 -68
- 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 +6 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -2
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/config-store.test.ts +499 -28
- package/src/config-store.ts +846 -250
- package/src/define-config.ts +47 -21
- package/src/index.ts +18 -13
package/dist/config-store.js
CHANGED
|
@@ -16,13 +16,56 @@
|
|
|
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:";
|
|
27
|
+
/**
|
|
28
|
+
* "Is this ref ready to call?" answered locally using the cached
|
|
29
|
+
* security-scheme requirements. Mirrors the `complete` boolean
|
|
30
|
+
* `auth-status` returns, but doesn't need a network round-trip — the
|
|
31
|
+
* cached `authFields` capture what the registry said is required, and
|
|
32
|
+
* we evaluate satisfaction against the entry's current `config`.
|
|
33
|
+
*
|
|
34
|
+
* Behavior:
|
|
35
|
+
* - `mode: 'proxy'` refs → always true. Auth lives server-side; the
|
|
36
|
+
* proxy is the source of truth, no entry-side fields involved.
|
|
37
|
+
* - Cache miss (no `authFields` for this ref yet) → returns `null`,
|
|
38
|
+
* signaling "I don't know — caller should fall back to its own
|
|
39
|
+
* heuristic or call `auth-status` to populate the cache".
|
|
40
|
+
* - Cache hit → for every required, non-automated field, checks
|
|
41
|
+
* presence in `entry.config`. Mirrors the `present || resolvable`
|
|
42
|
+
* check in `auth-status` but evaluates against current config.
|
|
43
|
+
* `automated` fields (e.g. dynamic OAuth client_id) count as
|
|
44
|
+
* satisfied even when absent — adk supplies them at call time.
|
|
45
|
+
*
|
|
46
|
+
* Returning `null` for cache miss is intentional. A boolean would
|
|
47
|
+
* force callers to choose a default that's wrong half the time;
|
|
48
|
+
* `null` lets them branch explicitly.
|
|
49
|
+
*/
|
|
50
|
+
export function isRefAuthComplete(entry, cacheEntry) {
|
|
51
|
+
if (typeof entry === "string")
|
|
52
|
+
return false;
|
|
53
|
+
if (entry.mode === "proxy")
|
|
54
|
+
return true;
|
|
55
|
+
const authFields = cacheEntry?.authFields;
|
|
56
|
+
if (!authFields)
|
|
57
|
+
return null;
|
|
58
|
+
const config = entry.config ?? {};
|
|
59
|
+
for (const [field, info] of Object.entries(authFields)) {
|
|
60
|
+
if (!info.required)
|
|
61
|
+
continue;
|
|
62
|
+
if (info.automated)
|
|
63
|
+
continue;
|
|
64
|
+
if (!(field in config))
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
67
|
+
return true;
|
|
68
|
+
}
|
|
26
69
|
// ============================================
|
|
27
70
|
// Internal helpers
|
|
28
71
|
// ============================================
|
|
@@ -74,7 +117,9 @@ async function decryptConfigSecrets(obj, encryptionKey) {
|
|
|
74
117
|
if (typeof value === "string" && value.startsWith(SECRET_PREFIX)) {
|
|
75
118
|
result[key] = await decryptSecret(value.slice(SECRET_PREFIX.length), encryptionKey);
|
|
76
119
|
}
|
|
77
|
-
else if (value !== null &&
|
|
120
|
+
else if (value !== null &&
|
|
121
|
+
typeof value === "object" &&
|
|
122
|
+
!Array.isArray(value)) {
|
|
78
123
|
result[key] = await decryptConfigSecrets(value, encryptionKey);
|
|
79
124
|
}
|
|
80
125
|
else {
|
|
@@ -92,7 +137,7 @@ async function decryptConfigSecrets(obj, encryptionKey) {
|
|
|
92
137
|
* Fallback: _httpStatus from tool result body
|
|
93
138
|
*/
|
|
94
139
|
function isUnauthorized(result) {
|
|
95
|
-
if (!result || typeof result !==
|
|
140
|
+
if (!result || typeof result !== "object")
|
|
96
141
|
return false;
|
|
97
142
|
const r = result;
|
|
98
143
|
// Primary: HTTP status forwarded by the registry and set by callRegistry
|
|
@@ -107,17 +152,21 @@ function isUnauthorized(result) {
|
|
|
107
152
|
// ============================================
|
|
108
153
|
// Local auth form HTML
|
|
109
154
|
// ============================================
|
|
110
|
-
const esc = (s) => s
|
|
155
|
+
const esc = (s) => s
|
|
156
|
+
.replace(/&/g, "&")
|
|
157
|
+
.replace(/</g, "<")
|
|
158
|
+
.replace(/>/g, ">")
|
|
159
|
+
.replace(/"/g, """);
|
|
111
160
|
function renderCredentialForm(name, fields, error) {
|
|
112
|
-
const fieldHtml = fields
|
|
161
|
+
const fieldHtml = fields
|
|
162
|
+
.map((f) => `
|
|
113
163
|
<div class="field">
|
|
114
164
|
<label for="${esc(f.name)}">${esc(f.label)}</label>
|
|
115
165
|
${f.description ? `<p class="desc">${esc(f.description)}</p>` : ""}
|
|
116
166
|
<input id="${esc(f.name)}" name="${esc(f.name)}" type="${f.secret ? "password" : "text"}" required autocomplete="off" spellcheck="false" />
|
|
117
|
-
</div>`)
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
: "";
|
|
167
|
+
</div>`)
|
|
168
|
+
.join("");
|
|
169
|
+
const errorHtml = error ? `<div class="error">${esc(error)}</div>` : "";
|
|
121
170
|
return `<!DOCTYPE html>
|
|
122
171
|
<html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
|
|
123
172
|
<title>Authenticate \u2014 ${esc(name)}</title>
|
|
@@ -180,6 +229,113 @@ export function createAdk(fs, options = {}) {
|
|
|
180
229
|
async function writeConfig(config) {
|
|
181
230
|
await fs.writeFile(CONFIG_PATH, JSON.stringify(config, null, 2));
|
|
182
231
|
}
|
|
232
|
+
// -------------------------------------------------------------------------
|
|
233
|
+
// Registry cache helpers
|
|
234
|
+
//
|
|
235
|
+
// The cache is purely an internal optimization for the adk's read paths
|
|
236
|
+
// (`ref.list()`, `ref.get()`). Writes happen as side-effects of methods
|
|
237
|
+
// that already call the registry (`ref.add()`, `ref.inspect()`); the
|
|
238
|
+
// public surface never grows new methods. Cache failures (missing file,
|
|
239
|
+
// malformed JSON, fs errors during write) are swallowed so the registry
|
|
240
|
+
// cache can never break a registry operation.
|
|
241
|
+
// -------------------------------------------------------------------------
|
|
242
|
+
async function readRegistryCache() {
|
|
243
|
+
try {
|
|
244
|
+
const content = await fs.readFile(REGISTRY_CACHE_PATH);
|
|
245
|
+
if (!content)
|
|
246
|
+
return { refs: {} };
|
|
247
|
+
const parsed = JSON.parse(content);
|
|
248
|
+
return { refs: parsed.refs ?? {} };
|
|
249
|
+
}
|
|
250
|
+
catch {
|
|
251
|
+
return { refs: {} };
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
async function writeRegistryCache(cache) {
|
|
255
|
+
try {
|
|
256
|
+
await fs.writeFile(REGISTRY_CACHE_PATH, JSON.stringify(cache, null, 2));
|
|
257
|
+
}
|
|
258
|
+
catch {
|
|
259
|
+
// Best-effort. A failed cache write should never break the operation
|
|
260
|
+
// that triggered it.
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
/**
|
|
264
|
+
* Project an inspect/list response into the slim shape we cache. Drops
|
|
265
|
+
* `inputSchema` (too large) and `fullTokens` (registry-internal). Returns
|
|
266
|
+
* undefined if the response carries nothing worth caching.
|
|
267
|
+
*/
|
|
268
|
+
function buildCacheEntry(ref, info) {
|
|
269
|
+
if (!info)
|
|
270
|
+
return undefined;
|
|
271
|
+
const toolSource = info.tools ?? info.toolSummaries;
|
|
272
|
+
const tools = toolSource?.map((t) => {
|
|
273
|
+
const slim = { name: t.name };
|
|
274
|
+
if (t.description !== undefined)
|
|
275
|
+
slim.description = t.description;
|
|
276
|
+
return slim;
|
|
277
|
+
});
|
|
278
|
+
if (info.description === undefined && (!tools || tools.length === 0)) {
|
|
279
|
+
return undefined;
|
|
280
|
+
}
|
|
281
|
+
const entry = {
|
|
282
|
+
ref,
|
|
283
|
+
fetchedAt: new Date().toISOString(),
|
|
284
|
+
};
|
|
285
|
+
if (info.description !== undefined)
|
|
286
|
+
entry.description = info.description;
|
|
287
|
+
if (tools && tools.length > 0)
|
|
288
|
+
entry.tools = tools;
|
|
289
|
+
return entry;
|
|
290
|
+
}
|
|
291
|
+
async function upsertRegistryCacheEntry(name, entry) {
|
|
292
|
+
if (!entry)
|
|
293
|
+
return;
|
|
294
|
+
const cache = await readRegistryCache();
|
|
295
|
+
cache.refs[name] = entry;
|
|
296
|
+
await writeRegistryCache(cache);
|
|
297
|
+
}
|
|
298
|
+
/**
|
|
299
|
+
* Merge `authFields` into an existing cache entry without clobbering
|
|
300
|
+
* description/tools, or create a minimal entry if one doesn't exist
|
|
301
|
+
* yet. Called from `authStatus` so the slim {required, automated}
|
|
302
|
+
* shape is always available for `isRefAuthComplete` to answer
|
|
303
|
+
* locally on subsequent calls.
|
|
304
|
+
*/
|
|
305
|
+
async function upsertRegistryCacheAuthFields(name, ref, authFields) {
|
|
306
|
+
const cache = await readRegistryCache();
|
|
307
|
+
const existing = cache.refs[name];
|
|
308
|
+
cache.refs[name] = {
|
|
309
|
+
...(existing ?? { ref, fetchedAt: new Date().toISOString() }),
|
|
310
|
+
authFields,
|
|
311
|
+
// Refresh fetchedAt so freshness telemetry stays accurate.
|
|
312
|
+
fetchedAt: new Date().toISOString(),
|
|
313
|
+
};
|
|
314
|
+
await writeRegistryCache(cache);
|
|
315
|
+
}
|
|
316
|
+
async function removeRegistryCacheEntry(name) {
|
|
317
|
+
const cache = await readRegistryCache();
|
|
318
|
+
if (!(name in cache.refs))
|
|
319
|
+
return;
|
|
320
|
+
delete cache.refs[name];
|
|
321
|
+
await writeRegistryCache(cache);
|
|
322
|
+
}
|
|
323
|
+
/**
|
|
324
|
+
* Hydrate a `ResolvedRef` with cached registry metadata when available.
|
|
325
|
+
* Pure: never mutates input. Leaves `description` / `tools` undefined when
|
|
326
|
+
* the cache has no entry, so callers can apply their own UX fallback.
|
|
327
|
+
*/
|
|
328
|
+
function hydrateFromCache(ref, cache) {
|
|
329
|
+
const cached = cache.refs[ref.name];
|
|
330
|
+
if (!cached)
|
|
331
|
+
return ref;
|
|
332
|
+
const next = { ...ref };
|
|
333
|
+
if (cached.description !== undefined)
|
|
334
|
+
next.description = cached.description;
|
|
335
|
+
if (cached.tools !== undefined)
|
|
336
|
+
next.tools = cached.tools;
|
|
337
|
+
return next;
|
|
338
|
+
}
|
|
183
339
|
/**
|
|
184
340
|
* Store a secret value in a ref's config, encrypted if encryptionKey is set.
|
|
185
341
|
* The value is stored inline as "secret:<encrypted>" in consumer-config.json.
|
|
@@ -290,7 +446,10 @@ export function createAdk(fs, options = {}) {
|
|
|
290
446
|
let reqId = 0;
|
|
291
447
|
let sessionId;
|
|
292
448
|
async function rpc(method, rpcParams) {
|
|
293
|
-
const reqHeaders = {
|
|
449
|
+
const reqHeaders = {
|
|
450
|
+
...headers,
|
|
451
|
+
...(sessionId ? { "Mcp-Session-Id": sessionId } : {}),
|
|
452
|
+
};
|
|
294
453
|
const res = await globalThis.fetch(url, {
|
|
295
454
|
method: "POST",
|
|
296
455
|
headers: reqHeaders,
|
|
@@ -331,7 +490,7 @@ export function createAdk(fs, options = {}) {
|
|
|
331
490
|
}
|
|
332
491
|
return undefined;
|
|
333
492
|
}
|
|
334
|
-
const json = await res.json();
|
|
493
|
+
const json = (await res.json());
|
|
335
494
|
if (json.error)
|
|
336
495
|
throw new Error(`MCP RPC error: ${json.error.message}`);
|
|
337
496
|
return json.result;
|
|
@@ -343,20 +502,32 @@ export function createAdk(fs, options = {}) {
|
|
|
343
502
|
clientInfo: { name: "adk", version: "1.0.0" },
|
|
344
503
|
});
|
|
345
504
|
await rpc("notifications/initialized").catch(() => { });
|
|
346
|
-
const result = await rpc("tools/call", {
|
|
505
|
+
const result = (await rpc("tools/call", {
|
|
506
|
+
name: toolName,
|
|
507
|
+
arguments: params,
|
|
508
|
+
}));
|
|
347
509
|
const textContent = result?.content?.find((c) => c.type === "text");
|
|
348
510
|
if (textContent?.text) {
|
|
349
511
|
try {
|
|
350
|
-
return {
|
|
512
|
+
return {
|
|
513
|
+
success: true,
|
|
514
|
+
result: JSON.parse(textContent.text),
|
|
515
|
+
};
|
|
351
516
|
}
|
|
352
517
|
catch {
|
|
353
|
-
return {
|
|
518
|
+
return {
|
|
519
|
+
success: true,
|
|
520
|
+
result: textContent.text,
|
|
521
|
+
};
|
|
354
522
|
}
|
|
355
523
|
}
|
|
356
524
|
return { success: true, result };
|
|
357
525
|
}
|
|
358
526
|
catch (err) {
|
|
359
|
-
return {
|
|
527
|
+
return {
|
|
528
|
+
success: false,
|
|
529
|
+
error: err instanceof Error ? err.message : String(err),
|
|
530
|
+
};
|
|
360
531
|
}
|
|
361
532
|
}
|
|
362
533
|
function callbackUrl() {
|
|
@@ -369,7 +540,7 @@ export function createAdk(fs, options = {}) {
|
|
|
369
540
|
const res = await globalThis.fetch(url);
|
|
370
541
|
if (!res.ok)
|
|
371
542
|
return null;
|
|
372
|
-
const data = await res.json();
|
|
543
|
+
const data = (await res.json());
|
|
373
544
|
if (data.authorization_endpoint && data.token_endpoint) {
|
|
374
545
|
return data;
|
|
375
546
|
}
|
|
@@ -739,7 +910,9 @@ export function createAdk(fs, options = {}) {
|
|
|
739
910
|
...final,
|
|
740
911
|
proxy: {
|
|
741
912
|
mode: discovered.proxy.mode,
|
|
742
|
-
...(discovered.proxy.agent && {
|
|
913
|
+
...(discovered.proxy.agent && {
|
|
914
|
+
agent: discovered.proxy.agent,
|
|
915
|
+
}),
|
|
743
916
|
},
|
|
744
917
|
};
|
|
745
918
|
}
|
|
@@ -866,7 +1039,12 @@ export function createAdk(fs, options = {}) {
|
|
|
866
1039
|
}));
|
|
867
1040
|
return results.map((r) => r.status === "fulfilled"
|
|
868
1041
|
? r.value
|
|
869
|
-
: {
|
|
1042
|
+
: {
|
|
1043
|
+
name: "unknown",
|
|
1044
|
+
url: "unknown",
|
|
1045
|
+
status: "error",
|
|
1046
|
+
error: "unknown",
|
|
1047
|
+
});
|
|
870
1048
|
},
|
|
871
1049
|
async auth(nameOrUrl, credential) {
|
|
872
1050
|
// Encrypt the secret value up-front so the write path is uniform;
|
|
@@ -1146,7 +1324,10 @@ export function createAdk(fs, options = {}) {
|
|
|
1146
1324
|
entry = { ...entry, scheme: "registry" };
|
|
1147
1325
|
}
|
|
1148
1326
|
else if (entry.url) {
|
|
1149
|
-
entry = {
|
|
1327
|
+
entry = {
|
|
1328
|
+
...entry,
|
|
1329
|
+
scheme: entry.url.startsWith("http") ? "https" : "mcp",
|
|
1330
|
+
};
|
|
1150
1331
|
}
|
|
1151
1332
|
else {
|
|
1152
1333
|
throw new AdkError({
|
|
@@ -1174,6 +1355,7 @@ export function createAdk(fs, options = {}) {
|
|
|
1174
1355
|
details: { ref: entry.ref, scheme: entry.scheme },
|
|
1175
1356
|
});
|
|
1176
1357
|
}
|
|
1358
|
+
let cacheEntry;
|
|
1177
1359
|
if (hasRegistries || entry.sourceRegistry?.url) {
|
|
1178
1360
|
try {
|
|
1179
1361
|
const consumer = await buildConsumerForRef(entry);
|
|
@@ -1181,9 +1363,10 @@ export function createAdk(fs, options = {}) {
|
|
|
1181
1363
|
const info = await consumer.inspect(agentToInspect);
|
|
1182
1364
|
const requiresValidation = !!entry.sourceRegistry;
|
|
1183
1365
|
if (requiresValidation) {
|
|
1184
|
-
const hasContent = info &&
|
|
1185
|
-
(info.
|
|
1186
|
-
|
|
1366
|
+
const hasContent = info &&
|
|
1367
|
+
(info.description ||
|
|
1368
|
+
(info.tools && info.tools.length > 0) ||
|
|
1369
|
+
(info.toolSummaries && info.toolSummaries.length > 0));
|
|
1187
1370
|
if (!hasContent) {
|
|
1188
1371
|
// Inspect returned empty — fall back to browse to check if agent exists
|
|
1189
1372
|
const registryUrl = entry.sourceRegistry?.url;
|
|
@@ -1205,7 +1388,11 @@ export function createAdk(fs, options = {}) {
|
|
|
1205
1388
|
code: "REF_NOT_FOUND",
|
|
1206
1389
|
message: `Agent "${entry.ref}" not found on ${registryHint}`,
|
|
1207
1390
|
hint: "Check available agents with: adk registry browse",
|
|
1208
|
-
details: {
|
|
1391
|
+
details: {
|
|
1392
|
+
ref: entry.ref,
|
|
1393
|
+
sourceRegistry: entry.sourceRegistry,
|
|
1394
|
+
scheme: entry.scheme,
|
|
1395
|
+
},
|
|
1209
1396
|
});
|
|
1210
1397
|
}
|
|
1211
1398
|
}
|
|
@@ -1215,10 +1402,11 @@ export function createAdk(fs, options = {}) {
|
|
|
1215
1402
|
const agentMode = info?.mode;
|
|
1216
1403
|
if (agentMode)
|
|
1217
1404
|
entry.mode = agentMode;
|
|
1218
|
-
if (info?.upstream && !entry.url && agentMode !==
|
|
1405
|
+
if (info?.upstream && !entry.url && agentMode !== "api") {
|
|
1219
1406
|
entry.url = info.upstream;
|
|
1220
1407
|
entry.scheme = entry.scheme ?? "mcp";
|
|
1221
1408
|
}
|
|
1409
|
+
cacheEntry = buildCacheEntry(entry.ref, info);
|
|
1222
1410
|
}
|
|
1223
1411
|
catch (err) {
|
|
1224
1412
|
if (err instanceof AdkError)
|
|
@@ -1227,13 +1415,17 @@ export function createAdk(fs, options = {}) {
|
|
|
1227
1415
|
code: "REGISTRY_UNREACHABLE",
|
|
1228
1416
|
message: `Could not reach registry to validate "${entry.ref}"`,
|
|
1229
1417
|
hint: "Check your registry connection with: adk registry test",
|
|
1230
|
-
details: {
|
|
1418
|
+
details: {
|
|
1419
|
+
ref: entry.ref,
|
|
1420
|
+
error: err instanceof Error ? err.message : String(err),
|
|
1421
|
+
},
|
|
1231
1422
|
cause: err,
|
|
1232
1423
|
});
|
|
1233
1424
|
}
|
|
1234
1425
|
}
|
|
1235
1426
|
const refs = [...(config.refs ?? []), entry];
|
|
1236
1427
|
await writeConfig({ ...config, refs });
|
|
1428
|
+
await upsertRegistryCacheEntry(name, cacheEntry);
|
|
1237
1429
|
return { security };
|
|
1238
1430
|
},
|
|
1239
1431
|
async remove(name) {
|
|
@@ -1245,15 +1437,27 @@ export function createAdk(fs, options = {}) {
|
|
|
1245
1437
|
if (refs.length === before)
|
|
1246
1438
|
return false;
|
|
1247
1439
|
await writeConfig({ ...config, refs });
|
|
1440
|
+
await removeRegistryCacheEntry(name);
|
|
1248
1441
|
return true;
|
|
1249
1442
|
},
|
|
1250
1443
|
async list() {
|
|
1251
|
-
const config = await
|
|
1252
|
-
|
|
1444
|
+
const [config, cache] = await Promise.all([
|
|
1445
|
+
readConfig(),
|
|
1446
|
+
readRegistryCache(),
|
|
1447
|
+
]);
|
|
1448
|
+
return (config.refs ?? [])
|
|
1449
|
+
.map(normalizeRef)
|
|
1450
|
+
.map((r) => hydrateFromCache(r, cache));
|
|
1253
1451
|
},
|
|
1254
1452
|
async get(name) {
|
|
1255
|
-
const config = await
|
|
1256
|
-
|
|
1453
|
+
const [config, cache] = await Promise.all([
|
|
1454
|
+
readConfig(),
|
|
1455
|
+
readRegistryCache(),
|
|
1456
|
+
]);
|
|
1457
|
+
const found = findRef(config.refs ?? [], name);
|
|
1458
|
+
if (!found)
|
|
1459
|
+
return null;
|
|
1460
|
+
return hydrateFromCache(found, cache);
|
|
1257
1461
|
},
|
|
1258
1462
|
async update(name, updates) {
|
|
1259
1463
|
const config = await readConfig();
|
|
@@ -1299,27 +1503,35 @@ export function createAdk(fs, options = {}) {
|
|
|
1299
1503
|
if (!entry)
|
|
1300
1504
|
throw new Error(`Ref "${name}" not found`);
|
|
1301
1505
|
const consumer = await buildConsumerForRef(entry);
|
|
1302
|
-
|
|
1506
|
+
const result = await consumer.inspect(entry.sourceRegistry?.agentPath ?? entry.ref, entry.sourceRegistry?.url, opts);
|
|
1507
|
+
// Side-effect: refresh the registry cache so subsequent ref.list()
|
|
1508
|
+
// / ref.get() calls see the latest description and tool summaries
|
|
1509
|
+
// without another network round-trip. Strips inputSchema (caller's
|
|
1510
|
+
// `result` is unaffected — it still carries the full data).
|
|
1511
|
+
await upsertRegistryCacheEntry(name, buildCacheEntry(entry.ref, result));
|
|
1512
|
+
return result;
|
|
1303
1513
|
},
|
|
1304
1514
|
async call(name, tool, params) {
|
|
1305
1515
|
const config = await readConfig();
|
|
1306
1516
|
const entry = findRef(config.refs ?? [], name);
|
|
1307
1517
|
if (!entry)
|
|
1308
1518
|
throw new Error(`Ref "${name}" not found`);
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1519
|
+
const accessToken = (await readRefSecret(name, "access_token")) ??
|
|
1520
|
+
(await readRefSecret(name, "api_key")) ??
|
|
1521
|
+
(await readRefSecret(name, "token"));
|
|
1312
1522
|
// Resolve custom headers from config (e.g. { "X-API-Key": "secret:..." })
|
|
1313
1523
|
const refConfig = (entry.config ?? {});
|
|
1314
1524
|
const rawHeaders = refConfig.headers;
|
|
1315
1525
|
let resolvedHeaders;
|
|
1316
|
-
if (rawHeaders && typeof rawHeaders ===
|
|
1526
|
+
if (rawHeaders && typeof rawHeaders === "object") {
|
|
1317
1527
|
resolvedHeaders = {};
|
|
1318
1528
|
for (const [k, v] of Object.entries(rawHeaders)) {
|
|
1319
|
-
if (typeof v ===
|
|
1529
|
+
if (typeof v === "string" &&
|
|
1530
|
+
v.startsWith(SECRET_PREFIX) &&
|
|
1531
|
+
options.encryptionKey) {
|
|
1320
1532
|
resolvedHeaders[k] = await decryptSecret(v.slice(SECRET_PREFIX.length), options.encryptionKey);
|
|
1321
1533
|
}
|
|
1322
|
-
else if (typeof v ===
|
|
1534
|
+
else if (typeof v === "string") {
|
|
1323
1535
|
resolvedHeaders[k] = v;
|
|
1324
1536
|
}
|
|
1325
1537
|
}
|
|
@@ -1327,8 +1539,8 @@ export function createAdk(fs, options = {}) {
|
|
|
1327
1539
|
const doCall = async (token) => {
|
|
1328
1540
|
// Direct MCP only for redirect/proxy agents with an MCP upstream.
|
|
1329
1541
|
// API-mode agents must go through the registry (it does REST translation).
|
|
1330
|
-
const agentMode = entry.mode ??
|
|
1331
|
-
if (token && entry.url && agentMode !==
|
|
1542
|
+
const agentMode = entry.mode ?? "redirect";
|
|
1543
|
+
if (token && entry.url && agentMode !== "api") {
|
|
1332
1544
|
return callMcpDirect(entry.url, tool, params ?? {}, token, resolvedHeaders);
|
|
1333
1545
|
}
|
|
1334
1546
|
const consumer = await buildConsumerForRef(entry);
|
|
@@ -1444,10 +1656,15 @@ export function createAdk(fs, options = {}) {
|
|
|
1444
1656
|
}
|
|
1445
1657
|
else if (security.type === "apiKey") {
|
|
1446
1658
|
const apiKeySec = security;
|
|
1447
|
-
const toStorageKey = (headerName) => headerName
|
|
1659
|
+
const toStorageKey = (headerName) => headerName
|
|
1660
|
+
.toLowerCase()
|
|
1661
|
+
.replace(/[^a-z0-9]+/g, "_")
|
|
1662
|
+
.replace(/^_|_$/g, "");
|
|
1448
1663
|
// config.headers: { "Header-Name": "value" } — check by header name (case-insensitive)
|
|
1449
1664
|
const configHeaders = entry?.config?.headers;
|
|
1450
|
-
const configHeaderKeys = configHeaders
|
|
1665
|
+
const configHeaderKeys = configHeaders
|
|
1666
|
+
? Object.keys(configHeaders)
|
|
1667
|
+
: [];
|
|
1451
1668
|
const hasConfigHeader = (name) => configHeaderKeys.some((k) => k.toLowerCase() === name.toLowerCase());
|
|
1452
1669
|
// Collect all declared header names from the security scheme
|
|
1453
1670
|
const declaredHeaders = apiKeySec.headers
|
|
@@ -1485,6 +1702,20 @@ export function createAdk(fs, options = {}) {
|
|
|
1485
1702
|
};
|
|
1486
1703
|
}
|
|
1487
1704
|
const complete = Object.values(fields).every((f) => !f.required || f.present || f.resolvable);
|
|
1705
|
+
// Persist the slim {required, automated} per-field shape into the
|
|
1706
|
+
// registry cache so `isRefAuthComplete` can answer subsequent
|
|
1707
|
+
// host-side "is this ref ready?" checks without re-fetching the
|
|
1708
|
+
// security scheme. We deliberately omit `present`/`resolvable`
|
|
1709
|
+
// because those are computed against the current entry.config and
|
|
1710
|
+
// host environment — caching them would go stale immediately.
|
|
1711
|
+
const authFields = {};
|
|
1712
|
+
for (const [field, info] of Object.entries(fields)) {
|
|
1713
|
+
authFields[field] = {
|
|
1714
|
+
required: info.required,
|
|
1715
|
+
automated: info.automated,
|
|
1716
|
+
};
|
|
1717
|
+
}
|
|
1718
|
+
await upsertRegistryCacheAuthFields(name, entry.ref, authFields);
|
|
1488
1719
|
return { name, security, complete, fields };
|
|
1489
1720
|
},
|
|
1490
1721
|
async auth(name, opts) {
|
|
@@ -1496,7 +1727,9 @@ export function createAdk(fs, options = {}) {
|
|
|
1496
1727
|
// agent. The registry owns the client_id/secret and returns an authorize
|
|
1497
1728
|
// URL pointing at the registry's OAuth callback domain, so the user
|
|
1498
1729
|
// completes the flow against the registry instead of localhost.
|
|
1499
|
-
const proxy = await resolveProxyForRef(entry, {
|
|
1730
|
+
const proxy = await resolveProxyForRef(entry, {
|
|
1731
|
+
preferLocal: opts?.preferLocal,
|
|
1732
|
+
});
|
|
1500
1733
|
if (proxy) {
|
|
1501
1734
|
const params = { name };
|
|
1502
1735
|
if (opts?.apiKey !== undefined)
|
|
@@ -1517,12 +1750,18 @@ export function createAdk(fs, options = {}) {
|
|
|
1517
1750
|
}
|
|
1518
1751
|
if (security.type === "apiKey") {
|
|
1519
1752
|
const apiKeySec = security;
|
|
1520
|
-
const toStorageKey = (headerName) => headerName
|
|
1753
|
+
const toStorageKey = (headerName) => headerName
|
|
1754
|
+
.toLowerCase()
|
|
1755
|
+
.replace(/[^a-z0-9]+/g, "_")
|
|
1756
|
+
.replace(/^_|_$/g, "");
|
|
1521
1757
|
// Check existing config.headers
|
|
1522
1758
|
const existingHeaders = (entry.config ?? {}).headers;
|
|
1523
1759
|
// Collect declared headers: from security.headers or security.name
|
|
1524
1760
|
const declaredHeaders = apiKeySec.headers
|
|
1525
|
-
? Object.entries(apiKeySec.headers).map(([h, meta]) => ({
|
|
1761
|
+
? Object.entries(apiKeySec.headers).map(([h, meta]) => ({
|
|
1762
|
+
headerName: h,
|
|
1763
|
+
description: meta.description,
|
|
1764
|
+
}))
|
|
1526
1765
|
: apiKeySec.name
|
|
1527
1766
|
? [{ headerName: apiKeySec.name }]
|
|
1528
1767
|
: [];
|
|
@@ -1532,12 +1771,13 @@ export function createAdk(fs, options = {}) {
|
|
|
1532
1771
|
for (const { headerName, description } of declaredHeaders) {
|
|
1533
1772
|
const storageKey = toStorageKey(headerName);
|
|
1534
1773
|
// Check: credentials param → existing config.headers → legacy config key → resolve callback
|
|
1535
|
-
const value = opts?.credentials?.[storageKey]
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
??
|
|
1540
|
-
|
|
1774
|
+
const value = opts?.credentials?.[storageKey] ??
|
|
1775
|
+
opts?.credentials?.[headerName] ??
|
|
1776
|
+
(existingHeaders &&
|
|
1777
|
+
Object.entries(existingHeaders).find(([k]) => k.toLowerCase() === headerName.toLowerCase())?.[1]) ??
|
|
1778
|
+
opts?.apiKey ??
|
|
1779
|
+
(await readRefSecret(name, storageKey)) ??
|
|
1780
|
+
(await tryResolve(storageKey));
|
|
1541
1781
|
if (value) {
|
|
1542
1782
|
resolvedHeaders[headerName] = value;
|
|
1543
1783
|
}
|
|
@@ -1557,22 +1797,28 @@ export function createAdk(fs, options = {}) {
|
|
|
1557
1797
|
const encKey = options.encryptionKey;
|
|
1558
1798
|
const headersToStore = {};
|
|
1559
1799
|
for (const [h, v] of Object.entries(resolvedHeaders)) {
|
|
1560
|
-
headersToStore[h] = encKey
|
|
1800
|
+
headersToStore[h] = encKey
|
|
1801
|
+
? `${SECRET_PREFIX}${await encryptSecret(v, encKey)}`
|
|
1802
|
+
: v;
|
|
1561
1803
|
}
|
|
1562
1804
|
await ref.update(name, { config: { headers: headersToStore } });
|
|
1563
1805
|
return { type: "apiKey", complete: true };
|
|
1564
1806
|
}
|
|
1565
1807
|
// Fallback: no headers declared → generic api_key
|
|
1566
|
-
const key = opts?.credentials?.["api_key"] ??
|
|
1808
|
+
const key = opts?.credentials?.["api_key"] ??
|
|
1809
|
+
opts?.apiKey ??
|
|
1810
|
+
(await tryResolve("api_key"));
|
|
1567
1811
|
if (!key) {
|
|
1568
1812
|
return {
|
|
1569
1813
|
type: "apiKey",
|
|
1570
1814
|
complete: false,
|
|
1571
|
-
fields: [
|
|
1815
|
+
fields: [
|
|
1816
|
+
{
|
|
1572
1817
|
name: "api_key",
|
|
1573
1818
|
label: "API Key",
|
|
1574
1819
|
secret: true,
|
|
1575
|
-
}
|
|
1820
|
+
},
|
|
1821
|
+
],
|
|
1576
1822
|
};
|
|
1577
1823
|
}
|
|
1578
1824
|
await storeRefSecret(name, "api_key", key);
|
|
@@ -1582,14 +1828,22 @@ export function createAdk(fs, options = {}) {
|
|
|
1582
1828
|
const httpSec = security;
|
|
1583
1829
|
const isBasic = httpSec.scheme === "basic";
|
|
1584
1830
|
if (isBasic) {
|
|
1585
|
-
const username = opts?.credentials?.["username"] ?? await tryResolve("username");
|
|
1586
|
-
const password = opts?.credentials?.["password"] ?? await tryResolve("password");
|
|
1831
|
+
const username = opts?.credentials?.["username"] ?? (await tryResolve("username"));
|
|
1832
|
+
const password = opts?.credentials?.["password"] ?? (await tryResolve("password"));
|
|
1587
1833
|
if (!username || !password) {
|
|
1588
1834
|
const missingFields = [];
|
|
1589
1835
|
if (!username)
|
|
1590
|
-
missingFields.push({
|
|
1836
|
+
missingFields.push({
|
|
1837
|
+
name: "username",
|
|
1838
|
+
label: "Username",
|
|
1839
|
+
secret: false,
|
|
1840
|
+
});
|
|
1591
1841
|
if (!password)
|
|
1592
|
-
missingFields.push({
|
|
1842
|
+
missingFields.push({
|
|
1843
|
+
name: "password",
|
|
1844
|
+
label: "Password",
|
|
1845
|
+
secret: true,
|
|
1846
|
+
});
|
|
1593
1847
|
return { type: "http", complete: false, fields: missingFields };
|
|
1594
1848
|
}
|
|
1595
1849
|
// Store as base64 encoded basic auth token
|
|
@@ -1598,7 +1852,9 @@ export function createAdk(fs, options = {}) {
|
|
|
1598
1852
|
return { type: "http", complete: true };
|
|
1599
1853
|
}
|
|
1600
1854
|
// Bearer token
|
|
1601
|
-
const token = opts?.credentials?.["token"] ??
|
|
1855
|
+
const token = opts?.credentials?.["token"] ??
|
|
1856
|
+
opts?.apiKey ??
|
|
1857
|
+
(await tryResolve("token"));
|
|
1602
1858
|
if (!token) {
|
|
1603
1859
|
return {
|
|
1604
1860
|
type: "http",
|
|
@@ -1655,8 +1911,9 @@ export function createAdk(fs, options = {}) {
|
|
|
1655
1911
|
const supportedAuthMethods = metadata.token_endpoint_auth_methods_supported ?? ["none"];
|
|
1656
1912
|
const preferredMethod = supportedAuthMethods.includes("none")
|
|
1657
1913
|
? "none"
|
|
1658
|
-
: supportedAuthMethods[0] ?? "client_secret_post";
|
|
1659
|
-
const securityClientName = security
|
|
1914
|
+
: (supportedAuthMethods[0] ?? "client_secret_post");
|
|
1915
|
+
const securityClientName = security
|
|
1916
|
+
.clientName;
|
|
1660
1917
|
const reg = await dynamicClientRegistration(metadata.registration_endpoint, {
|
|
1661
1918
|
clientName: securityClientName ?? options.oauthClientName ?? "adk",
|
|
1662
1919
|
redirectUris: [redirectUri],
|
|
@@ -1674,10 +1931,18 @@ export function createAdk(fs, options = {}) {
|
|
|
1674
1931
|
// Return fields telling the caller what OAuth credentials to provide
|
|
1675
1932
|
const missingFields = [];
|
|
1676
1933
|
if (!clientId) {
|
|
1677
|
-
missingFields.push({
|
|
1934
|
+
missingFields.push({
|
|
1935
|
+
name: "client_id",
|
|
1936
|
+
label: "Client ID",
|
|
1937
|
+
secret: false,
|
|
1938
|
+
});
|
|
1678
1939
|
}
|
|
1679
1940
|
// Always ask for client_secret alongside client_id — most providers need it
|
|
1680
|
-
missingFields.push({
|
|
1941
|
+
missingFields.push({
|
|
1942
|
+
name: "client_secret",
|
|
1943
|
+
label: "Client Secret",
|
|
1944
|
+
secret: true,
|
|
1945
|
+
});
|
|
1681
1946
|
return { type: "oauth2", complete: false, fields: missingFields };
|
|
1682
1947
|
}
|
|
1683
1948
|
// State ties the callback back to this ref. Encode as base64 JSON
|
|
@@ -1699,7 +1964,9 @@ export function createAdk(fs, options = {}) {
|
|
|
1699
1964
|
const scopes = agentScopes.length > 0
|
|
1700
1965
|
? [
|
|
1701
1966
|
...agentScopes,
|
|
1702
|
-
...(metadata.scopes_supported?.includes(
|
|
1967
|
+
...(metadata.scopes_supported?.includes("openid")
|
|
1968
|
+
? ["openid"]
|
|
1969
|
+
: []),
|
|
1703
1970
|
]
|
|
1704
1971
|
: metadata.scopes_supported;
|
|
1705
1972
|
// Read provider-specific authorization params from the agent's security section
|
|
@@ -1748,7 +2015,9 @@ export function createAdk(fs, options = {}) {
|
|
|
1748
2015
|
// owns the credential store, so the user needs to submit via
|
|
1749
2016
|
// whatever UI the registry exposes. Supporting this through the
|
|
1750
2017
|
// proxy would need a remote form endpoint — out of scope here.
|
|
1751
|
-
if (result.fields &&
|
|
2018
|
+
if (result.fields &&
|
|
2019
|
+
result.fields.length > 0 &&
|
|
2020
|
+
result.type !== "oauth2") {
|
|
1752
2021
|
if (proxy) {
|
|
1753
2022
|
throw new Error(`Ref "${name}" is sourced from a proxied registry; submit credentials through ${proxy.agent} instead of a local form.`);
|
|
1754
2023
|
}
|
|
@@ -1908,7 +2177,7 @@ export function createAdk(fs, options = {}) {
|
|
|
1908
2177
|
});
|
|
1909
2178
|
if (!res.ok)
|
|
1910
2179
|
return null;
|
|
1911
|
-
const data = await res.json();
|
|
2180
|
+
const data = (await res.json());
|
|
1912
2181
|
const newAccessToken = data.access_token;
|
|
1913
2182
|
if (!newAccessToken)
|
|
1914
2183
|
return null;
|
|
@@ -1943,7 +2212,9 @@ export function createAdk(fs, options = {}) {
|
|
|
1943
2212
|
try {
|
|
1944
2213
|
stateContext = JSON.parse(atob(params.state));
|
|
1945
2214
|
}
|
|
1946
|
-
catch {
|
|
2215
|
+
catch {
|
|
2216
|
+
/* state wasn't base64 JSON — legacy format */
|
|
2217
|
+
}
|
|
1947
2218
|
return { refName: pending.refName, complete: true, stateContext };
|
|
1948
2219
|
}
|
|
1949
2220
|
return { registry, ref, readConfig, writeConfig, handleCallback };
|