firegoat 0.27.0 → 0.28.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 +1 -1
- package/dist/cli.js +704 -118
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -16,7 +16,7 @@ When you run `firegoat eval`, you get visibility into how agents use your produc
|
|
|
16
16
|
npx firegoat@latest init
|
|
17
17
|
```
|
|
18
18
|
|
|
19
|
-
Then paste the prompt it prints into your agent, and let it set up the evals that point agents at your product. Then run them and see the results:
|
|
19
|
+
In a terminal it asks where to install the firegoat-onboarding skill (global by default); run by an agent, it installs it into this repo's `.claude/skills/`. Then paste the prompt it prints into your agent, and let it set up the evals that point agents at your product. Then run them and see the results:
|
|
20
20
|
|
|
21
21
|
```bash
|
|
22
22
|
npx firegoat@latest eval
|
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.28.0",
|
|
6951
6951
|
repository: {
|
|
6952
6952
|
type: "git",
|
|
6953
6953
|
url: "git+https://github.com/firegoat-ai/firegoat-cli.git"
|
|
@@ -7078,6 +7078,7 @@ function steps(events) {
|
|
|
7078
7078
|
if (b.type === "tool_use")
|
|
7079
7079
|
out.push({
|
|
7080
7080
|
kind: "tool_call",
|
|
7081
|
+
id: typeof b.id === "string" ? b.id : null,
|
|
7081
7082
|
name: typeof b.name === "string" ? b.name : "",
|
|
7082
7083
|
input: typeof b.input === "object" && b.input !== null ? b.input : null
|
|
7083
7084
|
});
|
|
@@ -7089,6 +7090,7 @@ function steps(events) {
|
|
|
7089
7090
|
if (b.type === "tool_result")
|
|
7090
7091
|
out.push({
|
|
7091
7092
|
kind: "tool_result",
|
|
7093
|
+
callId: typeof b.tool_use_id === "string" ? b.tool_use_id : null,
|
|
7092
7094
|
isError: b.is_error === true,
|
|
7093
7095
|
text: resultText(b.content).trimEnd()
|
|
7094
7096
|
});
|
|
@@ -7208,6 +7210,7 @@ var claude = {
|
|
|
7208
7210
|
bin: "claude",
|
|
7209
7211
|
installHint: "Install Claude Code (https://claude.com/claude-code) or pass --harness-bin PATH.",
|
|
7210
7212
|
configDirName: ".claude",
|
|
7213
|
+
argsKey: "claude_args",
|
|
7211
7214
|
reservedArgs: [
|
|
7212
7215
|
[["-p", "--print", ...FLAGS.filter((f) => f.startsWith("-"))], SET],
|
|
7213
7216
|
[["--model"], "choose the model with top-level model: (or eval --model)"],
|
|
@@ -7216,7 +7219,7 @@ var claude = {
|
|
|
7216
7219
|
"every run is a fresh session"
|
|
7217
7220
|
]
|
|
7218
7221
|
],
|
|
7219
|
-
|
|
7222
|
+
claudeSessionFile: true,
|
|
7220
7223
|
args: ({ prompt, model, extraArgs }) => [
|
|
7221
7224
|
"-p",
|
|
7222
7225
|
prompt,
|
|
@@ -7343,24 +7346,27 @@ function itemInput(item) {
|
|
|
7343
7346
|
function steps2(events) {
|
|
7344
7347
|
const out = [];
|
|
7345
7348
|
const started = new Set;
|
|
7346
|
-
for (const e of events) {
|
|
7349
|
+
for (const [i, e] of events.entries()) {
|
|
7347
7350
|
const item = e?.item;
|
|
7348
7351
|
if (!item)
|
|
7349
7352
|
continue;
|
|
7353
|
+
const id = typeof item.id === "string" ? item.id : `event_${i}`;
|
|
7350
7354
|
if (item.type === "command_execution") {
|
|
7351
7355
|
const call = {
|
|
7352
7356
|
kind: "tool_call",
|
|
7357
|
+
id,
|
|
7353
7358
|
name: "shell",
|
|
7354
7359
|
input: { command: unwrapShell(str(item.command)) }
|
|
7355
7360
|
};
|
|
7356
7361
|
if (e?.type === "item.started") {
|
|
7357
|
-
started.add(
|
|
7362
|
+
started.add(id);
|
|
7358
7363
|
out.push(call);
|
|
7359
7364
|
} else if (e?.type === "item.completed") {
|
|
7360
|
-
if (
|
|
7365
|
+
if (!started.has(id))
|
|
7361
7366
|
out.push(call);
|
|
7362
7367
|
out.push({
|
|
7363
7368
|
kind: "tool_result",
|
|
7369
|
+
callId: id,
|
|
7364
7370
|
isError: item.exit_code !== 0,
|
|
7365
7371
|
text: str(item.aggregated_output).trimEnd()
|
|
7366
7372
|
});
|
|
@@ -7375,6 +7381,7 @@ function steps2(events) {
|
|
|
7375
7381
|
} else if (item.type !== "reasoning" && item.type !== "error")
|
|
7376
7382
|
out.push({
|
|
7377
7383
|
kind: "tool_call",
|
|
7384
|
+
id,
|
|
7378
7385
|
name: str(item.type),
|
|
7379
7386
|
input: itemInput(item)
|
|
7380
7387
|
});
|
|
@@ -7523,6 +7530,7 @@ var codex = {
|
|
|
7523
7530
|
bin: "codex",
|
|
7524
7531
|
installHint: "Install Codex CLI (https://github.com/openai/codex) or pass --harness-bin PATH.",
|
|
7525
7532
|
configDirName: ".codex",
|
|
7533
|
+
argsKey: "codex_args",
|
|
7526
7534
|
reservedArgs: [
|
|
7527
7535
|
[[...FLAGS2, "-C", "--cd"], "firegoat sets it for every run"],
|
|
7528
7536
|
[
|
|
@@ -7532,7 +7540,7 @@ var codex = {
|
|
|
7532
7540
|
[["resume", "fork"], "every run is a fresh session"],
|
|
7533
7541
|
[["--ephemeral"], "firegoat reads the session file codex saves"]
|
|
7534
7542
|
],
|
|
7535
|
-
|
|
7543
|
+
claudeSessionFile: false,
|
|
7536
7544
|
args: ({ prompt, model, extraArgs, cwd }) => [
|
|
7537
7545
|
"exec",
|
|
7538
7546
|
...FLAGS2,
|
|
@@ -7629,6 +7637,7 @@ function toolResult(b) {
|
|
|
7629
7637
|
`) : "";
|
|
7630
7638
|
return {
|
|
7631
7639
|
kind: "tool_result",
|
|
7640
|
+
callId: typeof b.tool_use_id === "string" ? b.tool_use_id : null,
|
|
7632
7641
|
isError: b.is_error === true || failed,
|
|
7633
7642
|
text: text.trimEnd()
|
|
7634
7643
|
};
|
|
@@ -7641,6 +7650,7 @@ function steps3(events) {
|
|
|
7641
7650
|
if (b.type === "tool_use")
|
|
7642
7651
|
out.push({
|
|
7643
7652
|
kind: "tool_call",
|
|
7653
|
+
id: typeof b.id === "string" ? b.id : null,
|
|
7644
7654
|
name: typeof b.name === "string" ? b.name : "",
|
|
7645
7655
|
input: isObject(b.input) ? b.input : null
|
|
7646
7656
|
});
|
|
@@ -7791,6 +7801,7 @@ var grok = {
|
|
|
7791
7801
|
bin: BIN,
|
|
7792
7802
|
installHint: "Install grok (curl -fsSL https://x.ai/cli/install.sh | bash) or pass --harness-bin PATH.",
|
|
7793
7803
|
configDirName: ".grok",
|
|
7804
|
+
argsKey: "grok_args",
|
|
7794
7805
|
reservedArgs: [
|
|
7795
7806
|
[
|
|
7796
7807
|
[
|
|
@@ -7826,7 +7837,7 @@ var grok = {
|
|
|
7826
7837
|
"every run is a fresh session"
|
|
7827
7838
|
]
|
|
7828
7839
|
],
|
|
7829
|
-
|
|
7840
|
+
claudeSessionFile: false,
|
|
7830
7841
|
args: ({ prompt, model, extraArgs }) => [
|
|
7831
7842
|
"-p",
|
|
7832
7843
|
prompt,
|
|
@@ -7912,7 +7923,7 @@ function parseConfig(text) {
|
|
|
7912
7923
|
"cleanup",
|
|
7913
7924
|
"vars",
|
|
7914
7925
|
"env",
|
|
7915
|
-
|
|
7926
|
+
...ARGS_KEYS,
|
|
7916
7927
|
"tests"
|
|
7917
7928
|
]);
|
|
7918
7929
|
const runs = posInt(raw.runs, "runs", 1);
|
|
@@ -7927,7 +7938,7 @@ function parseConfig(text) {
|
|
|
7927
7938
|
const cleanup = commands(raw.cleanup, "cleanup", RUN_PLACEHOLDERS);
|
|
7928
7939
|
const vars = parseVars(raw.vars);
|
|
7929
7940
|
const env = parseEnv(raw.env, "env");
|
|
7930
|
-
const harnessArgs =
|
|
7941
|
+
const harnessArgs = parseAllHarnessArgs(raw, "");
|
|
7931
7942
|
if (!Array.isArray(raw.tests) || raw.tests.length === 0)
|
|
7932
7943
|
fail("tests", "must be a non-empty list");
|
|
7933
7944
|
const names = new Set;
|
|
@@ -7944,7 +7955,7 @@ function parseConfig(text) {
|
|
|
7944
7955
|
"env",
|
|
7945
7956
|
"install",
|
|
7946
7957
|
"cleanup",
|
|
7947
|
-
|
|
7958
|
+
...ARGS_KEYS
|
|
7948
7959
|
]);
|
|
7949
7960
|
const name = str2(t.name, `${p}.name`);
|
|
7950
7961
|
if (!NAME.test(name))
|
|
@@ -7974,10 +7985,7 @@ function parseConfig(text) {
|
|
|
7974
7985
|
env: { ...env, ...parseEnv(t.env, `${p}.env`) },
|
|
7975
7986
|
install: commands(t.install, `${p}.install`, RUN_PLACEHOLDERS),
|
|
7976
7987
|
cleanup: commands(t.cleanup, `${p}.cleanup`, RUN_PLACEHOLDERS),
|
|
7977
|
-
harnessArgs:
|
|
7978
|
-
...harnessArgs,
|
|
7979
|
-
...parseHarnessArgs(t.harness_args, `${p}.harness_args`)
|
|
7980
|
-
]
|
|
7988
|
+
harnessArgs: mergeHarnessArgs(harnessArgs, parseAllHarnessArgs(t, `${p}.`))
|
|
7981
7989
|
};
|
|
7982
7990
|
});
|
|
7983
7991
|
return {
|
|
@@ -8020,26 +8028,35 @@ function nameList(raw, key, what) {
|
|
|
8020
8028
|
return name;
|
|
8021
8029
|
});
|
|
8022
8030
|
}
|
|
8023
|
-
|
|
8031
|
+
var ARGS_KEYS = [...HARNESSES.values()].map((h) => h.argsKey);
|
|
8032
|
+
function parseAllHarnessArgs(raw, prefix) {
|
|
8033
|
+
const out = {};
|
|
8034
|
+
for (const h of HARNESSES.values())
|
|
8035
|
+
out[h.name] = parseHarnessArgs(raw[h.argsKey], `${prefix}${h.argsKey}`, h);
|
|
8036
|
+
return out;
|
|
8037
|
+
}
|
|
8038
|
+
function mergeHarnessArgs(top, test) {
|
|
8039
|
+
const out = {};
|
|
8040
|
+
for (const name of HARNESSES.keys())
|
|
8041
|
+
out[name] = [...top[name] ?? [], ...test[name] ?? []];
|
|
8042
|
+
return out;
|
|
8043
|
+
}
|
|
8044
|
+
function parseHarnessArgs(raw, path, harness) {
|
|
8024
8045
|
if (raw === undefined)
|
|
8025
8046
|
return [];
|
|
8026
8047
|
if (!Array.isArray(raw))
|
|
8027
|
-
fail(path,
|
|
8048
|
+
fail(path, `must be a list of ${harness.name} arguments, one per entry, e.g. [--flag, value]`);
|
|
8028
8049
|
return raw.map((a, i) => {
|
|
8029
8050
|
const p = `${path}[${i}]`;
|
|
8030
8051
|
const arg = str2(a, p);
|
|
8031
8052
|
checkPlaceholders(arg, p, RUN_PLACEHOLDERS);
|
|
8053
|
+
for (const [flags, why] of harness.reservedArgs)
|
|
8054
|
+
for (const f of flags)
|
|
8055
|
+
if (arg === f || arg.startsWith(`${f}=`))
|
|
8056
|
+
fail(p, `${f} is not allowed for ${harness.name}: ${why}`);
|
|
8032
8057
|
return { arg, path: p };
|
|
8033
8058
|
});
|
|
8034
8059
|
}
|
|
8035
|
-
function checkHarnessArgs(tests, harness) {
|
|
8036
|
-
for (const t of tests)
|
|
8037
|
-
for (const { arg, path } of t.harnessArgs)
|
|
8038
|
-
for (const [flags, why] of harness.reservedArgs)
|
|
8039
|
-
for (const f of flags)
|
|
8040
|
-
if (arg === f || arg.startsWith(`${f}=`))
|
|
8041
|
-
fail(path, `${f} is not allowed for ${harness.name}: ${why}`);
|
|
8042
|
-
}
|
|
8043
8060
|
var VAR_KEY = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
8044
8061
|
function parseVars(raw) {
|
|
8045
8062
|
if (raw === undefined)
|
|
@@ -8157,9 +8174,9 @@ import {
|
|
|
8157
8174
|
appendFileSync,
|
|
8158
8175
|
createWriteStream as createWriteStream2,
|
|
8159
8176
|
mkdirSync as mkdirSync5,
|
|
8160
|
-
writeFileSync as
|
|
8177
|
+
writeFileSync as writeFileSync5
|
|
8161
8178
|
} from "node:fs";
|
|
8162
|
-
import { basename as
|
|
8179
|
+
import { basename as basename2, delimiter as delimiter2, join as join10 } from "node:path";
|
|
8163
8180
|
|
|
8164
8181
|
// src/agent-dirs.ts
|
|
8165
8182
|
import { createHash, randomInt } from "node:crypto";
|
|
@@ -8796,19 +8813,22 @@ turns it off; FIREGOAT_TELEMETRY accepts only 0 or 1.
|
|
|
8796
8813
|
|
|
8797
8814
|
${EXIT_CODES}`;
|
|
8798
8815
|
}
|
|
8799
|
-
var INIT_HELP = `Usage: firegoat init [--force] [--local-skills | --
|
|
8816
|
+
var INIT_HELP = `Usage: firegoat init [--force] [--local-skills | --global-skills | --no-skills]
|
|
8817
|
+
[--no-login]
|
|
8800
8818
|
|
|
8801
8819
|
In the current folder: creates .firegoat/ and a starter firegoat.yaml, installs the
|
|
8802
|
-
firegoat-onboarding skill
|
|
8803
|
-
|
|
8804
|
-
firegoat login (in a terminal only; otherwise prints how), then prints a prompt to
|
|
8820
|
+
firegoat-onboarding skill (npx skills add firegoat-ai/skills; in a terminal it asks:
|
|
8821
|
+
global into ~/.claude/skills/, this repo, or none; otherwise, e.g. run by a coding
|
|
8822
|
+
agent, it installs into this repo's .claude/skills/), offers firegoat login (in a terminal only; otherwise prints how), then prints a prompt to
|
|
8805
8823
|
paste into your coding agent. .firegoat/ holds local run output and ignores itself
|
|
8806
8824
|
in git. Keep test fixtures (helper scripts, sample data) in fixtures/ next to
|
|
8807
8825
|
firegoat.yaml, committed.
|
|
8808
8826
|
|
|
8809
8827
|
--force Replace an existing firegoat.yaml
|
|
8810
|
-
--local-skills Install the skill into this repo's .claude/skills/
|
|
8811
|
-
|
|
8828
|
+
--local-skills Install the skill into this repo's .claude/skills/ (commit it
|
|
8829
|
+
so teammates and CI get it), without asking
|
|
8830
|
+
--global-skills Install the skill into ~/.claude/skills/ (skills add -g),
|
|
8831
|
+
without asking
|
|
8812
8832
|
--no-skills Don't install the skill
|
|
8813
8833
|
--no-login Don't offer firegoat login
|
|
8814
8834
|
|
|
@@ -8856,27 +8876,33 @@ firegoat.yaml reference:
|
|
|
8856
8876
|
tool that keeps state in ~ starts fresh in each
|
|
8857
8877
|
run. Only a tool that ignores HOME needs its own
|
|
8858
8878
|
var, e.g. MYTOOL_HOME: "$FIREGOAT_HOME/.mytool"
|
|
8859
|
-
|
|
8860
|
-
and --model
|
|
8861
|
-
|
|
8862
|
-
|
|
8863
|
-
|
|
8864
|
-
|
|
8865
|
-
|
|
8866
|
-
|
|
8879
|
+
claude_args: [<arg>, ...] Extra claude-code arguments, after firegoat's own
|
|
8880
|
+
flags and --model. Each entry is one argument,
|
|
8881
|
+
passed as-is (no shell splitting): [--chrome]
|
|
8882
|
+
(Claude in Chrome, for tests that need a browser),
|
|
8883
|
+
[--plugin-dir, ./plugins/x], --mcp-config,
|
|
8884
|
+
--agents, --append-system-prompt
|
|
8885
|
+
codex_args: [<arg>, ...] Extra codex-cli arguments, before the prompt:
|
|
8886
|
+
[-c, key=value], --enable, --add-dir
|
|
8887
|
+
grok_args: [<arg>, ...] Extra grok-cli arguments, after firegoat's flags.
|
|
8888
|
+
Each key goes to its harness only. A key for a
|
|
8889
|
+
harness this eval doesn't run is checked but not
|
|
8890
|
+
used, so one firegoat.yaml can carry args for
|
|
8891
|
+
every --harness.
|
|
8867
8892
|
Entries expand like env: values
|
|
8868
8893
|
($FIREGOAT_WORKDIR, $FIREGOAT_HOME, $FIREGOAT_OUT,
|
|
8869
8894
|
{{vars.KEY}}, {{run.*}}); run.json harnessArgs has
|
|
8870
|
-
the result. Flags firegoat sets
|
|
8871
|
-
as --flag=value.
|
|
8872
|
-
--output-format, --verbose,
|
|
8873
|
-
--
|
|
8895
|
+
the run's harness's result. Flags firegoat sets
|
|
8896
|
+
are errors, also as --flag=value. claude_args:
|
|
8897
|
+
-p, --print, --output-format, --verbose,
|
|
8898
|
+
--setting-sources, --strict-mcp-config,
|
|
8899
|
+
--disable-slash-commands,
|
|
8874
8900
|
--dangerously-skip-permissions, --model (use
|
|
8875
8901
|
model:), --session-id, --resume, -r, --continue,
|
|
8876
|
-
-c.
|
|
8902
|
+
-c. codex_args: --json, --skip-git-repo-check,
|
|
8877
8903
|
--dangerously-bypass-approvals-and-sandbox, -C,
|
|
8878
8904
|
--cd, -m, --model (use model:), resume, fork,
|
|
8879
|
-
--ephemeral.
|
|
8905
|
+
--ephemeral. grok_args: -p, --single,
|
|
8880
8906
|
--prompt-file, --prompt-json, --output-format,
|
|
8881
8907
|
--json-schema, --always-approve,
|
|
8882
8908
|
--permission-mode, --no-auto-update, --cwd, -w,
|
|
@@ -8894,7 +8920,8 @@ firegoat.yaml reference:
|
|
|
8894
8920
|
env: {NAME: value} Optional, on top of the top-level env:
|
|
8895
8921
|
install: <sh> | [...] Optional, runs after the top-level install
|
|
8896
8922
|
cleanup: <sh> | [...] Optional, runs before the top-level cleanup
|
|
8897
|
-
|
|
8923
|
+
claude_args: [...] Optional, after the top-level claude_args (same
|
|
8924
|
+
for codex_args, grok_args)
|
|
8898
8925
|
|
|
8899
8926
|
Step order: build once per eval; then per run: install (top-level, then the
|
|
8900
8927
|
test's), the agent, checks, cleanup (the test's, then top-level).
|
|
@@ -8930,7 +8957,7 @@ firegoat.yaml reference:
|
|
|
8930
8957
|
so the agent's commands see them too.
|
|
8931
8958
|
|
|
8932
8959
|
Testing an MCP server: commit its config as fixtures/mcp.json, copy it in build:
|
|
8933
|
-
and pass it with
|
|
8960
|
+
and pass it with claude_args. Only the configs you pass load: --strict-mcp-config
|
|
8934
8961
|
is always on. An MCP server inherits claude's env, which keeps your real HOME
|
|
8935
8962
|
(claude's login is there), so set the server's HOME to the run's home, or it
|
|
8936
8963
|
writes to yours. claude gets $FIREGOAT_HOME, $FIREGOAT_WORKDIR and $FIREGOAT_OUT
|
|
@@ -8941,7 +8968,7 @@ relative paths in it are from the run's folder, claude's cwd:
|
|
|
8941
8968
|
"env": {"HOME": "\${FIREGOAT_HOME}"}}}}
|
|
8942
8969
|
firegoat.yaml:
|
|
8943
8970
|
build: cp -R fixtures/. "$FIREGOAT_OUT"
|
|
8944
|
-
|
|
8971
|
+
claude_args: [--mcp-config, $FIREGOAT_OUT/mcp.json]
|
|
8945
8972
|
|
|
8946
8973
|
Example:
|
|
8947
8974
|
build: npm pack --pack-destination "$FIREGOAT_OUT"
|
|
@@ -8964,7 +8991,7 @@ Example:
|
|
|
8964
8991
|
Get an acme API key from the acme dashboard (I'm logged in in Chrome) and put
|
|
8965
8992
|
it in .env as ACME_API_KEY.
|
|
8966
8993
|
end_goal: .env holds a key copied from the dashboard, not a made-up one.
|
|
8967
|
-
|
|
8994
|
+
claude_args: [--chrome]
|
|
8968
8995
|
|
|
8969
8996
|
init also says that firegoat sends anonymous usage stats; FIREGOAT_TELEMETRY=0
|
|
8970
8997
|
(or DO_NOT_TRACK=1) turns them off. Fields: firegoat --help, Telemetry.
|
|
@@ -8973,7 +9000,7 @@ Exit codes: 0 done, 1 the firegoat login failed (the files stay), 2 the skill
|
|
|
8973
9000
|
install failed, 3 usage error.`;
|
|
8974
9001
|
var LOGIN_HELP = `Usage: firegoat login
|
|
8975
9002
|
|
|
8976
|
-
Logs in to firegoat.ai so eval uploads each run's
|
|
9003
|
+
Logs in to firegoat.ai so eval uploads each run's session there as a private
|
|
8977
9004
|
trace (you share it from its page). OAuth device flow: prints a URL and a code (and
|
|
8978
9005
|
opens the browser in a terminal); approve it there, and the trace key is saved to
|
|
8979
9006
|
~/.firegoat/credentials.json (mode 0600). The key is never passed to the agent,
|
|
@@ -8983,7 +9010,9 @@ read ~/.firegoat, and a FIREGOAT_API_KEY in firegoat's own process env (ps eww,
|
|
|
8983
9010
|
your own traces; revoke it on the firegoat.ai traces page.
|
|
8984
9011
|
|
|
8985
9012
|
What eval uploads per run: session.jsonl (prompts, tool calls and outputs, file
|
|
8986
|
-
contents: possibly secrets from your env or repo
|
|
9013
|
+
contents: possibly secrets from your env or repo; claude's own session file, or for
|
|
9014
|
+
codex and grok one in claude's format made from the run's stream, the run dir's
|
|
9015
|
+
session.jsonl), checks.txt and stderr.txt (check
|
|
8987
9016
|
output, which sees vars:). firegoat.ai masks secrets when showing a trace but stores
|
|
8988
9017
|
the text as sent. eval --no-upload keeps a run local.
|
|
8989
9018
|
|
|
@@ -9040,7 +9069,7 @@ the run's folder.
|
|
|
9040
9069
|
|
|
9041
9070
|
claude runs on its own login (or ANTHROPIC_API_KEY if set, as claude always does)
|
|
9042
9071
|
with a fresh config dir, ~/.firegoat/runs/<hash>/.claude: none of your settings,
|
|
9043
|
-
CLAUDE.md, plugins, skills or MCP servers (
|
|
9072
|
+
CLAUDE.md, plugins, skills or MCP servers (claude_args: in firegoat.yaml adds
|
|
9044
9073
|
claude flags, e.g. --chrome or --plugin-dir). claude saves its session there. There
|
|
9045
9074
|
is no sandbox: the agent can still reach the rest of your machine, including the
|
|
9046
9075
|
repo, if it goes looking.
|
|
@@ -9050,9 +9079,10 @@ fresh CODEX_HOME, ~/.firegoat/runs/<hash>/.codex, holding a link to your codex l
|
|
|
9050
9079
|
(~/.codex/auth.json, or $CODEX_HOME/auth.json; run codex login first), and with
|
|
9051
9080
|
-c project_doc_max_bytes=0: no AGENTS.md, config.toml, skills or plugins of
|
|
9052
9081
|
yours. codex's own HOME is the run's home (below), also for its Read/Write. It
|
|
9053
|
-
saves its session (rollout) under that CODEX_HOME's sessions/. firegoat.ai
|
|
9054
|
-
|
|
9055
|
-
cost; its model comes from the session
|
|
9082
|
+
saves its session (rollout) under that CODEX_HOME's sessions/. firegoat.ai reads
|
|
9083
|
+
claude's session format: the upload is the run dir's session.jsonl, made from the
|
|
9084
|
+
--json stream. codex reports no turns or cost; its model comes from the session
|
|
9085
|
+
file.
|
|
9056
9086
|
|
|
9057
9087
|
grok-cli runs ~/.grok/bin/grok (where grok's installer puts it; never a grok on
|
|
9058
9088
|
PATH, which may be a wrapper such as cmux's: a #! script is refused) as grok -p
|
|
@@ -9064,8 +9094,8 @@ your real one it loads Claude Code's skills, plugins, MCP servers and hooks. eva
|
|
|
9064
9094
|
refuses a login that expires within 5 minutes (run grok -p hi once to refresh it):
|
|
9065
9095
|
a refresh inside a run would replace the link with a file, which the next eval
|
|
9066
9096
|
asks you to move back over ~/.grok/auth.json. The session is a directory,
|
|
9067
|
-
$GROK_HOME/sessions/<cwd>/<id>/ ($FIREGOAT_SESSION).
|
|
9068
|
-
|
|
9097
|
+
$GROK_HOME/sessions/<cwd>/<id>/ ($FIREGOAT_SESSION). Like codex's, its upload is
|
|
9098
|
+
the run dir's session.jsonl, made from the stream in claude's session format.
|
|
9069
9099
|
|
|
9070
9100
|
Every run gets its own HOME, ~/.firegoat/runs/<hash>/<random>/home next to work
|
|
9071
9101
|
(with XDG_CONFIG_HOME, XDG_CACHE_HOME, XDG_DATA_HOME, XDG_STATE_HOME inside it), as
|
|
@@ -9112,7 +9142,8 @@ firegoat.yaml, fixtures and build: stay in the current folder.
|
|
|
9112
9142
|
--dry-run Check firegoat.yaml as eval would (test names, --var,
|
|
9113
9143
|
--model, --harness, missing vars), print each planned
|
|
9114
9144
|
run ("<test>#<n> [harness model]" with its install, checks
|
|
9115
|
-
and cleanup counts and
|
|
9145
|
+
and cleanup counts and its harness's rendered args
|
|
9146
|
+
(claude_args, ...), paths under
|
|
9116
9147
|
~/.firegoat/runs/<hash>/<random>/) and exit 0. Builds
|
|
9117
9148
|
nothing, starts no agent, uploads nothing, writes no
|
|
9118
9149
|
files. Config errors
|
|
@@ -9150,12 +9181,16 @@ Output: .firegoat/runs/<runId>/
|
|
|
9150
9181
|
<test>/<n>/run.json One run: status and reason (from the checks), harness,
|
|
9151
9182
|
model (asked for, or null) and reportedModel (what
|
|
9152
9183
|
the harness reported running, or null), harnessArgs
|
|
9153
|
-
(
|
|
9184
|
+
(its harness's claude_args/codex_args/grok_args
|
|
9185
|
+
passed, expanded), prompt, end_goal,
|
|
9154
9186
|
checks [{run, code}], cleanup [{run, code}],
|
|
9155
9187
|
leftover [{pid, command}] (processes stopped after
|
|
9156
9188
|
cleanup), and paths: dir (this folder), workdir, home
|
|
9157
9189
|
(the run's HOME; both real paths, under ~/.firegoat),
|
|
9158
|
-
transcript, session (the harness's session file),
|
|
9190
|
+
transcript, session (the harness's session file),
|
|
9191
|
+
claudeSession (the session.jsonl uploaded: claude's
|
|
9192
|
+
own file, else <test>/<n>/session.jsonl made from the
|
|
9193
|
+
stream) and sessionId (its id, the trace's), stderr,
|
|
9159
9194
|
checksLog, cleanupLog; startedAt; upload ({status: "uploaded",
|
|
9160
9195
|
url} | {status: "failed", error} | {status:
|
|
9161
9196
|
"skipped", reason}, null when not uploading)
|
|
@@ -9334,6 +9369,544 @@ async function stopLeftovers(opts) {
|
|
|
9334
9369
|
return leftover;
|
|
9335
9370
|
}
|
|
9336
9371
|
|
|
9372
|
+
// src/session.ts
|
|
9373
|
+
import { randomUUID } from "node:crypto";
|
|
9374
|
+
import { writeFileSync as writeFileSync3 } from "node:fs";
|
|
9375
|
+
|
|
9376
|
+
// src/atif.ts
|
|
9377
|
+
var MAX_BLOCK_CHARS = 50000;
|
|
9378
|
+
function flattenContent(value) {
|
|
9379
|
+
if (typeof value === "string")
|
|
9380
|
+
return value;
|
|
9381
|
+
if (Array.isArray(value)) {
|
|
9382
|
+
return value.map((item) => {
|
|
9383
|
+
if (typeof item === "string")
|
|
9384
|
+
return item;
|
|
9385
|
+
if (item && typeof item === "object") {
|
|
9386
|
+
const obj = item;
|
|
9387
|
+
if (typeof obj.text === "string")
|
|
9388
|
+
return obj.text;
|
|
9389
|
+
if (typeof obj.content === "string")
|
|
9390
|
+
return obj.content;
|
|
9391
|
+
}
|
|
9392
|
+
return JSON.stringify(item);
|
|
9393
|
+
}).join(`
|
|
9394
|
+
`);
|
|
9395
|
+
}
|
|
9396
|
+
if (value == null)
|
|
9397
|
+
return "";
|
|
9398
|
+
return JSON.stringify(value);
|
|
9399
|
+
}
|
|
9400
|
+
function cap(text) {
|
|
9401
|
+
return text.length > MAX_BLOCK_CHARS ? text.slice(0, MAX_BLOCK_CHARS) : text;
|
|
9402
|
+
}
|
|
9403
|
+
function shellQuote2(path) {
|
|
9404
|
+
return `'${path.replace(/'/g, `'\\''`)}'`;
|
|
9405
|
+
}
|
|
9406
|
+
var CAPPED_STRING_FIELDS = [
|
|
9407
|
+
"content",
|
|
9408
|
+
"old_string",
|
|
9409
|
+
"new_string",
|
|
9410
|
+
"command",
|
|
9411
|
+
"input"
|
|
9412
|
+
];
|
|
9413
|
+
function capToolInput(args) {
|
|
9414
|
+
const input = args ?? {};
|
|
9415
|
+
const serialized = JSON.stringify(input);
|
|
9416
|
+
if (serialized.length <= MAX_BLOCK_CHARS)
|
|
9417
|
+
return input;
|
|
9418
|
+
if (input && typeof input === "object") {
|
|
9419
|
+
const obj = input;
|
|
9420
|
+
const capped = {};
|
|
9421
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
9422
|
+
if (CAPPED_STRING_FIELDS.includes(key) && typeof value === "string") {
|
|
9423
|
+
capped[key] = cap(value);
|
|
9424
|
+
} else if (key === "edits" && Array.isArray(value)) {
|
|
9425
|
+
capped[key] = value.map((edit) => edit && typeof edit === "object" ? {
|
|
9426
|
+
...edit,
|
|
9427
|
+
old_string: typeof edit.old_string === "string" ? cap(edit.old_string) : edit.old_string,
|
|
9428
|
+
new_string: typeof edit.new_string === "string" ? cap(edit.new_string) : edit.new_string
|
|
9429
|
+
} : edit);
|
|
9430
|
+
} else {
|
|
9431
|
+
capped[key] = value;
|
|
9432
|
+
}
|
|
9433
|
+
}
|
|
9434
|
+
if (JSON.stringify(capped).length <= MAX_BLOCK_CHARS)
|
|
9435
|
+
return capped;
|
|
9436
|
+
}
|
|
9437
|
+
return { _truncated: serialized.slice(0, MAX_BLOCK_CHARS) };
|
|
9438
|
+
}
|
|
9439
|
+
var PATCH_FILE_MARKERS = [
|
|
9440
|
+
{ re: /^\*\*\* Update File: (.+)$/, op: "modify" },
|
|
9441
|
+
{ re: /^\*\*\* Add File: (.+)$/, op: "add" },
|
|
9442
|
+
{ re: /^\*\*\* Delete File: (.+)$/, op: "delete" }
|
|
9443
|
+
];
|
|
9444
|
+
function parseCodexPatch(patch) {
|
|
9445
|
+
if (typeof patch !== "string" || patch.length === 0)
|
|
9446
|
+
return null;
|
|
9447
|
+
const lines = patch.split(`
|
|
9448
|
+
`);
|
|
9449
|
+
const sections = [];
|
|
9450
|
+
let current = null;
|
|
9451
|
+
for (const line of lines) {
|
|
9452
|
+
if (line === "*** Begin Patch")
|
|
9453
|
+
continue;
|
|
9454
|
+
if (line === "*** End Patch")
|
|
9455
|
+
break;
|
|
9456
|
+
const marker = PATCH_FILE_MARKERS.map((m) => ({
|
|
9457
|
+
m,
|
|
9458
|
+
hit: line.match(m.re)
|
|
9459
|
+
})).find((x) => x.hit);
|
|
9460
|
+
if (marker) {
|
|
9461
|
+
current = {
|
|
9462
|
+
op: marker.m.op,
|
|
9463
|
+
path: marker.hit[1],
|
|
9464
|
+
body: []
|
|
9465
|
+
};
|
|
9466
|
+
sections.push(current);
|
|
9467
|
+
continue;
|
|
9468
|
+
}
|
|
9469
|
+
const move = line.match(/^\*\*\* Move to: (.+)$/);
|
|
9470
|
+
if (move && current) {
|
|
9471
|
+
current.renameTo = move[1];
|
|
9472
|
+
continue;
|
|
9473
|
+
}
|
|
9474
|
+
if (line.startsWith("*** "))
|
|
9475
|
+
continue;
|
|
9476
|
+
if (current)
|
|
9477
|
+
current.body.push(line);
|
|
9478
|
+
}
|
|
9479
|
+
if (sections.length === 0)
|
|
9480
|
+
return null;
|
|
9481
|
+
return sections;
|
|
9482
|
+
}
|
|
9483
|
+
function reconstructHunk(hunkLines) {
|
|
9484
|
+
const oldLines = [];
|
|
9485
|
+
const newLines = [];
|
|
9486
|
+
for (const line of hunkLines) {
|
|
9487
|
+
const marker = line[0];
|
|
9488
|
+
if (marker === "-") {
|
|
9489
|
+
oldLines.push(line.slice(1));
|
|
9490
|
+
} else if (marker === "+") {
|
|
9491
|
+
newLines.push(line.slice(1));
|
|
9492
|
+
} else if (marker === " ") {
|
|
9493
|
+
oldLines.push(line.slice(1));
|
|
9494
|
+
newLines.push(line.slice(1));
|
|
9495
|
+
} else if (line === "") {
|
|
9496
|
+
oldLines.push("");
|
|
9497
|
+
newLines.push("");
|
|
9498
|
+
}
|
|
9499
|
+
}
|
|
9500
|
+
return { old_string: oldLines.join(`
|
|
9501
|
+
`), new_string: newLines.join(`
|
|
9502
|
+
`) };
|
|
9503
|
+
}
|
|
9504
|
+
function splitHunks(body) {
|
|
9505
|
+
const hunks = [];
|
|
9506
|
+
let current = null;
|
|
9507
|
+
for (const line of body) {
|
|
9508
|
+
if (line.startsWith("@@")) {
|
|
9509
|
+
current = [];
|
|
9510
|
+
hunks.push(current);
|
|
9511
|
+
continue;
|
|
9512
|
+
}
|
|
9513
|
+
if (current)
|
|
9514
|
+
current.push(line);
|
|
9515
|
+
}
|
|
9516
|
+
return hunks.filter((h) => h.length > 0);
|
|
9517
|
+
}
|
|
9518
|
+
function sectionToBlock(section) {
|
|
9519
|
+
if (section.op === "delete") {
|
|
9520
|
+
return [
|
|
9521
|
+
{
|
|
9522
|
+
name: "Bash",
|
|
9523
|
+
input: { command: `rm -f -- ${shellQuote2(section.path)}` }
|
|
9524
|
+
}
|
|
9525
|
+
];
|
|
9526
|
+
}
|
|
9527
|
+
if (section.op === "add") {
|
|
9528
|
+
const content = section.body.filter((l) => l.startsWith("+")).map((l) => l.slice(1)).join(`
|
|
9529
|
+
`);
|
|
9530
|
+
return [{ name: "Write", input: { file_path: section.path, content } }];
|
|
9531
|
+
}
|
|
9532
|
+
const targetPath = section.renameTo ?? section.path;
|
|
9533
|
+
const rename = section.renameTo ? [
|
|
9534
|
+
{
|
|
9535
|
+
name: "Bash",
|
|
9536
|
+
input: {
|
|
9537
|
+
command: `mv -- ${shellQuote2(section.path)} ${shellQuote2(section.renameTo)}`
|
|
9538
|
+
}
|
|
9539
|
+
}
|
|
9540
|
+
] : [];
|
|
9541
|
+
const edits = splitHunks(section.body).map(reconstructHunk);
|
|
9542
|
+
if (edits.length === 0)
|
|
9543
|
+
return rename;
|
|
9544
|
+
const [only] = edits;
|
|
9545
|
+
if (edits.length === 1 && only) {
|
|
9546
|
+
return [
|
|
9547
|
+
...rename,
|
|
9548
|
+
{
|
|
9549
|
+
name: "Edit",
|
|
9550
|
+
input: {
|
|
9551
|
+
file_path: targetPath,
|
|
9552
|
+
old_string: only.old_string,
|
|
9553
|
+
new_string: only.new_string
|
|
9554
|
+
}
|
|
9555
|
+
}
|
|
9556
|
+
];
|
|
9557
|
+
}
|
|
9558
|
+
return [
|
|
9559
|
+
...rename,
|
|
9560
|
+
{ name: "MultiEdit", input: { file_path: targetPath, edits } }
|
|
9561
|
+
];
|
|
9562
|
+
}
|
|
9563
|
+
var str3 = (v) => typeof v === "string";
|
|
9564
|
+
function withReadRange(input, a) {
|
|
9565
|
+
if (typeof a.offset === "number")
|
|
9566
|
+
input.offset = a.offset;
|
|
9567
|
+
if (typeof a.limit === "number")
|
|
9568
|
+
input.limit = a.limit;
|
|
9569
|
+
return input;
|
|
9570
|
+
}
|
|
9571
|
+
var TOOL_TRANSFORMS = {
|
|
9572
|
+
shellToolCall: (a) => str3(a.command) ? { name: "Bash", input: { command: a.command } } : null,
|
|
9573
|
+
readToolCall: (a) => str3(a.path) ? { name: "Read", input: { file_path: a.path } } : null,
|
|
9574
|
+
editToolCall: (a) => str3(a.path) ? {
|
|
9575
|
+
name: "Write",
|
|
9576
|
+
input: { file_path: a.path, content: a.streamContent }
|
|
9577
|
+
} : null,
|
|
9578
|
+
globToolCall: (a) => str3(a.globPattern) ? {
|
|
9579
|
+
name: "Glob",
|
|
9580
|
+
input: { pattern: a.globPattern, path: a.targetDirectory }
|
|
9581
|
+
} : null,
|
|
9582
|
+
grepToolCall: (a) => str3(a.pattern) ? { name: "Grep", input: { pattern: a.pattern, path: a.path } } : null,
|
|
9583
|
+
webFetchToolCall: (a) => str3(a.url) ? { name: "WebFetch", input: { url: a.url } } : null,
|
|
9584
|
+
webSearchToolCall: (a) => str3(a.searchTerm) ? { name: "WebSearch", input: { query: a.searchTerm } } : null,
|
|
9585
|
+
bash: (a) => str3(a.command) ? { name: "Bash", input: { command: a.command } } : null,
|
|
9586
|
+
glob: (a) => str3(a.pattern) ? { name: "Glob", input: { pattern: a.pattern, path: a.path } } : null,
|
|
9587
|
+
grep: (a) => str3(a.pattern) ? { name: "Grep", input: { pattern: a.pattern, path: a.path } } : null,
|
|
9588
|
+
webfetch: (a) => str3(a.url) ? { name: "WebFetch", input: { url: a.url } } : null,
|
|
9589
|
+
todowrite: (a) => Array.isArray(a.todos) ? {
|
|
9590
|
+
name: "TodoWrite",
|
|
9591
|
+
input: {
|
|
9592
|
+
todos: a.todos.map((t) => ({
|
|
9593
|
+
content: t.content,
|
|
9594
|
+
status: t.status
|
|
9595
|
+
}))
|
|
9596
|
+
}
|
|
9597
|
+
} : null,
|
|
9598
|
+
read: (a) => {
|
|
9599
|
+
const filePath = str3(a.filePath) ? a.filePath : str3(a.path) ? a.path : null;
|
|
9600
|
+
return filePath ? { name: "Read", input: withReadRange({ file_path: filePath }, a) } : null;
|
|
9601
|
+
},
|
|
9602
|
+
exec: (a) => str3(a.command) ? { name: "Bash", input: { command: a.command } } : null,
|
|
9603
|
+
write: (a) => str3(a.path) ? { name: "Write", input: { file_path: a.path, content: a.content } } : null,
|
|
9604
|
+
edit: (a) => {
|
|
9605
|
+
if (!str3(a.path) || !Array.isArray(a.edits) || a.edits.length === 0)
|
|
9606
|
+
return null;
|
|
9607
|
+
if (a.edits.length === 1) {
|
|
9608
|
+
const e = a.edits[0];
|
|
9609
|
+
return {
|
|
9610
|
+
name: "Edit",
|
|
9611
|
+
input: {
|
|
9612
|
+
file_path: a.path,
|
|
9613
|
+
old_string: e.oldText,
|
|
9614
|
+
new_string: e.newText
|
|
9615
|
+
}
|
|
9616
|
+
};
|
|
9617
|
+
}
|
|
9618
|
+
return {
|
|
9619
|
+
name: "MultiEdit",
|
|
9620
|
+
input: {
|
|
9621
|
+
file_path: a.path,
|
|
9622
|
+
edits: a.edits.map((e) => ({
|
|
9623
|
+
old_string: e.oldText,
|
|
9624
|
+
new_string: e.newText
|
|
9625
|
+
}))
|
|
9626
|
+
}
|
|
9627
|
+
};
|
|
9628
|
+
},
|
|
9629
|
+
web_fetch: (a) => str3(a.url) ? { name: "WebFetch", input: { url: a.url } } : null,
|
|
9630
|
+
update_plan: (a) => Array.isArray(a.plan) ? {
|
|
9631
|
+
name: "TodoWrite",
|
|
9632
|
+
input: {
|
|
9633
|
+
todos: a.plan.map((p) => ({
|
|
9634
|
+
content: p.step,
|
|
9635
|
+
status: p.status
|
|
9636
|
+
}))
|
|
9637
|
+
}
|
|
9638
|
+
} : null,
|
|
9639
|
+
shell: (a) => str3(a.command) && (a.action === undefined || a.action === "run") ? { name: "Bash", input: { command: a.command } } : null,
|
|
9640
|
+
read_file: (a) => {
|
|
9641
|
+
if (!str3(a.path))
|
|
9642
|
+
return null;
|
|
9643
|
+
const input = { file_path: a.path };
|
|
9644
|
+
if (typeof a.start_line === "number")
|
|
9645
|
+
input.offset = a.start_line;
|
|
9646
|
+
if (typeof a.line_count === "number")
|
|
9647
|
+
input.limit = a.line_count;
|
|
9648
|
+
return { name: "Read", input };
|
|
9649
|
+
},
|
|
9650
|
+
write_file: (a) => str3(a.path) ? { name: "Write", input: { file_path: a.path, content: a.content } } : null,
|
|
9651
|
+
edit_file: (a) => str3(a.path) ? {
|
|
9652
|
+
name: "Edit",
|
|
9653
|
+
input: {
|
|
9654
|
+
file_path: a.path,
|
|
9655
|
+
old_string: a.old_string,
|
|
9656
|
+
new_string: a.new_string
|
|
9657
|
+
}
|
|
9658
|
+
} : null,
|
|
9659
|
+
glob_files: (a) => str3(a.pattern) ? { name: "Glob", input: { pattern: a.pattern, path: a.path } } : null,
|
|
9660
|
+
grep_files: (a) => str3(a.pattern) ? { name: "Grep", input: { pattern: a.pattern, path: a.path } } : null,
|
|
9661
|
+
exa_search: (a) => str3(a.query) ? { name: "WebSearch", input: { query: a.query } } : null
|
|
9662
|
+
};
|
|
9663
|
+
function normalizeToolCall(functionName, args) {
|
|
9664
|
+
const obj = args && typeof args === "object" ? args : null;
|
|
9665
|
+
if (functionName === "exec_command" && obj && "cmd" in obj) {
|
|
9666
|
+
const { cmd, ...rest } = obj;
|
|
9667
|
+
return [{ name: "Bash", input: { ...rest, command: cmd } }];
|
|
9668
|
+
}
|
|
9669
|
+
if (functionName === "apply_patch" && obj) {
|
|
9670
|
+
const rawPatch = obj.input ?? obj.patchText;
|
|
9671
|
+
const sections = parseCodexPatch(rawPatch);
|
|
9672
|
+
if (sections) {
|
|
9673
|
+
const blocks = sections.flatMap(sectionToBlock);
|
|
9674
|
+
if (blocks.length > 0)
|
|
9675
|
+
return blocks;
|
|
9676
|
+
}
|
|
9677
|
+
return [{ name: functionName, input: args }];
|
|
9678
|
+
}
|
|
9679
|
+
if (functionName === "web_search_call" && obj) {
|
|
9680
|
+
const actionType = obj.action_type;
|
|
9681
|
+
if (actionType === "open_page")
|
|
9682
|
+
return [{ name: "WebFetch", input: obj }];
|
|
9683
|
+
if (actionType === "search") {
|
|
9684
|
+
if ("query" in obj && typeof obj.query === "string")
|
|
9685
|
+
return [{ name: "WebSearch", input: obj }];
|
|
9686
|
+
const altKey = ["queries", "q", "search_query"].find((k) => (k in obj));
|
|
9687
|
+
if (altKey) {
|
|
9688
|
+
const { [altKey]: raw, ...rest } = obj;
|
|
9689
|
+
const query = Array.isArray(raw) ? raw.find((q) => typeof q === "string") : raw;
|
|
9690
|
+
if (typeof query !== "string")
|
|
9691
|
+
return [{ name: functionName, input: args }];
|
|
9692
|
+
const preserved = Array.isArray(raw) ? { queries: raw } : {};
|
|
9693
|
+
return [{ name: "WebSearch", input: { ...rest, ...preserved, query } }];
|
|
9694
|
+
}
|
|
9695
|
+
return [{ name: functionName, input: args }];
|
|
9696
|
+
}
|
|
9697
|
+
return [{ name: functionName, input: args }];
|
|
9698
|
+
}
|
|
9699
|
+
if (functionName && functionName in TOOL_TRANSFORMS && obj) {
|
|
9700
|
+
const block = TOOL_TRANSFORMS[functionName](obj);
|
|
9701
|
+
if (block)
|
|
9702
|
+
return [block];
|
|
9703
|
+
}
|
|
9704
|
+
return [{ name: functionName, input: args }];
|
|
9705
|
+
}
|
|
9706
|
+
function toolCallId(tc, step, index) {
|
|
9707
|
+
if (tc.tool_call_id)
|
|
9708
|
+
return tc.tool_call_id;
|
|
9709
|
+
return `call_${step.step_id}_${tc.function_name ?? "tool"}_${index}`;
|
|
9710
|
+
}
|
|
9711
|
+
var HARBOR_PLACEHOLDER_MESSAGES = new Set([
|
|
9712
|
+
"(tool use)",
|
|
9713
|
+
"(no assistant text)"
|
|
9714
|
+
]);
|
|
9715
|
+
function isExecutedRestatement(message, toolCalls) {
|
|
9716
|
+
const trimmed = message.trimStart();
|
|
9717
|
+
if (!trimmed.startsWith("Executed "))
|
|
9718
|
+
return false;
|
|
9719
|
+
const rest = trimmed.slice("Executed ".length);
|
|
9720
|
+
return toolCalls.some((tc) => tc.function_name && rest.startsWith(tc.function_name));
|
|
9721
|
+
}
|
|
9722
|
+
var ENVELOPE_META = [
|
|
9723
|
+
/^Chunk ID:/,
|
|
9724
|
+
/^Wall time:/,
|
|
9725
|
+
/^Original token count:/,
|
|
9726
|
+
/^Process exited with code /,
|
|
9727
|
+
/^Exit code:/
|
|
9728
|
+
];
|
|
9729
|
+
function stripCodexResultEnvelope(content) {
|
|
9730
|
+
const lines = content.split(`
|
|
9731
|
+
`);
|
|
9732
|
+
const outputIdx = lines.findIndex((l) => l === "Output:");
|
|
9733
|
+
if (outputIdx === -1)
|
|
9734
|
+
return content;
|
|
9735
|
+
const preamble = lines.slice(0, outputIdx);
|
|
9736
|
+
if (!preamble.some((l) => ENVELOPE_META.some((re) => re.test(l))))
|
|
9737
|
+
return content;
|
|
9738
|
+
let exitCode = 0;
|
|
9739
|
+
for (const line of preamble) {
|
|
9740
|
+
const m = line.match(/^(?:Process exited with code|Exit code:) (\d+)$/);
|
|
9741
|
+
if (m)
|
|
9742
|
+
exitCode = Number(m[1]);
|
|
9743
|
+
}
|
|
9744
|
+
const output = lines.slice(outputIdx + 1).join(`
|
|
9745
|
+
`);
|
|
9746
|
+
return exitCode === 0 ? output : `[exit code ${exitCode}]
|
|
9747
|
+
${output}`;
|
|
9748
|
+
}
|
|
9749
|
+
function convertAtifToSession(trajectory) {
|
|
9750
|
+
if (!Array.isArray(trajectory?.steps)) {
|
|
9751
|
+
throw new Error("atif: the trajectory has no steps array");
|
|
9752
|
+
}
|
|
9753
|
+
const events = [];
|
|
9754
|
+
for (const step of trajectory.steps) {
|
|
9755
|
+
if (step.source === "system")
|
|
9756
|
+
continue;
|
|
9757
|
+
if (step.source === "user") {
|
|
9758
|
+
const userText = flattenContent(step.message);
|
|
9759
|
+
if (userText.includes("<environment_context>"))
|
|
9760
|
+
continue;
|
|
9761
|
+
events.push({
|
|
9762
|
+
uuid: `atif-${step.step_id}-user`,
|
|
9763
|
+
type: "user",
|
|
9764
|
+
timestamp: step.timestamp,
|
|
9765
|
+
message: { role: "user", content: cap(userText) }
|
|
9766
|
+
});
|
|
9767
|
+
continue;
|
|
9768
|
+
}
|
|
9769
|
+
if (step.source !== "agent")
|
|
9770
|
+
continue;
|
|
9771
|
+
const toolCalls = step.tool_calls ?? [];
|
|
9772
|
+
const blocks = [];
|
|
9773
|
+
const reasoning = flattenContent(step.reasoning_content);
|
|
9774
|
+
if (reasoning)
|
|
9775
|
+
blocks.push({ type: "thinking", thinking: cap(reasoning) });
|
|
9776
|
+
const messageText = flattenContent(step.message);
|
|
9777
|
+
if (messageText && !isExecutedRestatement(messageText, toolCalls) && !HARBOR_PLACEHOLDER_MESSAGES.has(messageText.trim())) {
|
|
9778
|
+
blocks.push({ type: "text", text: cap(messageText) });
|
|
9779
|
+
}
|
|
9780
|
+
const toolUseIds = [];
|
|
9781
|
+
for (let i = 0;i < toolCalls.length; i++) {
|
|
9782
|
+
const tc = toolCalls[i];
|
|
9783
|
+
const id = toolCallId(tc, step, i);
|
|
9784
|
+
toolUseIds.push(id);
|
|
9785
|
+
const split = normalizeToolCall(tc.function_name, tc.arguments);
|
|
9786
|
+
split.forEach(({ name, input }, k) => {
|
|
9787
|
+
const isLast = k === split.length - 1;
|
|
9788
|
+
blocks.push({
|
|
9789
|
+
type: "tool_use",
|
|
9790
|
+
id: isLast ? id : `${id}__patch${k}`,
|
|
9791
|
+
name,
|
|
9792
|
+
input: capToolInput(input)
|
|
9793
|
+
});
|
|
9794
|
+
});
|
|
9795
|
+
}
|
|
9796
|
+
if (blocks.length > 0) {
|
|
9797
|
+
events.push({
|
|
9798
|
+
uuid: `atif-${step.step_id}-assistant`,
|
|
9799
|
+
type: "assistant",
|
|
9800
|
+
timestamp: step.timestamp,
|
|
9801
|
+
message: {
|
|
9802
|
+
id: `atif-step-${step.step_id}`,
|
|
9803
|
+
role: "assistant",
|
|
9804
|
+
model: step.model_name,
|
|
9805
|
+
content: blocks
|
|
9806
|
+
}
|
|
9807
|
+
});
|
|
9808
|
+
}
|
|
9809
|
+
const results = step.observation?.results;
|
|
9810
|
+
if (results && results.length > 0) {
|
|
9811
|
+
const resultBlocks = results.map((result, i) => {
|
|
9812
|
+
const content = result.subagent_trajectory_ref ? `[subagent trajectory: ${result.subagent_trajectory_ref}]` : stripCodexResultEnvelope(flattenContent(result.content));
|
|
9813
|
+
return {
|
|
9814
|
+
type: "tool_result",
|
|
9815
|
+
tool_use_id: result.source_call_id || toolUseIds[i] || `call_${step.step_id}_result${i}`,
|
|
9816
|
+
content: cap(content)
|
|
9817
|
+
};
|
|
9818
|
+
});
|
|
9819
|
+
events.push({
|
|
9820
|
+
uuid: `atif-${step.step_id}-results`,
|
|
9821
|
+
type: "user",
|
|
9822
|
+
timestamp: step.timestamp,
|
|
9823
|
+
message: { role: "user", content: resultBlocks }
|
|
9824
|
+
});
|
|
9825
|
+
}
|
|
9826
|
+
}
|
|
9827
|
+
return events;
|
|
9828
|
+
}
|
|
9829
|
+
|
|
9830
|
+
// src/session.ts
|
|
9831
|
+
var UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
9832
|
+
function convertedSessionId(harnessId) {
|
|
9833
|
+
return UUID.test(harnessId) ? harnessId.toLowerCase() : randomUUID();
|
|
9834
|
+
}
|
|
9835
|
+
function toTrajectory(t) {
|
|
9836
|
+
const steps = [{ step_id: 1, source: "user", message: t.prompt }];
|
|
9837
|
+
const agent = (fields) => {
|
|
9838
|
+
const step = {
|
|
9839
|
+
step_id: steps.length + 1,
|
|
9840
|
+
source: "agent",
|
|
9841
|
+
message: "",
|
|
9842
|
+
...t.model === null ? {} : { model_name: t.model },
|
|
9843
|
+
...fields
|
|
9844
|
+
};
|
|
9845
|
+
steps.push(step);
|
|
9846
|
+
return step;
|
|
9847
|
+
};
|
|
9848
|
+
const calls = new Map;
|
|
9849
|
+
const failed = new Set;
|
|
9850
|
+
let lastText = null;
|
|
9851
|
+
for (const s of t.steps) {
|
|
9852
|
+
if (s.kind === "text") {
|
|
9853
|
+
agent({ message: s.text });
|
|
9854
|
+
lastText = s.text;
|
|
9855
|
+
} else if (s.kind === "tool_call") {
|
|
9856
|
+
const id = s.id ?? `firegoat_call_${steps.length + 1}`;
|
|
9857
|
+
const step = agent({
|
|
9858
|
+
tool_calls: [
|
|
9859
|
+
{ tool_call_id: id, function_name: s.name, arguments: s.input }
|
|
9860
|
+
]
|
|
9861
|
+
});
|
|
9862
|
+
calls.set(id, step);
|
|
9863
|
+
} else {
|
|
9864
|
+
const call = s.callId === null ? undefined : calls.get(s.callId);
|
|
9865
|
+
const id = s.callId ?? `firegoat_result_${steps.length + 1}`;
|
|
9866
|
+
const result = { source_call_id: id, content: s.text };
|
|
9867
|
+
if (!call)
|
|
9868
|
+
agent({ observation: { results: [result] } });
|
|
9869
|
+
else if (call.observation)
|
|
9870
|
+
call.observation.results.push(result);
|
|
9871
|
+
else
|
|
9872
|
+
call.observation = { results: [result] };
|
|
9873
|
+
if (s.isError)
|
|
9874
|
+
failed.add(id);
|
|
9875
|
+
}
|
|
9876
|
+
}
|
|
9877
|
+
if (t.final !== "" && t.final !== lastText)
|
|
9878
|
+
agent({ message: t.final });
|
|
9879
|
+
return {
|
|
9880
|
+
trajectory: {
|
|
9881
|
+
schema_version: "ATIF-v1.6",
|
|
9882
|
+
session_id: t.sessionId,
|
|
9883
|
+
agent: {
|
|
9884
|
+
name: t.harness,
|
|
9885
|
+
...t.version === null ? {} : { version: t.version },
|
|
9886
|
+
...t.model === null ? {} : { model_name: t.model }
|
|
9887
|
+
},
|
|
9888
|
+
steps
|
|
9889
|
+
},
|
|
9890
|
+
failed
|
|
9891
|
+
};
|
|
9892
|
+
}
|
|
9893
|
+
function toClaudeSession(t) {
|
|
9894
|
+
const { trajectory, failed } = toTrajectory(t);
|
|
9895
|
+
return convertAtifToSession(trajectory).map((e) => {
|
|
9896
|
+
const content = e.message?.content;
|
|
9897
|
+
if (Array.isArray(content)) {
|
|
9898
|
+
for (const b of content)
|
|
9899
|
+
if (b.type === "tool_result" && failed.has(b.tool_use_id))
|
|
9900
|
+
b.is_error = true;
|
|
9901
|
+
}
|
|
9902
|
+
return { ...e, sessionId: t.sessionId };
|
|
9903
|
+
});
|
|
9904
|
+
}
|
|
9905
|
+
function writeClaudeSession(path, t) {
|
|
9906
|
+
writeFileSync3(path, toClaudeSession(t).map((e) => `${JSON.stringify(e)}
|
|
9907
|
+
`).join(""));
|
|
9908
|
+
}
|
|
9909
|
+
|
|
9337
9910
|
// src/steps.ts
|
|
9338
9911
|
import { createWriteStream } from "node:fs";
|
|
9339
9912
|
var STEP_TIMEOUT_MS = 600000;
|
|
@@ -9468,7 +10041,7 @@ async function sendTelemetry(event, exitCode, startedAt) {
|
|
|
9468
10041
|
}
|
|
9469
10042
|
|
|
9470
10043
|
// src/transcript.ts
|
|
9471
|
-
import { readdirSync as readdirSync5, statSync as statSync2, writeFileSync as
|
|
10044
|
+
import { readdirSync as readdirSync5, statSync as statSync2, writeFileSync as writeFileSync4 } from "node:fs";
|
|
9472
10045
|
import { join as join8, relative } from "node:path";
|
|
9473
10046
|
var OUTPUT_LINES = 15;
|
|
9474
10047
|
var OUTPUT_CHARS = 1500;
|
|
@@ -9552,7 +10125,7 @@ ${indent(truncate(s.text, OUTPUT_LINES, OUTPUT_CHARS))}` : ` → ${status}`);
|
|
|
9552
10125
|
out.pop();
|
|
9553
10126
|
out.push("", "## final answer", final ? prose(final) : "(none)");
|
|
9554
10127
|
out.push("", "## files in the workdir", ...workdirFiles(opts.workdir));
|
|
9555
|
-
|
|
10128
|
+
writeFileSync4(opts.path, `${out.join(`
|
|
9556
10129
|
`)}
|
|
9557
10130
|
`);
|
|
9558
10131
|
}
|
|
@@ -9857,11 +10430,9 @@ function ttyUI(out, opts) {
|
|
|
9857
10430
|
|
|
9858
10431
|
// src/upload.ts
|
|
9859
10432
|
import { existsSync as existsSync5, readFileSync as readFileSync5 } from "node:fs";
|
|
9860
|
-
import { basename as basename2 } from "node:path";
|
|
9861
10433
|
var MAX_FILE = 20 * 1024 * 1024;
|
|
9862
10434
|
var MAX_CHECKS = 200;
|
|
9863
10435
|
var MAX_TEXT = 8192;
|
|
9864
|
-
var UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
9865
10436
|
var FILES = ["session.jsonl", "checks.txt", "stderr.txt"];
|
|
9866
10437
|
function clip(s, max, marker) {
|
|
9867
10438
|
return s.length > max ? `${s.slice(0, max - marker.length)}${marker}` : s;
|
|
@@ -9918,9 +10489,11 @@ async function put(name, entry, bytes) {
|
|
|
9918
10489
|
throw new ApiError(`PUT ${name} ${r.status}`, r.status);
|
|
9919
10490
|
}
|
|
9920
10491
|
async function uploadRun(t, r) {
|
|
9921
|
-
if (!r.
|
|
10492
|
+
if (!r.claudeSession)
|
|
9922
10493
|
return { status: "skipped", reason: "No session file" };
|
|
9923
|
-
const sessionId =
|
|
10494
|
+
const { sessionId } = r;
|
|
10495
|
+
if (sessionId === null)
|
|
10496
|
+
throw new Error(`run ${r.test}#${r.n} has a session file but no session id`);
|
|
9924
10497
|
if (!UUID.test(sessionId))
|
|
9925
10498
|
return {
|
|
9926
10499
|
status: "failed",
|
|
@@ -9932,7 +10505,7 @@ async function uploadRun(t, r) {
|
|
|
9932
10505
|
error: `${r.checks.length} checks, the upload takes at most ${MAX_CHECKS}`
|
|
9933
10506
|
};
|
|
9934
10507
|
const paths = {
|
|
9935
|
-
"session.jsonl": r.
|
|
10508
|
+
"session.jsonl": r.claudeSession,
|
|
9936
10509
|
"checks.txt": r.checksLog,
|
|
9937
10510
|
"stderr.txt": r.stderr
|
|
9938
10511
|
};
|
|
@@ -9973,6 +10546,7 @@ async function uploadRun(t, r) {
|
|
|
9973
10546
|
num_turns: r.num_turns,
|
|
9974
10547
|
cost: r.cost,
|
|
9975
10548
|
duration_ms: r.duration_ms,
|
|
10549
|
+
harness: r.harness,
|
|
9976
10550
|
model,
|
|
9977
10551
|
...git && { git }
|
|
9978
10552
|
},
|
|
@@ -10122,7 +10696,7 @@ function newRunId() {
|
|
|
10122
10696
|
return `${ts}-${randomBytes2(2).toString("hex")}`;
|
|
10123
10697
|
}
|
|
10124
10698
|
function writeRunJson(r) {
|
|
10125
|
-
|
|
10699
|
+
writeFileSync5(join10(r.dir, "run.json"), `${JSON.stringify(r, null, 2)}
|
|
10126
10700
|
`);
|
|
10127
10701
|
}
|
|
10128
10702
|
async function uploadPreflight(opts, ui, project, runGroup, git) {
|
|
@@ -10173,8 +10747,8 @@ function runFill(runId, n, testName, out, agentDir) {
|
|
|
10173
10747
|
FIREGOAT_HOME: join10(agentDir, "home")
|
|
10174
10748
|
};
|
|
10175
10749
|
}
|
|
10176
|
-
function renderHarnessArgs(test, vars, fill) {
|
|
10177
|
-
return test.harnessArgs.map((a) => renderValue(a.arg, vars, fill, a.path));
|
|
10750
|
+
function renderHarnessArgs(test, harness, vars, fill) {
|
|
10751
|
+
return (test.harnessArgs[harness] ?? []).map((a) => renderValue(a.arg, vars, fill, a.path));
|
|
10178
10752
|
}
|
|
10179
10753
|
function renderSteps(what, commands, vars, fill) {
|
|
10180
10754
|
return commands.map((c, i) => renderStep(c, vars, fill, `${what}[${i}]`));
|
|
@@ -10211,7 +10785,7 @@ function dryRun(opts) {
|
|
|
10211
10785
|
renderEnv(test.env, opts.vars, fill);
|
|
10212
10786
|
return {
|
|
10213
10787
|
...p,
|
|
10214
|
-
harnessArgs: renderHarnessArgs(test, opts.vars, fill),
|
|
10788
|
+
harnessArgs: renderHarnessArgs(test, p.harness, opts.vars, fill),
|
|
10215
10789
|
...renderRunSteps(test, opts.config, opts.vars, fill)
|
|
10216
10790
|
};
|
|
10217
10791
|
});
|
|
@@ -10223,7 +10797,7 @@ function dryRun(opts) {
|
|
|
10223
10797
|
const count = (n, what) => `${n} ${what}${n === 1 ? "" : "s"}`;
|
|
10224
10798
|
opts.say(`Dry run: ${count(runs.length, "run")}, build ${count(opts.config.build.length, "step")}. Nothing was started.`);
|
|
10225
10799
|
for (const r of runs)
|
|
10226
|
-
opts.say(`${r.test}#${r.n}${runTag(opts.showHarness ? r.harness : null, r.model)} install ${r.install.length}, checks ${r.checks.length}, cleanup ${r.cleanup.length}${r.harnessArgs.length > 0 ? `,
|
|
10800
|
+
opts.say(`${r.test}#${r.n}${runTag(opts.showHarness ? r.harness : null, r.model)} install ${r.install.length}, checks ${r.checks.length}, cleanup ${r.cleanup.length}${r.harnessArgs.length > 0 ? `, ${HARNESSES.get(r.harness).argsKey} ${JSON.stringify(r.harnessArgs)}` : ""}`);
|
|
10227
10801
|
}
|
|
10228
10802
|
async function runOne(opts) {
|
|
10229
10803
|
const { test, n } = opts;
|
|
@@ -10243,7 +10817,7 @@ async function runOne(opts) {
|
|
|
10243
10817
|
harness: opts.harness.name,
|
|
10244
10818
|
model: opts.model,
|
|
10245
10819
|
reportedModel: null,
|
|
10246
|
-
harnessArgs: renderHarnessArgs(test, opts.vars, fill),
|
|
10820
|
+
harnessArgs: renderHarnessArgs(test, opts.harness.name, opts.vars, fill),
|
|
10247
10821
|
status: "error",
|
|
10248
10822
|
reason: "",
|
|
10249
10823
|
prompt: render(test.prompt, opts.vars),
|
|
@@ -10253,6 +10827,8 @@ async function runOne(opts) {
|
|
|
10253
10827
|
workdir,
|
|
10254
10828
|
home,
|
|
10255
10829
|
session: null,
|
|
10830
|
+
claudeSession: null,
|
|
10831
|
+
sessionId: null,
|
|
10256
10832
|
transcript: join10(dir, "transcript.txt"),
|
|
10257
10833
|
stderr: join10(dir, "stderr.txt"),
|
|
10258
10834
|
checksLog: join10(dir, "checks.txt"),
|
|
@@ -10341,6 +10917,24 @@ async function attempt(opts, r, steps, env, stepEnv, homeEnvFile, agent) {
|
|
|
10341
10917
|
if (outcome.sessionId)
|
|
10342
10918
|
r.session = harness.findSession(opts.configDir, outcome.sessionId);
|
|
10343
10919
|
r.reportedModel = outcome.reportedModel ?? (r.session && harness.sessionModel ? harness.sessionModel(r.session) : null);
|
|
10920
|
+
if (r.session && outcome.sessionId) {
|
|
10921
|
+
if (harness.claudeSessionFile) {
|
|
10922
|
+
r.claudeSession = r.session;
|
|
10923
|
+
r.sessionId = outcome.sessionId;
|
|
10924
|
+
} else {
|
|
10925
|
+
r.sessionId = convertedSessionId(outcome.sessionId);
|
|
10926
|
+
r.claudeSession = join10(dir, "session.jsonl");
|
|
10927
|
+
writeClaudeSession(r.claudeSession, {
|
|
10928
|
+
sessionId: r.sessionId,
|
|
10929
|
+
harness: harness.name,
|
|
10930
|
+
version: outcome.version,
|
|
10931
|
+
model: r.reportedModel,
|
|
10932
|
+
prompt: r.prompt,
|
|
10933
|
+
steps: outcome.steps,
|
|
10934
|
+
final: outcome.final
|
|
10935
|
+
});
|
|
10936
|
+
}
|
|
10937
|
+
}
|
|
10344
10938
|
const about = [
|
|
10345
10939
|
opts.shownHarness,
|
|
10346
10940
|
opts.model === null ? null : `model ${opts.model}`
|
|
@@ -10390,9 +10984,6 @@ async function attempt(opts, r, steps, env, stepEnv, homeEnvFile, agent) {
|
|
|
10390
10984
|
test.checks.length === 0 ? "no script checks" : `${r.checks.length}/${r.checks.length} checks passed`
|
|
10391
10985
|
];
|
|
10392
10986
|
}
|
|
10393
|
-
function notUploaded(h) {
|
|
10394
|
-
return `firegoat.ai doesn't read ${h.name} sessions yet: its runs stay local`;
|
|
10395
|
-
}
|
|
10396
10987
|
var MAX_LISTED_URLS = 5;
|
|
10397
10988
|
function uploadedSummary(origin, results, runDir) {
|
|
10398
10989
|
const urls = results.flatMap((r) => r.upload?.status === "uploaded" ? [r.upload.url] : []);
|
|
@@ -10431,7 +11022,7 @@ async function evalCommand(args, root) {
|
|
|
10431
11022
|
const say = opts.json ? console.error : console.log;
|
|
10432
11023
|
const stream = opts.json ? process.stderr : process.stdout;
|
|
10433
11024
|
const ui = stream.isTTY ? ttyUI(stream, {
|
|
10434
|
-
project:
|
|
11025
|
+
project: basename2(root),
|
|
10435
11026
|
version: package_default.version,
|
|
10436
11027
|
color: !process.env.NO_COLOR
|
|
10437
11028
|
}) : plainUI(say);
|
|
@@ -10460,14 +11051,6 @@ async function evalCommand(args, root) {
|
|
|
10460
11051
|
return fatal(EXIT.usage, `--harness-bin needs exactly one harness, this eval runs ${harnessNames.length} (${harnessNames.join(", ")}): pick one with --harness`, "usage");
|
|
10461
11052
|
const harnesses = harnessNames.map((h) => HARNESSES.get(h));
|
|
10462
11053
|
const showHarness = showsHarness(harnessNames);
|
|
10463
|
-
try {
|
|
10464
|
-
for (const h of harnesses)
|
|
10465
|
-
checkHarnessArgs(tests, h);
|
|
10466
|
-
} catch (e) {
|
|
10467
|
-
if (e instanceof ConfigError)
|
|
10468
|
-
return fatal(EXIT.usage, e.message, "config");
|
|
10469
|
-
throw e;
|
|
10470
|
-
}
|
|
10471
11054
|
evalStats({
|
|
10472
11055
|
testCount: tests.length,
|
|
10473
11056
|
runCount: tests.length * runs * Math.max(models.length, 1) * harnesses.length,
|
|
@@ -10480,7 +11063,7 @@ async function evalCommand(args, root) {
|
|
|
10480
11063
|
t.prompt,
|
|
10481
11064
|
...t.checks.map((c) => c.run),
|
|
10482
11065
|
...Object.values(t.env),
|
|
10483
|
-
...t.harnessArgs.map((a) => a.arg),
|
|
11066
|
+
...harnessNames.flatMap((h) => (t.harnessArgs[h] ?? []).map((a) => a.arg)),
|
|
10484
11067
|
...t.install,
|
|
10485
11068
|
...t.cleanup
|
|
10486
11069
|
]).concat(config.build, config.install, config.cleanup).flatMap(varRefs).filter((k) => !(k in vars)))
|
|
@@ -10552,15 +11135,10 @@ async function evalCommand(args, root) {
|
|
|
10552
11135
|
const runId = newRunId();
|
|
10553
11136
|
let upload;
|
|
10554
11137
|
try {
|
|
10555
|
-
const pre = await uploadPreflight(opts, ui,
|
|
11138
|
+
const pre = await uploadPreflight(opts, ui, basename2(repo?.mainCheckout ?? repo?.toplevel ?? root), runId, repo?.info ?? null);
|
|
10556
11139
|
if (pre && "exitCode" in pre)
|
|
10557
11140
|
return fatal(pre.exitCode, pre.error, pre.code);
|
|
10558
11141
|
upload = pre;
|
|
10559
|
-
if (upload) {
|
|
10560
|
-
for (const h of harnesses)
|
|
10561
|
-
if (!h.uploads)
|
|
10562
|
-
ui.note(notUploaded(h));
|
|
10563
|
-
}
|
|
10564
11142
|
} catch (e) {
|
|
10565
11143
|
if (e instanceof ConfigError)
|
|
10566
11144
|
return fatal(EXIT.usage, e.message, "config");
|
|
@@ -10645,10 +11223,7 @@ async function evalCommand(args, root) {
|
|
|
10645
11223
|
command: undefined
|
|
10646
11224
|
});
|
|
10647
11225
|
ui.update();
|
|
10648
|
-
if (upload
|
|
10649
|
-
r.upload = { status: "skipped", reason: notUploaded(harness) };
|
|
10650
|
-
writeRunJson(r);
|
|
10651
|
-
} else if (upload) {
|
|
11226
|
+
if (upload) {
|
|
10652
11227
|
row.upload = "uploading";
|
|
10653
11228
|
ui.update();
|
|
10654
11229
|
r.upload = await uploadRun(upload, r);
|
|
@@ -10694,7 +11269,7 @@ async function evalCommand(args, root) {
|
|
|
10694
11269
|
status,
|
|
10695
11270
|
exitCode
|
|
10696
11271
|
};
|
|
10697
|
-
|
|
11272
|
+
writeFileSync5(join10(runDir, "summary.json"), `${JSON.stringify(doc, null, 2)}
|
|
10698
11273
|
`);
|
|
10699
11274
|
appendFileSync(join10(runsRoot, "index.jsonl"), `${JSON.stringify({
|
|
10700
11275
|
runId,
|
|
@@ -11011,8 +11586,8 @@ firegoat ${package_default.version}` : `firegoat ${package_default.version}`,
|
|
|
11011
11586
|
|
|
11012
11587
|
// src/init.ts
|
|
11013
11588
|
import { spawn as spawn4, spawnSync as spawnSync4 } from "node:child_process";
|
|
11014
|
-
import { existsSync as existsSync6, writeFileSync as
|
|
11015
|
-
import { basename as
|
|
11589
|
+
import { existsSync as existsSync6, writeFileSync as writeFileSync6 } from "node:fs";
|
|
11590
|
+
import { basename as basename3, join as join11 } from "node:path";
|
|
11016
11591
|
|
|
11017
11592
|
// src/login.ts
|
|
11018
11593
|
import { spawn as spawn3 } from "node:child_process";
|
|
@@ -11023,7 +11598,7 @@ var FORM = { "Content-Type": "application/x-www-form-urlencoded" };
|
|
|
11023
11598
|
var UPLOADS_ON = "Eval uploads each session to firegoat.ai as a private trace; --no-upload keeps runs local.";
|
|
11024
11599
|
var UPLOAD_NOTICE = `${UPLOADS_ON} A session holds prompts, tool outputs and file contents: possibly secrets from your env or repo. The agent runs as you and can read ~/.firegoat (and a FIREGOAT_API_KEY in firegoat's own process env).`;
|
|
11025
11600
|
var TERMS = "By logging in, you agree to the firegoat.ai Terms of Service (https://firegoat.ai/terms) and Privacy Policy (https://firegoat.ai/privacy).";
|
|
11026
|
-
function
|
|
11601
|
+
function str4(o, k, step) {
|
|
11027
11602
|
const v = o?.[k];
|
|
11028
11603
|
if (typeof v !== "string" || v === "")
|
|
11029
11604
|
throw new ApiError(`${step}: response has no ${k}`);
|
|
@@ -11054,9 +11629,9 @@ async function loginFlow(show, interactive) {
|
|
|
11054
11629
|
});
|
|
11055
11630
|
if (cfg.status !== 200)
|
|
11056
11631
|
throw replyError(cfgStep, cfg);
|
|
11057
|
-
const clientId =
|
|
11058
|
-
const deviceUrl = assertSafeUrl(
|
|
11059
|
-
const tokenUrl = assertSafeUrl(
|
|
11632
|
+
const clientId = str4(cfg.json, "client_id", cfgStep);
|
|
11633
|
+
const deviceUrl = assertSafeUrl(str4(cfg.json, "device_authorization_endpoint", cfgStep), "device_authorization_endpoint");
|
|
11634
|
+
const tokenUrl = assertSafeUrl(str4(cfg.json, "token_endpoint", cfgStep), "token_endpoint");
|
|
11060
11635
|
if (tokenUrl.origin !== origin)
|
|
11061
11636
|
throw new ApiError(`token_endpoint must be on ${origin}, got ${printable(tokenUrl.origin, 100)}`);
|
|
11062
11637
|
const devStep = "Device authorization";
|
|
@@ -11065,16 +11640,19 @@ async function loginFlow(show, interactive) {
|
|
|
11065
11640
|
url: deviceUrl,
|
|
11066
11641
|
method: "POST",
|
|
11067
11642
|
headers: FORM,
|
|
11068
|
-
body: new URLSearchParams({
|
|
11643
|
+
body: new URLSearchParams({
|
|
11644
|
+
client_id: clientId,
|
|
11645
|
+
scope: "openid profile email"
|
|
11646
|
+
}).toString(),
|
|
11069
11647
|
timeoutMs: TIMEOUT_MS3
|
|
11070
11648
|
});
|
|
11071
11649
|
if (dev.status !== 200)
|
|
11072
11650
|
throw replyError(devStep, dev);
|
|
11073
|
-
const deviceCode =
|
|
11074
|
-
const userCode =
|
|
11651
|
+
const deviceCode = str4(dev.json, "device_code", devStep);
|
|
11652
|
+
const userCode = str4(dev.json, "user_code", devStep);
|
|
11075
11653
|
if (!/^[A-Za-z0-9-]{1,64}$/.test(userCode))
|
|
11076
11654
|
throw new ApiError(`${devStep}: user_code has unexpected characters`);
|
|
11077
|
-
const pageRaw = typeof dev.json?.verification_uri_complete === "string" ? dev.json.verification_uri_complete :
|
|
11655
|
+
const pageRaw = typeof dev.json?.verification_uri_complete === "string" ? dev.json.verification_uri_complete : str4(dev.json, "verification_uri", devStep);
|
|
11078
11656
|
const page = assertSafeUrl(pageRaw, "verification_uri");
|
|
11079
11657
|
if (page.protocol !== "https:" || pageRaw.startsWith("-"))
|
|
11080
11658
|
throw new ApiError("verification_uri must be https");
|
|
@@ -11376,6 +11954,11 @@ runs: 1
|
|
|
11376
11954
|
# MYTOOL_HOME: "$FIREGOAT_HOME/.mytool"
|
|
11377
11955
|
# UV_FIND_LINKS: "$FIREGOAT_OUT"
|
|
11378
11956
|
|
|
11957
|
+
# Optional: extra arguments for one harness, after firegoat's own flags, one per
|
|
11958
|
+
# entry: claude_args, codex_args, grok_args. A test can add its own. E.g. Claude in
|
|
11959
|
+
# Chrome, for tests that need a browser:
|
|
11960
|
+
# claude_args: [--chrome]
|
|
11961
|
+
|
|
11379
11962
|
# Optional: values for {{vars.KEY}} in prompts, checks and build/install/cleanup
|
|
11380
11963
|
# steps (pasted in as-is, unquoted); eval --var KEY=value overrides them. Steps
|
|
11381
11964
|
# and checks also get each one as an env var ("$KEY": safe for any value).
|
|
@@ -11532,8 +12115,7 @@ ${UPLOADS_ON}` : LOGIN_HINT);
|
|
|
11532
12115
|
}
|
|
11533
12116
|
async function initCommand(args, cwd) {
|
|
11534
12117
|
let force = false;
|
|
11535
|
-
|
|
11536
|
-
let localSkills = false;
|
|
12118
|
+
const skillFlags = [];
|
|
11537
12119
|
let login = true;
|
|
11538
12120
|
for (const a of args) {
|
|
11539
12121
|
if (a === "--help" || a === "-h") {
|
|
@@ -11543,9 +12125,11 @@ async function initCommand(args, cwd) {
|
|
|
11543
12125
|
if (a === "--force" || a === "-f")
|
|
11544
12126
|
force = true;
|
|
11545
12127
|
else if (a === "--no-skills")
|
|
11546
|
-
|
|
12128
|
+
skillFlags.push({ flag: a, scope: "none" });
|
|
11547
12129
|
else if (a === "--local-skills")
|
|
11548
|
-
|
|
12130
|
+
skillFlags.push({ flag: a, scope: "local" });
|
|
12131
|
+
else if (a === "--global-skills")
|
|
12132
|
+
skillFlags.push({ flag: a, scope: "global" });
|
|
11549
12133
|
else if (a === "--no-login")
|
|
11550
12134
|
login = false;
|
|
11551
12135
|
else {
|
|
@@ -11555,15 +12139,17 @@ ${INIT_HELP}`);
|
|
|
11555
12139
|
return EXIT.usage;
|
|
11556
12140
|
}
|
|
11557
12141
|
}
|
|
11558
|
-
|
|
11559
|
-
|
|
12142
|
+
const flags = [...new Set(skillFlags.map((f) => f.flag))];
|
|
12143
|
+
if (flags.length > 1) {
|
|
12144
|
+
console.error(`firegoat init: ${flags.join(" and ")} contradict each other: pass one
|
|
11560
12145
|
|
|
11561
12146
|
${INIT_HELP}`);
|
|
11562
12147
|
return EXIT.usage;
|
|
11563
12148
|
}
|
|
11564
|
-
let scope = noSkills ? "none" : localSkills ? "local" : "global";
|
|
11565
|
-
initSkills(scope);
|
|
11566
12149
|
const interactive = Boolean(process.stdin.isTTY && process.stdout.isTTY);
|
|
12150
|
+
const asked = skillFlags[0] === undefined && interactive;
|
|
12151
|
+
let scope = skillFlags[0]?.scope ?? (interactive ? "global" : "local");
|
|
12152
|
+
initSkills(scope);
|
|
11567
12153
|
const color = interactive && !process.env.NO_COLOR;
|
|
11568
12154
|
const paint = (code, text) => color ? `\x1B[${code}m${text}\x1B[0m` : text;
|
|
11569
12155
|
const width = Math.min(process.stdout.columns || 80, 80);
|
|
@@ -11578,7 +12164,7 @@ ${INIT_HELP}`);
|
|
|
11578
12164
|
const configPath = join11(cwd, CONFIG_FILE);
|
|
11579
12165
|
const keep = existsSync6(configPath) && !force;
|
|
11580
12166
|
if (!keep)
|
|
11581
|
-
|
|
12167
|
+
writeFileSync6(configPath, template(basename3(cwd)));
|
|
11582
12168
|
if (ui) {
|
|
11583
12169
|
console.log(ui.done(1, CONFIG_FILE, keep ? "Kept (--force replaces it)" : "Created"));
|
|
11584
12170
|
ui.gap();
|
|
@@ -11588,7 +12174,7 @@ ${INIT_HELP}`);
|
|
|
11588
12174
|
const skill = "firegoat-onboarding skill";
|
|
11589
12175
|
const block = liveBlock();
|
|
11590
12176
|
ui?.gap();
|
|
11591
|
-
if (ui &&
|
|
12177
|
+
if (ui && asked) {
|
|
11592
12178
|
const picked = await ui.ask(block, 3, "Install the firegoat-onboarding skill", [
|
|
11593
12179
|
["Global", skillsDir("global")],
|
|
11594
12180
|
["This repo", `${skillsDir("local")} (commit it for your team)`],
|