@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.
Files changed (48) hide show
  1. package/dist/agent-definitions/auth.d.ts +17 -1
  2. package/dist/agent-definitions/auth.d.ts.map +1 -1
  3. package/dist/agent-definitions/auth.js +123 -4
  4. package/dist/agent-definitions/auth.js.map +1 -1
  5. package/dist/agent-definitions/integrations.d.ts +2 -14
  6. package/dist/agent-definitions/integrations.d.ts.map +1 -1
  7. package/dist/agent-definitions/integrations.js +64 -19
  8. package/dist/agent-definitions/integrations.js.map +1 -1
  9. package/dist/agent-definitions/remote-registry.d.ts +19 -14
  10. package/dist/agent-definitions/remote-registry.d.ts.map +1 -1
  11. package/dist/agent-definitions/remote-registry.js +219 -381
  12. package/dist/agent-definitions/remote-registry.js.map +1 -1
  13. package/dist/agent-definitions/users.d.ts.map +1 -1
  14. package/dist/agent-definitions/users.js +29 -1
  15. package/dist/agent-definitions/users.js.map +1 -1
  16. package/dist/define.d.ts +6 -4
  17. package/dist/define.d.ts.map +1 -1
  18. package/dist/define.js +82 -3
  19. package/dist/define.js.map +1 -1
  20. package/dist/index.d.ts +3 -2
  21. package/dist/index.d.ts.map +1 -1
  22. package/dist/index.js +1 -0
  23. package/dist/index.js.map +1 -1
  24. package/dist/jwt.js +1 -1
  25. package/dist/jwt.js.map +1 -1
  26. package/dist/key-manager.d.ts +76 -0
  27. package/dist/key-manager.d.ts.map +1 -0
  28. package/dist/key-manager.js +156 -0
  29. package/dist/key-manager.js.map +1 -0
  30. package/dist/server.d.ts +39 -0
  31. package/dist/server.d.ts.map +1 -1
  32. package/dist/server.js +228 -52
  33. package/dist/server.js.map +1 -1
  34. package/dist/types.d.ts +53 -1
  35. package/dist/types.d.ts.map +1 -1
  36. package/package.json +1 -1
  37. package/src/agent-definitions/auth.ts +165 -6
  38. package/src/agent-definitions/integrations.ts +59 -28
  39. package/src/agent-definitions/remote-registry.ts +219 -513
  40. package/src/agent-definitions/users.ts +35 -1
  41. package/src/define.ts +98 -6
  42. package/src/index.ts +3 -1
  43. package/src/jwt.ts +1 -1
  44. package/src/key-manager.test.ts +273 -0
  45. package/src/key-manager.ts +257 -0
  46. package/src/server.test.ts +284 -0
  47. package/src/server.ts +334 -60
  48. 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 the IntegrationMethods pattern so @integrations can discover
6
- * and interact with it uniformly via setup/connect/list/get/update.
5
+ * Uses JWKS trust exchange + jwt_exchange for authentication.
6
+ * No client credentials stored uses signJwt for outbound calls.
7
7
  *
8
- * Each remote registry connection stores:
9
- * - url: the registry's base URL
10
- * - tenantId: the tenant created on the remote registry
11
- * - clientId + clientSecret: credentials for authentication
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, createAgentRegistry } from '@slashfi/agents-sdk';
15
+ * import { createRemoteRegistryAgent, createAgentServer } from '@slashfi/agents-sdk';
16
16
  *
17
- * const registry = createAgentRegistry();
18
- * registry.register(createRemoteRegistryAgent({ secretStore }));
17
+ * const server = createAgentServer(registry, { ... });
18
+ * await server.start();
19
19
  *
20
- * // Then via @integrations:
21
- * // setup_integration({ provider: 'remote-registry', params: { url: 'https://registry.slash.com', name: 'slash' } })
22
- * // connect_integration({ provider: 'remote-registry', params: { registryId: 'slash', userId: 'user_123' } })
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 credentials */
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
- // We store all registry connections as a single JSON blob per owner.
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
- * Store a registry connection (metadata + credentials).
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] = { ...conn, clientSecret };
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
- * Load all connections from the stored blob.
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 && secretIds.length > 0) {
199
- // Resolve the latest stored blob
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
- const entry = all[registryId];
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
- * List all registry connections for an owner.
230
- */
231
- async function listConnectionsList(
232
- ownerId: string,
233
- ): Promise<RegistryConnection[]> {
234
- const all = await loadAllConnections(ownerId);
235
- return Object.values(all).map(({ clientSecret: _, ...conn }) => conn);
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
- * Get an authenticated token for a registry connection.
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
- ): Promise<{ token: string; conn: RegistryConnection }> {
245
- const data = await loadConnection(ownerId, registryId);
246
- if (!data) {
247
- throw new Error(
248
- `No registry connection '${registryId}'. Use setup_integration first.`,
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 token = await getRegistryToken(
252
- data.conn.url,
253
- data.conn.clientId,
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
- // ---- Tools ----
132
+ // --- Tools ---
260
133
 
261
- const callRemoteTool = defineTool({
262
- name: "call_remote",
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
- type: "string",
272
- description: "Registry connection ID",
273
- },
274
- agentPath: {
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", "agentPath", "action", "tool"],
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.agentPath,
152
+ path: input.path,
310
153
  tool: input.tool,
311
154
  params: input.params,
312
155
  });
313
156
  },
314
157
  });
315
158
 
316
- const listRemoteAgentsTool = defineTool({
317
- name: "list_remote_agents",
318
- description: "List agents available on a remote registry.",
159
+ const listTool = defineTool({
160
+ name: "list_connections",
161
+ description: "List all connected remote registries.",
319
162
  visibility: "public" as const,
320
- inputSchema: {
321
- type: "object" as const,
322
- properties: {
323
- registryId: {
324
- type: "string",
325
- description: "Registry connection ID",
326
- },
327
- },
328
- required: ["registryId"],
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
- execute: async (
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
- if (!res.ok) {
345
- throw new Error(`Failed to list agents: ${res.status}`);
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
- return res.json();
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
- // ---- Agent Definition ----
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 register users, " +
359
- "and call_remote to proxy authenticated MCP calls.",
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
- integrationMethods: {
376
- async setup(
377
- params: Record<string, unknown>,
378
- ctx: IntegrationMethodContext,
379
- ): Promise<IntegrationMethodResult> {
380
- const url = params.url as string;
381
- const name = (params.name as string) ?? "registry";
382
-
383
- if (!url) {
384
- return { success: false, error: "url is required" };
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 { token, conn } = await getAuthenticatedToken(
497
- ctx.callerId,
498
- registryId,
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
- async list(
526
- _params: Record<string, unknown>,
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
- async get(
550
- params: Record<string, unknown>,
551
- ctx: IntegrationMethodContext,
552
- ): Promise<IntegrationMethodResult> {
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
- async update(
587
- params: Record<string, unknown>,
588
- ctx: IntegrationMethodContext,
589
- ): Promise<IntegrationMethodResult> {
590
- const registryId = params.registryId as string;
591
- if (!registryId) {
592
- return { success: false, error: "registryId is required" };
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: [callRemoteTool as any, listRemoteAgentsTool as any],
325
+ tools: [proxyTool, listTool] as any[],
620
326
  });
621
327
  }