firegoat 0.26.0 → 0.28.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/README.md +1 -1
- package/dist/cli.js +929 -153
- 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.28.0",
|
|
6951
6951
|
repository: {
|
|
6952
6952
|
type: "git",
|
|
6953
6953
|
url: "git+https://github.com/firegoat-ai/firegoat-cli.git"
|
|
@@ -7078,6 +7078,7 @@ function steps(events) {
|
|
|
7078
7078
|
if (b.type === "tool_use")
|
|
7079
7079
|
out.push({
|
|
7080
7080
|
kind: "tool_call",
|
|
7081
|
+
id: typeof b.id === "string" ? b.id : null,
|
|
7081
7082
|
name: typeof b.name === "string" ? b.name : "",
|
|
7082
7083
|
input: typeof b.input === "object" && b.input !== null ? b.input : null
|
|
7083
7084
|
});
|
|
@@ -7089,6 +7090,7 @@ function steps(events) {
|
|
|
7089
7090
|
if (b.type === "tool_result")
|
|
7090
7091
|
out.push({
|
|
7091
7092
|
kind: "tool_result",
|
|
7093
|
+
callId: typeof b.tool_use_id === "string" ? b.tool_use_id : null,
|
|
7092
7094
|
isError: b.is_error === true,
|
|
7093
7095
|
text: resultText(b.content).trimEnd()
|
|
7094
7096
|
});
|
|
@@ -7157,7 +7159,7 @@ function read(stdout, exited, timeoutMs) {
|
|
|
7157
7159
|
reportedModel,
|
|
7158
7160
|
version,
|
|
7159
7161
|
ok: false,
|
|
7160
|
-
reason: `claude
|
|
7162
|
+
reason: `claude reported an error${result.subtype === undefined || result.subtype === "success" ? "" : ` (${result.subtype})`}${result.result ? `: ${result.result.slice(0, 300)}` : ""}`,
|
|
7161
7163
|
sessionId
|
|
7162
7164
|
};
|
|
7163
7165
|
return { ...trace, reportedModel, version, ok: true, sessionId };
|
|
@@ -7208,6 +7210,7 @@ var claude = {
|
|
|
7208
7210
|
bin: "claude",
|
|
7209
7211
|
installHint: "Install Claude Code (https://claude.com/claude-code) or pass --harness-bin PATH.",
|
|
7210
7212
|
configDirName: ".claude",
|
|
7213
|
+
argsKey: "claude_args",
|
|
7211
7214
|
reservedArgs: [
|
|
7212
7215
|
[["-p", "--print", ...FLAGS.filter((f) => f.startsWith("-"))], SET],
|
|
7213
7216
|
[["--model"], "choose the model with top-level model: (or eval --model)"],
|
|
@@ -7216,7 +7219,7 @@ var claude = {
|
|
|
7216
7219
|
"every run is a fresh session"
|
|
7217
7220
|
]
|
|
7218
7221
|
],
|
|
7219
|
-
|
|
7222
|
+
claudeSessionFile: true,
|
|
7220
7223
|
args: ({ prompt, model, extraArgs }) => [
|
|
7221
7224
|
"-p",
|
|
7222
7225
|
prompt,
|
|
@@ -7231,6 +7234,7 @@ var claude = {
|
|
|
7231
7234
|
}),
|
|
7232
7235
|
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
7236
|
checkLogin,
|
|
7237
|
+
isFinal: (line) => parse(line)?.type === "result",
|
|
7234
7238
|
progress,
|
|
7235
7239
|
read,
|
|
7236
7240
|
sessionsDir: (configDir) => join(configDir, "projects"),
|
|
@@ -7342,24 +7346,27 @@ function itemInput(item) {
|
|
|
7342
7346
|
function steps2(events) {
|
|
7343
7347
|
const out = [];
|
|
7344
7348
|
const started = new Set;
|
|
7345
|
-
for (const e of events) {
|
|
7349
|
+
for (const [i, e] of events.entries()) {
|
|
7346
7350
|
const item = e?.item;
|
|
7347
7351
|
if (!item)
|
|
7348
7352
|
continue;
|
|
7353
|
+
const id = typeof item.id === "string" ? item.id : `event_${i}`;
|
|
7349
7354
|
if (item.type === "command_execution") {
|
|
7350
7355
|
const call = {
|
|
7351
7356
|
kind: "tool_call",
|
|
7357
|
+
id,
|
|
7352
7358
|
name: "shell",
|
|
7353
7359
|
input: { command: unwrapShell(str(item.command)) }
|
|
7354
7360
|
};
|
|
7355
7361
|
if (e?.type === "item.started") {
|
|
7356
|
-
started.add(
|
|
7362
|
+
started.add(id);
|
|
7357
7363
|
out.push(call);
|
|
7358
7364
|
} else if (e?.type === "item.completed") {
|
|
7359
|
-
if (
|
|
7365
|
+
if (!started.has(id))
|
|
7360
7366
|
out.push(call);
|
|
7361
7367
|
out.push({
|
|
7362
7368
|
kind: "tool_result",
|
|
7369
|
+
callId: id,
|
|
7363
7370
|
isError: item.exit_code !== 0,
|
|
7364
7371
|
text: str(item.aggregated_output).trimEnd()
|
|
7365
7372
|
});
|
|
@@ -7374,6 +7381,7 @@ function steps2(events) {
|
|
|
7374
7381
|
} else if (item.type !== "reasoning" && item.type !== "error")
|
|
7375
7382
|
out.push({
|
|
7376
7383
|
kind: "tool_call",
|
|
7384
|
+
id,
|
|
7377
7385
|
name: str(item.type),
|
|
7378
7386
|
input: itemInput(item)
|
|
7379
7387
|
});
|
|
@@ -7522,6 +7530,7 @@ var codex = {
|
|
|
7522
7530
|
bin: "codex",
|
|
7523
7531
|
installHint: "Install Codex CLI (https://github.com/openai/codex) or pass --harness-bin PATH.",
|
|
7524
7532
|
configDirName: ".codex",
|
|
7533
|
+
argsKey: "codex_args",
|
|
7525
7534
|
reservedArgs: [
|
|
7526
7535
|
[[...FLAGS2, "-C", "--cd"], "firegoat sets it for every run"],
|
|
7527
7536
|
[
|
|
@@ -7531,7 +7540,7 @@ var codex = {
|
|
|
7531
7540
|
[["resume", "fork"], "every run is a fresh session"],
|
|
7532
7541
|
[["--ephemeral"], "firegoat reads the session file codex saves"]
|
|
7533
7542
|
],
|
|
7534
|
-
|
|
7543
|
+
claudeSessionFile: false,
|
|
7535
7544
|
args: ({ prompt, model, extraArgs, cwd }) => [
|
|
7536
7545
|
"exec",
|
|
7537
7546
|
...FLAGS2,
|
|
@@ -7549,6 +7558,10 @@ var codex = {
|
|
|
7549
7558
|
}),
|
|
7550
7559
|
authLabel: () => `your codex login (${join3(userCodexHome(), "auth.json")})${process.env.CODEX_API_KEY ? "; CODEX_API_KEY is set and passes through" : ""}`,
|
|
7551
7560
|
checkLogin: checkLogin2,
|
|
7561
|
+
isFinal: (line) => {
|
|
7562
|
+
const type = parse2(line)?.type;
|
|
7563
|
+
return type === "turn.completed" || type === "turn.failed";
|
|
7564
|
+
},
|
|
7552
7565
|
progress: progress2,
|
|
7553
7566
|
read: read2,
|
|
7554
7567
|
sessionsDir: (configDir) => join3(configDir, "sessions"),
|
|
@@ -7624,6 +7637,7 @@ function toolResult(b) {
|
|
|
7624
7637
|
`) : "";
|
|
7625
7638
|
return {
|
|
7626
7639
|
kind: "tool_result",
|
|
7640
|
+
callId: typeof b.tool_use_id === "string" ? b.tool_use_id : null,
|
|
7627
7641
|
isError: b.is_error === true || failed,
|
|
7628
7642
|
text: text.trimEnd()
|
|
7629
7643
|
};
|
|
@@ -7636,6 +7650,7 @@ function steps3(events) {
|
|
|
7636
7650
|
if (b.type === "tool_use")
|
|
7637
7651
|
out.push({
|
|
7638
7652
|
kind: "tool_call",
|
|
7653
|
+
id: typeof b.id === "string" ? b.id : null,
|
|
7639
7654
|
name: typeof b.name === "string" ? b.name : "",
|
|
7640
7655
|
input: isObject(b.input) ? b.input : null
|
|
7641
7656
|
});
|
|
@@ -7653,7 +7668,7 @@ function steps3(events) {
|
|
|
7653
7668
|
function resultError(result) {
|
|
7654
7669
|
const errors = Array.isArray(result.errors) ? result.errors.filter((x) => typeof x === "string").join("; ") : "";
|
|
7655
7670
|
const said = errors || result.result || "";
|
|
7656
|
-
return `grok
|
|
7671
|
+
return `grok reported an error${result.subtype === undefined || result.subtype === "success" ? "" : ` (${result.subtype})`}${said ? `: ${said.slice(0, 300)}` : ""}`;
|
|
7657
7672
|
}
|
|
7658
7673
|
function read3(stdout, exited, timeoutMs) {
|
|
7659
7674
|
const events = stdout.split(`
|
|
@@ -7786,6 +7801,7 @@ var grok = {
|
|
|
7786
7801
|
bin: BIN,
|
|
7787
7802
|
installHint: "Install grok (curl -fsSL https://x.ai/cli/install.sh | bash) or pass --harness-bin PATH.",
|
|
7788
7803
|
configDirName: ".grok",
|
|
7804
|
+
argsKey: "grok_args",
|
|
7789
7805
|
reservedArgs: [
|
|
7790
7806
|
[
|
|
7791
7807
|
[
|
|
@@ -7821,7 +7837,7 @@ var grok = {
|
|
|
7821
7837
|
"every run is a fresh session"
|
|
7822
7838
|
]
|
|
7823
7839
|
],
|
|
7824
|
-
|
|
7840
|
+
claudeSessionFile: false,
|
|
7825
7841
|
args: ({ prompt, model, extraArgs }) => [
|
|
7826
7842
|
"-p",
|
|
7827
7843
|
prompt,
|
|
@@ -7836,6 +7852,7 @@ var grok = {
|
|
|
7836
7852
|
}),
|
|
7837
7853
|
authLabel: () => `your grok login (${USER_AUTH})`,
|
|
7838
7854
|
checkLogin: checkLogin3,
|
|
7855
|
+
isFinal: (line) => parse3(line)?.type === "result",
|
|
7839
7856
|
progress: progress3,
|
|
7840
7857
|
read: read3,
|
|
7841
7858
|
sessionsDir: (configDir) => join4(configDir, "sessions"),
|
|
@@ -7906,7 +7923,7 @@ function parseConfig(text) {
|
|
|
7906
7923
|
"cleanup",
|
|
7907
7924
|
"vars",
|
|
7908
7925
|
"env",
|
|
7909
|
-
|
|
7926
|
+
...ARGS_KEYS,
|
|
7910
7927
|
"tests"
|
|
7911
7928
|
]);
|
|
7912
7929
|
const runs = posInt(raw.runs, "runs", 1);
|
|
@@ -7921,7 +7938,7 @@ function parseConfig(text) {
|
|
|
7921
7938
|
const cleanup = commands(raw.cleanup, "cleanup", RUN_PLACEHOLDERS);
|
|
7922
7939
|
const vars = parseVars(raw.vars);
|
|
7923
7940
|
const env = parseEnv(raw.env, "env");
|
|
7924
|
-
const harnessArgs =
|
|
7941
|
+
const harnessArgs = parseAllHarnessArgs(raw, "");
|
|
7925
7942
|
if (!Array.isArray(raw.tests) || raw.tests.length === 0)
|
|
7926
7943
|
fail("tests", "must be a non-empty list");
|
|
7927
7944
|
const names = new Set;
|
|
@@ -7938,7 +7955,7 @@ function parseConfig(text) {
|
|
|
7938
7955
|
"env",
|
|
7939
7956
|
"install",
|
|
7940
7957
|
"cleanup",
|
|
7941
|
-
|
|
7958
|
+
...ARGS_KEYS
|
|
7942
7959
|
]);
|
|
7943
7960
|
const name = str2(t.name, `${p}.name`);
|
|
7944
7961
|
if (!NAME.test(name))
|
|
@@ -7968,10 +7985,7 @@ function parseConfig(text) {
|
|
|
7968
7985
|
env: { ...env, ...parseEnv(t.env, `${p}.env`) },
|
|
7969
7986
|
install: commands(t.install, `${p}.install`, RUN_PLACEHOLDERS),
|
|
7970
7987
|
cleanup: commands(t.cleanup, `${p}.cleanup`, RUN_PLACEHOLDERS),
|
|
7971
|
-
harnessArgs:
|
|
7972
|
-
...harnessArgs,
|
|
7973
|
-
...parseHarnessArgs(t.harness_args, `${p}.harness_args`)
|
|
7974
|
-
]
|
|
7988
|
+
harnessArgs: mergeHarnessArgs(harnessArgs, parseAllHarnessArgs(t, `${p}.`))
|
|
7975
7989
|
};
|
|
7976
7990
|
});
|
|
7977
7991
|
return {
|
|
@@ -8014,26 +8028,35 @@ function nameList(raw, key, what) {
|
|
|
8014
8028
|
return name;
|
|
8015
8029
|
});
|
|
8016
8030
|
}
|
|
8017
|
-
|
|
8031
|
+
var ARGS_KEYS = [...HARNESSES.values()].map((h) => h.argsKey);
|
|
8032
|
+
function parseAllHarnessArgs(raw, prefix) {
|
|
8033
|
+
const out = {};
|
|
8034
|
+
for (const h of HARNESSES.values())
|
|
8035
|
+
out[h.name] = parseHarnessArgs(raw[h.argsKey], `${prefix}${h.argsKey}`, h);
|
|
8036
|
+
return out;
|
|
8037
|
+
}
|
|
8038
|
+
function mergeHarnessArgs(top, test) {
|
|
8039
|
+
const out = {};
|
|
8040
|
+
for (const name of HARNESSES.keys())
|
|
8041
|
+
out[name] = [...top[name] ?? [], ...test[name] ?? []];
|
|
8042
|
+
return out;
|
|
8043
|
+
}
|
|
8044
|
+
function parseHarnessArgs(raw, path, harness) {
|
|
8018
8045
|
if (raw === undefined)
|
|
8019
8046
|
return [];
|
|
8020
8047
|
if (!Array.isArray(raw))
|
|
8021
|
-
fail(path,
|
|
8048
|
+
fail(path, `must be a list of ${harness.name} arguments, one per entry, e.g. [--flag, value]`);
|
|
8022
8049
|
return raw.map((a, i) => {
|
|
8023
8050
|
const p = `${path}[${i}]`;
|
|
8024
8051
|
const arg = str2(a, p);
|
|
8025
8052
|
checkPlaceholders(arg, p, RUN_PLACEHOLDERS);
|
|
8053
|
+
for (const [flags, why] of harness.reservedArgs)
|
|
8054
|
+
for (const f of flags)
|
|
8055
|
+
if (arg === f || arg.startsWith(`${f}=`))
|
|
8056
|
+
fail(p, `${f} is not allowed for ${harness.name}: ${why}`);
|
|
8026
8057
|
return { arg, path: p };
|
|
8027
8058
|
});
|
|
8028
8059
|
}
|
|
8029
|
-
function checkHarnessArgs(tests, harness) {
|
|
8030
|
-
for (const t of tests)
|
|
8031
|
-
for (const { arg, path } of t.harnessArgs)
|
|
8032
|
-
for (const [flags, why] of harness.reservedArgs)
|
|
8033
|
-
for (const f of flags)
|
|
8034
|
-
if (arg === f || arg.startsWith(`${f}=`))
|
|
8035
|
-
fail(path, `${f} is not allowed for ${harness.name}: ${why}`);
|
|
8036
|
-
}
|
|
8037
8060
|
var VAR_KEY = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
8038
8061
|
function parseVars(raw) {
|
|
8039
8062
|
if (raw === undefined)
|
|
@@ -8151,9 +8174,9 @@ import {
|
|
|
8151
8174
|
appendFileSync,
|
|
8152
8175
|
createWriteStream as createWriteStream2,
|
|
8153
8176
|
mkdirSync as mkdirSync5,
|
|
8154
|
-
writeFileSync as
|
|
8177
|
+
writeFileSync as writeFileSync5
|
|
8155
8178
|
} from "node:fs";
|
|
8156
|
-
import { basename as
|
|
8179
|
+
import { basename as basename2, delimiter as delimiter2, join as join10 } from "node:path";
|
|
8157
8180
|
|
|
8158
8181
|
// src/agent-dirs.ts
|
|
8159
8182
|
import { createHash, randomInt } from "node:crypto";
|
|
@@ -8516,6 +8539,26 @@ var CLAUDE_AUTH = /^CLAUDE_CODE_(OAUTH_TOKEN|OAUTH_REFRESH_TOKEN|USE_[A-Z_]+|SKI
|
|
|
8516
8539
|
function hostEnv() {
|
|
8517
8540
|
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
8541
|
}
|
|
8542
|
+
var GRACE_MS = 5000;
|
|
8543
|
+
var liveGroups = new Set;
|
|
8544
|
+
var signalsHooked = false;
|
|
8545
|
+
function hookSignals() {
|
|
8546
|
+
if (signalsHooked)
|
|
8547
|
+
return;
|
|
8548
|
+
signalsHooked = true;
|
|
8549
|
+
const onSignal = (signal) => {
|
|
8550
|
+
for (const g of liveGroups)
|
|
8551
|
+
try {
|
|
8552
|
+
process.kill(-g, "SIGKILL");
|
|
8553
|
+
} catch {}
|
|
8554
|
+
process.kill(process.pid, signal);
|
|
8555
|
+
};
|
|
8556
|
+
for (const s of ["SIGINT", "SIGTERM", "SIGHUP"])
|
|
8557
|
+
process.once(s, onSignal);
|
|
8558
|
+
}
|
|
8559
|
+
function forgetGroup(pgid) {
|
|
8560
|
+
liveGroups.delete(pgid);
|
|
8561
|
+
}
|
|
8519
8562
|
function processTree(pid) {
|
|
8520
8563
|
const children = new Map;
|
|
8521
8564
|
const ps = execFileSync("ps", ["-A", "-o", "pid=,ppid="], {
|
|
@@ -8533,7 +8576,11 @@ function processTree(pid) {
|
|
|
8533
8576
|
tree.push(...children.get(tree[i]) ?? []);
|
|
8534
8577
|
return tree;
|
|
8535
8578
|
}
|
|
8536
|
-
function killTree(pid) {
|
|
8579
|
+
function killTree(pid, group) {
|
|
8580
|
+
if (group)
|
|
8581
|
+
try {
|
|
8582
|
+
process.kill(-pid, "SIGKILL");
|
|
8583
|
+
} catch {}
|
|
8537
8584
|
let tree = [pid];
|
|
8538
8585
|
let problem;
|
|
8539
8586
|
try {
|
|
@@ -8549,18 +8596,27 @@ function killTree(pid) {
|
|
|
8549
8596
|
}
|
|
8550
8597
|
function run(opts) {
|
|
8551
8598
|
return new Promise((resolve) => {
|
|
8599
|
+
const detached = opts.detached === true;
|
|
8600
|
+
if (detached)
|
|
8601
|
+
hookSignals();
|
|
8552
8602
|
const child = spawn(opts.cmd, opts.args, {
|
|
8553
8603
|
cwd: opts.cwd,
|
|
8554
8604
|
env: opts.env,
|
|
8555
|
-
stdio: ["ignore", "pipe", "pipe"]
|
|
8605
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
8606
|
+
detached
|
|
8556
8607
|
});
|
|
8608
|
+
if (child.pid !== undefined) {
|
|
8609
|
+
if (detached)
|
|
8610
|
+
liveGroups.add(child.pid);
|
|
8611
|
+
opts.onSpawn?.(child.pid);
|
|
8612
|
+
}
|
|
8557
8613
|
let timedOut = false;
|
|
8558
8614
|
let settled = false;
|
|
8559
8615
|
const timer = setTimeout(() => {
|
|
8560
8616
|
timedOut = true;
|
|
8561
8617
|
if (child.pid === undefined)
|
|
8562
8618
|
return;
|
|
8563
|
-
const problem = killTree(child.pid);
|
|
8619
|
+
const problem = killTree(child.pid, detached);
|
|
8564
8620
|
if (problem)
|
|
8565
8621
|
opts.onStderr(Buffer.from(`firegoat: timed out; could not list ${opts.cmd}'s child processes (${problem}), killed only ${opts.cmd}
|
|
8566
8622
|
`));
|
|
@@ -8574,6 +8630,7 @@ function run(opts) {
|
|
|
8574
8630
|
};
|
|
8575
8631
|
child.stdout.on("data", opts.onStdout);
|
|
8576
8632
|
child.stderr.on("data", opts.onStderr);
|
|
8633
|
+
opts.finalSeen?.then(() => setTimeout(() => done({ code: null, timedOut: false, ended: true }), GRACE_MS).unref());
|
|
8577
8634
|
child.on("error", (error) => done({ code: null, timedOut, error }));
|
|
8578
8635
|
child.on("close", (code) => done({ code, timedOut }));
|
|
8579
8636
|
child.on("exit", (code) => setTimeout(() => {
|
|
@@ -8666,41 +8723,54 @@ function gitRepo(dir) {
|
|
|
8666
8723
|
async function runAgent(harness, opts) {
|
|
8667
8724
|
let stdout = "";
|
|
8668
8725
|
let pending = "";
|
|
8726
|
+
let group;
|
|
8727
|
+
let markFinal = () => {};
|
|
8728
|
+
const finalSeen = new Promise((r) => {
|
|
8729
|
+
markFinal = r;
|
|
8730
|
+
});
|
|
8669
8731
|
const exited = await run({
|
|
8670
8732
|
cmd: opts.bin,
|
|
8671
8733
|
args: harness.args(opts),
|
|
8672
8734
|
cwd: opts.cwd,
|
|
8673
8735
|
env: opts.env,
|
|
8674
8736
|
timeoutMs: opts.timeoutMs,
|
|
8737
|
+
detached: true,
|
|
8738
|
+
finalSeen,
|
|
8739
|
+
onSpawn: (pid) => {
|
|
8740
|
+
group = pid;
|
|
8741
|
+
},
|
|
8675
8742
|
onStdout: (c) => {
|
|
8676
8743
|
const text = c.toString();
|
|
8677
8744
|
stdout += text;
|
|
8678
|
-
if (!opts.onProgress)
|
|
8679
|
-
return;
|
|
8680
8745
|
pending += text;
|
|
8681
8746
|
const lines = pending.split(`
|
|
8682
8747
|
`);
|
|
8683
8748
|
pending = lines.pop() ?? "";
|
|
8684
8749
|
for (const line of lines) {
|
|
8685
|
-
|
|
8750
|
+
if (harness.isFinal(line))
|
|
8751
|
+
markFinal();
|
|
8752
|
+
const p = opts.onProgress && harness.progress(line);
|
|
8686
8753
|
if (p)
|
|
8687
|
-
opts.onProgress(p);
|
|
8754
|
+
opts.onProgress?.(p);
|
|
8688
8755
|
}
|
|
8689
8756
|
},
|
|
8690
8757
|
onStderr: (c) => opts.stderr.write(c)
|
|
8691
8758
|
});
|
|
8692
8759
|
if (exited.error)
|
|
8693
8760
|
return {
|
|
8694
|
-
|
|
8695
|
-
|
|
8696
|
-
|
|
8697
|
-
|
|
8698
|
-
|
|
8699
|
-
|
|
8700
|
-
|
|
8701
|
-
|
|
8761
|
+
group,
|
|
8762
|
+
outcome: {
|
|
8763
|
+
ok: false,
|
|
8764
|
+
reason: `could not start ${opts.bin}: ${exited.error.message}`,
|
|
8765
|
+
steps: [],
|
|
8766
|
+
final: "",
|
|
8767
|
+
reportedModel: null,
|
|
8768
|
+
version: null,
|
|
8769
|
+
turns: null,
|
|
8770
|
+
cost: null
|
|
8771
|
+
}
|
|
8702
8772
|
};
|
|
8703
|
-
return harness.read(stdout, exited, opts.timeoutMs);
|
|
8773
|
+
return { group, outcome: harness.read(stdout, exited, opts.timeoutMs) };
|
|
8704
8774
|
}
|
|
8705
8775
|
|
|
8706
8776
|
// src/help.ts
|
|
@@ -8743,19 +8813,22 @@ turns it off; FIREGOAT_TELEMETRY accepts only 0 or 1.
|
|
|
8743
8813
|
|
|
8744
8814
|
${EXIT_CODES}`;
|
|
8745
8815
|
}
|
|
8746
|
-
var INIT_HELP = `Usage: firegoat init [--force] [--local-skills | --
|
|
8816
|
+
var INIT_HELP = `Usage: firegoat init [--force] [--local-skills | --global-skills | --no-skills]
|
|
8817
|
+
[--no-login]
|
|
8747
8818
|
|
|
8748
8819
|
In the current folder: creates .firegoat/ and a starter firegoat.yaml, installs the
|
|
8749
|
-
firegoat-onboarding skill
|
|
8750
|
-
|
|
8751
|
-
firegoat login (in a terminal only; otherwise prints how), then prints a prompt to
|
|
8820
|
+
firegoat-onboarding skill (npx skills add firegoat-ai/skills; in a terminal it asks:
|
|
8821
|
+
global into ~/.claude/skills/, this repo, or none; otherwise, e.g. run by a coding
|
|
8822
|
+
agent, it installs into this repo's .claude/skills/), offers firegoat login (in a terminal only; otherwise prints how), then prints a prompt to
|
|
8752
8823
|
paste into your coding agent. .firegoat/ holds local run output and ignores itself
|
|
8753
8824
|
in git. Keep test fixtures (helper scripts, sample data) in fixtures/ next to
|
|
8754
8825
|
firegoat.yaml, committed.
|
|
8755
8826
|
|
|
8756
8827
|
--force Replace an existing firegoat.yaml
|
|
8757
|
-
--local-skills Install the skill into this repo's .claude/skills/
|
|
8758
|
-
|
|
8828
|
+
--local-skills Install the skill into this repo's .claude/skills/ (commit it
|
|
8829
|
+
so teammates and CI get it), without asking
|
|
8830
|
+
--global-skills Install the skill into ~/.claude/skills/ (skills add -g),
|
|
8831
|
+
without asking
|
|
8759
8832
|
--no-skills Don't install the skill
|
|
8760
8833
|
--no-login Don't offer firegoat login
|
|
8761
8834
|
|
|
@@ -8778,8 +8851,10 @@ firegoat.yaml reference:
|
|
|
8778
8851
|
install: <sh> | [<sh>, ...] In each fresh folder, before the agent starts; a
|
|
8779
8852
|
failure: that run is not graded
|
|
8780
8853
|
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
|
|
8854
|
+
checks, and also after an error or timeout. For
|
|
8855
|
+
what lives outside the run (e.g. seeded data);
|
|
8856
|
+
processes the agent left running are stopped by
|
|
8857
|
+
firegoat after cleanup (see eval --help).
|
|
8783
8858
|
Every step runs; a failure is a warning (run.json
|
|
8784
8859
|
cleanup, <test>/<n>/cleanup.txt), never the verdict
|
|
8785
8860
|
vars: {KEY: value} Defaults for {{vars.KEY}} in prompts, checks and
|
|
@@ -8801,27 +8876,33 @@ firegoat.yaml reference:
|
|
|
8801
8876
|
tool that keeps state in ~ starts fresh in each
|
|
8802
8877
|
run. Only a tool that ignores HOME needs its own
|
|
8803
8878
|
var, e.g. MYTOOL_HOME: "$FIREGOAT_HOME/.mytool"
|
|
8804
|
-
|
|
8805
|
-
and --model
|
|
8806
|
-
|
|
8807
|
-
|
|
8808
|
-
|
|
8809
|
-
|
|
8810
|
-
|
|
8811
|
-
|
|
8879
|
+
claude_args: [<arg>, ...] Extra claude-code arguments, after firegoat's own
|
|
8880
|
+
flags and --model. Each entry is one argument,
|
|
8881
|
+
passed as-is (no shell splitting): [--chrome]
|
|
8882
|
+
(Claude in Chrome, for tests that need a browser),
|
|
8883
|
+
[--plugin-dir, ./plugins/x], --mcp-config,
|
|
8884
|
+
--agents, --append-system-prompt
|
|
8885
|
+
codex_args: [<arg>, ...] Extra codex-cli arguments, before the prompt:
|
|
8886
|
+
[-c, key=value], --enable, --add-dir
|
|
8887
|
+
grok_args: [<arg>, ...] Extra grok-cli arguments, after firegoat's flags.
|
|
8888
|
+
Each key goes to its harness only. A key for a
|
|
8889
|
+
harness this eval doesn't run is checked but not
|
|
8890
|
+
used, so one firegoat.yaml can carry args for
|
|
8891
|
+
every --harness.
|
|
8812
8892
|
Entries expand like env: values
|
|
8813
8893
|
($FIREGOAT_WORKDIR, $FIREGOAT_HOME, $FIREGOAT_OUT,
|
|
8814
8894
|
{{vars.KEY}}, {{run.*}}); run.json harnessArgs has
|
|
8815
|
-
the result. Flags firegoat sets
|
|
8816
|
-
as --flag=value.
|
|
8817
|
-
--output-format, --verbose,
|
|
8818
|
-
--
|
|
8895
|
+
the run's harness's result. Flags firegoat sets
|
|
8896
|
+
are errors, also as --flag=value. claude_args:
|
|
8897
|
+
-p, --print, --output-format, --verbose,
|
|
8898
|
+
--setting-sources, --strict-mcp-config,
|
|
8899
|
+
--disable-slash-commands,
|
|
8819
8900
|
--dangerously-skip-permissions, --model (use
|
|
8820
8901
|
model:), --session-id, --resume, -r, --continue,
|
|
8821
|
-
-c.
|
|
8902
|
+
-c. codex_args: --json, --skip-git-repo-check,
|
|
8822
8903
|
--dangerously-bypass-approvals-and-sandbox, -C,
|
|
8823
8904
|
--cd, -m, --model (use model:), resume, fork,
|
|
8824
|
-
--ephemeral.
|
|
8905
|
+
--ephemeral. grok_args: -p, --single,
|
|
8825
8906
|
--prompt-file, --prompt-json, --output-format,
|
|
8826
8907
|
--json-schema, --always-approve,
|
|
8827
8908
|
--permission-mode, --no-auto-update, --cwd, -w,
|
|
@@ -8839,7 +8920,8 @@ firegoat.yaml reference:
|
|
|
8839
8920
|
env: {NAME: value} Optional, on top of the top-level env:
|
|
8840
8921
|
install: <sh> | [...] Optional, runs after the top-level install
|
|
8841
8922
|
cleanup: <sh> | [...] Optional, runs before the top-level cleanup
|
|
8842
|
-
|
|
8923
|
+
claude_args: [...] Optional, after the top-level claude_args (same
|
|
8924
|
+
for codex_args, grok_args)
|
|
8843
8925
|
|
|
8844
8926
|
Step order: build once per eval; then per run: install (top-level, then the
|
|
8845
8927
|
test's), the agent, checks, cleanup (the test's, then top-level).
|
|
@@ -8875,7 +8957,7 @@ firegoat.yaml reference:
|
|
|
8875
8957
|
so the agent's commands see them too.
|
|
8876
8958
|
|
|
8877
8959
|
Testing an MCP server: commit its config as fixtures/mcp.json, copy it in build:
|
|
8878
|
-
and pass it with
|
|
8960
|
+
and pass it with claude_args. Only the configs you pass load: --strict-mcp-config
|
|
8879
8961
|
is always on. An MCP server inherits claude's env, which keeps your real HOME
|
|
8880
8962
|
(claude's login is there), so set the server's HOME to the run's home, or it
|
|
8881
8963
|
writes to yours. claude gets $FIREGOAT_HOME, $FIREGOAT_WORKDIR and $FIREGOAT_OUT
|
|
@@ -8886,12 +8968,11 @@ relative paths in it are from the run's folder, claude's cwd:
|
|
|
8886
8968
|
"env": {"HOME": "\${FIREGOAT_HOME}"}}}}
|
|
8887
8969
|
firegoat.yaml:
|
|
8888
8970
|
build: cp -R fixtures/. "$FIREGOAT_OUT"
|
|
8889
|
-
|
|
8971
|
+
claude_args: [--mcp-config, $FIREGOAT_OUT/mcp.json]
|
|
8890
8972
|
|
|
8891
8973
|
Example:
|
|
8892
8974
|
build: npm pack --pack-destination "$FIREGOAT_OUT"
|
|
8893
8975
|
install: npm init -y >/dev/null && npm install "$FIREGOAT_OUT"/*.tgz
|
|
8894
|
-
cleanup: pkill -f "$FIREGOAT_WORKDIR" || true
|
|
8895
8976
|
tests:
|
|
8896
8977
|
- name: first-call
|
|
8897
8978
|
prompt: >-
|
|
@@ -8910,7 +8991,7 @@ Example:
|
|
|
8910
8991
|
Get an acme API key from the acme dashboard (I'm logged in in Chrome) and put
|
|
8911
8992
|
it in .env as ACME_API_KEY.
|
|
8912
8993
|
end_goal: .env holds a key copied from the dashboard, not a made-up one.
|
|
8913
|
-
|
|
8994
|
+
claude_args: [--chrome]
|
|
8914
8995
|
|
|
8915
8996
|
init also says that firegoat sends anonymous usage stats; FIREGOAT_TELEMETRY=0
|
|
8916
8997
|
(or DO_NOT_TRACK=1) turns them off. Fields: firegoat --help, Telemetry.
|
|
@@ -8919,7 +9000,7 @@ Exit codes: 0 done, 1 the firegoat login failed (the files stay), 2 the skill
|
|
|
8919
9000
|
install failed, 3 usage error.`;
|
|
8920
9001
|
var LOGIN_HELP = `Usage: firegoat login
|
|
8921
9002
|
|
|
8922
|
-
Logs in to firegoat.ai so eval uploads each run's
|
|
9003
|
+
Logs in to firegoat.ai so eval uploads each run's session there as a private
|
|
8923
9004
|
trace (you share it from its page). OAuth device flow: prints a URL and a code (and
|
|
8924
9005
|
opens the browser in a terminal); approve it there, and the trace key is saved to
|
|
8925
9006
|
~/.firegoat/credentials.json (mode 0600). The key is never passed to the agent,
|
|
@@ -8929,7 +9010,9 @@ read ~/.firegoat, and a FIREGOAT_API_KEY in firegoat's own process env (ps eww,
|
|
|
8929
9010
|
your own traces; revoke it on the firegoat.ai traces page.
|
|
8930
9011
|
|
|
8931
9012
|
What eval uploads per run: session.jsonl (prompts, tool calls and outputs, file
|
|
8932
|
-
contents: possibly secrets from your env or repo
|
|
9013
|
+
contents: possibly secrets from your env or repo; claude's own session file, or for
|
|
9014
|
+
codex and grok one in claude's format made from the run's stream, the run dir's
|
|
9015
|
+
session.jsonl), checks.txt and stderr.txt (check
|
|
8933
9016
|
output, which sees vars:). firegoat.ai masks secrets when showing a trace but stores
|
|
8934
9017
|
the text as sent. eval --no-upload keeps a run local.
|
|
8935
9018
|
|
|
@@ -8973,9 +9056,20 @@ top-level). cleanup: runs whenever the folder exists, also after a failed instal
|
|
|
8973
9056
|
a claude error or a timeout; a failing cleanup step prints a warning and never
|
|
8974
9057
|
changes the run's status or the exit code.
|
|
8975
9058
|
|
|
9059
|
+
The agent's run ends with its final result (claude's and grok's result line,
|
|
9060
|
+
codex's turn.completed or turn.failed): a harness that hasn't exited 5s later
|
|
9061
|
+
(a server it started in the background keeps it alive) is left running, so the
|
|
9062
|
+
checks can still reach that server. After cleanup, firegoat stops whatever is
|
|
9063
|
+
still running from the run (SIGTERM, then SIGKILL after 2s): the harness's
|
|
9064
|
+
process group, every process below it, and every process whose working
|
|
9065
|
+
directory is inside the run's folder (lsof on macOS, /proc on Linux). They are
|
|
9066
|
+
listed in run.json leftover, with one warning per run; never the verdict. Not
|
|
9067
|
+
found: a daemon that left the group and changed its working directory out of
|
|
9068
|
+
the run's folder.
|
|
9069
|
+
|
|
8976
9070
|
claude runs on its own login (or ANTHROPIC_API_KEY if set, as claude always does)
|
|
8977
9071
|
with a fresh config dir, ~/.firegoat/runs/<hash>/.claude: none of your settings,
|
|
8978
|
-
CLAUDE.md, plugins, skills or MCP servers (
|
|
9072
|
+
CLAUDE.md, plugins, skills or MCP servers (claude_args: in firegoat.yaml adds
|
|
8979
9073
|
claude flags, e.g. --chrome or --plugin-dir). claude saves its session there. There
|
|
8980
9074
|
is no sandbox: the agent can still reach the rest of your machine, including the
|
|
8981
9075
|
repo, if it goes looking.
|
|
@@ -8985,9 +9079,10 @@ fresh CODEX_HOME, ~/.firegoat/runs/<hash>/.codex, holding a link to your codex l
|
|
|
8985
9079
|
(~/.codex/auth.json, or $CODEX_HOME/auth.json; run codex login first), and with
|
|
8986
9080
|
-c project_doc_max_bytes=0: no AGENTS.md, config.toml, skills or plugins of
|
|
8987
9081
|
yours. codex's own HOME is the run's home (below), also for its Read/Write. It
|
|
8988
|
-
saves its session (rollout) under that CODEX_HOME's sessions/. firegoat.ai
|
|
8989
|
-
|
|
8990
|
-
cost; its model comes from the session
|
|
9082
|
+
saves its session (rollout) under that CODEX_HOME's sessions/. firegoat.ai reads
|
|
9083
|
+
claude's session format: the upload is the run dir's session.jsonl, made from the
|
|
9084
|
+
--json stream. codex reports no turns or cost; its model comes from the session
|
|
9085
|
+
file.
|
|
8991
9086
|
|
|
8992
9087
|
grok-cli runs ~/.grok/bin/grok (where grok's installer puts it; never a grok on
|
|
8993
9088
|
PATH, which may be a wrapper such as cmux's: a #! script is refused) as grok -p
|
|
@@ -8999,8 +9094,8 @@ your real one it loads Claude Code's skills, plugins, MCP servers and hooks. eva
|
|
|
8999
9094
|
refuses a login that expires within 5 minutes (run grok -p hi once to refresh it):
|
|
9000
9095
|
a refresh inside a run would replace the link with a file, which the next eval
|
|
9001
9096
|
asks you to move back over ~/.grok/auth.json. The session is a directory,
|
|
9002
|
-
$GROK_HOME/sessions/<cwd>/<id>/ ($FIREGOAT_SESSION).
|
|
9003
|
-
|
|
9097
|
+
$GROK_HOME/sessions/<cwd>/<id>/ ($FIREGOAT_SESSION). Like codex's, its upload is
|
|
9098
|
+
the run dir's session.jsonl, made from the stream in claude's session format.
|
|
9004
9099
|
|
|
9005
9100
|
Every run gets its own HOME, ~/.firegoat/runs/<hash>/<random>/home next to work
|
|
9006
9101
|
(with XDG_CONFIG_HOME, XDG_CACHE_HOME, XDG_DATA_HOME, XDG_STATE_HOME inside it), as
|
|
@@ -9047,7 +9142,8 @@ firegoat.yaml, fixtures and build: stay in the current folder.
|
|
|
9047
9142
|
--dry-run Check firegoat.yaml as eval would (test names, --var,
|
|
9048
9143
|
--model, --harness, missing vars), print each planned
|
|
9049
9144
|
run ("<test>#<n> [harness model]" with its install, checks
|
|
9050
|
-
and cleanup counts and
|
|
9145
|
+
and cleanup counts and its harness's rendered args
|
|
9146
|
+
(claude_args, ...), paths under
|
|
9051
9147
|
~/.firegoat/runs/<hash>/<random>/) and exit 0. Builds
|
|
9052
9148
|
nothing, starts no agent, uploads nothing, writes no
|
|
9053
9149
|
files. Config errors
|
|
@@ -9085,11 +9181,16 @@ Output: .firegoat/runs/<runId>/
|
|
|
9085
9181
|
<test>/<n>/run.json One run: status and reason (from the checks), harness,
|
|
9086
9182
|
model (asked for, or null) and reportedModel (what
|
|
9087
9183
|
the harness reported running, or null), harnessArgs
|
|
9088
|
-
(
|
|
9184
|
+
(its harness's claude_args/codex_args/grok_args
|
|
9185
|
+
passed, expanded), prompt, end_goal,
|
|
9089
9186
|
checks [{run, code}], cleanup [{run, code}],
|
|
9090
|
-
|
|
9091
|
-
|
|
9092
|
-
|
|
9187
|
+
leftover [{pid, command}] (processes stopped after
|
|
9188
|
+
cleanup), and paths: dir (this folder), workdir, home
|
|
9189
|
+
(the run's HOME; both real paths, under ~/.firegoat),
|
|
9190
|
+
transcript, session (the harness's session file),
|
|
9191
|
+
claudeSession (the session.jsonl uploaded: claude's
|
|
9192
|
+
own file, else <test>/<n>/session.jsonl made from the
|
|
9193
|
+
stream) and sessionId (its id, the trace's), stderr,
|
|
9093
9194
|
checksLog, cleanupLog; startedAt; upload ({status: "uploaded",
|
|
9094
9195
|
url} | {status: "failed", error} | {status:
|
|
9095
9196
|
"skipped", reason}, null when not uploading)
|
|
@@ -9153,6 +9254,659 @@ Example:
|
|
|
9153
9254
|
|
|
9154
9255
|
Exit codes: 0 sent, 2 the service failed (or rate limit), 3 usage error.`;
|
|
9155
9256
|
|
|
9257
|
+
// src/leftover.ts
|
|
9258
|
+
import { spawn as spawn2 } from "node:child_process";
|
|
9259
|
+
import { readdirSync as readdirSync4, readlinkSync as readlinkSync2, realpathSync as realpathSync2 } from "node:fs";
|
|
9260
|
+
var TERM_WAIT_MS = 2000;
|
|
9261
|
+
function output(cmd, args) {
|
|
9262
|
+
return new Promise((resolve, reject) => {
|
|
9263
|
+
const child = spawn2(cmd, args, { stdio: ["ignore", "pipe", "pipe"] });
|
|
9264
|
+
let out = "";
|
|
9265
|
+
let err = "";
|
|
9266
|
+
child.stdout.on("data", (c) => {
|
|
9267
|
+
out += c;
|
|
9268
|
+
});
|
|
9269
|
+
child.stderr.on("data", (c) => {
|
|
9270
|
+
err += c;
|
|
9271
|
+
});
|
|
9272
|
+
child.on("error", (e) => reject(new Error(`could not run ${cmd}: ${e.message}`)));
|
|
9273
|
+
child.on("close", (code) => code === 0 || out !== "" ? resolve(out) : reject(new Error(`${cmd} exited ${code}: ${err.trim().slice(0, 300)}`)));
|
|
9274
|
+
});
|
|
9275
|
+
}
|
|
9276
|
+
async function processes() {
|
|
9277
|
+
const ps = await output("ps", [
|
|
9278
|
+
"-A",
|
|
9279
|
+
"-o",
|
|
9280
|
+
"pid=,ppid=,pgid=,stat=,command="
|
|
9281
|
+
]);
|
|
9282
|
+
const procs = [];
|
|
9283
|
+
for (const line of ps.split(`
|
|
9284
|
+
`)) {
|
|
9285
|
+
const m = /^\s*(\d+)\s+(\d+)\s+(\d+)\s+(\S+)\s*(.*)$/.exec(line);
|
|
9286
|
+
if (!m || m[4]?.startsWith("Z"))
|
|
9287
|
+
continue;
|
|
9288
|
+
procs.push({
|
|
9289
|
+
pid: Number(m[1]),
|
|
9290
|
+
ppid: Number(m[2]),
|
|
9291
|
+
pgid: Number(m[3]),
|
|
9292
|
+
command: m[5] ?? ""
|
|
9293
|
+
});
|
|
9294
|
+
}
|
|
9295
|
+
return procs;
|
|
9296
|
+
}
|
|
9297
|
+
async function cwdInside(dir) {
|
|
9298
|
+
const inside = (p) => p === dir || p.startsWith(`${dir}/`);
|
|
9299
|
+
if (process.platform === "linux") {
|
|
9300
|
+
const pids = [];
|
|
9301
|
+
for (const name of readdirSync4("/proc")) {
|
|
9302
|
+
if (!/^\d+$/.test(name))
|
|
9303
|
+
continue;
|
|
9304
|
+
try {
|
|
9305
|
+
if (inside(readlinkSync2(`/proc/${name}/cwd`)))
|
|
9306
|
+
pids.push(Number(name));
|
|
9307
|
+
} catch {}
|
|
9308
|
+
}
|
|
9309
|
+
return pids;
|
|
9310
|
+
}
|
|
9311
|
+
if (process.platform === "darwin") {
|
|
9312
|
+
const pids = [];
|
|
9313
|
+
let pid = 0;
|
|
9314
|
+
for (const line of (await output("lsof", ["-d", "cwd", "-Fpn", "-w"])).split(`
|
|
9315
|
+
`)) {
|
|
9316
|
+
if (line.startsWith("p"))
|
|
9317
|
+
pid = Number(line.slice(1));
|
|
9318
|
+
else if (line.startsWith("n") && inside(line.slice(1)))
|
|
9319
|
+
pids.push(pid);
|
|
9320
|
+
}
|
|
9321
|
+
return pids;
|
|
9322
|
+
}
|
|
9323
|
+
throw new Error(`finding processes by cwd is not supported on ${process.platform}`);
|
|
9324
|
+
}
|
|
9325
|
+
function alive(pid) {
|
|
9326
|
+
try {
|
|
9327
|
+
process.kill(pid, 0);
|
|
9328
|
+
return true;
|
|
9329
|
+
} catch {
|
|
9330
|
+
return false;
|
|
9331
|
+
}
|
|
9332
|
+
}
|
|
9333
|
+
function signal(pids, group, sig) {
|
|
9334
|
+
for (const p of group === undefined ? pids : [...pids, -group])
|
|
9335
|
+
try {
|
|
9336
|
+
process.kill(p, sig);
|
|
9337
|
+
} catch {}
|
|
9338
|
+
}
|
|
9339
|
+
async function stopLeftovers(opts) {
|
|
9340
|
+
const workdir = realpathSync2(opts.workdir);
|
|
9341
|
+
const [procs, inWorkdir] = await Promise.all([
|
|
9342
|
+
processes(),
|
|
9343
|
+
cwdInside(workdir)
|
|
9344
|
+
]);
|
|
9345
|
+
const found = new Set(inWorkdir);
|
|
9346
|
+
if (opts.group !== undefined) {
|
|
9347
|
+
for (const p of procs)
|
|
9348
|
+
if (p.pgid === opts.group)
|
|
9349
|
+
found.add(p.pid);
|
|
9350
|
+
}
|
|
9351
|
+
for (let grew = true;grew; ) {
|
|
9352
|
+
grew = false;
|
|
9353
|
+
for (const p of procs)
|
|
9354
|
+
if (found.has(p.ppid) && !found.has(p.pid)) {
|
|
9355
|
+
found.add(p.pid);
|
|
9356
|
+
grew = true;
|
|
9357
|
+
}
|
|
9358
|
+
}
|
|
9359
|
+
found.delete(process.pid);
|
|
9360
|
+
const leftover = procs.filter((p) => found.has(p.pid)).map((p) => ({ pid: p.pid, command: printable(p.command, 200) }));
|
|
9361
|
+
const pids = leftover.map((l) => l.pid);
|
|
9362
|
+
signal(pids, opts.group, "SIGTERM");
|
|
9363
|
+
const deadline = Date.now() + TERM_WAIT_MS;
|
|
9364
|
+
while (pids.some(alive) && Date.now() < deadline)
|
|
9365
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
9366
|
+
signal(pids.filter(alive), opts.group, "SIGKILL");
|
|
9367
|
+
if (opts.group !== undefined)
|
|
9368
|
+
forgetGroup(opts.group);
|
|
9369
|
+
return leftover;
|
|
9370
|
+
}
|
|
9371
|
+
|
|
9372
|
+
// src/session.ts
|
|
9373
|
+
import { randomUUID } from "node:crypto";
|
|
9374
|
+
import { writeFileSync as writeFileSync3 } from "node:fs";
|
|
9375
|
+
|
|
9376
|
+
// src/atif.ts
|
|
9377
|
+
var MAX_BLOCK_CHARS = 50000;
|
|
9378
|
+
function flattenContent(value) {
|
|
9379
|
+
if (typeof value === "string")
|
|
9380
|
+
return value;
|
|
9381
|
+
if (Array.isArray(value)) {
|
|
9382
|
+
return value.map((item) => {
|
|
9383
|
+
if (typeof item === "string")
|
|
9384
|
+
return item;
|
|
9385
|
+
if (item && typeof item === "object") {
|
|
9386
|
+
const obj = item;
|
|
9387
|
+
if (typeof obj.text === "string")
|
|
9388
|
+
return obj.text;
|
|
9389
|
+
if (typeof obj.content === "string")
|
|
9390
|
+
return obj.content;
|
|
9391
|
+
}
|
|
9392
|
+
return JSON.stringify(item);
|
|
9393
|
+
}).join(`
|
|
9394
|
+
`);
|
|
9395
|
+
}
|
|
9396
|
+
if (value == null)
|
|
9397
|
+
return "";
|
|
9398
|
+
return JSON.stringify(value);
|
|
9399
|
+
}
|
|
9400
|
+
function cap(text) {
|
|
9401
|
+
return text.length > MAX_BLOCK_CHARS ? text.slice(0, MAX_BLOCK_CHARS) : text;
|
|
9402
|
+
}
|
|
9403
|
+
function shellQuote2(path) {
|
|
9404
|
+
return `'${path.replace(/'/g, `'\\''`)}'`;
|
|
9405
|
+
}
|
|
9406
|
+
var CAPPED_STRING_FIELDS = [
|
|
9407
|
+
"content",
|
|
9408
|
+
"old_string",
|
|
9409
|
+
"new_string",
|
|
9410
|
+
"command",
|
|
9411
|
+
"input"
|
|
9412
|
+
];
|
|
9413
|
+
function capToolInput(args) {
|
|
9414
|
+
const input = args ?? {};
|
|
9415
|
+
const serialized = JSON.stringify(input);
|
|
9416
|
+
if (serialized.length <= MAX_BLOCK_CHARS)
|
|
9417
|
+
return input;
|
|
9418
|
+
if (input && typeof input === "object") {
|
|
9419
|
+
const obj = input;
|
|
9420
|
+
const capped = {};
|
|
9421
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
9422
|
+
if (CAPPED_STRING_FIELDS.includes(key) && typeof value === "string") {
|
|
9423
|
+
capped[key] = cap(value);
|
|
9424
|
+
} else if (key === "edits" && Array.isArray(value)) {
|
|
9425
|
+
capped[key] = value.map((edit) => edit && typeof edit === "object" ? {
|
|
9426
|
+
...edit,
|
|
9427
|
+
old_string: typeof edit.old_string === "string" ? cap(edit.old_string) : edit.old_string,
|
|
9428
|
+
new_string: typeof edit.new_string === "string" ? cap(edit.new_string) : edit.new_string
|
|
9429
|
+
} : edit);
|
|
9430
|
+
} else {
|
|
9431
|
+
capped[key] = value;
|
|
9432
|
+
}
|
|
9433
|
+
}
|
|
9434
|
+
if (JSON.stringify(capped).length <= MAX_BLOCK_CHARS)
|
|
9435
|
+
return capped;
|
|
9436
|
+
}
|
|
9437
|
+
return { _truncated: serialized.slice(0, MAX_BLOCK_CHARS) };
|
|
9438
|
+
}
|
|
9439
|
+
var PATCH_FILE_MARKERS = [
|
|
9440
|
+
{ re: /^\*\*\* Update File: (.+)$/, op: "modify" },
|
|
9441
|
+
{ re: /^\*\*\* Add File: (.+)$/, op: "add" },
|
|
9442
|
+
{ re: /^\*\*\* Delete File: (.+)$/, op: "delete" }
|
|
9443
|
+
];
|
|
9444
|
+
function parseCodexPatch(patch) {
|
|
9445
|
+
if (typeof patch !== "string" || patch.length === 0)
|
|
9446
|
+
return null;
|
|
9447
|
+
const lines = patch.split(`
|
|
9448
|
+
`);
|
|
9449
|
+
const sections = [];
|
|
9450
|
+
let current = null;
|
|
9451
|
+
for (const line of lines) {
|
|
9452
|
+
if (line === "*** Begin Patch")
|
|
9453
|
+
continue;
|
|
9454
|
+
if (line === "*** End Patch")
|
|
9455
|
+
break;
|
|
9456
|
+
const marker = PATCH_FILE_MARKERS.map((m) => ({
|
|
9457
|
+
m,
|
|
9458
|
+
hit: line.match(m.re)
|
|
9459
|
+
})).find((x) => x.hit);
|
|
9460
|
+
if (marker) {
|
|
9461
|
+
current = {
|
|
9462
|
+
op: marker.m.op,
|
|
9463
|
+
path: marker.hit[1],
|
|
9464
|
+
body: []
|
|
9465
|
+
};
|
|
9466
|
+
sections.push(current);
|
|
9467
|
+
continue;
|
|
9468
|
+
}
|
|
9469
|
+
const move = line.match(/^\*\*\* Move to: (.+)$/);
|
|
9470
|
+
if (move && current) {
|
|
9471
|
+
current.renameTo = move[1];
|
|
9472
|
+
continue;
|
|
9473
|
+
}
|
|
9474
|
+
if (line.startsWith("*** "))
|
|
9475
|
+
continue;
|
|
9476
|
+
if (current)
|
|
9477
|
+
current.body.push(line);
|
|
9478
|
+
}
|
|
9479
|
+
if (sections.length === 0)
|
|
9480
|
+
return null;
|
|
9481
|
+
return sections;
|
|
9482
|
+
}
|
|
9483
|
+
function reconstructHunk(hunkLines) {
|
|
9484
|
+
const oldLines = [];
|
|
9485
|
+
const newLines = [];
|
|
9486
|
+
for (const line of hunkLines) {
|
|
9487
|
+
const marker = line[0];
|
|
9488
|
+
if (marker === "-") {
|
|
9489
|
+
oldLines.push(line.slice(1));
|
|
9490
|
+
} else if (marker === "+") {
|
|
9491
|
+
newLines.push(line.slice(1));
|
|
9492
|
+
} else if (marker === " ") {
|
|
9493
|
+
oldLines.push(line.slice(1));
|
|
9494
|
+
newLines.push(line.slice(1));
|
|
9495
|
+
} else if (line === "") {
|
|
9496
|
+
oldLines.push("");
|
|
9497
|
+
newLines.push("");
|
|
9498
|
+
}
|
|
9499
|
+
}
|
|
9500
|
+
return { old_string: oldLines.join(`
|
|
9501
|
+
`), new_string: newLines.join(`
|
|
9502
|
+
`) };
|
|
9503
|
+
}
|
|
9504
|
+
function splitHunks(body) {
|
|
9505
|
+
const hunks = [];
|
|
9506
|
+
let current = null;
|
|
9507
|
+
for (const line of body) {
|
|
9508
|
+
if (line.startsWith("@@")) {
|
|
9509
|
+
current = [];
|
|
9510
|
+
hunks.push(current);
|
|
9511
|
+
continue;
|
|
9512
|
+
}
|
|
9513
|
+
if (current)
|
|
9514
|
+
current.push(line);
|
|
9515
|
+
}
|
|
9516
|
+
return hunks.filter((h) => h.length > 0);
|
|
9517
|
+
}
|
|
9518
|
+
function sectionToBlock(section) {
|
|
9519
|
+
if (section.op === "delete") {
|
|
9520
|
+
return [
|
|
9521
|
+
{
|
|
9522
|
+
name: "Bash",
|
|
9523
|
+
input: { command: `rm -f -- ${shellQuote2(section.path)}` }
|
|
9524
|
+
}
|
|
9525
|
+
];
|
|
9526
|
+
}
|
|
9527
|
+
if (section.op === "add") {
|
|
9528
|
+
const content = section.body.filter((l) => l.startsWith("+")).map((l) => l.slice(1)).join(`
|
|
9529
|
+
`);
|
|
9530
|
+
return [{ name: "Write", input: { file_path: section.path, content } }];
|
|
9531
|
+
}
|
|
9532
|
+
const targetPath = section.renameTo ?? section.path;
|
|
9533
|
+
const rename = section.renameTo ? [
|
|
9534
|
+
{
|
|
9535
|
+
name: "Bash",
|
|
9536
|
+
input: {
|
|
9537
|
+
command: `mv -- ${shellQuote2(section.path)} ${shellQuote2(section.renameTo)}`
|
|
9538
|
+
}
|
|
9539
|
+
}
|
|
9540
|
+
] : [];
|
|
9541
|
+
const edits = splitHunks(section.body).map(reconstructHunk);
|
|
9542
|
+
if (edits.length === 0)
|
|
9543
|
+
return rename;
|
|
9544
|
+
const [only] = edits;
|
|
9545
|
+
if (edits.length === 1 && only) {
|
|
9546
|
+
return [
|
|
9547
|
+
...rename,
|
|
9548
|
+
{
|
|
9549
|
+
name: "Edit",
|
|
9550
|
+
input: {
|
|
9551
|
+
file_path: targetPath,
|
|
9552
|
+
old_string: only.old_string,
|
|
9553
|
+
new_string: only.new_string
|
|
9554
|
+
}
|
|
9555
|
+
}
|
|
9556
|
+
];
|
|
9557
|
+
}
|
|
9558
|
+
return [
|
|
9559
|
+
...rename,
|
|
9560
|
+
{ name: "MultiEdit", input: { file_path: targetPath, edits } }
|
|
9561
|
+
];
|
|
9562
|
+
}
|
|
9563
|
+
var str3 = (v) => typeof v === "string";
|
|
9564
|
+
function withReadRange(input, a) {
|
|
9565
|
+
if (typeof a.offset === "number")
|
|
9566
|
+
input.offset = a.offset;
|
|
9567
|
+
if (typeof a.limit === "number")
|
|
9568
|
+
input.limit = a.limit;
|
|
9569
|
+
return input;
|
|
9570
|
+
}
|
|
9571
|
+
var TOOL_TRANSFORMS = {
|
|
9572
|
+
shellToolCall: (a) => str3(a.command) ? { name: "Bash", input: { command: a.command } } : null,
|
|
9573
|
+
readToolCall: (a) => str3(a.path) ? { name: "Read", input: { file_path: a.path } } : null,
|
|
9574
|
+
editToolCall: (a) => str3(a.path) ? {
|
|
9575
|
+
name: "Write",
|
|
9576
|
+
input: { file_path: a.path, content: a.streamContent }
|
|
9577
|
+
} : null,
|
|
9578
|
+
globToolCall: (a) => str3(a.globPattern) ? {
|
|
9579
|
+
name: "Glob",
|
|
9580
|
+
input: { pattern: a.globPattern, path: a.targetDirectory }
|
|
9581
|
+
} : null,
|
|
9582
|
+
grepToolCall: (a) => str3(a.pattern) ? { name: "Grep", input: { pattern: a.pattern, path: a.path } } : null,
|
|
9583
|
+
webFetchToolCall: (a) => str3(a.url) ? { name: "WebFetch", input: { url: a.url } } : null,
|
|
9584
|
+
webSearchToolCall: (a) => str3(a.searchTerm) ? { name: "WebSearch", input: { query: a.searchTerm } } : null,
|
|
9585
|
+
bash: (a) => str3(a.command) ? { name: "Bash", input: { command: a.command } } : null,
|
|
9586
|
+
glob: (a) => str3(a.pattern) ? { name: "Glob", input: { pattern: a.pattern, path: a.path } } : null,
|
|
9587
|
+
grep: (a) => str3(a.pattern) ? { name: "Grep", input: { pattern: a.pattern, path: a.path } } : null,
|
|
9588
|
+
webfetch: (a) => str3(a.url) ? { name: "WebFetch", input: { url: a.url } } : null,
|
|
9589
|
+
todowrite: (a) => Array.isArray(a.todos) ? {
|
|
9590
|
+
name: "TodoWrite",
|
|
9591
|
+
input: {
|
|
9592
|
+
todos: a.todos.map((t) => ({
|
|
9593
|
+
content: t.content,
|
|
9594
|
+
status: t.status
|
|
9595
|
+
}))
|
|
9596
|
+
}
|
|
9597
|
+
} : null,
|
|
9598
|
+
read: (a) => {
|
|
9599
|
+
const filePath = str3(a.filePath) ? a.filePath : str3(a.path) ? a.path : null;
|
|
9600
|
+
return filePath ? { name: "Read", input: withReadRange({ file_path: filePath }, a) } : null;
|
|
9601
|
+
},
|
|
9602
|
+
exec: (a) => str3(a.command) ? { name: "Bash", input: { command: a.command } } : null,
|
|
9603
|
+
write: (a) => str3(a.path) ? { name: "Write", input: { file_path: a.path, content: a.content } } : null,
|
|
9604
|
+
edit: (a) => {
|
|
9605
|
+
if (!str3(a.path) || !Array.isArray(a.edits) || a.edits.length === 0)
|
|
9606
|
+
return null;
|
|
9607
|
+
if (a.edits.length === 1) {
|
|
9608
|
+
const e = a.edits[0];
|
|
9609
|
+
return {
|
|
9610
|
+
name: "Edit",
|
|
9611
|
+
input: {
|
|
9612
|
+
file_path: a.path,
|
|
9613
|
+
old_string: e.oldText,
|
|
9614
|
+
new_string: e.newText
|
|
9615
|
+
}
|
|
9616
|
+
};
|
|
9617
|
+
}
|
|
9618
|
+
return {
|
|
9619
|
+
name: "MultiEdit",
|
|
9620
|
+
input: {
|
|
9621
|
+
file_path: a.path,
|
|
9622
|
+
edits: a.edits.map((e) => ({
|
|
9623
|
+
old_string: e.oldText,
|
|
9624
|
+
new_string: e.newText
|
|
9625
|
+
}))
|
|
9626
|
+
}
|
|
9627
|
+
};
|
|
9628
|
+
},
|
|
9629
|
+
web_fetch: (a) => str3(a.url) ? { name: "WebFetch", input: { url: a.url } } : null,
|
|
9630
|
+
update_plan: (a) => Array.isArray(a.plan) ? {
|
|
9631
|
+
name: "TodoWrite",
|
|
9632
|
+
input: {
|
|
9633
|
+
todos: a.plan.map((p) => ({
|
|
9634
|
+
content: p.step,
|
|
9635
|
+
status: p.status
|
|
9636
|
+
}))
|
|
9637
|
+
}
|
|
9638
|
+
} : null,
|
|
9639
|
+
shell: (a) => str3(a.command) && (a.action === undefined || a.action === "run") ? { name: "Bash", input: { command: a.command } } : null,
|
|
9640
|
+
read_file: (a) => {
|
|
9641
|
+
if (!str3(a.path))
|
|
9642
|
+
return null;
|
|
9643
|
+
const input = { file_path: a.path };
|
|
9644
|
+
if (typeof a.start_line === "number")
|
|
9645
|
+
input.offset = a.start_line;
|
|
9646
|
+
if (typeof a.line_count === "number")
|
|
9647
|
+
input.limit = a.line_count;
|
|
9648
|
+
return { name: "Read", input };
|
|
9649
|
+
},
|
|
9650
|
+
write_file: (a) => str3(a.path) ? { name: "Write", input: { file_path: a.path, content: a.content } } : null,
|
|
9651
|
+
edit_file: (a) => str3(a.path) ? {
|
|
9652
|
+
name: "Edit",
|
|
9653
|
+
input: {
|
|
9654
|
+
file_path: a.path,
|
|
9655
|
+
old_string: a.old_string,
|
|
9656
|
+
new_string: a.new_string
|
|
9657
|
+
}
|
|
9658
|
+
} : null,
|
|
9659
|
+
glob_files: (a) => str3(a.pattern) ? { name: "Glob", input: { pattern: a.pattern, path: a.path } } : null,
|
|
9660
|
+
grep_files: (a) => str3(a.pattern) ? { name: "Grep", input: { pattern: a.pattern, path: a.path } } : null,
|
|
9661
|
+
exa_search: (a) => str3(a.query) ? { name: "WebSearch", input: { query: a.query } } : null
|
|
9662
|
+
};
|
|
9663
|
+
function normalizeToolCall(functionName, args) {
|
|
9664
|
+
const obj = args && typeof args === "object" ? args : null;
|
|
9665
|
+
if (functionName === "exec_command" && obj && "cmd" in obj) {
|
|
9666
|
+
const { cmd, ...rest } = obj;
|
|
9667
|
+
return [{ name: "Bash", input: { ...rest, command: cmd } }];
|
|
9668
|
+
}
|
|
9669
|
+
if (functionName === "apply_patch" && obj) {
|
|
9670
|
+
const rawPatch = obj.input ?? obj.patchText;
|
|
9671
|
+
const sections = parseCodexPatch(rawPatch);
|
|
9672
|
+
if (sections) {
|
|
9673
|
+
const blocks = sections.flatMap(sectionToBlock);
|
|
9674
|
+
if (blocks.length > 0)
|
|
9675
|
+
return blocks;
|
|
9676
|
+
}
|
|
9677
|
+
return [{ name: functionName, input: args }];
|
|
9678
|
+
}
|
|
9679
|
+
if (functionName === "web_search_call" && obj) {
|
|
9680
|
+
const actionType = obj.action_type;
|
|
9681
|
+
if (actionType === "open_page")
|
|
9682
|
+
return [{ name: "WebFetch", input: obj }];
|
|
9683
|
+
if (actionType === "search") {
|
|
9684
|
+
if ("query" in obj && typeof obj.query === "string")
|
|
9685
|
+
return [{ name: "WebSearch", input: obj }];
|
|
9686
|
+
const altKey = ["queries", "q", "search_query"].find((k) => (k in obj));
|
|
9687
|
+
if (altKey) {
|
|
9688
|
+
const { [altKey]: raw, ...rest } = obj;
|
|
9689
|
+
const query = Array.isArray(raw) ? raw.find((q) => typeof q === "string") : raw;
|
|
9690
|
+
if (typeof query !== "string")
|
|
9691
|
+
return [{ name: functionName, input: args }];
|
|
9692
|
+
const preserved = Array.isArray(raw) ? { queries: raw } : {};
|
|
9693
|
+
return [{ name: "WebSearch", input: { ...rest, ...preserved, query } }];
|
|
9694
|
+
}
|
|
9695
|
+
return [{ name: functionName, input: args }];
|
|
9696
|
+
}
|
|
9697
|
+
return [{ name: functionName, input: args }];
|
|
9698
|
+
}
|
|
9699
|
+
if (functionName && functionName in TOOL_TRANSFORMS && obj) {
|
|
9700
|
+
const block = TOOL_TRANSFORMS[functionName](obj);
|
|
9701
|
+
if (block)
|
|
9702
|
+
return [block];
|
|
9703
|
+
}
|
|
9704
|
+
return [{ name: functionName, input: args }];
|
|
9705
|
+
}
|
|
9706
|
+
function toolCallId(tc, step, index) {
|
|
9707
|
+
if (tc.tool_call_id)
|
|
9708
|
+
return tc.tool_call_id;
|
|
9709
|
+
return `call_${step.step_id}_${tc.function_name ?? "tool"}_${index}`;
|
|
9710
|
+
}
|
|
9711
|
+
var HARBOR_PLACEHOLDER_MESSAGES = new Set([
|
|
9712
|
+
"(tool use)",
|
|
9713
|
+
"(no assistant text)"
|
|
9714
|
+
]);
|
|
9715
|
+
function isExecutedRestatement(message, toolCalls) {
|
|
9716
|
+
const trimmed = message.trimStart();
|
|
9717
|
+
if (!trimmed.startsWith("Executed "))
|
|
9718
|
+
return false;
|
|
9719
|
+
const rest = trimmed.slice("Executed ".length);
|
|
9720
|
+
return toolCalls.some((tc) => tc.function_name && rest.startsWith(tc.function_name));
|
|
9721
|
+
}
|
|
9722
|
+
var ENVELOPE_META = [
|
|
9723
|
+
/^Chunk ID:/,
|
|
9724
|
+
/^Wall time:/,
|
|
9725
|
+
/^Original token count:/,
|
|
9726
|
+
/^Process exited with code /,
|
|
9727
|
+
/^Exit code:/
|
|
9728
|
+
];
|
|
9729
|
+
function stripCodexResultEnvelope(content) {
|
|
9730
|
+
const lines = content.split(`
|
|
9731
|
+
`);
|
|
9732
|
+
const outputIdx = lines.findIndex((l) => l === "Output:");
|
|
9733
|
+
if (outputIdx === -1)
|
|
9734
|
+
return content;
|
|
9735
|
+
const preamble = lines.slice(0, outputIdx);
|
|
9736
|
+
if (!preamble.some((l) => ENVELOPE_META.some((re) => re.test(l))))
|
|
9737
|
+
return content;
|
|
9738
|
+
let exitCode = 0;
|
|
9739
|
+
for (const line of preamble) {
|
|
9740
|
+
const m = line.match(/^(?:Process exited with code|Exit code:) (\d+)$/);
|
|
9741
|
+
if (m)
|
|
9742
|
+
exitCode = Number(m[1]);
|
|
9743
|
+
}
|
|
9744
|
+
const output = lines.slice(outputIdx + 1).join(`
|
|
9745
|
+
`);
|
|
9746
|
+
return exitCode === 0 ? output : `[exit code ${exitCode}]
|
|
9747
|
+
${output}`;
|
|
9748
|
+
}
|
|
9749
|
+
function convertAtifToSession(trajectory) {
|
|
9750
|
+
if (!Array.isArray(trajectory?.steps)) {
|
|
9751
|
+
throw new Error("atif: the trajectory has no steps array");
|
|
9752
|
+
}
|
|
9753
|
+
const events = [];
|
|
9754
|
+
for (const step of trajectory.steps) {
|
|
9755
|
+
if (step.source === "system")
|
|
9756
|
+
continue;
|
|
9757
|
+
if (step.source === "user") {
|
|
9758
|
+
const userText = flattenContent(step.message);
|
|
9759
|
+
if (userText.includes("<environment_context>"))
|
|
9760
|
+
continue;
|
|
9761
|
+
events.push({
|
|
9762
|
+
uuid: `atif-${step.step_id}-user`,
|
|
9763
|
+
type: "user",
|
|
9764
|
+
timestamp: step.timestamp,
|
|
9765
|
+
message: { role: "user", content: cap(userText) }
|
|
9766
|
+
});
|
|
9767
|
+
continue;
|
|
9768
|
+
}
|
|
9769
|
+
if (step.source !== "agent")
|
|
9770
|
+
continue;
|
|
9771
|
+
const toolCalls = step.tool_calls ?? [];
|
|
9772
|
+
const blocks = [];
|
|
9773
|
+
const reasoning = flattenContent(step.reasoning_content);
|
|
9774
|
+
if (reasoning)
|
|
9775
|
+
blocks.push({ type: "thinking", thinking: cap(reasoning) });
|
|
9776
|
+
const messageText = flattenContent(step.message);
|
|
9777
|
+
if (messageText && !isExecutedRestatement(messageText, toolCalls) && !HARBOR_PLACEHOLDER_MESSAGES.has(messageText.trim())) {
|
|
9778
|
+
blocks.push({ type: "text", text: cap(messageText) });
|
|
9779
|
+
}
|
|
9780
|
+
const toolUseIds = [];
|
|
9781
|
+
for (let i = 0;i < toolCalls.length; i++) {
|
|
9782
|
+
const tc = toolCalls[i];
|
|
9783
|
+
const id = toolCallId(tc, step, i);
|
|
9784
|
+
toolUseIds.push(id);
|
|
9785
|
+
const split = normalizeToolCall(tc.function_name, tc.arguments);
|
|
9786
|
+
split.forEach(({ name, input }, k) => {
|
|
9787
|
+
const isLast = k === split.length - 1;
|
|
9788
|
+
blocks.push({
|
|
9789
|
+
type: "tool_use",
|
|
9790
|
+
id: isLast ? id : `${id}__patch${k}`,
|
|
9791
|
+
name,
|
|
9792
|
+
input: capToolInput(input)
|
|
9793
|
+
});
|
|
9794
|
+
});
|
|
9795
|
+
}
|
|
9796
|
+
if (blocks.length > 0) {
|
|
9797
|
+
events.push({
|
|
9798
|
+
uuid: `atif-${step.step_id}-assistant`,
|
|
9799
|
+
type: "assistant",
|
|
9800
|
+
timestamp: step.timestamp,
|
|
9801
|
+
message: {
|
|
9802
|
+
id: `atif-step-${step.step_id}`,
|
|
9803
|
+
role: "assistant",
|
|
9804
|
+
model: step.model_name,
|
|
9805
|
+
content: blocks
|
|
9806
|
+
}
|
|
9807
|
+
});
|
|
9808
|
+
}
|
|
9809
|
+
const results = step.observation?.results;
|
|
9810
|
+
if (results && results.length > 0) {
|
|
9811
|
+
const resultBlocks = results.map((result, i) => {
|
|
9812
|
+
const content = result.subagent_trajectory_ref ? `[subagent trajectory: ${result.subagent_trajectory_ref}]` : stripCodexResultEnvelope(flattenContent(result.content));
|
|
9813
|
+
return {
|
|
9814
|
+
type: "tool_result",
|
|
9815
|
+
tool_use_id: result.source_call_id || toolUseIds[i] || `call_${step.step_id}_result${i}`,
|
|
9816
|
+
content: cap(content)
|
|
9817
|
+
};
|
|
9818
|
+
});
|
|
9819
|
+
events.push({
|
|
9820
|
+
uuid: `atif-${step.step_id}-results`,
|
|
9821
|
+
type: "user",
|
|
9822
|
+
timestamp: step.timestamp,
|
|
9823
|
+
message: { role: "user", content: resultBlocks }
|
|
9824
|
+
});
|
|
9825
|
+
}
|
|
9826
|
+
}
|
|
9827
|
+
return events;
|
|
9828
|
+
}
|
|
9829
|
+
|
|
9830
|
+
// src/session.ts
|
|
9831
|
+
var UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
9832
|
+
function convertedSessionId(harnessId) {
|
|
9833
|
+
return UUID.test(harnessId) ? harnessId.toLowerCase() : randomUUID();
|
|
9834
|
+
}
|
|
9835
|
+
function toTrajectory(t) {
|
|
9836
|
+
const steps = [{ step_id: 1, source: "user", message: t.prompt }];
|
|
9837
|
+
const agent = (fields) => {
|
|
9838
|
+
const step = {
|
|
9839
|
+
step_id: steps.length + 1,
|
|
9840
|
+
source: "agent",
|
|
9841
|
+
message: "",
|
|
9842
|
+
...t.model === null ? {} : { model_name: t.model },
|
|
9843
|
+
...fields
|
|
9844
|
+
};
|
|
9845
|
+
steps.push(step);
|
|
9846
|
+
return step;
|
|
9847
|
+
};
|
|
9848
|
+
const calls = new Map;
|
|
9849
|
+
const failed = new Set;
|
|
9850
|
+
let lastText = null;
|
|
9851
|
+
for (const s of t.steps) {
|
|
9852
|
+
if (s.kind === "text") {
|
|
9853
|
+
agent({ message: s.text });
|
|
9854
|
+
lastText = s.text;
|
|
9855
|
+
} else if (s.kind === "tool_call") {
|
|
9856
|
+
const id = s.id ?? `firegoat_call_${steps.length + 1}`;
|
|
9857
|
+
const step = agent({
|
|
9858
|
+
tool_calls: [
|
|
9859
|
+
{ tool_call_id: id, function_name: s.name, arguments: s.input }
|
|
9860
|
+
]
|
|
9861
|
+
});
|
|
9862
|
+
calls.set(id, step);
|
|
9863
|
+
} else {
|
|
9864
|
+
const call = s.callId === null ? undefined : calls.get(s.callId);
|
|
9865
|
+
const id = s.callId ?? `firegoat_result_${steps.length + 1}`;
|
|
9866
|
+
const result = { source_call_id: id, content: s.text };
|
|
9867
|
+
if (!call)
|
|
9868
|
+
agent({ observation: { results: [result] } });
|
|
9869
|
+
else if (call.observation)
|
|
9870
|
+
call.observation.results.push(result);
|
|
9871
|
+
else
|
|
9872
|
+
call.observation = { results: [result] };
|
|
9873
|
+
if (s.isError)
|
|
9874
|
+
failed.add(id);
|
|
9875
|
+
}
|
|
9876
|
+
}
|
|
9877
|
+
if (t.final !== "" && t.final !== lastText)
|
|
9878
|
+
agent({ message: t.final });
|
|
9879
|
+
return {
|
|
9880
|
+
trajectory: {
|
|
9881
|
+
schema_version: "ATIF-v1.6",
|
|
9882
|
+
session_id: t.sessionId,
|
|
9883
|
+
agent: {
|
|
9884
|
+
name: t.harness,
|
|
9885
|
+
...t.version === null ? {} : { version: t.version },
|
|
9886
|
+
...t.model === null ? {} : { model_name: t.model }
|
|
9887
|
+
},
|
|
9888
|
+
steps
|
|
9889
|
+
},
|
|
9890
|
+
failed
|
|
9891
|
+
};
|
|
9892
|
+
}
|
|
9893
|
+
function toClaudeSession(t) {
|
|
9894
|
+
const { trajectory, failed } = toTrajectory(t);
|
|
9895
|
+
return convertAtifToSession(trajectory).map((e) => {
|
|
9896
|
+
const content = e.message?.content;
|
|
9897
|
+
if (Array.isArray(content)) {
|
|
9898
|
+
for (const b of content)
|
|
9899
|
+
if (b.type === "tool_result" && failed.has(b.tool_use_id))
|
|
9900
|
+
b.is_error = true;
|
|
9901
|
+
}
|
|
9902
|
+
return { ...e, sessionId: t.sessionId };
|
|
9903
|
+
});
|
|
9904
|
+
}
|
|
9905
|
+
function writeClaudeSession(path, t) {
|
|
9906
|
+
writeFileSync3(path, toClaudeSession(t).map((e) => `${JSON.stringify(e)}
|
|
9907
|
+
`).join(""));
|
|
9908
|
+
}
|
|
9909
|
+
|
|
9156
9910
|
// src/steps.ts
|
|
9157
9911
|
import { createWriteStream } from "node:fs";
|
|
9158
9912
|
var STEP_TIMEOUT_MS = 600000;
|
|
@@ -9287,7 +10041,7 @@ async function sendTelemetry(event, exitCode, startedAt) {
|
|
|
9287
10041
|
}
|
|
9288
10042
|
|
|
9289
10043
|
// src/transcript.ts
|
|
9290
|
-
import { readdirSync as
|
|
10044
|
+
import { readdirSync as readdirSync5, statSync as statSync2, writeFileSync as writeFileSync4 } from "node:fs";
|
|
9291
10045
|
import { join as join8, relative } from "node:path";
|
|
9292
10046
|
var OUTPUT_LINES = 15;
|
|
9293
10047
|
var OUTPUT_CHARS = 1500;
|
|
@@ -9337,7 +10091,7 @@ function describeToolCall(step, c) {
|
|
|
9337
10091
|
function workdirFiles(dir) {
|
|
9338
10092
|
const out = [];
|
|
9339
10093
|
const walk = (d) => {
|
|
9340
|
-
for (const name of
|
|
10094
|
+
for (const name of readdirSync5(d)) {
|
|
9341
10095
|
if (SKIP_DIRS.has(name))
|
|
9342
10096
|
continue;
|
|
9343
10097
|
const p = join8(d, name);
|
|
@@ -9371,7 +10125,7 @@ ${indent(truncate(s.text, OUTPUT_LINES, OUTPUT_CHARS))}` : ` → ${status}`);
|
|
|
9371
10125
|
out.pop();
|
|
9372
10126
|
out.push("", "## final answer", final ? prose(final) : "(none)");
|
|
9373
10127
|
out.push("", "## files in the workdir", ...workdirFiles(opts.workdir));
|
|
9374
|
-
|
|
10128
|
+
writeFileSync4(opts.path, `${out.join(`
|
|
9375
10129
|
`)}
|
|
9376
10130
|
`);
|
|
9377
10131
|
}
|
|
@@ -9420,7 +10174,7 @@ function runTag(harness, model) {
|
|
|
9420
10174
|
return parts.length > 0 ? ` [${parts.join(" ")}]` : "";
|
|
9421
10175
|
}
|
|
9422
10176
|
function notGradedWhy(reason) {
|
|
9423
|
-
const said = /claude
|
|
10177
|
+
const said = /claude reported an error(?: \([^)]*\))?: (.*)$/s.exec(reason);
|
|
9424
10178
|
if (said)
|
|
9425
10179
|
return `claude said: "${said[1]}"`;
|
|
9426
10180
|
return reason.replace(/^not graded: /, "");
|
|
@@ -9676,11 +10430,9 @@ function ttyUI(out, opts) {
|
|
|
9676
10430
|
|
|
9677
10431
|
// src/upload.ts
|
|
9678
10432
|
import { existsSync as existsSync5, readFileSync as readFileSync5 } from "node:fs";
|
|
9679
|
-
import { basename as basename2 } from "node:path";
|
|
9680
10433
|
var MAX_FILE = 20 * 1024 * 1024;
|
|
9681
10434
|
var MAX_CHECKS = 200;
|
|
9682
10435
|
var MAX_TEXT = 8192;
|
|
9683
|
-
var UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
9684
10436
|
var FILES = ["session.jsonl", "checks.txt", "stderr.txt"];
|
|
9685
10437
|
function clip(s, max, marker) {
|
|
9686
10438
|
return s.length > max ? `${s.slice(0, max - marker.length)}${marker}` : s;
|
|
@@ -9737,9 +10489,11 @@ async function put(name, entry, bytes) {
|
|
|
9737
10489
|
throw new ApiError(`PUT ${name} ${r.status}`, r.status);
|
|
9738
10490
|
}
|
|
9739
10491
|
async function uploadRun(t, r) {
|
|
9740
|
-
if (!r.
|
|
10492
|
+
if (!r.claudeSession)
|
|
9741
10493
|
return { status: "skipped", reason: "No session file" };
|
|
9742
|
-
const sessionId =
|
|
10494
|
+
const { sessionId } = r;
|
|
10495
|
+
if (sessionId === null)
|
|
10496
|
+
throw new Error(`run ${r.test}#${r.n} has a session file but no session id`);
|
|
9743
10497
|
if (!UUID.test(sessionId))
|
|
9744
10498
|
return {
|
|
9745
10499
|
status: "failed",
|
|
@@ -9751,7 +10505,7 @@ async function uploadRun(t, r) {
|
|
|
9751
10505
|
error: `${r.checks.length} checks, the upload takes at most ${MAX_CHECKS}`
|
|
9752
10506
|
};
|
|
9753
10507
|
const paths = {
|
|
9754
|
-
"session.jsonl": r.
|
|
10508
|
+
"session.jsonl": r.claudeSession,
|
|
9755
10509
|
"checks.txt": r.checksLog,
|
|
9756
10510
|
"stderr.txt": r.stderr
|
|
9757
10511
|
};
|
|
@@ -9792,6 +10546,7 @@ async function uploadRun(t, r) {
|
|
|
9792
10546
|
num_turns: r.num_turns,
|
|
9793
10547
|
cost: r.cost,
|
|
9794
10548
|
duration_ms: r.duration_ms,
|
|
10549
|
+
harness: r.harness,
|
|
9795
10550
|
model,
|
|
9796
10551
|
...git && { git }
|
|
9797
10552
|
},
|
|
@@ -9941,7 +10696,7 @@ function newRunId() {
|
|
|
9941
10696
|
return `${ts}-${randomBytes2(2).toString("hex")}`;
|
|
9942
10697
|
}
|
|
9943
10698
|
function writeRunJson(r) {
|
|
9944
|
-
|
|
10699
|
+
writeFileSync5(join10(r.dir, "run.json"), `${JSON.stringify(r, null, 2)}
|
|
9945
10700
|
`);
|
|
9946
10701
|
}
|
|
9947
10702
|
async function uploadPreflight(opts, ui, project, runGroup, git) {
|
|
@@ -9992,8 +10747,8 @@ function runFill(runId, n, testName, out, agentDir) {
|
|
|
9992
10747
|
FIREGOAT_HOME: join10(agentDir, "home")
|
|
9993
10748
|
};
|
|
9994
10749
|
}
|
|
9995
|
-
function renderHarnessArgs(test, vars, fill) {
|
|
9996
|
-
return test.harnessArgs.map((a) => renderValue(a.arg, vars, fill, a.path));
|
|
10750
|
+
function renderHarnessArgs(test, harness, vars, fill) {
|
|
10751
|
+
return (test.harnessArgs[harness] ?? []).map((a) => renderValue(a.arg, vars, fill, a.path));
|
|
9997
10752
|
}
|
|
9998
10753
|
function renderSteps(what, commands, vars, fill) {
|
|
9999
10754
|
return commands.map((c, i) => renderStep(c, vars, fill, `${what}[${i}]`));
|
|
@@ -10030,7 +10785,7 @@ function dryRun(opts) {
|
|
|
10030
10785
|
renderEnv(test.env, opts.vars, fill);
|
|
10031
10786
|
return {
|
|
10032
10787
|
...p,
|
|
10033
|
-
harnessArgs: renderHarnessArgs(test, opts.vars, fill),
|
|
10788
|
+
harnessArgs: renderHarnessArgs(test, p.harness, opts.vars, fill),
|
|
10034
10789
|
...renderRunSteps(test, opts.config, opts.vars, fill)
|
|
10035
10790
|
};
|
|
10036
10791
|
});
|
|
@@ -10042,7 +10797,7 @@ function dryRun(opts) {
|
|
|
10042
10797
|
const count = (n, what) => `${n} ${what}${n === 1 ? "" : "s"}`;
|
|
10043
10798
|
opts.say(`Dry run: ${count(runs.length, "run")}, build ${count(opts.config.build.length, "step")}. Nothing was started.`);
|
|
10044
10799
|
for (const r of runs)
|
|
10045
|
-
opts.say(`${r.test}#${r.n}${runTag(opts.showHarness ? r.harness : null, r.model)} install ${r.install.length}, checks ${r.checks.length}, cleanup ${r.cleanup.length}${r.harnessArgs.length > 0 ? `,
|
|
10800
|
+
opts.say(`${r.test}#${r.n}${runTag(opts.showHarness ? r.harness : null, r.model)} install ${r.install.length}, checks ${r.checks.length}, cleanup ${r.cleanup.length}${r.harnessArgs.length > 0 ? `, ${HARNESSES.get(r.harness).argsKey} ${JSON.stringify(r.harnessArgs)}` : ""}`);
|
|
10046
10801
|
}
|
|
10047
10802
|
async function runOne(opts) {
|
|
10048
10803
|
const { test, n } = opts;
|
|
@@ -10062,7 +10817,7 @@ async function runOne(opts) {
|
|
|
10062
10817
|
harness: opts.harness.name,
|
|
10063
10818
|
model: opts.model,
|
|
10064
10819
|
reportedModel: null,
|
|
10065
|
-
harnessArgs: renderHarnessArgs(test, opts.vars, fill),
|
|
10820
|
+
harnessArgs: renderHarnessArgs(test, opts.harness.name, opts.vars, fill),
|
|
10066
10821
|
status: "error",
|
|
10067
10822
|
reason: "",
|
|
10068
10823
|
prompt: render(test.prompt, opts.vars),
|
|
@@ -10072,11 +10827,14 @@ async function runOne(opts) {
|
|
|
10072
10827
|
workdir,
|
|
10073
10828
|
home,
|
|
10074
10829
|
session: null,
|
|
10830
|
+
claudeSession: null,
|
|
10831
|
+
sessionId: null,
|
|
10075
10832
|
transcript: join10(dir, "transcript.txt"),
|
|
10076
10833
|
stderr: join10(dir, "stderr.txt"),
|
|
10077
10834
|
checksLog: join10(dir, "checks.txt"),
|
|
10078
10835
|
cleanup: [],
|
|
10079
10836
|
cleanupLog: join10(dir, "cleanup.txt"),
|
|
10837
|
+
leftover: [],
|
|
10080
10838
|
num_turns: null,
|
|
10081
10839
|
cost: null,
|
|
10082
10840
|
duration_ms: 0,
|
|
@@ -10094,7 +10852,8 @@ async function runOne(opts) {
|
|
|
10094
10852
|
FIREGOAT_REPO: opts.repo
|
|
10095
10853
|
};
|
|
10096
10854
|
const steps = renderRunSteps(test, opts, opts.vars, fill);
|
|
10097
|
-
const
|
|
10855
|
+
const agent = {};
|
|
10856
|
+
const [status, reason] = await attempt(opts, r, steps, env, stepEnv, envFile, agent);
|
|
10098
10857
|
if (steps.cleanup.length > 0)
|
|
10099
10858
|
r.cleanup = await runAllSteps({
|
|
10100
10859
|
commands: steps.cleanup,
|
|
@@ -10102,6 +10861,7 @@ async function runOne(opts) {
|
|
|
10102
10861
|
env: stepEnv,
|
|
10103
10862
|
logPath: r.cleanupLog
|
|
10104
10863
|
});
|
|
10864
|
+
r.leftover = await stopLeftovers({ group: agent.group, workdir });
|
|
10105
10865
|
r.status = status;
|
|
10106
10866
|
r.reason = reason;
|
|
10107
10867
|
r.duration_ms = Date.now() - started;
|
|
@@ -10111,7 +10871,7 @@ async function runOne(opts) {
|
|
|
10111
10871
|
function withoutOldPwd(env) {
|
|
10112
10872
|
return Object.fromEntries(Object.entries(env).filter(([k]) => k !== "OLDPWD"));
|
|
10113
10873
|
}
|
|
10114
|
-
async function attempt(opts, r, steps, env, stepEnv, homeEnvFile) {
|
|
10874
|
+
async function attempt(opts, r, steps, env, stepEnv, homeEnvFile, agent) {
|
|
10115
10875
|
const { test, n } = opts;
|
|
10116
10876
|
const { dir, workdir } = r;
|
|
10117
10877
|
if (steps.install.length > 0) {
|
|
@@ -10127,7 +10887,7 @@ async function attempt(opts, r, steps, env, stepEnv, homeEnvFile) {
|
|
|
10127
10887
|
}
|
|
10128
10888
|
const stderr = createWriteStream2(r.stderr);
|
|
10129
10889
|
const { harness } = opts;
|
|
10130
|
-
const outcome = await runAgent(harness, {
|
|
10890
|
+
const { outcome, group } = await runAgent(harness, {
|
|
10131
10891
|
bin: opts.bin,
|
|
10132
10892
|
prompt: r.prompt,
|
|
10133
10893
|
cwd: workdir,
|
|
@@ -10150,12 +10910,31 @@ async function attempt(opts, r, steps, env, stepEnv, homeEnvFile) {
|
|
|
10150
10910
|
extraArgs: r.harnessArgs,
|
|
10151
10911
|
onProgress: opts.onProgress
|
|
10152
10912
|
});
|
|
10913
|
+
agent.group = group;
|
|
10153
10914
|
if (outcome.version !== null)
|
|
10154
10915
|
evalStats({ claudeVersion: claudeVersion(outcome.version) });
|
|
10155
10916
|
await new Promise((res) => stderr.end(res));
|
|
10156
10917
|
if (outcome.sessionId)
|
|
10157
10918
|
r.session = harness.findSession(opts.configDir, outcome.sessionId);
|
|
10158
10919
|
r.reportedModel = outcome.reportedModel ?? (r.session && harness.sessionModel ? harness.sessionModel(r.session) : null);
|
|
10920
|
+
if (r.session && outcome.sessionId) {
|
|
10921
|
+
if (harness.claudeSessionFile) {
|
|
10922
|
+
r.claudeSession = r.session;
|
|
10923
|
+
r.sessionId = outcome.sessionId;
|
|
10924
|
+
} else {
|
|
10925
|
+
r.sessionId = convertedSessionId(outcome.sessionId);
|
|
10926
|
+
r.claudeSession = join10(dir, "session.jsonl");
|
|
10927
|
+
writeClaudeSession(r.claudeSession, {
|
|
10928
|
+
sessionId: r.sessionId,
|
|
10929
|
+
harness: harness.name,
|
|
10930
|
+
version: outcome.version,
|
|
10931
|
+
model: r.reportedModel,
|
|
10932
|
+
prompt: r.prompt,
|
|
10933
|
+
steps: outcome.steps,
|
|
10934
|
+
final: outcome.final
|
|
10935
|
+
});
|
|
10936
|
+
}
|
|
10937
|
+
}
|
|
10159
10938
|
const about = [
|
|
10160
10939
|
opts.shownHarness,
|
|
10161
10940
|
opts.model === null ? null : `model ${opts.model}`
|
|
@@ -10205,9 +10984,6 @@ async function attempt(opts, r, steps, env, stepEnv, homeEnvFile) {
|
|
|
10205
10984
|
test.checks.length === 0 ? "no script checks" : `${r.checks.length}/${r.checks.length} checks passed`
|
|
10206
10985
|
];
|
|
10207
10986
|
}
|
|
10208
|
-
function notUploaded(h) {
|
|
10209
|
-
return `firegoat.ai doesn't read ${h.name} sessions yet: its runs stay local`;
|
|
10210
|
-
}
|
|
10211
10987
|
var MAX_LISTED_URLS = 5;
|
|
10212
10988
|
function uploadedSummary(origin, results, runDir) {
|
|
10213
10989
|
const urls = results.flatMap((r) => r.upload?.status === "uploaded" ? [r.upload.url] : []);
|
|
@@ -10246,7 +11022,7 @@ async function evalCommand(args, root) {
|
|
|
10246
11022
|
const say = opts.json ? console.error : console.log;
|
|
10247
11023
|
const stream = opts.json ? process.stderr : process.stdout;
|
|
10248
11024
|
const ui = stream.isTTY ? ttyUI(stream, {
|
|
10249
|
-
project:
|
|
11025
|
+
project: basename2(root),
|
|
10250
11026
|
version: package_default.version,
|
|
10251
11027
|
color: !process.env.NO_COLOR
|
|
10252
11028
|
}) : plainUI(say);
|
|
@@ -10275,14 +11051,6 @@ async function evalCommand(args, root) {
|
|
|
10275
11051
|
return fatal(EXIT.usage, `--harness-bin needs exactly one harness, this eval runs ${harnessNames.length} (${harnessNames.join(", ")}): pick one with --harness`, "usage");
|
|
10276
11052
|
const harnesses = harnessNames.map((h) => HARNESSES.get(h));
|
|
10277
11053
|
const showHarness = showsHarness(harnessNames);
|
|
10278
|
-
try {
|
|
10279
|
-
for (const h of harnesses)
|
|
10280
|
-
checkHarnessArgs(tests, h);
|
|
10281
|
-
} catch (e) {
|
|
10282
|
-
if (e instanceof ConfigError)
|
|
10283
|
-
return fatal(EXIT.usage, e.message, "config");
|
|
10284
|
-
throw e;
|
|
10285
|
-
}
|
|
10286
11054
|
evalStats({
|
|
10287
11055
|
testCount: tests.length,
|
|
10288
11056
|
runCount: tests.length * runs * Math.max(models.length, 1) * harnesses.length,
|
|
@@ -10295,7 +11063,7 @@ async function evalCommand(args, root) {
|
|
|
10295
11063
|
t.prompt,
|
|
10296
11064
|
...t.checks.map((c) => c.run),
|
|
10297
11065
|
...Object.values(t.env),
|
|
10298
|
-
...t.harnessArgs.map((a) => a.arg),
|
|
11066
|
+
...harnessNames.flatMap((h) => (t.harnessArgs[h] ?? []).map((a) => a.arg)),
|
|
10299
11067
|
...t.install,
|
|
10300
11068
|
...t.cleanup
|
|
10301
11069
|
]).concat(config.build, config.install, config.cleanup).flatMap(varRefs).filter((k) => !(k in vars)))
|
|
@@ -10367,15 +11135,10 @@ async function evalCommand(args, root) {
|
|
|
10367
11135
|
const runId = newRunId();
|
|
10368
11136
|
let upload;
|
|
10369
11137
|
try {
|
|
10370
|
-
const pre = await uploadPreflight(opts, ui,
|
|
11138
|
+
const pre = await uploadPreflight(opts, ui, basename2(repo?.mainCheckout ?? repo?.toplevel ?? root), runId, repo?.info ?? null);
|
|
10371
11139
|
if (pre && "exitCode" in pre)
|
|
10372
11140
|
return fatal(pre.exitCode, pre.error, pre.code);
|
|
10373
11141
|
upload = pre;
|
|
10374
|
-
if (upload) {
|
|
10375
|
-
for (const h of harnesses)
|
|
10376
|
-
if (!h.uploads)
|
|
10377
|
-
ui.note(notUploaded(h));
|
|
10378
|
-
}
|
|
10379
11142
|
} catch (e) {
|
|
10380
11143
|
if (e instanceof ConfigError)
|
|
10381
11144
|
return fatal(EXIT.usage, e.message, "config");
|
|
@@ -10460,10 +11223,7 @@ async function evalCommand(args, root) {
|
|
|
10460
11223
|
command: undefined
|
|
10461
11224
|
});
|
|
10462
11225
|
ui.update();
|
|
10463
|
-
if (upload
|
|
10464
|
-
r.upload = { status: "skipped", reason: notUploaded(harness) };
|
|
10465
|
-
writeRunJson(r);
|
|
10466
|
-
} else if (upload) {
|
|
11226
|
+
if (upload) {
|
|
10467
11227
|
row.upload = "uploading";
|
|
10468
11228
|
ui.update();
|
|
10469
11229
|
r.upload = await uploadRun(upload, r);
|
|
@@ -10509,7 +11269,7 @@ async function evalCommand(args, root) {
|
|
|
10509
11269
|
status,
|
|
10510
11270
|
exitCode
|
|
10511
11271
|
};
|
|
10512
|
-
|
|
11272
|
+
writeFileSync5(join10(runDir, "summary.json"), `${JSON.stringify(doc, null, 2)}
|
|
10513
11273
|
`);
|
|
10514
11274
|
appendFileSync(join10(runsRoot, "index.jsonl"), `${JSON.stringify({
|
|
10515
11275
|
runId,
|
|
@@ -10538,6 +11298,9 @@ async function evalCommand(args, root) {
|
|
|
10538
11298
|
for (const c of r.cleanup)
|
|
10539
11299
|
if (c.code !== 0)
|
|
10540
11300
|
console.error(`firegoat eval: Warning: ${r.test}#${r.n} cleanup failed (${c.error ?? `exit ${c.code}`}): ${c.run} (log: ${r.cleanupLog})`);
|
|
11301
|
+
for (const r of results)
|
|
11302
|
+
if (r.leftover.length > 0)
|
|
11303
|
+
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
11304
|
if (uploadsFailed > 0)
|
|
10542
11305
|
console.error(`firegoat eval: ${uploadsFailed} upload${uploadsFailed === 1 ? "" : "s"} failed (each run's upload.error in summary.json)`);
|
|
10543
11306
|
if (opts.json)
|
|
@@ -10822,12 +11585,12 @@ firegoat ${package_default.version}` : `firegoat ${package_default.version}`,
|
|
|
10822
11585
|
}
|
|
10823
11586
|
|
|
10824
11587
|
// src/init.ts
|
|
10825
|
-
import { spawn as
|
|
10826
|
-
import { existsSync as existsSync6, writeFileSync as
|
|
10827
|
-
import { basename as
|
|
11588
|
+
import { spawn as spawn4, spawnSync as spawnSync4 } from "node:child_process";
|
|
11589
|
+
import { existsSync as existsSync6, writeFileSync as writeFileSync6 } from "node:fs";
|
|
11590
|
+
import { basename as basename3, join as join11 } from "node:path";
|
|
10828
11591
|
|
|
10829
11592
|
// src/login.ts
|
|
10830
|
-
import { spawn as
|
|
11593
|
+
import { spawn as spawn3 } from "node:child_process";
|
|
10831
11594
|
var TIMEOUT_MS3 = 30000;
|
|
10832
11595
|
var MAX_EXPIRES_S = 900;
|
|
10833
11596
|
var MAX_INTERVAL_S = 60;
|
|
@@ -10835,7 +11598,7 @@ var FORM = { "Content-Type": "application/x-www-form-urlencoded" };
|
|
|
10835
11598
|
var UPLOADS_ON = "Eval uploads each session to firegoat.ai as a private trace; --no-upload keeps runs local.";
|
|
10836
11599
|
var UPLOAD_NOTICE = `${UPLOADS_ON} A session holds prompts, tool outputs and file contents: possibly secrets from your env or repo. The agent runs as you and can read ~/.firegoat (and a FIREGOAT_API_KEY in firegoat's own process env).`;
|
|
10837
11600
|
var TERMS = "By logging in, you agree to the firegoat.ai Terms of Service (https://firegoat.ai/terms) and Privacy Policy (https://firegoat.ai/privacy).";
|
|
10838
|
-
function
|
|
11601
|
+
function str4(o, k, step) {
|
|
10839
11602
|
const v = o?.[k];
|
|
10840
11603
|
if (typeof v !== "string" || v === "")
|
|
10841
11604
|
throw new ApiError(`${step}: response has no ${k}`);
|
|
@@ -10844,7 +11607,7 @@ function str3(o, k, step) {
|
|
|
10844
11607
|
function openBrowser(url) {
|
|
10845
11608
|
const cmd = process.platform === "darwin" ? "open" : "xdg-open";
|
|
10846
11609
|
try {
|
|
10847
|
-
const child =
|
|
11610
|
+
const child = spawn3(cmd, [url], {
|
|
10848
11611
|
env: hostEnv(),
|
|
10849
11612
|
stdio: "ignore",
|
|
10850
11613
|
detached: true
|
|
@@ -10866,9 +11629,9 @@ async function loginFlow(show, interactive) {
|
|
|
10866
11629
|
});
|
|
10867
11630
|
if (cfg.status !== 200)
|
|
10868
11631
|
throw replyError(cfgStep, cfg);
|
|
10869
|
-
const clientId =
|
|
10870
|
-
const deviceUrl = assertSafeUrl(
|
|
10871
|
-
const tokenUrl = assertSafeUrl(
|
|
11632
|
+
const clientId = str4(cfg.json, "client_id", cfgStep);
|
|
11633
|
+
const deviceUrl = assertSafeUrl(str4(cfg.json, "device_authorization_endpoint", cfgStep), "device_authorization_endpoint");
|
|
11634
|
+
const tokenUrl = assertSafeUrl(str4(cfg.json, "token_endpoint", cfgStep), "token_endpoint");
|
|
10872
11635
|
if (tokenUrl.origin !== origin)
|
|
10873
11636
|
throw new ApiError(`token_endpoint must be on ${origin}, got ${printable(tokenUrl.origin, 100)}`);
|
|
10874
11637
|
const devStep = "Device authorization";
|
|
@@ -10877,16 +11640,19 @@ async function loginFlow(show, interactive) {
|
|
|
10877
11640
|
url: deviceUrl,
|
|
10878
11641
|
method: "POST",
|
|
10879
11642
|
headers: FORM,
|
|
10880
|
-
body: new URLSearchParams({
|
|
11643
|
+
body: new URLSearchParams({
|
|
11644
|
+
client_id: clientId,
|
|
11645
|
+
scope: "openid profile email"
|
|
11646
|
+
}).toString(),
|
|
10881
11647
|
timeoutMs: TIMEOUT_MS3
|
|
10882
11648
|
});
|
|
10883
11649
|
if (dev.status !== 200)
|
|
10884
11650
|
throw replyError(devStep, dev);
|
|
10885
|
-
const deviceCode =
|
|
10886
|
-
const userCode =
|
|
11651
|
+
const deviceCode = str4(dev.json, "device_code", devStep);
|
|
11652
|
+
const userCode = str4(dev.json, "user_code", devStep);
|
|
10887
11653
|
if (!/^[A-Za-z0-9-]{1,64}$/.test(userCode))
|
|
10888
11654
|
throw new ApiError(`${devStep}: user_code has unexpected characters`);
|
|
10889
|
-
const pageRaw = typeof dev.json?.verification_uri_complete === "string" ? dev.json.verification_uri_complete :
|
|
11655
|
+
const pageRaw = typeof dev.json?.verification_uri_complete === "string" ? dev.json.verification_uri_complete : str4(dev.json, "verification_uri", devStep);
|
|
10890
11656
|
const page = assertSafeUrl(pageRaw, "verification_uri");
|
|
10891
11657
|
if (page.protocol !== "https:" || pageRaw.startsWith("-"))
|
|
10892
11658
|
throw new ApiError("verification_uri must be https");
|
|
@@ -11168,10 +11934,12 @@ runs: 1
|
|
|
11168
11934
|
# install: cp -R "$FIREGOAT_OUT/starter-app/." .
|
|
11169
11935
|
|
|
11170
11936
|
# Optional: cleanup: runs in each run's folder once the run is over (after the
|
|
11171
|
-
# checks, also after an error or timeout),
|
|
11172
|
-
#
|
|
11937
|
+
# checks, also after an error or timeout), for what lives outside the run, e.g.
|
|
11938
|
+
# data install: seeded in a shared service. Servers the agent left running need
|
|
11939
|
+
# none: firegoat stops them after cleanup. A failing cleanup step is a warning,
|
|
11940
|
+
# never the verdict.
|
|
11173
11941
|
# cleanup:
|
|
11174
|
-
# -
|
|
11942
|
+
# - acme db drop "eval-{{run.id}}-{{run.n}}" || true
|
|
11175
11943
|
#
|
|
11176
11944
|
# A test can add its own install: (after this one) and cleanup: (before this one).
|
|
11177
11945
|
|
|
@@ -11186,6 +11954,11 @@ runs: 1
|
|
|
11186
11954
|
# MYTOOL_HOME: "$FIREGOAT_HOME/.mytool"
|
|
11187
11955
|
# UV_FIND_LINKS: "$FIREGOAT_OUT"
|
|
11188
11956
|
|
|
11957
|
+
# Optional: extra arguments for one harness, after firegoat's own flags, one per
|
|
11958
|
+
# entry: claude_args, codex_args, grok_args. A test can add its own. E.g. Claude in
|
|
11959
|
+
# Chrome, for tests that need a browser:
|
|
11960
|
+
# claude_args: [--chrome]
|
|
11961
|
+
|
|
11189
11962
|
# Optional: values for {{vars.KEY}} in prompts, checks and build/install/cleanup
|
|
11190
11963
|
# steps (pasted in as-is, unquoted); eval --var KEY=value overrides them. Steps
|
|
11191
11964
|
# and checks also get each one as an env var ("$KEY": safe for any value).
|
|
@@ -11273,7 +12046,7 @@ class Timeline {
|
|
|
11273
12046
|
}
|
|
11274
12047
|
function runQuiet(cmd, args, opts) {
|
|
11275
12048
|
return new Promise((resolve) => {
|
|
11276
|
-
const child =
|
|
12049
|
+
const child = spawn4(cmd, args, {
|
|
11277
12050
|
...opts,
|
|
11278
12051
|
stdio: ["ignore", "pipe", "pipe"]
|
|
11279
12052
|
});
|
|
@@ -11342,8 +12115,7 @@ ${UPLOADS_ON}` : LOGIN_HINT);
|
|
|
11342
12115
|
}
|
|
11343
12116
|
async function initCommand(args, cwd) {
|
|
11344
12117
|
let force = false;
|
|
11345
|
-
|
|
11346
|
-
let localSkills = false;
|
|
12118
|
+
const skillFlags = [];
|
|
11347
12119
|
let login = true;
|
|
11348
12120
|
for (const a of args) {
|
|
11349
12121
|
if (a === "--help" || a === "-h") {
|
|
@@ -11353,9 +12125,11 @@ async function initCommand(args, cwd) {
|
|
|
11353
12125
|
if (a === "--force" || a === "-f")
|
|
11354
12126
|
force = true;
|
|
11355
12127
|
else if (a === "--no-skills")
|
|
11356
|
-
|
|
12128
|
+
skillFlags.push({ flag: a, scope: "none" });
|
|
11357
12129
|
else if (a === "--local-skills")
|
|
11358
|
-
|
|
12130
|
+
skillFlags.push({ flag: a, scope: "local" });
|
|
12131
|
+
else if (a === "--global-skills")
|
|
12132
|
+
skillFlags.push({ flag: a, scope: "global" });
|
|
11359
12133
|
else if (a === "--no-login")
|
|
11360
12134
|
login = false;
|
|
11361
12135
|
else {
|
|
@@ -11365,15 +12139,17 @@ ${INIT_HELP}`);
|
|
|
11365
12139
|
return EXIT.usage;
|
|
11366
12140
|
}
|
|
11367
12141
|
}
|
|
11368
|
-
|
|
11369
|
-
|
|
12142
|
+
const flags = [...new Set(skillFlags.map((f) => f.flag))];
|
|
12143
|
+
if (flags.length > 1) {
|
|
12144
|
+
console.error(`firegoat init: ${flags.join(" and ")} contradict each other: pass one
|
|
11370
12145
|
|
|
11371
12146
|
${INIT_HELP}`);
|
|
11372
12147
|
return EXIT.usage;
|
|
11373
12148
|
}
|
|
11374
|
-
let scope = noSkills ? "none" : localSkills ? "local" : "global";
|
|
11375
|
-
initSkills(scope);
|
|
11376
12149
|
const interactive = Boolean(process.stdin.isTTY && process.stdout.isTTY);
|
|
12150
|
+
const asked = skillFlags[0] === undefined && interactive;
|
|
12151
|
+
let scope = skillFlags[0]?.scope ?? (interactive ? "global" : "local");
|
|
12152
|
+
initSkills(scope);
|
|
11377
12153
|
const color = interactive && !process.env.NO_COLOR;
|
|
11378
12154
|
const paint = (code, text) => color ? `\x1B[${code}m${text}\x1B[0m` : text;
|
|
11379
12155
|
const width = Math.min(process.stdout.columns || 80, 80);
|
|
@@ -11388,7 +12164,7 @@ ${INIT_HELP}`);
|
|
|
11388
12164
|
const configPath = join11(cwd, CONFIG_FILE);
|
|
11389
12165
|
const keep = existsSync6(configPath) && !force;
|
|
11390
12166
|
if (!keep)
|
|
11391
|
-
|
|
12167
|
+
writeFileSync6(configPath, template(basename3(cwd)));
|
|
11392
12168
|
if (ui) {
|
|
11393
12169
|
console.log(ui.done(1, CONFIG_FILE, keep ? "Kept (--force replaces it)" : "Created"));
|
|
11394
12170
|
ui.gap();
|
|
@@ -11398,7 +12174,7 @@ ${INIT_HELP}`);
|
|
|
11398
12174
|
const skill = "firegoat-onboarding skill";
|
|
11399
12175
|
const block = liveBlock();
|
|
11400
12176
|
ui?.gap();
|
|
11401
|
-
if (ui &&
|
|
12177
|
+
if (ui && asked) {
|
|
11402
12178
|
const picked = await ui.ask(block, 3, "Install the firegoat-onboarding skill", [
|
|
11403
12179
|
["Global", skillsDir("global")],
|
|
11404
12180
|
["This repo", `${skillsDir("local")} (commit it for your team)`],
|