@slashfi/agents-sdk 0.24.1 → 0.24.3
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/callback/index.d.ts +13 -24
- package/dist/callback/index.d.ts.map +1 -1
- package/dist/callback/index.js.map +1 -1
- package/dist/cjs/agent-definitions/auth.js +678 -0
- package/dist/cjs/agent-definitions/auth.js.map +1 -0
- package/dist/cjs/agent-definitions/integrations.js +1173 -0
- package/dist/cjs/agent-definitions/integrations.js.map +1 -0
- package/dist/cjs/agent-definitions/remote-registry.js +469 -0
- package/dist/cjs/agent-definitions/remote-registry.js.map +1 -0
- package/dist/cjs/agent-definitions/secrets.js +193 -0
- package/dist/cjs/agent-definitions/secrets.js.map +1 -0
- package/dist/cjs/agent-definitions/users.js +440 -0
- package/dist/cjs/agent-definitions/users.js.map +1 -0
- package/dist/cjs/build.js +162 -0
- package/dist/cjs/build.js.map +1 -0
- package/dist/cjs/callback/index.js +74 -0
- package/dist/cjs/callback/index.js.map +1 -0
- package/dist/cjs/client.js +193 -0
- package/dist/cjs/client.js.map +1 -0
- package/dist/cjs/codegen.js +1027 -0
- package/dist/cjs/codegen.js.map +1 -0
- package/dist/cjs/crypto.js +44 -0
- package/dist/cjs/crypto.js.map +1 -0
- package/dist/cjs/define-config.js +81 -0
- package/dist/cjs/define-config.js.map +1 -0
- package/dist/cjs/define.js +186 -0
- package/dist/cjs/define.js.map +1 -0
- package/dist/cjs/events.js +60 -0
- package/dist/cjs/events.js.map +1 -0
- package/dist/cjs/index.js +195 -0
- package/dist/cjs/index.js.map +1 -0
- package/dist/cjs/integration-interface.js +105 -0
- package/dist/cjs/integration-interface.js.map +1 -0
- package/dist/cjs/integrations-store.js +53 -0
- package/dist/cjs/integrations-store.js.map +1 -0
- package/dist/cjs/introspect.js +136 -0
- package/dist/cjs/introspect.js.map +1 -0
- package/dist/cjs/jsonc.js +74 -0
- package/dist/cjs/jsonc.js.map +1 -0
- package/dist/cjs/jwt.js +207 -0
- package/dist/cjs/jwt.js.map +1 -0
- package/dist/cjs/key-manager.js +161 -0
- package/dist/cjs/key-manager.js.map +1 -0
- package/dist/cjs/oidc-signin.js +141 -0
- package/dist/cjs/oidc-signin.js.map +1 -0
- package/dist/cjs/pack.js +256 -0
- package/dist/cjs/pack.js.map +1 -0
- package/dist/cjs/package.json +1 -0
- package/dist/cjs/registry-consumer.js +233 -0
- package/dist/cjs/registry-consumer.js.map +1 -0
- package/dist/cjs/registry.js +512 -0
- package/dist/cjs/registry.js.map +1 -0
- package/dist/cjs/secret-collection.js +42 -0
- package/dist/cjs/secret-collection.js.map +1 -0
- package/dist/cjs/serialized.js +45 -0
- package/dist/cjs/serialized.js.map +1 -0
- package/dist/cjs/server.js +974 -0
- package/dist/cjs/server.js.map +1 -0
- package/dist/cjs/test-utils/mock-oidc-server.js +99 -0
- package/dist/cjs/test-utils/mock-oidc-server.js.map +1 -0
- package/dist/cjs/types.js +8 -0
- package/dist/cjs/types.js.map +1 -0
- package/dist/cjs/validate.js +84 -0
- package/dist/cjs/validate.js.map +1 -0
- package/dist/registry.js +6 -6
- package/dist/registry.js.map +1 -1
- package/package.json +13 -5
- package/src/callback/index.ts +13 -24
- package/src/registry.ts +6 -6
|
@@ -0,0 +1,1173 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Integrations Agent (@integrations)
|
|
4
|
+
*
|
|
5
|
+
* Built-in agent for managing third-party API integrations.
|
|
6
|
+
* Provides OAuth2 flows, provider config management, and API calling.
|
|
7
|
+
*
|
|
8
|
+
* Features:
|
|
9
|
+
* - Provider config management (setup/list/get)
|
|
10
|
+
* - OAuth2 authorization code flow (connect)
|
|
11
|
+
* - API calling with auto-injected auth (REST + GraphQL)
|
|
12
|
+
* - Agent-registry type for agent-to-agent federation
|
|
13
|
+
* - Pluggable IntegrationStore interface
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* ```typescript
|
|
17
|
+
* import { createAgentRegistry, createIntegrationsAgent } from '@slashfi/agents-sdk';
|
|
18
|
+
*
|
|
19
|
+
* const registry = createAgentRegistry();
|
|
20
|
+
* registry.register(createIntegrationsAgent({
|
|
21
|
+
* store: myIntegrationStore,
|
|
22
|
+
* callbackBaseUrl: 'https://myapp.com/oauth/callback',
|
|
23
|
+
* }));
|
|
24
|
+
* ```
|
|
25
|
+
*/
|
|
26
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
27
|
+
exports.createInMemoryIntegrationStore = createInMemoryIntegrationStore;
|
|
28
|
+
exports.getDefaultTokenBodyParams = getDefaultTokenBodyParams;
|
|
29
|
+
exports.getDefaultRefreshBodyParams = getDefaultRefreshBodyParams;
|
|
30
|
+
exports.exchangeCodeForToken = exchangeCodeForToken;
|
|
31
|
+
exports.refreshAccessToken = refreshAccessToken;
|
|
32
|
+
exports.createIntegrationsAgent = createIntegrationsAgent;
|
|
33
|
+
const define_js_1 = require("../define.js");
|
|
34
|
+
const secret_collection_js_1 = require("../secret-collection.js");
|
|
35
|
+
// ============================================
|
|
36
|
+
// In-Memory Integration Store
|
|
37
|
+
// ============================================
|
|
38
|
+
function createInMemoryIntegrationStore() {
|
|
39
|
+
const providers = new Map();
|
|
40
|
+
const connections = new Map(); // key: `${userId}:${providerId}`
|
|
41
|
+
function connKey(userId, providerId) {
|
|
42
|
+
return `${userId}:${providerId}`;
|
|
43
|
+
}
|
|
44
|
+
return {
|
|
45
|
+
async getProvider(providerId) {
|
|
46
|
+
return providers.get(providerId) ?? null;
|
|
47
|
+
},
|
|
48
|
+
async listProviders() {
|
|
49
|
+
return Array.from(providers.values());
|
|
50
|
+
},
|
|
51
|
+
async upsertProvider(config) {
|
|
52
|
+
providers.set(config.id, config);
|
|
53
|
+
},
|
|
54
|
+
async deleteProvider(providerId) {
|
|
55
|
+
return providers.delete(providerId);
|
|
56
|
+
},
|
|
57
|
+
async getConnection(userId, providerId) {
|
|
58
|
+
return connections.get(connKey(userId, providerId)) ?? null;
|
|
59
|
+
},
|
|
60
|
+
async listConnections(userId) {
|
|
61
|
+
return Array.from(connections.values()).filter((c) => c.userId === userId);
|
|
62
|
+
},
|
|
63
|
+
async upsertConnection(connection) {
|
|
64
|
+
connections.set(connKey(connection.userId, connection.providerId), connection);
|
|
65
|
+
},
|
|
66
|
+
async deleteConnection(userId, providerId) {
|
|
67
|
+
return connections.delete(connKey(userId, providerId));
|
|
68
|
+
},
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
// ============================================
|
|
72
|
+
// Token Exchange Helpers
|
|
73
|
+
// ============================================
|
|
74
|
+
const DEFAULT_TOKEN_BODY_PARAMS = {
|
|
75
|
+
client_secret_post: [
|
|
76
|
+
"grant_type",
|
|
77
|
+
"code",
|
|
78
|
+
"redirect_uri",
|
|
79
|
+
"client_id",
|
|
80
|
+
"client_secret",
|
|
81
|
+
],
|
|
82
|
+
client_secret_basic: ["grant_type", "code", "redirect_uri"],
|
|
83
|
+
};
|
|
84
|
+
const DEFAULT_REFRESH_BODY_PARAMS = {
|
|
85
|
+
client_secret_post: [
|
|
86
|
+
"grant_type",
|
|
87
|
+
"refresh_token",
|
|
88
|
+
"client_id",
|
|
89
|
+
"client_secret",
|
|
90
|
+
],
|
|
91
|
+
client_secret_basic: ["grant_type", "refresh_token"],
|
|
92
|
+
};
|
|
93
|
+
function getDefaultTokenBodyParams(method) {
|
|
94
|
+
return DEFAULT_TOKEN_BODY_PARAMS[method];
|
|
95
|
+
}
|
|
96
|
+
function getDefaultRefreshBodyParams(method) {
|
|
97
|
+
return DEFAULT_REFRESH_BODY_PARAMS[method];
|
|
98
|
+
}
|
|
99
|
+
function buildBasicAuth(clientId, clientSecret) {
|
|
100
|
+
const encoded = Buffer.from(`${clientId}:${clientSecret}`).toString("base64");
|
|
101
|
+
return `Basic ${encoded}`;
|
|
102
|
+
}
|
|
103
|
+
function buildAuthHeaders(config, accessToken) {
|
|
104
|
+
if (!config.api)
|
|
105
|
+
return {};
|
|
106
|
+
const { auth } = config.api;
|
|
107
|
+
const headerName = auth.headerName ?? "Authorization";
|
|
108
|
+
const prefix = auth.prefix ?? "Bearer";
|
|
109
|
+
return auth.type === "bearer" || auth.type === "header"
|
|
110
|
+
? { [headerName]: `${prefix} ${accessToken}` }
|
|
111
|
+
: { [headerName]: buildBasicAuth(accessToken, "") };
|
|
112
|
+
}
|
|
113
|
+
async function exchangeCodeForToken(config, code, redirectUri, clientId, clientSecret) {
|
|
114
|
+
if (!config.auth)
|
|
115
|
+
throw new Error(`Provider ${config.id} has no OAuth config`);
|
|
116
|
+
const oauth = config.auth;
|
|
117
|
+
const grantType = oauth.tokenGrantType ?? "authorization_code";
|
|
118
|
+
const bodyParams = oauth.tokenBodyParams ?? getDefaultTokenBodyParams(oauth.clientAuthMethod);
|
|
119
|
+
const allParams = {
|
|
120
|
+
grant_type: grantType,
|
|
121
|
+
code,
|
|
122
|
+
redirect_uri: redirectUri,
|
|
123
|
+
client_id: clientId,
|
|
124
|
+
client_secret: clientSecret,
|
|
125
|
+
};
|
|
126
|
+
const body = {};
|
|
127
|
+
for (const key of bodyParams) {
|
|
128
|
+
if (allParams[key])
|
|
129
|
+
body[key] = allParams[key];
|
|
130
|
+
}
|
|
131
|
+
const headers = {
|
|
132
|
+
"Content-Type": oauth.tokenContentType,
|
|
133
|
+
...(oauth.tokenHeaders ?? {}),
|
|
134
|
+
};
|
|
135
|
+
if (oauth.clientAuthMethod === "client_secret_basic") {
|
|
136
|
+
headers.Authorization = buildBasicAuth(clientId, clientSecret);
|
|
137
|
+
}
|
|
138
|
+
const fetchBody = oauth.tokenContentType === "application/x-www-form-urlencoded"
|
|
139
|
+
? new URLSearchParams(body).toString()
|
|
140
|
+
: JSON.stringify(body);
|
|
141
|
+
const response = await fetch(oauth.tokenUrl, {
|
|
142
|
+
method: "POST",
|
|
143
|
+
headers,
|
|
144
|
+
body: fetchBody,
|
|
145
|
+
});
|
|
146
|
+
if (!response.ok) {
|
|
147
|
+
const text = await response.text();
|
|
148
|
+
throw new Error(`Token exchange failed (${response.status}): ${text}`);
|
|
149
|
+
}
|
|
150
|
+
const responseText = await response.text();
|
|
151
|
+
console.log("[token-exchange] Slack response:", responseText.substring(0, 500));
|
|
152
|
+
let data;
|
|
153
|
+
try {
|
|
154
|
+
data = JSON.parse(responseText);
|
|
155
|
+
}
|
|
156
|
+
catch (e) {
|
|
157
|
+
throw new Error(`Failed to parse JSON: ${responseText.substring(0, 200)}`);
|
|
158
|
+
}
|
|
159
|
+
return {
|
|
160
|
+
accessToken: String(data[oauth.accessTokenField ?? "access_token"] ?? ""),
|
|
161
|
+
refreshToken: data[oauth.refreshTokenField ?? "refresh_token"],
|
|
162
|
+
expiresIn: data[oauth.expiresInField ?? "expires_in"],
|
|
163
|
+
tokenType: data[oauth.tokenTypeField ?? "token_type"],
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
async function refreshAccessToken(config, refreshToken, clientId, clientSecret) {
|
|
167
|
+
if (!config.auth)
|
|
168
|
+
throw new Error(`Provider ${config.id} has no OAuth config`);
|
|
169
|
+
const oauth = config.auth;
|
|
170
|
+
const url = oauth.refreshUrl ?? oauth.tokenUrl;
|
|
171
|
+
const grantType = oauth.refreshGrantType ?? "refresh_token";
|
|
172
|
+
const method = oauth.refreshClientAuthMethod ?? oauth.clientAuthMethod;
|
|
173
|
+
const contentType = oauth.refreshContentType ?? oauth.tokenContentType;
|
|
174
|
+
const bodyParams = oauth.refreshBodyParams ?? getDefaultRefreshBodyParams(method);
|
|
175
|
+
const allParams = {
|
|
176
|
+
grant_type: grantType,
|
|
177
|
+
refresh_token: refreshToken,
|
|
178
|
+
client_id: clientId,
|
|
179
|
+
client_secret: clientSecret,
|
|
180
|
+
};
|
|
181
|
+
const body = {};
|
|
182
|
+
for (const key of bodyParams) {
|
|
183
|
+
if (allParams[key])
|
|
184
|
+
body[key] = allParams[key];
|
|
185
|
+
}
|
|
186
|
+
const headers = {
|
|
187
|
+
"Content-Type": contentType,
|
|
188
|
+
...(oauth.refreshHeaders ?? oauth.tokenHeaders ?? {}),
|
|
189
|
+
};
|
|
190
|
+
if (method === "client_secret_basic") {
|
|
191
|
+
headers.Authorization = buildBasicAuth(clientId, clientSecret);
|
|
192
|
+
}
|
|
193
|
+
const fetchBody = contentType === "application/x-www-form-urlencoded"
|
|
194
|
+
? new URLSearchParams(body).toString()
|
|
195
|
+
: JSON.stringify(body);
|
|
196
|
+
const response = await fetch(url, {
|
|
197
|
+
method: "POST",
|
|
198
|
+
headers,
|
|
199
|
+
body: fetchBody,
|
|
200
|
+
});
|
|
201
|
+
if (!response.ok) {
|
|
202
|
+
const text = await response.text();
|
|
203
|
+
throw new Error(`Token refresh failed (${response.status}): ${text}`);
|
|
204
|
+
}
|
|
205
|
+
const responseText = await response.text();
|
|
206
|
+
console.log("[token-exchange] Slack response:", responseText.substring(0, 500));
|
|
207
|
+
let data;
|
|
208
|
+
try {
|
|
209
|
+
data = JSON.parse(responseText);
|
|
210
|
+
}
|
|
211
|
+
catch (e) {
|
|
212
|
+
throw new Error(`Failed to parse JSON: ${responseText.substring(0, 200)}`);
|
|
213
|
+
}
|
|
214
|
+
return {
|
|
215
|
+
accessToken: String(data[oauth.accessTokenField ?? "access_token"] ?? ""),
|
|
216
|
+
refreshToken: data[oauth.refreshTokenField ?? "refresh_token"],
|
|
217
|
+
expiresIn: data[oauth.expiresInField ?? "expires_in"],
|
|
218
|
+
tokenType: data[oauth.tokenTypeField ?? "token_type"],
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
// ============================================
|
|
222
|
+
// API Call Execution
|
|
223
|
+
// ============================================
|
|
224
|
+
async function executeRestCall(config, input, accessToken) {
|
|
225
|
+
const url = new URL(input.path, config.api?.baseUrl);
|
|
226
|
+
if (input.query) {
|
|
227
|
+
for (const [k, v] of Object.entries(input.query)) {
|
|
228
|
+
url.searchParams.set(k, v);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
const headers = {
|
|
232
|
+
...buildAuthHeaders(config, accessToken),
|
|
233
|
+
...(config.api?.defaultHeaders ?? {}),
|
|
234
|
+
};
|
|
235
|
+
if (input.body) {
|
|
236
|
+
headers["Content-Type"] = "application/json";
|
|
237
|
+
}
|
|
238
|
+
const response = await fetch(url.toString(), {
|
|
239
|
+
method: input.method,
|
|
240
|
+
headers,
|
|
241
|
+
body: input.body ? JSON.stringify(input.body) : undefined,
|
|
242
|
+
});
|
|
243
|
+
const text = await response.text();
|
|
244
|
+
try {
|
|
245
|
+
return JSON.parse(text);
|
|
246
|
+
}
|
|
247
|
+
catch {
|
|
248
|
+
return { status: response.status, body: text };
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
async function executeGraphqlCall(config, input, accessToken) {
|
|
252
|
+
const headers = {
|
|
253
|
+
"Content-Type": "application/json",
|
|
254
|
+
...buildAuthHeaders(config, accessToken),
|
|
255
|
+
...(config.api?.defaultHeaders ?? {}),
|
|
256
|
+
};
|
|
257
|
+
if (!config.api?.baseUrl)
|
|
258
|
+
throw new Error("No baseUrl configured for this provider");
|
|
259
|
+
const response = await fetch(config.api.baseUrl, {
|
|
260
|
+
method: "POST",
|
|
261
|
+
headers,
|
|
262
|
+
body: JSON.stringify({ query: input.query, variables: input.variables }),
|
|
263
|
+
});
|
|
264
|
+
return response.json();
|
|
265
|
+
}
|
|
266
|
+
// ============================================
|
|
267
|
+
// Credential Storage Helpers
|
|
268
|
+
// ============================================
|
|
269
|
+
const SYSTEM_OWNER = "__integrations__";
|
|
270
|
+
// ============================================
|
|
271
|
+
// Create Integrations Agent
|
|
272
|
+
// ============================================
|
|
273
|
+
function createIntegrationsAgent(options) {
|
|
274
|
+
const { store, callbackBaseUrl, secretStore } = options;
|
|
275
|
+
// ---- setup_integration ----
|
|
276
|
+
const setupTool = (0, define_js_1.defineTool)({
|
|
277
|
+
name: "setup_integration",
|
|
278
|
+
description: "Create or update an integration provider config. " +
|
|
279
|
+
"Registers a third-party API (REST, GraphQL, or agent-registry) " +
|
|
280
|
+
"with its OAuth and API configuration.",
|
|
281
|
+
visibility: "public",
|
|
282
|
+
inputSchema: {
|
|
283
|
+
type: "object",
|
|
284
|
+
properties: {
|
|
285
|
+
id: {
|
|
286
|
+
type: "string",
|
|
287
|
+
description: "Provider ID (e.g. 'linear', 'notion')",
|
|
288
|
+
},
|
|
289
|
+
name: { type: "string", description: "Display name" },
|
|
290
|
+
agentPath: {
|
|
291
|
+
type: "string",
|
|
292
|
+
description: "Agent path that handles this integration (e.g. '@remote-registry', '@databases'). Omit for simple REST/GraphQL integrations.",
|
|
293
|
+
},
|
|
294
|
+
scope: {
|
|
295
|
+
type: "string",
|
|
296
|
+
enum: ["user", "tenant"],
|
|
297
|
+
description: "'user' for per-user tokens, 'tenant' for shared org-wide. Default: user",
|
|
298
|
+
},
|
|
299
|
+
api: {
|
|
300
|
+
type: "object",
|
|
301
|
+
description: "API config: baseUrl, auth type, default headers",
|
|
302
|
+
properties: {
|
|
303
|
+
baseUrl: { type: "string", description: "API base URL" },
|
|
304
|
+
docsUrl: { type: "string", description: "API docs URL" },
|
|
305
|
+
defaultHeaders: {
|
|
306
|
+
type: "object",
|
|
307
|
+
description: "Default headers for all requests",
|
|
308
|
+
additionalProperties: { type: "string" },
|
|
309
|
+
},
|
|
310
|
+
auth: {
|
|
311
|
+
type: "object",
|
|
312
|
+
description: "Auth config",
|
|
313
|
+
properties: {
|
|
314
|
+
type: { type: "string", enum: ["bearer", "basic", "header"] },
|
|
315
|
+
headerName: { type: "string" },
|
|
316
|
+
prefix: { type: "string" },
|
|
317
|
+
},
|
|
318
|
+
required: ["type"],
|
|
319
|
+
},
|
|
320
|
+
},
|
|
321
|
+
required: ["baseUrl", "auth"],
|
|
322
|
+
},
|
|
323
|
+
auth: {
|
|
324
|
+
type: "object",
|
|
325
|
+
description: "OAuth config (optional — omit for token-less integrations)",
|
|
326
|
+
properties: {
|
|
327
|
+
authUrl: { type: "string" },
|
|
328
|
+
tokenUrl: { type: "string" },
|
|
329
|
+
scopes: { type: "array", items: { type: "string" } },
|
|
330
|
+
scopeSeparator: { type: "string" },
|
|
331
|
+
clientAuthMethod: {
|
|
332
|
+
type: "string",
|
|
333
|
+
enum: ["client_secret_post", "client_secret_basic"],
|
|
334
|
+
},
|
|
335
|
+
tokenContentType: {
|
|
336
|
+
type: "string",
|
|
337
|
+
enum: ["application/x-www-form-urlencoded", "application/json"],
|
|
338
|
+
},
|
|
339
|
+
tokenGrantType: { type: "string" },
|
|
340
|
+
tokenBodyParams: { type: "array", items: { type: "string" } },
|
|
341
|
+
tokenHeaders: {
|
|
342
|
+
type: "object",
|
|
343
|
+
additionalProperties: { type: "string" },
|
|
344
|
+
},
|
|
345
|
+
authUrlExtraParams: {
|
|
346
|
+
type: "object",
|
|
347
|
+
additionalProperties: { type: "string" },
|
|
348
|
+
},
|
|
349
|
+
accessTokenField: { type: "string" },
|
|
350
|
+
refreshTokenField: { type: "string" },
|
|
351
|
+
expiresInField: { type: "string" },
|
|
352
|
+
tokenTypeField: { type: "string" },
|
|
353
|
+
refreshUrl: { type: "string" },
|
|
354
|
+
refreshClientAuthMethod: {
|
|
355
|
+
type: "string",
|
|
356
|
+
enum: ["client_secret_post", "client_secret_basic"],
|
|
357
|
+
},
|
|
358
|
+
refreshContentType: {
|
|
359
|
+
type: "string",
|
|
360
|
+
enum: ["application/x-www-form-urlencoded", "application/json"],
|
|
361
|
+
},
|
|
362
|
+
refreshGrantType: { type: "string" },
|
|
363
|
+
refreshBodyParams: { type: "array", items: { type: "string" } },
|
|
364
|
+
refreshHeaders: {
|
|
365
|
+
type: "object",
|
|
366
|
+
additionalProperties: { type: "string" },
|
|
367
|
+
},
|
|
368
|
+
},
|
|
369
|
+
},
|
|
370
|
+
docs: {
|
|
371
|
+
type: "object",
|
|
372
|
+
description: "Documentation links",
|
|
373
|
+
properties: {
|
|
374
|
+
llmsTxt: { type: "string" },
|
|
375
|
+
human: { type: "array", items: { type: "string" } },
|
|
376
|
+
},
|
|
377
|
+
},
|
|
378
|
+
clientId: {
|
|
379
|
+
secret: true,
|
|
380
|
+
type: "string",
|
|
381
|
+
description: "OAuth client ID for this provider. Stored encrypted.",
|
|
382
|
+
},
|
|
383
|
+
clientSecret: {
|
|
384
|
+
secret: true,
|
|
385
|
+
type: "string",
|
|
386
|
+
description: "OAuth client secret for this provider. Stored encrypted.",
|
|
387
|
+
},
|
|
388
|
+
},
|
|
389
|
+
required: ["id", "name", "type", "api"],
|
|
390
|
+
},
|
|
391
|
+
execute: async (input, _ctx) => {
|
|
392
|
+
const config = {
|
|
393
|
+
id: input.id,
|
|
394
|
+
name: input.name,
|
|
395
|
+
agentPath: input.agentPath,
|
|
396
|
+
scope: input.scope,
|
|
397
|
+
docs: input.docs,
|
|
398
|
+
auth: input.auth,
|
|
399
|
+
api: input.api,
|
|
400
|
+
};
|
|
401
|
+
// Store client credentials encrypted and save secret IDs
|
|
402
|
+
const result = { success: true };
|
|
403
|
+
if (input.clientId) {
|
|
404
|
+
const secretId = await secretStore.store(input.clientId, SYSTEM_OWNER);
|
|
405
|
+
config._clientIdSecretId = secretId;
|
|
406
|
+
result.clientIdStored = true;
|
|
407
|
+
}
|
|
408
|
+
if (input.clientSecret) {
|
|
409
|
+
const secretId = await secretStore.store(input.clientSecret, SYSTEM_OWNER);
|
|
410
|
+
config._clientSecretSecretId = secretId;
|
|
411
|
+
result.clientSecretStored = true;
|
|
412
|
+
}
|
|
413
|
+
await store.upsertProvider(config);
|
|
414
|
+
// Delegate to agent's setup_integration tool via registry.call()
|
|
415
|
+
if (config.agentPath && options.registry) {
|
|
416
|
+
try {
|
|
417
|
+
const setupResult = await options.registry.call({
|
|
418
|
+
action: "execute_tool",
|
|
419
|
+
path: config.agentPath,
|
|
420
|
+
tool: "setup_integration",
|
|
421
|
+
params: input.config ?? input,
|
|
422
|
+
callerType: "system",
|
|
423
|
+
});
|
|
424
|
+
result.setupResult = setupResult?.result ?? setupResult;
|
|
425
|
+
}
|
|
426
|
+
catch (err) {
|
|
427
|
+
result.setupError = err instanceof Error ? err.message : String(err);
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
result.provider = config;
|
|
431
|
+
return result;
|
|
432
|
+
},
|
|
433
|
+
});
|
|
434
|
+
// ---- discover_integrations ----
|
|
435
|
+
const discoverTool = (0, define_js_1.defineTool)({
|
|
436
|
+
name: "discover_integrations",
|
|
437
|
+
description: "Discover available integration types that can be set up. " +
|
|
438
|
+
"Returns a catalog of integrations with their setup/connect schemas " +
|
|
439
|
+
"so you know what parameters to pass to setup_integration.",
|
|
440
|
+
visibility: "public",
|
|
441
|
+
inputSchema: {
|
|
442
|
+
type: "object",
|
|
443
|
+
properties: {
|
|
444
|
+
query: {
|
|
445
|
+
type: "string",
|
|
446
|
+
description: "Search query to filter integrations by name or description",
|
|
447
|
+
},
|
|
448
|
+
category: {
|
|
449
|
+
type: "string",
|
|
450
|
+
description: "Filter by category (e.g. 'infrastructure', 'communication')",
|
|
451
|
+
},
|
|
452
|
+
},
|
|
453
|
+
},
|
|
454
|
+
execute: async (input, _ctx) => {
|
|
455
|
+
const catalog = [];
|
|
456
|
+
// 1. Agent-backed integrations
|
|
457
|
+
if (options.registry) {
|
|
458
|
+
for (const agent of options.registry.list?.() ?? []) {
|
|
459
|
+
if (agent.config?.integration) {
|
|
460
|
+
const ic = agent.config.integration;
|
|
461
|
+
catalog.push({
|
|
462
|
+
provider: ic.provider,
|
|
463
|
+
agentPath: agent.path,
|
|
464
|
+
displayName: ic.displayName,
|
|
465
|
+
icon: ic.icon,
|
|
466
|
+
category: ic.category,
|
|
467
|
+
description: ic.description,
|
|
468
|
+
setupSchema: ic.setupSchema,
|
|
469
|
+
connectSchema: ic.connectSchema,
|
|
470
|
+
});
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
// 2. DB-stored providers (legacy OAuth)
|
|
475
|
+
const providers = await store.listProviders();
|
|
476
|
+
for (const p of providers) {
|
|
477
|
+
// Skip if already in catalog from agent scan
|
|
478
|
+
if (catalog.some((c) => c.provider === p.id))
|
|
479
|
+
continue;
|
|
480
|
+
catalog.push({
|
|
481
|
+
provider: p.id,
|
|
482
|
+
displayName: p.name,
|
|
483
|
+
agentPath: p.agentPath,
|
|
484
|
+
hasOAuth: !!p.auth,
|
|
485
|
+
connectSchema: p.auth
|
|
486
|
+
? {
|
|
487
|
+
type: "object",
|
|
488
|
+
description: "OAuth flow — use connect_integration to start",
|
|
489
|
+
properties: {
|
|
490
|
+
provider: { type: "string", const: p.id },
|
|
491
|
+
},
|
|
492
|
+
}
|
|
493
|
+
: undefined,
|
|
494
|
+
});
|
|
495
|
+
}
|
|
496
|
+
// 3. Filter
|
|
497
|
+
let results = catalog;
|
|
498
|
+
if (input.query) {
|
|
499
|
+
const q = input.query.toLowerCase();
|
|
500
|
+
results = results.filter((r) => r.provider.toLowerCase().includes(q) ||
|
|
501
|
+
r.displayName.toLowerCase().includes(q) ||
|
|
502
|
+
(r.description?.toLowerCase().includes(q) ?? false));
|
|
503
|
+
}
|
|
504
|
+
if (input.category) {
|
|
505
|
+
results = results.filter((r) => r.category === input.category);
|
|
506
|
+
}
|
|
507
|
+
return { integrations: results };
|
|
508
|
+
},
|
|
509
|
+
});
|
|
510
|
+
// ---- list_integrations ----
|
|
511
|
+
const listTool = (0, define_js_1.defineTool)({
|
|
512
|
+
name: "list_integrations",
|
|
513
|
+
description: "List configured integration providers and user's connections.",
|
|
514
|
+
visibility: "public",
|
|
515
|
+
inputSchema: {
|
|
516
|
+
type: "object",
|
|
517
|
+
properties: {
|
|
518
|
+
userId: {
|
|
519
|
+
type: "string",
|
|
520
|
+
description: "User ID to check connections for (optional)",
|
|
521
|
+
},
|
|
522
|
+
},
|
|
523
|
+
},
|
|
524
|
+
execute: async (input, ctx) => {
|
|
525
|
+
const providers = await store.listProviders();
|
|
526
|
+
const userId = input.userId ?? ctx.callerId;
|
|
527
|
+
const connections = userId ? await store.listConnections(userId) : [];
|
|
528
|
+
// Build unified integrations list
|
|
529
|
+
const integrations = [];
|
|
530
|
+
// 1. DB-stored providers (legacy OAuth integrations)
|
|
531
|
+
for (const p of providers) {
|
|
532
|
+
integrations.push({
|
|
533
|
+
id: p.id,
|
|
534
|
+
name: p.name,
|
|
535
|
+
provider: p.id,
|
|
536
|
+
agentPath: p.agentPath,
|
|
537
|
+
scope: p.scope ?? "user",
|
|
538
|
+
hasOAuth: !!p.auth,
|
|
539
|
+
connected: connections.some((c) => c.providerId === p.id),
|
|
540
|
+
});
|
|
541
|
+
}
|
|
542
|
+
// 2. Agent-backed integrations (agents with config.integration + list_integrations tool)
|
|
543
|
+
if (options.registry) {
|
|
544
|
+
const agents = options.registry.list?.() ?? [];
|
|
545
|
+
for (const agent of agents) {
|
|
546
|
+
const hasListTool = agent.tools?.some((t) => t.name === "list_integrations");
|
|
547
|
+
if (hasListTool && agent.config?.integration) {
|
|
548
|
+
const meta = {
|
|
549
|
+
provider: agent.config.integration.provider,
|
|
550
|
+
agentPath: agent.path,
|
|
551
|
+
displayName: agent.config.integration.displayName,
|
|
552
|
+
icon: agent.config.integration.icon,
|
|
553
|
+
category: agent.config.integration.category,
|
|
554
|
+
description: agent.config.integration.description,
|
|
555
|
+
};
|
|
556
|
+
try {
|
|
557
|
+
const callResult = options.registry
|
|
558
|
+
? await options.registry.call({
|
|
559
|
+
action: "execute_tool",
|
|
560
|
+
path: agent.path,
|
|
561
|
+
tool: "list_integrations",
|
|
562
|
+
params: {},
|
|
563
|
+
callerType: "system",
|
|
564
|
+
})
|
|
565
|
+
: null;
|
|
566
|
+
const result = callResult?.result ??
|
|
567
|
+
callResult ?? { success: false };
|
|
568
|
+
if (result.success && result.data) {
|
|
569
|
+
// Flatten: if data has an array field, each item becomes an integration
|
|
570
|
+
const items = Array.isArray(result.data)
|
|
571
|
+
? result.data
|
|
572
|
+
: (Object.values(result.data).find(Array.isArray) ?? []);
|
|
573
|
+
for (const item of items) {
|
|
574
|
+
integrations.push({
|
|
575
|
+
...meta,
|
|
576
|
+
...(typeof item === "object" && item !== null
|
|
577
|
+
? item
|
|
578
|
+
: { value: item }),
|
|
579
|
+
});
|
|
580
|
+
}
|
|
581
|
+
// If no items found but agent exists, include it as a provider entry
|
|
582
|
+
if (items.length === 0) {
|
|
583
|
+
integrations.push({ ...meta, id: meta.provider });
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
else {
|
|
587
|
+
integrations.push({ ...meta, id: meta.provider });
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
catch {
|
|
591
|
+
integrations.push({ ...meta, id: meta.provider });
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
return {
|
|
597
|
+
integrations,
|
|
598
|
+
connections: connections.map((c) => ({
|
|
599
|
+
providerId: c.providerId,
|
|
600
|
+
connectedAt: c.connectedAt,
|
|
601
|
+
expiresAt: c.expiresAt,
|
|
602
|
+
})),
|
|
603
|
+
};
|
|
604
|
+
},
|
|
605
|
+
});
|
|
606
|
+
// ---- get_integration ----
|
|
607
|
+
const getTool = (0, define_js_1.defineTool)({
|
|
608
|
+
name: "get_integration",
|
|
609
|
+
description: "Get a specific integration provider config.",
|
|
610
|
+
visibility: "public",
|
|
611
|
+
inputSchema: {
|
|
612
|
+
type: "object",
|
|
613
|
+
properties: {
|
|
614
|
+
provider: { type: "string", description: "Provider ID" },
|
|
615
|
+
},
|
|
616
|
+
required: ["provider"],
|
|
617
|
+
},
|
|
618
|
+
execute: async (input, _ctx) => {
|
|
619
|
+
const config = await store.getProvider(input.provider);
|
|
620
|
+
if (!config)
|
|
621
|
+
return { error: `Provider '${input.provider}' not found` };
|
|
622
|
+
return { provider: config };
|
|
623
|
+
},
|
|
624
|
+
});
|
|
625
|
+
// ---- connect_integration ----
|
|
626
|
+
const connectTool = (0, define_js_1.defineTool)({
|
|
627
|
+
name: "connect_integration",
|
|
628
|
+
description: "Generate an OAuth authorization URL for a user to connect an integration. " +
|
|
629
|
+
"Returns the URL the user should visit to authorize.",
|
|
630
|
+
visibility: "public",
|
|
631
|
+
inputSchema: {
|
|
632
|
+
type: "object",
|
|
633
|
+
properties: {
|
|
634
|
+
provider: { type: "string", description: "Provider ID to connect" },
|
|
635
|
+
userId: {
|
|
636
|
+
type: "string",
|
|
637
|
+
description: "User ID (optional, defaults to caller)",
|
|
638
|
+
},
|
|
639
|
+
state: {
|
|
640
|
+
type: "string",
|
|
641
|
+
description: "Optional state param for the OAuth flow",
|
|
642
|
+
},
|
|
643
|
+
},
|
|
644
|
+
required: ["provider"],
|
|
645
|
+
},
|
|
646
|
+
execute: async (input, ctx) => {
|
|
647
|
+
const config = await store.getProvider(input.provider);
|
|
648
|
+
if (!config)
|
|
649
|
+
return { error: `Provider '${input.provider}' not found` };
|
|
650
|
+
// Delegate to agent's connect_integration tool via registry.call()
|
|
651
|
+
if (config.agentPath && options.registry) {
|
|
652
|
+
const connectResult = await options.registry.call({
|
|
653
|
+
action: "execute_tool",
|
|
654
|
+
path: config.agentPath,
|
|
655
|
+
tool: "connect_integration",
|
|
656
|
+
params: { ...input, registryId: config.id },
|
|
657
|
+
callerType: "system",
|
|
658
|
+
});
|
|
659
|
+
return connectResult?.result ?? connectResult;
|
|
660
|
+
}
|
|
661
|
+
if (!config.auth)
|
|
662
|
+
return { error: `Provider '${input.provider}' has no OAuth config` };
|
|
663
|
+
if (!callbackBaseUrl)
|
|
664
|
+
return { error: "No callbackBaseUrl configured for OAuth flows" };
|
|
665
|
+
const oauth = config.auth;
|
|
666
|
+
const redirectUri = callbackBaseUrl;
|
|
667
|
+
const userId = input.userId ?? ctx.callerId;
|
|
668
|
+
// Resolve client ID from secret store
|
|
669
|
+
// Check both _clientIdSecretId (from setup_integration direct) and
|
|
670
|
+
// clientId field as secret:ref (from collect_secrets flow)
|
|
671
|
+
let clientId = null;
|
|
672
|
+
const cidSecretId = config._clientIdSecretId;
|
|
673
|
+
if (cidSecretId && secretStore) {
|
|
674
|
+
clientId = await secretStore.resolve(cidSecretId, SYSTEM_OWNER);
|
|
675
|
+
}
|
|
676
|
+
// Also check if auth config has clientId as a secret:ref
|
|
677
|
+
if (!clientId &&
|
|
678
|
+
oauth.clientId &&
|
|
679
|
+
typeof oauth.clientId === "string") {
|
|
680
|
+
if (oauth.clientId.startsWith("secret:") && secretStore) {
|
|
681
|
+
const refId = oauth.clientId.slice("secret:".length);
|
|
682
|
+
clientId = await secretStore.resolve(refId, SYSTEM_OWNER);
|
|
683
|
+
}
|
|
684
|
+
else if (!oauth.clientId.startsWith("secret:")) {
|
|
685
|
+
clientId = oauth.clientId;
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
// Check top-level config too
|
|
689
|
+
if (!clientId && config.clientId) {
|
|
690
|
+
const cid = config.clientId;
|
|
691
|
+
if (typeof cid === "string" &&
|
|
692
|
+
cid.startsWith("secret:") &&
|
|
693
|
+
secretStore) {
|
|
694
|
+
clientId = await secretStore.resolve(cid.slice("secret:".length), SYSTEM_OWNER);
|
|
695
|
+
}
|
|
696
|
+
else if (typeof cid === "string" && !cid.startsWith("secret:")) {
|
|
697
|
+
clientId = cid;
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
if (!clientId) {
|
|
701
|
+
return {
|
|
702
|
+
error: `No client credentials stored for '${config.id}'. Use setup_integration with clientId/clientSecret or collect_secrets.`,
|
|
703
|
+
};
|
|
704
|
+
}
|
|
705
|
+
const separator = oauth.scopeSeparator ?? " ";
|
|
706
|
+
const scopeStr = oauth.scopes.join(separator);
|
|
707
|
+
const params = new URLSearchParams({
|
|
708
|
+
client_id: clientId,
|
|
709
|
+
redirect_uri: redirectUri,
|
|
710
|
+
response_type: "code",
|
|
711
|
+
...(scopeStr ? { scope: scopeStr } : {}),
|
|
712
|
+
state: input.state ??
|
|
713
|
+
btoa(JSON.stringify({
|
|
714
|
+
userId,
|
|
715
|
+
providerId: config.id,
|
|
716
|
+
redirectUrl: input.redirectUrl ?? "/",
|
|
717
|
+
})),
|
|
718
|
+
...(oauth.authUrlExtraParams ?? {}),
|
|
719
|
+
});
|
|
720
|
+
return {
|
|
721
|
+
authUrl: `${oauth.authUrl}?${params.toString()}`,
|
|
722
|
+
redirectUri,
|
|
723
|
+
provider: config.id,
|
|
724
|
+
};
|
|
725
|
+
},
|
|
726
|
+
});
|
|
727
|
+
// ---- call_integration ----
|
|
728
|
+
const callTool = (0, define_js_1.defineTool)({
|
|
729
|
+
name: "call_integration",
|
|
730
|
+
description: "Call a configured integration API. Supports REST, GraphQL, and agent-registry types. " +
|
|
731
|
+
"Automatically injects the user's access token.",
|
|
732
|
+
visibility: "public",
|
|
733
|
+
inputSchema: {
|
|
734
|
+
type: "object",
|
|
735
|
+
properties: {
|
|
736
|
+
provider: { type: "string", description: "Provider ID" },
|
|
737
|
+
type: {
|
|
738
|
+
type: "string",
|
|
739
|
+
enum: ["rest", "graphql"],
|
|
740
|
+
description: "Call type (rest or graphql)",
|
|
741
|
+
},
|
|
742
|
+
// REST fields
|
|
743
|
+
method: {
|
|
744
|
+
type: "string",
|
|
745
|
+
enum: ["GET", "POST", "PUT", "PATCH", "DELETE"],
|
|
746
|
+
},
|
|
747
|
+
path: { type: "string", description: "API path (for REST)" },
|
|
748
|
+
body: {
|
|
749
|
+
type: "object",
|
|
750
|
+
description: "Request body (for REST POST/PUT/PATCH)",
|
|
751
|
+
},
|
|
752
|
+
query: {
|
|
753
|
+
type: "object",
|
|
754
|
+
description: "Query params (for REST)",
|
|
755
|
+
additionalProperties: { type: "string" },
|
|
756
|
+
},
|
|
757
|
+
// GraphQL fields
|
|
758
|
+
graphqlQuery: { type: "string", description: "GraphQL query string" },
|
|
759
|
+
variables: { type: "object", description: "GraphQL variables" },
|
|
760
|
+
// Agent-registry fields
|
|
761
|
+
agent: {
|
|
762
|
+
type: "string",
|
|
763
|
+
description: "Agent path (for agent-registry)",
|
|
764
|
+
},
|
|
765
|
+
tool: { type: "string", description: "Tool name (for agent-registry)" },
|
|
766
|
+
params: {
|
|
767
|
+
type: "object",
|
|
768
|
+
description: "Tool params (for agent-registry)",
|
|
769
|
+
},
|
|
770
|
+
},
|
|
771
|
+
required: ["provider", "type"],
|
|
772
|
+
},
|
|
773
|
+
execute: async (input, ctx) => {
|
|
774
|
+
const config = await store.getProvider(input.provider);
|
|
775
|
+
if (!config)
|
|
776
|
+
return { error: `Provider '${input.provider}' not found` };
|
|
777
|
+
const userId = ctx.callerId;
|
|
778
|
+
// Get access token
|
|
779
|
+
const connection = await store.getConnection(userId, input.provider);
|
|
780
|
+
if (!connection) {
|
|
781
|
+
return {
|
|
782
|
+
error: `Not connected to '${input.provider}'. Use connect_integration first.`,
|
|
783
|
+
hint: "connect_integration",
|
|
784
|
+
};
|
|
785
|
+
}
|
|
786
|
+
// Check if token needs refresh
|
|
787
|
+
let accessToken = connection.accessToken;
|
|
788
|
+
if (connection.expiresAt &&
|
|
789
|
+
connection.expiresAt < Date.now() &&
|
|
790
|
+
connection.refreshToken) {
|
|
791
|
+
try {
|
|
792
|
+
const rCidId = config._clientIdSecretId;
|
|
793
|
+
const rCsecId = config._clientSecretSecretId;
|
|
794
|
+
if (!rCidId || !rCsecId) {
|
|
795
|
+
throw new Error("No client credentials stored. Re-run setup_integration with clientId/clientSecret.");
|
|
796
|
+
}
|
|
797
|
+
const clientId = await secretStore.resolve(rCidId, SYSTEM_OWNER);
|
|
798
|
+
const clientSecret = await secretStore.resolve(rCsecId, SYSTEM_OWNER);
|
|
799
|
+
if (!clientId || !clientSecret) {
|
|
800
|
+
throw new Error("Failed to resolve client credentials from secret store.");
|
|
801
|
+
}
|
|
802
|
+
const refreshed = await refreshAccessToken(config, connection.refreshToken, clientId, clientSecret);
|
|
803
|
+
accessToken = refreshed.accessToken;
|
|
804
|
+
// Update stored connection
|
|
805
|
+
await store.upsertConnection({
|
|
806
|
+
...connection,
|
|
807
|
+
accessToken: refreshed.accessToken,
|
|
808
|
+
refreshToken: refreshed.refreshToken ?? connection.refreshToken,
|
|
809
|
+
expiresAt: refreshed.expiresIn
|
|
810
|
+
? Date.now() + refreshed.expiresIn * 1000
|
|
811
|
+
: connection.expiresAt,
|
|
812
|
+
});
|
|
813
|
+
}
|
|
814
|
+
catch (err) {
|
|
815
|
+
return {
|
|
816
|
+
error: `Token refresh failed for '${input.provider}': ${err instanceof Error ? err.message : String(err)}`,
|
|
817
|
+
hint: "connect_integration",
|
|
818
|
+
};
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
// Execute the call based on type
|
|
822
|
+
switch (input.type) {
|
|
823
|
+
case "rest":
|
|
824
|
+
return executeRestCall(config, {
|
|
825
|
+
provider: input.provider,
|
|
826
|
+
type: "rest",
|
|
827
|
+
method: input.method ?? "GET",
|
|
828
|
+
path: input.path ?? "/",
|
|
829
|
+
body: input.body,
|
|
830
|
+
query: input.query,
|
|
831
|
+
}, accessToken);
|
|
832
|
+
case "graphql":
|
|
833
|
+
return executeGraphqlCall(config, {
|
|
834
|
+
provider: input.provider,
|
|
835
|
+
type: "graphql",
|
|
836
|
+
query: input.graphqlQuery ?? input.query ?? "",
|
|
837
|
+
variables: input.variables,
|
|
838
|
+
}, accessToken);
|
|
839
|
+
default:
|
|
840
|
+
return {
|
|
841
|
+
error: `Unknown call type: ${input.type}. Use 'rest' or 'graphql'.`,
|
|
842
|
+
};
|
|
843
|
+
}
|
|
844
|
+
},
|
|
845
|
+
});
|
|
846
|
+
// ---- handle_callback (OAuth callback handler) ----
|
|
847
|
+
const callbackTool = (0, define_js_1.defineTool)({
|
|
848
|
+
name: "handle_oauth_callback",
|
|
849
|
+
description: "Handle an OAuth callback. Exchanges the authorization code for tokens and stores the connection. " +
|
|
850
|
+
"This is typically called by the HTTP server when the OAuth redirect hits the callback URL.",
|
|
851
|
+
visibility: "internal",
|
|
852
|
+
inputSchema: {
|
|
853
|
+
type: "object",
|
|
854
|
+
properties: {
|
|
855
|
+
provider: { type: "string", description: "Provider ID" },
|
|
856
|
+
code: {
|
|
857
|
+
type: "string",
|
|
858
|
+
description: "Authorization code from the OAuth redirect",
|
|
859
|
+
},
|
|
860
|
+
state: {
|
|
861
|
+
type: "string",
|
|
862
|
+
description: "State param from the OAuth redirect",
|
|
863
|
+
},
|
|
864
|
+
},
|
|
865
|
+
required: ["provider", "code"],
|
|
866
|
+
},
|
|
867
|
+
execute: async (input, ctx) => {
|
|
868
|
+
const config = await store.getProvider(input.provider);
|
|
869
|
+
if (!config)
|
|
870
|
+
return { error: `Provider '${input.provider}' not found` };
|
|
871
|
+
// Delegate to agent's connect_integration tool via registry.call()
|
|
872
|
+
if (config.agentPath && options.registry) {
|
|
873
|
+
const connectResult = await options.registry.call({
|
|
874
|
+
action: "execute_tool",
|
|
875
|
+
path: config.agentPath,
|
|
876
|
+
tool: "connect_integration",
|
|
877
|
+
params: { ...input, registryId: config.id },
|
|
878
|
+
callerType: "system",
|
|
879
|
+
});
|
|
880
|
+
return connectResult?.result ?? connectResult;
|
|
881
|
+
}
|
|
882
|
+
if (!config.auth)
|
|
883
|
+
return { error: `Provider '${input.provider}' has no OAuth config` };
|
|
884
|
+
if (!callbackBaseUrl)
|
|
885
|
+
return { error: "No callbackBaseUrl configured" };
|
|
886
|
+
// Parse state to get userId
|
|
887
|
+
let userId = ctx.callerId;
|
|
888
|
+
if (input.state) {
|
|
889
|
+
try {
|
|
890
|
+
const parsed = JSON.parse(atob(input.state));
|
|
891
|
+
if (parsed.userId)
|
|
892
|
+
userId = parsed.userId;
|
|
893
|
+
}
|
|
894
|
+
catch { }
|
|
895
|
+
}
|
|
896
|
+
// Resolve client credentials from secret store via config
|
|
897
|
+
const cbCidId = config._clientIdSecretId;
|
|
898
|
+
const cbCsecId = config._clientSecretSecretId;
|
|
899
|
+
if (!cbCidId || !cbCsecId) {
|
|
900
|
+
return { error: "No client credentials stored for this provider." };
|
|
901
|
+
}
|
|
902
|
+
const clientId = await secretStore.resolve(cbCidId, SYSTEM_OWNER);
|
|
903
|
+
const clientSecret = await secretStore.resolve(cbCsecId, SYSTEM_OWNER);
|
|
904
|
+
if (!clientId || !clientSecret) {
|
|
905
|
+
return { error: "Failed to resolve client credentials." };
|
|
906
|
+
}
|
|
907
|
+
const redirectUri = callbackBaseUrl;
|
|
908
|
+
const result = await exchangeCodeForToken(config, input.code, redirectUri, clientId, clientSecret);
|
|
909
|
+
const connection = {
|
|
910
|
+
userId,
|
|
911
|
+
providerId: config.id,
|
|
912
|
+
accessToken: result.accessToken,
|
|
913
|
+
refreshToken: result.refreshToken,
|
|
914
|
+
expiresAt: result.expiresIn
|
|
915
|
+
? Date.now() + result.expiresIn * 1000
|
|
916
|
+
: undefined,
|
|
917
|
+
tokenType: result.tokenType,
|
|
918
|
+
scopes: config.auth.scopes,
|
|
919
|
+
connectedAt: Date.now(),
|
|
920
|
+
};
|
|
921
|
+
await store.upsertConnection(connection);
|
|
922
|
+
return {
|
|
923
|
+
success: true,
|
|
924
|
+
provider: config.id,
|
|
925
|
+
userId,
|
|
926
|
+
connectedAt: connection.connectedAt,
|
|
927
|
+
accessToken: result.accessToken,
|
|
928
|
+
};
|
|
929
|
+
},
|
|
930
|
+
});
|
|
931
|
+
// ---- collect_secrets ----
|
|
932
|
+
const collectSecretsTool = (0, define_js_1.defineTool)({
|
|
933
|
+
name: "collect_secrets",
|
|
934
|
+
description: "Collect secrets and missing fields for a tool via a secure form. " +
|
|
935
|
+
"Pass the target agent + tool + any params you already have. " +
|
|
936
|
+
"Returns a form spec with fields the user needs to fill in. " +
|
|
937
|
+
"Secrets bypass the LLM entirely. On form submission, the server auto-calls the target tool.",
|
|
938
|
+
visibility: "public",
|
|
939
|
+
inputSchema: {
|
|
940
|
+
type: "object",
|
|
941
|
+
properties: {
|
|
942
|
+
agent: {
|
|
943
|
+
type: "string",
|
|
944
|
+
description: "Target agent path (e.g. '@databases')",
|
|
945
|
+
},
|
|
946
|
+
tool: {
|
|
947
|
+
type: "string",
|
|
948
|
+
description: "Target tool name (e.g. 'add_connection')",
|
|
949
|
+
},
|
|
950
|
+
params: {
|
|
951
|
+
type: "object",
|
|
952
|
+
description: "Partial params already collected",
|
|
953
|
+
},
|
|
954
|
+
registry: {
|
|
955
|
+
type: "string",
|
|
956
|
+
description: "Remote registry URL. Omit for local.",
|
|
957
|
+
},
|
|
958
|
+
source: {
|
|
959
|
+
type: "object",
|
|
960
|
+
description: "Where to render the form. Determines form delivery method.",
|
|
961
|
+
properties: {
|
|
962
|
+
type: {
|
|
963
|
+
type: "string",
|
|
964
|
+
enum: ["slack", "web", "cli"],
|
|
965
|
+
description: "Platform type",
|
|
966
|
+
},
|
|
967
|
+
workspace: {
|
|
968
|
+
type: "string",
|
|
969
|
+
description: "Slack workspace ID (for slack)",
|
|
970
|
+
},
|
|
971
|
+
channel: {
|
|
972
|
+
type: "string",
|
|
973
|
+
description: "Slack channel ID (for slack)",
|
|
974
|
+
},
|
|
975
|
+
threadTs: {
|
|
976
|
+
type: "string",
|
|
977
|
+
description: "Slack thread timestamp (for slack)",
|
|
978
|
+
},
|
|
979
|
+
redirectUrl: {
|
|
980
|
+
type: "string",
|
|
981
|
+
description: "URL to redirect after submission (for web)",
|
|
982
|
+
},
|
|
983
|
+
},
|
|
984
|
+
required: ["type"],
|
|
985
|
+
},
|
|
986
|
+
},
|
|
987
|
+
required: ["agent", "tool"],
|
|
988
|
+
},
|
|
989
|
+
execute: async (input, ctx) => {
|
|
990
|
+
// Fetch tool schema from registry
|
|
991
|
+
let toolSchema = null;
|
|
992
|
+
const registryUrl = input.registry;
|
|
993
|
+
if (!registryUrl) {
|
|
994
|
+
return { error: "Registry URL required for now. Pass registry param." };
|
|
995
|
+
}
|
|
996
|
+
const res = await fetch(`${registryUrl}/mcp`, {
|
|
997
|
+
method: "POST",
|
|
998
|
+
headers: { "Content-Type": "application/json" },
|
|
999
|
+
body: JSON.stringify({
|
|
1000
|
+
jsonrpc: "2.0",
|
|
1001
|
+
id: 1,
|
|
1002
|
+
method: "tools/call",
|
|
1003
|
+
params: {
|
|
1004
|
+
name: "call_agent",
|
|
1005
|
+
arguments: {
|
|
1006
|
+
request: { action: "describe_tools", path: input.agent },
|
|
1007
|
+
},
|
|
1008
|
+
},
|
|
1009
|
+
}),
|
|
1010
|
+
});
|
|
1011
|
+
const data = (await res.json());
|
|
1012
|
+
const parsed = JSON.parse(data?.result?.content?.[0]?.text ?? "{}");
|
|
1013
|
+
const tools = parsed?.tools ?? parsed?.result?.tools ?? [];
|
|
1014
|
+
toolSchema = tools.find((t) => t.name === input.tool) ?? null;
|
|
1015
|
+
if (!toolSchema?.inputSchema) {
|
|
1016
|
+
return { error: `Tool '${input.tool}' not found on '${input.agent}'` };
|
|
1017
|
+
}
|
|
1018
|
+
const schema = toolSchema.inputSchema;
|
|
1019
|
+
const properties = schema.properties ?? {};
|
|
1020
|
+
const requiredFields = new Set(schema.required ?? []);
|
|
1021
|
+
const providedParams = input.params ?? {};
|
|
1022
|
+
// Compute fields: secret fields always, required fields if not provided
|
|
1023
|
+
const fields = [];
|
|
1024
|
+
for (const [name, def] of Object.entries(properties)) {
|
|
1025
|
+
const isSecret = def.secret === true;
|
|
1026
|
+
const isRequired = requiredFields.has(name);
|
|
1027
|
+
const isProvided = name in providedParams;
|
|
1028
|
+
if (isSecret || (isRequired && !isProvided)) {
|
|
1029
|
+
fields.push({
|
|
1030
|
+
name,
|
|
1031
|
+
type: def.type ?? "string",
|
|
1032
|
+
description: def.description ?? name,
|
|
1033
|
+
secret: isSecret,
|
|
1034
|
+
required: isRequired,
|
|
1035
|
+
});
|
|
1036
|
+
}
|
|
1037
|
+
}
|
|
1038
|
+
if (fields.length === 0) {
|
|
1039
|
+
return {
|
|
1040
|
+
message: "All fields provided. Call the tool directly.",
|
|
1041
|
+
canCallDirectly: true,
|
|
1042
|
+
};
|
|
1043
|
+
}
|
|
1044
|
+
// Register pending collection
|
|
1045
|
+
const token = (0, secret_collection_js_1.generateCollectionToken)();
|
|
1046
|
+
secret_collection_js_1.pendingCollections.set(token, {
|
|
1047
|
+
params: providedParams,
|
|
1048
|
+
agent: input.agent,
|
|
1049
|
+
tool: input.tool,
|
|
1050
|
+
auth: {
|
|
1051
|
+
callerId: ctx.callerId,
|
|
1052
|
+
callerType: ctx.callerType,
|
|
1053
|
+
scopes: [],
|
|
1054
|
+
isRoot: false,
|
|
1055
|
+
},
|
|
1056
|
+
fields: fields.map((f) => ({
|
|
1057
|
+
name: f.name,
|
|
1058
|
+
description: f.description,
|
|
1059
|
+
secret: f.secret,
|
|
1060
|
+
required: f.required,
|
|
1061
|
+
})),
|
|
1062
|
+
createdAt: Date.now(),
|
|
1063
|
+
});
|
|
1064
|
+
// Build callback URL from callbackBaseUrl
|
|
1065
|
+
const baseUrl = callbackBaseUrl
|
|
1066
|
+
?.replace(/\/oauth\/callback$/, "")
|
|
1067
|
+
.replace(/\/integrations\/callback$/, "") ?? "";
|
|
1068
|
+
return {
|
|
1069
|
+
url: `${baseUrl}/secrets/form/${token}`,
|
|
1070
|
+
message: `Open this link to securely enter credentials for ${input.tool} on ${input.agent}. They will be encrypted and never pass through the AI.`,
|
|
1071
|
+
expiresIn: 600,
|
|
1072
|
+
};
|
|
1073
|
+
},
|
|
1074
|
+
});
|
|
1075
|
+
// ---- Facade: discover_integrations (aggregates from all agents) ----
|
|
1076
|
+
const discoverFacadeTool = (0, define_js_1.defineTool)({
|
|
1077
|
+
name: "discover_integrations",
|
|
1078
|
+
description: "Discover all available integrations across all registered agents.",
|
|
1079
|
+
visibility: "public",
|
|
1080
|
+
inputSchema: { type: "object", properties: {} },
|
|
1081
|
+
execute: async () => {
|
|
1082
|
+
const agents = options.registry?.list?.() ?? [];
|
|
1083
|
+
const results = [];
|
|
1084
|
+
if (options.registry) {
|
|
1085
|
+
for (const agent of agents) {
|
|
1086
|
+
const hasDiscoverTool = agent.tools?.some((t) => t.name === "discover_integrations");
|
|
1087
|
+
if (hasDiscoverTool) {
|
|
1088
|
+
try {
|
|
1089
|
+
const res = await options.registry.call({
|
|
1090
|
+
action: "execute_tool",
|
|
1091
|
+
path: agent.path,
|
|
1092
|
+
tool: "discover_integrations",
|
|
1093
|
+
params: {},
|
|
1094
|
+
callerId: "@integrations",
|
|
1095
|
+
callerType: "system",
|
|
1096
|
+
});
|
|
1097
|
+
if (res?.result && Array.isArray(res.result)) {
|
|
1098
|
+
results.push(...res.result);
|
|
1099
|
+
}
|
|
1100
|
+
}
|
|
1101
|
+
catch { }
|
|
1102
|
+
}
|
|
1103
|
+
}
|
|
1104
|
+
}
|
|
1105
|
+
return results;
|
|
1106
|
+
},
|
|
1107
|
+
});
|
|
1108
|
+
// ---- Facade: list_integrations (aggregates from all agents) ----
|
|
1109
|
+
const listFacadeTool = (0, define_js_1.defineTool)({
|
|
1110
|
+
name: "list_integrations",
|
|
1111
|
+
description: "List all installed integrations across all agents.",
|
|
1112
|
+
visibility: "public",
|
|
1113
|
+
inputSchema: {
|
|
1114
|
+
type: "object",
|
|
1115
|
+
properties: {
|
|
1116
|
+
agent_path: { type: "string", description: "Filter by agent path" },
|
|
1117
|
+
},
|
|
1118
|
+
},
|
|
1119
|
+
execute: async (input) => {
|
|
1120
|
+
const agents = options.registry?.list?.() ?? [];
|
|
1121
|
+
const results = [];
|
|
1122
|
+
if (options.registry) {
|
|
1123
|
+
const targetAgents = input.agent_path
|
|
1124
|
+
? agents.filter((a) => a.path === input.agent_path)
|
|
1125
|
+
: agents;
|
|
1126
|
+
for (const agent of targetAgents) {
|
|
1127
|
+
const hasListTool = agent.tools?.some((t) => t.name === "list_integrations");
|
|
1128
|
+
if (hasListTool) {
|
|
1129
|
+
try {
|
|
1130
|
+
const res = await options.registry.call({
|
|
1131
|
+
action: "execute_tool",
|
|
1132
|
+
path: agent.path,
|
|
1133
|
+
tool: "list_integrations",
|
|
1134
|
+
params: {},
|
|
1135
|
+
callerId: "@integrations",
|
|
1136
|
+
callerType: "system",
|
|
1137
|
+
});
|
|
1138
|
+
if (res?.result && Array.isArray(res.result)) {
|
|
1139
|
+
results.push(...res.result);
|
|
1140
|
+
}
|
|
1141
|
+
}
|
|
1142
|
+
catch { }
|
|
1143
|
+
}
|
|
1144
|
+
}
|
|
1145
|
+
}
|
|
1146
|
+
return results;
|
|
1147
|
+
},
|
|
1148
|
+
});
|
|
1149
|
+
return (0, define_js_1.defineAgent)({
|
|
1150
|
+
path: "@integrations",
|
|
1151
|
+
entrypoint: "You are the integrations agent. You manage third-party API integrations " +
|
|
1152
|
+
"including OAuth connections, provider configs, and API calling.",
|
|
1153
|
+
config: {
|
|
1154
|
+
name: "Integrations",
|
|
1155
|
+
description: "Third-party API integration management with OAuth2 support",
|
|
1156
|
+
supportedActions: ["execute_tool", "describe_tools", "load"],
|
|
1157
|
+
},
|
|
1158
|
+
visibility: "public",
|
|
1159
|
+
tools: [
|
|
1160
|
+
setupTool,
|
|
1161
|
+
discoverTool,
|
|
1162
|
+
listTool,
|
|
1163
|
+
getTool,
|
|
1164
|
+
connectTool,
|
|
1165
|
+
callTool,
|
|
1166
|
+
callbackTool,
|
|
1167
|
+
collectSecretsTool,
|
|
1168
|
+
discoverFacadeTool,
|
|
1169
|
+
listFacadeTool,
|
|
1170
|
+
],
|
|
1171
|
+
});
|
|
1172
|
+
}
|
|
1173
|
+
//# sourceMappingURL=integrations.js.map
|