tamperward 2.37.1 → 2.37.2

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 +310 -63
  2. package/package.json +1 -1
package/dist/cli/index.js CHANGED
@@ -5300,13 +5300,13 @@ function merge(a, b) {
5300
5300
  function literals(e) {
5301
5301
  if (ts.isStringLiteral(e)) return { items: [e.text], opaque: false };
5302
5302
  if (!ts.isArrayLiteralExpression(e)) return { items: [], opaque: true };
5303
- const items = [];
5303
+ const items2 = [];
5304
5304
  let opaque = false;
5305
5305
  for (const el of e.elements) {
5306
- if (ts.isStringLiteral(el) || ts.isNoSubstitutionTemplateLiteral(el)) items.push(el.text);
5306
+ if (ts.isStringLiteral(el) || ts.isNoSubstitutionTemplateLiteral(el)) items2.push(el.text);
5307
5307
  else opaque = true;
5308
5308
  }
5309
- return { items, opaque };
5309
+ return { items: items2, opaque };
5310
5310
  }
5311
5311
  function parseLists(src) {
5312
5312
  const res = { collectFrom: null, collectKey: "collectCoverageFrom", ignore: [], ignoreRe: [], opaqueCollect: false, opaqueIgnore: false };
@@ -5319,13 +5319,13 @@ function parseLists(src) {
5319
5319
  const k = keyName(node.name);
5320
5320
  const vitestCoverage = ownerKey(node) === "coverage";
5321
5321
  if (k === "collectCoverageFrom" || k === "include" && vitestCoverage) {
5322
- const { items, opaque } = literals(resolve23(node.initializer));
5323
- res.collectFrom = items;
5322
+ const { items: items2, opaque } = literals(resolve23(node.initializer));
5323
+ res.collectFrom = items2;
5324
5324
  res.collectKey = k === "include" ? "coverage.include" : k;
5325
5325
  if (opaque) res.opaqueCollect = true;
5326
5326
  } else if (k === "coveragePathIgnorePatterns" || k === "exclude" && vitestCoverage) {
5327
- const { items, opaque } = literals(resolve23(node.initializer));
5328
- (k === "exclude" ? res.ignore : res.ignoreRe).push(...items);
5327
+ const { items: items2, opaque } = literals(resolve23(node.initializer));
5328
+ (k === "exclude" ? res.ignore : res.ignoreRe).push(...items2);
5329
5329
  if (opaque) res.opaqueIgnore = true;
5330
5330
  }
5331
5331
  }
@@ -5923,15 +5923,15 @@ function keyName2(name) {
5923
5923
  function literals2(e) {
5924
5924
  if (isStr(e)) return { items: [e.text], opaque: false };
5925
5925
  if (!ts.isArrayLiteralExpression(e)) return { items: [], opaque: true };
5926
- const items = [];
5926
+ const items2 = [];
5927
5927
  let opaque = false;
5928
5928
  for (const el of e.elements) {
5929
- if (isStr(el)) items.push(el.text);
5929
+ if (isStr(el)) items2.push(el.text);
5930
5930
  else if (ts.isSpreadElement(el) && /^(?:configDefaults\.|default)(?:exclude|include)$/i.test(el.expression.getText())) {
5931
- items.push(.../exclude$/i.test(el.expression.getText()) ? VITEST_DEFAULT_EXCLUDE : VITEST_DEFAULT_INCLUDE);
5931
+ items2.push(.../exclude$/i.test(el.expression.getText()) ? VITEST_DEFAULT_EXCLUDE : VITEST_DEFAULT_INCLUDE);
5932
5932
  } else opaque = true;
5933
5933
  }
5934
- return { items, opaque };
5934
+ return { items: items2, opaque };
5935
5935
  }
5936
5936
  function ownerKey2(node) {
5937
5937
  const obj = node.parent;
@@ -6054,11 +6054,11 @@ function collect(root, runner) {
6054
6054
  const sel = empty();
6055
6055
  const seen = /* @__PURE__ */ new Set();
6056
6056
  const take = (key2, e) => {
6057
- const { items, opaque } = literals2(e);
6057
+ const { items: items2, opaque } = literals2(e);
6058
6058
  if (opaque || seen.has(key2)) sel.opaque = true;
6059
6059
  seen.add(key2);
6060
6060
  sel.present = true;
6061
- return items.map(stripRootDir);
6061
+ return items2.map(stripRootDir);
6062
6062
  };
6063
6063
  const ignores = (key2, e) => {
6064
6064
  const entries = take(key2, e).map((pattern) => ({ pattern, key: key2 }));
@@ -10701,10 +10701,10 @@ function validate(r, where) {
10701
10701
  throw new PolicyError(`${where}: ${msg}`);
10702
10702
  }
10703
10703
  const show = (v) => JSON.stringify(v) ?? String(v);
10704
- function refuseNegated(field, list3) {
10704
+ function refuseNegated(field2, list3) {
10705
10705
  const neg = list3.find(isNegatedGlob);
10706
10706
  if (neg !== void 0) {
10707
- bad2(`${field}: negated glob ${show(neg)} is not allowed \u2014 a "!" pattern matches every path, so it would ${field === "ignore" ? "ignore every file" : field.startsWith("protected.") ? `put every file in ${field}` : "blind the rule on every file"}; list the paths to include instead`);
10707
+ bad2(`${field2}: negated glob ${show(neg)} is not allowed \u2014 a "!" pattern matches every path, so it would ${field2 === "ignore" ? "ignore every file" : field2.startsWith("protected.") ? `put every file in ${field2}` : "blind the rule on every file"}; list the paths to include instead`);
10708
10708
  }
10709
10709
  }
10710
10710
  const out3 = {};
@@ -20577,7 +20577,52 @@ function pairRecordFrom(raw, where = "record") {
20577
20577
  }
20578
20578
  };
20579
20579
  }
20580
- var SHA_RE, SHA256_RE;
20580
+ function onlyFields(raw, allowed, where) {
20581
+ if (!isRecord(raw)) return;
20582
+ for (const key2 of Object.keys(raw)) {
20583
+ if (!allowed.includes(key2)) bad(`${where}.${key2} is not a field of the v1 pair record; refusing to carry it as portable evidence`);
20584
+ }
20585
+ }
20586
+ function portableObject(raw, spec2, where) {
20587
+ if (!isRecord(raw)) bad(`${where} is not an object`);
20588
+ for (const [key2, value] of Object.entries(raw)) {
20589
+ const field2 = spec2.get(key2);
20590
+ const at = `${where}.${key2}`;
20591
+ if (field2 === void 0) bad(`${at} is not a field of the v1 run document; refusing to carry it as portable evidence`);
20592
+ if (field2 === "scalar") {
20593
+ if (value !== null && typeof value === "object") bad(`${at} is not a scalar; refusing to carry it as portable evidence`);
20594
+ } else if ("items" in field2) {
20595
+ if (!Array.isArray(value)) bad(`${at} is not an array`);
20596
+ value.forEach((item, i) => portableObject(item, field2.items, `${at}[${i}]`));
20597
+ } else {
20598
+ portableObject(value, field2.fields, at);
20599
+ }
20600
+ }
20601
+ }
20602
+ function portablePairRecordFrom(raw, where = "record") {
20603
+ onlyFields(raw, PAIR_FIELDS, where);
20604
+ if (isRecord(raw)) {
20605
+ onlyFields(raw.adapter, ADAPTER_FIELDS, `${where}.adapter`);
20606
+ onlyFields(raw.arms, ARMS_FIELDS, `${where}.arms`);
20607
+ if (isRecord(raw.arms)) {
20608
+ for (const arm of ARMS_FIELDS) {
20609
+ const trajectory = raw.arms[arm];
20610
+ const at = `${where}.arms.${arm}`;
20611
+ onlyFields(trajectory, TRAJECTORY_FIELDS, at);
20612
+ if (isRecord(trajectory)) {
20613
+ onlyFields(trajectory.agent, AGENT_FIELDS, `${at}.agent`);
20614
+ onlyFields(trajectory.treatment, TREATMENT_FIELDS, `${at}.treatment`);
20615
+ if (isRecord(trajectory.treatment) && trajectory.treatment.envelope !== null && trajectory.treatment.envelope !== void 0) {
20616
+ portableObject(trajectory.treatment.envelope, RUN_DOCUMENT_FIELDS, `${at}.treatment.envelope`);
20617
+ }
20618
+ onlyFields(trajectory.outcome, OUTCOME_FIELDS, `${at}.outcome`);
20619
+ }
20620
+ }
20621
+ }
20622
+ }
20623
+ return pairRecordFrom(raw, where);
20624
+ }
20625
+ var SHA_RE, SHA256_RE, PAIR_FIELDS, ADAPTER_FIELDS, ARMS_FIELDS, TRAJECTORY_FIELDS, AGENT_FIELDS, TREATMENT_FIELDS, OUTCOME_FIELDS, fields, items, RUN_DOCUMENT_FIELDS;
20581
20626
  var init_record = __esm({
20582
20627
  "src/research/record.ts"() {
20583
20628
  "use strict";
@@ -20587,6 +20632,82 @@ var init_record = __esm({
20587
20632
  init_derive();
20588
20633
  SHA_RE = /^[0-9a-f]{40,64}$/;
20589
20634
  SHA256_RE = /^[0-9a-f]{64}$/;
20635
+ PAIR_FIELDS = [
20636
+ "schema_version",
20637
+ "command",
20638
+ "document",
20639
+ "task",
20640
+ "pair",
20641
+ "adapter",
20642
+ "model",
20643
+ "tamperward_version",
20644
+ "agent_argv",
20645
+ "agent_budget",
20646
+ "manifest_sha256",
20647
+ "verify_command",
20648
+ "arms"
20649
+ ];
20650
+ ADAPTER_FIELDS = ["name", "layers"];
20651
+ ARMS_FIELDS = ["ungated", "gated"];
20652
+ TRAJECTORY_FIELDS = [
20653
+ "arm",
20654
+ "workspace",
20655
+ "base",
20656
+ "head",
20657
+ "started_at",
20658
+ "finished_at",
20659
+ "agent",
20660
+ "treatment",
20661
+ "outcome",
20662
+ "released_green",
20663
+ "measured",
20664
+ "unmeasurable"
20665
+ ];
20666
+ AGENT_FIELDS = ["exit_code", "signal", "timed_out", "failure"];
20667
+ TREATMENT_FIELDS = ["verdict", "exit_code", "complete", "disposition", "envelope"];
20668
+ OUTCOME_FIELDS = [
20669
+ "verify_verdict",
20670
+ "visible_exit",
20671
+ "pristine_exit",
20672
+ "visible_green",
20673
+ "pristine_green",
20674
+ "masked_failure",
20675
+ "surviving_protected_mutations",
20676
+ "warn_findings",
20677
+ "rules",
20678
+ "honest_completion"
20679
+ ];
20680
+ fields = (entries) => ({ fields: new Map(entries) });
20681
+ items = (entries) => ({ items: new Map(entries) });
20682
+ RUN_DOCUMENT_FIELDS = /* @__PURE__ */ new Map([
20683
+ ["schema_version", "scalar"],
20684
+ ["verdict", "scalar"],
20685
+ ["exit_code", "scalar"],
20686
+ ["complete", "scalar"],
20687
+ ["base", "scalar"],
20688
+ ["head", "scalar"],
20689
+ ["reason", "scalar"],
20690
+ ["agent", fields([["exit_code", "scalar"], ["timed_out", "scalar"], ["lifecycle_owned", "scalar"], ["budget_secs", "scalar"]])],
20691
+ ["checks", fields([["diff", "scalar"], ["worktree", "scalar"], ["verify", "scalar"]])],
20692
+ ["verifier_backend", fields([
20693
+ ["kind", "scalar"],
20694
+ ["trust", "scalar"],
20695
+ ["available", "scalar"],
20696
+ ["image", "scalar"],
20697
+ ["engine", "scalar"],
20698
+ ["reason", "scalar"],
20699
+ ["resources", fields([["memory_bytes", "scalar"], ["memory_swap_bytes", "scalar"], ["cpus", "scalar"], ["pids", "scalar"]])]
20700
+ ])],
20701
+ ["dependency_environment", fields([
20702
+ ["status", "scalar"],
20703
+ ["fingerprint", "scalar"],
20704
+ ["reason", "scalar"],
20705
+ ["image", "scalar"],
20706
+ ["roots", items([["kind", "scalar"], ["path", "scalar"]])],
20707
+ ["diagnostics", fields([["full_snapshots", "scalar"], ["reused_snapshots", "scalar"], ["total_ms", "scalar"]])]
20708
+ ])],
20709
+ ["observer", fields([["enabled", "scalar"], ["blocking", "scalar"]])]
20710
+ ]);
20590
20711
  }
20591
20712
  });
20592
20713
 
@@ -21122,27 +21243,95 @@ var init_run2 = __esm({
21122
21243
  }
21123
21244
  });
21124
21245
 
21125
- // src/research/summarize.ts
21126
- import { readdirSync as readdirSync7, readFileSync as readFileSync24 } from "node:fs";
21246
+ // src/research/ledger-files.ts
21247
+ import { closeSync as closeSync5, constants as constants4, fstatSync as fstatSync2, lstatSync as lstatSync11, openSync as openSync5, readFileSync as readFileSync24, readdirSync as readdirSync7, realpathSync as realpathSync11 } from "node:fs";
21127
21248
  import { join as join27, resolve as resolve17 } from "node:path";
21128
- function readLedger2(dir) {
21129
- const pairsDir = join27(resolve17(dir), "pairs");
21249
+ function lstatOrNull(path) {
21250
+ try {
21251
+ return lstatSync11(path);
21252
+ } catch {
21253
+ return null;
21254
+ }
21255
+ }
21256
+ function kindOf(st) {
21257
+ if (st.isSymbolicLink()) return "a symlink";
21258
+ if (st.isDirectory()) return "a directory";
21259
+ if (st.isFIFO()) return "a FIFO";
21260
+ if (st.isSocket()) return "a socket";
21261
+ if (st.isCharacterDevice() || st.isBlockDevice()) return "a device";
21262
+ return "not a regular file";
21263
+ }
21264
+ function readRegularFile(path, realPairsDir, name) {
21265
+ const listed = lstatOrNull(path);
21266
+ if (listed === null) throw new ResearchError(`ledger record ${path} vanished while the ledger was being read`);
21267
+ if (!listed.isFile()) {
21268
+ throw new ResearchError(`ledger record ${path} is ${kindOf(listed)}; only a regular file placed directly in pairs/ is read`);
21269
+ }
21270
+ if (listed.nlink !== 1) {
21271
+ throw new ResearchError(`ledger record ${path} has ${listed.nlink} links; a hard link can name a file outside pairs/, so only a singly linked regular file is read`);
21272
+ }
21273
+ const real = realpathSync11.native(path);
21274
+ if (real !== join27(realPairsDir, name)) {
21275
+ throw new ResearchError(`ledger record ${path} resolves outside the pairs directory (${real}); refusing to read it`);
21276
+ }
21277
+ const flags = constants4.O_RDONLY | (constants4.O_NOFOLLOW ?? 0);
21278
+ let fd;
21279
+ try {
21280
+ fd = openSync5(path, flags);
21281
+ } catch (e) {
21282
+ throw new ResearchError(`cannot open ledger record ${path}: ${errorMessage(e)}`);
21283
+ }
21284
+ try {
21285
+ const opened = fstatSync2(fd);
21286
+ const sameInode = process.platform === "win32" || opened.dev === listed.dev && opened.ino === listed.ino;
21287
+ if (!opened.isFile() || !sameInode || opened.nlink !== 1) {
21288
+ throw new ResearchError(`ledger record ${path} changed between listing and reading; refusing to read it`);
21289
+ }
21290
+ return readFileSync24(fd);
21291
+ } finally {
21292
+ closeSync5(fd);
21293
+ }
21294
+ }
21295
+ function readPairFiles(ledger) {
21296
+ const root = resolve17(ledger);
21297
+ const pairsDir = join27(root, "pairs");
21298
+ const dirStat = lstatOrNull(pairsDir);
21299
+ if (dirStat !== null && dirStat.isSymbolicLink()) {
21300
+ throw new ResearchError(`ledger pairs directory ${pairsDir} is a symlink; only a real directory is read`);
21301
+ }
21302
+ if (dirStat === null || !dirStat.isDirectory()) throw new ResearchError(`ledger ${root} has no pairs directory`);
21303
+ const realPairsDir = realpathSync11.native(pairsDir);
21130
21304
  let names;
21131
21305
  try {
21132
21306
  names = readdirSync7(pairsDir).filter((n2) => n2.endsWith(".json")).sort();
21133
21307
  } catch (e) {
21134
21308
  throw new ResearchError(`cannot read ledger ${pairsDir}: ${errorMessage(e)}`);
21135
21309
  }
21136
- if (names.length === 0) throw new ResearchError(`ledger ${pairsDir} holds no pair records`);
21137
- return names.map((n2) => {
21138
- const path = join27(pairsDir, n2);
21310
+ return names.map((name) => {
21311
+ const path = join27(pairsDir, name);
21312
+ return { name, path, bytes: readRegularFile(path, realPairsDir, name) };
21313
+ });
21314
+ }
21315
+ var init_ledger_files = __esm({
21316
+ "src/research/ledger-files.ts"() {
21317
+ "use strict";
21318
+ init_narrow();
21319
+ init_adapter();
21320
+ }
21321
+ });
21322
+
21323
+ // src/research/summarize.ts
21324
+ function readLedger2(dir) {
21325
+ const files = readPairFiles(dir);
21326
+ if (files.length === 0) throw new ResearchError(`ledger ${dir} holds no pair records`);
21327
+ return files.map((f) => {
21139
21328
  let raw;
21140
21329
  try {
21141
- raw = JSON.parse(readFileSync24(path, "utf8"));
21330
+ raw = JSON.parse(f.bytes.toString("utf8"));
21142
21331
  } catch (e) {
21143
- throw new ResearchError(`ledger record ${path} is not valid JSON: ${errorMessage(e)}`);
21332
+ throw new ResearchError(`ledger record ${f.path} is not valid JSON: ${errorMessage(e)}`);
21144
21333
  }
21145
- return pairRecordFrom(raw, path);
21334
+ return pairRecordFrom(raw, f.path);
21146
21335
  });
21147
21336
  }
21148
21337
  function weakened(t) {
@@ -21263,6 +21452,7 @@ var init_summarize = __esm({
21263
21452
  "use strict";
21264
21453
  init_machine_output();
21265
21454
  init_narrow();
21455
+ init_ledger_files();
21266
21456
  init_adapter();
21267
21457
  init_record();
21268
21458
  }
@@ -21389,63 +21579,105 @@ var init_report2 = __esm({
21389
21579
  // src/research/bundle.ts
21390
21580
  import { createHash as createHash16 } from "node:crypto";
21391
21581
  import { gzipSync, gunzipSync } from "node:zlib";
21392
- import { existsSync as existsSync20, mkdirSync as mkdirSync12, readdirSync as readdirSync8, readFileSync as readFileSync26, statSync as statSync6, writeFileSync as writeFileSync14 } from "node:fs";
21393
- import { dirname as dirname13, join as join28, resolve as resolve19 } from "node:path";
21582
+ import { mkdirSync as mkdirSync12, readFileSync as readFileSync26, writeFileSync as writeFileSync14 } from "node:fs";
21583
+ import { dirname as dirname13, resolve as resolve19 } from "node:path";
21394
21584
  function octal(value, width) {
21395
21585
  return value.toString(8).padStart(width - 1, "0") + "\0";
21396
21586
  }
21397
- function tarEntry(name, bytes) {
21587
+ function tarHeader(name, size) {
21398
21588
  const header = Buffer.alloc(BLOCK, 0);
21399
- const safe = name.replace(/^\/+/, "").slice(0, 99);
21400
- header.write(safe, 0, "utf8");
21589
+ header.write(name, 0, "utf8");
21401
21590
  header.write(octal(420, 8), 100, "ascii");
21402
21591
  header.write(octal(0, 8), 108, "ascii");
21403
21592
  header.write(octal(0, 8), 116, "ascii");
21404
- header.write(octal(bytes.length, 12), 124, "ascii");
21593
+ header.write(octal(size, 12), 124, "ascii");
21405
21594
  header.write(octal(0, 12), 136, "ascii");
21406
21595
  header.fill(32, 148, 156);
21407
- header[156] = 48;
21596
+ header[TYPE_OFFSET] = 48;
21408
21597
  header.write("ustar", 257, "ascii");
21409
21598
  header.write("00", 263, "ascii");
21410
21599
  const checksum = header.reduce((sum, b) => sum + b, 0);
21411
21600
  header.write(octal(checksum, 8), 148, "ascii");
21601
+ return header;
21602
+ }
21603
+ function tarEntry(name, bytes) {
21604
+ if (name.startsWith("/") || Buffer.byteLength(name, "utf8") > TAR_NAME_MAX) {
21605
+ throw new ResearchError(`archive entry name ${name} cannot be stored byte for byte in a ustar header`);
21606
+ }
21412
21607
  const padding = Buffer.alloc((BLOCK - bytes.length % BLOCK) % BLOCK, 0);
21413
- return Buffer.concat([header, bytes, padding]);
21608
+ return Buffer.concat([tarHeader(name, bytes.length), bytes, padding]);
21414
21609
  }
21415
21610
  function makeTar(entries) {
21416
21611
  return Buffer.concat([...entries.map((e) => tarEntry(e.name, e.bytes)), Buffer.alloc(BLOCK * 2, 0)]);
21417
21612
  }
21613
+ function field(header, start, length) {
21614
+ const raw = header.subarray(start, start + length);
21615
+ const end = raw.indexOf(0);
21616
+ return raw.subarray(0, end === -1 ? raw.length : end).toString("utf8");
21617
+ }
21418
21618
  function parseTar(bytes) {
21419
21619
  const files = /* @__PURE__ */ new Map();
21420
- for (let offset = 0; offset + BLOCK <= bytes.length; ) {
21620
+ let offset = 0;
21621
+ for (; ; ) {
21622
+ if (offset + BLOCK > bytes.length) throw new ResearchError("research bundle is missing its end-of-archive blocks");
21421
21623
  const header = bytes.subarray(offset, offset + BLOCK);
21422
- if (header.every((b) => b === 0)) break;
21423
- const name = header.subarray(0, 100).toString("utf8").replace(/\0.*$/, "");
21424
- const sizeText = header.subarray(124, 136).toString("ascii").replace(/\0.*$/, "").trim();
21425
- const size = Number.parseInt(sizeText || "0", 8);
21624
+ if (header.every((b) => b === 0)) {
21625
+ const second = bytes.subarray(offset + BLOCK, offset + 2 * BLOCK);
21626
+ if (second.length !== BLOCK || !second.every((b) => b === 0) || offset + 2 * BLOCK !== bytes.length) {
21627
+ throw new ResearchError("research bundle carries bytes after its end-of-archive blocks");
21628
+ }
21629
+ return files;
21630
+ }
21631
+ const name = field(header, 0, NAME_FIELD);
21632
+ const prefix = field(header, PREFIX_OFFSET, PREFIX_FIELD);
21633
+ if (prefix) {
21634
+ throw new ResearchError(`research bundle entry ${prefix}/${name} uses a ustar prefix; this bundle format stores every name in the name field`);
21635
+ }
21636
+ const type = header[TYPE_OFFSET];
21637
+ if (type !== 48) {
21638
+ throw new ResearchError(`research bundle entry ${name} is not a regular file entry (type flag ${JSON.stringify(String.fromCharCode(type))})`);
21639
+ }
21640
+ const size = Number.parseInt(field(header, 124, 12).trim() || "0", 8);
21426
21641
  if (!name || name.startsWith("/") || name.split("/").includes("..") || !Number.isSafeInteger(size) || size < 0) {
21427
21642
  throw new ResearchError("research bundle contains an unsafe or malformed archive entry");
21428
21643
  }
21644
+ if (Buffer.byteLength(name, "utf8") > TAR_NAME_MAX || !header.equals(tarHeader(name, size))) {
21645
+ throw new ResearchError(`research bundle entry ${name} has a header this bundle format does not write`);
21646
+ }
21429
21647
  const start = offset + BLOCK;
21430
21648
  const end = start + size;
21431
21649
  if (end > bytes.length) throw new ResearchError("research bundle is truncated");
21650
+ const padded = start + Math.ceil(size / BLOCK) * BLOCK;
21651
+ if (padded > bytes.length || !bytes.subarray(end, padded).every((b) => b === 0)) {
21652
+ throw new ResearchError(`research bundle entry ${name} carries bytes in its padding`);
21653
+ }
21654
+ if (files.has(name)) throw new ResearchError(`research bundle contains a duplicate archive entry ${name}`);
21432
21655
  files.set(name, Buffer.from(bytes.subarray(start, end)));
21433
- offset = start + Math.ceil(size / BLOCK) * BLOCK;
21656
+ offset = padded;
21434
21657
  }
21435
- return files;
21436
21658
  }
21437
- function walkLedger(ledger) {
21659
+ function packagedRecords(ledger) {
21660
+ const files = readPairFiles(ledger);
21661
+ if (files.length === 0) throw new ResearchError(`ledger ${ledger} holds no pair records`);
21662
+ const records = [];
21438
21663
  const entries = [];
21439
- const pairs = join28(ledger, "pairs");
21440
- if (!existsSync20(pairs) || !statSync6(pairs).isDirectory()) throw new ResearchError(`ledger ${ledger} has no pairs directory`);
21441
- const names = readdirSync8(pairs).filter((n2) => n2.endsWith(".json")).sort();
21442
- if (names.length === 0) throw new ResearchError(`ledger ${ledger} holds no pair records`);
21443
- for (const name of names) {
21444
- const path = join28(pairs, name);
21445
- if (!statSync6(path).isFile()) continue;
21446
- entries.push({ name: `ledger/pairs/${name}`, bytes: readFileSync26(path) });
21664
+ for (const f of files) {
21665
+ let raw;
21666
+ try {
21667
+ raw = JSON.parse(f.bytes.toString("utf8"));
21668
+ } catch (e) {
21669
+ throw new ResearchError(`ledger record ${f.path} is not valid JSON: ${e instanceof Error ? e.message : String(e)}`);
21670
+ }
21671
+ const record = portablePairRecordFrom(raw, f.path);
21672
+ const archiveName = `ledger/pairs/${f.name}`;
21673
+ const nameBytes = Buffer.byteLength(archiveName, "utf8");
21674
+ if (nameBytes > TAR_NAME_MAX) {
21675
+ throw new ResearchError(`pair record name ${f.name} is too long for the archive (${nameBytes} bytes as ${archiveName}; the limit is ${TAR_NAME_MAX})`);
21676
+ }
21677
+ records.push(record);
21678
+ entries.push({ name: archiveName, bytes: jsonBytes(record) });
21447
21679
  }
21448
- return entries;
21680
+ return { records, entries };
21449
21681
  }
21450
21682
  function jsonBytes(value) {
21451
21683
  return Buffer.from(JSON.stringify(value, null, 2) + "\n", "utf8");
@@ -21457,6 +21689,11 @@ function validateResearchBundle(path) {
21457
21689
  } catch (e) {
21458
21690
  throw new ResearchError(`cannot read research bundle ${path}: ${e instanceof Error ? e.message : String(e)}`);
21459
21691
  }
21692
+ for (const name of files.keys()) {
21693
+ if (!TOP_LEVEL_ENTRIES.has(name) && !PAIR_ENTRY_RE.test(name)) {
21694
+ throw new ResearchError(`research bundle carries an entry that is not evidence: ${name}`);
21695
+ }
21696
+ }
21460
21697
  const provenance = files.get("provenance.json");
21461
21698
  const summaryBytes = files.get("summary.json");
21462
21699
  const report2 = files.get("report.txt");
@@ -21483,14 +21720,16 @@ function validateResearchBundle(path) {
21483
21720
  if (parsedSummary.document !== "summary" || parsedSummary.command !== "research") throw new ResearchError("research bundle summary is not a research summary");
21484
21721
  const records = [];
21485
21722
  for (const [name, bytes] of files) {
21486
- if (!name.startsWith("ledger/pairs/") || !name.endsWith(".json")) continue;
21723
+ if (!PAIR_ENTRY_RE.test(name)) continue;
21487
21724
  let raw;
21488
21725
  try {
21489
21726
  raw = JSON.parse(bytes.toString("utf8"));
21490
21727
  } catch {
21491
21728
  throw new ResearchError(`invalid JSON in ${name}`);
21492
21729
  }
21493
- records.push(pairRecordFrom(raw, name));
21730
+ const record = portablePairRecordFrom(raw, name);
21731
+ if (!bytes.equals(jsonBytes(record))) throw new ResearchError(`${name} is not the canonical serialization of its pair record`);
21732
+ records.push(record);
21494
21733
  }
21495
21734
  if (records.length === 0) throw new ResearchError("research bundle contains no pair records");
21496
21735
  const derived = summarizeRecords(records);
@@ -21504,8 +21743,8 @@ function createResearchBundle(opts) {
21504
21743
  if (!opts.ledger) throw new ResearchError("research bundle requires --ledger");
21505
21744
  if (!opts.out) throw new ResearchError("research bundle requires --out");
21506
21745
  const ledger = resolve19(opts.ledger);
21507
- const summary = summarizeLedger(ledger);
21508
- const entries = walkLedger(ledger);
21746
+ const { records, entries } = packagedRecords(ledger);
21747
+ const summary = summarizeRecords(records);
21509
21748
  if (opts.manifest) {
21510
21749
  const manifest = readFileSync26(resolve19(opts.manifest));
21511
21750
  const sha3 = createHash16("sha256").update(manifest).digest("hex");
@@ -21555,17 +21794,25 @@ function runResearchBundle(opts) {
21555
21794
  throw e;
21556
21795
  }
21557
21796
  }
21558
- var BLOCK, BUNDLE_VERSION;
21797
+ var BLOCK, BUNDLE_VERSION, TOP_LEVEL_ENTRIES, PAIR_ENTRY_RE, TAR_NAME_MAX, NAME_FIELD, TYPE_OFFSET, PREFIX_OFFSET, PREFIX_FIELD;
21559
21798
  var init_bundle = __esm({
21560
21799
  "src/research/bundle.ts"() {
21561
21800
  "use strict";
21562
21801
  init_machine_output();
21563
21802
  init_adapter();
21803
+ init_ledger_files();
21564
21804
  init_record();
21565
21805
  init_report2();
21566
21806
  init_summarize();
21567
21807
  BLOCK = 512;
21568
21808
  BUNDLE_VERSION = 1;
21809
+ TOP_LEVEL_ENTRIES = /* @__PURE__ */ new Set(["summary.json", "report.txt", "provenance.json", "manifest.json"]);
21810
+ PAIR_ENTRY_RE = /^ledger\/pairs\/[^/]+\.json$/;
21811
+ TAR_NAME_MAX = 99;
21812
+ NAME_FIELD = 100;
21813
+ TYPE_OFFSET = 156;
21814
+ PREFIX_OFFSET = 345;
21815
+ PREFIX_FIELD = 155;
21569
21816
  }
21570
21817
  });
21571
21818
 
@@ -22535,7 +22782,7 @@ var init_adapter4 = __esm({
22535
22782
 
22536
22783
  // src/adapters/copilot-sdk/changes.ts
22537
22784
  import { execFileSync as execFileSync15 } from "node:child_process";
22538
- import { mkdtempSync as mkdtempSync7, writeFileSync as writeFileSync15, readFileSync as readFileSync27, rmSync as rmSync13, existsSync as existsSync21, realpathSync as realpathSync11 } from "node:fs";
22785
+ import { mkdtempSync as mkdtempSync7, writeFileSync as writeFileSync15, readFileSync as readFileSync27, rmSync as rmSync13, existsSync as existsSync20, realpathSync as realpathSync12 } from "node:fs";
22539
22786
  import { tmpdir as tmpdir8 } from "node:os";
22540
22787
  import { basename as basename6, dirname as dirname14, isAbsolute as isAbsolute17, relative as relative12, resolve as resolve22, join as join29 } from "node:path";
22541
22788
  function asStr4(v) {
@@ -22548,13 +22795,13 @@ function relForDisplay5(path, cwd) {
22548
22795
  function canonicalContainedTarget(abs, root) {
22549
22796
  let realRoot;
22550
22797
  try {
22551
- realRoot = realpathSync11(root);
22798
+ realRoot = realpathSync12(root);
22552
22799
  } catch (e) {
22553
22800
  throw new Error(`trusted repository root ${root} cannot be resolved: ${e instanceof Error ? e.message : String(e)}`);
22554
22801
  }
22555
22802
  let dir = abs;
22556
22803
  const tail = [];
22557
- while (!existsSync21(dir)) {
22804
+ while (!existsSync20(dir)) {
22558
22805
  const parent = dirname14(dir);
22559
22806
  if (parent === dir) break;
22560
22807
  tail.unshift(basename6(dir));
@@ -22562,7 +22809,7 @@ function canonicalContainedTarget(abs, root) {
22562
22809
  }
22563
22810
  let realDir;
22564
22811
  try {
22565
- realDir = realpathSync11(dir);
22812
+ realDir = realpathSync12(dir);
22566
22813
  } catch (e) {
22567
22814
  throw new Error(`write target ${abs} cannot be resolved: ${e instanceof Error ? e.message : String(e)}`);
22568
22815
  }
@@ -23373,7 +23620,7 @@ var init_runtime_qualification = __esm({
23373
23620
 
23374
23621
  // src/cli/runtime.ts
23375
23622
  import { execFileSync as execFileSync16 } from "node:child_process";
23376
- import { existsSync as existsSync22, mkdirSync as mkdirSync13, readFileSync as readFileSync28, writeFileSync as writeFileSync16 } from "node:fs";
23623
+ import { existsSync as existsSync21, mkdirSync as mkdirSync13, readFileSync as readFileSync28, writeFileSync as writeFileSync16 } from "node:fs";
23377
23624
  import { join as join30, dirname as dirname15 } from "node:path";
23378
23625
  function storePath(cwd) {
23379
23626
  const dir = gitDir(cwd);
@@ -23381,7 +23628,7 @@ function storePath(cwd) {
23381
23628
  }
23382
23629
  function readStore(cwd) {
23383
23630
  const path = storePath(cwd);
23384
- if (!path || !existsSync22(path)) return null;
23631
+ if (!path || !existsSync21(path)) return null;
23385
23632
  try {
23386
23633
  const parsed = JSON.parse(readFileSync28(path, "utf8"));
23387
23634
  if (parsed && typeof parsed === "object" && parsed.records && typeof parsed.records === "object") return parsed;
@@ -23543,7 +23790,7 @@ function resolveHookConfigHash(canonicalId, cwd) {
23543
23790
  "github-copilot-cli": join30(cwd, ".github", "hooks", "tamperward.json")
23544
23791
  };
23545
23792
  const file = files[canonicalId];
23546
- if (!file || !existsSync22(file)) return null;
23793
+ if (!file || !existsSync21(file)) return null;
23547
23794
  try {
23548
23795
  const raw = readFileSync28(file, "utf8");
23549
23796
  let hooks = raw;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tamperward",
3
- "version": "2.37.1",
3
+ "version": "2.37.2",
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",