firegoat 0.25.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 +2 -0
- package/dist/cli.js +153 -76
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -37,6 +37,8 @@ tests:
|
|
|
37
37
|
- run: grep -qE 'evt_[A-Za-z0-9]+' "$FIREGOAT_TRANSCRIPT"
|
|
38
38
|
```
|
|
39
39
|
|
|
40
|
+
Each run starts in a fresh, empty folder outside your repo, `~/.firegoat/runs/<hash>/<random>/work`, so its path doesn't name your project and the agent doesn't wander into your source. Logs and results stay in the repo, in `.firegoat/runs/`.
|
|
41
|
+
|
|
40
42
|
## Uploads
|
|
41
43
|
|
|
42
44
|
Log in to your firegoat.ai account and each trace is uploaded to our system automatically. Secrets are masked in everything you see, but the uploaded files are stored as sent. You can opt out of uploading with `eval --no-upload`. You can also delete your traces on the dashboard, and the files and the record are gone from our site.
|
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 .",
|
|
@@ -8150,10 +8150,54 @@ import { randomBytes as randomBytes2 } from "node:crypto";
|
|
|
8150
8150
|
import {
|
|
8151
8151
|
appendFileSync,
|
|
8152
8152
|
createWriteStream as createWriteStream2,
|
|
8153
|
-
mkdirSync as
|
|
8153
|
+
mkdirSync as mkdirSync5,
|
|
8154
8154
|
writeFileSync as writeFileSync4
|
|
8155
8155
|
} from "node:fs";
|
|
8156
|
-
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
|
+
}
|
|
8157
8201
|
|
|
8158
8202
|
// src/credentials.ts
|
|
8159
8203
|
import { randomBytes } from "node:crypto";
|
|
@@ -8162,15 +8206,15 @@ import {
|
|
|
8162
8206
|
closeSync as closeSync2,
|
|
8163
8207
|
fsyncSync,
|
|
8164
8208
|
lstatSync as lstatSync3,
|
|
8165
|
-
mkdirSync as
|
|
8209
|
+
mkdirSync as mkdirSync3,
|
|
8166
8210
|
openSync as openSync2,
|
|
8167
8211
|
readFileSync as readFileSync4,
|
|
8168
8212
|
renameSync,
|
|
8169
8213
|
unlinkSync as unlinkSync3,
|
|
8170
8214
|
writeSync
|
|
8171
8215
|
} from "node:fs";
|
|
8172
|
-
import { homedir as
|
|
8173
|
-
import { join as
|
|
8216
|
+
import { homedir as homedir4 } from "node:os";
|
|
8217
|
+
import { join as join6 } from "node:path";
|
|
8174
8218
|
|
|
8175
8219
|
// src/firegoat-api.ts
|
|
8176
8220
|
var DEFAULT_BASE_URL = "https://firegoat.ai/evals";
|
|
@@ -8322,10 +8366,10 @@ async function whoami(base, key) {
|
|
|
8322
8366
|
// src/credentials.ts
|
|
8323
8367
|
var KEY = /^fire_[\x21-\x7e]+$/;
|
|
8324
8368
|
function credentialsDir() {
|
|
8325
|
-
return
|
|
8369
|
+
return join6(homedir4(), ".firegoat");
|
|
8326
8370
|
}
|
|
8327
8371
|
function credentialsPath() {
|
|
8328
|
-
return
|
|
8372
|
+
return join6(credentialsDir(), "credentials.json");
|
|
8329
8373
|
}
|
|
8330
8374
|
function uid() {
|
|
8331
8375
|
if (!process.getuid)
|
|
@@ -8388,7 +8432,7 @@ function saveCredentials(c) {
|
|
|
8388
8432
|
if (st)
|
|
8389
8433
|
assertOwned(dir, st, "dir");
|
|
8390
8434
|
else
|
|
8391
|
-
|
|
8435
|
+
mkdirSync3(dir, { mode: 448 });
|
|
8392
8436
|
chmodSync(dir, 448);
|
|
8393
8437
|
const tmp = `${credentialsPath()}.${process.pid}.${randomBytes(6).toString("hex")}.tmp`;
|
|
8394
8438
|
const fd = openSync2(tmp, "wx", 384);
|
|
@@ -8444,15 +8488,15 @@ function resolveKey(base) {
|
|
|
8444
8488
|
var EXIT = { pass: 0, fail: 1, notGraded: 2, usage: 3 };
|
|
8445
8489
|
|
|
8446
8490
|
// src/firegoat-dir.ts
|
|
8447
|
-
import { mkdirSync as
|
|
8448
|
-
import { join as
|
|
8491
|
+
import { mkdirSync as mkdirSync4, writeFileSync as writeFileSync2 } from "node:fs";
|
|
8492
|
+
import { join as join7 } from "node:path";
|
|
8449
8493
|
var GITIGNORE = `*
|
|
8450
8494
|
`;
|
|
8451
8495
|
function ensureFiregoatDir(root) {
|
|
8452
|
-
const dir =
|
|
8453
|
-
|
|
8496
|
+
const dir = join7(root, ".firegoat");
|
|
8497
|
+
mkdirSync4(dir, { recursive: true });
|
|
8454
8498
|
try {
|
|
8455
|
-
writeFileSync2(
|
|
8499
|
+
writeFileSync2(join7(dir, ".gitignore"), GITIGNORE, { flag: "wx" });
|
|
8456
8500
|
} catch (e) {
|
|
8457
8501
|
if (e.code !== "EEXIST")
|
|
8458
8502
|
throw e;
|
|
@@ -8812,7 +8856,11 @@ firegoat.yaml reference:
|
|
|
8812
8856
|
for any value.
|
|
8813
8857
|
The build, install and cleanup commands and the checks get the vars as env
|
|
8814
8858
|
vars, plus $FIREGOAT_OUT and $FIREGOAT_WORKDIR; install, checks and cleanup also
|
|
8815
|
-
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.
|
|
8816
8864
|
Checks also get $FIREGOAT_TRANSCRIPT, a short plain-text
|
|
8817
8865
|
transcript, greppable line by line (format: eval --help, Transcript). So
|
|
8818
8866
|
'^\\[[0-9]+\\] \\$ .*foo' matches only commands. Checks also get
|
|
@@ -8911,8 +8959,13 @@ var EVAL_HELP = `Usage: firegoat eval [test..] [--runs N] [--model NAME].. [--pa
|
|
|
8911
8959
|
|
|
8912
8960
|
Runs each test in firegoat.yaml (in the current folder): your agent harness
|
|
8913
8961
|
(claude-code by default, codex-cli or grok-cli; harness: in firegoat.yaml) gets the prompt
|
|
8914
|
-
in a fresh, empty folder,
|
|
8915
|
-
outputs on PATH. Then the test's checks run in that folder.
|
|
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.
|
|
8916
8969
|
|
|
8917
8970
|
Step order: build: once, in this folder; then per run, in its folder: install:
|
|
8918
8971
|
(top-level, then the test's), the agent, checks, cleanup: (the test's, then
|
|
@@ -8921,38 +8974,39 @@ a claude error or a timeout; a failing cleanup step prints a warning and never
|
|
|
8921
8974
|
changes the run's status or the exit code.
|
|
8922
8975
|
|
|
8923
8976
|
claude runs on its own login (or ANTHROPIC_API_KEY if set, as claude always does)
|
|
8924
|
-
with a fresh config dir,
|
|
8925
|
-
plugins, skills or MCP servers (harness_args: in firegoat.yaml adds
|
|
8926
|
-
e.g. --chrome or --plugin-dir). claude saves its session there. There
|
|
8927
|
-
sandbox: the agent can reach the rest of your machine, including the
|
|
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.
|
|
8928
8982
|
|
|
8929
8983
|
codex-cli runs codex exec --json --dangerously-bypass-approvals-and-sandbox with a
|
|
8930
|
-
fresh CODEX_HOME,
|
|
8984
|
+
fresh CODEX_HOME, ~/.firegoat/runs/<hash>/.codex, holding a link to your codex login
|
|
8931
8985
|
(~/.codex/auth.json, or $CODEX_HOME/auth.json; run codex login first), and with
|
|
8932
8986
|
-c project_doc_max_bytes=0: no AGENTS.md, config.toml, skills or plugins of
|
|
8933
8987
|
yours. codex's own HOME is the run's home (below), also for its Read/Write. It
|
|
8934
|
-
saves its session (rollout) under
|
|
8988
|
+
saves its session (rollout) under that CODEX_HOME's sessions/. firegoat.ai doesn't
|
|
8935
8989
|
read codex sessions yet: those runs are not uploaded. codex reports no turns or
|
|
8936
8990
|
cost; its model comes from the session file.
|
|
8937
8991
|
|
|
8938
8992
|
grok-cli runs ~/.grok/bin/grok (where grok's installer puts it; never a grok on
|
|
8939
8993
|
PATH, which may be a wrapper such as cmux's: a #! script is refused) as grok -p
|
|
8940
8994
|
--output-format streaming-messages-json --always-approve --no-auto-update, with a
|
|
8941
|
-
fresh GROK_HOME,
|
|
8995
|
+
fresh GROK_HOME, ~/.firegoat/runs/<hash>/.grok, holding a link to your grok login
|
|
8942
8996
|
($GROK_HOME/auth.json or ~/.grok/auth.json; run grok login first; XAI_API_KEY alone
|
|
8943
8997
|
does not log headless grok in). grok's own HOME is the run's home (below): with
|
|
8944
8998
|
your real one it loads Claude Code's skills, plugins, MCP servers and hooks. eval
|
|
8945
8999
|
refuses a login that expires within 5 minutes (run grok -p hi once to refresh it):
|
|
8946
9000
|
a refresh inside a run would replace the link with a file, which the next eval
|
|
8947
9001
|
asks you to move back over ~/.grok/auth.json. The session is a directory,
|
|
8948
|
-
|
|
9002
|
+
$GROK_HOME/sessions/<cwd>/<id>/ ($FIREGOAT_SESSION). firegoat.ai doesn't read
|
|
8949
9003
|
grok sessions yet: those runs are not uploaded.
|
|
8950
9004
|
|
|
8951
|
-
Every run gets its own HOME,
|
|
8952
|
-
XDG_CONFIG_HOME, XDG_CACHE_HOME, XDG_DATA_HOME, XDG_STATE_HOME inside it), as
|
|
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
|
|
8953
9007
|
$FIREGOAT_HOME: tools that keep state in ~ start fresh and parallel runs don't
|
|
8954
9008
|
collide. claude itself keeps your real HOME (its login is there) and sources
|
|
8955
|
-
|
|
9009
|
+
home.env next to it (CLAUDE_ENV_FILE) before each Bash command the agent runs; install,
|
|
8956
9010
|
checks and cleanup get the run's HOME directly; build keeps yours. Limits: claude's
|
|
8957
9011
|
own Read/Write tools expand ~ to your real home; getpwuid lookups (os.userInfo,
|
|
8958
9012
|
python pwd) still see it; keychain logins (gh, docker, git credential helpers) look
|
|
@@ -8968,9 +9022,9 @@ nested session. Login and provider vars stay: ANTHROPIC_*, CLAUDE_CODE_OAUTH_TOK
|
|
|
8968
9022
|
CLAUDE_CODE_OAUTH_REFRESH_TOKEN, CLAUDE_CODE_USE_*, CLAUDE_CODE_SKIP_*_AUTH,
|
|
8969
9023
|
CLAUDE_CODE_CLIENT_CERT, CLAUDE_CODE_CLIENT_KEY, CLAUDE_CODE_CLIENT_KEY_PASSPHRASE.
|
|
8970
9024
|
|
|
8971
|
-
In a linked git worktree (git worktree add), .firegoat/runs/
|
|
8972
|
-
|
|
8973
|
-
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.
|
|
8974
9028
|
|
|
8975
9029
|
test.. Only these tests (by name)
|
|
8976
9030
|
--runs N Runs per test (overrides runs: in firegoat.yaml)
|
|
@@ -8994,8 +9048,9 @@ fixtures and build: stay in the current folder.
|
|
|
8994
9048
|
--model, --harness, missing vars), print each planned
|
|
8995
9049
|
run ("<test>#<n> [harness model]" with its install, checks
|
|
8996
9050
|
and cleanup counts and rendered harness_args, paths under
|
|
8997
|
-
runs/<
|
|
8998
|
-
agent, uploads nothing, writes no
|
|
9051
|
+
~/.firegoat/runs/<hash>/<random>/) and exit 0. Builds
|
|
9052
|
+
nothing, starts no agent, uploads nothing, writes no
|
|
9053
|
+
files. Config errors
|
|
8999
9054
|
exit 3. With --json: {dryRun, build, runs: [{test, n,
|
|
9000
9055
|
harness, model, harnessArgs, install, checks,
|
|
9001
9056
|
cleanup}]}, steps and checks with their placeholders
|
|
@@ -9017,6 +9072,8 @@ Runs without a session file are not uploaded. FIREGOAT_BASE_URL points elsewhere
|
|
|
9017
9072
|
(default https://firegoat.ai/evals). See firegoat login --help.
|
|
9018
9073
|
|
|
9019
9074
|
Output: .firegoat/runs/<runId>/
|
|
9075
|
+
out A symlink to $FIREGOAT_OUT,
|
|
9076
|
+
~/.firegoat/runs/<hash>/<random>/out
|
|
9020
9077
|
summary.json firegoatVersion, runId, runsDir, dir (where eval
|
|
9021
9078
|
ran), git {branch, commit, dirty} (no git outside a
|
|
9022
9079
|
repo), harnesses, upload {origin, email} (null when
|
|
@@ -9030,14 +9087,17 @@ Output: .firegoat/runs/<runId>/
|
|
|
9030
9087
|
the harness reported running, or null), harnessArgs
|
|
9031
9088
|
(harness_args passed), prompt, end_goal,
|
|
9032
9089
|
checks [{run, code}], cleanup [{run, code}],
|
|
9033
|
-
and paths: workdir, home (the
|
|
9090
|
+
and paths: dir (this folder), workdir, home (the
|
|
9091
|
+
run's HOME; both real paths, under ~/.firegoat),
|
|
9034
9092
|
transcript, session (the harness's session file), stderr,
|
|
9035
9093
|
checksLog, cleanupLog; startedAt; upload ({status: "uploaded",
|
|
9036
9094
|
url} | {status: "failed", error} | {status:
|
|
9037
9095
|
"skipped", reason}, null when not uploading)
|
|
9038
9096
|
<test>/<n>/transcript.txt What the agent did, below
|
|
9039
|
-
<test>/<n>/
|
|
9040
|
-
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
|
+
|
|
9041
9101
|
build.txt, <test>/<n>/install.txt, checks.txt, cleanup.txt Logs
|
|
9042
9102
|
|
|
9043
9103
|
Transcript ($FIREGOAT_TRANSCRIPT), greppable line by line; n counts tool calls:
|
|
@@ -9228,7 +9288,7 @@ async function sendTelemetry(event, exitCode, startedAt) {
|
|
|
9228
9288
|
|
|
9229
9289
|
// src/transcript.ts
|
|
9230
9290
|
import { readdirSync as readdirSync4, statSync as statSync2, writeFileSync as writeFileSync3 } from "node:fs";
|
|
9231
|
-
import { join as
|
|
9291
|
+
import { join as join8, relative } from "node:path";
|
|
9232
9292
|
var OUTPUT_LINES = 15;
|
|
9233
9293
|
var OUTPUT_CHARS = 1500;
|
|
9234
9294
|
var TEXT_CHARS = 1500;
|
|
@@ -9280,7 +9340,7 @@ function workdirFiles(dir) {
|
|
|
9280
9340
|
for (const name of readdirSync4(d)) {
|
|
9281
9341
|
if (SKIP_DIRS.has(name))
|
|
9282
9342
|
continue;
|
|
9283
|
-
const p =
|
|
9343
|
+
const p = join8(d, name);
|
|
9284
9344
|
if (statSync2(p).isDirectory())
|
|
9285
9345
|
walk(p);
|
|
9286
9346
|
else
|
|
@@ -9780,7 +9840,7 @@ async function uploadRun(t, r) {
|
|
|
9780
9840
|
|
|
9781
9841
|
// src/which.ts
|
|
9782
9842
|
import { accessSync, constants, statSync as statSync3 } from "node:fs";
|
|
9783
|
-
import { delimiter, join as
|
|
9843
|
+
import { delimiter, join as join9, resolve } from "node:path";
|
|
9784
9844
|
function resolveBin(bin) {
|
|
9785
9845
|
const executable = (p) => {
|
|
9786
9846
|
try {
|
|
@@ -9793,8 +9853,8 @@ function resolveBin(bin) {
|
|
|
9793
9853
|
if (bin.includes("/"))
|
|
9794
9854
|
return executable(bin) ? resolve(bin) : undefined;
|
|
9795
9855
|
for (const dir of (process.env.PATH ?? "").split(delimiter)) {
|
|
9796
|
-
if (dir && executable(
|
|
9797
|
-
return
|
|
9856
|
+
if (dir && executable(join9(dir, bin)))
|
|
9857
|
+
return join9(dir, bin);
|
|
9798
9858
|
}
|
|
9799
9859
|
return;
|
|
9800
9860
|
}
|
|
@@ -9881,7 +9941,7 @@ function newRunId() {
|
|
|
9881
9941
|
return `${ts}-${randomBytes2(2).toString("hex")}`;
|
|
9882
9942
|
}
|
|
9883
9943
|
function writeRunJson(r) {
|
|
9884
|
-
writeFileSync4(
|
|
9944
|
+
writeFileSync4(join10(r.dir, "run.json"), `${JSON.stringify(r, null, 2)}
|
|
9885
9945
|
`);
|
|
9886
9946
|
}
|
|
9887
9947
|
async function uploadPreflight(opts, ui, project, runGroup, git) {
|
|
@@ -9922,14 +9982,14 @@ async function uploadPreflight(opts, ui, project, runGroup, git) {
|
|
|
9922
9982
|
};
|
|
9923
9983
|
}
|
|
9924
9984
|
}
|
|
9925
|
-
function runFill(runId, n, testName, out,
|
|
9985
|
+
function runFill(runId, n, testName, out, agentDir) {
|
|
9926
9986
|
return {
|
|
9927
9987
|
"run.id": runId,
|
|
9928
9988
|
"run.n": String(n),
|
|
9929
9989
|
"test.name": testName,
|
|
9930
9990
|
FIREGOAT_OUT: out,
|
|
9931
|
-
FIREGOAT_WORKDIR:
|
|
9932
|
-
FIREGOAT_HOME:
|
|
9991
|
+
FIREGOAT_WORKDIR: join10(agentDir, "work"),
|
|
9992
|
+
FIREGOAT_HOME: join10(agentDir, "home")
|
|
9933
9993
|
};
|
|
9934
9994
|
}
|
|
9935
9995
|
function renderHarnessArgs(test, vars, fill) {
|
|
@@ -9962,10 +10022,11 @@ function showsHarness(harnesses) {
|
|
|
9962
10022
|
}
|
|
9963
10023
|
var DRY_RUN_ID = "<runId>";
|
|
9964
10024
|
function dryRun(opts) {
|
|
9965
|
-
const
|
|
10025
|
+
const agentDir = join10(opts.agentDirs, "<random>");
|
|
10026
|
+
const out = join10(agentDir, "out");
|
|
9966
10027
|
const runs = opts.plan.map((p) => {
|
|
9967
10028
|
const test = opts.tests.find((t) => t.name === p.test);
|
|
9968
|
-
const fill = runFill(DRY_RUN_ID, p.n, test.name, out,
|
|
10029
|
+
const fill = runFill(DRY_RUN_ID, p.n, test.name, out, agentDir);
|
|
9969
10030
|
renderEnv(test.env, opts.vars, fill);
|
|
9970
10031
|
return {
|
|
9971
10032
|
...p,
|
|
@@ -9986,11 +10047,15 @@ function dryRun(opts) {
|
|
|
9986
10047
|
async function runOne(opts) {
|
|
9987
10048
|
const { test, n } = opts;
|
|
9988
10049
|
const started = Date.now();
|
|
9989
|
-
const dir =
|
|
9990
|
-
|
|
9991
|
-
|
|
9992
|
-
const
|
|
9993
|
-
|
|
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);
|
|
9994
10059
|
const r = {
|
|
9995
10060
|
test: test.name,
|
|
9996
10061
|
n,
|
|
@@ -10007,11 +10072,11 @@ async function runOne(opts) {
|
|
|
10007
10072
|
workdir,
|
|
10008
10073
|
home,
|
|
10009
10074
|
session: null,
|
|
10010
|
-
transcript:
|
|
10011
|
-
stderr:
|
|
10012
|
-
checksLog:
|
|
10075
|
+
transcript: join10(dir, "transcript.txt"),
|
|
10076
|
+
stderr: join10(dir, "stderr.txt"),
|
|
10077
|
+
checksLog: join10(dir, "checks.txt"),
|
|
10013
10078
|
cleanup: [],
|
|
10014
|
-
cleanupLog:
|
|
10079
|
+
cleanupLog: join10(dir, "cleanup.txt"),
|
|
10015
10080
|
num_turns: null,
|
|
10016
10081
|
cost: null,
|
|
10017
10082
|
duration_ms: 0,
|
|
@@ -10024,7 +10089,9 @@ async function runOne(opts) {
|
|
|
10024
10089
|
...env,
|
|
10025
10090
|
...homeEnv(home),
|
|
10026
10091
|
FIREGOAT_WORKDIR: workdir,
|
|
10027
|
-
FIREGOAT_HOME: home
|
|
10092
|
+
FIREGOAT_HOME: home,
|
|
10093
|
+
FIREGOAT_RUN_DIR: dir,
|
|
10094
|
+
FIREGOAT_REPO: opts.repo
|
|
10028
10095
|
};
|
|
10029
10096
|
const steps = renderRunSteps(test, opts, opts.vars, fill);
|
|
10030
10097
|
const [status, reason] = await attempt(opts, r, steps, env, stepEnv, envFile);
|
|
@@ -10041,6 +10108,9 @@ async function runOne(opts) {
|
|
|
10041
10108
|
writeRunJson(r);
|
|
10042
10109
|
return r;
|
|
10043
10110
|
}
|
|
10111
|
+
function withoutOldPwd(env) {
|
|
10112
|
+
return Object.fromEntries(Object.entries(env).filter(([k]) => k !== "OLDPWD"));
|
|
10113
|
+
}
|
|
10044
10114
|
async function attempt(opts, r, steps, env, stepEnv, homeEnvFile) {
|
|
10045
10115
|
const { test, n } = opts;
|
|
10046
10116
|
const { dir, workdir } = r;
|
|
@@ -10050,7 +10120,7 @@ async function attempt(opts, r, steps, env, stepEnv, homeEnvFile) {
|
|
|
10050
10120
|
commands: steps.install,
|
|
10051
10121
|
cwd: workdir,
|
|
10052
10122
|
env: stepEnv,
|
|
10053
|
-
logPath:
|
|
10123
|
+
logPath: join10(dir, "install.txt")
|
|
10054
10124
|
});
|
|
10055
10125
|
if (error)
|
|
10056
10126
|
return ["error", `not graded: ${error}`];
|
|
@@ -10062,12 +10132,13 @@ async function attempt(opts, r, steps, env, stepEnv, homeEnvFile) {
|
|
|
10062
10132
|
prompt: r.prompt,
|
|
10063
10133
|
cwd: workdir,
|
|
10064
10134
|
env: {
|
|
10065
|
-
...hostEnv(),
|
|
10135
|
+
...withoutOldPwd(hostEnv()),
|
|
10066
10136
|
...env,
|
|
10067
10137
|
...harness.env({
|
|
10068
10138
|
configDir: opts.configDir,
|
|
10069
10139
|
run: { home: r.home, homeEnvFile }
|
|
10070
10140
|
}),
|
|
10141
|
+
PWD: workdir,
|
|
10071
10142
|
PATH: opts.stepEnv.PATH,
|
|
10072
10143
|
FIREGOAT_OUT: opts.out,
|
|
10073
10144
|
FIREGOAT_WORKDIR: workdir,
|
|
@@ -10144,7 +10215,7 @@ function uploadedSummary(origin, results, runDir) {
|
|
|
10144
10215
|
return;
|
|
10145
10216
|
const head = `Uploaded ${urls.length} session${urls.length === 1 ? "" : "s"} to ${new URL(origin).host} (private)`;
|
|
10146
10217
|
if (urls.length > MAX_LISTED_URLS) {
|
|
10147
|
-
console.log(`${head}: each run's upload.url in ${
|
|
10218
|
+
console.log(`${head}: each run's upload.url in ${join10(runDir, "summary.json")}`);
|
|
10148
10219
|
return;
|
|
10149
10220
|
}
|
|
10150
10221
|
console.log(`${head}:`);
|
|
@@ -10245,7 +10316,7 @@ async function evalCommand(args, root) {
|
|
|
10245
10316
|
plan: planRuns(tests, harnessNames, models, runs),
|
|
10246
10317
|
showHarness,
|
|
10247
10318
|
vars,
|
|
10248
|
-
|
|
10319
|
+
agentDirs: projectAgentDir(repo?.mainCheckout ?? root),
|
|
10249
10320
|
json: opts.json,
|
|
10250
10321
|
say
|
|
10251
10322
|
});
|
|
@@ -10271,11 +10342,13 @@ async function evalCommand(args, root) {
|
|
|
10271
10342
|
} catch (e) {
|
|
10272
10343
|
return fatal(EXIT.usage, e.message, "config");
|
|
10273
10344
|
}
|
|
10274
|
-
const
|
|
10345
|
+
const project = repo?.mainCheckout ?? root;
|
|
10346
|
+
const firegoatDir = ensureFiregoatDir(project);
|
|
10347
|
+
const agentDirs = ensureProjectAgentDir(project);
|
|
10275
10348
|
const configDirs = new Map;
|
|
10276
10349
|
for (const h of harnesses) {
|
|
10277
|
-
const configDir =
|
|
10278
|
-
|
|
10350
|
+
const configDir = join10(agentDirs, h.configDirName);
|
|
10351
|
+
mkdirSync5(configDir, { recursive: true });
|
|
10279
10352
|
configDirs.set(h, configDir);
|
|
10280
10353
|
}
|
|
10281
10354
|
ui.auth(harnesses.length === 1 ? harnesses[0].authLabel() : harnesses.map((h) => `${h.name}: ${h.authLabel()}`).join("; "));
|
|
@@ -10308,10 +10381,12 @@ async function evalCommand(args, root) {
|
|
|
10308
10381
|
return fatal(EXIT.usage, e.message, "config");
|
|
10309
10382
|
throw e;
|
|
10310
10383
|
}
|
|
10311
|
-
const runsRoot =
|
|
10312
|
-
const runDir =
|
|
10313
|
-
|
|
10314
|
-
|
|
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"));
|
|
10315
10390
|
const stepEnv = {
|
|
10316
10391
|
...hostEnv(),
|
|
10317
10392
|
...vars,
|
|
@@ -10336,7 +10411,7 @@ async function evalCommand(args, root) {
|
|
|
10336
10411
|
...stepEnv,
|
|
10337
10412
|
...renderEnv(Object.fromEntries(Object.entries(config.env).filter(([, v]) => isEvalLevel(v))), vars, { "run.id": runId, FIREGOAT_OUT: out })
|
|
10338
10413
|
},
|
|
10339
|
-
logPath:
|
|
10414
|
+
logPath: join10(runDir, "build.txt")
|
|
10340
10415
|
});
|
|
10341
10416
|
const secs = Math.round((Date.now() - buildStarted) / 1000);
|
|
10342
10417
|
ui.build(error ? "fail" : "ok", secs);
|
|
@@ -10363,6 +10438,8 @@ async function evalCommand(args, root) {
|
|
|
10363
10438
|
shownHarness: row.harness,
|
|
10364
10439
|
runId,
|
|
10365
10440
|
runDir,
|
|
10441
|
+
agentDirs,
|
|
10442
|
+
repo: root,
|
|
10366
10443
|
harness,
|
|
10367
10444
|
bin: bins.get(harness),
|
|
10368
10445
|
configDir: configDirs.get(harness),
|
|
@@ -10432,9 +10509,9 @@ async function evalCommand(args, root) {
|
|
|
10432
10509
|
status,
|
|
10433
10510
|
exitCode
|
|
10434
10511
|
};
|
|
10435
|
-
writeFileSync4(
|
|
10512
|
+
writeFileSync4(join10(runDir, "summary.json"), `${JSON.stringify(doc, null, 2)}
|
|
10436
10513
|
`);
|
|
10437
|
-
appendFileSync(
|
|
10514
|
+
appendFileSync(join10(runsRoot, "index.jsonl"), `${JSON.stringify({
|
|
10438
10515
|
runId,
|
|
10439
10516
|
startedAt: new Date(evalStarted).toISOString(),
|
|
10440
10517
|
dir: root,
|
|
@@ -10747,7 +10824,7 @@ firegoat ${package_default.version}` : `firegoat ${package_default.version}`,
|
|
|
10747
10824
|
// src/init.ts
|
|
10748
10825
|
import { spawn as spawn3, spawnSync as spawnSync4 } from "node:child_process";
|
|
10749
10826
|
import { existsSync as existsSync6, writeFileSync as writeFileSync5 } from "node:fs";
|
|
10750
|
-
import { basename as basename4, join as
|
|
10827
|
+
import { basename as basename4, join as join11 } from "node:path";
|
|
10751
10828
|
|
|
10752
10829
|
// src/login.ts
|
|
10753
10830
|
import { spawn as spawn2 } from "node:child_process";
|
|
@@ -11046,12 +11123,12 @@ function skillsDir(scope) {
|
|
|
11046
11123
|
if (scope === "local")
|
|
11047
11124
|
return ".claude/skills/";
|
|
11048
11125
|
const config = process.env.CLAUDE_CONFIG_DIR?.trim();
|
|
11049
|
-
return config ? `${
|
|
11126
|
+
return config ? `${join11(config, "skills")}/` : "~/.claude/skills/";
|
|
11050
11127
|
}
|
|
11051
11128
|
function template(product) {
|
|
11052
11129
|
return `# firegoat.yaml: evals for how well coding agents use ${product}.
|
|
11053
11130
|
# Each test runs your agent (claude by default) in an empty folder
|
|
11054
|
-
# (
|
|
11131
|
+
# (~/.firegoat/runs/...) with \`prompt\` as the first message, then runs \`checks\`
|
|
11055
11132
|
# there. Reference: npx firegoat init --help
|
|
11056
11133
|
|
|
11057
11134
|
# How many times to run each test.
|
|
@@ -11308,7 +11385,7 @@ ${INIT_HELP}`);
|
|
|
11308
11385
|
console.log(rule);
|
|
11309
11386
|
}
|
|
11310
11387
|
ensureFiregoatDir(cwd);
|
|
11311
|
-
const configPath =
|
|
11388
|
+
const configPath = join11(cwd, CONFIG_FILE);
|
|
11312
11389
|
const keep = existsSync6(configPath) && !force;
|
|
11313
11390
|
if (!keep)
|
|
11314
11391
|
writeFileSync5(configPath, template(basename4(cwd)));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "firegoat",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.26.0",
|
|
4
4
|
"repository": {
|
|
5
5
|
"type": "git",
|
|
6
6
|
"url": "git+https://github.com/firegoat-ai/firegoat-cli.git"
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
},
|
|
30
30
|
"scripts": {
|
|
31
31
|
"build": "bun build src/cli.ts --target node --outfile dist/cli.js",
|
|
32
|
-
"test": "bun test",
|
|
32
|
+
"test": "bun test --timeout 30000",
|
|
33
33
|
"typecheck": "bun check",
|
|
34
34
|
"lint": "biome check .",
|
|
35
35
|
"format": "biome check --write .",
|