feat(tools): propagate timeout abort signals to tool execution

This commit is contained in:
William Valentin
2026-02-15 22:05:43 -08:00
parent 0220ec10dd
commit 2cdfb66071
10 changed files with 113 additions and 18 deletions
+24 -3
View File
@@ -13,6 +13,7 @@ export interface DockerSandboxConfig {
export interface ExecOptions {
cwd?: string;
timeout?: number;
signal?: AbortSignal;
}
export interface ExecResult {
@@ -76,7 +77,7 @@ export class DockerSandbox {
args.push(this._containerId, 'bash', '-c', command);
const timeout = opts?.timeout ?? this.config.timeoutSeconds * 1000;
return this.dockerCmd(args, timeout);
return this.dockerCmd(args, timeout, opts?.signal);
}
/** Force-remove the container. */
@@ -109,15 +110,35 @@ export class DockerSandbox {
}
/** Run a docker CLI command. */
private dockerCmd(args: string[], timeout = 30_000): Promise<ExecResult> {
private dockerCmd(args: string[], timeout = 30_000, signal?: AbortSignal): Promise<ExecResult> {
return new Promise((resolve, reject) => {
execFile('docker', args, { timeout, maxBuffer: 1024 * 1024 }, (error, stdout, stderr) => {
let settled = false;
const child = execFile('docker', args, { timeout, maxBuffer: 1024 * 1024 }, (error, stdout, stderr) => {
if (signal) {
signal.removeEventListener('abort', onAbort);
}
if (settled) {return;}
settled = true;
if (error) {
reject(error);
return;
}
resolve({ stdout, stderr });
});
const onAbort = () => {
if (settled) {return;}
settled = true;
child.kill('SIGTERM');
reject(new Error('Sandbox command aborted'));
};
if (signal) {
if (signal.aborted) {
onAbort();
} else {
signal.addEventListener('abort', onAbort, { once: true });
}
}
});
}
}
+5 -4
View File
@@ -1,4 +1,4 @@
import type { Tool, ToolResult } from '../tools/types.js';
import type { Tool, ToolResult, ToolExecutionContext } from '../tools/types.js';
import type { DockerSandbox } from './docker.js';
interface ShellExecArgs {
@@ -29,7 +29,7 @@ export function createSandboxedShellTool(sandbox: DockerSandbox): Tool {
},
required: ['command'],
},
execute: async (rawArgs: unknown): Promise<ToolResult> => {
execute: async (rawArgs: unknown, context?: ToolExecutionContext): Promise<ToolResult> => {
const args = rawArgs as ShellExecArgs;
const timeout = args.timeout ?? 30_000;
@@ -37,6 +37,7 @@ export function createSandboxedShellTool(sandbox: DockerSandbox): Tool {
const result = await sandbox.exec(args.command, {
cwd: args.cwd,
timeout,
signal: context?.signal,
});
const output = result.stdout + (result.stderr ? `\nstderr: ${result.stderr}` : '');
@@ -68,12 +69,12 @@ export function createSandboxedProcessStartTool(sandbox: DockerSandbox): Tool {
},
required: ['command'],
},
execute: async (rawArgs: unknown): Promise<ToolResult> => {
execute: async (rawArgs: unknown, context?: ToolExecutionContext): Promise<ToolResult> => {
const args = rawArgs as ProcessStartArgs;
try {
const wrappedCmd = `nohup bash -c '${args.command.replace(/'/g, "'\\''")}' > /tmp/proc.log 2>&1 & echo $!`;
const result = await sandbox.exec(wrappedCmd, { cwd: args.cwd });
const result = await sandbox.exec(wrappedCmd, { cwd: args.cwd, signal: context?.signal });
const pid = result.stdout.trim();
return {