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.
- package/dist/cli/index.js +192 -1
- 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.
|
|
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",
|