tamperward 2.15.2 → 2.16.1

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 +20 -2
  2. package/dist/cli/index.js +561 -109
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -325,8 +325,14 @@ Public rulesets can be read anonymously; set `GH_TOKEN` or `GITHUB_TOKEN` when
325
325
  authentication is required.
326
326
 
327
327
  A real deployment needs a verify command configured — the generated CI verify step
328
- **fails closed (exit 2) without one** rather than passing quietly. In
329
- `.tamperward.yml`:
328
+ **fails closed (exit 2) without one** rather than passing quietly. Since 2.16.1,
329
+ `tamperward init` ends with a separate **VERIFICATION SETUP** status: it says either
330
+ `verification configured — <command>` or prominently reports
331
+ `INCOMPLETE: verification not configured — CI will fail closed`. It can suggest a
332
+ single high-confidence command such as `npm test`, `pytest`, `tox`, `cargo test`
333
+ or `go test ./...`; multiple candidates are listed without choosing one. Suggestions
334
+ are advisory only — init never writes an inferred verifier command because that command
335
+ is part of the trust anchor. In `.tamperward.yml`:
330
336
 
331
337
  ```yaml
332
338
  verify:
@@ -369,6 +375,18 @@ restored `"test": "sh scripts/test.sh"` will happily call a script nothing
369
375
  restored. It bounds the class rather than closing it — see
370
376
  [the threat model](./docs/THREAT-MODEL-pristine-run.md).
371
377
 
378
+ From **2.16.0**, verifier suite output is diagnostic evidence instead of discarded
379
+ noise. Both visible and pristine stages continuously drain stdout/stderr through a
380
+ trusted supervisor, retain only the final **16 KiB per stream**, and count the total
381
+ bytes observed. Structured output exposes this under
382
+ `visible.diagnostics` / `pristine.diagnostics` with
383
+ `captured_bytes`, `retained_bytes`, `truncated`, and a bounded `tail` on a
384
+ failed stage. Default human output remains quiet on success. On failure, retained
385
+ diagnostics are rendered with every line prefixed and terminal/control characters
386
+ escaped, so candidate output cannot become ANSI control traffic or a GitHub
387
+ `::command::`. These bytes are evidence produced by candidate code; they never own
388
+ the verifier verdict.
389
+
372
390
  The four primitives:
373
391
 
374
392
  ```bash
package/dist/cli/index.js CHANGED
@@ -7085,13 +7085,14 @@ function watchTree(dir, cb, onState, onError) {
7085
7085
  }
7086
7086
  }
7087
7087
  const watchers = /* @__PURE__ */ new Map();
7088
+ let closed = false;
7088
7089
  onState("fallback", 0);
7089
7090
  const addDir = (rel) => {
7090
- if (watchers.has(rel) || SKIP2.test(rel + "/")) return;
7091
+ if (closed || watchers.has(rel) || SKIP2.test(rel + "/")) return;
7091
7092
  let w;
7092
7093
  try {
7093
7094
  w = watch(rel ? join11(dir, rel) : dir, (kind, fname) => {
7094
- if (!fname) return;
7095
+ if (closed || !fname) return;
7095
7096
  const child = rel ? `${rel}/${String(fname)}` : String(fname);
7096
7097
  cb(kind, child);
7097
7098
  try {
@@ -7129,9 +7130,14 @@ function watchTree(dir, cb, onState, onError) {
7129
7130
  }
7130
7131
  };
7131
7132
  walk2("");
7132
- return { close: () => {
7133
- for (const w of watchers.values()) w.close();
7134
- } };
7133
+ return {
7134
+ close: () => {
7135
+ if (closed) return;
7136
+ closed = true;
7137
+ for (const w of watchers.values()) w.close();
7138
+ watchers.clear();
7139
+ }
7140
+ };
7135
7141
  }
7136
7142
  function startWatcher(dir, log, policy) {
7137
7143
  try {
@@ -7215,8 +7221,11 @@ function startWatcher(dir, log, policy) {
7215
7221
  },
7216
7222
  (detail) => degrade(detail)
7217
7223
  );
7224
+ let closed = false;
7218
7225
  return {
7219
7226
  close: () => {
7227
+ if (closed) return;
7228
+ closed = true;
7220
7229
  tree.close();
7221
7230
  health.state = "stopped";
7222
7231
  health.stopped_at = (/* @__PURE__ */ new Date()).toISOString();
@@ -8164,6 +8173,78 @@ function hooksShapeError(hooks) {
8164
8173
  }
8165
8174
  return null;
8166
8175
  }
8176
+ function verifierCandidates(cwd) {
8177
+ const candidates = [];
8178
+ const add = (command) => {
8179
+ if (!candidates.includes(command)) candidates.push(command);
8180
+ };
8181
+ try {
8182
+ const pkg = JSON.parse(readFileSync11(join12(cwd, "package.json"), "utf8"));
8183
+ const test = pkg.scripts?.test;
8184
+ if (typeof test === "string" && test.trim() !== "" && !/error:\s*no test specified/i.test(test)) add("npm test");
8185
+ } catch {
8186
+ }
8187
+ try {
8188
+ const pyproject = readFileSync11(join12(cwd, "pyproject.toml"), "utf8");
8189
+ if (/^\s*\[tool\.pytest(?:\.ini_options)?\]\s*$/m.test(pyproject)) add("pytest");
8190
+ } catch {
8191
+ }
8192
+ if (existsSync9(join12(cwd, "pytest.ini"))) add("pytest");
8193
+ try {
8194
+ const setupCfg = readFileSync11(join12(cwd, "setup.cfg"), "utf8");
8195
+ if (/^\s*\[tool:pytest\]\s*$/m.test(setupCfg)) add("pytest");
8196
+ } catch {
8197
+ }
8198
+ try {
8199
+ const tox = readFileSync11(join12(cwd, "tox.ini"), "utf8");
8200
+ if (/^\s*\[tox\]\s*$/m.test(tox)) add("tox");
8201
+ } catch {
8202
+ }
8203
+ try {
8204
+ const cargo = readFileSync11(join12(cwd, "Cargo.toml"), "utf8");
8205
+ if (/^\s*\[(?:package|workspace)\]\s*(?:#.*)?$/m.test(cargo)) add("cargo test");
8206
+ } catch {
8207
+ }
8208
+ try {
8209
+ const goMod = readFileSync11(join12(cwd, "go.mod"), "utf8");
8210
+ if (/^\s*module\s+\S+\s*(?:\/\/.*)?$/m.test(goMod)) add("go test ./...");
8211
+ } catch {
8212
+ }
8213
+ return candidates;
8214
+ }
8215
+ function verifierSetupMessage(cwd) {
8216
+ try {
8217
+ const policy = loadPolicy(cwd);
8218
+ const command = policy.verify?.command?.trim();
8219
+ if (command) return `verification configured \u2014 ${command}`;
8220
+ } catch (e) {
8221
+ return `INCOMPLETE: verification not configured \u2014 policy cannot currently be loaded; CI will fail closed until the policy is fixed (${errText3(e)}).`;
8222
+ }
8223
+ const candidates = verifierCandidates(cwd);
8224
+ const lines = [
8225
+ "INCOMPLETE: verification not configured \u2014 CI will fail closed until .tamperward.yml names a trusted suite command."
8226
+ ];
8227
+ if (candidates.length === 1) {
8228
+ lines.push(`Suggested verifier command: ${candidates[0]}`);
8229
+ lines.push("Review it, then add:");
8230
+ lines.push("verify:");
8231
+ lines.push(` command: "${candidates[0]}"`);
8232
+ lines.push(" budget: 300");
8233
+ } else if (candidates.length > 1) {
8234
+ lines.push(`Detected verifier candidates: ${candidates.join(", ")}`);
8235
+ lines.push("Choose the suite that defines merge authority, then add for example:");
8236
+ lines.push("verify:");
8237
+ lines.push(' command: "<reviewed suite command>"');
8238
+ lines.push(" budget: 300");
8239
+ } else {
8240
+ lines.push("Add the reviewed suite command to .tamperward.yml, for example:");
8241
+ lines.push("verify:");
8242
+ lines.push(' command: "<your test command>"');
8243
+ lines.push(" budget: 300");
8244
+ }
8245
+ lines.push("TamperWard does not write an inferred verifier command automatically because it is part of the trust anchor.");
8246
+ return lines.join("\n");
8247
+ }
8167
8248
  function planPolicy(cwd) {
8168
8249
  const path = join12(cwd, POLICY_FILE);
8169
8250
  if (existsSync9(path)) {
@@ -8554,6 +8635,10 @@ WARNING (${a.item}): ${a.warning}
8554
8635
  }
8555
8636
  const errors = plan.filter((a) => a.status === "error");
8556
8637
  const changed = opts.dryRun ? plan.filter((a) => a.apply).length : applied;
8638
+ w.write(`
8639
+ VERIFICATION SETUP
8640
+ ${verifierSetupMessage(cwd)}
8641
+ `);
8557
8642
  if (errors.length) {
8558
8643
  w.write(`
8559
8644
  tamperward init: ${errors.length} item(s) need your attention above; the rest ${opts.dryRun ? "are planned" : "were applied"}.
@@ -9152,11 +9237,11 @@ function runDoctor(opts = {}) {
9152
9237
  }
9153
9238
 
9154
9239
  // src/cli/verify.ts
9155
- import { execFileSync as execFileSync9, spawnSync as spawnSync2 } from "node:child_process";
9240
+ import { execFileSync as execFileSync9, spawnSync as spawnSync3 } from "node:child_process";
9156
9241
  import { createHash as createHash10 } from "node:crypto";
9157
- import { chmodSync as chmodSync2, cpSync, lstatSync as lstatSync5, mkdirSync as mkdirSync6, mkdtempSync as mkdtempSync2, readFileSync as readFileSync15, readlinkSync as readlinkSync3, rmSync as rmSync3, symlinkSync, writeFileSync as writeFileSync7 } from "node:fs";
9158
- import { tmpdir as tmpdir2 } from "node:os";
9159
- import { dirname as dirname7, isAbsolute as isAbsolute10, join as join16, relative as relative5, resolve as resolve7, sep as sep2 } from "node:path";
9242
+ import { chmodSync as chmodSync2, cpSync, lstatSync as lstatSync5, mkdirSync as mkdirSync6, mkdtempSync as mkdtempSync3, readFileSync as readFileSync16, readlinkSync as readlinkSync3, rmSync as rmSync4, symlinkSync, writeFileSync as writeFileSync8 } from "node:fs";
9243
+ import { tmpdir as tmpdir3 } from "node:os";
9244
+ import { dirname as dirname7, isAbsolute as isAbsolute10, join as join17, relative as relative5, resolve as resolve7, sep as sep2 } from "node:path";
9160
9245
 
9161
9246
  // src/fingerprint.ts
9162
9247
  import { execFileSync as execFileSync8 } from "node:child_process";
@@ -9539,9 +9624,321 @@ function dependencyEnvironmentSummary(descriptor) {
9539
9624
 
9540
9625
  // src/verifier-backend.ts
9541
9626
  import { createHash as createHash9, randomUUID } from "node:crypto";
9627
+ import { spawnSync as spawnSync2 } from "node:child_process";
9628
+ import { accessSync, constants as constants2, readFileSync as readFileSync15, realpathSync as realpathSync4, statSync as statSync4 } from "node:fs";
9629
+ import { delimiter as delimiter2, dirname as dirname6, join as join16, resolve as resolve6 } from "node:path";
9630
+
9631
+ // src/suite-diagnostics.ts
9542
9632
  import { spawnSync } from "node:child_process";
9543
- import { accessSync, constants as constants2, readFileSync as readFileSync14, realpathSync as realpathSync4, statSync as statSync4 } from "node:fs";
9544
- import { delimiter as delimiter2, dirname as dirname6, join as join15, resolve as resolve6 } from "node:path";
9633
+ import { mkdtempSync as mkdtempSync2, rmSync as rmSync3, writeFileSync as writeFileSync7 } from "node:fs";
9634
+ import { tmpdir as tmpdir2 } from "node:os";
9635
+ import { join as join15 } from "node:path";
9636
+ var DIAGNOSTIC_TAIL_BYTES = 16 * 1024;
9637
+ var EMPTY_STREAM = {
9638
+ captured_bytes: 0,
9639
+ retained_bytes: 0,
9640
+ truncated: false,
9641
+ tail: ""
9642
+ };
9643
+ function emptySuiteDiagnostics() {
9644
+ return {
9645
+ stdout: { ...EMPTY_STREAM },
9646
+ stderr: { ...EMPTY_STREAM }
9647
+ };
9648
+ }
9649
+ function decodeUtf8Tail(tail) {
9650
+ let start = 0;
9651
+ while (start < tail.length && (tail[start] & 192) === 128) start++;
9652
+ return tail.subarray(start).toString("utf8");
9653
+ }
9654
+ function streamFromRaw(raw) {
9655
+ let tail = Buffer.alloc(0);
9656
+ try {
9657
+ tail = Buffer.from(raw?.tail_b64 ?? "", "base64");
9658
+ } catch {
9659
+ tail = Buffer.alloc(0);
9660
+ }
9661
+ const captured = Number.isFinite(raw?.captured_bytes) ? Math.max(0, Number(raw?.captured_bytes)) : tail.length;
9662
+ return {
9663
+ captured_bytes: captured,
9664
+ retained_bytes: tail.length,
9665
+ truncated: captured > tail.length,
9666
+ tail: decodeUtf8Tail(tail)
9667
+ };
9668
+ }
9669
+ function scrubDiagnosticText(input) {
9670
+ let out2 = "";
9671
+ for (const ch of input) {
9672
+ const cp = ch.codePointAt(0);
9673
+ if (ch === "\n" || ch === " ") {
9674
+ out2 += ch;
9675
+ } else if (cp === 13) {
9676
+ out2 += "\\r";
9677
+ } else if (cp < 32 || cp >= 127 && cp <= 159) {
9678
+ out2 += cp <= 255 ? `\\x${cp.toString(16).padStart(2, "0")}` : `\\u{${cp.toString(16)}}`;
9679
+ } else {
9680
+ out2 += ch;
9681
+ }
9682
+ }
9683
+ return out2;
9684
+ }
9685
+ function diagnosticLines(stage, diagnostics) {
9686
+ const lines = [];
9687
+ for (const streamName of ["stdout", "stderr"]) {
9688
+ const stream = diagnostics[streamName];
9689
+ if (stream.captured_bytes === 0) continue;
9690
+ const suffix = stream.truncated ? ` \u2014 tail ${stream.retained_bytes}/${stream.captured_bytes} bytes retained` : ` \u2014 ${stream.captured_bytes} bytes`;
9691
+ lines.push(`${stage} suite ${streamName}${suffix}:`);
9692
+ const scrubbed = scrubDiagnosticText(stream.tail);
9693
+ const body = scrubbed.split("\n");
9694
+ if (body.at(-1) === "") body.pop();
9695
+ for (const line of body) lines.push(` | ${line}`);
9696
+ }
9697
+ return lines;
9698
+ }
9699
+ var CAPTURE_SUPERVISOR = String.raw`
9700
+ const cp = require('node:child_process');
9701
+ const fs = require('node:fs');
9702
+
9703
+ const [configFile] = process.argv.slice(1);
9704
+ const cfg = JSON.parse(fs.readFileSync(configFile, 'utf8'));
9705
+ const CAP = Number(cfg.captureBytes);
9706
+
9707
+ const fresh = () => ({ total: 0, tail: Buffer.alloc(0) });
9708
+ const stdout = fresh();
9709
+ const stderr = fresh();
9710
+
9711
+ // Linux ordinary-exit hardening (#371): process-group ownership alone misses a
9712
+ // descendant that calls setsid(). Track the real descendant tree while the
9713
+ // suite is alive so a reparented session escape is still known when the main
9714
+ // child exits. This supplements, rather than replaces, group termination.
9715
+ const trackedDescendants = new Set();
9716
+
9717
+ function linuxDescendants(rootPid) {
9718
+ if (process.platform !== 'linux' || !rootPid) return [];
9719
+ const byParent = new Map();
9720
+ let names = [];
9721
+ try { names = fs.readdirSync('/proc').filter((x) => /^\d+$/.test(x)); } catch { return []; }
9722
+ for (const name of names) {
9723
+ const pid = Number(name);
9724
+ try {
9725
+ const stat = fs.readFileSync('/proc/' + name + '/stat', 'utf8');
9726
+ const fields = stat.slice(stat.lastIndexOf(')') + 2).split(' ');
9727
+ const ppid = Number(fields[1]);
9728
+ if (!Number.isFinite(ppid)) continue;
9729
+ const kids = byParent.get(ppid) || [];
9730
+ kids.push(pid);
9731
+ byParent.set(ppid, kids);
9732
+ } catch {}
9733
+ }
9734
+ const out = [];
9735
+ const seen = new Set([rootPid]);
9736
+ const stack = [rootPid];
9737
+ while (stack.length) {
9738
+ const parent = stack.pop();
9739
+ for (const pid of (byParent.get(parent) || [])) {
9740
+ if (seen.has(pid)) continue;
9741
+ seen.add(pid);
9742
+ out.push(pid);
9743
+ stack.push(pid);
9744
+ }
9745
+ }
9746
+ return out;
9747
+ }
9748
+
9749
+ function trackDescendants() {
9750
+ if (!child || !child.pid || process.platform !== 'linux') return;
9751
+ for (const pid of linuxDescendants(child.pid)) trackedDescendants.add(pid);
9752
+ }
9753
+
9754
+ function append(state, chunk) {
9755
+ const b = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
9756
+ state.total += b.length;
9757
+ if (b.length >= CAP) {
9758
+ state.tail = Buffer.from(b.subarray(b.length - CAP));
9759
+ return;
9760
+ }
9761
+ if (state.tail.length + b.length <= CAP) {
9762
+ state.tail = Buffer.concat([state.tail, b]);
9763
+ return;
9764
+ }
9765
+ const keep = CAP - b.length;
9766
+ state.tail = Buffer.concat([state.tail.subarray(state.tail.length - keep), b]);
9767
+ }
9768
+
9769
+ function diag(state) {
9770
+ return {
9771
+ captured_bytes: state.total,
9772
+ tail_b64: state.tail.toString('base64'),
9773
+ };
9774
+ }
9775
+
9776
+ let child;
9777
+ let done = false;
9778
+ let timedOut = false;
9779
+ let exitInfo = { exit: null, signal: null };
9780
+ let descendantTracker = null;
9781
+
9782
+ function killOwned() {
9783
+ if (!child || !child.pid) return;
9784
+ trackDescendants();
9785
+ if (cfg.detached && process.platform !== 'win32') {
9786
+ try { process.kill(-child.pid, 'SIGKILL'); } catch {}
9787
+ }
9788
+ try { child.kill('SIGKILL'); } catch {}
9789
+ if (process.platform === 'linux') {
9790
+ // Deepest-first is friendlier to short-lived process trees. PIDs are still
9791
+ // rechecked by kill(2); races with natural exit are harmless.
9792
+ for (const pid of Array.from(trackedDescendants).reverse()) {
9793
+ try { process.kill(pid, 'SIGKILL'); } catch {}
9794
+ }
9795
+ }
9796
+ }
9797
+
9798
+ function finish(extra) {
9799
+ if (done) return;
9800
+ done = true;
9801
+ if (descendantTracker) clearInterval(descendantTracker);
9802
+ try {
9803
+ // stdout is reserved for the trusted supervisor result. Candidate suite
9804
+ // stdout/stderr are separate pipes and are never inherited here.
9805
+ process.stdout.write(JSON.stringify({
9806
+ ...exitInfo,
9807
+ timedOut,
9808
+ ...extra,
9809
+ stdout: diag(stdout),
9810
+ stderr: diag(stderr),
9811
+ }));
9812
+ } catch {}
9813
+ process.exit(0);
9814
+ }
9815
+
9816
+ try {
9817
+ child = cp.spawn(cfg.executable, cfg.args, {
9818
+ cwd: cfg.cwd,
9819
+ env: process.env,
9820
+ detached: Boolean(cfg.detached) && process.platform !== 'win32',
9821
+ stdio: ['ignore', 'pipe', 'pipe'],
9822
+ });
9823
+ } catch (e) {
9824
+ finish({ error: String(e) });
9825
+ }
9826
+
9827
+ if (child) {
9828
+ if (process.platform === 'linux') {
9829
+ trackDescendants();
9830
+ descendantTracker = setInterval(trackDescendants, 5);
9831
+ descendantTracker.unref();
9832
+ }
9833
+ child.stdout.on('data', (b) => append(stdout, b));
9834
+ child.stderr.on('data', (b) => append(stderr, b));
9835
+
9836
+ const timer = setTimeout(() => {
9837
+ timedOut = true;
9838
+ killOwned();
9839
+ // Backstop if a platform fails to emit close after the forced kill.
9840
+ setTimeout(() => finish({}), 1000).unref();
9841
+ }, Number(cfg.timeoutMs));
9842
+ timer.unref();
9843
+
9844
+ child.on('error', (e) => {
9845
+ clearTimeout(timer);
9846
+ killOwned();
9847
+ finish({ error: String(e) });
9848
+ });
9849
+
9850
+ child.on('exit', (code, signal) => {
9851
+ exitInfo = {
9852
+ exit: code == null ? null : Number(code),
9853
+ signal: signal == null ? null : String(signal),
9854
+ };
9855
+ if (cfg.killGroupOnFinish) killOwned();
9856
+ });
9857
+
9858
+ // close fires after the stdio pipes close, so the retained tails include all
9859
+ // output that was drained before owned descendants were terminated.
9860
+ child.on('close', (code, signal) => {
9861
+ clearTimeout(timer);
9862
+ if (exitInfo.exit === null && code != null) exitInfo.exit = Number(code);
9863
+ if (exitInfo.signal === null && signal != null) exitInfo.signal = String(signal);
9864
+ finish({});
9865
+ });
9866
+ }
9867
+ `;
9868
+ function parseCapturedSupervisorResult(stdoutText) {
9869
+ let raw;
9870
+ try {
9871
+ raw = JSON.parse(stdoutText);
9872
+ } catch {
9873
+ return null;
9874
+ }
9875
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) return null;
9876
+ const diagnostics = {
9877
+ stdout: streamFromRaw(raw.stdout),
9878
+ stderr: streamFromRaw(raw.stderr)
9879
+ };
9880
+ return {
9881
+ exit: typeof raw.exit === "number" ? raw.exit : null,
9882
+ signal: typeof raw.signal === "string" ? raw.signal : null,
9883
+ timedOut: Boolean(raw.timedOut),
9884
+ ...typeof raw.error === "string" && raw.error ? { error: raw.error } : {},
9885
+ diagnostics
9886
+ };
9887
+ }
9888
+ function runCapturedProcessSync(executable, args, opts) {
9889
+ const stateDir = mkdtempSync2(join15(tmpdir2(), "tw-suite-capture-"));
9890
+ const configFile = join15(stateDir, "config.json");
9891
+ const backstop = opts.backstopMs ?? 3e4;
9892
+ try {
9893
+ writeFileSync7(
9894
+ configFile,
9895
+ JSON.stringify({
9896
+ executable,
9897
+ args,
9898
+ cwd: opts.cwd,
9899
+ timeoutMs: opts.timeoutMs,
9900
+ detached: Boolean(opts.detached),
9901
+ killGroupOnFinish: Boolean(opts.killGroupOnFinish),
9902
+ captureBytes: DIAGNOSTIC_TAIL_BYTES
9903
+ }),
9904
+ { mode: 384 }
9905
+ );
9906
+ const supervisor = spawnSync(
9907
+ process.execPath,
9908
+ ["-e", CAPTURE_SUPERVISOR, configFile],
9909
+ {
9910
+ cwd: opts.cwd,
9911
+ env: opts.env,
9912
+ encoding: "utf8",
9913
+ stdio: ["ignore", "pipe", "ignore"],
9914
+ // The trusted result is at most two 16 KiB tails plus small metadata.
9915
+ // A candidate that somehow writes into the supervisor's stdout through
9916
+ // same-UID /proc can only overflow/corrupt this bounded channel and make
9917
+ // parsing fail closed; its suite streams are never inherited here.
9918
+ maxBuffer: 256 * 1024,
9919
+ timeout: opts.timeoutMs + backstop,
9920
+ killSignal: "SIGKILL"
9921
+ }
9922
+ );
9923
+ const parsed = parseCapturedSupervisorResult(supervisor.stdout ?? "");
9924
+ const diagnostics = parsed?.diagnostics ?? emptySuiteDiagnostics();
9925
+ if (!parsed) {
9926
+ const supervisorTimedOut = Boolean(supervisor.error) && supervisor.error.code === "ETIMEDOUT";
9927
+ return {
9928
+ exit: null,
9929
+ signal: supervisor.signal ? String(supervisor.signal) : null,
9930
+ timedOut: supervisorTimedOut,
9931
+ error: supervisor.error?.message ?? "suite capture supervisor did not produce a result",
9932
+ diagnostics
9933
+ };
9934
+ }
9935
+ return parsed;
9936
+ } finally {
9937
+ rmSync3(stateDir, { recursive: true, force: true });
9938
+ }
9939
+ }
9940
+
9941
+ // src/verifier-backend.ts
9545
9942
  var DEFAULT_CONTAINER_RESOURCE_LIMITS = Object.freeze({
9546
9943
  memoryBytes: 2 * 1024 * 1024 * 1024,
9547
9944
  // Docker interprets memory-swap == memory as no additional swap allowance.
@@ -9584,7 +9981,7 @@ function replaceableByCaller(path) {
9584
9981
  function findExecutable(name) {
9585
9982
  for (const raw of (process.env.PATH ?? "").split(delimiter2)) {
9586
9983
  if (!raw) continue;
9587
- const candidate = join15(raw, name);
9984
+ const candidate = join16(raw, name);
9588
9985
  try {
9589
9986
  accessSync(candidate, constants2.X_OK);
9590
9987
  return realpathSync4(candidate);
@@ -9594,7 +9991,7 @@ function findExecutable(name) {
9594
9991
  return null;
9595
9992
  }
9596
9993
  function fileSha256(path) {
9597
- return createHash9("sha256").update(readFileSync14(path)).digest("hex");
9994
+ return createHash9("sha256").update(readFileSync15(path)).digest("hex");
9598
9995
  }
9599
9996
  function engineClientEnv() {
9600
9997
  return {
@@ -9609,7 +10006,7 @@ function dockerArgs(host, args) {
9609
10006
  }
9610
10007
  function dockerAvailable(enginePath, host) {
9611
10008
  try {
9612
- const r = spawnSync(enginePath, dockerArgs(host, ["version"]), {
10009
+ const r = spawnSync2(enginePath, dockerArgs(host, ["version"]), {
9613
10010
  stdio: "ignore",
9614
10011
  timeout: 5e3,
9615
10012
  killSignal: "SIGKILL",
@@ -9637,7 +10034,7 @@ function declaredImageVolumePaths(metadata) {
9637
10034
  }
9638
10035
  function inspectImageMetadata(enginePath, host, image) {
9639
10036
  try {
9640
- const r = spawnSync(enginePath, dockerArgs(host, ["image", "inspect", image]), {
10037
+ const r = spawnSync2(enginePath, dockerArgs(host, ["image", "inspect", image]), {
9641
10038
  encoding: "utf8",
9642
10039
  stdio: ["ignore", "pipe", "ignore"],
9643
10040
  timeout: 1e4,
@@ -9812,7 +10209,7 @@ function verifierBackendSummary(backend) {
9812
10209
  function cleanupContainer(backend, name) {
9813
10210
  if (!backend.enginePath || !backend.daemonHost || !engineStable(backend)) return;
9814
10211
  try {
9815
- spawnSync(backend.enginePath, dockerArgs(backend.daemonHost, ["rm", "-f", "-v", name]), {
10212
+ spawnSync2(backend.enginePath, dockerArgs(backend.daemonHost, ["rm", "-f", "-v", name]), {
9816
10213
  stdio: "ignore",
9817
10214
  timeout: 1e4,
9818
10215
  killSignal: "SIGKILL",
@@ -9923,29 +10320,45 @@ function runContainerStage(backend, dir, command, budgetSecs) {
9923
10320
  resources: backend.resources ?? DEFAULT_CONTAINER_RESOURCE_LIMITS
9924
10321
  });
9925
10322
  try {
9926
- const r = spawnSync(
10323
+ const r = runCapturedProcessSync(
9927
10324
  backend.enginePath,
9928
10325
  dockerArgs(backend.daemonHost, args),
9929
10326
  {
9930
- stdio: "ignore",
9931
- timeout: budgetSecs * 1e3,
9932
- killSignal: "SIGKILL",
9933
- env: engineClientEnv()
10327
+ cwd: dirname6(backend.enginePath),
10328
+ env: engineClientEnv(),
10329
+ timeoutMs: budgetSecs * 1e3,
10330
+ backstopMs: 3e4
9934
10331
  }
9935
10332
  );
9936
10333
  const secs = Math.round((Date.now() - t0) / 1e3);
10334
+ if (r.timedOut) {
10335
+ cleanupContainer(backend, name);
10336
+ return {
10337
+ exit: null,
10338
+ secs,
10339
+ failure: "budget",
10340
+ reason: "verifier stage exceeded its budget",
10341
+ diagnostics: r.diagnostics
10342
+ };
10343
+ }
9937
10344
  if (r.error) {
9938
10345
  cleanupContainer(backend, name);
9939
- const code2 = r.error.code;
9940
- return code2 === "ETIMEDOUT" ? { exit: null, secs, failure: "budget", reason: "verifier stage exceeded its budget" } : { exit: null, secs, failure: "backend", reason: `Docker client failed: ${r.error.message}` };
10346
+ return {
10347
+ exit: null,
10348
+ secs,
10349
+ failure: "backend",
10350
+ reason: `Docker client failed: ${r.error}`,
10351
+ diagnostics: r.diagnostics
10352
+ };
9941
10353
  }
9942
- if (r.status === null) {
10354
+ if (r.exit === null) {
9943
10355
  cleanupContainer(backend, name);
9944
10356
  return {
9945
10357
  exit: null,
9946
10358
  secs,
9947
10359
  failure: "backend",
9948
- reason: `Docker client did not return a status${r.signal ? ` (signal ${r.signal})` : ""}`
10360
+ reason: `Docker client did not return a status${r.signal ? ` (signal ${r.signal})` : ""}`,
10361
+ diagnostics: r.diagnostics
9949
10362
  };
9950
10363
  }
9951
10364
  if (!engineStable(backend)) {
@@ -9953,10 +10366,11 @@ function runContainerStage(backend, dir, command, budgetSecs) {
9953
10366
  exit: null,
9954
10367
  secs,
9955
10368
  failure: "backend",
9956
- reason: "Docker engine identity changed before verifier result inspection"
10369
+ reason: "Docker engine identity changed before verifier result inspection",
10370
+ diagnostics: r.diagnostics
9957
10371
  };
9958
10372
  }
9959
- const inspected = spawnSync(
10373
+ const inspected = spawnSync2(
9960
10374
  backend.enginePath,
9961
10375
  dockerArgs(backend.daemonHost, ["inspect", "--format", "{{json .State}}", name]),
9962
10376
  {
@@ -9976,7 +10390,10 @@ function runContainerStage(backend, dir, command, budgetSecs) {
9976
10390
  }
9977
10391
  }
9978
10392
  cleanupContainer(backend, name);
9979
- return containerStateResult(state, secs);
10393
+ return {
10394
+ ...containerStateResult(state, secs),
10395
+ diagnostics: r.diagnostics
10396
+ };
9980
10397
  } catch (e) {
9981
10398
  cleanupContainer(backend, name);
9982
10399
  return {
@@ -10081,7 +10498,7 @@ function resolveBase(base, cwd) {
10081
10498
  }
10082
10499
  }
10083
10500
  function baseIsAncestorOfHead(base, cwd) {
10084
- const r = spawnSync2("git", ["merge-base", "--is-ancestor", base, "HEAD"], { cwd, env: trustedGitEnv() });
10501
+ const r = spawnSync3("git", ["merge-base", "--is-ancestor", base, "HEAD"], { cwd, env: trustedGitEnv() });
10085
10502
  return r.status === 0;
10086
10503
  }
10087
10504
  function inside2(root, path) {
@@ -10105,7 +10522,7 @@ function safeSymlink(target, out2, root, label) {
10105
10522
  if (rootedLinkTarget(target) || !inside2(root, resolve7(dirname7(out2), target))) {
10106
10523
  throw linkEscape(label, target);
10107
10524
  }
10108
- rmSync3(out2, { force: true });
10525
+ rmSync4(out2, { force: true });
10109
10526
  symlinkSync(target, out2);
10110
10527
  }
10111
10528
  function validateSymlinkGraph(target, out2, root, dependencyRoot, label) {
@@ -10128,7 +10545,7 @@ function validateSymlinkGraph(target, out2, root, dependencyRoot, label) {
10128
10545
  continue;
10129
10546
  }
10130
10547
  const base = domain === "tree" ? root : dependencyRoot;
10131
- const current = join16(base, ...stack);
10548
+ const current = join17(base, ...stack);
10132
10549
  let st;
10133
10550
  try {
10134
10551
  st = lstatSync5(current);
@@ -10164,7 +10581,7 @@ function rejectLinkedParent(cwd, rel) {
10164
10581
  const parts = rel.split("/").slice(0, -1);
10165
10582
  let current = cwd;
10166
10583
  for (const part of parts) {
10167
- current = join16(current, part);
10584
+ current = join17(current, part);
10168
10585
  let st;
10169
10586
  try {
10170
10587
  st = lstatSync5(current);
@@ -10180,7 +10597,7 @@ function materialize(cwd, dest, dependencyRoot) {
10180
10597
  const listed = git2(["ls-files", "-z", "--cached", "--others", "--exclude-standard"], cwd).split("\0").filter(Boolean);
10181
10598
  const links = [];
10182
10599
  for (const rel of listed) {
10183
- const src = join16(cwd, rel);
10600
+ const src = join17(cwd, rel);
10184
10601
  rejectLinkedParent(cwd, rel);
10185
10602
  let st;
10186
10603
  try {
@@ -10188,7 +10605,7 @@ function materialize(cwd, dest, dependencyRoot) {
10188
10605
  } catch {
10189
10606
  continue;
10190
10607
  }
10191
- const out2 = join16(dest, rel);
10608
+ const out2 = join17(dest, rel);
10192
10609
  mkdirSync6(dirname7(out2), { recursive: true });
10193
10610
  dropSymlink(out2);
10194
10611
  if (st.isSymbolicLink()) {
@@ -10205,11 +10622,11 @@ function materialize(cwd, dest, dependencyRoot) {
10205
10622
  for (const link of links) {
10206
10623
  validateSymlinkGraph(link.target, link.out, dest, dependencyRoot, link.rel);
10207
10624
  }
10208
- if (dependencyRoot) symlinkSync(dependencyRoot, join16(dest, "node_modules"), "dir");
10625
+ if (dependencyRoot) symlinkSync(dependencyRoot, join17(dest, "node_modules"), "dir");
10209
10626
  }
10210
10627
  function dropSymlink(p) {
10211
10628
  try {
10212
- if (lstatSync5(p).isSymbolicLink()) rmSync3(p, { force: true });
10629
+ if (lstatSync5(p).isSymbolicLink()) rmSync4(p, { force: true });
10213
10630
  } catch {
10214
10631
  }
10215
10632
  }
@@ -10238,11 +10655,11 @@ function overlayDigest(dest, paths) {
10238
10655
  for (const rel of [...paths].sort()) {
10239
10656
  h.update(rel);
10240
10657
  h.update("\0");
10241
- const p = join16(dest, rel);
10658
+ const p = join17(dest, rel);
10242
10659
  try {
10243
10660
  const st = lstatSync5(p);
10244
10661
  h.update(String(st.mode));
10245
- h.update(st.isSymbolicLink() ? readlinkSync3(p) : readFileSync15(p));
10662
+ h.update(st.isSymbolicLink() ? readlinkSync3(p) : readFileSync16(p));
10246
10663
  } catch {
10247
10664
  h.update("<gone>");
10248
10665
  }
@@ -10266,15 +10683,15 @@ function overlayPristine(cwd, base, dest, policy, cmd, dependencyRoot) {
10266
10683
  throw new Error(`the base's ${e.path} is a submodule; the pristine copy cannot reproduce it`);
10267
10684
  }
10268
10685
  const content = execFileSync9("git", ["show", `${base}:${e.path}`], { cwd, maxBuffer: 1 << 28, env: trustedGitEnv() });
10269
- const out2 = join16(dest, e.path);
10686
+ const out2 = join17(dest, e.path);
10270
10687
  mkdirSync6(dirname7(out2), { recursive: true });
10271
- rmSync3(out2, { force: true });
10688
+ rmSync4(out2, { force: true });
10272
10689
  if (e.mode === "120000") {
10273
10690
  const target = content.toString("utf8");
10274
10691
  safeSymlink(target, out2, dest, `the base's ${e.path}`);
10275
10692
  restoredLinks.push({ path: e.path, out: out2, target });
10276
10693
  } else {
10277
- writeFileSync7(out2, content);
10694
+ writeFileSync8(out2, content);
10278
10695
  chmodSync2(out2, parseInt(e.mode.slice(-4), 8) & 511);
10279
10696
  }
10280
10697
  restored.push(e.path);
@@ -10283,7 +10700,7 @@ function overlayPristine(cwd, base, dest, policy, cmd, dependencyRoot) {
10283
10700
  const inCopy = git2(["ls-files", "-z", "--cached", "--others", "--exclude-standard"], cwd).split("\0").filter(Boolean);
10284
10701
  for (const rel of inCopy) {
10285
10702
  if (!isOverlay(rel) || baseProtected.has(rel)) continue;
10286
- rmSync3(join16(dest, rel), { force: true });
10703
+ rmSync4(join17(dest, rel), { force: true });
10287
10704
  removed++;
10288
10705
  }
10289
10706
  for (const link of restoredLinks) {
@@ -10291,27 +10708,6 @@ function overlayPristine(cwd, base, dest, policy, cmd, dependencyRoot) {
10291
10708
  }
10292
10709
  return { restored, removed };
10293
10710
  }
10294
- var SUPERVISOR = `
10295
- const cp = require('node:child_process');
10296
- const fs = require('node:fs');
10297
- const [cmd, budgetMs, outFile] = process.argv.slice(1);
10298
- const child = cp.spawn('sh', ['-c', cmd], { stdio: 'ignore', detached: process.platform !== 'win32' });
10299
- const killGroup = () => {
10300
- try { process.kill(-child.pid, 'SIGKILL'); } catch {}
10301
- try { child.kill('SIGKILL'); } catch {}
10302
- };
10303
- let done = false;
10304
- const finish = (r) => {
10305
- if (done) return;
10306
- done = true;
10307
- killGroup();
10308
- fs.writeFileSync(outFile, JSON.stringify(r));
10309
- process.exit(0);
10310
- };
10311
- const timer = setTimeout(() => finish({ timedOut: true }), Number(budgetMs));
10312
- child.on('error', (e) => { clearTimeout(timer); finish({ error: String(e) }); });
10313
- child.on('exit', (code, signal) => { clearTimeout(timer); finish({ exit: code, signal }); });
10314
- `;
10315
10711
  var DROPPED_ENV = /* @__PURE__ */ new Set([
10316
10712
  "NODE_OPTIONS",
10317
10713
  "NODE_PATH",
@@ -10328,9 +10724,9 @@ function suiteEnv(scratch) {
10328
10724
  if (DROPPED_ENV.has(k) || PINNED_NPM.test(k)) continue;
10329
10725
  env[k] = v;
10330
10726
  }
10331
- const userRc = join16(scratch, "npmrc-user");
10332
- const globalRc = join16(scratch, "npmrc-global");
10333
- for (const f of [userRc, globalRc]) writeFileSync7(f, "", { mode: 292 });
10727
+ const userRc = join17(scratch, "npmrc-user");
10728
+ const globalRc = join17(scratch, "npmrc-global");
10729
+ for (const f of [userRc, globalRc]) writeFileSync8(f, "", { mode: 292 });
10334
10730
  env.npm_config_userconfig = userRc;
10335
10731
  env.npm_config_globalconfig = globalRc;
10336
10732
  env.npm_config_node_options = " ";
@@ -10339,31 +10735,75 @@ function suiteEnv(scratch) {
10339
10735
  }
10340
10736
  function runLocalSuite(dir, cmd, budgetSecs) {
10341
10737
  const t0 = Date.now();
10342
- const outDir = mkdtempSync2(join16(tmpdir2(), "tw-verify-run-"));
10343
- const outFile = join16(outDir, "result.json");
10738
+ const scratch = mkdtempSync3(join17(tmpdir3(), "tw-verify-run-"));
10344
10739
  try {
10345
- spawnSync2(process.execPath, ["-e", SUPERVISOR, cmd, String(budgetSecs * 1e3), outFile], {
10740
+ const r = runCapturedProcessSync("sh", ["-c", cmd], {
10346
10741
  cwd: dir,
10347
- env: suiteEnv(outDir),
10348
- stdio: "ignore",
10349
- timeout: budgetSecs * 1e3 + 3e4,
10350
- // backstop only; the supervisor enforces the budget
10351
- killSignal: "SIGKILL"
10742
+ env: suiteEnv(scratch),
10743
+ timeoutMs: budgetSecs * 1e3,
10744
+ detached: process.platform !== "win32",
10745
+ killGroupOnFinish: true,
10746
+ backstopMs: 3e4
10352
10747
  });
10353
10748
  const secs = Math.round((Date.now() - t0) / 1e3);
10354
- let r;
10355
- try {
10356
- r = JSON.parse(readFileSync15(outFile, "utf8"));
10357
- } catch {
10358
- return { exit: null, secs };
10749
+ if (r.timedOut) {
10750
+ return {
10751
+ exit: null,
10752
+ secs,
10753
+ failure: "budget",
10754
+ reason: "verifier stage exceeded its budget",
10755
+ diagnostics: r.diagnostics
10756
+ };
10757
+ }
10758
+ if (r.error) {
10759
+ return {
10760
+ exit: null,
10761
+ secs,
10762
+ failure: "backend",
10763
+ reason: r.error,
10764
+ diagnostics: r.diagnostics
10765
+ };
10359
10766
  }
10360
- if (r.timedOut || r.error) return { exit: null, secs };
10361
- if (r.exit === null || r.exit === void 0) return { exit: null, secs };
10362
- return { exit: r.exit, secs };
10767
+ if (r.exit === null) {
10768
+ return {
10769
+ exit: null,
10770
+ secs,
10771
+ failure: "backend",
10772
+ reason: `verifier suite did not return an exit code${r.signal ? ` (signal ${r.signal})` : ""}`,
10773
+ diagnostics: r.diagnostics
10774
+ };
10775
+ }
10776
+ return { exit: r.exit, secs, diagnostics: r.diagnostics };
10363
10777
  } finally {
10364
- rmSync3(outDir, { recursive: true, force: true });
10778
+ rmSync4(scratch, { recursive: true, force: true });
10365
10779
  }
10366
10780
  }
10781
+ function diagnosticsJson(diagnostics, includeTail) {
10782
+ if (!diagnostics) return void 0;
10783
+ const stream = (d) => ({
10784
+ captured_bytes: d.captured_bytes,
10785
+ retained_bytes: d.retained_bytes,
10786
+ truncated: d.truncated,
10787
+ ...includeTail && d.captured_bytes > 0 ? { tail: d.tail } : {}
10788
+ });
10789
+ return {
10790
+ stdout: stream(diagnostics.stdout),
10791
+ stderr: stream(diagnostics.stderr)
10792
+ };
10793
+ }
10794
+ function stageJson(result) {
10795
+ const includeTail = result.exit !== 0 || result.failure !== void 0;
10796
+ return {
10797
+ exit: result.exit,
10798
+ secs: result.secs,
10799
+ ...result.reason ? { reason: result.reason } : {},
10800
+ ...result.diagnostics ? { diagnostics: diagnosticsJson(result.diagnostics, includeTail) } : {}
10801
+ };
10802
+ }
10803
+ function renderStageDiagnostics(out2, stage, result) {
10804
+ if (!result.diagnostics) return;
10805
+ for (const line of diagnosticLines(stage, result.diagnostics)) out2(line);
10806
+ }
10367
10807
  function runVerify(opts) {
10368
10808
  const cwd = opts.cwd ?? process.cwd();
10369
10809
  const out2 = (s) => void process.stdout.write(s + "\n");
@@ -10445,11 +10885,11 @@ function runVerify(opts) {
10445
10885
  out2(`verify: cannot resolve base rev "${opts.base ?? "HEAD"}" \u2014 failing closed`);
10446
10886
  return 2;
10447
10887
  }
10448
- const visRoot = mkdtempSync2(join16(tmpdir2(), "tw-verify-vis-"));
10449
- const visDir = join16(visRoot, "t");
10888
+ const visRoot = mkdtempSync3(join17(tmpdir3(), "tw-verify-vis-"));
10889
+ const visDir = join17(visRoot, "t");
10450
10890
  const cleanup = (dirs) => {
10451
10891
  if (opts.keep) return;
10452
- for (const d of dirs) rmSync3(d, { recursive: true, force: true });
10892
+ for (const d of dirs) rmSync4(d, { recursive: true, force: true });
10453
10893
  };
10454
10894
  const protectedIgnored = (rel) => isProtected(rel, policy);
10455
10895
  const treeBefore = treeFingerprint(cwd, protectedIgnored);
@@ -10479,14 +10919,16 @@ function runVerify(opts) {
10479
10919
  stage: "visible",
10480
10920
  ...visible.resource ? { resource: visible.resource } : {},
10481
10921
  detail: visible.reason,
10922
+ diagnostics: diagnosticsJson(visible.diagnostics, true),
10482
10923
  verifier_backend: backendReport(),
10483
10924
  oracle_assurance: oracleAssuranceReport()
10484
10925
  }));
10485
10926
  } else {
10486
10927
  out2(
10487
- exhausted ? "verify: isolated verifier resource envelope was exhausted during the visible stage \u2014 failing closed" : "verify: isolated verifier backend failed while running the visible stage \u2014 failing closed"
10928
+ exhausted ? "verify: isolated verifier resource envelope was exhausted during the visible stage \u2014 failing closed" : "verify: verifier execution backend failed while running the visible stage \u2014 failing closed"
10488
10929
  );
10489
10930
  if (visible.reason) out2("verify: " + visible.reason);
10931
+ renderStageDiagnostics(out2, "visible", visible);
10490
10932
  }
10491
10933
  return 2;
10492
10934
  }
@@ -10503,8 +10945,8 @@ function runVerify(opts) {
10503
10945
  out2((dependencyAfterVisible.reason ?? "dependency identity changed") + ". Failing closed, not open.");
10504
10946
  return 2;
10505
10947
  }
10506
- const priRoot = mkdtempSync2(join16(tmpdir2(), "tw-verify-pri-"));
10507
- const priDir = join16(priRoot, "t");
10948
+ const priRoot = mkdtempSync3(join17(tmpdir3(), "tw-verify-pri-"));
10949
+ const priDir = join17(priRoot, "t");
10508
10950
  let restored = [];
10509
10951
  let removedAdded = 0;
10510
10952
  try {
@@ -10535,14 +10977,16 @@ function runVerify(opts) {
10535
10977
  stage: "pristine",
10536
10978
  ...pristine.resource ? { resource: pristine.resource } : {},
10537
10979
  detail: pristine.reason,
10980
+ diagnostics: diagnosticsJson(pristine.diagnostics, true),
10538
10981
  verifier_backend: backendReport(),
10539
10982
  oracle_assurance: oracleAssuranceReport()
10540
10983
  }));
10541
10984
  } else {
10542
10985
  out2(
10543
- exhausted ? "verify: isolated verifier resource envelope was exhausted during the pristine stage \u2014 failing closed" : "verify: isolated verifier backend failed while running the pristine stage \u2014 failing closed"
10986
+ exhausted ? "verify: isolated verifier resource envelope was exhausted during the pristine stage \u2014 failing closed" : "verify: verifier execution backend failed while running the pristine stage \u2014 failing closed"
10544
10987
  );
10545
10988
  if (pristine.reason) out2("verify: " + pristine.reason);
10989
+ renderStageDiagnostics(out2, "pristine", pristine);
10546
10990
  }
10547
10991
  return 2;
10548
10992
  }
@@ -10584,8 +11028,8 @@ function runVerify(opts) {
10584
11028
  base,
10585
11029
  command: cmd,
10586
11030
  budget_secs: budget,
10587
- visible: { exit: visible.exit, secs: visible.secs },
10588
- pristine: { exit: pristine.exit, secs: pristine.secs },
11031
+ visible: stageJson(visible),
11032
+ pristine: stageJson(pristine),
10589
11033
  protected_restored: restored.length,
10590
11034
  added_protected_removed: removedAdded,
10591
11035
  verifier_backend: backendReport(),
@@ -10610,6 +11054,14 @@ function runVerify(opts) {
10610
11054
  out2(
10611
11055
  dependencyEnvironment ? `dependency environment: ${dependencyEnvironmentSummary(dependencyEnvironment)}` + (opts.allowDepDrift && dependencyEnvironment.status === "unattestable" ? " (operator override)" : "") : `dependency environment: verifier-owned by ${verifierBackend.image ?? "isolated image"}`
10612
11056
  );
11057
+ if (verdict2 !== "VERIFIED") {
11058
+ if (visible.exit !== 0 || visible.failure !== void 0) {
11059
+ renderStageDiagnostics(out2, "visible", visible);
11060
+ }
11061
+ if (pristine.exit !== 0 || pristine.failure !== void 0) {
11062
+ renderStageDiagnostics(out2, "pristine", pristine);
11063
+ }
11064
+ }
10613
11065
  if (signedOff)
10614
11066
  out2(
10615
11067
  `masked failure cleared by out-of-band approval (tamperward:allow:${signedOff}): a reviewer accepted that the original suite no longer applies to this change. Exit 0.`
@@ -10654,11 +11106,11 @@ function parseVerify(args) {
10654
11106
  }
10655
11107
 
10656
11108
  // src/cli/run.ts
10657
- import { execFileSync as execFileSync10, spawn, spawnSync as spawnSync3 } from "node:child_process";
11109
+ import { execFileSync as execFileSync10, spawn, spawnSync as spawnSync4 } from "node:child_process";
10658
11110
  import { randomBytes } from "node:crypto";
10659
- import { mkdtempSync as mkdtempSync3, readdirSync as readdirSync5, readFileSync as readFileSync16, readlinkSync as readlinkSync4, realpathSync as realpathSync6, rmSync as rmSync4 } from "node:fs";
10660
- import { tmpdir as tmpdir3 } from "node:os";
10661
- import { join as join17, resolve as resolve8 } from "node:path";
11111
+ import { mkdtempSync as mkdtempSync4, readdirSync as readdirSync5, readFileSync as readFileSync17, readlinkSync as readlinkSync4, realpathSync as realpathSync6, rmSync as rmSync5 } from "node:fs";
11112
+ import { tmpdir as tmpdir4 } from "node:os";
11113
+ import { join as join18, resolve as resolve8 } from "node:path";
10662
11114
  var out = (s) => process.stdout.write(s + "\n");
10663
11115
  var err2 = (s) => process.stderr.write(s + "\n");
10664
11116
  var AGENT_SUPERVISOR = String.raw`
@@ -10778,7 +11230,7 @@ function linuxDescendantPids(rootPid) {
10778
11230
  for (const name of names) {
10779
11231
  const pid = Number(name);
10780
11232
  try {
10781
- const stat = readFileSync16(`/proc/${name}/stat`, "utf8");
11233
+ const stat = readFileSync17(`/proc/${name}/stat`, "utf8");
10782
11234
  const fields = stat.slice(stat.lastIndexOf(")") + 2).split(" ");
10783
11235
  const ppid = Number(fields[1]);
10784
11236
  if (!Number.isFinite(ppid)) continue;
@@ -10806,7 +11258,7 @@ function killAgentTree(pid) {
10806
11258
  if (!Number.isInteger(pid) || pid <= 1) return;
10807
11259
  if (process.platform === "win32") {
10808
11260
  try {
10809
- spawnSync3("taskkill", ["/pid", String(pid), "/t", "/f"], { stdio: "ignore", timeout: 5e3 });
11261
+ spawnSync4("taskkill", ["/pid", String(pid), "/t", "/f"], { stdio: "ignore", timeout: 5e3 });
10810
11262
  } catch {
10811
11263
  }
10812
11264
  return;
@@ -10828,11 +11280,11 @@ function killAgentTree(pid) {
10828
11280
  }
10829
11281
  }
10830
11282
  function runAgentWithBudget(argv2, cwd, budgetSecs) {
10831
- const stateDir = mkdtempSync3(join17(tmpdir3(), "tw-agent-supervisor-"));
10832
- const resultFile = join17(stateDir, "result.json");
11283
+ const stateDir = mkdtempSync4(join18(tmpdir4(), "tw-agent-supervisor-"));
11284
+ const resultFile = join18(stateDir, "result.json");
10833
11285
  let supervisorTimedOut = false;
10834
11286
  try {
10835
- const supervisor = spawnSync3(
11287
+ const supervisor = spawnSync4(
10836
11288
  process.execPath,
10837
11289
  ["-e", AGENT_SUPERVISOR, resultFile, String(budgetSecs * 1e3), ...argv2],
10838
11290
  {
@@ -10847,7 +11299,7 @@ function runAgentWithBudget(argv2, cwd, budgetSecs) {
10847
11299
  supervisorTimedOut = Boolean(supervisor.error) && supervisor.error.code === "ETIMEDOUT";
10848
11300
  let state = {};
10849
11301
  try {
10850
- state = JSON.parse(readFileSync16(resultFile, "utf8"));
11302
+ state = JSON.parse(readFileSync17(resultFile, "utf8"));
10851
11303
  } catch {
10852
11304
  }
10853
11305
  if (supervisorTimedOut || state.timedOut) {
@@ -10874,7 +11326,7 @@ function runAgentWithBudget(argv2, cwd, budgetSecs) {
10874
11326
  failure: state.failure ?? "agent supervisor did not produce a final status"
10875
11327
  };
10876
11328
  } finally {
10877
- rmSync4(stateDir, { recursive: true, force: true });
11329
+ rmSync5(stateDir, { recursive: true, force: true });
10878
11330
  }
10879
11331
  }
10880
11332
  var waitCell = new Int32Array(new SharedArrayBuffer(4));
@@ -10891,8 +11343,8 @@ function pidAlive2(pid) {
10891
11343
  }
10892
11344
  }
10893
11345
  function supervisedObserverLog(cwd) {
10894
- const gd = gitDir(cwd) ?? join17(cwd, ".git");
10895
- return join17(
11346
+ const gd = gitDir(cwd) ?? join18(cwd, ".git");
11347
+ return join18(
10896
11348
  gd,
10897
11349
  "tamperward",
10898
11350
  `run-observer-${process.pid}-${Date.now()}-${randomBytes(4).toString("hex")}.jsonl`
@@ -11009,7 +11461,7 @@ function git3(args, cwd) {
11009
11461
  }
11010
11462
  function nowTicks() {
11011
11463
  try {
11012
- return Math.floor(parseFloat(readFileSync16("/proc/uptime", "utf8").split(" ")[0]) * 100);
11464
+ return Math.floor(parseFloat(readFileSync17("/proc/uptime", "utf8").split(" ")[0]) * 100);
11013
11465
  } catch {
11014
11466
  return Number.POSITIVE_INFINITY;
11015
11467
  }
@@ -11032,7 +11484,7 @@ function survivorsHoldingTree(cwd, spawnedAfterTicks) {
11032
11484
  const n = Number(pid);
11033
11485
  if (n === process.pid) continue;
11034
11486
  try {
11035
- const stat = readFileSync16(`/proc/${pid}/stat`, "utf8");
11487
+ const stat = readFileSync17(`/proc/${pid}/stat`, "utf8");
11036
11488
  const startTicks = Number(stat.slice(stat.lastIndexOf(")") + 2).split(" ")[19]);
11037
11489
  if (!Number.isFinite(startTicks) || startTicks < spawnedAfterTicks) continue;
11038
11490
  } catch {
@@ -11137,7 +11589,7 @@ function runEnvelope(opts) {
11137
11589
  const rewritesBefore = objectRewriteState(cwd);
11138
11590
  const spawnTicks = nowTicks();
11139
11591
  const agentRun = opts.agentBudget === void 0 ? (() => {
11140
- const agent = spawnSync3(opts.argv[0], opts.argv.slice(1), { cwd, stdio: "inherit" });
11592
+ const agent = spawnSync4(opts.argv[0], opts.argv.slice(1), { cwd, stdio: "inherit" });
11141
11593
  return {
11142
11594
  exit: agent.status ?? 1,
11143
11595
  timedOut: false,
@@ -11177,7 +11629,7 @@ tamperward run \u2014 agent exit ${agentExit}; DEPENDENCY_DRIFT \u2192 ENFORCEME
11177
11629
  err2("(--allow-dep-drift: proceeding anyway, on the operator's judgement.)");
11178
11630
  }
11179
11631
  const head = git3(["rev-parse", "HEAD"], cwd).trim();
11180
- const isAncestor = spawnSync3("git", ["merge-base", "--is-ancestor", base, head], { cwd, env: trustedGitEnv() });
11632
+ const isAncestor = spawnSync4("git", ["merge-base", "--is-ancestor", base, head], { cwd, env: trustedGitEnv() });
11181
11633
  if (isAncestor.status !== 0) {
11182
11634
  err2(`tamperward run: HEAD ${head.slice(0, 10)} is not a descendant of the trusted base ${base.slice(0, 10)} \u2014`);
11183
11635
  err2("the agent rewrote history out from under the anchor. That is the finding.");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tamperward",
3
- "version": "2.15.2",
3
+ "version": "2.16.1",
4
4
  "description": "The deterministic agent-integrity gate. One ruleset, evaluated on the actual diff/commands as a verdict, enforced everywhere a change can be made.",
5
5
  "license": "Apache-2.0",
6
6
  "author": "hexrift",