firegoat 0.5.0 → 0.6.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 +15 -5
- package/dist/cli.js +448 -263
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -2,13 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
Local-first AX evals: how well coding agents use your SDK, CLI, API and docs.
|
|
4
4
|
|
|
5
|
-
Status: early (v0.
|
|
5
|
+
Status: early (v0.6). See `plan.md`.
|
|
6
6
|
|
|
7
7
|
## Use
|
|
8
8
|
|
|
9
9
|
```bash
|
|
10
|
-
npx firegoat init # writes firegoat.yaml with starter evals
|
|
11
|
-
|
|
10
|
+
npx firegoat init # writes firegoat.yaml with starter evals, asks how the agent signs in;
|
|
11
|
+
# --skills adds the firegoat-onboarding skill (from team2027/skills)
|
|
12
|
+
npx firegoat claude login # sign-in option 1, once per machine: claude in ~/.firegoat/claude
|
|
12
13
|
npx firegoat eval # runs each test with your local `claude`, sandboxed, in a fresh folder
|
|
13
14
|
npx firegoat open # opens the latest run's folder (--print to only print it)
|
|
14
15
|
```
|
|
@@ -36,14 +37,23 @@ Then say so in the prompt: "The package is already installed from the build unde
|
|
|
36
37
|
- Checks also get `$FIREGOAT_OUT` (the run's copy).
|
|
37
38
|
- A failed `setup:` or `workdir_setup:` means not graded (exit 2); logs are `.firegoat/runs/<runId>/setup.txt` and `<test>/<n>/workdir_setup.txt`.
|
|
38
39
|
|
|
40
|
+
### How the agent signs in
|
|
41
|
+
|
|
42
|
+
`firegoat init` asks (or pass `--auth`):
|
|
43
|
+
|
|
44
|
+
1. **Login with Claude** (`--auth claude`, same as `npx firegoat claude login`): your Claude plan, in firegoat's own config dir `~/.firegoat/claude`, never your `~/.claude`.
|
|
45
|
+
2. **Paste an Anthropic API key** (`--auth api-key`, piped on stdin or typed hidden): stored for firegoat only in `~/.firegoat/anthropic-api-key` (mode 0600), never in `firegoat.yaml` or the repo, and passed to the sandboxed claude as `ANTHROPIC_API_KEY`.
|
|
46
|
+
|
|
47
|
+
`eval` prints which one it uses. In CI (`CI` set), `ANTHROPIC_API_KEY` from the env wins. Outside CI, a key in your shell is not passed to the agent, so it never silently replaces the Claude login. `firegoat login` still works as a deprecated alias of `firegoat claude login`; the name is reserved for firegoat.ai.
|
|
48
|
+
|
|
39
49
|
### Isolation
|
|
40
50
|
|
|
41
51
|
On macOS, `eval` runs `claude` inside a Seatbelt (`sandbox-exec`) profile:
|
|
42
52
|
|
|
43
|
-
- it signs in
|
|
53
|
+
- it signs in as chosen above, never with your `~/.claude`;
|
|
44
54
|
- it cannot read your home dir or the repo, except toolchains (node, bun, cargo, ...) it needs to run;
|
|
45
55
|
- it can write only in its run dir (`work/`, its own `TMPDIR` and package caches) and system temp;
|
|
46
|
-
- `ANTHROPIC_API_KEY` is
|
|
56
|
+
- `ANTHROPIC_API_KEY` is set only for the API-key sign-in; firegoat checks claude's reported key source on every run.
|
|
47
57
|
|
|
48
58
|
A probe checks the sandbox before every run; if it does not hold, the run is not graded. Network is open.
|
|
49
59
|
|
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.6.0",
|
|
6951
6951
|
repository: {
|
|
6952
6952
|
type: "git",
|
|
6953
6953
|
url: "git+https://github.com/team2027/firegoat-cli.git"
|
|
@@ -6987,9 +6987,266 @@ 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/claude-login.ts
|
|
7212
|
+
function claudeLoginCommand(args) {
|
|
7213
|
+
let claudeBin = process.env.FIREGOAT_CLAUDE_BIN || "claude";
|
|
7214
|
+
for (let i = 0;i < args.length; i++) {
|
|
7215
|
+
const a = args[i];
|
|
7216
|
+
if (a === "--claude-bin" && args[i + 1] !== undefined)
|
|
7217
|
+
claudeBin = args[++i];
|
|
7218
|
+
else if (a.startsWith("--claude-bin="))
|
|
7219
|
+
claudeBin = a.slice(13);
|
|
7220
|
+
else {
|
|
7221
|
+
console.error(`firegoat claude login: unknown argument: ${a}`);
|
|
7222
|
+
return EXIT.usage;
|
|
7223
|
+
}
|
|
7224
|
+
}
|
|
7225
|
+
const configDir = claudeConfigDir();
|
|
7226
|
+
mkdirSync3(configDir, { recursive: true });
|
|
7227
|
+
const env = {
|
|
7228
|
+
...process.env,
|
|
7229
|
+
CLAUDE_CONFIG_DIR: configDir
|
|
7230
|
+
};
|
|
7231
|
+
delete env.ANTHROPIC_API_KEY;
|
|
7232
|
+
console.error(`Signing claude in to ${configDir}. Evals run with this login, never your own ~/.claude.`);
|
|
7233
|
+
const r = spawnSync2(claudeBin, ["auth", "login"], { env, stdio: "inherit" });
|
|
7234
|
+
if (r.error) {
|
|
7235
|
+
console.error(`firegoat claude login: could not run ${claudeBin}: ${r.error.message}`);
|
|
7236
|
+
return EXIT.notGraded;
|
|
7237
|
+
}
|
|
7238
|
+
if (r.status !== 0) {
|
|
7239
|
+
console.error(`firegoat claude login: claude auth login exited ${r.status}`);
|
|
7240
|
+
return EXIT.notGraded;
|
|
7241
|
+
}
|
|
7242
|
+
saveMethod("claude");
|
|
7243
|
+
console.log("logged in; evals now sign in with this Claude login. next: npx firegoat eval");
|
|
7244
|
+
return EXIT.pass;
|
|
7245
|
+
}
|
|
7246
|
+
|
|
6990
7247
|
// src/config.ts
|
|
6991
|
-
import { existsSync, readFileSync } from "node:fs";
|
|
6992
|
-
import { dirname, join } from "node:path";
|
|
7248
|
+
import { existsSync as existsSync3, readFileSync as readFileSync2 } from "node:fs";
|
|
7249
|
+
import { dirname as dirname2, join as join3 } from "node:path";
|
|
6993
7250
|
|
|
6994
7251
|
// node_modules/yaml/dist/index.js
|
|
6995
7252
|
var composer = require_composer();
|
|
@@ -7169,17 +7426,17 @@ function render(text, vars) {
|
|
|
7169
7426
|
}
|
|
7170
7427
|
function loadConfig(dir, warn) {
|
|
7171
7428
|
const file = `${dir}/${CONFIG_FILE}`;
|
|
7172
|
-
if (!
|
|
7429
|
+
if (!existsSync3(file))
|
|
7173
7430
|
throw new ConfigError(`${CONFIG_FILE} not found in ${dir}. Run: npx firegoat init`);
|
|
7174
|
-
return parseConfig(
|
|
7431
|
+
return parseConfig(readFileSync2(file, "utf8"), warn);
|
|
7175
7432
|
}
|
|
7176
7433
|
function findConfigDir(cwd) {
|
|
7177
7434
|
let dir = cwd;
|
|
7178
7435
|
for (;; ) {
|
|
7179
|
-
if (
|
|
7436
|
+
if (existsSync3(join3(dir, CONFIG_FILE)))
|
|
7180
7437
|
return dir;
|
|
7181
|
-
const parent =
|
|
7182
|
-
if (
|
|
7438
|
+
const parent = dirname2(dir);
|
|
7439
|
+
if (existsSync3(join3(dir, ".git")) || parent === dir)
|
|
7183
7440
|
return cwd;
|
|
7184
7441
|
dir = parent;
|
|
7185
7442
|
}
|
|
@@ -7189,164 +7446,20 @@ function findConfigDir(cwd) {
|
|
|
7189
7446
|
import { randomBytes } from "node:crypto";
|
|
7190
7447
|
import {
|
|
7191
7448
|
createWriteStream as createWriteStream3,
|
|
7192
|
-
existsSync as
|
|
7193
|
-
mkdirSync as
|
|
7449
|
+
existsSync as existsSync4,
|
|
7450
|
+
mkdirSync as mkdirSync5,
|
|
7194
7451
|
mkdtempSync,
|
|
7195
|
-
readFileSync as
|
|
7452
|
+
readFileSync as readFileSync3,
|
|
7196
7453
|
realpathSync as realpathSync2,
|
|
7197
|
-
writeFileSync
|
|
7454
|
+
writeFileSync as writeFileSync2
|
|
7198
7455
|
} from "node:fs";
|
|
7199
7456
|
import { tmpdir } from "node:os";
|
|
7200
|
-
import { join as
|
|
7457
|
+
import { join as join6, resolve as resolve2 } from "node:path";
|
|
7201
7458
|
|
|
7202
7459
|
// src/claude.ts
|
|
7203
|
-
import { spawn, spawnSync as
|
|
7460
|
+
import { spawn, spawnSync as spawnSync3 } from "node:child_process";
|
|
7204
7461
|
import { createWriteStream } from "node:fs";
|
|
7205
7462
|
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
7463
|
var CLAUDE_FLAGS = [
|
|
7351
7464
|
"--output-format",
|
|
7352
7465
|
"stream-json",
|
|
@@ -7511,8 +7624,8 @@ async function runClaude(opts) {
|
|
|
7511
7624
|
badStart = `claude output did not start with a system/init stream-json line (got: ${line.slice(0, 120)})`;
|
|
7512
7625
|
return "kill";
|
|
7513
7626
|
}
|
|
7514
|
-
if (msg.apiKeySource !==
|
|
7515
|
-
badStart = `claude would use API key billing (apiKeySource=${JSON.stringify(msg.apiKeySource)}) instead of your Claude
|
|
7627
|
+
if (msg.apiKeySource !== opts.keySource) {
|
|
7628
|
+
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
7629
|
return "kill";
|
|
7517
7630
|
}
|
|
7518
7631
|
return;
|
|
@@ -7592,7 +7705,7 @@ async function runShell(opts) {
|
|
|
7592
7705
|
}
|
|
7593
7706
|
function checkLogin(opts) {
|
|
7594
7707
|
const { cmd, args } = wrap(opts.profile, opts.bin, ["auth", "status"]);
|
|
7595
|
-
const r =
|
|
7708
|
+
const r = spawnSync3(cmd, args, {
|
|
7596
7709
|
cwd: opts.cwd,
|
|
7597
7710
|
env: opts.env,
|
|
7598
7711
|
encoding: "utf8",
|
|
@@ -7607,16 +7720,13 @@ function checkLogin(opts) {
|
|
|
7607
7720
|
if (status?.loggedIn === true)
|
|
7608
7721
|
return;
|
|
7609
7722
|
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`;
|
|
7723
|
+
return `claude is not logged in to firegoat's config dir (${opts.env.CLAUDE_CONFIG_DIR}). Run: npx firegoat claude login`;
|
|
7611
7724
|
return `claude auth status gave no login state (exit ${r.status}): ${(r.stdout + r.stderr).trim().slice(0, 300)}`;
|
|
7612
7725
|
}
|
|
7613
7726
|
|
|
7614
|
-
// src/exit.ts
|
|
7615
|
-
var EXIT = { pass: 0, fail: 1, notGraded: 2, usage: 3 };
|
|
7616
|
-
|
|
7617
7727
|
// src/setup.ts
|
|
7618
|
-
import { cpSync, createWriteStream as createWriteStream2, mkdirSync as
|
|
7619
|
-
import { delimiter, join as
|
|
7728
|
+
import { cpSync, createWriteStream as createWriteStream2, mkdirSync as mkdirSync4 } from "node:fs";
|
|
7729
|
+
import { delimiter, join as join4 } from "node:path";
|
|
7620
7730
|
var SETUP_TIMEOUT_MS = 600000;
|
|
7621
7731
|
async function runAll(commands, cwd, env, logPath, what) {
|
|
7622
7732
|
const log = createWriteStream2(logPath);
|
|
@@ -7638,13 +7748,13 @@ async function runAll(commands, cwd, env, logPath, what) {
|
|
|
7638
7748
|
}
|
|
7639
7749
|
}
|
|
7640
7750
|
async function setupOnce(opts) {
|
|
7641
|
-
|
|
7751
|
+
mkdirSync4(opts.out, { recursive: true });
|
|
7642
7752
|
return runAll(opts.commands, opts.root, { ...opts.env, FIREGOAT_OUT: opts.out }, opts.logPath, "setup");
|
|
7643
7753
|
}
|
|
7644
7754
|
async function prepareRun(opts) {
|
|
7645
|
-
const out =
|
|
7755
|
+
const out = join4(opts.runRoot, "out");
|
|
7646
7756
|
if (opts.out === null)
|
|
7647
|
-
|
|
7757
|
+
mkdirSync4(out);
|
|
7648
7758
|
else
|
|
7649
7759
|
cpSync(opts.out, out, { recursive: true });
|
|
7650
7760
|
const path = [out, opts.env.PATH ?? ""].join(delimiter);
|
|
@@ -7659,7 +7769,7 @@ async function prepareRun(opts) {
|
|
|
7659
7769
|
|
|
7660
7770
|
// src/which.ts
|
|
7661
7771
|
import { accessSync, constants, statSync } from "node:fs";
|
|
7662
|
-
import { delimiter as delimiter2, join as
|
|
7772
|
+
import { delimiter as delimiter2, join as join5, resolve } from "node:path";
|
|
7663
7773
|
function resolveBin(bin) {
|
|
7664
7774
|
const executable = (p) => {
|
|
7665
7775
|
try {
|
|
@@ -7672,8 +7782,8 @@ function resolveBin(bin) {
|
|
|
7672
7782
|
if (bin.includes("/"))
|
|
7673
7783
|
return executable(bin) ? resolve(bin) : undefined;
|
|
7674
7784
|
for (const dir of (process.env.PATH ?? "").split(delimiter2)) {
|
|
7675
|
-
if (dir && executable(
|
|
7676
|
-
return
|
|
7785
|
+
if (dir && executable(join5(dir, bin)))
|
|
7786
|
+
return join5(dir, bin);
|
|
7677
7787
|
}
|
|
7678
7788
|
return;
|
|
7679
7789
|
}
|
|
@@ -7735,16 +7845,16 @@ function newRunId() {
|
|
|
7735
7845
|
return `${ts}-${randomBytes(2).toString("hex")}`;
|
|
7736
7846
|
}
|
|
7737
7847
|
function gitignoreHasFiregoat(cwd) {
|
|
7738
|
-
const file =
|
|
7739
|
-
if (!
|
|
7848
|
+
const file = join6(cwd, ".gitignore");
|
|
7849
|
+
if (!existsSync4(file))
|
|
7740
7850
|
return false;
|
|
7741
|
-
return
|
|
7851
|
+
return readFileSync3(file, "utf8").split(`
|
|
7742
7852
|
`).map((l) => l.trim()).some((l) => /^\/?\.firegoat\/?(\*\*?)?$/.test(l));
|
|
7743
7853
|
}
|
|
7744
7854
|
function newRunRoot(label) {
|
|
7745
|
-
const runRoot = realpathSync2(mkdtempSync(
|
|
7746
|
-
const workdir =
|
|
7747
|
-
|
|
7855
|
+
const runRoot = realpathSync2(mkdtempSync(join6(tmpdir(), `firegoat-${label}-`)));
|
|
7856
|
+
const workdir = join6(runRoot, "work");
|
|
7857
|
+
mkdirSync5(workdir);
|
|
7748
7858
|
return { runRoot, workdir };
|
|
7749
7859
|
}
|
|
7750
7860
|
function isolate(iso, claudeBin, runRoot, workdir) {
|
|
@@ -7762,9 +7872,9 @@ function isolate(iso, claudeBin, runRoot, workdir) {
|
|
|
7762
7872
|
async function runOne(opts) {
|
|
7763
7873
|
const { test, n } = opts;
|
|
7764
7874
|
const started = Date.now();
|
|
7765
|
-
const dir =
|
|
7766
|
-
|
|
7767
|
-
const session =
|
|
7875
|
+
const dir = join6(opts.runDir, test.name, String(n));
|
|
7876
|
+
mkdirSync5(dir, { recursive: true });
|
|
7877
|
+
const session = join6(dir, "session.jsonl");
|
|
7768
7878
|
const summary = {
|
|
7769
7879
|
test: test.name,
|
|
7770
7880
|
n,
|
|
@@ -7797,7 +7907,7 @@ async function runOne(opts) {
|
|
|
7797
7907
|
workdir,
|
|
7798
7908
|
commands: opts.workdirSetup,
|
|
7799
7909
|
env: hostEnv,
|
|
7800
|
-
logPath:
|
|
7910
|
+
logPath: join6(dir, "workdir_setup.txt")
|
|
7801
7911
|
});
|
|
7802
7912
|
if ("error" in prepared)
|
|
7803
7913
|
return done("error", `not graded: ${prepared.error}`);
|
|
@@ -7806,13 +7916,17 @@ async function runOne(opts) {
|
|
|
7806
7916
|
}
|
|
7807
7917
|
const agentPath = extra.PATH ? { PATH: extra.PATH } : {};
|
|
7808
7918
|
const outcome = await runClaude({
|
|
7809
|
-
env: {
|
|
7919
|
+
env: {
|
|
7920
|
+
...agentEnv(runRoot, opts.auth.method === "api-key" ? opts.auth.apiKey : undefined),
|
|
7921
|
+
...agentPath
|
|
7922
|
+
},
|
|
7923
|
+
keySource: opts.auth.method === "api-key" ? "ANTHROPIC_API_KEY" : "none",
|
|
7810
7924
|
profile: sandbox.profile,
|
|
7811
7925
|
bin: opts.claudeBin,
|
|
7812
7926
|
prompt: render(test.prompt, opts.vars),
|
|
7813
7927
|
cwd: workdir,
|
|
7814
7928
|
sessionPath: session,
|
|
7815
|
-
stderrPath:
|
|
7929
|
+
stderrPath: join6(dir, "stderr.txt"),
|
|
7816
7930
|
timeoutMs: test.timeout * 1000
|
|
7817
7931
|
});
|
|
7818
7932
|
if (outcome.result) {
|
|
@@ -7825,7 +7939,7 @@ async function runOne(opts) {
|
|
|
7825
7939
|
opts.warn(`warning: ${test.name} has no checks, so its goal is unverified; counted as pass`);
|
|
7826
7940
|
return done("pass", "no checks (goal unverified)");
|
|
7827
7941
|
}
|
|
7828
|
-
const log = createWriteStream3(
|
|
7942
|
+
const log = createWriteStream3(join6(dir, "checks.txt"));
|
|
7829
7943
|
const env = {
|
|
7830
7944
|
...process.env,
|
|
7831
7945
|
...hostEnv,
|
|
@@ -7897,45 +8011,58 @@ async function evalCommand(args, cwd) {
|
|
|
7897
8011
|
}
|
|
7898
8012
|
if (opts.host)
|
|
7899
8013
|
warn("warning: --host: the agent runs without isolation and can read and write your files");
|
|
7900
|
-
else if (process.platform !== "darwin" || !
|
|
8014
|
+
else if (process.platform !== "darwin" || !existsSync4(SANDBOX_EXEC)) {
|
|
7901
8015
|
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
8016
|
return EXIT.usage;
|
|
7903
8017
|
}
|
|
7904
8018
|
const iso = {
|
|
7905
8019
|
host: opts.host,
|
|
7906
8020
|
repo: root,
|
|
7907
|
-
repoFile:
|
|
8021
|
+
repoFile: join6(root, "firegoat.yaml")
|
|
7908
8022
|
};
|
|
7909
|
-
|
|
8023
|
+
let auth;
|
|
8024
|
+
try {
|
|
8025
|
+
auth = resolveAuth(process.env);
|
|
8026
|
+
} catch (e) {
|
|
8027
|
+
if (e instanceof AuthError) {
|
|
8028
|
+
console.error(`firegoat eval: ${e.message}`);
|
|
8029
|
+
return EXIT.usage;
|
|
8030
|
+
}
|
|
8031
|
+
throw e;
|
|
8032
|
+
}
|
|
8033
|
+
say(`auth: ${auth.label}`);
|
|
8034
|
+
if (auth.method === "claude" && process.env.ANTHROPIC_API_KEY)
|
|
8035
|
+
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`)");
|
|
8036
|
+
mkdirSync5(claudeConfigDir(), { recursive: true });
|
|
7910
8037
|
const pre = newRunRoot("preflight");
|
|
7911
8038
|
const preSandbox = isolate(iso, claudePath, pre.runRoot, pre.workdir);
|
|
7912
|
-
const loginProblem = "error" in preSandbox ? preSandbox.error : checkLogin({
|
|
8039
|
+
const loginProblem = "error" in preSandbox ? preSandbox.error : auth.method === "claude" ? checkLogin({
|
|
7913
8040
|
bin: claudePath,
|
|
7914
8041
|
env: agentEnv(pre.runRoot),
|
|
7915
8042
|
profile: preSandbox.profile,
|
|
7916
8043
|
cwd: pre.workdir
|
|
7917
|
-
});
|
|
8044
|
+
}) : undefined;
|
|
7918
8045
|
if (loginProblem) {
|
|
7919
8046
|
console.error(`firegoat eval: not graded: ${loginProblem}`);
|
|
7920
8047
|
return EXIT.notGraded;
|
|
7921
8048
|
}
|
|
7922
|
-
|
|
8049
|
+
mkdirSync5(join6(root, ".firegoat"), { recursive: true });
|
|
7923
8050
|
if (!gitignoreHasFiregoat(root))
|
|
7924
8051
|
warn("hint: add .firegoat/ to .gitignore (it holds run traces)");
|
|
7925
8052
|
const runId = newRunId();
|
|
7926
|
-
const runDir =
|
|
7927
|
-
|
|
7928
|
-
const summaryPath =
|
|
8053
|
+
const runDir = join6(root, ".firegoat", "runs", runId);
|
|
8054
|
+
mkdirSync5(runDir, { recursive: true });
|
|
8055
|
+
const summaryPath = join6(runDir, "summary.json");
|
|
7929
8056
|
let out = null;
|
|
7930
8057
|
if (config.setup.length > 0) {
|
|
7931
8058
|
say("setup...");
|
|
7932
|
-
out =
|
|
8059
|
+
out = join6(runDir, "out");
|
|
7933
8060
|
const error = await setupOnce({
|
|
7934
8061
|
commands: config.setup,
|
|
7935
8062
|
root,
|
|
7936
8063
|
out,
|
|
7937
8064
|
env: { ...process.env, ...vars },
|
|
7938
|
-
logPath:
|
|
8065
|
+
logPath: join6(runDir, "setup.txt")
|
|
7939
8066
|
});
|
|
7940
8067
|
if (error) {
|
|
7941
8068
|
console.error(`firegoat eval: not graded: ${error}`);
|
|
@@ -7951,10 +8078,11 @@ async function evalCommand(args, cwd) {
|
|
|
7951
8078
|
runId,
|
|
7952
8079
|
runsDir: runDir,
|
|
7953
8080
|
isolation: opts.host ? "host" : "seatbelt",
|
|
8081
|
+
auth: auth.method,
|
|
7954
8082
|
totals,
|
|
7955
8083
|
runs: results
|
|
7956
8084
|
};
|
|
7957
|
-
|
|
8085
|
+
writeFileSync2(summaryPath, `${JSON.stringify(doc, null, 2)}
|
|
7958
8086
|
`);
|
|
7959
8087
|
return doc;
|
|
7960
8088
|
};
|
|
@@ -7968,6 +8096,7 @@ async function evalCommand(args, cwd) {
|
|
|
7968
8096
|
runDir,
|
|
7969
8097
|
claudeBin: claudePath,
|
|
7970
8098
|
iso,
|
|
8099
|
+
auth,
|
|
7971
8100
|
out,
|
|
7972
8101
|
workdirSetup: config.workdirSetup,
|
|
7973
8102
|
vars,
|
|
@@ -7980,7 +8109,7 @@ async function evalCommand(args, cwd) {
|
|
|
7980
8109
|
status: "error",
|
|
7981
8110
|
reason: `not graded: firegoat crashed: ${e.message}`,
|
|
7982
8111
|
workdir: null,
|
|
7983
|
-
session:
|
|
8112
|
+
session: join6(runDir, test.name, String(n), "session.jsonl"),
|
|
7984
8113
|
checks: [],
|
|
7985
8114
|
num_turns: null,
|
|
7986
8115
|
cost: null,
|
|
@@ -8003,16 +8132,21 @@ ${pass} passed, ${fail} failed, ${error} not graded (${results.length} runs)`);
|
|
|
8003
8132
|
}
|
|
8004
8133
|
|
|
8005
8134
|
// src/init.ts
|
|
8006
|
-
import { spawnSync as
|
|
8007
|
-
import { existsSync as
|
|
8008
|
-
import { basename as basename2, join as
|
|
8135
|
+
import { spawnSync as spawnSync4 } from "node:child_process";
|
|
8136
|
+
import { existsSync as existsSync5, readFileSync as readFileSync4, writeFileSync as writeFileSync3 } from "node:fs";
|
|
8137
|
+
import { basename as basename2, join as join7 } from "node:path";
|
|
8009
8138
|
import { createInterface as createInterface2 } from "node:readline/promises";
|
|
8010
|
-
|
|
8139
|
+
import { Writable } from "node:stream";
|
|
8140
|
+
var INIT_USAGE = `Usage: firegoat init [--yes] [--force] [--skills] [--auth claude|api-key]
|
|
8011
8141
|
|
|
8012
8142
|
--yes, -y don't ask questions (default when stdin is not a terminal)
|
|
8013
8143
|
--force, -f overwrite an existing ${CONFIG_FILE}
|
|
8014
8144
|
--skills install the firegoat-onboarding and suggesting-eval-prompts skills
|
|
8015
8145
|
into .claude/skills/ (npx skills add team2027/skills)
|
|
8146
|
+
--auth claude how the agent signs in: run \`firegoat claude login\` (your Claude plan)
|
|
8147
|
+
--auth api-key store an Anthropic API key for firegoat only (~/.firegoat, mode 0600),
|
|
8148
|
+
read from stdin when piped, else asked for without echo
|
|
8149
|
+
Without --auth, init asks (unless --yes). In CI, ANTHROPIC_API_KEY in env wins.
|
|
8016
8150
|
`;
|
|
8017
8151
|
var SKILLS = ["firegoat-onboarding", "suggesting-eval-prompts"];
|
|
8018
8152
|
var SKILLS_ARGV = [
|
|
@@ -8027,7 +8161,7 @@ var SKILLS_ARGV = [
|
|
|
8027
8161
|
var GITIGNORE_LINE = ".firegoat/";
|
|
8028
8162
|
function readText(file) {
|
|
8029
8163
|
try {
|
|
8030
|
-
return
|
|
8164
|
+
return readFileSync4(file, "utf8");
|
|
8031
8165
|
} catch {
|
|
8032
8166
|
return;
|
|
8033
8167
|
}
|
|
@@ -8053,9 +8187,9 @@ function tomlSections(text) {
|
|
|
8053
8187
|
return sections;
|
|
8054
8188
|
}
|
|
8055
8189
|
function detect(cwd) {
|
|
8056
|
-
const readme = ["README.md", "README", "readme.md", "Readme.md"].find((f) =>
|
|
8190
|
+
const readme = ["README.md", "README", "readme.md", "Readme.md"].find((f) => existsSync5(join7(cwd, f)));
|
|
8057
8191
|
const fallback = { name: basename2(cwd), library: true, readme };
|
|
8058
|
-
const pkgText = readText(
|
|
8192
|
+
const pkgText = readText(join7(cwd, "package.json"));
|
|
8059
8193
|
if (pkgText !== undefined) {
|
|
8060
8194
|
try {
|
|
8061
8195
|
const pkg = JSON.parse(pkgText);
|
|
@@ -8070,7 +8204,7 @@ function detect(cwd) {
|
|
|
8070
8204
|
return { ecosystem: "npm", name, bin, library, readme, docsUrl };
|
|
8071
8205
|
} catch {}
|
|
8072
8206
|
}
|
|
8073
|
-
const pyText = readText(
|
|
8207
|
+
const pyText = readText(join7(cwd, "pyproject.toml"));
|
|
8074
8208
|
if (pyText !== undefined) {
|
|
8075
8209
|
const s = tomlSections(pyText);
|
|
8076
8210
|
const name = s.get("project")?.get("name") ?? s.get("tool.poetry")?.get("name") ?? fallback.name;
|
|
@@ -8080,12 +8214,12 @@ function detect(cwd) {
|
|
|
8080
8214
|
const docsUrl = urls?.get("Documentation") ?? urls?.get("documentation") ?? urls?.get("Homepage") ?? urls?.get("homepage");
|
|
8081
8215
|
return { ecosystem: "python", name, bin, library: true, readme, docsUrl };
|
|
8082
8216
|
}
|
|
8083
|
-
const goText = readText(
|
|
8217
|
+
const goText = readText(join7(cwd, "go.mod"));
|
|
8084
8218
|
if (goText !== undefined) {
|
|
8085
8219
|
const name = goText.match(/^module\s+(\S+)/m)?.[1] ?? fallback.name;
|
|
8086
8220
|
return { ecosystem: "go", name, library: true, readme };
|
|
8087
8221
|
}
|
|
8088
|
-
const cargoText = readText(
|
|
8222
|
+
const cargoText = readText(join7(cwd, "Cargo.toml"));
|
|
8089
8223
|
if (cargoText !== undefined) {
|
|
8090
8224
|
const name = tomlSections(cargoText).get("package")?.get("name");
|
|
8091
8225
|
return {
|
|
@@ -8210,10 +8344,10 @@ function renderYaml(p, tests) {
|
|
|
8210
8344
|
`;
|
|
8211
8345
|
}
|
|
8212
8346
|
function ensureGitignore(cwd) {
|
|
8213
|
-
const file =
|
|
8347
|
+
const file = join7(cwd, ".gitignore");
|
|
8214
8348
|
const text = readText(file);
|
|
8215
8349
|
if (text === undefined) {
|
|
8216
|
-
|
|
8350
|
+
writeFileSync3(file, `${GITIGNORE_LINE}
|
|
8217
8351
|
`);
|
|
8218
8352
|
return `created .gitignore with ${GITIGNORE_LINE}`;
|
|
8219
8353
|
}
|
|
@@ -8223,12 +8357,12 @@ function ensureGitignore(cwd) {
|
|
|
8223
8357
|
const sep = text === "" || text.endsWith(`
|
|
8224
8358
|
`) ? "" : `
|
|
8225
8359
|
`;
|
|
8226
|
-
|
|
8360
|
+
writeFileSync3(file, `${text}${sep}${GITIGNORE_LINE}
|
|
8227
8361
|
`);
|
|
8228
8362
|
return `added ${GITIGNORE_LINE} to .gitignore`;
|
|
8229
8363
|
}
|
|
8230
8364
|
function installSkills(cwd) {
|
|
8231
|
-
const r =
|
|
8365
|
+
const r = spawnSync4("npx", ["-y", ...SKILLS_ARGV], {
|
|
8232
8366
|
cwd,
|
|
8233
8367
|
stdio: "inherit"
|
|
8234
8368
|
});
|
|
@@ -8238,6 +8372,69 @@ function installSkills(cwd) {
|
|
|
8238
8372
|
return `exit ${r.status ?? "by signal"}`;
|
|
8239
8373
|
return;
|
|
8240
8374
|
}
|
|
8375
|
+
async function ask(question) {
|
|
8376
|
+
const rl = createInterface2({ input: process.stdin, output: process.stdout });
|
|
8377
|
+
try {
|
|
8378
|
+
return (await rl.question(question)).trim();
|
|
8379
|
+
} finally {
|
|
8380
|
+
rl.close();
|
|
8381
|
+
}
|
|
8382
|
+
}
|
|
8383
|
+
async function readSecret(question) {
|
|
8384
|
+
if (!process.stdin.isTTY) {
|
|
8385
|
+
const chunks = [];
|
|
8386
|
+
for await (const c of process.stdin)
|
|
8387
|
+
chunks.push(c);
|
|
8388
|
+
return Buffer.concat(chunks).toString("utf8").trim();
|
|
8389
|
+
}
|
|
8390
|
+
process.stdout.write(question);
|
|
8391
|
+
const silent = new Writable({ write: (_c, _e, cb) => cb() });
|
|
8392
|
+
const rl = createInterface2({
|
|
8393
|
+
input: process.stdin,
|
|
8394
|
+
output: silent,
|
|
8395
|
+
terminal: true
|
|
8396
|
+
});
|
|
8397
|
+
try {
|
|
8398
|
+
return (await rl.question("")).trim();
|
|
8399
|
+
} finally {
|
|
8400
|
+
rl.close();
|
|
8401
|
+
process.stdout.write(`
|
|
8402
|
+
`);
|
|
8403
|
+
}
|
|
8404
|
+
}
|
|
8405
|
+
async function chooseAuth() {
|
|
8406
|
+
let current;
|
|
8407
|
+
try {
|
|
8408
|
+
current = resolveAuth({}).label;
|
|
8409
|
+
} catch (e) {
|
|
8410
|
+
current = e.message;
|
|
8411
|
+
}
|
|
8412
|
+
console.log(`How should the agent sign in? (now: ${current})
|
|
8413
|
+
1) Login with Claude (your Claude plan)
|
|
8414
|
+
2) Paste an Anthropic API key`);
|
|
8415
|
+
for (;; ) {
|
|
8416
|
+
const a = await ask("choose [1/2]: ");
|
|
8417
|
+
if (a === "1")
|
|
8418
|
+
return "claude";
|
|
8419
|
+
if (a === "2")
|
|
8420
|
+
return "api-key";
|
|
8421
|
+
console.log("type 1 or 2");
|
|
8422
|
+
}
|
|
8423
|
+
}
|
|
8424
|
+
async function setUpAuth(method) {
|
|
8425
|
+
if (method === "claude") {
|
|
8426
|
+
const code = claudeLoginCommand([]);
|
|
8427
|
+
return code === 0 ? undefined : code;
|
|
8428
|
+
}
|
|
8429
|
+
const key = await readSecret("Anthropic API key (input hidden): ");
|
|
8430
|
+
if (!looksLikeAnthropicKey(key)) {
|
|
8431
|
+
console.error("firegoat init: that does not look like an Anthropic API key (sk-ant-...); nothing stored");
|
|
8432
|
+
return EXIT.usage;
|
|
8433
|
+
}
|
|
8434
|
+
const file = saveApiKey(key);
|
|
8435
|
+
console.log(`stored the API key in ${file} (mode 0600, not in the repo); evals pass it to the agent as ANTHROPIC_API_KEY`);
|
|
8436
|
+
return;
|
|
8437
|
+
}
|
|
8241
8438
|
async function askYesNo(question) {
|
|
8242
8439
|
const rl = createInterface2({ input: process.stdin, output: process.stdout });
|
|
8243
8440
|
try {
|
|
@@ -8261,7 +8458,18 @@ async function initCommand(args, cwd) {
|
|
|
8261
8458
|
let yes = false;
|
|
8262
8459
|
let force = false;
|
|
8263
8460
|
let skills = false;
|
|
8264
|
-
|
|
8461
|
+
let auth;
|
|
8462
|
+
for (let i = 0;i < args.length; i++) {
|
|
8463
|
+
const a = args[i];
|
|
8464
|
+
if (a === "--auth" || a.startsWith("--auth=")) {
|
|
8465
|
+
const v = a === "--auth" ? args[++i] : a.slice(7);
|
|
8466
|
+
if (v !== "claude" && v !== "api-key") {
|
|
8467
|
+
console.error(`firegoat init: --auth must be claude or api-key, got ${v ?? "nothing"}`);
|
|
8468
|
+
return EXIT.usage;
|
|
8469
|
+
}
|
|
8470
|
+
auth = v;
|
|
8471
|
+
continue;
|
|
8472
|
+
}
|
|
8265
8473
|
switch (a) {
|
|
8266
8474
|
case "--yes":
|
|
8267
8475
|
case "-y":
|
|
@@ -8285,9 +8493,9 @@ ${INIT_USAGE}`);
|
|
|
8285
8493
|
return EXIT.usage;
|
|
8286
8494
|
}
|
|
8287
8495
|
}
|
|
8288
|
-
const configPath =
|
|
8289
|
-
const keepConfig =
|
|
8290
|
-
if (keepConfig && !skills) {
|
|
8496
|
+
const configPath = join7(cwd, CONFIG_FILE);
|
|
8497
|
+
const keepConfig = existsSync5(configPath) && !force;
|
|
8498
|
+
if (keepConfig && !skills && !auth) {
|
|
8291
8499
|
console.error(`firegoat init: ${CONFIG_FILE} already exists; not overwriting. Use --force to replace it.`);
|
|
8292
8500
|
return EXIT.usage;
|
|
8293
8501
|
}
|
|
@@ -8297,11 +8505,18 @@ ${INIT_USAGE}`);
|
|
|
8297
8505
|
const project = detect(cwd);
|
|
8298
8506
|
const tests = templates(project);
|
|
8299
8507
|
console.log(`detected: ${describe(project)}`);
|
|
8300
|
-
|
|
8508
|
+
writeFileSync3(configPath, renderYaml(project, tests));
|
|
8301
8509
|
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
8510
|
}
|
|
8303
8511
|
console.log(ensureGitignore(cwd));
|
|
8304
8512
|
const interactive = !yes && process.stdin.isTTY && process.stdout.isTTY;
|
|
8513
|
+
if (!auth && interactive)
|
|
8514
|
+
auth = await chooseAuth();
|
|
8515
|
+
if (auth) {
|
|
8516
|
+
const failed = await setUpAuth(auth);
|
|
8517
|
+
if (failed !== undefined)
|
|
8518
|
+
return failed;
|
|
8519
|
+
}
|
|
8305
8520
|
if (!skills && interactive)
|
|
8306
8521
|
skills = await askYesNo(`Add the ${SKILLS.join(" and ")} skills to .claude/skills/ (help your agent draft better evals)?`);
|
|
8307
8522
|
if (skills) {
|
|
@@ -8318,47 +8533,10 @@ ${INIT_USAGE}`);
|
|
|
8318
8533
|
return 0;
|
|
8319
8534
|
}
|
|
8320
8535
|
|
|
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
8536
|
// src/open.ts
|
|
8359
8537
|
import { spawnSync as spawnSync5 } from "node:child_process";
|
|
8360
|
-
import { existsSync as
|
|
8361
|
-
import { join as
|
|
8538
|
+
import { existsSync as existsSync6, readdirSync } from "node:fs";
|
|
8539
|
+
import { join as join8 } from "node:path";
|
|
8362
8540
|
var OPENER = {
|
|
8363
8541
|
darwin: "open",
|
|
8364
8542
|
linux: "xdg-open"
|
|
@@ -8379,8 +8557,8 @@ function openCommand(args, cwd) {
|
|
|
8379
8557
|
console.error("firegoat open: takes at most one run id");
|
|
8380
8558
|
return EXIT.usage;
|
|
8381
8559
|
}
|
|
8382
|
-
const base =
|
|
8383
|
-
const runs =
|
|
8560
|
+
const base = join8(findConfigDir(cwd), ".firegoat", "runs");
|
|
8561
|
+
const runs = existsSync6(base) ? readdirSync(base).sort() : [];
|
|
8384
8562
|
const id = ids[0] ?? runs.at(-1);
|
|
8385
8563
|
if (!id) {
|
|
8386
8564
|
console.error(`firegoat open: no runs in ${base}. Run: npx firegoat eval`);
|
|
@@ -8390,7 +8568,7 @@ function openCommand(args, cwd) {
|
|
|
8390
8568
|
console.error(`firegoat open: no run ${JSON.stringify(id)} in ${base} (latest: ${runs.at(-1) ?? "none"})`);
|
|
8391
8569
|
return EXIT.usage;
|
|
8392
8570
|
}
|
|
8393
|
-
const dir =
|
|
8571
|
+
const dir = join8(base, id);
|
|
8394
8572
|
console.log(dir);
|
|
8395
8573
|
const opener = OPENER[process.platform];
|
|
8396
8574
|
if (print || !opener)
|
|
@@ -8407,8 +8585,9 @@ function openCommand(args, cwd) {
|
|
|
8407
8585
|
var USAGE = `firegoat ${package_default.version}: local-first AX evals
|
|
8408
8586
|
|
|
8409
8587
|
Usage:
|
|
8410
|
-
firegoat init [--yes] [--force] [--skills]
|
|
8411
|
-
|
|
8588
|
+
firegoat init [--yes] [--force] [--skills] [--auth claude|api-key]
|
|
8589
|
+
write firegoat.yaml, choose how the agent signs in
|
|
8590
|
+
firegoat claude login [--claude-bin PATH] sign claude in to firegoat's own config dir (once)
|
|
8412
8591
|
firegoat eval [test..] [--runs N] [--var KEY=value].. [--json] [--host] [--claude-bin PATH]
|
|
8413
8592
|
run evals with claude in a sandboxed fresh workdir
|
|
8414
8593
|
firegoat open [runId] [--print] open a run's folder (default: the latest)
|
|
@@ -8444,8 +8623,14 @@ async function main(argv) {
|
|
|
8444
8623
|
return 0;
|
|
8445
8624
|
case "init":
|
|
8446
8625
|
return initCommand(rest, process.cwd());
|
|
8626
|
+
case "claude":
|
|
8627
|
+
if (rest[0] === "login")
|
|
8628
|
+
return claudeLoginCommand(rest.slice(1));
|
|
8629
|
+
console.error(`usage: firegoat claude login [--claude-bin PATH]`);
|
|
8630
|
+
return EXIT.usage;
|
|
8447
8631
|
case "login":
|
|
8448
|
-
|
|
8632
|
+
console.error("firegoat login is deprecated for the Claude sign-in: use `firegoat claude login`");
|
|
8633
|
+
return claudeLoginCommand(rest);
|
|
8449
8634
|
case "eval":
|
|
8450
8635
|
return evalCommand(rest, process.cwd());
|
|
8451
8636
|
case "open":
|