@clinebot/core 0.0.11 → 0.0.12
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/agents/agent-config-loader.d.ts +1 -1
- package/dist/agents/agent-config-parser.d.ts +5 -2
- package/dist/agents/index.d.ts +1 -1
- package/dist/agents/plugin-config-loader.d.ts +4 -0
- package/dist/agents/plugin-sandbox-bootstrap.js +446 -0
- package/dist/agents/plugin-sandbox.d.ts +4 -0
- package/dist/index.node.d.ts +1 -0
- package/dist/index.node.js +658 -407
- package/dist/runtime/sandbox/subprocess-sandbox.d.ts +8 -1
- package/dist/session/default-session-manager.d.ts +5 -0
- package/dist/session/session-config-builder.d.ts +4 -1
- package/dist/session/session-manager.d.ts +1 -0
- package/dist/session/unified-session-persistence-service.d.ts +6 -0
- package/dist/session/utils/types.d.ts +9 -0
- package/dist/tools/definitions.d.ts +2 -2
- package/dist/tools/presets.d.ts +3 -3
- package/dist/tools/schemas.d.ts +14 -14
- package/dist/types/config.d.ts +5 -0
- package/dist/types/events.d.ts +22 -0
- package/package.json +5 -4
- package/src/agents/agent-config-loader.test.ts +2 -0
- package/src/agents/agent-config-loader.ts +1 -0
- package/src/agents/agent-config-parser.ts +12 -5
- package/src/agents/index.ts +1 -0
- package/src/agents/plugin-config-loader.test.ts +49 -0
- package/src/agents/plugin-config-loader.ts +10 -73
- package/src/agents/plugin-loader.test.ts +128 -2
- package/src/agents/plugin-loader.ts +70 -5
- package/src/agents/plugin-sandbox-bootstrap.ts +445 -0
- package/src/agents/plugin-sandbox.test.ts +198 -1
- package/src/agents/plugin-sandbox.ts +223 -353
- package/src/index.node.ts +4 -0
- package/src/runtime/hook-file-hooks.test.ts +1 -1
- package/src/runtime/hook-file-hooks.ts +16 -6
- package/src/runtime/runtime-builder.test.ts +67 -0
- package/src/runtime/runtime-builder.ts +70 -16
- package/src/runtime/sandbox/subprocess-sandbox.ts +35 -11
- package/src/session/default-session-manager.e2e.test.ts +20 -1
- package/src/session/default-session-manager.test.ts +453 -1
- package/src/session/default-session-manager.ts +200 -0
- package/src/session/session-config-builder.ts +2 -0
- package/src/session/session-manager.ts +1 -0
- package/src/session/session-team-coordination.ts +30 -0
- package/src/session/unified-session-persistence-service.ts +45 -0
- package/src/session/utils/types.ts +10 -0
- package/src/storage/sqlite-team-store.ts +16 -5
- package/src/tools/definitions.test.ts +87 -8
- package/src/tools/definitions.ts +89 -70
- package/src/tools/presets.test.ts +2 -3
- package/src/tools/presets.ts +3 -3
- package/src/tools/schemas.ts +23 -22
- package/src/types/config.ts +5 -0
- package/src/types/events.ts +23 -0
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
1
4
|
import type { AgentConfig, Tool } from "@clinebot/agents";
|
|
2
5
|
import { SubprocessSandbox } from "../runtime/sandbox/subprocess-sandbox";
|
|
3
6
|
|
|
@@ -7,6 +10,7 @@ export interface PluginSandboxOptions {
|
|
|
7
10
|
importTimeoutMs?: number;
|
|
8
11
|
hookTimeoutMs?: number;
|
|
9
12
|
contributionTimeoutMs?: number;
|
|
13
|
+
onEvent?: (event: { name: string; payload?: unknown }) => void;
|
|
10
14
|
}
|
|
11
15
|
|
|
12
16
|
type AgentExtension = NonNullable<AgentConfig["extensions"]>[number];
|
|
@@ -48,192 +52,87 @@ type SandboxedPluginDescriptor = {
|
|
|
48
52
|
};
|
|
49
53
|
};
|
|
50
54
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
if (typeof plugin.name !== "string" || !plugin.name) {
|
|
82
|
-
throw new Error(\`Invalid plugin name: \${pluginPath}\`);
|
|
83
|
-
}
|
|
84
|
-
if (!plugin.manifest || typeof plugin.manifest !== "object") {
|
|
85
|
-
throw new Error(\`Invalid plugin manifest: \${pluginPath}\`);
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
const pluginId = \`plugin_\${++pluginCounter}\`;
|
|
89
|
-
const contributions = {
|
|
90
|
-
tools: [],
|
|
91
|
-
commands: [],
|
|
92
|
-
shortcuts: [],
|
|
93
|
-
flags: [],
|
|
94
|
-
messageRenderers: [],
|
|
95
|
-
providers: [],
|
|
96
|
-
};
|
|
97
|
-
const handlers = {
|
|
98
|
-
tools: new Map(),
|
|
99
|
-
commands: new Map(),
|
|
100
|
-
messageRenderers: new Map(),
|
|
101
|
-
};
|
|
102
|
-
|
|
103
|
-
const makeId = (prefix) => \`\${pluginId}_\${prefix}_\${Math.random().toString(36).slice(2, 10)}\`;
|
|
104
|
-
const api = {
|
|
105
|
-
registerTool: (tool) => {
|
|
106
|
-
const id = makeId("tool");
|
|
107
|
-
handlers.tools.set(id, tool.execute);
|
|
108
|
-
contributions.tools.push({
|
|
109
|
-
id,
|
|
110
|
-
name: tool.name,
|
|
111
|
-
description: tool.description,
|
|
112
|
-
inputSchema: tool.inputSchema,
|
|
113
|
-
timeoutMs: tool.timeoutMs,
|
|
114
|
-
retryable: tool.retryable,
|
|
115
|
-
});
|
|
116
|
-
},
|
|
117
|
-
registerCommand: (command) => {
|
|
118
|
-
const id = makeId("command");
|
|
119
|
-
if (typeof command.handler === "function") {
|
|
120
|
-
handlers.commands.set(id, command.handler);
|
|
121
|
-
}
|
|
122
|
-
contributions.commands.push({
|
|
123
|
-
id,
|
|
124
|
-
name: command.name,
|
|
125
|
-
description: command.description,
|
|
126
|
-
});
|
|
127
|
-
},
|
|
128
|
-
registerShortcut: (shortcut) => {
|
|
129
|
-
contributions.shortcuts.push({
|
|
130
|
-
id: makeId("shortcut"),
|
|
131
|
-
name: shortcut.name,
|
|
132
|
-
value: shortcut.value,
|
|
133
|
-
description: shortcut.description,
|
|
134
|
-
});
|
|
135
|
-
},
|
|
136
|
-
registerFlag: (flag) => {
|
|
137
|
-
contributions.flags.push({
|
|
138
|
-
id: makeId("flag"),
|
|
139
|
-
name: flag.name,
|
|
140
|
-
description: flag.description,
|
|
141
|
-
defaultValue: flag.defaultValue,
|
|
142
|
-
});
|
|
143
|
-
},
|
|
144
|
-
registerMessageRenderer: (renderer) => {
|
|
145
|
-
const id = makeId("renderer");
|
|
146
|
-
handlers.messageRenderers.set(id, renderer.render);
|
|
147
|
-
contributions.messageRenderers.push({ id, name: renderer.name });
|
|
148
|
-
},
|
|
149
|
-
registerProvider: (provider) => {
|
|
150
|
-
contributions.providers.push({
|
|
151
|
-
id: makeId("provider"),
|
|
152
|
-
name: provider.name,
|
|
153
|
-
description: provider.description,
|
|
154
|
-
metadata: sanitizeObject(provider.metadata),
|
|
155
|
-
});
|
|
156
|
-
},
|
|
157
|
-
};
|
|
158
|
-
|
|
159
|
-
if (typeof plugin.setup === "function") {
|
|
160
|
-
await plugin.setup(api);
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
pluginState.set(pluginId, { plugin, handlers });
|
|
164
|
-
descriptors.push({
|
|
165
|
-
pluginId,
|
|
166
|
-
name: plugin.name,
|
|
167
|
-
manifest: plugin.manifest,
|
|
168
|
-
contributions,
|
|
169
|
-
});
|
|
170
|
-
}
|
|
171
|
-
return descriptors;
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
function getPlugin(pluginId) {
|
|
175
|
-
const state = pluginState.get(pluginId);
|
|
176
|
-
if (!state) {
|
|
177
|
-
throw new Error(\`Unknown sandbox plugin id: \${pluginId}\`);
|
|
178
|
-
}
|
|
179
|
-
return state;
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
async function invokeHook(args) {
|
|
183
|
-
const state = getPlugin(args.pluginId);
|
|
184
|
-
const handler = state.plugin[args.hookName];
|
|
185
|
-
if (typeof handler !== "function") {
|
|
186
|
-
return undefined;
|
|
187
|
-
}
|
|
188
|
-
return await handler(args.payload);
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
async function executeTool(args) {
|
|
192
|
-
const state = getPlugin(args.pluginId);
|
|
193
|
-
const handler = state.handlers.tools.get(args.contributionId);
|
|
194
|
-
if (typeof handler !== "function") {
|
|
195
|
-
throw new Error("Unknown sandbox tool contribution");
|
|
196
|
-
}
|
|
197
|
-
return await handler(args.input, args.context);
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
async function executeCommand(args) {
|
|
201
|
-
const state = getPlugin(args.pluginId);
|
|
202
|
-
const handler = state.handlers.commands.get(args.contributionId);
|
|
203
|
-
if (typeof handler !== "function") {
|
|
204
|
-
return "";
|
|
205
|
-
}
|
|
206
|
-
return await handler(args.input);
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
async function renderMessage(args) {
|
|
210
|
-
const state = getPlugin(args.pluginId);
|
|
211
|
-
const handler = state.handlers.messageRenderers.get(args.contributionId);
|
|
212
|
-
if (typeof handler !== "function") {
|
|
213
|
-
return "";
|
|
214
|
-
}
|
|
215
|
-
return await handler(args.message);
|
|
55
|
+
/**
|
|
56
|
+
* Resolve the bootstrap for the sandbox subprocess.
|
|
57
|
+
*
|
|
58
|
+
* In production (bundled), the compiled `.js` file lives next to this module
|
|
59
|
+
* and can be passed directly as a file to spawn. In development/test
|
|
60
|
+
* (unbundled, where only the `.ts` source exists), we load the TypeScript
|
|
61
|
+
* bootstrap through jiti from an inline script.
|
|
62
|
+
*/
|
|
63
|
+
function resolveBootstrap(): { file: string } | { script: string } {
|
|
64
|
+
const dir = dirname(fileURLToPath(import.meta.url));
|
|
65
|
+
// In dev, the bootstrap sits next to this file in src/agents/.
|
|
66
|
+
// In production, the main bundle is at dist/ but bootstrap is at dist/agents/.
|
|
67
|
+
const candidates = [
|
|
68
|
+
join(dir, "plugin-sandbox-bootstrap.js"),
|
|
69
|
+
join(dir, "agents", "plugin-sandbox-bootstrap.js"),
|
|
70
|
+
];
|
|
71
|
+
for (const candidate of candidates) {
|
|
72
|
+
if (existsSync(candidate)) return { file: candidate };
|
|
73
|
+
}
|
|
74
|
+
const tsPath = join(dir, "plugin-sandbox-bootstrap.ts");
|
|
75
|
+
return {
|
|
76
|
+
script: [
|
|
77
|
+
"const createJiti = require('jiti');",
|
|
78
|
+
`const jiti = createJiti(${JSON.stringify(tsPath)}, { cache: false, requireCache: false, esmResolve: true, interopDefault: false });`,
|
|
79
|
+
`Promise.resolve(jiti.import(${JSON.stringify(tsPath)}, {})).catch((error) => {`,
|
|
80
|
+
" console.error(error);",
|
|
81
|
+
" process.exitCode = 1;",
|
|
82
|
+
"});",
|
|
83
|
+
].join("\n"),
|
|
84
|
+
};
|
|
216
85
|
}
|
|
217
86
|
|
|
218
|
-
const
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
87
|
+
const BOOTSTRAP = resolveBootstrap();
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Map from hook stage names in the manifest to the property name on AgentExtension
|
|
91
|
+
* and the corresponding hook method name inside the sandbox subprocess.
|
|
92
|
+
*/
|
|
93
|
+
const HOOK_BINDINGS: Array<{
|
|
94
|
+
stage: HookStage;
|
|
95
|
+
extensionKey: keyof AgentExtension;
|
|
96
|
+
sandboxHookName: string;
|
|
97
|
+
}> = [
|
|
98
|
+
{ stage: "input", extensionKey: "onInput", sandboxHookName: "onInput" },
|
|
99
|
+
{
|
|
100
|
+
stage: "session_start",
|
|
101
|
+
extensionKey: "onSessionStart",
|
|
102
|
+
sandboxHookName: "onSessionStart",
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
stage: "before_agent_start",
|
|
106
|
+
extensionKey: "onBeforeAgentStart",
|
|
107
|
+
sandboxHookName: "onBeforeAgentStart",
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
stage: "tool_call_before",
|
|
111
|
+
extensionKey: "onToolCall",
|
|
112
|
+
sandboxHookName: "onToolCall",
|
|
113
|
+
},
|
|
114
|
+
{
|
|
115
|
+
stage: "tool_call_after",
|
|
116
|
+
extensionKey: "onToolResult",
|
|
117
|
+
sandboxHookName: "onToolResult",
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
stage: "turn_end",
|
|
121
|
+
extensionKey: "onAgentEnd",
|
|
122
|
+
sandboxHookName: "onAgentEnd",
|
|
123
|
+
},
|
|
124
|
+
{
|
|
125
|
+
stage: "session_shutdown",
|
|
126
|
+
extensionKey: "onSessionShutdown",
|
|
127
|
+
sandboxHookName: "onSessionShutdown",
|
|
128
|
+
},
|
|
129
|
+
{
|
|
130
|
+
stage: "runtime_event",
|
|
131
|
+
extensionKey: "onRuntimeEvent",
|
|
132
|
+
sandboxHookName: "onRuntimeEvent",
|
|
133
|
+
},
|
|
134
|
+
{ stage: "error", extensionKey: "onError", sandboxHookName: "onError" },
|
|
135
|
+
];
|
|
237
136
|
|
|
238
137
|
function hasHookStage(extension: AgentExtension, stage: HookStage): boolean {
|
|
239
138
|
return extension.manifest.hookStages?.includes(stage) === true;
|
|
@@ -254,7 +153,10 @@ export async function loadSandboxedPlugins(
|
|
|
254
153
|
}> {
|
|
255
154
|
const sandbox = new SubprocessSandbox({
|
|
256
155
|
name: "plugin-sandbox",
|
|
257
|
-
|
|
156
|
+
...("file" in BOOTSTRAP
|
|
157
|
+
? { bootstrapFile: BOOTSTRAP.file }
|
|
158
|
+
: { bootstrapScript: BOOTSTRAP.script }),
|
|
159
|
+
onEvent: options.onEvent,
|
|
258
160
|
});
|
|
259
161
|
const importTimeoutMs = withTimeoutFallback(options.importTimeoutMs, 4000);
|
|
260
162
|
const hookTimeoutMs = withTimeoutFallback(options.hookTimeoutMs, 3000);
|
|
@@ -286,177 +188,13 @@ export async function loadSandboxedPlugins(
|
|
|
286
188
|
name: descriptor.name,
|
|
287
189
|
manifest: descriptor.manifest,
|
|
288
190
|
setup: (api: AgentExtensionApi) => {
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
description: toolDescriptor.description ?? "",
|
|
293
|
-
inputSchema: (toolDescriptor.inputSchema ?? {
|
|
294
|
-
type: "object",
|
|
295
|
-
properties: {},
|
|
296
|
-
}) as Tool["inputSchema"],
|
|
297
|
-
timeoutMs: toolDescriptor.timeoutMs,
|
|
298
|
-
retryable: toolDescriptor.retryable,
|
|
299
|
-
execute: async (input: unknown, context: unknown) =>
|
|
300
|
-
await sandbox.call(
|
|
301
|
-
"executeTool",
|
|
302
|
-
{
|
|
303
|
-
pluginId: descriptor.pluginId,
|
|
304
|
-
contributionId: toolDescriptor.id,
|
|
305
|
-
input,
|
|
306
|
-
context,
|
|
307
|
-
},
|
|
308
|
-
{ timeoutMs: contributionTimeoutMs },
|
|
309
|
-
),
|
|
310
|
-
};
|
|
311
|
-
api.registerTool(tool);
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
for (const commandDescriptor of descriptor.contributions.commands) {
|
|
315
|
-
api.registerCommand({
|
|
316
|
-
name: commandDescriptor.name,
|
|
317
|
-
description: commandDescriptor.description,
|
|
318
|
-
handler: async (input: string) =>
|
|
319
|
-
await sandbox.call<string>(
|
|
320
|
-
"executeCommand",
|
|
321
|
-
{
|
|
322
|
-
pluginId: descriptor.pluginId,
|
|
323
|
-
contributionId: commandDescriptor.id,
|
|
324
|
-
input,
|
|
325
|
-
},
|
|
326
|
-
{ timeoutMs: contributionTimeoutMs },
|
|
327
|
-
),
|
|
328
|
-
});
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
for (const shortcutDescriptor of descriptor.contributions.shortcuts) {
|
|
332
|
-
api.registerShortcut({
|
|
333
|
-
name: shortcutDescriptor.name,
|
|
334
|
-
value: shortcutDescriptor.value ?? "",
|
|
335
|
-
description: shortcutDescriptor.description,
|
|
336
|
-
});
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
for (const flagDescriptor of descriptor.contributions.flags) {
|
|
340
|
-
api.registerFlag({
|
|
341
|
-
name: flagDescriptor.name,
|
|
342
|
-
description: flagDescriptor.description,
|
|
343
|
-
defaultValue: flagDescriptor.defaultValue,
|
|
344
|
-
});
|
|
345
|
-
}
|
|
346
|
-
|
|
347
|
-
for (const rendererDescriptor of descriptor.contributions
|
|
348
|
-
.messageRenderers) {
|
|
349
|
-
api.registerMessageRenderer({
|
|
350
|
-
name: rendererDescriptor.name,
|
|
351
|
-
render: () =>
|
|
352
|
-
`[sandbox renderer ${rendererDescriptor.name} requires async bridge]`,
|
|
353
|
-
});
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
for (const providerDescriptor of descriptor.contributions.providers) {
|
|
357
|
-
api.registerProvider({
|
|
358
|
-
name: providerDescriptor.name,
|
|
359
|
-
description: providerDescriptor.description,
|
|
360
|
-
metadata: providerDescriptor.metadata,
|
|
361
|
-
});
|
|
362
|
-
}
|
|
191
|
+
registerTools(api, sandbox, descriptor, contributionTimeoutMs);
|
|
192
|
+
registerCommands(api, sandbox, descriptor, contributionTimeoutMs);
|
|
193
|
+
registerSimpleContributions(api, descriptor);
|
|
363
194
|
},
|
|
364
195
|
};
|
|
365
196
|
|
|
366
|
-
|
|
367
|
-
extension.onInput = async (payload: unknown) =>
|
|
368
|
-
await sandbox.call(
|
|
369
|
-
"invokeHook",
|
|
370
|
-
{ pluginId: descriptor.pluginId, hookName: "onInput", payload },
|
|
371
|
-
{ timeoutMs: hookTimeoutMs },
|
|
372
|
-
);
|
|
373
|
-
}
|
|
374
|
-
if (hasHookStage(extension, "session_start")) {
|
|
375
|
-
extension.onSessionStart = async (payload: unknown) =>
|
|
376
|
-
await sandbox.call(
|
|
377
|
-
"invokeHook",
|
|
378
|
-
{
|
|
379
|
-
pluginId: descriptor.pluginId,
|
|
380
|
-
hookName: "onSessionStart",
|
|
381
|
-
payload,
|
|
382
|
-
},
|
|
383
|
-
{ timeoutMs: hookTimeoutMs },
|
|
384
|
-
);
|
|
385
|
-
}
|
|
386
|
-
if (hasHookStage(extension, "before_agent_start")) {
|
|
387
|
-
extension.onBeforeAgentStart = async (payload: unknown) =>
|
|
388
|
-
await sandbox.call(
|
|
389
|
-
"invokeHook",
|
|
390
|
-
{
|
|
391
|
-
pluginId: descriptor.pluginId,
|
|
392
|
-
hookName: "onBeforeAgentStart",
|
|
393
|
-
payload,
|
|
394
|
-
},
|
|
395
|
-
{ timeoutMs: hookTimeoutMs },
|
|
396
|
-
);
|
|
397
|
-
}
|
|
398
|
-
if (hasHookStage(extension, "tool_call_before")) {
|
|
399
|
-
extension.onToolCall = async (payload: unknown) =>
|
|
400
|
-
await sandbox.call(
|
|
401
|
-
"invokeHook",
|
|
402
|
-
{ pluginId: descriptor.pluginId, hookName: "onToolCall", payload },
|
|
403
|
-
{ timeoutMs: hookTimeoutMs },
|
|
404
|
-
);
|
|
405
|
-
}
|
|
406
|
-
if (hasHookStage(extension, "tool_call_after")) {
|
|
407
|
-
extension.onToolResult = async (payload: unknown) =>
|
|
408
|
-
await sandbox.call(
|
|
409
|
-
"invokeHook",
|
|
410
|
-
{
|
|
411
|
-
pluginId: descriptor.pluginId,
|
|
412
|
-
hookName: "onToolResult",
|
|
413
|
-
payload,
|
|
414
|
-
},
|
|
415
|
-
{ timeoutMs: hookTimeoutMs },
|
|
416
|
-
);
|
|
417
|
-
}
|
|
418
|
-
if (hasHookStage(extension, "turn_end")) {
|
|
419
|
-
extension.onAgentEnd = async (payload: unknown) =>
|
|
420
|
-
await sandbox.call(
|
|
421
|
-
"invokeHook",
|
|
422
|
-
{ pluginId: descriptor.pluginId, hookName: "onAgentEnd", payload },
|
|
423
|
-
{ timeoutMs: hookTimeoutMs },
|
|
424
|
-
);
|
|
425
|
-
}
|
|
426
|
-
if (hasHookStage(extension, "session_shutdown")) {
|
|
427
|
-
extension.onSessionShutdown = async (payload: unknown) =>
|
|
428
|
-
await sandbox.call(
|
|
429
|
-
"invokeHook",
|
|
430
|
-
{
|
|
431
|
-
pluginId: descriptor.pluginId,
|
|
432
|
-
hookName: "onSessionShutdown",
|
|
433
|
-
payload,
|
|
434
|
-
},
|
|
435
|
-
{ timeoutMs: hookTimeoutMs },
|
|
436
|
-
);
|
|
437
|
-
}
|
|
438
|
-
if (hasHookStage(extension, "runtime_event")) {
|
|
439
|
-
extension.onRuntimeEvent = async (payload: unknown) => {
|
|
440
|
-
await sandbox.call(
|
|
441
|
-
"invokeHook",
|
|
442
|
-
{
|
|
443
|
-
pluginId: descriptor.pluginId,
|
|
444
|
-
hookName: "onRuntimeEvent",
|
|
445
|
-
payload,
|
|
446
|
-
},
|
|
447
|
-
{ timeoutMs: hookTimeoutMs },
|
|
448
|
-
);
|
|
449
|
-
};
|
|
450
|
-
}
|
|
451
|
-
if (hasHookStage(extension, "error")) {
|
|
452
|
-
extension.onError = async (payload: unknown) => {
|
|
453
|
-
await sandbox.call(
|
|
454
|
-
"invokeHook",
|
|
455
|
-
{ pluginId: descriptor.pluginId, hookName: "onError", payload },
|
|
456
|
-
{ timeoutMs: hookTimeoutMs },
|
|
457
|
-
);
|
|
458
|
-
};
|
|
459
|
-
}
|
|
197
|
+
bindHooks(extension, sandbox, descriptor.pluginId, hookTimeoutMs);
|
|
460
198
|
|
|
461
199
|
return extension;
|
|
462
200
|
},
|
|
@@ -469,3 +207,135 @@ export async function loadSandboxedPlugins(
|
|
|
469
207
|
},
|
|
470
208
|
};
|
|
471
209
|
}
|
|
210
|
+
|
|
211
|
+
// ---------------------------------------------------------------------------
|
|
212
|
+
// Contribution registration helpers
|
|
213
|
+
// ---------------------------------------------------------------------------
|
|
214
|
+
|
|
215
|
+
function registerTools(
|
|
216
|
+
api: AgentExtensionApi,
|
|
217
|
+
sandbox: SubprocessSandbox,
|
|
218
|
+
descriptor: SandboxedPluginDescriptor,
|
|
219
|
+
timeoutMs: number,
|
|
220
|
+
): void {
|
|
221
|
+
for (const td of descriptor.contributions.tools) {
|
|
222
|
+
const tool: Tool = {
|
|
223
|
+
name: td.name,
|
|
224
|
+
description: td.description ?? "",
|
|
225
|
+
inputSchema: (td.inputSchema ?? {
|
|
226
|
+
type: "object",
|
|
227
|
+
properties: {},
|
|
228
|
+
}) as Tool["inputSchema"],
|
|
229
|
+
timeoutMs: td.timeoutMs,
|
|
230
|
+
retryable: td.retryable,
|
|
231
|
+
execute: async (input: unknown, context: unknown) =>
|
|
232
|
+
await sandbox.call(
|
|
233
|
+
"executeTool",
|
|
234
|
+
{
|
|
235
|
+
pluginId: descriptor.pluginId,
|
|
236
|
+
contributionId: td.id,
|
|
237
|
+
input,
|
|
238
|
+
context,
|
|
239
|
+
},
|
|
240
|
+
{ timeoutMs },
|
|
241
|
+
),
|
|
242
|
+
};
|
|
243
|
+
api.registerTool(tool);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function registerCommands(
|
|
248
|
+
api: AgentExtensionApi,
|
|
249
|
+
sandbox: SubprocessSandbox,
|
|
250
|
+
descriptor: SandboxedPluginDescriptor,
|
|
251
|
+
timeoutMs: number,
|
|
252
|
+
): void {
|
|
253
|
+
for (const cd of descriptor.contributions.commands) {
|
|
254
|
+
api.registerCommand({
|
|
255
|
+
name: cd.name,
|
|
256
|
+
description: cd.description,
|
|
257
|
+
handler: async (input: string) =>
|
|
258
|
+
await sandbox.call<string>(
|
|
259
|
+
"executeCommand",
|
|
260
|
+
{
|
|
261
|
+
pluginId: descriptor.pluginId,
|
|
262
|
+
contributionId: cd.id,
|
|
263
|
+
input,
|
|
264
|
+
},
|
|
265
|
+
{ timeoutMs },
|
|
266
|
+
),
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function registerSimpleContributions(
|
|
272
|
+
api: AgentExtensionApi,
|
|
273
|
+
descriptor: SandboxedPluginDescriptor,
|
|
274
|
+
): void {
|
|
275
|
+
for (const sd of descriptor.contributions.shortcuts) {
|
|
276
|
+
api.registerShortcut({
|
|
277
|
+
name: sd.name,
|
|
278
|
+
value: sd.value ?? "",
|
|
279
|
+
description: sd.description,
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
for (const fd of descriptor.contributions.flags) {
|
|
284
|
+
api.registerFlag({
|
|
285
|
+
name: fd.name,
|
|
286
|
+
description: fd.description,
|
|
287
|
+
defaultValue: fd.defaultValue,
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
for (const rd of descriptor.contributions.messageRenderers) {
|
|
292
|
+
api.registerMessageRenderer({
|
|
293
|
+
name: rd.name,
|
|
294
|
+
render: () => `[sandbox renderer ${rd.name} requires async bridge]`,
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
for (const pd of descriptor.contributions.providers) {
|
|
299
|
+
api.registerProvider({
|
|
300
|
+
name: pd.name,
|
|
301
|
+
description: pd.description,
|
|
302
|
+
metadata: pd.metadata,
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
function makeHookHandler(
|
|
308
|
+
sandbox: SubprocessSandbox,
|
|
309
|
+
pluginId: string,
|
|
310
|
+
hookName: string,
|
|
311
|
+
timeoutMs: number,
|
|
312
|
+
): (payload: unknown) => Promise<unknown> {
|
|
313
|
+
return async (payload: unknown) =>
|
|
314
|
+
await sandbox.call(
|
|
315
|
+
"invokeHook",
|
|
316
|
+
{ pluginId, hookName, payload },
|
|
317
|
+
{ timeoutMs },
|
|
318
|
+
);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function bindHooks(
|
|
322
|
+
extension: AgentExtension,
|
|
323
|
+
sandbox: SubprocessSandbox,
|
|
324
|
+
pluginId: string,
|
|
325
|
+
hookTimeoutMs: number,
|
|
326
|
+
): void {
|
|
327
|
+
for (const { stage, extensionKey, sandboxHookName } of HOOK_BINDINGS) {
|
|
328
|
+
if (hasHookStage(extension, stage)) {
|
|
329
|
+
const handler = makeHookHandler(
|
|
330
|
+
sandbox,
|
|
331
|
+
pluginId,
|
|
332
|
+
sandboxHookName,
|
|
333
|
+
hookTimeoutMs,
|
|
334
|
+
);
|
|
335
|
+
// Each hook property on AgentExtension accepts (payload: unknown) => Promise<unknown>.
|
|
336
|
+
// TypeScript cannot narrow a union of optional callback keys via computed access,
|
|
337
|
+
// so we use Object.assign to set the property safely.
|
|
338
|
+
Object.assign(extension, { [extensionKey]: handler });
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
}
|
package/src/index.node.ts
CHANGED
|
@@ -207,6 +207,10 @@ export {
|
|
|
207
207
|
SqliteRpcSessionBackend,
|
|
208
208
|
type SqliteRpcSessionBackendOptions,
|
|
209
209
|
} from "./session/sqlite-rpc-session-backend";
|
|
210
|
+
export {
|
|
211
|
+
accumulateUsageTotals,
|
|
212
|
+
createInitialAccumulatedUsage,
|
|
213
|
+
} from "./session/utils/usage";
|
|
210
214
|
export type {
|
|
211
215
|
WorkspaceManager,
|
|
212
216
|
WorkspaceManagerEvent,
|
|
@@ -57,7 +57,7 @@ describe("createHookConfigFileHooks", () => {
|
|
|
57
57
|
it("executes extensionless legacy hook files via bash fallback", async () => {
|
|
58
58
|
const { workspace } = await createWorkspaceWithHook(
|
|
59
59
|
"PreToolUse",
|
|
60
|
-
'echo \'HOOK_CONTROL\t{"cancel":true,"context":"legacy-ok"}\'\n',
|
|
60
|
+
'echo \'HOOK_CONTROL\t{"cancel":true,"context":"legacy-ok"}\'\nexit 0\n',
|
|
61
61
|
);
|
|
62
62
|
try {
|
|
63
63
|
const hooks = createHookConfigFileHooks({
|
|
@@ -215,13 +215,17 @@ async function writeToChildStdin(
|
|
|
215
215
|
|
|
216
216
|
await new Promise<void>((resolve, reject) => {
|
|
217
217
|
let settled = false;
|
|
218
|
+
const cleanup = () => {
|
|
219
|
+
stdin.off("error", onError);
|
|
220
|
+
stdin.off("finish", onFinish);
|
|
221
|
+
child.off("close", onChildClose);
|
|
222
|
+
};
|
|
218
223
|
const finish = (error?: Error | null) => {
|
|
219
224
|
if (settled) {
|
|
220
225
|
return;
|
|
221
226
|
}
|
|
222
227
|
settled = true;
|
|
223
|
-
|
|
224
|
-
stdin.off("close", onClose);
|
|
228
|
+
cleanup();
|
|
225
229
|
if (error) {
|
|
226
230
|
const code = (error as Error & { code?: string }).code;
|
|
227
231
|
if (code === "EPIPE" || code === "ERR_STREAM_DESTROYED") {
|
|
@@ -234,10 +238,16 @@ async function writeToChildStdin(
|
|
|
234
238
|
resolve();
|
|
235
239
|
};
|
|
236
240
|
const onError = (error: Error) => finish(error);
|
|
237
|
-
const
|
|
241
|
+
const onFinish = () => finish();
|
|
242
|
+
const onChildClose = () => finish();
|
|
238
243
|
stdin.on("error", onError);
|
|
239
|
-
stdin.once("
|
|
240
|
-
|
|
244
|
+
stdin.once("finish", onFinish);
|
|
245
|
+
child.once("close", onChildClose);
|
|
246
|
+
try {
|
|
247
|
+
stdin.end(body);
|
|
248
|
+
} catch (error) {
|
|
249
|
+
finish(error as Error);
|
|
250
|
+
}
|
|
241
251
|
});
|
|
242
252
|
}
|
|
243
253
|
|
|
@@ -270,10 +280,10 @@ async function runHookCommand(
|
|
|
270
280
|
});
|
|
271
281
|
|
|
272
282
|
const body = JSON.stringify(payload);
|
|
283
|
+
await Promise.race([spawned, childError]);
|
|
273
284
|
await writeToChildStdin(child, body);
|
|
274
285
|
|
|
275
286
|
if (options.detached) {
|
|
276
|
-
await Promise.race([spawned, childError]);
|
|
277
287
|
child.unref();
|
|
278
288
|
return;
|
|
279
289
|
}
|