@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/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 = 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.toLowerCase()}=${accountId}`)
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 key = this.makeSessionCacheKey(userId, connectedAccounts);
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 session = await this.client.toolRouter.create(userId, connectedAccounts ? { connectedAccounts } : undefined);
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(toolkit.toLowerCase())) {
186
+ if (blockedToolkits?.includes(normalizedToolkit)) {
58
187
  return false;
59
188
  }
60
189
  if (allowedToolkits && allowedToolkits.length > 0) {
61
- return allowedToolkits.includes(toolkit.toLowerCase());
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.client.tools.execute(toolName, {
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 response;
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 (options?.toolkits && options.toolkits.length > 0) {
107
- if (!options.toolkits.some(t => t.toLowerCase() === toolkit.toLowerCase())) {
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 uid = this.getUserId(userId);
135
- const toolkit = toolSlug.split("_")[0]?.toLowerCase() || "";
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: uid,
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 session = await this.getSession(uid, accountResolution.connectedAccountId
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: toolSlug, arguments: args }],
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 { toolkit, userId } = params;
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
- const account = await this.client.connectedAccounts.get(explicitId);
323
- const accountToolkit = String(account?.toolkit?.slug || "").toLowerCase();
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
- return { connectedAccountId: explicitId };
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
- if (toolkits && toolkits.length > 0) {
365
- const requestedToolkits = toolkits.filter(t => this.isToolkitAllowed(t));
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.toLowerCase();
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.toLowerCase();
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
- ...(toolkits && toolkits.length > 0 ? { toolkitSlugs: toolkits } : {}),
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.toLowerCase());
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(s => allowed.has(s));
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
- if (!this.isToolkitAllowed(toolkit))
696
+ const normalizedToolkit = normalizeToolkitSlug(toolkit);
697
+ if (!this.isToolkitAllowed(normalizedToolkit))
512
698
  return [];
513
699
  const accounts = await this.listConnectedAccounts({
514
- toolkits: [toolkit],
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().toLowerCase();
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().toLowerCase();
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
- if (!this.isToolkitAllowed(toolkit)) {
619
- return { error: `Toolkit '${toolkit}' is not allowed by plugin configuration` };
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(toolkit);
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.toLowerCase();
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: [toolkit],
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 '${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 '${toolkit}' accounts found for user_id '${uid}': ${ids}. ` +
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({ connectedAccountId: activeAccounts[0].id });
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
  };