@slashfi/agents-sdk 0.31.0 → 0.32.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.
Files changed (52) hide show
  1. package/dist/auth-governance.d.ts +37 -0
  2. package/dist/auth-governance.d.ts.map +1 -0
  3. package/dist/auth-governance.js +73 -0
  4. package/dist/auth-governance.js.map +1 -0
  5. package/dist/call-agent-schema.d.ts +20 -0
  6. package/dist/call-agent-schema.d.ts.map +1 -1
  7. package/dist/call-agent-schema.js +19 -0
  8. package/dist/call-agent-schema.js.map +1 -1
  9. package/dist/cjs/auth-governance.js +79 -0
  10. package/dist/cjs/auth-governance.js.map +1 -0
  11. package/dist/cjs/call-agent-schema.js +20 -1
  12. package/dist/cjs/call-agent-schema.js.map +1 -1
  13. package/dist/cjs/define-config.js +1 -0
  14. package/dist/cjs/define-config.js.map +1 -1
  15. package/dist/cjs/index.js +4 -2
  16. package/dist/cjs/index.js.map +1 -1
  17. package/dist/cjs/key-manager.js +9 -4
  18. package/dist/cjs/key-manager.js.map +1 -1
  19. package/dist/cjs/registry-consumer.js +122 -37
  20. package/dist/cjs/registry-consumer.js.map +1 -1
  21. package/dist/cjs/server.js +149 -209
  22. package/dist/cjs/server.js.map +1 -1
  23. package/dist/define-config.d.ts +8 -0
  24. package/dist/define-config.d.ts.map +1 -1
  25. package/dist/define-config.js +1 -0
  26. package/dist/define-config.js.map +1 -1
  27. package/dist/index.d.ts +1 -1
  28. package/dist/index.d.ts.map +1 -1
  29. package/dist/index.js +1 -1
  30. package/dist/index.js.map +1 -1
  31. package/dist/key-manager.d.ts.map +1 -1
  32. package/dist/key-manager.js +9 -4
  33. package/dist/key-manager.js.map +1 -1
  34. package/dist/registry-consumer.d.ts +4 -0
  35. package/dist/registry-consumer.d.ts.map +1 -1
  36. package/dist/registry-consumer.js +122 -37
  37. package/dist/registry-consumer.js.map +1 -1
  38. package/dist/server.d.ts +3 -13
  39. package/dist/server.d.ts.map +1 -1
  40. package/dist/server.js +136 -199
  41. package/dist/server.js.map +1 -1
  42. package/package.json +1 -1
  43. package/src/auth-governance.ts +94 -0
  44. package/src/call-agent-schema.ts +33 -0
  45. package/src/codegen.test.ts +10 -0
  46. package/src/consumer.test.ts +132 -0
  47. package/src/define-config.ts +12 -0
  48. package/src/index.ts +2 -0
  49. package/src/key-manager.test.ts +17 -0
  50. package/src/key-manager.ts +10 -4
  51. package/src/registry-consumer.ts +161 -37
  52. package/src/server.ts +180 -215
package/src/server.ts CHANGED
@@ -4,7 +4,7 @@
4
4
  * Minimal JSON-RPC server implementing the MCP protocol for agent interaction.
5
5
  * Handles only core SDK concerns:
6
6
  * - MCP protocol (initialize, tools/list, tools/call)
7
- * - Agent registry routing (call_agent, list_agents, search_agent_tools)
7
+ * - Agent registry routing (call_agent, list_agents)
8
8
  * - Auth resolution (Bearer tokens, root key, JWT)
9
9
  * - OAuth2 token exchange (client_credentials)
10
10
  * - Health check
@@ -30,7 +30,7 @@ import {
30
30
  type SecretStore,
31
31
  processSecretParams,
32
32
  } from "./agent-definitions/secrets.js";
33
- import { type BM25Document, createBM25Index } from "./bm25.js";
33
+ import { createBM25Index } from "./bm25.js";
34
34
  import { verifyJwt } from "./jwt.js";
35
35
  import type { SigningKey } from "./jwt.js";
36
36
  import {
@@ -44,9 +44,13 @@ import {
44
44
  } from "./jwt.js";
45
45
  import { type OIDCProviderConfig, createOIDCSignIn } from "./oidc-signin.js";
46
46
  import type { AgentRegistry } from "./registry.js";
47
- import type { AgentDefinition, CallAgentRequest, Visibility } from "./types.js";
47
+ import type { AgentDefinition, CallAgentRequest } from "./types.js";
48
48
 
49
- import { callAgentInputSchema } from "./call-agent-schema.js";
49
+ import {
50
+ callAgentInputSchema,
51
+ listAgentsInputSchema,
52
+ listAgentsToolInputSchema,
53
+ } from "./call-agent-schema.js";
50
54
 
51
55
  // ============================================
52
56
  // Server Types
@@ -206,20 +210,22 @@ export interface AuthConfig {
206
210
  tokenTtl?: number;
207
211
  }
208
212
 
209
- export interface ResolvedAuth {
210
- issuer?: string;
211
- callerId: string;
212
- callerType: "agent" | "user" | "system";
213
- scopes: string[];
214
- /** All JWT claims from the verified token (passthrough) */
215
- claims: Record<string, unknown>;
216
- }
217
213
 
218
- /** Check if auth has admin-level access (wildcard or admin scope) */
219
- export function hasAdminScope(auth: ResolvedAuth | null): boolean {
220
- if (!auth) return false;
221
- return auth.scopes.includes("*") || auth.scopes.includes("admin");
222
- }
214
+ // Auth governance single source of truth for visibility/access control
215
+ export {
216
+ type ResolvedAuth,
217
+ hasAdminScope,
218
+ canSeeAgent,
219
+ canSeeTool,
220
+ getVisibleTools,
221
+ } from "./auth-governance.js";
222
+ import {
223
+ type ResolvedAuth,
224
+ hasAdminScope,
225
+ canSeeAgent,
226
+ canSeeTool,
227
+ getVisibleTools,
228
+ } from "./auth-governance.js";
223
229
 
224
230
  // ============================================
225
231
  // HTTP Helpers
@@ -415,19 +421,6 @@ export async function resolveAuth(
415
421
  };
416
422
  }
417
423
 
418
- export function canSeeAgent(
419
- agent: AgentDefinition,
420
- auth: ResolvedAuth | null,
421
- ): boolean {
422
- const visibility = ((agent as any).visibility ??
423
- agent.config?.visibility ??
424
- "internal") as Visibility;
425
- if (hasAdminScope(auth)) return true;
426
- if (visibility === "public") return true;
427
- if (visibility === "internal" && auth) return true;
428
- return false;
429
- }
430
-
431
424
  /**
432
425
  * Resolve an agent by path, handling @ prefix normalization.
433
426
  * Tries the path as-is first, then with @ prefix.
@@ -440,33 +433,6 @@ function resolveAgent(
440
433
  return registry.get(normalized) ?? registry.get(`@${normalized}`);
441
434
  }
442
435
 
443
- /**
444
- * Filter tools visible on a public agent endpoint.
445
- * For /agents/ routes, tools inherit the agent's visibility:
446
- * - If agent is public, tools without explicit visibility are shown
447
- * - Tool-level visibility still overrides (e.g. visibility: "private" hides it)
448
- */
449
- function getVisibleTools(
450
- agent: AgentDefinition,
451
- auth: ResolvedAuth | null,
452
- ): typeof agent.tools {
453
- const agentVisibility = ((agent as any).visibility ??
454
- agent.config?.visibility ??
455
- "internal") as Visibility;
456
- return agent.tools.filter((t) => {
457
- const tv = t.visibility;
458
- if (hasAdminScope(auth)) return true;
459
- // Tool has explicit visibility — respect it
460
- if (tv === "public") return true;
461
- if (tv === "private") return hasAdminScope(auth) ?? false;
462
- if (tv === "internal" && auth) return true;
463
- // No explicit tool visibility — inherit from agent
464
- if (!tv && agentVisibility === "public") return true;
465
- if (!tv && agentVisibility === "internal" && auth) return true;
466
- return false;
467
- });
468
- }
469
-
470
436
  // ============================================
471
437
  // MCP Tool Definitions
472
438
  // ============================================
@@ -481,37 +447,9 @@ function getToolDefinitions() {
481
447
  },
482
448
  {
483
449
  name: "list_agents",
484
- description: "List all registered agents and their available tools.",
485
- inputSchema: {
486
- type: "object",
487
- properties: {},
488
- },
489
- },
490
- {
491
- name: "search_agent_tools",
492
450
  description:
493
- "Search across all registered agent tools using natural language. Returns tools ranked by relevance using BM25 scoring.",
494
- inputSchema: {
495
- type: "object",
496
- properties: {
497
- query: {
498
- type: "string",
499
- description:
500
- "Natural language search query (e.g. 'send a message', 'database query')",
501
- },
502
- agents: {
503
- type: "array",
504
- items: { type: "string" },
505
- description:
506
- "Optional list of agent paths to search within (e.g. ['@notifications', '@db']). Searches all agents if omitted.",
507
- },
508
- limit: {
509
- type: "number",
510
- description: "Maximum number of results to return (default: 10)",
511
- },
512
- },
513
- required: ["query"],
514
- },
451
+ "List all registered agents and their available tools. Optionally search/filter by query using BM25 ranking.",
452
+ inputSchema: listAgentsInputSchema,
515
453
  },
516
454
  ];
517
455
  }
@@ -658,12 +596,84 @@ export function createAgentServer(
658
596
  }
659
597
 
660
598
  case "list_agents": {
599
+ const { query: listQuery, limit: listLimit, cursor: listCursor } =
600
+ listAgentsToolInputSchema.parse(args);
661
601
  const agents = registry.list();
662
- const visible = agents.filter((agent) => canSeeAgent(agent, auth));
602
+ let visible = agents.filter((agent) => canSeeAgent(agent, auth));
603
+
604
+ // Decode cursor if provided
605
+ const after = listCursor
606
+ ? (JSON.parse(
607
+ Buffer.from(listCursor, "base64url").toString(),
608
+ ) as { path: string; score?: number })
609
+ : undefined;
610
+
611
+ const pageSize = listLimit ?? 20;
612
+ let page: typeof visible;
613
+ let nextCursor: string | undefined;
614
+
615
+ if (listQuery) {
616
+ // BM25 search — ranked by score desc, path asc for tie-breaking
617
+ const docs = visible.map((agent, i) => ({
618
+ id: String(i),
619
+ text: [
620
+ agent.path,
621
+ agent.config?.name ?? "",
622
+ agent.config?.description ?? "",
623
+ ...agent.tools
624
+ .filter((t) => canSeeTool(t, auth))
625
+ .map((t) => `${t.name} ${t.description}`),
626
+ ].join(" "),
627
+ }));
628
+ const index = createBM25Index(docs);
629
+ const ranked = index.search(listQuery);
630
+
631
+ // Build scored list
632
+ type ScoredAgent = (typeof visible)[number] & { _score: number };
633
+ let scored: ScoredAgent[] = ranked.map((r) => ({
634
+ ...visible[Number(r.id)],
635
+ _score: r.score,
636
+ }));
637
+
638
+ // Apply cursor: skip past the after position
639
+ if (after?.score !== undefined) {
640
+ scored = scored.filter(
641
+ (a) =>
642
+ a._score < after.score! ||
643
+ (a._score === after.score! && a.path > after.path),
644
+ );
645
+ }
646
+
647
+ page = scored.slice(0, pageSize);
648
+ if (scored.length > pageSize) {
649
+ const last = scored[pageSize - 1] as ScoredAgent;
650
+ nextCursor = Buffer.from(
651
+ JSON.stringify({ path: last.path, score: last._score }),
652
+ ).toString("base64url");
653
+ }
654
+ } else {
655
+ // Alphabetical listing — sorted by path
656
+ visible.sort((a, b) => a.path.localeCompare(b.path));
657
+
658
+ // Apply cursor: skip past afterPath
659
+ if (after) {
660
+ visible = visible.filter((a) => a.path > after.path);
661
+ }
662
+
663
+ page = visible.slice(0, pageSize);
664
+ if (visible.length > pageSize) {
665
+ const last = page[page.length - 1];
666
+ nextCursor = Buffer.from(
667
+ JSON.stringify({ path: last.path }),
668
+ ).toString("base64url");
669
+ }
670
+ }
663
671
 
664
672
  return mcpResult({
665
673
  success: true,
666
- agents: visible.map((agent) => ({
674
+ total: agents.filter((a) => canSeeAgent(a, auth)).length,
675
+ nextCursor,
676
+ agents: page.map((agent) => ({
667
677
  path: agent.path,
668
678
  name: agent.config?.name,
669
679
  description: agent.config?.description,
@@ -678,122 +688,12 @@ export function createAgentServer(
678
688
  mimeType: r.mimeType,
679
689
  })),
680
690
  tools: agent.tools
681
- .filter((t) => {
682
- const tv = t.visibility ?? "internal";
683
- if (hasAdminScope(auth)) return true;
684
- if (tv === "public") return true;
685
- if (
686
- tv === "authenticated" &&
687
- auth?.callerId &&
688
- auth.callerId !== "anonymous"
689
- )
690
- return true;
691
- if (tv === "internal" && auth) return true;
692
- return false;
693
- })
691
+ .filter((t) => canSeeTool(t, auth))
694
692
  .map((t) => t.name),
695
693
  })),
696
694
  });
697
695
  }
698
696
 
699
- case "search_agent_tools": {
700
- const { query, agents: agentFilter, limit: resultLimit } = args as {
701
- query: string;
702
- agents?: string[];
703
- limit?: number;
704
- };
705
-
706
- const agents = registry.list();
707
- const visible = agents.filter((agent) => {
708
- if (!canSeeAgent(agent, auth)) return false;
709
- if (agentFilter && agentFilter.length > 0) {
710
- return agentFilter.includes(agent.path);
711
- }
712
- return true;
713
- });
714
-
715
- // Build search documents from all visible tools
716
- const documents: (BM25Document & {
717
- agentPath: string;
718
- toolName: string;
719
- description: string;
720
- agentName?: string;
721
- agentDescription?: string;
722
- })[] = [];
723
-
724
- for (const agent of visible) {
725
- const visibleTools = agent.tools.filter((t) => {
726
- const tv = t.visibility ?? "internal";
727
- if (hasAdminScope(auth)) return true;
728
- if (tv === "public") return true;
729
- if (
730
- tv === "authenticated" &&
731
- auth?.callerId &&
732
- auth.callerId !== "anonymous"
733
- )
734
- return true;
735
- if (tv === "internal" && auth) return true;
736
- return false;
737
- });
738
-
739
- for (const tool of visibleTools) {
740
- // Build searchable text from tool name, description, agent context, and schema
741
- const parts = [
742
- tool.name,
743
- tool.description,
744
- agent.config?.name ?? "",
745
- agent.config?.description ?? "",
746
- agent.path,
747
- ];
748
-
749
- // Include property names and descriptions from input schema
750
- const schema = tool.inputSchema as any;
751
- if (schema?.properties) {
752
- for (const [key, prop] of Object.entries(schema.properties)) {
753
- parts.push(key);
754
- if ((prop as any)?.description) {
755
- parts.push((prop as any).description);
756
- }
757
- }
758
- }
759
-
760
- documents.push({
761
- id: `${agent.path}/${tool.name}`,
762
- text: parts.join(" "),
763
- agentPath: agent.path,
764
- toolName: tool.name,
765
- description: tool.description,
766
- agentName: agent.config?.name,
767
- agentDescription: agent.config?.description,
768
- });
769
- }
770
- }
771
-
772
- const index = createBM25Index(documents);
773
- const results = index.search(query, resultLimit ?? 10);
774
-
775
- // Map results back to tool details
776
- const docMap = new Map(documents.map((d) => [d.id, d]));
777
- const matches = results.map((r) => {
778
- const doc = docMap.get(r.id)!;
779
- return {
780
- agentPath: doc.agentPath,
781
- tool: doc.toolName,
782
- description: doc.description,
783
- agentName: doc.agentName,
784
- agentDescription: doc.agentDescription,
785
- score: r.score,
786
- };
787
- });
788
-
789
- return mcpResult({
790
- success: true,
791
- query,
792
- results: matches,
793
- total: matches.length,
794
- });
795
- }
796
-
797
697
  default:
798
698
  throw new Error(`Unknown tool: ${toolName}`);
799
699
  }
@@ -1266,9 +1166,12 @@ export function createAgentServer(
1266
1166
  // Public registries (e.g. registry.slash.com) skip this entirely.
1267
1167
  if (
1268
1168
  path === "/.well-known/oauth-authorization-server" &&
1269
- req.method === "GET" &&
1270
- (options.registry?.oauthCallbackUrl || serverSigningKeys.length > 0)
1169
+ req.method === "GET"
1271
1170
  ) {
1171
+ if (!(options.registry?.oauthCallbackUrl || serverSigningKeys.length > 0)) {
1172
+ const res = new Response("Not Found", { status: 404 });
1173
+ return cors ? addCors(res) : res;
1174
+ }
1272
1175
  const baseUrl = resolveBaseUrl(req);
1273
1176
  const res = jsonResponse({
1274
1177
  issuer: baseUrl,
@@ -1286,33 +1189,95 @@ export function createAgentServer(
1286
1189
  return cors ? addCors(res) : res;
1287
1190
  }
1288
1191
 
1289
- // ── GET /list → List agents (legacy endpoint) ──
1192
+ // ── GET /list → List agents (──
1290
1193
  if (path === "/list" && req.method === "GET") {
1291
1194
  const agents = registry.list();
1292
- const visible = agents.filter((agent) =>
1195
+ let visible = agents.filter((agent) =>
1293
1196
  canSeeAgent(agent, effectiveAuth),
1294
1197
  );
1295
- const res = jsonResponse(
1296
- visible.map((agent) => ({
1198
+
1199
+ const searchQuery = url.searchParams.get("q");
1200
+ const searchLimit = url.searchParams.get("limit");
1201
+ const searchCursor = url.searchParams.get("cursor");
1202
+
1203
+ // Decode cursor
1204
+ const httpAfter = searchCursor
1205
+ ? (JSON.parse(
1206
+ Buffer.from(searchCursor, "base64url").toString(),
1207
+ ) as { path: string; score?: number })
1208
+ : undefined;
1209
+
1210
+ const httpPageSize = searchLimit ? Number(searchLimit) : 20;
1211
+ let httpPage: typeof visible;
1212
+ let httpNextCursor: string | undefined;
1213
+
1214
+ if (searchQuery) {
1215
+ const docs = visible.map((agent, i) => ({
1216
+ id: String(i),
1217
+ text: [
1218
+ agent.path,
1219
+ agent.config?.name ?? "",
1220
+ agent.config?.description ?? "",
1221
+ ...agent.tools
1222
+ .filter((t) => canSeeTool(t, effectiveAuth))
1223
+ .map((t) => `${t.name} ${t.description}`),
1224
+ ].join(" "),
1225
+ }));
1226
+ const index = createBM25Index(docs);
1227
+ const ranked = index.search(searchQuery);
1228
+
1229
+ type ScoredAgent = (typeof visible)[number] & { _score: number };
1230
+ let scored: ScoredAgent[] = ranked.map((r) => ({
1231
+ ...visible[Number(r.id)],
1232
+ _score: r.score,
1233
+ }));
1234
+
1235
+ if (httpAfter?.score !== undefined) {
1236
+ scored = scored.filter(
1237
+ (a) =>
1238
+ a._score < httpAfter.score! ||
1239
+ (a._score === httpAfter.score! && a.path > httpAfter.path),
1240
+ );
1241
+ }
1242
+
1243
+ httpPage = scored.slice(0, httpPageSize);
1244
+ if (scored.length > httpPageSize) {
1245
+ const last = scored[httpPageSize - 1] as ScoredAgent;
1246
+ httpNextCursor = Buffer.from(
1247
+ JSON.stringify({ path: last.path, score: last._score }),
1248
+ ).toString("base64url");
1249
+ }
1250
+ } else {
1251
+ visible.sort((a, b) => a.path.localeCompare(b.path));
1252
+ if (httpAfter) {
1253
+ visible = visible.filter((a) => a.path > httpAfter.path);
1254
+ }
1255
+ httpPage = visible.slice(0, httpPageSize);
1256
+ if (visible.length > httpPageSize) {
1257
+ const last = httpPage[httpPage.length - 1];
1258
+ httpNextCursor = Buffer.from(
1259
+ JSON.stringify({ path: last.path }),
1260
+ ).toString("base64url");
1261
+ }
1262
+ }
1263
+
1264
+ const res = jsonResponse({
1265
+ total: agents.filter((a) => canSeeAgent(a, effectiveAuth)).length,
1266
+ nextCursor: httpNextCursor,
1267
+ agents: httpPage.map((agent) => ({
1297
1268
  path: agent.path,
1298
1269
  name: agent.config?.name,
1299
1270
  description: agent.config?.description,
1300
1271
  supportedActions: agent.config?.supportedActions,
1301
1272
  integration: agent.config?.integration || null,
1302
1273
  tools: agent.tools
1303
- .filter((t) => {
1304
- const tv = t.visibility ?? "internal";
1305
- if (hasAdminScope(effectiveAuth)) return true;
1306
- if (tv === "public") return true;
1307
- if (tv === "internal" && effectiveAuth) return true;
1308
- return false;
1309
- })
1274
+ .filter((t) => canSeeTool(t, effectiveAuth))
1310
1275
  .map((t) => ({
1311
1276
  name: t.name,
1312
1277
  description: t.description,
1313
1278
  })),
1314
1279
  })),
1315
- );
1280
+ });
1316
1281
  return cors ? addCors(res) : res;
1317
1282
  }
1318
1283
 
@@ -1611,7 +1576,7 @@ export function createAgentServer(
1611
1576
  key.privateKey,
1612
1577
  key.kid,
1613
1578
  options.serverName ?? "agents-sdk",
1614
- "1h",
1579
+ claims.exp != null ? undefined : "1h",
1615
1580
  );
1616
1581
  },
1617
1582