@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/cjs/config-store.js
CHANGED
|
@@ -51,14 +51,58 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
51
51
|
};
|
|
52
52
|
})();
|
|
53
53
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
54
|
+
exports.isRefAuthComplete = isRefAuthComplete;
|
|
54
55
|
exports.createAdk = createAdk;
|
|
55
|
-
const define_config_js_1 = require("./define-config.js");
|
|
56
|
-
const registry_consumer_js_1 = require("./registry-consumer.js");
|
|
57
|
-
const crypto_js_1 = require("./crypto.js");
|
|
58
56
|
const adk_error_js_1 = require("./adk-error.js");
|
|
57
|
+
const crypto_js_1 = require("./crypto.js");
|
|
58
|
+
const define_config_js_1 = require("./define-config.js");
|
|
59
59
|
const mcp_client_js_1 = require("./mcp-client.js");
|
|
60
|
+
const registry_consumer_js_1 = require("./registry-consumer.js");
|
|
60
61
|
const CONFIG_PATH = "consumer-config.json";
|
|
62
|
+
const REGISTRY_CACHE_PATH = "registry-cache.json";
|
|
61
63
|
const SECRET_PREFIX = "secret:";
|
|
64
|
+
/**
|
|
65
|
+
* "Is this ref ready to call?" answered locally using the cached
|
|
66
|
+
* security-scheme requirements. Mirrors the `complete` boolean
|
|
67
|
+
* `auth-status` returns, but doesn't need a network round-trip — the
|
|
68
|
+
* cached `authFields` capture what the registry said is required, and
|
|
69
|
+
* we evaluate satisfaction against the entry's current `config`.
|
|
70
|
+
*
|
|
71
|
+
* Behavior:
|
|
72
|
+
* - `mode: 'proxy'` refs → always true. Auth lives server-side; the
|
|
73
|
+
* proxy is the source of truth, no entry-side fields involved.
|
|
74
|
+
* - Cache miss (no `authFields` for this ref yet) → returns `null`,
|
|
75
|
+
* signaling "I don't know — caller should fall back to its own
|
|
76
|
+
* heuristic or call `auth-status` to populate the cache".
|
|
77
|
+
* - Cache hit → for every required, non-automated field, checks
|
|
78
|
+
* presence in `entry.config`. Mirrors the `present || resolvable`
|
|
79
|
+
* check in `auth-status` but evaluates against current config.
|
|
80
|
+
* `automated` fields (e.g. dynamic OAuth client_id) count as
|
|
81
|
+
* satisfied even when absent — adk supplies them at call time.
|
|
82
|
+
*
|
|
83
|
+
* Returning `null` for cache miss is intentional. A boolean would
|
|
84
|
+
* force callers to choose a default that's wrong half the time;
|
|
85
|
+
* `null` lets them branch explicitly.
|
|
86
|
+
*/
|
|
87
|
+
function isRefAuthComplete(entry, cacheEntry) {
|
|
88
|
+
if (typeof entry === "string")
|
|
89
|
+
return false;
|
|
90
|
+
if (entry.mode === "proxy")
|
|
91
|
+
return true;
|
|
92
|
+
const authFields = cacheEntry?.authFields;
|
|
93
|
+
if (!authFields)
|
|
94
|
+
return null;
|
|
95
|
+
const config = entry.config ?? {};
|
|
96
|
+
for (const [field, info] of Object.entries(authFields)) {
|
|
97
|
+
if (!info.required)
|
|
98
|
+
continue;
|
|
99
|
+
if (info.automated)
|
|
100
|
+
continue;
|
|
101
|
+
if (!(field in config))
|
|
102
|
+
return false;
|
|
103
|
+
}
|
|
104
|
+
return true;
|
|
105
|
+
}
|
|
62
106
|
// ============================================
|
|
63
107
|
// Internal helpers
|
|
64
108
|
// ============================================
|
|
@@ -110,7 +154,9 @@ async function decryptConfigSecrets(obj, encryptionKey) {
|
|
|
110
154
|
if (typeof value === "string" && value.startsWith(SECRET_PREFIX)) {
|
|
111
155
|
result[key] = await (0, crypto_js_1.decryptSecret)(value.slice(SECRET_PREFIX.length), encryptionKey);
|
|
112
156
|
}
|
|
113
|
-
else if (value !== null &&
|
|
157
|
+
else if (value !== null &&
|
|
158
|
+
typeof value === "object" &&
|
|
159
|
+
!Array.isArray(value)) {
|
|
114
160
|
result[key] = await decryptConfigSecrets(value, encryptionKey);
|
|
115
161
|
}
|
|
116
162
|
else {
|
|
@@ -128,7 +174,7 @@ async function decryptConfigSecrets(obj, encryptionKey) {
|
|
|
128
174
|
* Fallback: _httpStatus from tool result body
|
|
129
175
|
*/
|
|
130
176
|
function isUnauthorized(result) {
|
|
131
|
-
if (!result || typeof result !==
|
|
177
|
+
if (!result || typeof result !== "object")
|
|
132
178
|
return false;
|
|
133
179
|
const r = result;
|
|
134
180
|
// Primary: HTTP status forwarded by the registry and set by callRegistry
|
|
@@ -143,17 +189,21 @@ function isUnauthorized(result) {
|
|
|
143
189
|
// ============================================
|
|
144
190
|
// Local auth form HTML
|
|
145
191
|
// ============================================
|
|
146
|
-
const esc = (s) => s
|
|
192
|
+
const esc = (s) => s
|
|
193
|
+
.replace(/&/g, "&")
|
|
194
|
+
.replace(/</g, "<")
|
|
195
|
+
.replace(/>/g, ">")
|
|
196
|
+
.replace(/"/g, """);
|
|
147
197
|
function renderCredentialForm(name, fields, error) {
|
|
148
|
-
const fieldHtml = fields
|
|
198
|
+
const fieldHtml = fields
|
|
199
|
+
.map((f) => `
|
|
149
200
|
<div class="field">
|
|
150
201
|
<label for="${esc(f.name)}">${esc(f.label)}</label>
|
|
151
202
|
${f.description ? `<p class="desc">${esc(f.description)}</p>` : ""}
|
|
152
203
|
<input id="${esc(f.name)}" name="${esc(f.name)}" type="${f.secret ? "password" : "text"}" required autocomplete="off" spellcheck="false" />
|
|
153
|
-
</div>`)
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
: "";
|
|
204
|
+
</div>`)
|
|
205
|
+
.join("");
|
|
206
|
+
const errorHtml = error ? `<div class="error">${esc(error)}</div>` : "";
|
|
157
207
|
return `<!DOCTYPE html>
|
|
158
208
|
<html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
|
|
159
209
|
<title>Authenticate \u2014 ${esc(name)}</title>
|
|
@@ -216,6 +266,113 @@ function createAdk(fs, options = {}) {
|
|
|
216
266
|
async function writeConfig(config) {
|
|
217
267
|
await fs.writeFile(CONFIG_PATH, JSON.stringify(config, null, 2));
|
|
218
268
|
}
|
|
269
|
+
// -------------------------------------------------------------------------
|
|
270
|
+
// Registry cache helpers
|
|
271
|
+
//
|
|
272
|
+
// The cache is purely an internal optimization for the adk's read paths
|
|
273
|
+
// (`ref.list()`, `ref.get()`). Writes happen as side-effects of methods
|
|
274
|
+
// that already call the registry (`ref.add()`, `ref.inspect()`); the
|
|
275
|
+
// public surface never grows new methods. Cache failures (missing file,
|
|
276
|
+
// malformed JSON, fs errors during write) are swallowed so the registry
|
|
277
|
+
// cache can never break a registry operation.
|
|
278
|
+
// -------------------------------------------------------------------------
|
|
279
|
+
async function readRegistryCache() {
|
|
280
|
+
try {
|
|
281
|
+
const content = await fs.readFile(REGISTRY_CACHE_PATH);
|
|
282
|
+
if (!content)
|
|
283
|
+
return { refs: {} };
|
|
284
|
+
const parsed = JSON.parse(content);
|
|
285
|
+
return { refs: parsed.refs ?? {} };
|
|
286
|
+
}
|
|
287
|
+
catch {
|
|
288
|
+
return { refs: {} };
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
async function writeRegistryCache(cache) {
|
|
292
|
+
try {
|
|
293
|
+
await fs.writeFile(REGISTRY_CACHE_PATH, JSON.stringify(cache, null, 2));
|
|
294
|
+
}
|
|
295
|
+
catch {
|
|
296
|
+
// Best-effort. A failed cache write should never break the operation
|
|
297
|
+
// that triggered it.
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
/**
|
|
301
|
+
* Project an inspect/list response into the slim shape we cache. Drops
|
|
302
|
+
* `inputSchema` (too large) and `fullTokens` (registry-internal). Returns
|
|
303
|
+
* undefined if the response carries nothing worth caching.
|
|
304
|
+
*/
|
|
305
|
+
function buildCacheEntry(ref, info) {
|
|
306
|
+
if (!info)
|
|
307
|
+
return undefined;
|
|
308
|
+
const toolSource = info.tools ?? info.toolSummaries;
|
|
309
|
+
const tools = toolSource?.map((t) => {
|
|
310
|
+
const slim = { name: t.name };
|
|
311
|
+
if (t.description !== undefined)
|
|
312
|
+
slim.description = t.description;
|
|
313
|
+
return slim;
|
|
314
|
+
});
|
|
315
|
+
if (info.description === undefined && (!tools || tools.length === 0)) {
|
|
316
|
+
return undefined;
|
|
317
|
+
}
|
|
318
|
+
const entry = {
|
|
319
|
+
ref,
|
|
320
|
+
fetchedAt: new Date().toISOString(),
|
|
321
|
+
};
|
|
322
|
+
if (info.description !== undefined)
|
|
323
|
+
entry.description = info.description;
|
|
324
|
+
if (tools && tools.length > 0)
|
|
325
|
+
entry.tools = tools;
|
|
326
|
+
return entry;
|
|
327
|
+
}
|
|
328
|
+
async function upsertRegistryCacheEntry(name, entry) {
|
|
329
|
+
if (!entry)
|
|
330
|
+
return;
|
|
331
|
+
const cache = await readRegistryCache();
|
|
332
|
+
cache.refs[name] = entry;
|
|
333
|
+
await writeRegistryCache(cache);
|
|
334
|
+
}
|
|
335
|
+
/**
|
|
336
|
+
* Merge `authFields` into an existing cache entry without clobbering
|
|
337
|
+
* description/tools, or create a minimal entry if one doesn't exist
|
|
338
|
+
* yet. Called from `authStatus` so the slim {required, automated}
|
|
339
|
+
* shape is always available for `isRefAuthComplete` to answer
|
|
340
|
+
* locally on subsequent calls.
|
|
341
|
+
*/
|
|
342
|
+
async function upsertRegistryCacheAuthFields(name, ref, authFields) {
|
|
343
|
+
const cache = await readRegistryCache();
|
|
344
|
+
const existing = cache.refs[name];
|
|
345
|
+
cache.refs[name] = {
|
|
346
|
+
...(existing ?? { ref, fetchedAt: new Date().toISOString() }),
|
|
347
|
+
authFields,
|
|
348
|
+
// Refresh fetchedAt so freshness telemetry stays accurate.
|
|
349
|
+
fetchedAt: new Date().toISOString(),
|
|
350
|
+
};
|
|
351
|
+
await writeRegistryCache(cache);
|
|
352
|
+
}
|
|
353
|
+
async function removeRegistryCacheEntry(name) {
|
|
354
|
+
const cache = await readRegistryCache();
|
|
355
|
+
if (!(name in cache.refs))
|
|
356
|
+
return;
|
|
357
|
+
delete cache.refs[name];
|
|
358
|
+
await writeRegistryCache(cache);
|
|
359
|
+
}
|
|
360
|
+
/**
|
|
361
|
+
* Hydrate a `ResolvedRef` with cached registry metadata when available.
|
|
362
|
+
* Pure: never mutates input. Leaves `description` / `tools` undefined when
|
|
363
|
+
* the cache has no entry, so callers can apply their own UX fallback.
|
|
364
|
+
*/
|
|
365
|
+
function hydrateFromCache(ref, cache) {
|
|
366
|
+
const cached = cache.refs[ref.name];
|
|
367
|
+
if (!cached)
|
|
368
|
+
return ref;
|
|
369
|
+
const next = { ...ref };
|
|
370
|
+
if (cached.description !== undefined)
|
|
371
|
+
next.description = cached.description;
|
|
372
|
+
if (cached.tools !== undefined)
|
|
373
|
+
next.tools = cached.tools;
|
|
374
|
+
return next;
|
|
375
|
+
}
|
|
219
376
|
/**
|
|
220
377
|
* Store a secret value in a ref's config, encrypted if encryptionKey is set.
|
|
221
378
|
* The value is stored inline as "secret:<encrypted>" in consumer-config.json.
|
|
@@ -326,7 +483,10 @@ function createAdk(fs, options = {}) {
|
|
|
326
483
|
let reqId = 0;
|
|
327
484
|
let sessionId;
|
|
328
485
|
async function rpc(method, rpcParams) {
|
|
329
|
-
const reqHeaders = {
|
|
486
|
+
const reqHeaders = {
|
|
487
|
+
...headers,
|
|
488
|
+
...(sessionId ? { "Mcp-Session-Id": sessionId } : {}),
|
|
489
|
+
};
|
|
330
490
|
const res = await globalThis.fetch(url, {
|
|
331
491
|
method: "POST",
|
|
332
492
|
headers: reqHeaders,
|
|
@@ -367,7 +527,7 @@ function createAdk(fs, options = {}) {
|
|
|
367
527
|
}
|
|
368
528
|
return undefined;
|
|
369
529
|
}
|
|
370
|
-
const json = await res.json();
|
|
530
|
+
const json = (await res.json());
|
|
371
531
|
if (json.error)
|
|
372
532
|
throw new Error(`MCP RPC error: ${json.error.message}`);
|
|
373
533
|
return json.result;
|
|
@@ -379,20 +539,32 @@ function createAdk(fs, options = {}) {
|
|
|
379
539
|
clientInfo: { name: "adk", version: "1.0.0" },
|
|
380
540
|
});
|
|
381
541
|
await rpc("notifications/initialized").catch(() => { });
|
|
382
|
-
const result = await rpc("tools/call", {
|
|
542
|
+
const result = (await rpc("tools/call", {
|
|
543
|
+
name: toolName,
|
|
544
|
+
arguments: params,
|
|
545
|
+
}));
|
|
383
546
|
const textContent = result?.content?.find((c) => c.type === "text");
|
|
384
547
|
if (textContent?.text) {
|
|
385
548
|
try {
|
|
386
|
-
return {
|
|
549
|
+
return {
|
|
550
|
+
success: true,
|
|
551
|
+
result: JSON.parse(textContent.text),
|
|
552
|
+
};
|
|
387
553
|
}
|
|
388
554
|
catch {
|
|
389
|
-
return {
|
|
555
|
+
return {
|
|
556
|
+
success: true,
|
|
557
|
+
result: textContent.text,
|
|
558
|
+
};
|
|
390
559
|
}
|
|
391
560
|
}
|
|
392
561
|
return { success: true, result };
|
|
393
562
|
}
|
|
394
563
|
catch (err) {
|
|
395
|
-
return {
|
|
564
|
+
return {
|
|
565
|
+
success: false,
|
|
566
|
+
error: err instanceof Error ? err.message : String(err),
|
|
567
|
+
};
|
|
396
568
|
}
|
|
397
569
|
}
|
|
398
570
|
function callbackUrl() {
|
|
@@ -405,7 +577,7 @@ function createAdk(fs, options = {}) {
|
|
|
405
577
|
const res = await globalThis.fetch(url);
|
|
406
578
|
if (!res.ok)
|
|
407
579
|
return null;
|
|
408
|
-
const data = await res.json();
|
|
580
|
+
const data = (await res.json());
|
|
409
581
|
if (data.authorization_endpoint && data.token_endpoint) {
|
|
410
582
|
return data;
|
|
411
583
|
}
|
|
@@ -775,7 +947,9 @@ function createAdk(fs, options = {}) {
|
|
|
775
947
|
...final,
|
|
776
948
|
proxy: {
|
|
777
949
|
mode: discovered.proxy.mode,
|
|
778
|
-
...(discovered.proxy.agent && {
|
|
950
|
+
...(discovered.proxy.agent && {
|
|
951
|
+
agent: discovered.proxy.agent,
|
|
952
|
+
}),
|
|
779
953
|
},
|
|
780
954
|
};
|
|
781
955
|
}
|
|
@@ -902,7 +1076,12 @@ function createAdk(fs, options = {}) {
|
|
|
902
1076
|
}));
|
|
903
1077
|
return results.map((r) => r.status === "fulfilled"
|
|
904
1078
|
? r.value
|
|
905
|
-
: {
|
|
1079
|
+
: {
|
|
1080
|
+
name: "unknown",
|
|
1081
|
+
url: "unknown",
|
|
1082
|
+
status: "error",
|
|
1083
|
+
error: "unknown",
|
|
1084
|
+
});
|
|
906
1085
|
},
|
|
907
1086
|
async auth(nameOrUrl, credential) {
|
|
908
1087
|
// Encrypt the secret value up-front so the write path is uniform;
|
|
@@ -1182,7 +1361,10 @@ function createAdk(fs, options = {}) {
|
|
|
1182
1361
|
entry = { ...entry, scheme: "registry" };
|
|
1183
1362
|
}
|
|
1184
1363
|
else if (entry.url) {
|
|
1185
|
-
entry = {
|
|
1364
|
+
entry = {
|
|
1365
|
+
...entry,
|
|
1366
|
+
scheme: entry.url.startsWith("http") ? "https" : "mcp",
|
|
1367
|
+
};
|
|
1186
1368
|
}
|
|
1187
1369
|
else {
|
|
1188
1370
|
throw new adk_error_js_1.AdkError({
|
|
@@ -1210,6 +1392,7 @@ function createAdk(fs, options = {}) {
|
|
|
1210
1392
|
details: { ref: entry.ref, scheme: entry.scheme },
|
|
1211
1393
|
});
|
|
1212
1394
|
}
|
|
1395
|
+
let cacheEntry;
|
|
1213
1396
|
if (hasRegistries || entry.sourceRegistry?.url) {
|
|
1214
1397
|
try {
|
|
1215
1398
|
const consumer = await buildConsumerForRef(entry);
|
|
@@ -1217,9 +1400,10 @@ function createAdk(fs, options = {}) {
|
|
|
1217
1400
|
const info = await consumer.inspect(agentToInspect);
|
|
1218
1401
|
const requiresValidation = !!entry.sourceRegistry;
|
|
1219
1402
|
if (requiresValidation) {
|
|
1220
|
-
const hasContent = info &&
|
|
1221
|
-
(info.
|
|
1222
|
-
|
|
1403
|
+
const hasContent = info &&
|
|
1404
|
+
(info.description ||
|
|
1405
|
+
(info.tools && info.tools.length > 0) ||
|
|
1406
|
+
(info.toolSummaries && info.toolSummaries.length > 0));
|
|
1223
1407
|
if (!hasContent) {
|
|
1224
1408
|
// Inspect returned empty — fall back to browse to check if agent exists
|
|
1225
1409
|
const registryUrl = entry.sourceRegistry?.url;
|
|
@@ -1241,7 +1425,11 @@ function createAdk(fs, options = {}) {
|
|
|
1241
1425
|
code: "REF_NOT_FOUND",
|
|
1242
1426
|
message: `Agent "${entry.ref}" not found on ${registryHint}`,
|
|
1243
1427
|
hint: "Check available agents with: adk registry browse",
|
|
1244
|
-
details: {
|
|
1428
|
+
details: {
|
|
1429
|
+
ref: entry.ref,
|
|
1430
|
+
sourceRegistry: entry.sourceRegistry,
|
|
1431
|
+
scheme: entry.scheme,
|
|
1432
|
+
},
|
|
1245
1433
|
});
|
|
1246
1434
|
}
|
|
1247
1435
|
}
|
|
@@ -1251,10 +1439,11 @@ function createAdk(fs, options = {}) {
|
|
|
1251
1439
|
const agentMode = info?.mode;
|
|
1252
1440
|
if (agentMode)
|
|
1253
1441
|
entry.mode = agentMode;
|
|
1254
|
-
if (info?.upstream && !entry.url && agentMode !==
|
|
1442
|
+
if (info?.upstream && !entry.url && agentMode !== "api") {
|
|
1255
1443
|
entry.url = info.upstream;
|
|
1256
1444
|
entry.scheme = entry.scheme ?? "mcp";
|
|
1257
1445
|
}
|
|
1446
|
+
cacheEntry = buildCacheEntry(entry.ref, info);
|
|
1258
1447
|
}
|
|
1259
1448
|
catch (err) {
|
|
1260
1449
|
if (err instanceof adk_error_js_1.AdkError)
|
|
@@ -1263,13 +1452,17 @@ function createAdk(fs, options = {}) {
|
|
|
1263
1452
|
code: "REGISTRY_UNREACHABLE",
|
|
1264
1453
|
message: `Could not reach registry to validate "${entry.ref}"`,
|
|
1265
1454
|
hint: "Check your registry connection with: adk registry test",
|
|
1266
|
-
details: {
|
|
1455
|
+
details: {
|
|
1456
|
+
ref: entry.ref,
|
|
1457
|
+
error: err instanceof Error ? err.message : String(err),
|
|
1458
|
+
},
|
|
1267
1459
|
cause: err,
|
|
1268
1460
|
});
|
|
1269
1461
|
}
|
|
1270
1462
|
}
|
|
1271
1463
|
const refs = [...(config.refs ?? []), entry];
|
|
1272
1464
|
await writeConfig({ ...config, refs });
|
|
1465
|
+
await upsertRegistryCacheEntry(name, cacheEntry);
|
|
1273
1466
|
return { security };
|
|
1274
1467
|
},
|
|
1275
1468
|
async remove(name) {
|
|
@@ -1281,15 +1474,27 @@ function createAdk(fs, options = {}) {
|
|
|
1281
1474
|
if (refs.length === before)
|
|
1282
1475
|
return false;
|
|
1283
1476
|
await writeConfig({ ...config, refs });
|
|
1477
|
+
await removeRegistryCacheEntry(name);
|
|
1284
1478
|
return true;
|
|
1285
1479
|
},
|
|
1286
1480
|
async list() {
|
|
1287
|
-
const config = await
|
|
1288
|
-
|
|
1481
|
+
const [config, cache] = await Promise.all([
|
|
1482
|
+
readConfig(),
|
|
1483
|
+
readRegistryCache(),
|
|
1484
|
+
]);
|
|
1485
|
+
return (config.refs ?? [])
|
|
1486
|
+
.map(define_config_js_1.normalizeRef)
|
|
1487
|
+
.map((r) => hydrateFromCache(r, cache));
|
|
1289
1488
|
},
|
|
1290
1489
|
async get(name) {
|
|
1291
|
-
const config = await
|
|
1292
|
-
|
|
1490
|
+
const [config, cache] = await Promise.all([
|
|
1491
|
+
readConfig(),
|
|
1492
|
+
readRegistryCache(),
|
|
1493
|
+
]);
|
|
1494
|
+
const found = findRef(config.refs ?? [], name);
|
|
1495
|
+
if (!found)
|
|
1496
|
+
return null;
|
|
1497
|
+
return hydrateFromCache(found, cache);
|
|
1293
1498
|
},
|
|
1294
1499
|
async update(name, updates) {
|
|
1295
1500
|
const config = await readConfig();
|
|
@@ -1335,27 +1540,35 @@ function createAdk(fs, options = {}) {
|
|
|
1335
1540
|
if (!entry)
|
|
1336
1541
|
throw new Error(`Ref "${name}" not found`);
|
|
1337
1542
|
const consumer = await buildConsumerForRef(entry);
|
|
1338
|
-
|
|
1543
|
+
const result = await consumer.inspect(entry.sourceRegistry?.agentPath ?? entry.ref, entry.sourceRegistry?.url, opts);
|
|
1544
|
+
// Side-effect: refresh the registry cache so subsequent ref.list()
|
|
1545
|
+
// / ref.get() calls see the latest description and tool summaries
|
|
1546
|
+
// without another network round-trip. Strips inputSchema (caller's
|
|
1547
|
+
// `result` is unaffected — it still carries the full data).
|
|
1548
|
+
await upsertRegistryCacheEntry(name, buildCacheEntry(entry.ref, result));
|
|
1549
|
+
return result;
|
|
1339
1550
|
},
|
|
1340
1551
|
async call(name, tool, params) {
|
|
1341
1552
|
const config = await readConfig();
|
|
1342
1553
|
const entry = findRef(config.refs ?? [], name);
|
|
1343
1554
|
if (!entry)
|
|
1344
1555
|
throw new Error(`Ref "${name}" not found`);
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1556
|
+
const accessToken = (await readRefSecret(name, "access_token")) ??
|
|
1557
|
+
(await readRefSecret(name, "api_key")) ??
|
|
1558
|
+
(await readRefSecret(name, "token"));
|
|
1348
1559
|
// Resolve custom headers from config (e.g. { "X-API-Key": "secret:..." })
|
|
1349
1560
|
const refConfig = (entry.config ?? {});
|
|
1350
1561
|
const rawHeaders = refConfig.headers;
|
|
1351
1562
|
let resolvedHeaders;
|
|
1352
|
-
if (rawHeaders && typeof rawHeaders ===
|
|
1563
|
+
if (rawHeaders && typeof rawHeaders === "object") {
|
|
1353
1564
|
resolvedHeaders = {};
|
|
1354
1565
|
for (const [k, v] of Object.entries(rawHeaders)) {
|
|
1355
|
-
if (typeof v ===
|
|
1566
|
+
if (typeof v === "string" &&
|
|
1567
|
+
v.startsWith(SECRET_PREFIX) &&
|
|
1568
|
+
options.encryptionKey) {
|
|
1356
1569
|
resolvedHeaders[k] = await (0, crypto_js_1.decryptSecret)(v.slice(SECRET_PREFIX.length), options.encryptionKey);
|
|
1357
1570
|
}
|
|
1358
|
-
else if (typeof v ===
|
|
1571
|
+
else if (typeof v === "string") {
|
|
1359
1572
|
resolvedHeaders[k] = v;
|
|
1360
1573
|
}
|
|
1361
1574
|
}
|
|
@@ -1363,8 +1576,8 @@ function createAdk(fs, options = {}) {
|
|
|
1363
1576
|
const doCall = async (token) => {
|
|
1364
1577
|
// Direct MCP only for redirect/proxy agents with an MCP upstream.
|
|
1365
1578
|
// API-mode agents must go through the registry (it does REST translation).
|
|
1366
|
-
const agentMode = entry.mode ??
|
|
1367
|
-
if (token && entry.url && agentMode !==
|
|
1579
|
+
const agentMode = entry.mode ?? "redirect";
|
|
1580
|
+
if (token && entry.url && agentMode !== "api") {
|
|
1368
1581
|
return callMcpDirect(entry.url, tool, params ?? {}, token, resolvedHeaders);
|
|
1369
1582
|
}
|
|
1370
1583
|
const consumer = await buildConsumerForRef(entry);
|
|
@@ -1480,10 +1693,15 @@ function createAdk(fs, options = {}) {
|
|
|
1480
1693
|
}
|
|
1481
1694
|
else if (security.type === "apiKey") {
|
|
1482
1695
|
const apiKeySec = security;
|
|
1483
|
-
const toStorageKey = (headerName) => headerName
|
|
1696
|
+
const toStorageKey = (headerName) => headerName
|
|
1697
|
+
.toLowerCase()
|
|
1698
|
+
.replace(/[^a-z0-9]+/g, "_")
|
|
1699
|
+
.replace(/^_|_$/g, "");
|
|
1484
1700
|
// config.headers: { "Header-Name": "value" } — check by header name (case-insensitive)
|
|
1485
1701
|
const configHeaders = entry?.config?.headers;
|
|
1486
|
-
const configHeaderKeys = configHeaders
|
|
1702
|
+
const configHeaderKeys = configHeaders
|
|
1703
|
+
? Object.keys(configHeaders)
|
|
1704
|
+
: [];
|
|
1487
1705
|
const hasConfigHeader = (name) => configHeaderKeys.some((k) => k.toLowerCase() === name.toLowerCase());
|
|
1488
1706
|
// Collect all declared header names from the security scheme
|
|
1489
1707
|
const declaredHeaders = apiKeySec.headers
|
|
@@ -1521,6 +1739,20 @@ function createAdk(fs, options = {}) {
|
|
|
1521
1739
|
};
|
|
1522
1740
|
}
|
|
1523
1741
|
const complete = Object.values(fields).every((f) => !f.required || f.present || f.resolvable);
|
|
1742
|
+
// Persist the slim {required, automated} per-field shape into the
|
|
1743
|
+
// registry cache so `isRefAuthComplete` can answer subsequent
|
|
1744
|
+
// host-side "is this ref ready?" checks without re-fetching the
|
|
1745
|
+
// security scheme. We deliberately omit `present`/`resolvable`
|
|
1746
|
+
// because those are computed against the current entry.config and
|
|
1747
|
+
// host environment — caching them would go stale immediately.
|
|
1748
|
+
const authFields = {};
|
|
1749
|
+
for (const [field, info] of Object.entries(fields)) {
|
|
1750
|
+
authFields[field] = {
|
|
1751
|
+
required: info.required,
|
|
1752
|
+
automated: info.automated,
|
|
1753
|
+
};
|
|
1754
|
+
}
|
|
1755
|
+
await upsertRegistryCacheAuthFields(name, entry.ref, authFields);
|
|
1524
1756
|
return { name, security, complete, fields };
|
|
1525
1757
|
},
|
|
1526
1758
|
async auth(name, opts) {
|
|
@@ -1532,7 +1764,9 @@ function createAdk(fs, options = {}) {
|
|
|
1532
1764
|
// agent. The registry owns the client_id/secret and returns an authorize
|
|
1533
1765
|
// URL pointing at the registry's OAuth callback domain, so the user
|
|
1534
1766
|
// completes the flow against the registry instead of localhost.
|
|
1535
|
-
const proxy = await resolveProxyForRef(entry, {
|
|
1767
|
+
const proxy = await resolveProxyForRef(entry, {
|
|
1768
|
+
preferLocal: opts?.preferLocal,
|
|
1769
|
+
});
|
|
1536
1770
|
if (proxy) {
|
|
1537
1771
|
const params = { name };
|
|
1538
1772
|
if (opts?.apiKey !== undefined)
|
|
@@ -1553,12 +1787,18 @@ function createAdk(fs, options = {}) {
|
|
|
1553
1787
|
}
|
|
1554
1788
|
if (security.type === "apiKey") {
|
|
1555
1789
|
const apiKeySec = security;
|
|
1556
|
-
const toStorageKey = (headerName) => headerName
|
|
1790
|
+
const toStorageKey = (headerName) => headerName
|
|
1791
|
+
.toLowerCase()
|
|
1792
|
+
.replace(/[^a-z0-9]+/g, "_")
|
|
1793
|
+
.replace(/^_|_$/g, "");
|
|
1557
1794
|
// Check existing config.headers
|
|
1558
1795
|
const existingHeaders = (entry.config ?? {}).headers;
|
|
1559
1796
|
// Collect declared headers: from security.headers or security.name
|
|
1560
1797
|
const declaredHeaders = apiKeySec.headers
|
|
1561
|
-
? Object.entries(apiKeySec.headers).map(([h, meta]) => ({
|
|
1798
|
+
? Object.entries(apiKeySec.headers).map(([h, meta]) => ({
|
|
1799
|
+
headerName: h,
|
|
1800
|
+
description: meta.description,
|
|
1801
|
+
}))
|
|
1562
1802
|
: apiKeySec.name
|
|
1563
1803
|
? [{ headerName: apiKeySec.name }]
|
|
1564
1804
|
: [];
|
|
@@ -1568,12 +1808,13 @@ function createAdk(fs, options = {}) {
|
|
|
1568
1808
|
for (const { headerName, description } of declaredHeaders) {
|
|
1569
1809
|
const storageKey = toStorageKey(headerName);
|
|
1570
1810
|
// Check: credentials param → existing config.headers → legacy config key → resolve callback
|
|
1571
|
-
const value = opts?.credentials?.[storageKey]
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
??
|
|
1576
|
-
|
|
1811
|
+
const value = opts?.credentials?.[storageKey] ??
|
|
1812
|
+
opts?.credentials?.[headerName] ??
|
|
1813
|
+
(existingHeaders &&
|
|
1814
|
+
Object.entries(existingHeaders).find(([k]) => k.toLowerCase() === headerName.toLowerCase())?.[1]) ??
|
|
1815
|
+
opts?.apiKey ??
|
|
1816
|
+
(await readRefSecret(name, storageKey)) ??
|
|
1817
|
+
(await tryResolve(storageKey));
|
|
1577
1818
|
if (value) {
|
|
1578
1819
|
resolvedHeaders[headerName] = value;
|
|
1579
1820
|
}
|
|
@@ -1593,22 +1834,28 @@ function createAdk(fs, options = {}) {
|
|
|
1593
1834
|
const encKey = options.encryptionKey;
|
|
1594
1835
|
const headersToStore = {};
|
|
1595
1836
|
for (const [h, v] of Object.entries(resolvedHeaders)) {
|
|
1596
|
-
headersToStore[h] = encKey
|
|
1837
|
+
headersToStore[h] = encKey
|
|
1838
|
+
? `${SECRET_PREFIX}${await (0, crypto_js_1.encryptSecret)(v, encKey)}`
|
|
1839
|
+
: v;
|
|
1597
1840
|
}
|
|
1598
1841
|
await ref.update(name, { config: { headers: headersToStore } });
|
|
1599
1842
|
return { type: "apiKey", complete: true };
|
|
1600
1843
|
}
|
|
1601
1844
|
// Fallback: no headers declared → generic api_key
|
|
1602
|
-
const key = opts?.credentials?.["api_key"] ??
|
|
1845
|
+
const key = opts?.credentials?.["api_key"] ??
|
|
1846
|
+
opts?.apiKey ??
|
|
1847
|
+
(await tryResolve("api_key"));
|
|
1603
1848
|
if (!key) {
|
|
1604
1849
|
return {
|
|
1605
1850
|
type: "apiKey",
|
|
1606
1851
|
complete: false,
|
|
1607
|
-
fields: [
|
|
1852
|
+
fields: [
|
|
1853
|
+
{
|
|
1608
1854
|
name: "api_key",
|
|
1609
1855
|
label: "API Key",
|
|
1610
1856
|
secret: true,
|
|
1611
|
-
}
|
|
1857
|
+
},
|
|
1858
|
+
],
|
|
1612
1859
|
};
|
|
1613
1860
|
}
|
|
1614
1861
|
await storeRefSecret(name, "api_key", key);
|
|
@@ -1618,14 +1865,22 @@ function createAdk(fs, options = {}) {
|
|
|
1618
1865
|
const httpSec = security;
|
|
1619
1866
|
const isBasic = httpSec.scheme === "basic";
|
|
1620
1867
|
if (isBasic) {
|
|
1621
|
-
const username = opts?.credentials?.["username"] ?? await tryResolve("username");
|
|
1622
|
-
const password = opts?.credentials?.["password"] ?? await tryResolve("password");
|
|
1868
|
+
const username = opts?.credentials?.["username"] ?? (await tryResolve("username"));
|
|
1869
|
+
const password = opts?.credentials?.["password"] ?? (await tryResolve("password"));
|
|
1623
1870
|
if (!username || !password) {
|
|
1624
1871
|
const missingFields = [];
|
|
1625
1872
|
if (!username)
|
|
1626
|
-
missingFields.push({
|
|
1873
|
+
missingFields.push({
|
|
1874
|
+
name: "username",
|
|
1875
|
+
label: "Username",
|
|
1876
|
+
secret: false,
|
|
1877
|
+
});
|
|
1627
1878
|
if (!password)
|
|
1628
|
-
missingFields.push({
|
|
1879
|
+
missingFields.push({
|
|
1880
|
+
name: "password",
|
|
1881
|
+
label: "Password",
|
|
1882
|
+
secret: true,
|
|
1883
|
+
});
|
|
1629
1884
|
return { type: "http", complete: false, fields: missingFields };
|
|
1630
1885
|
}
|
|
1631
1886
|
// Store as base64 encoded basic auth token
|
|
@@ -1634,7 +1889,9 @@ function createAdk(fs, options = {}) {
|
|
|
1634
1889
|
return { type: "http", complete: true };
|
|
1635
1890
|
}
|
|
1636
1891
|
// Bearer token
|
|
1637
|
-
const token = opts?.credentials?.["token"] ??
|
|
1892
|
+
const token = opts?.credentials?.["token"] ??
|
|
1893
|
+
opts?.apiKey ??
|
|
1894
|
+
(await tryResolve("token"));
|
|
1638
1895
|
if (!token) {
|
|
1639
1896
|
return {
|
|
1640
1897
|
type: "http",
|
|
@@ -1691,8 +1948,9 @@ function createAdk(fs, options = {}) {
|
|
|
1691
1948
|
const supportedAuthMethods = metadata.token_endpoint_auth_methods_supported ?? ["none"];
|
|
1692
1949
|
const preferredMethod = supportedAuthMethods.includes("none")
|
|
1693
1950
|
? "none"
|
|
1694
|
-
: supportedAuthMethods[0] ?? "client_secret_post";
|
|
1695
|
-
const securityClientName = security
|
|
1951
|
+
: (supportedAuthMethods[0] ?? "client_secret_post");
|
|
1952
|
+
const securityClientName = security
|
|
1953
|
+
.clientName;
|
|
1696
1954
|
const reg = await (0, mcp_client_js_1.dynamicClientRegistration)(metadata.registration_endpoint, {
|
|
1697
1955
|
clientName: securityClientName ?? options.oauthClientName ?? "adk",
|
|
1698
1956
|
redirectUris: [redirectUri],
|
|
@@ -1710,10 +1968,18 @@ function createAdk(fs, options = {}) {
|
|
|
1710
1968
|
// Return fields telling the caller what OAuth credentials to provide
|
|
1711
1969
|
const missingFields = [];
|
|
1712
1970
|
if (!clientId) {
|
|
1713
|
-
missingFields.push({
|
|
1971
|
+
missingFields.push({
|
|
1972
|
+
name: "client_id",
|
|
1973
|
+
label: "Client ID",
|
|
1974
|
+
secret: false,
|
|
1975
|
+
});
|
|
1714
1976
|
}
|
|
1715
1977
|
// Always ask for client_secret alongside client_id — most providers need it
|
|
1716
|
-
missingFields.push({
|
|
1978
|
+
missingFields.push({
|
|
1979
|
+
name: "client_secret",
|
|
1980
|
+
label: "Client Secret",
|
|
1981
|
+
secret: true,
|
|
1982
|
+
});
|
|
1717
1983
|
return { type: "oauth2", complete: false, fields: missingFields };
|
|
1718
1984
|
}
|
|
1719
1985
|
// State ties the callback back to this ref. Encode as base64 JSON
|
|
@@ -1735,7 +2001,9 @@ function createAdk(fs, options = {}) {
|
|
|
1735
2001
|
const scopes = agentScopes.length > 0
|
|
1736
2002
|
? [
|
|
1737
2003
|
...agentScopes,
|
|
1738
|
-
...(metadata.scopes_supported?.includes(
|
|
2004
|
+
...(metadata.scopes_supported?.includes("openid")
|
|
2005
|
+
? ["openid"]
|
|
2006
|
+
: []),
|
|
1739
2007
|
]
|
|
1740
2008
|
: metadata.scopes_supported;
|
|
1741
2009
|
// Read provider-specific authorization params from the agent's security section
|
|
@@ -1784,7 +2052,9 @@ function createAdk(fs, options = {}) {
|
|
|
1784
2052
|
// owns the credential store, so the user needs to submit via
|
|
1785
2053
|
// whatever UI the registry exposes. Supporting this through the
|
|
1786
2054
|
// proxy would need a remote form endpoint — out of scope here.
|
|
1787
|
-
if (result.fields &&
|
|
2055
|
+
if (result.fields &&
|
|
2056
|
+
result.fields.length > 0 &&
|
|
2057
|
+
result.type !== "oauth2") {
|
|
1788
2058
|
if (proxy) {
|
|
1789
2059
|
throw new Error(`Ref "${name}" is sourced from a proxied registry; submit credentials through ${proxy.agent} instead of a local form.`);
|
|
1790
2060
|
}
|
|
@@ -1944,7 +2214,7 @@ function createAdk(fs, options = {}) {
|
|
|
1944
2214
|
});
|
|
1945
2215
|
if (!res.ok)
|
|
1946
2216
|
return null;
|
|
1947
|
-
const data = await res.json();
|
|
2217
|
+
const data = (await res.json());
|
|
1948
2218
|
const newAccessToken = data.access_token;
|
|
1949
2219
|
if (!newAccessToken)
|
|
1950
2220
|
return null;
|
|
@@ -1979,7 +2249,9 @@ function createAdk(fs, options = {}) {
|
|
|
1979
2249
|
try {
|
|
1980
2250
|
stateContext = JSON.parse(atob(params.state));
|
|
1981
2251
|
}
|
|
1982
|
-
catch {
|
|
2252
|
+
catch {
|
|
2253
|
+
/* state wasn't base64 JSON — legacy format */
|
|
2254
|
+
}
|
|
1983
2255
|
return { refName: pending.refName, complete: true, stateContext };
|
|
1984
2256
|
}
|
|
1985
2257
|
return { registry, ref, readConfig, writeConfig, handleCallback };
|