tamperward 1.11.1 → 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 +72 -11
- 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,7 +2838,7 @@ 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
2843
|
import { dirname as dirname4, join as join9, resolve } from "node:path";
|
|
2795
2844
|
var OVERLAY_CLASSES = ["tests", "snapshots", "config"];
|
|
@@ -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
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,7 +3016,7 @@ 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
|
|
3019
|
+
import { readdirSync as readdirSync3, readFileSync as readFileSync12, readlinkSync, realpathSync, statSync as statSync6 } from "node:fs";
|
|
2959
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");
|
|
@@ -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
|
}
|
|
@@ -2993,7 +3054,7 @@ function depsFingerprint(cwd) {
|
|
|
2993
3054
|
else if (e.isFile()) {
|
|
2994
3055
|
h.update(p);
|
|
2995
3056
|
try {
|
|
2996
|
-
h.update(
|
|
3057
|
+
h.update(readFileSync12(p));
|
|
2997
3058
|
h.update(String(statSync6(p).mode));
|
|
2998
3059
|
} catch {
|
|
2999
3060
|
h.update("<unreadable>");
|
|
@@ -3018,7 +3079,7 @@ function depsFingerprint(cwd) {
|
|
|
3018
3079
|
}
|
|
3019
3080
|
function nowTicks() {
|
|
3020
3081
|
try {
|
|
3021
|
-
return Math.floor(parseFloat(
|
|
3082
|
+
return Math.floor(parseFloat(readFileSync12("/proc/uptime", "utf8").split(" ")[0]) * 100);
|
|
3022
3083
|
} catch {
|
|
3023
3084
|
return Number.POSITIVE_INFINITY;
|
|
3024
3085
|
}
|
|
@@ -3048,7 +3109,7 @@ function survivorsHoldingTree(cwd, spawnedAfterTicks) {
|
|
|
3048
3109
|
}
|
|
3049
3110
|
if (held !== real && !held.startsWith(real + "/")) continue;
|
|
3050
3111
|
try {
|
|
3051
|
-
const stat =
|
|
3112
|
+
const stat = readFileSync12(`/proc/${pid}/stat`, "utf8");
|
|
3052
3113
|
const startTicks = Number(stat.slice(stat.lastIndexOf(")") + 2).split(" ")[19]);
|
|
3053
3114
|
if (Number.isFinite(startTicks) && startTicks >= spawnedAfterTicks) out2.push(n);
|
|
3054
3115
|
} catch {
|
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",
|