tamperward 2.15.1 → 2.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +25 -0
  2. package/dist/cli/index.js +724 -136
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -167,6 +167,19 @@ observer down deterministically. Observer health remains advisory; transient fin
167
167
  remain warnings by default and affect the envelope only when the operator explicitly
168
168
  sets `TAMPERWARD_TRANSIENT=block`.
169
169
 
170
+ Since **2.15.2**, Stop-sweep event consumption uses the saved cursor as a real byte
171
+ offset: it performs positioned reads of only new JSONL bytes instead of decoding the
172
+ whole historical log on every turn. Each physical read is capped at **4 MiB**, and one
173
+ authority decision drains at most **16 MiB** in bounded chunks. Only complete
174
+ newline-terminated records advance the cursor, so a torn final watcher write is replayed
175
+ after completion instead of being lost. Stop commits the cursor only after the entire
176
+ bounded telemetry tail has been parsed and classified; malformed records, an oversized
177
+ single record, a torn tail, or telemetry beyond the 16 MiB aggregate ceiling blocks Stop
178
+ and retains the previous cursor rather than certifying unjudged bytes. Supervised
179
+ `run --observe-transients` keeps observer evidence advisory by default, but when
180
+ `TAMPERWARD_TRANSIENT=block` is explicitly enabled, any unclassified observer tail
181
+ also fails the envelope closed.
182
+
170
183
  After the runtime exits its exit code is treated as untrusted, and the envelope
171
184
  checks that post-agent `HEAD` still descends from the entry commit; the committed
172
185
  changes over `entry...HEAD`; staged, unstaged and untracked non-ignored worktree
@@ -356,6 +369,18 @@ restored `"test": "sh scripts/test.sh"` will happily call a script nothing
356
369
  restored. It bounds the class rather than closing it — see
357
370
  [the threat model](./docs/THREAT-MODEL-pristine-run.md).
358
371
 
372
+ From **2.16.0**, verifier suite output is diagnostic evidence instead of discarded
373
+ noise. Both visible and pristine stages continuously drain stdout/stderr through a
374
+ trusted supervisor, retain only the final **16 KiB per stream**, and count the total
375
+ bytes observed. Structured output exposes this under
376
+ `visible.diagnostics` / `pristine.diagnostics` with
377
+ `captured_bytes`, `retained_bytes`, `truncated`, and a bounded `tail` on a
378
+ failed stage. Default human output remains quiet on success. On failure, retained
379
+ diagnostics are rendered with every line prefixed and terminal/control characters
380
+ escaped, so candidate output cannot become ANSI control traffic or a GitHub
381
+ `::command::`. These bytes are evidence produced by candidate code; they never own
382
+ the verifier verdict.
383
+
359
384
  The four primitives:
360
385
 
361
386
  ```bash
package/dist/cli/index.js CHANGED
@@ -6829,7 +6829,7 @@ function runCheck(opts) {
6829
6829
  }
6830
6830
 
6831
6831
  // src/cli/hook.ts
6832
- import { readFileSync as readFileSync11, appendFileSync as appendFileSync4, writeFileSync as writeFileSync5, existsSync as existsSync8 } from "node:fs";
6832
+ import { readFileSync as readFileSync10, appendFileSync as appendFileSync4, writeFileSync as writeFileSync5, existsSync as existsSync8 } from "node:fs";
6833
6833
  import { createHash as createHash5 } from "node:crypto";
6834
6834
 
6835
6835
  // src/adapters/claude/deny.ts
@@ -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();
@@ -7253,26 +7262,200 @@ function runWatch(args) {
7253
7262
  }
7254
7263
 
7255
7264
  // src/detectors/fs-events.ts
7256
- import { readFileSync as readFileSync10, statSync } from "node:fs";
7265
+ import { closeSync as closeSync2, openSync as openSync2, readSync as readSync2, statSync } from "node:fs";
7257
7266
  var RULE11 = "transient-protected-mutation";
7258
- function readEvents(log, offset) {
7267
+ var MAX_EVENT_READ_BYTES = 4 * 1024 * 1024;
7268
+ var MAX_EVENT_SWEEP_BYTES = 16 * 1024 * 1024;
7269
+ var DEFAULT_EVENT_READ_IO = {
7270
+ read: (fd, buffer, bufferOffset, length, position) => readSync2(fd, buffer, bufferOffset, length, position)
7271
+ };
7272
+ function readEvents(log, offset, maxBytes = MAX_EVENT_READ_BYTES, io = DEFAULT_EVENT_READ_IO) {
7259
7273
  let size = 0;
7260
7274
  try {
7261
7275
  size = statSync(log).size;
7262
7276
  } catch {
7263
- return { events: [], newOffset: offset };
7277
+ return {
7278
+ events: [],
7279
+ newOffset: Math.max(0, Number.isFinite(offset) ? Math.floor(offset) : 0),
7280
+ bytesRead: 0,
7281
+ limitReached: false,
7282
+ malformedLines: 0,
7283
+ incompleteTail: false
7284
+ };
7285
+ }
7286
+ let start = Number.isFinite(offset) ? Math.max(0, Math.floor(offset)) : 0;
7287
+ if (start > size) start = 0;
7288
+ if (size <= start) {
7289
+ return {
7290
+ events: [],
7291
+ newOffset: start,
7292
+ bytesRead: 0,
7293
+ limitReached: false,
7294
+ malformedLines: 0,
7295
+ incompleteTail: false
7296
+ };
7297
+ }
7298
+ const cap = Number.isFinite(maxBytes) && maxBytes > 0 ? Math.max(1, Math.floor(maxBytes)) : MAX_EVENT_READ_BYTES;
7299
+ const available = size - start;
7300
+ const requested = Math.min(available, cap);
7301
+ const buffer = Buffer.allocUnsafe(requested);
7302
+ const fd = openSync2(log, "r");
7303
+ let bytesRead = 0;
7304
+ try {
7305
+ bytesRead = io.read(fd, buffer, 0, requested, start);
7306
+ } finally {
7307
+ closeSync2(fd);
7308
+ }
7309
+ if (bytesRead <= 0) {
7310
+ return {
7311
+ events: [],
7312
+ newOffset: start,
7313
+ bytesRead: 0,
7314
+ limitReached: available > 0,
7315
+ malformedLines: 0,
7316
+ incompleteTail: available > 0
7317
+ };
7318
+ }
7319
+ const chunk = buffer.subarray(0, bytesRead);
7320
+ const lastNewline = chunk.lastIndexOf(10);
7321
+ const limitReached = start + bytesRead < size;
7322
+ if (lastNewline < 0) {
7323
+ return {
7324
+ events: [],
7325
+ newOffset: start,
7326
+ bytesRead,
7327
+ limitReached,
7328
+ malformedLines: 0,
7329
+ incompleteTail: true
7330
+ };
7264
7331
  }
7265
- if (size <= offset) return { events: [], newOffset: offset >= 0 ? Math.min(offset, size) : 0 };
7266
- const raw = readFileSync10(log, "utf8").slice(offset);
7267
7332
  const events = [];
7268
- for (const line of raw.split("\n")) {
7333
+ let malformedLines = 0;
7334
+ const complete = chunk.subarray(0, lastNewline + 1).toString("utf8");
7335
+ for (const line of complete.split("\n")) {
7269
7336
  if (!line.trim()) continue;
7270
7337
  try {
7271
7338
  events.push(JSON.parse(line));
7272
7339
  } catch {
7340
+ malformedLines++;
7341
+ }
7342
+ }
7343
+ const newOffset = start + lastNewline + 1;
7344
+ return {
7345
+ events,
7346
+ newOffset,
7347
+ bytesRead,
7348
+ limitReached,
7349
+ malformedLines,
7350
+ incompleteTail: newOffset < start + bytesRead
7351
+ };
7352
+ }
7353
+ function drainEvents(log, offset, maxTotalBytes = MAX_EVENT_SWEEP_BYTES, io = DEFAULT_EVENT_READ_IO) {
7354
+ const start = Number.isFinite(offset) ? Math.max(0, Math.floor(offset)) : 0;
7355
+ const aggregateCap = Number.isFinite(maxTotalBytes) && maxTotalBytes > 0 ? Math.max(1, Math.floor(maxTotalBytes)) : MAX_EVENT_SWEEP_BYTES;
7356
+ const events = [];
7357
+ let cursor = start;
7358
+ let bytesRead = 0;
7359
+ let batches = 0;
7360
+ let malformedLines = 0;
7361
+ while (bytesRead < aggregateCap) {
7362
+ const remainingBudget = aggregateCap - bytesRead;
7363
+ const batch = readEvents(
7364
+ log,
7365
+ cursor,
7366
+ Math.min(MAX_EVENT_READ_BYTES, remainingBudget),
7367
+ io
7368
+ );
7369
+ batches++;
7370
+ bytesRead += batch.bytesRead;
7371
+ malformedLines += batch.malformedLines;
7372
+ events.push(...batch.events);
7373
+ if (batch.malformedLines > 0) {
7374
+ return {
7375
+ events,
7376
+ newOffset: cursor,
7377
+ bytesRead,
7378
+ batches,
7379
+ malformedLines,
7380
+ complete: false,
7381
+ issue: "malformed-record"
7382
+ };
7383
+ }
7384
+ if (batch.bytesRead === 0) {
7385
+ if (batch.limitReached || batch.incompleteTail) {
7386
+ return {
7387
+ events,
7388
+ newOffset: cursor,
7389
+ bytesRead,
7390
+ batches,
7391
+ malformedLines,
7392
+ complete: false,
7393
+ issue: "read-stalled"
7394
+ };
7395
+ }
7396
+ return {
7397
+ events,
7398
+ newOffset: cursor,
7399
+ bytesRead,
7400
+ batches,
7401
+ malformedLines,
7402
+ complete: true
7403
+ };
7404
+ }
7405
+ if (batch.limitReached && batch.newOffset === cursor) {
7406
+ return {
7407
+ events,
7408
+ newOffset: cursor,
7409
+ bytesRead,
7410
+ batches,
7411
+ malformedLines,
7412
+ complete: false,
7413
+ issue: "oversized-record"
7414
+ };
7415
+ }
7416
+ if (!batch.limitReached && batch.incompleteTail) {
7417
+ return {
7418
+ events,
7419
+ newOffset: cursor,
7420
+ bytesRead,
7421
+ batches,
7422
+ malformedLines,
7423
+ complete: false,
7424
+ issue: "incomplete-record"
7425
+ };
7426
+ }
7427
+ cursor = batch.newOffset;
7428
+ if (!batch.limitReached) {
7429
+ return {
7430
+ events,
7431
+ newOffset: cursor,
7432
+ bytesRead,
7433
+ batches,
7434
+ malformedLines,
7435
+ complete: true
7436
+ };
7437
+ }
7438
+ if (bytesRead >= aggregateCap) {
7439
+ return {
7440
+ events,
7441
+ newOffset: cursor,
7442
+ bytesRead,
7443
+ batches,
7444
+ malformedLines,
7445
+ complete: false,
7446
+ issue: "aggregate-limit"
7447
+ };
7273
7448
  }
7274
7449
  }
7275
- return { events, newOffset: size };
7450
+ return {
7451
+ events,
7452
+ newOffset: cursor,
7453
+ bytesRead,
7454
+ batches,
7455
+ malformedLines,
7456
+ complete: false,
7457
+ issue: "aggregate-limit"
7458
+ };
7276
7459
  }
7277
7460
  function transientFindings(events, persistentPaths, policy, finalHash) {
7278
7461
  if (!isEnabled(RULE11, policy)) return [];
@@ -7316,7 +7499,7 @@ var HookInputError = class extends Error {
7316
7499
  };
7317
7500
  function readStdin() {
7318
7501
  try {
7319
- return readFileSync11(0, "utf8");
7502
+ return readFileSync10(0, "utf8");
7320
7503
  } catch (e) {
7321
7504
  throw new HookInputError(`cannot read the hook payload on stdin: ${e instanceof Error ? e.message : String(e)}`);
7322
7505
  }
@@ -7564,6 +7747,17 @@ function cursorPath(cwd, sessionId) {
7564
7747
  const p = ptreePath(cwd, sessionId);
7565
7748
  return p ? p.replace(/ptree-/, "fscursor-") : null;
7566
7749
  }
7750
+ function observerIntegrityBlock(issue, bytesRead, malformedLines) {
7751
+ const detail = issue === "malformed-record" ? `${malformedLines} malformed complete JSONL record(s)` : issue === "oversized-record" ? `one event record exceeded the ${MAX_EVENT_READ_BYTES}-byte per-record/read ceiling` : issue === "incomplete-record" ? "the event log ended with an incomplete JSONL record" : issue === "aggregate-limit" ? `more than ${MAX_EVENT_SWEEP_BYTES} observer bytes required classification in one Stop sweep` : "the positioned event-log read made no safe progress";
7752
+ return {
7753
+ rule: "tamperward-unavailable",
7754
+ severity: "block",
7755
+ message: `TamperWard could not fully classify transient observer telemetry (${detail}), so Stop is blocked rather than certifying an unjudged tail.`,
7756
+ evidence: `observer_issue=${issue}; bytes_read=${bytesRead}; malformed_lines=${malformedLines}`,
7757
+ remediation: "Let the watcher finish its current record or repair/rotate the malformed observer log, then retry Stop. Do not delete the cursor/log to bypass unjudged telemetry.",
7758
+ signoff: { required: true, command: 'tamperward allow --reason "..."' }
7759
+ };
7760
+ }
7567
7761
  function turnTransientBlocks(cwd, sessionId, policy, changes) {
7568
7762
  const none = { blocks: [], commit: () => {
7569
7763
  } };
@@ -7576,16 +7770,34 @@ function turnTransientBlocks(cwd, sessionId, policy, changes) {
7576
7770
  if (!existsSync8(log)) return none;
7577
7771
  let offset = 0;
7578
7772
  if (cp && existsSync8(cp)) {
7579
- const n = Number(readFileSync11(cp, "utf8"));
7773
+ const n = Number(readFileSync10(cp, "utf8"));
7580
7774
  offset = Number.isFinite(n) ? n : 0;
7581
7775
  }
7582
- const { events, newOffset } = readEvents(log, offset);
7583
- if (events.length === 0) return { ...none, commit: () => {
7584
- if (cp) try {
7585
- writeFileSync5(cp, String(newOffset));
7586
- } catch {
7587
- }
7588
- } };
7776
+ const drained = drainEvents(log, offset);
7777
+ if (!drained.complete) {
7778
+ const issue = drained.issue ?? "read-stalled";
7779
+ recordObserverHealth(
7780
+ "degraded",
7781
+ `event telemetry was not fully classified (${issue}); Stop retained the prior cursor`
7782
+ );
7783
+ return {
7784
+ blocks: [observerIntegrityBlock(issue, drained.bytesRead, drained.malformedLines)],
7785
+ commit: () => {
7786
+ }
7787
+ };
7788
+ }
7789
+ const { events, newOffset } = drained;
7790
+ if (events.length === 0) {
7791
+ return {
7792
+ ...none,
7793
+ commit: () => {
7794
+ if (cp) try {
7795
+ writeFileSync5(cp, String(newOffset));
7796
+ } catch {
7797
+ }
7798
+ }
7799
+ };
7800
+ }
7589
7801
  const persistent = new Set(changes.filter((c) => c.kind === "file").map((c) => c.path));
7590
7802
  const finalHash = (path) => {
7591
7803
  const e = inspectRel(cwd, path);
@@ -7742,7 +7954,7 @@ Honored at LOCAL pre-commit only. The agent-layer hook ignores this file; CI req
7742
7954
  // src/cli/init.ts
7743
7955
  import { execFileSync as execFileSync6 } from "node:child_process";
7744
7956
  import { createHash as createHash6 } from "node:crypto";
7745
- import { existsSync as existsSync9, mkdirSync as mkdirSync5, readFileSync as readFileSync12, writeFileSync as writeFileSync6, chmodSync, statSync as statSync2 } from "node:fs";
7957
+ import { existsSync as existsSync9, mkdirSync as mkdirSync5, readFileSync as readFileSync11, writeFileSync as writeFileSync6, chmodSync, statSync as statSync2 } from "node:fs";
7746
7958
  import { basename as basename2, dirname as dirname4, isAbsolute as isAbsolute8, join as join12, relative as relative3, resolve as resolve3 } from "node:path";
7747
7959
 
7748
7960
  // src/verifier-limits.ts
@@ -7986,7 +8198,7 @@ function planClaudeHooks(cwd) {
7986
8198
  if (existsSync9(path)) {
7987
8199
  let parsed;
7988
8200
  try {
7989
- parsed = JSON.parse(readFileSync12(path, "utf8"));
8201
+ parsed = JSON.parse(readFileSync11(path, "utf8"));
7990
8202
  } catch {
7991
8203
  return {
7992
8204
  item: "agent",
@@ -8132,7 +8344,7 @@ function planPreCommit(cwd) {
8132
8344
  if (existsSync9(path) && !statSync2(path).isFile()) {
8133
8345
  return { item: "pre-commit", path: rel, status: "error", detail: "exists but is not a regular file \u2014 refusing to write" };
8134
8346
  }
8135
- const existing = existsSync9(path) ? readFileSync12(path, "utf8") : null;
8347
+ const existing = existsSync9(path) ? readFileSync11(path, "utf8") : null;
8136
8348
  if (existing === null) {
8137
8349
  return {
8138
8350
  item: "pre-commit",
@@ -8224,7 +8436,7 @@ function planCodeowners(cwd) {
8224
8436
  (r) => existsSync9(join12(cwd, r))
8225
8437
  );
8226
8438
  const path = join12(cwd, existingRel ?? rel);
8227
- const existing = existingRel ? readFileSync12(path, "utf8") : null;
8439
+ const existing = existingRel ? readFileSync11(path, "utf8") : null;
8228
8440
  const missing = CODEOWNERS_PATHS.filter((p) => existing === null || !coveredBy(existing, p));
8229
8441
  if (missing.length === 0) {
8230
8442
  return { item: "codeowners", path: existingRel, status: "ok", detail: "gate paths already have code owners" };
@@ -8273,7 +8485,7 @@ function planWorkflow(cwd, force) {
8273
8485
  }
8274
8486
  let src;
8275
8487
  try {
8276
- src = readFileSync12(path, "utf8");
8488
+ src = readFileSync11(path, "utf8");
8277
8489
  } catch {
8278
8490
  return { item: "ci", path: rel, status: "error", detail: "exists but cannot be read \u2014 refusing to overwrite" };
8279
8491
  }
@@ -8372,7 +8584,7 @@ tamperward init: ${changed} change(s) applied. Commit them so the gate travels w
8372
8584
 
8373
8585
  // src/cli/doctor.ts
8374
8586
  import { execFileSync as execFileSync7 } from "node:child_process";
8375
- import { existsSync as existsSync10, readFileSync as readFileSync13, readdirSync as readdirSync3 } from "node:fs";
8587
+ import { existsSync as existsSync10, readFileSync as readFileSync12, readdirSync as readdirSync3 } from "node:fs";
8376
8588
  import { join as join13, resolve as resolve4 } from "node:path";
8377
8589
  import { parse as parse3 } from "yaml";
8378
8590
  var VERIFY_COMMAND = /\btamperward(?:@\S+)?\s+verify\b/;
@@ -8410,7 +8622,7 @@ function pinsInFile(path) {
8410
8622
  if (!existsSync10(path)) return [];
8411
8623
  let src = "";
8412
8624
  try {
8413
- src = readFileSync13(path, "utf8");
8625
+ src = readFileSync12(path, "utf8");
8414
8626
  } catch {
8415
8627
  return [];
8416
8628
  }
@@ -8437,7 +8649,7 @@ function workflowPermissionCheck(cwd, workflowRels = [".github/workflows/tamperw
8437
8649
  }
8438
8650
  let doc;
8439
8651
  try {
8440
- doc = parse3(readFileSync13(path, "utf8"));
8652
+ doc = parse3(readFileSync12(path, "utf8"));
8441
8653
  } catch (e) {
8442
8654
  broken.push(`${rel} is not valid YAML (${e instanceof Error ? e.message : String(e)})`);
8443
8655
  continue;
@@ -8843,7 +9055,7 @@ function runDoctor(opts = {}) {
8843
9055
  }
8844
9056
  let doc;
8845
9057
  try {
8846
- doc = parse3(readFileSync13(workflowPath, "utf8"));
9058
+ doc = parse3(readFileSync12(workflowPath, "utf8"));
8847
9059
  } catch (e) {
8848
9060
  return fail(
8849
9061
  "ci-verifier",
@@ -8949,11 +9161,11 @@ function runDoctor(opts = {}) {
8949
9161
  }
8950
9162
 
8951
9163
  // src/cli/verify.ts
8952
- import { execFileSync as execFileSync9, spawnSync as spawnSync2 } from "node:child_process";
9164
+ import { execFileSync as execFileSync9, spawnSync as spawnSync3 } from "node:child_process";
8953
9165
  import { createHash as createHash10 } from "node:crypto";
8954
- import { chmodSync as chmodSync2, cpSync, lstatSync as lstatSync5, mkdirSync as mkdirSync6, mkdtempSync as mkdtempSync2, readFileSync as readFileSync16, readlinkSync as readlinkSync3, rmSync as rmSync3, symlinkSync, writeFileSync as writeFileSync7 } from "node:fs";
8955
- import { tmpdir as tmpdir2 } from "node:os";
8956
- import { dirname as dirname7, isAbsolute as isAbsolute10, join as join16, relative as relative5, resolve as resolve7, sep as sep2 } from "node:path";
9166
+ 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";
9167
+ import { tmpdir as tmpdir3 } from "node:os";
9168
+ import { dirname as dirname7, isAbsolute as isAbsolute10, join as join17, relative as relative5, resolve as resolve7, sep as sep2 } from "node:path";
8957
9169
 
8958
9170
  // src/fingerprint.ts
8959
9171
  import { execFileSync as execFileSync8 } from "node:child_process";
@@ -8987,7 +9199,7 @@ import {
8987
9199
  existsSync as existsSync11,
8988
9200
  lstatSync as lstatSync4,
8989
9201
  readdirSync as readdirSync4,
8990
- readFileSync as readFileSync14,
9202
+ readFileSync as readFileSync13,
8991
9203
  readlinkSync as readlinkSync2,
8992
9204
  realpathSync as realpathSync3,
8993
9205
  statSync as statSync3
@@ -9044,7 +9256,7 @@ function fingerprintRoot(root, cwd, h) {
9044
9256
  st = statSync3(path);
9045
9257
  if (!st.isFile()) throw new Error("not a regular file");
9046
9258
  h.update(label + "\0external-file\0" + path + "\0" + String(st.mode) + "\0");
9047
- h.update(readFileSync14(path));
9259
+ h.update(readFileSync13(path));
9048
9260
  h.update("\0");
9049
9261
  } catch (e) {
9050
9262
  throw new Unattestable(
@@ -9078,7 +9290,7 @@ function fingerprintRoot(root, cwd, h) {
9078
9290
  h.update(rel + "\0" + ident.kind + "\0" + String(ident.mode) + "\0");
9079
9291
  if (ident.kind === "file") {
9080
9292
  try {
9081
- h.update(readFileSync14(path));
9293
+ h.update(readFileSync13(path));
9082
9294
  h.update("\0");
9083
9295
  } catch (e) {
9084
9296
  throw new Unattestable(
@@ -9239,7 +9451,7 @@ function venvProblem(root) {
9239
9451
  const cfg = join14(root, "pyvenv.cfg");
9240
9452
  if (!existsSync11(cfg)) return "VIRTUAL_ENV " + JSON.stringify(root) + " has no pyvenv.cfg";
9241
9453
  try {
9242
- const body = readFileSync14(cfg, "utf8");
9454
+ const body = readFileSync13(cfg, "utf8");
9243
9455
  if (/^\s*include-system-site-packages\s*=\s*true\s*$/im.test(body)) {
9244
9456
  return "Python environment " + JSON.stringify(root) + " enables system site-packages; those external package roots are not bounded";
9245
9457
  }
@@ -9336,9 +9548,321 @@ function dependencyEnvironmentSummary(descriptor) {
9336
9548
 
9337
9549
  // src/verifier-backend.ts
9338
9550
  import { createHash as createHash9, randomUUID } from "node:crypto";
9339
- import { spawnSync } from "node:child_process";
9551
+ import { spawnSync as spawnSync2 } from "node:child_process";
9340
9552
  import { accessSync, constants as constants2, readFileSync as readFileSync15, realpathSync as realpathSync4, statSync as statSync4 } from "node:fs";
9341
- import { delimiter as delimiter2, dirname as dirname6, join as join15, resolve as resolve6 } from "node:path";
9553
+ import { delimiter as delimiter2, dirname as dirname6, join as join16, resolve as resolve6 } from "node:path";
9554
+
9555
+ // src/suite-diagnostics.ts
9556
+ import { spawnSync } from "node:child_process";
9557
+ import { mkdtempSync as mkdtempSync2, rmSync as rmSync3, writeFileSync as writeFileSync7 } from "node:fs";
9558
+ import { tmpdir as tmpdir2 } from "node:os";
9559
+ import { join as join15 } from "node:path";
9560
+ var DIAGNOSTIC_TAIL_BYTES = 16 * 1024;
9561
+ var EMPTY_STREAM = {
9562
+ captured_bytes: 0,
9563
+ retained_bytes: 0,
9564
+ truncated: false,
9565
+ tail: ""
9566
+ };
9567
+ function emptySuiteDiagnostics() {
9568
+ return {
9569
+ stdout: { ...EMPTY_STREAM },
9570
+ stderr: { ...EMPTY_STREAM }
9571
+ };
9572
+ }
9573
+ function decodeUtf8Tail(tail) {
9574
+ let start = 0;
9575
+ while (start < tail.length && (tail[start] & 192) === 128) start++;
9576
+ return tail.subarray(start).toString("utf8");
9577
+ }
9578
+ function streamFromRaw(raw) {
9579
+ let tail = Buffer.alloc(0);
9580
+ try {
9581
+ tail = Buffer.from(raw?.tail_b64 ?? "", "base64");
9582
+ } catch {
9583
+ tail = Buffer.alloc(0);
9584
+ }
9585
+ const captured = Number.isFinite(raw?.captured_bytes) ? Math.max(0, Number(raw?.captured_bytes)) : tail.length;
9586
+ return {
9587
+ captured_bytes: captured,
9588
+ retained_bytes: tail.length,
9589
+ truncated: captured > tail.length,
9590
+ tail: decodeUtf8Tail(tail)
9591
+ };
9592
+ }
9593
+ function scrubDiagnosticText(input) {
9594
+ let out2 = "";
9595
+ for (const ch of input) {
9596
+ const cp = ch.codePointAt(0);
9597
+ if (ch === "\n" || ch === " ") {
9598
+ out2 += ch;
9599
+ } else if (cp === 13) {
9600
+ out2 += "\\r";
9601
+ } else if (cp < 32 || cp >= 127 && cp <= 159) {
9602
+ out2 += cp <= 255 ? `\\x${cp.toString(16).padStart(2, "0")}` : `\\u{${cp.toString(16)}}`;
9603
+ } else {
9604
+ out2 += ch;
9605
+ }
9606
+ }
9607
+ return out2;
9608
+ }
9609
+ function diagnosticLines(stage, diagnostics) {
9610
+ const lines = [];
9611
+ for (const streamName of ["stdout", "stderr"]) {
9612
+ const stream = diagnostics[streamName];
9613
+ if (stream.captured_bytes === 0) continue;
9614
+ const suffix = stream.truncated ? ` \u2014 tail ${stream.retained_bytes}/${stream.captured_bytes} bytes retained` : ` \u2014 ${stream.captured_bytes} bytes`;
9615
+ lines.push(`${stage} suite ${streamName}${suffix}:`);
9616
+ const scrubbed = scrubDiagnosticText(stream.tail);
9617
+ const body = scrubbed.split("\n");
9618
+ if (body.at(-1) === "") body.pop();
9619
+ for (const line of body) lines.push(` | ${line}`);
9620
+ }
9621
+ return lines;
9622
+ }
9623
+ var CAPTURE_SUPERVISOR = String.raw`
9624
+ const cp = require('node:child_process');
9625
+ const fs = require('node:fs');
9626
+
9627
+ const [configFile] = process.argv.slice(1);
9628
+ const cfg = JSON.parse(fs.readFileSync(configFile, 'utf8'));
9629
+ const CAP = Number(cfg.captureBytes);
9630
+
9631
+ const fresh = () => ({ total: 0, tail: Buffer.alloc(0) });
9632
+ const stdout = fresh();
9633
+ const stderr = fresh();
9634
+
9635
+ // Linux ordinary-exit hardening (#371): process-group ownership alone misses a
9636
+ // descendant that calls setsid(). Track the real descendant tree while the
9637
+ // suite is alive so a reparented session escape is still known when the main
9638
+ // child exits. This supplements, rather than replaces, group termination.
9639
+ const trackedDescendants = new Set();
9640
+
9641
+ function linuxDescendants(rootPid) {
9642
+ if (process.platform !== 'linux' || !rootPid) return [];
9643
+ const byParent = new Map();
9644
+ let names = [];
9645
+ try { names = fs.readdirSync('/proc').filter((x) => /^\d+$/.test(x)); } catch { return []; }
9646
+ for (const name of names) {
9647
+ const pid = Number(name);
9648
+ try {
9649
+ const stat = fs.readFileSync('/proc/' + name + '/stat', 'utf8');
9650
+ const fields = stat.slice(stat.lastIndexOf(')') + 2).split(' ');
9651
+ const ppid = Number(fields[1]);
9652
+ if (!Number.isFinite(ppid)) continue;
9653
+ const kids = byParent.get(ppid) || [];
9654
+ kids.push(pid);
9655
+ byParent.set(ppid, kids);
9656
+ } catch {}
9657
+ }
9658
+ const out = [];
9659
+ const seen = new Set([rootPid]);
9660
+ const stack = [rootPid];
9661
+ while (stack.length) {
9662
+ const parent = stack.pop();
9663
+ for (const pid of (byParent.get(parent) || [])) {
9664
+ if (seen.has(pid)) continue;
9665
+ seen.add(pid);
9666
+ out.push(pid);
9667
+ stack.push(pid);
9668
+ }
9669
+ }
9670
+ return out;
9671
+ }
9672
+
9673
+ function trackDescendants() {
9674
+ if (!child || !child.pid || process.platform !== 'linux') return;
9675
+ for (const pid of linuxDescendants(child.pid)) trackedDescendants.add(pid);
9676
+ }
9677
+
9678
+ function append(state, chunk) {
9679
+ const b = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
9680
+ state.total += b.length;
9681
+ if (b.length >= CAP) {
9682
+ state.tail = Buffer.from(b.subarray(b.length - CAP));
9683
+ return;
9684
+ }
9685
+ if (state.tail.length + b.length <= CAP) {
9686
+ state.tail = Buffer.concat([state.tail, b]);
9687
+ return;
9688
+ }
9689
+ const keep = CAP - b.length;
9690
+ state.tail = Buffer.concat([state.tail.subarray(state.tail.length - keep), b]);
9691
+ }
9692
+
9693
+ function diag(state) {
9694
+ return {
9695
+ captured_bytes: state.total,
9696
+ tail_b64: state.tail.toString('base64'),
9697
+ };
9698
+ }
9699
+
9700
+ let child;
9701
+ let done = false;
9702
+ let timedOut = false;
9703
+ let exitInfo = { exit: null, signal: null };
9704
+ let descendantTracker = null;
9705
+
9706
+ function killOwned() {
9707
+ if (!child || !child.pid) return;
9708
+ trackDescendants();
9709
+ if (cfg.detached && process.platform !== 'win32') {
9710
+ try { process.kill(-child.pid, 'SIGKILL'); } catch {}
9711
+ }
9712
+ try { child.kill('SIGKILL'); } catch {}
9713
+ if (process.platform === 'linux') {
9714
+ // Deepest-first is friendlier to short-lived process trees. PIDs are still
9715
+ // rechecked by kill(2); races with natural exit are harmless.
9716
+ for (const pid of Array.from(trackedDescendants).reverse()) {
9717
+ try { process.kill(pid, 'SIGKILL'); } catch {}
9718
+ }
9719
+ }
9720
+ }
9721
+
9722
+ function finish(extra) {
9723
+ if (done) return;
9724
+ done = true;
9725
+ if (descendantTracker) clearInterval(descendantTracker);
9726
+ try {
9727
+ // stdout is reserved for the trusted supervisor result. Candidate suite
9728
+ // stdout/stderr are separate pipes and are never inherited here.
9729
+ process.stdout.write(JSON.stringify({
9730
+ ...exitInfo,
9731
+ timedOut,
9732
+ ...extra,
9733
+ stdout: diag(stdout),
9734
+ stderr: diag(stderr),
9735
+ }));
9736
+ } catch {}
9737
+ process.exit(0);
9738
+ }
9739
+
9740
+ try {
9741
+ child = cp.spawn(cfg.executable, cfg.args, {
9742
+ cwd: cfg.cwd,
9743
+ env: process.env,
9744
+ detached: Boolean(cfg.detached) && process.platform !== 'win32',
9745
+ stdio: ['ignore', 'pipe', 'pipe'],
9746
+ });
9747
+ } catch (e) {
9748
+ finish({ error: String(e) });
9749
+ }
9750
+
9751
+ if (child) {
9752
+ if (process.platform === 'linux') {
9753
+ trackDescendants();
9754
+ descendantTracker = setInterval(trackDescendants, 5);
9755
+ descendantTracker.unref();
9756
+ }
9757
+ child.stdout.on('data', (b) => append(stdout, b));
9758
+ child.stderr.on('data', (b) => append(stderr, b));
9759
+
9760
+ const timer = setTimeout(() => {
9761
+ timedOut = true;
9762
+ killOwned();
9763
+ // Backstop if a platform fails to emit close after the forced kill.
9764
+ setTimeout(() => finish({}), 1000).unref();
9765
+ }, Number(cfg.timeoutMs));
9766
+ timer.unref();
9767
+
9768
+ child.on('error', (e) => {
9769
+ clearTimeout(timer);
9770
+ killOwned();
9771
+ finish({ error: String(e) });
9772
+ });
9773
+
9774
+ child.on('exit', (code, signal) => {
9775
+ exitInfo = {
9776
+ exit: code == null ? null : Number(code),
9777
+ signal: signal == null ? null : String(signal),
9778
+ };
9779
+ if (cfg.killGroupOnFinish) killOwned();
9780
+ });
9781
+
9782
+ // close fires after the stdio pipes close, so the retained tails include all
9783
+ // output that was drained before owned descendants were terminated.
9784
+ child.on('close', (code, signal) => {
9785
+ clearTimeout(timer);
9786
+ if (exitInfo.exit === null && code != null) exitInfo.exit = Number(code);
9787
+ if (exitInfo.signal === null && signal != null) exitInfo.signal = String(signal);
9788
+ finish({});
9789
+ });
9790
+ }
9791
+ `;
9792
+ function parseCapturedSupervisorResult(stdoutText) {
9793
+ let raw;
9794
+ try {
9795
+ raw = JSON.parse(stdoutText);
9796
+ } catch {
9797
+ return null;
9798
+ }
9799
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) return null;
9800
+ const diagnostics = {
9801
+ stdout: streamFromRaw(raw.stdout),
9802
+ stderr: streamFromRaw(raw.stderr)
9803
+ };
9804
+ return {
9805
+ exit: typeof raw.exit === "number" ? raw.exit : null,
9806
+ signal: typeof raw.signal === "string" ? raw.signal : null,
9807
+ timedOut: Boolean(raw.timedOut),
9808
+ ...typeof raw.error === "string" && raw.error ? { error: raw.error } : {},
9809
+ diagnostics
9810
+ };
9811
+ }
9812
+ function runCapturedProcessSync(executable, args, opts) {
9813
+ const stateDir = mkdtempSync2(join15(tmpdir2(), "tw-suite-capture-"));
9814
+ const configFile = join15(stateDir, "config.json");
9815
+ const backstop = opts.backstopMs ?? 3e4;
9816
+ try {
9817
+ writeFileSync7(
9818
+ configFile,
9819
+ JSON.stringify({
9820
+ executable,
9821
+ args,
9822
+ cwd: opts.cwd,
9823
+ timeoutMs: opts.timeoutMs,
9824
+ detached: Boolean(opts.detached),
9825
+ killGroupOnFinish: Boolean(opts.killGroupOnFinish),
9826
+ captureBytes: DIAGNOSTIC_TAIL_BYTES
9827
+ }),
9828
+ { mode: 384 }
9829
+ );
9830
+ const supervisor = spawnSync(
9831
+ process.execPath,
9832
+ ["-e", CAPTURE_SUPERVISOR, configFile],
9833
+ {
9834
+ cwd: opts.cwd,
9835
+ env: opts.env,
9836
+ encoding: "utf8",
9837
+ stdio: ["ignore", "pipe", "ignore"],
9838
+ // The trusted result is at most two 16 KiB tails plus small metadata.
9839
+ // A candidate that somehow writes into the supervisor's stdout through
9840
+ // same-UID /proc can only overflow/corrupt this bounded channel and make
9841
+ // parsing fail closed; its suite streams are never inherited here.
9842
+ maxBuffer: 256 * 1024,
9843
+ timeout: opts.timeoutMs + backstop,
9844
+ killSignal: "SIGKILL"
9845
+ }
9846
+ );
9847
+ const parsed = parseCapturedSupervisorResult(supervisor.stdout ?? "");
9848
+ const diagnostics = parsed?.diagnostics ?? emptySuiteDiagnostics();
9849
+ if (!parsed) {
9850
+ const supervisorTimedOut = Boolean(supervisor.error) && supervisor.error.code === "ETIMEDOUT";
9851
+ return {
9852
+ exit: null,
9853
+ signal: supervisor.signal ? String(supervisor.signal) : null,
9854
+ timedOut: supervisorTimedOut,
9855
+ error: supervisor.error?.message ?? "suite capture supervisor did not produce a result",
9856
+ diagnostics
9857
+ };
9858
+ }
9859
+ return parsed;
9860
+ } finally {
9861
+ rmSync3(stateDir, { recursive: true, force: true });
9862
+ }
9863
+ }
9864
+
9865
+ // src/verifier-backend.ts
9342
9866
  var DEFAULT_CONTAINER_RESOURCE_LIMITS = Object.freeze({
9343
9867
  memoryBytes: 2 * 1024 * 1024 * 1024,
9344
9868
  // Docker interprets memory-swap == memory as no additional swap allowance.
@@ -9381,7 +9905,7 @@ function replaceableByCaller(path) {
9381
9905
  function findExecutable(name) {
9382
9906
  for (const raw of (process.env.PATH ?? "").split(delimiter2)) {
9383
9907
  if (!raw) continue;
9384
- const candidate = join15(raw, name);
9908
+ const candidate = join16(raw, name);
9385
9909
  try {
9386
9910
  accessSync(candidate, constants2.X_OK);
9387
9911
  return realpathSync4(candidate);
@@ -9406,7 +9930,7 @@ function dockerArgs(host, args) {
9406
9930
  }
9407
9931
  function dockerAvailable(enginePath, host) {
9408
9932
  try {
9409
- const r = spawnSync(enginePath, dockerArgs(host, ["version"]), {
9933
+ const r = spawnSync2(enginePath, dockerArgs(host, ["version"]), {
9410
9934
  stdio: "ignore",
9411
9935
  timeout: 5e3,
9412
9936
  killSignal: "SIGKILL",
@@ -9434,7 +9958,7 @@ function declaredImageVolumePaths(metadata) {
9434
9958
  }
9435
9959
  function inspectImageMetadata(enginePath, host, image) {
9436
9960
  try {
9437
- const r = spawnSync(enginePath, dockerArgs(host, ["image", "inspect", image]), {
9961
+ const r = spawnSync2(enginePath, dockerArgs(host, ["image", "inspect", image]), {
9438
9962
  encoding: "utf8",
9439
9963
  stdio: ["ignore", "pipe", "ignore"],
9440
9964
  timeout: 1e4,
@@ -9609,7 +10133,7 @@ function verifierBackendSummary(backend) {
9609
10133
  function cleanupContainer(backend, name) {
9610
10134
  if (!backend.enginePath || !backend.daemonHost || !engineStable(backend)) return;
9611
10135
  try {
9612
- spawnSync(backend.enginePath, dockerArgs(backend.daemonHost, ["rm", "-f", "-v", name]), {
10136
+ spawnSync2(backend.enginePath, dockerArgs(backend.daemonHost, ["rm", "-f", "-v", name]), {
9613
10137
  stdio: "ignore",
9614
10138
  timeout: 1e4,
9615
10139
  killSignal: "SIGKILL",
@@ -9720,29 +10244,45 @@ function runContainerStage(backend, dir, command, budgetSecs) {
9720
10244
  resources: backend.resources ?? DEFAULT_CONTAINER_RESOURCE_LIMITS
9721
10245
  });
9722
10246
  try {
9723
- const r = spawnSync(
10247
+ const r = runCapturedProcessSync(
9724
10248
  backend.enginePath,
9725
10249
  dockerArgs(backend.daemonHost, args),
9726
10250
  {
9727
- stdio: "ignore",
9728
- timeout: budgetSecs * 1e3,
9729
- killSignal: "SIGKILL",
9730
- env: engineClientEnv()
10251
+ cwd: dirname6(backend.enginePath),
10252
+ env: engineClientEnv(),
10253
+ timeoutMs: budgetSecs * 1e3,
10254
+ backstopMs: 3e4
9731
10255
  }
9732
10256
  );
9733
10257
  const secs = Math.round((Date.now() - t0) / 1e3);
10258
+ if (r.timedOut) {
10259
+ cleanupContainer(backend, name);
10260
+ return {
10261
+ exit: null,
10262
+ secs,
10263
+ failure: "budget",
10264
+ reason: "verifier stage exceeded its budget",
10265
+ diagnostics: r.diagnostics
10266
+ };
10267
+ }
9734
10268
  if (r.error) {
9735
10269
  cleanupContainer(backend, name);
9736
- const code2 = r.error.code;
9737
- 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}` };
10270
+ return {
10271
+ exit: null,
10272
+ secs,
10273
+ failure: "backend",
10274
+ reason: `Docker client failed: ${r.error}`,
10275
+ diagnostics: r.diagnostics
10276
+ };
9738
10277
  }
9739
- if (r.status === null) {
10278
+ if (r.exit === null) {
9740
10279
  cleanupContainer(backend, name);
9741
10280
  return {
9742
10281
  exit: null,
9743
10282
  secs,
9744
10283
  failure: "backend",
9745
- reason: `Docker client did not return a status${r.signal ? ` (signal ${r.signal})` : ""}`
10284
+ reason: `Docker client did not return a status${r.signal ? ` (signal ${r.signal})` : ""}`,
10285
+ diagnostics: r.diagnostics
9746
10286
  };
9747
10287
  }
9748
10288
  if (!engineStable(backend)) {
@@ -9750,10 +10290,11 @@ function runContainerStage(backend, dir, command, budgetSecs) {
9750
10290
  exit: null,
9751
10291
  secs,
9752
10292
  failure: "backend",
9753
- reason: "Docker engine identity changed before verifier result inspection"
10293
+ reason: "Docker engine identity changed before verifier result inspection",
10294
+ diagnostics: r.diagnostics
9754
10295
  };
9755
10296
  }
9756
- const inspected = spawnSync(
10297
+ const inspected = spawnSync2(
9757
10298
  backend.enginePath,
9758
10299
  dockerArgs(backend.daemonHost, ["inspect", "--format", "{{json .State}}", name]),
9759
10300
  {
@@ -9773,7 +10314,10 @@ function runContainerStage(backend, dir, command, budgetSecs) {
9773
10314
  }
9774
10315
  }
9775
10316
  cleanupContainer(backend, name);
9776
- return containerStateResult(state, secs);
10317
+ return {
10318
+ ...containerStateResult(state, secs),
10319
+ diagnostics: r.diagnostics
10320
+ };
9777
10321
  } catch (e) {
9778
10322
  cleanupContainer(backend, name);
9779
10323
  return {
@@ -9878,7 +10422,7 @@ function resolveBase(base, cwd) {
9878
10422
  }
9879
10423
  }
9880
10424
  function baseIsAncestorOfHead(base, cwd) {
9881
- const r = spawnSync2("git", ["merge-base", "--is-ancestor", base, "HEAD"], { cwd, env: trustedGitEnv() });
10425
+ const r = spawnSync3("git", ["merge-base", "--is-ancestor", base, "HEAD"], { cwd, env: trustedGitEnv() });
9882
10426
  return r.status === 0;
9883
10427
  }
9884
10428
  function inside2(root, path) {
@@ -9902,7 +10446,7 @@ function safeSymlink(target, out2, root, label) {
9902
10446
  if (rootedLinkTarget(target) || !inside2(root, resolve7(dirname7(out2), target))) {
9903
10447
  throw linkEscape(label, target);
9904
10448
  }
9905
- rmSync3(out2, { force: true });
10449
+ rmSync4(out2, { force: true });
9906
10450
  symlinkSync(target, out2);
9907
10451
  }
9908
10452
  function validateSymlinkGraph(target, out2, root, dependencyRoot, label) {
@@ -9925,7 +10469,7 @@ function validateSymlinkGraph(target, out2, root, dependencyRoot, label) {
9925
10469
  continue;
9926
10470
  }
9927
10471
  const base = domain === "tree" ? root : dependencyRoot;
9928
- const current = join16(base, ...stack);
10472
+ const current = join17(base, ...stack);
9929
10473
  let st;
9930
10474
  try {
9931
10475
  st = lstatSync5(current);
@@ -9961,7 +10505,7 @@ function rejectLinkedParent(cwd, rel) {
9961
10505
  const parts = rel.split("/").slice(0, -1);
9962
10506
  let current = cwd;
9963
10507
  for (const part of parts) {
9964
- current = join16(current, part);
10508
+ current = join17(current, part);
9965
10509
  let st;
9966
10510
  try {
9967
10511
  st = lstatSync5(current);
@@ -9977,7 +10521,7 @@ function materialize(cwd, dest, dependencyRoot) {
9977
10521
  const listed = git2(["ls-files", "-z", "--cached", "--others", "--exclude-standard"], cwd).split("\0").filter(Boolean);
9978
10522
  const links = [];
9979
10523
  for (const rel of listed) {
9980
- const src = join16(cwd, rel);
10524
+ const src = join17(cwd, rel);
9981
10525
  rejectLinkedParent(cwd, rel);
9982
10526
  let st;
9983
10527
  try {
@@ -9985,7 +10529,7 @@ function materialize(cwd, dest, dependencyRoot) {
9985
10529
  } catch {
9986
10530
  continue;
9987
10531
  }
9988
- const out2 = join16(dest, rel);
10532
+ const out2 = join17(dest, rel);
9989
10533
  mkdirSync6(dirname7(out2), { recursive: true });
9990
10534
  dropSymlink(out2);
9991
10535
  if (st.isSymbolicLink()) {
@@ -10002,11 +10546,11 @@ function materialize(cwd, dest, dependencyRoot) {
10002
10546
  for (const link of links) {
10003
10547
  validateSymlinkGraph(link.target, link.out, dest, dependencyRoot, link.rel);
10004
10548
  }
10005
- if (dependencyRoot) symlinkSync(dependencyRoot, join16(dest, "node_modules"), "dir");
10549
+ if (dependencyRoot) symlinkSync(dependencyRoot, join17(dest, "node_modules"), "dir");
10006
10550
  }
10007
10551
  function dropSymlink(p) {
10008
10552
  try {
10009
- if (lstatSync5(p).isSymbolicLink()) rmSync3(p, { force: true });
10553
+ if (lstatSync5(p).isSymbolicLink()) rmSync4(p, { force: true });
10010
10554
  } catch {
10011
10555
  }
10012
10556
  }
@@ -10035,7 +10579,7 @@ function overlayDigest(dest, paths) {
10035
10579
  for (const rel of [...paths].sort()) {
10036
10580
  h.update(rel);
10037
10581
  h.update("\0");
10038
- const p = join16(dest, rel);
10582
+ const p = join17(dest, rel);
10039
10583
  try {
10040
10584
  const st = lstatSync5(p);
10041
10585
  h.update(String(st.mode));
@@ -10063,15 +10607,15 @@ function overlayPristine(cwd, base, dest, policy, cmd, dependencyRoot) {
10063
10607
  throw new Error(`the base's ${e.path} is a submodule; the pristine copy cannot reproduce it`);
10064
10608
  }
10065
10609
  const content = execFileSync9("git", ["show", `${base}:${e.path}`], { cwd, maxBuffer: 1 << 28, env: trustedGitEnv() });
10066
- const out2 = join16(dest, e.path);
10610
+ const out2 = join17(dest, e.path);
10067
10611
  mkdirSync6(dirname7(out2), { recursive: true });
10068
- rmSync3(out2, { force: true });
10612
+ rmSync4(out2, { force: true });
10069
10613
  if (e.mode === "120000") {
10070
10614
  const target = content.toString("utf8");
10071
10615
  safeSymlink(target, out2, dest, `the base's ${e.path}`);
10072
10616
  restoredLinks.push({ path: e.path, out: out2, target });
10073
10617
  } else {
10074
- writeFileSync7(out2, content);
10618
+ writeFileSync8(out2, content);
10075
10619
  chmodSync2(out2, parseInt(e.mode.slice(-4), 8) & 511);
10076
10620
  }
10077
10621
  restored.push(e.path);
@@ -10080,7 +10624,7 @@ function overlayPristine(cwd, base, dest, policy, cmd, dependencyRoot) {
10080
10624
  const inCopy = git2(["ls-files", "-z", "--cached", "--others", "--exclude-standard"], cwd).split("\0").filter(Boolean);
10081
10625
  for (const rel of inCopy) {
10082
10626
  if (!isOverlay(rel) || baseProtected.has(rel)) continue;
10083
- rmSync3(join16(dest, rel), { force: true });
10627
+ rmSync4(join17(dest, rel), { force: true });
10084
10628
  removed++;
10085
10629
  }
10086
10630
  for (const link of restoredLinks) {
@@ -10088,27 +10632,6 @@ function overlayPristine(cwd, base, dest, policy, cmd, dependencyRoot) {
10088
10632
  }
10089
10633
  return { restored, removed };
10090
10634
  }
10091
- var SUPERVISOR = `
10092
- const cp = require('node:child_process');
10093
- const fs = require('node:fs');
10094
- const [cmd, budgetMs, outFile] = process.argv.slice(1);
10095
- const child = cp.spawn('sh', ['-c', cmd], { stdio: 'ignore', detached: process.platform !== 'win32' });
10096
- const killGroup = () => {
10097
- try { process.kill(-child.pid, 'SIGKILL'); } catch {}
10098
- try { child.kill('SIGKILL'); } catch {}
10099
- };
10100
- let done = false;
10101
- const finish = (r) => {
10102
- if (done) return;
10103
- done = true;
10104
- killGroup();
10105
- fs.writeFileSync(outFile, JSON.stringify(r));
10106
- process.exit(0);
10107
- };
10108
- const timer = setTimeout(() => finish({ timedOut: true }), Number(budgetMs));
10109
- child.on('error', (e) => { clearTimeout(timer); finish({ error: String(e) }); });
10110
- child.on('exit', (code, signal) => { clearTimeout(timer); finish({ exit: code, signal }); });
10111
- `;
10112
10635
  var DROPPED_ENV = /* @__PURE__ */ new Set([
10113
10636
  "NODE_OPTIONS",
10114
10637
  "NODE_PATH",
@@ -10125,9 +10648,9 @@ function suiteEnv(scratch) {
10125
10648
  if (DROPPED_ENV.has(k) || PINNED_NPM.test(k)) continue;
10126
10649
  env[k] = v;
10127
10650
  }
10128
- const userRc = join16(scratch, "npmrc-user");
10129
- const globalRc = join16(scratch, "npmrc-global");
10130
- for (const f of [userRc, globalRc]) writeFileSync7(f, "", { mode: 292 });
10651
+ const userRc = join17(scratch, "npmrc-user");
10652
+ const globalRc = join17(scratch, "npmrc-global");
10653
+ for (const f of [userRc, globalRc]) writeFileSync8(f, "", { mode: 292 });
10131
10654
  env.npm_config_userconfig = userRc;
10132
10655
  env.npm_config_globalconfig = globalRc;
10133
10656
  env.npm_config_node_options = " ";
@@ -10136,31 +10659,75 @@ function suiteEnv(scratch) {
10136
10659
  }
10137
10660
  function runLocalSuite(dir, cmd, budgetSecs) {
10138
10661
  const t0 = Date.now();
10139
- const outDir = mkdtempSync2(join16(tmpdir2(), "tw-verify-run-"));
10140
- const outFile = join16(outDir, "result.json");
10662
+ const scratch = mkdtempSync3(join17(tmpdir3(), "tw-verify-run-"));
10141
10663
  try {
10142
- spawnSync2(process.execPath, ["-e", SUPERVISOR, cmd, String(budgetSecs * 1e3), outFile], {
10664
+ const r = runCapturedProcessSync("sh", ["-c", cmd], {
10143
10665
  cwd: dir,
10144
- env: suiteEnv(outDir),
10145
- stdio: "ignore",
10146
- timeout: budgetSecs * 1e3 + 3e4,
10147
- // backstop only; the supervisor enforces the budget
10148
- killSignal: "SIGKILL"
10666
+ env: suiteEnv(scratch),
10667
+ timeoutMs: budgetSecs * 1e3,
10668
+ detached: process.platform !== "win32",
10669
+ killGroupOnFinish: true,
10670
+ backstopMs: 3e4
10149
10671
  });
10150
10672
  const secs = Math.round((Date.now() - t0) / 1e3);
10151
- let r;
10152
- try {
10153
- r = JSON.parse(readFileSync16(outFile, "utf8"));
10154
- } catch {
10155
- return { exit: null, secs };
10673
+ if (r.timedOut) {
10674
+ return {
10675
+ exit: null,
10676
+ secs,
10677
+ failure: "budget",
10678
+ reason: "verifier stage exceeded its budget",
10679
+ diagnostics: r.diagnostics
10680
+ };
10681
+ }
10682
+ if (r.error) {
10683
+ return {
10684
+ exit: null,
10685
+ secs,
10686
+ failure: "backend",
10687
+ reason: r.error,
10688
+ diagnostics: r.diagnostics
10689
+ };
10690
+ }
10691
+ if (r.exit === null) {
10692
+ return {
10693
+ exit: null,
10694
+ secs,
10695
+ failure: "backend",
10696
+ reason: `verifier suite did not return an exit code${r.signal ? ` (signal ${r.signal})` : ""}`,
10697
+ diagnostics: r.diagnostics
10698
+ };
10156
10699
  }
10157
- if (r.timedOut || r.error) return { exit: null, secs };
10158
- if (r.exit === null || r.exit === void 0) return { exit: null, secs };
10159
- return { exit: r.exit, secs };
10700
+ return { exit: r.exit, secs, diagnostics: r.diagnostics };
10160
10701
  } finally {
10161
- rmSync3(outDir, { recursive: true, force: true });
10702
+ rmSync4(scratch, { recursive: true, force: true });
10162
10703
  }
10163
10704
  }
10705
+ function diagnosticsJson(diagnostics, includeTail) {
10706
+ if (!diagnostics) return void 0;
10707
+ const stream = (d) => ({
10708
+ captured_bytes: d.captured_bytes,
10709
+ retained_bytes: d.retained_bytes,
10710
+ truncated: d.truncated,
10711
+ ...includeTail && d.captured_bytes > 0 ? { tail: d.tail } : {}
10712
+ });
10713
+ return {
10714
+ stdout: stream(diagnostics.stdout),
10715
+ stderr: stream(diagnostics.stderr)
10716
+ };
10717
+ }
10718
+ function stageJson(result) {
10719
+ const includeTail = result.exit !== 0 || result.failure !== void 0;
10720
+ return {
10721
+ exit: result.exit,
10722
+ secs: result.secs,
10723
+ ...result.reason ? { reason: result.reason } : {},
10724
+ ...result.diagnostics ? { diagnostics: diagnosticsJson(result.diagnostics, includeTail) } : {}
10725
+ };
10726
+ }
10727
+ function renderStageDiagnostics(out2, stage, result) {
10728
+ if (!result.diagnostics) return;
10729
+ for (const line of diagnosticLines(stage, result.diagnostics)) out2(line);
10730
+ }
10164
10731
  function runVerify(opts) {
10165
10732
  const cwd = opts.cwd ?? process.cwd();
10166
10733
  const out2 = (s) => void process.stdout.write(s + "\n");
@@ -10242,11 +10809,11 @@ function runVerify(opts) {
10242
10809
  out2(`verify: cannot resolve base rev "${opts.base ?? "HEAD"}" \u2014 failing closed`);
10243
10810
  return 2;
10244
10811
  }
10245
- const visRoot = mkdtempSync2(join16(tmpdir2(), "tw-verify-vis-"));
10246
- const visDir = join16(visRoot, "t");
10812
+ const visRoot = mkdtempSync3(join17(tmpdir3(), "tw-verify-vis-"));
10813
+ const visDir = join17(visRoot, "t");
10247
10814
  const cleanup = (dirs) => {
10248
10815
  if (opts.keep) return;
10249
- for (const d of dirs) rmSync3(d, { recursive: true, force: true });
10816
+ for (const d of dirs) rmSync4(d, { recursive: true, force: true });
10250
10817
  };
10251
10818
  const protectedIgnored = (rel) => isProtected(rel, policy);
10252
10819
  const treeBefore = treeFingerprint(cwd, protectedIgnored);
@@ -10276,14 +10843,16 @@ function runVerify(opts) {
10276
10843
  stage: "visible",
10277
10844
  ...visible.resource ? { resource: visible.resource } : {},
10278
10845
  detail: visible.reason,
10846
+ diagnostics: diagnosticsJson(visible.diagnostics, true),
10279
10847
  verifier_backend: backendReport(),
10280
10848
  oracle_assurance: oracleAssuranceReport()
10281
10849
  }));
10282
10850
  } else {
10283
10851
  out2(
10284
- 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"
10852
+ 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"
10285
10853
  );
10286
10854
  if (visible.reason) out2("verify: " + visible.reason);
10855
+ renderStageDiagnostics(out2, "visible", visible);
10287
10856
  }
10288
10857
  return 2;
10289
10858
  }
@@ -10300,8 +10869,8 @@ function runVerify(opts) {
10300
10869
  out2((dependencyAfterVisible.reason ?? "dependency identity changed") + ". Failing closed, not open.");
10301
10870
  return 2;
10302
10871
  }
10303
- const priRoot = mkdtempSync2(join16(tmpdir2(), "tw-verify-pri-"));
10304
- const priDir = join16(priRoot, "t");
10872
+ const priRoot = mkdtempSync3(join17(tmpdir3(), "tw-verify-pri-"));
10873
+ const priDir = join17(priRoot, "t");
10305
10874
  let restored = [];
10306
10875
  let removedAdded = 0;
10307
10876
  try {
@@ -10332,14 +10901,16 @@ function runVerify(opts) {
10332
10901
  stage: "pristine",
10333
10902
  ...pristine.resource ? { resource: pristine.resource } : {},
10334
10903
  detail: pristine.reason,
10904
+ diagnostics: diagnosticsJson(pristine.diagnostics, true),
10335
10905
  verifier_backend: backendReport(),
10336
10906
  oracle_assurance: oracleAssuranceReport()
10337
10907
  }));
10338
10908
  } else {
10339
10909
  out2(
10340
- 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"
10910
+ 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"
10341
10911
  );
10342
10912
  if (pristine.reason) out2("verify: " + pristine.reason);
10913
+ renderStageDiagnostics(out2, "pristine", pristine);
10343
10914
  }
10344
10915
  return 2;
10345
10916
  }
@@ -10381,8 +10952,8 @@ function runVerify(opts) {
10381
10952
  base,
10382
10953
  command: cmd,
10383
10954
  budget_secs: budget,
10384
- visible: { exit: visible.exit, secs: visible.secs },
10385
- pristine: { exit: pristine.exit, secs: pristine.secs },
10955
+ visible: stageJson(visible),
10956
+ pristine: stageJson(pristine),
10386
10957
  protected_restored: restored.length,
10387
10958
  added_protected_removed: removedAdded,
10388
10959
  verifier_backend: backendReport(),
@@ -10407,6 +10978,14 @@ function runVerify(opts) {
10407
10978
  out2(
10408
10979
  dependencyEnvironment ? `dependency environment: ${dependencyEnvironmentSummary(dependencyEnvironment)}` + (opts.allowDepDrift && dependencyEnvironment.status === "unattestable" ? " (operator override)" : "") : `dependency environment: verifier-owned by ${verifierBackend.image ?? "isolated image"}`
10409
10980
  );
10981
+ if (verdict2 !== "VERIFIED") {
10982
+ if (visible.exit !== 0 || visible.failure !== void 0) {
10983
+ renderStageDiagnostics(out2, "visible", visible);
10984
+ }
10985
+ if (pristine.exit !== 0 || pristine.failure !== void 0) {
10986
+ renderStageDiagnostics(out2, "pristine", pristine);
10987
+ }
10988
+ }
10410
10989
  if (signedOff)
10411
10990
  out2(
10412
10991
  `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.`
@@ -10451,11 +11030,11 @@ function parseVerify(args) {
10451
11030
  }
10452
11031
 
10453
11032
  // src/cli/run.ts
10454
- import { execFileSync as execFileSync10, spawn, spawnSync as spawnSync3 } from "node:child_process";
11033
+ import { execFileSync as execFileSync10, spawn, spawnSync as spawnSync4 } from "node:child_process";
10455
11034
  import { randomBytes } from "node:crypto";
10456
- import { mkdtempSync as mkdtempSync3, readdirSync as readdirSync5, readFileSync as readFileSync17, readlinkSync as readlinkSync4, realpathSync as realpathSync6, rmSync as rmSync4 } from "node:fs";
10457
- import { tmpdir as tmpdir3 } from "node:os";
10458
- import { join as join17, resolve as resolve8 } from "node:path";
11035
+ import { mkdtempSync as mkdtempSync4, readdirSync as readdirSync5, readFileSync as readFileSync17, readlinkSync as readlinkSync4, realpathSync as realpathSync6, rmSync as rmSync5 } from "node:fs";
11036
+ import { tmpdir as tmpdir4 } from "node:os";
11037
+ import { join as join18, resolve as resolve8 } from "node:path";
10459
11038
  var out = (s) => process.stdout.write(s + "\n");
10460
11039
  var err2 = (s) => process.stderr.write(s + "\n");
10461
11040
  var AGENT_SUPERVISOR = String.raw`
@@ -10603,7 +11182,7 @@ function killAgentTree(pid) {
10603
11182
  if (!Number.isInteger(pid) || pid <= 1) return;
10604
11183
  if (process.platform === "win32") {
10605
11184
  try {
10606
- spawnSync3("taskkill", ["/pid", String(pid), "/t", "/f"], { stdio: "ignore", timeout: 5e3 });
11185
+ spawnSync4("taskkill", ["/pid", String(pid), "/t", "/f"], { stdio: "ignore", timeout: 5e3 });
10607
11186
  } catch {
10608
11187
  }
10609
11188
  return;
@@ -10625,11 +11204,11 @@ function killAgentTree(pid) {
10625
11204
  }
10626
11205
  }
10627
11206
  function runAgentWithBudget(argv2, cwd, budgetSecs) {
10628
- const stateDir = mkdtempSync3(join17(tmpdir3(), "tw-agent-supervisor-"));
10629
- const resultFile = join17(stateDir, "result.json");
11207
+ const stateDir = mkdtempSync4(join18(tmpdir4(), "tw-agent-supervisor-"));
11208
+ const resultFile = join18(stateDir, "result.json");
10630
11209
  let supervisorTimedOut = false;
10631
11210
  try {
10632
- const supervisor = spawnSync3(
11211
+ const supervisor = spawnSync4(
10633
11212
  process.execPath,
10634
11213
  ["-e", AGENT_SUPERVISOR, resultFile, String(budgetSecs * 1e3), ...argv2],
10635
11214
  {
@@ -10671,7 +11250,7 @@ function runAgentWithBudget(argv2, cwd, budgetSecs) {
10671
11250
  failure: state.failure ?? "agent supervisor did not produce a final status"
10672
11251
  };
10673
11252
  } finally {
10674
- rmSync4(stateDir, { recursive: true, force: true });
11253
+ rmSync5(stateDir, { recursive: true, force: true });
10675
11254
  }
10676
11255
  }
10677
11256
  var waitCell = new Int32Array(new SharedArrayBuffer(4));
@@ -10688,8 +11267,8 @@ function pidAlive2(pid) {
10688
11267
  }
10689
11268
  }
10690
11269
  function supervisedObserverLog(cwd) {
10691
- const gd = gitDir(cwd) ?? join17(cwd, ".git");
10692
- return join17(
11270
+ const gd = gitDir(cwd) ?? join18(cwd, ".git");
11271
+ return join18(
10693
11272
  gd,
10694
11273
  "tamperward",
10695
11274
  `run-observer-${process.pid}-${Date.now()}-${randomBytes(4).toString("hex")}.jsonl`
@@ -10758,7 +11337,16 @@ function observerSummary(observer, telemetry) {
10758
11337
  }
10759
11338
  function collectObserverFindings(observer, telemetry, cwd, base, head, policy) {
10760
11339
  observerSummary(observer, telemetry);
10761
- const { events } = readEvents(observer.log, 0);
11340
+ const strict = process.env.TAMPERWARD_TRANSIENT === "block";
11341
+ const drained = drainEvents(observer.log, 0);
11342
+ const { events } = drained;
11343
+ if (!drained.complete) {
11344
+ const issue = drained.issue ?? "read-stalled";
11345
+ out(
11346
+ `tamperward run \u2014 transient observer: telemetry was not fully classified (${issue}; ${drained.bytesRead}/${MAX_EVENT_SWEEP_BYTES} bytes read). ` + (strict ? "Strict transient policy fails closed." : "Observer telemetry remains advisory, so this degrades evidence but does not independently fail the envelope.")
11347
+ );
11348
+ if (strict) return { blocking: true };
11349
+ }
10762
11350
  if (events.length === 0) return { blocking: false };
10763
11351
  let persistent = /* @__PURE__ */ new Set();
10764
11352
  try {
@@ -10771,9 +11359,9 @@ function collectObserverFindings(observer, telemetry, cwd, base, head, policy) {
10771
11359
  );
10772
11360
  } catch (e) {
10773
11361
  out(
10774
- `tamperward run \u2014 transient observer: recorded ${events.length} event(s), but could not classify them against the final diff (${e instanceof Error ? e.message : String(e)}).`
11362
+ `tamperward run \u2014 transient observer: recorded ${events.length} event(s), but could not classify them against the final diff (${e instanceof Error ? e.message : String(e)}).` + (strict ? " Strict transient policy fails closed." : "")
10775
11363
  );
10776
- return { blocking: false };
11364
+ return { blocking: strict };
10777
11365
  }
10778
11366
  const finalHash = (path) => {
10779
11367
  const e = inspectRel(cwd, path);
@@ -10925,7 +11513,7 @@ function runEnvelope(opts) {
10925
11513
  const rewritesBefore = objectRewriteState(cwd);
10926
11514
  const spawnTicks = nowTicks();
10927
11515
  const agentRun = opts.agentBudget === void 0 ? (() => {
10928
- const agent = spawnSync3(opts.argv[0], opts.argv.slice(1), { cwd, stdio: "inherit" });
11516
+ const agent = spawnSync4(opts.argv[0], opts.argv.slice(1), { cwd, stdio: "inherit" });
10929
11517
  return {
10930
11518
  exit: agent.status ?? 1,
10931
11519
  timedOut: false,
@@ -10965,7 +11553,7 @@ tamperward run \u2014 agent exit ${agentExit}; DEPENDENCY_DRIFT \u2192 ENFORCEME
10965
11553
  err2("(--allow-dep-drift: proceeding anyway, on the operator's judgement.)");
10966
11554
  }
10967
11555
  const head = git3(["rev-parse", "HEAD"], cwd).trim();
10968
- const isAncestor = spawnSync3("git", ["merge-base", "--is-ancestor", base, head], { cwd, env: trustedGitEnv() });
11556
+ const isAncestor = spawnSync4("git", ["merge-base", "--is-ancestor", base, head], { cwd, env: trustedGitEnv() });
10969
11557
  if (isAncestor.status !== 0) {
10970
11558
  err2(`tamperward run: HEAD ${head.slice(0, 10)} is not a descendant of the trusted base ${base.slice(0, 10)} \u2014`);
10971
11559
  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.1",
3
+ "version": "2.16.0",
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",