@sema-agent/core
Version:
Stateless, task-oriented AI agent core
266 lines (265 loc) • 9.9 kB
JavaScript
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;
}
}