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.
Files changed (3) hide show
  1. package/README.md +15 -5
  2. package/dist/cli.js +448 -263
  3. 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.4). See `plan.md`.
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; --skills adds the firegoat-onboarding skill (from team2027/skills)
11
- npx firegoat login # once per machine: signs claude in to ~/.firegoat/claude
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 with `~/.firegoat/claude` (from `firegoat login`), never your `~/.claude`;
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 removed, so runs bill your subscription, not the API.
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.5.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 (!existsSync(file))
7429
+ if (!existsSync3(file))
7173
7430
  throw new ConfigError(`${CONFIG_FILE} not found in ${dir}. Run: npx firegoat init`);
7174
- return parseConfig(readFileSync(file, "utf8"), warn);
7431
+ return parseConfig(readFileSync2(file, "utf8"), warn);
7175
7432
  }
7176
7433
  function findConfigDir(cwd) {
7177
7434
  let dir = cwd;
7178
7435
  for (;; ) {
7179
- if (existsSync(join(dir, CONFIG_FILE)))
7436
+ if (existsSync3(join3(dir, CONFIG_FILE)))
7180
7437
  return dir;
7181
- const parent = dirname(dir);
7182
- if (existsSync(join(dir, ".git")) || parent === dir)
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 existsSync3,
7193
- mkdirSync as mkdirSync3,
7449
+ existsSync as existsSync4,
7450
+ mkdirSync as mkdirSync5,
7194
7451
  mkdtempSync,
7195
- readFileSync as readFileSync2,
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 join5, resolve as resolve2 } from "node:path";
7457
+ import { join as join6, resolve as resolve2 } from "node:path";
7201
7458
 
7202
7459
  // src/claude.ts
7203
- import { spawn, spawnSync as spawnSync2 } from "node:child_process";
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 !== "none") {
7515
- badStart = `claude would use API key billing (apiKeySource=${JSON.stringify(msg.apiKeySource)}) instead of your Claude subscription. firegoat removes ANTHROPIC_API_KEY from the agent's env; check apiKeyHelper or other API key settings, then run \`claude /login\``;
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 = spawnSync2(cmd, args, {
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 mkdirSync2 } from "node:fs";
7619
- import { delimiter, join as join3 } from "node:path";
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
- mkdirSync2(opts.out, { recursive: true });
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 = join3(opts.runRoot, "out");
7755
+ const out = join4(opts.runRoot, "out");
7646
7756
  if (opts.out === null)
7647
- mkdirSync2(out);
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 join4, resolve } from "node:path";
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(join4(dir, bin)))
7676
- return join4(dir, bin);
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 = join5(cwd, ".gitignore");
7739
- if (!existsSync3(file))
7848
+ const file = join6(cwd, ".gitignore");
7849
+ if (!existsSync4(file))
7740
7850
  return false;
7741
- return readFileSync2(file, "utf8").split(`
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(join5(tmpdir(), `firegoat-${label}-`)));
7746
- const workdir = join5(runRoot, "work");
7747
- mkdirSync3(workdir);
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 = join5(opts.runDir, test.name, String(n));
7766
- mkdirSync3(dir, { recursive: true });
7767
- const session = join5(dir, "session.jsonl");
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: join5(dir, "workdir_setup.txt")
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: { ...agentEnv(runRoot), ...agentPath },
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: join5(dir, "stderr.txt"),
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(join5(dir, "checks.txt"));
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" || !existsSync3(SANDBOX_EXEC)) {
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: join5(root, "firegoat.yaml")
8021
+ repoFile: join6(root, "firegoat.yaml")
7908
8022
  };
7909
- mkdirSync3(claudeConfigDir(), { recursive: true });
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
- mkdirSync3(join5(root, ".firegoat"), { recursive: true });
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 = join5(root, ".firegoat", "runs", runId);
7927
- mkdirSync3(runDir, { recursive: true });
7928
- const summaryPath = join5(runDir, "summary.json");
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 = join5(runDir, "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: join5(runDir, "setup.txt")
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
- writeFileSync(summaryPath, `${JSON.stringify(doc, null, 2)}
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: join5(runDir, test.name, String(n), "session.jsonl"),
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 spawnSync3 } from "node:child_process";
8007
- import { existsSync as existsSync4, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "node:fs";
8008
- import { basename as basename2, join as join6 } from "node:path";
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
- var INIT_USAGE = `Usage: firegoat init [--yes] [--force] [--skills]
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 readFileSync3(file, "utf8");
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) => existsSync4(join6(cwd, 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(join6(cwd, "package.json"));
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(join6(cwd, "pyproject.toml"));
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(join6(cwd, "go.mod"));
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(join6(cwd, "Cargo.toml"));
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 = join6(cwd, ".gitignore");
8347
+ const file = join7(cwd, ".gitignore");
8214
8348
  const text = readText(file);
8215
8349
  if (text === undefined) {
8216
- writeFileSync2(file, `${GITIGNORE_LINE}
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
- writeFileSync2(file, `${text}${sep}${GITIGNORE_LINE}
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 = spawnSync3("npx", ["-y", ...SKILLS_ARGV], {
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
- for (const a of args) {
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 = join6(cwd, CONFIG_FILE);
8289
- const keepConfig = existsSync4(configPath) && !force;
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
- writeFileSync2(configPath, renderYaml(project, tests));
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 existsSync5, readdirSync } from "node:fs";
8361
- import { join as join7 } from "node:path";
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 = join7(findConfigDir(cwd), ".firegoat", "runs");
8383
- const runs = existsSync5(base) ? readdirSync(base).sort() : [];
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 = join7(base, id);
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] write firegoat.yaml with starter evals
8411
- firegoat login [--claude-bin PATH] sign claude in to firegoat's own config dir (once)
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
- return loginCommand(rest);
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":
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "firegoat",
3
- "version": "0.5.0",
3
+ "version": "0.6.0",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/team2027/firegoat-cli.git"