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.
Files changed (2) hide show
  1. package/dist/cli/index.js +72 -11
  2. 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
- hooks: [".husky/**", "**/lefthook.*", ".tamperward.yml", "**/.tamperward.yml"]
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 and was skipped: ${String(e)}
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
- cpSync(src, out2);
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 readFileSync11, readlinkSync, realpathSync, statSync as statSync6 } from "node:fs";
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(readFileSync11(join10(cwd, rel)));
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(readFileSync11(p));
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(readFileSync11("/proc/uptime", "utf8").split(" ")[0]) * 100);
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 = readFileSync11(`/proc/${pid}/stat`, "utf8");
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.11.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",