tamperward 2.10.9 → 2.11.1

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,14 @@ 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: verifier output reports `oracle_assurance: suite-exit-only`
214
+ because candidate source still executes inside the configured suite process and can
215
+ terminate or interpose on that in-process oracle. `isolated-container` therefore means
216
+ execution-domain isolation, not semantic/oracle isolation. `tamperward run` also does
217
+ not pretend that a same-identity host agent is isolated from the Docker daemon.
212
218
 
213
219
  Full assumptions and residual risks: [SPEC.md](./SPEC.md),
214
220
  [SECURITY-ENVELOPE.md](./SECURITY-ENVELOPE.md), and the
@@ -246,11 +252,26 @@ verify:
246
252
  command: npm test
247
253
  budget: 300
248
254
  inputs: ['scripts/**'] # what the command DELEGATES to
255
+ # Optional stronger final-verification boundary:
256
+ # backend: container
257
+ # image: ghcr.io/acme/verifier@sha256:<64-hex-digest>
249
258
  ```
250
259
 
260
+ `backend: local` is the default and is reported as `checkpointed-local`. With
261
+ `backend: container`, the image must be digest-pinned and already present on the fixed
262
+ local Docker daemon; Tamperward never pulls during adjudication. The materialised
263
+ candidate/pristine tree is mounted read-only, the image owns runtime/dependencies,
264
+ network is disabled, HOME/tmp are private, and optional suite output belongs in
265
+ `$TAMPERWARD_OUTPUT_DIR` (`/workspace-out`). Images that declare Dockerfile
266
+ `VOLUME` paths are refused: Docker mounts those paths writable even with
267
+ `--read-only`, which would undermine the immutable verifier-image boundary. Image
268
+ `ENTRYPOINT` is also overridden; the pinned image supplies the runtime/dependencies,
269
+ while the trusted policy's `verify.command` remains the command that is adjudicated.
270
+
251
271
  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.
272
+ narrowing `inputs`, removing `backend: container`, or changing its pinned image is
273
+ flagged as policy weakening — a verifier an agent can redirect is no verification at
274
+ all.
254
275
 
255
276
  `inputs` names the files the command *executes*, so the pristine run gets the
256
277
  base's copy of them too. A command token that names a file present at the base is
@@ -266,9 +287,14 @@ The four primitives:
266
287
  npx tamperward check --staged # pre-commit view
267
288
  npx tamperward check --diff "main...HEAD" # CI view over the PR's commit range
268
289
  npx tamperward verify --base main # pristine-suite re-execution
269
- npx tamperward run -- <agent command...> # the outer envelope around an agent
290
+ npx tamperward run -- <agent command...> # same-host envelope (local verifier only)
270
291
  ```
271
292
 
293
+ The isolated backend is a **frozen-artifact final verifier**. `tamperward run`
294
+ deliberately refuses `backend: container` before launching the agent because the agent
295
+ would share the host identity that controls Docker. Use isolated `tamperward verify`
296
+ from trusted CI, or after an externally isolated agent hands off the frozen candidate.
297
+
272
298
  ### CLI reference
273
299
 
274
300
  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,7 +8576,429 @@ 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
8994
+ function oracleAssuranceReport() {
8995
+ return {
8996
+ level: "suite-exit-only",
8997
+ semantic_isolation: false,
8998
+ candidate_code_executes_in_oracle_process: true,
8999
+ limitation: "candidate source executes inside the configured suite process and may terminate or interpose on that in-process oracle; isolated-container is execution-domain isolation, not semantic/oracle isolation"
9000
+ };
9001
+ }
8551
9002
  var OVERLAY_CLASSES = ["tests", "snapshots", "config"];
8552
9003
  var VERIFICATION_SURFACE = [
8553
9004
  // Python / pytest — read from the rootdir, and conftest at any depth
@@ -8632,11 +9083,11 @@ function resolveBase(base, cwd) {
8632
9083
  }
8633
9084
  }
8634
9085
  function baseIsAncestorOfHead(base, cwd) {
8635
- const r = spawnSync("git", ["merge-base", "--is-ancestor", base, "HEAD"], { cwd, env: trustedGitEnv() });
9086
+ const r = spawnSync2("git", ["merge-base", "--is-ancestor", base, "HEAD"], { cwd, env: trustedGitEnv() });
8636
9087
  return r.status === 0;
8637
9088
  }
8638
9089
  function inside2(root, path) {
8639
- const rel = relative5(resolve5(root), resolve5(path));
9090
+ const rel = relative5(resolve6(root), resolve6(path));
8640
9091
  return rel === "" || rel !== ".." && !rel.startsWith(`..${sep2}`) && !isAbsolute10(rel);
8641
9092
  }
8642
9093
  function linkParts(path) {
@@ -8653,7 +9104,7 @@ function rootedLinkTarget(target) {
8653
9104
  return isAbsolute10(target) || driveRelativeLinkTarget(target);
8654
9105
  }
8655
9106
  function safeSymlink(target, out2, root, label) {
8656
- if (rootedLinkTarget(target) || !inside2(root, resolve5(dirname6(out2), target))) {
9107
+ if (rootedLinkTarget(target) || !inside2(root, resolve6(dirname7(out2), target))) {
8657
9108
  throw linkEscape(label, target);
8658
9109
  }
8659
9110
  rmSync3(out2, { force: true });
@@ -8661,7 +9112,7 @@ function safeSymlink(target, out2, root, label) {
8661
9112
  }
8662
9113
  function validateSymlinkGraph(target, out2, root, dependencyRoot, label) {
8663
9114
  let domain = "tree";
8664
- let stack = linkParts(relative5(root, dirname6(out2)));
9115
+ let stack = linkParts(relative5(root, dirname7(out2)));
8665
9116
  const pending = linkParts(target);
8666
9117
  const seen = /* @__PURE__ */ new Set();
8667
9118
  let hops = 0;
@@ -8679,7 +9130,7 @@ function validateSymlinkGraph(target, out2, root, dependencyRoot, label) {
8679
9130
  continue;
8680
9131
  }
8681
9132
  const base = domain === "tree" ? root : dependencyRoot;
8682
- const current = join14(base, ...stack);
9133
+ const current = join15(base, ...stack);
8683
9134
  let st;
8684
9135
  try {
8685
9136
  st = lstatSync4(current);
@@ -8705,7 +9156,7 @@ function validateSymlinkGraph(target, out2, root, dependencyRoot, label) {
8705
9156
  throw linkEscape(label, target);
8706
9157
  }
8707
9158
  stack = [];
8708
- pending.unshift(...linkParts(relative5(dependencyRoot, resolve5(next))));
9159
+ pending.unshift(...linkParts(relative5(dependencyRoot, resolve6(next))));
8709
9160
  } else {
8710
9161
  pending.unshift(...linkParts(next));
8711
9162
  }
@@ -8715,7 +9166,7 @@ function rejectLinkedParent(cwd, rel) {
8715
9166
  const parts = rel.split("/").slice(0, -1);
8716
9167
  let current = cwd;
8717
9168
  for (const part of parts) {
8718
- current = join14(current, part);
9169
+ current = join15(current, part);
8719
9170
  let st;
8720
9171
  try {
8721
9172
  st = lstatSync4(current);
@@ -8731,7 +9182,7 @@ function materialize(cwd, dest, dependencyRoot) {
8731
9182
  const listed = git2(["ls-files", "-z", "--cached", "--others", "--exclude-standard"], cwd).split("\0").filter(Boolean);
8732
9183
  const links = [];
8733
9184
  for (const rel of listed) {
8734
- const src = join14(cwd, rel);
9185
+ const src = join15(cwd, rel);
8735
9186
  rejectLinkedParent(cwd, rel);
8736
9187
  let st;
8737
9188
  try {
@@ -8739,8 +9190,8 @@ function materialize(cwd, dest, dependencyRoot) {
8739
9190
  } catch {
8740
9191
  continue;
8741
9192
  }
8742
- const out2 = join14(dest, rel);
8743
- mkdirSync6(dirname6(out2), { recursive: true });
9193
+ const out2 = join15(dest, rel);
9194
+ mkdirSync6(dirname7(out2), { recursive: true });
8744
9195
  dropSymlink(out2);
8745
9196
  if (st.isSymbolicLink()) {
8746
9197
  const target = readlinkSync3(src);
@@ -8756,7 +9207,7 @@ function materialize(cwd, dest, dependencyRoot) {
8756
9207
  for (const link of links) {
8757
9208
  validateSymlinkGraph(link.target, link.out, dest, dependencyRoot, link.rel);
8758
9209
  }
8759
- if (dependencyRoot) symlinkSync(dependencyRoot, join14(dest, "node_modules"), "dir");
9210
+ if (dependencyRoot) symlinkSync(dependencyRoot, join15(dest, "node_modules"), "dir");
8760
9211
  }
8761
9212
  function dropSymlink(p) {
8762
9213
  try {
@@ -8785,15 +9236,15 @@ function baseEntries(base, cwd) {
8785
9236
  });
8786
9237
  }
8787
9238
  function overlayDigest(dest, paths) {
8788
- const h = createHash9("sha256");
9239
+ const h = createHash10("sha256");
8789
9240
  for (const rel of [...paths].sort()) {
8790
9241
  h.update(rel);
8791
9242
  h.update("\0");
8792
- const p = join14(dest, rel);
9243
+ const p = join15(dest, rel);
8793
9244
  try {
8794
9245
  const st = lstatSync4(p);
8795
9246
  h.update(String(st.mode));
8796
- h.update(st.isSymbolicLink() ? readlinkSync3(p) : readFileSync14(p));
9247
+ h.update(st.isSymbolicLink() ? readlinkSync3(p) : readFileSync15(p));
8797
9248
  } catch {
8798
9249
  h.update("<gone>");
8799
9250
  }
@@ -8817,8 +9268,8 @@ function overlayPristine(cwd, base, dest, policy, cmd, dependencyRoot) {
8817
9268
  throw new Error(`the base's ${e.path} is a submodule; the pristine copy cannot reproduce it`);
8818
9269
  }
8819
9270
  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 });
9271
+ const out2 = join15(dest, e.path);
9272
+ mkdirSync6(dirname7(out2), { recursive: true });
8822
9273
  rmSync3(out2, { force: true });
8823
9274
  if (e.mode === "120000") {
8824
9275
  const target = content.toString("utf8");
@@ -8834,7 +9285,7 @@ function overlayPristine(cwd, base, dest, policy, cmd, dependencyRoot) {
8834
9285
  const inCopy = git2(["ls-files", "-z", "--cached", "--others", "--exclude-standard"], cwd).split("\0").filter(Boolean);
8835
9286
  for (const rel of inCopy) {
8836
9287
  if (!isOverlay(rel) || baseProtected.has(rel)) continue;
8837
- rmSync3(join14(dest, rel), { force: true });
9288
+ rmSync3(join15(dest, rel), { force: true });
8838
9289
  removed++;
8839
9290
  }
8840
9291
  for (const link of restoredLinks) {
@@ -8879,8 +9330,8 @@ function suiteEnv(scratch) {
8879
9330
  if (DROPPED_ENV.has(k) || PINNED_NPM.test(k)) continue;
8880
9331
  env[k] = v;
8881
9332
  }
8882
- const userRc = join14(scratch, "npmrc-user");
8883
- const globalRc = join14(scratch, "npmrc-global");
9333
+ const userRc = join15(scratch, "npmrc-user");
9334
+ const globalRc = join15(scratch, "npmrc-global");
8884
9335
  for (const f of [userRc, globalRc]) writeFileSync6(f, "", { mode: 292 });
8885
9336
  env.npm_config_userconfig = userRc;
8886
9337
  env.npm_config_globalconfig = globalRc;
@@ -8888,12 +9339,12 @@ function suiteEnv(scratch) {
8888
9339
  env.YARN_IGNORE_PATH = "1";
8889
9340
  return env;
8890
9341
  }
8891
- function runSuite(dir, cmd, budgetSecs) {
9342
+ function runLocalSuite(dir, cmd, budgetSecs) {
8892
9343
  const t0 = Date.now();
8893
- const outDir = mkdtempSync2(join14(tmpdir2(), "tw-verify-run-"));
8894
- const outFile = join14(outDir, "result.json");
9344
+ const outDir = mkdtempSync2(join15(tmpdir2(), "tw-verify-run-"));
9345
+ const outFile = join15(outDir, "result.json");
8895
9346
  try {
8896
- spawnSync(process.execPath, ["-e", SUPERVISOR, cmd, String(budgetSecs * 1e3), outFile], {
9347
+ spawnSync2(process.execPath, ["-e", SUPERVISOR, cmd, String(budgetSecs * 1e3), outFile], {
8897
9348
  cwd: dir,
8898
9349
  env: suiteEnv(outDir),
8899
9350
  stdio: "ignore",
@@ -8904,7 +9355,7 @@ function runSuite(dir, cmd, budgetSecs) {
8904
9355
  const secs = Math.round((Date.now() - t0) / 1e3);
8905
9356
  let r;
8906
9357
  try {
8907
- r = JSON.parse(readFileSync14(outFile, "utf8"));
9358
+ r = JSON.parse(readFileSync15(outFile, "utf8"));
8908
9359
  } catch {
8909
9360
  return { exit: null, secs };
8910
9361
  }
@@ -8948,14 +9399,37 @@ function runVerify(opts) {
8948
9399
  return 2;
8949
9400
  }
8950
9401
  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) {
9402
+ const verifierBackend = opts.verifierBackend ?? prepareVerifierBackend(policy.verify);
9403
+ const backendReport = () => verifierBackendReport(verifierBackend);
9404
+ if (!verifierBackend.available) {
9405
+ if (opts.json) {
9406
+ out2(JSON.stringify({
9407
+ verdict: "CANNOT_VERIFY",
9408
+ reason: "VERIFIER_BACKEND_UNAVAILABLE",
9409
+ verifier_backend: backendReport(),
9410
+ oracle_assurance: oracleAssuranceReport()
9411
+ }));
9412
+ } else {
9413
+ out2("verify: isolated verifier backend is unavailable \u2014 failing closed");
9414
+ out2("verify: " + (verifierBackend.reason ?? "unknown backend failure"));
9415
+ }
9416
+ return 2;
9417
+ }
9418
+ const isolated = verifierBackend.kind === "container";
9419
+ const dependencyEnvironment = isolated ? null : opts.dependencyEnvironment ?? discoverDependencyEnvironment(cwd, cmd);
9420
+ const dependencyReport = () => dependencyEnvironment ? dependencyEnvironmentReport(dependencyEnvironment) : {
9421
+ status: "verifier-owned",
9422
+ roots: [],
9423
+ image: verifierBackend.image
9424
+ };
9425
+ if (dependencyEnvironment?.status === "unattestable" && !opts.allowDepDrift) {
8954
9426
  if (opts.json) {
8955
9427
  out2(JSON.stringify({
8956
9428
  verdict: "CANNOT_VERIFY",
8957
9429
  reason: "DEPENDENCY_ENVIRONMENT_UNATTESTABLE",
8958
- dependency_environment: dependencyReport()
9430
+ verifier_backend: backendReport(),
9431
+ dependency_environment: dependencyReport(),
9432
+ oracle_assurance: oracleAssuranceReport()
8959
9433
  }));
8960
9434
  } else {
8961
9435
  out2("verify: dependency environment is not attestable \u2014 failing closed");
@@ -8963,7 +9437,9 @@ function runVerify(opts) {
8963
9437
  }
8964
9438
  return 2;
8965
9439
  }
8966
- const frozenNodeModules = dependencyEnvironment.roots.find((root) => root.kind === "node_modules")?.realPath ?? null;
9440
+ const frozenNodeModules = dependencyEnvironment ? dependencyEnvironment.roots.find((root) => root.kind === "node_modules")?.realPath ?? null : null;
9441
+ const checkDeps = () => dependencyEnvironment ? checkDependencyEnvironment(cwd, dependencyEnvironment) : { ok: true };
9442
+ const runStage = (dir) => isolated ? runContainerStage(verifierBackend, dir, cmd, budget) : runLocalSuite(dir, cmd, budget);
8967
9443
  let base;
8968
9444
  try {
8969
9445
  base = resolveBase(opts.base ?? "HEAD", cwd);
@@ -8971,15 +9447,15 @@ function runVerify(opts) {
8971
9447
  out2(`verify: cannot resolve base rev "${opts.base ?? "HEAD"}" \u2014 failing closed`);
8972
9448
  return 2;
8973
9449
  }
8974
- const visRoot = mkdtempSync2(join14(tmpdir2(), "tw-verify-vis-"));
8975
- const visDir = join14(visRoot, "t");
9450
+ const visRoot = mkdtempSync2(join15(tmpdir2(), "tw-verify-vis-"));
9451
+ const visDir = join15(visRoot, "t");
8976
9452
  const cleanup = (dirs) => {
8977
9453
  if (opts.keep) return;
8978
9454
  for (const d of dirs) rmSync3(d, { recursive: true, force: true });
8979
9455
  };
8980
9456
  const protectedIgnored = (rel) => isProtected(rel, policy);
8981
9457
  const treeBefore = treeFingerprint(cwd, protectedIgnored);
8982
- const dependencyAtEntry = checkDependencyEnvironment(cwd, dependencyEnvironment);
9458
+ const dependencyAtEntry = checkDeps();
8983
9459
  if (!dependencyAtEntry.ok && !opts.allowDepDrift) {
8984
9460
  cleanup([visRoot]);
8985
9461
  out2("verify: the frozen dependency environment changed before the visible suite ran \u2014");
@@ -8994,22 +9470,39 @@ function runVerify(opts) {
8994
9470
  out2(`verify: could not materialize (${e instanceof Error ? e.message : String(e)}) \u2014 failing closed`);
8995
9471
  return 2;
8996
9472
  }
8997
- const visible = runSuite(visDir, cmd, budget);
9473
+ const visible = runStage(visDir);
9474
+ if (visible.failure === "backend") {
9475
+ cleanup([visRoot]);
9476
+ if (opts.json) {
9477
+ out2(JSON.stringify({
9478
+ verdict: "CANNOT_VERIFY",
9479
+ reason: "VERIFIER_BACKEND_RUNTIME_FAILURE",
9480
+ stage: "visible",
9481
+ detail: visible.reason,
9482
+ verifier_backend: backendReport(),
9483
+ oracle_assurance: oracleAssuranceReport()
9484
+ }));
9485
+ } else {
9486
+ out2("verify: isolated verifier backend failed while running the visible stage \u2014 failing closed");
9487
+ if (visible.reason) out2("verify: " + visible.reason);
9488
+ }
9489
+ return 2;
9490
+ }
8998
9491
  if (treeFingerprint(cwd, protectedIgnored) !== treeBefore) {
8999
9492
  cleanup([visRoot]);
9000
9493
  out2("verify: the working tree changed while the visible suite was running \u2014 the pristine copy");
9001
9494
  out2("would be materialised from a tree the candidate just edited. Failing closed, not open.");
9002
9495
  return 2;
9003
9496
  }
9004
- const dependencyAfterVisible = checkDependencyEnvironment(cwd, dependencyEnvironment);
9497
+ const dependencyAfterVisible = checkDeps();
9005
9498
  if (!dependencyAfterVisible.ok && !opts.allowDepDrift) {
9006
9499
  cleanup([visRoot]);
9007
9500
  out2("verify: the frozen dependency environment changed while the visible suite was running \u2014");
9008
9501
  out2((dependencyAfterVisible.reason ?? "dependency identity changed") + ". Failing closed, not open.");
9009
9502
  return 2;
9010
9503
  }
9011
- const priRoot = mkdtempSync2(join14(tmpdir2(), "tw-verify-pri-"));
9012
- const priDir = join14(priRoot, "t");
9504
+ const priRoot = mkdtempSync2(join15(tmpdir2(), "tw-verify-pri-"));
9505
+ const priDir = join15(priRoot, "t");
9013
9506
  let restored = [];
9014
9507
  let removedAdded = 0;
9015
9508
  try {
@@ -9029,10 +9522,27 @@ function runVerify(opts) {
9029
9522
  return 2;
9030
9523
  }
9031
9524
  const overlayBefore = overlayDigest(priDir, restored);
9032
- const pristine = runSuite(priDir, cmd, budget);
9525
+ const pristine = runStage(priDir);
9526
+ if (pristine.failure === "backend") {
9527
+ cleanup([visRoot, priRoot]);
9528
+ if (opts.json) {
9529
+ out2(JSON.stringify({
9530
+ verdict: "CANNOT_VERIFY",
9531
+ reason: "VERIFIER_BACKEND_RUNTIME_FAILURE",
9532
+ stage: "pristine",
9533
+ detail: pristine.reason,
9534
+ verifier_backend: backendReport(),
9535
+ oracle_assurance: oracleAssuranceReport()
9536
+ }));
9537
+ } else {
9538
+ out2("verify: isolated verifier backend failed while running the pristine stage \u2014 failing closed");
9539
+ if (pristine.reason) out2("verify: " + pristine.reason);
9540
+ }
9541
+ return 2;
9542
+ }
9033
9543
  const overlayMoved = overlayDigest(priDir, restored) !== overlayBefore;
9034
9544
  const treeMoved = treeFingerprint(cwd, protectedIgnored) !== treeBefore;
9035
- const dependencyAfterPristine = checkDependencyEnvironment(cwd, dependencyEnvironment);
9545
+ const dependencyAfterPristine = checkDeps();
9036
9546
  const depsMoved = !dependencyAfterPristine.ok && !opts.allowDepDrift;
9037
9547
  cleanup([visRoot, priRoot]);
9038
9548
  if (overlayMoved || treeMoved || depsMoved) {
@@ -9072,7 +9582,9 @@ function runVerify(opts) {
9072
9582
  pristine: { exit: pristine.exit, secs: pristine.secs },
9073
9583
  protected_restored: restored.length,
9074
9584
  added_protected_removed: removedAdded,
9585
+ verifier_backend: backendReport(),
9075
9586
  dependency_environment: dependencyReport(),
9587
+ oracle_assurance: oracleAssuranceReport(),
9076
9588
  ...signedOff ? { oob_signoff: signedOff } : {},
9077
9589
  ...opts.keep ? { visible_dir: visDir, pristine_dir: priDir } : {}
9078
9590
  })
@@ -9085,7 +9597,13 @@ function runVerify(opts) {
9085
9597
  BUDGET_EXCEEDED: `budget exceeded (${budget}s): could not verify \u2014 failing closed, not open.`
9086
9598
  };
9087
9599
  out2(`tamperward verify \u2014 ${lines[verdict2]}`);
9088
- out2(`dependency environment: ${dependencyEnvironmentSummary(dependencyEnvironment)}` + (opts.allowDepDrift && dependencyEnvironment.status === "unattestable" ? " (operator override)" : ""));
9600
+ out2(`verifier backend: ${verifierBackendSummary(verifierBackend)}`);
9601
+ out2(
9602
+ "oracle assurance: suite-exit-only (candidate source executes inside the suite process; execution-domain isolation is not semantic/oracle isolation)"
9603
+ );
9604
+ out2(
9605
+ dependencyEnvironment ? `dependency environment: ${dependencyEnvironmentSummary(dependencyEnvironment)}` + (opts.allowDepDrift && dependencyEnvironment.status === "unattestable" ? " (operator override)" : "") : `dependency environment: verifier-owned by ${verifierBackend.image ?? "isolated image"}`
9606
+ );
9089
9607
  if (signedOff)
9090
9608
  out2(
9091
9609
  `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 +9648,9 @@ function parseVerify(args) {
9130
9648
  }
9131
9649
 
9132
9650
  // 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";
9651
+ import { execFileSync as execFileSync9, spawnSync as spawnSync3 } from "node:child_process";
9652
+ import { readdirSync as readdirSync4, readFileSync as readFileSync16, readlinkSync as readlinkSync4, realpathSync as realpathSync6 } from "node:fs";
9653
+ import { resolve as resolve7 } from "node:path";
9136
9654
  var out = (s) => process.stdout.write(s + "\n");
9137
9655
  var err = (s) => process.stderr.write(s + "\n");
9138
9656
  function git3(args, cwd) {
@@ -9140,7 +9658,7 @@ function git3(args, cwd) {
9140
9658
  }
9141
9659
  function nowTicks() {
9142
9660
  try {
9143
- return Math.floor(parseFloat(readFileSync15("/proc/uptime", "utf8").split(" ")[0]) * 100);
9661
+ return Math.floor(parseFloat(readFileSync16("/proc/uptime", "utf8").split(" ")[0]) * 100);
9144
9662
  } catch {
9145
9663
  return Number.POSITIVE_INFINITY;
9146
9664
  }
@@ -9149,7 +9667,7 @@ function survivorsHoldingTree(cwd, spawnedAfterTicks) {
9149
9667
  const out2 = [];
9150
9668
  let real;
9151
9669
  try {
9152
- real = realpathSync5(cwd);
9670
+ real = realpathSync6(cwd);
9153
9671
  } catch {
9154
9672
  return out2;
9155
9673
  }
@@ -9163,7 +9681,7 @@ function survivorsHoldingTree(cwd, spawnedAfterTicks) {
9163
9681
  const n = Number(pid);
9164
9682
  if (n === process.pid) continue;
9165
9683
  try {
9166
- const stat = readFileSync15(`/proc/${pid}/stat`, "utf8");
9684
+ const stat = readFileSync16(`/proc/${pid}/stat`, "utf8");
9167
9685
  const startTicks = Number(stat.slice(stat.lastIndexOf(")") + 2).split(" ")[19]);
9168
9686
  if (!Number.isFinite(startTicks) || startTicks < spawnedAfterTicks) continue;
9169
9687
  } catch {
@@ -9178,7 +9696,7 @@ function survivorsHoldingTree(cwd, spawnedAfterTicks) {
9178
9696
  for (const link of procLinks) {
9179
9697
  try {
9180
9698
  const raw = readlinkSync4(link).replace(/ \(deleted\)$/, "");
9181
- const held = raw.startsWith("/") ? resolve6(raw) : raw;
9699
+ const held = raw.startsWith("/") ? resolve7(raw) : raw;
9182
9700
  if (held === real || held.startsWith(real + "/")) {
9183
9701
  holds = true;
9184
9702
  break;
@@ -9191,7 +9709,7 @@ function survivorsHoldingTree(cwd, spawnedAfterTicks) {
9191
9709
  return out2;
9192
9710
  }
9193
9711
  function runEnvelope(opts) {
9194
- const cwd = resolve6(opts.cwd ?? process.cwd());
9712
+ const cwd = resolve7(opts.cwd ?? process.cwd());
9195
9713
  if (!opts.argv.length) {
9196
9714
  err("tamperward run: no agent command given (usage: tamperward run [options] -- <command...>)");
9197
9715
  return 2;
@@ -9227,18 +9745,37 @@ function runEnvelope(opts) {
9227
9745
  return 2;
9228
9746
  }
9229
9747
  }
9230
- const dependencyEnvironment = discoverDependencyEnvironment(cwd, frozenCmd);
9748
+ const verifierBackend = prepareVerifierBackend(frozenPolicy.verify);
9231
9749
  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.)");
9750
+ out(`tamperward run \u2014 verifier backend: ${verifierBackendSummary(verifierBackend)}`);
9751
+ if (!verifierBackend.available) {
9752
+ err("tamperward run: the requested verifier backend cannot be established \u2014");
9753
+ err(`${verifierBackend.reason ?? "unknown backend failure"}. Failing closed before the agent starts.`);
9754
+ return 2;
9755
+ }
9756
+ if (verifierBackend.kind === "container") {
9757
+ err("tamperward run: container verification requires a frozen candidate handoff from an");
9758
+ err("agent domain that cannot control the verifier engine. `run` executes the agent under");
9759
+ err("this host identity, so it cannot honestly provide that separation. Failing closed");
9760
+ err("before the agent starts; use standalone `tamperward verify` in trusted CI or after");
9761
+ err("an externally isolated agent produces the frozen candidate artifact.");
9237
9762
  return 2;
9238
9763
  }
9764
+ const dependencyEnvironment = verifierBackend.kind === "local" ? discoverDependencyEnvironment(cwd, frozenCmd) : null;
9765
+ if (dependencyEnvironment) {
9766
+ out(`tamperward run \u2014 dependency environment: ${dependencyEnvironmentSummary(dependencyEnvironment)}`);
9767
+ if (dependencyEnvironment.status === "unattestable" && !opts.allowDepDrift) {
9768
+ err("tamperward run: the verifier dependency environment cannot be attested \u2014");
9769
+ err(`${dependencyEnvironment.reason ?? "unknown dependency environment"}. Failing closed before the agent starts.`);
9770
+ err("(Pass --allow-dep-drift only if you explicitly own this dependency-integrity risk.)");
9771
+ return 2;
9772
+ }
9773
+ } else {
9774
+ out(`tamperward run \u2014 dependency environment: verifier-owned by ${verifierBackend.image ?? "isolated image"}`);
9775
+ }
9239
9776
  const rewritesBefore = objectRewriteState(cwd);
9240
9777
  const spawnTicks = nowTicks();
9241
- const agent = spawnSync2(opts.argv[0], opts.argv.slice(1), { cwd, stdio: "inherit" });
9778
+ const agent = spawnSync3(opts.argv[0], opts.argv.slice(1), { cwd, stdio: "inherit" });
9242
9779
  const agentExit = agent.status ?? 1;
9243
9780
  if (objectRewriteState(cwd) !== rewritesBefore) {
9244
9781
  err("tamperward run: git object-replacement state changed during this run \u2014 a replace ref, graft");
@@ -9248,7 +9785,7 @@ function runEnvelope(opts) {
9248
9785
  tamperward run \u2014 agent exit ${agentExit}; OBJECT_REWRITE \u2192 ENFORCEMENT_FAILED`);
9249
9786
  return 1;
9250
9787
  }
9251
- const dependencyBeforeAdjudication = checkDependencyEnvironment(cwd, dependencyEnvironment);
9788
+ const dependencyBeforeAdjudication = dependencyEnvironment ? checkDependencyEnvironment(cwd, dependencyEnvironment) : { ok: true };
9252
9789
  if (!dependencyBeforeAdjudication.ok) {
9253
9790
  err("tamperward run: the frozen dependency environment changed before adjudication began \u2014");
9254
9791
  err(`${dependencyBeforeAdjudication.reason ?? "dependency identity changed"}.`);
@@ -9260,7 +9797,7 @@ tamperward run \u2014 agent exit ${agentExit}; DEPENDENCY_DRIFT \u2192 ENFORCEME
9260
9797
  err("(--allow-dep-drift: proceeding anyway, on the operator's judgement.)");
9261
9798
  }
9262
9799
  const head = git3(["rev-parse", "HEAD"], cwd).trim();
9263
- const isAncestor = spawnSync2("git", ["merge-base", "--is-ancestor", base, head], { cwd, env: trustedGitEnv() });
9800
+ const isAncestor = spawnSync3("git", ["merge-base", "--is-ancestor", base, head], { cwd, env: trustedGitEnv() });
9264
9801
  if (isAncestor.status !== 0) {
9265
9802
  err(`tamperward run: HEAD ${head.slice(0, 10)} is not a descendant of the trusted base ${base.slice(0, 10)} \u2014`);
9266
9803
  err("the agent rewrote history out from under the anchor. That is the finding.");
@@ -9285,8 +9822,9 @@ tamperward run \u2014 agent exit ${agentExit}; HISTORY_REWRITE \u2192 ENFORCEMEN
9285
9822
  cmd: frozenCmd,
9286
9823
  budget: frozenBudget,
9287
9824
  policyOverride: frozenPolicy,
9288
- dependencyEnvironment,
9289
- allowDepDrift: opts.allowDepDrift
9825
+ ...dependencyEnvironment ? { dependencyEnvironment } : {},
9826
+ allowDepDrift: opts.allowDepDrift,
9827
+ verifierBackend
9290
9828
  });
9291
9829
  if (opts.settle && opts.settle > 0) {
9292
9830
  try {
@@ -9299,8 +9837,8 @@ tamperward run \u2014 agent exit ${agentExit}; HISTORY_REWRITE \u2192 ENFORCEMEN
9299
9837
  err("tamperward run: the working tree changed while it was being adjudicated \u2014");
9300
9838
  err("the verdict below would describe a tree that no longer exists.");
9301
9839
  }
9302
- const dependencyAfter = checkDependencyEnvironment(cwd, dependencyEnvironment);
9303
- const depsDrifted = !opts.allowDepDrift && !dependencyAfter.ok;
9840
+ const dependencyAfter = dependencyEnvironment ? checkDependencyEnvironment(cwd, dependencyEnvironment) : { ok: true };
9841
+ const depsDrifted = Boolean(dependencyEnvironment) && !opts.allowDepDrift && !dependencyAfter.ok;
9304
9842
  if (depsDrifted) {
9305
9843
  err("tamperward run: the frozen dependency environment changed during this run \u2014");
9306
9844
  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.1",
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",