@slashfi/agents-sdk 0.1.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/LICENSE +21 -0
- package/README.md +274 -0
- package/dist/auth.d.ts +109 -0
- package/dist/auth.d.ts.map +1 -0
- package/dist/auth.js +329 -0
- package/dist/auth.js.map +1 -0
- package/dist/build.d.ts +68 -0
- package/dist/build.d.ts.map +1 -0
- package/dist/build.js +159 -0
- package/dist/build.js.map +1 -0
- package/dist/define.d.ts +87 -0
- package/dist/define.d.ts.map +1 -0
- package/dist/define.js +71 -0
- package/dist/define.js.map +1 -0
- package/dist/index.d.ts +61 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +60 -0
- package/dist/index.js.map +1 -0
- package/dist/registry.d.ts +48 -0
- package/dist/registry.d.ts.map +1 -0
- package/dist/registry.js +274 -0
- package/dist/registry.js.map +1 -0
- package/dist/server.d.ts +66 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +308 -0
- package/dist/server.js.map +1 -0
- package/dist/types.d.ts +389 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +7 -0
- package/dist/types.js.map +1 -0
- package/package.json +54 -0
- package/src/auth.ts +493 -0
- package/src/build.ts +238 -0
- package/src/define.ts +153 -0
- package/src/index.ts +111 -0
- package/src/registry.ts +403 -0
- package/src/server.ts +460 -0
- package/src/types.ts +524 -0
package/src/server.ts
ADDED
|
@@ -0,0 +1,460 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agent Server
|
|
3
|
+
*
|
|
4
|
+
* HTTP server that exposes the agent registry via JSON-RPC endpoints.
|
|
5
|
+
* Compatible with MCP (Model Context Protocol) over HTTP.
|
|
6
|
+
*
|
|
7
|
+
* Endpoints:
|
|
8
|
+
* - POST /call - Execute agent actions (execute_tool, describe_tools, load)
|
|
9
|
+
* - GET /list - List registered agents
|
|
10
|
+
* - POST /oauth/token - OAuth2 token endpoint (when @auth is registered)
|
|
11
|
+
*
|
|
12
|
+
* Auth Integration:
|
|
13
|
+
* When an `@auth` agent is registered, the server automatically:
|
|
14
|
+
* - Validates Bearer tokens on requests
|
|
15
|
+
* - Resolves tokens to identity + scopes
|
|
16
|
+
* - Populates callerId, callerType in the request context
|
|
17
|
+
* - Recognizes the root key for admin access
|
|
18
|
+
* - Mounts the /oauth/token endpoint
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import type { AuthStore } from "./auth.js";
|
|
22
|
+
import type { AgentRegistry } from "./registry.js";
|
|
23
|
+
import type { AgentDefinition, CallAgentRequest, Visibility } from "./types.js";
|
|
24
|
+
|
|
25
|
+
// ============================================
|
|
26
|
+
// Server Types
|
|
27
|
+
// ============================================
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Server configuration options.
|
|
31
|
+
*/
|
|
32
|
+
export interface AgentServerOptions {
|
|
33
|
+
/** Port to listen on (default: 3000) */
|
|
34
|
+
port?: number;
|
|
35
|
+
|
|
36
|
+
/** Hostname to bind to (default: 'localhost') */
|
|
37
|
+
hostname?: string;
|
|
38
|
+
|
|
39
|
+
/** Base path for endpoints (default: '') */
|
|
40
|
+
basePath?: string;
|
|
41
|
+
|
|
42
|
+
/** Enable CORS (default: true) */
|
|
43
|
+
cors?: boolean;
|
|
44
|
+
|
|
45
|
+
/** Custom request handler for unmatched routes */
|
|
46
|
+
onNotFound?: (req: Request) => Response | Promise<Response>;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Agent server instance.
|
|
51
|
+
*/
|
|
52
|
+
export interface AgentServer {
|
|
53
|
+
/** Start the server */
|
|
54
|
+
start(): Promise<void>;
|
|
55
|
+
|
|
56
|
+
/** Stop the server */
|
|
57
|
+
stop(): Promise<void>;
|
|
58
|
+
|
|
59
|
+
/** Handle a request (for custom integrations) */
|
|
60
|
+
fetch(req: Request): Promise<Response>;
|
|
61
|
+
|
|
62
|
+
/** Get the server URL (only available after start) */
|
|
63
|
+
url: string | null;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// ============================================
|
|
67
|
+
// Auth Integration Types
|
|
68
|
+
// ============================================
|
|
69
|
+
|
|
70
|
+
interface AuthConfig {
|
|
71
|
+
store: AuthStore;
|
|
72
|
+
rootKey: string;
|
|
73
|
+
tokenTtl: number;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
interface ResolvedAuth {
|
|
77
|
+
callerId: string;
|
|
78
|
+
callerType: "agent" | "user" | "system";
|
|
79
|
+
scopes: string[];
|
|
80
|
+
isRoot: boolean;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// ============================================
|
|
84
|
+
// Response Helpers
|
|
85
|
+
// ============================================
|
|
86
|
+
|
|
87
|
+
function jsonResponse(data: unknown, status = 200): Response {
|
|
88
|
+
return new Response(JSON.stringify(data), {
|
|
89
|
+
status,
|
|
90
|
+
headers: {
|
|
91
|
+
"Content-Type": "application/json",
|
|
92
|
+
},
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function corsHeaders(): Record<string, string> {
|
|
97
|
+
return {
|
|
98
|
+
"Access-Control-Allow-Origin": "*",
|
|
99
|
+
"Access-Control-Allow-Methods": "GET, POST, OPTIONS",
|
|
100
|
+
"Access-Control-Allow-Headers": "Content-Type, Authorization",
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// ============================================
|
|
105
|
+
// Auth Detection
|
|
106
|
+
// ============================================
|
|
107
|
+
|
|
108
|
+
function detectAuth(registry: AgentRegistry): AuthConfig | null {
|
|
109
|
+
const authAgent = registry.get("@auth") as
|
|
110
|
+
| (AgentDefinition & {
|
|
111
|
+
__authStore?: AuthStore;
|
|
112
|
+
__rootKey?: string;
|
|
113
|
+
__tokenTtl?: number;
|
|
114
|
+
})
|
|
115
|
+
| undefined;
|
|
116
|
+
|
|
117
|
+
if (!authAgent?.__authStore || !authAgent.__rootKey) return null;
|
|
118
|
+
|
|
119
|
+
return {
|
|
120
|
+
store: authAgent.__authStore,
|
|
121
|
+
rootKey: authAgent.__rootKey,
|
|
122
|
+
tokenTtl: authAgent.__tokenTtl ?? 3600,
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
async function resolveAuth(
|
|
127
|
+
req: Request,
|
|
128
|
+
authConfig: AuthConfig,
|
|
129
|
+
): Promise<ResolvedAuth | null> {
|
|
130
|
+
const authHeader = req.headers.get("Authorization");
|
|
131
|
+
if (!authHeader) return null;
|
|
132
|
+
|
|
133
|
+
const [scheme, credential] = authHeader.split(" ", 2);
|
|
134
|
+
if (scheme?.toLowerCase() !== "bearer" || !credential) return null;
|
|
135
|
+
|
|
136
|
+
// Check root key
|
|
137
|
+
if (credential === authConfig.rootKey) {
|
|
138
|
+
return {
|
|
139
|
+
callerId: "root",
|
|
140
|
+
callerType: "system",
|
|
141
|
+
scopes: ["*"],
|
|
142
|
+
isRoot: true,
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Validate token
|
|
147
|
+
const token = await authConfig.store.validateToken(credential);
|
|
148
|
+
if (!token) return null;
|
|
149
|
+
|
|
150
|
+
// Look up client name
|
|
151
|
+
const client = await authConfig.store.getClient(token.clientId);
|
|
152
|
+
|
|
153
|
+
return {
|
|
154
|
+
callerId: client?.name ?? token.clientId,
|
|
155
|
+
callerType: "agent",
|
|
156
|
+
scopes: token.scopes,
|
|
157
|
+
isRoot: false,
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// ============================================
|
|
162
|
+
// Visibility Filtering for /list
|
|
163
|
+
// ============================================
|
|
164
|
+
|
|
165
|
+
function canSeeAgent(
|
|
166
|
+
agent: AgentDefinition,
|
|
167
|
+
auth: ResolvedAuth | null,
|
|
168
|
+
): boolean {
|
|
169
|
+
const visibility: Visibility = agent.visibility ?? "internal";
|
|
170
|
+
|
|
171
|
+
if (auth?.isRoot) return true;
|
|
172
|
+
if (visibility === "public") return true;
|
|
173
|
+
if (visibility === "internal" && auth) return true;
|
|
174
|
+
return false;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// ============================================
|
|
178
|
+
// Create Server
|
|
179
|
+
// ============================================
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Create an HTTP server for the agent registry.
|
|
183
|
+
*
|
|
184
|
+
* @example
|
|
185
|
+
* ```typescript
|
|
186
|
+
* const registry = createAgentRegistry();
|
|
187
|
+
* registry.register(createAuthAgent({ rootKey: 'rk_xxx' }));
|
|
188
|
+
* registry.register(myAgent);
|
|
189
|
+
*
|
|
190
|
+
* const server = createAgentServer(registry, { port: 3000 });
|
|
191
|
+
* await server.start();
|
|
192
|
+
* // POST /call - Execute agent actions
|
|
193
|
+
* // GET /list - List agents (filtered by auth)
|
|
194
|
+
* // POST /oauth/token - OAuth2 token endpoint
|
|
195
|
+
* ```
|
|
196
|
+
*/
|
|
197
|
+
export function createAgentServer(
|
|
198
|
+
registry: AgentRegistry,
|
|
199
|
+
options: AgentServerOptions = {},
|
|
200
|
+
): AgentServer {
|
|
201
|
+
const {
|
|
202
|
+
port = 3000,
|
|
203
|
+
hostname = "localhost",
|
|
204
|
+
basePath = "",
|
|
205
|
+
cors = true,
|
|
206
|
+
onNotFound,
|
|
207
|
+
} = options;
|
|
208
|
+
|
|
209
|
+
let serverInstance: ReturnType<typeof Bun.serve> | null = null;
|
|
210
|
+
let serverUrl: string | null = null;
|
|
211
|
+
|
|
212
|
+
// Detect auth configuration
|
|
213
|
+
const authConfig = detectAuth(registry);
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Handle incoming requests.
|
|
217
|
+
*/
|
|
218
|
+
async function fetch(req: Request): Promise<Response> {
|
|
219
|
+
const url = new URL(req.url);
|
|
220
|
+
const path = url.pathname.replace(basePath, "") || "/";
|
|
221
|
+
|
|
222
|
+
// Handle CORS preflight
|
|
223
|
+
if (cors && req.method === "OPTIONS") {
|
|
224
|
+
return new Response(null, {
|
|
225
|
+
status: 204,
|
|
226
|
+
headers: corsHeaders(),
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// Add CORS headers to response
|
|
231
|
+
const addCors = (response: Response): Response => {
|
|
232
|
+
if (!cors) return response;
|
|
233
|
+
const headers = new Headers(response.headers);
|
|
234
|
+
for (const [key, value] of Object.entries(corsHeaders())) {
|
|
235
|
+
headers.set(key, value);
|
|
236
|
+
}
|
|
237
|
+
return new Response(response.body, {
|
|
238
|
+
status: response.status,
|
|
239
|
+
statusText: response.statusText,
|
|
240
|
+
headers,
|
|
241
|
+
});
|
|
242
|
+
};
|
|
243
|
+
|
|
244
|
+
// Resolve auth on every request
|
|
245
|
+
const auth = authConfig ? await resolveAuth(req, authConfig) : null;
|
|
246
|
+
|
|
247
|
+
try {
|
|
248
|
+
// POST /oauth/token - Standard OAuth2 endpoint
|
|
249
|
+
if (path === "/oauth/token" && req.method === "POST" && authConfig) {
|
|
250
|
+
const contentType = req.headers.get("Content-Type") ?? "";
|
|
251
|
+
let grantType: string;
|
|
252
|
+
let clientId: string;
|
|
253
|
+
let clientSecret: string;
|
|
254
|
+
|
|
255
|
+
if (contentType.includes("application/x-www-form-urlencoded")) {
|
|
256
|
+
const body = await req.text();
|
|
257
|
+
const params = new URLSearchParams(body);
|
|
258
|
+
grantType = params.get("grant_type") ?? "";
|
|
259
|
+
clientId = params.get("client_id") ?? "";
|
|
260
|
+
clientSecret = params.get("client_secret") ?? "";
|
|
261
|
+
} else {
|
|
262
|
+
const body = (await req.json()) as Record<string, string>;
|
|
263
|
+
grantType = body.grant_type ?? "";
|
|
264
|
+
clientId = body.client_id ?? "";
|
|
265
|
+
clientSecret = body.client_secret ?? "";
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
if (grantType !== "client_credentials") {
|
|
269
|
+
return addCors(
|
|
270
|
+
jsonResponse(
|
|
271
|
+
{
|
|
272
|
+
error: "unsupported_grant_type",
|
|
273
|
+
error_description: "Only client_credentials is supported",
|
|
274
|
+
},
|
|
275
|
+
400,
|
|
276
|
+
),
|
|
277
|
+
);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
if (!clientId || !clientSecret) {
|
|
281
|
+
return addCors(
|
|
282
|
+
jsonResponse(
|
|
283
|
+
{
|
|
284
|
+
error: "invalid_request",
|
|
285
|
+
error_description: "Missing client_id or client_secret",
|
|
286
|
+
},
|
|
287
|
+
400,
|
|
288
|
+
),
|
|
289
|
+
);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
const client = await authConfig.store.validateClient(
|
|
293
|
+
clientId,
|
|
294
|
+
clientSecret,
|
|
295
|
+
);
|
|
296
|
+
if (!client) {
|
|
297
|
+
return addCors(
|
|
298
|
+
jsonResponse(
|
|
299
|
+
{
|
|
300
|
+
error: "invalid_client",
|
|
301
|
+
error_description: "Invalid client credentials",
|
|
302
|
+
},
|
|
303
|
+
401,
|
|
304
|
+
),
|
|
305
|
+
);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// Generate token
|
|
309
|
+
const tokenString = `at_${Array.from({ length: 48 }, () => "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"[Math.floor(Math.random() * 62)]).join("")}`;
|
|
310
|
+
const token = {
|
|
311
|
+
token: tokenString,
|
|
312
|
+
clientId: client.clientId,
|
|
313
|
+
scopes: client.scopes,
|
|
314
|
+
issuedAt: Date.now(),
|
|
315
|
+
expiresAt: Date.now() + authConfig.tokenTtl * 1000,
|
|
316
|
+
};
|
|
317
|
+
await authConfig.store.storeToken(token);
|
|
318
|
+
|
|
319
|
+
// Standard OAuth2 response
|
|
320
|
+
return addCors(
|
|
321
|
+
jsonResponse({
|
|
322
|
+
access_token: token.token,
|
|
323
|
+
token_type: "bearer",
|
|
324
|
+
expires_in: authConfig.tokenTtl,
|
|
325
|
+
scope: client.scopes.join(" "),
|
|
326
|
+
}),
|
|
327
|
+
);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// POST /call - Execute agent action
|
|
331
|
+
if (path === "/call" && req.method === "POST") {
|
|
332
|
+
const body = (await req.json()) as CallAgentRequest;
|
|
333
|
+
|
|
334
|
+
if (!body.path || !body.action) {
|
|
335
|
+
return addCors(
|
|
336
|
+
jsonResponse(
|
|
337
|
+
{
|
|
338
|
+
success: false,
|
|
339
|
+
error: "Missing required fields: path, action",
|
|
340
|
+
code: "INVALID_REQUEST",
|
|
341
|
+
},
|
|
342
|
+
400,
|
|
343
|
+
),
|
|
344
|
+
);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// Inject auth context into request
|
|
348
|
+
if (auth) {
|
|
349
|
+
body.callerId = auth.callerId;
|
|
350
|
+
body.callerType = auth.callerType;
|
|
351
|
+
if (!body.metadata) body.metadata = {};
|
|
352
|
+
body.metadata.scopes = auth.scopes;
|
|
353
|
+
body.metadata.isRoot = auth.isRoot;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// Root key bypasses all access checks
|
|
357
|
+
if (auth?.isRoot) {
|
|
358
|
+
body.callerType = "system";
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
const result = await registry.call(body);
|
|
362
|
+
const status = "success" in result && result.success ? 200 : 400;
|
|
363
|
+
return addCors(jsonResponse(result, status));
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// GET /list - List agents (filtered by visibility)
|
|
367
|
+
if (path === "/list" && req.method === "GET") {
|
|
368
|
+
const agents = registry.list();
|
|
369
|
+
const visible = agents.filter((agent) => canSeeAgent(agent, auth));
|
|
370
|
+
|
|
371
|
+
return addCors(
|
|
372
|
+
jsonResponse({
|
|
373
|
+
success: true,
|
|
374
|
+
agents: visible.map((agent) => ({
|
|
375
|
+
path: agent.path,
|
|
376
|
+
name: agent.config?.name,
|
|
377
|
+
description: agent.config?.description,
|
|
378
|
+
supportedActions: agent.config?.supportedActions,
|
|
379
|
+
tools: agent.tools
|
|
380
|
+
.filter((t) => {
|
|
381
|
+
const tv = t.visibility ?? "internal";
|
|
382
|
+
if (auth?.isRoot) return true;
|
|
383
|
+
if (tv === "public") return true;
|
|
384
|
+
if (tv === "internal" && auth) return true;
|
|
385
|
+
return false;
|
|
386
|
+
})
|
|
387
|
+
.map((t) => t.name),
|
|
388
|
+
})),
|
|
389
|
+
}),
|
|
390
|
+
);
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
// Not found
|
|
394
|
+
if (onNotFound) {
|
|
395
|
+
return addCors(await onNotFound(req));
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
return addCors(
|
|
399
|
+
jsonResponse(
|
|
400
|
+
{
|
|
401
|
+
success: false,
|
|
402
|
+
error: `Not found: ${req.method} ${path}`,
|
|
403
|
+
code: "NOT_FOUND",
|
|
404
|
+
},
|
|
405
|
+
404,
|
|
406
|
+
),
|
|
407
|
+
);
|
|
408
|
+
} catch (err) {
|
|
409
|
+
return addCors(
|
|
410
|
+
jsonResponse(
|
|
411
|
+
{
|
|
412
|
+
success: false,
|
|
413
|
+
error: err instanceof Error ? err.message : String(err),
|
|
414
|
+
code: "INTERNAL_ERROR",
|
|
415
|
+
},
|
|
416
|
+
500,
|
|
417
|
+
),
|
|
418
|
+
);
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
const server: AgentServer = {
|
|
423
|
+
async start(): Promise<void> {
|
|
424
|
+
if (serverInstance) {
|
|
425
|
+
throw new Error("Server is already running");
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
serverInstance = Bun.serve({
|
|
429
|
+
port,
|
|
430
|
+
hostname,
|
|
431
|
+
fetch,
|
|
432
|
+
});
|
|
433
|
+
|
|
434
|
+
serverUrl = `http://${hostname}:${port}${basePath}`;
|
|
435
|
+
console.log(`Agent server running at ${serverUrl}`);
|
|
436
|
+
console.log(` POST ${basePath}/call - Execute agent actions`);
|
|
437
|
+
console.log(` GET ${basePath}/list - List agents`);
|
|
438
|
+
if (authConfig) {
|
|
439
|
+
console.log(` POST ${basePath}/oauth/token - OAuth2 token endpoint`);
|
|
440
|
+
console.log(" Auth: enabled (root key configured)");
|
|
441
|
+
}
|
|
442
|
+
},
|
|
443
|
+
|
|
444
|
+
async stop(): Promise<void> {
|
|
445
|
+
if (serverInstance) {
|
|
446
|
+
serverInstance.stop();
|
|
447
|
+
serverInstance = null;
|
|
448
|
+
serverUrl = null;
|
|
449
|
+
}
|
|
450
|
+
},
|
|
451
|
+
|
|
452
|
+
fetch,
|
|
453
|
+
|
|
454
|
+
get url(): string | null {
|
|
455
|
+
return serverUrl;
|
|
456
|
+
},
|
|
457
|
+
};
|
|
458
|
+
|
|
459
|
+
return server;
|
|
460
|
+
}
|