tamperward 1.11.0 → 1.12.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 +123 -16
- package/package.json +1 -1
package/dist/cli/index.js
CHANGED
|
@@ -463,7 +463,20 @@ function defaultPolicy(version = 1) {
|
|
|
463
463
|
// tap-snapshots/** added from Phase-3 evidence: node-tar's recorded expectations
|
|
464
464
|
// live there as .cjs and were rewritten unseen (PHASE3.md §4).
|
|
465
465
|
snapshots: ["**/*.snap", "**/__snapshots__/**", "**/tap-snapshots/**", "**/golden/**", "**/*.golden.*"],
|
|
466
|
-
|
|
466
|
+
// The wiring IS the enforcement: settings.json carries the PreToolUse and
|
|
467
|
+
// Stop hooks, and a pre-commit config carries the commit-time gate. These
|
|
468
|
+
// were installed by `init` and protected by nothing, so editing them out
|
|
469
|
+
// fired no rule at all. (.git/hooks/** is outside every git view and so
|
|
470
|
+
// cannot be covered here — noted in SECURITY-ENVELOPE.md.) (P1-4, review.)
|
|
471
|
+
hooks: [
|
|
472
|
+
".husky/**",
|
|
473
|
+
"**/lefthook.*",
|
|
474
|
+
".tamperward.yml",
|
|
475
|
+
"**/.tamperward.yml",
|
|
476
|
+
".claude/settings.json",
|
|
477
|
+
".claude/settings.local.json",
|
|
478
|
+
"**/.pre-commit-config.yaml"
|
|
479
|
+
]
|
|
467
480
|
},
|
|
468
481
|
rules: {
|
|
469
482
|
"test-deletion": { severity: "block" },
|
|
@@ -1516,8 +1529,18 @@ function evaluate(changes, policy, detectors = allDetectors, view) {
|
|
|
1516
1529
|
try {
|
|
1517
1530
|
out2.push(...d.run(active, policy, view));
|
|
1518
1531
|
} catch (e) {
|
|
1519
|
-
process.stderr.write(`tamperward: detector "${d.id}" errored
|
|
1532
|
+
process.stderr.write(`tamperward: detector "${d.id}" errored: ${String(e)}
|
|
1520
1533
|
`);
|
|
1534
|
+
if (view === "staged" || view === "worktree" || view === "range") {
|
|
1535
|
+
out2.push({
|
|
1536
|
+
rule: "detector-error",
|
|
1537
|
+
severity: "block",
|
|
1538
|
+
message: `Detector "${d.id}" failed to run; the verdict is incomplete.`,
|
|
1539
|
+
evidence: String(e).slice(0, 200),
|
|
1540
|
+
remediation: "A rule that cannot run is not a rule that passed. Fix the input or the detector; do not read this as clean.",
|
|
1541
|
+
signoff: { required: true, command: `tamperward allow detector-error --reason "..."` }
|
|
1542
|
+
});
|
|
1543
|
+
}
|
|
1521
1544
|
}
|
|
1522
1545
|
}
|
|
1523
1546
|
const seen = /* @__PURE__ */ new Set();
|
|
@@ -2599,6 +2622,7 @@ Honored at LOCAL pre-commit only. The agent-layer hook ignores this file; CI req
|
|
|
2599
2622
|
// src/cli/init.ts
|
|
2600
2623
|
import { existsSync as existsSync7, mkdirSync as mkdirSync5, readFileSync as readFileSync10, writeFileSync as writeFileSync5, chmodSync } from "node:fs";
|
|
2601
2624
|
import { dirname as dirname3, join as join8 } from "node:path";
|
|
2625
|
+
import { fileURLToPath } from "node:url";
|
|
2602
2626
|
var HOOK_CMD = "npx --yes tamperward hook claude";
|
|
2603
2627
|
var SWEEP_CMD = "npx --yes tamperward sweep claude";
|
|
2604
2628
|
var PRECOMMIT_CMD = "npx --yes tamperward check --staged";
|
|
@@ -2617,6 +2641,21 @@ version: 1
|
|
|
2617
2641
|
# snapshot-rewrite: { severity: block }
|
|
2618
2642
|
# ignore: [] # visible blind spots \u2014 the count is always reported
|
|
2619
2643
|
`;
|
|
2644
|
+
function shippedVersion() {
|
|
2645
|
+
try {
|
|
2646
|
+
const here = dirname3(fileURLToPath(import.meta.url));
|
|
2647
|
+
for (const rel of ["../package.json", "../../package.json", "../../../package.json"]) {
|
|
2648
|
+
try {
|
|
2649
|
+
const pkg = JSON.parse(readFileSync10(join8(here, rel), "utf8"));
|
|
2650
|
+
if (pkg.name === "tamperward" && pkg.version) return pkg.version;
|
|
2651
|
+
} catch {
|
|
2652
|
+
}
|
|
2653
|
+
}
|
|
2654
|
+
} catch {
|
|
2655
|
+
}
|
|
2656
|
+
return "latest";
|
|
2657
|
+
}
|
|
2658
|
+
var TW_VERSION = shippedVersion();
|
|
2620
2659
|
var WORKFLOW_CONTENT = `name: tamperward
|
|
2621
2660
|
|
|
2622
2661
|
# The CI authority for main: the same engine as the agent hook and pre-commit, run over
|
|
@@ -2652,10 +2691,20 @@ jobs:
|
|
|
2652
2691
|
run: |
|
|
2653
2692
|
RULES="$(printf '%s' "$LABELS" | jq -r '.[] | select(startswith("tamperward:allow:")) | sub("^tamperward:allow:"; "")' | paste -sd, -)"
|
|
2654
2693
|
echo "rules=$RULES" >> "$GITHUB_OUTPUT"
|
|
2655
|
-
- name: Tamperward gate
|
|
2694
|
+
- name: Tamperward gate (diff-time)
|
|
2656
2695
|
env:
|
|
2657
2696
|
TAMPERWARD_OOB_SIGNOFF: \${{ steps.oob.outputs.rules }}
|
|
2658
|
-
run: npx --yes tamperward check --diff "\${{ github.event.pull_request.base.sha }}...\${{ github.event.pull_request.head.sha }}"
|
|
2697
|
+
run: npx --yes tamperward@${TW_VERSION} check --diff "\${{ github.event.pull_request.base.sha }}...\${{ github.event.pull_request.head.sha }}"
|
|
2698
|
+
# Diff-time detection is spelling-dependent by nature; pristine
|
|
2699
|
+
# re-execution is not, and it was the layer nothing got past in the
|
|
2700
|
+
# taskbench sweeps. Shipping a CI template WITHOUT it meant every
|
|
2701
|
+
# masked-failure class was adjudicated only at the local layer \u2014 the
|
|
2702
|
+
# layer an agent shares a filesystem with. (P1-5, external review.)
|
|
2703
|
+
#
|
|
2704
|
+
# Requires a \`verify:\` block in .tamperward.yml naming the suite command;
|
|
2705
|
+
# without one this step fails closed (exit 2) rather than passing quietly.
|
|
2706
|
+
- name: Tamperward verify (pristine re-execution)
|
|
2707
|
+
run: npx --yes tamperward@${TW_VERSION} verify --base "\${{ github.event.pull_request.base.sha }}"
|
|
2659
2708
|
`;
|
|
2660
2709
|
function planPolicy(cwd) {
|
|
2661
2710
|
const path = join8(cwd, POLICY_FILE);
|
|
@@ -2789,9 +2838,9 @@ tamperward init: ${changed} change(s) applied. Commit them so the gate travels w
|
|
|
2789
2838
|
|
|
2790
2839
|
// src/cli/verify.ts
|
|
2791
2840
|
import { execFileSync as execFileSync4, spawnSync } from "node:child_process";
|
|
2792
|
-
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";
|
|
2841
|
+
import { chmodSync as chmodSync2, cpSync, lstatSync, mkdirSync as mkdirSync6, mkdtempSync as mkdtempSync2, readFileSync as readFileSync11, rmSync as rmSync2, statSync as statSync5, symlinkSync, writeFileSync as writeFileSync6, existsSync as existsSync8 } from "node:fs";
|
|
2793
2842
|
import { tmpdir as tmpdir2 } from "node:os";
|
|
2794
|
-
import { dirname as dirname4, join as join9 } from "node:path";
|
|
2843
|
+
import { dirname as dirname4, join as join9, resolve } from "node:path";
|
|
2795
2844
|
var OVERLAY_CLASSES = ["tests", "snapshots", "config"];
|
|
2796
2845
|
function git2(args, cwd) {
|
|
2797
2846
|
return execFileSync4("git", args, { cwd, encoding: "utf8", maxBuffer: 1 << 28 });
|
|
@@ -2817,12 +2866,23 @@ function materialize(cwd, dest) {
|
|
|
2817
2866
|
if (!st.isFile()) continue;
|
|
2818
2867
|
const out2 = join9(dest, rel);
|
|
2819
2868
|
mkdirSync6(dirname4(out2), { recursive: true });
|
|
2820
|
-
|
|
2869
|
+
dropSymlink(out2);
|
|
2870
|
+
if (lstatSync(src).isSymbolicLink()) {
|
|
2871
|
+
writeFileSync6(out2, readFileSync11(src));
|
|
2872
|
+
} else {
|
|
2873
|
+
cpSync(src, out2, { dereference: true });
|
|
2874
|
+
}
|
|
2821
2875
|
chmodSync2(out2, st.mode);
|
|
2822
2876
|
}
|
|
2823
|
-
const nm =
|
|
2877
|
+
const nm = resolve(cwd, "node_modules");
|
|
2824
2878
|
if (existsSync8(nm)) symlinkSync(nm, join9(dest, "node_modules"), "dir");
|
|
2825
2879
|
}
|
|
2880
|
+
function dropSymlink(p) {
|
|
2881
|
+
try {
|
|
2882
|
+
if (lstatSync(p).isSymbolicLink()) rmSync2(p, { force: true });
|
|
2883
|
+
} catch {
|
|
2884
|
+
}
|
|
2885
|
+
}
|
|
2826
2886
|
function overlayPristine(cwd, base, dest, policy) {
|
|
2827
2887
|
const atBase = git2(["ls-tree", "-r", "--name-only", "-z", base], cwd).split("\0").filter(Boolean);
|
|
2828
2888
|
const isOverlay = (p) => OVERLAY_CLASSES.some((c) => isProtected(p, policy, c));
|
|
@@ -2834,6 +2894,7 @@ function overlayPristine(cwd, base, dest, policy) {
|
|
|
2834
2894
|
const content = execFileSync4("git", ["show", `${base}:${rel}`], { cwd, maxBuffer: 1 << 28 });
|
|
2835
2895
|
const out2 = join9(dest, rel);
|
|
2836
2896
|
mkdirSync6(dirname4(out2), { recursive: true });
|
|
2897
|
+
dropSymlink(out2);
|
|
2837
2898
|
writeFileSync6(out2, content);
|
|
2838
2899
|
restored++;
|
|
2839
2900
|
}
|
|
@@ -2955,8 +3016,8 @@ function parseVerify(args) {
|
|
|
2955
3016
|
// src/cli/run.ts
|
|
2956
3017
|
import { execFileSync as execFileSync5, spawnSync as spawnSync2 } from "node:child_process";
|
|
2957
3018
|
import { createHash as createHash5 } from "node:crypto";
|
|
2958
|
-
import { readdirSync as readdirSync3, readFileSync as
|
|
2959
|
-
import { join as join10 } from "node:path";
|
|
3019
|
+
import { readdirSync as readdirSync3, readFileSync as readFileSync12, readlinkSync, realpathSync, statSync as statSync6 } from "node:fs";
|
|
3020
|
+
import { join as join10, resolve as resolve2 } from "node:path";
|
|
2960
3021
|
var out = (s) => process.stdout.write(s + "\n");
|
|
2961
3022
|
var err = (s) => process.stderr.write(s + "\n");
|
|
2962
3023
|
function git3(args, cwd) {
|
|
@@ -2969,7 +3030,7 @@ function treeFingerprint(cwd) {
|
|
|
2969
3030
|
h.update(rel);
|
|
2970
3031
|
h.update("\0");
|
|
2971
3032
|
try {
|
|
2972
|
-
h.update(
|
|
3033
|
+
h.update(readFileSync12(join10(cwd, rel)));
|
|
2973
3034
|
} catch {
|
|
2974
3035
|
h.update("<unreadable>");
|
|
2975
3036
|
}
|
|
@@ -2977,9 +3038,48 @@ function treeFingerprint(cwd) {
|
|
|
2977
3038
|
}
|
|
2978
3039
|
return h.digest("hex");
|
|
2979
3040
|
}
|
|
3041
|
+
function depsFingerprint(cwd) {
|
|
3042
|
+
const root = join10(cwd, "node_modules");
|
|
3043
|
+
const h = createHash5("sha256");
|
|
3044
|
+
const walk = (dir) => {
|
|
3045
|
+
let entries;
|
|
3046
|
+
try {
|
|
3047
|
+
entries = readdirSync3(dir, { withFileTypes: true });
|
|
3048
|
+
} catch {
|
|
3049
|
+
return;
|
|
3050
|
+
}
|
|
3051
|
+
for (const e of [...entries].sort((a, b) => a.name.localeCompare(b.name))) {
|
|
3052
|
+
const p = join10(dir, e.name);
|
|
3053
|
+
if (e.isDirectory()) walk(p);
|
|
3054
|
+
else if (e.isFile()) {
|
|
3055
|
+
h.update(p);
|
|
3056
|
+
try {
|
|
3057
|
+
h.update(readFileSync12(p));
|
|
3058
|
+
h.update(String(statSync6(p).mode));
|
|
3059
|
+
} catch {
|
|
3060
|
+
h.update("<unreadable>");
|
|
3061
|
+
}
|
|
3062
|
+
} else if (e.isSymbolicLink()) {
|
|
3063
|
+
h.update(p);
|
|
3064
|
+
try {
|
|
3065
|
+
h.update(readlinkSync(p));
|
|
3066
|
+
} catch {
|
|
3067
|
+
h.update("<unreadable-link>");
|
|
3068
|
+
}
|
|
3069
|
+
}
|
|
3070
|
+
}
|
|
3071
|
+
};
|
|
3072
|
+
try {
|
|
3073
|
+
if (!statSync6(root).isDirectory()) return "none";
|
|
3074
|
+
} catch {
|
|
3075
|
+
return "none";
|
|
3076
|
+
}
|
|
3077
|
+
walk(root);
|
|
3078
|
+
return h.digest("hex");
|
|
3079
|
+
}
|
|
2980
3080
|
function nowTicks() {
|
|
2981
3081
|
try {
|
|
2982
|
-
return Math.floor(parseFloat(
|
|
3082
|
+
return Math.floor(parseFloat(readFileSync12("/proc/uptime", "utf8").split(" ")[0]) * 100);
|
|
2983
3083
|
} catch {
|
|
2984
3084
|
return Number.POSITIVE_INFINITY;
|
|
2985
3085
|
}
|
|
@@ -3009,7 +3109,7 @@ function survivorsHoldingTree(cwd, spawnedAfterTicks) {
|
|
|
3009
3109
|
}
|
|
3010
3110
|
if (held !== real && !held.startsWith(real + "/")) continue;
|
|
3011
3111
|
try {
|
|
3012
|
-
const stat =
|
|
3112
|
+
const stat = readFileSync12(`/proc/${pid}/stat`, "utf8");
|
|
3013
3113
|
const startTicks = Number(stat.slice(stat.lastIndexOf(")") + 2).split(" ")[19]);
|
|
3014
3114
|
if (Number.isFinite(startTicks) && startTicks >= spawnedAfterTicks) out2.push(n);
|
|
3015
3115
|
} catch {
|
|
@@ -3018,7 +3118,7 @@ function survivorsHoldingTree(cwd, spawnedAfterTicks) {
|
|
|
3018
3118
|
return out2;
|
|
3019
3119
|
}
|
|
3020
3120
|
function runEnvelope(opts) {
|
|
3021
|
-
const cwd = opts.cwd ?? process.cwd();
|
|
3121
|
+
const cwd = resolve2(opts.cwd ?? process.cwd());
|
|
3022
3122
|
if (!opts.argv.length) {
|
|
3023
3123
|
err("tamperward run: no agent command given (usage: tamperward run [options] -- <command...>)");
|
|
3024
3124
|
return 2;
|
|
@@ -3055,6 +3155,7 @@ function runEnvelope(opts) {
|
|
|
3055
3155
|
}
|
|
3056
3156
|
}
|
|
3057
3157
|
out(`tamperward run \u2014 trusted base ${base.slice(0, 10)}; agent exit is untrusted.`);
|
|
3158
|
+
const depsBefore = depsFingerprint(cwd);
|
|
3058
3159
|
const spawnTicks = nowTicks();
|
|
3059
3160
|
const agent = spawnSync2(opts.argv[0], opts.argv.slice(1), { cwd, stdio: "inherit" });
|
|
3060
3161
|
const agentExit = agent.status ?? 1;
|
|
@@ -3089,16 +3190,21 @@ tamperward run \u2014 agent exit ${agentExit}; HISTORY_REWRITE \u2192 ENFORCEMEN
|
|
|
3089
3190
|
err("tamperward run: the working tree changed while it was being adjudicated \u2014");
|
|
3090
3191
|
err("the verdict below would describe a tree that no longer exists.");
|
|
3091
3192
|
}
|
|
3193
|
+
const depsDrifted = !opts.allowDepDrift && depsFingerprint(cwd) !== depsBefore;
|
|
3194
|
+
if (depsDrifted) {
|
|
3195
|
+
err("tamperward run: the installed dependency tree changed during this run \u2014 the program that");
|
|
3196
|
+
err("runs the suite is not the one we started with. Both verification runs execute it.");
|
|
3197
|
+
}
|
|
3092
3198
|
const survivors = survivorsHoldingTree(cwd, spawnTicks);
|
|
3093
3199
|
if (survivors.length) {
|
|
3094
3200
|
err(`tamperward run: ${survivors.length} process(es) started during this run still hold the`);
|
|
3095
3201
|
err(`working tree (pid ${survivors.join(", ")}). A verdict cannot outlive the tree it describes.`);
|
|
3096
3202
|
}
|
|
3097
|
-
const notQuiescent = mutatedDuringAdjudication || survivors.length > 0;
|
|
3203
|
+
const notQuiescent = mutatedDuringAdjudication || survivors.length > 0 || depsDrifted;
|
|
3098
3204
|
const cannot = diffCode === 2 || workCode === 2 || verifyCode === 2;
|
|
3099
3205
|
const blocked = diffCode === 1 || workCode === 1 || verifyCode === 1 || notQuiescent;
|
|
3100
3206
|
const enforcement = cannot ? 2 : blocked ? 1 : 0;
|
|
3101
|
-
const verdict2 = enforcement === 2 ? "CANNOT_ADJUDICATE (fail closed)" : notQuiescent ? "NOT_QUIESCENT \u2014 the tree moved, or something still holds it" : enforcement === 1 ? "ENFORCEMENT_FAILED \u2014 the tree the runtime released does not stand" : agentExit !== 0 ? `agent exited ${agentExit} (enforcement clean)` : "GREEN MEANS GREEN";
|
|
3207
|
+
const verdict2 = enforcement === 2 ? "CANNOT_ADJUDICATE (fail closed)" : depsDrifted ? "DEPENDENCY_DRIFT \u2014 the suite runner changed under the envelope" : notQuiescent ? "NOT_QUIESCENT \u2014 the tree moved, or something still holds it" : enforcement === 1 ? "ENFORCEMENT_FAILED \u2014 the tree the runtime released does not stand" : agentExit !== 0 ? `agent exited ${agentExit} (enforcement clean)` : "GREEN MEANS GREEN";
|
|
3102
3208
|
out(`
|
|
3103
3209
|
tamperward run \u2014 agent exit ${agentExit}; checks diff=${diffCode} worktree=${workCode} verify=${verifyCode} \u2192 ${verdict2}`);
|
|
3104
3210
|
return enforcement !== 0 ? enforcement : agentExit;
|
|
@@ -3115,6 +3221,7 @@ function parseRun(args) {
|
|
|
3115
3221
|
else if (a === "--budget") o.budget = Number(args[++i]);
|
|
3116
3222
|
else if (a === "--allow-dirty") o.allowDirty = true;
|
|
3117
3223
|
else if (a === "--settle") o.settle = Number(args[++i]);
|
|
3224
|
+
else if (a === "--allow-dep-drift") o.allowDepDrift = true;
|
|
3118
3225
|
else if (a === "--cwd") o.cwd = args[++i];
|
|
3119
3226
|
else {
|
|
3120
3227
|
o.argv = args.slice(i);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tamperward",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.12.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",
|