tamperward 2.11.1 → 2.11.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -262,7 +262,14 @@ verify:
262
262
  local Docker daemon; Tamperward never pulls during adjudication. The materialised
263
263
  candidate/pristine tree is mounted read-only, the image owns runtime/dependencies,
264
264
  network is disabled, HOME/tmp are private, and optional suite output belongs in
265
- `$TAMPERWARD_OUTPUT_DIR` (`/workspace-out`). Images that declare Dockerfile
265
+ `$TAMPERWARD_OUTPUT_DIR` (`/workspace-out`). The isolated verifier also applies a
266
+ fixed host-protection envelope: **2 GiB memory, no additional swap, 2 CPUs and 256
267
+ PIDs**, in addition to the configured wall-clock budget. Those limits are included in
268
+ the JSON backend report. Docker-confirmed memory OOM is `CANNOT_VERIFY` with
269
+ `VERIFIER_RESOURCE_EXHAUSTED`, never a suite failure; an exit such as 137 without
270
+ `OOMKilled=true` remains the suite's own exit. v2.11.2 deliberately has no environment
271
+ variable tuning knob for these ceilings because candidate-controlled CI/env must not
272
+ weaken verifier containment. Images that declare Dockerfile
266
273
  `VOLUME` paths are refused: Docker mounts those paths writable even with
267
274
  `--read-only`, which would undermine the immutable verifier-image boundary. Image
268
275
  `ENTRYPOINT` is also overridden; the pinned image supplies the runtime/dependencies,
package/dist/cli/index.js CHANGED
@@ -8581,6 +8581,13 @@ import { createHash as createHash9, randomUUID } from "node:crypto";
8581
8581
  import { spawnSync } from "node:child_process";
8582
8582
  import { accessSync, constants as constants2, readFileSync as readFileSync14, realpathSync as realpathSync4, statSync as statSync5 } from "node:fs";
8583
8583
  import { delimiter as delimiter2, dirname as dirname6, join as join14, resolve as resolve5 } from "node:path";
8584
+ var DEFAULT_CONTAINER_RESOURCE_LIMITS = Object.freeze({
8585
+ memoryBytes: 2 * 1024 * 1024 * 1024,
8586
+ // Docker interprets memory-swap == memory as no additional swap allowance.
8587
+ memorySwapBytes: 2 * 1024 * 1024 * 1024,
8588
+ cpus: 2,
8589
+ pids: 256
8590
+ });
8584
8591
  var DIGEST_IMAGE = /^(?!-)[^\s@]+@sha256:[0-9a-f]{64}$/i;
8585
8592
  var DEFAULT_DOCKER_HOST = "unix:///var/run/docker.sock";
8586
8593
  function identity() {
@@ -8808,7 +8815,8 @@ function prepareVerifierBackend(verify) {
8808
8815
  engine: "docker",
8809
8816
  enginePath,
8810
8817
  engineSha256: fileSha256(enginePath),
8811
- daemonHost
8818
+ daemonHost,
8819
+ resources: { ...DEFAULT_CONTAINER_RESOURCE_LIMITS }
8812
8820
  };
8813
8821
  }
8814
8822
  function verifierBackendReport(backend) {
@@ -8818,6 +8826,14 @@ function verifierBackendReport(backend) {
8818
8826
  available: backend.available,
8819
8827
  ...backend.image ? { image: backend.image } : {},
8820
8828
  ...backend.engine ? { engine: backend.engine } : {},
8829
+ ...backend.kind === "container" ? {
8830
+ resources: {
8831
+ memory_bytes: (backend.resources ?? DEFAULT_CONTAINER_RESOURCE_LIMITS).memoryBytes,
8832
+ memory_swap_bytes: (backend.resources ?? DEFAULT_CONTAINER_RESOURCE_LIMITS).memorySwapBytes,
8833
+ cpus: (backend.resources ?? DEFAULT_CONTAINER_RESOURCE_LIMITS).cpus,
8834
+ pids: (backend.resources ?? DEFAULT_CONTAINER_RESOURCE_LIMITS).pids
8835
+ }
8836
+ } : {},
8821
8837
  ...backend.reason ? { reason: backend.reason } : {}
8822
8838
  };
8823
8839
  }
@@ -8829,7 +8845,8 @@ function verifierBackendSummary(backend) {
8829
8845
  if (!backend.available) {
8830
8846
  return `container (isolated; unavailable: ${backend.reason ?? "unknown reason"}; image ${identity2})`;
8831
8847
  }
8832
- return `container (isolated via Docker; image ${identity2})`;
8848
+ const resources = backend.resources ?? DEFAULT_CONTAINER_RESOURCE_LIMITS;
8849
+ return `container (isolated via Docker; image ${identity2}; ${Math.round(resources.memoryBytes / (1024 * 1024))} MiB memory, no extra swap, ${resources.cpus} CPUs, ${resources.pids} PIDs)`;
8833
8850
  }
8834
8851
  function cleanupContainer(backend, name) {
8835
8852
  if (!backend.enginePath || !backend.daemonHost || !engineStable(backend)) return;
@@ -8844,6 +8861,7 @@ function cleanupContainer(backend, name) {
8844
8861
  }
8845
8862
  }
8846
8863
  function containerRunArgs(input) {
8864
+ const resources = input.resources ?? DEFAULT_CONTAINER_RESOURCE_LIMITS;
8847
8865
  return [
8848
8866
  "run",
8849
8867
  "--name",
@@ -8860,8 +8878,14 @@ function containerRunArgs(input) {
8860
8878
  "ALL",
8861
8879
  "--security-opt",
8862
8880
  "no-new-privileges",
8881
+ "--memory",
8882
+ String(resources.memoryBytes),
8883
+ "--memory-swap",
8884
+ String(resources.memorySwapBytes),
8885
+ "--cpus",
8886
+ String(resources.cpus),
8863
8887
  "--pids-limit",
8864
- "256",
8888
+ String(resources.pids),
8865
8889
  "--user",
8866
8890
  `${input.uid}:${input.gid}`,
8867
8891
  // Frozen candidate/pristine input is immutable in the verifier. A suite
@@ -8895,6 +8919,26 @@ function containerRunArgs(input) {
8895
8919
  input.command
8896
8920
  ];
8897
8921
  }
8922
+ function containerStateResult(state, secs) {
8923
+ if (state?.OOMKilled) {
8924
+ return {
8925
+ exit: null,
8926
+ secs,
8927
+ failure: "resource",
8928
+ resource: "memory",
8929
+ reason: "verifier container exceeded its memory limit and was OOM-killed"
8930
+ };
8931
+ }
8932
+ if (!state || state.Status !== "exited" || typeof state.ExitCode !== "number" || (state.Error ?? "") !== "") {
8933
+ return {
8934
+ exit: null,
8935
+ secs,
8936
+ failure: "backend",
8937
+ reason: state?.Error ? `verifier container runtime error: ${state.Error}` : "Docker did not provide a trustworthy exited-container state"
8938
+ };
8939
+ }
8940
+ return { exit: state.ExitCode, secs };
8941
+ }
8898
8942
  function runContainerStage(backend, dir, command, budgetSecs) {
8899
8943
  const t0 = Date.now();
8900
8944
  if (backend.kind !== "container" || !backend.available || !backend.image || !backend.enginePath || !backend.engineSha256 || !backend.daemonHost || !engineStable(backend)) {
@@ -8914,7 +8958,8 @@ function runContainerStage(backend, dir, command, budgetSecs) {
8914
8958
  workspace: resolve5(dir),
8915
8959
  command,
8916
8960
  uid,
8917
- gid
8961
+ gid,
8962
+ resources: backend.resources ?? DEFAULT_CONTAINER_RESOURCE_LIMITS
8918
8963
  });
8919
8964
  try {
8920
8965
  const r = spawnSync(
@@ -8970,15 +9015,7 @@ function runContainerStage(backend, dir, command, budgetSecs) {
8970
9015
  }
8971
9016
  }
8972
9017
  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 };
9018
+ return containerStateResult(state, secs);
8982
9019
  } catch (e) {
8983
9020
  cleanupContainer(backend, name);
8984
9021
  return {
@@ -9471,19 +9508,23 @@ function runVerify(opts) {
9471
9508
  return 2;
9472
9509
  }
9473
9510
  const visible = runStage(visDir);
9474
- if (visible.failure === "backend") {
9511
+ if (visible.failure === "backend" || visible.failure === "resource") {
9475
9512
  cleanup([visRoot]);
9513
+ const exhausted = visible.failure === "resource";
9476
9514
  if (opts.json) {
9477
9515
  out2(JSON.stringify({
9478
9516
  verdict: "CANNOT_VERIFY",
9479
- reason: "VERIFIER_BACKEND_RUNTIME_FAILURE",
9517
+ reason: exhausted ? "VERIFIER_RESOURCE_EXHAUSTED" : "VERIFIER_BACKEND_RUNTIME_FAILURE",
9480
9518
  stage: "visible",
9519
+ ...visible.resource ? { resource: visible.resource } : {},
9481
9520
  detail: visible.reason,
9482
9521
  verifier_backend: backendReport(),
9483
9522
  oracle_assurance: oracleAssuranceReport()
9484
9523
  }));
9485
9524
  } else {
9486
- out2("verify: isolated verifier backend failed while running the visible stage \u2014 failing closed");
9525
+ out2(
9526
+ exhausted ? "verify: isolated verifier resource envelope was exhausted during the visible stage \u2014 failing closed" : "verify: isolated verifier backend failed while running the visible stage \u2014 failing closed"
9527
+ );
9487
9528
  if (visible.reason) out2("verify: " + visible.reason);
9488
9529
  }
9489
9530
  return 2;
@@ -9523,19 +9564,23 @@ function runVerify(opts) {
9523
9564
  }
9524
9565
  const overlayBefore = overlayDigest(priDir, restored);
9525
9566
  const pristine = runStage(priDir);
9526
- if (pristine.failure === "backend") {
9567
+ if (pristine.failure === "backend" || pristine.failure === "resource") {
9527
9568
  cleanup([visRoot, priRoot]);
9569
+ const exhausted = pristine.failure === "resource";
9528
9570
  if (opts.json) {
9529
9571
  out2(JSON.stringify({
9530
9572
  verdict: "CANNOT_VERIFY",
9531
- reason: "VERIFIER_BACKEND_RUNTIME_FAILURE",
9573
+ reason: exhausted ? "VERIFIER_RESOURCE_EXHAUSTED" : "VERIFIER_BACKEND_RUNTIME_FAILURE",
9532
9574
  stage: "pristine",
9575
+ ...pristine.resource ? { resource: pristine.resource } : {},
9533
9576
  detail: pristine.reason,
9534
9577
  verifier_backend: backendReport(),
9535
9578
  oracle_assurance: oracleAssuranceReport()
9536
9579
  }));
9537
9580
  } else {
9538
- out2("verify: isolated verifier backend failed while running the pristine stage \u2014 failing closed");
9581
+ out2(
9582
+ exhausted ? "verify: isolated verifier resource envelope was exhausted during the pristine stage \u2014 failing closed" : "verify: isolated verifier backend failed while running the pristine stage \u2014 failing closed"
9583
+ );
9539
9584
  if (pristine.reason) out2("verify: " + pristine.reason);
9540
9585
  }
9541
9586
  return 2;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tamperward",
3
- "version": "2.11.1",
3
+ "version": "2.11.2",
4
4
  "description": "The deterministic agent-integrity gate. One ruleset, evaluated on the actual diff/commands as a verdict, enforced everywhere a change can be made.",
5
5
  "license": "Apache-2.0",
6
6
  "author": "hexrift",