tamperward 2.10.8 → 2.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +28 -5
  2. package/dist/cli/index.js +997 -118
  3. 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, resolve8) {
1655
+ const obj = resolve8(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(resolve8(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 resolve8 = 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 = resolve8(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, resolve8);
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, resolve8);
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 resolve8 = 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(resolve8(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(resolve8(node.initializer));
1784
1784
  res.ignore.push(...items);
1785
1785
  if (opaque) res.opaqueIgnore = true;
1786
1786
  }
@@ -3064,6 +3064,18 @@ function policyWeakening(before, after) {
3064
3064
  `verify.inputs narrowed (removed ${droppedInputs.join(", ")}) \u2014 the pristine run would execute the candidate's copy of those files`
3065
3065
  );
3066
3066
  }
3067
+ const beforeBackend = bv.backend ?? "local";
3068
+ const afterBackend = av.backend ?? "local";
3069
+ if (beforeBackend === "container" && afterBackend !== "container") {
3070
+ reasons.push(
3071
+ `verify.backend lowered container \u2192 ${afterBackend} \u2014 final verification would leave the isolated trust domain`
3072
+ );
3073
+ }
3074
+ if (beforeBackend === "container" && afterBackend === "container" && av.image !== bv.image) {
3075
+ reasons.push(
3076
+ `verify.image changed (${bv.image ?? "<missing>"} \u2192 ${av.image ?? "<missing>"}) \u2014 the trusted verifier image identity changed`
3077
+ );
3078
+ }
3067
3079
  }
3068
3080
  }
3069
3081
  return reasons;
@@ -6342,6 +6354,21 @@ function validate(r, where) {
6342
6354
  bad(`verify.budget must be a positive number of seconds, got ${show(v.budget)}`);
6343
6355
  }
6344
6356
  if (v.inputs !== void 0 && !isStringList(v.inputs)) bad(`verify.inputs must be a list of globs, got ${show(v.inputs)}`);
6357
+ if (v.backend !== void 0 && v.backend !== "local" && v.backend !== "container") {
6358
+ bad(`verify.backend must be "local" or "container", got ${show(v.backend)}`);
6359
+ }
6360
+ if (v.image !== void 0 && typeof v.image !== "string") {
6361
+ bad(`verify.image must be an immutable container image reference, got ${show(v.image)}`);
6362
+ }
6363
+ const backend = v.backend ?? "local";
6364
+ if (backend === "container") {
6365
+ if (typeof v.image !== "string") bad('verify.image is required when verify.backend is "container"');
6366
+ if (!/^(?!-)[^\s@]+@sha256:[0-9a-f]{64}$/i.test(v.image)) {
6367
+ bad(`verify.image must be pinned by sha256 digest (name@sha256:<64 hex>), got ${show(v.image)}`);
6368
+ }
6369
+ } else if (v.image !== void 0) {
6370
+ bad('verify.image is only valid when verify.backend is "container"');
6371
+ }
6345
6372
  }
6346
6373
  }
6347
6374
  function parsePolicy(raw, where = POLICY_FILE) {
@@ -6374,7 +6401,9 @@ function parsePolicy(raw, where = POLICY_FILE) {
6374
6401
  // Rebuilding the block field-by-field means a field added later and
6375
6402
  // not listed here is SILENTLY DROPPED — which is how `inputs` first
6376
6403
  // shipped as a no-op that parsed, validated, and restored nothing.
6377
- ...Array.isArray(r.verify.inputs) ? { inputs: r.verify.inputs.map(String) } : {}
6404
+ ...Array.isArray(r.verify.inputs) ? { inputs: r.verify.inputs.map(String) } : {},
6405
+ ...r.verify.backend ? { backend: r.verify.backend } : {},
6406
+ ...r.verify.image ? { image: r.verify.image } : {}
6378
6407
  }
6379
6408
  } : {}
6380
6409
  };
@@ -8162,17 +8191,15 @@ tamperward init: ${changed} change(s) applied. Commit them so the gate travels w
8162
8191
  }
8163
8192
 
8164
8193
  // src/cli/verify.ts
8165
- import { execFileSync as execFileSync8, spawnSync } from "node:child_process";
8166
- import { createHash as createHash8 } from "node:crypto";
8167
- 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";
8194
+ import { execFileSync as execFileSync8, spawnSync as spawnSync2 } from "node:child_process";
8195
+ import { createHash as createHash10 } from "node:crypto";
8196
+ import { chmodSync as chmodSync2, cpSync, lstatSync as lstatSync4, mkdirSync as mkdirSync6, mkdtempSync as mkdtempSync2, readFileSync as readFileSync15, readlinkSync as readlinkSync3, rmSync as rmSync3, symlinkSync, writeFileSync as writeFileSync6 } from "node:fs";
8168
8197
  import { tmpdir as tmpdir2 } from "node:os";
8169
- import { dirname as dirname5, isAbsolute as isAbsolute9, join as join14, relative as relative4, resolve as resolve4, sep } from "node:path";
8198
+ import { dirname as dirname7, isAbsolute as isAbsolute10, join as join15, relative as relative5, resolve as resolve6, sep as sep2 } from "node:path";
8170
8199
 
8171
8200
  // src/fingerprint.ts
8172
8201
  import { execFileSync as execFileSync7 } from "node:child_process";
8173
8202
  import { createHash as createHash7 } from "node:crypto";
8174
- import { readdirSync as readdirSync3, readFileSync as readFileSync13, readlinkSync as readlinkSync2, statSync as statSync4 } from "node:fs";
8175
- import { join as join13 } from "node:path";
8176
8203
  function treeFingerprint(cwd, keepIgnored) {
8177
8204
  const listed = execFileSync7("git", ["ls-files", "-z", "--cached", "--others", "--exclude-standard"], {
8178
8205
  cwd,
@@ -8195,44 +8222,772 @@ function treeFingerprint(cwd, keepIgnored) {
8195
8222
  }
8196
8223
  return h.digest("hex");
8197
8224
  }
8198
- function depsFingerprint(cwd) {
8199
- const root = join13(cwd, "node_modules");
8200
- const h = createHash7("sha256");
8201
- const walk2 = (dir) => {
8225
+
8226
+ // src/dependency-env.ts
8227
+ import { createHash as createHash8 } from "node:crypto";
8228
+ import {
8229
+ existsSync as existsSync10,
8230
+ lstatSync as lstatSync3,
8231
+ readdirSync as readdirSync3,
8232
+ readFileSync as readFileSync13,
8233
+ readlinkSync as readlinkSync2,
8234
+ realpathSync as realpathSync3,
8235
+ statSync as statSync4
8236
+ } from "node:fs";
8237
+ import {
8238
+ basename as basename3,
8239
+ delimiter,
8240
+ dirname as dirname5,
8241
+ isAbsolute as isAbsolute9,
8242
+ join as join13,
8243
+ relative as relative4,
8244
+ resolve as resolve4,
8245
+ sep
8246
+ } from "node:path";
8247
+ var Unattestable = class extends Error {
8248
+ };
8249
+ function inside(root, candidate) {
8250
+ const rel = relative4(resolve4(root), resolve4(candidate));
8251
+ return rel === "" || rel !== ".." && !rel.startsWith(".." + sep) && !isAbsolute9(rel);
8252
+ }
8253
+ function modeKind(path) {
8254
+ const st = lstatSync3(path);
8255
+ if (st.isFile()) return { kind: "file", mode: st.mode };
8256
+ if (st.isDirectory()) return { kind: "dir", mode: st.mode };
8257
+ if (st.isSymbolicLink()) return { kind: "symlink", mode: st.mode };
8258
+ return { kind: "special", mode: st.mode };
8259
+ }
8260
+ function fingerprintRoot(root, cwd, h) {
8261
+ let rootStat;
8262
+ let currentReal;
8263
+ try {
8264
+ rootStat = lstatSync3(root.path);
8265
+ currentReal = realpathSync3(root.path);
8266
+ } catch (e) {
8267
+ throw new Unattestable(
8268
+ root.kind + " root " + JSON.stringify(root.path) + " can no longer be resolved (" + (e instanceof Error ? e.message : String(e)) + ")"
8269
+ );
8270
+ }
8271
+ if (currentReal !== root.realPath) {
8272
+ throw new Unattestable(
8273
+ root.kind + " root moved from " + JSON.stringify(root.realPath) + " to " + JSON.stringify(currentReal)
8274
+ );
8275
+ }
8276
+ if (!statSync4(root.path).isDirectory()) {
8277
+ throw new Unattestable(root.kind + " root " + JSON.stringify(root.path) + " is not a directory");
8278
+ }
8279
+ h.update("root\0" + root.kind + "\0" + root.path + "\0" + root.realPath + "\0" + String(rootStat.mode) + "\0");
8280
+ if (rootStat.isSymbolicLink()) h.update(readlinkSync2(root.path) + "\0");
8281
+ const cwdReal = realpathSync3(cwd);
8282
+ const visited = /* @__PURE__ */ new Set();
8283
+ const hashFile = (path, label) => {
8284
+ let st;
8285
+ try {
8286
+ st = statSync4(path);
8287
+ if (!st.isFile()) throw new Error("not a regular file");
8288
+ h.update(label + "\0external-file\0" + path + "\0" + String(st.mode) + "\0");
8289
+ h.update(readFileSync13(path));
8290
+ h.update("\0");
8291
+ } catch (e) {
8292
+ throw new Unattestable(
8293
+ "dependency link target " + JSON.stringify(path) + " is not attestable (" + (e instanceof Error ? e.message : String(e)) + ")"
8294
+ );
8295
+ }
8296
+ };
8297
+ const walk2 = (dir, namespace) => {
8298
+ const realDir = realpathSync3(dir);
8299
+ if (visited.has(realDir)) return;
8300
+ visited.add(realDir);
8202
8301
  let entries;
8203
8302
  try {
8204
8303
  entries = readdirSync3(dir, { withFileTypes: true });
8205
- } catch {
8206
- return;
8304
+ } catch (e) {
8305
+ throw new Unattestable(
8306
+ "dependency directory " + JSON.stringify(dir) + " cannot be read (" + (e instanceof Error ? e.message : String(e)) + ")"
8307
+ );
8207
8308
  }
8208
- for (const e of [...entries].sort((a, b) => a.name.localeCompare(b.name))) {
8209
- const p = join13(dir, e.name);
8210
- if (e.isDirectory()) walk2(p);
8211
- else if (e.isFile()) {
8212
- h.update(p);
8309
+ for (const entry of [...entries].sort((a, b) => a.name.localeCompare(b.name))) {
8310
+ const path = join13(dir, entry.name);
8311
+ const rel = namespace ? namespace + "/" + entry.name : entry.name;
8312
+ let ident;
8313
+ try {
8314
+ ident = modeKind(path);
8315
+ } catch (e) {
8316
+ throw new Unattestable(
8317
+ "dependency entry " + JSON.stringify(path) + " disappeared while attesting (" + (e instanceof Error ? e.message : String(e)) + ")"
8318
+ );
8319
+ }
8320
+ h.update(rel + "\0" + ident.kind + "\0" + String(ident.mode) + "\0");
8321
+ if (ident.kind === "file") {
8213
8322
  try {
8214
- h.update(readFileSync13(p));
8215
- h.update(String(statSync4(p).mode));
8216
- } catch {
8217
- h.update("<unreadable>");
8323
+ h.update(readFileSync13(path));
8324
+ h.update("\0");
8325
+ } catch (e) {
8326
+ throw new Unattestable(
8327
+ "dependency file " + JSON.stringify(path) + " cannot be read (" + (e instanceof Error ? e.message : String(e)) + ")"
8328
+ );
8329
+ }
8330
+ continue;
8331
+ }
8332
+ if (ident.kind === "dir") {
8333
+ walk2(path, rel);
8334
+ continue;
8335
+ }
8336
+ if (ident.kind === "symlink") {
8337
+ let target;
8338
+ try {
8339
+ target = readlinkSync2(path);
8340
+ } catch (e) {
8341
+ throw new Unattestable(
8342
+ "dependency symlink " + JSON.stringify(path) + " cannot be read (" + (e instanceof Error ? e.message : String(e)) + ")"
8343
+ );
8218
8344
  }
8219
- } else if (e.isSymbolicLink()) {
8220
- h.update(p);
8345
+ h.update(target + "\0");
8346
+ let resolved;
8221
8347
  try {
8222
- h.update(readlinkSync2(p));
8348
+ resolved = realpathSync3(path);
8223
8349
  } catch {
8224
- h.update("<unreadable-link>");
8350
+ continue;
8225
8351
  }
8352
+ const followed = statSync4(path);
8353
+ if (followed.isFile()) {
8354
+ hashFile(resolved, "@target:" + rel);
8355
+ continue;
8356
+ }
8357
+ if (followed.isDirectory()) {
8358
+ if (inside(root.realPath, resolved)) {
8359
+ walk2(resolved, "@target:" + rel);
8360
+ continue;
8361
+ }
8362
+ if (inside(cwdReal, resolved)) {
8363
+ throw new Unattestable(
8364
+ "dependency workspace symlink " + JSON.stringify(path) + " points into the candidate worktree at " + JSON.stringify(resolved) + "; faithful per-copy remapping is not available"
8365
+ );
8366
+ }
8367
+ throw new Unattestable(
8368
+ "dependency directory symlink " + JSON.stringify(path) + " escapes the bounded root to " + JSON.stringify(resolved)
8369
+ );
8370
+ }
8371
+ throw new Unattestable("dependency symlink " + JSON.stringify(path) + " resolves to a special file");
8226
8372
  }
8373
+ throw new Unattestable("dependency entry " + JSON.stringify(path) + " is a special file");
8227
8374
  }
8228
8375
  };
8376
+ walk2(root.realPath, "");
8377
+ }
8378
+ function snapshot(roots, probes, cwd) {
8379
+ const h = createHash8("sha256");
8380
+ for (const probe of probes) {
8381
+ h.update(
8382
+ "probe\0" + probe.kind + "\0" + probe.path + "\0" + (existsSync10(probe.path) ? "present" : "absent") + "\0"
8383
+ );
8384
+ }
8385
+ for (const root of roots) fingerprintRoot(root, cwd, h);
8386
+ return h.digest("hex");
8387
+ }
8388
+ function freezeRoot(kind, path) {
8229
8389
  try {
8230
- if (!statSync4(root).isDirectory()) return "none";
8390
+ if (!statSync4(path).isDirectory()) {
8391
+ throw new Error("not a directory");
8392
+ }
8393
+ return { kind, path: resolve4(path), realPath: realpathSync3(path) };
8394
+ } catch (e) {
8395
+ throw new Unattestable(
8396
+ kind + " root " + JSON.stringify(path) + " is not resolvable (" + (e instanceof Error ? e.message : String(e)) + ")"
8397
+ );
8398
+ }
8399
+ }
8400
+ function shellTokens(command) {
8401
+ const matches = command.match(/(?:[^\s"'\\]+|"(?:\\.|[^"])*"|'[^']*')+/g) ?? [];
8402
+ return matches.map((token) => {
8403
+ if (token.startsWith("'") && token.endsWith("'") || token.startsWith('"') && token.endsWith('"')) return token.slice(1, -1);
8404
+ return token;
8405
+ });
8406
+ }
8407
+ function firstExecutable(command) {
8408
+ const tokens2 = shellTokens(command);
8409
+ let i = 0;
8410
+ while (i < tokens2.length && /^[A-Za-z_][A-Za-z0-9_]*=/.test(tokens2[i])) i++;
8411
+ if (tokens2[i] === "env") {
8412
+ i++;
8413
+ while (i < tokens2.length && (tokens2[i].startsWith("-") || /^[A-Za-z_][A-Za-z0-9_]*=/.test(tokens2[i]))) i++;
8414
+ }
8415
+ if (tokens2[i] === "command") {
8416
+ i++;
8417
+ while (i < tokens2.length && tokens2[i].startsWith("-")) i++;
8418
+ }
8419
+ if (tokens2[i] === "timeout") {
8420
+ i++;
8421
+ while (i < tokens2.length && tokens2[i].startsWith("-")) i++;
8422
+ if (i < tokens2.length) i++;
8423
+ }
8424
+ return tokens2[i] ?? null;
8425
+ }
8426
+ function resolveExecutable(token, cwd, env) {
8427
+ if (!token) return null;
8428
+ if (token.includes("/") || token.includes("\\")) {
8429
+ const path = isAbsolute9(token) ? token : resolve4(cwd, token);
8430
+ try {
8431
+ return existsSync10(path) ? realpathSync3(path) : null;
8432
+ } catch {
8433
+ return null;
8434
+ }
8435
+ }
8436
+ for (const entry of (env.PATH ?? "").split(delimiter).filter(Boolean)) {
8437
+ const candidate = resolve4(entry, token);
8438
+ try {
8439
+ if (existsSync10(candidate) && statSync4(candidate).isFile()) return realpathSync3(candidate);
8440
+ } catch {
8441
+ }
8442
+ }
8443
+ return null;
8444
+ }
8445
+ function pythonName(name) {
8446
+ return /^(?:python(?:\d+(?:\.\d+)*)?|pytest|py\.test|pip(?:\d+(?:\.\d+)*)?)$/.test(name.toLowerCase());
8447
+ }
8448
+ function looksPython(command, executable, token) {
8449
+ const exe = executable ? basename3(executable) : "";
8450
+ const raw = token ? basename3(token) : "";
8451
+ if (pythonName(exe) || pythonName(raw)) return true;
8452
+ return /(?:^|[\s;&|()])(?:python(?:\d+(?:\.\d+)*)?|pytest|py\.test|pip(?:\d+(?:\.\d+)*)?)(?=$|[\s;&|()])/.test(command);
8453
+ }
8454
+ function inferredVenvFromToken(token, cwd) {
8455
+ if (!token || !token.includes("/") && !token.includes("\\")) return null;
8456
+ const path = isAbsolute9(token) ? token : resolve4(cwd, token);
8457
+ if (!pythonName(basename3(path))) return null;
8458
+ const parent = dirname5(path);
8459
+ const parentName = basename3(parent).toLowerCase();
8460
+ if (parentName !== "bin" && parentName !== "scripts") return null;
8461
+ const root = dirname5(parent);
8462
+ return existsSync10(join13(root, "pyvenv.cfg")) ? root : null;
8463
+ }
8464
+ function unsupportedEcosystem(command, token) {
8465
+ const name = token ? basename3(token).toLowerCase() : "";
8466
+ const raw = command.toLowerCase();
8467
+ 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";
8468
+ 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";
8469
+ if (name === "dotnet" || /(?:^|[\s;&|()])dotnet(?=$|[\s;&|()])/.test(raw)) return ".NET dependency environment discovery is not yet attestable";
8470
+ return null;
8471
+ }
8472
+ function inferredVenv(executable) {
8473
+ if (!executable) return null;
8474
+ const parent = dirname5(executable);
8475
+ const parentName = basename3(parent).toLowerCase();
8476
+ if (parentName !== "bin" && parentName !== "scripts") return null;
8477
+ const root = dirname5(parent);
8478
+ return existsSync10(join13(root, "pyvenv.cfg")) ? root : null;
8479
+ }
8480
+ function venvProblem(root) {
8481
+ const cfg = join13(root, "pyvenv.cfg");
8482
+ if (!existsSync10(cfg)) return "VIRTUAL_ENV " + JSON.stringify(root) + " has no pyvenv.cfg";
8483
+ try {
8484
+ const body = readFileSync13(cfg, "utf8");
8485
+ if (/^\s*include-system-site-packages\s*=\s*true\s*$/im.test(body)) {
8486
+ return "Python environment " + JSON.stringify(root) + " enables system site-packages; those external package roots are not bounded";
8487
+ }
8488
+ } catch (e) {
8489
+ return "cannot read " + JSON.stringify(cfg) + " (" + (e instanceof Error ? e.message : String(e)) + ")";
8490
+ }
8491
+ return null;
8492
+ }
8493
+ function discoverDependencyEnvironment(cwdInput, command, env = process.env) {
8494
+ const cwd = resolve4(cwdInput);
8495
+ const roots = [];
8496
+ const probes = [];
8497
+ const seen = /* @__PURE__ */ new Set();
8498
+ const addRoot = (kind, path) => {
8499
+ const root = freezeRoot(kind, path);
8500
+ const key2 = kind + ":" + root.path;
8501
+ if (!seen.has(key2)) {
8502
+ roots.push(root);
8503
+ seen.add(key2);
8504
+ }
8505
+ };
8506
+ try {
8507
+ const nodeModules = join13(cwd, "node_modules");
8508
+ if (existsSync10(nodeModules)) addRoot("node_modules", nodeModules);
8509
+ else probes.push({ kind: "node_modules", path: nodeModules });
8510
+ const token = firstExecutable(command);
8511
+ const executable = resolveExecutable(token, cwd, env);
8512
+ let venv = null;
8513
+ if (env.VIRTUAL_ENV?.trim()) {
8514
+ if (!isAbsolute9(env.VIRTUAL_ENV)) {
8515
+ throw new Unattestable("VIRTUAL_ENV is relative (" + JSON.stringify(env.VIRTUAL_ENV) + ")");
8516
+ }
8517
+ venv = resolve4(env.VIRTUAL_ENV);
8518
+ } else {
8519
+ venv = inferredVenv(executable) ?? inferredVenvFromToken(token, cwd);
8520
+ if (!venv && /(?:^|\s)uv\s+run(?:\s|$)/.test(command) && existsSync10(join13(cwd, ".venv", "pyvenv.cfg"))) {
8521
+ venv = join13(cwd, ".venv");
8522
+ }
8523
+ }
8524
+ if (venv) {
8525
+ const problem = venvProblem(venv);
8526
+ if (problem) throw new Unattestable(problem);
8527
+ addRoot("python-venv", venv);
8528
+ } else if (looksPython(command, executable, token)) {
8529
+ throw new Unattestable(
8530
+ "Python verifier selected without an identifiable virtual environment; global/user site-packages are not bounded"
8531
+ );
8532
+ }
8533
+ const unsupported = unsupportedEcosystem(command, token);
8534
+ if (unsupported) throw new Unattestable(unsupported);
8535
+ const status = roots.length ? "attested" : "none";
8536
+ const fingerprint2 = snapshot(roots, probes, cwd);
8537
+ return { status, command, roots, probes, fingerprint: fingerprint2 };
8538
+ } catch (e) {
8539
+ return {
8540
+ status: "unattestable",
8541
+ command,
8542
+ roots,
8543
+ probes,
8544
+ reason: e instanceof Error ? e.message : String(e)
8545
+ };
8546
+ }
8547
+ }
8548
+ function checkDependencyEnvironment(cwdInput, descriptor) {
8549
+ if (descriptor.status === "unattestable") {
8550
+ return { ok: false, reason: descriptor.reason ?? "dependency environment is not attestable" };
8551
+ }
8552
+ try {
8553
+ const fingerprint2 = snapshot(descriptor.roots, descriptor.probes, resolve4(cwdInput));
8554
+ if (fingerprint2 !== descriptor.fingerprint) {
8555
+ return { ok: false, fingerprint: fingerprint2, reason: "dependency environment fingerprint changed" };
8556
+ }
8557
+ return { ok: true, fingerprint: fingerprint2 };
8558
+ } catch (e) {
8559
+ return { ok: false, reason: e instanceof Error ? e.message : String(e) };
8560
+ }
8561
+ }
8562
+ function dependencyEnvironmentReport(descriptor) {
8563
+ return {
8564
+ status: descriptor.status,
8565
+ roots: descriptor.roots.map((root) => ({ kind: root.kind, path: root.path })),
8566
+ ...descriptor.fingerprint ? { fingerprint: descriptor.fingerprint } : {},
8567
+ ...descriptor.reason ? { reason: descriptor.reason } : {}
8568
+ };
8569
+ }
8570
+ function dependencyEnvironmentSummary(descriptor) {
8571
+ if (descriptor.status === "unattestable") {
8572
+ return "unattestable - " + (descriptor.reason ?? "unknown dependency environment");
8573
+ }
8574
+ if (descriptor.status === "none") return "none detected (candidate node_modules remains absent)";
8575
+ const kinds = [...new Set(descriptor.roots.map((root) => root.kind))].join(", ");
8576
+ return "attested " + kinds + " (sha256 " + (descriptor.fingerprint?.slice(0, 12) ?? "unknown") + "...)";
8577
+ }
8578
+
8579
+ // src/verifier-backend.ts
8580
+ import { createHash as createHash9, randomUUID } from "node:crypto";
8581
+ import { spawnSync } from "node:child_process";
8582
+ import { accessSync, constants as constants2, readFileSync as readFileSync14, realpathSync as realpathSync4, statSync as statSync5 } from "node:fs";
8583
+ import { delimiter as delimiter2, dirname as dirname6, join as join14, resolve as resolve5 } from "node:path";
8584
+ var DIGEST_IMAGE = /^(?!-)[^\s@]+@sha256:[0-9a-f]{64}$/i;
8585
+ var DEFAULT_DOCKER_HOST = "unix:///var/run/docker.sock";
8586
+ function identity() {
8587
+ const uid = typeof process.getuid === "function" ? process.getuid() : null;
8588
+ const groups = typeof process.getgroups === "function" ? process.getgroups() : [];
8589
+ return { uid, groups };
8590
+ }
8591
+ function writableByCaller(path) {
8592
+ try {
8593
+ const st = statSync5(path);
8594
+ const { uid, groups } = identity();
8595
+ if (uid === 0) return true;
8596
+ if ((st.mode & 2) !== 0) return true;
8597
+ if (uid !== null && st.uid === uid && (st.mode & 128) !== 0) return true;
8598
+ if (groups.includes(st.gid) && (st.mode & 16) !== 0) return true;
8599
+ return false;
8231
8600
  } catch {
8232
- return "none";
8601
+ return true;
8602
+ }
8603
+ }
8604
+ function replaceableByCaller(path) {
8605
+ let current = resolve5(path);
8606
+ if (writableByCaller(current)) return true;
8607
+ current = dirname6(current);
8608
+ while (true) {
8609
+ if (writableByCaller(current)) return true;
8610
+ const parent = dirname6(current);
8611
+ if (parent === current) break;
8612
+ current = parent;
8613
+ }
8614
+ return false;
8615
+ }
8616
+ function findExecutable(name) {
8617
+ for (const raw of (process.env.PATH ?? "").split(delimiter2)) {
8618
+ if (!raw) continue;
8619
+ const candidate = join14(raw, name);
8620
+ try {
8621
+ accessSync(candidate, constants2.X_OK);
8622
+ return realpathSync4(candidate);
8623
+ } catch {
8624
+ }
8625
+ }
8626
+ return null;
8627
+ }
8628
+ function fileSha256(path) {
8629
+ return createHash9("sha256").update(readFileSync14(path)).digest("hex");
8630
+ }
8631
+ function engineClientEnv() {
8632
+ return {
8633
+ PATH: "/usr/bin:/bin",
8634
+ HOME: "/nonexistent",
8635
+ LANG: "C",
8636
+ LC_ALL: "C"
8637
+ };
8638
+ }
8639
+ function dockerArgs(host, args) {
8640
+ return ["--host", host, ...args];
8641
+ }
8642
+ function dockerAvailable(enginePath, host) {
8643
+ try {
8644
+ const r = spawnSync(enginePath, dockerArgs(host, ["version"]), {
8645
+ stdio: "ignore",
8646
+ timeout: 5e3,
8647
+ killSignal: "SIGKILL",
8648
+ env: engineClientEnv()
8649
+ });
8650
+ return r.status === 0;
8651
+ } catch {
8652
+ return false;
8653
+ }
8654
+ }
8655
+ function declaredImageVolumePaths(metadata) {
8656
+ if (!metadata || typeof metadata !== "object" || Array.isArray(metadata)) {
8657
+ throw new Error("image metadata must be a Docker image-inspect object");
8658
+ }
8659
+ const config = metadata.Config;
8660
+ if (!config || typeof config !== "object" || Array.isArray(config)) {
8661
+ throw new Error("image metadata Config must be an object");
8662
+ }
8663
+ const volumes = config.Volumes;
8664
+ if (volumes === void 0 || volumes === null) return [];
8665
+ if (typeof volumes !== "object" || Array.isArray(volumes)) {
8666
+ throw new Error("image metadata Config.Volumes must be a mapping or null");
8667
+ }
8668
+ return Object.keys(volumes).sort();
8669
+ }
8670
+ function inspectImageMetadata(enginePath, host, image) {
8671
+ try {
8672
+ const r = spawnSync(enginePath, dockerArgs(host, ["image", "inspect", image]), {
8673
+ encoding: "utf8",
8674
+ stdio: ["ignore", "pipe", "ignore"],
8675
+ timeout: 1e4,
8676
+ killSignal: "SIGKILL",
8677
+ env: engineClientEnv()
8678
+ });
8679
+ if (r.error || r.status !== 0) {
8680
+ return {
8681
+ metadata: null,
8682
+ reason: "the digest-pinned verifier image is not present locally; TamperWard refuses to pull during adjudication"
8683
+ };
8684
+ }
8685
+ const parsed = JSON.parse(r.stdout ?? "");
8686
+ if (!Array.isArray(parsed) || parsed.length !== 1) {
8687
+ return { metadata: null, reason: "Docker returned malformed verifier image metadata" };
8688
+ }
8689
+ return { metadata: parsed[0] };
8690
+ } catch {
8691
+ return { metadata: null, reason: "Docker returned unreadable verifier image metadata" };
8692
+ }
8693
+ }
8694
+ function engineStable(backend) {
8695
+ if (!backend.enginePath || !backend.engineSha256) return false;
8696
+ try {
8697
+ if (realpathSync4(backend.enginePath) !== backend.enginePath) return false;
8698
+ if (replaceableByCaller(backend.enginePath)) return false;
8699
+ return fileSha256(backend.enginePath) === backend.engineSha256;
8700
+ } catch {
8701
+ return false;
8702
+ }
8703
+ }
8704
+ function prepareVerifierBackend(verify) {
8705
+ const kind = verify?.backend ?? "local";
8706
+ if (kind === "local") {
8707
+ return { kind: "local", trust: "checkpointed-local", available: true };
8708
+ }
8709
+ const image = verify?.image;
8710
+ if (!image || !DIGEST_IMAGE.test(image)) {
8711
+ return {
8712
+ kind: "container",
8713
+ trust: "isolated-container",
8714
+ available: false,
8715
+ ...image ? { image } : {},
8716
+ reason: "container verifier requires an immutable runtime-safe image reference name@sha256:<64 hex>"
8717
+ };
8718
+ }
8719
+ const forced = process.env.TAMPERWARD_CONTAINER_ENGINE;
8720
+ if (forced && forced !== "docker") {
8721
+ return {
8722
+ kind: "container",
8723
+ trust: "isolated-container",
8724
+ available: false,
8725
+ image,
8726
+ reason: `unsupported isolated verifier engine ${JSON.stringify(forced)}; Docker is the tested backend`
8727
+ };
8728
+ }
8729
+ const enginePath = findExecutable("docker");
8730
+ if (!enginePath) {
8731
+ return {
8732
+ kind: "container",
8733
+ trust: "isolated-container",
8734
+ available: false,
8735
+ image,
8736
+ reason: "Docker executable was not found on the operator PATH"
8737
+ };
8738
+ }
8739
+ if (replaceableByCaller(enginePath)) {
8740
+ return {
8741
+ kind: "container",
8742
+ trust: "isolated-container",
8743
+ available: false,
8744
+ image,
8745
+ engine: "docker",
8746
+ enginePath,
8747
+ reason: "Docker engine executable is writable/replaceable by the caller identity; it cannot be verifier authority"
8748
+ };
8749
+ }
8750
+ const daemonHost = DEFAULT_DOCKER_HOST;
8751
+ if (!dockerAvailable(enginePath, daemonHost)) {
8752
+ return {
8753
+ kind: "container",
8754
+ trust: "isolated-container",
8755
+ available: false,
8756
+ image,
8757
+ engine: "docker",
8758
+ enginePath,
8759
+ daemonHost,
8760
+ reason: `Docker is unavailable at the fixed verifier endpoint ${daemonHost}`
8761
+ };
8762
+ }
8763
+ const inspectedImage = inspectImageMetadata(enginePath, daemonHost, image);
8764
+ if (!inspectedImage.metadata) {
8765
+ return {
8766
+ kind: "container",
8767
+ trust: "isolated-container",
8768
+ available: false,
8769
+ image,
8770
+ engine: "docker",
8771
+ enginePath,
8772
+ daemonHost,
8773
+ reason: inspectedImage.reason ?? "Docker could not attest the verifier image metadata"
8774
+ };
8775
+ }
8776
+ let declaredVolumes;
8777
+ try {
8778
+ declaredVolumes = declaredImageVolumePaths(inspectedImage.metadata);
8779
+ } catch (e) {
8780
+ return {
8781
+ kind: "container",
8782
+ trust: "isolated-container",
8783
+ available: false,
8784
+ image,
8785
+ engine: "docker",
8786
+ enginePath,
8787
+ daemonHost,
8788
+ reason: `Docker verifier image metadata is invalid: ${e instanceof Error ? e.message : String(e)}`
8789
+ };
8790
+ }
8791
+ if (declaredVolumes.length > 0) {
8792
+ return {
8793
+ kind: "container",
8794
+ trust: "isolated-container",
8795
+ available: false,
8796
+ image,
8797
+ engine: "docker",
8798
+ enginePath,
8799
+ daemonHost,
8800
+ reason: `the verifier image declares writable Docker VOLUME path(s): ${declaredVolumes.join(", ")}; --read-only does not make image-declared volumes immutable`
8801
+ };
8802
+ }
8803
+ return {
8804
+ kind: "container",
8805
+ trust: "isolated-container",
8806
+ available: true,
8807
+ image,
8808
+ engine: "docker",
8809
+ enginePath,
8810
+ engineSha256: fileSha256(enginePath),
8811
+ daemonHost
8812
+ };
8813
+ }
8814
+ function verifierBackendReport(backend) {
8815
+ return {
8816
+ kind: backend.kind,
8817
+ trust: backend.trust,
8818
+ available: backend.available,
8819
+ ...backend.image ? { image: backend.image } : {},
8820
+ ...backend.engine ? { engine: backend.engine } : {},
8821
+ ...backend.reason ? { reason: backend.reason } : {}
8822
+ };
8823
+ }
8824
+ function verifierBackendSummary(backend) {
8825
+ if (backend.kind === "local") {
8826
+ return "local (checkpointed same-host; weaker trust boundary)";
8827
+ }
8828
+ const identity2 = backend.image ?? "<missing image>";
8829
+ if (!backend.available) {
8830
+ return `container (isolated; unavailable: ${backend.reason ?? "unknown reason"}; image ${identity2})`;
8831
+ }
8832
+ return `container (isolated via Docker; image ${identity2})`;
8833
+ }
8834
+ function cleanupContainer(backend, name) {
8835
+ if (!backend.enginePath || !backend.daemonHost || !engineStable(backend)) return;
8836
+ try {
8837
+ spawnSync(backend.enginePath, dockerArgs(backend.daemonHost, ["rm", "-f", "-v", name]), {
8838
+ stdio: "ignore",
8839
+ timeout: 1e4,
8840
+ killSignal: "SIGKILL",
8841
+ env: engineClientEnv()
8842
+ });
8843
+ } catch {
8844
+ }
8845
+ }
8846
+ function containerRunArgs(input) {
8847
+ return [
8848
+ "run",
8849
+ "--name",
8850
+ input.name,
8851
+ // Keep the stopped container until the trusted host has inspected .State.
8852
+ // Docker CLI exit codes overlap candidate exit codes; container metadata is
8853
+ // the authority that tells us whether the suite actually ran.
8854
+ "--pull",
8855
+ "never",
8856
+ "--network",
8857
+ "none",
8858
+ "--read-only",
8859
+ "--cap-drop",
8860
+ "ALL",
8861
+ "--security-opt",
8862
+ "no-new-privileges",
8863
+ "--pids-limit",
8864
+ "256",
8865
+ "--user",
8866
+ `${input.uid}:${input.gid}`,
8867
+ // Frozen candidate/pristine input is immutable in the verifier. A suite
8868
+ // needing outputs writes to /workspace-out, HOME or /tmp instead.
8869
+ "--mount",
8870
+ `type=bind,src=${input.workspace},dst=/workspace,ro`,
8871
+ "--tmpfs",
8872
+ "/tmp:rw,nosuid,nodev,mode=1777",
8873
+ "--tmpfs",
8874
+ `/home/tamperward:rw,nosuid,nodev,mode=700,uid=${input.uid},gid=${input.gid}`,
8875
+ "--tmpfs",
8876
+ `/workspace-out:rw,nosuid,nodev,mode=700,uid=${input.uid},gid=${input.gid}`,
8877
+ "--workdir",
8878
+ "/workspace",
8879
+ "--env",
8880
+ "HOME=/home/tamperward",
8881
+ "--env",
8882
+ "TMPDIR=/tmp",
8883
+ "--env",
8884
+ "CI=1",
8885
+ "--env",
8886
+ "TAMPERWARD_OUTPUT_DIR=/workspace-out",
8887
+ "--env",
8888
+ "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
8889
+ // The image supplies immutable runtime/dependencies, not command policy.
8890
+ // Docker otherwise appends our command to an image ENTRYPOINT.
8891
+ "--entrypoint",
8892
+ "/bin/sh",
8893
+ input.image,
8894
+ "-c",
8895
+ input.command
8896
+ ];
8897
+ }
8898
+ function runContainerStage(backend, dir, command, budgetSecs) {
8899
+ const t0 = Date.now();
8900
+ if (backend.kind !== "container" || !backend.available || !backend.image || !backend.enginePath || !backend.engineSha256 || !backend.daemonHost || !engineStable(backend)) {
8901
+ return {
8902
+ exit: null,
8903
+ secs: 0,
8904
+ failure: "backend",
8905
+ reason: "isolated verifier authority is incomplete or changed before stage execution"
8906
+ };
8907
+ }
8908
+ const name = `tamperward-verify-${process.pid}-${randomUUID().slice(0, 12)}`;
8909
+ const uid = typeof process.getuid === "function" ? process.getuid() : 65534;
8910
+ const gid = typeof process.getgid === "function" ? process.getgid() : 65534;
8911
+ const args = containerRunArgs({
8912
+ image: backend.image,
8913
+ name,
8914
+ workspace: resolve5(dir),
8915
+ command,
8916
+ uid,
8917
+ gid
8918
+ });
8919
+ try {
8920
+ const r = spawnSync(
8921
+ backend.enginePath,
8922
+ dockerArgs(backend.daemonHost, args),
8923
+ {
8924
+ stdio: "ignore",
8925
+ timeout: budgetSecs * 1e3,
8926
+ killSignal: "SIGKILL",
8927
+ env: engineClientEnv()
8928
+ }
8929
+ );
8930
+ const secs = Math.round((Date.now() - t0) / 1e3);
8931
+ if (r.error) {
8932
+ cleanupContainer(backend, name);
8933
+ const code = r.error.code;
8934
+ return code === "ETIMEDOUT" ? { exit: null, secs, failure: "budget", reason: "verifier stage exceeded its budget" } : { exit: null, secs, failure: "backend", reason: `Docker client failed: ${r.error.message}` };
8935
+ }
8936
+ if (r.status === null) {
8937
+ cleanupContainer(backend, name);
8938
+ return {
8939
+ exit: null,
8940
+ secs,
8941
+ failure: "backend",
8942
+ reason: `Docker client did not return a status${r.signal ? ` (signal ${r.signal})` : ""}`
8943
+ };
8944
+ }
8945
+ if (!engineStable(backend)) {
8946
+ return {
8947
+ exit: null,
8948
+ secs,
8949
+ failure: "backend",
8950
+ reason: "Docker engine identity changed before verifier result inspection"
8951
+ };
8952
+ }
8953
+ const inspected = spawnSync(
8954
+ backend.enginePath,
8955
+ dockerArgs(backend.daemonHost, ["inspect", "--format", "{{json .State}}", name]),
8956
+ {
8957
+ encoding: "utf8",
8958
+ stdio: ["ignore", "pipe", "ignore"],
8959
+ timeout: 1e4,
8960
+ killSignal: "SIGKILL",
8961
+ env: engineClientEnv()
8962
+ }
8963
+ );
8964
+ let state = null;
8965
+ if (!inspected.error && inspected.status === 0) {
8966
+ try {
8967
+ state = JSON.parse((inspected.stdout ?? "").trim());
8968
+ } catch {
8969
+ state = null;
8970
+ }
8971
+ }
8972
+ cleanupContainer(backend, name);
8973
+ if (!state || state.Status !== "exited" || typeof state.ExitCode !== "number" || state.OOMKilled || (state.Error ?? "") !== "") {
8974
+ return {
8975
+ exit: null,
8976
+ secs,
8977
+ failure: "backend",
8978
+ reason: state?.OOMKilled ? "verifier container was OOM-killed" : state?.Error ? `verifier container runtime error: ${state.Error}` : "Docker did not provide a trustworthy exited-container state"
8979
+ };
8980
+ }
8981
+ return { exit: state.ExitCode, secs };
8982
+ } catch (e) {
8983
+ cleanupContainer(backend, name);
8984
+ return {
8985
+ exit: null,
8986
+ secs: Math.round((Date.now() - t0) / 1e3),
8987
+ failure: "backend",
8988
+ reason: `Docker verifier execution failed: ${e instanceof Error ? e.message : String(e)}`
8989
+ };
8233
8990
  }
8234
- walk2(root);
8235
- return h.digest("hex");
8236
8991
  }
8237
8992
 
8238
8993
  // src/cli/verify.ts
@@ -8320,28 +9075,28 @@ function resolveBase(base, cwd) {
8320
9075
  }
8321
9076
  }
8322
9077
  function baseIsAncestorOfHead(base, cwd) {
8323
- const r = spawnSync("git", ["merge-base", "--is-ancestor", base, "HEAD"], { cwd, env: trustedGitEnv() });
9078
+ const r = spawnSync2("git", ["merge-base", "--is-ancestor", base, "HEAD"], { cwd, env: trustedGitEnv() });
8324
9079
  return r.status === 0;
8325
9080
  }
8326
- function inside(root, path) {
8327
- const rel = relative4(resolve4(root), resolve4(path));
8328
- return rel === "" || rel !== ".." && !rel.startsWith(`..${sep}`) && !isAbsolute9(rel);
9081
+ function inside2(root, path) {
9082
+ const rel = relative5(resolve6(root), resolve6(path));
9083
+ return rel === "" || rel !== ".." && !rel.startsWith(`..${sep2}`) && !isAbsolute10(rel);
8329
9084
  }
8330
9085
  function linkParts(path) {
8331
- const parts = sep === "\\" ? path.split(/[\\/]+/) : path.split("/");
9086
+ const parts = sep2 === "\\" ? path.split(/[\\/]+/) : path.split("/");
8332
9087
  return parts.filter((part) => part !== "" && part !== ".");
8333
9088
  }
8334
9089
  function linkEscape(label, target) {
8335
9090
  return new Error(`${label} is a symlink that escapes the materialised tree (${JSON.stringify(target)})`);
8336
9091
  }
8337
9092
  function driveRelativeLinkTarget(target) {
8338
- return sep === "\\" && /^[A-Za-z]:(?![\\/])/.test(target);
9093
+ return sep2 === "\\" && /^[A-Za-z]:(?![\\/])/.test(target);
8339
9094
  }
8340
9095
  function rootedLinkTarget(target) {
8341
- return isAbsolute9(target) || driveRelativeLinkTarget(target);
9096
+ return isAbsolute10(target) || driveRelativeLinkTarget(target);
8342
9097
  }
8343
9098
  function safeSymlink(target, out2, root, label) {
8344
- if (rootedLinkTarget(target) || !inside(root, resolve4(dirname5(out2), target))) {
9099
+ if (rootedLinkTarget(target) || !inside2(root, resolve6(dirname7(out2), target))) {
8345
9100
  throw linkEscape(label, target);
8346
9101
  }
8347
9102
  rmSync3(out2, { force: true });
@@ -8349,7 +9104,7 @@ function safeSymlink(target, out2, root, label) {
8349
9104
  }
8350
9105
  function validateSymlinkGraph(target, out2, root, dependencyRoot, label) {
8351
9106
  let domain = "tree";
8352
- let stack = linkParts(relative4(root, dirname5(out2)));
9107
+ let stack = linkParts(relative5(root, dirname7(out2)));
8353
9108
  const pending = linkParts(target);
8354
9109
  const seen = /* @__PURE__ */ new Set();
8355
9110
  let hops = 0;
@@ -8367,10 +9122,10 @@ function validateSymlinkGraph(target, out2, root, dependencyRoot, label) {
8367
9122
  continue;
8368
9123
  }
8369
9124
  const base = domain === "tree" ? root : dependencyRoot;
8370
- const current = join14(base, ...stack);
9125
+ const current = join15(base, ...stack);
8371
9126
  let st;
8372
9127
  try {
8373
- st = lstatSync3(current);
9128
+ st = lstatSync4(current);
8374
9129
  } catch {
8375
9130
  continue;
8376
9131
  }
@@ -8378,7 +9133,7 @@ function validateSymlinkGraph(target, out2, root, dependencyRoot, label) {
8378
9133
  if (++hops > 128) {
8379
9134
  throw new Error(`${label} contains a symlink cycle while materialising (${JSON.stringify(target)})`);
8380
9135
  }
8381
- const key2 = `${domain}:${current}:${pending.join(sep)}`;
9136
+ const key2 = `${domain}:${current}:${pending.join(sep2)}`;
8382
9137
  if (seen.has(key2)) {
8383
9138
  throw new Error(`${label} contains a symlink cycle while materialising (${JSON.stringify(target)})`);
8384
9139
  }
@@ -8388,12 +9143,12 @@ function validateSymlinkGraph(target, out2, root, dependencyRoot, label) {
8388
9143
  if (driveRelativeLinkTarget(next)) {
8389
9144
  throw linkEscape(label, target);
8390
9145
  }
8391
- if (isAbsolute9(next)) {
8392
- if (domain === "tree" || !dependencyRoot || !inside(dependencyRoot, next)) {
9146
+ if (isAbsolute10(next)) {
9147
+ if (domain === "tree" || !dependencyRoot || !inside2(dependencyRoot, next)) {
8393
9148
  throw linkEscape(label, target);
8394
9149
  }
8395
9150
  stack = [];
8396
- pending.unshift(...linkParts(relative4(dependencyRoot, resolve4(next))));
9151
+ pending.unshift(...linkParts(relative5(dependencyRoot, resolve6(next))));
8397
9152
  } else {
8398
9153
  pending.unshift(...linkParts(next));
8399
9154
  }
@@ -8403,32 +9158,32 @@ function rejectLinkedParent(cwd, rel) {
8403
9158
  const parts = rel.split("/").slice(0, -1);
8404
9159
  let current = cwd;
8405
9160
  for (const part of parts) {
8406
- current = join14(current, part);
9161
+ current = join15(current, part);
8407
9162
  let st;
8408
9163
  try {
8409
- st = lstatSync3(current);
9164
+ st = lstatSync4(current);
8410
9165
  } catch {
8411
9166
  continue;
8412
9167
  }
8413
9168
  if (st.isSymbolicLink()) {
8414
- throw new Error(`${rel} has a symlinked parent directory (${relative4(cwd, current)})`);
9169
+ throw new Error(`${rel} has a symlinked parent directory (${relative5(cwd, current)})`);
8415
9170
  }
8416
9171
  }
8417
9172
  }
8418
- function materialize(cwd, dest) {
9173
+ function materialize(cwd, dest, dependencyRoot) {
8419
9174
  const listed = git2(["ls-files", "-z", "--cached", "--others", "--exclude-standard"], cwd).split("\0").filter(Boolean);
8420
9175
  const links = [];
8421
9176
  for (const rel of listed) {
8422
- const src = join14(cwd, rel);
9177
+ const src = join15(cwd, rel);
8423
9178
  rejectLinkedParent(cwd, rel);
8424
9179
  let st;
8425
9180
  try {
8426
- st = lstatSync3(src);
9181
+ st = lstatSync4(src);
8427
9182
  } catch {
8428
9183
  continue;
8429
9184
  }
8430
- const out2 = join14(dest, rel);
8431
- mkdirSync6(dirname5(out2), { recursive: true });
9185
+ const out2 = join15(dest, rel);
9186
+ mkdirSync6(dirname7(out2), { recursive: true });
8432
9187
  dropSymlink(out2);
8433
9188
  if (st.isSymbolicLink()) {
8434
9189
  const target = readlinkSync3(src);
@@ -8441,17 +9196,14 @@ function materialize(cwd, dest) {
8441
9196
  throw new Error(`${rel} is ${st.isDirectory() ? "a directory" : "a special file"} where git expects a file`);
8442
9197
  }
8443
9198
  }
8444
- const nm = resolve4(cwd, "node_modules");
8445
- let dependencyRoot = null;
8446
- if (existsSync10(nm)) dependencyRoot = realpathSync3(nm);
8447
9199
  for (const link of links) {
8448
9200
  validateSymlinkGraph(link.target, link.out, dest, dependencyRoot, link.rel);
8449
9201
  }
8450
- if (dependencyRoot) symlinkSync(nm, join14(dest, "node_modules"), "dir");
9202
+ if (dependencyRoot) symlinkSync(dependencyRoot, join15(dest, "node_modules"), "dir");
8451
9203
  }
8452
9204
  function dropSymlink(p) {
8453
9205
  try {
8454
- if (lstatSync3(p).isSymbolicLink()) rmSync3(p, { force: true });
9206
+ if (lstatSync4(p).isSymbolicLink()) rmSync3(p, { force: true });
8455
9207
  } catch {
8456
9208
  }
8457
9209
  }
@@ -8476,15 +9228,15 @@ function baseEntries(base, cwd) {
8476
9228
  });
8477
9229
  }
8478
9230
  function overlayDigest(dest, paths) {
8479
- const h = createHash8("sha256");
9231
+ const h = createHash10("sha256");
8480
9232
  for (const rel of [...paths].sort()) {
8481
9233
  h.update(rel);
8482
9234
  h.update("\0");
8483
- const p = join14(dest, rel);
9235
+ const p = join15(dest, rel);
8484
9236
  try {
8485
- const st = lstatSync3(p);
9237
+ const st = lstatSync4(p);
8486
9238
  h.update(String(st.mode));
8487
- h.update(st.isSymbolicLink() ? readlinkSync3(p) : readFileSync14(p));
9239
+ h.update(st.isSymbolicLink() ? readlinkSync3(p) : readFileSync15(p));
8488
9240
  } catch {
8489
9241
  h.update("<gone>");
8490
9242
  }
@@ -8492,7 +9244,7 @@ function overlayDigest(dest, paths) {
8492
9244
  }
8493
9245
  return h.digest("hex");
8494
9246
  }
8495
- function overlayPristine(cwd, base, dest, policy, cmd) {
9247
+ function overlayPristine(cwd, base, dest, policy, cmd, dependencyRoot) {
8496
9248
  const entries = baseEntries(base, cwd);
8497
9249
  const atBase = entries.map((e) => e.path);
8498
9250
  const verifierOwned = verifierInputs(cmd, atBase, policy);
@@ -8508,8 +9260,8 @@ function overlayPristine(cwd, base, dest, policy, cmd) {
8508
9260
  throw new Error(`the base's ${e.path} is a submodule; the pristine copy cannot reproduce it`);
8509
9261
  }
8510
9262
  const content = execFileSync8("git", ["show", `${base}:${e.path}`], { cwd, maxBuffer: 1 << 28, env: trustedGitEnv() });
8511
- const out2 = join14(dest, e.path);
8512
- mkdirSync6(dirname5(out2), { recursive: true });
9263
+ const out2 = join15(dest, e.path);
9264
+ mkdirSync6(dirname7(out2), { recursive: true });
8513
9265
  rmSync3(out2, { force: true });
8514
9266
  if (e.mode === "120000") {
8515
9267
  const target = content.toString("utf8");
@@ -8525,11 +9277,9 @@ function overlayPristine(cwd, base, dest, policy, cmd) {
8525
9277
  const inCopy = git2(["ls-files", "-z", "--cached", "--others", "--exclude-standard"], cwd).split("\0").filter(Boolean);
8526
9278
  for (const rel of inCopy) {
8527
9279
  if (!isOverlay(rel) || baseProtected.has(rel)) continue;
8528
- rmSync3(join14(dest, rel), { force: true });
9280
+ rmSync3(join15(dest, rel), { force: true });
8529
9281
  removed++;
8530
9282
  }
8531
- const nm = resolve4(cwd, "node_modules");
8532
- const dependencyRoot = existsSync10(nm) ? realpathSync3(nm) : null;
8533
9283
  for (const link of restoredLinks) {
8534
9284
  validateSymlinkGraph(link.target, link.out, dest, dependencyRoot, `the base's ${link.path}`);
8535
9285
  }
@@ -8572,8 +9322,8 @@ function suiteEnv(scratch) {
8572
9322
  if (DROPPED_ENV.has(k) || PINNED_NPM.test(k)) continue;
8573
9323
  env[k] = v;
8574
9324
  }
8575
- const userRc = join14(scratch, "npmrc-user");
8576
- const globalRc = join14(scratch, "npmrc-global");
9325
+ const userRc = join15(scratch, "npmrc-user");
9326
+ const globalRc = join15(scratch, "npmrc-global");
8577
9327
  for (const f of [userRc, globalRc]) writeFileSync6(f, "", { mode: 292 });
8578
9328
  env.npm_config_userconfig = userRc;
8579
9329
  env.npm_config_globalconfig = globalRc;
@@ -8581,12 +9331,12 @@ function suiteEnv(scratch) {
8581
9331
  env.YARN_IGNORE_PATH = "1";
8582
9332
  return env;
8583
9333
  }
8584
- function runSuite(dir, cmd, budgetSecs) {
9334
+ function runLocalSuite(dir, cmd, budgetSecs) {
8585
9335
  const t0 = Date.now();
8586
- const outDir = mkdtempSync2(join14(tmpdir2(), "tw-verify-run-"));
8587
- const outFile = join14(outDir, "result.json");
9336
+ const outDir = mkdtempSync2(join15(tmpdir2(), "tw-verify-run-"));
9337
+ const outFile = join15(outDir, "result.json");
8588
9338
  try {
8589
- spawnSync(process.execPath, ["-e", SUPERVISOR, cmd, String(budgetSecs * 1e3), outFile], {
9339
+ spawnSync2(process.execPath, ["-e", SUPERVISOR, cmd, String(budgetSecs * 1e3), outFile], {
8590
9340
  cwd: dir,
8591
9341
  env: suiteEnv(outDir),
8592
9342
  stdio: "ignore",
@@ -8597,7 +9347,7 @@ function runSuite(dir, cmd, budgetSecs) {
8597
9347
  const secs = Math.round((Date.now() - t0) / 1e3);
8598
9348
  let r;
8599
9349
  try {
8600
- r = JSON.parse(readFileSync14(outFile, "utf8"));
9350
+ r = JSON.parse(readFileSync15(outFile, "utf8"));
8601
9351
  } catch {
8602
9352
  return { exit: null, secs };
8603
9353
  }
@@ -8641,6 +9391,45 @@ function runVerify(opts) {
8641
9391
  return 2;
8642
9392
  }
8643
9393
  const budget = opts.budget ?? policy.verify?.budget ?? 300;
9394
+ const verifierBackend = opts.verifierBackend ?? prepareVerifierBackend(policy.verify);
9395
+ const backendReport = () => verifierBackendReport(verifierBackend);
9396
+ if (!verifierBackend.available) {
9397
+ if (opts.json) {
9398
+ out2(JSON.stringify({
9399
+ verdict: "CANNOT_VERIFY",
9400
+ reason: "VERIFIER_BACKEND_UNAVAILABLE",
9401
+ verifier_backend: backendReport()
9402
+ }));
9403
+ } else {
9404
+ out2("verify: isolated verifier backend is unavailable \u2014 failing closed");
9405
+ out2("verify: " + (verifierBackend.reason ?? "unknown backend failure"));
9406
+ }
9407
+ return 2;
9408
+ }
9409
+ const isolated = verifierBackend.kind === "container";
9410
+ const dependencyEnvironment = isolated ? null : opts.dependencyEnvironment ?? discoverDependencyEnvironment(cwd, cmd);
9411
+ const dependencyReport = () => dependencyEnvironment ? dependencyEnvironmentReport(dependencyEnvironment) : {
9412
+ status: "verifier-owned",
9413
+ roots: [],
9414
+ image: verifierBackend.image
9415
+ };
9416
+ if (dependencyEnvironment?.status === "unattestable" && !opts.allowDepDrift) {
9417
+ if (opts.json) {
9418
+ out2(JSON.stringify({
9419
+ verdict: "CANNOT_VERIFY",
9420
+ reason: "DEPENDENCY_ENVIRONMENT_UNATTESTABLE",
9421
+ verifier_backend: backendReport(),
9422
+ dependency_environment: dependencyReport()
9423
+ }));
9424
+ } else {
9425
+ out2("verify: dependency environment is not attestable \u2014 failing closed");
9426
+ out2("verify: " + (dependencyEnvironment.reason ?? "unknown dependency environment"));
9427
+ }
9428
+ return 2;
9429
+ }
9430
+ const frozenNodeModules = dependencyEnvironment ? dependencyEnvironment.roots.find((root) => root.kind === "node_modules")?.realPath ?? null : null;
9431
+ const checkDeps = () => dependencyEnvironment ? checkDependencyEnvironment(cwd, dependencyEnvironment) : { ok: true };
9432
+ const runStage = (dir) => isolated ? runContainerStage(verifierBackend, dir, cmd, budget) : runLocalSuite(dir, cmd, budget);
8644
9433
  let base;
8645
9434
  try {
8646
9435
  base = resolveBase(opts.base ?? "HEAD", cwd);
@@ -8648,58 +9437,105 @@ function runVerify(opts) {
8648
9437
  out2(`verify: cannot resolve base rev "${opts.base ?? "HEAD"}" \u2014 failing closed`);
8649
9438
  return 2;
8650
9439
  }
8651
- const visRoot = mkdtempSync2(join14(tmpdir2(), "tw-verify-vis-"));
8652
- const visDir = join14(visRoot, "t");
9440
+ const visRoot = mkdtempSync2(join15(tmpdir2(), "tw-verify-vis-"));
9441
+ const visDir = join15(visRoot, "t");
8653
9442
  const cleanup = (dirs) => {
8654
9443
  if (opts.keep) return;
8655
9444
  for (const d of dirs) rmSync3(d, { recursive: true, force: true });
8656
9445
  };
8657
9446
  const protectedIgnored = (rel) => isProtected(rel, policy);
8658
9447
  const treeBefore = treeFingerprint(cwd, protectedIgnored);
8659
- const depsBefore = depsFingerprint(cwd);
9448
+ const dependencyAtEntry = checkDeps();
9449
+ if (!dependencyAtEntry.ok && !opts.allowDepDrift) {
9450
+ cleanup([visRoot]);
9451
+ out2("verify: the frozen dependency environment changed before the visible suite ran \u2014");
9452
+ out2((dependencyAtEntry.reason ?? "dependency identity changed") + ". Failing closed, not open.");
9453
+ return 2;
9454
+ }
8660
9455
  try {
8661
9456
  mkdirSync6(visDir);
8662
- materialize(cwd, visDir);
9457
+ materialize(cwd, visDir, frozenNodeModules);
8663
9458
  } catch (e) {
8664
9459
  cleanup([visRoot]);
8665
9460
  out2(`verify: could not materialize (${e instanceof Error ? e.message : String(e)}) \u2014 failing closed`);
8666
9461
  return 2;
8667
9462
  }
8668
- const visible = runSuite(visDir, cmd, budget);
9463
+ const visible = runStage(visDir);
9464
+ if (visible.failure === "backend") {
9465
+ cleanup([visRoot]);
9466
+ if (opts.json) {
9467
+ out2(JSON.stringify({
9468
+ verdict: "CANNOT_VERIFY",
9469
+ reason: "VERIFIER_BACKEND_RUNTIME_FAILURE",
9470
+ stage: "visible",
9471
+ detail: visible.reason,
9472
+ verifier_backend: backendReport()
9473
+ }));
9474
+ } else {
9475
+ out2("verify: isolated verifier backend failed while running the visible stage \u2014 failing closed");
9476
+ if (visible.reason) out2("verify: " + visible.reason);
9477
+ }
9478
+ return 2;
9479
+ }
8669
9480
  if (treeFingerprint(cwd, protectedIgnored) !== treeBefore) {
8670
9481
  cleanup([visRoot]);
8671
9482
  out2("verify: the working tree changed while the visible suite was running \u2014 the pristine copy");
8672
9483
  out2("would be materialised from a tree the candidate just edited. Failing closed, not open.");
8673
9484
  return 2;
8674
9485
  }
8675
- if (depsFingerprint(cwd) !== depsBefore) {
9486
+ const dependencyAfterVisible = checkDeps();
9487
+ if (!dependencyAfterVisible.ok && !opts.allowDepDrift) {
8676
9488
  cleanup([visRoot]);
8677
- out2("verify: the installed dependency tree changed while the visible suite was running \u2014 the");
8678
- out2("pristine run executes through it too. Failing closed, not open.");
9489
+ out2("verify: the frozen dependency environment changed while the visible suite was running \u2014");
9490
+ out2((dependencyAfterVisible.reason ?? "dependency identity changed") + ". Failing closed, not open.");
8679
9491
  return 2;
8680
9492
  }
8681
- const priRoot = mkdtempSync2(join14(tmpdir2(), "tw-verify-pri-"));
8682
- const priDir = join14(priRoot, "t");
9493
+ const priRoot = mkdtempSync2(join15(tmpdir2(), "tw-verify-pri-"));
9494
+ const priDir = join15(priRoot, "t");
8683
9495
  let restored = [];
8684
9496
  let removedAdded = 0;
8685
9497
  try {
8686
9498
  mkdirSync6(priDir);
8687
- materialize(cwd, priDir);
8688
- ({ restored, removed: removedAdded } = overlayPristine(cwd, base, priDir, policy, cmd));
9499
+ materialize(cwd, priDir, frozenNodeModules);
9500
+ ({ restored, removed: removedAdded } = overlayPristine(
9501
+ cwd,
9502
+ base,
9503
+ priDir,
9504
+ policy,
9505
+ cmd,
9506
+ frozenNodeModules
9507
+ ));
8689
9508
  } catch (e) {
8690
9509
  cleanup([visRoot, priRoot]);
8691
9510
  out2(`verify: could not materialize (${e instanceof Error ? e.message : String(e)}) \u2014 failing closed`);
8692
9511
  return 2;
8693
9512
  }
8694
9513
  const overlayBefore = overlayDigest(priDir, restored);
8695
- const pristine = runSuite(priDir, cmd, budget);
9514
+ const pristine = runStage(priDir);
9515
+ if (pristine.failure === "backend") {
9516
+ cleanup([visRoot, priRoot]);
9517
+ if (opts.json) {
9518
+ out2(JSON.stringify({
9519
+ verdict: "CANNOT_VERIFY",
9520
+ reason: "VERIFIER_BACKEND_RUNTIME_FAILURE",
9521
+ stage: "pristine",
9522
+ detail: pristine.reason,
9523
+ verifier_backend: backendReport()
9524
+ }));
9525
+ } else {
9526
+ out2("verify: isolated verifier backend failed while running the pristine stage \u2014 failing closed");
9527
+ if (pristine.reason) out2("verify: " + pristine.reason);
9528
+ }
9529
+ return 2;
9530
+ }
8696
9531
  const overlayMoved = overlayDigest(priDir, restored) !== overlayBefore;
8697
9532
  const treeMoved = treeFingerprint(cwd, protectedIgnored) !== treeBefore;
8698
- const depsMoved = depsFingerprint(cwd) !== depsBefore;
9533
+ const dependencyAfterPristine = checkDeps();
9534
+ const depsMoved = !dependencyAfterPristine.ok && !opts.allowDepDrift;
8699
9535
  cleanup([visRoot, priRoot]);
8700
9536
  if (overlayMoved || treeMoved || depsMoved) {
8701
9537
  out2(
8702
- "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"
9538
+ "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"
8703
9539
  );
8704
9540
  out2("the verdict would describe something other than what ran. Failing closed, not open.");
8705
9541
  out2("(A suite that rewrites its own snapshots or test files in place will trip this; run it in");
@@ -8734,6 +9570,8 @@ function runVerify(opts) {
8734
9570
  pristine: { exit: pristine.exit, secs: pristine.secs },
8735
9571
  protected_restored: restored.length,
8736
9572
  added_protected_removed: removedAdded,
9573
+ verifier_backend: backendReport(),
9574
+ dependency_environment: dependencyReport(),
8737
9575
  ...signedOff ? { oob_signoff: signedOff } : {},
8738
9576
  ...opts.keep ? { visible_dir: visDir, pristine_dir: priDir } : {}
8739
9577
  })
@@ -8746,6 +9584,10 @@ function runVerify(opts) {
8746
9584
  BUDGET_EXCEEDED: `budget exceeded (${budget}s): could not verify \u2014 failing closed, not open.`
8747
9585
  };
8748
9586
  out2(`tamperward verify \u2014 ${lines[verdict2]}`);
9587
+ out2(`verifier backend: ${verifierBackendSummary(verifierBackend)}`);
9588
+ out2(
9589
+ dependencyEnvironment ? `dependency environment: ${dependencyEnvironmentSummary(dependencyEnvironment)}` + (opts.allowDepDrift && dependencyEnvironment.status === "unattestable" ? " (operator override)" : "") : `dependency environment: verifier-owned by ${verifierBackend.image ?? "isolated image"}`
9590
+ );
8749
9591
  if (signedOff)
8750
9592
  out2(
8751
9593
  `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.`
@@ -8790,9 +9632,9 @@ function parseVerify(args) {
8790
9632
  }
8791
9633
 
8792
9634
  // src/cli/run.ts
8793
- import { execFileSync as execFileSync9, spawnSync as spawnSync2 } from "node:child_process";
8794
- import { readdirSync as readdirSync4, readFileSync as readFileSync15, readlinkSync as readlinkSync4, realpathSync as realpathSync4 } from "node:fs";
8795
- import { resolve as resolve5 } from "node:path";
9635
+ import { execFileSync as execFileSync9, spawnSync as spawnSync3 } from "node:child_process";
9636
+ import { readdirSync as readdirSync4, readFileSync as readFileSync16, readlinkSync as readlinkSync4, realpathSync as realpathSync6 } from "node:fs";
9637
+ import { resolve as resolve7 } from "node:path";
8796
9638
  var out = (s) => process.stdout.write(s + "\n");
8797
9639
  var err = (s) => process.stderr.write(s + "\n");
8798
9640
  function git3(args, cwd) {
@@ -8800,7 +9642,7 @@ function git3(args, cwd) {
8800
9642
  }
8801
9643
  function nowTicks() {
8802
9644
  try {
8803
- return Math.floor(parseFloat(readFileSync15("/proc/uptime", "utf8").split(" ")[0]) * 100);
9645
+ return Math.floor(parseFloat(readFileSync16("/proc/uptime", "utf8").split(" ")[0]) * 100);
8804
9646
  } catch {
8805
9647
  return Number.POSITIVE_INFINITY;
8806
9648
  }
@@ -8809,7 +9651,7 @@ function survivorsHoldingTree(cwd, spawnedAfterTicks) {
8809
9651
  const out2 = [];
8810
9652
  let real;
8811
9653
  try {
8812
- real = realpathSync4(cwd);
9654
+ real = realpathSync6(cwd);
8813
9655
  } catch {
8814
9656
  return out2;
8815
9657
  }
@@ -8823,7 +9665,7 @@ function survivorsHoldingTree(cwd, spawnedAfterTicks) {
8823
9665
  const n = Number(pid);
8824
9666
  if (n === process.pid) continue;
8825
9667
  try {
8826
- const stat = readFileSync15(`/proc/${pid}/stat`, "utf8");
9668
+ const stat = readFileSync16(`/proc/${pid}/stat`, "utf8");
8827
9669
  const startTicks = Number(stat.slice(stat.lastIndexOf(")") + 2).split(" ")[19]);
8828
9670
  if (!Number.isFinite(startTicks) || startTicks < spawnedAfterTicks) continue;
8829
9671
  } catch {
@@ -8838,7 +9680,7 @@ function survivorsHoldingTree(cwd, spawnedAfterTicks) {
8838
9680
  for (const link of procLinks) {
8839
9681
  try {
8840
9682
  const raw = readlinkSync4(link).replace(/ \(deleted\)$/, "");
8841
- const held = raw.startsWith("/") ? resolve5(raw) : raw;
9683
+ const held = raw.startsWith("/") ? resolve7(raw) : raw;
8842
9684
  if (held === real || held.startsWith(real + "/")) {
8843
9685
  holds = true;
8844
9686
  break;
@@ -8851,7 +9693,7 @@ function survivorsHoldingTree(cwd, spawnedAfterTicks) {
8851
9693
  return out2;
8852
9694
  }
8853
9695
  function runEnvelope(opts) {
8854
- const cwd = resolve5(opts.cwd ?? process.cwd());
9696
+ const cwd = resolve7(opts.cwd ?? process.cwd());
8855
9697
  if (!opts.argv.length) {
8856
9698
  err("tamperward run: no agent command given (usage: tamperward run [options] -- <command...>)");
8857
9699
  return 2;
@@ -8887,11 +9729,37 @@ function runEnvelope(opts) {
8887
9729
  return 2;
8888
9730
  }
8889
9731
  }
9732
+ const verifierBackend = prepareVerifierBackend(frozenPolicy.verify);
8890
9733
  out(`tamperward run \u2014 trusted base ${base.slice(0, 10)}; agent exit is untrusted.`);
8891
- const depsBefore = depsFingerprint(cwd);
9734
+ out(`tamperward run \u2014 verifier backend: ${verifierBackendSummary(verifierBackend)}`);
9735
+ if (!verifierBackend.available) {
9736
+ err("tamperward run: the requested verifier backend cannot be established \u2014");
9737
+ err(`${verifierBackend.reason ?? "unknown backend failure"}. Failing closed before the agent starts.`);
9738
+ return 2;
9739
+ }
9740
+ if (verifierBackend.kind === "container") {
9741
+ err("tamperward run: container verification requires a frozen candidate handoff from an");
9742
+ err("agent domain that cannot control the verifier engine. `run` executes the agent under");
9743
+ err("this host identity, so it cannot honestly provide that separation. Failing closed");
9744
+ err("before the agent starts; use standalone `tamperward verify` in trusted CI or after");
9745
+ err("an externally isolated agent produces the frozen candidate artifact.");
9746
+ return 2;
9747
+ }
9748
+ const dependencyEnvironment = verifierBackend.kind === "local" ? discoverDependencyEnvironment(cwd, frozenCmd) : null;
9749
+ if (dependencyEnvironment) {
9750
+ out(`tamperward run \u2014 dependency environment: ${dependencyEnvironmentSummary(dependencyEnvironment)}`);
9751
+ if (dependencyEnvironment.status === "unattestable" && !opts.allowDepDrift) {
9752
+ err("tamperward run: the verifier dependency environment cannot be attested \u2014");
9753
+ err(`${dependencyEnvironment.reason ?? "unknown dependency environment"}. Failing closed before the agent starts.`);
9754
+ err("(Pass --allow-dep-drift only if you explicitly own this dependency-integrity risk.)");
9755
+ return 2;
9756
+ }
9757
+ } else {
9758
+ out(`tamperward run \u2014 dependency environment: verifier-owned by ${verifierBackend.image ?? "isolated image"}`);
9759
+ }
8892
9760
  const rewritesBefore = objectRewriteState(cwd);
8893
9761
  const spawnTicks = nowTicks();
8894
- const agent = spawnSync2(opts.argv[0], opts.argv.slice(1), { cwd, stdio: "inherit" });
9762
+ const agent = spawnSync3(opts.argv[0], opts.argv.slice(1), { cwd, stdio: "inherit" });
8895
9763
  const agentExit = agent.status ?? 1;
8896
9764
  if (objectRewriteState(cwd) !== rewritesBefore) {
8897
9765
  err("tamperward run: git object-replacement state changed during this run \u2014 a replace ref, graft");
@@ -8901,9 +9769,10 @@ function runEnvelope(opts) {
8901
9769
  tamperward run \u2014 agent exit ${agentExit}; OBJECT_REWRITE \u2192 ENFORCEMENT_FAILED`);
8902
9770
  return 1;
8903
9771
  }
8904
- if (depsFingerprint(cwd) !== depsBefore) {
8905
- err("tamperward run: the installed dependency tree changed before adjudication began \u2014 the program");
8906
- err("that runs the suite is not the one we started with, and both verification runs execute it.");
9772
+ const dependencyBeforeAdjudication = dependencyEnvironment ? checkDependencyEnvironment(cwd, dependencyEnvironment) : { ok: true };
9773
+ if (!dependencyBeforeAdjudication.ok) {
9774
+ err("tamperward run: the frozen dependency environment changed before adjudication began \u2014");
9775
+ err(`${dependencyBeforeAdjudication.reason ?? "dependency identity changed"}.`);
8907
9776
  if (!opts.allowDepDrift) {
8908
9777
  out(`
8909
9778
  tamperward run \u2014 agent exit ${agentExit}; DEPENDENCY_DRIFT \u2192 ENFORCEMENT_FAILED`);
@@ -8912,7 +9781,7 @@ tamperward run \u2014 agent exit ${agentExit}; DEPENDENCY_DRIFT \u2192 ENFORCEME
8912
9781
  err("(--allow-dep-drift: proceeding anyway, on the operator's judgement.)");
8913
9782
  }
8914
9783
  const head = git3(["rev-parse", "HEAD"], cwd).trim();
8915
- const isAncestor = spawnSync2("git", ["merge-base", "--is-ancestor", base, head], { cwd, env: trustedGitEnv() });
9784
+ const isAncestor = spawnSync3("git", ["merge-base", "--is-ancestor", base, head], { cwd, env: trustedGitEnv() });
8916
9785
  if (isAncestor.status !== 0) {
8917
9786
  err(`tamperward run: HEAD ${head.slice(0, 10)} is not a descendant of the trusted base ${base.slice(0, 10)} \u2014`);
8918
9787
  err("the agent rewrote history out from under the anchor. That is the finding.");
@@ -8931,7 +9800,16 @@ tamperward run \u2014 agent exit ${agentExit}; HISTORY_REWRITE \u2192 ENFORCEMEN
8931
9800
  out("\n[envelope] policy check over the working tree (frozen policy, untracked included, no local ledger):");
8932
9801
  const workCode = runCheck({ worktree: true, cwd, policyOverride: frozenPolicy, includeUntracked: true, ciLayer: true });
8933
9802
  out("\n[envelope] pristine verification against the trusted base:");
8934
- const verifyCode = runVerify({ cwd, base, cmd: frozenCmd, budget: frozenBudget, policyOverride: frozenPolicy });
9803
+ const verifyCode = runVerify({
9804
+ cwd,
9805
+ base,
9806
+ cmd: frozenCmd,
9807
+ budget: frozenBudget,
9808
+ policyOverride: frozenPolicy,
9809
+ ...dependencyEnvironment ? { dependencyEnvironment } : {},
9810
+ allowDepDrift: opts.allowDepDrift,
9811
+ verifierBackend
9812
+ });
8935
9813
  if (opts.settle && opts.settle > 0) {
8936
9814
  try {
8937
9815
  execFileSync9("sleep", [String(opts.settle)]);
@@ -8943,10 +9821,11 @@ tamperward run \u2014 agent exit ${agentExit}; HISTORY_REWRITE \u2192 ENFORCEMEN
8943
9821
  err("tamperward run: the working tree changed while it was being adjudicated \u2014");
8944
9822
  err("the verdict below would describe a tree that no longer exists.");
8945
9823
  }
8946
- const depsDrifted = !opts.allowDepDrift && depsFingerprint(cwd) !== depsBefore;
9824
+ const dependencyAfter = dependencyEnvironment ? checkDependencyEnvironment(cwd, dependencyEnvironment) : { ok: true };
9825
+ const depsDrifted = Boolean(dependencyEnvironment) && !opts.allowDepDrift && !dependencyAfter.ok;
8947
9826
  if (depsDrifted) {
8948
- err("tamperward run: the installed dependency tree changed during this run \u2014 the program that");
8949
- err("runs the suite is not the one we started with. Both verification runs execute it.");
9827
+ err("tamperward run: the frozen dependency environment changed during this run \u2014");
9828
+ err(`${dependencyAfter.reason ?? "dependency identity changed"}. Both verification runs depend on it.`);
8950
9829
  }
8951
9830
  const rewrote = objectRewriteState(cwd) !== rewritesBefore;
8952
9831
  if (rewrote) {