firegoat 0.19.0 → 0.20.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 +14 -1
- package/dist/cli.js +64 -24
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -156,7 +156,20 @@ tests:
|
|
|
156
156
|
claude_args: [--chrome] # Claude in Chrome, this test only
|
|
157
157
|
```
|
|
158
158
|
|
|
159
|
-
`claude_args:` (top level, and per test after the top-level ones) is passed to claude after firegoat's own flags and `--model`. Each entry is one argument, as-is, with no shell splitting: `[--plugin-dir, ./plugins/acme]`, not `["--plugin-dir ./plugins/acme"]`. Use it for `--chrome`, `--plugin-dir`, `--mcp-config`, `--agents`, `--append-system-prompt`. The flags firegoat sets itself are config errors, also as `--flag=value`: `-p`, `--print`, `--output-format`, `--verbose`, `--setting-sources`, `--strict-mcp-config`, `--disable-slash-commands`, `--dangerously-skip-permissions`, `--model` (use `model:`), and `--session-id`, `--resume`, `-r`, `--continue`, `-c` (every run is a fresh session). Each run's `run.json` records `claudeArgs`.
|
|
159
|
+
`claude_args:` (top level, and per test after the top-level ones) is passed to claude after firegoat's own flags and `--model`. Each entry is one argument, as-is, with no shell splitting: `[--plugin-dir, ./plugins/acme]`, not `["--plugin-dir ./plugins/acme"]`. Use it for `--chrome`, `--plugin-dir`, `--mcp-config`, `--agents`, `--append-system-prompt`. The flags firegoat sets itself are config errors, also as `--flag=value`: `-p`, `--print`, `--output-format`, `--verbose`, `--setting-sources`, `--strict-mcp-config`, `--disable-slash-commands`, `--dangerously-skip-permissions`, `--model` (use `model:`), and `--session-id`, `--resume`, `-r`, `--continue`, `-c` (every run is a fresh session). Entries expand like `env:` values: `$FIREGOAT_WORKDIR`, `$FIREGOAT_HOME`, `$FIREGOAT_OUT` (also `${...}`), `{{vars.KEY}}`, `{{run.id}}`, `{{run.n}}`, `{{test.name}}`; no other `$` expansion. Each run's `run.json` records the expanded `claudeArgs`.
|
|
160
|
+
|
|
161
|
+
### Testing an MCP server
|
|
162
|
+
|
|
163
|
+
```yaml
|
|
164
|
+
install: |
|
|
165
|
+
cat > .mcp.json <<EOF
|
|
166
|
+
{"mcpServers": {"acme": {"command": "npx", "args": ["-y", "acme-mcp"],
|
|
167
|
+
"env": {"HOME": "$FIREGOAT_HOME"}}}}
|
|
168
|
+
EOF
|
|
169
|
+
claude_args: [--mcp-config, .mcp.json]
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
Write the MCP config per run in `install:` (or copy a committed one to `$FIREGOAT_OUT` in `build:` and pass `"$FIREGOAT_OUT/mcp.json"`, but then it can't hold the run's home), and pass it with `claude_args: [--mcp-config, .mcp.json]`: relative paths are from the run's folder, claude's cwd. `--strict-mcp-config` is always on, so only the configs you pass load. MCP servers that claude starts inherit claude's own env, which keeps your real `HOME` (claude needs it for its login): set `"env": {"HOME": "<the run's $FIREGOAT_HOME>"}` on the server, as above, or it writes to your real home.
|
|
160
173
|
|
|
161
174
|
With `--chrome`, claude points the Claude in Chrome extension's native host (`~/Library/Application Support/<browser>/NativeMessagingHosts/com.anthropic.claude_code_browser_extension.json`) at `.firegoat/.claude`. If you delete `.firegoat/`, run a normal `claude --chrome` once to point it back.
|
|
162
175
|
|
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.20.0",
|
|
6951
6951
|
repository: {
|
|
6952
6952
|
type: "git",
|
|
6953
6953
|
url: "git+https://github.com/firegoat-ai/firegoat-cli.git"
|
|
@@ -7211,6 +7211,7 @@ function parseClaudeArgs(raw, path) {
|
|
|
7211
7211
|
for (const f of flags)
|
|
7212
7212
|
if (arg === f || arg.startsWith(`${f}=`))
|
|
7213
7213
|
fail(p, `${f} is not allowed: ${why}`);
|
|
7214
|
+
checkPlaceholders(arg, p);
|
|
7214
7215
|
return arg;
|
|
7215
7216
|
});
|
|
7216
7217
|
}
|
|
@@ -7266,15 +7267,18 @@ function parseEnv(raw, path) {
|
|
|
7266
7267
|
fail(p, "is set by firegoat and cannot be overridden (reserved: PATH, CLAUDE_CONFIG_DIR, CLAUDE_SECURESTORAGE_CONFIG_DIR, FIREGOAT_*)");
|
|
7267
7268
|
if (typeof v !== "string")
|
|
7268
7269
|
fail(p, "must be a string (quote numbers and booleans)");
|
|
7269
|
-
|
|
7270
|
-
const name = m[1];
|
|
7271
|
-
if (!/^vars\.[A-Za-z0-9_]+$/.test(name) && !RUN_PLACEHOLDERS.includes(name))
|
|
7272
|
-
fail(p, `unknown placeholder ${m[0]} (allowed: {{vars.KEY}}, ${RUN_PLACEHOLDERS.map((n) => `{{${n}}}`).join(", ")})`);
|
|
7273
|
-
}
|
|
7270
|
+
checkPlaceholders(v, p);
|
|
7274
7271
|
out[k] = v;
|
|
7275
7272
|
}
|
|
7276
7273
|
return out;
|
|
7277
7274
|
}
|
|
7275
|
+
function checkPlaceholders(v, p) {
|
|
7276
|
+
for (const m of v.matchAll(ANY_PLACEHOLDER)) {
|
|
7277
|
+
const name = m[1];
|
|
7278
|
+
if (!/^vars\.[A-Za-z0-9_]+$/.test(name) && !RUN_PLACEHOLDERS.includes(name))
|
|
7279
|
+
fail(p, `unknown placeholder ${m[0]} (allowed: {{vars.KEY}}, ${RUN_PLACEHOLDERS.map((n) => `{{${n}}}`).join(", ")})`);
|
|
7280
|
+
}
|
|
7281
|
+
}
|
|
7278
7282
|
function isEvalLevel(value) {
|
|
7279
7283
|
const dirsOk = [...value.matchAll(DIR_REF)].every((m) => (m[1] ?? m[2]) === "FIREGOAT_OUT");
|
|
7280
7284
|
return dirsOk && [...value.matchAll(ANY_PLACEHOLDER)].every((m) => {
|
|
@@ -7282,16 +7286,19 @@ function isEvalLevel(value) {
|
|
|
7282
7286
|
return name.startsWith("vars.") || EVAL_PLACEHOLDERS.includes(name);
|
|
7283
7287
|
});
|
|
7284
7288
|
}
|
|
7289
|
+
function renderValue(value, vars, run, where) {
|
|
7290
|
+
return value.replace(ENV_REF, (whole, name, braced, bare) => {
|
|
7291
|
+
const key = name ?? braced ?? bare;
|
|
7292
|
+
const filled = key.startsWith("vars.") ? vars[key.slice("vars.".length)] : run[key];
|
|
7293
|
+
if (filled === undefined)
|
|
7294
|
+
throw new ConfigError(`${where}: no value for ${whole}`);
|
|
7295
|
+
return filled;
|
|
7296
|
+
});
|
|
7297
|
+
}
|
|
7285
7298
|
function renderEnv(env, vars, run) {
|
|
7286
7299
|
const out = {};
|
|
7287
7300
|
for (const [k, v] of Object.entries(env))
|
|
7288
|
-
out[k] = v
|
|
7289
|
-
const key = name ?? braced ?? bare;
|
|
7290
|
-
const filled = key.startsWith("vars.") ? vars[key.slice("vars.".length)] : run[key];
|
|
7291
|
-
if (filled === undefined)
|
|
7292
|
-
throw new ConfigError(`env.${k}: no value for ${whole}`);
|
|
7293
|
-
return filled;
|
|
7294
|
-
});
|
|
7301
|
+
out[k] = renderValue(v, vars, run, `env.${k}`);
|
|
7295
7302
|
return out;
|
|
7296
7303
|
}
|
|
7297
7304
|
var PLACEHOLDER = /\{\{\s*vars\.([A-Za-z0-9_]+)\s*\}\}/g;
|
|
@@ -7957,7 +7964,10 @@ firegoat.yaml reference:
|
|
|
7957
7964
|
as-is (no shell splitting): [--chrome] (Claude in
|
|
7958
7965
|
Chrome, for tests that need a browser),
|
|
7959
7966
|
[--plugin-dir, ./plugins/x], --mcp-config, --agents,
|
|
7960
|
-
--append-system-prompt.
|
|
7967
|
+
--append-system-prompt. Entries expand like env:
|
|
7968
|
+
values ($FIREGOAT_WORKDIR, $FIREGOAT_HOME,
|
|
7969
|
+
$FIREGOAT_OUT, {{vars.KEY}}, {{run.*}}); run.json
|
|
7970
|
+
claudeArgs has the result. Flags firegoat sets are
|
|
7961
7971
|
errors, also as --flag=value: -p, --print,
|
|
7962
7972
|
--output-format, --verbose, --setting-sources,
|
|
7963
7973
|
--strict-mcp-config, --disable-slash-commands,
|
|
@@ -8000,6 +8010,18 @@ firegoat.yaml reference:
|
|
|
8000
8010
|
every env firegoat starts, unless env: sets them. So is every FIREGOAT_* var
|
|
8001
8011
|
(FIREGOAT_API_KEY above all) firegoat itself was started with.
|
|
8002
8012
|
|
|
8013
|
+
Testing an MCP server: write its config per run in install: and pass it with
|
|
8014
|
+
claude_args (relative paths are from the run's folder, claude's cwd). Only the
|
|
8015
|
+
configs you pass load: --strict-mcp-config is always on. An MCP server inherits
|
|
8016
|
+
claude's env, which keeps your real HOME (claude's login is there), so set the
|
|
8017
|
+
server's HOME to the run's home, or it writes to yours:
|
|
8018
|
+
install: |
|
|
8019
|
+
cat > .mcp.json <<EOF
|
|
8020
|
+
{"mcpServers": {"acme": {"command": "npx", "args": ["-y", "acme-mcp"],
|
|
8021
|
+
"env": {"HOME": "$FIREGOAT_HOME"}}}}
|
|
8022
|
+
EOF
|
|
8023
|
+
claude_args: [--mcp-config, .mcp.json]
|
|
8024
|
+
|
|
8003
8025
|
Example:
|
|
8004
8026
|
build: npm pack --pack-destination "$FIREGOAT_OUT"
|
|
8005
8027
|
install: npm init -y >/dev/null && npm install "$FIREGOAT_OUT"/*.tgz
|
|
@@ -8930,12 +8952,20 @@ async function runOne(opts) {
|
|
|
8930
8952
|
const workdir = join6(dir, "work");
|
|
8931
8953
|
mkdirSync4(workdir, { recursive: true });
|
|
8932
8954
|
const { home, envFile } = makeRunHome(dir);
|
|
8955
|
+
const fill = {
|
|
8956
|
+
"run.id": opts.runId,
|
|
8957
|
+
"run.n": String(n),
|
|
8958
|
+
"test.name": test.name,
|
|
8959
|
+
FIREGOAT_OUT: opts.out,
|
|
8960
|
+
FIREGOAT_WORKDIR: workdir,
|
|
8961
|
+
FIREGOAT_HOME: home
|
|
8962
|
+
};
|
|
8933
8963
|
const r = {
|
|
8934
8964
|
test: test.name,
|
|
8935
8965
|
n,
|
|
8936
8966
|
model: opts.model,
|
|
8937
8967
|
claudeModel: null,
|
|
8938
|
-
claudeArgs: test.claudeArgs,
|
|
8968
|
+
claudeArgs: test.claudeArgs.map((a, i) => renderValue(a, opts.vars, fill, `claude_args[${i}]`)),
|
|
8939
8969
|
status: "error",
|
|
8940
8970
|
reason: "",
|
|
8941
8971
|
prompt: render(test.prompt, opts.vars),
|
|
@@ -8956,14 +8986,7 @@ async function runOne(opts) {
|
|
|
8956
8986
|
startedAt: new Date(started).toISOString(),
|
|
8957
8987
|
upload: null
|
|
8958
8988
|
};
|
|
8959
|
-
const env = renderEnv(test.env, opts.vars,
|
|
8960
|
-
"run.id": opts.runId,
|
|
8961
|
-
"run.n": String(n),
|
|
8962
|
-
"test.name": test.name,
|
|
8963
|
-
FIREGOAT_OUT: opts.out,
|
|
8964
|
-
FIREGOAT_WORKDIR: workdir,
|
|
8965
|
-
FIREGOAT_HOME: home
|
|
8966
|
-
});
|
|
8989
|
+
const env = renderEnv(test.env, opts.vars, fill);
|
|
8967
8990
|
const stepEnv = {
|
|
8968
8991
|
...opts.stepEnv,
|
|
8969
8992
|
...env,
|
|
@@ -9064,6 +9087,20 @@ async function attempt(opts, r, env, stepEnv, homeEnvFile) {
|
|
|
9064
9087
|
test.checks.length === 0 ? "no script checks" : `${r.checks.length}/${r.checks.length} checks passed`
|
|
9065
9088
|
];
|
|
9066
9089
|
}
|
|
9090
|
+
var MAX_LISTED_URLS = 5;
|
|
9091
|
+
function uploadedSummary(origin, results, runDir) {
|
|
9092
|
+
const urls = results.flatMap((r) => r.upload?.status === "uploaded" ? [r.upload.url] : []);
|
|
9093
|
+
if (urls.length === 0)
|
|
9094
|
+
return;
|
|
9095
|
+
const head = `Uploaded ${urls.length} session${urls.length === 1 ? "" : "s"} to ${new URL(origin).host} (private)`;
|
|
9096
|
+
if (urls.length > MAX_LISTED_URLS) {
|
|
9097
|
+
console.log(`${head}: each run's upload.url in ${join6(runDir, "summary.json")}`);
|
|
9098
|
+
return;
|
|
9099
|
+
}
|
|
9100
|
+
console.log(`${head}:`);
|
|
9101
|
+
for (const u of urls)
|
|
9102
|
+
console.log(` ${u}`);
|
|
9103
|
+
}
|
|
9067
9104
|
async function evalCommand(args, root) {
|
|
9068
9105
|
if (args.includes("--help") || args.includes("-h")) {
|
|
9069
9106
|
console.log(EVAL_HELP);
|
|
@@ -9116,7 +9153,8 @@ async function evalCommand(args, root) {
|
|
|
9116
9153
|
...new Set(tests.flatMap((t) => [
|
|
9117
9154
|
t.prompt,
|
|
9118
9155
|
...t.checks.map((c) => c.run),
|
|
9119
|
-
...Object.values(t.env)
|
|
9156
|
+
...Object.values(t.env),
|
|
9157
|
+
...t.claudeArgs
|
|
9120
9158
|
]).flatMap(varRefs).filter((k) => !(k in vars)))
|
|
9121
9159
|
];
|
|
9122
9160
|
if (missing.length > 0)
|
|
@@ -9287,6 +9325,8 @@ async function evalCommand(args, root) {
|
|
|
9287
9325
|
rows,
|
|
9288
9326
|
seconds: Math.round((Date.now() - evalStarted) / 1000)
|
|
9289
9327
|
});
|
|
9328
|
+
if (upload && !opts.json)
|
|
9329
|
+
uploadedSummary(upload.origin, results, runDir);
|
|
9290
9330
|
for (const r of results)
|
|
9291
9331
|
for (const c of r.cleanup)
|
|
9292
9332
|
if (c.code !== 0)
|