@brainbase-labs/cli 0.25.0-eng1209.1 → 0.25.0-eng1209.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +14 -7
  2. package/dist/index.js +541 -112
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -72,21 +72,28 @@ synchronously. Schema v1 supports:
72
72
  - `workspace_assertion`: file existence, absence, SHA-256, or content checks.
73
73
  - `sandbox_command`: a bounded argv command rooted in the workspace or hidden
74
74
  tests directory. Exit zero passes; any other exit code is a valid failed
75
- verdict. Schema v1 allows one command evaluator and runs it after all
76
- read-only assertions so evaluator mutations cannot change candidate verdicts.
75
+ verdict. Schema v1 allows up to 20 command evaluators and runs them after all
76
+ read-only assertions. Each command receives either the verified live workspace
77
+ for legacy read-only evaluation or its own isolated frozen workspace copy.
77
78
 
78
79
  Valid failed verdicts still produce a successful evaluation phase. Invalid
79
- specs, digest/path violations, missing environment, launch failures, timeouts,
80
- and output-budget violations fail the phase. Results are written atomically
80
+ specs, digest/path violations, missing environment, phase-budget exhaustion,
81
+ and output-budget violations fail the phase. Individual command launch failures,
82
+ timeouts, and forced terminations are recorded as errored evaluator results so
83
+ the remaining evaluators can still run. Results are written atomically
81
84
  with mode `0600`, include the raw spec digest and checksummed evidence/log
82
85
  input/output references with explicit `staging`, `workspace`, `tests`, or
83
86
  `logs` roots, redact declared secret values from command logs, require the
84
87
  result path to be `<logs_root>/result.json`, and treat a matching successful
85
88
  phase result as authoritative on replay.
86
89
 
87
- On any phase-level execution failure, MAS must tear down the task sandbox. The
88
- CLI kills the command process group and descendants it can observe, but the
89
- runtime lifecycle remains the final cleanup boundary for daemonized processes.
90
+ On any phase-level execution failure, MAS must tear down the task sandbox.
91
+ Failed and timed-out commands clean up the process group and descendants the
92
+ CLI can observe. Successful evaluator commands receive the same cleanup so one
93
+ check cannot contaminate the next. Successful hydration setup commands may
94
+ intentionally leave services running for the agent turn; the runtime lifecycle
95
+ remains their final cleanup boundary. Isolated evaluator workspace copies omit
96
+ `.git` and unsafe absolute or escaping symlinks.
90
97
 
91
98
  ## Development
92
99
 
package/dist/index.js CHANGED
@@ -36008,7 +36008,7 @@ function padStart(s, n) {
36008
36008
  // package.json
36009
36009
  var package_default = {
36010
36010
  name: "@brainbase-labs/cli",
36011
- version: "0.25.0-eng1209.1",
36011
+ version: "0.25.0-eng1209.10",
36012
36012
  description: "Pack, share, and install agent templates across harnesses (Claude Code, Codex, ...).",
36013
36013
  type: "module",
36014
36014
  bin: {
@@ -78485,13 +78485,19 @@ var MAX_TRAJECTORY_BYTES = 100 * 1024 * 1024;
78485
78485
  var MAX_REMOTE_INPUT_BYTES = 2 * 1024 * 1024 * 1024;
78486
78486
  var MAX_ARCHIVE_EXTRACTED_BYTES = 2 * 1024 * 1024 * 1024;
78487
78487
  var MAX_ARCHIVE_SCAN_BYTES = 2 * 1024 * 1024 * 1024;
78488
+ var MAX_HYDRATE_COMMANDS = 150;
78489
+ var MAX_PHASE_RESULT_BYTES = 32 * 1024 * 1024;
78488
78490
  var MAX_SANDBOX_COMMANDS = 20;
78489
- var MAX_CRITERIA_PER_EVALUATOR = 100;
78490
- var MAX_CRITERIA_RESULT_BYTES = 1024 * 1024;
78491
+ var MAX_CRITERIA_PER_EVALUATOR = 200;
78492
+ var MAX_CRITERIA_RESULT_BYTES = 24 * 1024 * 1024;
78493
+ var MAX_CRITERIA_RESULT_PAYLOAD_BYTES = 20 * 1024 * 1024;
78491
78494
  var MAX_CRITERION_EXPLANATION_LENGTH = 16384;
78492
78495
  var MAX_CRITERION_EVIDENCE_IDS = 100;
78493
78496
  var MAX_CRITERION_EVIDENCE_ID_LENGTH = 256;
78494
78497
  var MAX_ALLOWED_EVIDENCE_IDS = 1e4;
78498
+ var COMMAND_PROCESS_MARKER_ENV = "BRAINBASE_BENCHMARK_COMMAND_MARKER";
78499
+ var COMMAND_PROCESS_POLL_MS = 10;
78500
+ var COMMAND_PROCESS_CLEANUP_MS = 500;
78495
78501
  var RESERVED_WORKSPACE_PATHS = new Set([
78496
78502
  ".brainbase",
78497
78503
  ".git",
@@ -78658,7 +78664,7 @@ var TrajectoryEvidenceSchema = EvidenceFileSchema.extend({
78658
78664
  var HydrateSpecSchema = BaseSpecSchema.extend({
78659
78665
  phase: exports_external.literal("hydrate"),
78660
78666
  materials: exports_external.array(MaterialSchema).max(1e4).default([]),
78661
- setup_commands: exports_external.array(CommandSchema).max(128).default([])
78667
+ setup_commands: exports_external.array(CommandSchema).max(MAX_HYDRATE_COMMANDS).default([])
78662
78668
  }).strict();
78663
78669
  var CandidateOutputSchema = exports_external.object({
78664
78670
  id: IdSchema,
@@ -78712,6 +78718,20 @@ var BenchmarkSpecSchema = exports_external.discriminatedUnion("phase", [
78712
78718
  HydrateSpecSchema,
78713
78719
  EvaluateSpecSchema
78714
78720
  ]).superRefine((value, context) => {
78721
+ if (Object.hasOwn(value.environment, COMMAND_PROCESS_MARKER_ENV)) {
78722
+ context.addIssue({
78723
+ code: exports_external.ZodIssueCode.custom,
78724
+ path: ["environment", COMMAND_PROCESS_MARKER_ENV],
78725
+ message: `${COMMAND_PROCESS_MARKER_ENV} is reserved for command supervision`
78726
+ });
78727
+ }
78728
+ if (value.secret_env.includes(COMMAND_PROCESS_MARKER_ENV)) {
78729
+ context.addIssue({
78730
+ code: exports_external.ZodIssueCode.custom,
78731
+ path: ["secret_env"],
78732
+ message: `${COMMAND_PROCESS_MARKER_ENV} is reserved for command supervision`
78733
+ });
78734
+ }
78715
78735
  for (const name of Object.keys(value.environment)) {
78716
78736
  if (SENSITIVE_ENV_NAME_RE.test(name)) {
78717
78737
  context.addIssue({
@@ -78785,12 +78805,12 @@ var BenchmarkSpecSchema = exports_external.discriminatedUnion("phase", [
78785
78805
  { items: value.candidate_outputs, path: "candidate_outputs" }
78786
78806
  ];
78787
78807
  for (const { items, path: issuePath } of uniqueLists) {
78788
- const ids = items.map((item) => item.id);
78808
+ const ids = items.map((item) => issuePath === "candidate_outputs" ? item.id.toLowerCase() : item.id);
78789
78809
  if (new Set(ids).size !== ids.length) {
78790
78810
  context.addIssue({
78791
78811
  code: exports_external.ZodIssueCode.custom,
78792
78812
  path: [issuePath],
78793
- message: `${issuePath} ids must be unique`
78813
+ message: `${issuePath} ids must be unique${issuePath === "candidate_outputs" ? " ignoring case" : ""}`
78794
78814
  });
78795
78815
  }
78796
78816
  }
@@ -78866,6 +78886,8 @@ var BENCHMARK_CAPABILITIES = {
78866
78886
  ],
78867
78887
  limits: {
78868
78888
  max_secret_bindings: 100,
78889
+ max_hydrate_commands: MAX_HYDRATE_COMMANDS,
78890
+ max_phase_result_bytes: MAX_PHASE_RESULT_BYTES,
78869
78891
  max_evaluators: 1000,
78870
78892
  max_sandbox_commands: MAX_SANDBOX_COMMANDS,
78871
78893
  max_criteria_per_evaluator: MAX_CRITERIA_PER_EVALUATOR
@@ -78882,6 +78904,19 @@ class BenchmarkPhaseError extends Error {
78882
78904
  this.name = "BenchmarkPhaseError";
78883
78905
  }
78884
78906
  }
78907
+
78908
+ class BenchmarkCommandExecutionError extends BenchmarkPhaseError {
78909
+ stdout;
78910
+ stderr;
78911
+ durationMs;
78912
+ constructor(code, message, stdout, stderr, durationMs) {
78913
+ super(code, message);
78914
+ this.stdout = stdout;
78915
+ this.stderr = stderr;
78916
+ this.durationMs = durationMs;
78917
+ this.name = "BenchmarkCommandExecutionError";
78918
+ }
78919
+ }
78885
78920
  var ZIP_EOCD_SIGNATURE = 101010256;
78886
78921
  var ZIP_CENTRAL_SIGNATURE = 33639248;
78887
78922
  var ZIP_LOCAL_SIGNATURE = 67324752;
@@ -79114,10 +79149,15 @@ async function verifyRecordsUnchanged(records, spec) {
79114
79149
  const relative = safeRelPath(record3.path);
79115
79150
  assertNoSymlinkTraversal(root, relative);
79116
79151
  const candidate = path88.resolve(root, relative);
79117
- if (!isWithin(root, candidate) || !fs81.existsSync(candidate)) {
79152
+ if (!isWithin(root, candidate)) {
79153
+ throw new BenchmarkPhaseError("evidence_tampered", `evidence was removed during evaluation: ${record3.root}:${record3.path}`);
79154
+ }
79155
+ let stat;
79156
+ try {
79157
+ stat = fs81.lstatSync(candidate);
79158
+ } catch {
79118
79159
  throw new BenchmarkPhaseError("evidence_tampered", `evidence was removed during evaluation: ${record3.root}:${record3.path}`);
79119
79160
  }
79120
- const stat = fs81.lstatSync(candidate);
79121
79161
  if (record3.kind === "symlink") {
79122
79162
  const target = stat.isSymbolicLink() ? fs81.readlinkSync(candidate) : null;
79123
79163
  if (target === null || Buffer.byteLength(target) !== record3.size || sha256(target) !== record3.sha256) {
@@ -79212,6 +79252,15 @@ function removeRemoteHydrationInputs(spec) {
79212
79252
  removedSources.add(material.source);
79213
79253
  }
79214
79254
  }
79255
+ function removePrivateHydrationSourcesBeforeSetup(spec, context) {
79256
+ removeRemoteHydrationInputs(spec);
79257
+ for (const temporary of context.temporaryRoots) {
79258
+ fs81.rmSync(temporary, { recursive: true, force: true });
79259
+ }
79260
+ context.temporaryRoots.clear();
79261
+ context.verifiedInputs.clear();
79262
+ context.preparedArchiveFiles.clear();
79263
+ }
79215
79264
  async function downloadInputReference(stagingRoot, input, context) {
79216
79265
  const cacheKey = inputCacheKey(input);
79217
79266
  const cached2 = context.verifiedInputs.get(cacheKey);
@@ -79893,7 +79942,7 @@ function buildEnvironment(spec, secretNames, additions = {}) {
79893
79942
  Object.assign(env3, additions);
79894
79943
  return env3;
79895
79944
  }
79896
- function redactCommandOutput(data, spec) {
79945
+ function redactCommandOutput(data, spec, additionalSecrets = []) {
79897
79946
  let value = data.toString("utf8");
79898
79947
  const sensitiveNames = new Set([
79899
79948
  ...spec.secret_env,
@@ -79904,6 +79953,10 @@ function redactCommandOutput(data, spec) {
79904
79953
  if (secret)
79905
79954
  value = value.split(secret).join("[REDACTED]");
79906
79955
  }
79956
+ for (const secret of additionalSecrets) {
79957
+ if (secret)
79958
+ value = value.split(secret).join("[REDACTED]");
79959
+ }
79907
79960
  return Buffer.from(value);
79908
79961
  }
79909
79962
  function descendantPids(parentPid) {
@@ -79955,7 +80008,99 @@ function terminate(child) {
79955
80008
  child.kill("SIGKILL");
79956
80009
  }
79957
80010
  }
79958
- async function runCommand(command, root, spec, context, additions = {}) {
80011
+ function markedProcessPids(marker) {
80012
+ const assignment = `${COMMAND_PROCESS_MARKER_ENV}=${marker}`;
80013
+ if (process.platform === "linux") {
80014
+ const matches2 = [];
80015
+ let entries;
80016
+ try {
80017
+ entries = fs81.readdirSync("/proc");
80018
+ } catch {
80019
+ return matches2;
80020
+ }
80021
+ for (const entry of entries) {
80022
+ if (!/^\d+$/.test(entry))
80023
+ continue;
80024
+ const pid = Number(entry);
80025
+ if (pid === process.pid)
80026
+ continue;
80027
+ try {
80028
+ const environment = fs81.readFileSync(path88.join("/proc", entry, "environ"), "utf8");
80029
+ if (environment.split("\x00").includes(assignment))
80030
+ matches2.push(pid);
80031
+ } catch {}
80032
+ }
80033
+ return matches2;
80034
+ }
80035
+ if (process.platform === "darwin") {
80036
+ try {
80037
+ const output = execFileSync2("ps", ["eww", "-axo", "pid=,command="], {
80038
+ encoding: "utf8",
80039
+ maxBuffer: 16 * 1024 * 1024,
80040
+ stdio: ["ignore", "pipe", "ignore"]
80041
+ });
80042
+ const matches2 = [];
80043
+ for (const line of output.split(`
80044
+ `)) {
80045
+ const match = line.match(/^\s*(\d+)\s+(.*)$/);
80046
+ if (!match || !match[2].includes(assignment))
80047
+ continue;
80048
+ const pid = Number(match[1]);
80049
+ if (Number.isInteger(pid) && pid !== process.pid)
80050
+ matches2.push(pid);
80051
+ }
80052
+ return matches2;
80053
+ } catch {
80054
+ return [];
80055
+ }
80056
+ }
80057
+ return [];
80058
+ }
80059
+ function processExists(pid) {
80060
+ if (process.platform === "linux") {
80061
+ try {
80062
+ const stat = fs81.readFileSync(path88.join("/proc", String(pid), "stat"), "utf8");
80063
+ const commandEnd = stat.lastIndexOf(")");
80064
+ const state = commandEnd >= 0 ? stat.slice(commandEnd + 2, commandEnd + 3) : "";
80065
+ if (state === "Z" || state === "X")
80066
+ return false;
80067
+ } catch (error2) {
80068
+ if (error2.code === "ENOENT")
80069
+ return false;
80070
+ }
80071
+ }
80072
+ try {
80073
+ process.kill(pid, 0);
80074
+ return true;
80075
+ } catch (error2) {
80076
+ return error2.code !== "ESRCH";
80077
+ }
80078
+ }
80079
+ async function terminateCommandProcesses(child, marker, observedDescendants) {
80080
+ if (child.pid !== undefined) {
80081
+ for (const pid of descendantPids(child.pid))
80082
+ observedDescendants.add(pid);
80083
+ }
80084
+ terminate(child);
80085
+ const deadline = Date.now() + COMMAND_PROCESS_CLEANUP_MS;
80086
+ while (true) {
80087
+ for (const pid of markedProcessPids(marker))
80088
+ observedDescendants.add(pid);
80089
+ const active = [...observedDescendants].filter(processExists);
80090
+ for (const pid of active) {
80091
+ try {
80092
+ process.kill(pid, "SIGKILL");
80093
+ } catch {}
80094
+ }
80095
+ if (active.length === 0)
80096
+ return;
80097
+ if (Date.now() >= deadline) {
80098
+ throw new BenchmarkPhaseError("command_cleanup_failed", "command descendants remained after bounded cleanup");
80099
+ }
80100
+ await new Promise((resolve) => setTimeout(resolve, COMMAND_PROCESS_POLL_MS));
80101
+ }
80102
+ }
80103
+ async function runCommand(command, root, spec, context, options = {}) {
79959
80104
  const cwdRel = normalizedRootRelative(command.cwd);
79960
80105
  assertNoSymlinkTraversal(root, cwdRel);
79961
80106
  const cwd2 = path88.resolve(root, cwdRel);
@@ -79971,29 +80116,59 @@ async function runCommand(command, root, spec, context, additions = {}) {
79971
80116
  const remainingMs = context.deadline - Date.now();
79972
80117
  if (remainingMs <= 0)
79973
80118
  throw new BenchmarkPhaseError("phase_timeout", "phase budget expired");
79974
- const timeoutMs2 = Math.min(command.timeout_ms ?? remainingMs, remainingMs);
80119
+ const reachesPhaseDeadline = command.timeout_ms === undefined || command.timeout_ms >= remainingMs;
80120
+ const timeoutMs2 = reachesPhaseDeadline ? remainingMs : command.timeout_ms;
79975
80121
  const started = Date.now();
80122
+ const commandMarker = crypto6.randomBytes(32).toString("hex");
79976
80123
  return await new Promise((resolve, reject2) => {
79977
80124
  const child = spawn4(command.argv[0], command.argv.slice(1), {
79978
80125
  cwd: cwd2,
79979
- env: buildEnvironment(spec, command.secret_env, additions),
80126
+ env: buildEnvironment(spec, command.secret_env, {
80127
+ ...options.additions,
80128
+ [COMMAND_PROCESS_MARKER_ENV]: commandMarker
80129
+ }),
79980
80130
  stdio: ["ignore", "pipe", "pipe"],
79981
80131
  detached: process.platform !== "win32"
79982
80132
  });
79983
80133
  const stdout = [];
79984
80134
  const stderr = [];
80135
+ const observedDescendants = new Set;
79985
80136
  let captured = 0;
79986
80137
  let settled = false;
79987
80138
  let timer;
80139
+ let observer;
80140
+ let cleanup;
80141
+ const observeDescendants = () => {
80142
+ if (child.pid === undefined)
80143
+ return;
80144
+ for (const pid of descendantPids(child.pid))
80145
+ observedDescendants.add(pid);
80146
+ };
80147
+ const cleanupProcesses = () => {
80148
+ if (observer)
80149
+ clearInterval(observer);
80150
+ cleanup ??= terminateCommandProcesses(child, commandMarker, observedDescendants);
80151
+ return cleanup;
80152
+ };
80153
+ const sanitizedOutput = (chunks) => redactCommandOutput(Buffer.concat(chunks), spec, [commandMarker]);
80154
+ const sanitizedError = (error2) => {
80155
+ if (!(error2 instanceof BenchmarkCommandExecutionError))
80156
+ return error2;
80157
+ return new BenchmarkCommandExecutionError(error2.code, error2.message, sanitizedOutput(stdout), sanitizedOutput(stderr), error2.durationMs);
80158
+ };
79988
80159
  const fail = (error2) => {
79989
80160
  if (settled)
79990
80161
  return;
79991
80162
  settled = true;
79992
80163
  if (timer)
79993
80164
  clearTimeout(timer);
79994
- terminate(child);
79995
- reject2(error2);
80165
+ cleanupProcesses().then(() => reject2(sanitizedError(error2)), reject2);
79996
80166
  };
80167
+ if (process.platform !== "linux") {
80168
+ observeDescendants();
80169
+ observer = setInterval(observeDescendants, COMMAND_PROCESS_POLL_MS);
80170
+ observer.unref();
80171
+ }
79997
80172
  const capture = (target, chunk2) => {
79998
80173
  if (settled)
79999
80174
  return;
@@ -80005,29 +80180,48 @@ async function runCommand(command, root, spec, context, additions = {}) {
80005
80180
  }
80006
80181
  target.push(chunk2);
80007
80182
  };
80183
+ const finish = (code, signal) => {
80184
+ if (observer)
80185
+ clearInterval(observer);
80186
+ if (code === null) {
80187
+ reject2(new BenchmarkCommandExecutionError("command_terminated", `command was terminated by ${signal ?? "an unknown signal"}: ${command.id}`, sanitizedOutput(stdout), sanitizedOutput(stderr), Date.now() - started));
80188
+ return;
80189
+ }
80190
+ resolve({
80191
+ exitCode: code,
80192
+ stdout: sanitizedOutput(stdout),
80193
+ stderr: sanitizedOutput(stderr),
80194
+ durationMs: Date.now() - started,
80195
+ redactions: [commandMarker]
80196
+ });
80197
+ };
80008
80198
  child.stdout?.on("data", (chunk2) => capture(stdout, chunk2));
80009
80199
  child.stderr?.on("data", (chunk2) => capture(stderr, chunk2));
80010
80200
  child.on("error", (error2) => {
80011
- fail(new BenchmarkPhaseError("command_start_failed", `failed to start ${command.id}: ${error2.message}`));
80201
+ fail(new BenchmarkCommandExecutionError("command_start_failed", `failed to start ${command.id}: ${error2.message}`, Buffer.concat(stdout), Buffer.concat(stderr), Date.now() - started));
80012
80202
  });
80013
80203
  timer = setTimeout(() => {
80014
- fail(new BenchmarkPhaseError("command_timeout", `command timed out: ${command.id}`));
80204
+ if (reachesPhaseDeadline) {
80205
+ fail(new BenchmarkPhaseError("phase_timeout", "phase budget expired"));
80206
+ return;
80207
+ }
80208
+ fail(new BenchmarkCommandExecutionError("command_timeout", `command timed out: ${command.id}`, Buffer.concat(stdout), Buffer.concat(stderr), Date.now() - started));
80015
80209
  }, timeoutMs2);
80016
- child.on("close", (code, signal) => {
80210
+ child.on("exit", (code, signal) => {
80017
80211
  if (settled)
80018
80212
  return;
80019
80213
  settled = true;
80020
80214
  clearTimeout(timer);
80021
- if (code === null) {
80022
- reject2(new BenchmarkPhaseError("command_terminated", `command was terminated by ${signal ?? "an unknown signal"}: ${command.id}`));
80023
- return;
80215
+ if (options.descendantCleanup === "always" || code === null || code !== 0) {
80216
+ cleanupProcesses().then(() => finish(code, signal), reject2);
80217
+ } else {
80218
+ if (observer)
80219
+ clearInterval(observer);
80220
+ child.unref();
80221
+ child.stdout?.unref?.();
80222
+ child.stderr?.unref?.();
80223
+ finish(code, signal);
80024
80224
  }
80025
- resolve({
80026
- exitCode: code,
80027
- stdout: Buffer.concat(stdout),
80028
- stderr: Buffer.concat(stderr),
80029
- durationMs: Date.now() - started
80030
- });
80031
80225
  });
80032
80226
  });
80033
80227
  }
@@ -80100,11 +80294,14 @@ async function executeHydrate(spec, context) {
80100
80294
  throw error2;
80101
80295
  }
80102
80296
  }
80297
+ removePrivateHydrationSourcesBeforeSetup(spec, context);
80103
80298
  for (const command of spec.setup_commands) {
80104
80299
  assertBudget(context);
80105
80300
  let result2;
80106
80301
  try {
80107
- result2 = await runCommand(command, spec.workspace_root, spec, context);
80302
+ result2 = await runCommand(command, spec.workspace_root, spec, context, {
80303
+ descendantCleanup: "failure_only"
80304
+ });
80108
80305
  } catch (error2) {
80109
80306
  context.steps.push({
80110
80307
  id: command.id,
@@ -80155,7 +80352,6 @@ async function executeHydrate(spec, context) {
80155
80352
  }
80156
80353
  outputs.splice(0, outputs.length, ...finalOutputs);
80157
80354
  context.outputs.splice(0, context.outputs.length, ...finalOutputs);
80158
- removeRemoteHydrationInputs(spec);
80159
80355
  assertBudget(context);
80160
80356
  return outputs;
80161
80357
  }
@@ -80220,6 +80416,49 @@ async function workspaceManifest(spec, context) {
80220
80416
  }
80221
80417
  return records.sort((a3, b4) => a3.path.localeCompare(b4.path));
80222
80418
  }
80419
+ async function verifyWorkspaceManifestUnchanged(expected, spec, context) {
80420
+ await verifyRecordsUnchanged(expected, spec);
80421
+ const actual = await workspaceManifest(spec, context);
80422
+ if (JSON.stringify(actual) !== JSON.stringify(expected)) {
80423
+ throw new BenchmarkPhaseError("evidence_tampered", "candidate workspace changed during read-only evaluation");
80424
+ }
80425
+ }
80426
+ function treeBytes(root, context) {
80427
+ let totalBytes = 0;
80428
+ const stack = [path88.resolve(root)];
80429
+ while (stack.length > 0) {
80430
+ const directory = stack.pop();
80431
+ const entries = fs81.readdirSync(directory, { withFileTypes: true });
80432
+ for (const entry of entries) {
80433
+ assertBudget(context);
80434
+ const candidate = path88.join(directory, entry.name);
80435
+ const stat = fs81.lstatSync(candidate);
80436
+ if (stat.isDirectory()) {
80437
+ stack.push(candidate);
80438
+ continue;
80439
+ }
80440
+ if (stat.isFile()) {
80441
+ totalBytes += stat.size;
80442
+ continue;
80443
+ }
80444
+ if (stat.isSymbolicLink()) {
80445
+ totalBytes += Buffer.byteLength(fs81.readlinkSync(candidate));
80446
+ continue;
80447
+ }
80448
+ throw new BenchmarkPhaseError("unsafe_path", `evaluate output contains an unsupported filesystem entry: ${path88.relative(root, candidate)}`);
80449
+ }
80450
+ }
80451
+ return totalBytes;
80452
+ }
80453
+ function assertEvaluateOutputBudget(spec, context, additionalBytes = 0) {
80454
+ const actual = treeBytes(spec.logs_root, context) + additionalBytes;
80455
+ if (actual > spec.workspace_limits.max_total_bytes) {
80456
+ throw new BenchmarkPhaseError("workspace_limit_exceeded", "evaluate output bytes exceed the workspace byte limit", {
80457
+ actual,
80458
+ max_total_bytes: spec.workspace_limits.max_total_bytes
80459
+ });
80460
+ }
80461
+ }
80223
80462
  function candidateGlob(pattern) {
80224
80463
  let source = "^";
80225
80464
  for (let index = 0;index < pattern.length; index += 1) {
@@ -80250,6 +80489,8 @@ function manifestDirectories(manifest, context) {
80250
80489
  const directories = new Set;
80251
80490
  for (const entry of manifest) {
80252
80491
  assertBudget(context);
80492
+ if (entry.kind === "symlink")
80493
+ continue;
80253
80494
  let current = path88.posix.dirname(entry.path);
80254
80495
  while (current !== ".") {
80255
80496
  assertBudget(context);
@@ -80267,9 +80508,8 @@ function candidateOutputFiles(output, manifest, context) {
80267
80508
  assertBudget(context);
80268
80509
  if (!matcher.test(entry.path))
80269
80510
  continue;
80270
- if (entry.kind === "symlink") {
80271
- throw new BenchmarkPhaseError("unsafe_path", `candidate output ${output.id} matches a symlink`);
80272
- }
80511
+ if (entry.kind === "symlink")
80512
+ continue;
80273
80513
  matched.push(entry);
80274
80514
  }
80275
80515
  return {
@@ -80277,12 +80517,6 @@ function candidateOutputFiles(output, manifest, context) {
80277
80517
  files: matched
80278
80518
  };
80279
80519
  }
80280
- for (const entry of manifest) {
80281
- assertBudget(context);
80282
- if (entry.kind === "symlink" && matcher.test(entry.path)) {
80283
- throw new BenchmarkPhaseError("unsafe_path", `candidate output ${output.id} matches a symlink`);
80284
- }
80285
- }
80286
80520
  const directories = [];
80287
80521
  for (const directory of manifestDirectories(manifest, context)) {
80288
80522
  assertBudget(context);
@@ -80297,9 +80531,8 @@ function candidateOutputFiles(output, manifest, context) {
80297
80531
  assertBudget(context);
80298
80532
  if (!entry.path.startsWith(prefix))
80299
80533
  continue;
80300
- if (entry.kind === "symlink") {
80301
- throw new BenchmarkPhaseError("unsafe_path", `candidate output ${output.id} contains a symlink`);
80302
- }
80534
+ if (entry.kind === "symlink")
80535
+ continue;
80303
80536
  selected.set(entry.path, entry);
80304
80537
  }
80305
80538
  }
@@ -80332,7 +80565,7 @@ async function copyCandidateOutput(output, manifest, spec, context) {
80332
80565
  for (const frozenFile of selected.files) {
80333
80566
  assertBudget(context);
80334
80567
  const source = path88.resolve(spec.workspace_root, safeRelPath(frozenFile.path));
80335
- const destination = path88.resolve(spec.logs_root, "candidate-artifacts", output.id, safeRelPath(frozenFile.path));
80568
+ const destination = path88.resolve(spec.logs_root, "candidate-outputs", output.id, safeRelPath(frozenFile.path));
80336
80569
  if (fs81.existsSync(destination)) {
80337
80570
  throw new BenchmarkPhaseError("destination_conflict", `candidate output destination already exists: ${output.id}/${frozenFile.path}`);
80338
80571
  }
@@ -80345,21 +80578,29 @@ async function copyCandidateOutput(output, manifest, spec, context) {
80345
80578
  }
80346
80579
  return copied;
80347
80580
  }
80348
- async function copyFrozenWorkspace(manifest, spec, context) {
80349
- for (const entry of manifest) {
80350
- assertBudget(context);
80351
- if (entry.kind === "symlink") {
80352
- throw new BenchmarkPhaseError("unsupported_workspace_symlink", `sandbox evaluator workspace cannot safely reproduce symlink: ${entry.path}`);
80353
- }
80354
- }
80581
+ async function copyFrozenWorkspace(manifest, spec, context, sourceRoot = spec.workspace_root, symlinkPolicy = "preserve") {
80355
80582
  const destinationRoot = fs81.mkdtempSync(path88.join(os16.tmpdir(), "brainbase-benchmark-workspace-"));
80356
80583
  fs81.chmodSync(destinationRoot, 448);
80357
80584
  context.temporaryRoots.add(destinationRoot);
80358
80585
  for (const frozenFile of manifest) {
80359
80586
  assertBudget(context);
80360
- const source = path88.resolve(spec.workspace_root, safeRelPath(frozenFile.path));
80587
+ const source = path88.resolve(sourceRoot, safeRelPath(frozenFile.path));
80361
80588
  const destination = path88.resolve(destinationRoot, safeRelPath(frozenFile.path));
80362
- await atomicCopy(source, destination, frozenFile.mode, spec.workspace_root);
80589
+ if (frozenFile.kind === "symlink") {
80590
+ const stat = fs81.lstatSync(source);
80591
+ const target = stat.isSymbolicLink() ? fs81.readlinkSync(source) : null;
80592
+ if (target === null || Buffer.byteLength(target) !== frozenFile.size || sha256(target) !== frozenFile.sha256) {
80593
+ throw new BenchmarkPhaseError("evidence_tampered", `workspace changed after it was frozen: ${frozenFile.path}`);
80594
+ }
80595
+ const resolvedTarget = path88.resolve(path88.dirname(source), target);
80596
+ if (symlinkPolicy === "contained_relative_only" && (path88.isAbsolute(target) || !isWithin(sourceRoot, resolvedTarget))) {
80597
+ continue;
80598
+ }
80599
+ fs81.mkdirSync(path88.dirname(destination), { recursive: true, mode: 448 });
80600
+ fs81.symlinkSync(target, destination);
80601
+ continue;
80602
+ }
80603
+ await atomicCopy(source, destination, frozenFile.mode, sourceRoot);
80363
80604
  const copied = await recordFile(destinationRoot, destination, "workspace");
80364
80605
  if (copied.sha256 !== frozenFile.sha256 || copied.size !== frozenFile.size || copied.mode !== frozenFile.mode) {
80365
80606
  throw new BenchmarkPhaseError("evidence_tampered", `workspace changed after it was frozen: ${frozenFile.path}`);
@@ -80367,13 +80608,52 @@ async function copyFrozenWorkspace(manifest, spec, context) {
80367
80608
  }
80368
80609
  return destinationRoot;
80369
80610
  }
80370
- function evaluatorTestsPath(evaluator, spec) {
80611
+ async function copyEvaluatorTests(spec, evaluator, context) {
80612
+ const destinationRoot = fs81.mkdtempSync(path88.join(os16.tmpdir(), "brainbase-benchmark-tests-"));
80613
+ fs81.chmodSync(destinationRoot, 448);
80614
+ context.temporaryRoots.add(destinationRoot);
80615
+ const sourceRoot = evaluatorTestsPath(evaluator, spec.tests_root);
80616
+ const relativeRoot = evaluator.tests_path ? normalizedRootRelative(evaluator.tests_path) : ".";
80617
+ const destinationStart = relativeRoot === "." ? destinationRoot : path88.resolve(destinationRoot, relativeRoot);
80618
+ fs81.mkdirSync(destinationStart, { recursive: true, mode: 448 });
80619
+ const stack = [{ source: sourceRoot, destination: destinationStart }];
80620
+ while (stack.length > 0) {
80621
+ assertBudget(context);
80622
+ const current = stack.pop();
80623
+ const entries = fs81.readdirSync(current.source, { withFileTypes: true }).sort((left, right) => left.name.localeCompare(right.name));
80624
+ for (const entry of entries) {
80625
+ assertBudget(context);
80626
+ const source = path88.join(current.source, entry.name);
80627
+ const destination = path88.join(current.destination, entry.name);
80628
+ const stat = fs81.lstatSync(source);
80629
+ if (stat.isSymbolicLink()) {
80630
+ throw new BenchmarkPhaseError("unsafe_path", `evaluator reference tree contains a symlink: ${path88.relative(spec.tests_root, source)}`);
80631
+ }
80632
+ if (stat.isDirectory()) {
80633
+ fs81.mkdirSync(destination, { recursive: true, mode: stat.mode & 511 });
80634
+ stack.push({ source, destination });
80635
+ continue;
80636
+ }
80637
+ if (!stat.isFile()) {
80638
+ throw new BenchmarkPhaseError("unsafe_path", `evaluator reference tree contains an unsupported filesystem entry: ${path88.relative(spec.tests_root, source)}`);
80639
+ }
80640
+ await atomicCopy(source, destination, stat.mode & 511, spec.tests_root);
80641
+ const sourceRecord = await recordFile(spec.tests_root, source, "tests");
80642
+ const copiedRecord = await recordFile(destinationRoot, destination, "tests");
80643
+ if (copiedRecord.path !== sourceRecord.path || copiedRecord.sha256 !== sourceRecord.sha256 || copiedRecord.size !== sourceRecord.size || copiedRecord.mode !== sourceRecord.mode) {
80644
+ throw new BenchmarkPhaseError("evidence_tampered", `evaluator reference changed while it was copied: ${sourceRecord.path}`);
80645
+ }
80646
+ }
80647
+ }
80648
+ return destinationRoot;
80649
+ }
80650
+ function evaluatorTestsPath(evaluator, testsRoot) {
80371
80651
  if (!evaluator.tests_path)
80372
- return spec.tests_root;
80652
+ return testsRoot;
80373
80653
  const relative = normalizedRootRelative(evaluator.tests_path);
80374
- assertNoSymlinkTraversal(spec.tests_root, relative);
80375
- const candidate = path88.resolve(spec.tests_root, relative);
80376
- if (!isWithin(spec.tests_root, candidate) || !fs81.existsSync(candidate)) {
80654
+ assertNoSymlinkTraversal(testsRoot, relative);
80655
+ const candidate = path88.resolve(testsRoot, relative);
80656
+ if (!isWithin(testsRoot, candidate) || !fs81.existsSync(candidate)) {
80377
80657
  throw new BenchmarkPhaseError("invalid_tests_path", `evaluator tests_path does not exist: ${evaluator.tests_path}`);
80378
80658
  }
80379
80659
  const stat = fs81.lstatSync(candidate);
@@ -80398,8 +80678,16 @@ var CriterionResultSchema = exports_external.object({
80398
80678
  });
80399
80679
  var CriteriaResultFileSchema = exports_external.object({
80400
80680
  criteria: exports_external.array(CriterionResultSchema).max(MAX_CRITERIA_PER_EVALUATOR)
80401
- }).strict();
80402
- function readCriterionResults(resultPath, criterionKeys, allowedEvidenceIds) {
80681
+ }).strict().superRefine((value, context) => {
80682
+ if (Buffer.byteLength(JSON.stringify(value)) > MAX_CRITERIA_RESULT_PAYLOAD_BYTES) {
80683
+ context.addIssue({
80684
+ code: exports_external.ZodIssueCode.custom,
80685
+ path: ["criteria"],
80686
+ message: "criterion result exceeds the 20 MiB aggregate payload limit"
80687
+ });
80688
+ }
80689
+ });
80690
+ function readCriterionResults(resultPath, criterionKeys, allowedEvidenceIds, spec, additionalSecrets = []) {
80403
80691
  let opened;
80404
80692
  try {
80405
80693
  opened = openRegularFileNoFollow(resultPath, "criterion result", path88.dirname(resultPath));
@@ -80411,7 +80699,7 @@ function readCriterionResults(resultPath, criterionKeys, allowedEvidenceIds) {
80411
80699
  }
80412
80700
  try {
80413
80701
  if (opened.stat.size > MAX_CRITERIA_RESULT_BYTES) {
80414
- throw new BenchmarkPhaseError("invalid_criterion_result", "criterion result exceeds the 1 MiB limit");
80702
+ throw new BenchmarkPhaseError("invalid_criterion_result", "criterion result exceeds the 24 MiB file limit");
80415
80703
  }
80416
80704
  let raw;
80417
80705
  try {
@@ -80443,7 +80731,13 @@ function readCriterionResults(resultPath, criterionKeys, allowedEvidenceIds) {
80443
80731
  if (unknownEvidenceIds.length > 0) {
80444
80732
  throw new BenchmarkPhaseError("invalid_criterion_result", "criterion result cites evidence outside the evaluator plan", { unknown_evidence_ids: unknownEvidenceIds });
80445
80733
  }
80446
- return criterionKeys.map((key2) => byKey.get(key2));
80734
+ return criterionKeys.map((key2) => {
80735
+ const criterion = byKey.get(key2);
80736
+ return {
80737
+ ...criterion,
80738
+ explanation: redactCommandOutput(Buffer.from(criterion.explanation), spec, additionalSecrets).toString("utf8")
80739
+ };
80740
+ });
80447
80741
  } finally {
80448
80742
  fs81.closeSync(opened.fd);
80449
80743
  }
@@ -80466,7 +80760,7 @@ function eventType(event) {
80466
80760
  }
80467
80761
  return;
80468
80762
  }
80469
- async function evaluateOne(evaluator, spec, manifest, finalOutput, trajectory, frozenEvidence, context) {
80763
+ async function evaluateOne(evaluator, spec, manifest, frozenWorkspaceRoot, finalOutput, trajectory, frozenEvidence, context) {
80470
80764
  const started = Date.now();
80471
80765
  const base2 = {
80472
80766
  id: evaluator.id,
@@ -80501,16 +80795,33 @@ async function evaluateOne(evaluator, spec, manifest, finalOutput, trajectory, f
80501
80795
  cwd: ".",
80502
80796
  secret_env: []
80503
80797
  }, spec.tests_root, spec, context, {
80504
- BB_REGEX_INPUT: frozenEvidence.finalOutputPath,
80505
- BB_REGEX_PATTERN_B64: Buffer.from(assertion.pattern).toString("base64"),
80506
- BB_REGEX_FLAGS: assertion.flags
80798
+ additions: {
80799
+ BB_REGEX_INPUT: frozenEvidence.finalOutputPath,
80800
+ BB_REGEX_PATTERN_B64: Buffer.from(assertion.pattern).toString("base64"),
80801
+ BB_REGEX_FLAGS: assertion.flags
80802
+ },
80803
+ descendantCleanup: "always"
80507
80804
  });
80508
80805
  if (regexResult.exitCode === 2) {
80509
80806
  throw new BenchmarkPhaseError("invalid_evaluator", `invalid regex in evaluator: ${evaluator.id}`);
80510
80807
  }
80511
80808
  verdict = regexResult.exitCode === 0;
80512
80809
  }
80513
- return { ...base2, status: verdict ? "passed" : "failed", verdict, duration_ms: Date.now() - started };
80810
+ return {
80811
+ ...base2,
80812
+ status: verdict ? "passed" : "failed",
80813
+ verdict,
80814
+ duration_ms: Date.now() - started,
80815
+ details: {
80816
+ operator: assertion.operator,
80817
+ actual_size_bytes: Buffer.byteLength(finalOutput),
80818
+ ...assertion.operator === "exact" ? {
80819
+ expected_size_bytes: Buffer.byteLength(assertion.expected),
80820
+ actual_sha256: sha256(finalOutput),
80821
+ expected_sha256: sha256(assertion.expected)
80822
+ } : {}
80823
+ }
80824
+ };
80514
80825
  }
80515
80826
  if (evaluator.type === "trajectory_assertion") {
80516
80827
  const count = trajectory.filter((event) => eventType(event) === evaluator.event_type).length;
@@ -80525,8 +80836,8 @@ async function evaluateOne(evaluator, spec, manifest, finalOutput, trajectory, f
80525
80836
  }
80526
80837
  if (evaluator.type === "workspace_assertion") {
80527
80838
  const relative = workspaceRel(evaluator.path);
80528
- assertNoSymlinkTraversal(spec.workspace_root, relative);
80529
- const candidate = path88.resolve(spec.workspace_root, relative);
80839
+ assertNoSymlinkTraversal(frozenWorkspaceRoot, relative);
80840
+ const candidate = path88.resolve(frozenWorkspaceRoot, relative);
80530
80841
  let stat = null;
80531
80842
  try {
80532
80843
  stat = fs81.lstatSync(candidate);
@@ -80540,15 +80851,17 @@ async function evaluateOne(evaluator, spec, manifest, finalOutput, trajectory, f
80540
80851
  }
80541
80852
  const exists2 = stat !== null;
80542
80853
  let verdict = false;
80854
+ let actualSha256;
80543
80855
  if (evaluator.assertion.operator === "exists")
80544
80856
  verdict = exists2;
80545
80857
  if (evaluator.assertion.operator === "not_exists")
80546
80858
  verdict = !exists2;
80547
80859
  if (evaluator.assertion.operator === "sha256") {
80548
80860
  if (stat?.isFile()) {
80549
- const opened = openRegularFileNoFollow(candidate, `workspace assertion ${relative}`, spec.workspace_root);
80861
+ const opened = openRegularFileNoFollow(candidate, `workspace assertion ${relative}`, frozenWorkspaceRoot);
80550
80862
  try {
80551
- verdict = await sha256OfDescriptor(opened.fd) === evaluator.assertion.expected;
80863
+ actualSha256 = await sha256OfDescriptor(opened.fd);
80864
+ verdict = actualSha256 === evaluator.assertion.expected;
80552
80865
  } finally {
80553
80866
  fs81.closeSync(opened.fd);
80554
80867
  }
@@ -80556,7 +80869,7 @@ async function evaluateOne(evaluator, spec, manifest, finalOutput, trajectory, f
80556
80869
  }
80557
80870
  if (evaluator.assertion.operator === "contains") {
80558
80871
  if (stat?.isFile()) {
80559
- const opened = openRegularFileNoFollow(candidate, `workspace assertion ${relative}`, spec.workspace_root);
80872
+ const opened = openRegularFileNoFollow(candidate, `workspace assertion ${relative}`, frozenWorkspaceRoot);
80560
80873
  try {
80561
80874
  verdict = readDescriptor(opened.fd).toString("utf8").includes(evaluator.assertion.expected);
80562
80875
  } finally {
@@ -80564,15 +80877,36 @@ async function evaluateOne(evaluator, spec, manifest, finalOutput, trajectory, f
80564
80877
  }
80565
80878
  }
80566
80879
  }
80567
- return { ...base2, status: verdict ? "passed" : "failed", verdict, duration_ms: Date.now() - started };
80880
+ return {
80881
+ ...base2,
80882
+ status: verdict ? "passed" : "failed",
80883
+ verdict,
80884
+ duration_ms: Date.now() - started,
80885
+ details: {
80886
+ operator: evaluator.assertion.operator,
80887
+ path: relative,
80888
+ exists: exists2,
80889
+ ...stat ? {
80890
+ actual_kind: stat.isFile() ? "file" : stat.isDirectory() ? "directory" : "other",
80891
+ actual_size_bytes: stat.size
80892
+ } : {},
80893
+ ...actualSha256 ? { actual_sha256: actualSha256 } : {}
80894
+ }
80895
+ };
80568
80896
  }
80569
80897
  let isolatedWorkspace;
80898
+ let isolatedTestsRoot;
80570
80899
  let privateResultRoot;
80900
+ const usesLiveWorkspace = evaluator.workspace_mode === "read_only";
80571
80901
  try {
80572
- const evaluatorWorkspace = await copyFrozenWorkspace(manifest, spec, context);
80573
- isolatedWorkspace = evaluatorWorkspace;
80574
- const testsPath = evaluatorTestsPath(evaluator, spec);
80575
- const commandRoot = evaluator.root === "workspace" ? evaluatorWorkspace : spec.tests_root;
80902
+ const evaluatorWorkspace = usesLiveWorkspace ? spec.workspace_root : await copyFrozenWorkspace(manifest, spec, context, frozenWorkspaceRoot, "contained_relative_only");
80903
+ if (!usesLiveWorkspace)
80904
+ isolatedWorkspace = evaluatorWorkspace;
80905
+ else
80906
+ await verifyWorkspaceManifestUnchanged(manifest, spec, context);
80907
+ isolatedTestsRoot = await copyEvaluatorTests(spec, evaluator, context);
80908
+ const testsPath = evaluatorTestsPath(evaluator, isolatedTestsRoot);
80909
+ const commandRoot = evaluator.root === "workspace" ? evaluatorWorkspace : isolatedTestsRoot;
80576
80910
  const command = { id: evaluator.id, ...evaluator.command };
80577
80911
  const environment = {
80578
80912
  BRAINBASE_BENCHMARK_WORKSPACE: evaluatorWorkspace,
@@ -80589,12 +80923,15 @@ async function evaluateOne(evaluator, spec, manifest, finalOutput, trajectory, f
80589
80923
  criterionResultPath = path88.join(privateResultRoot, "result.json");
80590
80924
  environment.BRAINBASE_BENCHMARK_CRITERIA_RESULT = criterionResultPath;
80591
80925
  }
80592
- const result2 = await runCommand(command, commandRoot, spec, context, environment);
80926
+ const result2 = await runCommand(command, commandRoot, spec, context, {
80927
+ additions: environment,
80928
+ descendantCleanup: "always"
80929
+ });
80593
80930
  const stdout = await writeLog(spec.logs_root, `${evaluator.id}.stdout.log`, result2.stdout, spec);
80594
80931
  const stderr = await writeLog(spec.logs_root, `${evaluator.id}.stderr.log`, result2.stderr, spec);
80595
80932
  let criterionResults;
80596
80933
  try {
80597
- criterionResults = evaluator.criterion_keys && criterionResultPath ? readCriterionResults(criterionResultPath, evaluator.criterion_keys, evaluator.allowed_evidence_ids ?? []) : undefined;
80934
+ criterionResults = evaluator.criterion_keys && criterionResultPath ? readCriterionResults(criterionResultPath, evaluator.criterion_keys, evaluator.allowed_evidence_ids ?? [], spec, result2.redactions) : undefined;
80598
80935
  } catch (error2) {
80599
80936
  const normalized = stableError(error2);
80600
80937
  return {
@@ -80623,11 +80960,17 @@ async function evaluateOne(evaluator, spec, manifest, finalOutput, trajectory, f
80623
80960
  ...criterionResults ? { criterion_results: criterionResults } : {}
80624
80961
  };
80625
80962
  } finally {
80626
- for (const temporary of [privateResultRoot, isolatedWorkspace]) {
80627
- if (!temporary)
80628
- continue;
80629
- fs81.rmSync(temporary, { recursive: true, force: true });
80630
- context.temporaryRoots.delete(temporary);
80963
+ try {
80964
+ if (usesLiveWorkspace) {
80965
+ await verifyWorkspaceManifestUnchanged(manifest, spec, context);
80966
+ }
80967
+ } finally {
80968
+ for (const temporary of [privateResultRoot, isolatedTestsRoot, isolatedWorkspace]) {
80969
+ if (!temporary)
80970
+ continue;
80971
+ fs81.rmSync(temporary, { recursive: true, force: true });
80972
+ context.temporaryRoots.delete(temporary);
80973
+ }
80631
80974
  }
80632
80975
  }
80633
80976
  }
@@ -80672,6 +81015,7 @@ async function executeEvaluate(spec, context) {
80672
81015
  ];
80673
81016
  outputs.push(...frozenEvidenceRecords);
80674
81017
  context.outputs.push(...frozenEvidenceRecords);
81018
+ assertEvaluateOutputBudget(spec, context);
80675
81019
  assertBudget(context);
80676
81020
  const manifest = await workspaceManifest(spec, context);
80677
81021
  const manifestPath2 = path88.join(spec.logs_root, "candidate-workspace-manifest.json");
@@ -80684,6 +81028,7 @@ async function executeEvaluate(spec, context) {
80684
81028
  const manifestRecord = await recordFile(spec.logs_root, manifestPath2, "logs");
80685
81029
  outputs.push(manifestRecord);
80686
81030
  context.outputs.push(manifestRecord);
81031
+ assertEvaluateOutputBudget(spec, context);
80687
81032
  for (const artifactRelInput of spec.candidate_artifacts) {
80688
81033
  const artifactRel = workspaceRel(artifactRelInput);
80689
81034
  assertNoSymlinkTraversal(spec.workspace_root, artifactRel);
@@ -80701,11 +81046,13 @@ async function executeEvaluate(spec, context) {
80701
81046
  outputs.push(artifact);
80702
81047
  context.outputs.push(artifact);
80703
81048
  }
81049
+ assertEvaluateOutputBudget(spec, context);
80704
81050
  for (const candidateOutput of spec.candidate_outputs) {
80705
81051
  const copied = await copyCandidateOutput(candidateOutput, manifest, spec, context);
80706
81052
  outputs.push(...copied);
80707
81053
  context.outputs.push(...copied);
80708
81054
  }
81055
+ assertEvaluateOutputBudget(spec, context);
80709
81056
  if (spec.capture_workspace_archive) {
80710
81057
  const regularFiles = manifest.filter((entry) => entry.kind !== "symlink").map((entry) => entry.path);
80711
81058
  const archive = path88.join(spec.logs_root, "candidate-workspace.tar.gz");
@@ -80719,23 +81066,23 @@ async function executeEvaluate(spec, context) {
80719
81066
  const archiveRecord = await recordFile(spec.logs_root, archive, "logs");
80720
81067
  outputs.push(archiveRecord);
80721
81068
  context.outputs.push(archiveRecord);
81069
+ assertEvaluateOutputBudget(spec, context);
80722
81070
  }
80723
81071
  await verifyRecordsUnchanged(manifest, spec);
81072
+ const frozenWorkspaceRoot = await copyFrozenWorkspace(manifest, spec, context, spec.workspace_root);
80724
81073
  for (const reference of spec.references) {
80725
81074
  const referenceOutputs = await copyMaterial(reference, spec.staging_root, spec.tests_root, "tests", false, context);
80726
81075
  outputs.push(...referenceOutputs);
80727
81076
  context.outputs.push(...referenceOutputs);
80728
81077
  }
80729
- const frozenOutputCount = context.outputs.length;
80730
81078
  const evaluators = [];
80731
81079
  const evaluatorOrder = new Map(spec.evaluators.map((evaluator, index) => [evaluator.id, index]));
80732
81080
  const executionOrder = [...spec.evaluators].sort((left, right) => Number(left.type === "sandbox_command") - Number(right.type === "sandbox_command"));
80733
81081
  for (const evaluator of executionOrder) {
80734
81082
  assertBudget(context);
80735
81083
  const started = Date.now();
80736
- let requiredResultError;
80737
81084
  try {
80738
- const evaluated = await evaluateOne(evaluator, spec, manifest, finalOutput.buffer.toString("utf8"), trajectory, { finalOutputPath, trajectoryPath }, context);
81085
+ const evaluated = await evaluateOne(evaluator, spec, manifest, frozenWorkspaceRoot, finalOutput.buffer.toString("utf8"), trajectory, { finalOutputPath, trajectoryPath }, context);
80739
81086
  assertBudget(context);
80740
81087
  evaluators.push(evaluated);
80741
81088
  context.evaluators.push(evaluated);
@@ -80747,13 +81094,14 @@ async function executeEvaluate(spec, context) {
80747
81094
  outputs.push(evaluated.stderr);
80748
81095
  context.outputs.push(evaluated.stderr);
80749
81096
  }
80750
- if (evaluated.status === "errored" && evaluator.required) {
80751
- const errorCode = typeof evaluated.details?.error_code === "string" ? evaluated.details.error_code : "evaluator_execution_failed";
80752
- const errorMessage2 = typeof evaluated.details?.error_message === "string" ? evaluated.details.error_message : "required evaluator execution failed";
80753
- requiredResultError = new BenchmarkPhaseError(errorCode, errorMessage2);
80754
- }
80755
81097
  } catch (error2) {
80756
81098
  const normalized = stableError(error2);
81099
+ let stdout;
81100
+ let stderr;
81101
+ if (error2 instanceof BenchmarkCommandExecutionError) {
81102
+ stdout = await writeLog(spec.logs_root, `${evaluator.id}.stdout.log`, error2.stdout, spec);
81103
+ stderr = await writeLog(spec.logs_root, `${evaluator.id}.stderr.log`, error2.stderr, spec);
81104
+ }
80757
81105
  const errored = {
80758
81106
  id: evaluator.id,
80759
81107
  type: evaluator.type,
@@ -80763,26 +81111,38 @@ async function executeEvaluate(spec, context) {
80763
81111
  verdict: null,
80764
81112
  engine: "brainbase-cli",
80765
81113
  engine_version: VERSION,
80766
- duration_ms: Date.now() - started,
81114
+ duration_ms: error2 instanceof BenchmarkCommandExecutionError ? error2.durationMs : Date.now() - started,
80767
81115
  details: {
80768
81116
  error_code: normalized?.code ?? "phase_failed",
80769
81117
  error_message: normalized?.message ?? "evaluator execution failed"
80770
- }
81118
+ },
81119
+ ...stdout ? { stdout } : {},
81120
+ ...stderr ? { stderr } : {}
80771
81121
  };
80772
81122
  evaluators.push(errored);
80773
81123
  context.evaluators.push(errored);
80774
- if (evaluator.required)
81124
+ if (stdout) {
81125
+ outputs.push(stdout);
81126
+ context.outputs.push(stdout);
81127
+ }
81128
+ if (stderr) {
81129
+ outputs.push(stderr);
81130
+ context.outputs.push(stderr);
81131
+ }
81132
+ if (!(error2 instanceof BenchmarkCommandExecutionError))
80775
81133
  throw error2;
80776
81134
  assertBudget(context);
80777
81135
  }
80778
- if (requiredResultError)
80779
- throw requiredResultError;
81136
+ if (evaluator.type === "sandbox_command") {
81137
+ assertEvaluateOutputBudget(spec, context);
81138
+ }
80780
81139
  }
80781
- await verifyRecordsUnchanged([...context.inputs, ...context.outputs.slice(0, frozenOutputCount)], spec);
81140
+ await verifyRecordsUnchanged([...context.inputs, ...context.outputs], spec);
80782
81141
  if (!verifyOwnedDirectory(spec.tests_root, "tests", spec) || !verifyOwnedDirectory(spec.logs_root, "logs", spec)) {
80783
81142
  throw new BenchmarkPhaseError("evidence_tampered", "benchmark evaluator changed an owned phase directory marker");
80784
81143
  }
80785
81144
  assertBudget(context);
81145
+ assertEvaluateOutputBudget(spec, context);
80786
81146
  evaluators.sort((left, right) => evaluatorOrder.get(left.id) - evaluatorOrder.get(right.id));
80787
81147
  return { outputs, evaluators };
80788
81148
  }
@@ -80809,6 +81169,67 @@ function stableError(error2) {
80809
81169
  message: error2 instanceof Error ? error2.message : "unknown benchmark phase failure"
80810
81170
  };
80811
81171
  }
81172
+ function compactOversizedResult(result2) {
81173
+ const actualBytes = Buffer.byteLength(`${JSON.stringify(result2, null, 2)}
81174
+ `);
81175
+ if (actualBytes <= MAX_PHASE_RESULT_BYTES)
81176
+ return result2;
81177
+ return {
81178
+ schema_version: result2.schema_version,
81179
+ cli_version: result2.cli_version,
81180
+ phase: result2.phase,
81181
+ attempt_id: result2.attempt_id,
81182
+ phase_id: result2.phase_id,
81183
+ spec_digest: result2.spec_digest,
81184
+ status: "failed",
81185
+ started_at: result2.started_at,
81186
+ completed_at: result2.completed_at,
81187
+ duration_ms: result2.duration_ms,
81188
+ steps: [],
81189
+ inputs: [],
81190
+ outputs: [],
81191
+ error: {
81192
+ code: "result_too_large",
81193
+ message: "benchmark phase result exceeds the 32 MiB contract limit",
81194
+ details: {
81195
+ actual_bytes: actualBytes,
81196
+ max_bytes: MAX_PHASE_RESULT_BYTES,
81197
+ step_count: result2.steps.length,
81198
+ input_count: result2.inputs.length,
81199
+ output_count: result2.outputs.length,
81200
+ evaluator_count: result2.evaluators?.length ?? 0
81201
+ }
81202
+ }
81203
+ };
81204
+ }
81205
+ function readCachedPhaseResult(resultPath) {
81206
+ let opened;
81207
+ try {
81208
+ opened = openRegularFileNoFollow(resultPath, "cached benchmark phase result");
81209
+ } catch {
81210
+ return;
81211
+ }
81212
+ try {
81213
+ if (opened.stat.size > MAX_PHASE_RESULT_BYTES)
81214
+ return;
81215
+ const chunks = [];
81216
+ let offset = 0;
81217
+ while (offset <= MAX_PHASE_RESULT_BYTES) {
81218
+ const buffer = Buffer.alloc(Math.min(64 * 1024, MAX_PHASE_RESULT_BYTES + 1 - offset));
81219
+ const bytesRead = fs81.readSync(opened.fd, buffer, 0, buffer.length, offset);
81220
+ if (bytesRead === 0) {
81221
+ return JSON.parse(Buffer.concat(chunks, offset).toString("utf8"));
81222
+ }
81223
+ chunks.push(buffer.subarray(0, bytesRead));
81224
+ offset += bytesRead;
81225
+ }
81226
+ return;
81227
+ } catch {
81228
+ return;
81229
+ } finally {
81230
+ fs81.closeSync(opened.fd);
81231
+ }
81232
+ }
80812
81233
  function rawIdentity(value) {
80813
81234
  if (!value || typeof value !== "object") {
80814
81235
  return { phase: "unknown", attemptId: null, phaseId: null };
@@ -80881,13 +81302,9 @@ function prepareBenchmarkPhaseInvocation(specPathInput, resultPathInput, expecte
80881
81302
  timeout_ms: spec.budget.timeout_ms
80882
81303
  };
80883
81304
  let cachedResult;
80884
- if (fs81.existsSync(resultPath)) {
80885
- try {
80886
- const cached2 = JSON.parse(fs81.readFileSync(resultPath, "utf8"));
80887
- if (cached2.schema_version === SCHEMA_VERSION && cached2.phase === spec.phase && cached2.attempt_id === spec.attempt_id && cached2.phase_id === spec.phase_id && cached2.spec_digest === digest && cached2.status === "succeeded") {
80888
- cachedResult = cached2;
80889
- }
80890
- } catch {}
81305
+ const cached2 = readCachedPhaseResult(resultPath);
81306
+ if (cached2?.schema_version === SCHEMA_VERSION && cached2.phase === spec.phase && cached2.attempt_id === spec.attempt_id && cached2.phase_id === spec.phase_id && cached2.spec_digest === digest && cached2.status === "succeeded") {
81307
+ cachedResult = cached2;
80891
81308
  }
80892
81309
  if (cachedResult) {
80893
81310
  if (spec.phase === "hydrate") {
@@ -80904,7 +81321,7 @@ function prepareBenchmarkPhaseInvocation(specPathInput, resultPathInput, expecte
80904
81321
  } catch (error2) {
80905
81322
  return {
80906
81323
  ok: false,
80907
- result: {
81324
+ result: compactOversizedResult({
80908
81325
  schema_version: SCHEMA_VERSION,
80909
81326
  cli_version: VERSION,
80910
81327
  phase: identity2.phase === "unknown" ? expectedPhase : identity2.phase,
@@ -80919,7 +81336,7 @@ function prepareBenchmarkPhaseInvocation(specPathInput, resultPathInput, expecte
80919
81336
  inputs: [],
80920
81337
  outputs: [],
80921
81338
  error: stableError(error2)
80922
- }
81339
+ })
80923
81340
  };
80924
81341
  }
80925
81342
  }
@@ -80968,6 +81385,7 @@ async function runBenchmarkPhase(specPathInput, resultPathInput, expectedPhase,
80968
81385
  let status = "failed";
80969
81386
  let error2;
80970
81387
  let context;
81388
+ let validatedSpec;
80971
81389
  let resultPathValidated = false;
80972
81390
  try {
80973
81391
  let bytes;
@@ -80987,6 +81405,7 @@ async function runBenchmarkPhase(specPathInput, resultPathInput, expectedPhase,
80987
81405
  }
80988
81406
  identity2 = rawIdentity(raw);
80989
81407
  const spec = BenchmarkSpecSchema.parse(raw);
81408
+ validatedSpec = spec;
80990
81409
  if (expectedPhase && spec.phase !== expectedPhase) {
80991
81410
  throw new BenchmarkPhaseError("phase_mismatch", `the ${expectedPhase} command cannot execute a ${spec.phase} spec`);
80992
81411
  }
@@ -80997,13 +81416,9 @@ async function runBenchmarkPhase(specPathInput, resultPathInput, expectedPhase,
80997
81416
  }
80998
81417
  resultPathValidated = true;
80999
81418
  let cachedResult;
81000
- if (fs81.existsSync(resultPath)) {
81001
- try {
81002
- const cached2 = JSON.parse(fs81.readFileSync(resultPath, "utf8"));
81003
- if (cached2.schema_version === SCHEMA_VERSION && cached2.phase === spec.phase && cached2.attempt_id === spec.attempt_id && cached2.phase_id === spec.phase_id && cached2.spec_digest === digest && cached2.status === "succeeded") {
81004
- cachedResult = cached2;
81005
- }
81006
- } catch {}
81419
+ const cached2 = readCachedPhaseResult(resultPath);
81420
+ if (cached2?.schema_version === SCHEMA_VERSION && cached2.phase === spec.phase && cached2.attempt_id === spec.attempt_id && cached2.phase_id === spec.phase_id && cached2.spec_digest === digest && cached2.status === "succeeded") {
81421
+ cachedResult = cached2;
81007
81422
  }
81008
81423
  if (cachedResult) {
81009
81424
  if (spec.phase === "hydrate") {
@@ -81046,7 +81461,7 @@ async function runBenchmarkPhase(specPathInput, resultPathInput, expectedPhase,
81046
81461
  fs81.rmSync(temporary, { recursive: true, force: true });
81047
81462
  }
81048
81463
  }
81049
- const result2 = {
81464
+ let result2 = {
81050
81465
  schema_version: SCHEMA_VERSION,
81051
81466
  cli_version: VERSION,
81052
81467
  phase: identity2.phase === "unknown" && expectedPhase ? expectedPhase : identity2.phase,
@@ -81064,8 +81479,22 @@ async function runBenchmarkPhase(specPathInput, resultPathInput, expectedPhase,
81064
81479
  ...error2 ? { error: error2 } : {}
81065
81480
  };
81066
81481
  if (!resultPathValidated || !context?.logsOwned) {
81482
+ result2 = compactOversizedResult(result2);
81067
81483
  return { exitCode: status === "succeeded" ? 0 : 1, result: result2 };
81068
81484
  }
81485
+ if (status === "succeeded" && validatedSpec?.phase === "evaluate") {
81486
+ try {
81487
+ const resultBytes = Buffer.byteLength(`${JSON.stringify(result2, null, 2)}
81488
+ `);
81489
+ assertEvaluateOutputBudget(validatedSpec, context, resultBytes);
81490
+ } catch (budgetError) {
81491
+ status = "failed";
81492
+ result2.status = "failed";
81493
+ result2.error = stableError(budgetError);
81494
+ }
81495
+ }
81496
+ result2 = compactOversizedResult(result2);
81497
+ status = result2.status;
81069
81498
  try {
81070
81499
  writeJsonAtomic(resultPath, result2);
81071
81500
  } catch (writeError) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@brainbase-labs/cli",
3
- "version": "0.25.0-eng1209.1",
3
+ "version": "0.25.0-eng1209.10",
4
4
  "description": "Pack, share, and install agent templates across harnesses (Claude Code, Codex, ...).",
5
5
  "type": "module",
6
6
  "bin": {