tamperward 2.10.7 → 2.10.9

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 +452 -92
  2. package/package.json +1 -1
package/dist/cli/index.js CHANGED
@@ -1651,8 +1651,8 @@ function numericOf(e) {
1651
1651
  }
1652
1652
  return void 0;
1653
1653
  }
1654
- function metricsOf(expr, resolve6) {
1655
- const obj = resolve6(expr);
1654
+ function metricsOf(expr, resolve7) {
1655
+ const obj = resolve7(expr);
1656
1656
  if (!ts2.isObjectLiteralExpression(obj)) return void 0;
1657
1657
  const out2 = { values: {}, opaque: false };
1658
1658
  let any = false;
@@ -1666,7 +1666,7 @@ function metricsOf(expr, resolve6) {
1666
1666
  const k = keyName(p.name);
1667
1667
  if (k === null || !isMetric(k)) continue;
1668
1668
  any = true;
1669
- const n = numericOf(resolve6(p.initializer));
1669
+ const n = numericOf(resolve7(p.initializer));
1670
1670
  if (n === void 0) out2.opaque = true;
1671
1671
  else out2.values[k] = n;
1672
1672
  }
@@ -1712,17 +1712,17 @@ function parseThresholds(src) {
1712
1712
  const res = { paths: /* @__PURE__ */ new Map(), present: false, opaque: false };
1713
1713
  try {
1714
1714
  const sf = sourceOf(src);
1715
- const resolve6 = resolverFor(sf);
1715
+ const resolve7 = resolverFor(sf);
1716
1716
  const visit = (node) => {
1717
1717
  if (ts2.isPropertyAssignment(node)) {
1718
1718
  const key2 = keyName(node.name);
1719
1719
  if (key2 === "coverageThreshold" || key2 === "thresholds" && underKey(node, "coverage")) {
1720
1720
  res.present = true;
1721
- const init = resolve6(node.initializer);
1721
+ const init = resolve7(node.initializer);
1722
1722
  if (!ts2.isObjectLiteralExpression(init)) {
1723
1723
  res.opaque = true;
1724
1724
  } else {
1725
- const flat = metricsOf(init, resolve6);
1725
+ const flat = metricsOf(init, resolve7);
1726
1726
  if (flat) res.global = merge(res.global, flat);
1727
1727
  for (const p of init.properties) {
1728
1728
  if (ts2.isSpreadAssignment(p)) {
@@ -1736,7 +1736,7 @@ function parseThresholds(src) {
1736
1736
  res.global = merge(res.global, { values: { branches: 100, functions: 100, lines: 100, statements: 100 }, opaque: false });
1737
1737
  continue;
1738
1738
  }
1739
- const m = metricsOf(p.initializer, resolve6);
1739
+ const m = metricsOf(p.initializer, resolve7);
1740
1740
  if (!m) continue;
1741
1741
  if (k === "global") res.global = merge(res.global, m);
1742
1742
  else res.paths.set(norm(k), m);
@@ -1769,18 +1769,18 @@ function parseLists(src) {
1769
1769
  const res = { collectFrom: null, collectKey: "collectCoverageFrom", ignore: [], opaqueCollect: false, opaqueIgnore: false };
1770
1770
  try {
1771
1771
  const sf = sourceOf(src);
1772
- const resolve6 = resolverFor(sf);
1772
+ const resolve7 = resolverFor(sf);
1773
1773
  const visit = (node) => {
1774
1774
  if (ts2.isPropertyAssignment(node)) {
1775
1775
  const k = keyName(node.name);
1776
1776
  const vitestCoverage = ownerKey(node) === "coverage";
1777
1777
  if (k === "collectCoverageFrom" || k === "include" && vitestCoverage) {
1778
- const { items, opaque } = literals(resolve6(node.initializer));
1778
+ const { items, opaque } = literals(resolve7(node.initializer));
1779
1779
  res.collectFrom = items;
1780
1780
  res.collectKey = k === "include" ? "coverage.include" : k;
1781
1781
  if (opaque) res.opaqueCollect = true;
1782
1782
  } else if (k === "coveragePathIgnorePatterns" || k === "exclude" && vitestCoverage) {
1783
- const { items, opaque } = literals(resolve6(node.initializer));
1783
+ const { items, opaque } = literals(resolve7(node.initializer));
1784
1784
  res.ignore.push(...items);
1785
1785
  if (opaque) res.opaqueIgnore = true;
1786
1786
  }
@@ -7663,7 +7663,7 @@ jobs:
7663
7663
  runs-on: ubuntu-latest
7664
7664
  timeout-minutes: 10
7665
7665
  steps:
7666
- - uses: actions/setup-node@v6
7666
+ - uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6
7667
7667
  with:
7668
7668
  node-version: 22
7669
7669
  # Install the authority before candidate files exist in the job workspace.
@@ -7673,9 +7673,12 @@ jobs:
7673
7673
  - name: Install Tamperward authority
7674
7674
  working-directory: \${{ runner.temp }}
7675
7675
  run: npm install --global --registry=https://registry.npmjs.org/ --node-options=' ' --script-shell= --ignore-scripts --offline=false --prefer-online tamperward@${TW_VERSION}
7676
- - uses: actions/checkout@v5
7676
+ - uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5
7677
7677
  with:
7678
7678
  fetch-depth: 0 # the range diff needs both endpoints
7679
+ # Candidate code runs later in this job. Do not leave the checkout
7680
+ # token in .git/config where a verifier command can read/reuse it.
7681
+ persist-credentials: false
7679
7682
  - name: Resolve out-of-band sign-off from PR labels
7680
7683
  id: oob
7681
7684
  env:
@@ -8160,16 +8163,14 @@ tamperward init: ${changed} change(s) applied. Commit them so the gate travels w
8160
8163
 
8161
8164
  // src/cli/verify.ts
8162
8165
  import { execFileSync as execFileSync8, spawnSync } from "node:child_process";
8163
- import { createHash as createHash8 } from "node:crypto";
8164
- import { chmodSync as chmodSync2, cpSync, lstatSync as lstatSync3, mkdirSync as mkdirSync6, mkdtempSync as mkdtempSync2, readFileSync as readFileSync14, readlinkSync as readlinkSync3, realpathSync as realpathSync3, rmSync as rmSync3, symlinkSync, writeFileSync as writeFileSync6, existsSync as existsSync10 } from "node:fs";
8166
+ import { createHash as createHash9 } from "node:crypto";
8167
+ import { chmodSync as chmodSync2, cpSync, lstatSync as lstatSync4, mkdirSync as mkdirSync6, mkdtempSync as mkdtempSync2, readFileSync as readFileSync14, readlinkSync as readlinkSync3, rmSync as rmSync3, symlinkSync, writeFileSync as writeFileSync6 } from "node:fs";
8165
8168
  import { tmpdir as tmpdir2 } from "node:os";
8166
- import { dirname as dirname5, isAbsolute as isAbsolute9, join as join14, relative as relative4, resolve as resolve4, sep } from "node:path";
8169
+ import { dirname as dirname6, isAbsolute as isAbsolute10, join as join14, relative as relative5, resolve as resolve5, sep as sep2 } from "node:path";
8167
8170
 
8168
8171
  // src/fingerprint.ts
8169
8172
  import { execFileSync as execFileSync7 } from "node:child_process";
8170
8173
  import { createHash as createHash7 } from "node:crypto";
8171
- import { readdirSync as readdirSync3, readFileSync as readFileSync13, readlinkSync as readlinkSync2, statSync as statSync4 } from "node:fs";
8172
- import { join as join13 } from "node:path";
8173
8174
  function treeFingerprint(cwd, keepIgnored) {
8174
8175
  const listed = execFileSync7("git", ["ls-files", "-z", "--cached", "--others", "--exclude-standard"], {
8175
8176
  cwd,
@@ -8192,45 +8193,359 @@ function treeFingerprint(cwd, keepIgnored) {
8192
8193
  }
8193
8194
  return h.digest("hex");
8194
8195
  }
8195
- function depsFingerprint(cwd) {
8196
- const root = join13(cwd, "node_modules");
8197
- const h = createHash7("sha256");
8198
- const walk2 = (dir) => {
8196
+
8197
+ // src/dependency-env.ts
8198
+ import { createHash as createHash8 } from "node:crypto";
8199
+ import {
8200
+ existsSync as existsSync10,
8201
+ lstatSync as lstatSync3,
8202
+ readdirSync as readdirSync3,
8203
+ readFileSync as readFileSync13,
8204
+ readlinkSync as readlinkSync2,
8205
+ realpathSync as realpathSync3,
8206
+ statSync as statSync4
8207
+ } from "node:fs";
8208
+ import {
8209
+ basename as basename3,
8210
+ delimiter,
8211
+ dirname as dirname5,
8212
+ isAbsolute as isAbsolute9,
8213
+ join as join13,
8214
+ relative as relative4,
8215
+ resolve as resolve4,
8216
+ sep
8217
+ } from "node:path";
8218
+ var Unattestable = class extends Error {
8219
+ };
8220
+ function inside(root, candidate) {
8221
+ const rel = relative4(resolve4(root), resolve4(candidate));
8222
+ return rel === "" || rel !== ".." && !rel.startsWith(".." + sep) && !isAbsolute9(rel);
8223
+ }
8224
+ function modeKind(path) {
8225
+ const st = lstatSync3(path);
8226
+ if (st.isFile()) return { kind: "file", mode: st.mode };
8227
+ if (st.isDirectory()) return { kind: "dir", mode: st.mode };
8228
+ if (st.isSymbolicLink()) return { kind: "symlink", mode: st.mode };
8229
+ return { kind: "special", mode: st.mode };
8230
+ }
8231
+ function fingerprintRoot(root, cwd, h) {
8232
+ let rootStat;
8233
+ let currentReal;
8234
+ try {
8235
+ rootStat = lstatSync3(root.path);
8236
+ currentReal = realpathSync3(root.path);
8237
+ } catch (e) {
8238
+ throw new Unattestable(
8239
+ root.kind + " root " + JSON.stringify(root.path) + " can no longer be resolved (" + (e instanceof Error ? e.message : String(e)) + ")"
8240
+ );
8241
+ }
8242
+ if (currentReal !== root.realPath) {
8243
+ throw new Unattestable(
8244
+ root.kind + " root moved from " + JSON.stringify(root.realPath) + " to " + JSON.stringify(currentReal)
8245
+ );
8246
+ }
8247
+ if (!statSync4(root.path).isDirectory()) {
8248
+ throw new Unattestable(root.kind + " root " + JSON.stringify(root.path) + " is not a directory");
8249
+ }
8250
+ h.update("root\0" + root.kind + "\0" + root.path + "\0" + root.realPath + "\0" + String(rootStat.mode) + "\0");
8251
+ if (rootStat.isSymbolicLink()) h.update(readlinkSync2(root.path) + "\0");
8252
+ const cwdReal = realpathSync3(cwd);
8253
+ const visited = /* @__PURE__ */ new Set();
8254
+ const hashFile = (path, label) => {
8255
+ let st;
8256
+ try {
8257
+ st = statSync4(path);
8258
+ if (!st.isFile()) throw new Error("not a regular file");
8259
+ h.update(label + "\0external-file\0" + path + "\0" + String(st.mode) + "\0");
8260
+ h.update(readFileSync13(path));
8261
+ h.update("\0");
8262
+ } catch (e) {
8263
+ throw new Unattestable(
8264
+ "dependency link target " + JSON.stringify(path) + " is not attestable (" + (e instanceof Error ? e.message : String(e)) + ")"
8265
+ );
8266
+ }
8267
+ };
8268
+ const walk2 = (dir, namespace) => {
8269
+ const realDir = realpathSync3(dir);
8270
+ if (visited.has(realDir)) return;
8271
+ visited.add(realDir);
8199
8272
  let entries;
8200
8273
  try {
8201
8274
  entries = readdirSync3(dir, { withFileTypes: true });
8202
- } catch {
8203
- return;
8275
+ } catch (e) {
8276
+ throw new Unattestable(
8277
+ "dependency directory " + JSON.stringify(dir) + " cannot be read (" + (e instanceof Error ? e.message : String(e)) + ")"
8278
+ );
8204
8279
  }
8205
- for (const e of [...entries].sort((a, b) => a.name.localeCompare(b.name))) {
8206
- const p = join13(dir, e.name);
8207
- if (e.isDirectory()) walk2(p);
8208
- else if (e.isFile()) {
8209
- h.update(p);
8280
+ for (const entry of [...entries].sort((a, b) => a.name.localeCompare(b.name))) {
8281
+ const path = join13(dir, entry.name);
8282
+ const rel = namespace ? namespace + "/" + entry.name : entry.name;
8283
+ let ident;
8284
+ try {
8285
+ ident = modeKind(path);
8286
+ } catch (e) {
8287
+ throw new Unattestable(
8288
+ "dependency entry " + JSON.stringify(path) + " disappeared while attesting (" + (e instanceof Error ? e.message : String(e)) + ")"
8289
+ );
8290
+ }
8291
+ h.update(rel + "\0" + ident.kind + "\0" + String(ident.mode) + "\0");
8292
+ if (ident.kind === "file") {
8210
8293
  try {
8211
- h.update(readFileSync13(p));
8212
- h.update(String(statSync4(p).mode));
8213
- } catch {
8214
- h.update("<unreadable>");
8294
+ h.update(readFileSync13(path));
8295
+ h.update("\0");
8296
+ } catch (e) {
8297
+ throw new Unattestable(
8298
+ "dependency file " + JSON.stringify(path) + " cannot be read (" + (e instanceof Error ? e.message : String(e)) + ")"
8299
+ );
8300
+ }
8301
+ continue;
8302
+ }
8303
+ if (ident.kind === "dir") {
8304
+ walk2(path, rel);
8305
+ continue;
8306
+ }
8307
+ if (ident.kind === "symlink") {
8308
+ let target;
8309
+ try {
8310
+ target = readlinkSync2(path);
8311
+ } catch (e) {
8312
+ throw new Unattestable(
8313
+ "dependency symlink " + JSON.stringify(path) + " cannot be read (" + (e instanceof Error ? e.message : String(e)) + ")"
8314
+ );
8215
8315
  }
8216
- } else if (e.isSymbolicLink()) {
8217
- h.update(p);
8316
+ h.update(target + "\0");
8317
+ let resolved;
8218
8318
  try {
8219
- h.update(readlinkSync2(p));
8319
+ resolved = realpathSync3(path);
8220
8320
  } catch {
8221
- h.update("<unreadable-link>");
8321
+ continue;
8322
+ }
8323
+ const followed = statSync4(path);
8324
+ if (followed.isFile()) {
8325
+ hashFile(resolved, "@target:" + rel);
8326
+ continue;
8327
+ }
8328
+ if (followed.isDirectory()) {
8329
+ if (inside(root.realPath, resolved)) {
8330
+ walk2(resolved, "@target:" + rel);
8331
+ continue;
8332
+ }
8333
+ if (inside(cwdReal, resolved)) {
8334
+ throw new Unattestable(
8335
+ "dependency workspace symlink " + JSON.stringify(path) + " points into the candidate worktree at " + JSON.stringify(resolved) + "; faithful per-copy remapping is not available"
8336
+ );
8337
+ }
8338
+ throw new Unattestable(
8339
+ "dependency directory symlink " + JSON.stringify(path) + " escapes the bounded root to " + JSON.stringify(resolved)
8340
+ );
8222
8341
  }
8342
+ throw new Unattestable("dependency symlink " + JSON.stringify(path) + " resolves to a special file");
8223
8343
  }
8344
+ throw new Unattestable("dependency entry " + JSON.stringify(path) + " is a special file");
8224
8345
  }
8225
8346
  };
8226
- try {
8227
- if (!statSync4(root).isDirectory()) return "none";
8228
- } catch {
8229
- return "none";
8347
+ walk2(root.realPath, "");
8348
+ }
8349
+ function snapshot(roots, probes, cwd) {
8350
+ const h = createHash8("sha256");
8351
+ for (const probe of probes) {
8352
+ h.update(
8353
+ "probe\0" + probe.kind + "\0" + probe.path + "\0" + (existsSync10(probe.path) ? "present" : "absent") + "\0"
8354
+ );
8230
8355
  }
8231
- walk2(root);
8356
+ for (const root of roots) fingerprintRoot(root, cwd, h);
8232
8357
  return h.digest("hex");
8233
8358
  }
8359
+ function freezeRoot(kind, path) {
8360
+ try {
8361
+ if (!statSync4(path).isDirectory()) {
8362
+ throw new Error("not a directory");
8363
+ }
8364
+ return { kind, path: resolve4(path), realPath: realpathSync3(path) };
8365
+ } catch (e) {
8366
+ throw new Unattestable(
8367
+ kind + " root " + JSON.stringify(path) + " is not resolvable (" + (e instanceof Error ? e.message : String(e)) + ")"
8368
+ );
8369
+ }
8370
+ }
8371
+ function shellTokens(command) {
8372
+ const matches = command.match(/(?:[^\s"'\\]+|"(?:\\.|[^"])*"|'[^']*')+/g) ?? [];
8373
+ return matches.map((token) => {
8374
+ if (token.startsWith("'") && token.endsWith("'") || token.startsWith('"') && token.endsWith('"')) return token.slice(1, -1);
8375
+ return token;
8376
+ });
8377
+ }
8378
+ function firstExecutable(command) {
8379
+ const tokens2 = shellTokens(command);
8380
+ let i = 0;
8381
+ while (i < tokens2.length && /^[A-Za-z_][A-Za-z0-9_]*=/.test(tokens2[i])) i++;
8382
+ if (tokens2[i] === "env") {
8383
+ i++;
8384
+ while (i < tokens2.length && (tokens2[i].startsWith("-") || /^[A-Za-z_][A-Za-z0-9_]*=/.test(tokens2[i]))) i++;
8385
+ }
8386
+ if (tokens2[i] === "command") {
8387
+ i++;
8388
+ while (i < tokens2.length && tokens2[i].startsWith("-")) i++;
8389
+ }
8390
+ if (tokens2[i] === "timeout") {
8391
+ i++;
8392
+ while (i < tokens2.length && tokens2[i].startsWith("-")) i++;
8393
+ if (i < tokens2.length) i++;
8394
+ }
8395
+ return tokens2[i] ?? null;
8396
+ }
8397
+ function resolveExecutable(token, cwd, env) {
8398
+ if (!token) return null;
8399
+ if (token.includes("/") || token.includes("\\")) {
8400
+ const path = isAbsolute9(token) ? token : resolve4(cwd, token);
8401
+ try {
8402
+ return existsSync10(path) ? realpathSync3(path) : null;
8403
+ } catch {
8404
+ return null;
8405
+ }
8406
+ }
8407
+ for (const entry of (env.PATH ?? "").split(delimiter).filter(Boolean)) {
8408
+ const candidate = resolve4(entry, token);
8409
+ try {
8410
+ if (existsSync10(candidate) && statSync4(candidate).isFile()) return realpathSync3(candidate);
8411
+ } catch {
8412
+ }
8413
+ }
8414
+ return null;
8415
+ }
8416
+ function pythonName(name) {
8417
+ return /^(?:python(?:\d+(?:\.\d+)*)?|pytest|py\.test|pip(?:\d+(?:\.\d+)*)?)$/.test(name.toLowerCase());
8418
+ }
8419
+ function looksPython(command, executable, token) {
8420
+ const exe = executable ? basename3(executable) : "";
8421
+ const raw = token ? basename3(token) : "";
8422
+ if (pythonName(exe) || pythonName(raw)) return true;
8423
+ return /(?:^|[\s;&|()])(?:python(?:\d+(?:\.\d+)*)?|pytest|py\.test|pip(?:\d+(?:\.\d+)*)?)(?=$|[\s;&|()])/.test(command);
8424
+ }
8425
+ function inferredVenvFromToken(token, cwd) {
8426
+ if (!token || !token.includes("/") && !token.includes("\\")) return null;
8427
+ const path = isAbsolute9(token) ? token : resolve4(cwd, token);
8428
+ if (!pythonName(basename3(path))) return null;
8429
+ const parent = dirname5(path);
8430
+ const parentName = basename3(parent).toLowerCase();
8431
+ if (parentName !== "bin" && parentName !== "scripts") return null;
8432
+ const root = dirname5(parent);
8433
+ return existsSync10(join13(root, "pyvenv.cfg")) ? root : null;
8434
+ }
8435
+ function unsupportedEcosystem(command, token) {
8436
+ const name = token ? basename3(token).toLowerCase() : "";
8437
+ const raw = command.toLowerCase();
8438
+ if (["ruby", "bundle", "bundler", "rspec", "rake"].includes(name) || /(?:^|[\s;&|()])(?:bundle\s+exec|ruby|rspec|rake)(?=$|[\s;&|()])/.test(raw)) return "Ruby dependency environment discovery is not yet attestable";
8439
+ if (["mvn", "mvnw", "gradle", "gradlew", "java"].includes(name) || /(?:^|[\s;&|()])(?:mvn|mvnw|gradle|gradlew|java)(?=$|[\s;&|()])/.test(raw)) return "JVM dependency environment discovery is not yet attestable";
8440
+ if (name === "dotnet" || /(?:^|[\s;&|()])dotnet(?=$|[\s;&|()])/.test(raw)) return ".NET dependency environment discovery is not yet attestable";
8441
+ return null;
8442
+ }
8443
+ function inferredVenv(executable) {
8444
+ if (!executable) return null;
8445
+ const parent = dirname5(executable);
8446
+ const parentName = basename3(parent).toLowerCase();
8447
+ if (parentName !== "bin" && parentName !== "scripts") return null;
8448
+ const root = dirname5(parent);
8449
+ return existsSync10(join13(root, "pyvenv.cfg")) ? root : null;
8450
+ }
8451
+ function venvProblem(root) {
8452
+ const cfg = join13(root, "pyvenv.cfg");
8453
+ if (!existsSync10(cfg)) return "VIRTUAL_ENV " + JSON.stringify(root) + " has no pyvenv.cfg";
8454
+ try {
8455
+ const body = readFileSync13(cfg, "utf8");
8456
+ if (/^\s*include-system-site-packages\s*=\s*true\s*$/im.test(body)) {
8457
+ return "Python environment " + JSON.stringify(root) + " enables system site-packages; those external package roots are not bounded";
8458
+ }
8459
+ } catch (e) {
8460
+ return "cannot read " + JSON.stringify(cfg) + " (" + (e instanceof Error ? e.message : String(e)) + ")";
8461
+ }
8462
+ return null;
8463
+ }
8464
+ function discoverDependencyEnvironment(cwdInput, command, env = process.env) {
8465
+ const cwd = resolve4(cwdInput);
8466
+ const roots = [];
8467
+ const probes = [];
8468
+ const seen = /* @__PURE__ */ new Set();
8469
+ const addRoot = (kind, path) => {
8470
+ const root = freezeRoot(kind, path);
8471
+ const key2 = kind + ":" + root.path;
8472
+ if (!seen.has(key2)) {
8473
+ roots.push(root);
8474
+ seen.add(key2);
8475
+ }
8476
+ };
8477
+ try {
8478
+ const nodeModules = join13(cwd, "node_modules");
8479
+ if (existsSync10(nodeModules)) addRoot("node_modules", nodeModules);
8480
+ else probes.push({ kind: "node_modules", path: nodeModules });
8481
+ const token = firstExecutable(command);
8482
+ const executable = resolveExecutable(token, cwd, env);
8483
+ let venv = null;
8484
+ if (env.VIRTUAL_ENV?.trim()) {
8485
+ if (!isAbsolute9(env.VIRTUAL_ENV)) {
8486
+ throw new Unattestable("VIRTUAL_ENV is relative (" + JSON.stringify(env.VIRTUAL_ENV) + ")");
8487
+ }
8488
+ venv = resolve4(env.VIRTUAL_ENV);
8489
+ } else {
8490
+ venv = inferredVenv(executable) ?? inferredVenvFromToken(token, cwd);
8491
+ if (!venv && /(?:^|\s)uv\s+run(?:\s|$)/.test(command) && existsSync10(join13(cwd, ".venv", "pyvenv.cfg"))) {
8492
+ venv = join13(cwd, ".venv");
8493
+ }
8494
+ }
8495
+ if (venv) {
8496
+ const problem = venvProblem(venv);
8497
+ if (problem) throw new Unattestable(problem);
8498
+ addRoot("python-venv", venv);
8499
+ } else if (looksPython(command, executable, token)) {
8500
+ throw new Unattestable(
8501
+ "Python verifier selected without an identifiable virtual environment; global/user site-packages are not bounded"
8502
+ );
8503
+ }
8504
+ const unsupported = unsupportedEcosystem(command, token);
8505
+ if (unsupported) throw new Unattestable(unsupported);
8506
+ const status = roots.length ? "attested" : "none";
8507
+ const fingerprint2 = snapshot(roots, probes, cwd);
8508
+ return { status, command, roots, probes, fingerprint: fingerprint2 };
8509
+ } catch (e) {
8510
+ return {
8511
+ status: "unattestable",
8512
+ command,
8513
+ roots,
8514
+ probes,
8515
+ reason: e instanceof Error ? e.message : String(e)
8516
+ };
8517
+ }
8518
+ }
8519
+ function checkDependencyEnvironment(cwdInput, descriptor) {
8520
+ if (descriptor.status === "unattestable") {
8521
+ return { ok: false, reason: descriptor.reason ?? "dependency environment is not attestable" };
8522
+ }
8523
+ try {
8524
+ const fingerprint2 = snapshot(descriptor.roots, descriptor.probes, resolve4(cwdInput));
8525
+ if (fingerprint2 !== descriptor.fingerprint) {
8526
+ return { ok: false, fingerprint: fingerprint2, reason: "dependency environment fingerprint changed" };
8527
+ }
8528
+ return { ok: true, fingerprint: fingerprint2 };
8529
+ } catch (e) {
8530
+ return { ok: false, reason: e instanceof Error ? e.message : String(e) };
8531
+ }
8532
+ }
8533
+ function dependencyEnvironmentReport(descriptor) {
8534
+ return {
8535
+ status: descriptor.status,
8536
+ roots: descriptor.roots.map((root) => ({ kind: root.kind, path: root.path })),
8537
+ ...descriptor.fingerprint ? { fingerprint: descriptor.fingerprint } : {},
8538
+ ...descriptor.reason ? { reason: descriptor.reason } : {}
8539
+ };
8540
+ }
8541
+ function dependencyEnvironmentSummary(descriptor) {
8542
+ if (descriptor.status === "unattestable") {
8543
+ return "unattestable - " + (descriptor.reason ?? "unknown dependency environment");
8544
+ }
8545
+ if (descriptor.status === "none") return "none detected (candidate node_modules remains absent)";
8546
+ const kinds = [...new Set(descriptor.roots.map((root) => root.kind))].join(", ");
8547
+ return "attested " + kinds + " (sha256 " + (descriptor.fingerprint?.slice(0, 12) ?? "unknown") + "...)";
8548
+ }
8234
8549
 
8235
8550
  // src/cli/verify.ts
8236
8551
  var OVERLAY_CLASSES = ["tests", "snapshots", "config"];
@@ -8320,25 +8635,25 @@ function baseIsAncestorOfHead(base, cwd) {
8320
8635
  const r = spawnSync("git", ["merge-base", "--is-ancestor", base, "HEAD"], { cwd, env: trustedGitEnv() });
8321
8636
  return r.status === 0;
8322
8637
  }
8323
- function inside(root, path) {
8324
- const rel = relative4(resolve4(root), resolve4(path));
8325
- return rel === "" || rel !== ".." && !rel.startsWith(`..${sep}`) && !isAbsolute9(rel);
8638
+ function inside2(root, path) {
8639
+ const rel = relative5(resolve5(root), resolve5(path));
8640
+ return rel === "" || rel !== ".." && !rel.startsWith(`..${sep2}`) && !isAbsolute10(rel);
8326
8641
  }
8327
8642
  function linkParts(path) {
8328
- const parts = sep === "\\" ? path.split(/[\\/]+/) : path.split("/");
8643
+ const parts = sep2 === "\\" ? path.split(/[\\/]+/) : path.split("/");
8329
8644
  return parts.filter((part) => part !== "" && part !== ".");
8330
8645
  }
8331
8646
  function linkEscape(label, target) {
8332
8647
  return new Error(`${label} is a symlink that escapes the materialised tree (${JSON.stringify(target)})`);
8333
8648
  }
8334
8649
  function driveRelativeLinkTarget(target) {
8335
- return sep === "\\" && /^[A-Za-z]:(?![\\/])/.test(target);
8650
+ return sep2 === "\\" && /^[A-Za-z]:(?![\\/])/.test(target);
8336
8651
  }
8337
8652
  function rootedLinkTarget(target) {
8338
- return isAbsolute9(target) || driveRelativeLinkTarget(target);
8653
+ return isAbsolute10(target) || driveRelativeLinkTarget(target);
8339
8654
  }
8340
8655
  function safeSymlink(target, out2, root, label) {
8341
- if (rootedLinkTarget(target) || !inside(root, resolve4(dirname5(out2), target))) {
8656
+ if (rootedLinkTarget(target) || !inside2(root, resolve5(dirname6(out2), target))) {
8342
8657
  throw linkEscape(label, target);
8343
8658
  }
8344
8659
  rmSync3(out2, { force: true });
@@ -8346,7 +8661,7 @@ function safeSymlink(target, out2, root, label) {
8346
8661
  }
8347
8662
  function validateSymlinkGraph(target, out2, root, dependencyRoot, label) {
8348
8663
  let domain = "tree";
8349
- let stack = linkParts(relative4(root, dirname5(out2)));
8664
+ let stack = linkParts(relative5(root, dirname6(out2)));
8350
8665
  const pending = linkParts(target);
8351
8666
  const seen = /* @__PURE__ */ new Set();
8352
8667
  let hops = 0;
@@ -8367,7 +8682,7 @@ function validateSymlinkGraph(target, out2, root, dependencyRoot, label) {
8367
8682
  const current = join14(base, ...stack);
8368
8683
  let st;
8369
8684
  try {
8370
- st = lstatSync3(current);
8685
+ st = lstatSync4(current);
8371
8686
  } catch {
8372
8687
  continue;
8373
8688
  }
@@ -8375,7 +8690,7 @@ function validateSymlinkGraph(target, out2, root, dependencyRoot, label) {
8375
8690
  if (++hops > 128) {
8376
8691
  throw new Error(`${label} contains a symlink cycle while materialising (${JSON.stringify(target)})`);
8377
8692
  }
8378
- const key2 = `${domain}:${current}:${pending.join(sep)}`;
8693
+ const key2 = `${domain}:${current}:${pending.join(sep2)}`;
8379
8694
  if (seen.has(key2)) {
8380
8695
  throw new Error(`${label} contains a symlink cycle while materialising (${JSON.stringify(target)})`);
8381
8696
  }
@@ -8385,12 +8700,12 @@ function validateSymlinkGraph(target, out2, root, dependencyRoot, label) {
8385
8700
  if (driveRelativeLinkTarget(next)) {
8386
8701
  throw linkEscape(label, target);
8387
8702
  }
8388
- if (isAbsolute9(next)) {
8389
- if (domain === "tree" || !dependencyRoot || !inside(dependencyRoot, next)) {
8703
+ if (isAbsolute10(next)) {
8704
+ if (domain === "tree" || !dependencyRoot || !inside2(dependencyRoot, next)) {
8390
8705
  throw linkEscape(label, target);
8391
8706
  }
8392
8707
  stack = [];
8393
- pending.unshift(...linkParts(relative4(dependencyRoot, resolve4(next))));
8708
+ pending.unshift(...linkParts(relative5(dependencyRoot, resolve5(next))));
8394
8709
  } else {
8395
8710
  pending.unshift(...linkParts(next));
8396
8711
  }
@@ -8403,16 +8718,16 @@ function rejectLinkedParent(cwd, rel) {
8403
8718
  current = join14(current, part);
8404
8719
  let st;
8405
8720
  try {
8406
- st = lstatSync3(current);
8721
+ st = lstatSync4(current);
8407
8722
  } catch {
8408
8723
  continue;
8409
8724
  }
8410
8725
  if (st.isSymbolicLink()) {
8411
- throw new Error(`${rel} has a symlinked parent directory (${relative4(cwd, current)})`);
8726
+ throw new Error(`${rel} has a symlinked parent directory (${relative5(cwd, current)})`);
8412
8727
  }
8413
8728
  }
8414
8729
  }
8415
- function materialize(cwd, dest) {
8730
+ function materialize(cwd, dest, dependencyRoot) {
8416
8731
  const listed = git2(["ls-files", "-z", "--cached", "--others", "--exclude-standard"], cwd).split("\0").filter(Boolean);
8417
8732
  const links = [];
8418
8733
  for (const rel of listed) {
@@ -8420,12 +8735,12 @@ function materialize(cwd, dest) {
8420
8735
  rejectLinkedParent(cwd, rel);
8421
8736
  let st;
8422
8737
  try {
8423
- st = lstatSync3(src);
8738
+ st = lstatSync4(src);
8424
8739
  } catch {
8425
8740
  continue;
8426
8741
  }
8427
8742
  const out2 = join14(dest, rel);
8428
- mkdirSync6(dirname5(out2), { recursive: true });
8743
+ mkdirSync6(dirname6(out2), { recursive: true });
8429
8744
  dropSymlink(out2);
8430
8745
  if (st.isSymbolicLink()) {
8431
8746
  const target = readlinkSync3(src);
@@ -8438,17 +8753,14 @@ function materialize(cwd, dest) {
8438
8753
  throw new Error(`${rel} is ${st.isDirectory() ? "a directory" : "a special file"} where git expects a file`);
8439
8754
  }
8440
8755
  }
8441
- const nm = resolve4(cwd, "node_modules");
8442
- let dependencyRoot = null;
8443
- if (existsSync10(nm)) dependencyRoot = realpathSync3(nm);
8444
8756
  for (const link of links) {
8445
8757
  validateSymlinkGraph(link.target, link.out, dest, dependencyRoot, link.rel);
8446
8758
  }
8447
- if (dependencyRoot) symlinkSync(nm, join14(dest, "node_modules"), "dir");
8759
+ if (dependencyRoot) symlinkSync(dependencyRoot, join14(dest, "node_modules"), "dir");
8448
8760
  }
8449
8761
  function dropSymlink(p) {
8450
8762
  try {
8451
- if (lstatSync3(p).isSymbolicLink()) rmSync3(p, { force: true });
8763
+ if (lstatSync4(p).isSymbolicLink()) rmSync3(p, { force: true });
8452
8764
  } catch {
8453
8765
  }
8454
8766
  }
@@ -8473,13 +8785,13 @@ function baseEntries(base, cwd) {
8473
8785
  });
8474
8786
  }
8475
8787
  function overlayDigest(dest, paths) {
8476
- const h = createHash8("sha256");
8788
+ const h = createHash9("sha256");
8477
8789
  for (const rel of [...paths].sort()) {
8478
8790
  h.update(rel);
8479
8791
  h.update("\0");
8480
8792
  const p = join14(dest, rel);
8481
8793
  try {
8482
- const st = lstatSync3(p);
8794
+ const st = lstatSync4(p);
8483
8795
  h.update(String(st.mode));
8484
8796
  h.update(st.isSymbolicLink() ? readlinkSync3(p) : readFileSync14(p));
8485
8797
  } catch {
@@ -8489,7 +8801,7 @@ function overlayDigest(dest, paths) {
8489
8801
  }
8490
8802
  return h.digest("hex");
8491
8803
  }
8492
- function overlayPristine(cwd, base, dest, policy, cmd) {
8804
+ function overlayPristine(cwd, base, dest, policy, cmd, dependencyRoot) {
8493
8805
  const entries = baseEntries(base, cwd);
8494
8806
  const atBase = entries.map((e) => e.path);
8495
8807
  const verifierOwned = verifierInputs(cmd, atBase, policy);
@@ -8506,7 +8818,7 @@ function overlayPristine(cwd, base, dest, policy, cmd) {
8506
8818
  }
8507
8819
  const content = execFileSync8("git", ["show", `${base}:${e.path}`], { cwd, maxBuffer: 1 << 28, env: trustedGitEnv() });
8508
8820
  const out2 = join14(dest, e.path);
8509
- mkdirSync6(dirname5(out2), { recursive: true });
8821
+ mkdirSync6(dirname6(out2), { recursive: true });
8510
8822
  rmSync3(out2, { force: true });
8511
8823
  if (e.mode === "120000") {
8512
8824
  const target = content.toString("utf8");
@@ -8525,8 +8837,6 @@ function overlayPristine(cwd, base, dest, policy, cmd) {
8525
8837
  rmSync3(join14(dest, rel), { force: true });
8526
8838
  removed++;
8527
8839
  }
8528
- const nm = resolve4(cwd, "node_modules");
8529
- const dependencyRoot = existsSync10(nm) ? realpathSync3(nm) : null;
8530
8840
  for (const link of restoredLinks) {
8531
8841
  validateSymlinkGraph(link.target, link.out, dest, dependencyRoot, `the base's ${link.path}`);
8532
8842
  }
@@ -8638,6 +8948,22 @@ function runVerify(opts) {
8638
8948
  return 2;
8639
8949
  }
8640
8950
  const budget = opts.budget ?? policy.verify?.budget ?? 300;
8951
+ const dependencyEnvironment = opts.dependencyEnvironment ?? discoverDependencyEnvironment(cwd, cmd);
8952
+ const dependencyReport = () => dependencyEnvironmentReport(dependencyEnvironment);
8953
+ if (dependencyEnvironment.status === "unattestable" && !opts.allowDepDrift) {
8954
+ if (opts.json) {
8955
+ out2(JSON.stringify({
8956
+ verdict: "CANNOT_VERIFY",
8957
+ reason: "DEPENDENCY_ENVIRONMENT_UNATTESTABLE",
8958
+ dependency_environment: dependencyReport()
8959
+ }));
8960
+ } else {
8961
+ out2("verify: dependency environment is not attestable \u2014 failing closed");
8962
+ out2("verify: " + (dependencyEnvironment.reason ?? "unknown dependency environment"));
8963
+ }
8964
+ return 2;
8965
+ }
8966
+ const frozenNodeModules = dependencyEnvironment.roots.find((root) => root.kind === "node_modules")?.realPath ?? null;
8641
8967
  let base;
8642
8968
  try {
8643
8969
  base = resolveBase(opts.base ?? "HEAD", cwd);
@@ -8653,10 +8979,16 @@ function runVerify(opts) {
8653
8979
  };
8654
8980
  const protectedIgnored = (rel) => isProtected(rel, policy);
8655
8981
  const treeBefore = treeFingerprint(cwd, protectedIgnored);
8656
- const depsBefore = depsFingerprint(cwd);
8982
+ const dependencyAtEntry = checkDependencyEnvironment(cwd, dependencyEnvironment);
8983
+ if (!dependencyAtEntry.ok && !opts.allowDepDrift) {
8984
+ cleanup([visRoot]);
8985
+ out2("verify: the frozen dependency environment changed before the visible suite ran \u2014");
8986
+ out2((dependencyAtEntry.reason ?? "dependency identity changed") + ". Failing closed, not open.");
8987
+ return 2;
8988
+ }
8657
8989
  try {
8658
8990
  mkdirSync6(visDir);
8659
- materialize(cwd, visDir);
8991
+ materialize(cwd, visDir, frozenNodeModules);
8660
8992
  } catch (e) {
8661
8993
  cleanup([visRoot]);
8662
8994
  out2(`verify: could not materialize (${e instanceof Error ? e.message : String(e)}) \u2014 failing closed`);
@@ -8669,10 +9001,11 @@ function runVerify(opts) {
8669
9001
  out2("would be materialised from a tree the candidate just edited. Failing closed, not open.");
8670
9002
  return 2;
8671
9003
  }
8672
- if (depsFingerprint(cwd) !== depsBefore) {
9004
+ const dependencyAfterVisible = checkDependencyEnvironment(cwd, dependencyEnvironment);
9005
+ if (!dependencyAfterVisible.ok && !opts.allowDepDrift) {
8673
9006
  cleanup([visRoot]);
8674
- out2("verify: the installed dependency tree changed while the visible suite was running \u2014 the");
8675
- out2("pristine run executes through it too. Failing closed, not open.");
9007
+ out2("verify: the frozen dependency environment changed while the visible suite was running \u2014");
9008
+ out2((dependencyAfterVisible.reason ?? "dependency identity changed") + ". Failing closed, not open.");
8676
9009
  return 2;
8677
9010
  }
8678
9011
  const priRoot = mkdtempSync2(join14(tmpdir2(), "tw-verify-pri-"));
@@ -8681,8 +9014,15 @@ function runVerify(opts) {
8681
9014
  let removedAdded = 0;
8682
9015
  try {
8683
9016
  mkdirSync6(priDir);
8684
- materialize(cwd, priDir);
8685
- ({ restored, removed: removedAdded } = overlayPristine(cwd, base, priDir, policy, cmd));
9017
+ materialize(cwd, priDir, frozenNodeModules);
9018
+ ({ restored, removed: removedAdded } = overlayPristine(
9019
+ cwd,
9020
+ base,
9021
+ priDir,
9022
+ policy,
9023
+ cmd,
9024
+ frozenNodeModules
9025
+ ));
8686
9026
  } catch (e) {
8687
9027
  cleanup([visRoot, priRoot]);
8688
9028
  out2(`verify: could not materialize (${e instanceof Error ? e.message : String(e)}) \u2014 failing closed`);
@@ -8692,11 +9032,12 @@ function runVerify(opts) {
8692
9032
  const pristine = runSuite(priDir, cmd, budget);
8693
9033
  const overlayMoved = overlayDigest(priDir, restored) !== overlayBefore;
8694
9034
  const treeMoved = treeFingerprint(cwd, protectedIgnored) !== treeBefore;
8695
- const depsMoved = depsFingerprint(cwd) !== depsBefore;
9035
+ const dependencyAfterPristine = checkDependencyEnvironment(cwd, dependencyEnvironment);
9036
+ const depsMoved = !dependencyAfterPristine.ok && !opts.allowDepDrift;
8696
9037
  cleanup([visRoot, priRoot]);
8697
9038
  if (overlayMoved || treeMoved || depsMoved) {
8698
9039
  out2(
8699
- "verify: " + (overlayMoved ? "a restored file in the pristine copy changed while the pristine suite was running" : treeMoved ? "the working tree changed while the pristine suite was running" : "the installed dependency tree changed while the pristine suite was running") + " \u2014"
9040
+ "verify: " + (overlayMoved ? "a restored file in the pristine copy changed while the pristine suite was running" : treeMoved ? "the working tree changed while the pristine suite was running" : "the frozen dependency environment changed while the pristine suite was running") + " \u2014"
8700
9041
  );
8701
9042
  out2("the verdict would describe something other than what ran. Failing closed, not open.");
8702
9043
  out2("(A suite that rewrites its own snapshots or test files in place will trip this; run it in");
@@ -8731,6 +9072,7 @@ function runVerify(opts) {
8731
9072
  pristine: { exit: pristine.exit, secs: pristine.secs },
8732
9073
  protected_restored: restored.length,
8733
9074
  added_protected_removed: removedAdded,
9075
+ dependency_environment: dependencyReport(),
8734
9076
  ...signedOff ? { oob_signoff: signedOff } : {},
8735
9077
  ...opts.keep ? { visible_dir: visDir, pristine_dir: priDir } : {}
8736
9078
  })
@@ -8743,6 +9085,7 @@ function runVerify(opts) {
8743
9085
  BUDGET_EXCEEDED: `budget exceeded (${budget}s): could not verify \u2014 failing closed, not open.`
8744
9086
  };
8745
9087
  out2(`tamperward verify \u2014 ${lines[verdict2]}`);
9088
+ out2(`dependency environment: ${dependencyEnvironmentSummary(dependencyEnvironment)}` + (opts.allowDepDrift && dependencyEnvironment.status === "unattestable" ? " (operator override)" : ""));
8746
9089
  if (signedOff)
8747
9090
  out2(
8748
9091
  `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.`
@@ -8788,8 +9131,8 @@ function parseVerify(args) {
8788
9131
 
8789
9132
  // src/cli/run.ts
8790
9133
  import { execFileSync as execFileSync9, spawnSync as spawnSync2 } from "node:child_process";
8791
- import { readdirSync as readdirSync4, readFileSync as readFileSync15, readlinkSync as readlinkSync4, realpathSync as realpathSync4 } from "node:fs";
8792
- import { resolve as resolve5 } from "node:path";
9134
+ import { readdirSync as readdirSync4, readFileSync as readFileSync15, readlinkSync as readlinkSync4, realpathSync as realpathSync5 } from "node:fs";
9135
+ import { resolve as resolve6 } from "node:path";
8793
9136
  var out = (s) => process.stdout.write(s + "\n");
8794
9137
  var err = (s) => process.stderr.write(s + "\n");
8795
9138
  function git3(args, cwd) {
@@ -8806,7 +9149,7 @@ function survivorsHoldingTree(cwd, spawnedAfterTicks) {
8806
9149
  const out2 = [];
8807
9150
  let real;
8808
9151
  try {
8809
- real = realpathSync4(cwd);
9152
+ real = realpathSync5(cwd);
8810
9153
  } catch {
8811
9154
  return out2;
8812
9155
  }
@@ -8835,7 +9178,7 @@ function survivorsHoldingTree(cwd, spawnedAfterTicks) {
8835
9178
  for (const link of procLinks) {
8836
9179
  try {
8837
9180
  const raw = readlinkSync4(link).replace(/ \(deleted\)$/, "");
8838
- const held = raw.startsWith("/") ? resolve5(raw) : raw;
9181
+ const held = raw.startsWith("/") ? resolve6(raw) : raw;
8839
9182
  if (held === real || held.startsWith(real + "/")) {
8840
9183
  holds = true;
8841
9184
  break;
@@ -8848,7 +9191,7 @@ function survivorsHoldingTree(cwd, spawnedAfterTicks) {
8848
9191
  return out2;
8849
9192
  }
8850
9193
  function runEnvelope(opts) {
8851
- const cwd = resolve5(opts.cwd ?? process.cwd());
9194
+ const cwd = resolve6(opts.cwd ?? process.cwd());
8852
9195
  if (!opts.argv.length) {
8853
9196
  err("tamperward run: no agent command given (usage: tamperward run [options] -- <command...>)");
8854
9197
  return 2;
@@ -8884,8 +9227,15 @@ function runEnvelope(opts) {
8884
9227
  return 2;
8885
9228
  }
8886
9229
  }
9230
+ const dependencyEnvironment = discoverDependencyEnvironment(cwd, frozenCmd);
8887
9231
  out(`tamperward run \u2014 trusted base ${base.slice(0, 10)}; agent exit is untrusted.`);
8888
- const depsBefore = depsFingerprint(cwd);
9232
+ out(`tamperward run \u2014 dependency environment: ${dependencyEnvironmentSummary(dependencyEnvironment)}`);
9233
+ if (dependencyEnvironment.status === "unattestable" && !opts.allowDepDrift) {
9234
+ err("tamperward run: the verifier dependency environment cannot be attested \u2014");
9235
+ err(`${dependencyEnvironment.reason ?? "unknown dependency environment"}. Failing closed before the agent starts.`);
9236
+ err("(Pass --allow-dep-drift only if you explicitly own this dependency-integrity risk.)");
9237
+ return 2;
9238
+ }
8889
9239
  const rewritesBefore = objectRewriteState(cwd);
8890
9240
  const spawnTicks = nowTicks();
8891
9241
  const agent = spawnSync2(opts.argv[0], opts.argv.slice(1), { cwd, stdio: "inherit" });
@@ -8898,9 +9248,10 @@ function runEnvelope(opts) {
8898
9248
  tamperward run \u2014 agent exit ${agentExit}; OBJECT_REWRITE \u2192 ENFORCEMENT_FAILED`);
8899
9249
  return 1;
8900
9250
  }
8901
- if (depsFingerprint(cwd) !== depsBefore) {
8902
- err("tamperward run: the installed dependency tree changed before adjudication began \u2014 the program");
8903
- err("that runs the suite is not the one we started with, and both verification runs execute it.");
9251
+ const dependencyBeforeAdjudication = checkDependencyEnvironment(cwd, dependencyEnvironment);
9252
+ if (!dependencyBeforeAdjudication.ok) {
9253
+ err("tamperward run: the frozen dependency environment changed before adjudication began \u2014");
9254
+ err(`${dependencyBeforeAdjudication.reason ?? "dependency identity changed"}.`);
8904
9255
  if (!opts.allowDepDrift) {
8905
9256
  out(`
8906
9257
  tamperward run \u2014 agent exit ${agentExit}; DEPENDENCY_DRIFT \u2192 ENFORCEMENT_FAILED`);
@@ -8928,7 +9279,15 @@ tamperward run \u2014 agent exit ${agentExit}; HISTORY_REWRITE \u2192 ENFORCEMEN
8928
9279
  out("\n[envelope] policy check over the working tree (frozen policy, untracked included, no local ledger):");
8929
9280
  const workCode = runCheck({ worktree: true, cwd, policyOverride: frozenPolicy, includeUntracked: true, ciLayer: true });
8930
9281
  out("\n[envelope] pristine verification against the trusted base:");
8931
- const verifyCode = runVerify({ cwd, base, cmd: frozenCmd, budget: frozenBudget, policyOverride: frozenPolicy });
9282
+ const verifyCode = runVerify({
9283
+ cwd,
9284
+ base,
9285
+ cmd: frozenCmd,
9286
+ budget: frozenBudget,
9287
+ policyOverride: frozenPolicy,
9288
+ dependencyEnvironment,
9289
+ allowDepDrift: opts.allowDepDrift
9290
+ });
8932
9291
  if (opts.settle && opts.settle > 0) {
8933
9292
  try {
8934
9293
  execFileSync9("sleep", [String(opts.settle)]);
@@ -8940,10 +9299,11 @@ tamperward run \u2014 agent exit ${agentExit}; HISTORY_REWRITE \u2192 ENFORCEMEN
8940
9299
  err("tamperward run: the working tree changed while it was being adjudicated \u2014");
8941
9300
  err("the verdict below would describe a tree that no longer exists.");
8942
9301
  }
8943
- const depsDrifted = !opts.allowDepDrift && depsFingerprint(cwd) !== depsBefore;
9302
+ const dependencyAfter = checkDependencyEnvironment(cwd, dependencyEnvironment);
9303
+ const depsDrifted = !opts.allowDepDrift && !dependencyAfter.ok;
8944
9304
  if (depsDrifted) {
8945
- err("tamperward run: the installed dependency tree changed during this run \u2014 the program that");
8946
- err("runs the suite is not the one we started with. Both verification runs execute it.");
9305
+ err("tamperward run: the frozen dependency environment changed during this run \u2014");
9306
+ err(`${dependencyAfter.reason ?? "dependency identity changed"}. Both verification runs depend on it.`);
8947
9307
  }
8948
9308
  const rewrote = objectRewriteState(cwd) !== rewritesBefore;
8949
9309
  if (rewrote) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tamperward",
3
- "version": "2.10.7",
3
+ "version": "2.10.9",
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",