tamperward 2.10.9 → 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.
package/README.md CHANGED
@@ -207,8 +207,11 @@ request would need the verify label; the self-gate stays a diff-time gate.
207
207
 
208
208
  This guarantee depends on a protected and immutable base, required status checks, a
209
209
  pinned Tamperward version, and label permissions restricted to trusted humans.
210
- Tamperward provides no OS isolation, no network confinement, no secret isolation,
211
- and no semantic-correctness oracle.
210
+ The default local verifier is checkpointed same-host execution, not OS isolation.
211
+ An optional digest-pinned container backend isolates final verification with no network,
212
+ host dependency tree, HOME, temp, credential or socket sharing. Tamperward still is not a
213
+ semantic-correctness oracle, and `tamperward run` does not pretend that a same-identity
214
+ host agent is isolated from the Docker daemon.
212
215
 
213
216
  Full assumptions and residual risks: [SPEC.md](./SPEC.md),
214
217
  [SECURITY-ENVELOPE.md](./SECURITY-ENVELOPE.md), and the
@@ -246,11 +249,26 @@ verify:
246
249
  command: npm test
247
250
  budget: 300
248
251
  inputs: ['scripts/**'] # what the command DELEGATES to
252
+ # Optional stronger final-verification boundary:
253
+ # backend: container
254
+ # image: ghcr.io/acme/verifier@sha256:<64-hex-digest>
249
255
  ```
250
256
 
257
+ `backend: local` is the default and is reported as `checkpointed-local`. With
258
+ `backend: container`, the image must be digest-pinned and already present on the fixed
259
+ local Docker daemon; Tamperward never pulls during adjudication. The materialised
260
+ candidate/pristine tree is mounted read-only, the image owns runtime/dependencies,
261
+ network is disabled, HOME/tmp are private, and optional suite output belongs in
262
+ `$TAMPERWARD_OUTPUT_DIR` (`/workspace-out`). Images that declare Dockerfile
263
+ `VOLUME` paths are refused: Docker mounts those paths writable even with
264
+ `--read-only`, which would undermine the immutable verifier-image boundary. Image
265
+ `ENTRYPOINT` is also overridden; the pinned image supplies the runtime/dependencies,
266
+ while the trusted policy's `verify.command` remains the command that is adjudicated.
267
+
251
268
  That block is itself a guarded surface: changing the command, lowering the budget,
252
- narrowing `inputs`, or removing it is flagged as policy weakening — a verifier an
253
- agent can point at `true` is no verification at all.
269
+ narrowing `inputs`, removing `backend: container`, or changing its pinned image is
270
+ flagged as policy weakening — a verifier an agent can redirect is no verification at
271
+ all.
254
272
 
255
273
  `inputs` names the files the command *executes*, so the pristine run gets the
256
274
  base's copy of them too. A command token that names a file present at the base is
@@ -266,9 +284,14 @@ The four primitives:
266
284
  npx tamperward check --staged # pre-commit view
267
285
  npx tamperward check --diff "main...HEAD" # CI view over the PR's commit range
268
286
  npx tamperward verify --base main # pristine-suite re-execution
269
- npx tamperward run -- <agent command...> # the outer envelope around an agent
287
+ npx tamperward run -- <agent command...> # same-host envelope (local verifier only)
270
288
  ```
271
289
 
290
+ The isolated backend is a **frozen-artifact final verifier**. `tamperward run`
291
+ deliberately refuses `backend: container` before launching the agent because the agent
292
+ would share the host identity that controls Docker. Use isolated `tamperward verify`
293
+ from trusted CI, or after an externally isolated agent hands off the frozen candidate.
294
+
272
295
  ### CLI reference
273
296
 
274
297
  Every flag below is what the command's parser actually reads (`src/cli/index.ts`,
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, resolve7) {
1655
- const obj = resolve7(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, resolve7) {
1666
1666
  const k = keyName(p.name);
1667
1667
  if (k === null || !isMetric(k)) continue;
1668
1668
  any = true;
1669
- const n = numericOf(resolve7(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 resolve7 = 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 = resolve7(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, resolve7);
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, resolve7);
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 resolve7 = 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(resolve7(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(resolve7(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,11 +8191,11 @@ 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 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";
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 dirname6, isAbsolute as isAbsolute10, join as join14, relative as relative5, resolve as resolve5, sep as sep2 } 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";
@@ -8547,6 +8576,420 @@ function dependencyEnvironmentSummary(descriptor) {
8547
8576
  return "attested " + kinds + " (sha256 " + (descriptor.fingerprint?.slice(0, 12) ?? "unknown") + "...)";
8548
8577
  }
8549
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;
8600
+ } catch {
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
+ };
8990
+ }
8991
+ }
8992
+
8550
8993
  // src/cli/verify.ts
8551
8994
  var OVERLAY_CLASSES = ["tests", "snapshots", "config"];
8552
8995
  var VERIFICATION_SURFACE = [
@@ -8632,11 +9075,11 @@ function resolveBase(base, cwd) {
8632
9075
  }
8633
9076
  }
8634
9077
  function baseIsAncestorOfHead(base, cwd) {
8635
- 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() });
8636
9079
  return r.status === 0;
8637
9080
  }
8638
9081
  function inside2(root, path) {
8639
- const rel = relative5(resolve5(root), resolve5(path));
9082
+ const rel = relative5(resolve6(root), resolve6(path));
8640
9083
  return rel === "" || rel !== ".." && !rel.startsWith(`..${sep2}`) && !isAbsolute10(rel);
8641
9084
  }
8642
9085
  function linkParts(path) {
@@ -8653,7 +9096,7 @@ function rootedLinkTarget(target) {
8653
9096
  return isAbsolute10(target) || driveRelativeLinkTarget(target);
8654
9097
  }
8655
9098
  function safeSymlink(target, out2, root, label) {
8656
- if (rootedLinkTarget(target) || !inside2(root, resolve5(dirname6(out2), target))) {
9099
+ if (rootedLinkTarget(target) || !inside2(root, resolve6(dirname7(out2), target))) {
8657
9100
  throw linkEscape(label, target);
8658
9101
  }
8659
9102
  rmSync3(out2, { force: true });
@@ -8661,7 +9104,7 @@ function safeSymlink(target, out2, root, label) {
8661
9104
  }
8662
9105
  function validateSymlinkGraph(target, out2, root, dependencyRoot, label) {
8663
9106
  let domain = "tree";
8664
- let stack = linkParts(relative5(root, dirname6(out2)));
9107
+ let stack = linkParts(relative5(root, dirname7(out2)));
8665
9108
  const pending = linkParts(target);
8666
9109
  const seen = /* @__PURE__ */ new Set();
8667
9110
  let hops = 0;
@@ -8679,7 +9122,7 @@ function validateSymlinkGraph(target, out2, root, dependencyRoot, label) {
8679
9122
  continue;
8680
9123
  }
8681
9124
  const base = domain === "tree" ? root : dependencyRoot;
8682
- const current = join14(base, ...stack);
9125
+ const current = join15(base, ...stack);
8683
9126
  let st;
8684
9127
  try {
8685
9128
  st = lstatSync4(current);
@@ -8705,7 +9148,7 @@ function validateSymlinkGraph(target, out2, root, dependencyRoot, label) {
8705
9148
  throw linkEscape(label, target);
8706
9149
  }
8707
9150
  stack = [];
8708
- pending.unshift(...linkParts(relative5(dependencyRoot, resolve5(next))));
9151
+ pending.unshift(...linkParts(relative5(dependencyRoot, resolve6(next))));
8709
9152
  } else {
8710
9153
  pending.unshift(...linkParts(next));
8711
9154
  }
@@ -8715,7 +9158,7 @@ function rejectLinkedParent(cwd, rel) {
8715
9158
  const parts = rel.split("/").slice(0, -1);
8716
9159
  let current = cwd;
8717
9160
  for (const part of parts) {
8718
- current = join14(current, part);
9161
+ current = join15(current, part);
8719
9162
  let st;
8720
9163
  try {
8721
9164
  st = lstatSync4(current);
@@ -8731,7 +9174,7 @@ function materialize(cwd, dest, dependencyRoot) {
8731
9174
  const listed = git2(["ls-files", "-z", "--cached", "--others", "--exclude-standard"], cwd).split("\0").filter(Boolean);
8732
9175
  const links = [];
8733
9176
  for (const rel of listed) {
8734
- const src = join14(cwd, rel);
9177
+ const src = join15(cwd, rel);
8735
9178
  rejectLinkedParent(cwd, rel);
8736
9179
  let st;
8737
9180
  try {
@@ -8739,8 +9182,8 @@ function materialize(cwd, dest, dependencyRoot) {
8739
9182
  } catch {
8740
9183
  continue;
8741
9184
  }
8742
- const out2 = join14(dest, rel);
8743
- mkdirSync6(dirname6(out2), { recursive: true });
9185
+ const out2 = join15(dest, rel);
9186
+ mkdirSync6(dirname7(out2), { recursive: true });
8744
9187
  dropSymlink(out2);
8745
9188
  if (st.isSymbolicLink()) {
8746
9189
  const target = readlinkSync3(src);
@@ -8756,7 +9199,7 @@ function materialize(cwd, dest, dependencyRoot) {
8756
9199
  for (const link of links) {
8757
9200
  validateSymlinkGraph(link.target, link.out, dest, dependencyRoot, link.rel);
8758
9201
  }
8759
- if (dependencyRoot) symlinkSync(dependencyRoot, join14(dest, "node_modules"), "dir");
9202
+ if (dependencyRoot) symlinkSync(dependencyRoot, join15(dest, "node_modules"), "dir");
8760
9203
  }
8761
9204
  function dropSymlink(p) {
8762
9205
  try {
@@ -8785,15 +9228,15 @@ function baseEntries(base, cwd) {
8785
9228
  });
8786
9229
  }
8787
9230
  function overlayDigest(dest, paths) {
8788
- const h = createHash9("sha256");
9231
+ const h = createHash10("sha256");
8789
9232
  for (const rel of [...paths].sort()) {
8790
9233
  h.update(rel);
8791
9234
  h.update("\0");
8792
- const p = join14(dest, rel);
9235
+ const p = join15(dest, rel);
8793
9236
  try {
8794
9237
  const st = lstatSync4(p);
8795
9238
  h.update(String(st.mode));
8796
- h.update(st.isSymbolicLink() ? readlinkSync3(p) : readFileSync14(p));
9239
+ h.update(st.isSymbolicLink() ? readlinkSync3(p) : readFileSync15(p));
8797
9240
  } catch {
8798
9241
  h.update("<gone>");
8799
9242
  }
@@ -8817,8 +9260,8 @@ function overlayPristine(cwd, base, dest, policy, cmd, dependencyRoot) {
8817
9260
  throw new Error(`the base's ${e.path} is a submodule; the pristine copy cannot reproduce it`);
8818
9261
  }
8819
9262
  const content = execFileSync8("git", ["show", `${base}:${e.path}`], { cwd, maxBuffer: 1 << 28, env: trustedGitEnv() });
8820
- const out2 = join14(dest, e.path);
8821
- mkdirSync6(dirname6(out2), { recursive: true });
9263
+ const out2 = join15(dest, e.path);
9264
+ mkdirSync6(dirname7(out2), { recursive: true });
8822
9265
  rmSync3(out2, { force: true });
8823
9266
  if (e.mode === "120000") {
8824
9267
  const target = content.toString("utf8");
@@ -8834,7 +9277,7 @@ function overlayPristine(cwd, base, dest, policy, cmd, dependencyRoot) {
8834
9277
  const inCopy = git2(["ls-files", "-z", "--cached", "--others", "--exclude-standard"], cwd).split("\0").filter(Boolean);
8835
9278
  for (const rel of inCopy) {
8836
9279
  if (!isOverlay(rel) || baseProtected.has(rel)) continue;
8837
- rmSync3(join14(dest, rel), { force: true });
9280
+ rmSync3(join15(dest, rel), { force: true });
8838
9281
  removed++;
8839
9282
  }
8840
9283
  for (const link of restoredLinks) {
@@ -8879,8 +9322,8 @@ function suiteEnv(scratch) {
8879
9322
  if (DROPPED_ENV.has(k) || PINNED_NPM.test(k)) continue;
8880
9323
  env[k] = v;
8881
9324
  }
8882
- const userRc = join14(scratch, "npmrc-user");
8883
- const globalRc = join14(scratch, "npmrc-global");
9325
+ const userRc = join15(scratch, "npmrc-user");
9326
+ const globalRc = join15(scratch, "npmrc-global");
8884
9327
  for (const f of [userRc, globalRc]) writeFileSync6(f, "", { mode: 292 });
8885
9328
  env.npm_config_userconfig = userRc;
8886
9329
  env.npm_config_globalconfig = globalRc;
@@ -8888,12 +9331,12 @@ function suiteEnv(scratch) {
8888
9331
  env.YARN_IGNORE_PATH = "1";
8889
9332
  return env;
8890
9333
  }
8891
- function runSuite(dir, cmd, budgetSecs) {
9334
+ function runLocalSuite(dir, cmd, budgetSecs) {
8892
9335
  const t0 = Date.now();
8893
- const outDir = mkdtempSync2(join14(tmpdir2(), "tw-verify-run-"));
8894
- const outFile = join14(outDir, "result.json");
9336
+ const outDir = mkdtempSync2(join15(tmpdir2(), "tw-verify-run-"));
9337
+ const outFile = join15(outDir, "result.json");
8895
9338
  try {
8896
- spawnSync(process.execPath, ["-e", SUPERVISOR, cmd, String(budgetSecs * 1e3), outFile], {
9339
+ spawnSync2(process.execPath, ["-e", SUPERVISOR, cmd, String(budgetSecs * 1e3), outFile], {
8897
9340
  cwd: dir,
8898
9341
  env: suiteEnv(outDir),
8899
9342
  stdio: "ignore",
@@ -8904,7 +9347,7 @@ function runSuite(dir, cmd, budgetSecs) {
8904
9347
  const secs = Math.round((Date.now() - t0) / 1e3);
8905
9348
  let r;
8906
9349
  try {
8907
- r = JSON.parse(readFileSync14(outFile, "utf8"));
9350
+ r = JSON.parse(readFileSync15(outFile, "utf8"));
8908
9351
  } catch {
8909
9352
  return { exit: null, secs };
8910
9353
  }
@@ -8948,13 +9391,34 @@ function runVerify(opts) {
8948
9391
  return 2;
8949
9392
  }
8950
9393
  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) {
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) {
8954
9417
  if (opts.json) {
8955
9418
  out2(JSON.stringify({
8956
9419
  verdict: "CANNOT_VERIFY",
8957
9420
  reason: "DEPENDENCY_ENVIRONMENT_UNATTESTABLE",
9421
+ verifier_backend: backendReport(),
8958
9422
  dependency_environment: dependencyReport()
8959
9423
  }));
8960
9424
  } else {
@@ -8963,7 +9427,9 @@ function runVerify(opts) {
8963
9427
  }
8964
9428
  return 2;
8965
9429
  }
8966
- const frozenNodeModules = dependencyEnvironment.roots.find((root) => root.kind === "node_modules")?.realPath ?? null;
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);
8967
9433
  let base;
8968
9434
  try {
8969
9435
  base = resolveBase(opts.base ?? "HEAD", cwd);
@@ -8971,15 +9437,15 @@ function runVerify(opts) {
8971
9437
  out2(`verify: cannot resolve base rev "${opts.base ?? "HEAD"}" \u2014 failing closed`);
8972
9438
  return 2;
8973
9439
  }
8974
- const visRoot = mkdtempSync2(join14(tmpdir2(), "tw-verify-vis-"));
8975
- const visDir = join14(visRoot, "t");
9440
+ const visRoot = mkdtempSync2(join15(tmpdir2(), "tw-verify-vis-"));
9441
+ const visDir = join15(visRoot, "t");
8976
9442
  const cleanup = (dirs) => {
8977
9443
  if (opts.keep) return;
8978
9444
  for (const d of dirs) rmSync3(d, { recursive: true, force: true });
8979
9445
  };
8980
9446
  const protectedIgnored = (rel) => isProtected(rel, policy);
8981
9447
  const treeBefore = treeFingerprint(cwd, protectedIgnored);
8982
- const dependencyAtEntry = checkDependencyEnvironment(cwd, dependencyEnvironment);
9448
+ const dependencyAtEntry = checkDeps();
8983
9449
  if (!dependencyAtEntry.ok && !opts.allowDepDrift) {
8984
9450
  cleanup([visRoot]);
8985
9451
  out2("verify: the frozen dependency environment changed before the visible suite ran \u2014");
@@ -8994,22 +9460,38 @@ function runVerify(opts) {
8994
9460
  out2(`verify: could not materialize (${e instanceof Error ? e.message : String(e)}) \u2014 failing closed`);
8995
9461
  return 2;
8996
9462
  }
8997
- 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
+ }
8998
9480
  if (treeFingerprint(cwd, protectedIgnored) !== treeBefore) {
8999
9481
  cleanup([visRoot]);
9000
9482
  out2("verify: the working tree changed while the visible suite was running \u2014 the pristine copy");
9001
9483
  out2("would be materialised from a tree the candidate just edited. Failing closed, not open.");
9002
9484
  return 2;
9003
9485
  }
9004
- const dependencyAfterVisible = checkDependencyEnvironment(cwd, dependencyEnvironment);
9486
+ const dependencyAfterVisible = checkDeps();
9005
9487
  if (!dependencyAfterVisible.ok && !opts.allowDepDrift) {
9006
9488
  cleanup([visRoot]);
9007
9489
  out2("verify: the frozen dependency environment changed while the visible suite was running \u2014");
9008
9490
  out2((dependencyAfterVisible.reason ?? "dependency identity changed") + ". Failing closed, not open.");
9009
9491
  return 2;
9010
9492
  }
9011
- const priRoot = mkdtempSync2(join14(tmpdir2(), "tw-verify-pri-"));
9012
- const priDir = join14(priRoot, "t");
9493
+ const priRoot = mkdtempSync2(join15(tmpdir2(), "tw-verify-pri-"));
9494
+ const priDir = join15(priRoot, "t");
9013
9495
  let restored = [];
9014
9496
  let removedAdded = 0;
9015
9497
  try {
@@ -9029,10 +9511,26 @@ function runVerify(opts) {
9029
9511
  return 2;
9030
9512
  }
9031
9513
  const overlayBefore = overlayDigest(priDir, restored);
9032
- 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
+ }
9033
9531
  const overlayMoved = overlayDigest(priDir, restored) !== overlayBefore;
9034
9532
  const treeMoved = treeFingerprint(cwd, protectedIgnored) !== treeBefore;
9035
- const dependencyAfterPristine = checkDependencyEnvironment(cwd, dependencyEnvironment);
9533
+ const dependencyAfterPristine = checkDeps();
9036
9534
  const depsMoved = !dependencyAfterPristine.ok && !opts.allowDepDrift;
9037
9535
  cleanup([visRoot, priRoot]);
9038
9536
  if (overlayMoved || treeMoved || depsMoved) {
@@ -9072,6 +9570,7 @@ function runVerify(opts) {
9072
9570
  pristine: { exit: pristine.exit, secs: pristine.secs },
9073
9571
  protected_restored: restored.length,
9074
9572
  added_protected_removed: removedAdded,
9573
+ verifier_backend: backendReport(),
9075
9574
  dependency_environment: dependencyReport(),
9076
9575
  ...signedOff ? { oob_signoff: signedOff } : {},
9077
9576
  ...opts.keep ? { visible_dir: visDir, pristine_dir: priDir } : {}
@@ -9085,7 +9584,10 @@ function runVerify(opts) {
9085
9584
  BUDGET_EXCEEDED: `budget exceeded (${budget}s): could not verify \u2014 failing closed, not open.`
9086
9585
  };
9087
9586
  out2(`tamperward verify \u2014 ${lines[verdict2]}`);
9088
- out2(`dependency environment: ${dependencyEnvironmentSummary(dependencyEnvironment)}` + (opts.allowDepDrift && dependencyEnvironment.status === "unattestable" ? " (operator override)" : ""));
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
+ );
9089
9591
  if (signedOff)
9090
9592
  out2(
9091
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.`
@@ -9130,9 +9632,9 @@ function parseVerify(args) {
9130
9632
  }
9131
9633
 
9132
9634
  // src/cli/run.ts
9133
- import { execFileSync as execFileSync9, spawnSync as spawnSync2 } from "node:child_process";
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";
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";
9136
9638
  var out = (s) => process.stdout.write(s + "\n");
9137
9639
  var err = (s) => process.stderr.write(s + "\n");
9138
9640
  function git3(args, cwd) {
@@ -9140,7 +9642,7 @@ function git3(args, cwd) {
9140
9642
  }
9141
9643
  function nowTicks() {
9142
9644
  try {
9143
- return Math.floor(parseFloat(readFileSync15("/proc/uptime", "utf8").split(" ")[0]) * 100);
9645
+ return Math.floor(parseFloat(readFileSync16("/proc/uptime", "utf8").split(" ")[0]) * 100);
9144
9646
  } catch {
9145
9647
  return Number.POSITIVE_INFINITY;
9146
9648
  }
@@ -9149,7 +9651,7 @@ function survivorsHoldingTree(cwd, spawnedAfterTicks) {
9149
9651
  const out2 = [];
9150
9652
  let real;
9151
9653
  try {
9152
- real = realpathSync5(cwd);
9654
+ real = realpathSync6(cwd);
9153
9655
  } catch {
9154
9656
  return out2;
9155
9657
  }
@@ -9163,7 +9665,7 @@ function survivorsHoldingTree(cwd, spawnedAfterTicks) {
9163
9665
  const n = Number(pid);
9164
9666
  if (n === process.pid) continue;
9165
9667
  try {
9166
- const stat = readFileSync15(`/proc/${pid}/stat`, "utf8");
9668
+ const stat = readFileSync16(`/proc/${pid}/stat`, "utf8");
9167
9669
  const startTicks = Number(stat.slice(stat.lastIndexOf(")") + 2).split(" ")[19]);
9168
9670
  if (!Number.isFinite(startTicks) || startTicks < spawnedAfterTicks) continue;
9169
9671
  } catch {
@@ -9178,7 +9680,7 @@ function survivorsHoldingTree(cwd, spawnedAfterTicks) {
9178
9680
  for (const link of procLinks) {
9179
9681
  try {
9180
9682
  const raw = readlinkSync4(link).replace(/ \(deleted\)$/, "");
9181
- const held = raw.startsWith("/") ? resolve6(raw) : raw;
9683
+ const held = raw.startsWith("/") ? resolve7(raw) : raw;
9182
9684
  if (held === real || held.startsWith(real + "/")) {
9183
9685
  holds = true;
9184
9686
  break;
@@ -9191,7 +9693,7 @@ function survivorsHoldingTree(cwd, spawnedAfterTicks) {
9191
9693
  return out2;
9192
9694
  }
9193
9695
  function runEnvelope(opts) {
9194
- const cwd = resolve6(opts.cwd ?? process.cwd());
9696
+ const cwd = resolve7(opts.cwd ?? process.cwd());
9195
9697
  if (!opts.argv.length) {
9196
9698
  err("tamperward run: no agent command given (usage: tamperward run [options] -- <command...>)");
9197
9699
  return 2;
@@ -9227,18 +9729,37 @@ function runEnvelope(opts) {
9227
9729
  return 2;
9228
9730
  }
9229
9731
  }
9230
- const dependencyEnvironment = discoverDependencyEnvironment(cwd, frozenCmd);
9732
+ const verifierBackend = prepareVerifierBackend(frozenPolicy.verify);
9231
9733
  out(`tamperward run \u2014 trusted base ${base.slice(0, 10)}; agent exit is untrusted.`);
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.)");
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.");
9237
9746
  return 2;
9238
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
+ }
9239
9760
  const rewritesBefore = objectRewriteState(cwd);
9240
9761
  const spawnTicks = nowTicks();
9241
- 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" });
9242
9763
  const agentExit = agent.status ?? 1;
9243
9764
  if (objectRewriteState(cwd) !== rewritesBefore) {
9244
9765
  err("tamperward run: git object-replacement state changed during this run \u2014 a replace ref, graft");
@@ -9248,7 +9769,7 @@ function runEnvelope(opts) {
9248
9769
  tamperward run \u2014 agent exit ${agentExit}; OBJECT_REWRITE \u2192 ENFORCEMENT_FAILED`);
9249
9770
  return 1;
9250
9771
  }
9251
- const dependencyBeforeAdjudication = checkDependencyEnvironment(cwd, dependencyEnvironment);
9772
+ const dependencyBeforeAdjudication = dependencyEnvironment ? checkDependencyEnvironment(cwd, dependencyEnvironment) : { ok: true };
9252
9773
  if (!dependencyBeforeAdjudication.ok) {
9253
9774
  err("tamperward run: the frozen dependency environment changed before adjudication began \u2014");
9254
9775
  err(`${dependencyBeforeAdjudication.reason ?? "dependency identity changed"}.`);
@@ -9260,7 +9781,7 @@ tamperward run \u2014 agent exit ${agentExit}; DEPENDENCY_DRIFT \u2192 ENFORCEME
9260
9781
  err("(--allow-dep-drift: proceeding anyway, on the operator's judgement.)");
9261
9782
  }
9262
9783
  const head = git3(["rev-parse", "HEAD"], cwd).trim();
9263
- 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() });
9264
9785
  if (isAncestor.status !== 0) {
9265
9786
  err(`tamperward run: HEAD ${head.slice(0, 10)} is not a descendant of the trusted base ${base.slice(0, 10)} \u2014`);
9266
9787
  err("the agent rewrote history out from under the anchor. That is the finding.");
@@ -9285,8 +9806,9 @@ tamperward run \u2014 agent exit ${agentExit}; HISTORY_REWRITE \u2192 ENFORCEMEN
9285
9806
  cmd: frozenCmd,
9286
9807
  budget: frozenBudget,
9287
9808
  policyOverride: frozenPolicy,
9288
- dependencyEnvironment,
9289
- allowDepDrift: opts.allowDepDrift
9809
+ ...dependencyEnvironment ? { dependencyEnvironment } : {},
9810
+ allowDepDrift: opts.allowDepDrift,
9811
+ verifierBackend
9290
9812
  });
9291
9813
  if (opts.settle && opts.settle > 0) {
9292
9814
  try {
@@ -9299,8 +9821,8 @@ tamperward run \u2014 agent exit ${agentExit}; HISTORY_REWRITE \u2192 ENFORCEMEN
9299
9821
  err("tamperward run: the working tree changed while it was being adjudicated \u2014");
9300
9822
  err("the verdict below would describe a tree that no longer exists.");
9301
9823
  }
9302
- const dependencyAfter = checkDependencyEnvironment(cwd, dependencyEnvironment);
9303
- const depsDrifted = !opts.allowDepDrift && !dependencyAfter.ok;
9824
+ const dependencyAfter = dependencyEnvironment ? checkDependencyEnvironment(cwd, dependencyEnvironment) : { ok: true };
9825
+ const depsDrifted = Boolean(dependencyEnvironment) && !opts.allowDepDrift && !dependencyAfter.ok;
9304
9826
  if (depsDrifted) {
9305
9827
  err("tamperward run: the frozen dependency environment changed during this run \u2014");
9306
9828
  err(`${dependencyAfter.reason ?? "dependency identity changed"}. Both verification runs depend on it.`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tamperward",
3
- "version": "2.10.9",
3
+ "version": "2.11.0",
4
4
  "description": "The deterministic agent-integrity gate. One ruleset, evaluated on the actual diff/commands as a verdict, enforced everywhere a change can be made.",
5
5
  "license": "Apache-2.0",
6
6
  "author": "hexrift",