@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,133 +1,51 @@
|
|
|
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
|
import { defineAgent, defineTool } from "../define.js";
|
|
26
|
-
|
|
27
|
-
// Helpers
|
|
28
|
-
// ============================================
|
|
29
|
-
/**
|
|
30
|
-
* Make an MCP JSON-RPC call to a remote registry.
|
|
31
|
-
*/
|
|
32
|
-
async function mcpCall(url, token, request) {
|
|
33
|
-
const res = await globalThis.fetch(url, {
|
|
34
|
-
method: "POST",
|
|
35
|
-
headers: {
|
|
36
|
-
"Content-Type": "application/json",
|
|
37
|
-
Authorization: `Bearer ${token}`,
|
|
38
|
-
},
|
|
39
|
-
body: JSON.stringify({
|
|
40
|
-
jsonrpc: "2.0",
|
|
41
|
-
id: Date.now(),
|
|
42
|
-
method: "tools/call",
|
|
43
|
-
params: {
|
|
44
|
-
name: "call_agent",
|
|
45
|
-
arguments: {
|
|
46
|
-
request: {
|
|
47
|
-
action: request.action,
|
|
48
|
-
path: request.path,
|
|
49
|
-
tool: request.tool,
|
|
50
|
-
params: request.params ?? {},
|
|
51
|
-
},
|
|
52
|
-
},
|
|
53
|
-
},
|
|
54
|
-
}),
|
|
55
|
-
});
|
|
56
|
-
if (!res.ok) {
|
|
57
|
-
throw new Error(`Registry call failed: ${res.status} ${res.statusText}`);
|
|
58
|
-
}
|
|
59
|
-
const json = (await res.json());
|
|
60
|
-
if (json.error) {
|
|
61
|
-
throw new Error(`Registry RPC error: ${json.error.message ?? JSON.stringify(json.error)}`);
|
|
62
|
-
}
|
|
63
|
-
// Parse the tool result from MCP response
|
|
64
|
-
const text = json?.result?.content?.[0]?.text;
|
|
65
|
-
if (!text)
|
|
66
|
-
return json?.result;
|
|
67
|
-
try {
|
|
68
|
-
return JSON.parse(text);
|
|
69
|
-
}
|
|
70
|
-
catch {
|
|
71
|
-
return { raw: text };
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
/**
|
|
75
|
-
* Get an access token from a remote registry via /oauth/token.
|
|
76
|
-
*/
|
|
77
|
-
async function getRegistryToken(url, clientId, clientSecret) {
|
|
78
|
-
const tokenUrl = url.replace(/\/$/, "") + "/oauth/token";
|
|
79
|
-
const res = await globalThis.fetch(tokenUrl, {
|
|
80
|
-
method: "POST",
|
|
81
|
-
headers: { "Content-Type": "application/json" },
|
|
82
|
-
body: JSON.stringify({
|
|
83
|
-
grant_type: "client_credentials",
|
|
84
|
-
client_id: clientId,
|
|
85
|
-
client_secret: clientSecret,
|
|
86
|
-
}),
|
|
87
|
-
});
|
|
88
|
-
if (!res.ok) {
|
|
89
|
-
const body = await res.text();
|
|
90
|
-
throw new Error(`Token exchange failed: ${res.status} ${body}`);
|
|
91
|
-
}
|
|
92
|
-
const json = (await res.json());
|
|
93
|
-
return json.access_token;
|
|
94
|
-
}
|
|
95
|
-
// ============================================
|
|
96
|
-
// Create Remote Registry Agent
|
|
97
|
-
// ============================================
|
|
27
|
+
const ENTITY_TYPE = "remote-registry-connections";
|
|
98
28
|
export function createRemoteRegistryAgent(options) {
|
|
99
|
-
const { secretStore } = options;
|
|
100
|
-
//
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
/**
|
|
104
|
-
* Store a registry connection (metadata + credentials).
|
|
105
|
-
*/
|
|
106
|
-
async function storeConnection(ownerId, conn, clientSecret) {
|
|
107
|
-
// Load existing connections, update, and store back
|
|
29
|
+
const { secretStore, signJwt, addTrustedIssuer } = options;
|
|
30
|
+
// --- Connection storage (KV via SecretStore) ---
|
|
31
|
+
async function storeConnection(ownerId, conn) {
|
|
32
|
+
console.error("[remote-registry] storeConnection for owner:", ownerId, "conn:", conn.id);
|
|
108
33
|
const all = await loadAllConnections(ownerId);
|
|
109
|
-
all[conn.id] =
|
|
34
|
+
all[conn.id] = conn;
|
|
110
35
|
const value = JSON.stringify(all);
|
|
111
|
-
// Store the blob
|
|
112
36
|
const scope = { tenantId: ownerId };
|
|
113
37
|
const secretId = await secretStore.store(value, ownerId);
|
|
114
|
-
// Link it so we can find it later
|
|
115
38
|
if (secretStore.associate) {
|
|
116
39
|
await secretStore.associate(secretId, ENTITY_TYPE, ownerId, scope);
|
|
117
40
|
}
|
|
118
41
|
}
|
|
119
|
-
/**
|
|
120
|
-
* Load all connections from the stored blob.
|
|
121
|
-
*/
|
|
122
42
|
async function loadAllConnections(ownerId) {
|
|
123
|
-
|
|
43
|
+
console.error("[remote-registry] loadAllConnections for owner:", ownerId);
|
|
124
44
|
if (secretStore.resolveByEntity) {
|
|
125
45
|
const scope = { tenantId: ownerId };
|
|
126
46
|
const secretIds = await secretStore.resolveByEntity(ENTITY_TYPE, ownerId, scope);
|
|
127
|
-
if (secretIds
|
|
128
|
-
|
|
129
|
-
const latestId = secretIds[secretIds.length - 1];
|
|
130
|
-
const raw = await secretStore.resolve(latestId, ownerId);
|
|
47
|
+
if (secretIds?.length) {
|
|
48
|
+
const raw = await secretStore.resolve(secretIds[secretIds.length - 1], ownerId);
|
|
131
49
|
if (raw) {
|
|
132
50
|
try {
|
|
133
51
|
return JSON.parse(raw);
|
|
@@ -140,321 +58,241 @@ export function createRemoteRegistryAgent(options) {
|
|
|
140
58
|
}
|
|
141
59
|
return {};
|
|
142
60
|
}
|
|
143
|
-
/**
|
|
144
|
-
* Load a registry connection.
|
|
145
|
-
*/
|
|
146
61
|
async function loadConnection(ownerId, registryId) {
|
|
147
62
|
const all = await loadAllConnections(ownerId);
|
|
148
|
-
|
|
149
|
-
if (!entry)
|
|
150
|
-
return null;
|
|
151
|
-
const { clientSecret, ...conn } = entry;
|
|
152
|
-
return { conn, clientSecret };
|
|
63
|
+
return all[registryId] ?? null;
|
|
153
64
|
}
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
65
|
+
// --- MCP call helper ---
|
|
66
|
+
async function mcpCall(url, jwt, request) {
|
|
67
|
+
const res = await globalThis.fetch(url, {
|
|
68
|
+
method: "POST",
|
|
69
|
+
headers: {
|
|
70
|
+
"Content-Type": "application/json",
|
|
71
|
+
Authorization: `Bearer ${jwt}`,
|
|
72
|
+
},
|
|
73
|
+
body: JSON.stringify({
|
|
74
|
+
jsonrpc: "2.0",
|
|
75
|
+
id: Date.now(),
|
|
76
|
+
method: "tools/call",
|
|
77
|
+
params: { name: "call_agent", arguments: { request } },
|
|
78
|
+
}),
|
|
79
|
+
});
|
|
80
|
+
const rpc = await res.json();
|
|
81
|
+
const text = rpc.result?.content?.[0]?.text;
|
|
82
|
+
return text ? JSON.parse(text) : rpc.result;
|
|
160
83
|
}
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
if (!data) {
|
|
167
|
-
throw new Error(`No registry connection '${registryId}'. Use setup_integration first.`);
|
|
84
|
+
// --- Proxy: sign JWT and call remote ---
|
|
85
|
+
async function proxyCall(ownerId, registryId, request) {
|
|
86
|
+
const conn = await loadConnection(ownerId, registryId);
|
|
87
|
+
if (!conn) {
|
|
88
|
+
return { success: false, error: `No connection '${registryId}'. Use setup_integration first.` };
|
|
168
89
|
}
|
|
169
|
-
const
|
|
170
|
-
|
|
90
|
+
const jwt = await signJwt({ tenantId: conn.remoteTenantId, action: "proxy", type: "agent-registry" });
|
|
91
|
+
const result = await mcpCall(conn.url, jwt, request);
|
|
92
|
+
return { success: true, result };
|
|
171
93
|
}
|
|
172
|
-
//
|
|
173
|
-
const
|
|
174
|
-
name: "
|
|
175
|
-
description: "
|
|
176
|
-
"Proxies the request with the stored tenant credentials.",
|
|
94
|
+
// --- Tools ---
|
|
95
|
+
const proxyTool = defineTool({
|
|
96
|
+
name: "proxy_call",
|
|
97
|
+
description: "Proxy an MCP call to a connected remote registry.",
|
|
177
98
|
visibility: "public",
|
|
178
99
|
inputSchema: {
|
|
179
100
|
type: "object",
|
|
180
101
|
properties: {
|
|
181
|
-
registryId: {
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
},
|
|
185
|
-
|
|
186
|
-
type: "string",
|
|
187
|
-
description: "Agent path on the remote registry (e.g. '@integrations')",
|
|
188
|
-
},
|
|
189
|
-
action: {
|
|
190
|
-
type: "string",
|
|
191
|
-
description: "Action to perform (e.g. 'execute_tool')",
|
|
192
|
-
},
|
|
193
|
-
tool: {
|
|
194
|
-
type: "string",
|
|
195
|
-
description: "Tool name to call",
|
|
196
|
-
},
|
|
197
|
-
params: {
|
|
198
|
-
type: "object",
|
|
199
|
-
description: "Tool parameters",
|
|
200
|
-
},
|
|
102
|
+
registryId: { type: "string", description: "Registry connection ID" },
|
|
103
|
+
action: { type: "string" },
|
|
104
|
+
path: { type: "string" },
|
|
105
|
+
tool: { type: "string" },
|
|
106
|
+
params: { type: "object" },
|
|
201
107
|
},
|
|
202
|
-
required: ["registryId", "
|
|
108
|
+
required: ["registryId", "action", "path", "tool"],
|
|
203
109
|
},
|
|
204
|
-
execute: async (input,
|
|
205
|
-
|
|
206
|
-
return mcpCall(conn.url, token, {
|
|
110
|
+
execute: async (input, _ctx) => {
|
|
111
|
+
return proxyCall("system", input.registryId, {
|
|
207
112
|
action: input.action,
|
|
208
|
-
path: input.
|
|
113
|
+
path: input.path,
|
|
209
114
|
tool: input.tool,
|
|
210
115
|
params: input.params,
|
|
211
116
|
});
|
|
212
117
|
},
|
|
213
118
|
});
|
|
214
|
-
const
|
|
215
|
-
name: "
|
|
216
|
-
description: "List
|
|
119
|
+
const listTool = defineTool({
|
|
120
|
+
name: "list_connections",
|
|
121
|
+
description: "List all connected remote registries.",
|
|
217
122
|
visibility: "public",
|
|
218
|
-
inputSchema: {
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
123
|
+
inputSchema: { type: "object", properties: {} },
|
|
124
|
+
execute: async (_input, _ctx) => {
|
|
125
|
+
const all = await loadAllConnections("system");
|
|
126
|
+
return {
|
|
127
|
+
connections: Object.values(all).map(c => ({
|
|
128
|
+
id: c.id,
|
|
129
|
+
name: c.name,
|
|
130
|
+
url: c.url,
|
|
131
|
+
remoteTenantId: c.remoteTenantId,
|
|
132
|
+
})),
|
|
133
|
+
};
|
|
227
134
|
},
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
135
|
+
});
|
|
136
|
+
// Extract setup/connect as standalone functions to avoid circular reference
|
|
137
|
+
const setupFn = async (params, _ctx) => {
|
|
138
|
+
console.log("[remote-registry] setupFn called with:", JSON.stringify(params));
|
|
139
|
+
const url = params.url;
|
|
140
|
+
const name = params.name ?? "registry";
|
|
141
|
+
const oidcUserId = params.oidcUserId;
|
|
142
|
+
if (!url)
|
|
143
|
+
return { success: false, error: "url is required" };
|
|
144
|
+
try {
|
|
145
|
+
const baseUrl = url.replace(/\/$/, "");
|
|
146
|
+
// Phase 2: Complete setup after OIDC — store connection with resolved tenant
|
|
147
|
+
if (oidcUserId) {
|
|
148
|
+
const jwt = await signJwt({ sub: oidcUserId, action: "setup", type: "agent-registry" });
|
|
149
|
+
const tokenRes = await globalThis.fetch(baseUrl + "/oauth/token", {
|
|
150
|
+
method: "POST", headers: { "Content-Type": "application/json" },
|
|
151
|
+
body: JSON.stringify({ grant_type: "jwt_exchange", assertion: jwt, scope: "setup", redirect_uri: params.redirect_uri ?? "" }),
|
|
152
|
+
});
|
|
153
|
+
const tokenData = await tokenRes.json();
|
|
154
|
+
if (!tokenData.access_token && !tokenData.tenant_id) {
|
|
155
|
+
return { success: false, error: tokenData.error_description ?? tokenData.error ?? "Setup completion failed" };
|
|
156
|
+
}
|
|
157
|
+
const remoteTenantId = tokenData.tenant_id ?? name;
|
|
158
|
+
const ownerId = "system";
|
|
159
|
+
await storeConnection(ownerId, { id: name, name, url: baseUrl, remoteTenantId, createdAt: Date.now() });
|
|
160
|
+
return { success: true, data: { registryId: name, url, remoteTenantId } };
|
|
161
|
+
}
|
|
162
|
+
// Phase 1: Discover JWKS, establish trust, then request OIDC
|
|
163
|
+
const configUrl = baseUrl + "/.well-known/configuration";
|
|
164
|
+
console.log("[setupFn] fetching config:", configUrl);
|
|
165
|
+
const configRes = await globalThis.fetch(configUrl);
|
|
166
|
+
console.log("[setupFn] config status:", configRes.status);
|
|
167
|
+
if (!configRes.ok)
|
|
168
|
+
return { success: false, error: "Failed to discover registry at " + configUrl };
|
|
169
|
+
const remoteConfig = await configRes.json();
|
|
170
|
+
if (remoteConfig.jwks_uri) {
|
|
171
|
+
console.log("[setupFn] fetching JWKS:", remoteConfig.jwks_uri);
|
|
172
|
+
const jwksRes = await globalThis.fetch(remoteConfig.jwks_uri);
|
|
173
|
+
console.log("[setupFn] JWKS status:", jwksRes.status);
|
|
174
|
+
if (!jwksRes.ok)
|
|
175
|
+
return { success: false, error: "JWKS not reachable" };
|
|
176
|
+
}
|
|
177
|
+
if (addTrustedIssuer) {
|
|
178
|
+
console.log("[setupFn] adding trusted issuer:", baseUrl);
|
|
179
|
+
await addTrustedIssuer(baseUrl);
|
|
180
|
+
console.log("[setupFn] added trusted issuer");
|
|
181
|
+
}
|
|
182
|
+
// Request identity — atlas will return authorize URL for Slack OIDC
|
|
183
|
+
console.log("[setupFn] Phase 1: requesting identity via jwt_exchange");
|
|
184
|
+
const jwt = await signJwt({ action: "setup", type: "agent-registry", targetUrl: url });
|
|
185
|
+
console.log("[setupFn] POSTing to:", baseUrl + "/oauth/token");
|
|
186
|
+
const tokenRes = await globalThis.fetch(baseUrl + "/oauth/token", {
|
|
187
|
+
method: "POST", headers: { "Content-Type": "application/json" },
|
|
188
|
+
body: JSON.stringify({ grant_type: "jwt_exchange", assertion: jwt, scope: "setup", redirect_uri: params.redirect_uri ?? "" }),
|
|
233
189
|
});
|
|
234
|
-
|
|
235
|
-
|
|
190
|
+
console.log("[setupFn] token status:", tokenRes.status);
|
|
191
|
+
const tokenData = await tokenRes.json();
|
|
192
|
+
console.log("[setupFn] tokenData:", JSON.stringify(tokenData).substring(0, 300));
|
|
193
|
+
// If already set up (user linked), store connection directly
|
|
194
|
+
if (tokenData.access_token) {
|
|
195
|
+
const remoteTenantId = tokenData.tenant_id ?? name;
|
|
196
|
+
const ownerId = "system";
|
|
197
|
+
await storeConnection(ownerId, { id: name, name, url: baseUrl, remoteTenantId, createdAt: Date.now() });
|
|
198
|
+
return { success: true, data: { registryId: name, url, remoteTenantId } };
|
|
236
199
|
}
|
|
237
|
-
return
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
200
|
+
// Need OIDC — return authorize URL to caller
|
|
201
|
+
if (tokenData.error === "identity_required") {
|
|
202
|
+
return { success: false, error: "identity_required", data: { authorizeUrl: tokenData.authorize_url, registryId: name, url } };
|
|
203
|
+
}
|
|
204
|
+
return { success: false, error: tokenData.error_description ?? tokenData.error ?? "Unexpected response from token endpoint" };
|
|
205
|
+
}
|
|
206
|
+
catch (err) {
|
|
207
|
+
return { success: false, error: err instanceof Error ? err.message : String(err) };
|
|
208
|
+
}
|
|
209
|
+
};
|
|
210
|
+
const connectFn = async (params, ctx) => {
|
|
211
|
+
const registryId = params.registryId;
|
|
212
|
+
const redirectUri = params.redirectUri ?? "";
|
|
213
|
+
const oidcUserId = params.oidcUserId;
|
|
214
|
+
if (!registryId)
|
|
215
|
+
return { success: false, error: "registryId is required" };
|
|
216
|
+
try {
|
|
217
|
+
const ownerId = "system"; // tenant-scoped
|
|
218
|
+
const conn = await loadConnection(ownerId, registryId);
|
|
219
|
+
if (!conn)
|
|
220
|
+
return { success: false, error: "No connection '" + registryId + "'" };
|
|
221
|
+
// Use OIDC-issued identity if available, never send "anonymous" as sub
|
|
222
|
+
const sub = oidcUserId ?? (ctx.callerId !== "anonymous" ? ctx.callerId : undefined);
|
|
223
|
+
const jwt = await signJwt({ ...(sub ? { sub } : {}), tenantId: conn.remoteTenantId, action: "connect", type: "agent-registry" });
|
|
224
|
+
const tokenRes = await globalThis.fetch(conn.url + "/oauth/token", {
|
|
225
|
+
method: "POST", headers: { "Content-Type": "application/json" },
|
|
226
|
+
body: JSON.stringify({ grant_type: "jwt_exchange", assertion: jwt, redirect_uri: redirectUri }),
|
|
227
|
+
});
|
|
228
|
+
const tokenData = await tokenRes.json();
|
|
229
|
+
if (tokenData.access_token)
|
|
230
|
+
return { success: true, data: { registryId, accessToken: tokenData.access_token, userId: tokenData.user_id, tenantId: tokenData.tenant_id } };
|
|
231
|
+
if (tokenData.error === "identity_required")
|
|
232
|
+
return { success: false, error: "identity_required", data: { authorizeUrl: tokenData.authorize_url, tenantId: tokenData.tenant_id } };
|
|
233
|
+
return { success: false, error: tokenData.error_description ?? tokenData.error ?? "Token exchange failed" };
|
|
234
|
+
}
|
|
235
|
+
catch (err) {
|
|
236
|
+
return { success: false, error: err instanceof Error ? err.message : String(err) };
|
|
237
|
+
}
|
|
238
|
+
};
|
|
241
239
|
return defineAgent({
|
|
242
240
|
path: "@remote-registry",
|
|
243
241
|
entrypoint: "You manage connections to remote agent registries. " +
|
|
244
|
-
"Use setup to connect a new registry, connect to
|
|
245
|
-
"and
|
|
242
|
+
"Use setup to connect a new registry, connect to link user identities, " +
|
|
243
|
+
"and proxy_call to make authenticated calls.",
|
|
246
244
|
config: {
|
|
247
245
|
name: "Remote Registry",
|
|
248
|
-
description: "Connect to remote agent registries
|
|
246
|
+
description: "Connect to remote agent registries via JWKS trust + jwt_exchange",
|
|
249
247
|
supportedActions: ["execute_tool", "describe_tools", "load"],
|
|
250
|
-
integration: {
|
|
251
|
-
provider: "remote-registry",
|
|
252
|
-
displayName: "Agent Registry",
|
|
253
|
-
icon: "server",
|
|
254
|
-
category: "infrastructure",
|
|
255
|
-
description: "Connect to a remote agent registry to access its integrations, databases, and agents.",
|
|
256
|
-
},
|
|
257
248
|
},
|
|
258
249
|
visibility: "public",
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
250
|
+
integration: {
|
|
251
|
+
provider: "remote-registry",
|
|
252
|
+
displayName: "Agent Registry",
|
|
253
|
+
icon: "server",
|
|
254
|
+
category: "infrastructure",
|
|
255
|
+
description: "Connect to a remote agent registry via JWKS trust exchange.",
|
|
256
|
+
setup: (params, ctx) => setupFn(params, ctx),
|
|
257
|
+
connect: (params, ctx) => connectFn(params, ctx),
|
|
258
|
+
async discover(params) {
|
|
259
|
+
const url = params.url ?? "";
|
|
266
260
|
try {
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
body: JSON.stringify({ tenant: name }),
|
|
273
|
-
});
|
|
274
|
-
if (!setupRes.ok) {
|
|
275
|
-
const body = await setupRes.text();
|
|
276
|
-
return {
|
|
277
|
-
success: false,
|
|
278
|
-
error: `Failed to create tenant on registry: ${setupRes.status} ${body}`,
|
|
279
|
-
};
|
|
280
|
-
}
|
|
281
|
-
const setupResult = (await setupRes.json());
|
|
282
|
-
if (!setupResult.success || !setupResult.result?.tenantId) {
|
|
283
|
-
return {
|
|
284
|
-
success: false,
|
|
285
|
-
error: "Registry /setup did not return a tenantId",
|
|
286
|
-
};
|
|
287
|
-
}
|
|
288
|
-
const remoteTenantId = setupResult.result.tenantId;
|
|
289
|
-
// 2. Register a client for this tenant
|
|
290
|
-
// Use the setup token (or root key) to create client credentials
|
|
291
|
-
const token = setupResult.result.token;
|
|
292
|
-
if (!token) {
|
|
293
|
-
return {
|
|
294
|
-
success: false,
|
|
295
|
-
error: "Registry /setup did not return a token for client creation",
|
|
296
|
-
};
|
|
297
|
-
}
|
|
298
|
-
const registerResult = await mcpCall(url, token, {
|
|
299
|
-
action: "execute_tool",
|
|
300
|
-
path: "@auth",
|
|
301
|
-
tool: "register",
|
|
302
|
-
params: {
|
|
303
|
-
name: `${name}-client`,
|
|
304
|
-
scopes: ["integrations", "secrets", "users"],
|
|
305
|
-
},
|
|
306
|
-
});
|
|
307
|
-
const clientId = registerResult?.clientId ?? registerResult?.result?.clientId;
|
|
308
|
-
const clientSecret = registerResult?.clientSecret?.value ??
|
|
309
|
-
registerResult?.result?.clientSecret?.value ??
|
|
310
|
-
registerResult?.clientSecret;
|
|
311
|
-
if (!clientId || !clientSecret) {
|
|
312
|
-
return {
|
|
313
|
-
success: false,
|
|
314
|
-
error: `Failed to register client: ${JSON.stringify(registerResult)}`,
|
|
315
|
-
};
|
|
316
|
-
}
|
|
317
|
-
// 3. Store connection
|
|
318
|
-
const conn = {
|
|
319
|
-
id: name,
|
|
320
|
-
name,
|
|
321
|
-
url: url.replace(/\/$/, ""),
|
|
322
|
-
remoteTenantId,
|
|
323
|
-
clientId,
|
|
324
|
-
createdAt: Date.now(),
|
|
325
|
-
};
|
|
326
|
-
await storeConnection(ctx.callerId, conn, clientSecret);
|
|
327
|
-
return {
|
|
328
|
-
success: true,
|
|
329
|
-
data: {
|
|
330
|
-
registryId: name,
|
|
331
|
-
url: conn.url,
|
|
332
|
-
remoteTenantId,
|
|
333
|
-
clientId,
|
|
334
|
-
},
|
|
335
|
-
};
|
|
261
|
+
const res = await globalThis.fetch(url.replace(/\/$/, "") + "/.well-known/configuration");
|
|
262
|
+
if (!res.ok)
|
|
263
|
+
return { success: false, error: "No configuration endpoint at " + url };
|
|
264
|
+
const config = await res.json();
|
|
265
|
+
return { success: true, data: { url, issuer: config.issuer, grantTypes: config.supported_grant_types, jwksUri: config.jwks_uri } };
|
|
336
266
|
}
|
|
337
267
|
catch (err) {
|
|
338
|
-
return {
|
|
339
|
-
success: false,
|
|
340
|
-
error: err instanceof Error ? err.message : String(err),
|
|
341
|
-
};
|
|
268
|
+
return { success: false, error: err instanceof Error ? err.message : String(err) };
|
|
342
269
|
}
|
|
343
270
|
},
|
|
344
|
-
async
|
|
345
|
-
const
|
|
346
|
-
|
|
347
|
-
if (!registryId) {
|
|
348
|
-
return { success: false, error: "registryId is required" };
|
|
349
|
-
}
|
|
350
|
-
try {
|
|
351
|
-
const { token, conn } = await getAuthenticatedToken(ctx.callerId, registryId);
|
|
352
|
-
// Register user on the remote registry
|
|
353
|
-
const result = await mcpCall(conn.url, token, {
|
|
354
|
-
action: "execute_tool",
|
|
355
|
-
path: "@users",
|
|
356
|
-
tool: "create_user",
|
|
357
|
-
params: { name: userId, tenantId: conn.remoteTenantId },
|
|
358
|
-
});
|
|
359
|
-
return {
|
|
360
|
-
success: true,
|
|
361
|
-
data: {
|
|
362
|
-
registryId,
|
|
363
|
-
userId,
|
|
364
|
-
remoteUser: result,
|
|
365
|
-
},
|
|
366
|
-
};
|
|
367
|
-
}
|
|
368
|
-
catch (err) {
|
|
369
|
-
return {
|
|
370
|
-
success: false,
|
|
371
|
-
error: err instanceof Error ? err.message : String(err),
|
|
372
|
-
};
|
|
373
|
-
}
|
|
271
|
+
async list() {
|
|
272
|
+
const all = await loadAllConnections("system");
|
|
273
|
+
return { success: true, data: { connections: Object.values(all).map(c => ({ id: c.id, name: c.name, url: c.url, remoteTenantId: c.remoteTenantId })) } };
|
|
374
274
|
},
|
|
375
|
-
async
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
id: c.id,
|
|
382
|
-
name: c.name,
|
|
383
|
-
url: c.url,
|
|
384
|
-
remoteTenantId: c.remoteTenantId,
|
|
385
|
-
createdAt: c.createdAt,
|
|
386
|
-
})),
|
|
387
|
-
};
|
|
388
|
-
}
|
|
389
|
-
catch (err) {
|
|
390
|
-
return {
|
|
391
|
-
success: false,
|
|
392
|
-
error: err instanceof Error ? err.message : String(err),
|
|
393
|
-
};
|
|
394
|
-
}
|
|
275
|
+
async get(params) {
|
|
276
|
+
const id = params.registryId ?? "";
|
|
277
|
+
const conn = await loadConnection("system", id);
|
|
278
|
+
if (!conn)
|
|
279
|
+
return { success: false, error: "No connection '" + id + "'" };
|
|
280
|
+
return { success: true, data: conn };
|
|
395
281
|
},
|
|
396
|
-
async
|
|
397
|
-
const
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
};
|
|
408
|
-
}
|
|
409
|
-
return {
|
|
410
|
-
success: true,
|
|
411
|
-
data: {
|
|
412
|
-
id: data.conn.id,
|
|
413
|
-
name: data.conn.name,
|
|
414
|
-
url: data.conn.url,
|
|
415
|
-
remoteTenantId: data.conn.remoteTenantId,
|
|
416
|
-
clientId: data.conn.clientId,
|
|
417
|
-
createdAt: data.conn.createdAt,
|
|
418
|
-
},
|
|
419
|
-
};
|
|
420
|
-
}
|
|
421
|
-
catch (err) {
|
|
422
|
-
return {
|
|
423
|
-
success: false,
|
|
424
|
-
error: err instanceof Error ? err.message : String(err),
|
|
425
|
-
};
|
|
426
|
-
}
|
|
427
|
-
},
|
|
428
|
-
async update(params, ctx) {
|
|
429
|
-
const registryId = params.registryId;
|
|
430
|
-
if (!registryId) {
|
|
431
|
-
return { success: false, error: "registryId is required" };
|
|
432
|
-
}
|
|
433
|
-
try {
|
|
434
|
-
const data = await loadConnection(ctx.callerId, registryId);
|
|
435
|
-
if (!data) {
|
|
436
|
-
return {
|
|
437
|
-
success: false,
|
|
438
|
-
error: `No registry connection '${registryId}'`,
|
|
439
|
-
};
|
|
440
|
-
}
|
|
441
|
-
// Update mutable fields
|
|
442
|
-
if (params.name)
|
|
443
|
-
data.conn.name = params.name;
|
|
444
|
-
if (params.url)
|
|
445
|
-
data.conn.url = params.url.replace(/\/$/, "");
|
|
446
|
-
await storeConnection(ctx.callerId, data.conn, data.clientSecret);
|
|
447
|
-
return { success: true, data: { id: data.conn.id, updated: true } };
|
|
448
|
-
}
|
|
449
|
-
catch (err) {
|
|
450
|
-
return {
|
|
451
|
-
success: false,
|
|
452
|
-
error: err instanceof Error ? err.message : String(err),
|
|
453
|
-
};
|
|
454
|
-
}
|
|
282
|
+
async update(params) {
|
|
283
|
+
const id = params.registryId ?? "";
|
|
284
|
+
const conn = await loadConnection("system", id);
|
|
285
|
+
if (!conn)
|
|
286
|
+
return { success: false, error: "No connection '" + id + "'" };
|
|
287
|
+
if (params.name)
|
|
288
|
+
conn.name = params.name;
|
|
289
|
+
if (params.url)
|
|
290
|
+
conn.url = params.url;
|
|
291
|
+
await storeConnection("system", conn);
|
|
292
|
+
return { success: true, data: conn };
|
|
455
293
|
},
|
|
456
294
|
},
|
|
457
|
-
tools: [
|
|
295
|
+
tools: [proxyTool, listTool],
|
|
458
296
|
});
|
|
459
297
|
}
|
|
460
298
|
//# sourceMappingURL=remote-registry.js.map
|