blob: c49be73064194cffedac5dc03fbf75c58dffd267 [file]
import { type ChildProcess, spawn } from 'node:child_process';
/**
* Kill the verification command and any children it spawned. The child is
* a process-group leader (spawned detached), so a negative pid signals the
* whole group. Windows has no process groups — fall back to the shell.
*/
function killTree(child: ChildProcess): void {
try {
if (child.pid !== undefined) process.kill(-child.pid, 'SIGKILL');
else child.kill('SIGKILL');
} catch {
child.kill('SIGKILL');
}
}
export interface EvaluationResult {
/** Exit code 0 and not timed out. */
passed: boolean;
exitCode: number | null;
stdout: string;
stderr: string;
timedOut: boolean;
}
/** Cap captured output so a runaway command can't exhaust memory. */
const MAX_OUTPUT_BYTES = 1024 * 1024;
const DEFAULT_TIMEOUT_MS = 120_000;
/**
* Run a Task's verification command in the throwaway workspace AFTER the
* agent has finished. The evaluator runs separately from the agent so a
* config under test cannot tamper with its own grading.
*
* `shell: true` so ordinary commands ("npm test", "pytest -q") work; the
* lab is a trusted batch context running known fixtures, distinct from
* Maka's interactive permission model.
*/
export function runVerification(
command: string,
cwd: string,
timeoutMs: number = DEFAULT_TIMEOUT_MS,
env?: Record<string, string>,
): Promise<EvaluationResult> {
return new Promise((resolve) => {
// detached: the shell becomes its own process-group leader so a
// timeout can kill the WHOLE tree (backgrounded grandchildren
// included), not just the shell.
const child = spawn(command, {
cwd,
shell: true,
detached: true,
env: env ? { ...process.env, ...env } : undefined,
});
let stdout = '';
let stderr = '';
let timedOut = false;
const cap = (buf: string, chunk: Buffer): string =>
buf.length >= MAX_OUTPUT_BYTES
? buf
: (buf + chunk.toString('utf8')).slice(0, MAX_OUTPUT_BYTES);
const timer = setTimeout(() => {
timedOut = true;
killTree(child);
}, timeoutMs);
child.stdout?.on('data', (chunk: Buffer) => {
stdout = cap(stdout, chunk);
});
child.stderr?.on('data', (chunk: Buffer) => {
stderr = cap(stderr, chunk);
});
child.on('error', (err) => {
clearTimeout(timer);
resolve({
passed: false,
exitCode: null,
stdout,
stderr: `${stderr}${String(err)}`,
timedOut,
});
});
child.on('close', (code) => {
clearTimeout(timer);
resolve({ passed: code === 0 && !timedOut, exitCode: code, stdout, stderr, timedOut });
});
});
}