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