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