Compare commits

...

1 Commits

Author SHA1 Message Date
Saoud Rizwan 0354f4a665 fix(core): honor sandbox contribution timeouts 2026-05-19 16:08:07 -07:00
2 changed files with 67 additions and 6 deletions
@@ -617,6 +617,64 @@ describe("plugin-sandbox", () => {
expect(result).toEqual({ echoed: "ok" });
});
it("uses tool timeout when executing sandbox tool contributions", async () => {
const pluginPath = join(dir, "plugin-tool-timeout.mjs");
await writeFile(
pluginPath,
[
"export default {",
" name: 'sandbox-tool-timeout',",
" manifest: { capabilities: ['tools'] },",
" setup(api) {",
" api.registerTool({",
" name: 'slow_tool',",
" description: 'slow tool',",
" inputSchema: { type: 'object', properties: {} },",
" timeoutMs: 25,",
" execute: async () => {",
" await new Promise((resolve) => setTimeout(resolve, 250));",
" return { ok: true };",
" },",
" });",
" },",
"};",
].join("\n"),
"utf8",
);
const sandboxed = await loadSandboxedPlugins({
pluginPaths: [pluginPath],
contributionTimeoutMs: 1_000,
});
try {
const extension = sandboxed.extensions?.find(
(entry) => entry.name === "sandbox-tool-timeout",
);
if (!extension) {
throw new Error("Expected sandbox-tool-timeout extension to load");
}
const { tools, api } = createApiCapture();
await extension.setup?.(api, {});
const tool = tools.find((entry) => entry.name === "slow_tool");
if (!tool) {
throw new Error("Expected slow_tool to be registered");
}
const startedAt = Date.now();
await expect(
tool.execute({}, {
agentId: "agent-1",
conversationId: "conv-1",
iteration: 1,
} as AgentToolContext),
).rejects.toThrow("timed out after 25ms: executeTool");
expect(Date.now() - startedAt).toBeLessThan(800);
} finally {
await sandboxed.shutdown();
}
});
it("continues loading remaining sandbox plugins when one setup fails", async () => {
const sandboxed = await loadSandboxedPlugins({
pluginPaths: [
@@ -327,6 +327,7 @@ function registerTools(
reinitialize: () => Promise<void>,
): void {
for (const td of descriptor.contributions?.tools ?? []) {
const toolTimeoutMs = withTimeoutFallback(td.timeoutMs, timeoutMs);
const tool: AgentTool = {
name: td.name,
description: td.description ?? "",
@@ -346,7 +347,7 @@ function registerTools(
input,
context,
},
{ timeoutMs },
{ timeoutMs: toolTimeoutMs },
);
} catch (error) {
if (!isUnknownPluginIdError(error)) {
@@ -361,7 +362,7 @@ function registerTools(
input,
context,
},
{ timeoutMs },
{ timeoutMs: toolTimeoutMs },
);
}
},
@@ -378,6 +379,7 @@ function registerCommands(
reinitialize: () => Promise<void>,
): void {
for (const cd of descriptor.contributions?.commands ?? []) {
const commandTimeoutMs = withTimeoutFallback(cd.timeoutMs, timeoutMs);
api.registerCommand({
name: cd.name,
description: cd.description,
@@ -390,7 +392,7 @@ function registerCommands(
contributionId: cd.id,
input,
},
{ timeoutMs },
{ timeoutMs: commandTimeoutMs },
);
} catch (error) {
if (!isUnknownPluginIdError(error)) {
@@ -404,7 +406,7 @@ function registerCommands(
contributionId: cd.id,
input,
},
{ timeoutMs },
{ timeoutMs: commandTimeoutMs },
);
}
},
@@ -446,6 +448,7 @@ function registerMessageBuilders(
reinitialize: () => Promise<void>,
): void {
for (const bd of descriptor.contributions?.messageBuilders ?? []) {
const builderTimeoutMs = withTimeoutFallback(bd.timeoutMs, timeoutMs);
api.registerMessageBuilder({
name: bd.name,
async build(messages) {
@@ -457,7 +460,7 @@ function registerMessageBuilders(
contributionId: bd.id,
messages,
},
{ timeoutMs },
{ timeoutMs: builderTimeoutMs },
);
return isMessageArray(result) ? result : messages;
} catch (error) {
@@ -472,7 +475,7 @@ function registerMessageBuilders(
contributionId: bd.id,
messages,
},
{ timeoutMs },
{ timeoutMs: builderTimeoutMs },
);
return isMessageArray(result) ? result : messages;
}