@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.
Files changed (69) hide show
  1. package/dist/callback/index.d.ts +13 -24
  2. package/dist/callback/index.d.ts.map +1 -1
  3. package/dist/callback/index.js.map +1 -1
  4. package/dist/cjs/agent-definitions/auth.js +678 -0
  5. package/dist/cjs/agent-definitions/auth.js.map +1 -0
  6. package/dist/cjs/agent-definitions/integrations.js +1173 -0
  7. package/dist/cjs/agent-definitions/integrations.js.map +1 -0
  8. package/dist/cjs/agent-definitions/remote-registry.js +469 -0
  9. package/dist/cjs/agent-definitions/remote-registry.js.map +1 -0
  10. package/dist/cjs/agent-definitions/secrets.js +193 -0
  11. package/dist/cjs/agent-definitions/secrets.js.map +1 -0
  12. package/dist/cjs/agent-definitions/users.js +440 -0
  13. package/dist/cjs/agent-definitions/users.js.map +1 -0
  14. package/dist/cjs/build.js +162 -0
  15. package/dist/cjs/build.js.map +1 -0
  16. package/dist/cjs/callback/index.js +74 -0
  17. package/dist/cjs/callback/index.js.map +1 -0
  18. package/dist/cjs/client.js +193 -0
  19. package/dist/cjs/client.js.map +1 -0
  20. package/dist/cjs/codegen.js +1027 -0
  21. package/dist/cjs/codegen.js.map +1 -0
  22. package/dist/cjs/crypto.js +44 -0
  23. package/dist/cjs/crypto.js.map +1 -0
  24. package/dist/cjs/define-config.js +81 -0
  25. package/dist/cjs/define-config.js.map +1 -0
  26. package/dist/cjs/define.js +186 -0
  27. package/dist/cjs/define.js.map +1 -0
  28. package/dist/cjs/events.js +60 -0
  29. package/dist/cjs/events.js.map +1 -0
  30. package/dist/cjs/index.js +195 -0
  31. package/dist/cjs/index.js.map +1 -0
  32. package/dist/cjs/integration-interface.js +105 -0
  33. package/dist/cjs/integration-interface.js.map +1 -0
  34. package/dist/cjs/integrations-store.js +53 -0
  35. package/dist/cjs/integrations-store.js.map +1 -0
  36. package/dist/cjs/introspect.js +136 -0
  37. package/dist/cjs/introspect.js.map +1 -0
  38. package/dist/cjs/jsonc.js +74 -0
  39. package/dist/cjs/jsonc.js.map +1 -0
  40. package/dist/cjs/jwt.js +207 -0
  41. package/dist/cjs/jwt.js.map +1 -0
  42. package/dist/cjs/key-manager.js +161 -0
  43. package/dist/cjs/key-manager.js.map +1 -0
  44. package/dist/cjs/oidc-signin.js +141 -0
  45. package/dist/cjs/oidc-signin.js.map +1 -0
  46. package/dist/cjs/pack.js +256 -0
  47. package/dist/cjs/pack.js.map +1 -0
  48. package/dist/cjs/package.json +1 -0
  49. package/dist/cjs/registry-consumer.js +233 -0
  50. package/dist/cjs/registry-consumer.js.map +1 -0
  51. package/dist/cjs/registry.js +512 -0
  52. package/dist/cjs/registry.js.map +1 -0
  53. package/dist/cjs/secret-collection.js +42 -0
  54. package/dist/cjs/secret-collection.js.map +1 -0
  55. package/dist/cjs/serialized.js +45 -0
  56. package/dist/cjs/serialized.js.map +1 -0
  57. package/dist/cjs/server.js +974 -0
  58. package/dist/cjs/server.js.map +1 -0
  59. package/dist/cjs/test-utils/mock-oidc-server.js +99 -0
  60. package/dist/cjs/test-utils/mock-oidc-server.js.map +1 -0
  61. package/dist/cjs/types.js +8 -0
  62. package/dist/cjs/types.js.map +1 -0
  63. package/dist/cjs/validate.js +84 -0
  64. package/dist/cjs/validate.js.map +1 -0
  65. package/dist/registry.js +6 -6
  66. package/dist/registry.js.map +1 -1
  67. package/package.json +13 -5
  68. package/src/callback/index.ts +13 -24
  69. 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