@slashfi/agents-sdk 0.8.0 → 0.10.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 +17 -0
- package/dist/agent-definitions/auth.d.ts.map +1 -1
- package/dist/agent-definitions/auth.js +135 -1
- package/dist/agent-definitions/auth.js.map +1 -1
- package/dist/agent-definitions/integrations.d.ts +19 -0
- package/dist/agent-definitions/integrations.d.ts.map +1 -1
- package/dist/agent-definitions/integrations.js +218 -5
- package/dist/agent-definitions/integrations.js.map +1 -1
- package/dist/index.d.ts +9 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -2
- package/dist/index.js.map +1 -1
- package/dist/integration-interface.d.ts +37 -0
- package/dist/integration-interface.d.ts.map +1 -0
- package/dist/integration-interface.js +94 -0
- package/dist/integration-interface.js.map +1 -0
- package/dist/integrations-store.d.ts +33 -0
- package/dist/integrations-store.d.ts.map +1 -0
- package/dist/integrations-store.js +50 -0
- package/dist/integrations-store.js.map +1 -0
- package/dist/jwt.d.ts +86 -17
- package/dist/jwt.d.ts.map +1 -1
- package/dist/jwt.js +140 -17
- package/dist/jwt.js.map +1 -1
- package/dist/registry.d.ts +7 -0
- package/dist/registry.d.ts.map +1 -1
- package/dist/registry.js +8 -21
- package/dist/registry.js.map +1 -1
- package/dist/secret-collection.d.ts +37 -0
- package/dist/secret-collection.d.ts.map +1 -0
- package/dist/secret-collection.js +37 -0
- package/dist/secret-collection.js.map +1 -0
- package/dist/server.d.ts +41 -44
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +236 -592
- package/dist/server.js.map +1 -1
- package/dist/types.d.ts +7 -1
- package/dist/types.d.ts.map +1 -1
- package/package.json +5 -2
- package/src/agent-definitions/auth.ts +187 -1
- package/src/agent-definitions/integrations.ts +260 -5
- package/src/index.ts +18 -4
- package/src/integration-interface.ts +118 -0
- package/src/integrations-store.ts +84 -0
- package/src/jwt.ts +233 -65
- package/src/registry.ts +17 -2
- package/src/secret-collection.ts +66 -0
- package/src/server.ts +272 -681
- package/src/types.ts +8 -1
- package/dist/slack-oauth.d.ts +0 -27
- package/dist/slack-oauth.d.ts.map +0 -1
- package/dist/slack-oauth.js +0 -48
- package/dist/slack-oauth.js.map +0 -1
- package/dist/web-pages.d.ts +0 -8
- package/dist/web-pages.d.ts.map +0 -1
- package/dist/web-pages.js +0 -169
- package/dist/web-pages.js.map +0 -1
- package/src/slack-oauth.ts +0 -66
- package/src/web-pages.ts +0 -178
package/src/server.ts
CHANGED
|
@@ -1,30 +1,28 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Agent Server (MCP over HTTP)
|
|
3
3
|
*
|
|
4
|
-
* JSON-RPC server implementing the MCP protocol for agent interaction.
|
|
5
|
-
*
|
|
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
|
-
*
|
|
8
|
-
*
|
|
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
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
16
|
+
* @example
|
|
17
|
+
* ```typescript
|
|
18
|
+
* // Standalone usage
|
|
19
|
+
* const server = createAgentServer(registry, { port: 3000 });
|
|
20
|
+
* await server.start();
|
|
15
21
|
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
* - GET /health → Health check
|
|
21
|
-
*
|
|
22
|
-
* Auth Integration:
|
|
23
|
-
* When an `@auth` agent is registered, the server automatically:
|
|
24
|
-
* - Validates Bearer tokens on requests
|
|
25
|
-
* - Resolves tokens to identity + scopes
|
|
26
|
-
* - Populates caller context from headers (X-Atlas-Actor-Id, etc.)
|
|
27
|
-
* - 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
|
+
* ```
|
|
28
26
|
*/
|
|
29
27
|
|
|
30
28
|
import type { AuthStore } from "./agent-definitions/auth.js";
|
|
@@ -33,17 +31,10 @@ import {
|
|
|
33
31
|
processSecretParams,
|
|
34
32
|
} from "./agent-definitions/secrets.js";
|
|
35
33
|
import { verifyJwt } from "./jwt.js";
|
|
34
|
+
import type { SigningKey } from "./jwt.js";
|
|
35
|
+
import { generateSigningKey, importSigningKey, exportSigningKey, buildJwks, verifyJwtLocal, verifyJwtFromIssuer } from "./jwt.js";
|
|
36
36
|
import type { AgentRegistry } from "./registry.js";
|
|
37
37
|
import type { AgentDefinition, CallAgentRequest, Visibility } from "./types.js";
|
|
38
|
-
import { renderLoginPage, renderDashboardPage, renderTenantPage } from "./web-pages.js";
|
|
39
|
-
import { slackAuthUrl, exchangeSlackCode, getSlackProfile, type SlackOAuthConfig } from "./slack-oauth.js";
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
function resolveBaseUrl(req: Request, url: URL): string {
|
|
43
|
-
const proto = req.headers.get("x-forwarded-proto") || url.protocol.replace(":", "");
|
|
44
|
-
const host = req.headers.get("x-forwarded-host") || url.host;
|
|
45
|
-
return `${proto}://${host}`;
|
|
46
|
-
}
|
|
47
38
|
|
|
48
39
|
// ============================================
|
|
49
40
|
// Server Types
|
|
@@ -62,9 +53,12 @@ export interface AgentServerOptions {
|
|
|
62
53
|
serverName?: string;
|
|
63
54
|
/** Server version reported in MCP initialize (default: '1.0.0') */
|
|
64
55
|
serverVersion?: string;
|
|
65
|
-
|
|
66
56
|
/** Secret store for handling secret: refs in tool params */
|
|
67
57
|
secretStore?: SecretStore;
|
|
58
|
+
/** URLs of trusted registries for cross-registry JWT verification */
|
|
59
|
+
trustedIssuers?: string[];
|
|
60
|
+
/** Pre-generated signing key (if not provided, one is generated on start) */
|
|
61
|
+
signingKey?: SigningKey;
|
|
68
62
|
}
|
|
69
63
|
|
|
70
64
|
export interface AgentServer {
|
|
@@ -72,10 +66,12 @@ export interface AgentServer {
|
|
|
72
66
|
start(): Promise<void>;
|
|
73
67
|
/** Stop the server */
|
|
74
68
|
stop(): Promise<void>;
|
|
75
|
-
/** Handle a request (for custom integrations) */
|
|
69
|
+
/** Handle a request (for custom integrations / framework composition) */
|
|
76
70
|
fetch(req: Request): Promise<Response>;
|
|
77
71
|
/** Get the server URL (only available after start) */
|
|
78
72
|
url: string | null;
|
|
73
|
+
/** The agent registry this server uses */
|
|
74
|
+
registry: AgentRegistry;
|
|
79
75
|
}
|
|
80
76
|
|
|
81
77
|
// ============================================
|
|
@@ -97,79 +93,24 @@ interface JsonRpcResponse {
|
|
|
97
93
|
}
|
|
98
94
|
|
|
99
95
|
// ============================================
|
|
100
|
-
// Auth Types
|
|
96
|
+
// Auth Types (exported for use by custom routes)
|
|
101
97
|
// ============================================
|
|
102
98
|
|
|
103
|
-
interface AuthConfig {
|
|
99
|
+
export interface AuthConfig {
|
|
104
100
|
store: AuthStore;
|
|
105
101
|
rootKey: string;
|
|
106
102
|
tokenTtl: number;
|
|
107
103
|
}
|
|
108
104
|
|
|
109
|
-
interface ResolvedAuth {
|
|
105
|
+
export interface ResolvedAuth {
|
|
110
106
|
callerId: string;
|
|
111
107
|
callerType: "agent" | "user" | "system";
|
|
112
108
|
scopes: string[];
|
|
113
109
|
isRoot: boolean;
|
|
114
110
|
}
|
|
115
111
|
|
|
116
|
-
|
|
117
|
-
// ============================================
|
|
118
|
-
// Secrets Collection (one-time tokens)
|
|
119
|
-
// ============================================
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
function escHtml(s: string): string {
|
|
123
|
-
return s.replace(/&/g,"\&").replace(/</g,"\<").replace(/>/g,"\>").replace(/"/g,"\"");
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
function renderSecretForm(token: string, pending: PendingCollection, baseUrl: string): string {
|
|
127
|
-
const fields = pending.fields.map(f => `
|
|
128
|
-
<div class="field">
|
|
129
|
-
<label>${escHtml(f.name)}${f.secret ? ` <span class="badge">SECRET</span>` : ""}${f.required ? ` <span class="req">*</span>` : ""}</label>
|
|
130
|
-
${f.description ? `<p class="desc">${escHtml(f.description)}</p>` : ""}
|
|
131
|
-
<input type="${f.secret ? "password" : "text"}" name="${escHtml(f.name)}" ${f.required ? "required" : ""} autocomplete="off" />
|
|
132
|
-
</div>`).join("");
|
|
133
|
-
|
|
134
|
-
return `<!DOCTYPE html>
|
|
135
|
-
<html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Secure Setup</title>
|
|
136
|
-
<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>
|
|
137
|
-
<div class="card" id="fc"><div class="header"><span class="lock">🔐</span><h1>${escHtml(pending.tool)} on ${escHtml(pending.agent)}</h1></div>
|
|
138
|
-
<p class="subtitle">Enter credentials below. They are encrypted and stored securely — they never pass through the AI.</p>
|
|
139
|
-
<div class="shield">🛡️ End-to-end encrypted</div><div id="err" class="error"></div>
|
|
140
|
-
<form id="f">${fields}<button type="submit">Submit Securely</button></form>
|
|
141
|
-
<p class="footer">Expires in 10 minutes</p></div>
|
|
142
|
-
<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>
|
|
143
|
-
<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>`;
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
export interface PendingCollection {
|
|
147
|
-
/** Partial params already provided by agent */
|
|
148
|
-
params: Record<string, unknown>;
|
|
149
|
-
/** Target agent + tool to call after collection */
|
|
150
|
-
agent: string;
|
|
151
|
-
tool: string;
|
|
152
|
-
/** Auth context from original request */
|
|
153
|
-
auth: ResolvedAuth | null;
|
|
154
|
-
/** Fields the form needs to collect */
|
|
155
|
-
fields: Array<{ name: string; description?: string; secret: boolean; required: boolean }>;
|
|
156
|
-
/** Created timestamp for expiry */
|
|
157
|
-
createdAt: number;
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
export const pendingCollections = new Map<string, PendingCollection>();
|
|
161
|
-
|
|
162
|
-
export function generateCollectionToken(): string {
|
|
163
|
-
const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
|
164
|
-
let token = "sc_";
|
|
165
|
-
for (let i = 0; i < 32; i++) {
|
|
166
|
-
token += chars[Math.floor(Math.random() * chars.length)];
|
|
167
|
-
}
|
|
168
|
-
return token;
|
|
169
|
-
}
|
|
170
|
-
|
|
171
112
|
// ============================================
|
|
172
|
-
// Helpers
|
|
113
|
+
// HTTP Helpers
|
|
173
114
|
// ============================================
|
|
174
115
|
|
|
175
116
|
function jsonResponse(data: unknown, status = 200): Response {
|
|
@@ -184,10 +125,22 @@ function corsHeaders(): Record<string, string> {
|
|
|
184
125
|
"Access-Control-Allow-Origin": "*",
|
|
185
126
|
"Access-Control-Allow-Methods": "GET, POST, OPTIONS",
|
|
186
127
|
"Access-Control-Allow-Headers":
|
|
187
|
-
"Content-Type, Authorization, X-Atlas-Actor-Id, X-Atlas-
|
|
128
|
+
"Content-Type, Authorization, X-Atlas-Actor-Id, X-Atlas-Actor-Type",
|
|
188
129
|
};
|
|
189
130
|
}
|
|
190
131
|
|
|
132
|
+
function addCors(res: Response): Response {
|
|
133
|
+
const headers = new Headers(res.headers);
|
|
134
|
+
for (const [k, v] of Object.entries(corsHeaders())) {
|
|
135
|
+
headers.set(k, v);
|
|
136
|
+
}
|
|
137
|
+
return new Response(res.body, { status: res.status, headers });
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// ============================================
|
|
141
|
+
// JSON-RPC Helpers
|
|
142
|
+
// ============================================
|
|
143
|
+
|
|
191
144
|
function jsonRpcSuccess(id: unknown, result: unknown): JsonRpcResponse {
|
|
192
145
|
return { jsonrpc: "2.0", id, result };
|
|
193
146
|
}
|
|
@@ -220,10 +173,10 @@ function mcpResult(value: unknown, isError = false) {
|
|
|
220
173
|
}
|
|
221
174
|
|
|
222
175
|
// ============================================
|
|
223
|
-
// Auth Detection
|
|
176
|
+
// Auth Detection & Resolution
|
|
224
177
|
// ============================================
|
|
225
178
|
|
|
226
|
-
function detectAuth(registry: AgentRegistry): AuthConfig | null {
|
|
179
|
+
export function detectAuth(registry: AgentRegistry): AuthConfig | null {
|
|
227
180
|
const authAgent = registry.get("@auth") as
|
|
228
181
|
| (AgentDefinition & {
|
|
229
182
|
__authStore?: AuthStore;
|
|
@@ -241,9 +194,10 @@ function detectAuth(registry: AgentRegistry): AuthConfig | null {
|
|
|
241
194
|
};
|
|
242
195
|
}
|
|
243
196
|
|
|
244
|
-
async function resolveAuth(
|
|
197
|
+
export async function resolveAuth(
|
|
245
198
|
req: Request,
|
|
246
199
|
authConfig: AuthConfig,
|
|
200
|
+
jwksOptions?: { signingKeys?: SigningKey[]; trustedIssuers?: string[] },
|
|
247
201
|
): Promise<ResolvedAuth | null> {
|
|
248
202
|
const authHeader = req.headers.get("Authorization");
|
|
249
203
|
if (!authHeader) return null;
|
|
@@ -251,6 +205,7 @@ async function resolveAuth(
|
|
|
251
205
|
const [scheme, credential] = authHeader.split(" ", 2);
|
|
252
206
|
if (scheme?.toLowerCase() !== "bearer" || !credential) return null;
|
|
253
207
|
|
|
208
|
+
// Root key check
|
|
254
209
|
if (credential === authConfig.rootKey) {
|
|
255
210
|
return {
|
|
256
211
|
callerId: "root",
|
|
@@ -260,12 +215,47 @@ async function resolveAuth(
|
|
|
260
215
|
};
|
|
261
216
|
}
|
|
262
217
|
|
|
263
|
-
// Try
|
|
264
|
-
// JWT is signed with the client's secret hash
|
|
265
|
-
// Decode payload to get client_id, look up client, verify signature
|
|
218
|
+
// Try ES256 verification against own signing keys
|
|
266
219
|
const parts = credential.split(".");
|
|
220
|
+
if (parts.length === 3 && jwksOptions?.signingKeys?.length) {
|
|
221
|
+
for (const key of jwksOptions.signingKeys) {
|
|
222
|
+
try {
|
|
223
|
+
const verified = await verifyJwtLocal(credential, key.publicKey);
|
|
224
|
+
if (verified) {
|
|
225
|
+
return {
|
|
226
|
+
callerId: verified.sub ?? verified.name ?? "unknown",
|
|
227
|
+
callerType: "agent",
|
|
228
|
+
scopes: verified.scopes ?? ["*"],
|
|
229
|
+
isRoot: false,
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
} catch {
|
|
233
|
+
continue;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// Try trusted issuers (remote JWKS verification)
|
|
239
|
+
if (parts.length === 3 && jwksOptions?.trustedIssuers?.length) {
|
|
240
|
+
for (const issuer of jwksOptions.trustedIssuers) {
|
|
241
|
+
try {
|
|
242
|
+
const verified = await verifyJwtFromIssuer(credential, issuer);
|
|
243
|
+
if (verified) {
|
|
244
|
+
return {
|
|
245
|
+
callerId: verified.sub ?? verified.name ?? "unknown",
|
|
246
|
+
callerType: "agent",
|
|
247
|
+
scopes: verified.scopes ?? ["*"],
|
|
248
|
+
isRoot: false,
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
} catch {
|
|
252
|
+
continue;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// Try HMAC JWT verification (legacy, stateless)
|
|
267
258
|
if (parts.length === 3) {
|
|
268
|
-
// Looks like a JWT - decode payload to get client_id
|
|
269
259
|
try {
|
|
270
260
|
const payloadB64 = parts[1];
|
|
271
261
|
const padded = payloadB64.replace(/-/g, "+").replace(/_/g, "/");
|
|
@@ -277,7 +267,6 @@ async function resolveAuth(
|
|
|
277
267
|
};
|
|
278
268
|
|
|
279
269
|
if (payload.sub) {
|
|
280
|
-
// Look up client to get the signing secret (secret hash)
|
|
281
270
|
const client = await authConfig.store.getClient(payload.sub);
|
|
282
271
|
if (client) {
|
|
283
272
|
const verified = await verifyJwt(credential, client.clientSecretHash);
|
|
@@ -309,7 +298,7 @@ async function resolveAuth(
|
|
|
309
298
|
};
|
|
310
299
|
}
|
|
311
300
|
|
|
312
|
-
function canSeeAgent(
|
|
301
|
+
export function canSeeAgent(
|
|
313
302
|
agent: AgentDefinition,
|
|
314
303
|
auth: ResolvedAuth | null,
|
|
315
304
|
): boolean {
|
|
@@ -346,11 +335,11 @@ function getToolDefinitions() {
|
|
|
346
335
|
},
|
|
347
336
|
path: {
|
|
348
337
|
type: "string",
|
|
349
|
-
description: "Agent path (e.g
|
|
338
|
+
description: "Agent path (e.g., '@my-agent')",
|
|
350
339
|
},
|
|
351
340
|
tool: {
|
|
352
341
|
type: "string",
|
|
353
|
-
description: "Tool name to call
|
|
342
|
+
description: "Tool name to call",
|
|
354
343
|
},
|
|
355
344
|
params: {
|
|
356
345
|
type: "object",
|
|
@@ -366,7 +355,8 @@ function getToolDefinitions() {
|
|
|
366
355
|
},
|
|
367
356
|
{
|
|
368
357
|
name: "list_agents",
|
|
369
|
-
description:
|
|
358
|
+
description:
|
|
359
|
+
"List all registered agents and their available tools.",
|
|
370
360
|
inputSchema: {
|
|
371
361
|
type: "object",
|
|
372
362
|
properties: {},
|
|
@@ -393,13 +383,15 @@ export function createAgentServer(
|
|
|
393
383
|
secretStore,
|
|
394
384
|
} = options;
|
|
395
385
|
|
|
396
|
-
|
|
397
|
-
|
|
386
|
+
// Signing keys for JWKS-based auth
|
|
387
|
+
const serverSigningKeys: SigningKey[] = [];
|
|
388
|
+
const configTrustedIssuers: string[] = options.trustedIssuers ?? [];
|
|
398
389
|
|
|
399
390
|
const authConfig = detectAuth(registry);
|
|
391
|
+
let serverInstance: ReturnType<typeof Bun.serve> | null = null;
|
|
400
392
|
|
|
401
393
|
// ──────────────────────────────────────────
|
|
402
|
-
//
|
|
394
|
+
// JSON-RPC handler
|
|
403
395
|
// ──────────────────────────────────────────
|
|
404
396
|
|
|
405
397
|
async function handleJsonRpc(
|
|
@@ -407,24 +399,21 @@ export function createAgentServer(
|
|
|
407
399
|
auth: ResolvedAuth | null,
|
|
408
400
|
): Promise<JsonRpcResponse> {
|
|
409
401
|
switch (request.method) {
|
|
410
|
-
// MCP protocol handshake
|
|
411
402
|
case "initialize":
|
|
412
403
|
return jsonRpcSuccess(request.id, {
|
|
413
404
|
protocolVersion: "2024-11-05",
|
|
414
|
-
capabilities: { tools: {} },
|
|
405
|
+
capabilities: { tools: { listChanged: false } },
|
|
415
406
|
serverInfo: { name: serverName, version: serverVersion },
|
|
416
407
|
});
|
|
417
408
|
|
|
418
409
|
case "notifications/initialized":
|
|
419
410
|
return jsonRpcSuccess(request.id, {});
|
|
420
411
|
|
|
421
|
-
// List MCP tools
|
|
422
412
|
case "tools/list":
|
|
423
413
|
return jsonRpcSuccess(request.id, {
|
|
424
414
|
tools: getToolDefinitions(),
|
|
425
415
|
});
|
|
426
416
|
|
|
427
|
-
// Call an MCP tool
|
|
428
417
|
case "tools/call": {
|
|
429
418
|
const { name, arguments: args } = (request.params ?? {}) as {
|
|
430
419
|
name: string;
|
|
@@ -435,7 +424,7 @@ export function createAgentServer(
|
|
|
435
424
|
const result = await handleToolCall(name, args ?? {}, auth);
|
|
436
425
|
return jsonRpcSuccess(request.id, result);
|
|
437
426
|
} catch (err) {
|
|
438
|
-
console.error("[server]
|
|
427
|
+
console.error("[server] Tool call error:", err);
|
|
439
428
|
return jsonRpcSuccess(
|
|
440
429
|
request.id,
|
|
441
430
|
mcpResult(
|
|
@@ -469,6 +458,10 @@ export function createAgentServer(
|
|
|
469
458
|
const req = (args.request ?? args) as CallAgentRequest;
|
|
470
459
|
|
|
471
460
|
// Inject auth context
|
|
461
|
+
// No auth = internal/trusted call (e.g., atlas-api → atlas-os RPC)
|
|
462
|
+
if (!auth) {
|
|
463
|
+
req.callerType = "system";
|
|
464
|
+
}
|
|
472
465
|
if (auth) {
|
|
473
466
|
req.callerId = auth.callerId;
|
|
474
467
|
req.callerType = auth.callerType;
|
|
@@ -481,10 +474,8 @@ export function createAgentServer(
|
|
|
481
474
|
}
|
|
482
475
|
|
|
483
476
|
// Process secret params: resolve refs, store raw secrets
|
|
484
|
-
// Auto-resolve secret:xxx refs in tool params before execution
|
|
485
477
|
if ((req as any).params && secretStore) {
|
|
486
478
|
const ownerId = auth?.callerId ?? "anonymous";
|
|
487
|
-
// Find the tool schema to check for secret: true fields
|
|
488
479
|
const agent = registry.get(req.path);
|
|
489
480
|
const tool = agent?.tools.find((t) => t.name === (req as any).tool);
|
|
490
481
|
const schema = tool?.inputSchema as any;
|
|
@@ -518,6 +509,7 @@ export function createAgentServer(
|
|
|
518
509
|
const tv = t.visibility ?? "internal";
|
|
519
510
|
if (auth?.isRoot) return true;
|
|
520
511
|
if (tv === "public") return true;
|
|
512
|
+
if (tv === "authenticated" && auth?.callerId && auth.callerId !== "anonymous") return true;
|
|
521
513
|
if (tv === "internal" && auth) return true;
|
|
522
514
|
return false;
|
|
523
515
|
})
|
|
@@ -532,7 +524,7 @@ export function createAgentServer(
|
|
|
532
524
|
}
|
|
533
525
|
|
|
534
526
|
// ──────────────────────────────────────────
|
|
535
|
-
// OAuth2 token handler
|
|
527
|
+
// OAuth2 token handler
|
|
536
528
|
// ──────────────────────────────────────────
|
|
537
529
|
|
|
538
530
|
async function handleOAuthToken(req: Request): Promise<Response> {
|
|
@@ -578,583 +570,175 @@ export function createAgentServer(
|
|
|
578
570
|
);
|
|
579
571
|
}
|
|
580
572
|
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
{
|
|
588
|
-
error: "invalid_client",
|
|
589
|
-
error_description: "Invalid client credentials",
|
|
590
|
-
},
|
|
591
|
-
401,
|
|
592
|
-
);
|
|
593
|
-
}
|
|
594
|
-
|
|
595
|
-
// Delegate to @auth agent's token tool which generates proper JWTs
|
|
596
|
-
const tokenResult = await registry.call({
|
|
597
|
-
action: "execute_tool",
|
|
598
|
-
path: "@auth",
|
|
599
|
-
tool: "token",
|
|
600
|
-
params: {
|
|
601
|
-
grantType: "client_credentials",
|
|
602
|
-
clientId,
|
|
603
|
-
clientSecret,
|
|
604
|
-
},
|
|
605
|
-
context: {
|
|
606
|
-
tenantId: "default",
|
|
607
|
-
agentPath: "@auth",
|
|
608
|
-
callerId: "oauth_endpoint",
|
|
573
|
+
try {
|
|
574
|
+
const result = await registry.call({
|
|
575
|
+
action: "execute_tool",
|
|
576
|
+
path: "@auth",
|
|
577
|
+
tool: "token",
|
|
578
|
+
params: { clientId, clientSecret },
|
|
609
579
|
callerType: "system",
|
|
610
|
-
}
|
|
611
|
-
|
|
580
|
+
});
|
|
581
|
+
|
|
582
|
+
const tokenResult = (result as any)?.result;
|
|
583
|
+
if (!tokenResult?.accessToken) {
|
|
584
|
+
return jsonResponse(
|
|
585
|
+
{ error: "invalid_client", error_description: "Authentication failed" },
|
|
586
|
+
401,
|
|
587
|
+
);
|
|
588
|
+
}
|
|
612
589
|
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
590
|
+
return jsonResponse({
|
|
591
|
+
access_token: tokenResult.accessToken,
|
|
592
|
+
token_type: "Bearer",
|
|
593
|
+
expires_in: tokenResult.expiresIn ?? authConfig.tokenTtl,
|
|
594
|
+
refresh_token: tokenResult.refreshToken,
|
|
595
|
+
});
|
|
596
|
+
} catch (err) {
|
|
597
|
+
console.error("[oauth] Token error:", err);
|
|
598
|
+
return jsonResponse(
|
|
599
|
+
{ error: "server_error", error_description: "Token exchange failed" },
|
|
600
|
+
500,
|
|
601
|
+
);
|
|
617
602
|
}
|
|
618
|
-
const tokenData = callResponse.result;
|
|
619
|
-
|
|
620
|
-
// accessToken may be wrapped as { $agent_type: "secret", value: "<jwt>" }
|
|
621
|
-
const accessToken = tokenData.accessToken?.$agent_type === "secret"
|
|
622
|
-
? tokenData.accessToken.value
|
|
623
|
-
: tokenData.accessToken;
|
|
624
|
-
|
|
625
|
-
return jsonResponse({
|
|
626
|
-
access_token: accessToken,
|
|
627
|
-
token_type: tokenData.tokenType ?? "Bearer",
|
|
628
|
-
expires_in: tokenData.expiresIn ?? authConfig.tokenTtl,
|
|
629
|
-
scope: Array.isArray(tokenData.scopes) ? tokenData.scopes.join(" ") : client.scopes.join(" "),
|
|
630
|
-
});
|
|
631
603
|
}
|
|
632
604
|
|
|
633
605
|
// ──────────────────────────────────────────
|
|
634
|
-
//
|
|
635
|
-
//
|
|
606
|
+
// Main fetch handler
|
|
607
|
+
// ──────────────────────────────────────────
|
|
636
608
|
|
|
637
609
|
async function fetch(req: Request): Promise<Response> {
|
|
638
|
-
const url = new URL(req.url);
|
|
639
|
-
const path = url.pathname.replace(basePath, "") || "/";
|
|
640
|
-
|
|
641
|
-
// CORS preflight
|
|
642
|
-
if (cors && req.method === "OPTIONS") {
|
|
643
|
-
return new Response(null, { status: 204, headers: corsHeaders() });
|
|
644
|
-
}
|
|
645
|
-
|
|
646
|
-
const addCors = (response: Response): Response => {
|
|
647
|
-
if (!cors) return response;
|
|
648
|
-
const headers = new Headers(response.headers);
|
|
649
|
-
for (const [key, value] of Object.entries(corsHeaders())) {
|
|
650
|
-
headers.set(key, value);
|
|
651
|
-
}
|
|
652
|
-
return new Response(response.body, {
|
|
653
|
-
status: response.status,
|
|
654
|
-
statusText: response.statusText,
|
|
655
|
-
headers,
|
|
656
|
-
});
|
|
657
|
-
};
|
|
658
|
-
|
|
659
|
-
const auth = authConfig ? await resolveAuth(req, authConfig) : null;
|
|
660
|
-
|
|
661
610
|
try {
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
const body = (await req.json()) as JsonRpcRequest;
|
|
665
|
-
const response = await handleJsonRpc(body, auth);
|
|
666
|
-
return addCors(jsonResponse(response));
|
|
667
|
-
}
|
|
668
|
-
|
|
669
|
-
// OAuth2 token endpoint
|
|
670
|
-
if (path === "/oauth/token" && req.method === "POST") {
|
|
671
|
-
return addCors(await handleOAuthToken(req));
|
|
672
|
-
}
|
|
611
|
+
const url = new URL(req.url);
|
|
612
|
+
const path = url.pathname.replace(basePath, "") || "/";
|
|
673
613
|
|
|
674
|
-
//
|
|
675
|
-
if (
|
|
676
|
-
return
|
|
677
|
-
}
|
|
678
|
-
|
|
679
|
-
// Backwards compat: GET /list (returns agents directly)
|
|
680
|
-
if (path === "/list" && req.method === "GET") {
|
|
681
|
-
const agents = registry.list();
|
|
682
|
-
const visible = agents.filter((agent) => canSeeAgent(agent, auth));
|
|
683
|
-
return addCors(
|
|
684
|
-
jsonResponse({
|
|
685
|
-
success: true,
|
|
686
|
-
agents: visible.map((agent) => ({
|
|
687
|
-
path: agent.path,
|
|
688
|
-
name: agent.config?.name,
|
|
689
|
-
description: agent.config?.description,
|
|
690
|
-
supportedActions: agent.config?.supportedActions,
|
|
691
|
-
integration: agent.config?.integration || null,
|
|
692
|
-
tools: agent.tools
|
|
693
|
-
.filter((t) => {
|
|
694
|
-
const tv = t.visibility ?? "internal";
|
|
695
|
-
if (auth?.isRoot) return true;
|
|
696
|
-
if (tv === "public") return true;
|
|
697
|
-
if (tv === "internal" && auth) return true;
|
|
698
|
-
return false;
|
|
699
|
-
})
|
|
700
|
-
.map((t) => t.name),
|
|
701
|
-
})),
|
|
702
|
-
}),
|
|
703
|
-
);
|
|
614
|
+
// CORS preflight
|
|
615
|
+
if (cors && req.method === "OPTIONS") {
|
|
616
|
+
return new Response(null, { status: 204, headers: corsHeaders() });
|
|
704
617
|
}
|
|
705
618
|
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
}
|
|
725
|
-
|
|
726
|
-
try {
|
|
727
|
-
await registry.call({
|
|
728
|
-
action: "execute_tool",
|
|
729
|
-
path: "@integrations",
|
|
730
|
-
tool: "handle_oauth_callback",
|
|
731
|
-
params: { provider, code, state: state ?? undefined },
|
|
732
|
-
context: {
|
|
733
|
-
tenantId: "default",
|
|
734
|
-
agentPath: "@integrations",
|
|
735
|
-
callerId: "oauth_callback",
|
|
736
|
-
callerType: "system",
|
|
737
|
-
},
|
|
738
|
-
} as any);
|
|
739
|
-
|
|
740
|
-
// Parse redirect URL from state (base64-encoded JSON)
|
|
741
|
-
let redirectUrl = "/";
|
|
742
|
-
if (state) {
|
|
743
|
-
try {
|
|
744
|
-
const parsed = JSON.parse(atob(state));
|
|
745
|
-
if (parsed.redirectUrl) redirectUrl = parsed.redirectUrl;
|
|
746
|
-
} catch {
|
|
747
|
-
// Fallback: try raw JSON for backward compat
|
|
748
|
-
try {
|
|
749
|
-
const parsed = JSON.parse(state);
|
|
750
|
-
if (parsed.redirectUrl) redirectUrl = parsed.redirectUrl;
|
|
751
|
-
} catch {}
|
|
619
|
+
// Resolve auth for all requests
|
|
620
|
+
const auth = authConfig ? await resolveAuth(req, authConfig, {
|
|
621
|
+
signingKeys: serverSigningKeys,
|
|
622
|
+
trustedIssuers: configTrustedIssuers,
|
|
623
|
+
}) : null;
|
|
624
|
+
|
|
625
|
+
// Also check header-based identity (for proxied requests)
|
|
626
|
+
const headerAuth: ResolvedAuth | null = !auth
|
|
627
|
+
? (() => {
|
|
628
|
+
const actorId = req.headers.get("X-Atlas-Actor-Id");
|
|
629
|
+
const actorType = req.headers.get("X-Atlas-Actor-Type");
|
|
630
|
+
if (actorId) {
|
|
631
|
+
return {
|
|
632
|
+
callerId: actorId,
|
|
633
|
+
callerType: (actorType as any) ?? "agent",
|
|
634
|
+
scopes: ["*"],
|
|
635
|
+
isRoot: false,
|
|
636
|
+
};
|
|
752
637
|
}
|
|
753
|
-
|
|
638
|
+
return null;
|
|
639
|
+
})()
|
|
640
|
+
: null;
|
|
754
641
|
|
|
755
|
-
|
|
756
|
-
return Response.redirect(`${redirectUrl}${sep}connected=${provider}`, 302);
|
|
757
|
-
} catch (err) {
|
|
758
|
-
return new Response(
|
|
759
|
-
`<html><body><h1>Connection Failed</h1><p>${err instanceof Error ? err.message : String(err)}</p></body></html>`,
|
|
760
|
-
{ status: 500, headers: { "Content-Type": "text/html", ...corsHeaders() } },
|
|
761
|
-
);
|
|
762
|
-
}
|
|
763
|
-
}
|
|
642
|
+
const effectiveAuth = auth ?? headerAuth;
|
|
764
643
|
|
|
765
|
-
//
|
|
766
|
-
if (path === "/
|
|
767
|
-
const
|
|
768
|
-
const
|
|
769
|
-
|
|
770
|
-
if (state) {
|
|
771
|
-
try {
|
|
772
|
-
const parsed = JSON.parse(atob(state));
|
|
773
|
-
provider = parsed.providerId;
|
|
774
|
-
} catch {
|
|
775
|
-
// Fallback: try raw JSON for backward compat
|
|
776
|
-
try {
|
|
777
|
-
const parsed = JSON.parse(state);
|
|
778
|
-
provider = parsed.providerId;
|
|
779
|
-
} catch {}
|
|
780
|
-
}
|
|
781
|
-
}
|
|
782
|
-
if (!provider) {
|
|
783
|
-
return addCors(jsonResponse({ error: "Missing provider in state param" }, 400));
|
|
784
|
-
}
|
|
785
|
-
return handleIntegrationOAuthCallback(provider, req);
|
|
786
|
-
}
|
|
787
|
-
|
|
788
|
-
// GET /integrations/callback/:provider - Legacy OAuth callback (provider from URL path)
|
|
789
|
-
if (path.startsWith("/integrations/callback/") && req.method === "GET") {
|
|
790
|
-
const provider = path.split("/integrations/callback/")[1]?.split("?")[0];
|
|
791
|
-
if (!provider) {
|
|
792
|
-
return addCors(jsonResponse({ error: "Missing provider" }, 400));
|
|
793
|
-
}
|
|
794
|
-
return handleIntegrationOAuthCallback(provider, req);
|
|
795
|
-
}
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
// GET /secrets/form/:token - Serve hosted secrets form
|
|
799
|
-
if (path.startsWith("/secrets/form/") && req.method === "GET") {
|
|
800
|
-
const token = path.split("/").pop() ?? "";
|
|
801
|
-
const pending = pendingCollections.get(token);
|
|
802
|
-
if (!pending) {
|
|
803
|
-
return addCors(new Response("Invalid or expired form link", { status: 404 }));
|
|
804
|
-
}
|
|
805
|
-
if (Date.now() - pending.createdAt > 10 * 60 * 1000) {
|
|
806
|
-
pendingCollections.delete(token);
|
|
807
|
-
return addCors(new Response("Form link expired", { status: 410 }));
|
|
808
|
-
}
|
|
809
|
-
const reqUrl = new URL(req.url); const baseUrl = resolveBaseUrl(req, reqUrl);
|
|
810
|
-
const html = renderSecretForm(token, pending, baseUrl);
|
|
811
|
-
return addCors(new Response(html, { headers: { "Content-Type": "text/html" } }));
|
|
812
|
-
}
|
|
813
|
-
|
|
814
|
-
// POST /secrets/collect - Submit collected secrets and auto-forward to tool
|
|
815
|
-
if (path === "/secrets/collect" && req.method === "POST") {
|
|
816
|
-
const body = (await req.json()) as {
|
|
817
|
-
token: string;
|
|
818
|
-
values: Record<string, string>;
|
|
819
|
-
};
|
|
820
|
-
|
|
821
|
-
const pending = pendingCollections.get(body.token);
|
|
822
|
-
if (!pending) {
|
|
823
|
-
return addCors(
|
|
824
|
-
jsonResponse({ error: "Invalid or expired collection token" }, 400),
|
|
825
|
-
);
|
|
826
|
-
}
|
|
827
|
-
|
|
828
|
-
// One-time use
|
|
829
|
-
pendingCollections.delete(body.token);
|
|
830
|
-
|
|
831
|
-
// Check expiry (10 min)
|
|
832
|
-
if (Date.now() - pending.createdAt > 10 * 60 * 1000) {
|
|
833
|
-
return addCors(
|
|
834
|
-
jsonResponse({ error: "Collection token expired" }, 400),
|
|
835
|
-
);
|
|
836
|
-
}
|
|
837
|
-
|
|
838
|
-
// Encrypt secret values and store as refs
|
|
839
|
-
const mergedParams = { ...pending.params };
|
|
840
|
-
for (const [fieldName, value] of Object.entries(body.values)) {
|
|
841
|
-
const fieldDef = pending.fields.find((f) => f.name === fieldName);
|
|
842
|
-
if (fieldDef?.secret && secretStore) {
|
|
843
|
-
// Store encrypted, get ref
|
|
844
|
-
const ownerId = pending.auth?.callerId ?? "anonymous";
|
|
845
|
-
const secretId = await secretStore.store(value, ownerId);
|
|
846
|
-
mergedParams[fieldName] = `secret:${secretId}`;
|
|
847
|
-
} else {
|
|
848
|
-
mergedParams[fieldName] = value;
|
|
849
|
-
}
|
|
850
|
-
}
|
|
851
|
-
|
|
852
|
-
// Auto-forward to the target tool
|
|
853
|
-
const callRequest = {
|
|
854
|
-
action: "execute_tool" as const,
|
|
855
|
-
path: pending.agent,
|
|
856
|
-
tool: pending.tool,
|
|
857
|
-
params: mergedParams,
|
|
858
|
-
};
|
|
859
|
-
|
|
860
|
-
const toolCtx = {
|
|
861
|
-
tenantId: "default",
|
|
862
|
-
agentPath: pending.agent,
|
|
863
|
-
callerId: pending.auth?.callerId ?? "anonymous",
|
|
864
|
-
callerType: pending.auth?.callerType ?? ("system" as const),
|
|
865
|
-
};
|
|
866
|
-
|
|
867
|
-
const result = await registry.call({
|
|
868
|
-
...callRequest,
|
|
869
|
-
context: toolCtx,
|
|
870
|
-
} as any);
|
|
871
|
-
|
|
872
|
-
return addCors(jsonResponse({ success: true, result }));
|
|
873
|
-
}
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
// --- Web pages (plain HTML, served from same server) ---
|
|
877
|
-
const htmlRes = (body: string) => addCors(new Response(body, { headers: { "Content-Type": "text/html; charset=utf-8" } }));
|
|
878
|
-
const reqUrl = new URL(req.url);
|
|
879
|
-
const baseUrl = resolveBaseUrl(req, reqUrl);
|
|
880
|
-
|
|
881
|
-
const slackConfig: SlackOAuthConfig | null =
|
|
882
|
-
process.env.SLACK_CLIENT_ID && process.env.SLACK_CLIENT_SECRET
|
|
883
|
-
? {
|
|
884
|
-
clientId: process.env.SLACK_CLIENT_ID,
|
|
885
|
-
clientSecret: process.env.SLACK_CLIENT_SECRET,
|
|
886
|
-
redirectUri: `${baseUrl}/auth/slack/callback`,
|
|
887
|
-
}
|
|
888
|
-
: null;
|
|
889
|
-
|
|
890
|
-
// Helper: read session from cookie
|
|
891
|
-
function getSession(r: Request): Record<string, any> | null {
|
|
892
|
-
const c = r.headers.get("Cookie") || "";
|
|
893
|
-
const m = c.match(/s_session=([^;]+)/);
|
|
894
|
-
if (!m) return null;
|
|
895
|
-
try { return JSON.parse(Buffer.from(m[1], "base64url").toString()); }
|
|
896
|
-
catch { return null; }
|
|
897
|
-
}
|
|
898
|
-
|
|
899
|
-
// Helper: generate JWT from client credentials
|
|
900
|
-
async function generateMcpToken(): Promise<string> {
|
|
901
|
-
const clientRes = await registry.call({ action: "execute_tool", path: "@auth", tool: "create_client", callerType: "system", params: {
|
|
902
|
-
name: "mcp-" + Date.now(),
|
|
903
|
-
scopes: ["*"],
|
|
904
|
-
}} as any) as any;
|
|
905
|
-
const cid = clientRes?.result?.clientId;
|
|
906
|
-
const csec = clientRes?.result?.clientSecret;
|
|
907
|
-
if (!cid || !csec) throw new Error("Failed to create client: " + JSON.stringify(clientRes));
|
|
908
|
-
|
|
909
|
-
const tokenRes = await globalThis.fetch(`http://localhost:${port}/oauth/token`, {
|
|
910
|
-
method: "POST",
|
|
911
|
-
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
912
|
-
body: new URLSearchParams({ grant_type: "client_credentials", client_id: cid, client_secret: csec }),
|
|
913
|
-
});
|
|
914
|
-
const tokenData = await tokenRes.json() as any;
|
|
915
|
-
if (!tokenData.access_token) throw new Error("Failed to get JWT: " + JSON.stringify(tokenData));
|
|
916
|
-
return tokenData.access_token;
|
|
917
|
-
}
|
|
918
|
-
|
|
919
|
-
// Helper: set session cookie and redirect
|
|
920
|
-
function sessionRedirect(location: string, session: Record<string, any>): Response {
|
|
921
|
-
const data = Buffer.from(JSON.stringify(session)).toString("base64url");
|
|
922
|
-
return new Response(null, {
|
|
923
|
-
status: 302,
|
|
924
|
-
headers: {
|
|
925
|
-
Location: location,
|
|
926
|
-
"Set-Cookie": `s_session=${data}; Path=/; HttpOnly; SameSite=Lax; Max-Age=604800`,
|
|
927
|
-
},
|
|
928
|
-
});
|
|
929
|
-
}
|
|
930
|
-
|
|
931
|
-
// GET / — login page (or redirect to dashboard if session exists)
|
|
932
|
-
if (path === "/" && req.method === "GET") {
|
|
933
|
-
const session = getSession(req);
|
|
934
|
-
if (session?.token) return Response.redirect(`${baseUrl}/dashboard`, 302);
|
|
935
|
-
return htmlRes(renderLoginPage(baseUrl, !!slackConfig));
|
|
936
|
-
}
|
|
937
|
-
|
|
938
|
-
// GET /auth/slack — start Slack OAuth
|
|
939
|
-
if (path === "/auth/slack" && req.method === "GET") {
|
|
940
|
-
if (!slackConfig) return htmlRes("<h1>Slack OAuth not configured</h1>");
|
|
941
|
-
return Response.redirect(slackAuthUrl(slackConfig), 302);
|
|
644
|
+
// ── POST / → MCP JSON-RPC ──
|
|
645
|
+
if (path === "/" && req.method === "POST") {
|
|
646
|
+
const body = (await req.json()) as JsonRpcRequest;
|
|
647
|
+
const result = await handleJsonRpc(body, effectiveAuth);
|
|
648
|
+
return cors ? addCors(jsonResponse(result)) : jsonResponse(result);
|
|
942
649
|
}
|
|
943
650
|
|
|
944
|
-
//
|
|
945
|
-
if (path === "/
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
const authError = reqUrl.searchParams.get("error");
|
|
949
|
-
if (authError || !authCode) return Response.redirect(`${baseUrl}/?error=${authError || "no_code"}`, 302);
|
|
950
|
-
|
|
951
|
-
try {
|
|
952
|
-
const tokens = await exchangeSlackCode(authCode, slackConfig);
|
|
953
|
-
const profile = await getSlackProfile(tokens.access_token);
|
|
954
|
-
const teamId = profile["https://slack.com/team_id"] || "";
|
|
955
|
-
const teamName = profile["https://slack.com/team_name"] || "";
|
|
956
|
-
|
|
957
|
-
// Check if user already exists
|
|
958
|
-
console.log("[auth] Looking up slack user:", profile.sub, profile.email);
|
|
959
|
-
const existing = await registry.call({
|
|
960
|
-
action: "execute_tool", path: "@users", callerType: "system", tool: "resolve_identity",
|
|
961
|
-
params: { provider: "slack", providerUserId: profile.sub },
|
|
962
|
-
} as any) as any;
|
|
963
|
-
console.log("[auth] resolve_identity:", JSON.stringify(existing));
|
|
964
|
-
|
|
965
|
-
if (existing?.result?.found && existing?.result?.user?.tenantId) {
|
|
966
|
-
// Returning user — generate token and go to dashboard
|
|
967
|
-
console.log("[auth] Returning user, generating token...");
|
|
968
|
-
const mcpToken = await generateMcpToken();
|
|
969
|
-
return sessionRedirect(`${baseUrl}/dashboard`, {
|
|
970
|
-
userId: existing.result.user.id,
|
|
971
|
-
tenantId: existing.result.user.tenantId,
|
|
972
|
-
email: existing.result.user.email,
|
|
973
|
-
name: existing.result.user.name,
|
|
974
|
-
token: mcpToken,
|
|
975
|
-
});
|
|
976
|
-
}
|
|
977
|
-
|
|
978
|
-
// Check if Slack team already has a tenant
|
|
979
|
-
if (teamId) {
|
|
980
|
-
console.log("[auth] Checking tenant_identities for team:", teamId);
|
|
981
|
-
try {
|
|
982
|
-
// Direct DB query via the auth store's underlying connection
|
|
983
|
-
// We'll use a simple fetch to our own MCP endpoint to call @db-connections
|
|
984
|
-
// Actually, simpler: just query the DB directly via the server's context
|
|
985
|
-
// For now, use registry.call to a custom tool or direct SQL
|
|
986
|
-
// Simplest: call @auth list_tenants and check metadata
|
|
987
|
-
// Even simpler: direct SQL via globalThis.fetch to ourselves
|
|
988
|
-
const dbUrl = process.env.DATABASE_URL;
|
|
989
|
-
if (dbUrl) {
|
|
990
|
-
const { default: postgres } = await import("postgres");
|
|
991
|
-
const sql = postgres(dbUrl);
|
|
992
|
-
const rows = await sql`SELECT tenant_id FROM tenant_identities WHERE provider = 'slack' AND provider_org_id = ${teamId} LIMIT 1`;
|
|
993
|
-
await sql.end();
|
|
994
|
-
if (rows.length > 0) {
|
|
995
|
-
const existingTenantId = rows[0].tenant_id;
|
|
996
|
-
console.log("[auth] Found existing tenant for team:", existingTenantId);
|
|
997
|
-
|
|
998
|
-
// Create user on existing tenant
|
|
999
|
-
const userRes = await registry.call({ action: "execute_tool", path: "@users", callerType: "system", tool: "create_user", params: {
|
|
1000
|
-
email: profile.email, name: profile.name, tenantId: existingTenantId,
|
|
1001
|
-
}} as any) as any;
|
|
1002
|
-
const newUserId = userRes?.result?.id || userRes?.result?.user?.id;
|
|
1003
|
-
console.log("[auth] Created user on existing tenant:", newUserId);
|
|
1004
|
-
|
|
1005
|
-
// Link identity
|
|
1006
|
-
if (newUserId) {
|
|
1007
|
-
await registry.call({ action: "execute_tool", path: "@users", callerType: "system", tool: "link_identity", params: {
|
|
1008
|
-
userId: newUserId, provider: "slack", providerUserId: profile.sub,
|
|
1009
|
-
email: profile.email, name: profile.name,
|
|
1010
|
-
metadata: { slackTeamId: teamId, slackTeamName: teamName },
|
|
1011
|
-
}} as any);
|
|
1012
|
-
}
|
|
1013
|
-
|
|
1014
|
-
// Generate token and go to dashboard
|
|
1015
|
-
const mcpToken = await generateMcpToken();
|
|
1016
|
-
return sessionRedirect(`${baseUrl}/dashboard`, {
|
|
1017
|
-
userId: newUserId, tenantId: existingTenantId,
|
|
1018
|
-
email: profile.email, name: profile.name, token: mcpToken,
|
|
1019
|
-
});
|
|
1020
|
-
}
|
|
1021
|
-
}
|
|
1022
|
-
} catch (e: any) {
|
|
1023
|
-
console.error("[auth] tenant_identity lookup error:", e.message);
|
|
1024
|
-
}
|
|
1025
|
-
}
|
|
1026
|
-
|
|
1027
|
-
// New user — redirect to setup
|
|
1028
|
-
return sessionRedirect(`${baseUrl}/setup`, {
|
|
1029
|
-
email: profile.email,
|
|
1030
|
-
name: profile.name,
|
|
1031
|
-
picture: profile.picture,
|
|
1032
|
-
slackUserId: profile.sub,
|
|
1033
|
-
slackTeamId: teamId,
|
|
1034
|
-
slackTeamName: teamName,
|
|
1035
|
-
});
|
|
1036
|
-
} catch (err: any) {
|
|
1037
|
-
console.error("[auth] callback error:", err);
|
|
1038
|
-
return Response.redirect(`${baseUrl}/?error=oauth_failed`, 302);
|
|
1039
|
-
}
|
|
651
|
+
// ── POST /oauth/token → OAuth2 client_credentials ──
|
|
652
|
+
if (path === "/oauth/token" && req.method === "POST") {
|
|
653
|
+
const res = await handleOAuthToken(req);
|
|
654
|
+
return cors ? addCors(res) : res;
|
|
1040
655
|
}
|
|
1041
656
|
|
|
1042
|
-
// GET /
|
|
1043
|
-
if (path === "/
|
|
1044
|
-
const
|
|
1045
|
-
|
|
1046
|
-
return htmlRes(renderTenantPage(baseUrl, session.email, session.name || ""));
|
|
657
|
+
// ── GET /health → Health check ──
|
|
658
|
+
if (path === "/health" && req.method === "GET") {
|
|
659
|
+
const res = jsonResponse({ status: "ok", agents: registry.listPaths() });
|
|
660
|
+
return cors ? addCors(res) : res;
|
|
1047
661
|
}
|
|
1048
662
|
|
|
1049
|
-
//
|
|
1050
|
-
if (path === "/
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
// 1. Create tenant
|
|
1057
|
-
const tenantRes = await registry.call({ action: "execute_tool", path: "@auth", callerType: "system", tool: "create_tenant", params: { name: body.tenant } } as any) as any;
|
|
1058
|
-
const tenantId = tenantRes?.result?.tenantId;
|
|
1059
|
-
if (!tenantId) return addCors(jsonResponse({ error: "Failed to create tenant" }, 400));
|
|
1060
|
-
console.log("[setup] tenant created:", tenantId);
|
|
1061
|
-
|
|
1062
|
-
// 2. Create user
|
|
1063
|
-
const userRes = await registry.call({ action: "execute_tool", path: "@users", callerType: "system", tool: "create_user", params: { email: body.email, name: session?.name, tenantId } } as any) as any;
|
|
1064
|
-
const userId = userRes?.result?.id || userRes?.result?.user?.id;
|
|
1065
|
-
console.log("[setup] user created:", userId);
|
|
1066
|
-
|
|
1067
|
-
// 2b. Link tenant to Slack team
|
|
1068
|
-
if (session?.slackTeamId) {
|
|
1069
|
-
try {
|
|
1070
|
-
const dbUrl = process.env.DATABASE_URL;
|
|
1071
|
-
if (dbUrl) {
|
|
1072
|
-
const { default: postgres } = await import("postgres");
|
|
1073
|
-
const sql = postgres(dbUrl);
|
|
1074
|
-
const id = "ti_" + Math.random().toString(36).slice(2, 14);
|
|
1075
|
-
await sql`INSERT INTO tenant_identities (id, tenant_id, provider, provider_org_id, name) VALUES (${id}, ${tenantId}, 'slack', ${session.slackTeamId}, ${session.slackTeamName || ''})`;
|
|
1076
|
-
await sql.end();
|
|
1077
|
-
console.log("[setup] Created tenant_identity for slack team:", session.slackTeamId);
|
|
1078
|
-
}
|
|
1079
|
-
} catch (e: any) { console.error("[setup] tenant_identity insert error:", e.message); }
|
|
1080
|
-
}
|
|
1081
|
-
|
|
1082
|
-
// 3. Link Slack identity
|
|
1083
|
-
if (session?.slackUserId && userId) {
|
|
1084
|
-
console.log("[setup] linking slack identity:", session.slackUserId);
|
|
1085
|
-
const linkRes = await registry.call({ action: "execute_tool", path: "@users", callerType: "system", tool: "link_identity", params: {
|
|
1086
|
-
userId,
|
|
1087
|
-
provider: "slack",
|
|
1088
|
-
providerUserId: session.slackUserId,
|
|
1089
|
-
email: body.email,
|
|
1090
|
-
name: session.name,
|
|
1091
|
-
metadata:{ slackTeamId: session.slackTeamId, slackTeamName: session.slackTeamName }, callerType: "system" }} as any);
|
|
1092
|
-
console.log("[setup] link_identity result:", JSON.stringify(linkRes));
|
|
1093
|
-
}
|
|
1094
|
-
|
|
1095
|
-
// 4. Generate MCP token
|
|
1096
|
-
const mcpToken = await generateMcpToken();
|
|
1097
|
-
console.log("[setup] token generated, length:", mcpToken.length);
|
|
1098
|
-
|
|
1099
|
-
return addCors(jsonResponse({ success: true, result: { tenantId, userId, token: mcpToken } }));
|
|
1100
|
-
} catch (err: any) {
|
|
1101
|
-
console.error("[setup] error:", err);
|
|
1102
|
-
return addCors(jsonResponse({ error: err.message }, 400));
|
|
1103
|
-
}
|
|
663
|
+
// ── GET /.well-known/jwks.json → JWKS public keys ──
|
|
664
|
+
if (path === "/.well-known/jwks.json" && req.method === "GET") {
|
|
665
|
+
const jwks = serverSigningKeys.length > 0
|
|
666
|
+
? await buildJwks(serverSigningKeys)
|
|
667
|
+
: { keys: [] };
|
|
668
|
+
const res = jsonResponse(jwks);
|
|
669
|
+
return cors ? addCors(res) : res;
|
|
1104
670
|
}
|
|
1105
671
|
|
|
1106
|
-
//
|
|
1107
|
-
if (path === "/
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
}
|
|
672
|
+
// ── GET /.well-known/configuration → Server discovery ──
|
|
673
|
+
if (path === "/.well-known/configuration" && req.method === "GET") {
|
|
674
|
+
const baseUrl = new URL(req.url).origin;
|
|
675
|
+
const res = jsonResponse({
|
|
676
|
+
issuer: baseUrl,
|
|
677
|
+
jwks_uri: `${baseUrl}/.well-known/jwks.json`,
|
|
678
|
+
token_endpoint: `${baseUrl}/oauth/token`,
|
|
679
|
+
agents_endpoint: `${baseUrl}/list`,
|
|
680
|
+
call_endpoint: baseUrl,
|
|
681
|
+
supported_grant_types: ["client_credentials"],
|
|
682
|
+
agents: registry.listPaths(),
|
|
1114
683
|
});
|
|
684
|
+
return cors ? addCors(res) : res;
|
|
1115
685
|
}
|
|
1116
686
|
|
|
1117
|
-
// GET /
|
|
1118
|
-
if (path === "/
|
|
1119
|
-
const
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
687
|
+
// ── GET /list → List agents (legacy endpoint) ──
|
|
688
|
+
if (path === "/list" && req.method === "GET") {
|
|
689
|
+
const agents = registry.list();
|
|
690
|
+
const visible = agents.filter((agent) => canSeeAgent(agent, effectiveAuth));
|
|
691
|
+
const res = jsonResponse(
|
|
692
|
+
visible.map((agent) => ({
|
|
693
|
+
path: agent.path,
|
|
694
|
+
name: agent.config?.name,
|
|
695
|
+
description: agent.config?.description,
|
|
696
|
+
supportedActions: agent.config?.supportedActions,
|
|
697
|
+
integration: agent.config?.integration || null,
|
|
698
|
+
tools: agent.tools
|
|
699
|
+
.filter((t) => {
|
|
700
|
+
const tv = t.visibility ?? "internal";
|
|
701
|
+
if (effectiveAuth?.isRoot) return true;
|
|
702
|
+
if (tv === "public") return true;
|
|
703
|
+
if (tv === "internal" && effectiveAuth) return true;
|
|
704
|
+
return false;
|
|
705
|
+
})
|
|
706
|
+
.map((t) => ({
|
|
707
|
+
name: t.name,
|
|
708
|
+
description: t.description,
|
|
709
|
+
})),
|
|
710
|
+
})),
|
|
711
|
+
);
|
|
712
|
+
return cors ? addCors(res) : res;
|
|
1131
713
|
}
|
|
1132
714
|
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
},
|
|
715
|
+
// ── Not found ──
|
|
716
|
+
const res = jsonResponse(
|
|
717
|
+
{
|
|
718
|
+
jsonrpc: "2.0",
|
|
719
|
+
id: null,
|
|
720
|
+
error: {
|
|
721
|
+
code: -32601,
|
|
722
|
+
message: `Not found: ${req.method} ${path}`,
|
|
1142
723
|
},
|
|
1143
|
-
|
|
1144
|
-
|
|
724
|
+
},
|
|
725
|
+
404,
|
|
1145
726
|
);
|
|
727
|
+
return cors ? addCors(res) : res;
|
|
1146
728
|
} catch (err) {
|
|
1147
729
|
console.error("[server] Request error:", err);
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
730
|
+
const res = jsonResponse(
|
|
731
|
+
{
|
|
732
|
+
jsonrpc: "2.0",
|
|
733
|
+
id: null,
|
|
734
|
+
error: {
|
|
735
|
+
code: -32603,
|
|
736
|
+
message: err instanceof Error ? err.message : "Internal error",
|
|
1154
737
|
},
|
|
1155
|
-
|
|
1156
|
-
|
|
738
|
+
},
|
|
739
|
+
500,
|
|
1157
740
|
);
|
|
741
|
+
return cors ? addCors(res) : res;
|
|
1158
742
|
}
|
|
1159
743
|
}
|
|
1160
744
|
|
|
@@ -1162,38 +746,45 @@ export function createAgentServer(
|
|
|
1162
746
|
// Server lifecycle
|
|
1163
747
|
// ──────────────────────────────────────────
|
|
1164
748
|
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
749
|
+
return {
|
|
750
|
+
url: null,
|
|
751
|
+
registry,
|
|
752
|
+
|
|
753
|
+
async start() {
|
|
754
|
+
// Load or generate signing key for JWKS
|
|
755
|
+
if (options.signingKey) {
|
|
756
|
+
serverSigningKeys.push(options.signingKey);
|
|
757
|
+
} else if (authConfig?.store?.getSigningKeys) {
|
|
758
|
+
const stored = await authConfig.store.getSigningKeys() ?? [];
|
|
759
|
+
for (const exported of stored) {
|
|
760
|
+
serverSigningKeys.push(await importSigningKey(exported));
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
if (serverSigningKeys.length === 0) {
|
|
764
|
+
const key = await generateSigningKey();
|
|
765
|
+
serverSigningKeys.push(key);
|
|
766
|
+
if (authConfig?.store?.storeSigningKey) {
|
|
767
|
+
await authConfig.store.storeSigningKey(await exportSigningKey(key));
|
|
768
|
+
}
|
|
1179
769
|
}
|
|
1180
|
-
|
|
770
|
+
|
|
771
|
+
serverInstance = Bun.serve({
|
|
772
|
+
port,
|
|
773
|
+
hostname,
|
|
774
|
+
fetch,
|
|
775
|
+
});
|
|
776
|
+
(this as any).url = `http://${hostname}:${port}`;
|
|
777
|
+
console.log(`[agents-sdk] Server listening on http://${hostname}:${port}`);
|
|
1181
778
|
},
|
|
1182
779
|
|
|
1183
|
-
async stop()
|
|
780
|
+
async stop() {
|
|
1184
781
|
if (serverInstance) {
|
|
1185
782
|
serverInstance.stop();
|
|
1186
783
|
serverInstance = null;
|
|
1187
|
-
|
|
784
|
+
(this as any).url = null;
|
|
1188
785
|
}
|
|
1189
786
|
},
|
|
1190
787
|
|
|
1191
788
|
fetch,
|
|
1192
|
-
|
|
1193
|
-
get url(): string | null {
|
|
1194
|
-
return serverUrl;
|
|
1195
|
-
},
|
|
1196
789
|
};
|
|
1197
|
-
|
|
1198
|
-
return server;
|
|
1199
790
|
}
|