@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.
- package/dist/auth-governance.d.ts +37 -0
- package/dist/auth-governance.d.ts.map +1 -0
- package/dist/auth-governance.js +73 -0
- package/dist/auth-governance.js.map +1 -0
- package/dist/call-agent-schema.d.ts +20 -0
- package/dist/call-agent-schema.d.ts.map +1 -1
- package/dist/call-agent-schema.js +19 -0
- package/dist/call-agent-schema.js.map +1 -1
- package/dist/cjs/auth-governance.js +79 -0
- package/dist/cjs/auth-governance.js.map +1 -0
- package/dist/cjs/call-agent-schema.js +20 -1
- package/dist/cjs/call-agent-schema.js.map +1 -1
- package/dist/cjs/define-config.js +1 -0
- package/dist/cjs/define-config.js.map +1 -1
- package/dist/cjs/index.js +4 -2
- package/dist/cjs/index.js.map +1 -1
- package/dist/cjs/key-manager.js +9 -4
- package/dist/cjs/key-manager.js.map +1 -1
- package/dist/cjs/registry-consumer.js +122 -37
- package/dist/cjs/registry-consumer.js.map +1 -1
- package/dist/cjs/server.js +149 -209
- package/dist/cjs/server.js.map +1 -1
- package/dist/define-config.d.ts +8 -0
- package/dist/define-config.d.ts.map +1 -1
- package/dist/define-config.js +1 -0
- 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 +1 -1
- package/dist/index.js.map +1 -1
- package/dist/key-manager.d.ts.map +1 -1
- package/dist/key-manager.js +9 -4
- package/dist/key-manager.js.map +1 -1
- package/dist/registry-consumer.d.ts +4 -0
- package/dist/registry-consumer.d.ts.map +1 -1
- package/dist/registry-consumer.js +122 -37
- package/dist/registry-consumer.js.map +1 -1
- package/dist/server.d.ts +3 -13
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +136 -199
- package/dist/server.js.map +1 -1
- package/package.json +1 -1
- package/src/auth-governance.ts +94 -0
- package/src/call-agent-schema.ts +33 -0
- package/src/codegen.test.ts +10 -0
- package/src/consumer.test.ts +132 -0
- package/src/define-config.ts +12 -0
- package/src/index.ts +2 -0
- package/src/key-manager.test.ts +17 -0
- package/src/key-manager.ts +10 -4
- package/src/registry-consumer.ts +161 -37
- 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
|
+
}
|
package/src/call-agent-schema.ts
CHANGED
|
@@ -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
|
+
);
|
package/src/codegen.test.ts
CHANGED
|
@@ -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;
|
package/src/consumer.test.ts
CHANGED
|
@@ -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
|
+
});
|
package/src/define-config.ts
CHANGED
|
@@ -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
package/src/key-manager.test.ts
CHANGED
|
@@ -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 () => {
|
package/src/key-manager.ts
CHANGED
|
@@ -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
|
-
|
|
239
|
+
let builder = new SignJWT({ ...claims } as any)
|
|
240
240
|
.setProtectedHeader({ alg: ALG, kid: key.kid })
|
|
241
241
|
.setIssuer(issuer)
|
|
242
|
-
.setIssuedAt()
|
|
243
|
-
|
|
244
|
-
|
|
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> {
|