feat(tools): propagate timeout abort signals to tool execution
This commit is contained in:
+24
-3
@@ -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 });
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user