tamperward 1.8.0 → 1.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/cli/index.js +192 -1
  2. package/package.json +1 -1
package/dist/cli/index.js CHANGED
@@ -1014,6 +1014,24 @@ function policyWeakening(before, after) {
1014
1014
  if (be.requiredFor.includes("block") && !new Set(ae.requiredFor).has("block")) {
1015
1015
  reasons.push("sign-off no longer required for blocking findings");
1016
1016
  }
1017
+ const bv = b.verify;
1018
+ const av = a.verify;
1019
+ if (bv?.command) {
1020
+ if (!av?.command) {
1021
+ reasons.push("verify block removed \u2014 pristine-suite re-execution disabled");
1022
+ } else {
1023
+ if (av.command !== bv.command) {
1024
+ reasons.push(
1025
+ `verify.command changed ("${bv.command}" \u2192 "${av.command}") \u2014 the re-executed suite is no longer the one that was configured`
1026
+ );
1027
+ }
1028
+ const bb = bv.budget ?? 300;
1029
+ const ab = av.budget ?? 300;
1030
+ if (ab < bb) {
1031
+ reasons.push(`verify.budget lowered ${bb}s \u2192 ${ab}s \u2014 starving the budget turns "cannot verify" into a pressure point`);
1032
+ }
1033
+ }
1034
+ }
1017
1035
  return reasons;
1018
1036
  }
1019
1037
 
@@ -1511,7 +1529,8 @@ function parsePolicy(raw) {
1511
1529
  signoff: {
1512
1530
  requiredFor: r.signoff?.required_for ?? r.signoff?.requiredFor ?? base.signoff.requiredFor,
1513
1531
  ledger: r.signoff?.ledger ?? base.signoff.ledger
1514
- }
1532
+ },
1533
+ ...r.verify?.command ? { verify: { command: r.verify.command, budget: r.verify.budget ?? 300 } } : {}
1515
1534
  };
1516
1535
  }
1517
1536
  function parseOrThrow(src, where) {
@@ -2728,6 +2747,171 @@ tamperward init: ${changed} change(s) applied. Commit them so the gate travels w
2728
2747
  return 0;
2729
2748
  }
2730
2749
 
2750
+ // src/cli/verify.ts
2751
+ import { execFileSync as execFileSync4, spawnSync } from "node:child_process";
2752
+ import { chmodSync as chmodSync2, cpSync, mkdirSync as mkdirSync6, mkdtempSync as mkdtempSync2, rmSync as rmSync2, statSync as statSync5, symlinkSync, writeFileSync as writeFileSync6, existsSync as existsSync8 } from "node:fs";
2753
+ import { tmpdir as tmpdir2 } from "node:os";
2754
+ import { dirname as dirname4, join as join9 } from "node:path";
2755
+ var OVERLAY_CLASSES = ["tests", "snapshots", "config"];
2756
+ function git2(args, cwd) {
2757
+ return execFileSync4("git", args, { cwd, encoding: "utf8", maxBuffer: 1 << 28 });
2758
+ }
2759
+ function resolveBase(base, cwd) {
2760
+ const rev = git2(["rev-parse", "--verify", `${base}^{commit}`], cwd).trim();
2761
+ try {
2762
+ return git2(["merge-base", rev, "HEAD"], cwd).trim();
2763
+ } catch {
2764
+ return rev;
2765
+ }
2766
+ }
2767
+ function materialize(cwd, dest) {
2768
+ const listed = git2(["ls-files", "-z", "--cached", "--others", "--exclude-standard"], cwd).split("\0").filter(Boolean);
2769
+ for (const rel of listed) {
2770
+ const src = join9(cwd, rel);
2771
+ let st;
2772
+ try {
2773
+ st = statSync5(src);
2774
+ } catch {
2775
+ continue;
2776
+ }
2777
+ if (!st.isFile()) continue;
2778
+ const out = join9(dest, rel);
2779
+ mkdirSync6(dirname4(out), { recursive: true });
2780
+ cpSync(src, out);
2781
+ chmodSync2(out, st.mode);
2782
+ }
2783
+ const nm = join9(cwd, "node_modules");
2784
+ if (existsSync8(nm)) symlinkSync(nm, join9(dest, "node_modules"), "dir");
2785
+ }
2786
+ function overlayPristine(cwd, base, dest, policy) {
2787
+ const atBase = git2(["ls-tree", "-r", "--name-only", "-z", base], cwd).split("\0").filter(Boolean);
2788
+ const isOverlay = (p) => OVERLAY_CLASSES.some((c) => isProtected(p, policy, c));
2789
+ let restored = 0;
2790
+ const baseProtected = /* @__PURE__ */ new Set();
2791
+ for (const rel of atBase) {
2792
+ if (!isOverlay(rel)) continue;
2793
+ baseProtected.add(rel);
2794
+ const content = execFileSync4("git", ["show", `${base}:${rel}`], { cwd, maxBuffer: 1 << 28 });
2795
+ const out = join9(dest, rel);
2796
+ mkdirSync6(dirname4(out), { recursive: true });
2797
+ writeFileSync6(out, content);
2798
+ restored++;
2799
+ }
2800
+ let kept = 0;
2801
+ const inCopy = git2(["ls-files", "-z", "--cached", "--others", "--exclude-standard"], cwd).split("\0").filter(Boolean);
2802
+ for (const rel of inCopy) if (isOverlay(rel) && !baseProtected.has(rel)) kept++;
2803
+ return [restored, kept];
2804
+ }
2805
+ function runSuite(dir, cmd, budgetSecs) {
2806
+ const t0 = Date.now();
2807
+ const r = spawnSync("sh", ["-c", cmd], {
2808
+ cwd: dir,
2809
+ stdio: "ignore",
2810
+ timeout: budgetSecs * 1e3,
2811
+ killSignal: "SIGKILL"
2812
+ });
2813
+ const secs = Math.round((Date.now() - t0) / 1e3);
2814
+ if (r.error && r.error.code === "ETIMEDOUT") return { exit: null, secs };
2815
+ if (r.status === null && r.signal) return { exit: null, secs };
2816
+ return { exit: r.status ?? 2, secs };
2817
+ }
2818
+ function runVerify(opts) {
2819
+ const cwd = opts.cwd ?? process.cwd();
2820
+ const out = (s) => void process.stdout.write(s + "\n");
2821
+ let policy;
2822
+ try {
2823
+ policy = loadPolicy(cwd);
2824
+ } catch (e) {
2825
+ out(`verify: cannot load policy (${e instanceof Error ? e.message : String(e)}) \u2014 failing closed`);
2826
+ return 2;
2827
+ }
2828
+ const cmd = opts.cmd ?? policy.verify?.command;
2829
+ if (!cmd) {
2830
+ out("verify: no suite command \u2014 set policy `verify: { command: ... }` or pass --cmd");
2831
+ return 2;
2832
+ }
2833
+ const budget = opts.budget ?? policy.verify?.budget ?? 300;
2834
+ let base;
2835
+ try {
2836
+ base = resolveBase(opts.base ?? "HEAD", cwd);
2837
+ } catch {
2838
+ out(`verify: cannot resolve base rev "${opts.base ?? "HEAD"}" \u2014 failing closed`);
2839
+ return 2;
2840
+ }
2841
+ const work = mkdtempSync2(join9(tmpdir2(), "tw-verify-"));
2842
+ const visDir = join9(work, "visible");
2843
+ const priDir = join9(work, "pristine");
2844
+ let restored = 0;
2845
+ let kept = 0;
2846
+ try {
2847
+ mkdirSync6(visDir);
2848
+ mkdirSync6(priDir);
2849
+ materialize(cwd, visDir);
2850
+ materialize(cwd, priDir);
2851
+ [restored, kept] = overlayPristine(cwd, base, priDir, policy);
2852
+ } catch (e) {
2853
+ if (!opts.keep) rmSync2(work, { recursive: true, force: true });
2854
+ out(`verify: could not materialize (${e instanceof Error ? e.message : String(e)}) \u2014 failing closed`);
2855
+ return 2;
2856
+ }
2857
+ const visible = runSuite(visDir, cmd, budget);
2858
+ const pristine = runSuite(priDir, cmd, budget);
2859
+ if (!opts.keep) rmSync2(work, { recursive: true, force: true });
2860
+ let verdict2;
2861
+ let code;
2862
+ if (visible.exit === null || pristine.exit === null) {
2863
+ verdict2 = "BUDGET_EXCEEDED";
2864
+ code = 2;
2865
+ } else if (visible.exit === 0 && pristine.exit === 0) {
2866
+ verdict2 = "VERIFIED";
2867
+ code = 0;
2868
+ } else if (visible.exit === 0) {
2869
+ verdict2 = "MASKED_FAILURE";
2870
+ code = 1;
2871
+ } else {
2872
+ verdict2 = "SUITE_RED";
2873
+ code = 1;
2874
+ }
2875
+ if (opts.json) {
2876
+ out(
2877
+ JSON.stringify({
2878
+ verdict: verdict2,
2879
+ base,
2880
+ command: cmd,
2881
+ budget_secs: budget,
2882
+ visible: { exit: visible.exit, secs: visible.secs },
2883
+ pristine: { exit: pristine.exit, secs: pristine.secs },
2884
+ protected_restored: restored,
2885
+ added_protected_kept: kept,
2886
+ ...opts.keep ? { workdir: work } : {}
2887
+ })
2888
+ );
2889
+ } else {
2890
+ const lines = {
2891
+ VERIFIED: `verified: the suite passes, and still passes with all ${restored} protected files restored from ${base.slice(0, 10)}.`,
2892
+ MASKED_FAILURE: `MASKED FAILURE: the visible suite passes, but with the ${restored} protected files restored from ${base.slice(0, 10)} it FAILS (exit ${pristine.exit}). Something weakened the checks; the code does not pass the original suite.`,
2893
+ SUITE_RED: `suite red: the visible suite fails (exit ${visible.exit}) \u2014 fix the code first (pristine exit ${pristine.exit}).`,
2894
+ BUDGET_EXCEEDED: `budget exceeded (${budget}s): could not verify \u2014 failing closed, not open.`
2895
+ };
2896
+ out(`tamperward verify \u2014 ${lines[verdict2]}`);
2897
+ if (kept > 0) out(`(${kept} protected file(s) added since ${base.slice(0, 10)} were kept in the pristine run.)`);
2898
+ }
2899
+ return code;
2900
+ }
2901
+ function parseVerify(args) {
2902
+ const o = {};
2903
+ for (let i = 0; i < args.length; i++) {
2904
+ const a = args[i];
2905
+ if (a === "--base" && args[i + 1]) o.base = args[++i];
2906
+ else if (a === "--cmd" && args[i + 1]) o.cmd = args[++i];
2907
+ else if (a === "--budget" && args[i + 1]) o.budget = Number(args[++i]);
2908
+ else if (a === "--json") o.json = true;
2909
+ else if (a === "--keep") o.keep = true;
2910
+ else if (a === "--cwd" && args[i + 1]) o.cwd = args[++i];
2911
+ }
2912
+ return o;
2913
+ }
2914
+
2731
2915
  // src/cli/index.ts
2732
2916
  function parseAllow(args) {
2733
2917
  const o = {};
@@ -2803,6 +2987,11 @@ Formats:
2803
2987
  tamperward watch [--dir D] [--log F] filesystem-event observer daemon: records
2804
2988
  protected-file events so the sweep can
2805
2989
  observe supported transient effects
2990
+ tamperward verify [--base R] [--cmd C] pristine-suite re-execution: run the suite
2991
+ [--budget S] [--json] [--keep] as-is AND with protected files restored
2992
+ from the trusted base; a visible-green /
2993
+ pristine-red result is a MASKED FAILURE
2994
+ (exit 1); cannot-verify fails closed (2)
2806
2995
  tamperward allow <rule> --reason "..." record a human sign-off (local audit ledger)
2807
2996
  tamperward init [--dry-run] wire the policy file plus every
2808
2997
  enforcement point: Claude Code hooks,
@@ -2826,6 +3015,8 @@ function main(argv2) {
2826
3015
  return runAllow(parseAllow(rest));
2827
3016
  case "init":
2828
3017
  return runInit(parseInit(rest));
3018
+ case "verify":
3019
+ return runVerify(parseVerify(rest));
2829
3020
  case void 0:
2830
3021
  case "-h":
2831
3022
  case "--help":
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tamperward",
3
- "version": "1.8.0",
3
+ "version": "1.9.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",