@frockbot/plugin-composio 0.3.2 → 0.3.4
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/package.json +6 -6
- package/src/agent.test.ts +108 -27
- package/src/agent.ts +90 -26
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@frockbot/plugin-composio",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.4",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -22,11 +22,11 @@
|
|
|
22
22
|
"typecheck": "tsc --noEmit -p tsconfig.json"
|
|
23
23
|
},
|
|
24
24
|
"dependencies": {
|
|
25
|
-
"@frockbot/configuration-core": "0.3.
|
|
26
|
-
"@frockbot/connection-core": "0.3.
|
|
27
|
-
"@frockbot/kernel-contracts": "0.3.
|
|
28
|
-
"@frockbot/plugin-settings": "0.3.
|
|
29
|
-
"@frockbot/plugin-tools": "0.3.
|
|
25
|
+
"@frockbot/configuration-core": "0.3.4",
|
|
26
|
+
"@frockbot/connection-core": "0.3.4",
|
|
27
|
+
"@frockbot/kernel-contracts": "0.3.4",
|
|
28
|
+
"@frockbot/plugin-settings": "0.3.4",
|
|
29
|
+
"@frockbot/plugin-tools": "0.3.4",
|
|
30
30
|
"cordis": "4.0.0-rc.8"
|
|
31
31
|
},
|
|
32
32
|
"devDependencies": {
|
package/src/agent.test.ts
CHANGED
|
@@ -2,10 +2,32 @@ import { afterEach, describe, expect, test } from "bun:test";
|
|
|
2
2
|
import { type SessionEvent, SessionStore } from "@frockbot/kernel-contracts";
|
|
3
3
|
import { ToolRegistry } from "@frockbot/plugin-tools";
|
|
4
4
|
import { Context } from "cordis";
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
composioNamespaceV1,
|
|
7
|
+
createComposioPlugin,
|
|
8
|
+
createComposioRouterPlugin,
|
|
9
|
+
} from "./agent.js";
|
|
6
10
|
import { ComposioClient } from "./composio-client.js";
|
|
7
11
|
|
|
8
12
|
const roots: Context[] = [];
|
|
13
|
+
const namespace = composioNamespaceV1("gmail", "ca_123");
|
|
14
|
+
|
|
15
|
+
function dynamicCall(
|
|
16
|
+
id: string,
|
|
17
|
+
toolName: string,
|
|
18
|
+
arguments_: Record<string, unknown>,
|
|
19
|
+
) {
|
|
20
|
+
return {
|
|
21
|
+
id,
|
|
22
|
+
name: "call_dynamic_tool",
|
|
23
|
+
input: {
|
|
24
|
+
namespace,
|
|
25
|
+
toolName,
|
|
26
|
+
arguments: arguments_,
|
|
27
|
+
mcpDetails: { description: `Use ${toolName} for this test.` },
|
|
28
|
+
},
|
|
29
|
+
};
|
|
30
|
+
}
|
|
9
31
|
|
|
10
32
|
afterEach(async () => {
|
|
11
33
|
await Promise.all(roots.splice(0).map((root) => root.fiber.dispose()));
|
|
@@ -65,11 +87,10 @@ describe("Composio router Plugin", () => {
|
|
|
65
87
|
};
|
|
66
88
|
|
|
67
89
|
const invented = await root.tools.prepare(
|
|
68
|
-
{
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
},
|
|
90
|
+
dynamicCall("invented", "composio_execute_tool", {
|
|
91
|
+
toolSlug: "GMAIL_FETCH_EMAILS",
|
|
92
|
+
arguments: {},
|
|
93
|
+
}),
|
|
73
94
|
context,
|
|
74
95
|
);
|
|
75
96
|
if (invented.kind !== "ready") throw new Error("execute tool was denied");
|
|
@@ -79,11 +100,9 @@ describe("Composio router Plugin", () => {
|
|
|
79
100
|
expect(calls).toEqual([]);
|
|
80
101
|
|
|
81
102
|
const search = await root.tools.prepare(
|
|
82
|
-
{
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
input: { query: "fetch emails" },
|
|
86
|
-
},
|
|
103
|
+
dynamicCall("search", "composio_search_tools", {
|
|
104
|
+
query: "fetch emails",
|
|
105
|
+
}),
|
|
87
106
|
context,
|
|
88
107
|
);
|
|
89
108
|
if (search.kind !== "ready") throw new Error("search tool was denied");
|
|
@@ -92,11 +111,10 @@ describe("Composio router Plugin", () => {
|
|
|
92
111
|
});
|
|
93
112
|
|
|
94
113
|
const exact = await root.tools.prepare(
|
|
95
|
-
{
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
},
|
|
114
|
+
dynamicCall("exact", "composio_execute_tool", {
|
|
115
|
+
toolSlug: "GMAIL_FETCH_EMAILS",
|
|
116
|
+
arguments: {},
|
|
117
|
+
}),
|
|
100
118
|
context,
|
|
101
119
|
);
|
|
102
120
|
if (exact.kind !== "ready") throw new Error("execute tool was denied");
|
|
@@ -104,7 +122,7 @@ describe("Composio router Plugin", () => {
|
|
|
104
122
|
isError: false,
|
|
105
123
|
});
|
|
106
124
|
expect(calls).toHaveLength(2);
|
|
107
|
-
expect(authorizationCalls).toBe(
|
|
125
|
+
expect(authorizationCalls).toBe(3);
|
|
108
126
|
});
|
|
109
127
|
|
|
110
128
|
test("restores searched slug authorization from the durable session", async () => {
|
|
@@ -123,8 +141,12 @@ describe("Composio router Plugin", () => {
|
|
|
123
141
|
toolCalls: [
|
|
124
142
|
{
|
|
125
143
|
id: "search-before-eviction",
|
|
126
|
-
name: "
|
|
127
|
-
input:
|
|
144
|
+
name: "call_dynamic_tool",
|
|
145
|
+
input: dynamicCall(
|
|
146
|
+
"search-before-eviction",
|
|
147
|
+
"composio_search_tools",
|
|
148
|
+
{},
|
|
149
|
+
).input,
|
|
128
150
|
},
|
|
129
151
|
],
|
|
130
152
|
},
|
|
@@ -133,15 +155,19 @@ describe("Composio router Plugin", () => {
|
|
|
133
155
|
turn: 1,
|
|
134
156
|
step: 1,
|
|
135
157
|
occurrenceId: "tool:1:1:0",
|
|
136
|
-
name: "
|
|
137
|
-
input:
|
|
158
|
+
name: "call_dynamic_tool",
|
|
159
|
+
input: dynamicCall(
|
|
160
|
+
"search-before-eviction",
|
|
161
|
+
"composio_search_tools",
|
|
162
|
+
{},
|
|
163
|
+
).input,
|
|
138
164
|
},
|
|
139
165
|
{
|
|
140
166
|
type: "tool/result",
|
|
141
167
|
turn: 1,
|
|
142
168
|
step: 1,
|
|
143
169
|
occurrenceId: "tool:1:1:0",
|
|
144
|
-
name: "
|
|
170
|
+
name: "call_dynamic_tool",
|
|
145
171
|
content: JSON.stringify([
|
|
146
172
|
{
|
|
147
173
|
slug: "GMAIL_FETCH_EMAILS",
|
|
@@ -190,11 +216,10 @@ describe("Composio router Plugin", () => {
|
|
|
190
216
|
signal: new AbortController().signal,
|
|
191
217
|
};
|
|
192
218
|
const execution = await root.tools.prepare(
|
|
193
|
-
{
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
},
|
|
219
|
+
dynamicCall("execute-after-eviction", "composio_execute_tool", {
|
|
220
|
+
toolSlug: "GMAIL_FETCH_EMAILS",
|
|
221
|
+
arguments: {},
|
|
222
|
+
}),
|
|
198
223
|
context,
|
|
199
224
|
);
|
|
200
225
|
if (execution.kind !== "ready") throw new Error("execute tool was denied");
|
|
@@ -204,4 +229,60 @@ describe("Composio router Plugin", () => {
|
|
|
204
229
|
).resolves.toMatchObject({ isError: false });
|
|
205
230
|
expect(calls).toHaveLength(1);
|
|
206
231
|
});
|
|
232
|
+
|
|
233
|
+
test("derives a stable GrokBot-style namespace from toolkit and account", () => {
|
|
234
|
+
expect(composioNamespaceV1("github", "acct_acme-42")).toBe(
|
|
235
|
+
"user-Github--acct-acme-42",
|
|
236
|
+
);
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
test("registers static per-tool integrations under the account namespace", async () => {
|
|
240
|
+
const root = new Context();
|
|
241
|
+
roots.push(root);
|
|
242
|
+
await root.plugin(ToolRegistry);
|
|
243
|
+
await root.plugin(
|
|
244
|
+
createComposioPlugin({
|
|
245
|
+
client: new ComposioClient({ apiKey: "secret" }),
|
|
246
|
+
userId: "user-1",
|
|
247
|
+
toolkitSlug: "github",
|
|
248
|
+
connectedAccountId: "acct_acme-42",
|
|
249
|
+
tools: [
|
|
250
|
+
{
|
|
251
|
+
slug: "GITHUB_SEARCH_ISSUES",
|
|
252
|
+
name: "search_issues",
|
|
253
|
+
description: "Search issues.",
|
|
254
|
+
inputSchema: { type: "object" },
|
|
255
|
+
},
|
|
256
|
+
],
|
|
257
|
+
}),
|
|
258
|
+
);
|
|
259
|
+
|
|
260
|
+
expect(root.tools.registeredNames?.()).toContain(
|
|
261
|
+
"user-Github--acct-acme-42/search_issues",
|
|
262
|
+
);
|
|
263
|
+
const call = {
|
|
264
|
+
id: "static",
|
|
265
|
+
name: "call_dynamic_tool",
|
|
266
|
+
input: {
|
|
267
|
+
namespace: "user-Github--acct-acme-42",
|
|
268
|
+
toolName: "search_issues",
|
|
269
|
+
},
|
|
270
|
+
};
|
|
271
|
+
const preparation = await root.tools.prepare(call, {
|
|
272
|
+
botId: "primary",
|
|
273
|
+
agentId: "primary",
|
|
274
|
+
compositionGenerationId: "bootstrap",
|
|
275
|
+
turnType: "chat",
|
|
276
|
+
sessionId: "user-1:primary",
|
|
277
|
+
effectId: "tool:1:1:0",
|
|
278
|
+
signal: new AbortController().signal,
|
|
279
|
+
});
|
|
280
|
+
expect(preparation).toMatchObject({
|
|
281
|
+
kind: "denied",
|
|
282
|
+
result: {
|
|
283
|
+
content:
|
|
284
|
+
'External namespace "user-Github--acct-acme-42" requires mcpDetails.description',
|
|
285
|
+
},
|
|
286
|
+
});
|
|
287
|
+
});
|
|
207
288
|
});
|
package/src/agent.ts
CHANGED
|
@@ -14,6 +14,7 @@ export interface ComposioPluginConfig {
|
|
|
14
14
|
client: ComposioClient;
|
|
15
15
|
userId: string;
|
|
16
16
|
connectedAccountId: string;
|
|
17
|
+
toolkitSlug: string;
|
|
17
18
|
tools: ComposioToolDeclaration[];
|
|
18
19
|
}
|
|
19
20
|
|
|
@@ -21,6 +22,28 @@ function isObject(value: unknown): value is Record<string, unknown> {
|
|
|
21
22
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
22
23
|
}
|
|
23
24
|
|
|
25
|
+
function slugSegment(value: string): string {
|
|
26
|
+
return value
|
|
27
|
+
.trim()
|
|
28
|
+
.replace(/[^A-Za-z0-9]+/g, "-")
|
|
29
|
+
.replace(/^-+|-+$/g, "");
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** The stable, account-specific namespace exposed to the model. */
|
|
33
|
+
export function composioNamespaceV1(
|
|
34
|
+
toolkitSlug: string,
|
|
35
|
+
connectedAccountId: string,
|
|
36
|
+
): string {
|
|
37
|
+
const toolkit = slugSegment(toolkitSlug);
|
|
38
|
+
const account = slugSegment(connectedAccountId);
|
|
39
|
+
if (!toolkit || !account) {
|
|
40
|
+
throw new Error("Composio toolkit and connected account must be slug-like");
|
|
41
|
+
}
|
|
42
|
+
const displayToolkit =
|
|
43
|
+
toolkit.charAt(0).toUpperCase() + toolkit.slice(1).toLowerCase();
|
|
44
|
+
return `user-${displayToolkit}--${account}`;
|
|
45
|
+
}
|
|
46
|
+
|
|
24
47
|
export interface ComposioRouterPluginConfig {
|
|
25
48
|
client: ComposioClient;
|
|
26
49
|
userId: string;
|
|
@@ -75,6 +98,7 @@ export function createComposioRouterPlugin(
|
|
|
75
98
|
): Plugin.Function {
|
|
76
99
|
const allowedToolSlugs = new Set<string>();
|
|
77
100
|
let runtimeContext: Context | undefined;
|
|
101
|
+
let runtimeNamespace: string | undefined;
|
|
78
102
|
const search: ToolDefinition = {
|
|
79
103
|
name: "composio_search_tools",
|
|
80
104
|
// A general work tool: the full toolset an `executor` subagent gets, and
|
|
@@ -130,29 +154,37 @@ export function createComposioRouterPlugin(
|
|
|
130
154
|
) {
|
|
131
155
|
return { content: "Invalid Composio tool input", isError: true };
|
|
132
156
|
}
|
|
133
|
-
const
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
157
|
+
const events = runtimeContext?.sessions.get(context.sessionId)?.events;
|
|
158
|
+
const wasDurablySearched = events?.some((event) => {
|
|
159
|
+
if (event.type !== "tool/result" || event.isError) {
|
|
160
|
+
return false;
|
|
161
|
+
}
|
|
162
|
+
const isSearchResult =
|
|
163
|
+
event.name === "composio_search_tools" ||
|
|
164
|
+
(event.name === "call_dynamic_tool" &&
|
|
165
|
+
events?.some(
|
|
166
|
+
(candidate) =>
|
|
167
|
+
candidate.type === "tool/call" &&
|
|
168
|
+
candidate.occurrenceId === event.occurrenceId &&
|
|
169
|
+
candidate.name === "call_dynamic_tool" &&
|
|
170
|
+
isObject(candidate.input) &&
|
|
171
|
+
candidate.input.namespace === runtimeNamespace &&
|
|
172
|
+
candidate.input.toolName === "composio_search_tools",
|
|
173
|
+
));
|
|
174
|
+
if (!isSearchResult) return false;
|
|
175
|
+
try {
|
|
176
|
+
const result: unknown = JSON.parse(event.content);
|
|
177
|
+
return (
|
|
178
|
+
Array.isArray(result) &&
|
|
179
|
+
result.some(
|
|
180
|
+
(candidate) =>
|
|
181
|
+
isObject(candidate) && candidate.slug === input.toolSlug,
|
|
182
|
+
)
|
|
183
|
+
);
|
|
184
|
+
} catch {
|
|
185
|
+
return false;
|
|
186
|
+
}
|
|
187
|
+
});
|
|
156
188
|
if (!allowedToolSlugs.has(input.toolSlug) && !wasDurablySearched) {
|
|
157
189
|
return {
|
|
158
190
|
content:
|
|
@@ -176,14 +208,35 @@ export function createComposioRouterPlugin(
|
|
|
176
208
|
return { content: JSON.stringify(result), isError: false };
|
|
177
209
|
},
|
|
178
210
|
};
|
|
179
|
-
const plugin: Plugin.Function = (ctx: Context) => {
|
|
211
|
+
const plugin: Plugin.Function = async (ctx: Context) => {
|
|
212
|
+
const grant = await config.authorizeEffect();
|
|
213
|
+
if (grant.toolkitSlug !== config.toolkitSlug) {
|
|
214
|
+
throw new Error("Composio effect grant changed toolkit");
|
|
215
|
+
}
|
|
216
|
+
runtimeNamespace = composioNamespaceV1(
|
|
217
|
+
grant.toolkitSlug,
|
|
218
|
+
grant.connectedAccountId,
|
|
219
|
+
);
|
|
180
220
|
runtimeContext = ctx;
|
|
181
|
-
const
|
|
182
|
-
|
|
221
|
+
const removeNamespace = ctx.tools.registerNamespace({
|
|
222
|
+
name: runtimeNamespace,
|
|
223
|
+
external: true,
|
|
224
|
+
status: "ready",
|
|
225
|
+
});
|
|
226
|
+
const removeSearch = ctx.tools.register({
|
|
227
|
+
...search,
|
|
228
|
+
namespace: runtimeNamespace,
|
|
229
|
+
});
|
|
230
|
+
const removeExecute = ctx.tools.register({
|
|
231
|
+
...execute,
|
|
232
|
+
namespace: runtimeNamespace,
|
|
233
|
+
});
|
|
183
234
|
return () => {
|
|
184
235
|
runtimeContext = undefined;
|
|
236
|
+
runtimeNamespace = undefined;
|
|
185
237
|
removeExecute();
|
|
186
238
|
removeSearch();
|
|
239
|
+
removeNamespace();
|
|
187
240
|
};
|
|
188
241
|
};
|
|
189
242
|
plugin.inject = ["tools", "sessions"];
|
|
@@ -194,9 +247,19 @@ export function createComposioPlugin(
|
|
|
194
247
|
config: ComposioPluginConfig,
|
|
195
248
|
): Plugin.Function {
|
|
196
249
|
const plugin: Plugin.Function = (ctx: Context) => {
|
|
250
|
+
const namespace = composioNamespaceV1(
|
|
251
|
+
config.toolkitSlug,
|
|
252
|
+
config.connectedAccountId,
|
|
253
|
+
);
|
|
254
|
+
const removeNamespace = ctx.tools.registerNamespace({
|
|
255
|
+
name: namespace,
|
|
256
|
+
external: true,
|
|
257
|
+
status: "ready",
|
|
258
|
+
});
|
|
197
259
|
const disposers = config.tools.map((declaration) => {
|
|
198
260
|
const tool: ToolDefinition = {
|
|
199
261
|
name: declaration.name,
|
|
262
|
+
namespace,
|
|
200
263
|
description: declaration.description,
|
|
201
264
|
inputSchema: declaration.inputSchema,
|
|
202
265
|
validate: isObject,
|
|
@@ -218,6 +281,7 @@ export function createComposioPlugin(
|
|
|
218
281
|
});
|
|
219
282
|
return () => {
|
|
220
283
|
for (const dispose of disposers.toReversed()) dispose();
|
|
284
|
+
removeNamespace();
|
|
221
285
|
};
|
|
222
286
|
};
|
|
223
287
|
plugin.inject = ["tools"];
|