UNPKG

@sema-agent/core

Version:

Stateless, task-oriented AI agent core

266 lines (265 loc) • 9.9 kB
import { execFile } from "node:child_process"; import { randomBytes } from "node:crypto"; import { lstatSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs"; import { dirname, isAbsolute, resolve, sep } from "node:path"; import { announceSecretEnvScrub, inheritEnvPolicyProblem, inheritedHostEnv } from "./secret-env.js"; import { physicalPathOf } from "./physical-path.js"; const DEFAULT_GATE_TIMEOUT_MS = 60_000; const GATE_MAX_BUFFER = 16 * 1024 * 1024; const OUTPUT_TAIL_CHARS = 1500; const REGEX_SCAN_CAP = 2 * 1024 * 1024; function execGate(gate, cwd, env, signal) { return new Promise((resolvePromise) => { const settle = (err, stdout, stderr) => { const output = (stdout ?? "") + (stderr ?? ""); if (!err) return resolvePromise({ exitCode: 0, output }); const e = err; if (typeof e.code === "number") return resolvePromise({ exitCode: e.code, output }); if (e.killed || e.signal) { return resolvePromise({ exitCode: null, output, infraError: `timeout/killed (${e.signal ?? "timeout"})` }); } return resolvePromise({ exitCode: null, output, infraError: String(e.code ?? e.message ?? "spawn error") }); }; try { execFile(gate.cmd[0], gate.cmd.slice(1), { cwd, env, timeout: gate.timeoutMs ?? DEFAULT_GATE_TIMEOUT_MS, maxBuffer: GATE_MAX_BUFFER, encoding: "utf8", signal }, settle); } catch (e) { settle(e instanceof Error ? e : new Error(String(e))); } }); } function safeTest(re, output) { try { return new RegExp(re).test(output); } catch { return undefined; } } function firstCount(re, output) { let m; try { m = new RegExp(re).exec(output); } catch { return undefined; } if (!m) return undefined; const n = Number(m[1] ?? m[0]); return Number.isFinite(n) ? n : undefined; } function evaluateGate(gate, exec, baseline) { const base = { name: gate.name, exitCode: exec.exitCode, outputTail: exec.output.slice(-OUTPUT_TAIL_CHARS), }; if (exec.infraError) return { ...base, verdict: "error", reason: exec.infraError }; const green = gate.green ?? {}; const scanned = exec.output.length > REGEX_SCAN_CAP ? `${exec.output.slice(0, REGEX_SCAN_CAP / 2)}\n…[${exec.output.length - REGEX_SCAN_CAP} bytes elided]…\n${exec.output.slice(-REGEX_SCAN_CAP / 2)}` : exec.output; const green_output = scanned; let pass; let fail; if (green.parse) { pass = firstCount(green.parse.passRe, green_output); fail = firstCount(green.parse.failRe, green_output); if (pass === undefined || fail === undefined) { return { ...base, verdict: "error", reason: "parse: pass/fail counts not extractable from output" }; } } const failures = []; const expectedExit = green.exitCode ?? (green.parse ? undefined : 0); if (expectedExit !== undefined && exec.exitCode !== expectedExit) { failures.push(`exitCode ${exec.exitCode} !== ${expectedExit}`); } if (green.mustMatch !== undefined) { const m = safeTest(green.mustMatch, green_output); if (m === undefined) return { ...base, pass, fail, verdict: "error", reason: `mustMatch is not a valid regex: /${green.mustMatch}/` }; if (!m) failures.push(`mustMatch /${green.mustMatch}/ not found`); } if (green.mustNotMatch !== undefined) { const m = safeTest(green.mustNotMatch, green_output); if (m === undefined) return { ...base, pass, fail, verdict: "error", reason: `mustNotMatch is not a valid regex: /${green.mustNotMatch}/` }; if (m) failures.push(`mustNotMatch /${green.mustNotMatch}/ matched`); } if (green.parse) { if (fail !== 0) failures.push(`fail count ${fail} !== 0`); let minPass; if (green.minPass === "baseline") { if (baseline === undefined) { minPass = undefined; } else { const b = baseline[gate.name]; if (!b) { return { ...base, pass, fail, verdict: "error", reason: `minPass:"baseline" but no baseline captured for gate "${gate.name}"` }; } minPass = b.pass + b.fail; } } else { minPass = green.minPass ?? 1; } if (minPass !== undefined && (pass ?? 0) < minPass) failures.push(`pass count ${pass} < minPass ${minPass}`); } if (failures.length > 0) return { ...base, pass, fail, verdict: "red", reason: failures.join("; ") }; return { ...base, pass, fail, verdict: "green" }; } export async function runOracle(gates, cwdRoot, opts) { const root = resolve(cwdRoot); const results = []; const err = (gate, reason) => ({ name: gate.name, verdict: "error", exitCode: null, reason, outputTail: "", }); for (const gate of gates) { if (!Array.isArray(gate.cmd) || gate.cmd.length === 0 || typeof gate.cmd[0] !== "string" || gate.cmd[0].length === 0) { results.push(err(gate, "gate cmd is empty — no executable to run")); continue; } const envProblem = inheritEnvPolicyProblem(gate.inheritEnv); if (envProblem !== undefined) { results.push(err(gate, `gate ${envProblem}`)); continue; } let cwd = root; if (gate.cwd !== undefined) { const candidate = resolve(root, gate.cwd); if (candidate !== root && !candidate.startsWith(root + sep)) { results.push(err(gate, `gate cwd escapes the spec root: ${gate.cwd}`)); continue; } try { const physRoot = physicalPathOf(root); const phys = physicalPathOf(candidate); if (phys !== physRoot && !phys.startsWith(physRoot + sep)) { results.push(err(gate, `gate cwd physically escapes the spec root (symlink): ${gate.cwd}`)); continue; } } catch { } cwd = candidate; } const withheld = []; const env = inheritedHostEnv(gate.inheritEnv, withheld); announceSecretEnvScrub(opts?.onNotice, withheld); const exec = await execGate(gate, cwd, env, opts?.signal); results.push(evaluateGate(gate, exec, opts?.baseline)); } const verdict = results.some((r) => r.verdict === "error") ? "error" : results.some((r) => r.verdict === "red") ? "red" : "green"; return { verdict, gates: results }; } export function captureBaseline(report) { const baseline = {}; for (const g of report.gates) { if (g.pass !== undefined && g.fail !== undefined) baseline[g.name] = { pass: g.pass, fail: g.fail }; } return baseline; } export function specError(code, message) { const e = new Error(message); e.code = code; return e; } export function resolveFrozenPaths(rootDir, frozenPaths) { const root = resolve(rootDir); return frozenPaths.map((p) => { if (isAbsolute(p) || p.split(/[\\/]/).includes("..")) { throw specError("spec.frozen_path_invalid", `frozenPaths entries must be relative and ..-free: ${p}`); } const abs = resolve(root, p); if (abs !== root && !abs.startsWith(root + sep)) { throw specError("spec.frozen_path_invalid", `frozenPaths entry escapes the root after resolution: ${p}`); } return abs; }); } export function snapshotFrozenPaths(resolvedPaths) { const snap = new Map(); for (const p of resolvedPaths) { try { if (!lstatSync(p).isFile()) { throw specError("spec.frozen_path_invalid", `frozen path must be a regular file (no symlinks/dirs): ${p}`); } snap.set(p, readFileSync(p)); } catch (e) { if (e.code === "spec.frozen_path_invalid") throw e; throw specError("spec.frozen_missing", `frozen path does not exist or is unreadable: ${p}`); } } return snap; } export function restoreFrozenPaths(snapshot, rootDir) { const physRoot = rootDir !== undefined ? safeRealpath(resolve(rootDir)) : undefined; let restored = 0; for (const [p, bytes] of snapshot) { let isRegular = false; let current; try { isRegular = lstatSync(p).isFile(); current = isRegular ? readFileSync(p) : undefined; } catch { current = undefined; } if (isRegular && current !== undefined && current.equals(bytes)) continue; try { if (physRoot !== undefined) { const physParent = safeRealpath(dirname(p)); if (physParent === undefined || (physParent !== physRoot && !physParent.startsWith(physRoot + sep))) { continue; } } if (!isRegular) rmSync(p, { recursive: true, force: true }); restoreOneAtomic(p, bytes); restored++; } catch { } } return restored; } function restoreOneAtomic(p, bytes) { const tmp = `${p}.sema-restore-${randomBytes(9).toString("hex")}`; try { writeFileSync(tmp, bytes, { flag: "wx" }); renameSync(tmp, p); } finally { rmSync(tmp, { force: true }); } } function safeRealpath(p) { try { return physicalPathOf(p); } catch { return undefined; } }