@slashfi/agents-sdk 0.76.0 → 0.77.1
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 +2 -2
- package/dist/adk-tools.d.ts.map +1 -1
- package/dist/adk-tools.js +9 -18
- package/dist/adk-tools.js.map +1 -1
- package/dist/adk.js +190 -120
- package/dist/adk.js.map +1 -1
- package/dist/agent-definitions/config.d.ts.map +1 -1
- package/dist/agent-definitions/config.js +12 -14
- package/dist/agent-definitions/config.js.map +1 -1
- package/dist/cjs/adk-tools.js +9 -18
- package/dist/cjs/adk-tools.js.map +1 -1
- package/dist/cjs/agent-definitions/config.js +12 -14
- package/dist/cjs/agent-definitions/config.js.map +1 -1
- package/dist/cjs/config-store.js +527 -30
- package/dist/cjs/config-store.js.map +1 -1
- package/dist/cjs/define-config.js +5 -7
- package/dist/cjs/define-config.js.map +1 -1
- package/dist/cjs/index.js.map +1 -1
- package/dist/cjs/materialize.js +1 -1
- package/dist/cjs/materialize.js.map +1 -1
- package/dist/cjs/mcp-client.js +98 -0
- package/dist/cjs/mcp-client.js.map +1 -1
- package/dist/cjs/registry-consumer.js +69 -11
- package/dist/cjs/registry-consumer.js.map +1 -1
- package/dist/config-store.d.ts +39 -4
- package/dist/config-store.d.ts.map +1 -1
- package/dist/config-store.js +528 -31
- package/dist/config-store.js.map +1 -1
- package/dist/define-config.d.ts +65 -18
- package/dist/define-config.d.ts.map +1 -1
- package/dist/define-config.js +5 -7
- package/dist/define-config.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/materialize.js +1 -1
- package/dist/materialize.js.map +1 -1
- package/dist/mcp-client.d.ts +44 -0
- package/dist/mcp-client.d.ts.map +1 -1
- package/dist/mcp-client.js +95 -0
- package/dist/mcp-client.js.map +1 -1
- package/dist/registry-consumer.d.ts +1 -1
- package/dist/registry-consumer.d.ts.map +1 -1
- package/dist/registry-consumer.js +69 -11
- package/dist/registry-consumer.js.map +1 -1
- package/dist/validate.d.ts +8 -8
- package/package.json +1 -1
- package/src/adk-tools.ts +11 -18
- package/src/adk.ts +78 -11
- package/src/agent-definitions/config.ts +15 -16
- package/src/config-store.test.ts +212 -0
- package/src/config-store.ts +615 -37
- package/src/consumer.test.ts +7 -7
- package/src/define-config.ts +69 -20
- package/src/index.ts +1 -0
- package/src/materialize.ts +1 -1
- package/src/mcp-client.ts +121 -0
- package/src/ref-naming.test.ts +115 -90
- package/src/registry-consumer.ts +75 -13
package/src/adk.ts
CHANGED
|
@@ -60,7 +60,8 @@ const HELP_SECTIONS: Record<string, string> = {
|
|
|
60
60
|
adk registry list
|
|
61
61
|
adk registry browse <name> [--query <q>]
|
|
62
62
|
adk registry inspect <name>
|
|
63
|
-
adk registry test [name]
|
|
63
|
+
adk registry test [name]
|
|
64
|
+
adk registry auth <name> [--token <t>] [--api-key <k>] [--header <h>]`,
|
|
64
65
|
ref: `Ref operations:
|
|
65
66
|
adk ref add <name> Install from default (public) registry
|
|
66
67
|
adk ref add <name> --registry <reg> Install from a specific registry
|
|
@@ -106,9 +107,10 @@ Registry operations:
|
|
|
106
107
|
adk registry browse <name> [--query <q>]
|
|
107
108
|
adk registry inspect <name>
|
|
108
109
|
adk registry test [name]
|
|
110
|
+
adk registry auth <name> [--token <t>] [--api-key <k>] [--header <h>]
|
|
109
111
|
|
|
110
112
|
Ref operations:
|
|
111
|
-
adk ref add <ref> [--
|
|
113
|
+
adk ref add <ref> [--name <name>] [--registry <name>] [--url <url>] [--scheme mcp|https|registry]
|
|
112
114
|
adk ref remove <name>
|
|
113
115
|
adk ref list
|
|
114
116
|
adk ref inspect <name> [--full]
|
|
@@ -156,6 +158,7 @@ async function runRegistry() {
|
|
|
156
158
|
const op = args[1];
|
|
157
159
|
const adk = getAdk();
|
|
158
160
|
|
|
161
|
+
try {
|
|
159
162
|
switch (op) {
|
|
160
163
|
case "add": {
|
|
161
164
|
const url = args[2];
|
|
@@ -187,7 +190,7 @@ async function runRegistry() {
|
|
|
187
190
|
}
|
|
188
191
|
: undefined;
|
|
189
192
|
const displayName = name ?? new URL(url).hostname;
|
|
190
|
-
await adk.registry.add({
|
|
193
|
+
const addResult = await adk.registry.add({
|
|
191
194
|
url,
|
|
192
195
|
name: displayName,
|
|
193
196
|
...(auth && { auth }),
|
|
@@ -202,6 +205,17 @@ async function runRegistry() {
|
|
|
202
205
|
console.log(
|
|
203
206
|
`Added registry: ${displayName}${effectiveProxy ? ` (proxy: ${effectiveProxy.mode} → ${effectiveProxy.agent ?? "@config"}${source})` : ""}`,
|
|
204
207
|
);
|
|
208
|
+
if (addResult.authRequirement) {
|
|
209
|
+
const req = addResult.authRequirement;
|
|
210
|
+
console.log(`\n \x1b[33m!\x1b[0m Auth required: ${req.scheme ?? "Bearer"}${req.realm ? ` (realm: ${req.realm})` : ""}`);
|
|
211
|
+
if (req.authorizationServers?.length) {
|
|
212
|
+
console.log(` authorization servers: ${req.authorizationServers.join(", ")}`);
|
|
213
|
+
}
|
|
214
|
+
if (req.scopes?.length) {
|
|
215
|
+
console.log(` scopes: ${req.scopes.join(" ")}`);
|
|
216
|
+
}
|
|
217
|
+
console.log(`\n Run: adk registry auth ${displayName} --token <token>`);
|
|
218
|
+
}
|
|
205
219
|
break;
|
|
206
220
|
}
|
|
207
221
|
case "remove": {
|
|
@@ -264,10 +278,64 @@ async function runRegistry() {
|
|
|
264
278
|
}
|
|
265
279
|
break;
|
|
266
280
|
}
|
|
281
|
+
case "auth": {
|
|
282
|
+
const name = args[2];
|
|
283
|
+
if (!name) { console.error("Usage: adk registry auth <name> [--token <t>] [--api-key <k>] [--header <h>]"); process.exit(1); }
|
|
284
|
+
const token = getArg("--token");
|
|
285
|
+
const apiKey = getArg("--api-key");
|
|
286
|
+
const header = getArg("--header");
|
|
287
|
+
|
|
288
|
+
// Explicit credential mode — user supplied a token/key directly.
|
|
289
|
+
if (token || apiKey) {
|
|
290
|
+
const credential = token
|
|
291
|
+
? { token }
|
|
292
|
+
: { apiKey: apiKey!, ...(header && { header }) };
|
|
293
|
+
const updated = await adk.registry.auth(name, credential);
|
|
294
|
+
if (!updated) {
|
|
295
|
+
console.error(`Registry not found: ${name}`);
|
|
296
|
+
process.exit(1);
|
|
297
|
+
}
|
|
298
|
+
console.log(`\x1b[32m✓\x1b[0m Auth saved for ${name}`);
|
|
299
|
+
break;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// Auto-resolve: OAuth (when registry advertised an AS) or local
|
|
303
|
+
// credential form otherwise. Mirrors `adk ref auth` UX. Always force
|
|
304
|
+
// a fresh flow — invoking the command implies the existing creds
|
|
305
|
+
// aren't trusted, so skip the "already looks valid" short-circuit.
|
|
306
|
+
try {
|
|
307
|
+
const result = await adk.registry.authLocal(name, {
|
|
308
|
+
force: true,
|
|
309
|
+
onAuthorizeUrl: (url) => {
|
|
310
|
+
console.log(`\nOpen this URL to authenticate:\n\n ${url}\n`);
|
|
311
|
+
const opener = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
|
|
312
|
+
import("node:child_process").then(({ exec }) => exec(`${opener} "${url}"`)).catch(() => {});
|
|
313
|
+
console.log("Waiting ...");
|
|
314
|
+
},
|
|
315
|
+
});
|
|
316
|
+
if (result.complete) {
|
|
317
|
+
console.log(`\x1b[32m✓\x1b[0m Auth complete for ${name}`);
|
|
318
|
+
}
|
|
319
|
+
} catch (err) {
|
|
320
|
+
if (err instanceof AdkError) throw err;
|
|
321
|
+
console.error(`Auth failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
322
|
+
process.exit(1);
|
|
323
|
+
}
|
|
324
|
+
break;
|
|
325
|
+
}
|
|
267
326
|
default:
|
|
268
327
|
console.error(`Unknown registry operation: ${op}`);
|
|
269
|
-
console.error("Operations: add, remove, list, browse, inspect, test");
|
|
328
|
+
console.error("Operations: add, remove, list, browse, inspect, test, auth");
|
|
329
|
+
process.exit(1);
|
|
330
|
+
}
|
|
331
|
+
} catch (err) {
|
|
332
|
+
if (err instanceof AdkError) {
|
|
333
|
+
console.error(`\n\x1b[31m✗\x1b[0m ${err.message}`);
|
|
334
|
+
console.error(` ${err.hint}`);
|
|
335
|
+
console.error(` Error ID: ${err.errorId}`);
|
|
270
336
|
process.exit(1);
|
|
337
|
+
}
|
|
338
|
+
throw err;
|
|
271
339
|
}
|
|
272
340
|
}
|
|
273
341
|
|
|
@@ -283,15 +351,14 @@ async function runRef() {
|
|
|
283
351
|
switch (op) {
|
|
284
352
|
case "add": {
|
|
285
353
|
const refArg = args[2];
|
|
286
|
-
if (!refArg) { console.error("Usage: adk ref add <ref> [--
|
|
287
|
-
const
|
|
288
|
-
const
|
|
354
|
+
if (!refArg) { console.error("Usage: adk ref add <ref> [--name <name>] [--registry <name>]"); process.exit(1); }
|
|
355
|
+
const name = getArg("--name") ?? refArg;
|
|
356
|
+
const entry: Record<string, unknown> = { ref: refArg, name };
|
|
289
357
|
const url = getArg("--url");
|
|
290
358
|
const registryName = getArg("--registry");
|
|
291
359
|
// Auto-detect: if no --registry and no --url, try default registry
|
|
292
360
|
const effectiveRegistry = registryName ?? (url ? undefined : "public");
|
|
293
361
|
const scheme = getArg("--scheme") ?? (effectiveRegistry ? "registry" : undefined);
|
|
294
|
-
if (alias) entry.as = alias;
|
|
295
362
|
if (url) entry.url = url;
|
|
296
363
|
if (scheme) entry.scheme = scheme;
|
|
297
364
|
if (effectiveRegistry) {
|
|
@@ -302,15 +369,15 @@ async function runRef() {
|
|
|
302
369
|
}
|
|
303
370
|
try {
|
|
304
371
|
const { security } = await adk.ref.add(entry as import("./define-config.js").RefEntry);
|
|
305
|
-
console.log(`Added ref: ${
|
|
372
|
+
console.log(`Added ref: ${name}`);
|
|
306
373
|
if (security && security.type !== "none") {
|
|
307
374
|
console.log(`\n Auth required: ${security.type}`);
|
|
308
|
-
console.log(` Run: adk ref auth ${
|
|
375
|
+
console.log(` Run: adk ref auth ${name}`);
|
|
309
376
|
}
|
|
310
377
|
|
|
311
378
|
// Materialize local docs
|
|
312
379
|
const configDir = process.env.ADK_CONFIG_DIR ?? join(homedir(), ".adk");
|
|
313
|
-
const refDisplayName =
|
|
380
|
+
const refDisplayName = name;
|
|
314
381
|
try {
|
|
315
382
|
const result = await materializeRef(adk, refDisplayName, configDir);
|
|
316
383
|
if (result.toolCount > 0) {
|
|
@@ -108,9 +108,9 @@ export function createConfigAgent(
|
|
|
108
108
|
type: "string",
|
|
109
109
|
description: 'Agent ref name (e.g. "notion", "linear")',
|
|
110
110
|
},
|
|
111
|
-
|
|
111
|
+
name: {
|
|
112
112
|
type: "string",
|
|
113
|
-
description: "Local
|
|
113
|
+
description: "Local ref name. Defaults to ref when omitted.",
|
|
114
114
|
},
|
|
115
115
|
url: {
|
|
116
116
|
type: "string",
|
|
@@ -132,7 +132,7 @@ export function createConfigAgent(
|
|
|
132
132
|
execute: async (
|
|
133
133
|
input: {
|
|
134
134
|
ref: string;
|
|
135
|
-
|
|
135
|
+
name?: string;
|
|
136
136
|
url?: string;
|
|
137
137
|
config?: Record<string, string>;
|
|
138
138
|
registry?: string;
|
|
@@ -141,28 +141,27 @@ export function createConfigAgent(
|
|
|
141
141
|
) => {
|
|
142
142
|
const fs = getStore(ctx);
|
|
143
143
|
const currentConfig = await readConfig(fs);
|
|
144
|
+
const name = input.name ?? input.ref;
|
|
144
145
|
|
|
145
|
-
const entry
|
|
146
|
+
const entry = {
|
|
146
147
|
ref: input.ref,
|
|
147
|
-
|
|
148
|
+
name,
|
|
148
149
|
...(input.url && { url: input.url }),
|
|
149
150
|
...(input.config && { config: input.config }),
|
|
150
151
|
...(input.registry && { registry: input.registry }),
|
|
151
|
-
};
|
|
152
|
+
} as RefEntry;
|
|
152
153
|
|
|
153
|
-
// Upsert: find existing ref by name/alias, replace or append
|
|
154
|
-
const name = input.as ?? input.ref;
|
|
155
154
|
const refs = currentConfig.refs ?? [];
|
|
156
|
-
const
|
|
155
|
+
const altName = name.startsWith("@") ? name.slice(1) : `@${name}`;
|
|
156
|
+
const duplicate = refs.some((r) => {
|
|
157
157
|
const normalized = normalizeRef(r);
|
|
158
|
-
return normalized.name === name;
|
|
158
|
+
return normalized.name === name || normalized.name === altName;
|
|
159
159
|
});
|
|
160
160
|
|
|
161
|
-
if (
|
|
162
|
-
|
|
163
|
-
} else {
|
|
164
|
-
refs.push(entry);
|
|
161
|
+
if (duplicate) {
|
|
162
|
+
throw new Error(`Cannot add ref "${input.ref}" as "${name}": a ref with that name already exists`);
|
|
165
163
|
}
|
|
164
|
+
refs.push(entry);
|
|
166
165
|
|
|
167
166
|
currentConfig.refs = refs;
|
|
168
167
|
await writeConfig(fs, currentConfig);
|
|
@@ -178,13 +177,13 @@ export function createConfigAgent(
|
|
|
178
177
|
// ---- remove_ref ----
|
|
179
178
|
const removeRefTool = defineTool({
|
|
180
179
|
name: "remove_ref",
|
|
181
|
-
description: "Remove an agent ref from the consumer config by name
|
|
180
|
+
description: "Remove an agent ref from the consumer config by name.",
|
|
182
181
|
inputSchema: {
|
|
183
182
|
type: "object" as const,
|
|
184
183
|
properties: {
|
|
185
184
|
name: {
|
|
186
185
|
type: "string",
|
|
187
|
-
description: "Ref name
|
|
186
|
+
description: "Ref name to remove",
|
|
188
187
|
},
|
|
189
188
|
},
|
|
190
189
|
required: ["name"],
|
package/src/config-store.test.ts
CHANGED
|
@@ -626,3 +626,215 @@ describe("ADK registry proxy routing", () => {
|
|
|
626
626
|
expect((result as { authorizeUrl?: string }).authorizeUrl).toBeUndefined();
|
|
627
627
|
});
|
|
628
628
|
});
|
|
629
|
+
|
|
630
|
+
// ─── Registry auth lifecycle ─────────────────────────────────────
|
|
631
|
+
|
|
632
|
+
describe("ADK registry auth lifecycle", () => {
|
|
633
|
+
const PORT = 19930;
|
|
634
|
+
const MCP_URL = `http://localhost:${PORT}/mcp`;
|
|
635
|
+
const AS_URL = `http://localhost:${PORT}`;
|
|
636
|
+
|
|
637
|
+
let mcpServer: ReturnType<typeof Bun.serve>;
|
|
638
|
+
let activeAccessToken = "access-token-v1";
|
|
639
|
+
let tokenExchangeCount = 0;
|
|
640
|
+
let tokenRefreshCount = 0;
|
|
641
|
+
|
|
642
|
+
beforeAll(() => {
|
|
643
|
+
// Fake registry that speaks MCP when authenticated, emits an RFC 6750
|
|
644
|
+
// challenge pointing at RFC 9728 metadata when not, and doubles as the
|
|
645
|
+
// OAuth authorization server (registration + authorize + token) so the
|
|
646
|
+
// whole adk registry.auth flow can run end-to-end in-process.
|
|
647
|
+
mcpServer = Bun.serve({
|
|
648
|
+
port: PORT,
|
|
649
|
+
async fetch(req) {
|
|
650
|
+
const url = new URL(req.url);
|
|
651
|
+
const path = url.pathname;
|
|
652
|
+
|
|
653
|
+
// RFC 9728 protected-resource metadata
|
|
654
|
+
if (path === "/.well-known/oauth-protected-resource") {
|
|
655
|
+
return Response.json({
|
|
656
|
+
resource: MCP_URL,
|
|
657
|
+
authorization_servers: [AS_URL],
|
|
658
|
+
scopes_supported: ["mcp:full"],
|
|
659
|
+
bearer_methods_supported: ["header"],
|
|
660
|
+
});
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
// RFC 8414 authorization-server metadata
|
|
664
|
+
if (path === "/.well-known/oauth-authorization-server") {
|
|
665
|
+
return Response.json({
|
|
666
|
+
issuer: AS_URL,
|
|
667
|
+
authorization_endpoint: `${AS_URL}/oauth/authorize`,
|
|
668
|
+
token_endpoint: `${AS_URL}/oauth/token`,
|
|
669
|
+
registration_endpoint: `${AS_URL}/oauth/register`,
|
|
670
|
+
});
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
// Dynamic client registration (RFC 7591)
|
|
674
|
+
if (path === "/oauth/register" && req.method === "POST") {
|
|
675
|
+
return Response.json({
|
|
676
|
+
client_id: "test-client-id",
|
|
677
|
+
client_secret: "test-client-secret",
|
|
678
|
+
});
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
// Token endpoint — supports authorization_code + refresh_token grants
|
|
682
|
+
if (path === "/oauth/token" && req.method === "POST") {
|
|
683
|
+
const body = new URLSearchParams(await req.text());
|
|
684
|
+
const grant = body.get("grant_type");
|
|
685
|
+
if (grant === "authorization_code") {
|
|
686
|
+
tokenExchangeCount++;
|
|
687
|
+
return Response.json({
|
|
688
|
+
access_token: activeAccessToken,
|
|
689
|
+
refresh_token: "refresh-token-v1",
|
|
690
|
+
token_type: "Bearer",
|
|
691
|
+
expires_in: 3600,
|
|
692
|
+
});
|
|
693
|
+
}
|
|
694
|
+
if (grant === "refresh_token") {
|
|
695
|
+
tokenRefreshCount++;
|
|
696
|
+
if (body.get("refresh_token") !== "refresh-token-v1") {
|
|
697
|
+
return new Response(
|
|
698
|
+
JSON.stringify({ error: "invalid_grant" }),
|
|
699
|
+
{ status: 400 },
|
|
700
|
+
);
|
|
701
|
+
}
|
|
702
|
+
// Rotate to a new access token so the test can tell refresh ran.
|
|
703
|
+
activeAccessToken = "access-token-v2";
|
|
704
|
+
return Response.json({
|
|
705
|
+
access_token: activeAccessToken,
|
|
706
|
+
token_type: "Bearer",
|
|
707
|
+
expires_in: 3600,
|
|
708
|
+
});
|
|
709
|
+
}
|
|
710
|
+
return new Response("unsupported_grant_type", { status: 400 });
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
// MCP endpoint
|
|
714
|
+
if (path === "/mcp" && req.method === "POST") {
|
|
715
|
+
const auth = req.headers.get("authorization") ?? "";
|
|
716
|
+
const expected = `Bearer ${activeAccessToken}`;
|
|
717
|
+
if (auth !== expected) {
|
|
718
|
+
return new Response(
|
|
719
|
+
JSON.stringify({ error: { code: "UNAUTHORIZED", message: "No token" } }),
|
|
720
|
+
{
|
|
721
|
+
status: 401,
|
|
722
|
+
headers: {
|
|
723
|
+
"Content-Type": "application/json",
|
|
724
|
+
"WWW-Authenticate": `Bearer realm="test", resource_metadata="${AS_URL}/.well-known/oauth-protected-resource"`,
|
|
725
|
+
},
|
|
726
|
+
},
|
|
727
|
+
);
|
|
728
|
+
}
|
|
729
|
+
const rpc = (await req.json()) as { id: number; method: string };
|
|
730
|
+
if (rpc.method === "initialize") {
|
|
731
|
+
return Response.json({
|
|
732
|
+
jsonrpc: "2.0",
|
|
733
|
+
id: rpc.id,
|
|
734
|
+
result: { serverInfo: { name: "test-mcp" }, capabilities: {} },
|
|
735
|
+
});
|
|
736
|
+
}
|
|
737
|
+
if (rpc.method === "tools/call") {
|
|
738
|
+
return Response.json({
|
|
739
|
+
jsonrpc: "2.0",
|
|
740
|
+
id: rpc.id,
|
|
741
|
+
result: {
|
|
742
|
+
content: [
|
|
743
|
+
{
|
|
744
|
+
type: "text",
|
|
745
|
+
text: JSON.stringify({
|
|
746
|
+
agents: [
|
|
747
|
+
{ path: "@test-agent", description: "An agent", toolCount: 1 },
|
|
748
|
+
],
|
|
749
|
+
}),
|
|
750
|
+
},
|
|
751
|
+
],
|
|
752
|
+
},
|
|
753
|
+
});
|
|
754
|
+
}
|
|
755
|
+
return new Response("method not found", { status: 404 });
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
return new Response("not found", { status: 404 });
|
|
759
|
+
},
|
|
760
|
+
});
|
|
761
|
+
});
|
|
762
|
+
|
|
763
|
+
afterAll(() => {
|
|
764
|
+
mcpServer.stop();
|
|
765
|
+
});
|
|
766
|
+
|
|
767
|
+
test("registry.add records auth challenge; browse refuses; auth() unlocks", async () => {
|
|
768
|
+
const fs = createMemoryFs();
|
|
769
|
+
const adk = createAdk(fs, { encryptionKey: "test-key-32-chars-long-enough!!" });
|
|
770
|
+
|
|
771
|
+
const addResult = await adk.registry.add({ name: "test", url: MCP_URL });
|
|
772
|
+
|
|
773
|
+
expect(addResult.authRequirement).toBeDefined();
|
|
774
|
+
expect(addResult.authRequirement?.scheme).toBe("Bearer");
|
|
775
|
+
expect(addResult.authRequirement?.authorizationServers).toEqual([AS_URL]);
|
|
776
|
+
expect(addResult.authRequirement?.scopes).toEqual(["mcp:full"]);
|
|
777
|
+
|
|
778
|
+
await expect(adk.registry.browse("test")).rejects.toMatchObject({
|
|
779
|
+
code: "registry_auth_required",
|
|
780
|
+
});
|
|
781
|
+
|
|
782
|
+
await adk.registry.auth("test", { token: activeAccessToken });
|
|
783
|
+
|
|
784
|
+
// Stored token is encrypted (secret: prefix) — buildConsumer decrypts
|
|
785
|
+
// it transparently so browse should now land the MCP call.
|
|
786
|
+
const stored = await adk.registry.get("test");
|
|
787
|
+
expect(stored?.auth?.type).toBe("bearer");
|
|
788
|
+
expect((stored?.auth as { token: string }).token).toMatch(/^secret:/);
|
|
789
|
+
expect(stored?.authRequirement).toBeUndefined();
|
|
790
|
+
|
|
791
|
+
const agents = await adk.registry.browse("test");
|
|
792
|
+
expect(agents).toHaveLength(1);
|
|
793
|
+
expect(agents[0]?.path).toBe("@test-agent");
|
|
794
|
+
});
|
|
795
|
+
|
|
796
|
+
test("browse 401 triggers refresh via stored refresh_token and retries", async () => {
|
|
797
|
+
const fs = createMemoryFs();
|
|
798
|
+
const adk = createAdk(fs, { encryptionKey: "test-key-32-chars-long-enough!!" });
|
|
799
|
+
|
|
800
|
+
// Reset server-side token so the next refresh rotates predictably.
|
|
801
|
+
activeAccessToken = "access-token-v1";
|
|
802
|
+
tokenRefreshCount = 0;
|
|
803
|
+
|
|
804
|
+
await adk.registry.add({ name: "test", url: MCP_URL });
|
|
805
|
+
await adk.registry.auth("test", { token: activeAccessToken });
|
|
806
|
+
|
|
807
|
+
// Seed the entry with OAuth state as if `authLocal` had completed.
|
|
808
|
+
// Refresh token / endpoint / clientId are written directly so the
|
|
809
|
+
// test isn't dependent on the full browser-redirect flow.
|
|
810
|
+
const config = await adk.readConfig();
|
|
811
|
+
await adk.writeConfig({
|
|
812
|
+
...config,
|
|
813
|
+
registries: config.registries?.map((r: any) => {
|
|
814
|
+
if (typeof r !== "string" && r.name === "test") {
|
|
815
|
+
return {
|
|
816
|
+
...r,
|
|
817
|
+
oauth: {
|
|
818
|
+
tokenEndpoint: `${AS_URL}/oauth/token`,
|
|
819
|
+
clientId: "test-client-id",
|
|
820
|
+
refreshToken: "refresh-token-v1",
|
|
821
|
+
},
|
|
822
|
+
};
|
|
823
|
+
}
|
|
824
|
+
return r;
|
|
825
|
+
}),
|
|
826
|
+
});
|
|
827
|
+
|
|
828
|
+
// Rotate the server token — the client's stored token is now stale.
|
|
829
|
+
activeAccessToken = "access-token-v2";
|
|
830
|
+
|
|
831
|
+
const agents = await adk.registry.browse("test");
|
|
832
|
+
|
|
833
|
+
// Refresh was called exactly once; the browse call succeeded on retry.
|
|
834
|
+
expect(tokenRefreshCount).toBe(1);
|
|
835
|
+
expect(agents).toHaveLength(1);
|
|
836
|
+
|
|
837
|
+
const stored = await adk.registry.get("test");
|
|
838
|
+
expect((stored?.auth as { token: string }).token).toMatch(/^secret:/);
|
|
839
|
+
});
|
|
840
|
+
});
|