@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.
Files changed (59) hide show
  1. package/dist/adk-tools.d.ts +2 -2
  2. package/dist/adk-tools.d.ts.map +1 -1
  3. package/dist/adk-tools.js +9 -18
  4. package/dist/adk-tools.js.map +1 -1
  5. package/dist/adk.js +190 -120
  6. package/dist/adk.js.map +1 -1
  7. package/dist/agent-definitions/config.d.ts.map +1 -1
  8. package/dist/agent-definitions/config.js +12 -14
  9. package/dist/agent-definitions/config.js.map +1 -1
  10. package/dist/cjs/adk-tools.js +9 -18
  11. package/dist/cjs/adk-tools.js.map +1 -1
  12. package/dist/cjs/agent-definitions/config.js +12 -14
  13. package/dist/cjs/agent-definitions/config.js.map +1 -1
  14. package/dist/cjs/config-store.js +527 -30
  15. package/dist/cjs/config-store.js.map +1 -1
  16. package/dist/cjs/define-config.js +5 -7
  17. package/dist/cjs/define-config.js.map +1 -1
  18. package/dist/cjs/index.js.map +1 -1
  19. package/dist/cjs/materialize.js +1 -1
  20. package/dist/cjs/materialize.js.map +1 -1
  21. package/dist/cjs/mcp-client.js +98 -0
  22. package/dist/cjs/mcp-client.js.map +1 -1
  23. package/dist/cjs/registry-consumer.js +69 -11
  24. package/dist/cjs/registry-consumer.js.map +1 -1
  25. package/dist/config-store.d.ts +39 -4
  26. package/dist/config-store.d.ts.map +1 -1
  27. package/dist/config-store.js +528 -31
  28. package/dist/config-store.js.map +1 -1
  29. package/dist/define-config.d.ts +65 -18
  30. package/dist/define-config.d.ts.map +1 -1
  31. package/dist/define-config.js +5 -7
  32. package/dist/define-config.js.map +1 -1
  33. package/dist/index.d.ts +1 -1
  34. package/dist/index.d.ts.map +1 -1
  35. package/dist/index.js.map +1 -1
  36. package/dist/materialize.js +1 -1
  37. package/dist/materialize.js.map +1 -1
  38. package/dist/mcp-client.d.ts +44 -0
  39. package/dist/mcp-client.d.ts.map +1 -1
  40. package/dist/mcp-client.js +95 -0
  41. package/dist/mcp-client.js.map +1 -1
  42. package/dist/registry-consumer.d.ts +1 -1
  43. package/dist/registry-consumer.d.ts.map +1 -1
  44. package/dist/registry-consumer.js +69 -11
  45. package/dist/registry-consumer.js.map +1 -1
  46. package/dist/validate.d.ts +8 -8
  47. package/package.json +1 -1
  48. package/src/adk-tools.ts +11 -18
  49. package/src/adk.ts +78 -11
  50. package/src/agent-definitions/config.ts +15 -16
  51. package/src/config-store.test.ts +212 -0
  52. package/src/config-store.ts +615 -37
  53. package/src/consumer.test.ts +7 -7
  54. package/src/define-config.ts +69 -20
  55. package/src/index.ts +1 -0
  56. package/src/materialize.ts +1 -1
  57. package/src/mcp-client.ts +121 -0
  58. package/src/ref-naming.test.ts +115 -90
  59. 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> [--registry <name>] [--as <alias>] [--url <url>] [--scheme mcp|https|registry]
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> [--registry <name>] [--as <alias>]"); process.exit(1); }
287
- const entry: Record<string, unknown> = { ref: refArg };
288
- const alias = getArg("--as");
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: ${alias ?? refArg}`);
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 ${alias ?? refArg}`);
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 = alias ?? refArg;
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
- as: {
111
+ name: {
112
112
  type: "string",
113
- description: "Local alias (for multi-instance refs)",
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
- as?: string;
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: RefEntry = {
146
+ const entry = {
146
147
  ref: input.ref,
147
- ...(input.as && { as: input.as }),
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 existingIdx = refs.findIndex((r) => {
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 (existingIdx >= 0) {
162
- refs[existingIdx] = entry;
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 or alias.",
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 or alias to remove",
186
+ description: "Ref name to remove",
188
187
  },
189
188
  },
190
189
  required: ["name"],
@@ -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
+ });