tamperward 2.29.14 → 2.29.16

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 (2) hide show
  1. package/dist/cli/index.js +187 -52
  2. package/package.json +1 -1
package/dist/cli/index.js CHANGED
@@ -1381,6 +1381,38 @@ import { execFileSync as execFileSync4 } from "node:child_process";
1381
1381
  import { writeFileSync, mkdtempSync, rmSync } from "node:fs";
1382
1382
  import { tmpdir as tmpdir2 } from "node:os";
1383
1383
  import { isAbsolute as isAbsolute4, join as join5, relative, resolve as resolve2 } from "node:path";
1384
+ function sideSize(s) {
1385
+ if (s == null) return { bytes: 0, lines: 0 };
1386
+ let lines = 1;
1387
+ for (let i = 0; i < s.length; i++) if (s.charCodeAt(i) === 10) lines++;
1388
+ return { bytes: Buffer.byteLength(s), lines };
1389
+ }
1390
+ function overReconstructCeiling(before, after) {
1391
+ const maxBytes = RECONSTRUCT_MAX_BYTES();
1392
+ const maxLines = RECONSTRUCT_MAX_LINES();
1393
+ for (const [label, side] of [["before", before], ["after", after]]) {
1394
+ const { bytes, lines } = sideSize(side);
1395
+ if (bytes > maxBytes) return `${label} is ${bytes} bytes (over the ${maxBytes}-byte reconstruction budget)`;
1396
+ if (lines > maxLines) return `${label} is ${lines} lines (over the ${maxLines}-line reconstruction budget)`;
1397
+ }
1398
+ return null;
1399
+ }
1400
+ function linearFallbackHunks(before, after) {
1401
+ const b = before == null ? [] : before.split("\n");
1402
+ const a = after == null ? [] : after.split("\n");
1403
+ let p = 0;
1404
+ while (p < b.length && p < a.length && b[p] === a[p]) p++;
1405
+ let s = 0;
1406
+ while (s < b.length - p && s < a.length - p && b[b.length - 1 - s] === a[a.length - 1 - s]) s++;
1407
+ const delContent = b.slice(p, b.length - s);
1408
+ const addContent = a.slice(p, a.length - s);
1409
+ if (delContent.length === 0 && addContent.length === 0) return [];
1410
+ const lines = [
1411
+ ...delContent.map((content, i) => ({ type: "del", content, oldLine: p + 1 + i, newLine: null })),
1412
+ ...addContent.map((content, i) => ({ type: "add", content, oldLine: null, newLine: p + 1 + i }))
1413
+ ];
1414
+ return [{ oldStart: p + 1, oldLines: delContent.length, newStart: p + 1, newLines: addContent.length, lines }];
1415
+ }
1384
1416
  function asStr(v) {
1385
1417
  return typeof v === "string" ? v : "";
1386
1418
  }
@@ -1402,31 +1434,39 @@ function synthFileChange(displayPath, before, after) {
1402
1434
  if (before === null) op = "add";
1403
1435
  else if (after === null) op = "delete";
1404
1436
  else op = "modify";
1437
+ const over = overReconstructCeiling(before, after);
1438
+ if (over) {
1439
+ throw new Error(`cannot safely reconstruct the incoming edit to ${displayPath} within the hook budget: ${over}`);
1440
+ }
1441
+ const mk = (hunks) => [
1442
+ { kind: "file", path: displayPath, oldPath: null, op, before, after, binary: false, hunks }
1443
+ ];
1405
1444
  const dir = mkdtempSync(join5(tmpdir2(), "hf-"));
1406
- let raw = "";
1407
1445
  try {
1408
1446
  const a = join5(dir, "a");
1409
1447
  const b = join5(dir, "b");
1410
1448
  writeFileSync(a, before ?? "");
1411
1449
  writeFileSync(b, after ?? "");
1412
1450
  try {
1413
- raw = execFileSync4("git", ["diff", "--no-index", "--no-color", "--text", a, b], {
1451
+ const raw = execFileSync4("git", ["diff", "--no-index", "--no-color", "--text", a, b], {
1414
1452
  encoding: "utf8",
1415
- maxBuffer: 256 * 1024 * 1024
1453
+ maxBuffer: RECONSTRUCT_MAXBUFFER(),
1454
+ timeout: RECONSTRUCT_TIMEOUT_MS(),
1455
+ killSignal: "SIGKILL"
1416
1456
  });
1457
+ const first = parseDiff(raw)[0];
1458
+ return mk(first && first.kind === "file" ? first.hunks : []);
1417
1459
  } catch (e) {
1418
1460
  const failure = execFailure(e);
1419
- if (failure.status !== 1) {
1420
- throw new Error(`cannot diff the incoming edit to ${displayPath}: ${failure.code ?? failure.message}`);
1461
+ if (failure.status === 1) {
1462
+ const first = parseDiff(failure.stdout)[0];
1463
+ return mk(first && first.kind === "file" ? first.hunks : []);
1421
1464
  }
1422
- raw = failure.stdout;
1465
+ return mk(linearFallbackHunks(before, after));
1423
1466
  }
1424
1467
  } finally {
1425
1468
  rmSync(dir, { recursive: true, force: true });
1426
1469
  }
1427
- const first = parseDiff(raw)[0];
1428
- const hunks = first && first.kind === "file" ? first.hunks : [];
1429
- return [{ kind: "file", path: displayPath, oldPath: null, op, before, after, binary: false, hunks }];
1430
1470
  }
1431
1471
  function changesFromClaudeHook(input, cwd, base = cwd) {
1432
1472
  const ti = input.tool_input ?? {};
@@ -1471,12 +1511,22 @@ function changesFromClaudeHook(input, cwd, base = cwd) {
1471
1511
  return [];
1472
1512
  }
1473
1513
  }
1514
+ var num, RECONSTRUCT_TIMEOUT_MS, RECONSTRUCT_MAXBUFFER, RECONSTRUCT_MAX_BYTES, RECONSTRUCT_MAX_LINES;
1474
1515
  var init_changes = __esm({
1475
1516
  "src/adapters/claude/changes.ts"() {
1476
1517
  "use strict";
1477
1518
  init_parse();
1478
1519
  init_disk();
1479
1520
  init_narrow();
1521
+ num = (name, fallback) => {
1522
+ const raw = process.env[name];
1523
+ const n = raw === void 0 ? NaN : Number(raw);
1524
+ return Number.isFinite(n) && n > 0 ? n : fallback;
1525
+ };
1526
+ RECONSTRUCT_TIMEOUT_MS = () => num("TAMPERWARD_RECONSTRUCT_TIMEOUT_MS", 5e3);
1527
+ RECONSTRUCT_MAXBUFFER = () => num("TAMPERWARD_RECONSTRUCT_MAXBUFFER", 32 * 1024 * 1024);
1528
+ RECONSTRUCT_MAX_BYTES = () => num("TAMPERWARD_RECONSTRUCT_MAX_BYTES", 384 * 1024);
1529
+ RECONSTRUCT_MAX_LINES = () => num("TAMPERWARD_RECONSTRUCT_MAX_LINES", 4e3);
1480
1530
  }
1481
1531
  });
1482
1532
 
@@ -3337,6 +3387,7 @@ var init_no_verify = __esm({
3337
3387
 
3338
3388
  // src/ts-lazy.ts
3339
3389
  import { createRequire as createRequire2 } from "node:module";
3390
+ import { createHash as createHash2 } from "node:crypto";
3340
3391
  function loadTs() {
3341
3392
  if (loaded === null) {
3342
3393
  const required = createRequire2(import.meta.url)("typescript");
@@ -3357,17 +3408,37 @@ function nestsWithinCeiling(src) {
3357
3408
  return true;
3358
3409
  }
3359
3410
  function parseSource(fileName, src, kind) {
3360
- if (Buffer.byteLength(src) > MAX_PARSE_BYTES) return null;
3361
- if (!nestsWithinCeiling(src)) return null;
3362
- const t = loadTs();
3363
- try {
3364
- return t.createSourceFile(fileName, src, t.ScriptTarget.Latest, true, kind ?? t.ScriptKind.TS);
3365
- } catch (e) {
3366
- if (e instanceof RangeError) return null;
3367
- throw e;
3411
+ const bytes = Buffer.byteLength(src);
3412
+ if (bytes > MAX_PARSE_BYTES) return null;
3413
+ const key2 = `${kind ?? ""}\0${fileName}\0${createHash2("sha256").update(src).digest("hex")}`;
3414
+ const at = parseCache.findIndex((e) => e.key === key2);
3415
+ if (at !== -1) {
3416
+ const [entry] = parseCache.splice(at, 1);
3417
+ parseCache.push(entry);
3418
+ return entry.sf;
3419
+ }
3420
+ let sf;
3421
+ if (!nestsWithinCeiling(src)) {
3422
+ sf = null;
3423
+ } else {
3424
+ const t = loadTs();
3425
+ try {
3426
+ sf = t.createSourceFile(fileName, src, t.ScriptTarget.Latest, true, kind ?? t.ScriptKind.TS);
3427
+ } catch (e) {
3428
+ if (e instanceof RangeError) sf = null;
3429
+ else throw e;
3430
+ }
3368
3431
  }
3432
+ parseCache.push({ key: key2, sf, bytes });
3433
+ parseCacheBytes += bytes;
3434
+ while (parseCache.length > PARSE_CACHE_MAX_ENTRIES || parseCacheBytes > PARSE_CACHE_MAX_BYTES) {
3435
+ const evicted = parseCache.shift();
3436
+ if (!evicted) break;
3437
+ parseCacheBytes -= evicted.bytes;
3438
+ }
3439
+ return sf;
3369
3440
  }
3370
- var loaded, handler, ts, MAX_PARSE_BYTES, MAX_PARSE_DEPTH;
3441
+ var loaded, handler, ts, MAX_PARSE_BYTES, MAX_PARSE_DEPTH, PARSE_CACHE_MAX_ENTRIES, PARSE_CACHE_MAX_BYTES, parseCache, parseCacheBytes;
3371
3442
  var init_ts_lazy = __esm({
3372
3443
  "src/ts-lazy.ts"() {
3373
3444
  "use strict";
@@ -3390,6 +3461,10 @@ var init_ts_lazy = __esm({
3390
3461
  ts = new Proxy(/* @__PURE__ */ Object.create(null), handler);
3391
3462
  MAX_PARSE_BYTES = 4 * 1024 * 1024;
3392
3463
  MAX_PARSE_DEPTH = 256;
3464
+ PARSE_CACHE_MAX_ENTRIES = 32;
3465
+ PARSE_CACHE_MAX_BYTES = 24 * 1024 * 1024;
3466
+ parseCache = [];
3467
+ parseCacheBytes = 0;
3393
3468
  }
3394
3469
  });
3395
3470
 
@@ -6903,10 +6978,10 @@ function lex(src) {
6903
6978
  i = j + 1;
6904
6979
  continue;
6905
6980
  }
6906
- const num = src.slice(i).match(/^-?\d+(?:\.\d+)?/);
6907
- if (num) {
6908
- out3.push({ t: "num", v: num[0] });
6909
- i += num[0].length;
6981
+ const num2 = src.slice(i).match(/^-?\d+(?:\.\d+)?/);
6982
+ if (num2) {
6983
+ out3.push({ t: "num", v: num2[0] });
6984
+ i += num2[0].length;
6910
6985
  continue;
6911
6986
  }
6912
6987
  const op = src.slice(i).match(/^(?:==|!=|&&|\|\||<=|>=|[!()<>,[\]])/);
@@ -6931,13 +7006,13 @@ function foldConst(src) {
6931
7006
  let p = 0;
6932
7007
  const peek = () => toks[p];
6933
7008
  const eat = (v2) => toks[p]?.t === "op" && toks[p].v === v2 ? (p++, true) : false;
6934
- const num = (v2) => v2 === TRUTHY ? NaN : typeof v2 === "number" ? v2 : typeof v2 === "boolean" ? v2 ? 1 : 0 : v2 === null ? 0 : v2.trim() === "" ? 0 : Number(v2);
7009
+ const num2 = (v2) => v2 === TRUTHY ? NaN : typeof v2 === "number" ? v2 : typeof v2 === "boolean" ? v2 ? 1 : 0 : v2 === null ? 0 : v2.trim() === "" ? 0 : Number(v2);
6935
7010
  const eq = (a, b) => {
6936
7011
  if (a === TRUTHY || b === TRUTHY) return void 0;
6937
7012
  if (typeof a === "string" && typeof b === "string") return a.toLowerCase() === b.toLowerCase();
6938
7013
  if (a === null && b === null) return true;
6939
- const x = num(a);
6940
- const y2 = num(b);
7014
+ const x = num2(a);
7015
+ const y2 = num2(b);
6941
7016
  return !Number.isNaN(x) && x === y2;
6942
7017
  };
6943
7018
  const or = () => {
@@ -6975,8 +7050,8 @@ function foldConst(src) {
6975
7050
  const e = eq(l, r);
6976
7051
  l = e === void 0 ? void 0 : t.v === "==" ? e : !e;
6977
7052
  } else {
6978
- const x = num(l);
6979
- const y2 = num(r);
7053
+ const x = num2(l);
7054
+ const y2 = num2(r);
6980
7055
  if (Number.isNaN(x) || Number.isNaN(y2)) l = void 0;
6981
7056
  else l = t.v === "<" ? x < y2 : t.v === ">" ? x > y2 : t.v === "<=" ? x <= y2 : x >= y2;
6982
7057
  }
@@ -9914,6 +9989,14 @@ var init_test_content_removal = __esm({
9914
9989
  KeptPool = class {
9915
9990
  text = "";
9916
9991
  inLiteral = [];
9992
+ // O(1) fast path (#517): the ws-text of each kept significant line whose retained
9993
+ // text BEGINS at a code position (inLiteral 0). A removed line whose ws-text equals
9994
+ // one of these is kept — necessarily the SAME answer `indexOf` gives, since that exact
9995
+ // text then occurs in `text` at a code position — but in O(1) instead of scanning the
9996
+ // whole concatenated pool per removed line (a 20k-line high-churn spec was O(n²) here,
9997
+ // ~13s, #517). The substring scan below is unchanged and still covers rewrapped/joined
9998
+ // lines and moved fixtures, so `keeps` returns exactly what it did before.
9999
+ wholeCodeLines = /* @__PURE__ */ new Set();
9917
10000
  addLive(src, path) {
9918
10001
  const p = partition(src, path);
9919
10002
  const lang = langOf(path);
@@ -9922,11 +10005,16 @@ var init_test_content_removal = __esm({
9922
10005
  if (i < p.live.length && p.live[i] !== "\n") continue;
9923
10006
  const line = p.live.slice(lineStart, i);
9924
10007
  if (isSignificantLine(line.trim(), lang)) {
10008
+ let lineWs = "";
10009
+ let startsAtCode = false;
9925
10010
  for (let j = lineStart; j < i; j++) {
9926
10011
  if (/\s/.test(p.live[j])) continue;
10012
+ if (lineWs.length === 0) startsAtCode = p.inLiteral[j] === 0;
9927
10013
  this.text += p.live[j];
9928
10014
  this.inLiteral.push(p.inLiteral[j]);
10015
+ lineWs += p.live[j];
9929
10016
  }
10017
+ if (lineWs.length > 0 && startsAtCode) this.wholeCodeLines.add(lineWs);
9930
10018
  }
9931
10019
  lineStart = i + 1;
9932
10020
  }
@@ -9938,6 +10026,7 @@ var init_test_content_removal = __esm({
9938
10026
  }
9939
10027
  keeps(wsLine) {
9940
10028
  if (wsLine.length === 0) return true;
10029
+ if (this.wholeCodeLines.has(wsLine)) return true;
9941
10030
  let at = this.text.indexOf(wsLine);
9942
10031
  while (at !== -1) {
9943
10032
  if (this.inLiteral[at] === 0) return true;
@@ -10763,7 +10852,7 @@ var init_policy_load = __esm({
10763
10852
  });
10764
10853
 
10765
10854
  // src/signoff.ts
10766
- import { createHash as createHash2 } from "node:crypto";
10855
+ import { createHash as createHash3 } from "node:crypto";
10767
10856
  import { appendFileSync, mkdirSync, readFileSync as readFileSync6, existsSync as existsSync6 } from "node:fs";
10768
10857
  import { dirname as dirname2, join as join9 } from "node:path";
10769
10858
  function ledgerEntryFrom(value) {
@@ -10775,7 +10864,7 @@ function ledgerEntryFrom(value) {
10775
10864
  return { rule, ...file !== void 0 ? { file } : {}, fingerprint: fingerprint2, reason, recordedAt, expiresAt };
10776
10865
  }
10777
10866
  function fingerprint(rule, file, evidence) {
10778
- return createHash2("sha256").update(`${rule}\0${file ?? ""}\0${evidence}`).digest("hex").slice(0, 16);
10867
+ return createHash3("sha256").update(`${rule}\0${file ?? ""}\0${evidence}`).digest("hex").slice(0, 16);
10779
10868
  }
10780
10869
  function ledgerPath(cwd, policy) {
10781
10870
  const rel = policy.signoff?.ledger ?? ".tamperward/ledger.jsonl";
@@ -11335,7 +11424,7 @@ var init_session = __esm({
11335
11424
  });
11336
11425
 
11337
11426
  // src/effect.ts
11338
- import { createHash as createHash3 } from "node:crypto";
11427
+ import { createHash as createHash4 } from "node:crypto";
11339
11428
  import { existsSync as existsSync8, lstatSync as lstatSync3, mkdirSync as mkdirSync3, readdirSync, readFileSync as readFileSync8, renameSync as renameSync2, rmSync as rmSync3, writeFileSync as writeFileSync3 } from "node:fs";
11340
11429
  import { join as join11 } from "node:path";
11341
11430
  function ptreeFrom(value) {
@@ -11478,19 +11567,19 @@ var init_effect = __esm({
11478
11567
  init_narrow();
11479
11568
  UNSAFE2 = /[^A-Za-z0-9._-]/g;
11480
11569
  SKIP_DIRS2 = /* @__PURE__ */ new Set([".git", "node_modules", ".hg", ".svn"]);
11481
- sha = (buf) => createHash3("sha256").update(buf).digest("hex").slice(0, 16);
11570
+ sha = (buf) => createHash4("sha256").update(buf).digest("hex").slice(0, 16);
11482
11571
  }
11483
11572
  });
11484
11573
 
11485
11574
  // src/cli/watch.ts
11486
11575
  import { appendFileSync as appendFileSync3, lstatSync as lstatSync4, mkdirSync as mkdirSync4, readFileSync as readFileSync9, readdirSync as readdirSync2, watch, writeFileSync as writeFileSync4 } from "node:fs";
11487
11576
  import { join as join12 } from "node:path";
11488
- import { createHash as createHash4 } from "node:crypto";
11577
+ import { createHash as createHash5 } from "node:crypto";
11489
11578
  function snap(abs) {
11490
11579
  const e = inspectPath(abs);
11491
11580
  if (e.kind === "absent" || e.kind === "unreadable") return { mode: null, size: null, hash: null };
11492
11581
  if (e.kind !== "file" || e.content == null) return { mode: e.mode, size: null, hash: null };
11493
- const hash = createHash4("sha256").update(e.content).digest("hex").slice(0, 16);
11582
+ const hash = createHash5("sha256").update(e.content).digest("hex").slice(0, 16);
11494
11583
  return { mode: e.mode, size: e.size, hash };
11495
11584
  }
11496
11585
  function defaultEventLog(cwd) {
@@ -12003,15 +12092,15 @@ var init_fs_events = __esm({
12003
12092
  });
12004
12093
 
12005
12094
  // src/cli/audit.ts
12006
- import { createHash as createHash5, randomBytes } from "node:crypto";
12095
+ import { createHash as createHash6, randomBytes } from "node:crypto";
12007
12096
  import { appendFileSync as appendFileSync4, existsSync as existsSync9, mkdirSync as mkdirSync5, readFileSync as readFileSync10 } from "node:fs";
12008
12097
  import { dirname as dirname4, isAbsolute as isAbsolute8, join as join13, resolve as resolve4 } from "node:path";
12009
12098
  function sessionHash(sessionId) {
12010
12099
  if (!sessionId) return void 0;
12011
- return "sha256:" + createHash5("sha256").update(sessionId).digest("hex").slice(0, 24);
12100
+ return "sha256:" + createHash6("sha256").update(sessionId).digest("hex").slice(0, 24);
12012
12101
  }
12013
12102
  function eventId() {
12014
- return "sha256:" + createHash5("sha256").update(randomBytes(32)).digest("hex").slice(0, 32);
12103
+ return "sha256:" + createHash6("sha256").update(randomBytes(32)).digest("hex").slice(0, 32);
12015
12104
  }
12016
12105
  function configuredAuditPath(cwd, env = process.env) {
12017
12106
  const value = env[AUDIT_LOG_ENV];
@@ -12240,7 +12329,7 @@ var init_audit = __esm({
12240
12329
 
12241
12330
  // src/cli/hook.ts
12242
12331
  import { readFileSync as readFileSync11, appendFileSync as appendFileSync5, writeFileSync as writeFileSync5, existsSync as existsSync10 } from "node:fs";
12243
- import { createHash as createHash6 } from "node:crypto";
12332
+ import { createHash as createHash7 } from "node:crypto";
12244
12333
  function setSnapshotCache(cache3) {
12245
12334
  snapshotCache = cache3;
12246
12335
  }
@@ -12478,7 +12567,7 @@ function sanctionPredictedWrites(cwd, sessionId, policy, changes) {
12478
12567
  let dirty = false;
12479
12568
  for (const c of changes) {
12480
12569
  if (c.kind !== "file" || c.after == null || !isProtected(c.path, policy)) continue;
12481
- const hash = createHash6("sha256").update(c.after).digest("hex").slice(0, 16);
12570
+ const hash = createHash7("sha256").update(c.after).digest("hex").slice(0, 16);
12482
12571
  const prev = tree[c.path];
12483
12572
  tree[c.path] = {
12484
12573
  hash,
@@ -13325,7 +13414,7 @@ var init_safe_write = __esm({
13325
13414
 
13326
13415
  // src/cli/init.ts
13327
13416
  import { execFileSync as execFileSync6 } from "node:child_process";
13328
- import { createHash as createHash7 } from "node:crypto";
13417
+ import { createHash as createHash8 } from "node:crypto";
13329
13418
  import { existsSync as existsSync12, mkdirSync as mkdirSync7, readFileSync as readFileSync13, realpathSync as realpathSync5 } from "node:fs";
13330
13419
  import { basename as basename3, dirname as dirname5, isAbsolute as isAbsolute9, join as join15, relative as relative3, resolve as resolve5 } from "node:path";
13331
13420
  function gitConfig(cwd, key2) {
@@ -13375,7 +13464,7 @@ function stalePin(command) {
13375
13464
  return pin !== null && (pin !== TW_VERSION || !OURS.test(command));
13376
13465
  }
13377
13466
  function bodyHash(body) {
13378
- return createHash7("sha256").update(body).digest("hex").slice(0, 16);
13467
+ return createHash8("sha256").update(body).digest("hex").slice(0, 16);
13379
13468
  }
13380
13469
  function workflowFile(body) {
13381
13470
  return `${WORKFLOW_MARK} v${TW_VERSION} sha256:${bodyHash(body)}
@@ -14120,7 +14209,7 @@ jobs:
14120
14209
 
14121
14210
  // src/fingerprint.ts
14122
14211
  import { execFileSync as execFileSync7 } from "node:child_process";
14123
- import { createHash as createHash8 } from "node:crypto";
14212
+ import { createHash as createHash9 } from "node:crypto";
14124
14213
  function treeFingerprint(cwd, keepIgnored) {
14125
14214
  const listed = execFileSync7("git", ["ls-files", "-z", "--cached", "--others", "--exclude-standard"], {
14126
14215
  cwd,
@@ -14129,7 +14218,7 @@ function treeFingerprint(cwd, keepIgnored) {
14129
14218
  env: trustedGitEnv()
14130
14219
  }).split("\0").filter(Boolean);
14131
14220
  const files = [.../* @__PURE__ */ new Set([...listed, ...keepIgnored ? ignoredPaths({ cwd }, keepIgnored) : []])].sort();
14132
- const h = createHash8("sha256");
14221
+ const h = createHash9("sha256");
14133
14222
  for (const rel of files) {
14134
14223
  h.update(rel);
14135
14224
  h.update("\0");
@@ -14153,7 +14242,7 @@ var init_fingerprint = __esm({
14153
14242
  });
14154
14243
 
14155
14244
  // src/dependency-env.ts
14156
- import { createHash as createHash9 } from "node:crypto";
14245
+ import { createHash as createHash10 } from "node:crypto";
14157
14246
  import {
14158
14247
  existsSync as existsSync13,
14159
14248
  lstatSync as lstatSync8,
@@ -14306,7 +14395,7 @@ function fingerprintRoot(root, cwd, h) {
14306
14395
  walk2(root.realPath, "");
14307
14396
  }
14308
14397
  function snapshot(roots, probes, cwd) {
14309
- const h = createHash9("sha256");
14398
+ const h = createHash10("sha256");
14310
14399
  for (const probe of probes) {
14311
14400
  h.update(
14312
14401
  "probe\0" + probe.kind + "\0" + probe.path + "\0" + (existsSync13(probe.path) ? "present" : "absent") + "\0"
@@ -14640,6 +14729,7 @@ function parseCapturedSupervisorResult(stdoutText) {
14640
14729
  signal: typeof raw.signal === "string" ? raw.signal : null,
14641
14730
  timedOut: Boolean(raw.timedOut),
14642
14731
  ...typeof raw.error === "string" && raw.error ? { error: raw.error } : {},
14732
+ ...raw.pipeHeldOpen === true ? { pipeHeldOpen: true } : {},
14643
14733
  diagnostics
14644
14734
  };
14645
14735
  }
@@ -14657,6 +14747,7 @@ function runCapturedProcessSync(executable, args, opts) {
14657
14747
  timeoutMs: opts.timeoutMs,
14658
14748
  detached: Boolean(opts.detached),
14659
14749
  killGroupOnFinish: Boolean(opts.killGroupOnFinish),
14750
+ drainMs: opts.drainMs ?? 1e3,
14660
14751
  captureBytes: DIAGNOSTIC_TAIL_BYTES
14661
14752
  }),
14662
14753
  { mode: 384 }
@@ -14789,6 +14880,7 @@ let done = false;
14789
14880
  let timedOut = false;
14790
14881
  let exitInfo = { exit: null, signal: null };
14791
14882
  let descendantTracker = null;
14883
+ let drainTimer = null;
14792
14884
 
14793
14885
  function killOwned() {
14794
14886
  if (!child || !child.pid) return;
@@ -14810,6 +14902,7 @@ function finish(extra) {
14810
14902
  if (done) return;
14811
14903
  done = true;
14812
14904
  if (descendantTracker) clearInterval(descendantTracker);
14905
+ if (drainTimer) clearTimeout(drainTimer);
14813
14906
  try {
14814
14907
  // stdout is reserved for the trusted supervisor result. Candidate suite
14815
14908
  // stdout/stderr are separate pipes and are never inherited here.
@@ -14864,12 +14957,19 @@ if (child) {
14864
14957
  signal: signal == null ? null : String(signal),
14865
14958
  };
14866
14959
  if (cfg.killGroupOnFinish) killOwned();
14960
+ // A leaked descendant can hold a pipe open so 'close' never fires; finish off
14961
+ // exit after a bounded drain instead of hanging. 'close' still wins if first.
14962
+ if (!drainTimer) {
14963
+ drainTimer = setTimeout(() => finish({ pipeHeldOpen: true }), Number(cfg.drainMs));
14964
+ drainTimer.unref();
14965
+ }
14867
14966
  });
14868
14967
 
14869
14968
  // close fires after the stdio pipes close, so the retained tails include all
14870
14969
  // output that was drained before owned descendants were terminated.
14871
14970
  child.on('close', (code, signal) => {
14872
14971
  clearTimeout(timer);
14972
+ if (drainTimer) clearTimeout(drainTimer);
14873
14973
  if (exitInfo.exit === null && code != null) exitInfo.exit = Number(code);
14874
14974
  if (exitInfo.signal === null && signal != null) exitInfo.signal = String(signal);
14875
14975
  finish({});
@@ -14880,7 +14980,7 @@ if (child) {
14880
14980
  });
14881
14981
 
14882
14982
  // src/verifier-backend.ts
14883
- import { createHash as createHash10, randomUUID } from "node:crypto";
14983
+ import { createHash as createHash11, randomUUID } from "node:crypto";
14884
14984
  import { spawnSync as spawnSync2 } from "node:child_process";
14885
14985
  import { accessSync as accessSync2, constants as constants3, readFileSync as readFileSync16, realpathSync as realpathSync7, statSync as statSync4 } from "node:fs";
14886
14986
  import { delimiter as delimiter2, dirname as dirname7, join as join18, resolve as resolve7 } from "node:path";
@@ -14927,7 +15027,7 @@ function findExecutable(name) {
14927
15027
  return null;
14928
15028
  }
14929
15029
  function fileSha256(path) {
14930
- return createHash10("sha256").update(readFileSync16(path)).digest("hex");
15030
+ return createHash11("sha256").update(readFileSync16(path)).digest("hex");
14931
15031
  }
14932
15032
  function engineClientEnv() {
14933
15033
  return {
@@ -15360,7 +15460,7 @@ var init_verifier_backend = __esm({
15360
15460
 
15361
15461
  // src/cli/verify.ts
15362
15462
  import { execFileSync as execFileSync8, spawnSync as spawnSync3 } from "node:child_process";
15363
- import { createHash as createHash11 } from "node:crypto";
15463
+ import { createHash as createHash12 } from "node:crypto";
15364
15464
  import { chmodSync as chmodSync2, cpSync, lstatSync as lstatSync9, mkdirSync as mkdirSync8, mkdtempSync as mkdtempSync3, readFileSync as readFileSync17, readlinkSync as readlinkSync3, rmSync as rmSync7, symlinkSync, writeFileSync as writeFileSync9, existsSync as existsSync14 } from "node:fs";
15365
15465
  import { tmpdir as tmpdir4 } from "node:os";
15366
15466
  import { dirname as dirname8, isAbsolute as isAbsolute11, join as join19, relative as relative5, resolve as resolve8, sep as sep3 } from "node:path";
@@ -15574,7 +15674,7 @@ function baseEntries(base, cwd) {
15574
15674
  });
15575
15675
  }
15576
15676
  function overlayDigest(dest, paths) {
15577
- const h = createHash11("sha256");
15677
+ const h = createHash12("sha256");
15578
15678
  for (const rel of [...paths].sort()) {
15579
15679
  h.update(rel);
15580
15680
  h.update("\0");
@@ -15714,7 +15814,12 @@ function runLocalSuite(dir, cmd, budgetSecs) {
15714
15814
  diagnostics: r.diagnostics
15715
15815
  };
15716
15816
  }
15717
- return { exit: r.exit, secs, diagnostics: r.diagnostics };
15817
+ return {
15818
+ exit: r.exit,
15819
+ secs,
15820
+ ...r.pipeHeldOpen ? { pipeHeldOpen: true } : {},
15821
+ diagnostics: r.diagnostics
15822
+ };
15718
15823
  } finally {
15719
15824
  rmSync7(scratch, { recursive: true, force: true });
15720
15825
  }
@@ -15873,7 +15978,29 @@ function runVerify(opts) {
15873
15978
  }
15874
15979
  const frozenNodeModules = dependencyEnvironment ? dependencyEnvironment.roots.find((root) => root.kind === "node_modules")?.realPath ?? null : null;
15875
15980
  const checkDeps = () => dependencyEnvironment ? checkDependencyEnvironment(cwd, dependencyEnvironment) : { ok: true };
15876
- const runStage = (dir) => isolated ? runContainerStage(verifierBackend, dir, cmd, budget) : runLocalSuite(dir, cmd, budget);
15981
+ const runStage = opts.runStage ?? ((dir) => isolated ? runContainerStage(verifierBackend, dir, cmd, budget) : runLocalSuite(dir, cmd, budget));
15982
+ const notQuiescent = (stage, r) => {
15983
+ const detail = `the ${stage} suite exited (code ${r.exit ?? "unknown"}) but left a process holding stdout open after termination was attempted; the stage is not quiescent, so the run cannot be certified`;
15984
+ opts.onVerdict?.({ verdict: "CANNOT_VERIFY", reason: "VERIFIER_BACKEND_RUNTIME_FAILURE", detail });
15985
+ if (opts.json) {
15986
+ out3(JSON.stringify({
15987
+ schema_version: MACHINE_SCHEMA_VERSION,
15988
+ verdict: "CANNOT_VERIFY",
15989
+ reason: "VERIFIER_BACKEND_RUNTIME_FAILURE",
15990
+ stage,
15991
+ ...caseReport(),
15992
+ detail,
15993
+ diagnostics: diagnosticsJson(r.diagnostics, true),
15994
+ verifier_backend: backendReport(),
15995
+ oracle_assurance: oracleAssuranceReport()
15996
+ }));
15997
+ } else {
15998
+ out3(`verify: the ${stage} suite exited but left a process holding stdout open \u2014 the stage is not quiescent, failing closed`);
15999
+ out3("verify: " + detail);
16000
+ renderStageDiagnostics(out3, stage, r);
16001
+ }
16002
+ return 2;
16003
+ };
15877
16004
  let base;
15878
16005
  try {
15879
16006
  base = resolveBase(opts.base ?? "HEAD", cwd);
@@ -15933,6 +16060,10 @@ function runVerify(opts) {
15933
16060
  );
15934
16061
  }
15935
16062
  const visible = runStage(visDir);
16063
+ if (visible.pipeHeldOpen) {
16064
+ cleanup([visRoot]);
16065
+ return notQuiescent("visible", visible);
16066
+ }
15936
16067
  if (visible.failure === "backend" || visible.failure === "resource") {
15937
16068
  cleanup([visRoot]);
15938
16069
  const exhausted = visible.failure === "resource";
@@ -16002,6 +16133,10 @@ function runVerify(opts) {
16002
16133
  }
16003
16134
  const overlayBefore = overlayDigest(priDir, restored);
16004
16135
  const pristine = runStage(priDir);
16136
+ if (pristine.pipeHeldOpen) {
16137
+ cleanup([visRoot, priRoot]);
16138
+ return notQuiescent("pristine", pristine);
16139
+ }
16005
16140
  if (pristine.failure === "backend" || pristine.failure === "resource") {
16006
16141
  cleanup([visRoot, priRoot]);
16007
16142
  const exhausted = pristine.failure === "resource";
@@ -19101,7 +19236,7 @@ var init_capture = __esm({
19101
19236
  });
19102
19237
 
19103
19238
  // src/research/manifest.ts
19104
- import { createHash as createHash13 } from "node:crypto";
19239
+ import { createHash as createHash14 } from "node:crypto";
19105
19240
  import { readFileSync as readFileSync23 } from "node:fs";
19106
19241
  import { dirname as dirname10, resolve as resolve14 } from "node:path";
19107
19242
  function taskFrom(raw, index, manifestDir) {
@@ -19159,7 +19294,7 @@ function readManifest(path) {
19159
19294
  if (seen.has(t.id)) throw new ResearchError(`task manifest ${abs}: duplicate task id "${t.id}"`);
19160
19295
  seen.add(t.id);
19161
19296
  }
19162
- return { path: abs, sha256: createHash13("sha256").update(bytes).digest("hex"), tasks };
19297
+ return { path: abs, sha256: createHash14("sha256").update(bytes).digest("hex"), tasks };
19163
19298
  }
19164
19299
  var MANIFEST_VERSION;
19165
19300
  var init_manifest = __esm({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tamperward",
3
- "version": "2.29.14",
3
+ "version": "2.29.16",
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",