firegoat 0.23.0 → 0.24.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/dist/cli.js +343 -132
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -6947,7 +6947,7 @@ var require_public_api = __commonJS(function(exports) {
|
|
|
6947
6947
|
// package.json
|
|
6948
6948
|
var package_default = {
|
|
6949
6949
|
name: "firegoat",
|
|
6950
|
-
version: "0.
|
|
6950
|
+
version: "0.24.0",
|
|
6951
6951
|
repository: {
|
|
6952
6952
|
type: "git",
|
|
6953
6953
|
url: "git+https://github.com/firegoat-ai/firegoat-cli.git"
|
|
@@ -7086,9 +7086,9 @@ function parseConfig(text) {
|
|
|
7086
7086
|
]);
|
|
7087
7087
|
const runs = posInt(raw.runs, "runs", 1);
|
|
7088
7088
|
const models = parseModels(raw.model);
|
|
7089
|
-
const build = commands(raw.build, "build");
|
|
7090
|
-
const install = commands(raw.install, "install");
|
|
7091
|
-
const cleanup = commands(raw.cleanup, "cleanup");
|
|
7089
|
+
const build = commands(raw.build, "build", EVAL_PLACEHOLDERS);
|
|
7090
|
+
const install = commands(raw.install, "install", RUN_PLACEHOLDERS);
|
|
7091
|
+
const cleanup = commands(raw.cleanup, "cleanup", RUN_PLACEHOLDERS);
|
|
7092
7092
|
const vars = parseVars(raw.vars);
|
|
7093
7093
|
const env = parseEnv(raw.env, "env");
|
|
7094
7094
|
const claudeArgs = parseClaudeArgs(raw.claude_args, "claude_args");
|
|
@@ -7125,7 +7125,9 @@ function parseConfig(text) {
|
|
|
7125
7125
|
if (!isRecord(c))
|
|
7126
7126
|
fail(cp, "must be a mapping");
|
|
7127
7127
|
onlyKeys(c, cp, ["run"]);
|
|
7128
|
-
|
|
7128
|
+
const run = str(c.run, `${cp}.run`);
|
|
7129
|
+
checkPlaceholders(run, `${cp}.run`, RUN_PLACEHOLDERS);
|
|
7130
|
+
checks.push({ run });
|
|
7129
7131
|
});
|
|
7130
7132
|
return {
|
|
7131
7133
|
name,
|
|
@@ -7134,8 +7136,8 @@ function parseConfig(text) {
|
|
|
7134
7136
|
checks,
|
|
7135
7137
|
timeout: posInt(t.timeout, `${p}.timeout`, DEFAULT_TIMEOUT),
|
|
7136
7138
|
env: { ...env, ...parseEnv(t.env, `${p}.env`) },
|
|
7137
|
-
install: commands(t.install, `${p}.install
|
|
7138
|
-
cleanup: commands(t.cleanup, `${p}.cleanup
|
|
7139
|
+
install: commands(t.install, `${p}.install`, RUN_PLACEHOLDERS),
|
|
7140
|
+
cleanup: commands(t.cleanup, `${p}.cleanup`, RUN_PLACEHOLDERS),
|
|
7139
7141
|
claudeArgs: [
|
|
7140
7142
|
...claudeArgs,
|
|
7141
7143
|
...parseClaudeArgs(t.claude_args, `${p}.claude_args`)
|
|
@@ -7154,14 +7156,18 @@ function parseConfig(text) {
|
|
|
7154
7156
|
};
|
|
7155
7157
|
}
|
|
7156
7158
|
var NAME = /^[A-Za-z0-9._-]+$/;
|
|
7157
|
-
function commands(raw, path) {
|
|
7159
|
+
function commands(raw, path, allowed) {
|
|
7158
7160
|
if (raw === undefined)
|
|
7159
7161
|
return [];
|
|
7160
|
-
if (typeof raw
|
|
7161
|
-
return [str(raw, path)];
|
|
7162
|
-
if (!Array.isArray(raw))
|
|
7162
|
+
if (typeof raw !== "string" && !Array.isArray(raw))
|
|
7163
7163
|
fail(path, "must be a shell command or a list of them");
|
|
7164
|
-
|
|
7164
|
+
const list = typeof raw === "string" ? [raw] : raw;
|
|
7165
|
+
return list.map((c, i) => {
|
|
7166
|
+
const p = typeof raw === "string" ? path : `${path}[${i}]`;
|
|
7167
|
+
const cmd = str(c, p);
|
|
7168
|
+
checkPlaceholders(cmd, p, allowed);
|
|
7169
|
+
return cmd;
|
|
7170
|
+
});
|
|
7165
7171
|
}
|
|
7166
7172
|
function parseModels(raw) {
|
|
7167
7173
|
if (raw === undefined)
|
|
@@ -7211,7 +7217,7 @@ function parseClaudeArgs(raw, path) {
|
|
|
7211
7217
|
for (const f of flags)
|
|
7212
7218
|
if (arg === f || arg.startsWith(`${f}=`))
|
|
7213
7219
|
fail(p, `${f} is not allowed: ${why}`);
|
|
7214
|
-
checkPlaceholders(arg, p);
|
|
7220
|
+
checkPlaceholders(arg, p, RUN_PLACEHOLDERS);
|
|
7215
7221
|
return arg;
|
|
7216
7222
|
});
|
|
7217
7223
|
}
|
|
@@ -7267,16 +7273,16 @@ function parseEnv(raw, path) {
|
|
|
7267
7273
|
fail(p, "is set by firegoat and cannot be overridden (reserved: PATH, CLAUDE_CONFIG_DIR, CLAUDE_SECURESTORAGE_CONFIG_DIR, FIREGOAT_*)");
|
|
7268
7274
|
if (typeof v !== "string")
|
|
7269
7275
|
fail(p, "must be a string (quote numbers and booleans)");
|
|
7270
|
-
checkPlaceholders(v, p);
|
|
7276
|
+
checkPlaceholders(v, p, RUN_PLACEHOLDERS);
|
|
7271
7277
|
out[k] = v;
|
|
7272
7278
|
}
|
|
7273
7279
|
return out;
|
|
7274
7280
|
}
|
|
7275
|
-
function checkPlaceholders(v, p) {
|
|
7281
|
+
function checkPlaceholders(v, p, allowed) {
|
|
7276
7282
|
for (const m of v.matchAll(ANY_PLACEHOLDER)) {
|
|
7277
7283
|
const name = m[1];
|
|
7278
|
-
if (!/^vars\.[A-Za-z0-9_]+$/.test(name) && !
|
|
7279
|
-
fail(p, `unknown placeholder ${m[0]} (allowed: {{vars.KEY}}, ${
|
|
7284
|
+
if (!/^vars\.[A-Za-z0-9_]+$/.test(name) && !allowed.includes(name))
|
|
7285
|
+
fail(p, `unknown placeholder ${m[0]} (allowed: {{vars.KEY}}, ${allowed.map((n) => `{{${n}}}`).join(", ")})`);
|
|
7280
7286
|
}
|
|
7281
7287
|
}
|
|
7282
7288
|
function isEvalLevel(value) {
|
|
@@ -7287,13 +7293,16 @@ function isEvalLevel(value) {
|
|
|
7287
7293
|
});
|
|
7288
7294
|
}
|
|
7289
7295
|
function renderValue(value, vars, run, where) {
|
|
7290
|
-
return value.replace(ENV_REF, (whole, name, braced, bare) =>
|
|
7291
|
-
|
|
7292
|
-
|
|
7293
|
-
|
|
7294
|
-
|
|
7295
|
-
|
|
7296
|
-
|
|
7296
|
+
return value.replace(ENV_REF, (whole, name, braced, bare) => lookup(whole, name ?? braced ?? bare, vars, run, where));
|
|
7297
|
+
}
|
|
7298
|
+
function renderStep(cmd, vars, run, where) {
|
|
7299
|
+
return cmd.replace(ANY_PLACEHOLDER, (whole, name) => lookup(whole, name, vars, run, where));
|
|
7300
|
+
}
|
|
7301
|
+
function lookup(whole, key, vars, run, where) {
|
|
7302
|
+
const filled = key.startsWith("vars.") ? vars[key.slice("vars.".length)] : run[key];
|
|
7303
|
+
if (filled === undefined)
|
|
7304
|
+
throw new ConfigError(`${where}: no value for ${whole}`);
|
|
7305
|
+
return filled;
|
|
7297
7306
|
}
|
|
7298
7307
|
function renderEnv(env, vars, run) {
|
|
7299
7308
|
const out = {};
|
|
@@ -8071,8 +8080,9 @@ firegoat.yaml reference:
|
|
|
8071
8080
|
what the agent left running (servers, daemons).
|
|
8072
8081
|
Every step runs; a failure is a warning (run.json
|
|
8073
8082
|
cleanup, <test>/<n>/cleanup.txt), never the verdict
|
|
8074
|
-
vars: {KEY: value} Defaults for {{vars.KEY}} in prompts and
|
|
8075
|
-
--var KEY=value
|
|
8083
|
+
vars: {KEY: value} Defaults for {{vars.KEY}} in prompts, checks and
|
|
8084
|
+
build, install and cleanup steps; --var KEY=value
|
|
8085
|
+
overrides. A missing value is a config error
|
|
8076
8086
|
env: {NAME: value} Env vars for the agent, and for install, checks
|
|
8077
8087
|
and cleanup of the same run; values are strings and can
|
|
8078
8088
|
use {{vars.KEY}}, {{run.id}}, {{run.n}},
|
|
@@ -8123,6 +8133,11 @@ firegoat.yaml reference:
|
|
|
8123
8133
|
from a template project, commit it as e.g. fixtures/starter-app/, then:
|
|
8124
8134
|
build: cp -R fixtures/starter-app "$FIREGOAT_OUT/starter-app"
|
|
8125
8135
|
install: cp -R "$FIREGOAT_OUT/starter-app/." .
|
|
8136
|
+
Placeholders in build, install, cleanup and checks: {{vars.KEY}}, {{run.id}},
|
|
8137
|
+
{{run.n}}, {{test.name}} (build: only {{vars.KEY}} and {{run.id}}); others are
|
|
8138
|
+
config errors. Values are pasted into the shell text as-is, unquoted: quote
|
|
8139
|
+
them yourself ('{{vars.KEY}}'), or use the env var below ("$KEY"), which is safe
|
|
8140
|
+
for any value.
|
|
8126
8141
|
The build, install and cleanup commands and the checks get the vars as env
|
|
8127
8142
|
vars, plus $FIREGOAT_OUT and $FIREGOAT_WORKDIR; install, checks and cleanup also
|
|
8128
8143
|
get $FIREGOAT_HOME, the run's own home, as HOME (build keeps your real HOME).
|
|
@@ -8269,10 +8284,12 @@ fixtures and build: stay in the current folder.
|
|
|
8269
8284
|
--parallel N Runs at once (default 4; 1 runs them one after another).
|
|
8270
8285
|
Each run has its own HOME; state outside it (a fixed port,
|
|
8271
8286
|
/tmp paths) can still collide: use 1 then
|
|
8272
|
-
--var KEY=value Fills {{vars.KEY}}
|
|
8273
|
-
cleanup
|
|
8287
|
+
--var KEY=value Fills {{vars.KEY}} (prompts, checks, build, install,
|
|
8288
|
+
cleanup); env for build, install, checks and cleanup
|
|
8274
8289
|
--json The summary as JSON on stdout (or {error, exitCode} if eval
|
|
8275
|
-
stops early); progress goes to stderr
|
|
8290
|
+
stops early); progress goes to stderr. The verdict comes
|
|
8291
|
+
last, after runs: totals {pass, fail, error}, status
|
|
8292
|
+
("pass", "fail" or "error" = not graded) and exitCode
|
|
8276
8293
|
--no-upload Keep runs local even when logged in to firegoat.ai
|
|
8277
8294
|
--dry-run Check firegoat.yaml as eval would (test names, --var,
|
|
8278
8295
|
--model, missing vars), print each planned run
|
|
@@ -8281,7 +8298,8 @@ fixtures and build: stay in the current folder.
|
|
|
8281
8298
|
runs/<runId>/) and exit 0. Builds nothing, starts no
|
|
8282
8299
|
claude, uploads nothing, writes no files. Config errors
|
|
8283
8300
|
exit 3. With --json: {dryRun, build, runs: [{test, n,
|
|
8284
|
-
model, claudeArgs, install, checks, cleanup}]}
|
|
8301
|
+
model, claudeArgs, install, checks, cleanup}]}, steps
|
|
8302
|
+
and checks with their placeholders filled
|
|
8285
8303
|
--claude-bin PATH Path to claude (default: claude on PATH)
|
|
8286
8304
|
|
|
8287
8305
|
Upload: after firegoat login (or with FIREGOAT_API_KEY), each run's claude session,
|
|
@@ -8295,9 +8313,11 @@ Runs without a session file are not uploaded. FIREGOAT_BASE_URL points elsewhere
|
|
|
8295
8313
|
(default https://firegoat.ai/evals). See firegoat login --help.
|
|
8296
8314
|
|
|
8297
8315
|
Output: .firegoat/runs/<runId>/
|
|
8298
|
-
summary.json
|
|
8299
|
-
git {branch, commit, dirty} (no git outside a
|
|
8300
|
-
|
|
8316
|
+
summary.json firegoatVersion, runId, runsDir, dir (where eval
|
|
8317
|
+
ran), git {branch, commit, dirty} (no git outside a
|
|
8318
|
+
repo), upload {origin, email} (null when not
|
|
8319
|
+
uploading), runs (every run.json), then the verdict:
|
|
8320
|
+
totals, status and exitCode
|
|
8301
8321
|
../index.jsonl One line per eval: runId, startedAt, dir, branch,
|
|
8302
8322
|
commit, passed, failed, notGraded, cost (null when
|
|
8303
8323
|
any run's cost is unknown), uploaded, uploadFailed
|
|
@@ -9212,6 +9232,19 @@ function runFill(runId, n, testName, out, dir) {
|
|
|
9212
9232
|
function renderClaudeArgs(test, vars, fill) {
|
|
9213
9233
|
return test.claudeArgs.map((a, i) => renderValue(a, vars, fill, `claude_args[${i}]`));
|
|
9214
9234
|
}
|
|
9235
|
+
function renderSteps(what, commands, vars, fill) {
|
|
9236
|
+
return commands.map((c, i) => renderStep(c, vars, fill, `${what}[${i}]`));
|
|
9237
|
+
}
|
|
9238
|
+
function renderRunSteps(test, top, vars, fill) {
|
|
9239
|
+
return {
|
|
9240
|
+
install: renderSteps("install", [...top.install, ...test.install], vars, fill),
|
|
9241
|
+
checks: renderSteps("checks", test.checks.map((c) => c.run), vars, fill),
|
|
9242
|
+
cleanup: renderSteps("cleanup", [...test.cleanup, ...top.cleanup], vars, fill)
|
|
9243
|
+
};
|
|
9244
|
+
}
|
|
9245
|
+
function renderBuild(config, vars, runId) {
|
|
9246
|
+
return renderSteps("build", config.build, vars, { "run.id": runId });
|
|
9247
|
+
}
|
|
9215
9248
|
function planRuns(tests, models, runs) {
|
|
9216
9249
|
const matrix = models.length > 0 ? models : [null];
|
|
9217
9250
|
return tests.flatMap((t) => matrix.flatMap((model, m) => Array.from({ length: runs }, (_, i) => ({
|
|
@@ -9230,13 +9263,12 @@ function dryRun(opts) {
|
|
|
9230
9263
|
return {
|
|
9231
9264
|
...p,
|
|
9232
9265
|
claudeArgs: renderClaudeArgs(test, opts.vars, fill),
|
|
9233
|
-
|
|
9234
|
-
checks: test.checks.map((c) => render(c.run, opts.vars)),
|
|
9235
|
-
cleanup: [...test.cleanup, ...opts.config.cleanup]
|
|
9266
|
+
...renderRunSteps(test, opts.config, opts.vars, fill)
|
|
9236
9267
|
};
|
|
9237
9268
|
});
|
|
9269
|
+
const build = renderBuild(opts.config, opts.vars, DRY_RUN_ID);
|
|
9238
9270
|
if (opts.json) {
|
|
9239
|
-
console.log(JSON.stringify({ dryRun: true, build
|
|
9271
|
+
console.log(JSON.stringify({ dryRun: true, build, runs }, null, 2));
|
|
9240
9272
|
return;
|
|
9241
9273
|
}
|
|
9242
9274
|
const count = (n, what) => `${n} ${what}${n === 1 ? "" : "s"}`;
|
|
@@ -9286,11 +9318,11 @@ async function runOne(opts) {
|
|
|
9286
9318
|
FIREGOAT_WORKDIR: workdir,
|
|
9287
9319
|
FIREGOAT_HOME: home
|
|
9288
9320
|
};
|
|
9289
|
-
const
|
|
9290
|
-
const
|
|
9291
|
-
if (cleanup.length > 0)
|
|
9321
|
+
const steps = renderRunSteps(test, opts, opts.vars, fill);
|
|
9322
|
+
const [status, reason] = await attempt(opts, r, steps, env, stepEnv, envFile);
|
|
9323
|
+
if (steps.cleanup.length > 0)
|
|
9292
9324
|
r.cleanup = await runAllSteps({
|
|
9293
|
-
commands: cleanup,
|
|
9325
|
+
commands: steps.cleanup,
|
|
9294
9326
|
cwd: workdir,
|
|
9295
9327
|
env: stepEnv,
|
|
9296
9328
|
logPath: r.cleanupLog
|
|
@@ -9301,14 +9333,13 @@ async function runOne(opts) {
|
|
|
9301
9333
|
writeRunJson(r);
|
|
9302
9334
|
return r;
|
|
9303
9335
|
}
|
|
9304
|
-
async function attempt(opts, r, env, stepEnv, homeEnvFile) {
|
|
9336
|
+
async function attempt(opts, r, steps, env, stepEnv, homeEnvFile) {
|
|
9305
9337
|
const { test, n } = opts;
|
|
9306
9338
|
const { dir, workdir } = r;
|
|
9307
|
-
|
|
9308
|
-
if (install.length > 0) {
|
|
9339
|
+
if (steps.install.length > 0) {
|
|
9309
9340
|
const error = await runSteps({
|
|
9310
9341
|
what: "install",
|
|
9311
|
-
commands: install,
|
|
9342
|
+
commands: steps.install,
|
|
9312
9343
|
cwd: workdir,
|
|
9313
9344
|
env: stepEnv,
|
|
9314
9345
|
logPath: join7(dir, "install.txt")
|
|
@@ -9362,9 +9393,9 @@ async function attempt(opts, r, env, stepEnv, homeEnvFile) {
|
|
|
9362
9393
|
];
|
|
9363
9394
|
const log = createWriteStream2(r.checksLog);
|
|
9364
9395
|
try {
|
|
9365
|
-
for (const
|
|
9396
|
+
for (const run of steps.checks)
|
|
9366
9397
|
r.checks.push(await runShell({
|
|
9367
|
-
run
|
|
9398
|
+
run,
|
|
9368
9399
|
cwd: workdir,
|
|
9369
9400
|
env: {
|
|
9370
9401
|
...stepEnv,
|
|
@@ -9462,8 +9493,10 @@ async function evalCommand(args, root) {
|
|
|
9462
9493
|
t.prompt,
|
|
9463
9494
|
...t.checks.map((c) => c.run),
|
|
9464
9495
|
...Object.values(t.env),
|
|
9465
|
-
...t.claudeArgs
|
|
9466
|
-
|
|
9496
|
+
...t.claudeArgs,
|
|
9497
|
+
...t.install,
|
|
9498
|
+
...t.cleanup
|
|
9499
|
+
]).concat(config.build, config.install, config.cleanup).flatMap(varRefs).filter((k) => !(k in vars)))
|
|
9467
9500
|
];
|
|
9468
9501
|
if (missing.length > 0)
|
|
9469
9502
|
return fatal(EXIT.usage, `No value for ${missing.map((k) => `{{vars.${k}}}`).join(", ")}: pass --var ${missing[0]}=... or set it under vars: in firegoat.yaml`, "config");
|
|
@@ -9546,7 +9579,7 @@ async function evalCommand(args, root) {
|
|
|
9546
9579
|
ui.build("running");
|
|
9547
9580
|
const error = await runSteps({
|
|
9548
9581
|
what: "build",
|
|
9549
|
-
commands: config
|
|
9582
|
+
commands: renderBuild(config, vars, runId),
|
|
9550
9583
|
cwd: root,
|
|
9551
9584
|
env: {
|
|
9552
9585
|
...stepEnv,
|
|
@@ -9624,6 +9657,12 @@ async function evalCommand(args, root) {
|
|
|
9624
9657
|
uploaded,
|
|
9625
9658
|
uploadFailed: uploadsFailed
|
|
9626
9659
|
});
|
|
9660
|
+
const status = totals.error > 0 ? "error" : totals.fail > 0 ? "fail" : "pass";
|
|
9661
|
+
const exitCode = {
|
|
9662
|
+
error: EXIT.notGraded,
|
|
9663
|
+
fail: EXIT.fail,
|
|
9664
|
+
pass: EXIT.pass
|
|
9665
|
+
}[status];
|
|
9627
9666
|
const doc = {
|
|
9628
9667
|
firegoatVersion: package_default.version,
|
|
9629
9668
|
runId,
|
|
@@ -9631,8 +9670,10 @@ async function evalCommand(args, root) {
|
|
|
9631
9670
|
dir: root,
|
|
9632
9671
|
...repo && { git: repo.info },
|
|
9633
9672
|
upload: upload && { origin: upload.origin, email: upload.email },
|
|
9673
|
+
runs: results,
|
|
9634
9674
|
totals,
|
|
9635
|
-
|
|
9675
|
+
status,
|
|
9676
|
+
exitCode
|
|
9636
9677
|
};
|
|
9637
9678
|
writeFileSync4(join7(runDir, "summary.json"), `${JSON.stringify(doc, null, 2)}
|
|
9638
9679
|
`);
|
|
@@ -9666,11 +9707,11 @@ async function evalCommand(args, root) {
|
|
|
9666
9707
|
console.error(`firegoat eval: ${uploadsFailed} upload${uploadsFailed === 1 ? "" : "s"} failed (each run's upload.error in summary.json)`);
|
|
9667
9708
|
if (opts.json)
|
|
9668
9709
|
console.log(JSON.stringify(doc, null, 2));
|
|
9669
|
-
if (
|
|
9710
|
+
if (status === "error")
|
|
9670
9711
|
failWith("not_graded");
|
|
9671
|
-
else if (
|
|
9712
|
+
else if (status === "fail")
|
|
9672
9713
|
failWith("checks_failed");
|
|
9673
|
-
return
|
|
9714
|
+
return exitCode;
|
|
9674
9715
|
}
|
|
9675
9716
|
|
|
9676
9717
|
// src/feedback.ts
|
|
@@ -9946,10 +9987,9 @@ firegoat ${package_default.version}` : `firegoat ${package_default.version}`,
|
|
|
9946
9987
|
}
|
|
9947
9988
|
|
|
9948
9989
|
// src/init.ts
|
|
9949
|
-
import { spawnSync as spawnSync3 } from "node:child_process";
|
|
9990
|
+
import { spawn as spawn3, spawnSync as spawnSync3 } from "node:child_process";
|
|
9950
9991
|
import { existsSync as existsSync4, writeFileSync as writeFileSync5 } from "node:fs";
|
|
9951
9992
|
import { basename as basename4, join as join8 } from "node:path";
|
|
9952
|
-
import { createInterface } from "node:readline/promises";
|
|
9953
9993
|
|
|
9954
9994
|
// src/login.ts
|
|
9955
9995
|
import { spawn as spawn2 } from "node:child_process";
|
|
@@ -9979,7 +10019,7 @@ function openBrowser(url) {
|
|
|
9979
10019
|
} catch {}
|
|
9980
10020
|
}
|
|
9981
10021
|
var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
9982
|
-
async function loginFlow(
|
|
10022
|
+
async function loginFlow(show, interactive) {
|
|
9983
10023
|
const base = baseUrl();
|
|
9984
10024
|
const origin = new URL(base).origin;
|
|
9985
10025
|
const cfgStep = "GET /api/traces/oauth/config";
|
|
@@ -10024,8 +10064,11 @@ async function loginFlow(say, interactive) {
|
|
|
10024
10064
|
let interval = Math.min(Math.max(rawInterval ?? 5, 5), MAX_INTERVAL_S);
|
|
10025
10065
|
const expiresS = Math.min(expiresIn, MAX_EXPIRES_S);
|
|
10026
10066
|
const deadline = Date.now() + expiresS * 1000;
|
|
10027
|
-
|
|
10028
|
-
|
|
10067
|
+
show({
|
|
10068
|
+
page: printable(page.href, 500),
|
|
10069
|
+
code: userCode,
|
|
10070
|
+
minutes: Math.max(1, Math.round(expiresS / 60))
|
|
10071
|
+
});
|
|
10029
10072
|
if (interactive)
|
|
10030
10073
|
openBrowser(page.href);
|
|
10031
10074
|
const tokStep = "Token request";
|
|
@@ -10093,7 +10136,10 @@ ${LOGIN_HELP}`);
|
|
|
10093
10136
|
console.error("Note: FIREGOAT_API_KEY is set, so eval uses it instead of this login.");
|
|
10094
10137
|
try {
|
|
10095
10138
|
console.log(UPLOAD_NOTICE);
|
|
10096
|
-
const email = await loginFlow((
|
|
10139
|
+
const email = await loginFlow(({ page, code, minutes }) => {
|
|
10140
|
+
console.log(TERMS);
|
|
10141
|
+
console.log(`Open ${page} and enter code ${code} (expires in ${minutes}m).`);
|
|
10142
|
+
}, Boolean(process.stdout.isTTY));
|
|
10097
10143
|
console.log(`Logged in as ${email}. Key saved to ${credentialsPath()}.`);
|
|
10098
10144
|
return EXIT.pass;
|
|
10099
10145
|
} catch (e) {
|
|
@@ -10130,6 +10176,102 @@ ${LOGOUT_HELP}`);
|
|
|
10130
10176
|
return EXIT.pass;
|
|
10131
10177
|
}
|
|
10132
10178
|
|
|
10179
|
+
// src/tty.ts
|
|
10180
|
+
import { emitKeypressEvents } from "node:readline";
|
|
10181
|
+
var ANSI2 = new RegExp(`${String.fromCharCode(27)}\\[[0-9;]*m`, "g");
|
|
10182
|
+
var visible = (s) => s.replace(ANSI2, "");
|
|
10183
|
+
function liveBlock(out = process.stdout) {
|
|
10184
|
+
let drawn = 0;
|
|
10185
|
+
const rowsOf = (line) => Math.max(1, Math.ceil(visible(line).length / (out.columns || 80)));
|
|
10186
|
+
return {
|
|
10187
|
+
draw(lines) {
|
|
10188
|
+
out.write(`${drawn > 0 ? `\x1B[${drawn}A\x1B[J` : ""}${lines.join(`
|
|
10189
|
+
`)}
|
|
10190
|
+
`);
|
|
10191
|
+
drawn = lines.reduce((n, l) => n + rowsOf(l), 0);
|
|
10192
|
+
}
|
|
10193
|
+
};
|
|
10194
|
+
}
|
|
10195
|
+
function animate(block, render) {
|
|
10196
|
+
let frame = 0;
|
|
10197
|
+
const draw = () => block.draw(render(SPIN[frame++ % SPIN.length]));
|
|
10198
|
+
draw();
|
|
10199
|
+
const timer = setInterval(draw, 120);
|
|
10200
|
+
return () => clearInterval(timer);
|
|
10201
|
+
}
|
|
10202
|
+
function wrap(text, indent, width) {
|
|
10203
|
+
const room = Math.max(10, width - indent.length);
|
|
10204
|
+
const lines = [];
|
|
10205
|
+
let line = "";
|
|
10206
|
+
for (const word of text.split(" ")) {
|
|
10207
|
+
for (let w = word;w.length > 0; ) {
|
|
10208
|
+
const next = line ? `${line} ${w}` : w;
|
|
10209
|
+
if (next.length <= room) {
|
|
10210
|
+
line = next;
|
|
10211
|
+
w = "";
|
|
10212
|
+
} else if (line) {
|
|
10213
|
+
lines.push(line);
|
|
10214
|
+
line = "";
|
|
10215
|
+
} else {
|
|
10216
|
+
lines.push(w.slice(0, room));
|
|
10217
|
+
w = w.slice(room);
|
|
10218
|
+
}
|
|
10219
|
+
}
|
|
10220
|
+
}
|
|
10221
|
+
if (line)
|
|
10222
|
+
lines.push(line);
|
|
10223
|
+
return lines.map((l) => indent + l);
|
|
10224
|
+
}
|
|
10225
|
+
function abortError() {
|
|
10226
|
+
return Object.assign(new Error("Aborted"), { code: "ABORT_ERR" });
|
|
10227
|
+
}
|
|
10228
|
+
function select(block, count, render, initial = 0) {
|
|
10229
|
+
const input = process.stdin;
|
|
10230
|
+
let current = initial;
|
|
10231
|
+
block.draw(render(current));
|
|
10232
|
+
return new Promise((resolve, reject) => {
|
|
10233
|
+
const restore = () => {
|
|
10234
|
+
input.off("keypress", onKey);
|
|
10235
|
+
input.off("end", onEnd);
|
|
10236
|
+
process.off("exit", showCursor);
|
|
10237
|
+
input.setRawMode(false);
|
|
10238
|
+
input.pause();
|
|
10239
|
+
showCursor();
|
|
10240
|
+
};
|
|
10241
|
+
const onKey = (_, key) => {
|
|
10242
|
+
if (!key)
|
|
10243
|
+
return;
|
|
10244
|
+
if (key.ctrl && (key.name === "c" || key.name === "d")) {
|
|
10245
|
+
restore();
|
|
10246
|
+
reject(abortError());
|
|
10247
|
+
} else if (key.name === "return" || key.name === "enter") {
|
|
10248
|
+
restore();
|
|
10249
|
+
resolve(current);
|
|
10250
|
+
} else if (key.name === "up" || key.name === "k") {
|
|
10251
|
+
current = (current + count - 1) % count;
|
|
10252
|
+
block.draw(render(current));
|
|
10253
|
+
} else if (key.name === "down" || key.name === "j") {
|
|
10254
|
+
current = (current + 1) % count;
|
|
10255
|
+
block.draw(render(current));
|
|
10256
|
+
}
|
|
10257
|
+
};
|
|
10258
|
+
const onEnd = () => {
|
|
10259
|
+
restore();
|
|
10260
|
+
reject(abortError());
|
|
10261
|
+
};
|
|
10262
|
+
emitKeypressEvents(input);
|
|
10263
|
+
input.setRawMode(true);
|
|
10264
|
+
input.resume();
|
|
10265
|
+
process.stdout.write("\x1B[?25l");
|
|
10266
|
+
process.on("exit", showCursor);
|
|
10267
|
+
input.on("keypress", onKey);
|
|
10268
|
+
input.on("end", onEnd);
|
|
10269
|
+
});
|
|
10270
|
+
}
|
|
10271
|
+
function showCursor() {
|
|
10272
|
+
process.stdout.write("\x1B[?25h");
|
|
10273
|
+
}
|
|
10274
|
+
|
|
10133
10275
|
// src/init.ts
|
|
10134
10276
|
var SKILLS_ARGV = [
|
|
10135
10277
|
"skills",
|
|
@@ -10204,12 +10346,18 @@ runs: 1
|
|
|
10204
10346
|
# MYTOOL_HOME: "$FIREGOAT_HOME/.mytool"
|
|
10205
10347
|
# UV_FIND_LINKS: "$FIREGOAT_OUT"
|
|
10206
10348
|
|
|
10349
|
+
# Optional: values for {{vars.KEY}} in prompts, checks and build/install/cleanup
|
|
10350
|
+
# steps (pasted in as-is, unquoted); eval --var KEY=value overrides them. Steps
|
|
10351
|
+
# and checks also get each one as an env var ("$KEY": safe for any value).
|
|
10352
|
+
# vars:
|
|
10353
|
+
# ACCOUNT: acme-test
|
|
10354
|
+
|
|
10207
10355
|
tests:
|
|
10208
10356
|
# A smoke test that firegoat and claude work. Replace it with tests for your
|
|
10209
10357
|
# product: the firegoat-onboarding skill writes them.
|
|
10210
10358
|
- name: goat
|
|
10211
10359
|
# What a user would ask their agent.
|
|
10212
|
-
prompt: Who's the GOAT? Answer in one short sentence.
|
|
10360
|
+
prompt: "Who's the GOAT? Hint: it's me, the person typing this. Answer in one short sentence with the word GOAT in it."
|
|
10213
10361
|
# True when the user's problem is solved. Not graded by firegoat: it goes to
|
|
10214
10362
|
# your agent (eval --json, run.json) to judge.
|
|
10215
10363
|
end_goal: Claude answers that you (the user) are the goat, e.g. YOU'RE THE GOAT.
|
|
@@ -10218,7 +10366,7 @@ tests:
|
|
|
10218
10366
|
checks:
|
|
10219
10367
|
- run: >-
|
|
10220
10368
|
test -s "$FIREGOAT_TRANSCRIPT" &&
|
|
10221
|
-
grep -Eiq '^agent: .*goat' "$FIREGOAT_TRANSCRIPT"
|
|
10369
|
+
grep -Eiq '^agent: .*(you.*goat|goat.*you)' "$FIREGOAT_TRANSCRIPT"
|
|
10222
10370
|
# Seconds before the agent run is stopped.
|
|
10223
10371
|
timeout: 600
|
|
10224
10372
|
`;
|
|
@@ -10236,61 +10384,115 @@ function copyToClipboard(text) {
|
|
|
10236
10384
|
return !r.error && r.status === 0;
|
|
10237
10385
|
});
|
|
10238
10386
|
}
|
|
10239
|
-
|
|
10240
|
-
|
|
10241
|
-
|
|
10242
|
-
|
|
10243
|
-
|
|
10244
|
-
|
|
10387
|
+
var LOGIN_HINT = "Runs stay local: npx firegoat login uploads them to firegoat.ai";
|
|
10388
|
+
function cut(s, n) {
|
|
10389
|
+
return s.length <= n ? s : `${s.slice(0, Math.max(0, n - 1))}~`;
|
|
10390
|
+
}
|
|
10391
|
+
|
|
10392
|
+
class Timeline {
|
|
10393
|
+
paint;
|
|
10394
|
+
width;
|
|
10395
|
+
static INDENT = " ".repeat(9);
|
|
10396
|
+
constructor(paint, width) {
|
|
10397
|
+
this.paint = paint;
|
|
10398
|
+
this.width = width;
|
|
10399
|
+
}
|
|
10400
|
+
line(mark, n, label, status = "") {
|
|
10401
|
+
const head = ` ${mark} ${this.paint("90", String(n).padStart(2, "0"))} `;
|
|
10402
|
+
if (!status)
|
|
10403
|
+
return head + cut(label, this.width - 9);
|
|
10404
|
+
return `${head}${label.padEnd(28)}${this.paint("90", cut(status, this.width - 37))}`;
|
|
10405
|
+
}
|
|
10406
|
+
done(n, label, status) {
|
|
10407
|
+
return this.line(this.paint("32", "●"), n, label, status);
|
|
10408
|
+
}
|
|
10409
|
+
failed(n, label) {
|
|
10410
|
+
return this.line(this.paint("31", "●"), n, label, "Failed");
|
|
10411
|
+
}
|
|
10412
|
+
busy(spin, n, label, status) {
|
|
10413
|
+
return this.line(this.paint("38;5;208", spin), n, label, status);
|
|
10414
|
+
}
|
|
10415
|
+
detail(text) {
|
|
10416
|
+
return wrap(text, Timeline.INDENT, this.width).map((l) => this.paint("90", l));
|
|
10417
|
+
}
|
|
10418
|
+
gap() {
|
|
10419
|
+
console.log(this.paint("90", " │"));
|
|
10420
|
+
}
|
|
10421
|
+
ask(block, n, question, options) {
|
|
10422
|
+
const cols = Math.max(...options.map(([l]) => l.length)) + 3;
|
|
10423
|
+
const room = this.width - Timeline.INDENT.length - 2;
|
|
10424
|
+
return select(block, options.length, (current) => [
|
|
10425
|
+
this.line(this.paint("38;5;208", "◉"), n, question),
|
|
10426
|
+
...options.map(([label, note], i) => {
|
|
10427
|
+
const text = cut(`${label.padEnd(cols)}${note}`, room);
|
|
10428
|
+
return i === current ? `${Timeline.INDENT}${this.paint("38;5;208", `● ${text}`)}` : this.paint("90", `${Timeline.INDENT}○ ${text}`);
|
|
10429
|
+
}),
|
|
10430
|
+
this.paint("90", `${Timeline.INDENT}Up/down to move, enter to pick`)
|
|
10431
|
+
]);
|
|
10245
10432
|
}
|
|
10246
10433
|
}
|
|
10247
|
-
|
|
10248
|
-
|
|
10249
|
-
|
|
10250
|
-
|
|
10251
|
-
|
|
10252
|
-
|
|
10253
|
-
|
|
10254
|
-
|
|
10255
|
-
|
|
10256
|
-
|
|
10257
|
-
|
|
10258
|
-
|
|
10259
|
-
|
|
10260
|
-
|
|
10261
|
-
}
|
|
10434
|
+
function runQuiet(cmd, args, opts) {
|
|
10435
|
+
return new Promise((resolve) => {
|
|
10436
|
+
const child = spawn3(cmd, args, {
|
|
10437
|
+
...opts,
|
|
10438
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
10439
|
+
});
|
|
10440
|
+
let output = "";
|
|
10441
|
+
const collect = (d) => {
|
|
10442
|
+
output += d;
|
|
10443
|
+
};
|
|
10444
|
+
child.stdout.on("data", collect);
|
|
10445
|
+
child.stderr.on("data", collect);
|
|
10446
|
+
child.on("error", (error) => resolve({ status: null, output, error }));
|
|
10447
|
+
child.on("close", (status) => resolve({ status, output }));
|
|
10448
|
+
});
|
|
10262
10449
|
}
|
|
10263
|
-
|
|
10264
|
-
async function loginStep(interactive, done, paint) {
|
|
10450
|
+
async function loginStep(ui) {
|
|
10265
10451
|
const label = "firegoat.ai login";
|
|
10266
|
-
const uploadsOn = () => console.log(` ${paint("90", UPLOADS_ON)}`);
|
|
10267
10452
|
const key = resolveKey(baseUrl());
|
|
10268
|
-
|
|
10269
|
-
|
|
10270
|
-
|
|
10271
|
-
|
|
10272
|
-
uploadsOn();
|
|
10273
|
-
} else
|
|
10274
|
-
console.log(`${who}
|
|
10275
|
-
${UPLOADS_ON}`);
|
|
10453
|
+
const who = key ? key.source === "env" ? "FIREGOAT_API_KEY is set" : `Logged in as ${key.email}` : undefined;
|
|
10454
|
+
if (!ui) {
|
|
10455
|
+
console.log(who ? `${who}
|
|
10456
|
+
${UPLOADS_ON}` : LOGIN_HINT);
|
|
10276
10457
|
return EXIT.pass;
|
|
10277
10458
|
}
|
|
10278
|
-
|
|
10279
|
-
|
|
10459
|
+
const block = liveBlock();
|
|
10460
|
+
if (who) {
|
|
10461
|
+
block.draw([ui.done(4, label, who), ...ui.detail(UPLOADS_ON)]);
|
|
10280
10462
|
return EXIT.pass;
|
|
10281
10463
|
}
|
|
10282
|
-
|
|
10283
|
-
|
|
10284
|
-
|
|
10285
|
-
|
|
10464
|
+
const picked = await ui.ask(block, 4, "Login to firegoat.ai to upload runs?", [
|
|
10465
|
+
["Yes", "Opens firegoat.ai in your browser"],
|
|
10466
|
+
["Not now", "Runs stay local"]
|
|
10467
|
+
]);
|
|
10468
|
+
if (picked !== 0) {
|
|
10469
|
+
block.draw([ui.done(4, label, "Skipped (npx firegoat login later)")]);
|
|
10286
10470
|
return EXIT.pass;
|
|
10287
10471
|
}
|
|
10472
|
+
let status = "Connecting";
|
|
10473
|
+
let details = [];
|
|
10474
|
+
const stop = animate(block, (spin) => [
|
|
10475
|
+
ui.busy(spin, 4, label, status),
|
|
10476
|
+
...details
|
|
10477
|
+
]);
|
|
10288
10478
|
try {
|
|
10289
|
-
const email = await loginFlow((
|
|
10290
|
-
|
|
10291
|
-
|
|
10479
|
+
const email = await loginFlow(({ page, code, minutes }) => {
|
|
10480
|
+
status = "Waiting for the browser";
|
|
10481
|
+
details = [
|
|
10482
|
+
...ui.detail(TERMS),
|
|
10483
|
+
...ui.detail(`Open ${page}`),
|
|
10484
|
+
...ui.detail(`Code: ${code} (expires in ${minutes}m)`)
|
|
10485
|
+
];
|
|
10486
|
+
}, true);
|
|
10487
|
+
stop();
|
|
10488
|
+
block.draw([
|
|
10489
|
+
ui.done(4, label, `Logged in as ${email}`),
|
|
10490
|
+
...ui.detail(UPLOADS_ON)
|
|
10491
|
+
]);
|
|
10292
10492
|
return EXIT.pass;
|
|
10293
10493
|
} catch (e) {
|
|
10494
|
+
stop();
|
|
10495
|
+
block.draw([ui.failed(4, label), ...details]);
|
|
10294
10496
|
if (!(e instanceof ApiError))
|
|
10295
10497
|
throw e;
|
|
10296
10498
|
failWith(e instanceof NetworkError ? "network" : "login_failed");
|
|
@@ -10336,8 +10538,8 @@ ${INIT_HELP}`);
|
|
|
10336
10538
|
const paint = (code, text) => color ? `\x1B[${code}m${text}\x1B[0m` : text;
|
|
10337
10539
|
const width = Math.min(process.stdout.columns || 80, 80);
|
|
10338
10540
|
const rule = paint("90", ` ${"─".repeat(width - 2)}`);
|
|
10339
|
-
const
|
|
10340
|
-
if (
|
|
10541
|
+
const ui = interactive ? new Timeline(paint, width) : undefined;
|
|
10542
|
+
if (ui) {
|
|
10341
10543
|
for (const line of logoHeader(paint, "firegoat init", paint("90", "Get agent-ready."), width))
|
|
10342
10544
|
console.log(line);
|
|
10343
10545
|
console.log(rule);
|
|
@@ -10347,39 +10549,45 @@ ${INIT_HELP}`);
|
|
|
10347
10549
|
const keep = existsSync4(configPath) && !force;
|
|
10348
10550
|
if (!keep)
|
|
10349
10551
|
writeFileSync5(configPath, template(basename4(cwd)));
|
|
10350
|
-
if (
|
|
10351
|
-
|
|
10352
|
-
|
|
10353
|
-
|
|
10552
|
+
if (ui) {
|
|
10553
|
+
console.log(ui.done(1, CONFIG_FILE, keep ? "Kept (--force replaces it)" : "Created"));
|
|
10554
|
+
ui.gap();
|
|
10555
|
+
console.log(ui.done(2, ".firegoat/", "Ready"));
|
|
10354
10556
|
} else
|
|
10355
10557
|
console.log(keep ? `Kept ${CONFIG_FILE} (--force replaces it)` : `Wrote ${CONFIG_FILE}: replace the goat smoke test with tests for your product`);
|
|
10356
|
-
|
|
10357
|
-
|
|
10358
|
-
|
|
10359
|
-
|
|
10360
|
-
|
|
10558
|
+
const skill = "firegoat-onboarding skill";
|
|
10559
|
+
const block = liveBlock();
|
|
10560
|
+
ui?.gap();
|
|
10561
|
+
if (ui && !noSkills && !localSkills) {
|
|
10562
|
+
const picked = await ui.ask(block, 3, "Install the firegoat-onboarding skill", [
|
|
10563
|
+
["Global", skillsDir("global")],
|
|
10564
|
+
["This repo", `${skillsDir("local")} (commit it for your team)`],
|
|
10565
|
+
["Skip", "Don't install it"]
|
|
10566
|
+
]);
|
|
10567
|
+
scope = ["global", "local", "none"][picked] ?? "none";
|
|
10361
10568
|
initSkills(scope);
|
|
10362
10569
|
}
|
|
10363
10570
|
if (scope !== "none") {
|
|
10364
10571
|
const argv = [...SKILLS_ARGV, ...scope === "global" ? ["-g"] : []];
|
|
10365
10572
|
const where = skillsDir(scope);
|
|
10366
|
-
if (!
|
|
10573
|
+
if (!ui)
|
|
10367
10574
|
console.log(`Installing the skill: npx ${argv.join(" ")}`);
|
|
10368
10575
|
const env = Object.fromEntries(Object.entries(process.env).filter(([k]) => k !== "npm_config_package" && !k.startsWith("FIREGOAT_")));
|
|
10369
|
-
const
|
|
10370
|
-
|
|
10371
|
-
|
|
10372
|
-
|
|
10373
|
-
|
|
10374
|
-
});
|
|
10576
|
+
const stop = ui ? animate(block, (spin) => [
|
|
10577
|
+
ui.busy(spin, 3, skill, `Installing → ${where}`)
|
|
10578
|
+
]) : undefined;
|
|
10579
|
+
const r = await runQuiet("npx", ["-y", ...argv], { cwd, env });
|
|
10580
|
+
stop?.();
|
|
10375
10581
|
if (r.error || r.status !== 0) {
|
|
10376
10582
|
failWith("skills_failed");
|
|
10377
|
-
|
|
10583
|
+
if (ui)
|
|
10584
|
+
block.draw([ui.failed(3, skill)]);
|
|
10585
|
+
process.stderr.write(r.output);
|
|
10378
10586
|
console.error(`firegoat init: Installing the skill failed (${r.error?.message ?? `exit ${r.status}`}). Run it yourself: npx ${argv.join(" ")}`);
|
|
10379
10587
|
return EXIT.notGraded;
|
|
10380
10588
|
}
|
|
10381
|
-
if (
|
|
10382
|
-
|
|
10589
|
+
if (ui)
|
|
10590
|
+
block.draw([ui.done(3, skill, `Installed → ${where}`)]);
|
|
10383
10591
|
else
|
|
10384
10592
|
console.log(`Installed skill firegoat-onboarding in ${where}`);
|
|
10385
10593
|
const ignored = scope === "local" && spawnSync3("git", ["check-ignore", "-q", ".claude/skills"], {
|
|
@@ -10388,32 +10596,35 @@ ${INIT_HELP}`);
|
|
|
10388
10596
|
}).status === 0;
|
|
10389
10597
|
if (ignored)
|
|
10390
10598
|
console.error("Warning: git ignores .claude/skills, so teammates and CI won't get the skill. Commit them with: git add -f .claude/skills");
|
|
10391
|
-
} else if (
|
|
10392
|
-
|
|
10599
|
+
} else if (ui)
|
|
10600
|
+
block.draw([ui.done(3, skill, "Skipped")]);
|
|
10393
10601
|
if (login) {
|
|
10394
|
-
|
|
10395
|
-
|
|
10396
|
-
const loginExit = await loginStep(interactive, (label, status) => step(4, label, status), paint);
|
|
10602
|
+
ui?.gap();
|
|
10603
|
+
const loginExit = await loginStep(ui);
|
|
10397
10604
|
if (loginExit !== EXIT.pass)
|
|
10398
10605
|
return loginExit;
|
|
10399
10606
|
}
|
|
10400
10607
|
const telemetryOn = telemetryEnabled();
|
|
10401
|
-
if (!
|
|
10608
|
+
if (!ui) {
|
|
10402
10609
|
if (telemetryOn)
|
|
10403
10610
|
console.log(TELEMETRY_NOTICE);
|
|
10404
10611
|
console.log(`
|
|
10405
10612
|
Prompt for your coding agent: ${AGENT_PROMPT}`);
|
|
10406
10613
|
return EXIT.pass;
|
|
10407
10614
|
}
|
|
10408
|
-
if (telemetryOn)
|
|
10409
|
-
console.log(` ${paint("90", TELEMETRY_NOTICE)}`);
|
|
10410
10615
|
console.log(rule);
|
|
10616
|
+
console.log("");
|
|
10411
10617
|
console.log(` Next: hand this to your coding agent
|
|
10412
10618
|
`);
|
|
10413
10619
|
console.log(AGENT_PROMPT);
|
|
10414
10620
|
console.log("");
|
|
10415
10621
|
const copied = copyToClipboard(AGENT_PROMPT);
|
|
10416
10622
|
console.log(` ${copied ? `${paint("32", "✓ Prompt copied!")} ` : ""}Then: ${paint("36", "npx firegoat@latest eval")}`);
|
|
10623
|
+
if (telemetryOn) {
|
|
10624
|
+
console.log("");
|
|
10625
|
+
for (const l of wrap(TELEMETRY_NOTICE, " ", width))
|
|
10626
|
+
console.log(paint("90", l));
|
|
10627
|
+
}
|
|
10417
10628
|
return EXIT.pass;
|
|
10418
10629
|
}
|
|
10419
10630
|
|