@customclaw/composio 0.0.7 → 0.0.8
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 +224 -46
- 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
|
}
|
|
@@ -132,7 +291,12 @@ export class ComposioClient {
|
|
|
132
291
|
*/
|
|
133
292
|
async executeTool(toolSlug, args, userId, connectedAccountId) {
|
|
134
293
|
const uid = this.getUserId(userId);
|
|
135
|
-
const
|
|
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,
|
|
@@ -151,15 +315,14 @@ export class ComposioClient {
|
|
|
151
315
|
? { [toolkit]: accountResolution.connectedAccountId }
|
|
152
316
|
: undefined);
|
|
153
317
|
try {
|
|
154
|
-
const response = await this.executeMetaTool("COMPOSIO_MULTI_EXECUTE_TOOL", {
|
|
155
|
-
tools: [{ tool_slug:
|
|
156
|
-
session: { id: session.sessionId },
|
|
318
|
+
const response = await this.executeMetaTool(session.sessionId, "COMPOSIO_MULTI_EXECUTE_TOOL", {
|
|
319
|
+
tools: [{ tool_slug: normalizedToolSlug, arguments: args }],
|
|
157
320
|
sync_response_to_workbench: false,
|
|
158
321
|
});
|
|
159
322
|
if (!response.successful) {
|
|
160
323
|
const recovered = await this.tryExecutionRecovery({
|
|
161
324
|
uid,
|
|
162
|
-
toolSlug,
|
|
325
|
+
toolSlug: normalizedToolSlug,
|
|
163
326
|
args,
|
|
164
327
|
connectedAccountId: accountResolution.connectedAccountId,
|
|
165
328
|
metaError: response.error,
|
|
@@ -179,7 +342,7 @@ export class ComposioClient {
|
|
|
179
342
|
if (!toolResponse.successful) {
|
|
180
343
|
const recovered = await this.tryExecutionRecovery({
|
|
181
344
|
uid,
|
|
182
|
-
toolSlug,
|
|
345
|
+
toolSlug: normalizedToolSlug,
|
|
183
346
|
args,
|
|
184
347
|
connectedAccountId: accountResolution.connectedAccountId,
|
|
185
348
|
metaError: toolResponse.error ?? undefined,
|
|
@@ -315,12 +478,13 @@ export class ComposioClient {
|
|
|
315
478
|
return String(first?.error || "");
|
|
316
479
|
}
|
|
317
480
|
async resolveConnectedAccountForExecution(params) {
|
|
318
|
-
const
|
|
481
|
+
const toolkit = normalizeToolkitSlug(params.toolkit);
|
|
482
|
+
const { userId } = params;
|
|
319
483
|
const explicitId = params.connectedAccountId?.trim();
|
|
320
484
|
if (explicitId) {
|
|
321
485
|
try {
|
|
322
486
|
const account = await this.client.connectedAccounts.get(explicitId);
|
|
323
|
-
const accountToolkit = String(account?.toolkit?.slug || "")
|
|
487
|
+
const accountToolkit = normalizeToolkitSlug(String(account?.toolkit?.slug || ""));
|
|
324
488
|
const accountStatus = String(account?.status || "").toUpperCase();
|
|
325
489
|
if (accountToolkit && accountToolkit !== toolkit) {
|
|
326
490
|
return {
|
|
@@ -361,14 +525,15 @@ export class ComposioClient {
|
|
|
361
525
|
const uid = this.getUserId(userId);
|
|
362
526
|
const session = await this.getSession(uid);
|
|
363
527
|
try {
|
|
364
|
-
|
|
365
|
-
|
|
528
|
+
const normalizedToolkits = normalizeToolkitList(toolkits);
|
|
529
|
+
if (normalizedToolkits && normalizedToolkits.length > 0) {
|
|
530
|
+
const requestedToolkits = normalizedToolkits.filter((t) => this.isToolkitAllowed(t));
|
|
366
531
|
if (requestedToolkits.length === 0)
|
|
367
532
|
return [];
|
|
368
533
|
const toolkitStateMap = await this.getToolkitStateMap(session, requestedToolkits);
|
|
369
534
|
const activeAccountToolkits = await this.getActiveConnectedAccountToolkits(uid, requestedToolkits);
|
|
370
535
|
return requestedToolkits.map((toolkit) => {
|
|
371
|
-
const key = toolkit
|
|
536
|
+
const key = normalizeToolkitSlug(toolkit);
|
|
372
537
|
return {
|
|
373
538
|
toolkit,
|
|
374
539
|
connected: (toolkitStateMap.get(key) ?? false) || activeAccountToolkits.has(key),
|
|
@@ -413,7 +578,7 @@ export class ComposioClient {
|
|
|
413
578
|
});
|
|
414
579
|
const items = response.items || [];
|
|
415
580
|
for (const tk of items) {
|
|
416
|
-
const key = tk.slug
|
|
581
|
+
const key = normalizeToolkitSlug(tk.slug);
|
|
417
582
|
const isActive = tk.connection?.isActive ?? false;
|
|
418
583
|
map.set(key, (map.get(key) ?? false) || isActive);
|
|
419
584
|
}
|
|
@@ -428,6 +593,7 @@ export class ComposioClient {
|
|
|
428
593
|
}
|
|
429
594
|
async getActiveConnectedAccountToolkits(userId, toolkits) {
|
|
430
595
|
const connected = new Set();
|
|
596
|
+
const normalizedToolkits = normalizeToolkitList(toolkits);
|
|
431
597
|
let cursor;
|
|
432
598
|
const seenCursors = new Set();
|
|
433
599
|
try {
|
|
@@ -435,7 +601,7 @@ export class ComposioClient {
|
|
|
435
601
|
const response = await this.client.connectedAccounts.list({
|
|
436
602
|
userIds: [userId],
|
|
437
603
|
statuses: ["ACTIVE"],
|
|
438
|
-
...(
|
|
604
|
+
...(normalizedToolkits && normalizedToolkits.length > 0 ? { toolkitSlugs: normalizedToolkits } : {}),
|
|
439
605
|
limit: 100,
|
|
440
606
|
...(cursor ? { cursor } : {}),
|
|
441
607
|
});
|
|
@@ -443,12 +609,12 @@ export class ComposioClient {
|
|
|
443
609
|
? response
|
|
444
610
|
: response?.items || []);
|
|
445
611
|
for (const item of items) {
|
|
446
|
-
const slug = item.toolkit?.slug;
|
|
612
|
+
const slug = normalizeToolkitSlug(item.toolkit?.slug || "");
|
|
447
613
|
if (!slug)
|
|
448
614
|
continue;
|
|
449
615
|
if (item.status && String(item.status).toUpperCase() !== "ACTIVE")
|
|
450
616
|
continue;
|
|
451
|
-
connected.add(slug
|
|
617
|
+
connected.add(slug);
|
|
452
618
|
}
|
|
453
619
|
cursor = Array.isArray(response)
|
|
454
620
|
? null
|
|
@@ -469,10 +635,9 @@ export class ComposioClient {
|
|
|
469
635
|
normalizeStatuses(statuses) {
|
|
470
636
|
if (!statuses || statuses.length === 0)
|
|
471
637
|
return undefined;
|
|
472
|
-
const allowed = new Set(["INITIALIZING", "INITIATED", "ACTIVE", "FAILED", "EXPIRED", "INACTIVE"]);
|
|
473
638
|
const normalized = statuses
|
|
474
639
|
.map(s => String(s || "").trim().toUpperCase())
|
|
475
|
-
.filter(
|
|
640
|
+
.filter(isConnectedAccountStatusFilter);
|
|
476
641
|
return normalized.length > 0 ? Array.from(new Set(normalized)) : undefined;
|
|
477
642
|
}
|
|
478
643
|
/**
|
|
@@ -480,9 +645,7 @@ export class ComposioClient {
|
|
|
480
645
|
* Uses raw API first to preserve user_id in responses, then falls back to SDK-normalized output.
|
|
481
646
|
*/
|
|
482
647
|
async listConnectedAccounts(options) {
|
|
483
|
-
const toolkits = options?.toolkits
|
|
484
|
-
?.map(t => String(t || "").trim())
|
|
485
|
-
.filter(t => t.length > 0 && this.isToolkitAllowed(t));
|
|
648
|
+
const toolkits = normalizeToolkitList(options?.toolkits)?.filter((t) => this.isToolkitAllowed(t));
|
|
486
649
|
const userIds = options?.userIds
|
|
487
650
|
?.map(u => String(u || "").trim())
|
|
488
651
|
.filter(Boolean);
|
|
@@ -508,10 +671,11 @@ export class ComposioClient {
|
|
|
508
671
|
* Find user IDs that have an active connected account for a toolkit.
|
|
509
672
|
*/
|
|
510
673
|
async findActiveUserIdsForToolkit(toolkit) {
|
|
511
|
-
|
|
674
|
+
const normalizedToolkit = normalizeToolkitSlug(toolkit);
|
|
675
|
+
if (!this.isToolkitAllowed(normalizedToolkit))
|
|
512
676
|
return [];
|
|
513
677
|
const accounts = await this.listConnectedAccounts({
|
|
514
|
-
toolkits: [
|
|
678
|
+
toolkits: [normalizedToolkit],
|
|
515
679
|
statuses: ["ACTIVE"],
|
|
516
680
|
});
|
|
517
681
|
const userIds = new Set();
|
|
@@ -537,7 +701,7 @@ export class ComposioClient {
|
|
|
537
701
|
? response
|
|
538
702
|
: response?.items || []);
|
|
539
703
|
for (const item of items) {
|
|
540
|
-
const toolkitSlug = (item.toolkit?.slug || "").toString()
|
|
704
|
+
const toolkitSlug = normalizeToolkitSlug((item.toolkit?.slug || "").toString());
|
|
541
705
|
if (!toolkitSlug)
|
|
542
706
|
continue;
|
|
543
707
|
if (!this.isToolkitAllowed(toolkitSlug))
|
|
@@ -582,7 +746,7 @@ export class ComposioClient {
|
|
|
582
746
|
? response
|
|
583
747
|
: response?.items || []);
|
|
584
748
|
for (const item of items) {
|
|
585
|
-
const toolkitSlug = (item.toolkit?.slug || "").toString()
|
|
749
|
+
const toolkitSlug = normalizeToolkitSlug((item.toolkit?.slug || "").toString());
|
|
586
750
|
if (!toolkitSlug)
|
|
587
751
|
continue;
|
|
588
752
|
if (!this.isToolkitAllowed(toolkitSlug))
|
|
@@ -615,12 +779,16 @@ export class ComposioClient {
|
|
|
615
779
|
*/
|
|
616
780
|
async createConnection(toolkit, userId) {
|
|
617
781
|
const uid = this.getUserId(userId);
|
|
618
|
-
|
|
619
|
-
|
|
782
|
+
const toolkitSlug = normalizeToolkitSlug(toolkit);
|
|
783
|
+
if (!toolkitSlug) {
|
|
784
|
+
return { error: "Toolkit is required" };
|
|
785
|
+
}
|
|
786
|
+
if (!this.isToolkitAllowed(toolkitSlug)) {
|
|
787
|
+
return { error: `Toolkit '${toolkitSlug}' is not allowed by plugin configuration` };
|
|
620
788
|
}
|
|
621
789
|
try {
|
|
622
790
|
const session = await this.getSession(uid);
|
|
623
|
-
const result = await session.authorize(
|
|
791
|
+
const result = await session.authorize(toolkitSlug);
|
|
624
792
|
return { authUrl: result.redirectUrl || result.url || "" };
|
|
625
793
|
}
|
|
626
794
|
catch (err) {
|
|
@@ -646,7 +814,7 @@ export class ComposioClient {
|
|
|
646
814
|
});
|
|
647
815
|
const allToolkits = response.items || [];
|
|
648
816
|
for (const tk of allToolkits) {
|
|
649
|
-
const slug = tk.slug
|
|
817
|
+
const slug = normalizeToolkitSlug(tk.slug);
|
|
650
818
|
if (!this.isToolkitAllowed(slug))
|
|
651
819
|
continue;
|
|
652
820
|
seen.add(slug);
|
|
@@ -674,24 +842,34 @@ export class ComposioClient {
|
|
|
674
842
|
*/
|
|
675
843
|
async disconnectToolkit(toolkit, userId) {
|
|
676
844
|
const uid = this.getUserId(userId);
|
|
845
|
+
const toolkitSlug = normalizeToolkitSlug(toolkit);
|
|
846
|
+
if (!toolkitSlug) {
|
|
847
|
+
return { success: false, error: "Toolkit is required" };
|
|
848
|
+
}
|
|
849
|
+
if (this.config.readOnlyMode) {
|
|
850
|
+
return {
|
|
851
|
+
success: false,
|
|
852
|
+
error: "Disconnect is blocked by readOnlyMode.",
|
|
853
|
+
};
|
|
854
|
+
}
|
|
677
855
|
try {
|
|
678
856
|
const activeAccounts = await this.listConnectedAccounts({
|
|
679
|
-
toolkits: [
|
|
857
|
+
toolkits: [toolkitSlug],
|
|
680
858
|
userIds: [uid],
|
|
681
859
|
statuses: ["ACTIVE"],
|
|
682
860
|
});
|
|
683
861
|
if (activeAccounts.length === 0) {
|
|
684
|
-
return { success: false, error: `No connection found for toolkit '${
|
|
862
|
+
return { success: false, error: `No connection found for toolkit '${toolkitSlug}'` };
|
|
685
863
|
}
|
|
686
864
|
if (activeAccounts.length > 1) {
|
|
687
865
|
const ids = activeAccounts.map(a => a.id).join(", ");
|
|
688
866
|
return {
|
|
689
867
|
success: false,
|
|
690
|
-
error: `Multiple ACTIVE '${
|
|
868
|
+
error: `Multiple ACTIVE '${toolkitSlug}' accounts found for user_id '${uid}': ${ids}. ` +
|
|
691
869
|
"Use the dashboard to disconnect a specific account.",
|
|
692
870
|
};
|
|
693
871
|
}
|
|
694
|
-
await this.client.connectedAccounts.delete(
|
|
872
|
+
await this.client.connectedAccounts.delete(activeAccounts[0].id);
|
|
695
873
|
// Clear session cache to refresh connection status
|
|
696
874
|
this.clearUserSessionCache(uid);
|
|
697
875
|
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
|
};
|
package/dist/config.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
+
import { LEGACY_ENTRY_FLAT_CONFIG_KEYS, LEGACY_SHAPE_ERROR, SESSION_TAGS, isRecord, normalizeSessionTags, normalizeToolkitList, normalizeToolSlugList, } from "./utils.js";
|
|
2
3
|
/**
|
|
3
4
|
* Zod schema for Composio plugin configuration
|
|
4
5
|
*/
|
|
@@ -8,36 +9,39 @@ export const ComposioConfigSchema = z.object({
|
|
|
8
9
|
defaultUserId: z.string().optional(),
|
|
9
10
|
allowedToolkits: z.array(z.string()).optional(),
|
|
10
11
|
blockedToolkits: z.array(z.string()).optional(),
|
|
12
|
+
readOnlyMode: z.boolean().default(false),
|
|
13
|
+
sessionTags: z.array(z.enum(SESSION_TAGS)).optional(),
|
|
14
|
+
allowedToolSlugs: z.array(z.string()).optional(),
|
|
15
|
+
blockedToolSlugs: z.array(z.string()).optional(),
|
|
11
16
|
});
|
|
12
17
|
/**
|
|
13
18
|
* Parse and validate plugin config with environment fallbacks
|
|
14
19
|
*/
|
|
15
20
|
export function parseComposioConfig(value) {
|
|
16
|
-
const raw = value
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
const
|
|
25
|
-
|
|
26
|
-
const
|
|
27
|
-
|
|
28
|
-
const blockedToolkits = (Array.isArray(raw.blockedToolkits) ? raw.blockedToolkits : undefined) ??
|
|
29
|
-
(Array.isArray(configObj?.blockedToolkits) ? configObj.blockedToolkits : undefined);
|
|
30
|
-
// Allow API key from config.apiKey, top-level apiKey, or environment.
|
|
31
|
-
const apiKey = (typeof configObj?.apiKey === "string" && configObj.apiKey.trim()) ||
|
|
32
|
-
(typeof raw.apiKey === "string" && raw.apiKey.trim()) ||
|
|
21
|
+
const raw = isRecord(value) ? value : {};
|
|
22
|
+
const configObj = isRecord(raw.config) ? raw.config : undefined;
|
|
23
|
+
if (configObj) {
|
|
24
|
+
const hasLegacyFlatKeys = LEGACY_ENTRY_FLAT_CONFIG_KEYS.some((key) => key in raw);
|
|
25
|
+
if (hasLegacyFlatKeys) {
|
|
26
|
+
throw new Error(LEGACY_SHAPE_ERROR);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
const source = configObj ?? raw;
|
|
30
|
+
const enabled = typeof raw.enabled === "boolean" ? raw.enabled : true;
|
|
31
|
+
const readOnlyMode = typeof source.readOnlyMode === "boolean" ? source.readOnlyMode : false;
|
|
32
|
+
const apiKey = (typeof source.apiKey === "string" && source.apiKey.trim()) ||
|
|
33
33
|
process.env.COMPOSIO_API_KEY ||
|
|
34
34
|
"";
|
|
35
35
|
return ComposioConfigSchema.parse({
|
|
36
36
|
enabled,
|
|
37
37
|
apiKey,
|
|
38
|
-
defaultUserId,
|
|
39
|
-
allowedToolkits,
|
|
40
|
-
blockedToolkits,
|
|
38
|
+
defaultUserId: typeof source.defaultUserId === "string" ? source.defaultUserId : undefined,
|
|
39
|
+
allowedToolkits: normalizeToolkitList(Array.isArray(source.allowedToolkits) ? source.allowedToolkits : undefined),
|
|
40
|
+
blockedToolkits: normalizeToolkitList(Array.isArray(source.blockedToolkits) ? source.blockedToolkits : undefined),
|
|
41
|
+
readOnlyMode,
|
|
42
|
+
sessionTags: normalizeSessionTags(Array.isArray(source.sessionTags) ? source.sessionTags : undefined),
|
|
43
|
+
allowedToolSlugs: normalizeToolSlugList(Array.isArray(source.allowedToolSlugs) ? source.allowedToolSlugs : undefined),
|
|
44
|
+
blockedToolSlugs: normalizeToolSlugList(Array.isArray(source.blockedToolSlugs) ? source.blockedToolSlugs : undefined),
|
|
41
45
|
});
|
|
42
46
|
}
|
|
43
47
|
/**
|
|
@@ -67,6 +71,26 @@ export const composioConfigUiHints = {
|
|
|
67
71
|
help: "Block specific toolkits from being used",
|
|
68
72
|
advanced: true,
|
|
69
73
|
},
|
|
74
|
+
readOnlyMode: {
|
|
75
|
+
label: "Read-Only Mode",
|
|
76
|
+
help: "Block likely-destructive tool actions by token matching; allow specific slugs with allowedToolSlugs if needed",
|
|
77
|
+
advanced: true,
|
|
78
|
+
},
|
|
79
|
+
sessionTags: {
|
|
80
|
+
label: "Session Tags",
|
|
81
|
+
help: "Composio Tool Router behavior tags (e.g., readOnlyHint, destructiveHint)",
|
|
82
|
+
advanced: true,
|
|
83
|
+
},
|
|
84
|
+
allowedToolSlugs: {
|
|
85
|
+
label: "Allowed Tool Slugs",
|
|
86
|
+
help: "Optional explicit allowlist for tool slugs (UPPERCASE)",
|
|
87
|
+
advanced: true,
|
|
88
|
+
},
|
|
89
|
+
blockedToolSlugs: {
|
|
90
|
+
label: "Blocked Tool Slugs",
|
|
91
|
+
help: "Explicit denylist for tool slugs (UPPERCASE)",
|
|
92
|
+
advanced: true,
|
|
93
|
+
},
|
|
70
94
|
};
|
|
71
95
|
/**
|
|
72
96
|
* Plugin config schema object for openclaw
|
package/dist/index.d.ts
CHANGED
|
@@ -49,6 +49,26 @@ declare const composioPlugin: {
|
|
|
49
49
|
help: string;
|
|
50
50
|
advanced: boolean;
|
|
51
51
|
};
|
|
52
|
+
readOnlyMode: {
|
|
53
|
+
label: string;
|
|
54
|
+
help: string;
|
|
55
|
+
advanced: boolean;
|
|
56
|
+
};
|
|
57
|
+
sessionTags: {
|
|
58
|
+
label: string;
|
|
59
|
+
help: string;
|
|
60
|
+
advanced: boolean;
|
|
61
|
+
};
|
|
62
|
+
allowedToolSlugs: {
|
|
63
|
+
label: string;
|
|
64
|
+
help: string;
|
|
65
|
+
advanced: boolean;
|
|
66
|
+
};
|
|
67
|
+
blockedToolSlugs: {
|
|
68
|
+
label: string;
|
|
69
|
+
help: string;
|
|
70
|
+
advanced: boolean;
|
|
71
|
+
};
|
|
52
72
|
};
|
|
53
73
|
};
|
|
54
74
|
register(api: any): void;
|