From 7840fdbada9bb4b4ea397f8831cb69efe96a4387 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sat, 30 May 2026 02:40:32 -0400 Subject: [PATCH] fix(agent-core): cap shell exec timeouts --- .../agent-core/src/harness/env/nodejs.test.ts | 21 +++++++++++++++ packages/agent-core/src/harness/env/nodejs.ts | 26 ++++++++++++++++--- 2 files changed, 43 insertions(+), 4 deletions(-) create mode 100644 packages/agent-core/src/harness/env/nodejs.test.ts diff --git a/packages/agent-core/src/harness/env/nodejs.test.ts b/packages/agent-core/src/harness/env/nodejs.test.ts new file mode 100644 index 00000000000..919f1615de5 --- /dev/null +++ b/packages/agent-core/src/harness/env/nodejs.test.ts @@ -0,0 +1,21 @@ +import { describe, expect, it } from "vitest"; +import { resolveExecTimeoutMs } from "./nodejs.js"; + +describe("NodeExecutionEnv timeout helpers", () => { + it("converts positive timeout seconds to milliseconds", () => { + expect(resolveExecTimeoutMs(1)).toBe(1_000); + expect(resolveExecTimeoutMs(1.5)).toBe(1_500); + expect(resolveExecTimeoutMs(0.0005)).toBe(1); + }); + + it("caps oversized timeout seconds to a timer-safe delay", () => { + expect(resolveExecTimeoutMs(Number.MAX_SAFE_INTEGER)).toBe(2_147_000_000); + }); + + it("ignores absent, invalid, or non-positive timeout seconds", () => { + expect(resolveExecTimeoutMs(undefined)).toBeUndefined(); + expect(resolveExecTimeoutMs(Number.NaN)).toBeUndefined(); + expect(resolveExecTimeoutMs(0)).toBeUndefined(); + expect(resolveExecTimeoutMs(-1)).toBeUndefined(); + }); +}); diff --git a/packages/agent-core/src/harness/env/nodejs.ts b/packages/agent-core/src/harness/env/nodejs.ts index a112f4ac700..366d22b8fd3 100644 --- a/packages/agent-core/src/harness/env/nodejs.ts +++ b/packages/agent-core/src/harness/env/nodejs.ts @@ -29,10 +29,27 @@ import { } from "../types.js"; import { killProcessTree } from "./kill-tree.js"; +const MAX_TIMER_TIMEOUT_MS = 2_147_000_000; + function resolvePath(cwd: string, path: string): string { return isAbsolute(path) ? path : resolve(cwd, path); } +export function resolveExecTimeoutMs(timeoutSeconds: unknown): number | undefined { + if ( + typeof timeoutSeconds !== "number" || + !Number.isFinite(timeoutSeconds) || + timeoutSeconds <= 0 + ) { + return undefined; + } + const milliseconds = Math.floor(timeoutSeconds * 1000); + if (!Number.isFinite(milliseconds) || milliseconds <= 0) { + return 1; + } + return Math.min(milliseconds, MAX_TIMER_TIMEOUT_MS); +} + function fileKindFromStats(stats: { isFile(): boolean; isDirectory(): boolean; @@ -309,15 +326,16 @@ export class NodeExecutionEnv implements ExecutionEnv { return; } + const timeoutMs = resolveExecTimeoutMs(options?.timeout); timeoutId = - typeof options?.timeout === "number" - ? setTimeout(() => { + timeoutMs === undefined + ? undefined + : setTimeout(() => { timedOut = true; if (child?.pid) { killProcessTree(child.pid, { force: true }); } - }, options.timeout * 1000) - : undefined; + }, timeoutMs); if (options?.abortSignal) { if (options.abortSignal.aborted) {