@slashfi/agents-sdk 0.7.0 → 0.9.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 (64) hide show
  1. package/dist/agent-definitions/auth.d.ts +17 -0
  2. package/dist/agent-definitions/auth.d.ts.map +1 -1
  3. package/dist/agent-definitions/auth.js +135 -1
  4. package/dist/agent-definitions/auth.js.map +1 -1
  5. package/dist/agent-definitions/integrations.d.ts +28 -12
  6. package/dist/agent-definitions/integrations.d.ts.map +1 -1
  7. package/dist/agent-definitions/integrations.js +239 -41
  8. package/dist/agent-definitions/integrations.js.map +1 -1
  9. package/dist/agent-definitions/remote-registry.d.ts +32 -0
  10. package/dist/agent-definitions/remote-registry.d.ts.map +1 -0
  11. package/dist/agent-definitions/remote-registry.js +460 -0
  12. package/dist/agent-definitions/remote-registry.js.map +1 -0
  13. package/dist/index.d.ts +12 -5
  14. package/dist/index.d.ts.map +1 -1
  15. package/dist/index.js +8 -2
  16. package/dist/index.js.map +1 -1
  17. package/dist/integration-interface.d.ts +37 -0
  18. package/dist/integration-interface.d.ts.map +1 -0
  19. package/dist/integration-interface.js +94 -0
  20. package/dist/integration-interface.js.map +1 -0
  21. package/dist/integrations-store.d.ts +33 -0
  22. package/dist/integrations-store.d.ts.map +1 -0
  23. package/dist/integrations-store.js +50 -0
  24. package/dist/integrations-store.js.map +1 -0
  25. package/dist/jwt.d.ts +86 -17
  26. package/dist/jwt.d.ts.map +1 -1
  27. package/dist/jwt.js +140 -17
  28. package/dist/jwt.js.map +1 -1
  29. package/dist/registry.d.ts +7 -0
  30. package/dist/registry.d.ts.map +1 -1
  31. package/dist/registry.js +14 -21
  32. package/dist/registry.js.map +1 -1
  33. package/dist/secret-collection.d.ts +37 -0
  34. package/dist/secret-collection.d.ts.map +1 -0
  35. package/dist/secret-collection.js +37 -0
  36. package/dist/secret-collection.js.map +1 -0
  37. package/dist/server.d.ts +41 -42
  38. package/dist/server.d.ts.map +1 -1
  39. package/dist/server.js +232 -555
  40. package/dist/server.js.map +1 -1
  41. package/dist/types.d.ts +24 -2
  42. package/dist/types.d.ts.map +1 -1
  43. package/package.json +5 -2
  44. package/src/agent-definitions/auth.ts +187 -1
  45. package/src/agent-definitions/integrations.ts +287 -55
  46. package/src/agent-definitions/remote-registry.ts +621 -0
  47. package/src/index.ts +22 -5
  48. package/src/integration-interface.ts +118 -0
  49. package/src/integrations-store.ts +84 -0
  50. package/src/jwt.ts +233 -65
  51. package/src/registry.ts +23 -2
  52. package/src/secret-collection.ts +66 -0
  53. package/src/server.ts +268 -647
  54. package/src/types.ts +28 -2
  55. package/dist/slack-oauth.d.ts +0 -27
  56. package/dist/slack-oauth.d.ts.map +0 -1
  57. package/dist/slack-oauth.js +0 -48
  58. package/dist/slack-oauth.js.map +0 -1
  59. package/dist/web-pages.d.ts +0 -8
  60. package/dist/web-pages.d.ts.map +0 -1
  61. package/dist/web-pages.js +0 -169
  62. package/dist/web-pages.js.map +0 -1
  63. package/src/slack-oauth.ts +0 -66
  64. package/src/web-pages.ts +0 -178
package/dist/server.js CHANGED
@@ -1,73 +1,34 @@
1
1
  /**
2
2
  * Agent Server (MCP over HTTP)
3
3
  *
4
- * JSON-RPC server implementing the MCP protocol for agent interaction.
5
- * Compatible with atlas-environments and any MCP client.
4
+ * Minimal JSON-RPC server implementing the MCP protocol for agent interaction.
5
+ * Handles only core SDK concerns:
6
+ * - MCP protocol (initialize, tools/list, tools/call)
7
+ * - Agent registry routing (call_agent, list_agents)
8
+ * - Auth resolution (Bearer tokens, root key, JWT)
9
+ * - OAuth2 token exchange (client_credentials)
10
+ * - Health check
11
+ * - CORS
6
12
  *
7
- * MCP Methods:
8
- * - initialize → Protocol handshake
9
- * - tools/list → List available MCP tools (call_agent, list_agents)
10
- * - tools/call → Execute an MCP tool
13
+ * Application-specific routes (web UI, OAuth callbacks, tenant management)
14
+ * should be built on top using the exported `fetch` handler.
11
15
  *
12
- * MCP Tools exposed:
13
- * - call_agent → Execute a tool on a registered agent
14
- * - list_agents → List registered agents and their tools
16
+ * @example
17
+ * ```typescript
18
+ * // Standalone usage
19
+ * const server = createAgentServer(registry, { port: 3000 });
20
+ * await server.start();
15
21
  *
16
- * Additional endpoints:
17
- * - POST /oauth/token → OAuth2 client_credentials (when @auth registered)
18
- * - GET /health → Health check
19
- *
20
- * Auth Integration:
21
- * When an `@auth` agent is registered, the server automatically:
22
- * - Validates Bearer tokens on requests
23
- * - Resolves tokens to identity + scopes
24
- * - Populates caller context from headers (X-Atlas-Actor-Id, etc.)
25
- * - Recognizes the root key for admin access
22
+ * // Composable with any HTTP framework
23
+ * const server = createAgentServer(registry);
24
+ * app.all('/mcp/*', (req) => server.fetch(req));
25
+ * ```
26
26
  */
27
27
  import { processSecretParams, } from "./agent-definitions/secrets.js";
28
28
  import { verifyJwt } from "./jwt.js";
29
- import { renderLoginPage, renderDashboardPage, renderTenantPage } from "./web-pages.js";
30
- import { slackAuthUrl, exchangeSlackCode, getSlackProfile } from "./slack-oauth.js";
31
- function resolveBaseUrl(req, url) {
32
- const proto = req.headers.get("x-forwarded-proto") || url.protocol.replace(":", "");
33
- const host = req.headers.get("x-forwarded-host") || url.host;
34
- return `${proto}://${host}`;
35
- }
29
+ import { generateSigningKey, importSigningKey, exportSigningKey, buildJwks, verifyJwtLocal, verifyJwtFromIssuer } from "./jwt.js";
36
30
  // ============================================
37
- // Secrets Collection (one-time tokens)
38
- // ============================================
39
- function escHtml(s) {
40
- return s.replace(/&/g, "\&amp;").replace(/</g, "\&lt;").replace(/>/g, "\&gt;").replace(/"/g, "\&quot;");
41
- }
42
- function renderSecretForm(token, pending, baseUrl) {
43
- const fields = pending.fields.map(f => `
44
- <div class="field">
45
- <label>${escHtml(f.name)}${f.secret ? ` <span class="badge">SECRET</span>` : ""}${f.required ? ` <span class="req">*</span>` : ""}</label>
46
- ${f.description ? `<p class="desc">${escHtml(f.description)}</p>` : ""}
47
- <input type="${f.secret ? "password" : "text"}" name="${escHtml(f.name)}" ${f.required ? "required" : ""} autocomplete="off" />
48
- </div>`).join("");
49
- return `<!DOCTYPE html>
50
- <html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Secure Setup</title>
51
- <style>*{box-sizing:border-box;margin:0;padding:0}body{font-family:-apple-system,BlinkMacSystemFont,sans-serif;background:#0d1117;color:#c9d1d9;min-height:100vh;display:flex;align-items:center;justify-content:center}.card{background:#161b22;border:1px solid #30363d;border-radius:12px;padding:32px;max-width:480px;width:100%}.header{display:flex;align-items:center;gap:12px;margin-bottom:8px}.lock{font-size:24px}h1{font-size:20px;font-weight:600}.subtitle{color:#8b949e;font-size:14px;margin-bottom:24px}.shield{display:inline-flex;align-items:center;gap:4px;background:#1a2332;border:1px solid #1f6feb33;color:#58a6ff;font-size:12px;padding:2px 8px;border-radius:12px;margin-bottom:20px}label{display:block;font-size:14px;font-weight:500;margin-bottom:6px}.desc{font-size:12px;color:#8b949e;margin-bottom:4px}.badge{background:#3d1f00;color:#f0883e;font-size:10px;padding:1px 6px;border-radius:4px}.req{color:#f85149}input{width:100%;padding:10px 12px;background:#0d1117;border:1px solid #30363d;border-radius:6px;color:#c9d1d9;font-size:14px;margin-bottom:16px;outline:none}input:focus{border-color:#58a6ff;box-shadow:0 0 0 3px #1f6feb33}button{width:100%;padding:12px;background:#238636;border:none;border-radius:6px;color:#fff;font-size:14px;font-weight:600;cursor:pointer}button:hover{background:#2ea043}button:disabled{opacity:.5;cursor:not-allowed}.footer{text-align:center;margin-top:16px;font-size:12px;color:#484f58}.error{background:#3d1418;border:1px solid #f8514966;color:#f85149;padding:10px 12px;border-radius:6px;font-size:13px;margin-bottom:16px;display:none}.ok{text-align:center;padding:40px 0}.ok .icon{font-size:48px;margin-bottom:12px}.ok h2{font-size:18px;margin-bottom:8px;color:#3fb950}.ok p{color:#8b949e;font-size:14px}.field{position:relative}</style></head><body>
52
- <div class="card" id="fc"><div class="header"><span class="lock">🔐</span><h1>${escHtml(pending.tool)} on ${escHtml(pending.agent)}</h1></div>
53
- <p class="subtitle">Enter credentials below. They are encrypted and stored securely — they never pass through the AI.</p>
54
- <div class="shield">🛡️ End-to-end encrypted</div><div id="err" class="error"></div>
55
- <form id="f">${fields}<button type="submit">Submit Securely</button></form>
56
- <p class="footer">Expires in 10 minutes</p></div>
57
- <div class="card ok" id="ok" style="display:none"><div class="icon">✅</div><h2>Done</h2><p>Credentials stored securely. You can close this window.</p></div>
58
- <script>document.getElementById("f").addEventListener("submit",async e=>{e.preventDefault();const b=e.target.querySelector("button");b.disabled=true;b.textContent="Submitting...";try{const fd=new FormData(e.target),vals=Object.fromEntries(fd.entries());const r=await fetch("${baseUrl}/secrets/collect",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({token:"${token}",values:vals})});const d=await r.json();if(d.success){document.getElementById("fc").style.display="none";document.getElementById("ok").style.display="block";}else throw new Error(d.error?.message||JSON.stringify(d));}catch(err){const el=document.getElementById("err");el.textContent=err.message;el.style.display="block";b.disabled=false;b.textContent="Submit Securely";}});</script></body></html>`;
59
- }
60
- export const pendingCollections = new Map();
61
- export function generateCollectionToken() {
62
- const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
63
- let token = "sc_";
64
- for (let i = 0; i < 32; i++) {
65
- token += chars[Math.floor(Math.random() * chars.length)];
66
- }
67
- return token;
68
- }
69
- // ============================================
70
- // Helpers
31
+ // HTTP Helpers
71
32
  // ============================================
72
33
  function jsonResponse(data, status = 200) {
73
34
  return new Response(JSON.stringify(data), {
@@ -79,9 +40,19 @@ function corsHeaders() {
79
40
  return {
80
41
  "Access-Control-Allow-Origin": "*",
81
42
  "Access-Control-Allow-Methods": "GET, POST, OPTIONS",
82
- "Access-Control-Allow-Headers": "Content-Type, Authorization, X-Atlas-Actor-Id, X-Atlas-Agent-Id, X-Atlas-Session-Id",
43
+ "Access-Control-Allow-Headers": "Content-Type, Authorization, X-Atlas-Actor-Id, X-Atlas-Actor-Type",
83
44
  };
84
45
  }
46
+ function addCors(res) {
47
+ const headers = new Headers(res.headers);
48
+ for (const [k, v] of Object.entries(corsHeaders())) {
49
+ headers.set(k, v);
50
+ }
51
+ return new Response(res.body, { status: res.status, headers });
52
+ }
53
+ // ============================================
54
+ // JSON-RPC Helpers
55
+ // ============================================
85
56
  function jsonRpcSuccess(id, result) {
86
57
  return { jsonrpc: "2.0", id, result };
87
58
  }
@@ -105,9 +76,9 @@ function mcpResult(value, isError = false) {
105
76
  };
106
77
  }
107
78
  // ============================================
108
- // Auth Detection
79
+ // Auth Detection & Resolution
109
80
  // ============================================
110
- function detectAuth(registry) {
81
+ export function detectAuth(registry) {
111
82
  const authAgent = registry.get("@auth");
112
83
  if (!authAgent?.__authStore || !authAgent.__rootKey)
113
84
  return null;
@@ -117,13 +88,14 @@ function detectAuth(registry) {
117
88
  tokenTtl: authAgent.__tokenTtl ?? 3600,
118
89
  };
119
90
  }
120
- async function resolveAuth(req, authConfig) {
91
+ export async function resolveAuth(req, authConfig, jwksOptions) {
121
92
  const authHeader = req.headers.get("Authorization");
122
93
  if (!authHeader)
123
94
  return null;
124
95
  const [scheme, credential] = authHeader.split(" ", 2);
125
96
  if (scheme?.toLowerCase() !== "bearer" || !credential)
126
97
  return null;
98
+ // Root key check
127
99
  if (credential === authConfig.rootKey) {
128
100
  return {
129
101
  callerId: "root",
@@ -132,18 +104,52 @@ async function resolveAuth(req, authConfig) {
132
104
  isRoot: true,
133
105
  };
134
106
  }
135
- // Try JWT verification first (stateless)
136
- // JWT is signed with the client's secret hash
137
- // Decode payload to get client_id, look up client, verify signature
107
+ // Try ES256 verification against own signing keys
138
108
  const parts = credential.split(".");
109
+ if (parts.length === 3 && jwksOptions?.signingKeys?.length) {
110
+ for (const key of jwksOptions.signingKeys) {
111
+ try {
112
+ const verified = await verifyJwtLocal(credential, key.publicKey);
113
+ if (verified) {
114
+ return {
115
+ callerId: verified.sub ?? verified.name ?? "unknown",
116
+ callerType: "agent",
117
+ scopes: verified.scopes ?? ["*"],
118
+ isRoot: false,
119
+ };
120
+ }
121
+ }
122
+ catch {
123
+ continue;
124
+ }
125
+ }
126
+ }
127
+ // Try trusted issuers (remote JWKS verification)
128
+ if (parts.length === 3 && jwksOptions?.trustedIssuers?.length) {
129
+ for (const issuer of jwksOptions.trustedIssuers) {
130
+ try {
131
+ const verified = await verifyJwtFromIssuer(credential, issuer);
132
+ if (verified) {
133
+ return {
134
+ callerId: verified.sub ?? verified.name ?? "unknown",
135
+ callerType: "agent",
136
+ scopes: verified.scopes ?? ["*"],
137
+ isRoot: false,
138
+ };
139
+ }
140
+ }
141
+ catch {
142
+ continue;
143
+ }
144
+ }
145
+ }
146
+ // Try HMAC JWT verification (legacy, stateless)
139
147
  if (parts.length === 3) {
140
- // Looks like a JWT - decode payload to get client_id
141
148
  try {
142
149
  const payloadB64 = parts[1];
143
150
  const padded = payloadB64.replace(/-/g, "+").replace(/_/g, "/");
144
151
  const payload = JSON.parse(atob(padded));
145
152
  if (payload.sub) {
146
- // Look up client to get the signing secret (secret hash)
147
153
  const client = await authConfig.store.getClient(payload.sub);
148
154
  if (client) {
149
155
  const verified = await verifyJwt(credential, client.clientSecretHash);
@@ -174,7 +180,7 @@ async function resolveAuth(req, authConfig) {
174
180
  isRoot: false,
175
181
  };
176
182
  }
177
- function canSeeAgent(agent, auth) {
183
+ export function canSeeAgent(agent, auth) {
178
184
  const visibility = (agent.visibility ??
179
185
  agent.config?.visibility ??
180
186
  "internal");
@@ -208,11 +214,11 @@ function getToolDefinitions() {
208
214
  },
209
215
  path: {
210
216
  type: "string",
211
- description: "Agent path (e.g. '@registry')",
217
+ description: "Agent path (e.g., '@my-agent')",
212
218
  },
213
219
  tool: {
214
220
  type: "string",
215
- description: "Tool name to call (for execute_tool)",
221
+ description: "Tool name to call",
216
222
  },
217
223
  params: {
218
224
  type: "object",
@@ -241,29 +247,28 @@ function getToolDefinitions() {
241
247
  // ============================================
242
248
  export function createAgentServer(registry, options = {}) {
243
249
  const { port = 3000, hostname = "localhost", basePath = "", cors = true, serverName = "agents-sdk", serverVersion = "1.0.0", secretStore, } = options;
244
- let serverInstance = null;
245
- let serverUrl = null;
250
+ // Signing keys for JWKS-based auth
251
+ const serverSigningKeys = [];
252
+ const configTrustedIssuers = options.trustedIssuers ?? [];
246
253
  const authConfig = detectAuth(registry);
254
+ let serverInstance = null;
247
255
  // ──────────────────────────────────────────
248
- // MCP JSON-RPC handler
256
+ // JSON-RPC handler
249
257
  // ──────────────────────────────────────────
250
258
  async function handleJsonRpc(request, auth) {
251
259
  switch (request.method) {
252
- // MCP protocol handshake
253
260
  case "initialize":
254
261
  return jsonRpcSuccess(request.id, {
255
262
  protocolVersion: "2024-11-05",
256
- capabilities: { tools: {} },
263
+ capabilities: { tools: { listChanged: false } },
257
264
  serverInfo: { name: serverName, version: serverVersion },
258
265
  });
259
266
  case "notifications/initialized":
260
267
  return jsonRpcSuccess(request.id, {});
261
- // List MCP tools
262
268
  case "tools/list":
263
269
  return jsonRpcSuccess(request.id, {
264
270
  tools: getToolDefinitions(),
265
271
  });
266
- // Call an MCP tool
267
272
  case "tools/call": {
268
273
  const { name, arguments: args } = (request.params ?? {});
269
274
  try {
@@ -271,7 +276,7 @@ export function createAgentServer(registry, options = {}) {
271
276
  return jsonRpcSuccess(request.id, result);
272
277
  }
273
278
  catch (err) {
274
- console.error("[server] Request error:", err);
279
+ console.error("[server] Tool call error:", err);
275
280
  return jsonRpcSuccess(request.id, mcpResult(`Error: ${err instanceof Error ? err.message : String(err)}`, true));
276
281
  }
277
282
  }
@@ -299,10 +304,8 @@ export function createAgentServer(registry, options = {}) {
299
304
  req.callerType = "system";
300
305
  }
301
306
  // Process secret params: resolve refs, store raw secrets
302
- // Auto-resolve secret:xxx refs in tool params before execution
303
307
  if (req.params && secretStore) {
304
308
  const ownerId = auth?.callerId ?? "anonymous";
305
- // Find the tool schema to check for secret: true fields
306
309
  const agent = registry.get(req.path);
307
310
  const tool = agent?.tools.find((t) => t.name === req.tool);
308
311
  const schema = tool?.inputSchema;
@@ -330,6 +333,8 @@ export function createAgentServer(registry, options = {}) {
330
333
  return true;
331
334
  if (tv === "public")
332
335
  return true;
336
+ if (tv === "authenticated" && auth?.callerId && auth.callerId !== "anonymous")
337
+ return true;
333
338
  if (tv === "internal" && auth)
334
339
  return true;
335
340
  return false;
@@ -343,7 +348,7 @@ export function createAgentServer(registry, options = {}) {
343
348
  }
344
349
  }
345
350
  // ──────────────────────────────────────────
346
- // OAuth2 token handler (unchanged)
351
+ // OAuth2 token handler
347
352
  // ──────────────────────────────────────────
348
353
  async function handleOAuthToken(req) {
349
354
  if (!authConfig) {
@@ -378,521 +383,193 @@ export function createAgentServer(registry, options = {}) {
378
383
  error_description: "Missing client_id or client_secret",
379
384
  }, 400);
380
385
  }
381
- const client = await authConfig.store.validateClient(clientId, clientSecret);
382
- if (!client) {
386
+ try {
387
+ const result = await registry.call({
388
+ action: "execute_tool",
389
+ path: "@auth",
390
+ tool: "token",
391
+ params: { clientId, clientSecret },
392
+ callerType: "system",
393
+ });
394
+ const tokenResult = result?.result;
395
+ if (!tokenResult?.accessToken) {
396
+ return jsonResponse({ error: "invalid_client", error_description: "Authentication failed" }, 401);
397
+ }
383
398
  return jsonResponse({
384
- error: "invalid_client",
385
- error_description: "Invalid client credentials",
386
- }, 401);
399
+ access_token: tokenResult.accessToken,
400
+ token_type: "Bearer",
401
+ expires_in: tokenResult.expiresIn ?? authConfig.tokenTtl,
402
+ refresh_token: tokenResult.refreshToken,
403
+ });
387
404
  }
388
- // Delegate to @auth agent's token tool which generates proper JWTs
389
- const tokenResult = await registry.call({
390
- action: "execute_tool",
391
- path: "@auth",
392
- tool: "token",
393
- params: {
394
- grantType: "client_credentials",
395
- clientId,
396
- clientSecret,
397
- },
398
- context: {
399
- tenantId: "default",
400
- agentPath: "@auth",
401
- callerId: "oauth_endpoint",
402
- callerType: "system",
403
- },
404
- });
405
- // Extract the result - registry.call returns { success, result: { accessToken, tokenType, expiresIn, scopes } }
406
- const callResponse = tokenResult;
407
- if (!callResponse.success) {
408
- return jsonResponse({ error: "token_generation_failed", error_description: callResponse.error ?? "Unknown error" }, 500);
405
+ catch (err) {
406
+ console.error("[oauth] Token error:", err);
407
+ return jsonResponse({ error: "server_error", error_description: "Token exchange failed" }, 500);
409
408
  }
410
- const tokenData = callResponse.result;
411
- // accessToken may be wrapped as { $agent_type: "secret", value: "<jwt>" }
412
- const accessToken = tokenData.accessToken?.$agent_type === "secret"
413
- ? tokenData.accessToken.value
414
- : tokenData.accessToken;
415
- return jsonResponse({
416
- access_token: accessToken,
417
- token_type: tokenData.tokenType ?? "Bearer",
418
- expires_in: tokenData.expiresIn ?? authConfig.tokenTtl,
419
- scope: Array.isArray(tokenData.scopes) ? tokenData.scopes.join(" ") : client.scopes.join(" "),
420
- });
421
409
  }
422
410
  // ──────────────────────────────────────────
423
- // HTTP request handler
424
- // ─��────────────────────────────────────────
411
+ // Main fetch handler
412
+ // ──────────────────────────────────────────
425
413
  async function fetch(req) {
426
- const url = new URL(req.url);
427
- const path = url.pathname.replace(basePath, "") || "/";
428
- // CORS preflight
429
- if (cors && req.method === "OPTIONS") {
430
- return new Response(null, { status: 204, headers: corsHeaders() });
431
- }
432
- const addCors = (response) => {
433
- if (!cors)
434
- return response;
435
- const headers = new Headers(response.headers);
436
- for (const [key, value] of Object.entries(corsHeaders())) {
437
- headers.set(key, value);
438
- }
439
- return new Response(response.body, {
440
- status: response.status,
441
- statusText: response.statusText,
442
- headers,
443
- });
444
- };
445
- const auth = authConfig ? await resolveAuth(req, authConfig) : null;
446
414
  try {
447
- // MCP endpoint: POST / or POST /mcp
448
- if ((path === "/" || path === "/mcp") && req.method === "POST") {
449
- const body = (await req.json());
450
- const response = await handleJsonRpc(body, auth);
451
- return addCors(jsonResponse(response));
452
- }
453
- // OAuth2 token endpoint
454
- if (path === "/oauth/token" && req.method === "POST") {
455
- return addCors(await handleOAuthToken(req));
456
- }
457
- // Health check
458
- if (path === "/health" && req.method === "GET") {
459
- return addCors(jsonResponse({ status: "ok" }));
460
- }
461
- // Backwards compat: GET /list (returns agents directly)
462
- if (path === "/list" && req.method === "GET") {
463
- const agents = registry.list();
464
- const visible = agents.filter((agent) => canSeeAgent(agent, auth));
465
- return addCors(jsonResponse({
466
- success: true,
467
- agents: visible.map((agent) => ({
468
- path: agent.path,
469
- name: agent.config?.name,
470
- description: agent.config?.description,
471
- supportedActions: agent.config?.supportedActions,
472
- integration: agent.config?.integration || null,
473
- tools: agent.tools
474
- .filter((t) => {
475
- const tv = t.visibility ?? "internal";
476
- if (auth?.isRoot)
477
- return true;
478
- if (tv === "public")
479
- return true;
480
- if (tv === "internal" && auth)
481
- return true;
482
- return false;
483
- })
484
- .map((t) => t.name),
485
- })),
486
- }));
415
+ const url = new URL(req.url);
416
+ const path = url.pathname.replace(basePath, "") || "/";
417
+ // CORS preflight
418
+ if (cors && req.method === "OPTIONS") {
419
+ return new Response(null, { status: 204, headers: corsHeaders() });
487
420
  }
488
- // GET /integrations/callback/:provider - OAuth callback
489
- if (path.startsWith("/integrations/callback/") && req.method === "GET") {
490
- const provider = path.split("/integrations/callback/")[1]?.split("?")[0];
491
- if (!provider) {
492
- return addCors(jsonResponse({ error: "Missing provider" }, 400));
493
- }
494
- const url = new URL(req.url);
495
- const code = url.searchParams.get("code");
496
- const state = url.searchParams.get("state");
497
- const oauthError = url.searchParams.get("error");
498
- const errorDescription = url.searchParams.get("error_description");
499
- if (oauthError) {
500
- return new Response(`<html><body><h1>Authorization Failed</h1><p>${errorDescription ?? oauthError}</p></body></html>`, { status: 400, headers: { "Content-Type": "text/html", ...corsHeaders() } });
501
- }
502
- if (!code) {
503
- return addCors(jsonResponse({ error: "Missing authorization code" }, 400));
504
- }
505
- // Call handle_oauth_callback tool on @integrations
506
- try {
507
- await registry.call({
508
- action: "execute_tool",
509
- path: "@integrations",
510
- tool: "handle_oauth_callback",
511
- params: { provider, code, state: state ?? undefined },
512
- context: {
513
- tenantId: "default",
514
- agentPath: "@integrations",
515
- callerId: "oauth_callback",
516
- callerType: "system",
517
- },
518
- });
519
- // Parse redirect URL from state
520
- let redirectUrl = "/";
521
- if (state) {
522
- try {
523
- const parsed = JSON.parse(state);
524
- if (parsed.redirectUrl)
525
- redirectUrl = parsed.redirectUrl;
526
- }
527
- catch { }
528
- }
529
- const sep = redirectUrl.includes("?") ? "&" : "?";
530
- return Response.redirect(`${redirectUrl}${sep}connected=${provider}`, 302);
531
- }
532
- catch (err) {
533
- return new Response(`<html><body><h1>Connection Failed</h1><p>${err instanceof Error ? err.message : String(err)}</p></body></html>`, { status: 500, headers: { "Content-Type": "text/html", ...corsHeaders() } });
534
- }
535
- }
536
- // GET /secrets/form/:token - Serve hosted secrets form
537
- if (path.startsWith("/secrets/form/") && req.method === "GET") {
538
- const token = path.split("/").pop() ?? "";
539
- const pending = pendingCollections.get(token);
540
- if (!pending) {
541
- return addCors(new Response("Invalid or expired form link", { status: 404 }));
542
- }
543
- if (Date.now() - pending.createdAt > 10 * 60 * 1000) {
544
- pendingCollections.delete(token);
545
- return addCors(new Response("Form link expired", { status: 410 }));
546
- }
547
- const reqUrl = new URL(req.url);
548
- const baseUrl = resolveBaseUrl(req, reqUrl);
549
- const html = renderSecretForm(token, pending, baseUrl);
550
- return addCors(new Response(html, { headers: { "Content-Type": "text/html" } }));
551
- }
552
- // POST /secrets/collect - Submit collected secrets and auto-forward to tool
553
- if (path === "/secrets/collect" && req.method === "POST") {
554
- const body = (await req.json());
555
- const pending = pendingCollections.get(body.token);
556
- if (!pending) {
557
- return addCors(jsonResponse({ error: "Invalid or expired collection token" }, 400));
558
- }
559
- // One-time use
560
- pendingCollections.delete(body.token);
561
- // Check expiry (10 min)
562
- if (Date.now() - pending.createdAt > 10 * 60 * 1000) {
563
- return addCors(jsonResponse({ error: "Collection token expired" }, 400));
564
- }
565
- // Encrypt secret values and store as refs
566
- const mergedParams = { ...pending.params };
567
- for (const [fieldName, value] of Object.entries(body.values)) {
568
- const fieldDef = pending.fields.find((f) => f.name === fieldName);
569
- if (fieldDef?.secret && secretStore) {
570
- // Store encrypted, get ref
571
- const ownerId = pending.auth?.callerId ?? "anonymous";
572
- const secretId = await secretStore.store(value, ownerId);
573
- mergedParams[fieldName] = `secret:${secretId}`;
574
- }
575
- else {
576
- mergedParams[fieldName] = value;
421
+ // Resolve auth for all requests
422
+ const auth = authConfig ? await resolveAuth(req, authConfig, {
423
+ signingKeys: serverSigningKeys,
424
+ trustedIssuers: configTrustedIssuers,
425
+ }) : null;
426
+ // Also check header-based identity (for proxied requests)
427
+ const headerAuth = !auth
428
+ ? (() => {
429
+ const actorId = req.headers.get("X-Atlas-Actor-Id");
430
+ const actorType = req.headers.get("X-Atlas-Actor-Type");
431
+ if (actorId) {
432
+ return {
433
+ callerId: actorId,
434
+ callerType: actorType ?? "agent",
435
+ scopes: ["*"],
436
+ isRoot: false,
437
+ };
577
438
  }
578
- }
579
- // Auto-forward to the target tool
580
- const callRequest = {
581
- action: "execute_tool",
582
- path: pending.agent,
583
- tool: pending.tool,
584
- params: mergedParams,
585
- };
586
- const toolCtx = {
587
- tenantId: "default",
588
- agentPath: pending.agent,
589
- callerId: pending.auth?.callerId ?? "anonymous",
590
- callerType: pending.auth?.callerType ?? "system",
591
- };
592
- const result = await registry.call({
593
- ...callRequest,
594
- context: toolCtx,
595
- });
596
- return addCors(jsonResponse({ success: true, result }));
597
- }
598
- // --- Web pages (plain HTML, served from same server) ---
599
- const htmlRes = (body) => addCors(new Response(body, { headers: { "Content-Type": "text/html; charset=utf-8" } }));
600
- const reqUrl = new URL(req.url);
601
- const baseUrl = resolveBaseUrl(req, reqUrl);
602
- const slackConfig = process.env.SLACK_CLIENT_ID && process.env.SLACK_CLIENT_SECRET
603
- ? {
604
- clientId: process.env.SLACK_CLIENT_ID,
605
- clientSecret: process.env.SLACK_CLIENT_SECRET,
606
- redirectUri: `${baseUrl}/auth/slack/callback`,
607
- }
608
- : null;
609
- // Helper: read session from cookie
610
- function getSession(r) {
611
- const c = r.headers.get("Cookie") || "";
612
- const m = c.match(/s_session=([^;]+)/);
613
- if (!m)
614
439
  return null;
615
- try {
616
- return JSON.parse(Buffer.from(m[1], "base64url").toString());
617
- }
618
- catch {
619
- return null;
620
- }
621
- }
622
- // Helper: generate JWT from client credentials
623
- async function generateMcpToken() {
624
- const clientRes = await registry.call({ action: "execute_tool", path: "@auth", tool: "create_client", callerType: "system", params: {
625
- name: "mcp-" + Date.now(),
626
- scopes: ["*"],
627
- } });
628
- const cid = clientRes?.result?.clientId;
629
- const csec = clientRes?.result?.clientSecret;
630
- if (!cid || !csec)
631
- throw new Error("Failed to create client: " + JSON.stringify(clientRes));
632
- const tokenRes = await globalThis.fetch(`http://localhost:${port}/oauth/token`, {
633
- method: "POST",
634
- headers: { "Content-Type": "application/x-www-form-urlencoded" },
635
- body: new URLSearchParams({ grant_type: "client_credentials", client_id: cid, client_secret: csec }),
636
- });
637
- const tokenData = await tokenRes.json();
638
- if (!tokenData.access_token)
639
- throw new Error("Failed to get JWT: " + JSON.stringify(tokenData));
640
- return tokenData.access_token;
641
- }
642
- // Helper: set session cookie and redirect
643
- function sessionRedirect(location, session) {
644
- const data = Buffer.from(JSON.stringify(session)).toString("base64url");
645
- return new Response(null, {
646
- status: 302,
647
- headers: {
648
- Location: location,
649
- "Set-Cookie": `s_session=${data}; Path=/; HttpOnly; SameSite=Lax; Max-Age=604800`,
650
- },
651
- });
652
- }
653
- // GET / — login page (or redirect to dashboard if session exists)
654
- if (path === "/" && req.method === "GET") {
655
- const session = getSession(req);
656
- if (session?.token)
657
- return Response.redirect(`${baseUrl}/dashboard`, 302);
658
- return htmlRes(renderLoginPage(baseUrl, !!slackConfig));
440
+ })()
441
+ : null;
442
+ const effectiveAuth = auth ?? headerAuth;
443
+ // ── POST / → MCP JSON-RPC ──
444
+ if (path === "/" && req.method === "POST") {
445
+ const body = (await req.json());
446
+ const result = await handleJsonRpc(body, effectiveAuth);
447
+ return cors ? addCors(jsonResponse(result)) : jsonResponse(result);
659
448
  }
660
- // GET /auth/slack start Slack OAuth
661
- if (path === "/auth/slack" && req.method === "GET") {
662
- if (!slackConfig)
663
- return htmlRes("<h1>Slack OAuth not configured</h1>");
664
- return Response.redirect(slackAuthUrl(slackConfig), 302);
449
+ // ── POST /oauth/token OAuth2 client_credentials ──
450
+ if (path === "/oauth/token" && req.method === "POST") {
451
+ const res = await handleOAuthToken(req);
452
+ return cors ? addCors(res) : res;
665
453
  }
666
- // GET /auth/slack/callback handle Slack OAuth callback
667
- if (path === "/auth/slack/callback" && req.method === "GET") {
668
- if (!slackConfig)
669
- return htmlRes("<h1>Slack OAuth not configured</h1>");
670
- const authCode = reqUrl.searchParams.get("code");
671
- const authError = reqUrl.searchParams.get("error");
672
- if (authError || !authCode)
673
- return Response.redirect(`${baseUrl}/?error=${authError || "no_code"}`, 302);
674
- try {
675
- const tokens = await exchangeSlackCode(authCode, slackConfig);
676
- const profile = await getSlackProfile(tokens.access_token);
677
- const teamId = profile["https://slack.com/team_id"] || "";
678
- const teamName = profile["https://slack.com/team_name"] || "";
679
- // Check if user already exists
680
- console.log("[auth] Looking up slack user:", profile.sub, profile.email);
681
- const existing = await registry.call({
682
- action: "execute_tool", path: "@users", callerType: "system", tool: "resolve_identity",
683
- params: { provider: "slack", providerUserId: profile.sub },
684
- });
685
- console.log("[auth] resolve_identity:", JSON.stringify(existing));
686
- if (existing?.result?.found && existing?.result?.user?.tenantId) {
687
- // Returning user — generate token and go to dashboard
688
- console.log("[auth] Returning user, generating token...");
689
- const mcpToken = await generateMcpToken();
690
- return sessionRedirect(`${baseUrl}/dashboard`, {
691
- userId: existing.result.user.id,
692
- tenantId: existing.result.user.tenantId,
693
- email: existing.result.user.email,
694
- name: existing.result.user.name,
695
- token: mcpToken,
696
- });
697
- }
698
- // Check if Slack team already has a tenant
699
- if (teamId) {
700
- console.log("[auth] Checking tenant_identities for team:", teamId);
701
- try {
702
- // Direct DB query via the auth store's underlying connection
703
- // We'll use a simple fetch to our own MCP endpoint to call @db-connections
704
- // Actually, simpler: just query the DB directly via the server's context
705
- // For now, use registry.call to a custom tool or direct SQL
706
- // Simplest: call @auth list_tenants and check metadata
707
- // Even simpler: direct SQL via globalThis.fetch to ourselves
708
- const dbUrl = process.env.DATABASE_URL;
709
- if (dbUrl) {
710
- const { default: postgres } = await import("postgres");
711
- const sql = postgres(dbUrl);
712
- const rows = await sql `SELECT tenant_id FROM tenant_identities WHERE provider = 'slack' AND provider_org_id = ${teamId} LIMIT 1`;
713
- await sql.end();
714
- if (rows.length > 0) {
715
- const existingTenantId = rows[0].tenant_id;
716
- console.log("[auth] Found existing tenant for team:", existingTenantId);
717
- // Create user on existing tenant
718
- const userRes = await registry.call({ action: "execute_tool", path: "@users", callerType: "system", tool: "create_user", params: {
719
- email: profile.email, name: profile.name, tenantId: existingTenantId,
720
- } });
721
- const newUserId = userRes?.result?.id || userRes?.result?.user?.id;
722
- console.log("[auth] Created user on existing tenant:", newUserId);
723
- // Link identity
724
- if (newUserId) {
725
- await registry.call({ action: "execute_tool", path: "@users", callerType: "system", tool: "link_identity", params: {
726
- userId: newUserId, provider: "slack", providerUserId: profile.sub,
727
- email: profile.email, name: profile.name,
728
- metadata: { slackTeamId: teamId, slackTeamName: teamName },
729
- } });
730
- }
731
- // Generate token and go to dashboard
732
- const mcpToken = await generateMcpToken();
733
- return sessionRedirect(`${baseUrl}/dashboard`, {
734
- userId: newUserId, tenantId: existingTenantId,
735
- email: profile.email, name: profile.name, token: mcpToken,
736
- });
737
- }
738
- }
739
- }
740
- catch (e) {
741
- console.error("[auth] tenant_identity lookup error:", e.message);
742
- }
743
- }
744
- // New user — redirect to setup
745
- return sessionRedirect(`${baseUrl}/setup`, {
746
- email: profile.email,
747
- name: profile.name,
748
- picture: profile.picture,
749
- slackUserId: profile.sub,
750
- slackTeamId: teamId,
751
- slackTeamName: teamName,
752
- });
753
- }
754
- catch (err) {
755
- console.error("[auth] callback error:", err);
756
- return Response.redirect(`${baseUrl}/?error=oauth_failed`, 302);
757
- }
454
+ // ── GET /health Health check ──
455
+ if (path === "/health" && req.method === "GET") {
456
+ const res = jsonResponse({ status: "ok", agents: registry.listPaths() });
457
+ return cors ? addCors(res) : res;
758
458
  }
759
- // GET /setup tenant creation page
760
- if (path === "/setup" && req.method === "GET") {
761
- const session = getSession(req);
762
- if (!session?.email)
763
- return Response.redirect(`${baseUrl}/`, 302);
764
- return htmlRes(renderTenantPage(baseUrl, session.email, session.name || ""));
459
+ // ── GET /.well-known/jwks.json JWKS public keys ──
460
+ if (path === "/.well-known/jwks.json" && req.method === "GET") {
461
+ const jwks = serverSigningKeys.length > 0
462
+ ? await buildJwks(serverSigningKeys)
463
+ : { keys: [] };
464
+ const res = jsonResponse(jwks);
465
+ return cors ? addCors(res) : res;
765
466
  }
766
- // POST /setup create tenant + user + link identity + generate token
767
- if (path === "/setup" && req.method === "POST") {
768
- try {
769
- const body = await req.json();
770
- const session = getSession(req);
771
- console.log("[setup] body:", JSON.stringify(body), "session:", JSON.stringify(session));
772
- // 1. Create tenant
773
- const tenantRes = await registry.call({ action: "execute_tool", path: "@auth", callerType: "system", tool: "create_tenant", params: { name: body.tenant } });
774
- const tenantId = tenantRes?.result?.tenantId;
775
- if (!tenantId)
776
- return addCors(jsonResponse({ error: "Failed to create tenant" }, 400));
777
- console.log("[setup] tenant created:", tenantId);
778
- // 2. Create user
779
- const userRes = await registry.call({ action: "execute_tool", path: "@users", callerType: "system", tool: "create_user", params: { email: body.email, name: session?.name, tenantId } });
780
- const userId = userRes?.result?.id || userRes?.result?.user?.id;
781
- console.log("[setup] user created:", userId);
782
- // 2b. Link tenant to Slack team
783
- if (session?.slackTeamId) {
784
- try {
785
- const dbUrl = process.env.DATABASE_URL;
786
- if (dbUrl) {
787
- const { default: postgres } = await import("postgres");
788
- const sql = postgres(dbUrl);
789
- const id = "ti_" + Math.random().toString(36).slice(2, 14);
790
- await sql `INSERT INTO tenant_identities (id, tenant_id, provider, provider_org_id, name) VALUES (${id}, ${tenantId}, 'slack', ${session.slackTeamId}, ${session.slackTeamName || ''})`;
791
- await sql.end();
792
- console.log("[setup] Created tenant_identity for slack team:", session.slackTeamId);
793
- }
794
- }
795
- catch (e) {
796
- console.error("[setup] tenant_identity insert error:", e.message);
797
- }
798
- }
799
- // 3. Link Slack identity
800
- if (session?.slackUserId && userId) {
801
- console.log("[setup] linking slack identity:", session.slackUserId);
802
- const linkRes = await registry.call({ action: "execute_tool", path: "@users", callerType: "system", tool: "link_identity", params: {
803
- userId,
804
- provider: "slack",
805
- providerUserId: session.slackUserId,
806
- email: body.email,
807
- name: session.name,
808
- metadata: { slackTeamId: session.slackTeamId, slackTeamName: session.slackTeamName }, callerType: "system"
809
- } });
810
- console.log("[setup] link_identity result:", JSON.stringify(linkRes));
811
- }
812
- // 4. Generate MCP token
813
- const mcpToken = await generateMcpToken();
814
- console.log("[setup] token generated, length:", mcpToken.length);
815
- return addCors(jsonResponse({ success: true, result: { tenantId, userId, token: mcpToken } }));
816
- }
817
- catch (err) {
818
- console.error("[setup] error:", err);
819
- return addCors(jsonResponse({ error: err.message }, 400));
820
- }
821
- }
822
- // POST /logout — clear session
823
- if (path === "/logout" && req.method === "POST") {
824
- return new Response(null, {
825
- status: 302,
826
- headers: {
827
- Location: `${baseUrl}/`,
828
- "Set-Cookie": "s_session=; Path=/; HttpOnly; SameSite=Lax; Max-Age=0",
829
- },
467
+ // ── GET /.well-known/configuration Server discovery ──
468
+ if (path === "/.well-known/configuration" && req.method === "GET") {
469
+ const baseUrl = new URL(req.url).origin;
470
+ const res = jsonResponse({
471
+ issuer: baseUrl,
472
+ jwks_uri: `${baseUrl}/.well-known/jwks.json`,
473
+ token_endpoint: `${baseUrl}/oauth/token`,
474
+ agents_endpoint: `${baseUrl}/list`,
475
+ call_endpoint: baseUrl,
476
+ supported_grant_types: ["client_credentials"],
477
+ agents: registry.listPaths(),
830
478
  });
479
+ return cors ? addCors(res) : res;
831
480
  }
832
- // GET /dashboard show MCP URL and setup instructions
833
- if (path === "/dashboard" && req.method === "GET") {
834
- const session = getSession(req);
835
- let token = session?.token || reqUrl.searchParams.get("token") || "";
836
- if (!token)
837
- return Response.redirect(`${baseUrl}/`, 302);
838
- // Persist token in cookie
839
- const sessData = Buffer.from(JSON.stringify({ ...session, token })).toString("base64url");
840
- return new Response(renderDashboardPage(baseUrl, token, session || undefined), {
841
- headers: {
842
- "Content-Type": "text/html; charset=utf-8",
843
- "Set-Cookie": `s_session=${sessData}; Path=/; HttpOnly; SameSite=Lax; Max-Age=604800`,
844
- },
845
- });
481
+ // ── GET /list List agents (legacy endpoint) ──
482
+ if (path === "/list" && req.method === "GET") {
483
+ const agents = registry.list();
484
+ const visible = agents.filter((agent) => canSeeAgent(agent, effectiveAuth));
485
+ const res = jsonResponse(visible.map((agent) => ({
486
+ path: agent.path,
487
+ name: agent.config?.name,
488
+ description: agent.config?.description,
489
+ supportedActions: agent.config?.supportedActions,
490
+ integration: agent.config?.integration || null,
491
+ tools: agent.tools
492
+ .filter((t) => {
493
+ const tv = t.visibility ?? "internal";
494
+ if (effectiveAuth?.isRoot)
495
+ return true;
496
+ if (tv === "public")
497
+ return true;
498
+ if (tv === "internal" && effectiveAuth)
499
+ return true;
500
+ return false;
501
+ })
502
+ .map((t) => ({
503
+ name: t.name,
504
+ description: t.description,
505
+ })),
506
+ })));
507
+ return cors ? addCors(res) : res;
846
508
  }
847
- return addCors(jsonResponse({
509
+ // ── Not found ──
510
+ const res = jsonResponse({
848
511
  jsonrpc: "2.0",
849
512
  id: null,
850
513
  error: {
851
514
  code: -32601,
852
515
  message: `Not found: ${req.method} ${path}`,
853
516
  },
854
- }, 404));
517
+ }, 404);
518
+ return cors ? addCors(res) : res;
855
519
  }
856
520
  catch (err) {
857
521
  console.error("[server] Request error:", err);
858
- return addCors(jsonResponse({
522
+ const res = jsonResponse({
859
523
  jsonrpc: "2.0",
860
524
  id: null,
861
- error: { code: -32603, message: "Internal error" },
862
- }, 500));
525
+ error: {
526
+ code: -32603,
527
+ message: err instanceof Error ? err.message : "Internal error",
528
+ },
529
+ }, 500);
530
+ return cors ? addCors(res) : res;
863
531
  }
864
532
  }
865
533
  // ──────────────────────────────────────────
866
534
  // Server lifecycle
867
535
  // ──────────────────────────────────────────
868
- const server = {
536
+ return {
537
+ url: null,
538
+ registry,
869
539
  async start() {
870
- if (serverInstance)
871
- throw new Error("Server is already running");
872
- serverInstance = Bun.serve({ port, hostname, fetch });
873
- serverUrl = `http://${hostname}:${port}${basePath}`;
874
- console.log(`Agent server running at ${serverUrl}`);
875
- console.log(" POST / - MCP JSON-RPC endpoint");
876
- console.log(" POST /mcp - MCP JSON-RPC endpoint (alias)");
877
- console.log(" GET /health - Health check");
878
- if (authConfig) {
879
- console.log(" POST /oauth/token - OAuth2 token endpoint");
880
- console.log(" Auth: enabled");
540
+ // Load or generate signing key for JWKS
541
+ if (options.signingKey) {
542
+ serverSigningKeys.push(options.signingKey);
543
+ }
544
+ else if (authConfig?.store?.getSigningKeys) {
545
+ const stored = await authConfig.store.getSigningKeys() ?? [];
546
+ for (const exported of stored) {
547
+ serverSigningKeys.push(await importSigningKey(exported));
548
+ }
881
549
  }
882
- console.log(" MCP tools: call_agent, list_agents");
550
+ if (serverSigningKeys.length === 0) {
551
+ const key = await generateSigningKey();
552
+ serverSigningKeys.push(key);
553
+ if (authConfig?.store?.storeSigningKey) {
554
+ await authConfig.store.storeSigningKey(await exportSigningKey(key));
555
+ }
556
+ }
557
+ serverInstance = Bun.serve({
558
+ port,
559
+ hostname,
560
+ fetch,
561
+ });
562
+ this.url = `http://${hostname}:${port}`;
563
+ console.log(`[agents-sdk] Server listening on http://${hostname}:${port}`);
883
564
  },
884
565
  async stop() {
885
566
  if (serverInstance) {
886
567
  serverInstance.stop();
887
568
  serverInstance = null;
888
- serverUrl = null;
569
+ this.url = null;
889
570
  }
890
571
  },
891
572
  fetch,
892
- get url() {
893
- return serverUrl;
894
- },
895
573
  };
896
- return server;
897
574
  }
898
575
  //# sourceMappingURL=server.js.map