firegoat 0.20.0 → 0.22.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 +29 -11
- package/dist/cli.js +210 -76
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -12,7 +12,7 @@ npx firegoat@latest init # Creates firegoat.yaml and .firegoat/, installs th
|
|
|
12
12
|
npx firegoat@latest eval # Runs your claude on each test, in .firegoat/runs/<runId>/<test>/<n>/work
|
|
13
13
|
```
|
|
14
14
|
|
|
15
|
-
That's it: two commands. Use `npx firegoat@latest`: plain `npx firegoat` can keep running an old cached version. `eval` prints its version first (`firegoat
|
|
15
|
+
That's it: two commands. `npx firegoat@latest eval --dry-run` checks `firegoat.yaml` and lists the runs it would start (`<test>#<n> [model]`, with step counts) without building, running claude or uploading; config errors exit 3 as in a real eval. Use `npx firegoat@latest`: plain `npx firegoat` can keep running an old cached version. `eval` prints its version first (`firegoat X.Y.Z`), and `summary.json` records it as `firegoatVersion`. `claude` must be installed and logged in (run `claude` once).
|
|
16
16
|
|
|
17
17
|
`init` installs the firegoat-onboarding skill globally, into `~/.claude/skills/` (in a terminal it asks: global, this repo, or none). `--local-skills` puts it in this repo's `.claude/skills/` instead (commit it so teammates and CI get it); `--no-skills` skips it.
|
|
18
18
|
|
|
@@ -160,16 +160,24 @@ tests:
|
|
|
160
160
|
|
|
161
161
|
### Testing an MCP server
|
|
162
162
|
|
|
163
|
+
Commit the server's config as `fixtures/mcp.json`:
|
|
164
|
+
|
|
165
|
+
```json
|
|
166
|
+
{"mcpServers": {"acme": {"command": "./node_modules/.bin/acme-mcp",
|
|
167
|
+
"env": {"HOME": "${FIREGOAT_HOME}"}}}}
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
Copy it in `build:` and pass it with `claude_args:`:
|
|
171
|
+
|
|
163
172
|
```yaml
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
claude_args: [--mcp-config, .mcp.json]
|
|
173
|
+
build:
|
|
174
|
+
- npm pack --pack-destination "$FIREGOAT_OUT"
|
|
175
|
+
- cp -R fixtures/. "$FIREGOAT_OUT"
|
|
176
|
+
install: npm init -y >/dev/null && npm install "$FIREGOAT_OUT"/*.tgz
|
|
177
|
+
claude_args: [--mcp-config, $FIREGOAT_OUT/mcp.json]
|
|
170
178
|
```
|
|
171
179
|
|
|
172
|
-
|
|
180
|
+
claude gets `FIREGOAT_HOME`, `FIREGOAT_WORKDIR` and `FIREGOAT_OUT` in its own env, set per run, and expands `${VAR}` in an MCP config's `command`, `args`, `env`, `url` and `headers` from its env (an unset var stays the literal text). Relative paths in the config are from the run's folder, claude's cwd: `./node_modules/.bin/acme-mcp` is the build under test, installed by `install:`. MCP servers that claude starts inherit claude's own env, which keeps your real `HOME` (claude needs it for its login): set `"env": {"HOME": "${FIREGOAT_HOME}"}` on the server, as above, or it writes to your real home. Per-run state the server keeps elsewhere goes the same way, e.g. `"ACME_DB": "${FIREGOAT_WORKDIR}/acme.db"`. `--strict-mcp-config` is always on, so only the configs you pass load. To check that the agent used a tool, grep the transcript: `grep -Eq '^\[[0-9]+\] mcp__acme__search' "$FIREGOAT_TRANSCRIPT"`.
|
|
173
181
|
|
|
174
182
|
With `--chrome`, claude points the Claude in Chrome extension's native host (`~/Library/Application Support/<browser>/NativeMessagingHosts/com.anthropic.claude_code_browser_extension.json`) at `.firegoat/.claude`. If you delete `.firegoat/`, run a normal `claude --chrome` once to point it back.
|
|
175
183
|
|
|
@@ -211,12 +219,22 @@ Limits:
|
|
|
211
219
|
## What eval does
|
|
212
220
|
|
|
213
221
|
- claude runs on its own login (or `ANTHROPIC_API_KEY`, if set) with `CLAUDE_CONFIG_DIR=.firegoat/.claude` and `--setting-sources "" --strict-mcp-config --disable-slash-commands`: none of your settings, CLAUDE.md, plugins, skills or MCP servers (add flags with `claude_args:`, see above). claude saves its session there, in its own format.
|
|
214
|
-
- No sandbox: the agent's folder is inside `.firegoat/runs/`, and it can reach the rest of your machine. `.firegoat/` holds local run output and ignores itself in git. Keep test fixtures (scripts, sample data)
|
|
222
|
+
- No sandbox: the agent's folder is inside `.firegoat/runs/`, and it can reach the rest of your machine. `.firegoat/` holds local run output and ignores itself in git. Keep test fixtures (scripts, sample data) in `fixtures/` next to `firegoat.yaml`, committed.
|
|
215
223
|
- In a linked git worktree (`git worktree add`), `.firegoat/runs/` and `.firegoat/.claude/` are in the main checkout, so runs (and their session files) survive `git worktree remove`; `firegoat.yaml`, fixtures and `build:` stay in the worktree. Each `summary.json` records `dir` (where eval ran) and `git: {branch, commit, dirty}`, and `.firegoat/runs/index.jsonl` gets one line per eval (`runId, startedAt, dir, branch, commit, passed, failed, notGraded, cost, uploaded, uploadFailed`) to tell runs from different branches apart.
|
|
216
|
-
- The agent gets your environment plus `env:`, never `vars:`. Vars that npm/npx set for firegoat itself (`npm_config_*`, `npm_lifecycle_*`, `npm_package_*`, `npm_execpath`, `npm_node_execpath`, `npm_command`, `INIT_CWD`) are removed from every env firegoat starts (agent, build, install, checks): `npx -y firegoat` sets `npm_config_yes=true`, which would make the agent's `npm init` / `npx create-*` skip their prompts. Set one under `env:` to keep it. Every `FIREGOAT_*` var firegoat was started with (`FIREGOAT_API_KEY` above all) is removed too, and so are the vars a parent Claude Code session sets (when firegoat runs from claude, or its agent runs firegoat), so the claude under test doesn't act as a nested 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`. Login and provider vars stay: `ANTHROPIC_*`, `CLAUDE_CODE_OAUTH_TOKEN`, `CLAUDE_CODE_OAUTH_REFRESH_TOKEN`, `CLAUDE_CODE_USE_*` (Bedrock, Vertex, ...), `CLAUDE_CODE_SKIP_*_AUTH`, `CLAUDE_CODE_CLIENT_CERT`, `CLAUDE_CODE_CLIENT_KEY`, `CLAUDE_CODE_CLIENT_KEY_PASSPHRASE`.
|
|
224
|
+
- The agent gets your environment plus `env:`, never `vars:`. Vars that npm/npx set for firegoat itself (`npm_config_*`, `npm_lifecycle_*`, `npm_package_*`, `npm_execpath`, `npm_node_execpath`, `npm_command`, `INIT_CWD`) are removed from every env firegoat starts (agent, build, install, checks): `npx -y firegoat` sets `npm_config_yes=true`, which would make the agent's `npm init` / `npx create-*` skip their prompts. Set one under `env:` to keep it. Every `FIREGOAT_*` var firegoat was started with (`FIREGOAT_API_KEY` above all) is removed too (claude then gets the run's own `FIREGOAT_OUT`, `FIREGOAT_WORKDIR` and `FIREGOAT_HOME`, for MCP configs; the agent's commands see them too), and so are the vars a parent Claude Code session sets (when firegoat runs from claude, or its agent runs firegoat), so the claude under test doesn't act as a nested 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`. Login and provider vars stay: `ANTHROPIC_*`, `CLAUDE_CODE_OAUTH_TOKEN`, `CLAUDE_CODE_OAUTH_REFRESH_TOKEN`, `CLAUDE_CODE_USE_*` (Bedrock, Vertex, ...), `CLAUDE_CODE_SKIP_*_AUTH`, `CLAUDE_CODE_CLIENT_CERT`, `CLAUDE_CODE_CLIENT_KEY`, `CLAUDE_CODE_CLIENT_KEY_PASSPHRASE`.
|
|
217
225
|
- Runs (tests × models × `runs`) go 4 at a time, each in its own folder with its own session and HOME; `--parallel N` changes that, `--parallel 1` runs them one after another. `summary.json` lists them in `firegoat.yaml` order.
|
|
218
226
|
- Script checks run in the agent's folder afterwards and set the exit code. `end_goal` is not graded by firegoat: it's for you, or your coding agent, to judge from `run.json`, the session and the folder.
|
|
219
|
-
- Output per run, in `.firegoat/runs/<runId>/<test>/<n>/`: `transcript.txt` (each tool call with its command and short output, the final answer, files made; checks get it as `$FIREGOAT_TRANSCRIPT`;
|
|
227
|
+
- Output per run, in `.firegoat/runs/<runId>/<test>/<n>/`: `transcript.txt` (each tool call with its command and short output, the final answer, files made; checks get it as `$FIREGOAT_TRANSCRIPT`; format below), `run.json` (status, prompt, end_goal, check results, paths), `work/`, `home/` and `home.env` (the run's HOME), logs. Read the transcript to judge `end_goal`; claude's full session file is ~50x larger. `summary.json` covers the whole eval; `--json` prints it.
|
|
228
|
+
|
|
229
|
+
### The transcript
|
|
230
|
+
|
|
231
|
+
`transcript.txt` is greppable line by line; `n` counts the agent's tool calls:
|
|
232
|
+
|
|
233
|
+
- `[n] $ <command>`: a shell call, on every line of the command (heredocs and multi-line scripts too), never cut. So `grep -E '^\[[0-9]+\] \$ .*npm install'` matches only commands.
|
|
234
|
+
- `[n] <Tool> <target>`: a tool (MCP ones too) whose input has a `file_path`, `path`, `url`, `pattern` or `query`: just that value, e.g. `[2] Write src/app.ts`. The written body is not in the transcript: check the file in the workdir.
|
|
235
|
+
- `[n] <Tool> <json>`: any other tool, MCP tools included, with its whole input as one line of JSON, cut at 4000 chars: `[3] mcp__acme__push_html {"canvas":"demo","html":"<p>Hi</p>"}`. So `grep -Eq '^\[[0-9]+\] mcp__acme__push_html .*<h1>' "$FIREGOAT_TRANSCRIPT"` checks what the agent pushed.
|
|
236
|
+
- ` → ok` or ` → error`, then the tool's output indented 4 spaces, cut at 15 lines or 1500 chars (`… (N more chars)`).
|
|
237
|
+
- `agent: ` on every line of the agent's text (cut at 30 lines or 1500 chars) and of its final answer (uncut), then the files in the workdir (without `node_modules`, `.git`, `.venv`, `__pycache__`).
|
|
220
238
|
|
|
221
239
|
`firegoat.yaml` keys: `runs`, `model`, `build`, `install`, `cleanup`, `vars`, `env`, `claude_args`, and `tests` (`name`, `prompt`, `end_goal`, `checks`, `timeout`, `env`, `install`, `cleanup`, `claude_args`). Unknown keys are errors. Full reference: `npx firegoat init --help`.
|
|
222
240
|
|
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.22.0",
|
|
6951
6951
|
repository: {
|
|
6952
6952
|
type: "git",
|
|
6953
6953
|
url: "git+https://github.com/firegoat-ai/firegoat-cli.git"
|
|
@@ -7448,8 +7448,15 @@ async function runClaude(opts) {
|
|
|
7448
7448
|
reason: `claude did not start a session (exit ${exited.code ?? "by signal"})${said ? `: ${said}` : ", see stderr.txt"}`
|
|
7449
7449
|
};
|
|
7450
7450
|
}
|
|
7451
|
-
const sessionId = String(init.session_id);
|
|
7452
7451
|
const claudeModel = typeof init.model === "string" ? init.model : null;
|
|
7452
|
+
if (typeof init.session_id !== "string" || init.session_id === "")
|
|
7453
|
+
return {
|
|
7454
|
+
events,
|
|
7455
|
+
claudeModel,
|
|
7456
|
+
ok: false,
|
|
7457
|
+
reason: "claude's init line has no session_id"
|
|
7458
|
+
};
|
|
7459
|
+
const sessionId = init.session_id;
|
|
7453
7460
|
const result = events.findLast((e) => e?.type === "result");
|
|
7454
7461
|
if (exited.timedOut)
|
|
7455
7462
|
return {
|
|
@@ -7646,7 +7653,7 @@ async function send(opts) {
|
|
|
7646
7653
|
function replyError(step, r) {
|
|
7647
7654
|
const j = r.json ?? {};
|
|
7648
7655
|
const error = typeof j.error === "string" ? j.error : "no error message";
|
|
7649
|
-
const more = j.error_description ?? j.details
|
|
7656
|
+
const more = j.error_description ?? j.details;
|
|
7650
7657
|
const details = more === undefined ? "" : ` ${typeof more === "string" ? more : JSON.stringify(more)}`;
|
|
7651
7658
|
return new ApiError(printable(`${step} ${r.status}: ${error}${details}`), r.status);
|
|
7652
7659
|
}
|
|
@@ -7816,7 +7823,12 @@ var GITIGNORE = `*
|
|
|
7816
7823
|
function ensureFiregoatDir(root) {
|
|
7817
7824
|
const dir = join2(root, ".firegoat");
|
|
7818
7825
|
mkdirSync2(dir, { recursive: true });
|
|
7819
|
-
|
|
7826
|
+
try {
|
|
7827
|
+
writeFileSync(join2(dir, ".gitignore"), GITIGNORE, { flag: "wx" });
|
|
7828
|
+
} catch (e) {
|
|
7829
|
+
if (e.code !== "EEXIST")
|
|
7830
|
+
throw e;
|
|
7831
|
+
}
|
|
7820
7832
|
return dir;
|
|
7821
7833
|
}
|
|
7822
7834
|
|
|
@@ -7918,8 +7930,8 @@ firegoat-onboarding skill globally, into ~/.claude/skills/ (npx skills add
|
|
|
7918
7930
|
firegoat-ai/skills -g; in a terminal it asks: global, this repo, or none), offers
|
|
7919
7931
|
firegoat login (in a terminal only; otherwise prints how), then prints a prompt to
|
|
7920
7932
|
paste into your coding agent. .firegoat/ holds local run output and ignores itself
|
|
7921
|
-
in git. Keep test fixtures (helper scripts, sample data)
|
|
7922
|
-
|
|
7933
|
+
in git. Keep test fixtures (helper scripts, sample data) in fixtures/ next to
|
|
7934
|
+
firegoat.yaml, committed.
|
|
7923
7935
|
|
|
7924
7936
|
--force Replace an existing firegoat.yaml
|
|
7925
7937
|
--local-skills Install the skill into this repo's .claude/skills/ instead
|
|
@@ -7998,29 +8010,31 @@ firegoat.yaml reference:
|
|
|
7998
8010
|
vars, plus $FIREGOAT_OUT and $FIREGOAT_WORKDIR; install, checks and cleanup also
|
|
7999
8011
|
get $FIREGOAT_HOME, the run's own home, as HOME (build keeps your real HOME).
|
|
8000
8012
|
Checks also get $FIREGOAT_TRANSCRIPT, a short plain-text
|
|
8001
|
-
transcript, greppable line by line:
|
|
8002
|
-
|
|
8003
|
-
|
|
8004
|
-
final answer. So '^\\[[0-9]+\\] \\$ .*foo' matches only commands. Checks also
|
|
8005
|
-
get $FIREGOAT_SESSION (claude's full session file, JSON lines).
|
|
8013
|
+
transcript, greppable line by line (format: eval --help, Transcript). So
|
|
8014
|
+
'^\\[[0-9]+\\] \\$ .*foo' matches only commands. Checks also get
|
|
8015
|
+
$FIREGOAT_SESSION (claude's full session file, JSON lines).
|
|
8006
8016
|
|
|
8007
8017
|
The agent never gets vars, only env:. The vars npm/npx set for firegoat itself
|
|
8008
8018
|
(npm_config_*, npm_lifecycle_*, npm_package_*, npm_execpath, npm_node_execpath,
|
|
8009
8019
|
npm_command, INIT_CWD; e.g. npm_config_yes=true from npx -y) are removed from
|
|
8010
8020
|
every env firegoat starts, unless env: sets them. So is every FIREGOAT_* var
|
|
8011
|
-
(FIREGOAT_API_KEY above all) firegoat itself was started with.
|
|
8012
|
-
|
|
8013
|
-
|
|
8014
|
-
|
|
8015
|
-
|
|
8016
|
-
|
|
8017
|
-
|
|
8018
|
-
|
|
8019
|
-
|
|
8020
|
-
|
|
8021
|
-
|
|
8022
|
-
|
|
8023
|
-
|
|
8021
|
+
(FIREGOAT_API_KEY above all) firegoat itself was started with. claude itself
|
|
8022
|
+
gets $FIREGOAT_OUT, $FIREGOAT_WORKDIR and $FIREGOAT_HOME (for MCP configs, below),
|
|
8023
|
+
so the agent's commands see them too.
|
|
8024
|
+
|
|
8025
|
+
Testing an MCP server: commit its config as fixtures/mcp.json, copy it in build:
|
|
8026
|
+
and pass it with claude_args. Only the configs you pass load: --strict-mcp-config
|
|
8027
|
+
is always on. An MCP server inherits claude's env, which keeps your real HOME
|
|
8028
|
+
(claude's login is there), so set the server's HOME to the run's home, or it
|
|
8029
|
+
writes to yours. claude gets $FIREGOAT_HOME, $FIREGOAT_WORKDIR and $FIREGOAT_OUT
|
|
8030
|
+
in its env and expands \${VAR} in the config (an unset one stays literal);
|
|
8031
|
+
relative paths in it are from the run's folder, claude's cwd:
|
|
8032
|
+
fixtures/mcp.json:
|
|
8033
|
+
{"mcpServers": {"acme": {"command": "./node_modules/.bin/acme-mcp",
|
|
8034
|
+
"env": {"HOME": "\${FIREGOAT_HOME}"}}}}
|
|
8035
|
+
firegoat.yaml:
|
|
8036
|
+
build: cp -R fixtures/. "$FIREGOAT_OUT"
|
|
8037
|
+
claude_args: [--mcp-config, $FIREGOAT_OUT/mcp.json]
|
|
8024
8038
|
|
|
8025
8039
|
Example:
|
|
8026
8040
|
build: npm pack --pack-destination "$FIREGOAT_OUT"
|
|
@@ -8082,7 +8096,8 @@ page.
|
|
|
8082
8096
|
Exit codes: 0 logged out (or was not logged in), 3 the file or ~/.firegoat is a
|
|
8083
8097
|
symlink, not a regular file/directory, or someone else's (left alone).`;
|
|
8084
8098
|
var EVAL_HELP = `Usage: firegoat eval [test..] [--runs N] [--model NAME].. [--parallel N]
|
|
8085
|
-
[--var KEY=value].. [--json] [--no-upload] [--
|
|
8099
|
+
[--var KEY=value].. [--json] [--no-upload] [--dry-run]
|
|
8100
|
+
[--claude-bin PATH]
|
|
8086
8101
|
|
|
8087
8102
|
Runs each test in firegoat.yaml (in the current folder): your claude gets the prompt
|
|
8088
8103
|
in a fresh, empty folder, .firegoat/runs/<runId>/<test>/<n>/work, with build:'s
|
|
@@ -8136,8 +8151,15 @@ fixtures and build: stay in the current folder.
|
|
|
8136
8151
|
--json The summary as JSON on stdout (or {error, exitCode} if eval
|
|
8137
8152
|
stops early); progress goes to stderr
|
|
8138
8153
|
--no-upload Keep runs local even when logged in to firegoat.ai
|
|
8139
|
-
--
|
|
8140
|
-
|
|
8154
|
+
--dry-run Check firegoat.yaml as eval would (test names, --var,
|
|
8155
|
+
--model, missing vars), print each planned run
|
|
8156
|
+
("<test>#<n> [model]" with its install, checks and cleanup
|
|
8157
|
+
counts and rendered claude_args, paths under
|
|
8158
|
+
runs/<runId>/) and exit 0. Builds nothing, starts no
|
|
8159
|
+
claude, uploads nothing, writes no files. Config errors
|
|
8160
|
+
exit 3. With --json: {dryRun, build, runs: [{test, n,
|
|
8161
|
+
model, claudeArgs, install, checks, cleanup}]}
|
|
8162
|
+
--claude-bin PATH Path to claude (default: claude on PATH)
|
|
8141
8163
|
|
|
8142
8164
|
Upload: after firegoat login (or with FIREGOAT_API_KEY), each run's claude session,
|
|
8143
8165
|
checks.txt and stderr.txt go to firegoat.ai as a private trace, and its URL is
|
|
@@ -8154,8 +8176,8 @@ Output: .firegoat/runs/<runId>/
|
|
|
8154
8176
|
git {branch, commit, dirty} (no git outside a repo)
|
|
8155
8177
|
and upload {origin, email} (null when not uploading)
|
|
8156
8178
|
../index.jsonl One line per eval: runId, startedAt, dir, branch,
|
|
8157
|
-
commit, passed, failed, notGraded, cost
|
|
8158
|
-
uploadFailed
|
|
8179
|
+
commit, passed, failed, notGraded, cost (null when
|
|
8180
|
+
any run's cost is unknown), uploaded, uploadFailed
|
|
8159
8181
|
<test>/<n>/run.json One run: status and reason (from the checks), model
|
|
8160
8182
|
(asked for, or null) and claudeModel (what claude's
|
|
8161
8183
|
init line reported), claudeArgs (claude_args
|
|
@@ -8166,14 +8188,25 @@ Output: .firegoat/runs/<runId>/
|
|
|
8166
8188
|
checksLog, cleanupLog; startedAt; upload ({status: "uploaded",
|
|
8167
8189
|
url} | {status: "failed", error} | {status:
|
|
8168
8190
|
"skipped", reason}, null when not uploading)
|
|
8169
|
-
<test>/<n>/transcript.txt What the agent did
|
|
8170
|
-
("[n] $ " on every line of a shell call), short output
|
|
8171
|
-
(indented), its text and final answer ("agent: " on
|
|
8172
|
-
every line), files it made
|
|
8191
|
+
<test>/<n>/transcript.txt What the agent did, below
|
|
8173
8192
|
<test>/<n>/home/, home.env The run's HOME, and the file claude sources
|
|
8174
8193
|
before the agent's Bash commands
|
|
8175
8194
|
build.txt, <test>/<n>/install.txt, checks.txt, cleanup.txt Logs
|
|
8176
8195
|
|
|
8196
|
+
Transcript ($FIREGOAT_TRANSCRIPT), greppable line by line; n counts tool calls:
|
|
8197
|
+
[n] $ <command> A shell call, on every line of the command, uncut
|
|
8198
|
+
[n] <Tool> <target> A tool (MCP too) with a file_path, path, url, pattern
|
|
8199
|
+
or query input: that value, e.g. "[2] Write src/a.ts"
|
|
8200
|
+
[n] <Tool> <json> Any other tool, MCP ones included: its input as one
|
|
8201
|
+
line of JSON, e.g. [3] mcp__<server>__<tool> {"q":"x"};
|
|
8202
|
+
cut at 4000 chars
|
|
8203
|
+
→ ok | → error Then the tool's output, indented 4 spaces, cut at 15
|
|
8204
|
+
lines or 1500 chars ("… (N more chars)")
|
|
8205
|
+
agent: <text> Every line of the agent's text (cut at 30 lines or
|
|
8206
|
+
1500 chars) and of its final answer (uncut)
|
|
8207
|
+
Then the files in the workdir (not node_modules, .git, .venv, __pycache__).
|
|
8208
|
+
A written file's body is not in the transcript: check the file in the workdir.
|
|
8209
|
+
|
|
8177
8210
|
Grading: firegoat runs only the script checks; they set the exit code. end_goal is
|
|
8178
8211
|
yours to judge: read transcript.txt first (a few KB); the session file has everything
|
|
8179
8212
|
(often 50x larger).
|
|
@@ -8184,6 +8217,7 @@ Examples:
|
|
|
8184
8217
|
npx firegoat eval --model claude-haiku-5-5 --model claude-sonnet-5-5
|
|
8185
8218
|
Each test on both models
|
|
8186
8219
|
npx firegoat eval --var SHA=$(git rev-parse HEAD)
|
|
8220
|
+
npx firegoat eval --dry-run Validate firegoat.yaml, list the runs
|
|
8187
8221
|
|
|
8188
8222
|
A run can take minutes (default timeout 600s per test).
|
|
8189
8223
|
|
|
@@ -8280,6 +8314,7 @@ import { join as join4, relative } from "node:path";
|
|
|
8280
8314
|
var OUTPUT_LINES = 15;
|
|
8281
8315
|
var OUTPUT_CHARS = 1500;
|
|
8282
8316
|
var TEXT_CHARS = 1500;
|
|
8317
|
+
var INPUT_CHARS = 4000;
|
|
8283
8318
|
var SKIP_DIRS = new Set(["node_modules", ".git", ".venv", "__pycache__"]);
|
|
8284
8319
|
function truncate(text, maxLines, maxChars) {
|
|
8285
8320
|
let out = text.length > maxChars ? text.slice(0, maxChars) : text;
|
|
@@ -8304,7 +8339,14 @@ function describeCall(step, name, input) {
|
|
|
8304
8339
|
for (const key of ["file_path", "path", "url", "pattern", "query"])
|
|
8305
8340
|
if (typeof input[key] === "string")
|
|
8306
8341
|
return `[${step}] ${name} ${input[key]}`;
|
|
8307
|
-
return `[${step}] ${name} ${truncate(JSON.stringify(input), 1,
|
|
8342
|
+
return `[${step}] ${name} ${truncate(JSON.stringify(input), 1, INPUT_CHARS)}`;
|
|
8343
|
+
}
|
|
8344
|
+
function describeToolUse(step, b) {
|
|
8345
|
+
if (typeof b.name !== "string" || b.name === "")
|
|
8346
|
+
return `[${step}] (tool call without a name)`;
|
|
8347
|
+
if (typeof b.input !== "object" || b.input === null)
|
|
8348
|
+
return `[${step}] ${b.name} (no input)`;
|
|
8349
|
+
return describeCall(step, b.name, b.input);
|
|
8308
8350
|
}
|
|
8309
8351
|
function resultText(content) {
|
|
8310
8352
|
if (typeof content === "string")
|
|
@@ -8341,7 +8383,7 @@ function writeTranscript(opts) {
|
|
|
8341
8383
|
if (e?.type === "assistant")
|
|
8342
8384
|
for (const b of content) {
|
|
8343
8385
|
if (b.type === "tool_use")
|
|
8344
|
-
out.push(
|
|
8386
|
+
out.push(describeToolUse(++step, b));
|
|
8345
8387
|
else if (b.type === "text" && typeof b.text === "string") {
|
|
8346
8388
|
lastText = out.length;
|
|
8347
8389
|
out.push(prose(truncate(b.text, 30, TEXT_CHARS)));
|
|
@@ -8418,23 +8460,38 @@ function ramp(r) {
|
|
|
8418
8460
|
const end = r.state === "pass" ? "▲" : r.state === "fail" ? "╳" : r.state === "error" ? "╌" : r.state === "running" ? "●" : "";
|
|
8419
8461
|
return (blocks + end).padEnd(8);
|
|
8420
8462
|
}
|
|
8421
|
-
function
|
|
8463
|
+
function usageParts(r) {
|
|
8422
8464
|
const parts = [];
|
|
8423
|
-
if (
|
|
8465
|
+
if (typeof r.cost === "number")
|
|
8424
8466
|
parts.push(`$${r.cost.toFixed(2)}`);
|
|
8425
|
-
|
|
8467
|
+
if (typeof r.reportedTurns === "number")
|
|
8468
|
+
parts.push(`${r.reportedTurns} turns`);
|
|
8426
8469
|
if (r.seconds !== undefined)
|
|
8427
8470
|
parts.push(`${r.seconds}s`);
|
|
8428
|
-
return parts
|
|
8471
|
+
return parts;
|
|
8472
|
+
}
|
|
8473
|
+
function usage(r) {
|
|
8474
|
+
return usageParts(r).join(" ");
|
|
8429
8475
|
}
|
|
8430
8476
|
function checksPassed(r) {
|
|
8431
8477
|
const all = r.checks ?? [];
|
|
8432
8478
|
return `${all.filter((c) => c.code === 0).length}/${all.length} checks`;
|
|
8433
8479
|
}
|
|
8434
|
-
function
|
|
8435
|
-
const
|
|
8436
|
-
const
|
|
8437
|
-
|
|
8480
|
+
function totalCost(rows) {
|
|
8481
|
+
const done = rows.filter((r) => r.state !== "queued" && r.state !== "running");
|
|
8482
|
+
const unknown = done.filter((r) => typeof r.cost !== "number").length;
|
|
8483
|
+
if (unknown > 0)
|
|
8484
|
+
return `cost unknown for ${unknown} run${unknown === 1 ? "" : "s"}`;
|
|
8485
|
+
return `$${done.reduce((s, r) => s + r.cost, 0).toFixed(2)}`;
|
|
8486
|
+
}
|
|
8487
|
+
function totalTurns(rows) {
|
|
8488
|
+
let sum = 0;
|
|
8489
|
+
for (const r of rows) {
|
|
8490
|
+
if (typeof r.reportedTurns !== "number")
|
|
8491
|
+
return null;
|
|
8492
|
+
sum += r.reportedTurns;
|
|
8493
|
+
}
|
|
8494
|
+
return sum;
|
|
8438
8495
|
}
|
|
8439
8496
|
function duration(seconds) {
|
|
8440
8497
|
return seconds >= 60 ? `${Math.floor(seconds / 60)}m ${seconds % 60}s` : `${seconds}s`;
|
|
@@ -8476,9 +8533,9 @@ function plainUI(write) {
|
|
|
8476
8533
|
continue;
|
|
8477
8534
|
printed.add(r);
|
|
8478
8535
|
const label = r.state === "error" ? "ERROR" : r.state.toUpperCase();
|
|
8479
|
-
const u =
|
|
8536
|
+
const u = usageParts(r).join(", ");
|
|
8480
8537
|
const model = r.model === null ? "" : ` [${r.model}]`;
|
|
8481
|
-
write(`${label} ${r.test}#${r.n}${model} ${r.reason} (${u})`);
|
|
8538
|
+
write(`${label} ${r.test}#${r.n}${model} ${r.reason}${u ? ` (${u})` : ""}`);
|
|
8482
8539
|
}
|
|
8483
8540
|
for (const r of rows) {
|
|
8484
8541
|
if (uploadPrinted.has(r) || !r.upload || r.upload === "uploading")
|
|
@@ -8489,10 +8546,9 @@ function plainUI(write) {
|
|
|
8489
8546
|
},
|
|
8490
8547
|
done: ({ runsDir, rows: all }) => {
|
|
8491
8548
|
const c = counts(all);
|
|
8492
|
-
const t = total(all);
|
|
8493
8549
|
const tr = tier(all);
|
|
8494
8550
|
write("");
|
|
8495
|
-
write(`${c.pass} passed, ${c.fail} failed, ${c.error} not graded (${all.length} runs,
|
|
8551
|
+
write(`${c.pass} passed, ${c.fail} failed, ${c.error} not graded (${all.length} runs, ${totalCost(all)})${tr ? `, tier ${tr}` : ""}`);
|
|
8496
8552
|
write(`Runs: ${runsDir} (end_goal is for you to judge: read <test>/<n>/transcript.txt)`);
|
|
8497
8553
|
}
|
|
8498
8554
|
};
|
|
@@ -8585,12 +8641,11 @@ function ttyUI(out, opts) {
|
|
|
8585
8641
|
out.push(p(C.amber, ` │ ${cut(r.upload.status === "failed" ? `↑ Upload failed: ${r.upload.error}` : `↑ Not uploaded: ${r.upload.reason}`, w - 12)}`));
|
|
8586
8642
|
});
|
|
8587
8643
|
out.push(rule);
|
|
8588
|
-
const t = total(rows);
|
|
8589
8644
|
if (!finished) {
|
|
8590
8645
|
const track = Math.max(10, w - 34);
|
|
8591
8646
|
const at = rows.length ? Math.round(doneCount / rows.length * (track - 1)) : 0;
|
|
8592
8647
|
const progress = `${"·".repeat(at)}▶${"·".repeat(Math.max(0, track - at - 1))}`;
|
|
8593
|
-
out.push(` ${p(C.ember, progress)} ${p(C.gray, `${doneCount}/${rows.length} ·
|
|
8648
|
+
out.push(` ${p(C.ember, progress)} ${p(C.gray, `${doneCount}/${rows.length} · ${totalCost(rows)} so far`)}`);
|
|
8594
8649
|
return out;
|
|
8595
8650
|
}
|
|
8596
8651
|
const tr = tier(rows);
|
|
@@ -8599,7 +8654,8 @@ function ttyUI(out, opts) {
|
|
|
8599
8654
|
const left = goated ? `${rows.length}/${rows.length} runs passed their checks` : tally;
|
|
8600
8655
|
const right = goated ? `GOATED · tier ${tr}` : tr ? `Tier ${tr}` : "";
|
|
8601
8656
|
out.push(` ${goated ? p(C.green, left) : left}${" ".repeat(Math.max(2, w - left.length - right.length - 3))}${goated ? p(C.ember, right) : right}`);
|
|
8602
|
-
|
|
8657
|
+
const turns = totalTurns(rows);
|
|
8658
|
+
out.push(p(C.gray, ` Total ${totalCost(rows)}${turns === null ? "" : ` · ${turns} turns`} · ${duration(finished.seconds)}`));
|
|
8603
8659
|
out.push(p(C.gray, ` Runs ${shortPath(finished.runsDir)}/`));
|
|
8604
8660
|
out.push(` Checks are a score, not the verdict: judge each end_goal from ${p(C.cyan, "<test>/<n>/transcript.txt")}`);
|
|
8605
8661
|
return out;
|
|
@@ -8666,6 +8722,12 @@ function label(v, what) {
|
|
|
8666
8722
|
throw new Error(`${what} is ${v.length} characters, the upload takes at most ${MAX_LABEL}`);
|
|
8667
8723
|
return v;
|
|
8668
8724
|
}
|
|
8725
|
+
function projectName(v) {
|
|
8726
|
+
const name = label(v, "Project name");
|
|
8727
|
+
if (name === null)
|
|
8728
|
+
throw new Error("Project name is empty");
|
|
8729
|
+
return name;
|
|
8730
|
+
}
|
|
8669
8731
|
function runModel(r) {
|
|
8670
8732
|
return {
|
|
8671
8733
|
requested: label(r.model, "Model"),
|
|
@@ -8731,9 +8793,11 @@ async function uploadRun(t, r) {
|
|
|
8731
8793
|
};
|
|
8732
8794
|
bytes[name] = b;
|
|
8733
8795
|
}
|
|
8796
|
+
let project;
|
|
8734
8797
|
let model;
|
|
8735
8798
|
let git;
|
|
8736
8799
|
try {
|
|
8800
|
+
project = projectName(t.project);
|
|
8737
8801
|
model = runModel(r);
|
|
8738
8802
|
git = t.git ? runGit(t.git) : undefined;
|
|
8739
8803
|
} catch (e) {
|
|
@@ -8742,7 +8806,7 @@ async function uploadRun(t, r) {
|
|
|
8742
8806
|
const body = {
|
|
8743
8807
|
sessionId,
|
|
8744
8808
|
runGroup: t.runGroup,
|
|
8745
|
-
project
|
|
8809
|
+
project,
|
|
8746
8810
|
timestamp: r.startedAt,
|
|
8747
8811
|
run: {
|
|
8748
8812
|
test: r.test,
|
|
@@ -8835,8 +8899,9 @@ function parseArgs(args) {
|
|
|
8835
8899
|
parallel: 4,
|
|
8836
8900
|
json: false,
|
|
8837
8901
|
noUpload: false,
|
|
8902
|
+
dryRun: false,
|
|
8838
8903
|
vars: {},
|
|
8839
|
-
claudeBin:
|
|
8904
|
+
claudeBin: "claude"
|
|
8840
8905
|
};
|
|
8841
8906
|
for (let i = 0;i < args.length; i++) {
|
|
8842
8907
|
const a = args[i];
|
|
@@ -8855,6 +8920,8 @@ function parseArgs(args) {
|
|
|
8855
8920
|
opts.json = true;
|
|
8856
8921
|
else if (flag === "--no-upload")
|
|
8857
8922
|
opts.noUpload = true;
|
|
8923
|
+
else if (flag === "--dry-run")
|
|
8924
|
+
opts.dryRun = true;
|
|
8858
8925
|
else if (flag === "--var") {
|
|
8859
8926
|
const kv = value();
|
|
8860
8927
|
const at = kv.indexOf("=");
|
|
@@ -8945,6 +9012,51 @@ async function uploadPreflight(opts, ui, project, runGroup, git) {
|
|
|
8945
9012
|
};
|
|
8946
9013
|
}
|
|
8947
9014
|
}
|
|
9015
|
+
function runFill(runId, n, testName, out, dir) {
|
|
9016
|
+
return {
|
|
9017
|
+
"run.id": runId,
|
|
9018
|
+
"run.n": String(n),
|
|
9019
|
+
"test.name": testName,
|
|
9020
|
+
FIREGOAT_OUT: out,
|
|
9021
|
+
FIREGOAT_WORKDIR: join6(dir, "work"),
|
|
9022
|
+
FIREGOAT_HOME: join6(dir, "home")
|
|
9023
|
+
};
|
|
9024
|
+
}
|
|
9025
|
+
function renderClaudeArgs(test, vars, fill) {
|
|
9026
|
+
return test.claudeArgs.map((a, i) => renderValue(a, vars, fill, `claude_args[${i}]`));
|
|
9027
|
+
}
|
|
9028
|
+
function planRuns(tests, models, runs) {
|
|
9029
|
+
const matrix = models.length > 0 ? models : [null];
|
|
9030
|
+
return tests.flatMap((t) => matrix.flatMap((model, m) => Array.from({ length: runs }, (_, i) => ({
|
|
9031
|
+
test: t.name,
|
|
9032
|
+
n: m * runs + i + 1,
|
|
9033
|
+
model
|
|
9034
|
+
}))));
|
|
9035
|
+
}
|
|
9036
|
+
var DRY_RUN_ID = "<runId>";
|
|
9037
|
+
function dryRun(opts) {
|
|
9038
|
+
const out = join6(opts.runsRoot, DRY_RUN_ID, "out");
|
|
9039
|
+
const runs = opts.plan.map((p) => {
|
|
9040
|
+
const test = opts.tests.find((t) => t.name === p.test);
|
|
9041
|
+
const fill = runFill(DRY_RUN_ID, p.n, test.name, out, join6(opts.runsRoot, DRY_RUN_ID, test.name, String(p.n)));
|
|
9042
|
+
renderEnv(test.env, opts.vars, fill);
|
|
9043
|
+
return {
|
|
9044
|
+
...p,
|
|
9045
|
+
claudeArgs: renderClaudeArgs(test, opts.vars, fill),
|
|
9046
|
+
install: [...opts.config.install, ...test.install],
|
|
9047
|
+
checks: test.checks.map((c) => render(c.run, opts.vars)),
|
|
9048
|
+
cleanup: [...test.cleanup, ...opts.config.cleanup]
|
|
9049
|
+
};
|
|
9050
|
+
});
|
|
9051
|
+
if (opts.json) {
|
|
9052
|
+
console.log(JSON.stringify({ dryRun: true, build: opts.config.build, runs }, null, 2));
|
|
9053
|
+
return;
|
|
9054
|
+
}
|
|
9055
|
+
const count = (n, what) => `${n} ${what}${n === 1 ? "" : "s"}`;
|
|
9056
|
+
opts.say(`Dry run: ${count(runs.length, "run")}, build ${count(opts.config.build.length, "step")}. Nothing was started.`);
|
|
9057
|
+
for (const r of runs)
|
|
9058
|
+
opts.say(`${r.test}#${r.n}${r.model === null ? "" : ` [${r.model}]`} install ${r.install.length}, checks ${r.checks.length}, cleanup ${r.cleanup.length}${r.claudeArgs.length > 0 ? `, claude_args ${JSON.stringify(r.claudeArgs)}` : ""}`);
|
|
9059
|
+
}
|
|
8948
9060
|
async function runOne(opts) {
|
|
8949
9061
|
const { test, n } = opts;
|
|
8950
9062
|
const started = Date.now();
|
|
@@ -8952,20 +9064,13 @@ async function runOne(opts) {
|
|
|
8952
9064
|
const workdir = join6(dir, "work");
|
|
8953
9065
|
mkdirSync4(workdir, { recursive: true });
|
|
8954
9066
|
const { home, envFile } = makeRunHome(dir);
|
|
8955
|
-
const fill =
|
|
8956
|
-
"run.id": opts.runId,
|
|
8957
|
-
"run.n": String(n),
|
|
8958
|
-
"test.name": test.name,
|
|
8959
|
-
FIREGOAT_OUT: opts.out,
|
|
8960
|
-
FIREGOAT_WORKDIR: workdir,
|
|
8961
|
-
FIREGOAT_HOME: home
|
|
8962
|
-
};
|
|
9067
|
+
const fill = runFill(opts.runId, n, test.name, opts.out, dir);
|
|
8963
9068
|
const r = {
|
|
8964
9069
|
test: test.name,
|
|
8965
9070
|
n,
|
|
8966
9071
|
model: opts.model,
|
|
8967
9072
|
claudeModel: null,
|
|
8968
|
-
claudeArgs: test
|
|
9073
|
+
claudeArgs: renderClaudeArgs(test, opts.vars, fill),
|
|
8969
9074
|
status: "error",
|
|
8970
9075
|
reason: "",
|
|
8971
9076
|
prompt: render(test.prompt, opts.vars),
|
|
@@ -9031,7 +9136,10 @@ async function attempt(opts, r, env, stepEnv, homeEnvFile) {
|
|
|
9031
9136
|
cwd: workdir,
|
|
9032
9137
|
env: {
|
|
9033
9138
|
...claudeEnv(opts.configDir, env, homeEnvFile),
|
|
9034
|
-
PATH: opts.stepEnv.PATH
|
|
9139
|
+
PATH: opts.stepEnv.PATH,
|
|
9140
|
+
FIREGOAT_OUT: opts.out,
|
|
9141
|
+
FIREGOAT_WORKDIR: workdir,
|
|
9142
|
+
FIREGOAT_HOME: r.home
|
|
9035
9143
|
},
|
|
9036
9144
|
stderr,
|
|
9037
9145
|
timeoutMs: test.timeout * 1000,
|
|
@@ -9159,9 +9267,33 @@ async function evalCommand(args, root) {
|
|
|
9159
9267
|
];
|
|
9160
9268
|
if (missing.length > 0)
|
|
9161
9269
|
return fatal(EXIT.usage, `No value for ${missing.map((k) => `{{vars.${k}}}`).join(", ")}: pass --var ${missing[0]}=... or set it under vars: in firegoat.yaml`);
|
|
9270
|
+
if (opts.dryRun) {
|
|
9271
|
+
let repo;
|
|
9272
|
+
try {
|
|
9273
|
+
repo = gitRepo(root);
|
|
9274
|
+
} catch (e) {
|
|
9275
|
+
return fatal(EXIT.usage, e.message);
|
|
9276
|
+
}
|
|
9277
|
+
try {
|
|
9278
|
+
dryRun({
|
|
9279
|
+
config,
|
|
9280
|
+
tests,
|
|
9281
|
+
plan: planRuns(tests, models, runs),
|
|
9282
|
+
vars,
|
|
9283
|
+
runsRoot: join6(repo?.mainCheckout ?? root, ".firegoat", "runs"),
|
|
9284
|
+
json: opts.json,
|
|
9285
|
+
say
|
|
9286
|
+
});
|
|
9287
|
+
} catch (e) {
|
|
9288
|
+
if (e instanceof ConfigError)
|
|
9289
|
+
return fatal(EXIT.usage, e.message);
|
|
9290
|
+
throw e;
|
|
9291
|
+
}
|
|
9292
|
+
return EXIT.pass;
|
|
9293
|
+
}
|
|
9162
9294
|
const claudePath = resolveBin(opts.claudeBin);
|
|
9163
9295
|
if (!claudePath)
|
|
9164
|
-
return fatal(EXIT.notGraded, `Not graded: claude binary ${JSON.stringify(opts.claudeBin)} not found. Install Claude Code (https://claude.com/claude-code) or pass --claude-bin PATH
|
|
9296
|
+
return fatal(EXIT.notGraded, `Not graded: claude binary ${JSON.stringify(opts.claudeBin)} not found. Install Claude Code (https://claude.com/claude-code) or pass --claude-bin PATH.`);
|
|
9165
9297
|
let repo;
|
|
9166
9298
|
try {
|
|
9167
9299
|
repo = gitRepo(root);
|
|
@@ -9183,7 +9315,7 @@ async function evalCommand(args, root) {
|
|
|
9183
9315
|
const runId = newRunId();
|
|
9184
9316
|
let upload;
|
|
9185
9317
|
try {
|
|
9186
|
-
const pre = await uploadPreflight(opts, ui, basename3(repo?.mainCheckout ?? repo?.toplevel ?? root)
|
|
9318
|
+
const pre = await uploadPreflight(opts, ui, basename3(repo?.mainCheckout ?? repo?.toplevel ?? root), runId, repo?.info ?? null);
|
|
9187
9319
|
if (pre && "exitCode" in pre)
|
|
9188
9320
|
return fatal(pre.exitCode, pre.error);
|
|
9189
9321
|
upload = pre;
|
|
@@ -9202,14 +9334,11 @@ async function evalCommand(args, root) {
|
|
|
9202
9334
|
FIREGOAT_OUT: out,
|
|
9203
9335
|
PATH: [out, process.env.PATH ?? ""].join(delimiter2)
|
|
9204
9336
|
};
|
|
9205
|
-
const
|
|
9206
|
-
|
|
9207
|
-
test: t.name,
|
|
9208
|
-
n: m * runs + i + 1,
|
|
9209
|
-
model,
|
|
9337
|
+
const rows = planRuns(tests, models, runs).map((p) => ({
|
|
9338
|
+
...p,
|
|
9210
9339
|
state: "queued",
|
|
9211
9340
|
turns: 0
|
|
9212
|
-
}))
|
|
9341
|
+
}));
|
|
9213
9342
|
if (config.build.length > 0) {
|
|
9214
9343
|
const buildStarted = Date.now();
|
|
9215
9344
|
ui.build("running");
|
|
@@ -9266,7 +9395,7 @@ async function evalCommand(args, root) {
|
|
|
9266
9395
|
results[i] = r;
|
|
9267
9396
|
Object.assign(row, {
|
|
9268
9397
|
state: r.status,
|
|
9269
|
-
|
|
9398
|
+
reportedTurns: r.num_turns,
|
|
9270
9399
|
reason: r.reason,
|
|
9271
9400
|
checks: r.checks,
|
|
9272
9401
|
cost: r.cost,
|
|
@@ -9314,7 +9443,7 @@ async function evalCommand(args, root) {
|
|
|
9314
9443
|
passed: totals.pass,
|
|
9315
9444
|
failed: totals.fail,
|
|
9316
9445
|
notGraded: totals.error,
|
|
9317
|
-
cost: results.reduce((sum, r) => sum +
|
|
9446
|
+
cost: results.every((r) => r.cost !== null) ? results.reduce((sum, r) => sum + r.cost, 0) : null,
|
|
9318
9447
|
uploaded: results.filter((r) => r.upload?.status === "uploaded").length,
|
|
9319
9448
|
uploadFailed: uploadsFailed
|
|
9320
9449
|
})}
|
|
@@ -9617,7 +9746,8 @@ var TIMEOUT_MS2 = 30000;
|
|
|
9617
9746
|
var MAX_EXPIRES_S = 900;
|
|
9618
9747
|
var MAX_INTERVAL_S = 60;
|
|
9619
9748
|
var FORM = { "Content-Type": "application/x-www-form-urlencoded" };
|
|
9620
|
-
var
|
|
9749
|
+
var UPLOADS_ON = "Eval uploads each session to firegoat.ai as a private trace; --no-upload keeps runs local.";
|
|
9750
|
+
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).`;
|
|
9621
9751
|
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).";
|
|
9622
9752
|
function str2(o, k, step) {
|
|
9623
9753
|
const v = o?.[k];
|
|
@@ -9920,13 +10050,16 @@ async function askScope(question) {
|
|
|
9920
10050
|
var LOGIN_HINT = "Runs stay local: npx firegoat login uploads them to firegoat.ai";
|
|
9921
10051
|
async function loginStep(interactive, done, paint) {
|
|
9922
10052
|
const label = "firegoat.ai login";
|
|
10053
|
+
const uploadsOn = () => console.log(` ${paint("90", UPLOADS_ON)}`);
|
|
9923
10054
|
const key = resolveKey(baseUrl());
|
|
9924
10055
|
if (key) {
|
|
9925
10056
|
const who = key.source === "env" ? "FIREGOAT_API_KEY is set" : `Logged in as ${key.email}`;
|
|
9926
|
-
if (interactive)
|
|
10057
|
+
if (interactive) {
|
|
9927
10058
|
done(label, who);
|
|
9928
|
-
|
|
9929
|
-
|
|
10059
|
+
uploadsOn();
|
|
10060
|
+
} else
|
|
10061
|
+
console.log(`${who}
|
|
10062
|
+
${UPLOADS_ON}`);
|
|
9930
10063
|
return EXIT.pass;
|
|
9931
10064
|
}
|
|
9932
10065
|
if (!interactive) {
|
|
@@ -9942,6 +10075,7 @@ async function loginStep(interactive, done, paint) {
|
|
|
9942
10075
|
try {
|
|
9943
10076
|
const email = await loginFlow((l) => console.log(` ${l}`), true);
|
|
9944
10077
|
done(label, `Logged in as ${email}`);
|
|
10078
|
+
uploadsOn();
|
|
9945
10079
|
return EXIT.pass;
|
|
9946
10080
|
} catch (e) {
|
|
9947
10081
|
if (!(e instanceof ApiError))
|