@customclaw/composio 0.0.7 → 0.0.9
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/README.md +42 -3
- package/dist/cli.d.ts +2 -2
- package/dist/cli.js +268 -37
- package/dist/client.d.ts +6 -0
- package/dist/client.js +253 -53
- package/dist/config.d.ts +49 -0
- package/dist/config.js +44 -20
- package/dist/index.d.ts +20 -0
- package/dist/index.js +21 -14
- package/dist/types.d.ts +5 -0
- package/dist/utils.d.ts +12 -0
- package/dist/utils.js +76 -0
- package/openclaw.plugin.json +39 -0
- package/package.json +3 -2
- package/dist/client.test.d.ts +0 -1
- package/dist/client.test.js +0 -506
package/dist/client.js
CHANGED
|
@@ -1,4 +1,40 @@
|
|
|
1
1
|
import { Composio } from "@composio/core";
|
|
2
|
+
import { normalizeSessionTags, normalizeToolkitList, normalizeToolkitSlug, normalizeToolSlug, normalizeToolSlugList, } from "./utils.js";
|
|
3
|
+
// Heuristic only: token matching may block some benign tools.
|
|
4
|
+
// Use `allowedToolSlugs` to explicitly override specific slugs.
|
|
5
|
+
const DESTRUCTIVE_TOOL_VERBS = new Set([
|
|
6
|
+
"CREATE",
|
|
7
|
+
"DELETE",
|
|
8
|
+
"DESTROY",
|
|
9
|
+
"DISABLE",
|
|
10
|
+
"DISCONNECT",
|
|
11
|
+
"ERASE",
|
|
12
|
+
"MODIFY",
|
|
13
|
+
"PATCH",
|
|
14
|
+
"POST",
|
|
15
|
+
"PUT",
|
|
16
|
+
"REMOVE",
|
|
17
|
+
"RENAME",
|
|
18
|
+
"REPLACE",
|
|
19
|
+
"REVOKE",
|
|
20
|
+
"SEND",
|
|
21
|
+
"SET",
|
|
22
|
+
"TRUNCATE",
|
|
23
|
+
"UNSUBSCRIBE",
|
|
24
|
+
"UPDATE",
|
|
25
|
+
"UPSERT",
|
|
26
|
+
"WRITE",
|
|
27
|
+
]);
|
|
28
|
+
function isConnectedAccountStatusFilter(value) {
|
|
29
|
+
return [
|
|
30
|
+
"INITIALIZING",
|
|
31
|
+
"INITIATED",
|
|
32
|
+
"ACTIVE",
|
|
33
|
+
"FAILED",
|
|
34
|
+
"EXPIRED",
|
|
35
|
+
"INACTIVE",
|
|
36
|
+
].includes(value);
|
|
37
|
+
}
|
|
2
38
|
/**
|
|
3
39
|
* Composio client wrapper using Tool Router pattern
|
|
4
40
|
*/
|
|
@@ -10,7 +46,15 @@ export class ComposioClient {
|
|
|
10
46
|
if (!config.apiKey) {
|
|
11
47
|
throw new Error("Composio API key required. Set COMPOSIO_API_KEY env var or plugins.composio.apiKey in config.");
|
|
12
48
|
}
|
|
13
|
-
this.config =
|
|
49
|
+
this.config = {
|
|
50
|
+
...config,
|
|
51
|
+
allowedToolkits: normalizeToolkitList(config.allowedToolkits),
|
|
52
|
+
blockedToolkits: normalizeToolkitList(config.blockedToolkits),
|
|
53
|
+
sessionTags: normalizeSessionTags(config.sessionTags),
|
|
54
|
+
allowedToolSlugs: normalizeToolSlugList(config.allowedToolSlugs),
|
|
55
|
+
blockedToolSlugs: normalizeToolSlugList(config.blockedToolSlugs),
|
|
56
|
+
readOnlyMode: Boolean(config.readOnlyMode),
|
|
57
|
+
};
|
|
14
58
|
this.client = new Composio({ apiKey: config.apiKey });
|
|
15
59
|
}
|
|
16
60
|
/**
|
|
@@ -27,20 +71,102 @@ export class ComposioClient {
|
|
|
27
71
|
return `uid:${userId}`;
|
|
28
72
|
}
|
|
29
73
|
const normalized = Object.entries(connectedAccounts)
|
|
30
|
-
.map(([toolkit, accountId]) => `${toolkit
|
|
74
|
+
.map(([toolkit, accountId]) => `${normalizeToolkitSlug(toolkit)}=${accountId}`)
|
|
31
75
|
.sort()
|
|
32
76
|
.join(",");
|
|
33
77
|
return `uid:${userId}::ca:${normalized}`;
|
|
34
78
|
}
|
|
79
|
+
normalizeConnectedAccountsOverride(connectedAccounts) {
|
|
80
|
+
if (!connectedAccounts)
|
|
81
|
+
return undefined;
|
|
82
|
+
const normalized = Object.entries(connectedAccounts)
|
|
83
|
+
.map(([toolkit, accountId]) => [normalizeToolkitSlug(toolkit), String(accountId || "").trim()])
|
|
84
|
+
.filter(([toolkit, accountId]) => toolkit.length > 0 && accountId.length > 0);
|
|
85
|
+
if (normalized.length === 0)
|
|
86
|
+
return undefined;
|
|
87
|
+
return Object.fromEntries(normalized);
|
|
88
|
+
}
|
|
89
|
+
buildToolRouterBlockedToolsConfig() {
|
|
90
|
+
const blocked = this.config.blockedToolSlugs;
|
|
91
|
+
if (!blocked || blocked.length === 0)
|
|
92
|
+
return undefined;
|
|
93
|
+
const byToolkit = new Map();
|
|
94
|
+
for (const slug of blocked) {
|
|
95
|
+
const normalizedSlug = normalizeToolSlug(slug);
|
|
96
|
+
const toolkit = normalizeToolkitSlug(normalizedSlug.split("_")[0] || "");
|
|
97
|
+
if (!toolkit)
|
|
98
|
+
continue;
|
|
99
|
+
if (!this.isToolkitAllowed(toolkit))
|
|
100
|
+
continue;
|
|
101
|
+
if (!byToolkit.has(toolkit))
|
|
102
|
+
byToolkit.set(toolkit, new Set());
|
|
103
|
+
byToolkit.get(toolkit).add(normalizedSlug);
|
|
104
|
+
}
|
|
105
|
+
if (byToolkit.size === 0)
|
|
106
|
+
return undefined;
|
|
107
|
+
const tools = {};
|
|
108
|
+
for (const [toolkit, slugs] of byToolkit.entries()) {
|
|
109
|
+
tools[toolkit] = { disable: Array.from(slugs) };
|
|
110
|
+
}
|
|
111
|
+
return tools;
|
|
112
|
+
}
|
|
113
|
+
buildSessionConfig(connectedAccounts) {
|
|
114
|
+
const sessionConfig = {};
|
|
115
|
+
const normalizedConnectedAccounts = this.normalizeConnectedAccountsOverride(connectedAccounts);
|
|
116
|
+
if (normalizedConnectedAccounts) {
|
|
117
|
+
sessionConfig.connectedAccounts = normalizedConnectedAccounts;
|
|
118
|
+
}
|
|
119
|
+
if (this.config.allowedToolkits && this.config.allowedToolkits.length > 0) {
|
|
120
|
+
sessionConfig.toolkits = { enable: this.config.allowedToolkits };
|
|
121
|
+
}
|
|
122
|
+
else if (this.config.blockedToolkits && this.config.blockedToolkits.length > 0) {
|
|
123
|
+
sessionConfig.toolkits = { disable: this.config.blockedToolkits };
|
|
124
|
+
}
|
|
125
|
+
const tags = new Set(this.config.sessionTags || []);
|
|
126
|
+
if (this.config.readOnlyMode)
|
|
127
|
+
tags.add("readOnlyHint");
|
|
128
|
+
if (tags.size > 0) {
|
|
129
|
+
sessionConfig.tags = Array.from(tags);
|
|
130
|
+
}
|
|
131
|
+
const blockedToolsConfig = this.buildToolRouterBlockedToolsConfig();
|
|
132
|
+
if (blockedToolsConfig) {
|
|
133
|
+
sessionConfig.tools = blockedToolsConfig;
|
|
134
|
+
}
|
|
135
|
+
return Object.keys(sessionConfig).length > 0 ? sessionConfig : undefined;
|
|
136
|
+
}
|
|
35
137
|
async getSession(userId, connectedAccounts) {
|
|
36
|
-
const
|
|
138
|
+
const normalizedConnectedAccounts = this.normalizeConnectedAccountsOverride(connectedAccounts);
|
|
139
|
+
const key = this.makeSessionCacheKey(userId, normalizedConnectedAccounts);
|
|
37
140
|
if (this.sessionCache.has(key)) {
|
|
38
141
|
return this.sessionCache.get(key);
|
|
39
142
|
}
|
|
40
|
-
const
|
|
143
|
+
const sessionConfig = this.buildSessionConfig(normalizedConnectedAccounts);
|
|
144
|
+
let session;
|
|
145
|
+
try {
|
|
146
|
+
session = await this.client.toolRouter.create(userId, sessionConfig);
|
|
147
|
+
}
|
|
148
|
+
catch (err) {
|
|
149
|
+
if (!this.shouldRetrySessionWithoutToolkitFilters(err, sessionConfig)) {
|
|
150
|
+
throw err;
|
|
151
|
+
}
|
|
152
|
+
const { toolkits: _removedToolkits, ...retryWithoutToolkits } = sessionConfig ?? {};
|
|
153
|
+
const retryConfig = Object.keys(retryWithoutToolkits).length > 0
|
|
154
|
+
? retryWithoutToolkits
|
|
155
|
+
: undefined;
|
|
156
|
+
session = await this.client.toolRouter.create(userId, retryConfig);
|
|
157
|
+
}
|
|
41
158
|
this.sessionCache.set(key, session);
|
|
42
159
|
return session;
|
|
43
160
|
}
|
|
161
|
+
shouldRetrySessionWithoutToolkitFilters(err, sessionConfig) {
|
|
162
|
+
const enabledToolkits = sessionConfig?.toolkits?.enable;
|
|
163
|
+
if (!Array.isArray(enabledToolkits) || enabledToolkits.length === 0) {
|
|
164
|
+
return false;
|
|
165
|
+
}
|
|
166
|
+
const message = String(err instanceof Error ? err.message : err || "").toLowerCase();
|
|
167
|
+
return (message.includes("require auth configs but none exist") &&
|
|
168
|
+
message.includes("please specify them in auth_configs"));
|
|
169
|
+
}
|
|
44
170
|
clearUserSessionCache(userId) {
|
|
45
171
|
const prefix = `uid:${userId}`;
|
|
46
172
|
for (const key of this.sessionCache.keys()) {
|
|
@@ -53,23 +179,56 @@ export class ComposioClient {
|
|
|
53
179
|
* Check if a toolkit is allowed based on config
|
|
54
180
|
*/
|
|
55
181
|
isToolkitAllowed(toolkit) {
|
|
182
|
+
const normalizedToolkit = normalizeToolkitSlug(toolkit);
|
|
183
|
+
if (!normalizedToolkit)
|
|
184
|
+
return false;
|
|
56
185
|
const { allowedToolkits, blockedToolkits } = this.config;
|
|
57
|
-
if (blockedToolkits?.includes(
|
|
186
|
+
if (blockedToolkits?.includes(normalizedToolkit)) {
|
|
58
187
|
return false;
|
|
59
188
|
}
|
|
60
189
|
if (allowedToolkits && allowedToolkits.length > 0) {
|
|
61
|
-
return allowedToolkits.includes(
|
|
190
|
+
return allowedToolkits.includes(normalizedToolkit);
|
|
62
191
|
}
|
|
63
192
|
return true;
|
|
64
193
|
}
|
|
194
|
+
isLikelyDestructiveToolSlug(toolSlug) {
|
|
195
|
+
const tokens = normalizeToolSlug(toolSlug)
|
|
196
|
+
.split("_")
|
|
197
|
+
.filter(Boolean);
|
|
198
|
+
return tokens.some((token) => DESTRUCTIVE_TOOL_VERBS.has(token));
|
|
199
|
+
}
|
|
200
|
+
getToolSlugRestrictionError(toolSlug) {
|
|
201
|
+
const normalizedToolSlug = normalizeToolSlug(toolSlug);
|
|
202
|
+
if (!normalizedToolSlug)
|
|
203
|
+
return "tool_slug is required";
|
|
204
|
+
const isExplicitlyAllowed = this.config.allowedToolSlugs?.includes(normalizedToolSlug) ?? false;
|
|
205
|
+
if (this.config.allowedToolSlugs && this.config.allowedToolSlugs.length > 0) {
|
|
206
|
+
if (!isExplicitlyAllowed) {
|
|
207
|
+
return `Tool '${normalizedToolSlug}' is not in allowedToolSlugs`;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
if (this.config.blockedToolSlugs?.includes(normalizedToolSlug)) {
|
|
211
|
+
return `Tool '${normalizedToolSlug}' is blocked by plugin configuration`;
|
|
212
|
+
}
|
|
213
|
+
if (this.config.readOnlyMode && !isExplicitlyAllowed && this.isLikelyDestructiveToolSlug(normalizedToolSlug)) {
|
|
214
|
+
return (`Tool '${normalizedToolSlug}' was blocked by readOnlyMode because it appears to modify data. ` +
|
|
215
|
+
"Disable readOnlyMode or add this slug to allowedToolSlugs if execution is intentional.");
|
|
216
|
+
}
|
|
217
|
+
return undefined;
|
|
218
|
+
}
|
|
65
219
|
/**
|
|
66
220
|
* Execute a Tool Router meta-tool
|
|
67
221
|
*/
|
|
68
|
-
async executeMetaTool(toolName, args) {
|
|
69
|
-
const response = await this.client.
|
|
222
|
+
async executeMetaTool(sessionId, toolName, args) {
|
|
223
|
+
const response = await this.client.tools.executeMetaTool(toolName, {
|
|
224
|
+
sessionId,
|
|
70
225
|
arguments: args,
|
|
71
226
|
});
|
|
72
|
-
return
|
|
227
|
+
return {
|
|
228
|
+
successful: Boolean(response.successful),
|
|
229
|
+
data: response.data,
|
|
230
|
+
error: response.error ?? undefined,
|
|
231
|
+
};
|
|
73
232
|
}
|
|
74
233
|
/**
|
|
75
234
|
* Search for tools matching a query using COMPOSIO_SEARCH_TOOLS
|
|
@@ -77,10 +236,10 @@ export class ComposioClient {
|
|
|
77
236
|
async searchTools(query, options) {
|
|
78
237
|
const userId = this.getUserId(options?.userId);
|
|
79
238
|
const session = await this.getSession(userId);
|
|
239
|
+
const requestedToolkits = normalizeToolkitList(options?.toolkits);
|
|
80
240
|
try {
|
|
81
|
-
const response = await this.executeMetaTool("COMPOSIO_SEARCH_TOOLS", {
|
|
241
|
+
const response = await this.executeMetaTool(session.sessionId, "COMPOSIO_SEARCH_TOOLS", {
|
|
82
242
|
queries: [{ use_case: query }],
|
|
83
|
-
session: { id: session.sessionId },
|
|
84
243
|
});
|
|
85
244
|
if (!response.successful || !response.data) {
|
|
86
245
|
throw new Error(response.error || "Search failed");
|
|
@@ -100,11 +259,11 @@ export class ComposioClient {
|
|
|
100
259
|
continue;
|
|
101
260
|
seenSlugs.add(slug);
|
|
102
261
|
const schema = toolSchemas[slug];
|
|
103
|
-
const toolkit = schema?.toolkit || slug.split("_")[0] || "";
|
|
262
|
+
const toolkit = normalizeToolkitSlug(schema?.toolkit || slug.split("_")[0] || "");
|
|
104
263
|
if (!this.isToolkitAllowed(toolkit))
|
|
105
264
|
continue;
|
|
106
|
-
if (
|
|
107
|
-
if (!
|
|
265
|
+
if (requestedToolkits && requestedToolkits.length > 0) {
|
|
266
|
+
if (!requestedToolkits.includes(toolkit)) {
|
|
108
267
|
continue;
|
|
109
268
|
}
|
|
110
269
|
}
|
|
@@ -131,8 +290,13 @@ export class ComposioClient {
|
|
|
131
290
|
* Execute a single tool using COMPOSIO_MULTI_EXECUTE_TOOL
|
|
132
291
|
*/
|
|
133
292
|
async executeTool(toolSlug, args, userId, connectedAccountId) {
|
|
134
|
-
const
|
|
135
|
-
const
|
|
293
|
+
const requestedUid = this.getUserId(userId);
|
|
294
|
+
const normalizedToolSlug = normalizeToolSlug(toolSlug);
|
|
295
|
+
const toolRestrictionError = this.getToolSlugRestrictionError(normalizedToolSlug);
|
|
296
|
+
if (toolRestrictionError) {
|
|
297
|
+
return { success: false, error: toolRestrictionError };
|
|
298
|
+
}
|
|
299
|
+
const toolkit = normalizeToolkitSlug(normalizedToolSlug.split("_")[0] || "");
|
|
136
300
|
if (!this.isToolkitAllowed(toolkit)) {
|
|
137
301
|
return {
|
|
138
302
|
success: false,
|
|
@@ -141,25 +305,26 @@ export class ComposioClient {
|
|
|
141
305
|
}
|
|
142
306
|
const accountResolution = await this.resolveConnectedAccountForExecution({
|
|
143
307
|
toolkit,
|
|
144
|
-
userId:
|
|
308
|
+
userId: requestedUid,
|
|
145
309
|
connectedAccountId,
|
|
310
|
+
userIdWasExplicit: typeof userId === "string" && userId.trim().length > 0,
|
|
146
311
|
});
|
|
147
312
|
if ("error" in accountResolution) {
|
|
148
313
|
return { success: false, error: accountResolution.error };
|
|
149
314
|
}
|
|
150
|
-
const
|
|
315
|
+
const effectiveUid = accountResolution.userId || requestedUid;
|
|
316
|
+
const session = await this.getSession(effectiveUid, accountResolution.connectedAccountId
|
|
151
317
|
? { [toolkit]: accountResolution.connectedAccountId }
|
|
152
318
|
: undefined);
|
|
153
319
|
try {
|
|
154
|
-
const response = await this.executeMetaTool("COMPOSIO_MULTI_EXECUTE_TOOL", {
|
|
155
|
-
tools: [{ tool_slug:
|
|
156
|
-
session: { id: session.sessionId },
|
|
320
|
+
const response = await this.executeMetaTool(session.sessionId, "COMPOSIO_MULTI_EXECUTE_TOOL", {
|
|
321
|
+
tools: [{ tool_slug: normalizedToolSlug, arguments: args }],
|
|
157
322
|
sync_response_to_workbench: false,
|
|
158
323
|
});
|
|
159
324
|
if (!response.successful) {
|
|
160
325
|
const recovered = await this.tryExecutionRecovery({
|
|
161
|
-
uid,
|
|
162
|
-
toolSlug,
|
|
326
|
+
uid: effectiveUid,
|
|
327
|
+
toolSlug: normalizedToolSlug,
|
|
163
328
|
args,
|
|
164
329
|
connectedAccountId: accountResolution.connectedAccountId,
|
|
165
330
|
metaError: response.error,
|
|
@@ -178,8 +343,8 @@ export class ComposioClient {
|
|
|
178
343
|
const toolResponse = result.response;
|
|
179
344
|
if (!toolResponse.successful) {
|
|
180
345
|
const recovered = await this.tryExecutionRecovery({
|
|
181
|
-
uid,
|
|
182
|
-
toolSlug,
|
|
346
|
+
uid: effectiveUid,
|
|
347
|
+
toolSlug: normalizedToolSlug,
|
|
183
348
|
args,
|
|
184
349
|
connectedAccountId: accountResolution.connectedAccountId,
|
|
185
350
|
metaError: toolResponse.error ?? undefined,
|
|
@@ -315,13 +480,28 @@ export class ComposioClient {
|
|
|
315
480
|
return String(first?.error || "");
|
|
316
481
|
}
|
|
317
482
|
async resolveConnectedAccountForExecution(params) {
|
|
318
|
-
const
|
|
483
|
+
const toolkit = normalizeToolkitSlug(params.toolkit);
|
|
484
|
+
const { userId } = params;
|
|
319
485
|
const explicitId = params.connectedAccountId?.trim();
|
|
320
486
|
if (explicitId) {
|
|
321
487
|
try {
|
|
322
|
-
|
|
323
|
-
const
|
|
488
|
+
let rawAccount;
|
|
489
|
+
const retrieve = this.client?.client?.connectedAccounts?.retrieve;
|
|
490
|
+
if (typeof retrieve === "function") {
|
|
491
|
+
try {
|
|
492
|
+
rawAccount = await retrieve.call(this.client.client.connectedAccounts, explicitId);
|
|
493
|
+
}
|
|
494
|
+
catch {
|
|
495
|
+
// Best-effort: fall through to SDK get() which may omit user_id.
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
if (!rawAccount) {
|
|
499
|
+
rawAccount = await this.client.connectedAccounts.get(explicitId);
|
|
500
|
+
}
|
|
501
|
+
const account = rawAccount;
|
|
502
|
+
const accountToolkit = normalizeToolkitSlug(String(account?.toolkit?.slug || ""));
|
|
324
503
|
const accountStatus = String(account?.status || "").toUpperCase();
|
|
504
|
+
const accountUserId = String(account?.user_id || account?.userId || "").trim();
|
|
325
505
|
if (accountToolkit && accountToolkit !== toolkit) {
|
|
326
506
|
return {
|
|
327
507
|
error: `Connected account '${explicitId}' belongs to toolkit '${accountToolkit}', but tool '${toolkit}' was requested.`,
|
|
@@ -332,7 +512,13 @@ export class ComposioClient {
|
|
|
332
512
|
error: `Connected account '${explicitId}' is '${accountStatus}', not ACTIVE.`,
|
|
333
513
|
};
|
|
334
514
|
}
|
|
335
|
-
|
|
515
|
+
if (params.userIdWasExplicit && accountUserId && accountUserId !== userId) {
|
|
516
|
+
return {
|
|
517
|
+
error: `Connected account '${explicitId}' belongs to user_id '${accountUserId}', ` +
|
|
518
|
+
`but '${userId}' was requested. Use matching user_id or omit user_id when providing connected_account_id.`,
|
|
519
|
+
};
|
|
520
|
+
}
|
|
521
|
+
return { connectedAccountId: explicitId, userId: accountUserId || undefined };
|
|
336
522
|
}
|
|
337
523
|
catch (err) {
|
|
338
524
|
return {
|
|
@@ -361,14 +547,15 @@ export class ComposioClient {
|
|
|
361
547
|
const uid = this.getUserId(userId);
|
|
362
548
|
const session = await this.getSession(uid);
|
|
363
549
|
try {
|
|
364
|
-
|
|
365
|
-
|
|
550
|
+
const normalizedToolkits = normalizeToolkitList(toolkits);
|
|
551
|
+
if (normalizedToolkits && normalizedToolkits.length > 0) {
|
|
552
|
+
const requestedToolkits = normalizedToolkits.filter((t) => this.isToolkitAllowed(t));
|
|
366
553
|
if (requestedToolkits.length === 0)
|
|
367
554
|
return [];
|
|
368
555
|
const toolkitStateMap = await this.getToolkitStateMap(session, requestedToolkits);
|
|
369
556
|
const activeAccountToolkits = await this.getActiveConnectedAccountToolkits(uid, requestedToolkits);
|
|
370
557
|
return requestedToolkits.map((toolkit) => {
|
|
371
|
-
const key = toolkit
|
|
558
|
+
const key = normalizeToolkitSlug(toolkit);
|
|
372
559
|
return {
|
|
373
560
|
toolkit,
|
|
374
561
|
connected: (toolkitStateMap.get(key) ?? false) || activeAccountToolkits.has(key),
|
|
@@ -413,7 +600,7 @@ export class ComposioClient {
|
|
|
413
600
|
});
|
|
414
601
|
const items = response.items || [];
|
|
415
602
|
for (const tk of items) {
|
|
416
|
-
const key = tk.slug
|
|
603
|
+
const key = normalizeToolkitSlug(tk.slug);
|
|
417
604
|
const isActive = tk.connection?.isActive ?? false;
|
|
418
605
|
map.set(key, (map.get(key) ?? false) || isActive);
|
|
419
606
|
}
|
|
@@ -428,6 +615,7 @@ export class ComposioClient {
|
|
|
428
615
|
}
|
|
429
616
|
async getActiveConnectedAccountToolkits(userId, toolkits) {
|
|
430
617
|
const connected = new Set();
|
|
618
|
+
const normalizedToolkits = normalizeToolkitList(toolkits);
|
|
431
619
|
let cursor;
|
|
432
620
|
const seenCursors = new Set();
|
|
433
621
|
try {
|
|
@@ -435,7 +623,7 @@ export class ComposioClient {
|
|
|
435
623
|
const response = await this.client.connectedAccounts.list({
|
|
436
624
|
userIds: [userId],
|
|
437
625
|
statuses: ["ACTIVE"],
|
|
438
|
-
...(
|
|
626
|
+
...(normalizedToolkits && normalizedToolkits.length > 0 ? { toolkitSlugs: normalizedToolkits } : {}),
|
|
439
627
|
limit: 100,
|
|
440
628
|
...(cursor ? { cursor } : {}),
|
|
441
629
|
});
|
|
@@ -443,12 +631,12 @@ export class ComposioClient {
|
|
|
443
631
|
? response
|
|
444
632
|
: response?.items || []);
|
|
445
633
|
for (const item of items) {
|
|
446
|
-
const slug = item.toolkit?.slug;
|
|
634
|
+
const slug = normalizeToolkitSlug(item.toolkit?.slug || "");
|
|
447
635
|
if (!slug)
|
|
448
636
|
continue;
|
|
449
637
|
if (item.status && String(item.status).toUpperCase() !== "ACTIVE")
|
|
450
638
|
continue;
|
|
451
|
-
connected.add(slug
|
|
639
|
+
connected.add(slug);
|
|
452
640
|
}
|
|
453
641
|
cursor = Array.isArray(response)
|
|
454
642
|
? null
|
|
@@ -469,10 +657,9 @@ export class ComposioClient {
|
|
|
469
657
|
normalizeStatuses(statuses) {
|
|
470
658
|
if (!statuses || statuses.length === 0)
|
|
471
659
|
return undefined;
|
|
472
|
-
const allowed = new Set(["INITIALIZING", "INITIATED", "ACTIVE", "FAILED", "EXPIRED", "INACTIVE"]);
|
|
473
660
|
const normalized = statuses
|
|
474
661
|
.map(s => String(s || "").trim().toUpperCase())
|
|
475
|
-
.filter(
|
|
662
|
+
.filter(isConnectedAccountStatusFilter);
|
|
476
663
|
return normalized.length > 0 ? Array.from(new Set(normalized)) : undefined;
|
|
477
664
|
}
|
|
478
665
|
/**
|
|
@@ -480,9 +667,7 @@ export class ComposioClient {
|
|
|
480
667
|
* Uses raw API first to preserve user_id in responses, then falls back to SDK-normalized output.
|
|
481
668
|
*/
|
|
482
669
|
async listConnectedAccounts(options) {
|
|
483
|
-
const toolkits = options?.toolkits
|
|
484
|
-
?.map(t => String(t || "").trim())
|
|
485
|
-
.filter(t => t.length > 0 && this.isToolkitAllowed(t));
|
|
670
|
+
const toolkits = normalizeToolkitList(options?.toolkits)?.filter((t) => this.isToolkitAllowed(t));
|
|
486
671
|
const userIds = options?.userIds
|
|
487
672
|
?.map(u => String(u || "").trim())
|
|
488
673
|
.filter(Boolean);
|
|
@@ -508,10 +693,11 @@ export class ComposioClient {
|
|
|
508
693
|
* Find user IDs that have an active connected account for a toolkit.
|
|
509
694
|
*/
|
|
510
695
|
async findActiveUserIdsForToolkit(toolkit) {
|
|
511
|
-
|
|
696
|
+
const normalizedToolkit = normalizeToolkitSlug(toolkit);
|
|
697
|
+
if (!this.isToolkitAllowed(normalizedToolkit))
|
|
512
698
|
return [];
|
|
513
699
|
const accounts = await this.listConnectedAccounts({
|
|
514
|
-
toolkits: [
|
|
700
|
+
toolkits: [normalizedToolkit],
|
|
515
701
|
statuses: ["ACTIVE"],
|
|
516
702
|
});
|
|
517
703
|
const userIds = new Set();
|
|
@@ -537,7 +723,7 @@ export class ComposioClient {
|
|
|
537
723
|
? response
|
|
538
724
|
: response?.items || []);
|
|
539
725
|
for (const item of items) {
|
|
540
|
-
const toolkitSlug = (item.toolkit?.slug || "").toString()
|
|
726
|
+
const toolkitSlug = normalizeToolkitSlug((item.toolkit?.slug || "").toString());
|
|
541
727
|
if (!toolkitSlug)
|
|
542
728
|
continue;
|
|
543
729
|
if (!this.isToolkitAllowed(toolkitSlug))
|
|
@@ -582,7 +768,7 @@ export class ComposioClient {
|
|
|
582
768
|
? response
|
|
583
769
|
: response?.items || []);
|
|
584
770
|
for (const item of items) {
|
|
585
|
-
const toolkitSlug = (item.toolkit?.slug || "").toString()
|
|
771
|
+
const toolkitSlug = normalizeToolkitSlug((item.toolkit?.slug || "").toString());
|
|
586
772
|
if (!toolkitSlug)
|
|
587
773
|
continue;
|
|
588
774
|
if (!this.isToolkitAllowed(toolkitSlug))
|
|
@@ -615,12 +801,16 @@ export class ComposioClient {
|
|
|
615
801
|
*/
|
|
616
802
|
async createConnection(toolkit, userId) {
|
|
617
803
|
const uid = this.getUserId(userId);
|
|
618
|
-
|
|
619
|
-
|
|
804
|
+
const toolkitSlug = normalizeToolkitSlug(toolkit);
|
|
805
|
+
if (!toolkitSlug) {
|
|
806
|
+
return { error: "Toolkit is required" };
|
|
807
|
+
}
|
|
808
|
+
if (!this.isToolkitAllowed(toolkitSlug)) {
|
|
809
|
+
return { error: `Toolkit '${toolkitSlug}' is not allowed by plugin configuration` };
|
|
620
810
|
}
|
|
621
811
|
try {
|
|
622
812
|
const session = await this.getSession(uid);
|
|
623
|
-
const result = await session.authorize(
|
|
813
|
+
const result = await session.authorize(toolkitSlug);
|
|
624
814
|
return { authUrl: result.redirectUrl || result.url || "" };
|
|
625
815
|
}
|
|
626
816
|
catch (err) {
|
|
@@ -646,7 +836,7 @@ export class ComposioClient {
|
|
|
646
836
|
});
|
|
647
837
|
const allToolkits = response.items || [];
|
|
648
838
|
for (const tk of allToolkits) {
|
|
649
|
-
const slug = tk.slug
|
|
839
|
+
const slug = normalizeToolkitSlug(tk.slug);
|
|
650
840
|
if (!this.isToolkitAllowed(slug))
|
|
651
841
|
continue;
|
|
652
842
|
seen.add(slug);
|
|
@@ -674,24 +864,34 @@ export class ComposioClient {
|
|
|
674
864
|
*/
|
|
675
865
|
async disconnectToolkit(toolkit, userId) {
|
|
676
866
|
const uid = this.getUserId(userId);
|
|
867
|
+
const toolkitSlug = normalizeToolkitSlug(toolkit);
|
|
868
|
+
if (!toolkitSlug) {
|
|
869
|
+
return { success: false, error: "Toolkit is required" };
|
|
870
|
+
}
|
|
871
|
+
if (this.config.readOnlyMode) {
|
|
872
|
+
return {
|
|
873
|
+
success: false,
|
|
874
|
+
error: "Disconnect is blocked by readOnlyMode.",
|
|
875
|
+
};
|
|
876
|
+
}
|
|
677
877
|
try {
|
|
678
878
|
const activeAccounts = await this.listConnectedAccounts({
|
|
679
|
-
toolkits: [
|
|
879
|
+
toolkits: [toolkitSlug],
|
|
680
880
|
userIds: [uid],
|
|
681
881
|
statuses: ["ACTIVE"],
|
|
682
882
|
});
|
|
683
883
|
if (activeAccounts.length === 0) {
|
|
684
|
-
return { success: false, error: `No connection found for toolkit '${
|
|
884
|
+
return { success: false, error: `No connection found for toolkit '${toolkitSlug}'` };
|
|
685
885
|
}
|
|
686
886
|
if (activeAccounts.length > 1) {
|
|
687
887
|
const ids = activeAccounts.map(a => a.id).join(", ");
|
|
688
888
|
return {
|
|
689
889
|
success: false,
|
|
690
|
-
error: `Multiple ACTIVE '${
|
|
890
|
+
error: `Multiple ACTIVE '${toolkitSlug}' accounts found for user_id '${uid}': ${ids}. ` +
|
|
691
891
|
"Use the dashboard to disconnect a specific account.",
|
|
692
892
|
};
|
|
693
893
|
}
|
|
694
|
-
await this.client.connectedAccounts.delete(
|
|
894
|
+
await this.client.connectedAccounts.delete(activeAccounts[0].id);
|
|
695
895
|
// Clear session cache to refresh connection status
|
|
696
896
|
this.clearUserSessionCache(uid);
|
|
697
897
|
return { success: true };
|
package/dist/config.d.ts
CHANGED
|
@@ -9,6 +9,15 @@ export declare const ComposioConfigSchema: z.ZodObject<{
|
|
|
9
9
|
defaultUserId: z.ZodOptional<z.ZodString>;
|
|
10
10
|
allowedToolkits: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
11
11
|
blockedToolkits: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
12
|
+
readOnlyMode: z.ZodDefault<z.ZodBoolean>;
|
|
13
|
+
sessionTags: z.ZodOptional<z.ZodArray<z.ZodEnum<{
|
|
14
|
+
readOnlyHint: "readOnlyHint";
|
|
15
|
+
destructiveHint: "destructiveHint";
|
|
16
|
+
idempotentHint: "idempotentHint";
|
|
17
|
+
openWorldHint: "openWorldHint";
|
|
18
|
+
}>>>;
|
|
19
|
+
allowedToolSlugs: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
20
|
+
blockedToolSlugs: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
12
21
|
}, z.core.$strip>;
|
|
13
22
|
/**
|
|
14
23
|
* Parse and validate plugin config with environment fallbacks
|
|
@@ -41,6 +50,26 @@ export declare const composioConfigUiHints: {
|
|
|
41
50
|
help: string;
|
|
42
51
|
advanced: boolean;
|
|
43
52
|
};
|
|
53
|
+
readOnlyMode: {
|
|
54
|
+
label: string;
|
|
55
|
+
help: string;
|
|
56
|
+
advanced: boolean;
|
|
57
|
+
};
|
|
58
|
+
sessionTags: {
|
|
59
|
+
label: string;
|
|
60
|
+
help: string;
|
|
61
|
+
advanced: boolean;
|
|
62
|
+
};
|
|
63
|
+
allowedToolSlugs: {
|
|
64
|
+
label: string;
|
|
65
|
+
help: string;
|
|
66
|
+
advanced: boolean;
|
|
67
|
+
};
|
|
68
|
+
blockedToolSlugs: {
|
|
69
|
+
label: string;
|
|
70
|
+
help: string;
|
|
71
|
+
advanced: boolean;
|
|
72
|
+
};
|
|
44
73
|
};
|
|
45
74
|
/**
|
|
46
75
|
* Plugin config schema object for openclaw
|
|
@@ -71,5 +100,25 @@ export declare const composioPluginConfigSchema: {
|
|
|
71
100
|
help: string;
|
|
72
101
|
advanced: boolean;
|
|
73
102
|
};
|
|
103
|
+
readOnlyMode: {
|
|
104
|
+
label: string;
|
|
105
|
+
help: string;
|
|
106
|
+
advanced: boolean;
|
|
107
|
+
};
|
|
108
|
+
sessionTags: {
|
|
109
|
+
label: string;
|
|
110
|
+
help: string;
|
|
111
|
+
advanced: boolean;
|
|
112
|
+
};
|
|
113
|
+
allowedToolSlugs: {
|
|
114
|
+
label: string;
|
|
115
|
+
help: string;
|
|
116
|
+
advanced: boolean;
|
|
117
|
+
};
|
|
118
|
+
blockedToolSlugs: {
|
|
119
|
+
label: string;
|
|
120
|
+
help: string;
|
|
121
|
+
advanced: boolean;
|
|
122
|
+
};
|
|
74
123
|
};
|
|
75
124
|
};
|