@slashfi/agents-sdk 0.11.2 → 0.13.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 -1
- package/dist/agent-definitions/auth.d.ts.map +1 -1
- package/dist/agent-definitions/auth.js +123 -4
- package/dist/agent-definitions/auth.js.map +1 -1
- package/dist/agent-definitions/integrations.d.ts +2 -14
- package/dist/agent-definitions/integrations.d.ts.map +1 -1
- package/dist/agent-definitions/integrations.js +64 -19
- package/dist/agent-definitions/integrations.js.map +1 -1
- package/dist/agent-definitions/remote-registry.d.ts +19 -14
- package/dist/agent-definitions/remote-registry.d.ts.map +1 -1
- package/dist/agent-definitions/remote-registry.js +219 -381
- package/dist/agent-definitions/remote-registry.js.map +1 -1
- package/dist/agent-definitions/users.d.ts.map +1 -1
- package/dist/agent-definitions/users.js +29 -1
- package/dist/agent-definitions/users.js.map +1 -1
- package/dist/define.d.ts +6 -4
- package/dist/define.d.ts.map +1 -1
- package/dist/define.js +82 -3
- package/dist/define.js.map +1 -1
- package/dist/index.d.ts +3 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/jwt.js +1 -1
- package/dist/jwt.js.map +1 -1
- package/dist/key-manager.d.ts +76 -0
- package/dist/key-manager.d.ts.map +1 -0
- package/dist/key-manager.js +156 -0
- package/dist/key-manager.js.map +1 -0
- package/dist/server.d.ts +39 -0
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +228 -52
- package/dist/server.js.map +1 -1
- package/dist/types.d.ts +53 -1
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/agent-definitions/auth.ts +165 -6
- package/src/agent-definitions/integrations.ts +59 -28
- package/src/agent-definitions/remote-registry.ts +219 -513
- package/src/agent-definitions/users.ts +35 -1
- package/src/define.ts +98 -6
- package/src/index.ts +3 -1
- package/src/jwt.ts +1 -1
- package/src/key-manager.test.ts +273 -0
- package/src/key-manager.ts +257 -0
- package/src/server.test.ts +284 -0
- package/src/server.ts +334 -60
- package/src/types.ts +44 -1
|
@@ -1,25 +1,26 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Remote Registry Agent
|
|
2
|
+
* Remote Registry Agent (JWKS Auth)
|
|
3
3
|
*
|
|
4
4
|
* Integration agent for connecting to remote agent registries.
|
|
5
|
-
* Uses
|
|
6
|
-
*
|
|
5
|
+
* Uses JWKS trust exchange + jwt_exchange for authentication.
|
|
6
|
+
* No client credentials stored — uses signJwt for outbound calls.
|
|
7
7
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
8
|
+
* Flow:
|
|
9
|
+
* setup: discover JWKS → add trusted issuer → call @auth/create_tenant → store connection
|
|
10
|
+
* connect: POST /oauth/token jwt_exchange → identity_required → /oauth/authorize → linked
|
|
11
|
+
* proxy: sign JWT → POST to remote MCP endpoint
|
|
12
12
|
*
|
|
13
13
|
* @example
|
|
14
14
|
* ```typescript
|
|
15
|
-
* import { createRemoteRegistryAgent,
|
|
15
|
+
* import { createRemoteRegistryAgent, createAgentServer } from '@slashfi/agents-sdk';
|
|
16
16
|
*
|
|
17
|
-
* const
|
|
18
|
-
*
|
|
17
|
+
* const server = createAgentServer(registry, { ... });
|
|
18
|
+
* await server.start();
|
|
19
19
|
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
20
|
+
* registry.register(createRemoteRegistryAgent({
|
|
21
|
+
* secretStore,
|
|
22
|
+
* signJwt: (claims) => server.signJwt(claims),
|
|
23
|
+
* }));
|
|
23
24
|
* ```
|
|
24
25
|
*/
|
|
25
26
|
|
|
@@ -32,590 +33,295 @@ import type {
|
|
|
32
33
|
} from "../types.js";
|
|
33
34
|
import type { SecretStore } from "./secrets.js";
|
|
34
35
|
|
|
35
|
-
// ============================================
|
|
36
|
-
// Types
|
|
37
|
-
// ============================================
|
|
38
|
-
|
|
39
36
|
export interface RemoteRegistryAgentOptions {
|
|
40
|
-
/** Secret store for persisting registry
|
|
37
|
+
/** Secret store for persisting registry connections */
|
|
41
38
|
secretStore: SecretStore;
|
|
39
|
+
/** Sign a JWT with this server's keys for outbound calls */
|
|
40
|
+
signJwt: (claims: Record<string, unknown>) => Promise<string>;
|
|
41
|
+
/** Add a trusted JWKS issuer (optional — for bidirectional trust) */
|
|
42
|
+
addTrustedIssuer?: (issuerUrl: string) => Promise<void>;
|
|
42
43
|
}
|
|
43
44
|
|
|
44
45
|
/** Stored connection to a remote registry */
|
|
45
46
|
interface RegistryConnection {
|
|
46
|
-
/** Registry identifier (user-chosen name) */
|
|
47
47
|
id: string;
|
|
48
|
-
/** Display name */
|
|
49
48
|
name: string;
|
|
50
|
-
/** Registry base URL */
|
|
51
49
|
url: string;
|
|
52
|
-
/** Tenant ID on the remote registry */
|
|
53
50
|
remoteTenantId: string;
|
|
54
|
-
/** Client ID for authentication */
|
|
55
|
-
clientId: string;
|
|
56
|
-
/** When the connection was created */
|
|
57
51
|
createdAt: number;
|
|
58
52
|
}
|
|
59
53
|
|
|
60
|
-
|
|
61
|
-
// ============================================
|
|
62
|
-
// Helpers
|
|
63
|
-
// ============================================
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
/**
|
|
67
|
-
* Make an MCP JSON-RPC call to a remote registry.
|
|
68
|
-
*/
|
|
69
|
-
async function mcpCall(
|
|
70
|
-
url: string,
|
|
71
|
-
token: string,
|
|
72
|
-
request: {
|
|
73
|
-
action: string;
|
|
74
|
-
path: string;
|
|
75
|
-
tool: string;
|
|
76
|
-
params?: Record<string, unknown>;
|
|
77
|
-
},
|
|
78
|
-
): Promise<any> {
|
|
79
|
-
const res = await globalThis.fetch(url, {
|
|
80
|
-
method: "POST",
|
|
81
|
-
headers: {
|
|
82
|
-
"Content-Type": "application/json",
|
|
83
|
-
Authorization: `Bearer ${token}`,
|
|
84
|
-
},
|
|
85
|
-
body: JSON.stringify({
|
|
86
|
-
jsonrpc: "2.0",
|
|
87
|
-
id: Date.now(),
|
|
88
|
-
method: "tools/call",
|
|
89
|
-
params: {
|
|
90
|
-
name: "call_agent",
|
|
91
|
-
arguments: {
|
|
92
|
-
request: {
|
|
93
|
-
action: request.action,
|
|
94
|
-
path: request.path,
|
|
95
|
-
tool: request.tool,
|
|
96
|
-
params: request.params ?? {},
|
|
97
|
-
},
|
|
98
|
-
},
|
|
99
|
-
},
|
|
100
|
-
}),
|
|
101
|
-
});
|
|
102
|
-
|
|
103
|
-
if (!res.ok) {
|
|
104
|
-
throw new Error(`Registry call failed: ${res.status} ${res.statusText}`);
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
const json = (await res.json()) as any;
|
|
108
|
-
if (json.error) {
|
|
109
|
-
throw new Error(
|
|
110
|
-
`Registry RPC error: ${json.error.message ?? JSON.stringify(json.error)}`,
|
|
111
|
-
);
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
// Parse the tool result from MCP response
|
|
115
|
-
const text = json?.result?.content?.[0]?.text;
|
|
116
|
-
if (!text) return json?.result;
|
|
117
|
-
try {
|
|
118
|
-
return JSON.parse(text);
|
|
119
|
-
} catch {
|
|
120
|
-
return { raw: text };
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
/**
|
|
125
|
-
* Get an access token from a remote registry via /oauth/token.
|
|
126
|
-
*/
|
|
127
|
-
async function getRegistryToken(
|
|
128
|
-
url: string,
|
|
129
|
-
clientId: string,
|
|
130
|
-
clientSecret: string,
|
|
131
|
-
): Promise<string> {
|
|
132
|
-
const tokenUrl = url.replace(/\/$/, "") + "/oauth/token";
|
|
133
|
-
const res = await globalThis.fetch(tokenUrl, {
|
|
134
|
-
method: "POST",
|
|
135
|
-
headers: { "Content-Type": "application/json" },
|
|
136
|
-
body: JSON.stringify({
|
|
137
|
-
grant_type: "client_credentials",
|
|
138
|
-
client_id: clientId,
|
|
139
|
-
client_secret: clientSecret,
|
|
140
|
-
}),
|
|
141
|
-
});
|
|
142
|
-
|
|
143
|
-
if (!res.ok) {
|
|
144
|
-
const body = await res.text();
|
|
145
|
-
throw new Error(`Token exchange failed: ${res.status} ${body}`);
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
const json = (await res.json()) as { access_token: string };
|
|
149
|
-
return json.access_token;
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
// ============================================
|
|
153
|
-
// Create Remote Registry Agent
|
|
154
|
-
// ============================================
|
|
54
|
+
const ENTITY_TYPE = "remote-registry-connections";
|
|
155
55
|
|
|
156
56
|
export function createRemoteRegistryAgent(
|
|
157
57
|
options: RemoteRegistryAgentOptions,
|
|
158
58
|
): AgentDefinition {
|
|
159
|
-
const { secretStore } = options;
|
|
59
|
+
const { secretStore, signJwt, addTrustedIssuer } = options;
|
|
160
60
|
|
|
161
|
-
//
|
|
162
|
-
// The secret ID is stored via associate/resolveByEntity for lookup.
|
|
163
|
-
const ENTITY_TYPE = "remote-registry-connections";
|
|
61
|
+
// --- Connection storage (KV via SecretStore) ---
|
|
164
62
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
*/
|
|
168
|
-
async function storeConnection(
|
|
169
|
-
ownerId: string,
|
|
170
|
-
conn: RegistryConnection,
|
|
171
|
-
clientSecret: string,
|
|
172
|
-
): Promise<void> {
|
|
173
|
-
// Load existing connections, update, and store back
|
|
63
|
+
async function storeConnection(ownerId: string, conn: RegistryConnection): Promise<void> {
|
|
64
|
+
console.error("[remote-registry] storeConnection for owner:", ownerId, "conn:", conn.id);
|
|
174
65
|
const all = await loadAllConnections(ownerId);
|
|
175
|
-
all[conn.id] =
|
|
66
|
+
all[conn.id] = conn;
|
|
176
67
|
const value = JSON.stringify(all);
|
|
177
|
-
|
|
178
|
-
// Store the blob
|
|
179
68
|
const scope = { tenantId: ownerId };
|
|
180
69
|
const secretId = await secretStore.store(value, ownerId);
|
|
181
|
-
|
|
182
|
-
// Link it so we can find it later
|
|
183
70
|
if (secretStore.associate) {
|
|
184
71
|
await secretStore.associate(secretId, ENTITY_TYPE, ownerId, scope);
|
|
185
72
|
}
|
|
186
73
|
}
|
|
187
74
|
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
*/
|
|
191
|
-
async function loadAllConnections(
|
|
192
|
-
ownerId: string,
|
|
193
|
-
): Promise<Record<string, RegistryConnection & { clientSecret: string }>> {
|
|
194
|
-
// Try resolveByEntity first (v0.7.0+)
|
|
75
|
+
async function loadAllConnections(ownerId: string): Promise<Record<string, RegistryConnection>> {
|
|
76
|
+
console.error("[remote-registry] loadAllConnections for owner:", ownerId);
|
|
195
77
|
if (secretStore.resolveByEntity) {
|
|
196
78
|
const scope = { tenantId: ownerId };
|
|
197
79
|
const secretIds = await secretStore.resolveByEntity(ENTITY_TYPE, ownerId, scope);
|
|
198
|
-
if (secretIds
|
|
199
|
-
|
|
200
|
-
const latestId = secretIds[secretIds.length - 1];
|
|
201
|
-
const raw = await secretStore.resolve(latestId, ownerId);
|
|
80
|
+
if (secretIds?.length) {
|
|
81
|
+
const raw = await secretStore.resolve(secretIds[secretIds.length - 1], ownerId);
|
|
202
82
|
if (raw) {
|
|
203
|
-
try {
|
|
204
|
-
return JSON.parse(raw);
|
|
205
|
-
} catch {
|
|
206
|
-
return {};
|
|
207
|
-
}
|
|
83
|
+
try { return JSON.parse(raw); } catch { return {}; }
|
|
208
84
|
}
|
|
209
85
|
}
|
|
210
86
|
}
|
|
211
87
|
return {};
|
|
212
88
|
}
|
|
213
89
|
|
|
214
|
-
|
|
215
|
-
* Load a registry connection.
|
|
216
|
-
*/
|
|
217
|
-
async function loadConnection(
|
|
218
|
-
ownerId: string,
|
|
219
|
-
registryId: string,
|
|
220
|
-
): Promise<{ conn: RegistryConnection; clientSecret: string } | null> {
|
|
90
|
+
async function loadConnection(ownerId: string, registryId: string): Promise<RegistryConnection | null> {
|
|
221
91
|
const all = await loadAllConnections(ownerId);
|
|
222
|
-
|
|
223
|
-
if (!entry) return null;
|
|
224
|
-
const { clientSecret, ...conn } = entry;
|
|
225
|
-
return { conn, clientSecret };
|
|
92
|
+
return all[registryId] ?? null;
|
|
226
93
|
}
|
|
227
94
|
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
95
|
+
// --- MCP call helper ---
|
|
96
|
+
|
|
97
|
+
async function mcpCall(url: string, jwt: string, request: Record<string, unknown>): Promise<any> {
|
|
98
|
+
const res = await globalThis.fetch(url, {
|
|
99
|
+
method: "POST",
|
|
100
|
+
headers: {
|
|
101
|
+
"Content-Type": "application/json",
|
|
102
|
+
Authorization: `Bearer ${jwt}`,
|
|
103
|
+
},
|
|
104
|
+
body: JSON.stringify({
|
|
105
|
+
jsonrpc: "2.0",
|
|
106
|
+
id: Date.now(),
|
|
107
|
+
method: "tools/call",
|
|
108
|
+
params: { name: "call_agent", arguments: { request } },
|
|
109
|
+
}),
|
|
110
|
+
});
|
|
111
|
+
const rpc = await res.json() as any;
|
|
112
|
+
const text = rpc.result?.content?.[0]?.text;
|
|
113
|
+
return text ? JSON.parse(text) : rpc.result;
|
|
236
114
|
}
|
|
237
115
|
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
async function getAuthenticatedToken(
|
|
116
|
+
// --- Proxy: sign JWT and call remote ---
|
|
117
|
+
|
|
118
|
+
async function proxyCall(
|
|
242
119
|
ownerId: string,
|
|
243
120
|
registryId: string,
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
);
|
|
121
|
+
request: { action: string; path: string; tool: string; params?: Record<string, unknown> },
|
|
122
|
+
): Promise<{ success: boolean; result?: any; error?: string }> {
|
|
123
|
+
const conn = await loadConnection(ownerId, registryId);
|
|
124
|
+
if (!conn) {
|
|
125
|
+
return { success: false, error: `No connection '${registryId}'. Use setup_integration first.` };
|
|
250
126
|
}
|
|
251
|
-
const
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
data.clientSecret,
|
|
255
|
-
);
|
|
256
|
-
return { token, conn: data.conn };
|
|
127
|
+
const jwt = await signJwt({ tenantId: conn.remoteTenantId, action: "proxy", type: "agent-registry" });
|
|
128
|
+
const result = await mcpCall(conn.url, jwt, request);
|
|
129
|
+
return { success: true, result };
|
|
257
130
|
}
|
|
258
131
|
|
|
259
|
-
//
|
|
132
|
+
// --- Tools ---
|
|
260
133
|
|
|
261
|
-
const
|
|
262
|
-
name: "
|
|
263
|
-
description:
|
|
264
|
-
"Make an authenticated MCP call to a remote agent registry. " +
|
|
265
|
-
"Proxies the request with the stored tenant credentials.",
|
|
134
|
+
const proxyTool = defineTool({
|
|
135
|
+
name: "proxy_call",
|
|
136
|
+
description: "Proxy an MCP call to a connected remote registry.",
|
|
266
137
|
visibility: "public" as const,
|
|
267
138
|
inputSchema: {
|
|
268
139
|
type: "object" as const,
|
|
269
140
|
properties: {
|
|
270
|
-
registryId: {
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
},
|
|
274
|
-
|
|
275
|
-
type: "string",
|
|
276
|
-
description: "Agent path on the remote registry (e.g. '@integrations')",
|
|
277
|
-
},
|
|
278
|
-
action: {
|
|
279
|
-
type: "string",
|
|
280
|
-
description: "Action to perform (e.g. 'execute_tool')",
|
|
281
|
-
},
|
|
282
|
-
tool: {
|
|
283
|
-
type: "string",
|
|
284
|
-
description: "Tool name to call",
|
|
285
|
-
},
|
|
286
|
-
params: {
|
|
287
|
-
type: "object",
|
|
288
|
-
description: "Tool parameters",
|
|
289
|
-
},
|
|
141
|
+
registryId: { type: "string", description: "Registry connection ID" },
|
|
142
|
+
action: { type: "string" },
|
|
143
|
+
path: { type: "string" },
|
|
144
|
+
tool: { type: "string" },
|
|
145
|
+
params: { type: "object" },
|
|
290
146
|
},
|
|
291
|
-
required: ["registryId", "
|
|
147
|
+
required: ["registryId", "action", "path", "tool"],
|
|
292
148
|
},
|
|
293
|
-
execute: async (
|
|
294
|
-
input
|
|
295
|
-
registryId: string;
|
|
296
|
-
agentPath: string;
|
|
297
|
-
action: string;
|
|
298
|
-
tool: string;
|
|
299
|
-
params?: Record<string, unknown>;
|
|
300
|
-
},
|
|
301
|
-
ctx: ToolContext,
|
|
302
|
-
) => {
|
|
303
|
-
const { token, conn } = await getAuthenticatedToken(
|
|
304
|
-
ctx.callerId,
|
|
305
|
-
input.registryId,
|
|
306
|
-
);
|
|
307
|
-
return mcpCall(conn.url, token, {
|
|
149
|
+
execute: async (input: any, _ctx: ToolContext) => {
|
|
150
|
+
return proxyCall("system", input.registryId, {
|
|
308
151
|
action: input.action,
|
|
309
|
-
path: input.
|
|
152
|
+
path: input.path,
|
|
310
153
|
tool: input.tool,
|
|
311
154
|
params: input.params,
|
|
312
155
|
});
|
|
313
156
|
},
|
|
314
157
|
});
|
|
315
158
|
|
|
316
|
-
const
|
|
317
|
-
name: "
|
|
318
|
-
description: "List
|
|
159
|
+
const listTool = defineTool({
|
|
160
|
+
name: "list_connections",
|
|
161
|
+
description: "List all connected remote registries.",
|
|
319
162
|
visibility: "public" as const,
|
|
320
|
-
inputSchema: {
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
163
|
+
inputSchema: { type: "object" as const, properties: {} },
|
|
164
|
+
execute: async (_input: any, _ctx: ToolContext) => {
|
|
165
|
+
const all = await loadAllConnections("system");
|
|
166
|
+
return {
|
|
167
|
+
connections: Object.values(all).map(c => ({
|
|
168
|
+
id: c.id,
|
|
169
|
+
name: c.name,
|
|
170
|
+
url: c.url,
|
|
171
|
+
remoteTenantId: c.remoteTenantId,
|
|
172
|
+
})),
|
|
173
|
+
};
|
|
329
174
|
},
|
|
330
|
-
|
|
331
|
-
input: { registryId: string },
|
|
332
|
-
ctx: ToolContext,
|
|
333
|
-
) => {
|
|
334
|
-
const { token, conn } = await getAuthenticatedToken(
|
|
335
|
-
ctx.callerId,
|
|
336
|
-
input.registryId,
|
|
337
|
-
);
|
|
175
|
+
});
|
|
338
176
|
|
|
339
|
-
const listUrl = conn.url.replace(/\/$/, "") + "/list";
|
|
340
|
-
const res = await globalThis.fetch(listUrl, {
|
|
341
|
-
headers: { Authorization: `Bearer ${token}` },
|
|
342
|
-
});
|
|
343
177
|
|
|
344
|
-
|
|
345
|
-
|
|
178
|
+
// Extract setup/connect as standalone functions to avoid circular reference
|
|
179
|
+
const setupFn = async (params: Record<string, unknown>, _ctx: IntegrationMethodContext): Promise<IntegrationMethodResult> => {
|
|
180
|
+
console.log("[remote-registry] setupFn called with:", JSON.stringify(params));
|
|
181
|
+
const url = params.url as string;
|
|
182
|
+
const name = (params.name as string) ?? "registry";
|
|
183
|
+
const oidcUserId = params.oidcUserId as string | undefined;
|
|
184
|
+
if (!url) return { success: false, error: "url is required" };
|
|
185
|
+
try {
|
|
186
|
+
const baseUrl = url.replace(/\/$/, "");
|
|
187
|
+
|
|
188
|
+
// Phase 2: Complete setup after OIDC — store connection with resolved tenant
|
|
189
|
+
if (oidcUserId) {
|
|
190
|
+
const jwt = await signJwt({ sub: oidcUserId, action: "setup", type: "agent-registry" });
|
|
191
|
+
const tokenRes = await globalThis.fetch(baseUrl + "/oauth/token", {
|
|
192
|
+
method: "POST", headers: { "Content-Type": "application/json" },
|
|
193
|
+
body: JSON.stringify({ grant_type: "jwt_exchange", assertion: jwt, scope: "setup", redirect_uri: params.redirect_uri ?? "" }),
|
|
194
|
+
});
|
|
195
|
+
const tokenData = await tokenRes.json() as any;
|
|
196
|
+
if (!tokenData.access_token && !tokenData.tenant_id) {
|
|
197
|
+
return { success: false, error: tokenData.error_description ?? tokenData.error ?? "Setup completion failed" };
|
|
198
|
+
}
|
|
199
|
+
const remoteTenantId = tokenData.tenant_id ?? name;
|
|
200
|
+
const ownerId = "system";
|
|
201
|
+
await storeConnection(ownerId, { id: name, name, url: baseUrl, remoteTenantId, createdAt: Date.now() });
|
|
202
|
+
return { success: true, data: { registryId: name, url, remoteTenantId } };
|
|
346
203
|
}
|
|
347
204
|
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
205
|
+
// Phase 1: Discover JWKS, establish trust, then request OIDC
|
|
206
|
+
const configUrl = baseUrl + "/.well-known/configuration";
|
|
207
|
+
console.log("[setupFn] fetching config:", configUrl);
|
|
208
|
+
const configRes = await globalThis.fetch(configUrl);
|
|
209
|
+
console.log("[setupFn] config status:", configRes.status);
|
|
210
|
+
if (!configRes.ok) return { success: false, error: "Failed to discover registry at " + configUrl };
|
|
211
|
+
const remoteConfig = await configRes.json() as any;
|
|
212
|
+
if (remoteConfig.jwks_uri) {
|
|
213
|
+
console.log("[setupFn] fetching JWKS:", remoteConfig.jwks_uri);
|
|
214
|
+
const jwksRes = await globalThis.fetch(remoteConfig.jwks_uri);
|
|
215
|
+
console.log("[setupFn] JWKS status:", jwksRes.status);
|
|
216
|
+
if (!jwksRes.ok) return { success: false, error: "JWKS not reachable" };
|
|
217
|
+
}
|
|
218
|
+
if (addTrustedIssuer) { console.log("[setupFn] adding trusted issuer:", baseUrl); await addTrustedIssuer(baseUrl); console.log("[setupFn] added trusted issuer"); }
|
|
219
|
+
|
|
220
|
+
// Request identity — atlas will return authorize URL for Slack OIDC
|
|
221
|
+
console.log("[setupFn] Phase 1: requesting identity via jwt_exchange");
|
|
222
|
+
const jwt = await signJwt({ action: "setup", type: "agent-registry", targetUrl: url });
|
|
223
|
+
console.log("[setupFn] POSTing to:", baseUrl + "/oauth/token");
|
|
224
|
+
const tokenRes = await globalThis.fetch(baseUrl + "/oauth/token", {
|
|
225
|
+
method: "POST", headers: { "Content-Type": "application/json" },
|
|
226
|
+
body: JSON.stringify({ grant_type: "jwt_exchange", assertion: jwt, scope: "setup", redirect_uri: params.redirect_uri ?? "" }),
|
|
227
|
+
});
|
|
228
|
+
console.log("[setupFn] token status:", tokenRes.status);
|
|
229
|
+
const tokenData = await tokenRes.json() as any;
|
|
230
|
+
console.log("[setupFn] tokenData:", JSON.stringify(tokenData).substring(0, 300));
|
|
231
|
+
|
|
232
|
+
// If already set up (user linked), store connection directly
|
|
233
|
+
if (tokenData.access_token) {
|
|
234
|
+
const remoteTenantId = tokenData.tenant_id ?? name;
|
|
235
|
+
const ownerId = "system";
|
|
236
|
+
await storeConnection(ownerId, { id: name, name, url: baseUrl, remoteTenantId, createdAt: Date.now() });
|
|
237
|
+
return { success: true, data: { registryId: name, url, remoteTenantId } };
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// Need OIDC — return authorize URL to caller
|
|
241
|
+
if (tokenData.error === "identity_required") {
|
|
242
|
+
return { success: false, error: "identity_required", data: { authorizeUrl: tokenData.authorize_url, registryId: name, url } };
|
|
243
|
+
}
|
|
351
244
|
|
|
352
|
-
|
|
245
|
+
return { success: false, error: tokenData.error_description ?? tokenData.error ?? "Unexpected response from token endpoint" };
|
|
246
|
+
} catch (err) {
|
|
247
|
+
return { success: false, error: err instanceof Error ? err.message : String(err) };
|
|
248
|
+
}
|
|
249
|
+
};
|
|
250
|
+
|
|
251
|
+
const connectFn = async (params: Record<string, unknown>, ctx: IntegrationMethodContext): Promise<IntegrationMethodResult> => {
|
|
252
|
+
const registryId = params.registryId as string;
|
|
253
|
+
const redirectUri = (params.redirectUri as string) ?? "";
|
|
254
|
+
const oidcUserId = params.oidcUserId as string | undefined;
|
|
255
|
+
if (!registryId) return { success: false, error: "registryId is required" };
|
|
256
|
+
try {
|
|
257
|
+
const ownerId = "system"; // tenant-scoped
|
|
258
|
+
const conn = await loadConnection(ownerId, registryId);
|
|
259
|
+
if (!conn) return { success: false, error: "No connection '" + registryId + "'" };
|
|
260
|
+
// Use OIDC-issued identity if available, never send "anonymous" as sub
|
|
261
|
+
const sub = oidcUserId ?? (ctx.callerId !== "anonymous" ? ctx.callerId : undefined);
|
|
262
|
+
const jwt = await signJwt({ ...(sub ? { sub } : {}), tenantId: conn.remoteTenantId, action: "connect", type: "agent-registry" });
|
|
263
|
+
const tokenRes = await globalThis.fetch(conn.url + "/oauth/token", {
|
|
264
|
+
method: "POST", headers: { "Content-Type": "application/json" },
|
|
265
|
+
body: JSON.stringify({ grant_type: "jwt_exchange", assertion: jwt, redirect_uri: redirectUri }),
|
|
266
|
+
});
|
|
267
|
+
const tokenData = await tokenRes.json() as any;
|
|
268
|
+
if (tokenData.access_token) return { success: true, data: { registryId, accessToken: tokenData.access_token, userId: tokenData.user_id, tenantId: tokenData.tenant_id } };
|
|
269
|
+
if (tokenData.error === "identity_required") return { success: false, error: "identity_required", data: { authorizeUrl: tokenData.authorize_url, tenantId: tokenData.tenant_id } };
|
|
270
|
+
return { success: false, error: tokenData.error_description ?? tokenData.error ?? "Token exchange failed" };
|
|
271
|
+
} catch (err) {
|
|
272
|
+
return { success: false, error: err instanceof Error ? err.message : String(err) };
|
|
273
|
+
}
|
|
274
|
+
};
|
|
353
275
|
|
|
354
276
|
return defineAgent({
|
|
355
277
|
path: "@remote-registry",
|
|
356
278
|
entrypoint:
|
|
357
279
|
"You manage connections to remote agent registries. " +
|
|
358
|
-
"Use setup to connect a new registry, connect to
|
|
359
|
-
"and
|
|
280
|
+
"Use setup to connect a new registry, connect to link user identities, " +
|
|
281
|
+
"and proxy_call to make authenticated calls.",
|
|
360
282
|
config: {
|
|
361
283
|
name: "Remote Registry",
|
|
362
|
-
description:
|
|
363
|
-
"Connect to remote agent registries (MCP over HTTP) for federated integrations",
|
|
284
|
+
description: "Connect to remote agent registries via JWKS trust + jwt_exchange",
|
|
364
285
|
supportedActions: ["execute_tool", "describe_tools", "load"],
|
|
365
|
-
integration: {
|
|
366
|
-
provider: "remote-registry",
|
|
367
|
-
displayName: "Agent Registry",
|
|
368
|
-
icon: "server",
|
|
369
|
-
category: "infrastructure",
|
|
370
|
-
description:
|
|
371
|
-
"Connect to a remote agent registry to access its integrations, databases, and agents.",
|
|
372
|
-
},
|
|
373
286
|
},
|
|
374
287
|
visibility: "public",
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
}
|
|
386
|
-
|
|
387
|
-
try {
|
|
388
|
-
// 1. Create tenant on remote registry
|
|
389
|
-
const setupUrl = url.replace(/\/$/, "") + "/setup";
|
|
390
|
-
const setupRes = await globalThis.fetch(setupUrl, {
|
|
391
|
-
method: "POST",
|
|
392
|
-
headers: { "Content-Type": "application/json" },
|
|
393
|
-
body: JSON.stringify({ tenant: name }),
|
|
394
|
-
});
|
|
395
|
-
|
|
396
|
-
if (!setupRes.ok) {
|
|
397
|
-
const body = await setupRes.text();
|
|
398
|
-
return {
|
|
399
|
-
success: false,
|
|
400
|
-
error: `Failed to create tenant on registry: ${setupRes.status} ${body}`,
|
|
401
|
-
};
|
|
402
|
-
}
|
|
403
|
-
|
|
404
|
-
const setupResult = (await setupRes.json()) as {
|
|
405
|
-
success: boolean;
|
|
406
|
-
result?: {
|
|
407
|
-
tenantId: string;
|
|
408
|
-
token?: string;
|
|
409
|
-
};
|
|
410
|
-
};
|
|
411
|
-
|
|
412
|
-
if (!setupResult.success || !setupResult.result?.tenantId) {
|
|
413
|
-
return {
|
|
414
|
-
success: false,
|
|
415
|
-
error: "Registry /setup did not return a tenantId",
|
|
416
|
-
};
|
|
417
|
-
}
|
|
418
|
-
|
|
419
|
-
const remoteTenantId = setupResult.result.tenantId;
|
|
420
|
-
|
|
421
|
-
// 2. Register a client for this tenant
|
|
422
|
-
// Use the setup token (or root key) to create client credentials
|
|
423
|
-
const token = setupResult.result.token;
|
|
424
|
-
if (!token) {
|
|
425
|
-
return {
|
|
426
|
-
success: false,
|
|
427
|
-
error: "Registry /setup did not return a token for client creation",
|
|
428
|
-
};
|
|
429
|
-
}
|
|
430
|
-
|
|
431
|
-
const registerResult = await mcpCall(url, token, {
|
|
432
|
-
action: "execute_tool",
|
|
433
|
-
path: "@auth",
|
|
434
|
-
tool: "register",
|
|
435
|
-
params: {
|
|
436
|
-
name: `${name}-client`,
|
|
437
|
-
scopes: ["integrations", "secrets", "users"],
|
|
438
|
-
},
|
|
439
|
-
});
|
|
440
|
-
|
|
441
|
-
const clientId =
|
|
442
|
-
registerResult?.clientId ?? registerResult?.result?.clientId;
|
|
443
|
-
const clientSecret =
|
|
444
|
-
registerResult?.clientSecret?.value ??
|
|
445
|
-
registerResult?.result?.clientSecret?.value ??
|
|
446
|
-
registerResult?.clientSecret;
|
|
447
|
-
|
|
448
|
-
if (!clientId || !clientSecret) {
|
|
449
|
-
return {
|
|
450
|
-
success: false,
|
|
451
|
-
error: `Failed to register client: ${JSON.stringify(registerResult)}`,
|
|
452
|
-
};
|
|
453
|
-
}
|
|
454
|
-
|
|
455
|
-
// 3. Store connection
|
|
456
|
-
const conn: RegistryConnection = {
|
|
457
|
-
id: name,
|
|
458
|
-
name,
|
|
459
|
-
url: url.replace(/\/$/, ""),
|
|
460
|
-
remoteTenantId,
|
|
461
|
-
clientId,
|
|
462
|
-
createdAt: Date.now(),
|
|
463
|
-
};
|
|
464
|
-
|
|
465
|
-
await storeConnection(ctx.callerId, conn, clientSecret);
|
|
466
|
-
|
|
467
|
-
return {
|
|
468
|
-
success: true,
|
|
469
|
-
data: {
|
|
470
|
-
registryId: name,
|
|
471
|
-
url: conn.url,
|
|
472
|
-
remoteTenantId,
|
|
473
|
-
clientId,
|
|
474
|
-
},
|
|
475
|
-
};
|
|
476
|
-
} catch (err) {
|
|
477
|
-
return {
|
|
478
|
-
success: false,
|
|
479
|
-
error: err instanceof Error ? err.message : String(err),
|
|
480
|
-
};
|
|
481
|
-
}
|
|
482
|
-
},
|
|
483
|
-
|
|
484
|
-
async connect(
|
|
485
|
-
params: Record<string, unknown>,
|
|
486
|
-
ctx: IntegrationMethodContext,
|
|
487
|
-
): Promise<IntegrationMethodResult> {
|
|
488
|
-
const registryId = params.registryId as string;
|
|
489
|
-
const userId = (params.userId as string) ?? ctx.callerId;
|
|
490
|
-
|
|
491
|
-
if (!registryId) {
|
|
492
|
-
return { success: false, error: "registryId is required" };
|
|
493
|
-
}
|
|
494
|
-
|
|
288
|
+
integration: {
|
|
289
|
+
provider: "remote-registry",
|
|
290
|
+
displayName: "Agent Registry",
|
|
291
|
+
icon: "server",
|
|
292
|
+
category: "infrastructure",
|
|
293
|
+
description: "Connect to a remote agent registry via JWKS trust exchange.",
|
|
294
|
+
setup: (params, ctx) => setupFn(params, ctx as any),
|
|
295
|
+
connect: (params, ctx) => connectFn(params, ctx as any),
|
|
296
|
+
async discover(params) {
|
|
297
|
+
const url = (params.url as string) ?? "";
|
|
495
298
|
try {
|
|
496
|
-
const
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
// Register user on the remote registry
|
|
502
|
-
const result = await mcpCall(conn.url, token, {
|
|
503
|
-
action: "execute_tool",
|
|
504
|
-
path: "@users",
|
|
505
|
-
tool: "create_user",
|
|
506
|
-
params: { name: userId, tenantId: conn.remoteTenantId },
|
|
507
|
-
});
|
|
508
|
-
|
|
509
|
-
return {
|
|
510
|
-
success: true,
|
|
511
|
-
data: {
|
|
512
|
-
registryId,
|
|
513
|
-
userId,
|
|
514
|
-
remoteUser: result,
|
|
515
|
-
},
|
|
516
|
-
};
|
|
517
|
-
} catch (err) {
|
|
518
|
-
return {
|
|
519
|
-
success: false,
|
|
520
|
-
error: err instanceof Error ? err.message : String(err),
|
|
521
|
-
};
|
|
522
|
-
}
|
|
299
|
+
const res = await globalThis.fetch(url.replace(/\/$/, "") + "/.well-known/configuration");
|
|
300
|
+
if (!res.ok) return { success: false, error: "No configuration endpoint at " + url };
|
|
301
|
+
const config = await res.json() as any;
|
|
302
|
+
return { success: true, data: { url, issuer: config.issuer, grantTypes: config.supported_grant_types, jwksUri: config.jwks_uri } };
|
|
303
|
+
} catch (err) { return { success: false, error: err instanceof Error ? err.message : String(err) }; }
|
|
523
304
|
},
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
ctx: IntegrationMethodContext,
|
|
528
|
-
): Promise<IntegrationMethodResult> {
|
|
529
|
-
try {
|
|
530
|
-
const conns = await listConnectionsList(ctx.callerId);
|
|
531
|
-
return {
|
|
532
|
-
success: true,
|
|
533
|
-
data: conns.map((c) => ({
|
|
534
|
-
id: c.id,
|
|
535
|
-
name: c.name,
|
|
536
|
-
url: c.url,
|
|
537
|
-
remoteTenantId: c.remoteTenantId,
|
|
538
|
-
createdAt: c.createdAt,
|
|
539
|
-
})),
|
|
540
|
-
};
|
|
541
|
-
} catch (err) {
|
|
542
|
-
return {
|
|
543
|
-
success: false,
|
|
544
|
-
error: err instanceof Error ? err.message : String(err),
|
|
545
|
-
};
|
|
546
|
-
}
|
|
305
|
+
async list() {
|
|
306
|
+
const all = await loadAllConnections("system");
|
|
307
|
+
return { success: true, data: { connections: Object.values(all).map(c => ({ id: c.id, name: c.name, url: c.url, remoteTenantId: c.remoteTenantId })) } };
|
|
547
308
|
},
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
const registryId = params.registryId as string;
|
|
554
|
-
if (!registryId) {
|
|
555
|
-
return { success: false, error: "registryId is required" };
|
|
556
|
-
}
|
|
557
|
-
|
|
558
|
-
try {
|
|
559
|
-
const data = await loadConnection(ctx.callerId, registryId);
|
|
560
|
-
if (!data) {
|
|
561
|
-
return {
|
|
562
|
-
success: false,
|
|
563
|
-
error: `No registry connection '${registryId}'`,
|
|
564
|
-
};
|
|
565
|
-
}
|
|
566
|
-
|
|
567
|
-
return {
|
|
568
|
-
success: true,
|
|
569
|
-
data: {
|
|
570
|
-
id: data.conn.id,
|
|
571
|
-
name: data.conn.name,
|
|
572
|
-
url: data.conn.url,
|
|
573
|
-
remoteTenantId: data.conn.remoteTenantId,
|
|
574
|
-
clientId: data.conn.clientId,
|
|
575
|
-
createdAt: data.conn.createdAt,
|
|
576
|
-
},
|
|
577
|
-
};
|
|
578
|
-
} catch (err) {
|
|
579
|
-
return {
|
|
580
|
-
success: false,
|
|
581
|
-
error: err instanceof Error ? err.message : String(err),
|
|
582
|
-
};
|
|
583
|
-
}
|
|
309
|
+
async get(params) {
|
|
310
|
+
const id = (params.registryId as string) ?? "";
|
|
311
|
+
const conn = await loadConnection("system", id);
|
|
312
|
+
if (!conn) return { success: false, error: "No connection '" + id + "'" };
|
|
313
|
+
return { success: true, data: conn };
|
|
584
314
|
},
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
}
|
|
594
|
-
|
|
595
|
-
try {
|
|
596
|
-
const data = await loadConnection(ctx.callerId, registryId);
|
|
597
|
-
if (!data) {
|
|
598
|
-
return {
|
|
599
|
-
success: false,
|
|
600
|
-
error: `No registry connection '${registryId}'`,
|
|
601
|
-
};
|
|
602
|
-
}
|
|
603
|
-
|
|
604
|
-
// Update mutable fields
|
|
605
|
-
if (params.name) data.conn.name = params.name as string;
|
|
606
|
-
if (params.url) data.conn.url = (params.url as string).replace(/\/$/, "");
|
|
607
|
-
|
|
608
|
-
await storeConnection(ctx.callerId, data.conn, data.clientSecret);
|
|
609
|
-
|
|
610
|
-
return { success: true, data: { id: data.conn.id, updated: true } };
|
|
611
|
-
} catch (err) {
|
|
612
|
-
return {
|
|
613
|
-
success: false,
|
|
614
|
-
error: err instanceof Error ? err.message : String(err),
|
|
615
|
-
};
|
|
616
|
-
}
|
|
315
|
+
async update(params) {
|
|
316
|
+
const id = (params.registryId as string) ?? "";
|
|
317
|
+
const conn = await loadConnection("system", id);
|
|
318
|
+
if (!conn) return { success: false, error: "No connection '" + id + "'" };
|
|
319
|
+
if (params.name) conn.name = params.name as string;
|
|
320
|
+
if (params.url) conn.url = params.url as string;
|
|
321
|
+
await storeConnection("system", conn);
|
|
322
|
+
return { success: true, data: conn };
|
|
617
323
|
},
|
|
618
324
|
},
|
|
619
|
-
tools: [
|
|
325
|
+
tools: [proxyTool, listTool] as any[],
|
|
620
326
|
});
|
|
621
327
|
}
|