firegoat 0.24.0 → 0.26.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 +3 -1
- package/dist/cli.js +1323 -483
- package/package.json +2 -2
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.26.0",
|
|
6951
6951
|
repository: {
|
|
6952
6952
|
type: "git",
|
|
6953
6953
|
url: "git+https://github.com/firegoat-ai/firegoat-cli.git"
|
|
@@ -6976,7 +6976,7 @@ var package_default = {
|
|
|
6976
6976
|
},
|
|
6977
6977
|
scripts: {
|
|
6978
6978
|
build: "bun build src/cli.ts --target node --outfile dist/cli.js",
|
|
6979
|
-
test: "bun test",
|
|
6979
|
+
test: "bun test --timeout 30000",
|
|
6980
6980
|
typecheck: "bun check",
|
|
6981
6981
|
lint: "biome check .",
|
|
6982
6982
|
format: "biome check --write .",
|
|
@@ -6987,7 +6987,7 @@ var package_default = {
|
|
|
6987
6987
|
};
|
|
6988
6988
|
|
|
6989
6989
|
// src/config.ts
|
|
6990
|
-
import { existsSync, readFileSync } from "node:fs";
|
|
6990
|
+
import { existsSync as existsSync4, readFileSync as readFileSync3 } from "node:fs";
|
|
6991
6991
|
|
|
6992
6992
|
// node_modules/yaml/dist/index.js
|
|
6993
6993
|
var composer = require_composer();
|
|
@@ -7035,6 +7035,830 @@ var $stringify = publicApi.stringify;
|
|
|
7035
7035
|
var $visit = visit.visit;
|
|
7036
7036
|
var $visitAsync = visit.visitAsync;
|
|
7037
7037
|
|
|
7038
|
+
// src/harnesses/claude.ts
|
|
7039
|
+
import { spawnSync } from "node:child_process";
|
|
7040
|
+
import { existsSync, readdirSync } from "node:fs";
|
|
7041
|
+
import { join } from "node:path";
|
|
7042
|
+
var FLAGS = [
|
|
7043
|
+
"--output-format",
|
|
7044
|
+
"stream-json",
|
|
7045
|
+
"--verbose",
|
|
7046
|
+
"--dangerously-skip-permissions",
|
|
7047
|
+
"--strict-mcp-config",
|
|
7048
|
+
"--setting-sources",
|
|
7049
|
+
"",
|
|
7050
|
+
"--disable-slash-commands"
|
|
7051
|
+
];
|
|
7052
|
+
var SET = "firegoat sets it for every run";
|
|
7053
|
+
function parse(line) {
|
|
7054
|
+
try {
|
|
7055
|
+
const e = JSON.parse(line);
|
|
7056
|
+
return typeof e === "object" && e !== null ? e : undefined;
|
|
7057
|
+
} catch {
|
|
7058
|
+
return;
|
|
7059
|
+
}
|
|
7060
|
+
}
|
|
7061
|
+
function content(e) {
|
|
7062
|
+
const message = e?.message;
|
|
7063
|
+
return Array.isArray(message?.content) ? message.content : [];
|
|
7064
|
+
}
|
|
7065
|
+
function resultText(content) {
|
|
7066
|
+
if (typeof content === "string")
|
|
7067
|
+
return content;
|
|
7068
|
+
if (Array.isArray(content))
|
|
7069
|
+
return content.map((c) => typeof c.text === "string" ? c.text : `[${c.type}]`).join(`
|
|
7070
|
+
`);
|
|
7071
|
+
return "";
|
|
7072
|
+
}
|
|
7073
|
+
function steps(events) {
|
|
7074
|
+
const out = [];
|
|
7075
|
+
for (const e of events) {
|
|
7076
|
+
if (e?.type === "assistant")
|
|
7077
|
+
for (const b of content(e)) {
|
|
7078
|
+
if (b.type === "tool_use")
|
|
7079
|
+
out.push({
|
|
7080
|
+
kind: "tool_call",
|
|
7081
|
+
name: typeof b.name === "string" ? b.name : "",
|
|
7082
|
+
input: typeof b.input === "object" && b.input !== null ? b.input : null
|
|
7083
|
+
});
|
|
7084
|
+
else if (b.type === "text" && typeof b.text === "string")
|
|
7085
|
+
out.push({ kind: "text", text: b.text });
|
|
7086
|
+
}
|
|
7087
|
+
else if (e?.type === "user") {
|
|
7088
|
+
for (const b of content(e))
|
|
7089
|
+
if (b.type === "tool_result")
|
|
7090
|
+
out.push({
|
|
7091
|
+
kind: "tool_result",
|
|
7092
|
+
isError: b.is_error === true,
|
|
7093
|
+
text: resultText(b.content).trimEnd()
|
|
7094
|
+
});
|
|
7095
|
+
}
|
|
7096
|
+
}
|
|
7097
|
+
return out;
|
|
7098
|
+
}
|
|
7099
|
+
function read(stdout, exited, timeoutMs) {
|
|
7100
|
+
const events = stdout.split(`
|
|
7101
|
+
`).filter((l) => l.trim() !== "").map(parse);
|
|
7102
|
+
let final = "";
|
|
7103
|
+
for (const e of events)
|
|
7104
|
+
if (e?.type === "result" && typeof e.result === "string")
|
|
7105
|
+
final = e.result;
|
|
7106
|
+
const result = events.findLast((e) => e?.type === "result");
|
|
7107
|
+
const trace = {
|
|
7108
|
+
steps: steps(events),
|
|
7109
|
+
final,
|
|
7110
|
+
turns: result?.num_turns ?? null,
|
|
7111
|
+
cost: result?.total_cost_usd ?? null
|
|
7112
|
+
};
|
|
7113
|
+
const init = events.find((e) => e?.type === "system" && e.subtype === "init");
|
|
7114
|
+
if (!init) {
|
|
7115
|
+
const said = stdout.trim().split(`
|
|
7116
|
+
`)[0]?.slice(0, 300);
|
|
7117
|
+
return {
|
|
7118
|
+
...trace,
|
|
7119
|
+
reportedModel: null,
|
|
7120
|
+
version: null,
|
|
7121
|
+
ok: false,
|
|
7122
|
+
reason: `claude did not start a session (exit ${exited.code ?? "by signal"})${said ? `: ${said}` : ", see stderr.txt"}`
|
|
7123
|
+
};
|
|
7124
|
+
}
|
|
7125
|
+
const reportedModel = typeof init.model === "string" ? init.model : null;
|
|
7126
|
+
const version = typeof init.claude_code_version === "string" ? init.claude_code_version : null;
|
|
7127
|
+
if (typeof init.session_id !== "string" || init.session_id === "")
|
|
7128
|
+
return {
|
|
7129
|
+
...trace,
|
|
7130
|
+
reportedModel,
|
|
7131
|
+
version,
|
|
7132
|
+
ok: false,
|
|
7133
|
+
reason: "claude's init line has no session_id"
|
|
7134
|
+
};
|
|
7135
|
+
const sessionId = init.session_id;
|
|
7136
|
+
if (exited.timedOut)
|
|
7137
|
+
return {
|
|
7138
|
+
...trace,
|
|
7139
|
+
reportedModel,
|
|
7140
|
+
version,
|
|
7141
|
+
ok: false,
|
|
7142
|
+
reason: `timed out after ${timeoutMs / 1000}s`,
|
|
7143
|
+
sessionId
|
|
7144
|
+
};
|
|
7145
|
+
if (!result)
|
|
7146
|
+
return {
|
|
7147
|
+
...trace,
|
|
7148
|
+
reportedModel,
|
|
7149
|
+
version,
|
|
7150
|
+
ok: false,
|
|
7151
|
+
reason: `claude exited ${exited.code ?? "by signal"} without a result, see stderr.txt`,
|
|
7152
|
+
sessionId
|
|
7153
|
+
};
|
|
7154
|
+
if (result.is_error === true || result.subtype !== "success")
|
|
7155
|
+
return {
|
|
7156
|
+
...trace,
|
|
7157
|
+
reportedModel,
|
|
7158
|
+
version,
|
|
7159
|
+
ok: false,
|
|
7160
|
+
reason: `claude result is an error (subtype=${result.subtype})${result.result ? `: ${result.result.slice(0, 300)}` : ""}`,
|
|
7161
|
+
sessionId
|
|
7162
|
+
};
|
|
7163
|
+
return { ...trace, reportedModel, version, ok: true, sessionId };
|
|
7164
|
+
}
|
|
7165
|
+
function progress(line) {
|
|
7166
|
+
const e = parse(line);
|
|
7167
|
+
if (e?.type !== "assistant")
|
|
7168
|
+
return;
|
|
7169
|
+
const p = {};
|
|
7170
|
+
for (const b of content(e)) {
|
|
7171
|
+
const cmd = b.input?.command;
|
|
7172
|
+
if (b.type === "tool_use" && typeof cmd === "string")
|
|
7173
|
+
p.command = cmd.split(`
|
|
7174
|
+
`)[0];
|
|
7175
|
+
}
|
|
7176
|
+
return p;
|
|
7177
|
+
}
|
|
7178
|
+
function checkLogin(opts) {
|
|
7179
|
+
const r = spawnSync(opts.bin, ["auth", "status"], {
|
|
7180
|
+
cwd: opts.cwd,
|
|
7181
|
+
env: opts.env,
|
|
7182
|
+
encoding: "utf8",
|
|
7183
|
+
timeout: 30000
|
|
7184
|
+
});
|
|
7185
|
+
if (r.error)
|
|
7186
|
+
return {
|
|
7187
|
+
loggedOut: false,
|
|
7188
|
+
reason: `could not run ${opts.bin}: ${r.error.message}`
|
|
7189
|
+
};
|
|
7190
|
+
let status;
|
|
7191
|
+
try {
|
|
7192
|
+
status = JSON.parse(r.stdout);
|
|
7193
|
+
} catch {}
|
|
7194
|
+
if (status?.loggedIn === true)
|
|
7195
|
+
return;
|
|
7196
|
+
if (status?.loggedIn === false)
|
|
7197
|
+
return {
|
|
7198
|
+
loggedOut: true,
|
|
7199
|
+
reason: "claude is not logged in. Run `claude` once and log in (or set ANTHROPIC_API_KEY), then run eval again"
|
|
7200
|
+
};
|
|
7201
|
+
return {
|
|
7202
|
+
loggedOut: false,
|
|
7203
|
+
reason: `claude auth status gave no login state (exit ${r.status}): ${(r.stdout + r.stderr).trim().slice(0, 300)}`
|
|
7204
|
+
};
|
|
7205
|
+
}
|
|
7206
|
+
var claude = {
|
|
7207
|
+
name: "claude-code",
|
|
7208
|
+
bin: "claude",
|
|
7209
|
+
installHint: "Install Claude Code (https://claude.com/claude-code) or pass --harness-bin PATH.",
|
|
7210
|
+
configDirName: ".claude",
|
|
7211
|
+
reservedArgs: [
|
|
7212
|
+
[["-p", "--print", ...FLAGS.filter((f) => f.startsWith("-"))], SET],
|
|
7213
|
+
[["--model"], "choose the model with top-level model: (or eval --model)"],
|
|
7214
|
+
[
|
|
7215
|
+
["--session-id", "--resume", "-r", "--continue", "-c"],
|
|
7216
|
+
"every run is a fresh session"
|
|
7217
|
+
]
|
|
7218
|
+
],
|
|
7219
|
+
uploads: true,
|
|
7220
|
+
args: ({ prompt, model, extraArgs }) => [
|
|
7221
|
+
"-p",
|
|
7222
|
+
prompt,
|
|
7223
|
+
...FLAGS,
|
|
7224
|
+
...model === undefined ? [] : ["--model", model],
|
|
7225
|
+
...extraArgs
|
|
7226
|
+
],
|
|
7227
|
+
env: ({ configDir, run }) => ({
|
|
7228
|
+
CLAUDE_CONFIG_DIR: configDir,
|
|
7229
|
+
CLAUDE_SECURESTORAGE_CONFIG_DIR: "",
|
|
7230
|
+
...run === undefined ? {} : { CLAUDE_ENV_FILE: run.homeEnvFile }
|
|
7231
|
+
}),
|
|
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
|
+
checkLogin,
|
|
7234
|
+
progress,
|
|
7235
|
+
read,
|
|
7236
|
+
sessionsDir: (configDir) => join(configDir, "projects"),
|
|
7237
|
+
findSession(configDir, sessionId) {
|
|
7238
|
+
const projects = join(configDir, "projects");
|
|
7239
|
+
if (!existsSync(projects))
|
|
7240
|
+
return null;
|
|
7241
|
+
for (const d of readdirSync(projects)) {
|
|
7242
|
+
const f = join(projects, d, `${sessionId}.jsonl`);
|
|
7243
|
+
if (existsSync(f))
|
|
7244
|
+
return f;
|
|
7245
|
+
}
|
|
7246
|
+
return null;
|
|
7247
|
+
}
|
|
7248
|
+
};
|
|
7249
|
+
|
|
7250
|
+
// src/harnesses/codex.ts
|
|
7251
|
+
import { spawnSync as spawnSync2 } from "node:child_process";
|
|
7252
|
+
import {
|
|
7253
|
+
existsSync as existsSync2,
|
|
7254
|
+
lstatSync,
|
|
7255
|
+
readdirSync as readdirSync2,
|
|
7256
|
+
readFileSync,
|
|
7257
|
+
symlinkSync,
|
|
7258
|
+
unlinkSync
|
|
7259
|
+
} from "node:fs";
|
|
7260
|
+
import { homedir } from "node:os";
|
|
7261
|
+
import { join as join3 } from "node:path";
|
|
7262
|
+
|
|
7263
|
+
// src/home.ts
|
|
7264
|
+
import { mkdirSync, writeFileSync } from "node:fs";
|
|
7265
|
+
import { join as join2 } from "node:path";
|
|
7266
|
+
function homeEnv(home) {
|
|
7267
|
+
return {
|
|
7268
|
+
HOME: home,
|
|
7269
|
+
XDG_CONFIG_HOME: join2(home, ".config"),
|
|
7270
|
+
XDG_CACHE_HOME: join2(home, ".cache"),
|
|
7271
|
+
XDG_DATA_HOME: join2(home, ".local", "share"),
|
|
7272
|
+
XDG_STATE_HOME: join2(home, ".local", "state")
|
|
7273
|
+
};
|
|
7274
|
+
}
|
|
7275
|
+
var shellQuote = (s) => `'${s.replaceAll("'", `'\\''`)}'`;
|
|
7276
|
+
function makeRunHome(dir) {
|
|
7277
|
+
const home = join2(dir, "home");
|
|
7278
|
+
const env = homeEnv(home);
|
|
7279
|
+
for (const d of Object.values(env))
|
|
7280
|
+
mkdirSync(d, { recursive: true });
|
|
7281
|
+
const envFile = join2(dir, "home.env");
|
|
7282
|
+
writeFileSync(envFile, Object.entries(env).map(([k, v]) => `export ${k}=${shellQuote(v)}
|
|
7283
|
+
`).join(""));
|
|
7284
|
+
return { home, envFile };
|
|
7285
|
+
}
|
|
7286
|
+
|
|
7287
|
+
// src/harnesses/codex.ts
|
|
7288
|
+
var FLAGS2 = [
|
|
7289
|
+
"--json",
|
|
7290
|
+
"--skip-git-repo-check",
|
|
7291
|
+
"--dangerously-bypass-approvals-and-sandbox"
|
|
7292
|
+
];
|
|
7293
|
+
var NO_PROJECT_DOC = ["-c", "project_doc_max_bytes=0"];
|
|
7294
|
+
function parse2(line) {
|
|
7295
|
+
try {
|
|
7296
|
+
const e = JSON.parse(line);
|
|
7297
|
+
return typeof e === "object" && e !== null ? e : undefined;
|
|
7298
|
+
} catch {
|
|
7299
|
+
return;
|
|
7300
|
+
}
|
|
7301
|
+
}
|
|
7302
|
+
function unwrapShell(command) {
|
|
7303
|
+
const m = /^\/bin\/(?:ba|z)?sh -l?c (.+)$/s.exec(command);
|
|
7304
|
+
if (!m)
|
|
7305
|
+
return command;
|
|
7306
|
+
const word = m[1];
|
|
7307
|
+
let out = "";
|
|
7308
|
+
for (let i = 0;i < word.length; i++) {
|
|
7309
|
+
const c = word[i];
|
|
7310
|
+
if (c === "'") {
|
|
7311
|
+
const end = word.indexOf("'", i + 1);
|
|
7312
|
+
if (end < 0)
|
|
7313
|
+
return command;
|
|
7314
|
+
out += word.slice(i + 1, end);
|
|
7315
|
+
i = end;
|
|
7316
|
+
} else if (c === "\\" && i + 1 < word.length)
|
|
7317
|
+
out += word[++i];
|
|
7318
|
+
else if (c === '"') {
|
|
7319
|
+
let j = i + 1;
|
|
7320
|
+
for (;j < word.length && word[j] !== '"'; j++) {
|
|
7321
|
+
if (word[j] === "\\" && '"\\$`'.includes(word[j + 1] ?? ""))
|
|
7322
|
+
j++;
|
|
7323
|
+
out += word[j];
|
|
7324
|
+
}
|
|
7325
|
+
if (j >= word.length)
|
|
7326
|
+
return command;
|
|
7327
|
+
i = j;
|
|
7328
|
+
} else if (/\s/.test(c))
|
|
7329
|
+
return command;
|
|
7330
|
+
else
|
|
7331
|
+
out += c;
|
|
7332
|
+
}
|
|
7333
|
+
return out;
|
|
7334
|
+
}
|
|
7335
|
+
function str(v) {
|
|
7336
|
+
return typeof v === "string" ? v : "";
|
|
7337
|
+
}
|
|
7338
|
+
function itemInput(item) {
|
|
7339
|
+
const { id: _id, type: _type, status: _status, ...rest } = item;
|
|
7340
|
+
return rest;
|
|
7341
|
+
}
|
|
7342
|
+
function steps2(events) {
|
|
7343
|
+
const out = [];
|
|
7344
|
+
const started = new Set;
|
|
7345
|
+
for (const e of events) {
|
|
7346
|
+
const item = e?.item;
|
|
7347
|
+
if (!item)
|
|
7348
|
+
continue;
|
|
7349
|
+
if (item.type === "command_execution") {
|
|
7350
|
+
const call = {
|
|
7351
|
+
kind: "tool_call",
|
|
7352
|
+
name: "shell",
|
|
7353
|
+
input: { command: unwrapShell(str(item.command)) }
|
|
7354
|
+
};
|
|
7355
|
+
if (e?.type === "item.started") {
|
|
7356
|
+
started.add(item.id);
|
|
7357
|
+
out.push(call);
|
|
7358
|
+
} else if (e?.type === "item.completed") {
|
|
7359
|
+
if (item.id === undefined || !started.has(item.id))
|
|
7360
|
+
out.push(call);
|
|
7361
|
+
out.push({
|
|
7362
|
+
kind: "tool_result",
|
|
7363
|
+
isError: item.exit_code !== 0,
|
|
7364
|
+
text: str(item.aggregated_output).trimEnd()
|
|
7365
|
+
});
|
|
7366
|
+
}
|
|
7367
|
+
continue;
|
|
7368
|
+
}
|
|
7369
|
+
if (e?.type !== "item.completed")
|
|
7370
|
+
continue;
|
|
7371
|
+
if (item.type === "agent_message") {
|
|
7372
|
+
if (str(item.text).trim() !== "")
|
|
7373
|
+
out.push({ kind: "text", text: str(item.text) });
|
|
7374
|
+
} else if (item.type !== "reasoning" && item.type !== "error")
|
|
7375
|
+
out.push({
|
|
7376
|
+
kind: "tool_call",
|
|
7377
|
+
name: str(item.type),
|
|
7378
|
+
input: itemInput(item)
|
|
7379
|
+
});
|
|
7380
|
+
}
|
|
7381
|
+
return out;
|
|
7382
|
+
}
|
|
7383
|
+
function errorMessage(raw) {
|
|
7384
|
+
try {
|
|
7385
|
+
const inner = JSON.parse(raw)?.error?.message;
|
|
7386
|
+
if (typeof inner === "string")
|
|
7387
|
+
return inner;
|
|
7388
|
+
} catch {}
|
|
7389
|
+
return raw;
|
|
7390
|
+
}
|
|
7391
|
+
function read2(stdout, exited, timeoutMs) {
|
|
7392
|
+
const events = stdout.split(`
|
|
7393
|
+
`).filter((l) => l.trim() !== "").map(parse2);
|
|
7394
|
+
const trace = {
|
|
7395
|
+
steps: steps2(events),
|
|
7396
|
+
final: events.filter((e) => e?.type === "item.completed" && e.item?.type === "agent_message").map((e) => str(e?.item?.text)).at(-1) ?? "",
|
|
7397
|
+
reportedModel: null,
|
|
7398
|
+
version: null,
|
|
7399
|
+
turns: null,
|
|
7400
|
+
cost: null
|
|
7401
|
+
};
|
|
7402
|
+
const thread = events.find((e) => e?.type === "thread.started");
|
|
7403
|
+
if (!thread) {
|
|
7404
|
+
const said = stdout.trim().split(`
|
|
7405
|
+
`)[0]?.slice(0, 300);
|
|
7406
|
+
return {
|
|
7407
|
+
...trace,
|
|
7408
|
+
ok: false,
|
|
7409
|
+
reason: `codex did not start a thread (exit ${exited.code ?? "by signal"})${said ? `: ${said}` : ", see stderr.txt"}`
|
|
7410
|
+
};
|
|
7411
|
+
}
|
|
7412
|
+
const sessionId = str(thread.thread_id);
|
|
7413
|
+
if (sessionId === "")
|
|
7414
|
+
return {
|
|
7415
|
+
...trace,
|
|
7416
|
+
ok: false,
|
|
7417
|
+
reason: "codex's thread.started line has no thread_id"
|
|
7418
|
+
};
|
|
7419
|
+
if (exited.timedOut)
|
|
7420
|
+
return {
|
|
7421
|
+
...trace,
|
|
7422
|
+
ok: false,
|
|
7423
|
+
reason: `timed out after ${timeoutMs / 1000}s`,
|
|
7424
|
+
sessionId
|
|
7425
|
+
};
|
|
7426
|
+
const failed = events.find((e) => e?.type === "turn.failed");
|
|
7427
|
+
if (failed) {
|
|
7428
|
+
const message = str(failed.error?.message);
|
|
7429
|
+
return {
|
|
7430
|
+
...trace,
|
|
7431
|
+
ok: false,
|
|
7432
|
+
reason: `codex turn failed${message ? `: ${errorMessage(message).slice(0, 300)}` : ""}`,
|
|
7433
|
+
sessionId
|
|
7434
|
+
};
|
|
7435
|
+
}
|
|
7436
|
+
if (!events.some((e) => e?.type === "turn.completed"))
|
|
7437
|
+
return {
|
|
7438
|
+
...trace,
|
|
7439
|
+
ok: false,
|
|
7440
|
+
reason: `codex exited ${exited.code ?? "by signal"} without completing its turn, see stderr.txt`,
|
|
7441
|
+
sessionId
|
|
7442
|
+
};
|
|
7443
|
+
return { ...trace, ok: true, sessionId };
|
|
7444
|
+
}
|
|
7445
|
+
function progress2(line) {
|
|
7446
|
+
const e = parse2(line);
|
|
7447
|
+
const item = e?.item;
|
|
7448
|
+
if (e?.type === "item.started" && item?.type === "command_execution")
|
|
7449
|
+
return { command: unwrapShell(str(item.command)).split(`
|
|
7450
|
+
`)[0] };
|
|
7451
|
+
if (e?.type === "item.completed" && item?.type === "agent_message")
|
|
7452
|
+
return {};
|
|
7453
|
+
return;
|
|
7454
|
+
}
|
|
7455
|
+
function userCodexHome() {
|
|
7456
|
+
return process.env.CODEX_HOME || join3(homedir(), ".codex");
|
|
7457
|
+
}
|
|
7458
|
+
function checkLogin2(opts) {
|
|
7459
|
+
const source = join3(userCodexHome(), "auth.json");
|
|
7460
|
+
if (!existsSync2(source))
|
|
7461
|
+
return {
|
|
7462
|
+
loggedOut: true,
|
|
7463
|
+
reason: `codex is not logged in (no ${source}). Run \`codex login\`, then run eval again`
|
|
7464
|
+
};
|
|
7465
|
+
const link = join3(opts.configDir, "auth.json");
|
|
7466
|
+
let current;
|
|
7467
|
+
try {
|
|
7468
|
+
current = lstatSync(link);
|
|
7469
|
+
} catch {}
|
|
7470
|
+
if (current && !current.isSymbolicLink())
|
|
7471
|
+
return {
|
|
7472
|
+
loggedOut: false,
|
|
7473
|
+
reason: `${link} is a file, not firegoat's link to ${source}: codex may have refreshed its login into it. Move it over ${source} if it is newer, else delete it, then run eval again`
|
|
7474
|
+
};
|
|
7475
|
+
if (current)
|
|
7476
|
+
unlinkSync(link);
|
|
7477
|
+
symlinkSync(source, link);
|
|
7478
|
+
const r = spawnSync2(opts.bin, ["login", "status"], {
|
|
7479
|
+
cwd: opts.cwd,
|
|
7480
|
+
env: opts.env,
|
|
7481
|
+
encoding: "utf8",
|
|
7482
|
+
timeout: 30000
|
|
7483
|
+
});
|
|
7484
|
+
if (r.error)
|
|
7485
|
+
return {
|
|
7486
|
+
loggedOut: false,
|
|
7487
|
+
reason: `could not run ${opts.bin}: ${r.error.message}`
|
|
7488
|
+
};
|
|
7489
|
+
if (r.status === 0)
|
|
7490
|
+
return;
|
|
7491
|
+
return {
|
|
7492
|
+
loggedOut: true,
|
|
7493
|
+
reason: `codex is not logged in (codex login status exit ${r.status}: ${(r.stdout + r.stderr).trim().slice(0, 300)}). Run \`codex login\`, then run eval again`
|
|
7494
|
+
};
|
|
7495
|
+
}
|
|
7496
|
+
function findSession(configDir, sessionId) {
|
|
7497
|
+
const sessions = join3(configDir, "sessions");
|
|
7498
|
+
if (!existsSync2(sessions))
|
|
7499
|
+
return null;
|
|
7500
|
+
const suffix = `-${sessionId}.jsonl`;
|
|
7501
|
+
for (const y of readdirSync2(sessions))
|
|
7502
|
+
for (const m of readdirSync2(join3(sessions, y)))
|
|
7503
|
+
for (const d of readdirSync2(join3(sessions, y, m)))
|
|
7504
|
+
for (const f of readdirSync2(join3(sessions, y, m, d)))
|
|
7505
|
+
if (f.startsWith("rollout-") && f.endsWith(suffix))
|
|
7506
|
+
return join3(sessions, y, m, d, f);
|
|
7507
|
+
return null;
|
|
7508
|
+
}
|
|
7509
|
+
function sessionModel(sessionFile) {
|
|
7510
|
+
let model = null;
|
|
7511
|
+
for (const line of readFileSync(sessionFile, "utf8").split(`
|
|
7512
|
+
`)) {
|
|
7513
|
+
const e = parse2(line);
|
|
7514
|
+
const m = e?.payload?.model;
|
|
7515
|
+
if (e?.type === "turn_context" && typeof m === "string")
|
|
7516
|
+
model = m;
|
|
7517
|
+
}
|
|
7518
|
+
return model;
|
|
7519
|
+
}
|
|
7520
|
+
var codex = {
|
|
7521
|
+
name: "codex-cli",
|
|
7522
|
+
bin: "codex",
|
|
7523
|
+
installHint: "Install Codex CLI (https://github.com/openai/codex) or pass --harness-bin PATH.",
|
|
7524
|
+
configDirName: ".codex",
|
|
7525
|
+
reservedArgs: [
|
|
7526
|
+
[[...FLAGS2, "-C", "--cd"], "firegoat sets it for every run"],
|
|
7527
|
+
[
|
|
7528
|
+
["-m", "--model"],
|
|
7529
|
+
"choose the model with top-level model: (or eval --model)"
|
|
7530
|
+
],
|
|
7531
|
+
[["resume", "fork"], "every run is a fresh session"],
|
|
7532
|
+
[["--ephemeral"], "firegoat reads the session file codex saves"]
|
|
7533
|
+
],
|
|
7534
|
+
uploads: false,
|
|
7535
|
+
args: ({ prompt, model, extraArgs, cwd }) => [
|
|
7536
|
+
"exec",
|
|
7537
|
+
...FLAGS2,
|
|
7538
|
+
"-C",
|
|
7539
|
+
cwd,
|
|
7540
|
+
...model === undefined ? [] : ["-m", model],
|
|
7541
|
+
...NO_PROJECT_DOC,
|
|
7542
|
+
...extraArgs,
|
|
7543
|
+
"--",
|
|
7544
|
+
prompt
|
|
7545
|
+
],
|
|
7546
|
+
env: ({ configDir, run }) => ({
|
|
7547
|
+
CODEX_HOME: configDir,
|
|
7548
|
+
...run === undefined ? {} : homeEnv(run.home)
|
|
7549
|
+
}),
|
|
7550
|
+
authLabel: () => `your codex login (${join3(userCodexHome(), "auth.json")})${process.env.CODEX_API_KEY ? "; CODEX_API_KEY is set and passes through" : ""}`,
|
|
7551
|
+
checkLogin: checkLogin2,
|
|
7552
|
+
progress: progress2,
|
|
7553
|
+
read: read2,
|
|
7554
|
+
sessionsDir: (configDir) => join3(configDir, "sessions"),
|
|
7555
|
+
findSession,
|
|
7556
|
+
sessionModel
|
|
7557
|
+
};
|
|
7558
|
+
|
|
7559
|
+
// src/harnesses/grok.ts
|
|
7560
|
+
import {
|
|
7561
|
+
closeSync,
|
|
7562
|
+
existsSync as existsSync3,
|
|
7563
|
+
lstatSync as lstatSync2,
|
|
7564
|
+
openSync,
|
|
7565
|
+
readdirSync as readdirSync3,
|
|
7566
|
+
readFileSync as readFileSync2,
|
|
7567
|
+
readlinkSync,
|
|
7568
|
+
readSync,
|
|
7569
|
+
statSync,
|
|
7570
|
+
symlinkSync as symlinkSync2,
|
|
7571
|
+
unlinkSync as unlinkSync2
|
|
7572
|
+
} from "node:fs";
|
|
7573
|
+
import { homedir as homedir2 } from "node:os";
|
|
7574
|
+
import { join as join4 } from "node:path";
|
|
7575
|
+
var BIN = join4(homedir2(), ".grok", "bin", "grok");
|
|
7576
|
+
var USER_AUTH = join4(process.env.GROK_HOME || join4(homedir2(), ".grok"), "auth.json");
|
|
7577
|
+
var REFRESH_WINDOW_MS = 5 * 60000;
|
|
7578
|
+
var FLAGS3 = [
|
|
7579
|
+
"--output-format",
|
|
7580
|
+
"streaming-messages-json",
|
|
7581
|
+
"--always-approve",
|
|
7582
|
+
"--no-auto-update"
|
|
7583
|
+
];
|
|
7584
|
+
function parse3(line) {
|
|
7585
|
+
try {
|
|
7586
|
+
const e = JSON.parse(line);
|
|
7587
|
+
return typeof e === "object" && e !== null ? e : undefined;
|
|
7588
|
+
} catch {
|
|
7589
|
+
return;
|
|
7590
|
+
}
|
|
7591
|
+
}
|
|
7592
|
+
var isObject = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
7593
|
+
function content2(e) {
|
|
7594
|
+
const message = e?.message;
|
|
7595
|
+
return Array.isArray(message?.content) ? message.content : [];
|
|
7596
|
+
}
|
|
7597
|
+
var PROMPT_KEYS = ["output_for_prompt", "tool_output_for_prompt", "content"];
|
|
7598
|
+
function promptText(v) {
|
|
7599
|
+
if (typeof v === "string")
|
|
7600
|
+
return v;
|
|
7601
|
+
if (!isObject(v))
|
|
7602
|
+
return;
|
|
7603
|
+
for (const k of PROMPT_KEYS)
|
|
7604
|
+
if (typeof v[k] === "string")
|
|
7605
|
+
return v[k];
|
|
7606
|
+
for (const [k, x] of Object.entries(v)) {
|
|
7607
|
+
if (k === "type")
|
|
7608
|
+
continue;
|
|
7609
|
+
const t = promptText(x);
|
|
7610
|
+
if (t !== undefined)
|
|
7611
|
+
return t;
|
|
7612
|
+
}
|
|
7613
|
+
return;
|
|
7614
|
+
}
|
|
7615
|
+
function toolResult(b) {
|
|
7616
|
+
const raw = b.content;
|
|
7617
|
+
let json;
|
|
7618
|
+
if (typeof raw === "string")
|
|
7619
|
+
try {
|
|
7620
|
+
json = JSON.parse(raw);
|
|
7621
|
+
} catch {}
|
|
7622
|
+
const failed = isObject(json) && (typeof json.exit_code === "number" && json.exit_code !== 0 || json.timed_out === true);
|
|
7623
|
+
const text = isObject(json) ? promptText(json) ?? raw : typeof raw === "string" ? raw : Array.isArray(raw) ? raw.map((c) => typeof c.text === "string" ? c.text : `[${c.type}]`).join(`
|
|
7624
|
+
`) : "";
|
|
7625
|
+
return {
|
|
7626
|
+
kind: "tool_result",
|
|
7627
|
+
isError: b.is_error === true || failed,
|
|
7628
|
+
text: text.trimEnd()
|
|
7629
|
+
};
|
|
7630
|
+
}
|
|
7631
|
+
function steps3(events) {
|
|
7632
|
+
const out = [];
|
|
7633
|
+
for (const e of events) {
|
|
7634
|
+
if (e?.type === "assistant")
|
|
7635
|
+
for (const b of content2(e)) {
|
|
7636
|
+
if (b.type === "tool_use")
|
|
7637
|
+
out.push({
|
|
7638
|
+
kind: "tool_call",
|
|
7639
|
+
name: typeof b.name === "string" ? b.name : "",
|
|
7640
|
+
input: isObject(b.input) ? b.input : null
|
|
7641
|
+
});
|
|
7642
|
+
else if (b.type === "text" && typeof b.text === "string")
|
|
7643
|
+
out.push({ kind: "text", text: b.text });
|
|
7644
|
+
}
|
|
7645
|
+
else if (e?.type === "user") {
|
|
7646
|
+
for (const b of content2(e))
|
|
7647
|
+
if (b.type === "tool_result")
|
|
7648
|
+
out.push(toolResult(b));
|
|
7649
|
+
}
|
|
7650
|
+
}
|
|
7651
|
+
return out;
|
|
7652
|
+
}
|
|
7653
|
+
function resultError(result) {
|
|
7654
|
+
const errors = Array.isArray(result.errors) ? result.errors.filter((x) => typeof x === "string").join("; ") : "";
|
|
7655
|
+
const said = errors || result.result || "";
|
|
7656
|
+
return `grok result is an error (subtype=${result.subtype})${said ? `: ${said.slice(0, 300)}` : ""}`;
|
|
7657
|
+
}
|
|
7658
|
+
function read3(stdout, exited, timeoutMs) {
|
|
7659
|
+
const events = stdout.split(`
|
|
7660
|
+
`).filter((l) => l.trim() !== "").map(parse3);
|
|
7661
|
+
const result = events.findLast((e) => e?.type === "result");
|
|
7662
|
+
const trace = {
|
|
7663
|
+
steps: steps3(events),
|
|
7664
|
+
final: typeof result?.result === "string" ? result.result : "",
|
|
7665
|
+
turns: result?.num_turns ?? null,
|
|
7666
|
+
cost: result?.total_cost_usd ?? null
|
|
7667
|
+
};
|
|
7668
|
+
const init = events.find((e) => e?.type === "system" && e.subtype === "init");
|
|
7669
|
+
const reportedModel = typeof init?.model === "string" && init.model !== "unknown" ? init.model : null;
|
|
7670
|
+
const sessionId = typeof init?.session_id === "string" && init.session_id !== "" ? init.session_id : undefined;
|
|
7671
|
+
const fail = (reason) => ({
|
|
7672
|
+
...trace,
|
|
7673
|
+
reportedModel,
|
|
7674
|
+
version: null,
|
|
7675
|
+
ok: false,
|
|
7676
|
+
reason,
|
|
7677
|
+
...sessionId === undefined ? {} : { sessionId }
|
|
7678
|
+
});
|
|
7679
|
+
if (exited.timedOut)
|
|
7680
|
+
return fail(`timed out after ${timeoutMs / 1000}s`);
|
|
7681
|
+
if (!init) {
|
|
7682
|
+
const said = stdout.trim().split(`
|
|
7683
|
+
`)[0]?.slice(0, 300);
|
|
7684
|
+
return fail(`grok did not start a session (exit ${exited.code ?? "by signal"})${said ? `: ${said}` : ", see stderr.txt"}`);
|
|
7685
|
+
}
|
|
7686
|
+
if (result && (result.is_error === true || result.subtype !== "success"))
|
|
7687
|
+
return fail(resultError(result));
|
|
7688
|
+
if (sessionId === undefined)
|
|
7689
|
+
return fail("grok's init line has no session_id");
|
|
7690
|
+
if (!result)
|
|
7691
|
+
return fail(`grok exited ${exited.code ?? "by signal"} without a result, see stderr.txt`);
|
|
7692
|
+
return { ...trace, reportedModel, version: null, ok: true, sessionId };
|
|
7693
|
+
}
|
|
7694
|
+
function progress3(line) {
|
|
7695
|
+
const e = parse3(line);
|
|
7696
|
+
if (e?.type !== "assistant")
|
|
7697
|
+
return;
|
|
7698
|
+
const p = {};
|
|
7699
|
+
for (const b of content2(e)) {
|
|
7700
|
+
const cmd = b.input?.command;
|
|
7701
|
+
if (b.type === "tool_use" && typeof cmd === "string")
|
|
7702
|
+
p.command = cmd.split(`
|
|
7703
|
+
`)[0];
|
|
7704
|
+
}
|
|
7705
|
+
return p;
|
|
7706
|
+
}
|
|
7707
|
+
function isScript(bin) {
|
|
7708
|
+
const fd = openSync(bin, "r");
|
|
7709
|
+
try {
|
|
7710
|
+
const head = Buffer.alloc(2);
|
|
7711
|
+
return readSync(fd, head, 0, 2, 0) === 2 && head.toString() === "#!";
|
|
7712
|
+
} finally {
|
|
7713
|
+
closeSync(fd);
|
|
7714
|
+
}
|
|
7715
|
+
}
|
|
7716
|
+
function loginProblem() {
|
|
7717
|
+
const relogin = "Run `grok login`, then run eval again";
|
|
7718
|
+
if (!existsSync3(USER_AUTH))
|
|
7719
|
+
return {
|
|
7720
|
+
loggedOut: true,
|
|
7721
|
+
reason: `grok is not logged in (no ${USER_AUTH}). ${relogin}. Headless grok needs that login: XAI_API_KEY alone is not enough`
|
|
7722
|
+
};
|
|
7723
|
+
let auth;
|
|
7724
|
+
try {
|
|
7725
|
+
auth = JSON.parse(readFileSync2(USER_AUTH, "utf8"));
|
|
7726
|
+
} catch {
|
|
7727
|
+
return {
|
|
7728
|
+
loggedOut: false,
|
|
7729
|
+
reason: `${USER_AUTH} is not valid JSON. ${relogin}`
|
|
7730
|
+
};
|
|
7731
|
+
}
|
|
7732
|
+
const logins = isObject(auth) ? Object.values(auth).filter((v) => isObject(v) && typeof v.key === "string" && v.key !== "") : [];
|
|
7733
|
+
if (logins.length === 0)
|
|
7734
|
+
return {
|
|
7735
|
+
loggedOut: true,
|
|
7736
|
+
reason: `grok is not logged in (${USER_AUTH} holds no login). ${relogin}`
|
|
7737
|
+
};
|
|
7738
|
+
const expiries = logins.map((l) => l.expires_at);
|
|
7739
|
+
if (expiries.some((x) => x === undefined))
|
|
7740
|
+
return;
|
|
7741
|
+
const times = expiries.map((x) => Date.parse(String(x)));
|
|
7742
|
+
if (times.some(Number.isNaN))
|
|
7743
|
+
return {
|
|
7744
|
+
loggedOut: false,
|
|
7745
|
+
reason: `${USER_AUTH} has an expires_at that is not a date (${expiries.join(", ")}). ${relogin}`
|
|
7746
|
+
};
|
|
7747
|
+
const latest = Math.max(...times);
|
|
7748
|
+
if (latest - Date.now() > REFRESH_WINDOW_MS)
|
|
7749
|
+
return;
|
|
7750
|
+
return {
|
|
7751
|
+
loggedOut: false,
|
|
7752
|
+
reason: `grok's login ${latest < Date.now() ? "expired" : "expires"} at ${new Date(latest).toISOString()}. Run grok once (e.g. \`grok -p hi\`) so it refreshes the login in ${USER_AUTH}, then run eval again: eval's runs must not refresh it themselves`
|
|
7753
|
+
};
|
|
7754
|
+
}
|
|
7755
|
+
function linkAuth(configDir) {
|
|
7756
|
+
const link = join4(configDir, "auth.json");
|
|
7757
|
+
let st;
|
|
7758
|
+
try {
|
|
7759
|
+
st = lstatSync2(link);
|
|
7760
|
+
} catch (e) {
|
|
7761
|
+
if (e.code !== "ENOENT")
|
|
7762
|
+
throw e;
|
|
7763
|
+
}
|
|
7764
|
+
if (st && !st.isSymbolicLink())
|
|
7765
|
+
return {
|
|
7766
|
+
loggedOut: false,
|
|
7767
|
+
reason: `${link} is a file, not a link to ${USER_AUTH}: a grok run refreshed its login there, so it is newer than ${USER_AUTH}. Move it back with \`mv ${link} ${USER_AUTH}\`, then run eval again`
|
|
7768
|
+
};
|
|
7769
|
+
if (st && readlinkSync(link) === USER_AUTH)
|
|
7770
|
+
return;
|
|
7771
|
+
if (st)
|
|
7772
|
+
unlinkSync2(link);
|
|
7773
|
+
symlinkSync2(USER_AUTH, link);
|
|
7774
|
+
return;
|
|
7775
|
+
}
|
|
7776
|
+
function checkLogin3(opts) {
|
|
7777
|
+
if (isScript(opts.bin))
|
|
7778
|
+
return {
|
|
7779
|
+
loggedOut: false,
|
|
7780
|
+
reason: `${opts.bin} is a script, not the grok executable (cmux, for one, puts a bash wrapper named grok on PATH). Pass grok's own binary with --harness-bin, ${BIN} by default`
|
|
7781
|
+
};
|
|
7782
|
+
return loginProblem() ?? linkAuth(opts.configDir);
|
|
7783
|
+
}
|
|
7784
|
+
var grok = {
|
|
7785
|
+
name: "grok-cli",
|
|
7786
|
+
bin: BIN,
|
|
7787
|
+
installHint: "Install grok (curl -fsSL https://x.ai/cli/install.sh | bash) or pass --harness-bin PATH.",
|
|
7788
|
+
configDirName: ".grok",
|
|
7789
|
+
reservedArgs: [
|
|
7790
|
+
[
|
|
7791
|
+
[
|
|
7792
|
+
"-p",
|
|
7793
|
+
"--single",
|
|
7794
|
+
"--prompt-file",
|
|
7795
|
+
"--prompt-json",
|
|
7796
|
+
"--output-format",
|
|
7797
|
+
"--json-schema",
|
|
7798
|
+
"--always-approve",
|
|
7799
|
+
"--permission-mode",
|
|
7800
|
+
"--no-auto-update",
|
|
7801
|
+
"--cwd",
|
|
7802
|
+
"-w",
|
|
7803
|
+
"--worktree"
|
|
7804
|
+
],
|
|
7805
|
+
"firegoat sets it for every run"
|
|
7806
|
+
],
|
|
7807
|
+
[
|
|
7808
|
+
["-m", "--model"],
|
|
7809
|
+
"choose the model with top-level model: (or eval --model)"
|
|
7810
|
+
],
|
|
7811
|
+
[
|
|
7812
|
+
[
|
|
7813
|
+
"-r",
|
|
7814
|
+
"--resume",
|
|
7815
|
+
"-c",
|
|
7816
|
+
"--continue",
|
|
7817
|
+
"-s",
|
|
7818
|
+
"--session-id",
|
|
7819
|
+
"--fork-session"
|
|
7820
|
+
],
|
|
7821
|
+
"every run is a fresh session"
|
|
7822
|
+
]
|
|
7823
|
+
],
|
|
7824
|
+
uploads: false,
|
|
7825
|
+
args: ({ prompt, model, extraArgs }) => [
|
|
7826
|
+
"-p",
|
|
7827
|
+
prompt,
|
|
7828
|
+
...FLAGS3,
|
|
7829
|
+
...model === undefined ? [] : ["-m", model],
|
|
7830
|
+
...extraArgs
|
|
7831
|
+
],
|
|
7832
|
+
env: ({ configDir, run }) => ({
|
|
7833
|
+
GROK_HOME: configDir,
|
|
7834
|
+
GROK_DISABLE_AUTOUPDATER: "1",
|
|
7835
|
+
...run === undefined ? {} : homeEnv(run.home)
|
|
7836
|
+
}),
|
|
7837
|
+
authLabel: () => `your grok login (${USER_AUTH})`,
|
|
7838
|
+
checkLogin: checkLogin3,
|
|
7839
|
+
progress: progress3,
|
|
7840
|
+
read: read3,
|
|
7841
|
+
sessionsDir: (configDir) => join4(configDir, "sessions"),
|
|
7842
|
+
findSession(configDir, sessionId) {
|
|
7843
|
+
const sessions = join4(configDir, "sessions");
|
|
7844
|
+
if (!existsSync3(sessions))
|
|
7845
|
+
return null;
|
|
7846
|
+
for (const d of readdirSync3(sessions)) {
|
|
7847
|
+
const dir = join4(sessions, d, sessionId);
|
|
7848
|
+
if (existsSync3(dir) && statSync(dir).isDirectory())
|
|
7849
|
+
return dir;
|
|
7850
|
+
}
|
|
7851
|
+
return null;
|
|
7852
|
+
}
|
|
7853
|
+
};
|
|
7854
|
+
|
|
7855
|
+
// src/harnesses/index.ts
|
|
7856
|
+
var HARNESSES = new Map([claude, codex, grok].map((h) => [h.name, h]));
|
|
7857
|
+
var DEFAULT_HARNESS = claude.name;
|
|
7858
|
+
function validHarnesses() {
|
|
7859
|
+
return `(valid: ${[...HARNESSES.keys()].join(", ")})`;
|
|
7860
|
+
}
|
|
7861
|
+
|
|
7038
7862
|
// src/config.ts
|
|
7039
7863
|
var CONFIG_FILE = "firegoat.yaml";
|
|
7040
7864
|
var DEFAULT_TIMEOUT = 600;
|
|
@@ -7047,7 +7871,7 @@ function fail(path, msg) {
|
|
|
7047
7871
|
function isRecord(v) {
|
|
7048
7872
|
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
7049
7873
|
}
|
|
7050
|
-
function
|
|
7874
|
+
function str2(v, path) {
|
|
7051
7875
|
if (typeof v !== "string" || v.trim() === "")
|
|
7052
7876
|
fail(path, "must be a non-empty string");
|
|
7053
7877
|
return v;
|
|
@@ -7075,23 +7899,29 @@ function parseConfig(text) {
|
|
|
7075
7899
|
fail("(root)", "must be a mapping");
|
|
7076
7900
|
onlyKeys(raw, "(root)", [
|
|
7077
7901
|
"runs",
|
|
7902
|
+
"harness",
|
|
7078
7903
|
"model",
|
|
7079
7904
|
"build",
|
|
7080
7905
|
"install",
|
|
7081
7906
|
"cleanup",
|
|
7082
7907
|
"vars",
|
|
7083
7908
|
"env",
|
|
7084
|
-
"
|
|
7909
|
+
"harness_args",
|
|
7085
7910
|
"tests"
|
|
7086
7911
|
]);
|
|
7087
7912
|
const runs = posInt(raw.runs, "runs", 1);
|
|
7088
|
-
const
|
|
7913
|
+
const harnesses = raw.harness === undefined ? [DEFAULT_HARNESS] : nameList(raw.harness, "harness", "a harness name");
|
|
7914
|
+
harnesses.forEach((h, i) => {
|
|
7915
|
+
if (!HARNESSES.has(h))
|
|
7916
|
+
fail(Array.isArray(raw.harness) ? `harness[${i}]` : "harness", `unknown harness ${JSON.stringify(h)} ${validHarnesses()}`);
|
|
7917
|
+
});
|
|
7918
|
+
const models = raw.model === undefined ? [] : nameList(raw.model, "model", "a model name");
|
|
7089
7919
|
const build = commands(raw.build, "build", EVAL_PLACEHOLDERS);
|
|
7090
7920
|
const install = commands(raw.install, "install", RUN_PLACEHOLDERS);
|
|
7091
7921
|
const cleanup = commands(raw.cleanup, "cleanup", RUN_PLACEHOLDERS);
|
|
7092
7922
|
const vars = parseVars(raw.vars);
|
|
7093
7923
|
const env = parseEnv(raw.env, "env");
|
|
7094
|
-
const
|
|
7924
|
+
const harnessArgs = parseHarnessArgs(raw.harness_args, "harness_args");
|
|
7095
7925
|
if (!Array.isArray(raw.tests) || raw.tests.length === 0)
|
|
7096
7926
|
fail("tests", "must be a non-empty list");
|
|
7097
7927
|
const names = new Set;
|
|
@@ -7108,9 +7938,9 @@ function parseConfig(text) {
|
|
|
7108
7938
|
"env",
|
|
7109
7939
|
"install",
|
|
7110
7940
|
"cleanup",
|
|
7111
|
-
"
|
|
7941
|
+
"harness_args"
|
|
7112
7942
|
]);
|
|
7113
|
-
const name =
|
|
7943
|
+
const name = str2(t.name, `${p}.name`);
|
|
7114
7944
|
if (!NAME.test(name))
|
|
7115
7945
|
fail(`${p}.name`, "may only contain letters, digits, '.', '_' and '-'");
|
|
7116
7946
|
if (names.has(name))
|
|
@@ -7125,27 +7955,28 @@ function parseConfig(text) {
|
|
|
7125
7955
|
if (!isRecord(c))
|
|
7126
7956
|
fail(cp, "must be a mapping");
|
|
7127
7957
|
onlyKeys(c, cp, ["run"]);
|
|
7128
|
-
const run =
|
|
7958
|
+
const run = str2(c.run, `${cp}.run`);
|
|
7129
7959
|
checkPlaceholders(run, `${cp}.run`, RUN_PLACEHOLDERS);
|
|
7130
7960
|
checks.push({ run });
|
|
7131
7961
|
});
|
|
7132
7962
|
return {
|
|
7133
7963
|
name,
|
|
7134
|
-
prompt:
|
|
7135
|
-
end_goal:
|
|
7964
|
+
prompt: str2(t.prompt, `${p}.prompt`),
|
|
7965
|
+
end_goal: str2(t.end_goal, `${p}.end_goal`),
|
|
7136
7966
|
checks,
|
|
7137
7967
|
timeout: posInt(t.timeout, `${p}.timeout`, DEFAULT_TIMEOUT),
|
|
7138
7968
|
env: { ...env, ...parseEnv(t.env, `${p}.env`) },
|
|
7139
7969
|
install: commands(t.install, `${p}.install`, RUN_PLACEHOLDERS),
|
|
7140
7970
|
cleanup: commands(t.cleanup, `${p}.cleanup`, RUN_PLACEHOLDERS),
|
|
7141
|
-
|
|
7142
|
-
...
|
|
7143
|
-
...
|
|
7971
|
+
harnessArgs: [
|
|
7972
|
+
...harnessArgs,
|
|
7973
|
+
...parseHarnessArgs(t.harness_args, `${p}.harness_args`)
|
|
7144
7974
|
]
|
|
7145
7975
|
};
|
|
7146
7976
|
});
|
|
7147
7977
|
return {
|
|
7148
7978
|
runs,
|
|
7979
|
+
harnesses,
|
|
7149
7980
|
models,
|
|
7150
7981
|
build,
|
|
7151
7982
|
install,
|
|
@@ -7164,63 +7995,45 @@ function commands(raw, path, allowed) {
|
|
|
7164
7995
|
const list = typeof raw === "string" ? [raw] : raw;
|
|
7165
7996
|
return list.map((c, i) => {
|
|
7166
7997
|
const p = typeof raw === "string" ? path : `${path}[${i}]`;
|
|
7167
|
-
const cmd =
|
|
7998
|
+
const cmd = str2(c, p);
|
|
7168
7999
|
checkPlaceholders(cmd, p, allowed);
|
|
7169
8000
|
return cmd;
|
|
7170
8001
|
});
|
|
7171
8002
|
}
|
|
7172
|
-
function
|
|
7173
|
-
if (raw === undefined)
|
|
7174
|
-
return [];
|
|
8003
|
+
function nameList(raw, key, what) {
|
|
7175
8004
|
if (typeof raw === "string")
|
|
7176
|
-
return [
|
|
8005
|
+
return [str2(raw, key)];
|
|
7177
8006
|
if (!Array.isArray(raw) || raw.length === 0)
|
|
7178
|
-
fail(
|
|
8007
|
+
fail(key, `must be ${what} or a non-empty list of them`);
|
|
7179
8008
|
const seen = new Set;
|
|
7180
8009
|
return raw.map((m, i) => {
|
|
7181
|
-
const name =
|
|
8010
|
+
const name = str2(m, `${key}[${i}]`);
|
|
7182
8011
|
if (seen.has(name))
|
|
7183
|
-
fail(
|
|
8012
|
+
fail(`${key}[${i}]`, `duplicate ${key} ${JSON.stringify(name)}`);
|
|
7184
8013
|
seen.add(name);
|
|
7185
8014
|
return name;
|
|
7186
8015
|
});
|
|
7187
8016
|
}
|
|
7188
|
-
|
|
7189
|
-
[
|
|
7190
|
-
[
|
|
7191
|
-
"-p",
|
|
7192
|
-
"--print",
|
|
7193
|
-
"--output-format",
|
|
7194
|
-
"--verbose",
|
|
7195
|
-
"--setting-sources",
|
|
7196
|
-
"--strict-mcp-config",
|
|
7197
|
-
"--disable-slash-commands",
|
|
7198
|
-
"--dangerously-skip-permissions"
|
|
7199
|
-
],
|
|
7200
|
-
"firegoat sets it for every run"
|
|
7201
|
-
],
|
|
7202
|
-
[["--model"], "choose the model with top-level model: (or eval --model)"],
|
|
7203
|
-
[
|
|
7204
|
-
["--session-id", "--resume", "-r", "--continue", "-c"],
|
|
7205
|
-
"every run is a fresh session"
|
|
7206
|
-
]
|
|
7207
|
-
];
|
|
7208
|
-
function parseClaudeArgs(raw, path) {
|
|
8017
|
+
function parseHarnessArgs(raw, path) {
|
|
7209
8018
|
if (raw === undefined)
|
|
7210
8019
|
return [];
|
|
7211
8020
|
if (!Array.isArray(raw))
|
|
7212
|
-
fail(path, "must be a list of
|
|
8021
|
+
fail(path, "must be a list of harness arguments, one per entry, e.g. [--chrome]");
|
|
7213
8022
|
return raw.map((a, i) => {
|
|
7214
8023
|
const p = `${path}[${i}]`;
|
|
7215
|
-
const arg =
|
|
7216
|
-
for (const [flags, why] of OWNED_FLAGS)
|
|
7217
|
-
for (const f of flags)
|
|
7218
|
-
if (arg === f || arg.startsWith(`${f}=`))
|
|
7219
|
-
fail(p, `${f} is not allowed: ${why}`);
|
|
8024
|
+
const arg = str2(a, p);
|
|
7220
8025
|
checkPlaceholders(arg, p, RUN_PLACEHOLDERS);
|
|
7221
|
-
return arg;
|
|
8026
|
+
return { arg, path: p };
|
|
7222
8027
|
});
|
|
7223
8028
|
}
|
|
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
|
+
}
|
|
7224
8037
|
var VAR_KEY = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
7225
8038
|
function parseVars(raw) {
|
|
7226
8039
|
if (raw === undefined)
|
|
@@ -7240,7 +8053,10 @@ function parseVars(raw) {
|
|
|
7240
8053
|
var RESERVED_ENV = new Set([
|
|
7241
8054
|
"PATH",
|
|
7242
8055
|
"CLAUDE_CONFIG_DIR",
|
|
7243
|
-
"CLAUDE_SECURESTORAGE_CONFIG_DIR"
|
|
8056
|
+
"CLAUDE_SECURESTORAGE_CONFIG_DIR",
|
|
8057
|
+
"CODEX_HOME",
|
|
8058
|
+
"GROK_HOME",
|
|
8059
|
+
"GROK_DISABLE_AUTOUPDATER"
|
|
7244
8060
|
]);
|
|
7245
8061
|
var HOME_ENV = new Set([
|
|
7246
8062
|
"HOME",
|
|
@@ -7270,7 +8086,7 @@ function parseEnv(raw, path) {
|
|
|
7270
8086
|
if (HOME_ENV.has(k))
|
|
7271
8087
|
fail(p, "Reserved: every run gets its own home ($FIREGOAT_HOME)");
|
|
7272
8088
|
if (RESERVED_ENV.has(k) || k.startsWith("FIREGOAT_"))
|
|
7273
|
-
fail(p, "is set by firegoat and cannot be overridden (reserved: PATH, CLAUDE_CONFIG_DIR, CLAUDE_SECURESTORAGE_CONFIG_DIR, FIREGOAT_*)");
|
|
8089
|
+
fail(p, "is set by firegoat and cannot be overridden (reserved: PATH, CLAUDE_CONFIG_DIR, CLAUDE_SECURESTORAGE_CONFIG_DIR, CODEX_HOME, GROK_HOME, GROK_DISABLE_AUTOUPDATER, FIREGOAT_*)");
|
|
7274
8090
|
if (typeof v !== "string")
|
|
7275
8091
|
fail(p, "must be a string (quote numbers and booleans)");
|
|
7276
8092
|
checkPlaceholders(v, p, RUN_PLACEHOLDERS);
|
|
@@ -7324,9 +8140,9 @@ function render(text, vars) {
|
|
|
7324
8140
|
}
|
|
7325
8141
|
function loadConfig(dir) {
|
|
7326
8142
|
const file = `${dir}/${CONFIG_FILE}`;
|
|
7327
|
-
if (!
|
|
8143
|
+
if (!existsSync4(file))
|
|
7328
8144
|
throw new ConfigError(`${CONFIG_FILE} not found in ${dir}. Run: npx firegoat init`);
|
|
7329
|
-
return parseConfig(
|
|
8145
|
+
return parseConfig(readFileSync3(file, "utf8"));
|
|
7330
8146
|
}
|
|
7331
8147
|
|
|
7332
8148
|
// src/eval.ts
|
|
@@ -7334,27 +8150,71 @@ import { randomBytes as randomBytes2 } from "node:crypto";
|
|
|
7334
8150
|
import {
|
|
7335
8151
|
appendFileSync,
|
|
7336
8152
|
createWriteStream as createWriteStream2,
|
|
7337
|
-
mkdirSync as
|
|
8153
|
+
mkdirSync as mkdirSync5,
|
|
7338
8154
|
writeFileSync as writeFileSync4
|
|
7339
8155
|
} from "node:fs";
|
|
7340
|
-
import { basename as basename3, delimiter as delimiter2, join as
|
|
8156
|
+
import { basename as basename3, delimiter as delimiter2, join as join10 } from "node:path";
|
|
8157
|
+
|
|
8158
|
+
// src/agent-dirs.ts
|
|
8159
|
+
import { createHash, randomInt } from "node:crypto";
|
|
8160
|
+
import { mkdirSync as mkdirSync2, realpathSync, symlinkSync as symlinkSync3 } from "node:fs";
|
|
8161
|
+
import { homedir as homedir3 } from "node:os";
|
|
8162
|
+
import { join as join5 } from "node:path";
|
|
8163
|
+
var ID_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789";
|
|
8164
|
+
var ID_LENGTH = 8;
|
|
8165
|
+
var MAX_TRIES = 10;
|
|
8166
|
+
function projectAgentDir(projectRoot) {
|
|
8167
|
+
const hash = createHash("sha256").update(realpathSync(projectRoot)).digest("hex").slice(0, 12);
|
|
8168
|
+
return join5(firegoatHome(), "runs", hash);
|
|
8169
|
+
}
|
|
8170
|
+
function firegoatHome() {
|
|
8171
|
+
return join5(realpathSync(homedir3()), ".firegoat");
|
|
8172
|
+
}
|
|
8173
|
+
function randomId() {
|
|
8174
|
+
let id = "";
|
|
8175
|
+
for (let i = 0;i < ID_LENGTH; i++)
|
|
8176
|
+
id += ID_CHARS[randomInt(ID_CHARS.length)];
|
|
8177
|
+
return id;
|
|
8178
|
+
}
|
|
8179
|
+
function ensureProjectAgentDir(projectRoot) {
|
|
8180
|
+
mkdirSync2(firegoatHome(), { recursive: true, mode: 448 });
|
|
8181
|
+
const dir = projectAgentDir(projectRoot);
|
|
8182
|
+
mkdirSync2(dir, { recursive: true });
|
|
8183
|
+
return dir;
|
|
8184
|
+
}
|
|
8185
|
+
function makeAgentDir(projectDir) {
|
|
8186
|
+
for (let i = 0;i < MAX_TRIES; i++) {
|
|
8187
|
+
const dir = join5(projectDir, randomId());
|
|
8188
|
+
try {
|
|
8189
|
+
mkdirSync2(dir);
|
|
8190
|
+
return dir;
|
|
8191
|
+
} catch (e) {
|
|
8192
|
+
if (e.code !== "EEXIST")
|
|
8193
|
+
throw e;
|
|
8194
|
+
}
|
|
8195
|
+
}
|
|
8196
|
+
throw new Error(`Could not create a new run folder in ${projectDir}: ${MAX_TRIES} random names were taken`);
|
|
8197
|
+
}
|
|
8198
|
+
function linkDir(target, link) {
|
|
8199
|
+
symlinkSync3(target, link, "dir");
|
|
8200
|
+
}
|
|
7341
8201
|
|
|
7342
8202
|
// src/credentials.ts
|
|
7343
8203
|
import { randomBytes } from "node:crypto";
|
|
7344
8204
|
import {
|
|
7345
8205
|
chmodSync,
|
|
7346
|
-
closeSync,
|
|
8206
|
+
closeSync as closeSync2,
|
|
7347
8207
|
fsyncSync,
|
|
7348
|
-
lstatSync,
|
|
7349
|
-
mkdirSync,
|
|
7350
|
-
openSync,
|
|
7351
|
-
readFileSync as
|
|
8208
|
+
lstatSync as lstatSync3,
|
|
8209
|
+
mkdirSync as mkdirSync3,
|
|
8210
|
+
openSync as openSync2,
|
|
8211
|
+
readFileSync as readFileSync4,
|
|
7352
8212
|
renameSync,
|
|
7353
|
-
unlinkSync,
|
|
8213
|
+
unlinkSync as unlinkSync3,
|
|
7354
8214
|
writeSync
|
|
7355
8215
|
} from "node:fs";
|
|
7356
|
-
import { homedir } from "node:os";
|
|
7357
|
-
import { join } from "node:path";
|
|
8216
|
+
import { homedir as homedir4 } from "node:os";
|
|
8217
|
+
import { join as join6 } from "node:path";
|
|
7358
8218
|
|
|
7359
8219
|
// src/firegoat-api.ts
|
|
7360
8220
|
var DEFAULT_BASE_URL = "https://firegoat.ai/evals";
|
|
@@ -7506,10 +8366,10 @@ async function whoami(base, key) {
|
|
|
7506
8366
|
// src/credentials.ts
|
|
7507
8367
|
var KEY = /^fire_[\x21-\x7e]+$/;
|
|
7508
8368
|
function credentialsDir() {
|
|
7509
|
-
return
|
|
8369
|
+
return join6(homedir4(), ".firegoat");
|
|
7510
8370
|
}
|
|
7511
8371
|
function credentialsPath() {
|
|
7512
|
-
return
|
|
8372
|
+
return join6(credentialsDir(), "credentials.json");
|
|
7513
8373
|
}
|
|
7514
8374
|
function uid() {
|
|
7515
8375
|
if (!process.getuid)
|
|
@@ -7518,7 +8378,7 @@ function uid() {
|
|
|
7518
8378
|
}
|
|
7519
8379
|
function lstatOrNull(p) {
|
|
7520
8380
|
try {
|
|
7521
|
-
return
|
|
8381
|
+
return lstatSync3(p);
|
|
7522
8382
|
} catch (e) {
|
|
7523
8383
|
if (e.code === "ENOENT")
|
|
7524
8384
|
return null;
|
|
@@ -7548,7 +8408,7 @@ function loadCredentials() {
|
|
|
7548
8408
|
throw new ConfigError(`${path} is readable by others (mode ${(st.mode & 511).toString(8)}): chmod 600 it or run firegoat login again`);
|
|
7549
8409
|
let c;
|
|
7550
8410
|
try {
|
|
7551
|
-
c = JSON.parse(
|
|
8411
|
+
c = JSON.parse(readFileSync4(path, "utf8"));
|
|
7552
8412
|
} catch {
|
|
7553
8413
|
throw new ConfigError(`${path} is not valid JSON: run firegoat logout, then firegoat login`);
|
|
7554
8414
|
}
|
|
@@ -7572,20 +8432,20 @@ function saveCredentials(c) {
|
|
|
7572
8432
|
if (st)
|
|
7573
8433
|
assertOwned(dir, st, "dir");
|
|
7574
8434
|
else
|
|
7575
|
-
|
|
8435
|
+
mkdirSync3(dir, { mode: 448 });
|
|
7576
8436
|
chmodSync(dir, 448);
|
|
7577
8437
|
const tmp = `${credentialsPath()}.${process.pid}.${randomBytes(6).toString("hex")}.tmp`;
|
|
7578
|
-
const fd =
|
|
8438
|
+
const fd = openSync2(tmp, "wx", 384);
|
|
7579
8439
|
try {
|
|
7580
8440
|
writeSync(fd, `${JSON.stringify(c, null, 2)}
|
|
7581
8441
|
`);
|
|
7582
8442
|
fsyncSync(fd);
|
|
7583
8443
|
} catch (e) {
|
|
7584
|
-
|
|
7585
|
-
|
|
8444
|
+
closeSync2(fd);
|
|
8445
|
+
unlinkSync3(tmp);
|
|
7586
8446
|
throw e;
|
|
7587
8447
|
}
|
|
7588
|
-
|
|
8448
|
+
closeSync2(fd);
|
|
7589
8449
|
renameSync(tmp, credentialsPath());
|
|
7590
8450
|
}
|
|
7591
8451
|
function removeCredentials() {
|
|
@@ -7606,7 +8466,7 @@ function removeCredentials() {
|
|
|
7606
8466
|
if (!(e instanceof ConfigError))
|
|
7607
8467
|
throw e;
|
|
7608
8468
|
}
|
|
7609
|
-
|
|
8469
|
+
unlinkSync3(path);
|
|
7610
8470
|
return { saved };
|
|
7611
8471
|
}
|
|
7612
8472
|
function resolveKey(base) {
|
|
@@ -7628,15 +8488,15 @@ function resolveKey(base) {
|
|
|
7628
8488
|
var EXIT = { pass: 0, fail: 1, notGraded: 2, usage: 3 };
|
|
7629
8489
|
|
|
7630
8490
|
// src/firegoat-dir.ts
|
|
7631
|
-
import { mkdirSync as
|
|
7632
|
-
import { join as
|
|
8491
|
+
import { mkdirSync as mkdirSync4, writeFileSync as writeFileSync2 } from "node:fs";
|
|
8492
|
+
import { join as join7 } from "node:path";
|
|
7633
8493
|
var GITIGNORE = `*
|
|
7634
8494
|
`;
|
|
7635
8495
|
function ensureFiregoatDir(root) {
|
|
7636
|
-
const dir =
|
|
7637
|
-
|
|
8496
|
+
const dir = join7(root, ".firegoat");
|
|
8497
|
+
mkdirSync4(dir, { recursive: true });
|
|
7638
8498
|
try {
|
|
7639
|
-
|
|
8499
|
+
writeFileSync2(join7(dir, ".gitignore"), GITIGNORE, { flag: "wx" });
|
|
7640
8500
|
} catch (e) {
|
|
7641
8501
|
if (e.code !== "EEXIST")
|
|
7642
8502
|
throw e;
|
|
@@ -7645,17 +8505,48 @@ function ensureFiregoatDir(root) {
|
|
|
7645
8505
|
}
|
|
7646
8506
|
|
|
7647
8507
|
// src/git.ts
|
|
7648
|
-
import { spawnSync } from "node:child_process";
|
|
8508
|
+
import { spawnSync as spawnSync3 } from "node:child_process";
|
|
7649
8509
|
import { basename, dirname } from "node:path";
|
|
7650
8510
|
|
|
7651
8511
|
// src/process.ts
|
|
7652
|
-
import { spawn } from "node:child_process";
|
|
8512
|
+
import { execFileSync, spawn } from "node:child_process";
|
|
7653
8513
|
var NPM_INJECTED = /^(npm_config_|npm_lifecycle_|npm_package_)|^(npm_execpath|npm_node_execpath|npm_command|INIT_CWD)$/;
|
|
7654
8514
|
var CLAUDE_SESSION = /^(CLAUDECODE|CLAUDE_CODE_.+|CLAUDE_PID|CLAUDE_ENV_FILE|CLAUDE_SESSION_ID|CLAUDE_PROJECT_DIR|CLAUDE_EFFORT|CLAUDE_AGENT_SDK_.+|CLAUDE_PLUGIN_ROOT|CLAUDE_PLUGIN_DATA|CLAUDE_SKILL_DIR)$/;
|
|
7655
8515
|
var CLAUDE_AUTH = /^CLAUDE_CODE_(OAUTH_TOKEN|OAUTH_REFRESH_TOKEN|USE_[A-Z_]+|SKIP_[A-Z]+_AUTH|CLIENT_CERT|CLIENT_KEY|CLIENT_KEY_PASSPHRASE)$/;
|
|
7656
8516
|
function hostEnv() {
|
|
7657
8517
|
return Object.fromEntries(Object.entries(process.env).filter(([k]) => !NPM_INJECTED.test(k) && !k.startsWith("FIREGOAT_") && (!CLAUDE_SESSION.test(k) || CLAUDE_AUTH.test(k))));
|
|
7658
8518
|
}
|
|
8519
|
+
function processTree(pid) {
|
|
8520
|
+
const children = new Map;
|
|
8521
|
+
const ps = execFileSync("ps", ["-A", "-o", "pid=,ppid="], {
|
|
8522
|
+
encoding: "utf8"
|
|
8523
|
+
});
|
|
8524
|
+
for (const line of ps.split(`
|
|
8525
|
+
`)) {
|
|
8526
|
+
const [child, parent] = line.trim().split(/\s+/).map(Number);
|
|
8527
|
+
if (!child || parent === undefined)
|
|
8528
|
+
continue;
|
|
8529
|
+
children.set(parent, [...children.get(parent) ?? [], child]);
|
|
8530
|
+
}
|
|
8531
|
+
const tree = [pid];
|
|
8532
|
+
for (let i = 0;i < tree.length; i++)
|
|
8533
|
+
tree.push(...children.get(tree[i]) ?? []);
|
|
8534
|
+
return tree;
|
|
8535
|
+
}
|
|
8536
|
+
function killTree(pid) {
|
|
8537
|
+
let tree = [pid];
|
|
8538
|
+
let problem;
|
|
8539
|
+
try {
|
|
8540
|
+
tree = processTree(pid);
|
|
8541
|
+
} catch (e) {
|
|
8542
|
+
problem = e.message;
|
|
8543
|
+
}
|
|
8544
|
+
for (const p of tree)
|
|
8545
|
+
try {
|
|
8546
|
+
process.kill(p, "SIGKILL");
|
|
8547
|
+
} catch {}
|
|
8548
|
+
return problem;
|
|
8549
|
+
}
|
|
7659
8550
|
function run(opts) {
|
|
7660
8551
|
return new Promise((resolve) => {
|
|
7661
8552
|
const child = spawn(opts.cmd, opts.args, {
|
|
@@ -7667,7 +8558,12 @@ function run(opts) {
|
|
|
7667
8558
|
let settled = false;
|
|
7668
8559
|
const timer = setTimeout(() => {
|
|
7669
8560
|
timedOut = true;
|
|
7670
|
-
child.
|
|
8561
|
+
if (child.pid === undefined)
|
|
8562
|
+
return;
|
|
8563
|
+
const problem = killTree(child.pid);
|
|
8564
|
+
if (problem)
|
|
8565
|
+
opts.onStderr(Buffer.from(`firegoat: timed out; could not list ${opts.cmd}'s child processes (${problem}), killed only ${opts.cmd}
|
|
8566
|
+
`));
|
|
7671
8567
|
}, opts.timeoutMs);
|
|
7672
8568
|
const done = (r) => {
|
|
7673
8569
|
if (settled)
|
|
@@ -7714,7 +8610,7 @@ async function runShell(opts) {
|
|
|
7714
8610
|
|
|
7715
8611
|
// src/git.ts
|
|
7716
8612
|
function git(dir, args) {
|
|
7717
|
-
const r =
|
|
8613
|
+
const r = spawnSync3("git", args, {
|
|
7718
8614
|
cwd: dir,
|
|
7719
8615
|
env: hostEnv(),
|
|
7720
8616
|
encoding: "utf8"
|
|
@@ -7724,7 +8620,7 @@ function git(dir, args) {
|
|
|
7724
8620
|
return { code: r.status ?? -1, stdout: r.stdout.trim() };
|
|
7725
8621
|
}
|
|
7726
8622
|
function gitRepo(dir) {
|
|
7727
|
-
const r =
|
|
8623
|
+
const r = spawnSync3("git", [
|
|
7728
8624
|
"rev-parse",
|
|
7729
8625
|
"--path-format=absolute",
|
|
7730
8626
|
"--git-dir",
|
|
@@ -7807,213 +8703,11 @@ async function runAgent(harness, opts) {
|
|
|
7807
8703
|
return harness.read(stdout, exited, opts.timeoutMs);
|
|
7808
8704
|
}
|
|
7809
8705
|
|
|
7810
|
-
// src/harnesses/claude.ts
|
|
7811
|
-
import { spawnSync as spawnSync2 } from "node:child_process";
|
|
7812
|
-
import { existsSync as existsSync2, readdirSync } from "node:fs";
|
|
7813
|
-
import { join as join3 } from "node:path";
|
|
7814
|
-
var FLAGS = [
|
|
7815
|
-
"--output-format",
|
|
7816
|
-
"stream-json",
|
|
7817
|
-
"--verbose",
|
|
7818
|
-
"--dangerously-skip-permissions",
|
|
7819
|
-
"--strict-mcp-config",
|
|
7820
|
-
"--setting-sources",
|
|
7821
|
-
"",
|
|
7822
|
-
"--disable-slash-commands"
|
|
7823
|
-
];
|
|
7824
|
-
function parse(line) {
|
|
7825
|
-
try {
|
|
7826
|
-
const e = JSON.parse(line);
|
|
7827
|
-
return typeof e === "object" && e !== null ? e : undefined;
|
|
7828
|
-
} catch {
|
|
7829
|
-
return;
|
|
7830
|
-
}
|
|
7831
|
-
}
|
|
7832
|
-
function content(e) {
|
|
7833
|
-
const message = e?.message;
|
|
7834
|
-
return Array.isArray(message?.content) ? message.content : [];
|
|
7835
|
-
}
|
|
7836
|
-
function resultText(content) {
|
|
7837
|
-
if (typeof content === "string")
|
|
7838
|
-
return content;
|
|
7839
|
-
if (Array.isArray(content))
|
|
7840
|
-
return content.map((c) => typeof c.text === "string" ? c.text : `[${c.type}]`).join(`
|
|
7841
|
-
`);
|
|
7842
|
-
return "";
|
|
7843
|
-
}
|
|
7844
|
-
function steps(events) {
|
|
7845
|
-
const out = [];
|
|
7846
|
-
for (const e of events) {
|
|
7847
|
-
if (e?.type === "assistant")
|
|
7848
|
-
for (const b of content(e)) {
|
|
7849
|
-
if (b.type === "tool_use")
|
|
7850
|
-
out.push({
|
|
7851
|
-
kind: "tool_call",
|
|
7852
|
-
name: typeof b.name === "string" ? b.name : "",
|
|
7853
|
-
input: typeof b.input === "object" && b.input !== null ? b.input : null
|
|
7854
|
-
});
|
|
7855
|
-
else if (b.type === "text" && typeof b.text === "string")
|
|
7856
|
-
out.push({ kind: "text", text: b.text });
|
|
7857
|
-
}
|
|
7858
|
-
else if (e?.type === "user") {
|
|
7859
|
-
for (const b of content(e))
|
|
7860
|
-
if (b.type === "tool_result")
|
|
7861
|
-
out.push({
|
|
7862
|
-
kind: "tool_result",
|
|
7863
|
-
isError: b.is_error === true,
|
|
7864
|
-
text: resultText(b.content).trimEnd()
|
|
7865
|
-
});
|
|
7866
|
-
}
|
|
7867
|
-
}
|
|
7868
|
-
return out;
|
|
7869
|
-
}
|
|
7870
|
-
function read(stdout, exited, timeoutMs) {
|
|
7871
|
-
const events = stdout.split(`
|
|
7872
|
-
`).filter((l) => l.trim() !== "").map(parse);
|
|
7873
|
-
let final = "";
|
|
7874
|
-
for (const e of events)
|
|
7875
|
-
if (e?.type === "result" && typeof e.result === "string")
|
|
7876
|
-
final = e.result;
|
|
7877
|
-
const result = events.findLast((e) => e?.type === "result");
|
|
7878
|
-
const trace = {
|
|
7879
|
-
steps: steps(events),
|
|
7880
|
-
final,
|
|
7881
|
-
turns: result?.num_turns ?? null,
|
|
7882
|
-
cost: result?.total_cost_usd ?? null
|
|
7883
|
-
};
|
|
7884
|
-
const init = events.find((e) => e?.type === "system" && e.subtype === "init");
|
|
7885
|
-
if (!init) {
|
|
7886
|
-
const said = stdout.trim().split(`
|
|
7887
|
-
`)[0]?.slice(0, 300);
|
|
7888
|
-
return {
|
|
7889
|
-
...trace,
|
|
7890
|
-
reportedModel: null,
|
|
7891
|
-
version: null,
|
|
7892
|
-
ok: false,
|
|
7893
|
-
reason: `claude did not start a session (exit ${exited.code ?? "by signal"})${said ? `: ${said}` : ", see stderr.txt"}`
|
|
7894
|
-
};
|
|
7895
|
-
}
|
|
7896
|
-
const reportedModel = typeof init.model === "string" ? init.model : null;
|
|
7897
|
-
const version = typeof init.claude_code_version === "string" ? init.claude_code_version : null;
|
|
7898
|
-
if (typeof init.session_id !== "string" || init.session_id === "")
|
|
7899
|
-
return {
|
|
7900
|
-
...trace,
|
|
7901
|
-
reportedModel,
|
|
7902
|
-
version,
|
|
7903
|
-
ok: false,
|
|
7904
|
-
reason: "claude's init line has no session_id"
|
|
7905
|
-
};
|
|
7906
|
-
const sessionId = init.session_id;
|
|
7907
|
-
if (exited.timedOut)
|
|
7908
|
-
return {
|
|
7909
|
-
...trace,
|
|
7910
|
-
reportedModel,
|
|
7911
|
-
version,
|
|
7912
|
-
ok: false,
|
|
7913
|
-
reason: `timed out after ${timeoutMs / 1000}s`,
|
|
7914
|
-
sessionId
|
|
7915
|
-
};
|
|
7916
|
-
if (!result)
|
|
7917
|
-
return {
|
|
7918
|
-
...trace,
|
|
7919
|
-
reportedModel,
|
|
7920
|
-
version,
|
|
7921
|
-
ok: false,
|
|
7922
|
-
reason: `claude exited ${exited.code ?? "by signal"} without a result, see stderr.txt`,
|
|
7923
|
-
sessionId
|
|
7924
|
-
};
|
|
7925
|
-
if (result.is_error === true || result.subtype !== "success")
|
|
7926
|
-
return {
|
|
7927
|
-
...trace,
|
|
7928
|
-
reportedModel,
|
|
7929
|
-
version,
|
|
7930
|
-
ok: false,
|
|
7931
|
-
reason: `claude result is an error (subtype=${result.subtype})${result.result ? `: ${result.result.slice(0, 300)}` : ""}`,
|
|
7932
|
-
sessionId
|
|
7933
|
-
};
|
|
7934
|
-
return { ...trace, reportedModel, version, ok: true, sessionId };
|
|
7935
|
-
}
|
|
7936
|
-
function progress(line) {
|
|
7937
|
-
const e = parse(line);
|
|
7938
|
-
if (e?.type !== "assistant")
|
|
7939
|
-
return;
|
|
7940
|
-
const p = {};
|
|
7941
|
-
for (const b of content(e)) {
|
|
7942
|
-
const cmd = b.input?.command;
|
|
7943
|
-
if (b.type === "tool_use" && typeof cmd === "string")
|
|
7944
|
-
p.command = cmd.split(`
|
|
7945
|
-
`)[0];
|
|
7946
|
-
}
|
|
7947
|
-
return p;
|
|
7948
|
-
}
|
|
7949
|
-
function checkLogin(opts) {
|
|
7950
|
-
const r = spawnSync2(opts.bin, ["auth", "status"], {
|
|
7951
|
-
cwd: opts.cwd,
|
|
7952
|
-
env: opts.env,
|
|
7953
|
-
encoding: "utf8",
|
|
7954
|
-
timeout: 30000
|
|
7955
|
-
});
|
|
7956
|
-
if (r.error)
|
|
7957
|
-
return {
|
|
7958
|
-
loggedOut: false,
|
|
7959
|
-
reason: `could not run ${opts.bin}: ${r.error.message}`
|
|
7960
|
-
};
|
|
7961
|
-
let status;
|
|
7962
|
-
try {
|
|
7963
|
-
status = JSON.parse(r.stdout);
|
|
7964
|
-
} catch {}
|
|
7965
|
-
if (status?.loggedIn === true)
|
|
7966
|
-
return;
|
|
7967
|
-
if (status?.loggedIn === false)
|
|
7968
|
-
return {
|
|
7969
|
-
loggedOut: true,
|
|
7970
|
-
reason: "claude is not logged in. Run `claude` once and log in (or set ANTHROPIC_API_KEY), then run eval again"
|
|
7971
|
-
};
|
|
7972
|
-
return {
|
|
7973
|
-
loggedOut: false,
|
|
7974
|
-
reason: `claude auth status gave no login state (exit ${r.status}): ${(r.stdout + r.stderr).trim().slice(0, 300)}`
|
|
7975
|
-
};
|
|
7976
|
-
}
|
|
7977
|
-
var claude = {
|
|
7978
|
-
name: "claude",
|
|
7979
|
-
bin: "claude",
|
|
7980
|
-
installHint: "Install Claude Code (https://claude.com/claude-code) or pass --claude-bin PATH.",
|
|
7981
|
-
configDirName: ".claude",
|
|
7982
|
-
args: ({ prompt, model, extraArgs }) => [
|
|
7983
|
-
"-p",
|
|
7984
|
-
prompt,
|
|
7985
|
-
...FLAGS,
|
|
7986
|
-
...model === undefined ? [] : ["--model", model],
|
|
7987
|
-
...extraArgs
|
|
7988
|
-
],
|
|
7989
|
-
env: ({ configDir, homeEnvFile }) => ({
|
|
7990
|
-
CLAUDE_CONFIG_DIR: configDir,
|
|
7991
|
-
CLAUDE_SECURESTORAGE_CONFIG_DIR: "",
|
|
7992
|
-
...homeEnvFile === undefined ? {} : { CLAUDE_ENV_FILE: homeEnvFile }
|
|
7993
|
-
}),
|
|
7994
|
-
authLabel: () => process.env.ANTHROPIC_API_KEY ? "ANTHROPIC_API_KEY (claude bills the API key; unset it to use your claude login)" : "your claude login",
|
|
7995
|
-
checkLogin,
|
|
7996
|
-
progress,
|
|
7997
|
-
read,
|
|
7998
|
-
sessionsDir: (configDir) => join3(configDir, "projects"),
|
|
7999
|
-
findSession(configDir, sessionId) {
|
|
8000
|
-
const projects = join3(configDir, "projects");
|
|
8001
|
-
if (!existsSync2(projects))
|
|
8002
|
-
return null;
|
|
8003
|
-
for (const d of readdirSync(projects)) {
|
|
8004
|
-
const f = join3(projects, d, `${sessionId}.jsonl`);
|
|
8005
|
-
if (existsSync2(f))
|
|
8006
|
-
return f;
|
|
8007
|
-
}
|
|
8008
|
-
return null;
|
|
8009
|
-
}
|
|
8010
|
-
};
|
|
8011
|
-
|
|
8012
8706
|
// src/help.ts
|
|
8013
8707
|
var EXIT_CODES = `Exit codes:
|
|
8014
8708
|
0 Every run's script checks passed
|
|
8015
8709
|
1 A script check failed
|
|
8016
|
-
2 At least one run was not graded (
|
|
8710
|
+
2 At least one run was not graded (the harness missing or logged out, crash, timeout,
|
|
8017
8711
|
build failed); wins over 1
|
|
8018
8712
|
3 Usage or config error (bad flag, unknown test, invalid or missing firegoat.yaml,
|
|
8019
8713
|
firegoat.ai answered 401/403 for the upload key); any other failure of
|
|
@@ -8067,10 +8761,18 @@ firegoat.yaml, committed.
|
|
|
8067
8761
|
|
|
8068
8762
|
firegoat.yaml reference:
|
|
8069
8763
|
runs: 1 Runs per test
|
|
8764
|
+
harness: <name> | [<name>, ...]
|
|
8765
|
+
The agent CLI: claude-code (default), codex-cli
|
|
8766
|
+
or grok-cli.
|
|
8767
|
+
A list runs every test runs times per harness;
|
|
8768
|
+
eval --harness overrides
|
|
8070
8769
|
model: <name> | [<name>, ...]
|
|
8071
|
-
|
|
8072
|
-
|
|
8073
|
-
|
|
8770
|
+
The harness's --model (-m for codex, grok); a list
|
|
8771
|
+
runs
|
|
8772
|
+
every test runs times per model, on each harness
|
|
8773
|
+
(n counts 1..runs x harnesses x models, harness by
|
|
8774
|
+
harness, then model). Unset: no --model, the
|
|
8775
|
+
harness's default. eval --model overrides
|
|
8074
8776
|
build: <sh> | [<sh>, ...] Once per eval, in this folder; what it writes to
|
|
8075
8777
|
$FIREGOAT_OUT is first on the agent's PATH
|
|
8076
8778
|
install: <sh> | [<sh>, ...] In each fresh folder, before the agent starts; a
|
|
@@ -8090,7 +8792,8 @@ firegoat.yaml reference:
|
|
|
8090
8792
|
$FIREGOAT_HOME (no other $ expansion: $HOME stays
|
|
8091
8793
|
literal). The build step gets only the entries that
|
|
8092
8794
|
use only {{vars.*}}, {{run.id}} and $FIREGOAT_OUT.
|
|
8093
|
-
PATH, CLAUDE_CONFIG_DIR,
|
|
8795
|
+
PATH, CLAUDE_CONFIG_DIR, CODEX_HOME, GROK_HOME,
|
|
8796
|
+
GROK_DISABLE_AUTOUPDATER, FIREGOAT_* and
|
|
8094
8797
|
ANTHROPIC_MODEL (use model:) are reserved, and so
|
|
8095
8798
|
are HOME, XDG_CONFIG_HOME, XDG_CACHE_HOME,
|
|
8096
8799
|
XDG_DATA_HOME, XDG_STATE_HOME and CLAUDE_ENV_FILE:
|
|
@@ -8098,20 +8801,33 @@ firegoat.yaml reference:
|
|
|
8098
8801
|
tool that keeps state in ~ starts fresh in each
|
|
8099
8802
|
run. Only a tool that ignores HOME needs its own
|
|
8100
8803
|
var, e.g. MYTOOL_HOME: "$FIREGOAT_HOME/.mytool"
|
|
8101
|
-
|
|
8102
|
-
and --model
|
|
8103
|
-
|
|
8104
|
-
|
|
8105
|
-
[--
|
|
8106
|
-
|
|
8107
|
-
|
|
8108
|
-
|
|
8109
|
-
|
|
8110
|
-
|
|
8804
|
+
harness_args: [<arg>, ...] Extra harness arguments, after firegoat's own flags
|
|
8805
|
+
and --model (for codex, before the prompt), to
|
|
8806
|
+
every selected harness. Each entry is one
|
|
8807
|
+
argument, passed as-is (no shell splitting):
|
|
8808
|
+
claude: [--chrome] (Claude in Chrome, for tests
|
|
8809
|
+
that need a browser), [--plugin-dir, ./plugins/x],
|
|
8810
|
+
--mcp-config, --agents, --append-system-prompt;
|
|
8811
|
+
codex: [-c, key=value], --enable, --add-dir.
|
|
8812
|
+
Entries expand like env: values
|
|
8813
|
+
($FIREGOAT_WORKDIR, $FIREGOAT_HOME, $FIREGOAT_OUT,
|
|
8814
|
+
{{vars.KEY}}, {{run.*}}); run.json harnessArgs has
|
|
8815
|
+
the result. Flags firegoat sets are errors, also
|
|
8816
|
+
as --flag=value. claude-code: -p, --print,
|
|
8111
8817
|
--output-format, --verbose, --setting-sources,
|
|
8112
8818
|
--strict-mcp-config, --disable-slash-commands,
|
|
8113
8819
|
--dangerously-skip-permissions, --model (use
|
|
8114
|
-
model:), --session-id, --resume, -r, --continue,
|
|
8820
|
+
model:), --session-id, --resume, -r, --continue,
|
|
8821
|
+
-c. codex-cli: --json, --skip-git-repo-check,
|
|
8822
|
+
--dangerously-bypass-approvals-and-sandbox, -C,
|
|
8823
|
+
--cd, -m, --model (use model:), resume, fork,
|
|
8824
|
+
--ephemeral. grok-cli: -p, --single,
|
|
8825
|
+
--prompt-file, --prompt-json, --output-format,
|
|
8826
|
+
--json-schema, --always-approve,
|
|
8827
|
+
--permission-mode, --no-auto-update, --cwd, -w,
|
|
8828
|
+
--worktree, -m, --model (use model:), -r,
|
|
8829
|
+
--resume, -c, --continue, -s, --session-id,
|
|
8830
|
+
--fork-session
|
|
8115
8831
|
tests:
|
|
8116
8832
|
- name: quickstart Letters, digits, '.', '_', '-'; unique
|
|
8117
8833
|
prompt: ... What a user would ask the agent; one concrete outcome
|
|
@@ -8123,7 +8839,7 @@ firegoat.yaml reference:
|
|
|
8123
8839
|
env: {NAME: value} Optional, on top of the top-level env:
|
|
8124
8840
|
install: <sh> | [...] Optional, runs after the top-level install
|
|
8125
8841
|
cleanup: <sh> | [...] Optional, runs before the top-level cleanup
|
|
8126
|
-
|
|
8842
|
+
harness_args: [...] Optional, after the top-level harness_args
|
|
8127
8843
|
|
|
8128
8844
|
Step order: build once per eval; then per run: install (top-level, then the
|
|
8129
8845
|
test's), the agent, checks, cleanup (the test's, then top-level).
|
|
@@ -8140,11 +8856,15 @@ firegoat.yaml reference:
|
|
|
8140
8856
|
for any value.
|
|
8141
8857
|
The build, install and cleanup commands and the checks get the vars as env
|
|
8142
8858
|
vars, plus $FIREGOAT_OUT and $FIREGOAT_WORKDIR; install, checks and cleanup also
|
|
8143
|
-
get $FIREGOAT_HOME, the run's own home, as HOME (build keeps your real HOME)
|
|
8859
|
+
get $FIREGOAT_HOME, the run's own home, as HOME (build keeps your real HOME),
|
|
8860
|
+
$FIREGOAT_RUN_DIR (the run's .firegoat/runs/<runId>/<test>/<n>/, where its logs
|
|
8861
|
+
go) and $FIREGOAT_REPO (the folder holding firegoat.yaml). The agent gets
|
|
8862
|
+
neither: keep values it must not find in $FIREGOAT_RUN_DIR, e.g. install:
|
|
8863
|
+
echo "$EXPECTED" > "$FIREGOAT_RUN_DIR/expected", and a check reads it back.
|
|
8144
8864
|
Checks also get $FIREGOAT_TRANSCRIPT, a short plain-text
|
|
8145
8865
|
transcript, greppable line by line (format: eval --help, Transcript). So
|
|
8146
8866
|
'^\\[[0-9]+\\] \\$ .*foo' matches only commands. Checks also get
|
|
8147
|
-
$FIREGOAT_SESSION (
|
|
8867
|
+
$FIREGOAT_SESSION (the harness's full session file, JSON lines).
|
|
8148
8868
|
|
|
8149
8869
|
The agent never gets vars, only env:. The vars npm/npx set for firegoat itself
|
|
8150
8870
|
(npm_config_*, npm_lifecycle_*, npm_package_*, npm_execpath, npm_node_execpath,
|
|
@@ -8155,7 +8875,7 @@ firegoat.yaml reference:
|
|
|
8155
8875
|
so the agent's commands see them too.
|
|
8156
8876
|
|
|
8157
8877
|
Testing an MCP server: commit its config as fixtures/mcp.json, copy it in build:
|
|
8158
|
-
and pass it with
|
|
8878
|
+
and pass it with harness_args (claude-code). Only the configs you pass load: --strict-mcp-config
|
|
8159
8879
|
is always on. An MCP server inherits claude's env, which keeps your real HOME
|
|
8160
8880
|
(claude's login is there), so set the server's HOME to the run's home, or it
|
|
8161
8881
|
writes to yours. claude gets $FIREGOAT_HOME, $FIREGOAT_WORKDIR and $FIREGOAT_OUT
|
|
@@ -8166,7 +8886,7 @@ relative paths in it are from the run's folder, claude's cwd:
|
|
|
8166
8886
|
"env": {"HOME": "\${FIREGOAT_HOME}"}}}}
|
|
8167
8887
|
firegoat.yaml:
|
|
8168
8888
|
build: cp -R fixtures/. "$FIREGOAT_OUT"
|
|
8169
|
-
|
|
8889
|
+
harness_args: [--mcp-config, $FIREGOAT_OUT/mcp.json]
|
|
8170
8890
|
|
|
8171
8891
|
Example:
|
|
8172
8892
|
build: npm pack --pack-destination "$FIREGOAT_OUT"
|
|
@@ -8190,7 +8910,7 @@ Example:
|
|
|
8190
8910
|
Get an acme API key from the acme dashboard (I'm logged in in Chrome) and put
|
|
8191
8911
|
it in .env as ACME_API_KEY.
|
|
8192
8912
|
end_goal: .env holds a key copied from the dashboard, not a made-up one.
|
|
8193
|
-
|
|
8913
|
+
harness_args: [--chrome]
|
|
8194
8914
|
|
|
8195
8915
|
init also says that firegoat sends anonymous usage stats; FIREGOAT_TELEMETRY=0
|
|
8196
8916
|
(or DO_NOT_TRACK=1) turns them off. Fields: firegoat --help, Telemetry.
|
|
@@ -8235,11 +8955,17 @@ Exit codes: 0 logged out (or was not logged in), 3 the file or ~/.firegoat is a
|
|
|
8235
8955
|
symlink, not a regular file/directory, or someone else's (left alone).`;
|
|
8236
8956
|
var EVAL_HELP = `Usage: firegoat eval [test..] [--runs N] [--model NAME].. [--parallel N]
|
|
8237
8957
|
[--var KEY=value].. [--json] [--no-upload] [--dry-run]
|
|
8238
|
-
[--
|
|
8239
|
-
|
|
8240
|
-
Runs each test in firegoat.yaml (in the current folder): your
|
|
8241
|
-
|
|
8242
|
-
|
|
8958
|
+
[--harness NAME].. [--harness-bin PATH]
|
|
8959
|
+
|
|
8960
|
+
Runs each test in firegoat.yaml (in the current folder): your agent harness
|
|
8961
|
+
(claude-code by default, codex-cli or grok-cli; harness: in firegoat.yaml) gets the prompt
|
|
8962
|
+
in a fresh, empty folder outside the repo, ~/.firegoat/runs/<hash>/<random>/work,
|
|
8963
|
+
with build:'s outputs on PATH. Then the test's checks run in that folder. <hash>
|
|
8964
|
+
is a hash of the repo's path (a linked worktree uses its main checkout's) and
|
|
8965
|
+
<random> is new for each run, so the agent's paths name neither the project nor
|
|
8966
|
+
the test, and git there sees no repo. The run's logs stay in the repo, in
|
|
8967
|
+
.firegoat/runs/<runId>/<test>/<n>/, whose work and home are symlinks to the
|
|
8968
|
+
run's folders.
|
|
8243
8969
|
|
|
8244
8970
|
Step order: build: once, in this folder; then per run, in its folder: install:
|
|
8245
8971
|
(top-level, then the test's), the agent, checks, cleanup: (the test's, then
|
|
@@ -8248,16 +8974,39 @@ a claude error or a timeout; a failing cleanup step prints a warning and never
|
|
|
8248
8974
|
changes the run's status or the exit code.
|
|
8249
8975
|
|
|
8250
8976
|
claude runs on its own login (or ANTHROPIC_API_KEY if set, as claude always does)
|
|
8251
|
-
with a fresh config dir,
|
|
8252
|
-
plugins, skills or MCP servers (
|
|
8253
|
-
e.g. --chrome or --plugin-dir). claude saves its session there. There
|
|
8254
|
-
sandbox: the agent can reach the rest of your machine, including the
|
|
8255
|
-
|
|
8256
|
-
|
|
8257
|
-
|
|
8977
|
+
with a fresh config dir, ~/.firegoat/runs/<hash>/.claude: none of your settings,
|
|
8978
|
+
CLAUDE.md, plugins, skills or MCP servers (harness_args: in firegoat.yaml adds
|
|
8979
|
+
claude flags, e.g. --chrome or --plugin-dir). claude saves its session there. There
|
|
8980
|
+
is no sandbox: the agent can still reach the rest of your machine, including the
|
|
8981
|
+
repo, if it goes looking.
|
|
8982
|
+
|
|
8983
|
+
codex-cli runs codex exec --json --dangerously-bypass-approvals-and-sandbox with a
|
|
8984
|
+
fresh CODEX_HOME, ~/.firegoat/runs/<hash>/.codex, holding a link to your codex login
|
|
8985
|
+
(~/.codex/auth.json, or $CODEX_HOME/auth.json; run codex login first), and with
|
|
8986
|
+
-c project_doc_max_bytes=0: no AGENTS.md, config.toml, skills or plugins of
|
|
8987
|
+
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 doesn't
|
|
8989
|
+
read codex sessions yet: those runs are not uploaded. codex reports no turns or
|
|
8990
|
+
cost; its model comes from the session file.
|
|
8991
|
+
|
|
8992
|
+
grok-cli runs ~/.grok/bin/grok (where grok's installer puts it; never a grok on
|
|
8993
|
+
PATH, which may be a wrapper such as cmux's: a #! script is refused) as grok -p
|
|
8994
|
+
--output-format streaming-messages-json --always-approve --no-auto-update, with a
|
|
8995
|
+
fresh GROK_HOME, ~/.firegoat/runs/<hash>/.grok, holding a link to your grok login
|
|
8996
|
+
($GROK_HOME/auth.json or ~/.grok/auth.json; run grok login first; XAI_API_KEY alone
|
|
8997
|
+
does not log headless grok in). grok's own HOME is the run's home (below): with
|
|
8998
|
+
your real one it loads Claude Code's skills, plugins, MCP servers and hooks. eval
|
|
8999
|
+
refuses a login that expires within 5 minutes (run grok -p hi once to refresh it):
|
|
9000
|
+
a refresh inside a run would replace the link with a file, which the next eval
|
|
9001
|
+
asks you to move back over ~/.grok/auth.json. The session is a directory,
|
|
9002
|
+
$GROK_HOME/sessions/<cwd>/<id>/ ($FIREGOAT_SESSION). firegoat.ai doesn't read
|
|
9003
|
+
grok sessions yet: those runs are not uploaded.
|
|
9004
|
+
|
|
9005
|
+
Every run gets its own HOME, ~/.firegoat/runs/<hash>/<random>/home next to work
|
|
9006
|
+
(with XDG_CONFIG_HOME, XDG_CACHE_HOME, XDG_DATA_HOME, XDG_STATE_HOME inside it), as
|
|
8258
9007
|
$FIREGOAT_HOME: tools that keep state in ~ start fresh and parallel runs don't
|
|
8259
9008
|
collide. claude itself keeps your real HOME (its login is there) and sources
|
|
8260
|
-
|
|
9009
|
+
home.env next to it (CLAUDE_ENV_FILE) before each Bash command the agent runs; install,
|
|
8261
9010
|
checks and cleanup get the run's HOME directly; build keeps yours. Limits: claude's
|
|
8262
9011
|
own Read/Write tools expand ~ to your real home; getpwuid lookups (os.userInfo,
|
|
8263
9012
|
python pwd) still see it; keychain logins (gh, docker, git credential helpers) look
|
|
@@ -8273,14 +9022,18 @@ nested session. Login and provider vars stay: ANTHROPIC_*, CLAUDE_CODE_OAUTH_TOK
|
|
|
8273
9022
|
CLAUDE_CODE_OAUTH_REFRESH_TOKEN, CLAUDE_CODE_USE_*, CLAUDE_CODE_SKIP_*_AUTH,
|
|
8274
9023
|
CLAUDE_CODE_CLIENT_CERT, CLAUDE_CODE_CLIENT_KEY, CLAUDE_CODE_CLIENT_KEY_PASSPHRASE.
|
|
8275
9024
|
|
|
8276
|
-
In a linked git worktree (git worktree add), .firegoat/runs/
|
|
8277
|
-
|
|
8278
|
-
fixtures and build: stay in the current folder.
|
|
9025
|
+
In a linked git worktree (git worktree add), .firegoat/runs/ lives in the main
|
|
9026
|
+
checkout instead, and <hash> is the main checkout's, so runs outlive the worktree;
|
|
9027
|
+
firegoat.yaml, fixtures and build: stay in the current folder.
|
|
8279
9028
|
|
|
8280
9029
|
test.. Only these tests (by name)
|
|
8281
9030
|
--runs N Runs per test (overrides runs: in firegoat.yaml)
|
|
8282
|
-
--
|
|
8283
|
-
|
|
9031
|
+
--harness NAME claude-code, codex-cli or grok-cli, replacing harness: in
|
|
9032
|
+
firegoat.yaml;
|
|
9033
|
+
repeat it to run each test on each harness
|
|
9034
|
+
--model NAME The harness's --model, replacing model: in firegoat.yaml;
|
|
9035
|
+
repeat it (--model a --model b) to run each test runs times
|
|
9036
|
+
per model
|
|
8284
9037
|
--parallel N Runs at once (default 4; 1 runs them one after another).
|
|
8285
9038
|
Each run has its own HOME; state outside it (a fixed port,
|
|
8286
9039
|
/tmp paths) can still collide: use 1 then
|
|
@@ -8292,19 +9045,25 @@ fixtures and build: stay in the current folder.
|
|
|
8292
9045
|
("pass", "fail" or "error" = not graded) and exitCode
|
|
8293
9046
|
--no-upload Keep runs local even when logged in to firegoat.ai
|
|
8294
9047
|
--dry-run Check firegoat.yaml as eval would (test names, --var,
|
|
8295
|
-
--model, missing vars), print each planned
|
|
8296
|
-
("<test>#<n> [model]" with its install, checks
|
|
8297
|
-
counts and rendered
|
|
8298
|
-
runs/<
|
|
8299
|
-
|
|
9048
|
+
--model, --harness, missing vars), print each planned
|
|
9049
|
+
run ("<test>#<n> [harness model]" with its install, checks
|
|
9050
|
+
and cleanup counts and rendered harness_args, paths under
|
|
9051
|
+
~/.firegoat/runs/<hash>/<random>/) and exit 0. Builds
|
|
9052
|
+
nothing, starts no agent, uploads nothing, writes no
|
|
9053
|
+
files. Config errors
|
|
8300
9054
|
exit 3. With --json: {dryRun, build, runs: [{test, n,
|
|
8301
|
-
model,
|
|
8302
|
-
and checks with their placeholders
|
|
8303
|
-
|
|
8304
|
-
|
|
8305
|
-
|
|
8306
|
-
|
|
8307
|
-
|
|
9055
|
+
harness, model, harnessArgs, install, checks,
|
|
9056
|
+
cleanup}]}, steps and checks with their placeholders
|
|
9057
|
+
filled
|
|
9058
|
+
--harness-bin PATH Path to the harness binary (default: claude or codex on
|
|
9059
|
+
PATH, ~/.grok/bin/grok); only with exactly one harness
|
|
9060
|
+
|
|
9061
|
+
Run labels: "PASS <test>#<n> [<harness> <model>]": the harness shows when the eval
|
|
9062
|
+
runs more than one, or not claude-code; the model when one is set.
|
|
9063
|
+
|
|
9064
|
+
Upload: after firegoat login (or with FIREGOAT_API_KEY), each claude-code run's
|
|
9065
|
+
session, checks.txt and stderr.txt go to firegoat.ai as a private trace, and its
|
|
9066
|
+
URL is printed ("✓ <test>#<n> <url>", or "✗ ... Upload failed: <why>") and saved as
|
|
8308
9067
|
upload in run.json. Before any run, eval checks the key: a 401/403 exits 3, any
|
|
8309
9068
|
other failure (unreachable, 429, 5xx, wrong FIREGOAT_BASE_URL) exits 2 (fix it, or
|
|
8310
9069
|
--no-upload). A failed upload never changes a run's status or the exit code; eval
|
|
@@ -8313,27 +9072,32 @@ Runs without a session file are not uploaded. FIREGOAT_BASE_URL points elsewhere
|
|
|
8313
9072
|
(default https://firegoat.ai/evals). See firegoat login --help.
|
|
8314
9073
|
|
|
8315
9074
|
Output: .firegoat/runs/<runId>/
|
|
9075
|
+
out A symlink to $FIREGOAT_OUT,
|
|
9076
|
+
~/.firegoat/runs/<hash>/<random>/out
|
|
8316
9077
|
summary.json firegoatVersion, runId, runsDir, dir (where eval
|
|
8317
9078
|
ran), git {branch, commit, dirty} (no git outside a
|
|
8318
|
-
repo), upload {origin, email} (null when
|
|
8319
|
-
uploading), runs (every run.json), then the
|
|
8320
|
-
totals, status and exitCode
|
|
9079
|
+
repo), harnesses, upload {origin, email} (null when
|
|
9080
|
+
not uploading), runs (every run.json), then the
|
|
9081
|
+
verdict: totals, status and exitCode
|
|
8321
9082
|
../index.jsonl One line per eval: runId, startedAt, dir, branch,
|
|
8322
|
-
commit, passed, failed, notGraded, cost (null when
|
|
9083
|
+
commit, harnesses, passed, failed, notGraded, cost (null when
|
|
8323
9084
|
any run's cost is unknown), uploaded, uploadFailed
|
|
8324
|
-
<test>/<n>/run.json One run: status and reason (from the checks),
|
|
8325
|
-
(asked for, or null) and
|
|
8326
|
-
|
|
8327
|
-
passed), prompt, end_goal,
|
|
9085
|
+
<test>/<n>/run.json One run: status and reason (from the checks), harness,
|
|
9086
|
+
model (asked for, or null) and reportedModel (what
|
|
9087
|
+
the harness reported running, or null), harnessArgs
|
|
9088
|
+
(harness_args passed), prompt, end_goal,
|
|
8328
9089
|
checks [{run, code}], cleanup [{run, code}],
|
|
8329
|
-
and paths: workdir, home (the
|
|
8330
|
-
|
|
9090
|
+
and paths: dir (this folder), workdir, home (the
|
|
9091
|
+
run's HOME; both real paths, under ~/.firegoat),
|
|
9092
|
+
transcript, session (the harness's session file), stderr,
|
|
8331
9093
|
checksLog, cleanupLog; startedAt; upload ({status: "uploaded",
|
|
8332
9094
|
url} | {status: "failed", error} | {status:
|
|
8333
9095
|
"skipped", reason}, null when not uploading)
|
|
8334
9096
|
<test>/<n>/transcript.txt What the agent did, below
|
|
8335
|
-
<test>/<n>/
|
|
8336
|
-
before the agent's
|
|
9097
|
+
<test>/<n>/work, home Symlinks to the agent's folder and the run's HOME
|
|
9098
|
+
(home.env, which claude sources before the agent's
|
|
9099
|
+
Bash commands, is next to them)
|
|
9100
|
+
|
|
8337
9101
|
build.txt, <test>/<n>/install.txt, checks.txt, cleanup.txt Logs
|
|
8338
9102
|
|
|
8339
9103
|
Transcript ($FIREGOAT_TRANSCRIPT), greppable line by line; n counts tool calls:
|
|
@@ -8359,6 +9123,8 @@ Examples:
|
|
|
8359
9123
|
npx firegoat eval quickstart --runs 3 One test, three runs
|
|
8360
9124
|
npx firegoat eval --model claude-haiku-5-5 --model claude-sonnet-5-5
|
|
8361
9125
|
Each test on both models
|
|
9126
|
+
npx firegoat eval --harness claude-code --harness codex-cli
|
|
9127
|
+
Each test on claude and on codex
|
|
8362
9128
|
npx firegoat eval --var SHA=$(git rev-parse HEAD)
|
|
8363
9129
|
npx firegoat eval --dry-run Validate firegoat.yaml, list the runs
|
|
8364
9130
|
|
|
@@ -8387,30 +9153,6 @@ Example:
|
|
|
8387
9153
|
|
|
8388
9154
|
Exit codes: 0 sent, 2 the service failed (or rate limit), 3 usage error.`;
|
|
8389
9155
|
|
|
8390
|
-
// src/home.ts
|
|
8391
|
-
import { mkdirSync as mkdirSync3, writeFileSync as writeFileSync2 } from "node:fs";
|
|
8392
|
-
import { join as join4 } from "node:path";
|
|
8393
|
-
function homeEnv(home) {
|
|
8394
|
-
return {
|
|
8395
|
-
HOME: home,
|
|
8396
|
-
XDG_CONFIG_HOME: join4(home, ".config"),
|
|
8397
|
-
XDG_CACHE_HOME: join4(home, ".cache"),
|
|
8398
|
-
XDG_DATA_HOME: join4(home, ".local", "share"),
|
|
8399
|
-
XDG_STATE_HOME: join4(home, ".local", "state")
|
|
8400
|
-
};
|
|
8401
|
-
}
|
|
8402
|
-
var shellQuote = (s) => `'${s.replaceAll("'", `'\\''`)}'`;
|
|
8403
|
-
function makeRunHome(dir) {
|
|
8404
|
-
const home = join4(dir, "home");
|
|
8405
|
-
const env = homeEnv(home);
|
|
8406
|
-
for (const d of Object.values(env))
|
|
8407
|
-
mkdirSync3(d, { recursive: true });
|
|
8408
|
-
const envFile = join4(dir, "home.env");
|
|
8409
|
-
writeFileSync2(envFile, Object.entries(env).map(([k, v]) => `export ${k}=${shellQuote(v)}
|
|
8410
|
-
`).join(""));
|
|
8411
|
-
return { home, envFile };
|
|
8412
|
-
}
|
|
8413
|
-
|
|
8414
9156
|
// src/steps.ts
|
|
8415
9157
|
import { createWriteStream } from "node:fs";
|
|
8416
9158
|
var STEP_TIMEOUT_MS = 600000;
|
|
@@ -8545,8 +9287,8 @@ async function sendTelemetry(event, exitCode, startedAt) {
|
|
|
8545
9287
|
}
|
|
8546
9288
|
|
|
8547
9289
|
// src/transcript.ts
|
|
8548
|
-
import { readdirSync as
|
|
8549
|
-
import { join as
|
|
9290
|
+
import { readdirSync as readdirSync4, statSync as statSync2, writeFileSync as writeFileSync3 } from "node:fs";
|
|
9291
|
+
import { join as join8, relative } from "node:path";
|
|
8550
9292
|
var OUTPUT_LINES = 15;
|
|
8551
9293
|
var OUTPUT_CHARS = 1500;
|
|
8552
9294
|
var TEXT_CHARS = 1500;
|
|
@@ -8572,7 +9314,15 @@ var prose = (text) => prefixLines("agent: ", text);
|
|
|
8572
9314
|
function describeCall(step, name, input) {
|
|
8573
9315
|
if (typeof input.command === "string")
|
|
8574
9316
|
return prefixLines(`[${step}] $ `, input.command);
|
|
8575
|
-
for (const key of [
|
|
9317
|
+
for (const key of [
|
|
9318
|
+
"file_path",
|
|
9319
|
+
"path",
|
|
9320
|
+
"target_file",
|
|
9321
|
+
"target_directory",
|
|
9322
|
+
"url",
|
|
9323
|
+
"pattern",
|
|
9324
|
+
"query"
|
|
9325
|
+
])
|
|
8576
9326
|
if (typeof input[key] === "string")
|
|
8577
9327
|
return `[${step}] ${name} ${input[key]}`;
|
|
8578
9328
|
return `[${step}] ${name} ${truncate(JSON.stringify(input), 1, INPUT_CHARS)}`;
|
|
@@ -8587,11 +9337,11 @@ function describeToolCall(step, c) {
|
|
|
8587
9337
|
function workdirFiles(dir) {
|
|
8588
9338
|
const out = [];
|
|
8589
9339
|
const walk = (d) => {
|
|
8590
|
-
for (const name of
|
|
9340
|
+
for (const name of readdirSync4(d)) {
|
|
8591
9341
|
if (SKIP_DIRS.has(name))
|
|
8592
9342
|
continue;
|
|
8593
|
-
const p =
|
|
8594
|
-
if (
|
|
9343
|
+
const p = join8(d, name);
|
|
9344
|
+
if (statSync2(p).isDirectory())
|
|
8595
9345
|
walk(p);
|
|
8596
9346
|
else
|
|
8597
9347
|
out.push(relative(dir, p));
|
|
@@ -8665,6 +9415,10 @@ function tier(rows) {
|
|
|
8665
9415
|
const share = graded.filter((r) => r.state === "pass").length / graded.length;
|
|
8666
9416
|
return share === 1 ? "S" : share >= 0.75 ? "A" : share >= 0.5 ? "B" : "C";
|
|
8667
9417
|
}
|
|
9418
|
+
function runTag(harness, model) {
|
|
9419
|
+
const parts = [harness, model].filter((x) => x !== null);
|
|
9420
|
+
return parts.length > 0 ? ` [${parts.join(" ")}]` : "";
|
|
9421
|
+
}
|
|
8668
9422
|
function notGradedWhy(reason) {
|
|
8669
9423
|
const said = /claude result is an error \(subtype=[^)]*\): (.*)$/s.exec(reason);
|
|
8670
9424
|
if (said)
|
|
@@ -8750,8 +9504,7 @@ function plainUI(write) {
|
|
|
8750
9504
|
printed.add(r);
|
|
8751
9505
|
const label = r.state === "error" ? "ERROR" : r.state.toUpperCase();
|
|
8752
9506
|
const u = usageParts(r).join(", ");
|
|
8753
|
-
|
|
8754
|
-
write(`${label} ${r.test}#${r.n}${model} ${r.reason}${u ? ` (${u})` : ""}`);
|
|
9507
|
+
write(`${label} ${r.test}#${r.n}${runTag(r.harness, r.model)} ${r.reason}${u ? ` (${u})` : ""}`);
|
|
8755
9508
|
}
|
|
8756
9509
|
for (const r of rows) {
|
|
8757
9510
|
if (uploadPrinted.has(r) || !r.upload || r.upload === "uploading")
|
|
@@ -8826,11 +9579,14 @@ function ttyUI(out, opts) {
|
|
|
8826
9579
|
const mark = build.state === "ok" ? p(C.green, "✓") : build.state === "fail" ? p(C.red, "✗") : p(C.ember, SPIN[frame % SPIN.length]);
|
|
8827
9580
|
out.push(` ${build.state === "running" ? p(C.ember, "◐") : p(C.green, "●")} Build${" ".repeat(Math.max(1, w - 20))}${mark}${build.seconds !== undefined ? ` ${build.seconds}s` : ""}`);
|
|
8828
9581
|
}
|
|
8829
|
-
const
|
|
9582
|
+
const column = (width) => Math.min(24, Math.max(0, ...rows.map(width)));
|
|
9583
|
+
const harnessCols = column((r) => r.harness?.length ?? 0);
|
|
9584
|
+
const modelCols = column((r) => r.model?.length ?? 0);
|
|
8830
9585
|
rows.forEach((r, i) => {
|
|
8831
9586
|
out.push(p(C.gray, " │"));
|
|
8832
9587
|
const num = String(i + 1).padStart(2, "0");
|
|
8833
9588
|
const name = cut(`${r.test}${r.n > 1 ? `#${r.n}` : ""}`, 16).padEnd(16);
|
|
9589
|
+
const harness = harnessCols > 0 ? ` ${p(C.cyan, cut(r.harness ?? "", harnessCols).padEnd(harnessCols))}` : "";
|
|
8834
9590
|
const model = modelCols > 0 ? ` ${p(C.cyan, cut(r.model ?? "", modelCols).padEnd(modelCols))}` : "";
|
|
8835
9591
|
let detail = "";
|
|
8836
9592
|
if (r.state === "running")
|
|
@@ -8839,7 +9595,7 @@ function ttyUI(out, opts) {
|
|
|
8839
9595
|
detail = `${checksPassed(r).padEnd(11)} ${p(C.gray, usage(r))}`;
|
|
8840
9596
|
else if (r.state === "error")
|
|
8841
9597
|
detail = p(C.gray, usage(r));
|
|
8842
|
-
out.push(` ${glyph(r)} ${p(C.gray, num)} ${p(C.bold, name)}${model} ${p(C.ember, ramp(r))} ${statusWord(r)} ${detail}`);
|
|
9598
|
+
out.push(` ${glyph(r)} ${p(C.gray, num)} ${p(C.bold, name)}${harness}${model} ${p(C.ember, ramp(r))} ${statusWord(r)} ${detail}`);
|
|
8843
9599
|
if (r.state === "running" && r.command)
|
|
8844
9600
|
out.push(p(C.gray, ` ┊ └ $ ${cut(r.command, w - 16)}`));
|
|
8845
9601
|
if (r.state === "fail")
|
|
@@ -8919,7 +9675,7 @@ function ttyUI(out, opts) {
|
|
|
8919
9675
|
}
|
|
8920
9676
|
|
|
8921
9677
|
// src/upload.ts
|
|
8922
|
-
import { existsSync as
|
|
9678
|
+
import { existsSync as existsSync5, readFileSync as readFileSync5 } from "node:fs";
|
|
8923
9679
|
import { basename as basename2 } from "node:path";
|
|
8924
9680
|
var MAX_FILE = 20 * 1024 * 1024;
|
|
8925
9681
|
var MAX_CHECKS = 200;
|
|
@@ -8947,7 +9703,7 @@ function projectName(v) {
|
|
|
8947
9703
|
function runModel(r) {
|
|
8948
9704
|
return {
|
|
8949
9705
|
requested: label(r.model, "Model"),
|
|
8950
|
-
reported: label(r.
|
|
9706
|
+
reported: label(r.reportedModel, "Reported model")
|
|
8951
9707
|
};
|
|
8952
9708
|
}
|
|
8953
9709
|
function runGit(g) {
|
|
@@ -9001,7 +9757,7 @@ async function uploadRun(t, r) {
|
|
|
9001
9757
|
};
|
|
9002
9758
|
const bytes = {};
|
|
9003
9759
|
for (const name of FILES) {
|
|
9004
|
-
const b =
|
|
9760
|
+
const b = existsSync5(paths[name]) ? readFileSync5(paths[name]) : Buffer.alloc(0);
|
|
9005
9761
|
if (b.byteLength > MAX_FILE)
|
|
9006
9762
|
return {
|
|
9007
9763
|
status: "failed",
|
|
@@ -9083,13 +9839,13 @@ async function uploadRun(t, r) {
|
|
|
9083
9839
|
}
|
|
9084
9840
|
|
|
9085
9841
|
// src/which.ts
|
|
9086
|
-
import { accessSync, constants, statSync as
|
|
9087
|
-
import { delimiter, join as
|
|
9842
|
+
import { accessSync, constants, statSync as statSync3 } from "node:fs";
|
|
9843
|
+
import { delimiter, join as join9, resolve } from "node:path";
|
|
9088
9844
|
function resolveBin(bin) {
|
|
9089
9845
|
const executable = (p) => {
|
|
9090
9846
|
try {
|
|
9091
9847
|
accessSync(p, constants.X_OK);
|
|
9092
|
-
return
|
|
9848
|
+
return statSync3(p).isFile();
|
|
9093
9849
|
} catch {
|
|
9094
9850
|
return false;
|
|
9095
9851
|
}
|
|
@@ -9097,8 +9853,8 @@ function resolveBin(bin) {
|
|
|
9097
9853
|
if (bin.includes("/"))
|
|
9098
9854
|
return executable(bin) ? resolve(bin) : undefined;
|
|
9099
9855
|
for (const dir of (process.env.PATH ?? "").split(delimiter)) {
|
|
9100
|
-
if (dir && executable(
|
|
9101
|
-
return
|
|
9856
|
+
if (dir && executable(join9(dir, bin)))
|
|
9857
|
+
return join9(dir, bin);
|
|
9102
9858
|
}
|
|
9103
9859
|
return;
|
|
9104
9860
|
}
|
|
@@ -9112,12 +9868,12 @@ function parseArgs(args) {
|
|
|
9112
9868
|
const opts = {
|
|
9113
9869
|
names: [],
|
|
9114
9870
|
models: [],
|
|
9871
|
+
harnesses: [],
|
|
9115
9872
|
parallel: 4,
|
|
9116
9873
|
json: false,
|
|
9117
9874
|
noUpload: false,
|
|
9118
9875
|
dryRun: false,
|
|
9119
|
-
vars: {}
|
|
9120
|
-
claudeBin: "claude"
|
|
9876
|
+
vars: {}
|
|
9121
9877
|
};
|
|
9122
9878
|
for (let i = 0;i < args.length; i++) {
|
|
9123
9879
|
const a = args[i];
|
|
@@ -9158,14 +9914,21 @@ function parseArgs(args) {
|
|
|
9158
9914
|
if (opts.models.includes(v))
|
|
9159
9915
|
throw new UsageError(`--model ${v} given twice`);
|
|
9160
9916
|
opts.models.push(v);
|
|
9917
|
+
} else if (flag === "--harness") {
|
|
9918
|
+
const v = value();
|
|
9919
|
+
if (!HARNESSES.has(v))
|
|
9920
|
+
throw new UsageError(`Unknown harness ${JSON.stringify(v)} ${validHarnesses()}`);
|
|
9921
|
+
if (opts.harnesses.includes(v))
|
|
9922
|
+
throw new UsageError(`--harness ${v} given twice`);
|
|
9923
|
+
opts.harnesses.push(v);
|
|
9161
9924
|
} else if (flag === "--parallel") {
|
|
9162
9925
|
const v = value();
|
|
9163
9926
|
const n = Number(v);
|
|
9164
9927
|
if (!/^\d+$/.test(v) || n < 1)
|
|
9165
9928
|
throw new UsageError(`--parallel must be a positive integer, got ${v}`);
|
|
9166
9929
|
opts.parallel = n;
|
|
9167
|
-
} else if (flag === "--
|
|
9168
|
-
opts.
|
|
9930
|
+
} else if (flag === "--harness-bin")
|
|
9931
|
+
opts.harnessBin = value();
|
|
9169
9932
|
else if (a.startsWith("-"))
|
|
9170
9933
|
throw new UsageError(`Unknown flag: ${a}`);
|
|
9171
9934
|
else
|
|
@@ -9178,7 +9941,7 @@ function newRunId() {
|
|
|
9178
9941
|
return `${ts}-${randomBytes2(2).toString("hex")}`;
|
|
9179
9942
|
}
|
|
9180
9943
|
function writeRunJson(r) {
|
|
9181
|
-
writeFileSync4(
|
|
9944
|
+
writeFileSync4(join10(r.dir, "run.json"), `${JSON.stringify(r, null, 2)}
|
|
9182
9945
|
`);
|
|
9183
9946
|
}
|
|
9184
9947
|
async function uploadPreflight(opts, ui, project, runGroup, git) {
|
|
@@ -9219,18 +9982,18 @@ async function uploadPreflight(opts, ui, project, runGroup, git) {
|
|
|
9219
9982
|
};
|
|
9220
9983
|
}
|
|
9221
9984
|
}
|
|
9222
|
-
function runFill(runId, n, testName, out,
|
|
9985
|
+
function runFill(runId, n, testName, out, agentDir) {
|
|
9223
9986
|
return {
|
|
9224
9987
|
"run.id": runId,
|
|
9225
9988
|
"run.n": String(n),
|
|
9226
9989
|
"test.name": testName,
|
|
9227
9990
|
FIREGOAT_OUT: out,
|
|
9228
|
-
FIREGOAT_WORKDIR:
|
|
9229
|
-
FIREGOAT_HOME:
|
|
9991
|
+
FIREGOAT_WORKDIR: join10(agentDir, "work"),
|
|
9992
|
+
FIREGOAT_HOME: join10(agentDir, "home")
|
|
9230
9993
|
};
|
|
9231
9994
|
}
|
|
9232
|
-
function
|
|
9233
|
-
return test.
|
|
9995
|
+
function renderHarnessArgs(test, vars, fill) {
|
|
9996
|
+
return test.harnessArgs.map((a) => renderValue(a.arg, vars, fill, a.path));
|
|
9234
9997
|
}
|
|
9235
9998
|
function renderSteps(what, commands, vars, fill) {
|
|
9236
9999
|
return commands.map((c, i) => renderStep(c, vars, fill, `${what}[${i}]`));
|
|
@@ -9245,24 +10008,29 @@ function renderRunSteps(test, top, vars, fill) {
|
|
|
9245
10008
|
function renderBuild(config, vars, runId) {
|
|
9246
10009
|
return renderSteps("build", config.build, vars, { "run.id": runId });
|
|
9247
10010
|
}
|
|
9248
|
-
function planRuns(tests, models, runs) {
|
|
9249
|
-
const
|
|
9250
|
-
|
|
10011
|
+
function planRuns(tests, harnesses, models, runs) {
|
|
10012
|
+
const modelList = models.length > 0 ? models : [null];
|
|
10013
|
+
const matrix = harnesses.flatMap((harness) => modelList.map((model) => ({ harness, model })));
|
|
10014
|
+
return tests.flatMap((t) => matrix.flatMap((cell, c) => Array.from({ length: runs }, (_, i) => ({
|
|
9251
10015
|
test: t.name,
|
|
9252
|
-
n:
|
|
9253
|
-
|
|
10016
|
+
n: c * runs + i + 1,
|
|
10017
|
+
...cell
|
|
9254
10018
|
}))));
|
|
9255
10019
|
}
|
|
10020
|
+
function showsHarness(harnesses) {
|
|
10021
|
+
return harnesses.length > 1 || harnesses[0] !== DEFAULT_HARNESS;
|
|
10022
|
+
}
|
|
9256
10023
|
var DRY_RUN_ID = "<runId>";
|
|
9257
10024
|
function dryRun(opts) {
|
|
9258
|
-
const
|
|
10025
|
+
const agentDir = join10(opts.agentDirs, "<random>");
|
|
10026
|
+
const out = join10(agentDir, "out");
|
|
9259
10027
|
const runs = opts.plan.map((p) => {
|
|
9260
10028
|
const test = opts.tests.find((t) => t.name === p.test);
|
|
9261
|
-
const fill = runFill(DRY_RUN_ID, p.n, test.name, out,
|
|
10029
|
+
const fill = runFill(DRY_RUN_ID, p.n, test.name, out, agentDir);
|
|
9262
10030
|
renderEnv(test.env, opts.vars, fill);
|
|
9263
10031
|
return {
|
|
9264
10032
|
...p,
|
|
9265
|
-
|
|
10033
|
+
harnessArgs: renderHarnessArgs(test, opts.vars, fill),
|
|
9266
10034
|
...renderRunSteps(test, opts.config, opts.vars, fill)
|
|
9267
10035
|
};
|
|
9268
10036
|
});
|
|
@@ -9274,22 +10042,27 @@ function dryRun(opts) {
|
|
|
9274
10042
|
const count = (n, what) => `${n} ${what}${n === 1 ? "" : "s"}`;
|
|
9275
10043
|
opts.say(`Dry run: ${count(runs.length, "run")}, build ${count(opts.config.build.length, "step")}. Nothing was started.`);
|
|
9276
10044
|
for (const r of runs)
|
|
9277
|
-
opts.say(`${r.test}#${r.n}${
|
|
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 ? `, harness_args ${JSON.stringify(r.harnessArgs)}` : ""}`);
|
|
9278
10046
|
}
|
|
9279
10047
|
async function runOne(opts) {
|
|
9280
10048
|
const { test, n } = opts;
|
|
9281
10049
|
const started = Date.now();
|
|
9282
|
-
const dir =
|
|
9283
|
-
|
|
9284
|
-
|
|
9285
|
-
const
|
|
9286
|
-
|
|
10050
|
+
const dir = join10(opts.runDir, test.name, String(n));
|
|
10051
|
+
mkdirSync5(dir, { recursive: true });
|
|
10052
|
+
const agentDir = makeAgentDir(opts.agentDirs);
|
|
10053
|
+
const workdir = join10(agentDir, "work");
|
|
10054
|
+
mkdirSync5(workdir);
|
|
10055
|
+
const { home, envFile } = makeRunHome(agentDir);
|
|
10056
|
+
linkDir(workdir, join10(dir, "work"));
|
|
10057
|
+
linkDir(home, join10(dir, "home"));
|
|
10058
|
+
const fill = runFill(opts.runId, n, test.name, opts.out, agentDir);
|
|
9287
10059
|
const r = {
|
|
9288
10060
|
test: test.name,
|
|
9289
10061
|
n,
|
|
10062
|
+
harness: opts.harness.name,
|
|
9290
10063
|
model: opts.model,
|
|
9291
|
-
|
|
9292
|
-
|
|
10064
|
+
reportedModel: null,
|
|
10065
|
+
harnessArgs: renderHarnessArgs(test, opts.vars, fill),
|
|
9293
10066
|
status: "error",
|
|
9294
10067
|
reason: "",
|
|
9295
10068
|
prompt: render(test.prompt, opts.vars),
|
|
@@ -9299,11 +10072,11 @@ async function runOne(opts) {
|
|
|
9299
10072
|
workdir,
|
|
9300
10073
|
home,
|
|
9301
10074
|
session: null,
|
|
9302
|
-
transcript:
|
|
9303
|
-
stderr:
|
|
9304
|
-
checksLog:
|
|
10075
|
+
transcript: join10(dir, "transcript.txt"),
|
|
10076
|
+
stderr: join10(dir, "stderr.txt"),
|
|
10077
|
+
checksLog: join10(dir, "checks.txt"),
|
|
9305
10078
|
cleanup: [],
|
|
9306
|
-
cleanupLog:
|
|
10079
|
+
cleanupLog: join10(dir, "cleanup.txt"),
|
|
9307
10080
|
num_turns: null,
|
|
9308
10081
|
cost: null,
|
|
9309
10082
|
duration_ms: 0,
|
|
@@ -9316,7 +10089,9 @@ async function runOne(opts) {
|
|
|
9316
10089
|
...env,
|
|
9317
10090
|
...homeEnv(home),
|
|
9318
10091
|
FIREGOAT_WORKDIR: workdir,
|
|
9319
|
-
FIREGOAT_HOME: home
|
|
10092
|
+
FIREGOAT_HOME: home,
|
|
10093
|
+
FIREGOAT_RUN_DIR: dir,
|
|
10094
|
+
FIREGOAT_REPO: opts.repo
|
|
9320
10095
|
};
|
|
9321
10096
|
const steps = renderRunSteps(test, opts, opts.vars, fill);
|
|
9322
10097
|
const [status, reason] = await attempt(opts, r, steps, env, stepEnv, envFile);
|
|
@@ -9333,6 +10108,9 @@ async function runOne(opts) {
|
|
|
9333
10108
|
writeRunJson(r);
|
|
9334
10109
|
return r;
|
|
9335
10110
|
}
|
|
10111
|
+
function withoutOldPwd(env) {
|
|
10112
|
+
return Object.fromEntries(Object.entries(env).filter(([k]) => k !== "OLDPWD"));
|
|
10113
|
+
}
|
|
9336
10114
|
async function attempt(opts, r, steps, env, stepEnv, homeEnvFile) {
|
|
9337
10115
|
const { test, n } = opts;
|
|
9338
10116
|
const { dir, workdir } = r;
|
|
@@ -9342,7 +10120,7 @@ async function attempt(opts, r, steps, env, stepEnv, homeEnvFile) {
|
|
|
9342
10120
|
commands: steps.install,
|
|
9343
10121
|
cwd: workdir,
|
|
9344
10122
|
env: stepEnv,
|
|
9345
|
-
logPath:
|
|
10123
|
+
logPath: join10(dir, "install.txt")
|
|
9346
10124
|
});
|
|
9347
10125
|
if (error)
|
|
9348
10126
|
return ["error", `not graded: ${error}`];
|
|
@@ -9354,9 +10132,13 @@ async function attempt(opts, r, steps, env, stepEnv, homeEnvFile) {
|
|
|
9354
10132
|
prompt: r.prompt,
|
|
9355
10133
|
cwd: workdir,
|
|
9356
10134
|
env: {
|
|
9357
|
-
...hostEnv(),
|
|
10135
|
+
...withoutOldPwd(hostEnv()),
|
|
9358
10136
|
...env,
|
|
9359
|
-
...harness.env({
|
|
10137
|
+
...harness.env({
|
|
10138
|
+
configDir: opts.configDir,
|
|
10139
|
+
run: { home: r.home, homeEnvFile }
|
|
10140
|
+
}),
|
|
10141
|
+
PWD: workdir,
|
|
9360
10142
|
PATH: opts.stepEnv.PATH,
|
|
9361
10143
|
FIREGOAT_OUT: opts.out,
|
|
9362
10144
|
FIREGOAT_WORKDIR: workdir,
|
|
@@ -9365,18 +10147,22 @@ async function attempt(opts, r, steps, env, stepEnv, homeEnvFile) {
|
|
|
9365
10147
|
stderr,
|
|
9366
10148
|
timeoutMs: test.timeout * 1000,
|
|
9367
10149
|
model: opts.model ?? undefined,
|
|
9368
|
-
extraArgs: r.
|
|
10150
|
+
extraArgs: r.harnessArgs,
|
|
9369
10151
|
onProgress: opts.onProgress
|
|
9370
10152
|
});
|
|
9371
|
-
r.claudeModel = outcome.reportedModel;
|
|
9372
10153
|
if (outcome.version !== null)
|
|
9373
10154
|
evalStats({ claudeVersion: claudeVersion(outcome.version) });
|
|
9374
10155
|
await new Promise((res) => stderr.end(res));
|
|
9375
10156
|
if (outcome.sessionId)
|
|
9376
10157
|
r.session = harness.findSession(opts.configDir, outcome.sessionId);
|
|
10158
|
+
r.reportedModel = outcome.reportedModel ?? (r.session && harness.sessionModel ? harness.sessionModel(r.session) : null);
|
|
10159
|
+
const about = [
|
|
10160
|
+
opts.shownHarness,
|
|
10161
|
+
opts.model === null ? null : `model ${opts.model}`
|
|
10162
|
+
].filter((x) => x !== null);
|
|
9377
10163
|
writeTranscript({
|
|
9378
10164
|
path: r.transcript,
|
|
9379
|
-
title: `${test.name}#${n}${
|
|
10165
|
+
title: `${test.name}#${n}${about.length > 0 ? ` (${about.join(", ")})` : ""}`,
|
|
9380
10166
|
prompt: r.prompt,
|
|
9381
10167
|
steps: outcome.steps,
|
|
9382
10168
|
final: outcome.final,
|
|
@@ -9419,6 +10205,9 @@ async function attempt(opts, r, steps, env, stepEnv, homeEnvFile) {
|
|
|
9419
10205
|
test.checks.length === 0 ? "no script checks" : `${r.checks.length}/${r.checks.length} checks passed`
|
|
9420
10206
|
];
|
|
9421
10207
|
}
|
|
10208
|
+
function notUploaded(h) {
|
|
10209
|
+
return `firegoat.ai doesn't read ${h.name} sessions yet: its runs stay local`;
|
|
10210
|
+
}
|
|
9422
10211
|
var MAX_LISTED_URLS = 5;
|
|
9423
10212
|
function uploadedSummary(origin, results, runDir) {
|
|
9424
10213
|
const urls = results.flatMap((r) => r.upload?.status === "uploaded" ? [r.upload.url] : []);
|
|
@@ -9426,7 +10215,7 @@ function uploadedSummary(origin, results, runDir) {
|
|
|
9426
10215
|
return;
|
|
9427
10216
|
const head = `Uploaded ${urls.length} session${urls.length === 1 ? "" : "s"} to ${new URL(origin).host} (private)`;
|
|
9428
10217
|
if (urls.length > MAX_LISTED_URLS) {
|
|
9429
|
-
console.log(`${head}: each run's upload.url in ${
|
|
10218
|
+
console.log(`${head}: each run's upload.url in ${join10(runDir, "summary.json")}`);
|
|
9430
10219
|
return;
|
|
9431
10220
|
}
|
|
9432
10221
|
console.log(`${head}:`);
|
|
@@ -9481,9 +10270,22 @@ async function evalCommand(args, root) {
|
|
|
9481
10270
|
}
|
|
9482
10271
|
const runs = opts.runs ?? config.runs;
|
|
9483
10272
|
const models = opts.models.length > 0 ? opts.models : config.models;
|
|
10273
|
+
const harnessNames = opts.harnesses.length > 0 ? opts.harnesses : config.harnesses;
|
|
10274
|
+
if (opts.harnessBin !== undefined && harnessNames.length !== 1)
|
|
10275
|
+
return fatal(EXIT.usage, `--harness-bin needs exactly one harness, this eval runs ${harnessNames.length} (${harnessNames.join(", ")}): pick one with --harness`, "usage");
|
|
10276
|
+
const harnesses = harnessNames.map((h) => HARNESSES.get(h));
|
|
10277
|
+
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
|
+
}
|
|
9484
10286
|
evalStats({
|
|
9485
10287
|
testCount: tests.length,
|
|
9486
|
-
runCount: tests.length * runs * Math.max(models.length, 1),
|
|
10288
|
+
runCount: tests.length * runs * Math.max(models.length, 1) * harnesses.length,
|
|
9487
10289
|
modelCount: models.length,
|
|
9488
10290
|
parallel: opts.parallel
|
|
9489
10291
|
});
|
|
@@ -9493,7 +10295,7 @@ async function evalCommand(args, root) {
|
|
|
9493
10295
|
t.prompt,
|
|
9494
10296
|
...t.checks.map((c) => c.run),
|
|
9495
10297
|
...Object.values(t.env),
|
|
9496
|
-
...t.
|
|
10298
|
+
...t.harnessArgs.map((a) => a.arg),
|
|
9497
10299
|
...t.install,
|
|
9498
10300
|
...t.cleanup
|
|
9499
10301
|
]).concat(config.build, config.install, config.cleanup).flatMap(varRefs).filter((k) => !(k in vars)))
|
|
@@ -9511,9 +10313,10 @@ async function evalCommand(args, root) {
|
|
|
9511
10313
|
dryRun({
|
|
9512
10314
|
config,
|
|
9513
10315
|
tests,
|
|
9514
|
-
plan: planRuns(tests, models, runs),
|
|
10316
|
+
plan: planRuns(tests, harnessNames, models, runs),
|
|
10317
|
+
showHarness,
|
|
9515
10318
|
vars,
|
|
9516
|
-
|
|
10319
|
+
agentDirs: projectAgentDir(repo?.mainCheckout ?? root),
|
|
9517
10320
|
json: opts.json,
|
|
9518
10321
|
say
|
|
9519
10322
|
});
|
|
@@ -9524,28 +10327,42 @@ async function evalCommand(args, root) {
|
|
|
9524
10327
|
}
|
|
9525
10328
|
return EXIT.pass;
|
|
9526
10329
|
}
|
|
9527
|
-
|
|
9528
|
-
|
|
9529
|
-
const
|
|
9530
|
-
|
|
9531
|
-
|
|
10330
|
+
evalStats({ harness: harnessNames.join(",") });
|
|
10331
|
+
const bins = new Map;
|
|
10332
|
+
for (const h of harnesses) {
|
|
10333
|
+
const bin = opts.harnessBin ?? h.bin;
|
|
10334
|
+
const path = resolveBin(bin);
|
|
10335
|
+
if (!path)
|
|
10336
|
+
return fatal(EXIT.notGraded, `Not graded: ${h.name} binary ${JSON.stringify(bin)} not found. ${h.installHint}`, "harness_missing");
|
|
10337
|
+
bins.set(h, path);
|
|
10338
|
+
}
|
|
9532
10339
|
let repo;
|
|
9533
10340
|
try {
|
|
9534
10341
|
repo = gitRepo(root);
|
|
9535
10342
|
} catch (e) {
|
|
9536
10343
|
return fatal(EXIT.usage, e.message, "config");
|
|
9537
10344
|
}
|
|
9538
|
-
const
|
|
9539
|
-
const
|
|
9540
|
-
|
|
9541
|
-
|
|
9542
|
-
const
|
|
9543
|
-
|
|
9544
|
-
|
|
9545
|
-
|
|
9546
|
-
}
|
|
9547
|
-
|
|
9548
|
-
|
|
10345
|
+
const project = repo?.mainCheckout ?? root;
|
|
10346
|
+
const firegoatDir = ensureFiregoatDir(project);
|
|
10347
|
+
const agentDirs = ensureProjectAgentDir(project);
|
|
10348
|
+
const configDirs = new Map;
|
|
10349
|
+
for (const h of harnesses) {
|
|
10350
|
+
const configDir = join10(agentDirs, h.configDirName);
|
|
10351
|
+
mkdirSync5(configDir, { recursive: true });
|
|
10352
|
+
configDirs.set(h, configDir);
|
|
10353
|
+
}
|
|
10354
|
+
ui.auth(harnesses.length === 1 ? harnesses[0].authLabel() : harnesses.map((h) => `${h.name}: ${h.authLabel()}`).join("; "));
|
|
10355
|
+
for (const h of harnesses) {
|
|
10356
|
+
const configDir = configDirs.get(h);
|
|
10357
|
+
const loginProblem = h.checkLogin({
|
|
10358
|
+
bin: bins.get(h),
|
|
10359
|
+
env: { ...hostEnv(), ...h.env({ configDir }) },
|
|
10360
|
+
cwd: root,
|
|
10361
|
+
configDir
|
|
10362
|
+
});
|
|
10363
|
+
if (loginProblem)
|
|
10364
|
+
return fatal(EXIT.notGraded, `Not graded: ${loginProblem.reason}`, loginProblem.loggedOut ? "harness_logged_out" : "harness_auth_status");
|
|
10365
|
+
}
|
|
9549
10366
|
const evalStarted = Date.now();
|
|
9550
10367
|
const runId = newRunId();
|
|
9551
10368
|
let upload;
|
|
@@ -9554,23 +10371,32 @@ async function evalCommand(args, root) {
|
|
|
9554
10371
|
if (pre && "exitCode" in pre)
|
|
9555
10372
|
return fatal(pre.exitCode, pre.error, pre.code);
|
|
9556
10373
|
upload = pre;
|
|
10374
|
+
if (upload) {
|
|
10375
|
+
for (const h of harnesses)
|
|
10376
|
+
if (!h.uploads)
|
|
10377
|
+
ui.note(notUploaded(h));
|
|
10378
|
+
}
|
|
9557
10379
|
} catch (e) {
|
|
9558
10380
|
if (e instanceof ConfigError)
|
|
9559
10381
|
return fatal(EXIT.usage, e.message, "config");
|
|
9560
10382
|
throw e;
|
|
9561
10383
|
}
|
|
9562
|
-
const runsRoot =
|
|
9563
|
-
const runDir =
|
|
9564
|
-
|
|
9565
|
-
|
|
10384
|
+
const runsRoot = join10(firegoatDir, "runs");
|
|
10385
|
+
const runDir = join10(runsRoot, runId);
|
|
10386
|
+
mkdirSync5(runDir, { recursive: true });
|
|
10387
|
+
const out = join10(makeAgentDir(agentDirs), "out");
|
|
10388
|
+
mkdirSync5(out);
|
|
10389
|
+
linkDir(out, join10(runDir, "out"));
|
|
9566
10390
|
const stepEnv = {
|
|
9567
10391
|
...hostEnv(),
|
|
9568
10392
|
...vars,
|
|
9569
10393
|
FIREGOAT_OUT: out,
|
|
9570
10394
|
PATH: [out, process.env.PATH ?? ""].join(delimiter2)
|
|
9571
10395
|
};
|
|
9572
|
-
const
|
|
10396
|
+
const plan = planRuns(tests, harnessNames, models, runs);
|
|
10397
|
+
const rows = plan.map((p) => ({
|
|
9573
10398
|
...p,
|
|
10399
|
+
harness: showHarness ? p.harness : null,
|
|
9574
10400
|
state: "queued",
|
|
9575
10401
|
turns: 0
|
|
9576
10402
|
}));
|
|
@@ -9585,7 +10411,7 @@ async function evalCommand(args, root) {
|
|
|
9585
10411
|
...stepEnv,
|
|
9586
10412
|
...renderEnv(Object.fromEntries(Object.entries(config.env).filter(([, v]) => isEvalLevel(v))), vars, { "run.id": runId, FIREGOAT_OUT: out })
|
|
9587
10413
|
},
|
|
9588
|
-
logPath:
|
|
10414
|
+
logPath: join10(runDir, "build.txt")
|
|
9589
10415
|
});
|
|
9590
10416
|
const secs = Math.round((Date.now() - buildStarted) / 1000);
|
|
9591
10417
|
ui.build(error ? "fail" : "ok", secs);
|
|
@@ -9597,6 +10423,7 @@ async function evalCommand(args, root) {
|
|
|
9597
10423
|
const runAt = async (i) => {
|
|
9598
10424
|
const row = rows[i];
|
|
9599
10425
|
const test = tests.find((t) => t.name === row.test);
|
|
10426
|
+
const harness = HARNESSES.get(plan[i].harness);
|
|
9600
10427
|
row.state = "running";
|
|
9601
10428
|
ui.update();
|
|
9602
10429
|
const r = await runOne({
|
|
@@ -9608,11 +10435,14 @@ async function evalCommand(args, root) {
|
|
|
9608
10435
|
test,
|
|
9609
10436
|
n: row.n,
|
|
9610
10437
|
model: row.model,
|
|
10438
|
+
shownHarness: row.harness,
|
|
9611
10439
|
runId,
|
|
9612
10440
|
runDir,
|
|
10441
|
+
agentDirs,
|
|
10442
|
+
repo: root,
|
|
9613
10443
|
harness,
|
|
9614
|
-
bin:
|
|
9615
|
-
configDir,
|
|
10444
|
+
bin: bins.get(harness),
|
|
10445
|
+
configDir: configDirs.get(harness),
|
|
9616
10446
|
install: config.install,
|
|
9617
10447
|
cleanup: config.cleanup,
|
|
9618
10448
|
out,
|
|
@@ -9630,7 +10460,10 @@ async function evalCommand(args, root) {
|
|
|
9630
10460
|
command: undefined
|
|
9631
10461
|
});
|
|
9632
10462
|
ui.update();
|
|
9633
|
-
if (upload) {
|
|
10463
|
+
if (upload && !harness.uploads) {
|
|
10464
|
+
r.upload = { status: "skipped", reason: notUploaded(harness) };
|
|
10465
|
+
writeRunJson(r);
|
|
10466
|
+
} else if (upload) {
|
|
9634
10467
|
row.upload = "uploading";
|
|
9635
10468
|
ui.update();
|
|
9636
10469
|
r.upload = await uploadRun(upload, r);
|
|
@@ -9669,20 +10502,22 @@ async function evalCommand(args, root) {
|
|
|
9669
10502
|
runsDir: runDir,
|
|
9670
10503
|
dir: root,
|
|
9671
10504
|
...repo && { git: repo.info },
|
|
10505
|
+
harnesses: harnessNames,
|
|
9672
10506
|
upload: upload && { origin: upload.origin, email: upload.email },
|
|
9673
10507
|
runs: results,
|
|
9674
10508
|
totals,
|
|
9675
10509
|
status,
|
|
9676
10510
|
exitCode
|
|
9677
10511
|
};
|
|
9678
|
-
writeFileSync4(
|
|
10512
|
+
writeFileSync4(join10(runDir, "summary.json"), `${JSON.stringify(doc, null, 2)}
|
|
9679
10513
|
`);
|
|
9680
|
-
appendFileSync(
|
|
10514
|
+
appendFileSync(join10(runsRoot, "index.jsonl"), `${JSON.stringify({
|
|
9681
10515
|
runId,
|
|
9682
10516
|
startedAt: new Date(evalStarted).toISOString(),
|
|
9683
10517
|
dir: root,
|
|
9684
10518
|
branch: repo?.info.branch ?? null,
|
|
9685
10519
|
commit: repo?.info.commit ?? null,
|
|
10520
|
+
harnesses: harnessNames,
|
|
9686
10521
|
passed: totals.pass,
|
|
9687
10522
|
failed: totals.fail,
|
|
9688
10523
|
notGraded: totals.error,
|
|
@@ -9715,7 +10550,7 @@ async function evalCommand(args, root) {
|
|
|
9715
10550
|
}
|
|
9716
10551
|
|
|
9717
10552
|
// src/feedback.ts
|
|
9718
|
-
import { readFileSync as
|
|
10553
|
+
import { readFileSync as readFileSync6 } from "node:fs";
|
|
9719
10554
|
|
|
9720
10555
|
// node_modules/@firegoat/feedback/dist/index.js
|
|
9721
10556
|
function memoryDeviceStore() {
|
|
@@ -9934,7 +10769,7 @@ async function feedbackCommand(args) {
|
|
|
9934
10769
|
try {
|
|
9935
10770
|
if (a === "--body") {
|
|
9936
10771
|
const v = value();
|
|
9937
|
-
body = v === "-" ?
|
|
10772
|
+
body = v === "-" ? readFileSync6(0, "utf8") : v;
|
|
9938
10773
|
} else if (a === "--type") {
|
|
9939
10774
|
const v = value();
|
|
9940
10775
|
if (!TYPES.includes(v))
|
|
@@ -9987,9 +10822,9 @@ firegoat ${package_default.version}` : `firegoat ${package_default.version}`,
|
|
|
9987
10822
|
}
|
|
9988
10823
|
|
|
9989
10824
|
// src/init.ts
|
|
9990
|
-
import { spawn as spawn3, spawnSync as
|
|
9991
|
-
import { existsSync as
|
|
9992
|
-
import { basename as basename4, join as
|
|
10825
|
+
import { spawn as spawn3, spawnSync as spawnSync4 } from "node:child_process";
|
|
10826
|
+
import { existsSync as existsSync6, writeFileSync as writeFileSync5 } from "node:fs";
|
|
10827
|
+
import { basename as basename4, join as join11 } from "node:path";
|
|
9993
10828
|
|
|
9994
10829
|
// src/login.ts
|
|
9995
10830
|
import { spawn as spawn2 } from "node:child_process";
|
|
@@ -10000,7 +10835,7 @@ var FORM = { "Content-Type": "application/x-www-form-urlencoded" };
|
|
|
10000
10835
|
var UPLOADS_ON = "Eval uploads each session to firegoat.ai as a private trace; --no-upload keeps runs local.";
|
|
10001
10836
|
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).`;
|
|
10002
10837
|
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).";
|
|
10003
|
-
function
|
|
10838
|
+
function str3(o, k, step) {
|
|
10004
10839
|
const v = o?.[k];
|
|
10005
10840
|
if (typeof v !== "string" || v === "")
|
|
10006
10841
|
throw new ApiError(`${step}: response has no ${k}`);
|
|
@@ -10031,9 +10866,9 @@ async function loginFlow(show, interactive) {
|
|
|
10031
10866
|
});
|
|
10032
10867
|
if (cfg.status !== 200)
|
|
10033
10868
|
throw replyError(cfgStep, cfg);
|
|
10034
|
-
const clientId =
|
|
10035
|
-
const deviceUrl = assertSafeUrl(
|
|
10036
|
-
const tokenUrl = assertSafeUrl(
|
|
10869
|
+
const clientId = str3(cfg.json, "client_id", cfgStep);
|
|
10870
|
+
const deviceUrl = assertSafeUrl(str3(cfg.json, "device_authorization_endpoint", cfgStep), "device_authorization_endpoint");
|
|
10871
|
+
const tokenUrl = assertSafeUrl(str3(cfg.json, "token_endpoint", cfgStep), "token_endpoint");
|
|
10037
10872
|
if (tokenUrl.origin !== origin)
|
|
10038
10873
|
throw new ApiError(`token_endpoint must be on ${origin}, got ${printable(tokenUrl.origin, 100)}`);
|
|
10039
10874
|
const devStep = "Device authorization";
|
|
@@ -10047,11 +10882,11 @@ async function loginFlow(show, interactive) {
|
|
|
10047
10882
|
});
|
|
10048
10883
|
if (dev.status !== 200)
|
|
10049
10884
|
throw replyError(devStep, dev);
|
|
10050
|
-
const deviceCode =
|
|
10051
|
-
const userCode =
|
|
10885
|
+
const deviceCode = str3(dev.json, "device_code", devStep);
|
|
10886
|
+
const userCode = str3(dev.json, "user_code", devStep);
|
|
10052
10887
|
if (!/^[A-Za-z0-9-]{1,64}$/.test(userCode))
|
|
10053
10888
|
throw new ApiError(`${devStep}: user_code has unexpected characters`);
|
|
10054
|
-
const pageRaw = typeof dev.json?.verification_uri_complete === "string" ? dev.json.verification_uri_complete :
|
|
10889
|
+
const pageRaw = typeof dev.json?.verification_uri_complete === "string" ? dev.json.verification_uri_complete : str3(dev.json, "verification_uri", devStep);
|
|
10055
10890
|
const page = assertSafeUrl(pageRaw, "verification_uri");
|
|
10056
10891
|
if (page.protocol !== "https:" || pageRaw.startsWith("-"))
|
|
10057
10892
|
throw new ApiError("verification_uri must be https");
|
|
@@ -10288,18 +11123,23 @@ function skillsDir(scope) {
|
|
|
10288
11123
|
if (scope === "local")
|
|
10289
11124
|
return ".claude/skills/";
|
|
10290
11125
|
const config = process.env.CLAUDE_CONFIG_DIR?.trim();
|
|
10291
|
-
return config ? `${
|
|
11126
|
+
return config ? `${join11(config, "skills")}/` : "~/.claude/skills/";
|
|
10292
11127
|
}
|
|
10293
11128
|
function template(product) {
|
|
10294
11129
|
return `# firegoat.yaml: evals for how well coding agents use ${product}.
|
|
10295
|
-
# Each test runs your claude in an empty folder
|
|
10296
|
-
# as the first message, then runs \`checks\`
|
|
11130
|
+
# Each test runs your agent (claude by default) in an empty folder
|
|
11131
|
+
# (~/.firegoat/runs/...) with \`prompt\` as the first message, then runs \`checks\`
|
|
11132
|
+
# there. Reference: npx firegoat init --help
|
|
10297
11133
|
|
|
10298
11134
|
# How many times to run each test.
|
|
10299
11135
|
runs: 1
|
|
10300
11136
|
|
|
10301
|
-
# Optional:
|
|
10302
|
-
#
|
|
11137
|
+
# Optional: the agent CLI each test runs on: claude-code (default), codex-cli or grok-cli.
|
|
11138
|
+
# A list runs every test \`runs\` times per harness. eval --harness NAME overrides it.
|
|
11139
|
+
# harness: [claude-code, codex-cli]
|
|
11140
|
+
|
|
11141
|
+
# Optional: the harness's --model. A list runs every test \`runs\` times per model.
|
|
11142
|
+
# Unset: the harness's default. eval --model NAME overrides it.
|
|
10303
11143
|
# model: [claude-haiku-5-5, claude-sonnet-5-5]
|
|
10304
11144
|
|
|
10305
11145
|
# Optional: test your local build instead of the published package.
|
|
@@ -10380,7 +11220,7 @@ var CLIPBOARD = [
|
|
|
10380
11220
|
];
|
|
10381
11221
|
function copyToClipboard(text) {
|
|
10382
11222
|
return CLIPBOARD.some(([cmd, args]) => {
|
|
10383
|
-
const r =
|
|
11223
|
+
const r = spawnSync4(cmd, args, { input: text, env: hostEnv() });
|
|
10384
11224
|
return !r.error && r.status === 0;
|
|
10385
11225
|
});
|
|
10386
11226
|
}
|
|
@@ -10545,8 +11385,8 @@ ${INIT_HELP}`);
|
|
|
10545
11385
|
console.log(rule);
|
|
10546
11386
|
}
|
|
10547
11387
|
ensureFiregoatDir(cwd);
|
|
10548
|
-
const configPath =
|
|
10549
|
-
const keep =
|
|
11388
|
+
const configPath = join11(cwd, CONFIG_FILE);
|
|
11389
|
+
const keep = existsSync6(configPath) && !force;
|
|
10550
11390
|
if (!keep)
|
|
10551
11391
|
writeFileSync5(configPath, template(basename4(cwd)));
|
|
10552
11392
|
if (ui) {
|
|
@@ -10590,7 +11430,7 @@ ${INIT_HELP}`);
|
|
|
10590
11430
|
block.draw([ui.done(3, skill, `Installed → ${where}`)]);
|
|
10591
11431
|
else
|
|
10592
11432
|
console.log(`Installed skill firegoat-onboarding in ${where}`);
|
|
10593
|
-
const ignored = scope === "local" &&
|
|
11433
|
+
const ignored = scope === "local" && spawnSync4("git", ["check-ignore", "-q", ".claude/skills"], {
|
|
10594
11434
|
cwd,
|
|
10595
11435
|
env: hostEnv()
|
|
10596
11436
|
}).status === 0;
|