firegoat 0.26.0 → 0.27.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +227 -37
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -6947,7 +6947,7 @@ var require_public_api = __commonJS(function(exports) {
|
|
|
6947
6947
|
// package.json
|
|
6948
6948
|
var package_default = {
|
|
6949
6949
|
name: "firegoat",
|
|
6950
|
-
version: "0.
|
|
6950
|
+
version: "0.27.0",
|
|
6951
6951
|
repository: {
|
|
6952
6952
|
type: "git",
|
|
6953
6953
|
url: "git+https://github.com/firegoat-ai/firegoat-cli.git"
|
|
@@ -7157,7 +7157,7 @@ function read(stdout, exited, timeoutMs) {
|
|
|
7157
7157
|
reportedModel,
|
|
7158
7158
|
version,
|
|
7159
7159
|
ok: false,
|
|
7160
|
-
reason: `claude
|
|
7160
|
+
reason: `claude reported an error${result.subtype === undefined || result.subtype === "success" ? "" : ` (${result.subtype})`}${result.result ? `: ${result.result.slice(0, 300)}` : ""}`,
|
|
7161
7161
|
sessionId
|
|
7162
7162
|
};
|
|
7163
7163
|
return { ...trace, reportedModel, version, ok: true, sessionId };
|
|
@@ -7231,6 +7231,7 @@ var claude = {
|
|
|
7231
7231
|
}),
|
|
7232
7232
|
authLabel: () => process.env.ANTHROPIC_API_KEY ? "ANTHROPIC_API_KEY (claude bills the API key; unset it to use your claude login)" : "your claude login",
|
|
7233
7233
|
checkLogin,
|
|
7234
|
+
isFinal: (line) => parse(line)?.type === "result",
|
|
7234
7235
|
progress,
|
|
7235
7236
|
read,
|
|
7236
7237
|
sessionsDir: (configDir) => join(configDir, "projects"),
|
|
@@ -7549,6 +7550,10 @@ var codex = {
|
|
|
7549
7550
|
}),
|
|
7550
7551
|
authLabel: () => `your codex login (${join3(userCodexHome(), "auth.json")})${process.env.CODEX_API_KEY ? "; CODEX_API_KEY is set and passes through" : ""}`,
|
|
7551
7552
|
checkLogin: checkLogin2,
|
|
7553
|
+
isFinal: (line) => {
|
|
7554
|
+
const type = parse2(line)?.type;
|
|
7555
|
+
return type === "turn.completed" || type === "turn.failed";
|
|
7556
|
+
},
|
|
7552
7557
|
progress: progress2,
|
|
7553
7558
|
read: read2,
|
|
7554
7559
|
sessionsDir: (configDir) => join3(configDir, "sessions"),
|
|
@@ -7653,7 +7658,7 @@ function steps3(events) {
|
|
|
7653
7658
|
function resultError(result) {
|
|
7654
7659
|
const errors = Array.isArray(result.errors) ? result.errors.filter((x) => typeof x === "string").join("; ") : "";
|
|
7655
7660
|
const said = errors || result.result || "";
|
|
7656
|
-
return `grok
|
|
7661
|
+
return `grok reported an error${result.subtype === undefined || result.subtype === "success" ? "" : ` (${result.subtype})`}${said ? `: ${said.slice(0, 300)}` : ""}`;
|
|
7657
7662
|
}
|
|
7658
7663
|
function read3(stdout, exited, timeoutMs) {
|
|
7659
7664
|
const events = stdout.split(`
|
|
@@ -7836,6 +7841,7 @@ var grok = {
|
|
|
7836
7841
|
}),
|
|
7837
7842
|
authLabel: () => `your grok login (${USER_AUTH})`,
|
|
7838
7843
|
checkLogin: checkLogin3,
|
|
7844
|
+
isFinal: (line) => parse3(line)?.type === "result",
|
|
7839
7845
|
progress: progress3,
|
|
7840
7846
|
read: read3,
|
|
7841
7847
|
sessionsDir: (configDir) => join4(configDir, "sessions"),
|
|
@@ -8516,6 +8522,26 @@ var CLAUDE_AUTH = /^CLAUDE_CODE_(OAUTH_TOKEN|OAUTH_REFRESH_TOKEN|USE_[A-Z_]+|SKI
|
|
|
8516
8522
|
function hostEnv() {
|
|
8517
8523
|
return Object.fromEntries(Object.entries(process.env).filter(([k]) => !NPM_INJECTED.test(k) && !k.startsWith("FIREGOAT_") && (!CLAUDE_SESSION.test(k) || CLAUDE_AUTH.test(k))));
|
|
8518
8524
|
}
|
|
8525
|
+
var GRACE_MS = 5000;
|
|
8526
|
+
var liveGroups = new Set;
|
|
8527
|
+
var signalsHooked = false;
|
|
8528
|
+
function hookSignals() {
|
|
8529
|
+
if (signalsHooked)
|
|
8530
|
+
return;
|
|
8531
|
+
signalsHooked = true;
|
|
8532
|
+
const onSignal = (signal) => {
|
|
8533
|
+
for (const g of liveGroups)
|
|
8534
|
+
try {
|
|
8535
|
+
process.kill(-g, "SIGKILL");
|
|
8536
|
+
} catch {}
|
|
8537
|
+
process.kill(process.pid, signal);
|
|
8538
|
+
};
|
|
8539
|
+
for (const s of ["SIGINT", "SIGTERM", "SIGHUP"])
|
|
8540
|
+
process.once(s, onSignal);
|
|
8541
|
+
}
|
|
8542
|
+
function forgetGroup(pgid) {
|
|
8543
|
+
liveGroups.delete(pgid);
|
|
8544
|
+
}
|
|
8519
8545
|
function processTree(pid) {
|
|
8520
8546
|
const children = new Map;
|
|
8521
8547
|
const ps = execFileSync("ps", ["-A", "-o", "pid=,ppid="], {
|
|
@@ -8533,7 +8559,11 @@ function processTree(pid) {
|
|
|
8533
8559
|
tree.push(...children.get(tree[i]) ?? []);
|
|
8534
8560
|
return tree;
|
|
8535
8561
|
}
|
|
8536
|
-
function killTree(pid) {
|
|
8562
|
+
function killTree(pid, group) {
|
|
8563
|
+
if (group)
|
|
8564
|
+
try {
|
|
8565
|
+
process.kill(-pid, "SIGKILL");
|
|
8566
|
+
} catch {}
|
|
8537
8567
|
let tree = [pid];
|
|
8538
8568
|
let problem;
|
|
8539
8569
|
try {
|
|
@@ -8549,18 +8579,27 @@ function killTree(pid) {
|
|
|
8549
8579
|
}
|
|
8550
8580
|
function run(opts) {
|
|
8551
8581
|
return new Promise((resolve) => {
|
|
8582
|
+
const detached = opts.detached === true;
|
|
8583
|
+
if (detached)
|
|
8584
|
+
hookSignals();
|
|
8552
8585
|
const child = spawn(opts.cmd, opts.args, {
|
|
8553
8586
|
cwd: opts.cwd,
|
|
8554
8587
|
env: opts.env,
|
|
8555
|
-
stdio: ["ignore", "pipe", "pipe"]
|
|
8588
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
8589
|
+
detached
|
|
8556
8590
|
});
|
|
8591
|
+
if (child.pid !== undefined) {
|
|
8592
|
+
if (detached)
|
|
8593
|
+
liveGroups.add(child.pid);
|
|
8594
|
+
opts.onSpawn?.(child.pid);
|
|
8595
|
+
}
|
|
8557
8596
|
let timedOut = false;
|
|
8558
8597
|
let settled = false;
|
|
8559
8598
|
const timer = setTimeout(() => {
|
|
8560
8599
|
timedOut = true;
|
|
8561
8600
|
if (child.pid === undefined)
|
|
8562
8601
|
return;
|
|
8563
|
-
const problem = killTree(child.pid);
|
|
8602
|
+
const problem = killTree(child.pid, detached);
|
|
8564
8603
|
if (problem)
|
|
8565
8604
|
opts.onStderr(Buffer.from(`firegoat: timed out; could not list ${opts.cmd}'s child processes (${problem}), killed only ${opts.cmd}
|
|
8566
8605
|
`));
|
|
@@ -8574,6 +8613,7 @@ function run(opts) {
|
|
|
8574
8613
|
};
|
|
8575
8614
|
child.stdout.on("data", opts.onStdout);
|
|
8576
8615
|
child.stderr.on("data", opts.onStderr);
|
|
8616
|
+
opts.finalSeen?.then(() => setTimeout(() => done({ code: null, timedOut: false, ended: true }), GRACE_MS).unref());
|
|
8577
8617
|
child.on("error", (error) => done({ code: null, timedOut, error }));
|
|
8578
8618
|
child.on("close", (code) => done({ code, timedOut }));
|
|
8579
8619
|
child.on("exit", (code) => setTimeout(() => {
|
|
@@ -8666,41 +8706,54 @@ function gitRepo(dir) {
|
|
|
8666
8706
|
async function runAgent(harness, opts) {
|
|
8667
8707
|
let stdout = "";
|
|
8668
8708
|
let pending = "";
|
|
8709
|
+
let group;
|
|
8710
|
+
let markFinal = () => {};
|
|
8711
|
+
const finalSeen = new Promise((r) => {
|
|
8712
|
+
markFinal = r;
|
|
8713
|
+
});
|
|
8669
8714
|
const exited = await run({
|
|
8670
8715
|
cmd: opts.bin,
|
|
8671
8716
|
args: harness.args(opts),
|
|
8672
8717
|
cwd: opts.cwd,
|
|
8673
8718
|
env: opts.env,
|
|
8674
8719
|
timeoutMs: opts.timeoutMs,
|
|
8720
|
+
detached: true,
|
|
8721
|
+
finalSeen,
|
|
8722
|
+
onSpawn: (pid) => {
|
|
8723
|
+
group = pid;
|
|
8724
|
+
},
|
|
8675
8725
|
onStdout: (c) => {
|
|
8676
8726
|
const text = c.toString();
|
|
8677
8727
|
stdout += text;
|
|
8678
|
-
if (!opts.onProgress)
|
|
8679
|
-
return;
|
|
8680
8728
|
pending += text;
|
|
8681
8729
|
const lines = pending.split(`
|
|
8682
8730
|
`);
|
|
8683
8731
|
pending = lines.pop() ?? "";
|
|
8684
8732
|
for (const line of lines) {
|
|
8685
|
-
|
|
8733
|
+
if (harness.isFinal(line))
|
|
8734
|
+
markFinal();
|
|
8735
|
+
const p = opts.onProgress && harness.progress(line);
|
|
8686
8736
|
if (p)
|
|
8687
|
-
opts.onProgress(p);
|
|
8737
|
+
opts.onProgress?.(p);
|
|
8688
8738
|
}
|
|
8689
8739
|
},
|
|
8690
8740
|
onStderr: (c) => opts.stderr.write(c)
|
|
8691
8741
|
});
|
|
8692
8742
|
if (exited.error)
|
|
8693
8743
|
return {
|
|
8694
|
-
|
|
8695
|
-
|
|
8696
|
-
|
|
8697
|
-
|
|
8698
|
-
|
|
8699
|
-
|
|
8700
|
-
|
|
8701
|
-
|
|
8744
|
+
group,
|
|
8745
|
+
outcome: {
|
|
8746
|
+
ok: false,
|
|
8747
|
+
reason: `could not start ${opts.bin}: ${exited.error.message}`,
|
|
8748
|
+
steps: [],
|
|
8749
|
+
final: "",
|
|
8750
|
+
reportedModel: null,
|
|
8751
|
+
version: null,
|
|
8752
|
+
turns: null,
|
|
8753
|
+
cost: null
|
|
8754
|
+
}
|
|
8702
8755
|
};
|
|
8703
|
-
return harness.read(stdout, exited, opts.timeoutMs);
|
|
8756
|
+
return { group, outcome: harness.read(stdout, exited, opts.timeoutMs) };
|
|
8704
8757
|
}
|
|
8705
8758
|
|
|
8706
8759
|
// src/help.ts
|
|
@@ -8778,8 +8831,10 @@ firegoat.yaml reference:
|
|
|
8778
8831
|
install: <sh> | [<sh>, ...] In each fresh folder, before the agent starts; a
|
|
8779
8832
|
failure: that run is not graded
|
|
8780
8833
|
cleanup: <sh> | [<sh>, ...] In each run's folder once the run is over: after the
|
|
8781
|
-
checks, and also after an error or timeout.
|
|
8782
|
-
what
|
|
8834
|
+
checks, and also after an error or timeout. For
|
|
8835
|
+
what lives outside the run (e.g. seeded data);
|
|
8836
|
+
processes the agent left running are stopped by
|
|
8837
|
+
firegoat after cleanup (see eval --help).
|
|
8783
8838
|
Every step runs; a failure is a warning (run.json
|
|
8784
8839
|
cleanup, <test>/<n>/cleanup.txt), never the verdict
|
|
8785
8840
|
vars: {KEY: value} Defaults for {{vars.KEY}} in prompts, checks and
|
|
@@ -8891,7 +8946,6 @@ relative paths in it are from the run's folder, claude's cwd:
|
|
|
8891
8946
|
Example:
|
|
8892
8947
|
build: npm pack --pack-destination "$FIREGOAT_OUT"
|
|
8893
8948
|
install: npm init -y >/dev/null && npm install "$FIREGOAT_OUT"/*.tgz
|
|
8894
|
-
cleanup: pkill -f "$FIREGOAT_WORKDIR" || true
|
|
8895
8949
|
tests:
|
|
8896
8950
|
- name: first-call
|
|
8897
8951
|
prompt: >-
|
|
@@ -8973,6 +9027,17 @@ top-level). cleanup: runs whenever the folder exists, also after a failed instal
|
|
|
8973
9027
|
a claude error or a timeout; a failing cleanup step prints a warning and never
|
|
8974
9028
|
changes the run's status or the exit code.
|
|
8975
9029
|
|
|
9030
|
+
The agent's run ends with its final result (claude's and grok's result line,
|
|
9031
|
+
codex's turn.completed or turn.failed): a harness that hasn't exited 5s later
|
|
9032
|
+
(a server it started in the background keeps it alive) is left running, so the
|
|
9033
|
+
checks can still reach that server. After cleanup, firegoat stops whatever is
|
|
9034
|
+
still running from the run (SIGTERM, then SIGKILL after 2s): the harness's
|
|
9035
|
+
process group, every process below it, and every process whose working
|
|
9036
|
+
directory is inside the run's folder (lsof on macOS, /proc on Linux). They are
|
|
9037
|
+
listed in run.json leftover, with one warning per run; never the verdict. Not
|
|
9038
|
+
found: a daemon that left the group and changed its working directory out of
|
|
9039
|
+
the run's folder.
|
|
9040
|
+
|
|
8976
9041
|
claude runs on its own login (or ANTHROPIC_API_KEY if set, as claude always does)
|
|
8977
9042
|
with a fresh config dir, ~/.firegoat/runs/<hash>/.claude: none of your settings,
|
|
8978
9043
|
CLAUDE.md, plugins, skills or MCP servers (harness_args: in firegoat.yaml adds
|
|
@@ -9087,8 +9152,9 @@ Output: .firegoat/runs/<runId>/
|
|
|
9087
9152
|
the harness reported running, or null), harnessArgs
|
|
9088
9153
|
(harness_args passed), prompt, end_goal,
|
|
9089
9154
|
checks [{run, code}], cleanup [{run, code}],
|
|
9090
|
-
|
|
9091
|
-
|
|
9155
|
+
leftover [{pid, command}] (processes stopped after
|
|
9156
|
+
cleanup), and paths: dir (this folder), workdir, home
|
|
9157
|
+
(the run's HOME; both real paths, under ~/.firegoat),
|
|
9092
9158
|
transcript, session (the harness's session file), stderr,
|
|
9093
9159
|
checksLog, cleanupLog; startedAt; upload ({status: "uploaded",
|
|
9094
9160
|
url} | {status: "failed", error} | {status:
|
|
@@ -9153,6 +9219,121 @@ Example:
|
|
|
9153
9219
|
|
|
9154
9220
|
Exit codes: 0 sent, 2 the service failed (or rate limit), 3 usage error.`;
|
|
9155
9221
|
|
|
9222
|
+
// src/leftover.ts
|
|
9223
|
+
import { spawn as spawn2 } from "node:child_process";
|
|
9224
|
+
import { readdirSync as readdirSync4, readlinkSync as readlinkSync2, realpathSync as realpathSync2 } from "node:fs";
|
|
9225
|
+
var TERM_WAIT_MS = 2000;
|
|
9226
|
+
function output(cmd, args) {
|
|
9227
|
+
return new Promise((resolve, reject) => {
|
|
9228
|
+
const child = spawn2(cmd, args, { stdio: ["ignore", "pipe", "pipe"] });
|
|
9229
|
+
let out = "";
|
|
9230
|
+
let err = "";
|
|
9231
|
+
child.stdout.on("data", (c) => {
|
|
9232
|
+
out += c;
|
|
9233
|
+
});
|
|
9234
|
+
child.stderr.on("data", (c) => {
|
|
9235
|
+
err += c;
|
|
9236
|
+
});
|
|
9237
|
+
child.on("error", (e) => reject(new Error(`could not run ${cmd}: ${e.message}`)));
|
|
9238
|
+
child.on("close", (code) => code === 0 || out !== "" ? resolve(out) : reject(new Error(`${cmd} exited ${code}: ${err.trim().slice(0, 300)}`)));
|
|
9239
|
+
});
|
|
9240
|
+
}
|
|
9241
|
+
async function processes() {
|
|
9242
|
+
const ps = await output("ps", [
|
|
9243
|
+
"-A",
|
|
9244
|
+
"-o",
|
|
9245
|
+
"pid=,ppid=,pgid=,stat=,command="
|
|
9246
|
+
]);
|
|
9247
|
+
const procs = [];
|
|
9248
|
+
for (const line of ps.split(`
|
|
9249
|
+
`)) {
|
|
9250
|
+
const m = /^\s*(\d+)\s+(\d+)\s+(\d+)\s+(\S+)\s*(.*)$/.exec(line);
|
|
9251
|
+
if (!m || m[4]?.startsWith("Z"))
|
|
9252
|
+
continue;
|
|
9253
|
+
procs.push({
|
|
9254
|
+
pid: Number(m[1]),
|
|
9255
|
+
ppid: Number(m[2]),
|
|
9256
|
+
pgid: Number(m[3]),
|
|
9257
|
+
command: m[5] ?? ""
|
|
9258
|
+
});
|
|
9259
|
+
}
|
|
9260
|
+
return procs;
|
|
9261
|
+
}
|
|
9262
|
+
async function cwdInside(dir) {
|
|
9263
|
+
const inside = (p) => p === dir || p.startsWith(`${dir}/`);
|
|
9264
|
+
if (process.platform === "linux") {
|
|
9265
|
+
const pids = [];
|
|
9266
|
+
for (const name of readdirSync4("/proc")) {
|
|
9267
|
+
if (!/^\d+$/.test(name))
|
|
9268
|
+
continue;
|
|
9269
|
+
try {
|
|
9270
|
+
if (inside(readlinkSync2(`/proc/${name}/cwd`)))
|
|
9271
|
+
pids.push(Number(name));
|
|
9272
|
+
} catch {}
|
|
9273
|
+
}
|
|
9274
|
+
return pids;
|
|
9275
|
+
}
|
|
9276
|
+
if (process.platform === "darwin") {
|
|
9277
|
+
const pids = [];
|
|
9278
|
+
let pid = 0;
|
|
9279
|
+
for (const line of (await output("lsof", ["-d", "cwd", "-Fpn", "-w"])).split(`
|
|
9280
|
+
`)) {
|
|
9281
|
+
if (line.startsWith("p"))
|
|
9282
|
+
pid = Number(line.slice(1));
|
|
9283
|
+
else if (line.startsWith("n") && inside(line.slice(1)))
|
|
9284
|
+
pids.push(pid);
|
|
9285
|
+
}
|
|
9286
|
+
return pids;
|
|
9287
|
+
}
|
|
9288
|
+
throw new Error(`finding processes by cwd is not supported on ${process.platform}`);
|
|
9289
|
+
}
|
|
9290
|
+
function alive(pid) {
|
|
9291
|
+
try {
|
|
9292
|
+
process.kill(pid, 0);
|
|
9293
|
+
return true;
|
|
9294
|
+
} catch {
|
|
9295
|
+
return false;
|
|
9296
|
+
}
|
|
9297
|
+
}
|
|
9298
|
+
function signal(pids, group, sig) {
|
|
9299
|
+
for (const p of group === undefined ? pids : [...pids, -group])
|
|
9300
|
+
try {
|
|
9301
|
+
process.kill(p, sig);
|
|
9302
|
+
} catch {}
|
|
9303
|
+
}
|
|
9304
|
+
async function stopLeftovers(opts) {
|
|
9305
|
+
const workdir = realpathSync2(opts.workdir);
|
|
9306
|
+
const [procs, inWorkdir] = await Promise.all([
|
|
9307
|
+
processes(),
|
|
9308
|
+
cwdInside(workdir)
|
|
9309
|
+
]);
|
|
9310
|
+
const found = new Set(inWorkdir);
|
|
9311
|
+
if (opts.group !== undefined) {
|
|
9312
|
+
for (const p of procs)
|
|
9313
|
+
if (p.pgid === opts.group)
|
|
9314
|
+
found.add(p.pid);
|
|
9315
|
+
}
|
|
9316
|
+
for (let grew = true;grew; ) {
|
|
9317
|
+
grew = false;
|
|
9318
|
+
for (const p of procs)
|
|
9319
|
+
if (found.has(p.ppid) && !found.has(p.pid)) {
|
|
9320
|
+
found.add(p.pid);
|
|
9321
|
+
grew = true;
|
|
9322
|
+
}
|
|
9323
|
+
}
|
|
9324
|
+
found.delete(process.pid);
|
|
9325
|
+
const leftover = procs.filter((p) => found.has(p.pid)).map((p) => ({ pid: p.pid, command: printable(p.command, 200) }));
|
|
9326
|
+
const pids = leftover.map((l) => l.pid);
|
|
9327
|
+
signal(pids, opts.group, "SIGTERM");
|
|
9328
|
+
const deadline = Date.now() + TERM_WAIT_MS;
|
|
9329
|
+
while (pids.some(alive) && Date.now() < deadline)
|
|
9330
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
9331
|
+
signal(pids.filter(alive), opts.group, "SIGKILL");
|
|
9332
|
+
if (opts.group !== undefined)
|
|
9333
|
+
forgetGroup(opts.group);
|
|
9334
|
+
return leftover;
|
|
9335
|
+
}
|
|
9336
|
+
|
|
9156
9337
|
// src/steps.ts
|
|
9157
9338
|
import { createWriteStream } from "node:fs";
|
|
9158
9339
|
var STEP_TIMEOUT_MS = 600000;
|
|
@@ -9287,7 +9468,7 @@ async function sendTelemetry(event, exitCode, startedAt) {
|
|
|
9287
9468
|
}
|
|
9288
9469
|
|
|
9289
9470
|
// src/transcript.ts
|
|
9290
|
-
import { readdirSync as
|
|
9471
|
+
import { readdirSync as readdirSync5, statSync as statSync2, writeFileSync as writeFileSync3 } from "node:fs";
|
|
9291
9472
|
import { join as join8, relative } from "node:path";
|
|
9292
9473
|
var OUTPUT_LINES = 15;
|
|
9293
9474
|
var OUTPUT_CHARS = 1500;
|
|
@@ -9337,7 +9518,7 @@ function describeToolCall(step, c) {
|
|
|
9337
9518
|
function workdirFiles(dir) {
|
|
9338
9519
|
const out = [];
|
|
9339
9520
|
const walk = (d) => {
|
|
9340
|
-
for (const name of
|
|
9521
|
+
for (const name of readdirSync5(d)) {
|
|
9341
9522
|
if (SKIP_DIRS.has(name))
|
|
9342
9523
|
continue;
|
|
9343
9524
|
const p = join8(d, name);
|
|
@@ -9420,7 +9601,7 @@ function runTag(harness, model) {
|
|
|
9420
9601
|
return parts.length > 0 ? ` [${parts.join(" ")}]` : "";
|
|
9421
9602
|
}
|
|
9422
9603
|
function notGradedWhy(reason) {
|
|
9423
|
-
const said = /claude
|
|
9604
|
+
const said = /claude reported an error(?: \([^)]*\))?: (.*)$/s.exec(reason);
|
|
9424
9605
|
if (said)
|
|
9425
9606
|
return `claude said: "${said[1]}"`;
|
|
9426
9607
|
return reason.replace(/^not graded: /, "");
|
|
@@ -10077,6 +10258,7 @@ async function runOne(opts) {
|
|
|
10077
10258
|
checksLog: join10(dir, "checks.txt"),
|
|
10078
10259
|
cleanup: [],
|
|
10079
10260
|
cleanupLog: join10(dir, "cleanup.txt"),
|
|
10261
|
+
leftover: [],
|
|
10080
10262
|
num_turns: null,
|
|
10081
10263
|
cost: null,
|
|
10082
10264
|
duration_ms: 0,
|
|
@@ -10094,7 +10276,8 @@ async function runOne(opts) {
|
|
|
10094
10276
|
FIREGOAT_REPO: opts.repo
|
|
10095
10277
|
};
|
|
10096
10278
|
const steps = renderRunSteps(test, opts, opts.vars, fill);
|
|
10097
|
-
const
|
|
10279
|
+
const agent = {};
|
|
10280
|
+
const [status, reason] = await attempt(opts, r, steps, env, stepEnv, envFile, agent);
|
|
10098
10281
|
if (steps.cleanup.length > 0)
|
|
10099
10282
|
r.cleanup = await runAllSteps({
|
|
10100
10283
|
commands: steps.cleanup,
|
|
@@ -10102,6 +10285,7 @@ async function runOne(opts) {
|
|
|
10102
10285
|
env: stepEnv,
|
|
10103
10286
|
logPath: r.cleanupLog
|
|
10104
10287
|
});
|
|
10288
|
+
r.leftover = await stopLeftovers({ group: agent.group, workdir });
|
|
10105
10289
|
r.status = status;
|
|
10106
10290
|
r.reason = reason;
|
|
10107
10291
|
r.duration_ms = Date.now() - started;
|
|
@@ -10111,7 +10295,7 @@ async function runOne(opts) {
|
|
|
10111
10295
|
function withoutOldPwd(env) {
|
|
10112
10296
|
return Object.fromEntries(Object.entries(env).filter(([k]) => k !== "OLDPWD"));
|
|
10113
10297
|
}
|
|
10114
|
-
async function attempt(opts, r, steps, env, stepEnv, homeEnvFile) {
|
|
10298
|
+
async function attempt(opts, r, steps, env, stepEnv, homeEnvFile, agent) {
|
|
10115
10299
|
const { test, n } = opts;
|
|
10116
10300
|
const { dir, workdir } = r;
|
|
10117
10301
|
if (steps.install.length > 0) {
|
|
@@ -10127,7 +10311,7 @@ async function attempt(opts, r, steps, env, stepEnv, homeEnvFile) {
|
|
|
10127
10311
|
}
|
|
10128
10312
|
const stderr = createWriteStream2(r.stderr);
|
|
10129
10313
|
const { harness } = opts;
|
|
10130
|
-
const outcome = await runAgent(harness, {
|
|
10314
|
+
const { outcome, group } = await runAgent(harness, {
|
|
10131
10315
|
bin: opts.bin,
|
|
10132
10316
|
prompt: r.prompt,
|
|
10133
10317
|
cwd: workdir,
|
|
@@ -10150,6 +10334,7 @@ async function attempt(opts, r, steps, env, stepEnv, homeEnvFile) {
|
|
|
10150
10334
|
extraArgs: r.harnessArgs,
|
|
10151
10335
|
onProgress: opts.onProgress
|
|
10152
10336
|
});
|
|
10337
|
+
agent.group = group;
|
|
10153
10338
|
if (outcome.version !== null)
|
|
10154
10339
|
evalStats({ claudeVersion: claudeVersion(outcome.version) });
|
|
10155
10340
|
await new Promise((res) => stderr.end(res));
|
|
@@ -10538,6 +10723,9 @@ async function evalCommand(args, root) {
|
|
|
10538
10723
|
for (const c of r.cleanup)
|
|
10539
10724
|
if (c.code !== 0)
|
|
10540
10725
|
console.error(`firegoat eval: Warning: ${r.test}#${r.n} cleanup failed (${c.error ?? `exit ${c.code}`}): ${c.run} (log: ${r.cleanupLog})`);
|
|
10726
|
+
for (const r of results)
|
|
10727
|
+
if (r.leftover.length > 0)
|
|
10728
|
+
console.error(`firegoat eval: Warning: ${r.test}#${r.n} left ${r.leftover.length} process${r.leftover.length === 1 ? "" : "es"} running, stopped after cleanup: ${r.leftover.map((l) => `${l.pid} ${l.command}`).join("; ")} (run.json leftover)`);
|
|
10541
10729
|
if (uploadsFailed > 0)
|
|
10542
10730
|
console.error(`firegoat eval: ${uploadsFailed} upload${uploadsFailed === 1 ? "" : "s"} failed (each run's upload.error in summary.json)`);
|
|
10543
10731
|
if (opts.json)
|
|
@@ -10822,12 +11010,12 @@ firegoat ${package_default.version}` : `firegoat ${package_default.version}`,
|
|
|
10822
11010
|
}
|
|
10823
11011
|
|
|
10824
11012
|
// src/init.ts
|
|
10825
|
-
import { spawn as
|
|
11013
|
+
import { spawn as spawn4, spawnSync as spawnSync4 } from "node:child_process";
|
|
10826
11014
|
import { existsSync as existsSync6, writeFileSync as writeFileSync5 } from "node:fs";
|
|
10827
11015
|
import { basename as basename4, join as join11 } from "node:path";
|
|
10828
11016
|
|
|
10829
11017
|
// src/login.ts
|
|
10830
|
-
import { spawn as
|
|
11018
|
+
import { spawn as spawn3 } from "node:child_process";
|
|
10831
11019
|
var TIMEOUT_MS3 = 30000;
|
|
10832
11020
|
var MAX_EXPIRES_S = 900;
|
|
10833
11021
|
var MAX_INTERVAL_S = 60;
|
|
@@ -10844,7 +11032,7 @@ function str3(o, k, step) {
|
|
|
10844
11032
|
function openBrowser(url) {
|
|
10845
11033
|
const cmd = process.platform === "darwin" ? "open" : "xdg-open";
|
|
10846
11034
|
try {
|
|
10847
|
-
const child =
|
|
11035
|
+
const child = spawn3(cmd, [url], {
|
|
10848
11036
|
env: hostEnv(),
|
|
10849
11037
|
stdio: "ignore",
|
|
10850
11038
|
detached: true
|
|
@@ -11168,10 +11356,12 @@ runs: 1
|
|
|
11168
11356
|
# install: cp -R "$FIREGOAT_OUT/starter-app/." .
|
|
11169
11357
|
|
|
11170
11358
|
# Optional: cleanup: runs in each run's folder once the run is over (after the
|
|
11171
|
-
# checks, also after an error or timeout),
|
|
11172
|
-
#
|
|
11359
|
+
# checks, also after an error or timeout), for what lives outside the run, e.g.
|
|
11360
|
+
# data install: seeded in a shared service. Servers the agent left running need
|
|
11361
|
+
# none: firegoat stops them after cleanup. A failing cleanup step is a warning,
|
|
11362
|
+
# never the verdict.
|
|
11173
11363
|
# cleanup:
|
|
11174
|
-
# -
|
|
11364
|
+
# - acme db drop "eval-{{run.id}}-{{run.n}}" || true
|
|
11175
11365
|
#
|
|
11176
11366
|
# A test can add its own install: (after this one) and cleanup: (before this one).
|
|
11177
11367
|
|
|
@@ -11273,7 +11463,7 @@ class Timeline {
|
|
|
11273
11463
|
}
|
|
11274
11464
|
function runQuiet(cmd, args, opts) {
|
|
11275
11465
|
return new Promise((resolve) => {
|
|
11276
|
-
const child =
|
|
11466
|
+
const child = spawn4(cmd, args, {
|
|
11277
11467
|
...opts,
|
|
11278
11468
|
stdio: ["ignore", "pipe", "pipe"]
|
|
11279
11469
|
});
|