@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/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
  }
@@ -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 toolkit = toolSlug.split("_")[0]?.toLowerCase() || "";
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: toolSlug, arguments: args }],
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 { toolkit, userId } = params;
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 || "").toLowerCase();
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
- if (toolkits && toolkits.length > 0) {
365
- const requestedToolkits = toolkits.filter(t => this.isToolkitAllowed(t));
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.toLowerCase();
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.toLowerCase();
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
- ...(toolkits && toolkits.length > 0 ? { toolkitSlugs: toolkits } : {}),
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.toLowerCase());
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(s => allowed.has(s));
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
- if (!this.isToolkitAllowed(toolkit))
674
+ const normalizedToolkit = normalizeToolkitSlug(toolkit);
675
+ if (!this.isToolkitAllowed(normalizedToolkit))
512
676
  return [];
513
677
  const accounts = await this.listConnectedAccounts({
514
- toolkits: [toolkit],
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().toLowerCase();
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().toLowerCase();
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
- if (!this.isToolkitAllowed(toolkit)) {
619
- return { error: `Toolkit '${toolkit}' is not allowed by plugin configuration` };
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(toolkit);
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.toLowerCase();
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: [toolkit],
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 '${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 '${toolkit}' accounts found for user_id '${uid}': ${ids}. ` +
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({ connectedAccountId: activeAccounts[0].id });
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 && typeof value === "object" && !Array.isArray(value)
17
- ? value
18
- : {};
19
- // Support both plugin entry shape ({ enabled, config: {...} }) and flat shape.
20
- const configObj = raw.config;
21
- const enabled = (typeof raw.enabled === "boolean" ? raw.enabled : undefined) ??
22
- (typeof configObj?.enabled === "boolean" ? configObj.enabled : undefined) ??
23
- true;
24
- const defaultUserId = (typeof raw.defaultUserId === "string" ? raw.defaultUserId : undefined) ??
25
- (typeof configObj?.defaultUserId === "string" ? configObj.defaultUserId : undefined);
26
- const allowedToolkits = (Array.isArray(raw.allowedToolkits) ? raw.allowedToolkits : undefined) ??
27
- (Array.isArray(configObj?.allowedToolkits) ? configObj.allowedToolkits : undefined);
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;