@slashfi/agents-sdk 0.31.0 → 0.33.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 +121 -43
  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 +121 -43
  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 +176 -57
  52. package/src/server.ts +180 -215
@@ -0,0 +1,94 @@
1
+ /**
2
+ * Auth Governance
3
+ *
4
+ * Single source of truth for visibility and access control decisions.
5
+ * Used by the server, middleware, and any custom implementations.
6
+ */
7
+
8
+ import type { Visibility, AgentDefinition } from "./types.js";
9
+
10
+ // ─────────────────────────────────────────────────────────────────────────────
11
+ // Types
12
+ // ─────────────────────────────────────────────────────────────────────────────
13
+
14
+ export interface ResolvedAuth {
15
+ issuer?: string;
16
+ callerId: string;
17
+ callerType: "agent" | "user" | "system";
18
+ scopes: string[];
19
+ /** All JWT claims from the verified token (passthrough) */
20
+ claims: Record<string, unknown>;
21
+ }
22
+
23
+ // ─────────────────────────────────────────────────────────────────────────────
24
+ // Governance Functions
25
+ // ─────────────────────────────────────────────────────────────────────────────
26
+
27
+ /**
28
+ * Check if the auth context has admin scope.
29
+ */
30
+ export function hasAdminScope(auth: ResolvedAuth | null): boolean {
31
+ if (!auth) return false;
32
+ return auth.scopes.includes("*") || auth.scopes.includes("admin");
33
+ }
34
+
35
+ /**
36
+ * Check if an agent is visible to the given auth context.
37
+ */
38
+ export function canSeeAgent(
39
+ agent: AgentDefinition,
40
+ auth: ResolvedAuth | null,
41
+ ): boolean {
42
+ const visibility = ((agent as any).visibility ??
43
+ agent.config?.visibility ??
44
+ "internal") as Visibility;
45
+ if (hasAdminScope(auth)) return true;
46
+ if (visibility === "public") return true;
47
+ if (visibility === "internal" && auth) return true;
48
+ return false;
49
+ }
50
+
51
+ /**
52
+ * Check if a tool is visible to the given auth context.
53
+ *
54
+ * When agentVisibility is provided, tools without explicit visibility
55
+ * inherit from their parent agent.
56
+ */
57
+ export function canSeeTool(
58
+ tool: { visibility?: Visibility },
59
+ auth: ResolvedAuth | null,
60
+ agentVisibility?: Visibility,
61
+ ): boolean {
62
+ const tv = tool.visibility;
63
+ if (hasAdminScope(auth)) return true;
64
+ // Tool has explicit visibility — respect it
65
+ if (tv === "public") return true;
66
+ if (tv === "private") return false;
67
+ if (
68
+ tv === "authenticated" &&
69
+ auth?.callerId &&
70
+ auth.callerId !== "anonymous"
71
+ )
72
+ return true;
73
+ if (tv === "internal" && auth) return true;
74
+ // No explicit tool visibility — inherit from agent or default to internal
75
+ if (!tv) {
76
+ const inherited = agentVisibility ?? "internal";
77
+ if (inherited === "public") return true;
78
+ if (inherited === "internal" && auth) return true;
79
+ }
80
+ return false;
81
+ }
82
+
83
+ /**
84
+ * Get visible tools for an agent, respecting visibility inheritance.
85
+ */
86
+ export function getVisibleTools(
87
+ agent: AgentDefinition,
88
+ auth: ResolvedAuth | null,
89
+ ): typeof agent.tools {
90
+ const agentVisibility = ((agent as any).visibility ??
91
+ agent.config?.visibility ??
92
+ "internal") as Visibility;
93
+ return agent.tools.filter((t) => canSeeTool(t, auth, agentVisibility));
94
+ }
@@ -168,3 +168,36 @@ export const callAgentInputSchema = zodToJsonSchema(
168
168
  callAgentToolInputSchema as any,
169
169
  { target: "openAi" }
170
170
  );
171
+
172
+ // ─────────────────────────────────────────────────────────────────────────────
173
+ // list_agents schema
174
+ // ─────────────────────────────────────────────────────────────────────────────
175
+
176
+ export const listAgentsToolInputSchema = z.object({
177
+ query: z
178
+ .string()
179
+ .optional()
180
+ .describe(
181
+ "Search query. When provided, returns agents ranked by BM25 relevance over paths, names, descriptions, and tool names.",
182
+ ),
183
+ limit: z
184
+ .number()
185
+ .optional()
186
+ .describe(
187
+ "Maximum number of results per page (default: 20)",
188
+ ),
189
+ cursor: z
190
+ .string()
191
+ .optional()
192
+ .describe(
193
+ "Pagination cursor from a previous response's nextCursor field.",
194
+ ),
195
+ });
196
+
197
+ export type ListAgentsInput = z.infer<typeof listAgentsToolInputSchema>;
198
+
199
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
200
+ export const listAgentsInputSchema = zodToJsonSchema(
201
+ listAgentsToolInputSchema as any,
202
+ { target: "openAi" }
203
+ );
@@ -62,6 +62,10 @@ beforeAll(() => {
62
62
  port: 0,
63
63
  fetch(req) {
64
64
  return (async () => {
65
+ // Handle GET requests (well-known endpoints, etc.)
66
+ if (req.method === "GET") {
67
+ return new Response("Not Found", { status: 404 });
68
+ }
65
69
  const body = (await req.json()) as {
66
70
  id?: number;
67
71
  method: string;
@@ -305,6 +309,9 @@ describe("codegen JSON Schema support", () => {
305
309
  port: 0,
306
310
  fetch(req) {
307
311
  return (async () => {
312
+ if (req.method === "GET") {
313
+ return new Response("Not Found", { status: 404 });
314
+ }
308
315
  const body = (await req.json()) as { id?: number; method: string; params?: Record<string, unknown> };
309
316
  if (body.id === undefined) return new Response(null, { status: 202 });
310
317
 
@@ -401,6 +408,9 @@ describe("codegen pagination", () => {
401
408
  port: 0,
402
409
  fetch(req) {
403
410
  return (async () => {
411
+ if (req.method === "GET") {
412
+ return new Response("Not Found", { status: 404 });
413
+ }
404
414
  const body = (await req.json()) as {
405
415
  id?: number;
406
416
  method: string;
@@ -534,3 +534,135 @@ describe("Secret URI resolution", () => {
534
534
  );
535
535
  });
536
536
  });
537
+
538
+ // ─── API Key Auth Tests ──────────────────────────────────────────
539
+
540
+ describe("Registry Consumer — API Key Auth", () => {
541
+ let server: AgentServer;
542
+ const PORT = 19894;
543
+ const API_KEY = "test-secret-key-12345";
544
+
545
+ beforeAll(async () => {
546
+ const registry = createAgentRegistry();
547
+ registry.register(mathAgent);
548
+ registry.register(echoAgent);
549
+
550
+ server = createAgentServer(registry, {
551
+ port: PORT,
552
+ resolveAuth: async (req) => {
553
+ const apiKey = req.headers.get("x-api-key");
554
+ if (apiKey === API_KEY) {
555
+ return {
556
+ callerId: "api-key-user",
557
+ callerType: "system" as const,
558
+ scopes: ["*"],
559
+ };
560
+ }
561
+ return null;
562
+ },
563
+ });
564
+ await server.start();
565
+ });
566
+
567
+ afterAll(async () => {
568
+ await server.stop();
569
+ });
570
+
571
+ test("consumer with api-key auth type can list agents", async () => {
572
+ const consumer = await createRegistryConsumer({
573
+ registries: [
574
+ {
575
+ url: `http://localhost:${PORT}`,
576
+ auth: { type: "api-key", key: API_KEY, header: "x-api-key" },
577
+ },
578
+ ],
579
+ refs: [{ ref: "@math" }],
580
+ });
581
+
582
+ const agents = await consumer.list();
583
+ expect(agents.length).toBeGreaterThanOrEqual(2);
584
+ const paths = agents.map((a) => a.path);
585
+ expect(paths).toContain("@math");
586
+ expect(paths).toContain("@echo");
587
+ });
588
+
589
+ test("consumer with custom headers can list agents", async () => {
590
+ const consumer = await createRegistryConsumer({
591
+ registries: [
592
+ {
593
+ url: `http://localhost:${PORT}`,
594
+ headers: { "x-api-key": API_KEY },
595
+ },
596
+ ],
597
+ refs: [{ ref: "@math" }],
598
+ });
599
+
600
+ const agents = await consumer.list();
601
+ expect(agents.length).toBeGreaterThanOrEqual(2);
602
+ });
603
+
604
+ test("consumer with wrong api-key gets different auth context", async () => {
605
+ // Without the right key, resolveAuth returns null (no auth context)
606
+ // The server still processes the request but without auth identity
607
+ const consumer = await createRegistryConsumer({
608
+ registries: [
609
+ {
610
+ url: `http://localhost:${PORT}`,
611
+ auth: { type: "api-key", key: "wrong-key", header: "x-api-key" },
612
+ },
613
+ ],
614
+ refs: [{ ref: "@math" }],
615
+ });
616
+
617
+ // Can still list public agents (discovery doesn't require auth)
618
+ const agents = await consumer.list();
619
+ expect(agents.length).toBeGreaterThanOrEqual(1);
620
+ });
621
+
622
+ test("consumer with api-key auth can call tools", async () => {
623
+ const consumer = await createRegistryConsumer({
624
+ registries: [
625
+ {
626
+ url: `http://localhost:${PORT}`,
627
+ auth: { type: "api-key", key: API_KEY, header: "x-api-key" },
628
+ },
629
+ ],
630
+ refs: [{ ref: "@math" }],
631
+ });
632
+
633
+ const result = await consumer.call("@math", "add", { a: 10, b: 20 });
634
+ expect(result).toBeDefined();
635
+ });
636
+
637
+ test("inspect returns agent details via describe_tools", async () => {
638
+ const consumer = await createRegistryConsumer({
639
+ registries: [
640
+ {
641
+ url: `http://localhost:${PORT}`,
642
+ auth: { type: "api-key", key: API_KEY, header: "x-api-key" },
643
+ },
644
+ ],
645
+ refs: [{ ref: "@math" }],
646
+ });
647
+
648
+ const listing = await consumer.inspect("@math");
649
+ expect(listing).not.toBeNull();
650
+ expect(listing!.path).toBe("@math");
651
+ });
652
+
653
+ test("browse lists agents from a specific registry", async () => {
654
+ const consumer = await createRegistryConsumer({
655
+ registries: [
656
+ {
657
+ url: `http://localhost:${PORT}`,
658
+ auth: { type: "api-key", key: API_KEY, header: "x-api-key" },
659
+ },
660
+ ],
661
+ refs: [{ ref: "@math" }],
662
+ });
663
+
664
+ const agents = await consumer.browse(`http://localhost:${PORT}`);
665
+ expect(agents.length).toBeGreaterThan(0);
666
+ expect(agents.some((a) => a.path === "@math")).toBe(true);
667
+ });
668
+ });
@@ -42,11 +42,17 @@ export interface RegistryEntry {
42
42
  /** How to authenticate with this registry */
43
43
  auth?: RegistryAuth;
44
44
 
45
+ /** Arbitrary headers to send with every request to this registry (values can be secret URIs) */
46
+ headers?: Record<string, string>;
47
+
45
48
  /** Human-readable name / alias for this registry */
46
49
  name?: string;
47
50
 
48
51
  /** Publisher name shown in the app store UI */
49
52
  publisher?: string;
53
+
54
+ /** Connection status — set by validation/test, used to filter active entries */
55
+ status?: 'active' | 'inactive' | 'error';
50
56
  }
51
57
 
52
58
  // ============================================
@@ -75,6 +81,9 @@ export type RefEntry = {
75
81
 
76
82
  /** The registry where this ref was discovered */
77
83
  sourceRegistry?: { url: string; agentPath: string };
84
+
85
+ /** Connection status — set by validation/test, used to filter active entries */
86
+ status?: 'active' | 'inactive' | 'error';
78
87
  };
79
88
 
80
89
  // ============================================
@@ -109,6 +118,8 @@ export interface ResolvedRegistry {
109
118
  name: string;
110
119
  publisher: string;
111
120
  auth: RegistryAuth;
121
+ /** Resolved headers (secret URIs replaced with values at resolution time) */
122
+ headers?: Record<string, string>;
112
123
  }
113
124
 
114
125
  /** A normalized ref entry (after resolution) */
@@ -170,6 +181,7 @@ export function normalizeRegistry(
170
181
  name: entry.name ?? url.hostname,
171
182
  publisher: entry.publisher ?? url.hostname.split(".")[0],
172
183
  auth: entry.auth ?? { type: "none" },
184
+ ...(entry.headers && { headers: entry.headers }),
173
185
  };
174
186
  }
175
187
 
package/src/index.ts CHANGED
@@ -136,6 +136,8 @@ export {
136
136
  detectAuth,
137
137
  resolveAuth,
138
138
  canSeeAgent,
139
+ canSeeTool,
140
+ getVisibleTools,
139
141
  hasAdminScope,
140
142
  } from "./server.js";
141
143
  export type {
@@ -118,6 +118,23 @@ describe("KeyManager", () => {
118
118
  expect(payload.exp - payload.iat).toBe(60);
119
119
  });
120
120
 
121
+ test("respects exp claim instead of default TTL", async () => {
122
+ const store = createMemoryKeyStore();
123
+ km = await createKeyManager({
124
+ store,
125
+ issuer: "http://test:3000",
126
+ tokenTtlSeconds: 300,
127
+ checkIntervalMs: 60_000,
128
+ });
129
+
130
+ const customExp = Math.floor(Date.now() / 1000) + 86400; // 24h from now
131
+ const token = await km.signJwt({ sub: "custom-exp", exp: customExp });
132
+ const payload = JSON.parse(
133
+ Buffer.from(token.split(".")[1], "base64url").toString(),
134
+ );
135
+ expect(payload.exp).toBe(customExp);
136
+ });
137
+
121
138
  // ---- Rotation tests ----
122
139
 
123
140
  test("rotation: creates new key when threshold exceeded", async () => {
@@ -236,12 +236,18 @@ export async function createKeyManager(
236
236
 
237
237
  async signJwt(claims: Record<string, unknown>): Promise<string> {
238
238
  const key = getActiveKey();
239
- return new SignJWT({ ...claims } as any)
239
+ let builder = new SignJWT({ ...claims } as any)
240
240
  .setProtectedHeader({ alg: ALG, kid: key.kid })
241
241
  .setIssuer(issuer)
242
- .setIssuedAt()
243
- .setExpirationTime(`${tokenTtlSeconds}s`)
244
- .sign(key.privateKey);
242
+ .setIssuedAt();
243
+
244
+ if (claims.exp != null) {
245
+ builder = builder.setExpirationTime(claims.exp as number);
246
+ } else {
247
+ builder = builder.setExpirationTime(`${tokenTtlSeconds}s`);
248
+ }
249
+
250
+ return builder.sign(key.privateKey);
245
251
  },
246
252
 
247
253
  async rotate(): Promise<void> {