@slashfi/agents-sdk 0.11.1 → 0.12.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/agent-definitions/auth.d.ts +4 -1
- package/dist/agent-definitions/auth.d.ts.map +1 -1
- package/dist/agent-definitions/auth.js +48 -3
- package/dist/agent-definitions/auth.js.map +1 -1
- package/dist/agent-definitions/integrations.d.ts +2 -14
- package/dist/agent-definitions/integrations.d.ts.map +1 -1
- package/dist/agent-definitions/integrations.js +46 -17
- package/dist/agent-definitions/integrations.js.map +1 -1
- package/dist/agent-definitions/remote-registry.d.ts +19 -14
- package/dist/agent-definitions/remote-registry.d.ts.map +1 -1
- package/dist/agent-definitions/remote-registry.js +207 -381
- package/dist/agent-definitions/remote-registry.js.map +1 -1
- package/dist/agent-definitions/users.d.ts.map +1 -1
- package/dist/agent-definitions/users.js +29 -1
- package/dist/agent-definitions/users.js.map +1 -1
- package/dist/define.d.ts +6 -4
- package/dist/define.d.ts.map +1 -1
- package/dist/define.js +82 -3
- package/dist/define.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/jwt.js +1 -1
- package/dist/jwt.js.map +1 -1
- package/dist/server.d.ts +42 -5
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +223 -62
- package/dist/server.js.map +1 -1
- package/dist/types.d.ts +53 -1
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/agent-definitions/auth.ts +57 -5
- package/src/agent-definitions/integrations.ts +51 -26
- package/src/agent-definitions/remote-registry.ts +210 -513
- package/src/agent-definitions/users.ts +35 -1
- package/src/define.ts +98 -6
- package/src/index.ts +2 -1
- package/src/jwt.ts +1 -1
- package/src/server.test.ts +284 -0
- package/src/server.ts +331 -75
- package/src/types.ts +44 -1
|
@@ -250,10 +250,30 @@ export function createUsersAgent(options: UsersAgentOptions): AgentDefinition {
|
|
|
250
250
|
name: { type: "string", description: "Display name" },
|
|
251
251
|
avatarUrl: { type: "string", description: "Avatar URL" },
|
|
252
252
|
metadata: { type: "object", description: "Additional metadata" },
|
|
253
|
+
externalRef: {
|
|
254
|
+
type: "object",
|
|
255
|
+
description: "Link to a user on a remote system. Creates identity automatically.",
|
|
256
|
+
properties: {
|
|
257
|
+
issuer: { type: "string", description: "Issuer URL of the remote system" },
|
|
258
|
+
userId: { type: "string", description: "User ID on the remote system" },
|
|
259
|
+
},
|
|
260
|
+
},
|
|
253
261
|
},
|
|
254
262
|
required: ["tenantId"],
|
|
255
263
|
},
|
|
256
264
|
execute: async (input: any, __ctx: ToolContext) => {
|
|
265
|
+
// If externalRef provided, check if identity already exists
|
|
266
|
+
if (input.externalRef) {
|
|
267
|
+
const existing = await store.findIdentityByProviderUserId(
|
|
268
|
+
input.externalRef.issuer,
|
|
269
|
+
input.externalRef.userId,
|
|
270
|
+
);
|
|
271
|
+
if (existing) {
|
|
272
|
+
const user = await store.getUser(existing.userId);
|
|
273
|
+
return { success: true, user, identity: existing, alreadyLinked: true };
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
257
277
|
const user = await store.createUser({
|
|
258
278
|
id: input.id ?? generateId("user_"),
|
|
259
279
|
tenantId: input.tenantId,
|
|
@@ -262,7 +282,21 @@ export function createUsersAgent(options: UsersAgentOptions): AgentDefinition {
|
|
|
262
282
|
avatarUrl: input.avatarUrl,
|
|
263
283
|
metadata: input.metadata,
|
|
264
284
|
});
|
|
265
|
-
|
|
285
|
+
|
|
286
|
+
// Auto-create identity link if externalRef provided
|
|
287
|
+
let identity: UserIdentity | undefined;
|
|
288
|
+
if (input.externalRef) {
|
|
289
|
+
identity = await store.createIdentity({
|
|
290
|
+
id: generateId("uid_"),
|
|
291
|
+
userId: user.id,
|
|
292
|
+
provider: input.externalRef.issuer,
|
|
293
|
+
providerUserId: input.externalRef.userId,
|
|
294
|
+
email: input.email,
|
|
295
|
+
name: input.name,
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
return { success: true, user, identity };
|
|
266
300
|
},
|
|
267
301
|
});
|
|
268
302
|
|
package/src/define.ts
CHANGED
|
@@ -5,10 +5,10 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import type {
|
|
8
|
+
IntegrationHooks,
|
|
8
9
|
AgentConfig,
|
|
9
10
|
AgentDefinition,
|
|
10
11
|
AgentRuntime,
|
|
11
|
-
IntegrationMethods,
|
|
12
12
|
JsonSchema,
|
|
13
13
|
ToolContext,
|
|
14
14
|
ToolDefinition,
|
|
@@ -115,10 +115,18 @@ export interface DefineAgentOptions<
|
|
|
115
115
|
allowedCallers?: string[];
|
|
116
116
|
|
|
117
117
|
/**
|
|
118
|
+
* Integration hooks. When provided, defineAgent auto-generates
|
|
119
|
+
* setup_integration, connect_integration, etc. as tools.
|
|
120
|
+
*/
|
|
121
|
+
integration?: IntegrationHooks;
|
|
122
|
+
|
|
123
|
+
/** Lazy loader for event listeners (e.g. entrypoint.ts) */
|
|
124
|
+
loadListeners?: () => Promise<unknown>;
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* @deprecated Use `integration` instead.
|
|
118
128
|
* Integration method callbacks.
|
|
119
|
-
* Implement these when this agent acts as an integration.
|
|
120
129
|
*/
|
|
121
|
-
integrationMethods?: IntegrationMethods;
|
|
122
130
|
}
|
|
123
131
|
|
|
124
132
|
/**
|
|
@@ -148,14 +156,98 @@ export interface DefineAgentOptions<
|
|
|
148
156
|
export function defineAgent<TContext extends ToolContext = ToolContext>(
|
|
149
157
|
options: DefineAgentOptions<TContext>,
|
|
150
158
|
): AgentDefinition<TContext> {
|
|
159
|
+
const tools = [...(options.tools ?? [])];
|
|
160
|
+
let config = options.config;
|
|
161
|
+
|
|
162
|
+
// Auto-generate integration tools from hooks
|
|
163
|
+
if (options.integration) {
|
|
164
|
+
const h = options.integration;
|
|
165
|
+
|
|
166
|
+
// Set config.integration metadata if not already set
|
|
167
|
+
if (!config?.integration) {
|
|
168
|
+
config = {
|
|
169
|
+
...config,
|
|
170
|
+
integration: {
|
|
171
|
+
provider: h.provider,
|
|
172
|
+
displayName: h.displayName,
|
|
173
|
+
icon: h.icon,
|
|
174
|
+
category: h.category,
|
|
175
|
+
description: h.description,
|
|
176
|
+
},
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
if (h.setup) {
|
|
181
|
+
const fn = h.setup;
|
|
182
|
+
tools.push(defineTool({
|
|
183
|
+
name: "setup_integration",
|
|
184
|
+
description: `Set up ${h.displayName} integration.`,
|
|
185
|
+
visibility: "public" as const,
|
|
186
|
+
inputSchema: { type: "object" as const, properties: { url: { type: "string" }, name: { type: "string" }, config: { type: "object" } } },
|
|
187
|
+
execute: (input: any, ctx: any) => fn(input, ctx),
|
|
188
|
+
}) as any);
|
|
189
|
+
}
|
|
190
|
+
if (h.connect) {
|
|
191
|
+
const fn = h.connect;
|
|
192
|
+
tools.push(defineTool({
|
|
193
|
+
name: "connect_integration",
|
|
194
|
+
description: `Connect a user to ${h.displayName}.`,
|
|
195
|
+
visibility: "public" as const,
|
|
196
|
+
inputSchema: { type: "object" as const, properties: { registryId: { type: "string" }, oidcUserId: { type: "string" }, redirectUri: { type: "string" } }, required: ["registryId"] as const },
|
|
197
|
+
execute: (input: any, ctx: any) => fn(input, ctx),
|
|
198
|
+
}) as any);
|
|
199
|
+
}
|
|
200
|
+
if (h.discover) {
|
|
201
|
+
const fn = h.discover;
|
|
202
|
+
tools.push(defineTool({
|
|
203
|
+
name: "discover_integrations",
|
|
204
|
+
description: `Discover available ${h.displayName} instances.`,
|
|
205
|
+
visibility: "public" as const,
|
|
206
|
+
inputSchema: { type: "object" as const, properties: { url: { type: "string" } } },
|
|
207
|
+
execute: (input: any, ctx: any) => fn(input, ctx),
|
|
208
|
+
}) as any);
|
|
209
|
+
}
|
|
210
|
+
if (h.list) {
|
|
211
|
+
const fn = h.list;
|
|
212
|
+
tools.push(defineTool({
|
|
213
|
+
name: "list_integrations",
|
|
214
|
+
description: `List connected ${h.displayName} instances.`,
|
|
215
|
+
visibility: "public" as const,
|
|
216
|
+
inputSchema: { type: "object" as const, properties: {} },
|
|
217
|
+
execute: (input: any, ctx: any) => fn(input, ctx),
|
|
218
|
+
}) as any);
|
|
219
|
+
}
|
|
220
|
+
if (h.get) {
|
|
221
|
+
const fn = h.get;
|
|
222
|
+
tools.push(defineTool({
|
|
223
|
+
name: "get_integration",
|
|
224
|
+
description: `Get details of a ${h.displayName} instance.`,
|
|
225
|
+
visibility: "public" as const,
|
|
226
|
+
inputSchema: { type: "object" as const, properties: { registryId: { type: "string" } }, required: ["registryId"] as const },
|
|
227
|
+
execute: (input: any, ctx: any) => fn(input, ctx),
|
|
228
|
+
}) as any);
|
|
229
|
+
}
|
|
230
|
+
if (h.update) {
|
|
231
|
+
const fn = h.update;
|
|
232
|
+
tools.push(defineTool({
|
|
233
|
+
name: "update_integration",
|
|
234
|
+
description: `Update a ${h.displayName} instance.`,
|
|
235
|
+
visibility: "public" as const,
|
|
236
|
+
inputSchema: { type: "object" as const, properties: { registryId: { type: "string" }, name: { type: "string" }, url: { type: "string" } }, required: ["registryId"] as const },
|
|
237
|
+
execute: (input: any, ctx: any) => fn(input, ctx),
|
|
238
|
+
}) as any);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
|
|
151
243
|
return {
|
|
152
244
|
path: options.path,
|
|
153
245
|
entrypoint: options.entrypoint,
|
|
154
|
-
config
|
|
155
|
-
tools
|
|
246
|
+
config,
|
|
247
|
+
tools,
|
|
156
248
|
runtime: options.runtime,
|
|
157
249
|
visibility: options.visibility,
|
|
158
250
|
allowedCallers: options.allowedCallers,
|
|
159
|
-
|
|
251
|
+
loadListeners: options.loadListeners,
|
|
160
252
|
};
|
|
161
253
|
}
|
package/src/index.ts
CHANGED
|
@@ -85,6 +85,7 @@ export type {
|
|
|
85
85
|
IntegrationMethods,
|
|
86
86
|
IntegrationMethodResult,
|
|
87
87
|
IntegrationMethodContext,
|
|
88
|
+
IntegrationHooks,
|
|
88
89
|
Visibility,
|
|
89
90
|
} from "./types.js";
|
|
90
91
|
|
|
@@ -98,7 +99,7 @@ export type { AgentRegistry, AgentRegistryOptions } from "./registry.js";
|
|
|
98
99
|
|
|
99
100
|
// Server
|
|
100
101
|
export { createAgentServer, detectAuth, resolveAuth, canSeeAgent } from "./server.js";
|
|
101
|
-
export type { AgentServer, AgentServerOptions, AuthConfig, ResolvedAuth, TrustedIssuer } from "./server.js";
|
|
102
|
+
export type { AgentServer, AgentServerOptions, AuthConfig, OAuthIdentityProvider, ResolvedAuth, TrustedIssuer } from "./server.js";
|
|
102
103
|
|
|
103
104
|
// Secret Collection
|
|
104
105
|
export {
|
package/src/jwt.ts
CHANGED
|
@@ -80,7 +80,7 @@ export interface ExportedKeyPair {
|
|
|
80
80
|
* Generate a new ES256 signing key pair.
|
|
81
81
|
*/
|
|
82
82
|
export async function generateSigningKey(kid?: string): Promise<SigningKey> {
|
|
83
|
-
const { privateKey, publicKey } = await generateKeyPair("ES256");
|
|
83
|
+
const { privateKey, publicKey } = await generateKeyPair("ES256", { extractable: true });
|
|
84
84
|
return {
|
|
85
85
|
kid: kid ?? `key-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
|
|
86
86
|
privateKey,
|
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
import { describe, test, expect, beforeAll, afterAll } from 'bun:test';
|
|
2
|
+
import {
|
|
3
|
+
createAgentServer,
|
|
4
|
+
createAgentRegistry,
|
|
5
|
+
detectAuth,
|
|
6
|
+
resolveAuth,
|
|
7
|
+
canSeeAgent,
|
|
8
|
+
} from './index';
|
|
9
|
+
import type { AgentDefinition, TrustedIssuer, AgentServer } from './index';
|
|
10
|
+
import { generateKeyPair, exportJWK, SignJWT } from 'jose';
|
|
11
|
+
|
|
12
|
+
// ─── Helpers ─────────────────────────────────────────────────────
|
|
13
|
+
|
|
14
|
+
function makeAgent(
|
|
15
|
+
path: string,
|
|
16
|
+
opts: Partial<AgentDefinition> = {},
|
|
17
|
+
): AgentDefinition {
|
|
18
|
+
return {
|
|
19
|
+
path,
|
|
20
|
+
entrypoint: 'test',
|
|
21
|
+
tools: [],
|
|
22
|
+
visibility: 'internal',
|
|
23
|
+
config: { name: path.split('/').pop(), supportedActions: ['load'] },
|
|
24
|
+
...opts,
|
|
25
|
+
} as AgentDefinition;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async function mcpCall(
|
|
29
|
+
port: number,
|
|
30
|
+
toolName: string,
|
|
31
|
+
args: Record<string, unknown>,
|
|
32
|
+
authToken?: string,
|
|
33
|
+
) {
|
|
34
|
+
const headers: Record<string, string> = { 'Content-Type': 'application/json' };
|
|
35
|
+
if (authToken) headers.Authorization = `Bearer ${authToken}`;
|
|
36
|
+
|
|
37
|
+
const res = await fetch(`http://localhost:${port}`, {
|
|
38
|
+
method: 'POST',
|
|
39
|
+
headers,
|
|
40
|
+
body: JSON.stringify({
|
|
41
|
+
jsonrpc: '2.0',
|
|
42
|
+
id: Date.now(),
|
|
43
|
+
method: 'tools/call',
|
|
44
|
+
params: { name: toolName, arguments: args },
|
|
45
|
+
}),
|
|
46
|
+
});
|
|
47
|
+
return res.json() as Promise<any>;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function parseResult(rpc: any): any {
|
|
51
|
+
const text = rpc.result?.content?.[0]?.text;
|
|
52
|
+
return text ? JSON.parse(text) : null;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// ─── E2E: Full server auth flow ──────────────────────────────────
|
|
56
|
+
//
|
|
57
|
+
// These tests spin up a real createAgentServer, send actual HTTP
|
|
58
|
+
// requests, and verify the complete path:
|
|
59
|
+
// HTTP request → auth resolution → handleToolCall → registry.call → access check
|
|
60
|
+
//
|
|
61
|
+
// This is what actually broke today: authConfig was null → resolveAuth
|
|
62
|
+
// was skipped → trusted issuer tokens were ignored.
|
|
63
|
+
|
|
64
|
+
describe('E2E: createAgentServer with trusted issuers', () => {
|
|
65
|
+
let privateKey: CryptoKey;
|
|
66
|
+
let publicJwk: any;
|
|
67
|
+
let jwksHttpServer: ReturnType<typeof Bun.serve>;
|
|
68
|
+
let server: AgentServer;
|
|
69
|
+
const JWKS_PORT = 19880;
|
|
70
|
+
const SDK_PORT = 19881;
|
|
71
|
+
const ISSUER_URL = `http://localhost:${JWKS_PORT}`;
|
|
72
|
+
const KID = 'test-e2e-key';
|
|
73
|
+
|
|
74
|
+
beforeAll(async () => {
|
|
75
|
+
// 1. Generate ES256 keypair and serve JWKS
|
|
76
|
+
const keyPair = await generateKeyPair('ES256', { extractable: true });
|
|
77
|
+
privateKey = keyPair.privateKey;
|
|
78
|
+
publicJwk = await exportJWK(keyPair.publicKey);
|
|
79
|
+
publicJwk.kid = KID;
|
|
80
|
+
publicJwk.alg = 'ES256';
|
|
81
|
+
publicJwk.use = 'sig';
|
|
82
|
+
|
|
83
|
+
jwksHttpServer = Bun.serve({
|
|
84
|
+
port: JWKS_PORT,
|
|
85
|
+
fetch(req) {
|
|
86
|
+
const url = new URL(req.url);
|
|
87
|
+
if (url.pathname === '/.well-known/jwks.json') {
|
|
88
|
+
return new Response(JSON.stringify({ keys: [publicJwk] }), {
|
|
89
|
+
headers: { 'Content-Type': 'application/json' },
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
return new Response('Not found', { status: 404 });
|
|
93
|
+
},
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
// 2. Create registry with internal + public agents
|
|
97
|
+
const registry = createAgentRegistry();
|
|
98
|
+
registry.register(makeAgent('/agents/@clock', { visibility: 'internal' }));
|
|
99
|
+
registry.register(makeAgent('/agents/public-bot', { visibility: 'public' }));
|
|
100
|
+
|
|
101
|
+
// 3. Create server with trusted issuer — NO @auth agent registered
|
|
102
|
+
// This is the exact scenario that was broken.
|
|
103
|
+
server = createAgentServer(registry, {
|
|
104
|
+
port: SDK_PORT,
|
|
105
|
+
trustedIssuers: [{
|
|
106
|
+
issuer: ISSUER_URL,
|
|
107
|
+
scopes: ['agents:admin'],
|
|
108
|
+
}],
|
|
109
|
+
});
|
|
110
|
+
await server.start();
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
afterAll(() => {
|
|
114
|
+
server?.stop?.();
|
|
115
|
+
jwksHttpServer?.stop();
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
async function signToken(claims: Record<string, unknown> = {}): Promise<string> {
|
|
119
|
+
return new SignJWT({ sub: 'atlas-api', ...claims } as any)
|
|
120
|
+
.setProtectedHeader({ alg: 'ES256', kid: KID })
|
|
121
|
+
.setIssuer(ISSUER_URL)
|
|
122
|
+
.setIssuedAt()
|
|
123
|
+
.setExpirationTime('5m')
|
|
124
|
+
.sign(privateKey);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// ─── Core auth flow tests ───────────────────────────────────
|
|
128
|
+
|
|
129
|
+
test('system token → can load internal agent', async () => {
|
|
130
|
+
const token = await signToken();
|
|
131
|
+
const rpc = await mcpCall(SDK_PORT, 'call_agent', {
|
|
132
|
+
request: { action: 'load', path: '/agents/@clock' },
|
|
133
|
+
}, token);
|
|
134
|
+
|
|
135
|
+
const result = parseResult(rpc);
|
|
136
|
+
expect(result.success).toBe(true);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
test('no token → access denied for internal agent', async () => {
|
|
140
|
+
const rpc = await mcpCall(SDK_PORT, 'call_agent', {
|
|
141
|
+
request: { action: 'load', path: '/agents/@clock' },
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
const result = parseResult(rpc);
|
|
145
|
+
expect(result.success).toBe(false);
|
|
146
|
+
expect(result.code).toBe('ACCESS_DENIED');
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
test('no token → public agent still accessible', async () => {
|
|
150
|
+
const rpc = await mcpCall(SDK_PORT, 'call_agent', {
|
|
151
|
+
request: { action: 'load', path: '/agents/public-bot' },
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
const result = parseResult(rpc);
|
|
155
|
+
expect(result.success).toBe(true);
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
test('garbage token → access denied', async () => {
|
|
159
|
+
const rpc = await mcpCall(SDK_PORT, 'call_agent', {
|
|
160
|
+
request: { action: 'load', path: '/agents/@clock' },
|
|
161
|
+
}, 'not.a.valid.jwt');
|
|
162
|
+
|
|
163
|
+
const result = parseResult(rpc);
|
|
164
|
+
expect(result.success).toBe(false);
|
|
165
|
+
expect(result.code).toBe('ACCESS_DENIED');
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
test('token with wrong issuer → access denied', async () => {
|
|
169
|
+
// Sign with correct key but wrong iss claim
|
|
170
|
+
const token = await new SignJWT({ sub: 'evil' } as any)
|
|
171
|
+
.setProtectedHeader({ alg: 'ES256', kid: KID })
|
|
172
|
+
.setIssuer('http://evil:9999') // not in trustedIssuers
|
|
173
|
+
.setIssuedAt()
|
|
174
|
+
.setExpirationTime('5m')
|
|
175
|
+
.sign(privateKey);
|
|
176
|
+
|
|
177
|
+
const rpc = await mcpCall(SDK_PORT, 'call_agent', {
|
|
178
|
+
request: { action: 'load', path: '/agents/@clock' },
|
|
179
|
+
}, token);
|
|
180
|
+
|
|
181
|
+
const result = parseResult(rpc);
|
|
182
|
+
expect(result.success).toBe(false);
|
|
183
|
+
expect(result.code).toBe('ACCESS_DENIED');
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
// ─── Visibility in list_agents ─────────────────────────────
|
|
187
|
+
|
|
188
|
+
test('list_agents without token → only public agents', async () => {
|
|
189
|
+
const rpc = await mcpCall(SDK_PORT, 'list_agents', {});
|
|
190
|
+
const result = parseResult(rpc);
|
|
191
|
+
expect(result.success).toBe(true);
|
|
192
|
+
|
|
193
|
+
const paths = result.agents.map((a: any) => a.path);
|
|
194
|
+
expect(paths).toContain('/agents/public-bot');
|
|
195
|
+
expect(paths).not.toContain('/agents/@clock');
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
test('list_agents with system token → all agents visible', async () => {
|
|
199
|
+
const token = await signToken();
|
|
200
|
+
const rpc = await mcpCall(SDK_PORT, 'list_agents', {}, token);
|
|
201
|
+
const result = parseResult(rpc);
|
|
202
|
+
expect(result.success).toBe(true);
|
|
203
|
+
|
|
204
|
+
const paths = result.agents.map((a: any) => a.path);
|
|
205
|
+
expect(paths).toContain('/agents/public-bot');
|
|
206
|
+
expect(paths).toContain('/agents/@clock');
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
// ─── Scopes: limited issuer ────────────────────────────────
|
|
210
|
+
|
|
211
|
+
test('issuer with limited scopes → resolves as agent, not system', async () => {
|
|
212
|
+
// Create a separate server with limited-scope issuer
|
|
213
|
+
const limitedRegistry = createAgentRegistry();
|
|
214
|
+
limitedRegistry.register(makeAgent('/agents/@private-agent', { visibility: 'private' }));
|
|
215
|
+
limitedRegistry.register(makeAgent('/agents/@internal-agent', { visibility: 'internal' }));
|
|
216
|
+
|
|
217
|
+
const limitedServer = createAgentServer(limitedRegistry, {
|
|
218
|
+
port: 19882,
|
|
219
|
+
trustedIssuers: [{
|
|
220
|
+
issuer: ISSUER_URL,
|
|
221
|
+
scopes: ['agents:read'], // NOT agents:admin or *
|
|
222
|
+
}],
|
|
223
|
+
});
|
|
224
|
+
await limitedServer.start();
|
|
225
|
+
|
|
226
|
+
try {
|
|
227
|
+
const token = await signToken();
|
|
228
|
+
|
|
229
|
+
// agents:read grants agent-level access (not system)
|
|
230
|
+
// Internal agents are accessible to authenticated agents
|
|
231
|
+
const internalRpc = await mcpCall(19882, 'call_agent', {
|
|
232
|
+
request: { action: 'load', path: '/agents/@internal-agent' },
|
|
233
|
+
}, token);
|
|
234
|
+
expect(parseResult(internalRpc).success).toBe(true);
|
|
235
|
+
|
|
236
|
+
// Private agents should be denied (only self can access)
|
|
237
|
+
const privateRpc = await mcpCall(19882, 'call_agent', {
|
|
238
|
+
request: { action: 'load', path: '/agents/@private-agent' },
|
|
239
|
+
}, token);
|
|
240
|
+
expect(parseResult(privateRpc).success).toBe(false);
|
|
241
|
+
expect(parseResult(privateRpc).code).toBe('ACCESS_DENIED');
|
|
242
|
+
} finally {
|
|
243
|
+
limitedServer?.stop?.();
|
|
244
|
+
}
|
|
245
|
+
});
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
// ─── Unit: detectAuth ────────────────────────────────────────────
|
|
249
|
+
|
|
250
|
+
describe('detectAuth', () => {
|
|
251
|
+
test('returns non-null even without @auth agent', () => {
|
|
252
|
+
const registry = createAgentRegistry();
|
|
253
|
+
const config = detectAuth(registry);
|
|
254
|
+
expect(config).toBeDefined();
|
|
255
|
+
expect(config).not.toBeNull();
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
test('returns empty config (no store, no rootKey) without @auth agent', () => {
|
|
259
|
+
const registry = createAgentRegistry();
|
|
260
|
+
const config = detectAuth(registry);
|
|
261
|
+
expect(config.store).toBeUndefined();
|
|
262
|
+
expect(config.rootKey).toBeUndefined();
|
|
263
|
+
});
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
// ─── Unit: canSeeAgent ───────────────────────────────────────────
|
|
267
|
+
|
|
268
|
+
describe('canSeeAgent', () => {
|
|
269
|
+
test('system auth can see internal agents', () => {
|
|
270
|
+
const agent = makeAgent('/agents/@clock', { visibility: 'internal' });
|
|
271
|
+
const auth = { callerId: 'api', callerType: 'system' as const, scopes: ['*'], isRoot: true };
|
|
272
|
+
expect(canSeeAgent(agent, auth)).toBe(true);
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
test('null auth cannot see internal agents', () => {
|
|
276
|
+
const agent = makeAgent('/agents/@clock', { visibility: 'internal' });
|
|
277
|
+
expect(canSeeAgent(agent, null)).toBe(false);
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
test('null auth can see public agents', () => {
|
|
281
|
+
const agent = makeAgent('/agents/public', { visibility: 'public' });
|
|
282
|
+
expect(canSeeAgent(agent, null)).toBe(true);
|
|
283
|
+
});
|
|
284
|
+
});
|