firegoat 0.5.0 → 0.7.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 +26 -8
- package/dist/cli.js +658 -334
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -6947,7 +6947,7 @@ var require_public_api = __commonJS(function(exports) {
|
|
|
6947
6947
|
// package.json
|
|
6948
6948
|
var package_default = {
|
|
6949
6949
|
name: "firegoat",
|
|
6950
|
-
version: "0.
|
|
6950
|
+
version: "0.7.0",
|
|
6951
6951
|
repository: {
|
|
6952
6952
|
type: "git",
|
|
6953
6953
|
url: "git+https://github.com/team2027/firegoat-cli.git"
|
|
@@ -6987,9 +6987,411 @@ var package_default = {
|
|
|
6987
6987
|
packageManager: "bun@1.4.3"
|
|
6988
6988
|
};
|
|
6989
6989
|
|
|
6990
|
+
// src/claude-login.ts
|
|
6991
|
+
import { spawnSync as spawnSync2 } from "node:child_process";
|
|
6992
|
+
import { mkdirSync as mkdirSync3 } from "node:fs";
|
|
6993
|
+
|
|
6994
|
+
// src/auth.ts
|
|
6995
|
+
import {
|
|
6996
|
+
chmodSync,
|
|
6997
|
+
existsSync as existsSync2,
|
|
6998
|
+
mkdirSync as mkdirSync2,
|
|
6999
|
+
readFileSync,
|
|
7000
|
+
writeFileSync
|
|
7001
|
+
} from "node:fs";
|
|
7002
|
+
import { join as join2 } from "node:path";
|
|
7003
|
+
|
|
7004
|
+
// src/sandbox.ts
|
|
7005
|
+
import { spawnSync } from "node:child_process";
|
|
7006
|
+
import { existsSync, mkdirSync, realpathSync } from "node:fs";
|
|
7007
|
+
import { homedir } from "node:os";
|
|
7008
|
+
import { basename, dirname, join } from "node:path";
|
|
7009
|
+
var SANDBOX_EXEC = "/usr/bin/sandbox-exec";
|
|
7010
|
+
function firegoatHome() {
|
|
7011
|
+
return process.env.FIREGOAT_HOME || join(homedir(), ".firegoat");
|
|
7012
|
+
}
|
|
7013
|
+
function claudeConfigDir() {
|
|
7014
|
+
return join(firegoatHome(), "claude");
|
|
7015
|
+
}
|
|
7016
|
+
var TOOLCHAINS = [
|
|
7017
|
+
".nvm",
|
|
7018
|
+
".fnm",
|
|
7019
|
+
".volta",
|
|
7020
|
+
".bun",
|
|
7021
|
+
".deno",
|
|
7022
|
+
".cargo",
|
|
7023
|
+
".rustup",
|
|
7024
|
+
".pyenv",
|
|
7025
|
+
".rbenv",
|
|
7026
|
+
".asdf",
|
|
7027
|
+
".sdkman",
|
|
7028
|
+
".local/share/mise",
|
|
7029
|
+
".local/share/fnm",
|
|
7030
|
+
".local/share/claude",
|
|
7031
|
+
".local/share/uv",
|
|
7032
|
+
"go",
|
|
7033
|
+
"Library/pnpm"
|
|
7034
|
+
];
|
|
7035
|
+
var TOOLCHAIN_SECRETS = [".cargo/credentials", ".cargo/credentials.toml"];
|
|
7036
|
+
function real(p) {
|
|
7037
|
+
return realpathSync(p);
|
|
7038
|
+
}
|
|
7039
|
+
function under(p, dir) {
|
|
7040
|
+
return p === dir || p.startsWith(`${dir}/`);
|
|
7041
|
+
}
|
|
7042
|
+
function readableToolDirs(home, claudeBin) {
|
|
7043
|
+
const dirs = new Set;
|
|
7044
|
+
const add = (p) => {
|
|
7045
|
+
if (existsSync(p))
|
|
7046
|
+
dirs.add(real(p));
|
|
7047
|
+
};
|
|
7048
|
+
for (const t of TOOLCHAINS)
|
|
7049
|
+
add(join(home, t));
|
|
7050
|
+
for (const p of (process.env.PATH ?? "").split(":")) {
|
|
7051
|
+
if (!p.startsWith("/") || !existsSync(p))
|
|
7052
|
+
continue;
|
|
7053
|
+
const r = real(p);
|
|
7054
|
+
if (!under(r, home) || r === home)
|
|
7055
|
+
continue;
|
|
7056
|
+
const prefix = dirname(r);
|
|
7057
|
+
dirs.add(basename(r) === "bin" && prefix !== home && prefix !== join(home, ".local") ? prefix : r);
|
|
7058
|
+
}
|
|
7059
|
+
add(dirname(dirname(process.execPath)));
|
|
7060
|
+
add(dirname(claudeBin));
|
|
7061
|
+
add(dirname(real(claudeBin)));
|
|
7062
|
+
return [...dirs];
|
|
7063
|
+
}
|
|
7064
|
+
var q = (s) => JSON.stringify(s);
|
|
7065
|
+
function sandboxProfile(opts) {
|
|
7066
|
+
const home = real(homedir());
|
|
7067
|
+
const reads = readableToolDirs(home, opts.claudeBin).map((d) => `(subpath ${q(d)})`);
|
|
7068
|
+
return `(version 1)
|
|
7069
|
+
(allow default)
|
|
7070
|
+
|
|
7071
|
+
(deny file-read* (subpath ${q(home)}))
|
|
7072
|
+
(allow file-read-metadata)
|
|
7073
|
+
(allow file-read*
|
|
7074
|
+
${reads.join(`
|
|
7075
|
+
`)}
|
|
7076
|
+
(subpath ${q(join(home, "Library/Keychains"))})
|
|
7077
|
+
(regex ${q(`^${home}/Library/Preferences/com\\.apple\\.security`)})
|
|
7078
|
+
(regex ${q(`^${home}/Library/Preferences/\\.GlobalPreferences`)}))
|
|
7079
|
+
(deny file-read* file-write*
|
|
7080
|
+
${TOOLCHAIN_SECRETS.map((s) => `(literal ${q(join(home, s))})`).join(`
|
|
7081
|
+
`)})
|
|
7082
|
+
|
|
7083
|
+
(deny file-write* (subpath "/"))
|
|
7084
|
+
(allow file-write*
|
|
7085
|
+
(subpath "/dev")
|
|
7086
|
+
(subpath "/private/tmp")
|
|
7087
|
+
(subpath "/private/var/folders"))
|
|
7088
|
+
(allow file-read* file-write*
|
|
7089
|
+
(subpath ${q(real(opts.configDir))})
|
|
7090
|
+
(subpath ${q(real(opts.runRoot))}))
|
|
7091
|
+
|
|
7092
|
+
(deny file-read* file-write* (subpath ${q(real(opts.repo))}))
|
|
7093
|
+
`;
|
|
7094
|
+
}
|
|
7095
|
+
function agentEnv(runRoot, apiKey) {
|
|
7096
|
+
const env = { ...process.env };
|
|
7097
|
+
delete env.ANTHROPIC_API_KEY;
|
|
7098
|
+
if (apiKey)
|
|
7099
|
+
env.ANTHROPIC_API_KEY = apiKey;
|
|
7100
|
+
const cache = join(runRoot, "cache");
|
|
7101
|
+
const tmp = join(runRoot, "tmp");
|
|
7102
|
+
mkdirSync(cache, { recursive: true });
|
|
7103
|
+
mkdirSync(tmp, { recursive: true });
|
|
7104
|
+
return {
|
|
7105
|
+
...env,
|
|
7106
|
+
CLAUDE_CONFIG_DIR: claudeConfigDir(),
|
|
7107
|
+
TMPDIR: tmp,
|
|
7108
|
+
XDG_CACHE_HOME: cache,
|
|
7109
|
+
npm_config_cache: join(cache, "npm"),
|
|
7110
|
+
npm_config_store_dir: join(cache, "pnpm-store"),
|
|
7111
|
+
YARN_CACHE_FOLDER: join(cache, "yarn"),
|
|
7112
|
+
BUN_INSTALL_CACHE_DIR: join(cache, "bun"),
|
|
7113
|
+
PIP_CACHE_DIR: join(cache, "pip"),
|
|
7114
|
+
UV_CACHE_DIR: join(cache, "uv"),
|
|
7115
|
+
GOMODCACHE: join(cache, "gomod"),
|
|
7116
|
+
GOCACHE: join(cache, "gobuild"),
|
|
7117
|
+
CARGO_HOME: join(cache, "cargo")
|
|
7118
|
+
};
|
|
7119
|
+
}
|
|
7120
|
+
function wrap(profile, cmd, args) {
|
|
7121
|
+
if (profile === null)
|
|
7122
|
+
return { cmd, args };
|
|
7123
|
+
return { cmd: SANDBOX_EXEC, args: ["-p", profile, cmd, ...args] };
|
|
7124
|
+
}
|
|
7125
|
+
function probeSandbox(opts) {
|
|
7126
|
+
const outside = join(homedir(), `.firegoat-escape-probe-${process.pid}-${Date.now()}`);
|
|
7127
|
+
const script = 'touch "$1" 2>/dev/null && echo home-write; cat "$2" >/dev/null 2>&1 && echo repo-read; touch "$3" 2>/dev/null && echo work-write; rm -f "$1" "$3"';
|
|
7128
|
+
const { cmd, args } = wrap(opts.profile, "/bin/sh", [
|
|
7129
|
+
"-c",
|
|
7130
|
+
script,
|
|
7131
|
+
"probe",
|
|
7132
|
+
outside,
|
|
7133
|
+
opts.repoFile,
|
|
7134
|
+
join(opts.workdir, ".firegoat-probe")
|
|
7135
|
+
]);
|
|
7136
|
+
const r = spawnSync(cmd, args, { encoding: "utf8", cwd: opts.workdir });
|
|
7137
|
+
if (r.error)
|
|
7138
|
+
return `sandbox probe could not start: ${r.error.message}`;
|
|
7139
|
+
const got = r.stdout.trim().split(`
|
|
7140
|
+
`).filter(Boolean);
|
|
7141
|
+
if (got.includes("home-write") || got.includes("repo-read"))
|
|
7142
|
+
return `sandbox does not hold (escaped: ${got.filter((g) => g !== "work-write").join(", ")}); refusing to run the agent`;
|
|
7143
|
+
if (!got.includes("work-write"))
|
|
7144
|
+
return `sandbox blocks writes in the workdir (stderr: ${r.stderr.trim() || "none"})`;
|
|
7145
|
+
return;
|
|
7146
|
+
}
|
|
7147
|
+
|
|
7148
|
+
// src/auth.ts
|
|
7149
|
+
var authFile = () => join2(firegoatHome(), "auth.json");
|
|
7150
|
+
var keyFile = () => join2(firegoatHome(), "anthropic-api-key");
|
|
7151
|
+
|
|
7152
|
+
class AuthError extends Error {
|
|
7153
|
+
}
|
|
7154
|
+
function readMethod() {
|
|
7155
|
+
const file = authFile();
|
|
7156
|
+
if (!existsSync2(file))
|
|
7157
|
+
return;
|
|
7158
|
+
let doc;
|
|
7159
|
+
try {
|
|
7160
|
+
doc = JSON.parse(readFileSync(file, "utf8"));
|
|
7161
|
+
} catch (e) {
|
|
7162
|
+
throw new AuthError(`${file} is not valid JSON: ${e.message}`);
|
|
7163
|
+
}
|
|
7164
|
+
const method = doc?.method;
|
|
7165
|
+
if (method !== "claude" && method !== "api-key")
|
|
7166
|
+
throw new AuthError(`${file}: method must be "claude" or "api-key", got ${JSON.stringify(method)}. Run: npx firegoat init`);
|
|
7167
|
+
return method;
|
|
7168
|
+
}
|
|
7169
|
+
function saveMethod(method) {
|
|
7170
|
+
mkdirSync2(firegoatHome(), { recursive: true });
|
|
7171
|
+
writeFileSync(authFile(), `${JSON.stringify({ method }, null, 2)}
|
|
7172
|
+
`);
|
|
7173
|
+
}
|
|
7174
|
+
function saveApiKey(apiKey) {
|
|
7175
|
+
mkdirSync2(firegoatHome(), { recursive: true });
|
|
7176
|
+
const file = keyFile();
|
|
7177
|
+
writeFileSync(file, `${apiKey}
|
|
7178
|
+
`, { mode: 384 });
|
|
7179
|
+
chmodSync(file, 384);
|
|
7180
|
+
saveMethod("api-key");
|
|
7181
|
+
return file;
|
|
7182
|
+
}
|
|
7183
|
+
function looksLikeAnthropicKey(s) {
|
|
7184
|
+
return /^sk-ant-[A-Za-z0-9_-]{20,}$/.test(s);
|
|
7185
|
+
}
|
|
7186
|
+
function resolveAuth(env) {
|
|
7187
|
+
if (env.CI && env.ANTHROPIC_API_KEY)
|
|
7188
|
+
return {
|
|
7189
|
+
method: "api-key",
|
|
7190
|
+
label: "Anthropic API key from ANTHROPIC_API_KEY (CI)",
|
|
7191
|
+
apiKey: env.ANTHROPIC_API_KEY
|
|
7192
|
+
};
|
|
7193
|
+
const method = readMethod() ?? "claude";
|
|
7194
|
+
if (method === "claude")
|
|
7195
|
+
return {
|
|
7196
|
+
method,
|
|
7197
|
+
label: `Claude login in ${claudeConfigDir()}`
|
|
7198
|
+
};
|
|
7199
|
+
const file = keyFile();
|
|
7200
|
+
if (!existsSync2(file))
|
|
7201
|
+
throw new AuthError(`auth is set to api-key but ${file} is missing. Run: npx firegoat init`);
|
|
7202
|
+
const apiKey = readFileSync(file, "utf8").trim();
|
|
7203
|
+
if (!apiKey)
|
|
7204
|
+
throw new AuthError(`${file} is empty. Run: npx firegoat init`);
|
|
7205
|
+
return { method, label: `Anthropic API key in ${file}`, apiKey };
|
|
7206
|
+
}
|
|
7207
|
+
|
|
7208
|
+
// src/exit.ts
|
|
7209
|
+
var EXIT = { pass: 0, fail: 1, notGraded: 2, usage: 3 };
|
|
7210
|
+
|
|
7211
|
+
// src/help.ts
|
|
7212
|
+
var EXIT_CODES = `Exit codes:
|
|
7213
|
+
0 every run's script checks passed
|
|
7214
|
+
1 a script check failed
|
|
7215
|
+
2 at least one run was not graded (claude missing or logged out, crash, timeout,
|
|
7216
|
+
setup failed); wins over 1
|
|
7217
|
+
3 usage or config error (bad flag, unknown test, invalid or missing firegoat.yaml)`;
|
|
7218
|
+
function topHelp(version) {
|
|
7219
|
+
return `firegoat ${version}: local AX evals. A coding agent gets one prompt in a fresh,
|
|
7220
|
+
sandboxed folder; firegoat records the transcript and runs your script checks.
|
|
7221
|
+
|
|
7222
|
+
Typical flow (for an agent driving firegoat on behalf of a human):
|
|
7223
|
+
1. npx firegoat init --yes write firegoat.yaml (read: npx firegoat init --help)
|
|
7224
|
+
2. edit firegoat.yaml real user problems as prompts, a goal, checks
|
|
7225
|
+
3. ask the human to run once: npx firegoat claude login (opens a browser)
|
|
7226
|
+
4. npx firegoat eval --json run the tests; JSON summary on stdout
|
|
7227
|
+
5. judge each run's goal + rubric yourself, from its run.json, transcript and
|
|
7228
|
+
workdir: firegoat only runs the script checks
|
|
7229
|
+
|
|
7230
|
+
Commands:
|
|
7231
|
+
init write firegoat.yaml; choose how the agent signs in
|
|
7232
|
+
claude login sign claude in to firegoat's own config dir (interactive, once)
|
|
7233
|
+
eval run the tests
|
|
7234
|
+
open open (or --print) a run's folder
|
|
7235
|
+
|
|
7236
|
+
Run firegoat <command> --help for details and examples.
|
|
7237
|
+
|
|
7238
|
+
${EXIT_CODES}`;
|
|
7239
|
+
}
|
|
7240
|
+
var INIT_HELP = `Usage: firegoat init [--yes] [--force] [--skills] [--auth claude|api-key]
|
|
7241
|
+
|
|
7242
|
+
Writes firegoat.yaml in the current folder with starter tests from what the repo
|
|
7243
|
+
ships (package.json, pyproject.toml, go.mod, Cargo.toml, README), and adds
|
|
7244
|
+
.firegoat/ to .gitignore. Edit the tests afterwards; they are templates.
|
|
7245
|
+
|
|
7246
|
+
--yes, -y don't ask questions (default when stdin is not a terminal)
|
|
7247
|
+
--force, -f overwrite an existing firegoat.yaml
|
|
7248
|
+
--skills also run: npx skills add team2027/skills --skill firegoat-onboarding
|
|
7249
|
+
--skill suggesting-eval-prompts (into .claude/skills/)
|
|
7250
|
+
--auth claude the agent signs in with a Claude login: runs \`firegoat claude login\`
|
|
7251
|
+
(interactive: a human must complete it in the browser)
|
|
7252
|
+
--auth api-key the agent signs in with an Anthropic API key, read from stdin when
|
|
7253
|
+
piped (else asked for without echo) and stored in
|
|
7254
|
+
~/.firegoat/anthropic-api-key (mode 0600), never in the repo
|
|
7255
|
+
Without --auth, init asks (unless --yes). In CI, ANTHROPIC_API_KEY in
|
|
7256
|
+
the env wins.
|
|
7257
|
+
|
|
7258
|
+
firegoat.yaml reference:
|
|
7259
|
+
agents: [claude-code] only claude-code for now
|
|
7260
|
+
runs: 1 runs per test
|
|
7261
|
+
setup: <sh> | [<sh>, ...] once per eval, in the repo, on the host; whatever it
|
|
7262
|
+
writes to $FIREGOAT_OUT is copied into each run and put
|
|
7263
|
+
first on the agent's PATH
|
|
7264
|
+
workdir_setup: <sh> | [...] in each fresh workdir before the agent starts
|
|
7265
|
+
(env: $FIREGOAT_OUT, $FIREGOAT_WORKDIR, vars)
|
|
7266
|
+
vars: {KEY: value} defaults for {{vars.KEY}}; --var KEY=value overrides
|
|
7267
|
+
tests:
|
|
7268
|
+
- name: quickstart letters, digits, '.', '_', '-'; unique
|
|
7269
|
+
prompt: ... what a user would ask the agent; one concrete outcome
|
|
7270
|
+
goal: ... one sentence that is true when the problem is solved
|
|
7271
|
+
rubric: [...] optional criteria for whoever grades the run (you)
|
|
7272
|
+
checks: optional shell commands, run in the workdir after the
|
|
7273
|
+
- run: ... agent; exit 0 = pass; they set eval's exit code
|
|
7274
|
+
timeout: 600 optional, seconds
|
|
7275
|
+
|
|
7276
|
+
The agent's folder has no repo files. Checks get $FIREGOAT_WORKDIR,
|
|
7277
|
+
$FIREGOAT_SESSION (stream-json transcript), $FIREGOAT_OUT and the vars.
|
|
7278
|
+
goal and rubric are not run by firegoat: they are handed to the caller to judge.
|
|
7279
|
+
|
|
7280
|
+
Example:
|
|
7281
|
+
setup: npm pack --pack-destination "$FIREGOAT_OUT"
|
|
7282
|
+
workdir_setup: npm init -y >/dev/null && npm install "$FIREGOAT_OUT"/*.tgz
|
|
7283
|
+
tests:
|
|
7284
|
+
- name: first-call
|
|
7285
|
+
prompt: >-
|
|
7286
|
+
Using the acme SDK (already installed from the build under test), send a test
|
|
7287
|
+
event and print the event id. Credentials are in ACME_API_KEY. Keep it minimal.
|
|
7288
|
+
goal: A script sends one event and prints its id.
|
|
7289
|
+
rubric:
|
|
7290
|
+
- uses the SDK's client, not raw HTTP
|
|
7291
|
+
- does not install acme from the registry
|
|
7292
|
+
checks:
|
|
7293
|
+
- run: grep -qE 'evt_[A-Za-z0-9]+' "$FIREGOAT_SESSION"
|
|
7294
|
+
|
|
7295
|
+
Next: npx firegoat claude login (a human, once), then npx firegoat eval.
|
|
7296
|
+
|
|
7297
|
+
Exit codes: 0 done, 2 the skills install or claude login failed, 3 usage error.`;
|
|
7298
|
+
var EVAL_HELP = `Usage: firegoat eval [test..] [--runs N] [--var KEY=value].. [--json] [--host]
|
|
7299
|
+
[--claude-bin PATH]
|
|
7300
|
+
|
|
7301
|
+
Runs each test headless: claude gets the prompt in a fresh folder outside the repo,
|
|
7302
|
+
sandboxed on macOS (it can't read your home dir or the repo), with setup:'s outputs
|
|
7303
|
+
on PATH. Then the test's script checks run in that folder.
|
|
7304
|
+
|
|
7305
|
+
test.. only these tests (by name)
|
|
7306
|
+
--runs N runs per test (overrides runs: in firegoat.yaml)
|
|
7307
|
+
--var KEY=value fills {{vars.KEY}}; env for setup, workdir_setup and checks
|
|
7308
|
+
--json the summary as JSON on stdout; progress goes to stderr
|
|
7309
|
+
--host no sandbox (required off macOS; use a throwaway container)
|
|
7310
|
+
--claude-bin PATH claude binary (env FIREGOAT_CLAUDE_BIN; default: claude on PATH)
|
|
7311
|
+
|
|
7312
|
+
Sign-in: the one chosen in firegoat init (Claude login or stored API key); in CI,
|
|
7313
|
+
ANTHROPIC_API_KEY from the env. eval prints which. Not logged in = exit 2 with the
|
|
7314
|
+
command to run.
|
|
7315
|
+
|
|
7316
|
+
Output: .firegoat/runs/<runId>/
|
|
7317
|
+
summary.json every run, plus totals and a grading hint
|
|
7318
|
+
<test>/<n>/run.json one run: test, n, status and reason (from the checks),
|
|
7319
|
+
prompt, goal, rubric, checks [{run, code}], and paths:
|
|
7320
|
+
dir, workdir, session, stderr, checksLog
|
|
7321
|
+
<test>/<n>/session.jsonl the agent's transcript, stream-json (one event per line)
|
|
7322
|
+
setup.txt, <test>/<n>/workdir_setup.txt, checks.txt logs
|
|
7323
|
+
|
|
7324
|
+
Grading: firegoat runs only the script checks; they set the exit code. The goal and
|
|
7325
|
+
rubric are yours to judge, with your own process. session.jsonl is stream-json: the
|
|
7326
|
+
last line has type "result" with the agent's final message; tool calls are
|
|
7327
|
+
"tool_use" items in "assistant" messages; tool output is in "user" messages.
|
|
7328
|
+
|
|
7329
|
+
Examples:
|
|
7330
|
+
npx firegoat eval --json all tests, JSON summary on stdout
|
|
7331
|
+
npx firegoat eval quickstart --runs 3 one test, three runs
|
|
7332
|
+
npx firegoat eval --var SHA=$(git rev-parse HEAD)
|
|
7333
|
+
|
|
7334
|
+
firegoat.yaml is looked up in the current folder, then parents up to the git root.
|
|
7335
|
+
A run can take minutes (default timeout 600s per test).
|
|
7336
|
+
|
|
7337
|
+
${EXIT_CODES}`;
|
|
7338
|
+
var OPEN_HELP = `Usage: firegoat open [runId] [--print]
|
|
7339
|
+
|
|
7340
|
+
Opens a run's folder (.firegoat/runs/<runId>/) with the system opener; the latest
|
|
7341
|
+
run by default. --print only prints the path (use this from an agent).
|
|
7342
|
+
|
|
7343
|
+
Exit codes: 0 done, 2 the opener failed, 3 no such run.`;
|
|
7344
|
+
var CLAUDE_LOGIN_HELP = `Usage: firegoat claude login [--claude-bin PATH]
|
|
7345
|
+
|
|
7346
|
+
Signs claude in to firegoat's own config dir (~/.firegoat/claude), once per machine.
|
|
7347
|
+
Evals then run on that login, never your ~/.claude. Interactive: it opens a browser,
|
|
7348
|
+
so an agent should ask the human to run it. \`firegoat login\` is a deprecated alias.
|
|
7349
|
+
|
|
7350
|
+
Exit codes: 0 logged in, 2 claude missing or the login failed, 3 usage error.`;
|
|
7351
|
+
|
|
7352
|
+
// src/claude-login.ts
|
|
7353
|
+
function claudeLoginCommand(args) {
|
|
7354
|
+
let claudeBin = process.env.FIREGOAT_CLAUDE_BIN || "claude";
|
|
7355
|
+
for (let i = 0;i < args.length; i++) {
|
|
7356
|
+
const a = args[i];
|
|
7357
|
+
if (a === "--help" || a === "-h") {
|
|
7358
|
+
console.log(CLAUDE_LOGIN_HELP);
|
|
7359
|
+
return EXIT.pass;
|
|
7360
|
+
}
|
|
7361
|
+
if (a === "--claude-bin" && args[i + 1] !== undefined)
|
|
7362
|
+
claudeBin = args[++i];
|
|
7363
|
+
else if (a.startsWith("--claude-bin="))
|
|
7364
|
+
claudeBin = a.slice(13);
|
|
7365
|
+
else {
|
|
7366
|
+
console.error(`firegoat claude login: unknown argument: ${a}`);
|
|
7367
|
+
return EXIT.usage;
|
|
7368
|
+
}
|
|
7369
|
+
}
|
|
7370
|
+
const configDir = claudeConfigDir();
|
|
7371
|
+
mkdirSync3(configDir, { recursive: true });
|
|
7372
|
+
const env = {
|
|
7373
|
+
...process.env,
|
|
7374
|
+
CLAUDE_CONFIG_DIR: configDir
|
|
7375
|
+
};
|
|
7376
|
+
delete env.ANTHROPIC_API_KEY;
|
|
7377
|
+
console.error(`Signing claude in to ${configDir}. Evals run with this login, never your own ~/.claude.`);
|
|
7378
|
+
const r = spawnSync2(claudeBin, ["auth", "login"], { env, stdio: "inherit" });
|
|
7379
|
+
if (r.error) {
|
|
7380
|
+
console.error(`firegoat claude login: could not run ${claudeBin}: ${r.error.message}`);
|
|
7381
|
+
return EXIT.notGraded;
|
|
7382
|
+
}
|
|
7383
|
+
if (r.status !== 0) {
|
|
7384
|
+
console.error(`firegoat claude login: claude auth login exited ${r.status}`);
|
|
7385
|
+
return EXIT.notGraded;
|
|
7386
|
+
}
|
|
7387
|
+
saveMethod("claude");
|
|
7388
|
+
console.log("logged in; evals now sign in with this Claude login. next: npx firegoat eval");
|
|
7389
|
+
return EXIT.pass;
|
|
7390
|
+
}
|
|
7391
|
+
|
|
6990
7392
|
// src/config.ts
|
|
6991
|
-
import { existsSync, readFileSync } from "node:fs";
|
|
6992
|
-
import { dirname, join } from "node:path";
|
|
7393
|
+
import { existsSync as existsSync3, readFileSync as readFileSync2 } from "node:fs";
|
|
7394
|
+
import { dirname as dirname2, join as join3 } from "node:path";
|
|
6993
7395
|
|
|
6994
7396
|
// node_modules/yaml/dist/index.js
|
|
6995
7397
|
var composer = require_composer();
|
|
@@ -7062,7 +7464,7 @@ function posInt(v, path, fallback) {
|
|
|
7062
7464
|
fail(path, "must be a positive integer");
|
|
7063
7465
|
return v;
|
|
7064
7466
|
}
|
|
7065
|
-
function parseConfig(text
|
|
7467
|
+
function parseConfig(text) {
|
|
7066
7468
|
let raw;
|
|
7067
7469
|
try {
|
|
7068
7470
|
raw = $parse(text);
|
|
@@ -7102,20 +7504,21 @@ function parseConfig(text, warn = console.error) {
|
|
|
7102
7504
|
if (!Array.isArray(checksRaw))
|
|
7103
7505
|
fail(`${p}.checks`, "must be a list");
|
|
7104
7506
|
const checks = [];
|
|
7507
|
+
const rubric = rubricOf(t.rubric, `${p}.rubric`);
|
|
7105
7508
|
checksRaw.forEach((c, j) => {
|
|
7106
7509
|
const cp = `${p}.checks[${j}]`;
|
|
7107
7510
|
if (!isRecord(c))
|
|
7108
7511
|
fail(cp, "must be a mapping");
|
|
7109
|
-
if ("judge" in c)
|
|
7110
|
-
|
|
7111
|
-
|
|
7112
|
-
|
|
7113
|
-
checks.push({ run: str(c.run, `${cp}.run`) });
|
|
7512
|
+
if ("judge" in c)
|
|
7513
|
+
rubric.push(str(c.judge, `${cp}.judge`));
|
|
7514
|
+
else
|
|
7515
|
+
checks.push({ run: str(c.run, `${cp}.run`) });
|
|
7114
7516
|
});
|
|
7115
7517
|
return {
|
|
7116
7518
|
name,
|
|
7117
7519
|
prompt: str(t.prompt, `${p}.prompt`),
|
|
7118
7520
|
goal: str(t.goal, `${p}.goal`),
|
|
7521
|
+
rubric,
|
|
7119
7522
|
checks,
|
|
7120
7523
|
timeout: posInt(t.timeout, `${p}.timeout`, DEFAULT_TIMEOUT)
|
|
7121
7524
|
};
|
|
@@ -7130,6 +7533,15 @@ function parseConfig(text, warn = console.error) {
|
|
|
7130
7533
|
};
|
|
7131
7534
|
}
|
|
7132
7535
|
var NAME = /^[A-Za-z0-9._-]+$/;
|
|
7536
|
+
function rubricOf(raw, path) {
|
|
7537
|
+
if (raw === undefined)
|
|
7538
|
+
return [];
|
|
7539
|
+
if (typeof raw === "string")
|
|
7540
|
+
return [str(raw, path)];
|
|
7541
|
+
if (!Array.isArray(raw))
|
|
7542
|
+
fail(path, "must be a string or a list of strings");
|
|
7543
|
+
return raw.map((r, i) => str(r, `${path}[${i}]`));
|
|
7544
|
+
}
|
|
7133
7545
|
function commands(raw, path) {
|
|
7134
7546
|
if (raw === undefined)
|
|
7135
7547
|
return [];
|
|
@@ -7167,19 +7579,19 @@ function render(text, vars) {
|
|
|
7167
7579
|
return v;
|
|
7168
7580
|
});
|
|
7169
7581
|
}
|
|
7170
|
-
function loadConfig(dir
|
|
7582
|
+
function loadConfig(dir) {
|
|
7171
7583
|
const file = `${dir}/${CONFIG_FILE}`;
|
|
7172
|
-
if (!
|
|
7584
|
+
if (!existsSync3(file))
|
|
7173
7585
|
throw new ConfigError(`${CONFIG_FILE} not found in ${dir}. Run: npx firegoat init`);
|
|
7174
|
-
return parseConfig(
|
|
7586
|
+
return parseConfig(readFileSync2(file, "utf8"));
|
|
7175
7587
|
}
|
|
7176
7588
|
function findConfigDir(cwd) {
|
|
7177
7589
|
let dir = cwd;
|
|
7178
7590
|
for (;; ) {
|
|
7179
|
-
if (
|
|
7591
|
+
if (existsSync3(join3(dir, CONFIG_FILE)))
|
|
7180
7592
|
return dir;
|
|
7181
|
-
const parent =
|
|
7182
|
-
if (
|
|
7593
|
+
const parent = dirname2(dir);
|
|
7594
|
+
if (existsSync3(join3(dir, ".git")) || parent === dir)
|
|
7183
7595
|
return cwd;
|
|
7184
7596
|
dir = parent;
|
|
7185
7597
|
}
|
|
@@ -7189,164 +7601,20 @@ function findConfigDir(cwd) {
|
|
|
7189
7601
|
import { randomBytes } from "node:crypto";
|
|
7190
7602
|
import {
|
|
7191
7603
|
createWriteStream as createWriteStream3,
|
|
7192
|
-
existsSync as
|
|
7193
|
-
mkdirSync as
|
|
7604
|
+
existsSync as existsSync4,
|
|
7605
|
+
mkdirSync as mkdirSync5,
|
|
7194
7606
|
mkdtempSync,
|
|
7195
|
-
readFileSync as
|
|
7607
|
+
readFileSync as readFileSync3,
|
|
7196
7608
|
realpathSync as realpathSync2,
|
|
7197
|
-
writeFileSync
|
|
7609
|
+
writeFileSync as writeFileSync2
|
|
7198
7610
|
} from "node:fs";
|
|
7199
7611
|
import { tmpdir } from "node:os";
|
|
7200
|
-
import { join as
|
|
7612
|
+
import { join as join6, resolve as resolve2 } from "node:path";
|
|
7201
7613
|
|
|
7202
7614
|
// src/claude.ts
|
|
7203
|
-
import { spawn, spawnSync as
|
|
7615
|
+
import { spawn, spawnSync as spawnSync3 } from "node:child_process";
|
|
7204
7616
|
import { createWriteStream } from "node:fs";
|
|
7205
7617
|
import { createInterface } from "node:readline";
|
|
7206
|
-
|
|
7207
|
-
// src/sandbox.ts
|
|
7208
|
-
import { spawnSync } from "node:child_process";
|
|
7209
|
-
import { existsSync as existsSync2, mkdirSync, realpathSync } from "node:fs";
|
|
7210
|
-
import { homedir } from "node:os";
|
|
7211
|
-
import { basename, dirname as dirname2, join as join2 } from "node:path";
|
|
7212
|
-
var SANDBOX_EXEC = "/usr/bin/sandbox-exec";
|
|
7213
|
-
function firegoatHome() {
|
|
7214
|
-
return process.env.FIREGOAT_HOME || join2(homedir(), ".firegoat");
|
|
7215
|
-
}
|
|
7216
|
-
function claudeConfigDir() {
|
|
7217
|
-
return join2(firegoatHome(), "claude");
|
|
7218
|
-
}
|
|
7219
|
-
var TOOLCHAINS = [
|
|
7220
|
-
".nvm",
|
|
7221
|
-
".fnm",
|
|
7222
|
-
".volta",
|
|
7223
|
-
".bun",
|
|
7224
|
-
".deno",
|
|
7225
|
-
".cargo",
|
|
7226
|
-
".rustup",
|
|
7227
|
-
".pyenv",
|
|
7228
|
-
".rbenv",
|
|
7229
|
-
".asdf",
|
|
7230
|
-
".sdkman",
|
|
7231
|
-
".local/share/mise",
|
|
7232
|
-
".local/share/fnm",
|
|
7233
|
-
".local/share/claude",
|
|
7234
|
-
".local/share/uv",
|
|
7235
|
-
"go",
|
|
7236
|
-
"Library/pnpm"
|
|
7237
|
-
];
|
|
7238
|
-
var TOOLCHAIN_SECRETS = [".cargo/credentials", ".cargo/credentials.toml"];
|
|
7239
|
-
function real(p) {
|
|
7240
|
-
return realpathSync(p);
|
|
7241
|
-
}
|
|
7242
|
-
function under(p, dir) {
|
|
7243
|
-
return p === dir || p.startsWith(`${dir}/`);
|
|
7244
|
-
}
|
|
7245
|
-
function readableToolDirs(home, claudeBin) {
|
|
7246
|
-
const dirs = new Set;
|
|
7247
|
-
const add = (p) => {
|
|
7248
|
-
if (existsSync2(p))
|
|
7249
|
-
dirs.add(real(p));
|
|
7250
|
-
};
|
|
7251
|
-
for (const t of TOOLCHAINS)
|
|
7252
|
-
add(join2(home, t));
|
|
7253
|
-
for (const p of (process.env.PATH ?? "").split(":")) {
|
|
7254
|
-
if (!p.startsWith("/") || !existsSync2(p))
|
|
7255
|
-
continue;
|
|
7256
|
-
const r = real(p);
|
|
7257
|
-
if (!under(r, home) || r === home)
|
|
7258
|
-
continue;
|
|
7259
|
-
const prefix = dirname2(r);
|
|
7260
|
-
dirs.add(basename(r) === "bin" && prefix !== home && prefix !== join2(home, ".local") ? prefix : r);
|
|
7261
|
-
}
|
|
7262
|
-
add(dirname2(dirname2(process.execPath)));
|
|
7263
|
-
add(dirname2(claudeBin));
|
|
7264
|
-
add(dirname2(real(claudeBin)));
|
|
7265
|
-
return [...dirs];
|
|
7266
|
-
}
|
|
7267
|
-
var q = (s) => JSON.stringify(s);
|
|
7268
|
-
function sandboxProfile(opts) {
|
|
7269
|
-
const home = real(homedir());
|
|
7270
|
-
const reads = readableToolDirs(home, opts.claudeBin).map((d) => `(subpath ${q(d)})`);
|
|
7271
|
-
return `(version 1)
|
|
7272
|
-
(allow default)
|
|
7273
|
-
|
|
7274
|
-
(deny file-read* (subpath ${q(home)}))
|
|
7275
|
-
(allow file-read-metadata)
|
|
7276
|
-
(allow file-read*
|
|
7277
|
-
${reads.join(`
|
|
7278
|
-
`)}
|
|
7279
|
-
(subpath ${q(join2(home, "Library/Keychains"))})
|
|
7280
|
-
(regex ${q(`^${home}/Library/Preferences/com\\.apple\\.security`)})
|
|
7281
|
-
(regex ${q(`^${home}/Library/Preferences/\\.GlobalPreferences`)}))
|
|
7282
|
-
(deny file-read* file-write*
|
|
7283
|
-
${TOOLCHAIN_SECRETS.map((s) => `(literal ${q(join2(home, s))})`).join(`
|
|
7284
|
-
`)})
|
|
7285
|
-
|
|
7286
|
-
(deny file-write* (subpath "/"))
|
|
7287
|
-
(allow file-write*
|
|
7288
|
-
(subpath "/dev")
|
|
7289
|
-
(subpath "/private/tmp")
|
|
7290
|
-
(subpath "/private/var/folders"))
|
|
7291
|
-
(allow file-read* file-write*
|
|
7292
|
-
(subpath ${q(real(opts.configDir))})
|
|
7293
|
-
(subpath ${q(real(opts.runRoot))}))
|
|
7294
|
-
|
|
7295
|
-
(deny file-read* file-write* (subpath ${q(real(opts.repo))}))
|
|
7296
|
-
`;
|
|
7297
|
-
}
|
|
7298
|
-
function agentEnv(runRoot) {
|
|
7299
|
-
const env = { ...process.env };
|
|
7300
|
-
delete env.ANTHROPIC_API_KEY;
|
|
7301
|
-
const cache = join2(runRoot, "cache");
|
|
7302
|
-
const tmp = join2(runRoot, "tmp");
|
|
7303
|
-
mkdirSync(cache, { recursive: true });
|
|
7304
|
-
mkdirSync(tmp, { recursive: true });
|
|
7305
|
-
return {
|
|
7306
|
-
...env,
|
|
7307
|
-
CLAUDE_CONFIG_DIR: claudeConfigDir(),
|
|
7308
|
-
TMPDIR: tmp,
|
|
7309
|
-
XDG_CACHE_HOME: cache,
|
|
7310
|
-
npm_config_cache: join2(cache, "npm"),
|
|
7311
|
-
npm_config_store_dir: join2(cache, "pnpm-store"),
|
|
7312
|
-
YARN_CACHE_FOLDER: join2(cache, "yarn"),
|
|
7313
|
-
BUN_INSTALL_CACHE_DIR: join2(cache, "bun"),
|
|
7314
|
-
PIP_CACHE_DIR: join2(cache, "pip"),
|
|
7315
|
-
UV_CACHE_DIR: join2(cache, "uv"),
|
|
7316
|
-
GOMODCACHE: join2(cache, "gomod"),
|
|
7317
|
-
GOCACHE: join2(cache, "gobuild"),
|
|
7318
|
-
CARGO_HOME: join2(cache, "cargo")
|
|
7319
|
-
};
|
|
7320
|
-
}
|
|
7321
|
-
function wrap(profile, cmd, args) {
|
|
7322
|
-
if (profile === null)
|
|
7323
|
-
return { cmd, args };
|
|
7324
|
-
return { cmd: SANDBOX_EXEC, args: ["-p", profile, cmd, ...args] };
|
|
7325
|
-
}
|
|
7326
|
-
function probeSandbox(opts) {
|
|
7327
|
-
const outside = join2(homedir(), `.firegoat-escape-probe-${process.pid}-${Date.now()}`);
|
|
7328
|
-
const script = 'touch "$1" 2>/dev/null && echo home-write; cat "$2" >/dev/null 2>&1 && echo repo-read; touch "$3" 2>/dev/null && echo work-write; rm -f "$1" "$3"';
|
|
7329
|
-
const { cmd, args } = wrap(opts.profile, "/bin/sh", [
|
|
7330
|
-
"-c",
|
|
7331
|
-
script,
|
|
7332
|
-
"probe",
|
|
7333
|
-
outside,
|
|
7334
|
-
opts.repoFile,
|
|
7335
|
-
join2(opts.workdir, ".firegoat-probe")
|
|
7336
|
-
]);
|
|
7337
|
-
const r = spawnSync(cmd, args, { encoding: "utf8", cwd: opts.workdir });
|
|
7338
|
-
if (r.error)
|
|
7339
|
-
return `sandbox probe could not start: ${r.error.message}`;
|
|
7340
|
-
const got = r.stdout.trim().split(`
|
|
7341
|
-
`).filter(Boolean);
|
|
7342
|
-
if (got.includes("home-write") || got.includes("repo-read"))
|
|
7343
|
-
return `sandbox does not hold (escaped: ${got.filter((g) => g !== "work-write").join(", ")}); refusing to run the agent`;
|
|
7344
|
-
if (!got.includes("work-write"))
|
|
7345
|
-
return `sandbox blocks writes in the workdir (stderr: ${r.stderr.trim() || "none"})`;
|
|
7346
|
-
return;
|
|
7347
|
-
}
|
|
7348
|
-
|
|
7349
|
-
// src/claude.ts
|
|
7350
7618
|
var CLAUDE_FLAGS = [
|
|
7351
7619
|
"--output-format",
|
|
7352
7620
|
"stream-json",
|
|
@@ -7511,8 +7779,8 @@ async function runClaude(opts) {
|
|
|
7511
7779
|
badStart = `claude output did not start with a system/init stream-json line (got: ${line.slice(0, 120)})`;
|
|
7512
7780
|
return "kill";
|
|
7513
7781
|
}
|
|
7514
|
-
if (msg.apiKeySource !==
|
|
7515
|
-
badStart = `claude would use API key billing (apiKeySource=${JSON.stringify(msg.apiKeySource)}) instead of your Claude
|
|
7782
|
+
if (msg.apiKeySource !== opts.keySource) {
|
|
7783
|
+
badStart = opts.keySource === "none" ? `claude would use API key billing (apiKeySource=${JSON.stringify(msg.apiKeySource)}) instead of your Claude login. firegoat removes ANTHROPIC_API_KEY from the agent's env; check apiKeyHelper or other API key settings, then run \`npx firegoat claude login\`` : `claude did not use the configured API key (apiKeySource=${JSON.stringify(msg.apiKeySource)}, expected "ANTHROPIC_API_KEY")`;
|
|
7516
7784
|
return "kill";
|
|
7517
7785
|
}
|
|
7518
7786
|
return;
|
|
@@ -7592,7 +7860,7 @@ async function runShell(opts) {
|
|
|
7592
7860
|
}
|
|
7593
7861
|
function checkLogin(opts) {
|
|
7594
7862
|
const { cmd, args } = wrap(opts.profile, opts.bin, ["auth", "status"]);
|
|
7595
|
-
const r =
|
|
7863
|
+
const r = spawnSync3(cmd, args, {
|
|
7596
7864
|
cwd: opts.cwd,
|
|
7597
7865
|
env: opts.env,
|
|
7598
7866
|
encoding: "utf8",
|
|
@@ -7607,16 +7875,13 @@ function checkLogin(opts) {
|
|
|
7607
7875
|
if (status?.loggedIn === true)
|
|
7608
7876
|
return;
|
|
7609
7877
|
if (status?.loggedIn === false)
|
|
7610
|
-
return `claude is not logged in to firegoat's config dir (${opts.env.CLAUDE_CONFIG_DIR}). Run: npx firegoat login`;
|
|
7878
|
+
return `claude is not logged in to firegoat's config dir (${opts.env.CLAUDE_CONFIG_DIR}). Run: npx firegoat claude login`;
|
|
7611
7879
|
return `claude auth status gave no login state (exit ${r.status}): ${(r.stdout + r.stderr).trim().slice(0, 300)}`;
|
|
7612
7880
|
}
|
|
7613
7881
|
|
|
7614
|
-
// src/exit.ts
|
|
7615
|
-
var EXIT = { pass: 0, fail: 1, notGraded: 2, usage: 3 };
|
|
7616
|
-
|
|
7617
7882
|
// src/setup.ts
|
|
7618
|
-
import { cpSync, createWriteStream as createWriteStream2, mkdirSync as
|
|
7619
|
-
import { delimiter, join as
|
|
7883
|
+
import { cpSync, createWriteStream as createWriteStream2, mkdirSync as mkdirSync4 } from "node:fs";
|
|
7884
|
+
import { delimiter, join as join4 } from "node:path";
|
|
7620
7885
|
var SETUP_TIMEOUT_MS = 600000;
|
|
7621
7886
|
async function runAll(commands, cwd, env, logPath, what) {
|
|
7622
7887
|
const log = createWriteStream2(logPath);
|
|
@@ -7638,13 +7903,13 @@ async function runAll(commands, cwd, env, logPath, what) {
|
|
|
7638
7903
|
}
|
|
7639
7904
|
}
|
|
7640
7905
|
async function setupOnce(opts) {
|
|
7641
|
-
|
|
7906
|
+
mkdirSync4(opts.out, { recursive: true });
|
|
7642
7907
|
return runAll(opts.commands, opts.root, { ...opts.env, FIREGOAT_OUT: opts.out }, opts.logPath, "setup");
|
|
7643
7908
|
}
|
|
7644
7909
|
async function prepareRun(opts) {
|
|
7645
|
-
const out =
|
|
7910
|
+
const out = join4(opts.runRoot, "out");
|
|
7646
7911
|
if (opts.out === null)
|
|
7647
|
-
|
|
7912
|
+
mkdirSync4(out);
|
|
7648
7913
|
else
|
|
7649
7914
|
cpSync(opts.out, out, { recursive: true });
|
|
7650
7915
|
const path = [out, opts.env.PATH ?? ""].join(delimiter);
|
|
@@ -7659,7 +7924,7 @@ async function prepareRun(opts) {
|
|
|
7659
7924
|
|
|
7660
7925
|
// src/which.ts
|
|
7661
7926
|
import { accessSync, constants, statSync } from "node:fs";
|
|
7662
|
-
import { delimiter as delimiter2, join as
|
|
7927
|
+
import { delimiter as delimiter2, join as join5, resolve } from "node:path";
|
|
7663
7928
|
function resolveBin(bin) {
|
|
7664
7929
|
const executable = (p) => {
|
|
7665
7930
|
try {
|
|
@@ -7672,14 +7937,39 @@ function resolveBin(bin) {
|
|
|
7672
7937
|
if (bin.includes("/"))
|
|
7673
7938
|
return executable(bin) ? resolve(bin) : undefined;
|
|
7674
7939
|
for (const dir of (process.env.PATH ?? "").split(delimiter2)) {
|
|
7675
|
-
if (dir && executable(
|
|
7676
|
-
return
|
|
7940
|
+
if (dir && executable(join5(dir, bin)))
|
|
7941
|
+
return join5(dir, bin);
|
|
7677
7942
|
}
|
|
7678
7943
|
return;
|
|
7679
7944
|
}
|
|
7680
7945
|
|
|
7681
7946
|
// src/eval.ts
|
|
7682
7947
|
var CHECK_TIMEOUT_MS = 120000;
|
|
7948
|
+
var GRADING_HINT = "firegoat ran only the script checks. The goal and rubric are for the caller to judge, from each run's session (stream-json transcript) and workdir.";
|
|
7949
|
+
function blankRun(test, n, dir, prompt) {
|
|
7950
|
+
return {
|
|
7951
|
+
test: test.name,
|
|
7952
|
+
n,
|
|
7953
|
+
status: "error",
|
|
7954
|
+
reason: "",
|
|
7955
|
+
prompt,
|
|
7956
|
+
goal: test.goal,
|
|
7957
|
+
rubric: test.rubric,
|
|
7958
|
+
checks: [],
|
|
7959
|
+
dir,
|
|
7960
|
+
workdir: null,
|
|
7961
|
+
session: join6(dir, "session.jsonl"),
|
|
7962
|
+
stderr: join6(dir, "stderr.txt"),
|
|
7963
|
+
checksLog: join6(dir, "checks.txt"),
|
|
7964
|
+
num_turns: null,
|
|
7965
|
+
cost: null,
|
|
7966
|
+
duration_ms: 0
|
|
7967
|
+
};
|
|
7968
|
+
}
|
|
7969
|
+
function writeRunJson(r) {
|
|
7970
|
+
writeFileSync2(join6(r.dir, "run.json"), `${JSON.stringify(r, null, 2)}
|
|
7971
|
+
`);
|
|
7972
|
+
}
|
|
7683
7973
|
|
|
7684
7974
|
class UsageError extends Error {
|
|
7685
7975
|
}
|
|
@@ -7735,16 +8025,16 @@ function newRunId() {
|
|
|
7735
8025
|
return `${ts}-${randomBytes(2).toString("hex")}`;
|
|
7736
8026
|
}
|
|
7737
8027
|
function gitignoreHasFiregoat(cwd) {
|
|
7738
|
-
const file =
|
|
7739
|
-
if (!
|
|
8028
|
+
const file = join6(cwd, ".gitignore");
|
|
8029
|
+
if (!existsSync4(file))
|
|
7740
8030
|
return false;
|
|
7741
|
-
return
|
|
8031
|
+
return readFileSync3(file, "utf8").split(`
|
|
7742
8032
|
`).map((l) => l.trim()).some((l) => /^\/?\.firegoat\/?(\*\*?)?$/.test(l));
|
|
7743
8033
|
}
|
|
7744
8034
|
function newRunRoot(label) {
|
|
7745
|
-
const runRoot = realpathSync2(mkdtempSync(
|
|
7746
|
-
const workdir =
|
|
7747
|
-
|
|
8035
|
+
const runRoot = realpathSync2(mkdtempSync(join6(tmpdir(), `firegoat-${label}-`)));
|
|
8036
|
+
const workdir = join6(runRoot, "work");
|
|
8037
|
+
mkdirSync5(workdir);
|
|
7748
8038
|
return { runRoot, workdir };
|
|
7749
8039
|
}
|
|
7750
8040
|
function isolate(iso, claudeBin, runRoot, workdir) {
|
|
@@ -7762,25 +8052,15 @@ function isolate(iso, claudeBin, runRoot, workdir) {
|
|
|
7762
8052
|
async function runOne(opts) {
|
|
7763
8053
|
const { test, n } = opts;
|
|
7764
8054
|
const started = Date.now();
|
|
7765
|
-
const dir =
|
|
7766
|
-
|
|
7767
|
-
const
|
|
7768
|
-
const
|
|
7769
|
-
test: test.name,
|
|
7770
|
-
n,
|
|
7771
|
-
status: "error",
|
|
7772
|
-
reason: "",
|
|
7773
|
-
workdir: null,
|
|
7774
|
-
session,
|
|
7775
|
-
checks: [],
|
|
7776
|
-
num_turns: null,
|
|
7777
|
-
cost: null,
|
|
7778
|
-
duration_ms: 0
|
|
7779
|
-
};
|
|
8055
|
+
const dir = join6(opts.runDir, test.name, String(n));
|
|
8056
|
+
mkdirSync5(dir, { recursive: true });
|
|
8057
|
+
const summary = blankRun(test, n, dir, render(test.prompt, opts.vars));
|
|
8058
|
+
const session = summary.session;
|
|
7780
8059
|
const done = (status, reason) => {
|
|
7781
8060
|
summary.status = status;
|
|
7782
8061
|
summary.reason = reason;
|
|
7783
8062
|
summary.duration_ms = Date.now() - started;
|
|
8063
|
+
writeRunJson(summary);
|
|
7784
8064
|
return summary;
|
|
7785
8065
|
};
|
|
7786
8066
|
const { runRoot, workdir } = newRunRoot(`${test.name}-${n}`);
|
|
@@ -7797,7 +8077,7 @@ async function runOne(opts) {
|
|
|
7797
8077
|
workdir,
|
|
7798
8078
|
commands: opts.workdirSetup,
|
|
7799
8079
|
env: hostEnv,
|
|
7800
|
-
logPath:
|
|
8080
|
+
logPath: join6(dir, "workdir_setup.txt")
|
|
7801
8081
|
});
|
|
7802
8082
|
if ("error" in prepared)
|
|
7803
8083
|
return done("error", `not graded: ${prepared.error}`);
|
|
@@ -7806,13 +8086,17 @@ async function runOne(opts) {
|
|
|
7806
8086
|
}
|
|
7807
8087
|
const agentPath = extra.PATH ? { PATH: extra.PATH } : {};
|
|
7808
8088
|
const outcome = await runClaude({
|
|
7809
|
-
env: {
|
|
8089
|
+
env: {
|
|
8090
|
+
...agentEnv(runRoot, opts.auth.method === "api-key" ? opts.auth.apiKey : undefined),
|
|
8091
|
+
...agentPath
|
|
8092
|
+
},
|
|
8093
|
+
keySource: opts.auth.method === "api-key" ? "ANTHROPIC_API_KEY" : "none",
|
|
7810
8094
|
profile: sandbox.profile,
|
|
7811
8095
|
bin: opts.claudeBin,
|
|
7812
|
-
prompt:
|
|
8096
|
+
prompt: summary.prompt,
|
|
7813
8097
|
cwd: workdir,
|
|
7814
8098
|
sessionPath: session,
|
|
7815
|
-
stderrPath:
|
|
8099
|
+
stderrPath: summary.stderr,
|
|
7816
8100
|
timeoutMs: test.timeout * 1000
|
|
7817
8101
|
});
|
|
7818
8102
|
if (outcome.result) {
|
|
@@ -7821,11 +8105,9 @@ async function runOne(opts) {
|
|
|
7821
8105
|
}
|
|
7822
8106
|
if (!outcome.ok)
|
|
7823
8107
|
return done("error", `not graded: ${outcome.reason}`);
|
|
7824
|
-
if (test.checks.length === 0)
|
|
7825
|
-
|
|
7826
|
-
|
|
7827
|
-
}
|
|
7828
|
-
const log = createWriteStream3(join5(dir, "checks.txt"));
|
|
8108
|
+
if (test.checks.length === 0)
|
|
8109
|
+
return done("pass", "no script checks; the goal is for the grader");
|
|
8110
|
+
const log = createWriteStream3(summary.checksLog);
|
|
7829
8111
|
const env = {
|
|
7830
8112
|
...process.env,
|
|
7831
8113
|
...hostEnv,
|
|
@@ -7847,7 +8129,7 @@ async function runOne(opts) {
|
|
|
7847
8129
|
} finally {
|
|
7848
8130
|
await new Promise((r) => log.end(r));
|
|
7849
8131
|
}
|
|
7850
|
-
summary.checks = results.map((r) =>
|
|
8132
|
+
summary.checks = results.map((r) => r.error === undefined ? { run: r.run, code: r.code } : { run: r.run, code: r.code, error: r.error });
|
|
7851
8133
|
const broken = results.find((r) => r.notStarted);
|
|
7852
8134
|
if (broken)
|
|
7853
8135
|
return done("error", `not graded: check ${broken.run}: ${broken.error}`);
|
|
@@ -7857,6 +8139,10 @@ async function runOne(opts) {
|
|
|
7857
8139
|
return done("pass", `${results.length}/${results.length} checks passed`);
|
|
7858
8140
|
}
|
|
7859
8141
|
async function evalCommand(args, cwd) {
|
|
8142
|
+
if (args.includes("--help") || args.includes("-h")) {
|
|
8143
|
+
console.log(EVAL_HELP);
|
|
8144
|
+
return EXIT.pass;
|
|
8145
|
+
}
|
|
7860
8146
|
let opts;
|
|
7861
8147
|
try {
|
|
7862
8148
|
opts = parseArgs(args);
|
|
@@ -7870,7 +8156,7 @@ async function evalCommand(args, cwd) {
|
|
|
7870
8156
|
const say = opts.json ? console.error : console.log;
|
|
7871
8157
|
const warn = console.error;
|
|
7872
8158
|
const root = findConfigDir(cwd);
|
|
7873
|
-
const config = loadConfig(root
|
|
8159
|
+
const config = loadConfig(root);
|
|
7874
8160
|
let tests = config.tests;
|
|
7875
8161
|
if (opts.names.length > 0) {
|
|
7876
8162
|
const known = new Set(tests.map((t) => t.name));
|
|
@@ -7897,45 +8183,58 @@ async function evalCommand(args, cwd) {
|
|
|
7897
8183
|
}
|
|
7898
8184
|
if (opts.host)
|
|
7899
8185
|
warn("warning: --host: the agent runs without isolation and can read and write your files");
|
|
7900
|
-
else if (process.platform !== "darwin" || !
|
|
8186
|
+
else if (process.platform !== "darwin" || !existsSync4(SANDBOX_EXEC)) {
|
|
7901
8187
|
console.error(`firegoat eval: isolation needs macOS (${SANDBOX_EXEC}); this is ${process.platform}. In CI or Linux, run inside a throwaway container and pass --host.`);
|
|
7902
8188
|
return EXIT.usage;
|
|
7903
8189
|
}
|
|
7904
8190
|
const iso = {
|
|
7905
8191
|
host: opts.host,
|
|
7906
8192
|
repo: root,
|
|
7907
|
-
repoFile:
|
|
8193
|
+
repoFile: join6(root, "firegoat.yaml")
|
|
7908
8194
|
};
|
|
7909
|
-
|
|
8195
|
+
let auth;
|
|
8196
|
+
try {
|
|
8197
|
+
auth = resolveAuth(process.env);
|
|
8198
|
+
} catch (e) {
|
|
8199
|
+
if (e instanceof AuthError) {
|
|
8200
|
+
console.error(`firegoat eval: ${e.message}`);
|
|
8201
|
+
return EXIT.usage;
|
|
8202
|
+
}
|
|
8203
|
+
throw e;
|
|
8204
|
+
}
|
|
8205
|
+
say(`auth: ${auth.label}`);
|
|
8206
|
+
if (auth.method === "claude" && process.env.ANTHROPIC_API_KEY)
|
|
8207
|
+
warn("note: ANTHROPIC_API_KEY in your shell is not passed to the agent (it is used only in CI, or after `firegoat init --auth api-key`)");
|
|
8208
|
+
mkdirSync5(claudeConfigDir(), { recursive: true });
|
|
7910
8209
|
const pre = newRunRoot("preflight");
|
|
7911
8210
|
const preSandbox = isolate(iso, claudePath, pre.runRoot, pre.workdir);
|
|
7912
|
-
const loginProblem = "error" in preSandbox ? preSandbox.error : checkLogin({
|
|
8211
|
+
const loginProblem = "error" in preSandbox ? preSandbox.error : auth.method === "claude" ? checkLogin({
|
|
7913
8212
|
bin: claudePath,
|
|
7914
8213
|
env: agentEnv(pre.runRoot),
|
|
7915
8214
|
profile: preSandbox.profile,
|
|
7916
8215
|
cwd: pre.workdir
|
|
7917
|
-
});
|
|
8216
|
+
}) : undefined;
|
|
7918
8217
|
if (loginProblem) {
|
|
7919
8218
|
console.error(`firegoat eval: not graded: ${loginProblem}`);
|
|
7920
8219
|
return EXIT.notGraded;
|
|
7921
8220
|
}
|
|
7922
|
-
|
|
8221
|
+
mkdirSync5(join6(root, ".firegoat"), { recursive: true });
|
|
7923
8222
|
if (!gitignoreHasFiregoat(root))
|
|
7924
8223
|
warn("hint: add .firegoat/ to .gitignore (it holds run traces)");
|
|
7925
8224
|
const runId = newRunId();
|
|
7926
|
-
const runDir =
|
|
7927
|
-
|
|
7928
|
-
const summaryPath =
|
|
8225
|
+
const runDir = join6(root, ".firegoat", "runs", runId);
|
|
8226
|
+
mkdirSync5(runDir, { recursive: true });
|
|
8227
|
+
const summaryPath = join6(runDir, "summary.json");
|
|
7929
8228
|
let out = null;
|
|
7930
8229
|
if (config.setup.length > 0) {
|
|
7931
8230
|
say("setup...");
|
|
7932
|
-
out =
|
|
8231
|
+
out = join6(runDir, "out");
|
|
7933
8232
|
const error = await setupOnce({
|
|
7934
8233
|
commands: config.setup,
|
|
7935
8234
|
root,
|
|
7936
8235
|
out,
|
|
7937
8236
|
env: { ...process.env, ...vars },
|
|
7938
|
-
logPath:
|
|
8237
|
+
logPath: join6(runDir, "setup.txt")
|
|
7939
8238
|
});
|
|
7940
8239
|
if (error) {
|
|
7941
8240
|
console.error(`firegoat eval: not graded: ${error}`);
|
|
@@ -7951,10 +8250,12 @@ async function evalCommand(args, cwd) {
|
|
|
7951
8250
|
runId,
|
|
7952
8251
|
runsDir: runDir,
|
|
7953
8252
|
isolation: opts.host ? "host" : "seatbelt",
|
|
8253
|
+
auth: auth.method,
|
|
8254
|
+
grading: GRADING_HINT,
|
|
7954
8255
|
totals,
|
|
7955
8256
|
runs: results
|
|
7956
8257
|
};
|
|
7957
|
-
|
|
8258
|
+
writeFileSync2(summaryPath, `${JSON.stringify(doc, null, 2)}
|
|
7958
8259
|
`);
|
|
7959
8260
|
return doc;
|
|
7960
8261
|
};
|
|
@@ -7968,24 +8269,18 @@ async function evalCommand(args, cwd) {
|
|
|
7968
8269
|
runDir,
|
|
7969
8270
|
claudeBin: claudePath,
|
|
7970
8271
|
iso,
|
|
8272
|
+
auth,
|
|
7971
8273
|
out,
|
|
7972
8274
|
workdirSetup: config.workdirSetup,
|
|
7973
8275
|
vars,
|
|
7974
8276
|
warn
|
|
7975
8277
|
});
|
|
7976
8278
|
} catch (e) {
|
|
7977
|
-
|
|
7978
|
-
|
|
7979
|
-
|
|
7980
|
-
|
|
7981
|
-
|
|
7982
|
-
workdir: null,
|
|
7983
|
-
session: join5(runDir, test.name, String(n), "session.jsonl"),
|
|
7984
|
-
checks: [],
|
|
7985
|
-
num_turns: null,
|
|
7986
|
-
cost: null,
|
|
7987
|
-
duration_ms: 0
|
|
7988
|
-
};
|
|
8279
|
+
const dir = join6(runDir, test.name, String(n));
|
|
8280
|
+
mkdirSync5(dir, { recursive: true });
|
|
8281
|
+
r = blankRun(test, n, dir, test.prompt);
|
|
8282
|
+
r.reason = `not graded: firegoat crashed: ${e.message}`;
|
|
8283
|
+
writeRunJson(r);
|
|
7989
8284
|
}
|
|
7990
8285
|
results.push(r);
|
|
7991
8286
|
writeSummary();
|
|
@@ -7997,23 +8292,18 @@ async function evalCommand(args, cwd) {
|
|
|
7997
8292
|
say(`
|
|
7998
8293
|
${pass} passed, ${fail} failed, ${error} not graded (${results.length} runs)`);
|
|
7999
8294
|
say(`runs: ${runDir}`);
|
|
8295
|
+
say(`goal and rubric are not graded by firegoat; per run: ${join6(runDir, "<test>", "<n>", "run.json")}`);
|
|
8000
8296
|
if (opts.json)
|
|
8001
8297
|
console.log(JSON.stringify(doc, null, 2));
|
|
8002
8298
|
return error > 0 ? EXIT.notGraded : fail > 0 ? EXIT.fail : EXIT.pass;
|
|
8003
8299
|
}
|
|
8004
8300
|
|
|
8005
8301
|
// src/init.ts
|
|
8006
|
-
import { spawnSync as
|
|
8007
|
-
import { existsSync as
|
|
8008
|
-
import { basename as basename2, join as
|
|
8302
|
+
import { spawnSync as spawnSync4 } from "node:child_process";
|
|
8303
|
+
import { existsSync as existsSync5, readFileSync as readFileSync4, writeFileSync as writeFileSync3 } from "node:fs";
|
|
8304
|
+
import { basename as basename2, join as join7 } from "node:path";
|
|
8009
8305
|
import { createInterface as createInterface2 } from "node:readline/promises";
|
|
8010
|
-
|
|
8011
|
-
|
|
8012
|
-
--yes, -y don't ask questions (default when stdin is not a terminal)
|
|
8013
|
-
--force, -f overwrite an existing ${CONFIG_FILE}
|
|
8014
|
-
--skills install the firegoat-onboarding and suggesting-eval-prompts skills
|
|
8015
|
-
into .claude/skills/ (npx skills add team2027/skills)
|
|
8016
|
-
`;
|
|
8306
|
+
import { Writable } from "node:stream";
|
|
8017
8307
|
var SKILLS = ["firegoat-onboarding", "suggesting-eval-prompts"];
|
|
8018
8308
|
var SKILLS_ARGV = [
|
|
8019
8309
|
"skills",
|
|
@@ -8027,7 +8317,7 @@ var SKILLS_ARGV = [
|
|
|
8027
8317
|
var GITIGNORE_LINE = ".firegoat/";
|
|
8028
8318
|
function readText(file) {
|
|
8029
8319
|
try {
|
|
8030
|
-
return
|
|
8320
|
+
return readFileSync4(file, "utf8");
|
|
8031
8321
|
} catch {
|
|
8032
8322
|
return;
|
|
8033
8323
|
}
|
|
@@ -8053,9 +8343,9 @@ function tomlSections(text) {
|
|
|
8053
8343
|
return sections;
|
|
8054
8344
|
}
|
|
8055
8345
|
function detect(cwd) {
|
|
8056
|
-
const readme = ["README.md", "README", "readme.md", "Readme.md"].find((f) =>
|
|
8346
|
+
const readme = ["README.md", "README", "readme.md", "Readme.md"].find((f) => existsSync5(join7(cwd, f)));
|
|
8057
8347
|
const fallback = { name: basename2(cwd), library: true, readme };
|
|
8058
|
-
const pkgText = readText(
|
|
8348
|
+
const pkgText = readText(join7(cwd, "package.json"));
|
|
8059
8349
|
if (pkgText !== undefined) {
|
|
8060
8350
|
try {
|
|
8061
8351
|
const pkg = JSON.parse(pkgText);
|
|
@@ -8070,7 +8360,7 @@ function detect(cwd) {
|
|
|
8070
8360
|
return { ecosystem: "npm", name, bin, library, readme, docsUrl };
|
|
8071
8361
|
} catch {}
|
|
8072
8362
|
}
|
|
8073
|
-
const pyText = readText(
|
|
8363
|
+
const pyText = readText(join7(cwd, "pyproject.toml"));
|
|
8074
8364
|
if (pyText !== undefined) {
|
|
8075
8365
|
const s = tomlSections(pyText);
|
|
8076
8366
|
const name = s.get("project")?.get("name") ?? s.get("tool.poetry")?.get("name") ?? fallback.name;
|
|
@@ -8080,12 +8370,12 @@ function detect(cwd) {
|
|
|
8080
8370
|
const docsUrl = urls?.get("Documentation") ?? urls?.get("documentation") ?? urls?.get("Homepage") ?? urls?.get("homepage");
|
|
8081
8371
|
return { ecosystem: "python", name, bin, library: true, readme, docsUrl };
|
|
8082
8372
|
}
|
|
8083
|
-
const goText = readText(
|
|
8373
|
+
const goText = readText(join7(cwd, "go.mod"));
|
|
8084
8374
|
if (goText !== undefined) {
|
|
8085
8375
|
const name = goText.match(/^module\s+(\S+)/m)?.[1] ?? fallback.name;
|
|
8086
8376
|
return { ecosystem: "go", name, library: true, readme };
|
|
8087
8377
|
}
|
|
8088
|
-
const cargoText = readText(
|
|
8378
|
+
const cargoText = readText(join7(cwd, "Cargo.toml"));
|
|
8089
8379
|
if (cargoText !== undefined) {
|
|
8090
8380
|
const name = tomlSections(cargoText).get("package")?.get("name");
|
|
8091
8381
|
return {
|
|
@@ -8210,10 +8500,10 @@ function renderYaml(p, tests) {
|
|
|
8210
8500
|
`;
|
|
8211
8501
|
}
|
|
8212
8502
|
function ensureGitignore(cwd) {
|
|
8213
|
-
const file =
|
|
8503
|
+
const file = join7(cwd, ".gitignore");
|
|
8214
8504
|
const text = readText(file);
|
|
8215
8505
|
if (text === undefined) {
|
|
8216
|
-
|
|
8506
|
+
writeFileSync3(file, `${GITIGNORE_LINE}
|
|
8217
8507
|
`);
|
|
8218
8508
|
return `created .gitignore with ${GITIGNORE_LINE}`;
|
|
8219
8509
|
}
|
|
@@ -8223,12 +8513,12 @@ function ensureGitignore(cwd) {
|
|
|
8223
8513
|
const sep = text === "" || text.endsWith(`
|
|
8224
8514
|
`) ? "" : `
|
|
8225
8515
|
`;
|
|
8226
|
-
|
|
8516
|
+
writeFileSync3(file, `${text}${sep}${GITIGNORE_LINE}
|
|
8227
8517
|
`);
|
|
8228
8518
|
return `added ${GITIGNORE_LINE} to .gitignore`;
|
|
8229
8519
|
}
|
|
8230
8520
|
function installSkills(cwd) {
|
|
8231
|
-
const r =
|
|
8521
|
+
const r = spawnSync4("npx", ["-y", ...SKILLS_ARGV], {
|
|
8232
8522
|
cwd,
|
|
8233
8523
|
stdio: "inherit"
|
|
8234
8524
|
});
|
|
@@ -8238,6 +8528,69 @@ function installSkills(cwd) {
|
|
|
8238
8528
|
return `exit ${r.status ?? "by signal"}`;
|
|
8239
8529
|
return;
|
|
8240
8530
|
}
|
|
8531
|
+
async function ask(question) {
|
|
8532
|
+
const rl = createInterface2({ input: process.stdin, output: process.stdout });
|
|
8533
|
+
try {
|
|
8534
|
+
return (await rl.question(question)).trim();
|
|
8535
|
+
} finally {
|
|
8536
|
+
rl.close();
|
|
8537
|
+
}
|
|
8538
|
+
}
|
|
8539
|
+
async function readSecret(question) {
|
|
8540
|
+
if (!process.stdin.isTTY) {
|
|
8541
|
+
const chunks = [];
|
|
8542
|
+
for await (const c of process.stdin)
|
|
8543
|
+
chunks.push(c);
|
|
8544
|
+
return Buffer.concat(chunks).toString("utf8").trim();
|
|
8545
|
+
}
|
|
8546
|
+
process.stdout.write(question);
|
|
8547
|
+
const silent = new Writable({ write: (_c, _e, cb) => cb() });
|
|
8548
|
+
const rl = createInterface2({
|
|
8549
|
+
input: process.stdin,
|
|
8550
|
+
output: silent,
|
|
8551
|
+
terminal: true
|
|
8552
|
+
});
|
|
8553
|
+
try {
|
|
8554
|
+
return (await rl.question("")).trim();
|
|
8555
|
+
} finally {
|
|
8556
|
+
rl.close();
|
|
8557
|
+
process.stdout.write(`
|
|
8558
|
+
`);
|
|
8559
|
+
}
|
|
8560
|
+
}
|
|
8561
|
+
async function chooseAuth() {
|
|
8562
|
+
let current;
|
|
8563
|
+
try {
|
|
8564
|
+
current = resolveAuth({}).label;
|
|
8565
|
+
} catch (e) {
|
|
8566
|
+
current = e.message;
|
|
8567
|
+
}
|
|
8568
|
+
console.log(`How should the agent sign in? (now: ${current})
|
|
8569
|
+
1) Login with Claude (your Claude plan)
|
|
8570
|
+
2) Paste an Anthropic API key`);
|
|
8571
|
+
for (;; ) {
|
|
8572
|
+
const a = await ask("choose [1/2]: ");
|
|
8573
|
+
if (a === "1")
|
|
8574
|
+
return "claude";
|
|
8575
|
+
if (a === "2")
|
|
8576
|
+
return "api-key";
|
|
8577
|
+
console.log("type 1 or 2");
|
|
8578
|
+
}
|
|
8579
|
+
}
|
|
8580
|
+
async function setUpAuth(method) {
|
|
8581
|
+
if (method === "claude") {
|
|
8582
|
+
const code = claudeLoginCommand([]);
|
|
8583
|
+
return code === 0 ? undefined : code;
|
|
8584
|
+
}
|
|
8585
|
+
const key = await readSecret("Anthropic API key (input hidden): ");
|
|
8586
|
+
if (!looksLikeAnthropicKey(key)) {
|
|
8587
|
+
console.error("firegoat init: that does not look like an Anthropic API key (sk-ant-...); nothing stored");
|
|
8588
|
+
return EXIT.usage;
|
|
8589
|
+
}
|
|
8590
|
+
const file = saveApiKey(key);
|
|
8591
|
+
console.log(`stored the API key in ${file} (mode 0600, not in the repo); evals pass it to the agent as ANTHROPIC_API_KEY`);
|
|
8592
|
+
return;
|
|
8593
|
+
}
|
|
8241
8594
|
async function askYesNo(question) {
|
|
8242
8595
|
const rl = createInterface2({ input: process.stdin, output: process.stdout });
|
|
8243
8596
|
try {
|
|
@@ -8261,7 +8614,18 @@ async function initCommand(args, cwd) {
|
|
|
8261
8614
|
let yes = false;
|
|
8262
8615
|
let force = false;
|
|
8263
8616
|
let skills = false;
|
|
8264
|
-
|
|
8617
|
+
let auth;
|
|
8618
|
+
for (let i = 0;i < args.length; i++) {
|
|
8619
|
+
const a = args[i];
|
|
8620
|
+
if (a === "--auth" || a.startsWith("--auth=")) {
|
|
8621
|
+
const v = a === "--auth" ? args[++i] : a.slice(7);
|
|
8622
|
+
if (v !== "claude" && v !== "api-key") {
|
|
8623
|
+
console.error(`firegoat init: --auth must be claude or api-key, got ${v ?? "nothing"}`);
|
|
8624
|
+
return EXIT.usage;
|
|
8625
|
+
}
|
|
8626
|
+
auth = v;
|
|
8627
|
+
continue;
|
|
8628
|
+
}
|
|
8265
8629
|
switch (a) {
|
|
8266
8630
|
case "--yes":
|
|
8267
8631
|
case "-y":
|
|
@@ -8276,18 +8640,18 @@ async function initCommand(args, cwd) {
|
|
|
8276
8640
|
break;
|
|
8277
8641
|
case "--help":
|
|
8278
8642
|
case "-h":
|
|
8279
|
-
console.log(
|
|
8643
|
+
console.log(INIT_HELP);
|
|
8280
8644
|
return 0;
|
|
8281
8645
|
default:
|
|
8282
8646
|
console.error(`firegoat init: unknown argument: ${a}
|
|
8283
8647
|
|
|
8284
|
-
${
|
|
8648
|
+
${INIT_HELP}`);
|
|
8285
8649
|
return EXIT.usage;
|
|
8286
8650
|
}
|
|
8287
8651
|
}
|
|
8288
|
-
const configPath =
|
|
8289
|
-
const keepConfig =
|
|
8290
|
-
if (keepConfig && !skills) {
|
|
8652
|
+
const configPath = join7(cwd, CONFIG_FILE);
|
|
8653
|
+
const keepConfig = existsSync5(configPath) && !force;
|
|
8654
|
+
if (keepConfig && !skills && !auth) {
|
|
8291
8655
|
console.error(`firegoat init: ${CONFIG_FILE} already exists; not overwriting. Use --force to replace it.`);
|
|
8292
8656
|
return EXIT.usage;
|
|
8293
8657
|
}
|
|
@@ -8297,11 +8661,18 @@ ${INIT_USAGE}`);
|
|
|
8297
8661
|
const project = detect(cwd);
|
|
8298
8662
|
const tests = templates(project);
|
|
8299
8663
|
console.log(`detected: ${describe(project)}`);
|
|
8300
|
-
|
|
8664
|
+
writeFileSync3(configPath, renderYaml(project, tests));
|
|
8301
8665
|
console.log(`wrote ${CONFIG_FILE} (${tests.length} test${tests.length === 1 ? "" : "s"}: ${tests.map((t) => t.name).join(", ")}); edit the prompts to fit your users`);
|
|
8302
8666
|
}
|
|
8303
8667
|
console.log(ensureGitignore(cwd));
|
|
8304
8668
|
const interactive = !yes && process.stdin.isTTY && process.stdout.isTTY;
|
|
8669
|
+
if (!auth && interactive)
|
|
8670
|
+
auth = await chooseAuth();
|
|
8671
|
+
if (auth) {
|
|
8672
|
+
const failed = await setUpAuth(auth);
|
|
8673
|
+
if (failed !== undefined)
|
|
8674
|
+
return failed;
|
|
8675
|
+
}
|
|
8305
8676
|
if (!skills && interactive)
|
|
8306
8677
|
skills = await askYesNo(`Add the ${SKILLS.join(" and ")} skills to .claude/skills/ (help your agent draft better evals)?`);
|
|
8307
8678
|
if (skills) {
|
|
@@ -8318,47 +8689,10 @@ ${INIT_USAGE}`);
|
|
|
8318
8689
|
return 0;
|
|
8319
8690
|
}
|
|
8320
8691
|
|
|
8321
|
-
// src/login.ts
|
|
8322
|
-
import { spawnSync as spawnSync4 } from "node:child_process";
|
|
8323
|
-
import { mkdirSync as mkdirSync4 } from "node:fs";
|
|
8324
|
-
function loginCommand(args) {
|
|
8325
|
-
let claudeBin = process.env.FIREGOAT_CLAUDE_BIN || "claude";
|
|
8326
|
-
for (let i = 0;i < args.length; i++) {
|
|
8327
|
-
const a = args[i];
|
|
8328
|
-
if (a === "--claude-bin" && args[i + 1] !== undefined)
|
|
8329
|
-
claudeBin = args[++i];
|
|
8330
|
-
else if (a.startsWith("--claude-bin="))
|
|
8331
|
-
claudeBin = a.slice(13);
|
|
8332
|
-
else {
|
|
8333
|
-
console.error(`firegoat login: unknown argument: ${a}`);
|
|
8334
|
-
return EXIT.usage;
|
|
8335
|
-
}
|
|
8336
|
-
}
|
|
8337
|
-
const configDir = claudeConfigDir();
|
|
8338
|
-
mkdirSync4(configDir, { recursive: true });
|
|
8339
|
-
const env = {
|
|
8340
|
-
...process.env,
|
|
8341
|
-
CLAUDE_CONFIG_DIR: configDir
|
|
8342
|
-
};
|
|
8343
|
-
delete env.ANTHROPIC_API_KEY;
|
|
8344
|
-
console.error(`Signing claude in to ${configDir}. Evals run with this login, never your own ~/.claude.`);
|
|
8345
|
-
const r = spawnSync4(claudeBin, ["auth", "login"], { env, stdio: "inherit" });
|
|
8346
|
-
if (r.error) {
|
|
8347
|
-
console.error(`firegoat login: could not run ${claudeBin}: ${r.error.message}`);
|
|
8348
|
-
return EXIT.notGraded;
|
|
8349
|
-
}
|
|
8350
|
-
if (r.status !== 0) {
|
|
8351
|
-
console.error(`firegoat login: claude auth login exited ${r.status}`);
|
|
8352
|
-
return EXIT.notGraded;
|
|
8353
|
-
}
|
|
8354
|
-
console.log("logged in. next: npx firegoat eval");
|
|
8355
|
-
return EXIT.pass;
|
|
8356
|
-
}
|
|
8357
|
-
|
|
8358
8692
|
// src/open.ts
|
|
8359
8693
|
import { spawnSync as spawnSync5 } from "node:child_process";
|
|
8360
|
-
import { existsSync as
|
|
8361
|
-
import { join as
|
|
8694
|
+
import { existsSync as existsSync6, readdirSync } from "node:fs";
|
|
8695
|
+
import { join as join8 } from "node:path";
|
|
8362
8696
|
var OPENER = {
|
|
8363
8697
|
darwin: "open",
|
|
8364
8698
|
linux: "xdg-open"
|
|
@@ -8367,6 +8701,10 @@ function openCommand(args, cwd) {
|
|
|
8367
8701
|
let print = false;
|
|
8368
8702
|
const ids = [];
|
|
8369
8703
|
for (const a of args) {
|
|
8704
|
+
if (a === "--help" || a === "-h") {
|
|
8705
|
+
console.log(OPEN_HELP);
|
|
8706
|
+
return EXIT.pass;
|
|
8707
|
+
}
|
|
8370
8708
|
if (a === "--print")
|
|
8371
8709
|
print = true;
|
|
8372
8710
|
else if (a.startsWith("-")) {
|
|
@@ -8379,8 +8717,8 @@ function openCommand(args, cwd) {
|
|
|
8379
8717
|
console.error("firegoat open: takes at most one run id");
|
|
8380
8718
|
return EXIT.usage;
|
|
8381
8719
|
}
|
|
8382
|
-
const base =
|
|
8383
|
-
const runs =
|
|
8720
|
+
const base = join8(findConfigDir(cwd), ".firegoat", "runs");
|
|
8721
|
+
const runs = existsSync6(base) ? readdirSync(base).sort() : [];
|
|
8384
8722
|
const id = ids[0] ?? runs.at(-1);
|
|
8385
8723
|
if (!id) {
|
|
8386
8724
|
console.error(`firegoat open: no runs in ${base}. Run: npx firegoat eval`);
|
|
@@ -8390,7 +8728,7 @@ function openCommand(args, cwd) {
|
|
|
8390
8728
|
console.error(`firegoat open: no run ${JSON.stringify(id)} in ${base} (latest: ${runs.at(-1) ?? "none"})`);
|
|
8391
8729
|
return EXIT.usage;
|
|
8392
8730
|
}
|
|
8393
|
-
const dir =
|
|
8731
|
+
const dir = join8(base, id);
|
|
8394
8732
|
console.log(dir);
|
|
8395
8733
|
const opener = OPENER[process.platform];
|
|
8396
8734
|
if (print || !opener)
|
|
@@ -8404,32 +8742,6 @@ function openCommand(args, cwd) {
|
|
|
8404
8742
|
}
|
|
8405
8743
|
|
|
8406
8744
|
// src/cli.ts
|
|
8407
|
-
var USAGE = `firegoat ${package_default.version}: local-first AX evals
|
|
8408
|
-
|
|
8409
|
-
Usage:
|
|
8410
|
-
firegoat init [--yes] [--force] [--skills] write firegoat.yaml with starter evals
|
|
8411
|
-
firegoat login [--claude-bin PATH] sign claude in to firegoat's own config dir (once)
|
|
8412
|
-
firegoat eval [test..] [--runs N] [--var KEY=value].. [--json] [--host] [--claude-bin PATH]
|
|
8413
|
-
run evals with claude in a sandboxed fresh workdir
|
|
8414
|
-
firegoat open [runId] [--print] open a run's folder (default: the latest)
|
|
8415
|
-
firegoat --version
|
|
8416
|
-
|
|
8417
|
-
eval options:
|
|
8418
|
-
test.. only run these tests (by name)
|
|
8419
|
-
--runs N runs per test (overrides runs: in firegoat.yaml)
|
|
8420
|
-
--var KEY=value fills {{vars.KEY}} in prompts and checks; env for setup, workdir_setup and checks
|
|
8421
|
-
--json JSON summary on stdout, progress on stderr
|
|
8422
|
-
--host no sandbox (needed off macOS; use a throwaway container)
|
|
8423
|
-
--claude-bin PATH claude binary to run (env: FIREGOAT_CLAUDE_BIN; default: claude on PATH)
|
|
8424
|
-
|
|
8425
|
-
firegoat.yaml is looked up in the current dir, then parents up to the git root.
|
|
8426
|
-
|
|
8427
|
-
Exit codes:
|
|
8428
|
-
0 every run passed
|
|
8429
|
-
1 a goal or check failed
|
|
8430
|
-
2 at least one run was not graded (claude missing, auth, crash, timeout); wins over 1
|
|
8431
|
-
3 usage or config error (bad flag, unknown test, invalid or missing firegoat.yaml)
|
|
8432
|
-
`;
|
|
8433
8745
|
async function main(argv) {
|
|
8434
8746
|
const [cmd, ...rest] = argv;
|
|
8435
8747
|
switch (cmd) {
|
|
@@ -8440,12 +8752,24 @@ async function main(argv) {
|
|
|
8440
8752
|
case undefined:
|
|
8441
8753
|
case "--help":
|
|
8442
8754
|
case "-h":
|
|
8443
|
-
console.log(
|
|
8755
|
+
console.log(topHelp(package_default.version));
|
|
8444
8756
|
return 0;
|
|
8445
8757
|
case "init":
|
|
8446
8758
|
return initCommand(rest, process.cwd());
|
|
8759
|
+
case "claude":
|
|
8760
|
+
if (rest[0] === "login")
|
|
8761
|
+
return claudeLoginCommand(rest.slice(1));
|
|
8762
|
+
if (rest[0] === "--help" || rest[0] === "-h") {
|
|
8763
|
+
console.log(CLAUDE_LOGIN_HELP);
|
|
8764
|
+
return 0;
|
|
8765
|
+
}
|
|
8766
|
+
console.error(`firegoat claude: unknown subcommand
|
|
8767
|
+
|
|
8768
|
+
${CLAUDE_LOGIN_HELP}`);
|
|
8769
|
+
return EXIT.usage;
|
|
8447
8770
|
case "login":
|
|
8448
|
-
|
|
8771
|
+
console.error("firegoat login is deprecated for the Claude sign-in: use `firegoat claude login`");
|
|
8772
|
+
return claudeLoginCommand(rest);
|
|
8449
8773
|
case "eval":
|
|
8450
8774
|
return evalCommand(rest, process.cwd());
|
|
8451
8775
|
case "open":
|
|
@@ -8453,7 +8777,7 @@ async function main(argv) {
|
|
|
8453
8777
|
default:
|
|
8454
8778
|
console.error(`unknown command: ${cmd}
|
|
8455
8779
|
|
|
8456
|
-
${
|
|
8780
|
+
${topHelp(package_default.version)}`);
|
|
8457
8781
|
return EXIT.usage;
|
|
8458
8782
|
}
|
|
8459
8783
|
}
|