tamperward 2.3.0 → 2.4.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 +238 -60
- package/package.json +1 -1
package/dist/cli/index.js
CHANGED
|
@@ -202,6 +202,27 @@ function parseHunk(lines, start) {
|
|
|
202
202
|
return { hunk: { oldStart, oldLines, newStart, newLines, lines: out2 }, next: i };
|
|
203
203
|
}
|
|
204
204
|
|
|
205
|
+
// src/diff/synth.ts
|
|
206
|
+
function linesOf(content) {
|
|
207
|
+
if (content === "") return [];
|
|
208
|
+
const lines = content.split("\n");
|
|
209
|
+
if (content.endsWith("\n")) lines.pop();
|
|
210
|
+
return lines;
|
|
211
|
+
}
|
|
212
|
+
function addHunks(content) {
|
|
213
|
+
const lines = linesOf(content);
|
|
214
|
+
if (lines.length === 0) return [];
|
|
215
|
+
return [
|
|
216
|
+
{
|
|
217
|
+
oldStart: 0,
|
|
218
|
+
oldLines: 0,
|
|
219
|
+
newStart: 1,
|
|
220
|
+
newLines: lines.length,
|
|
221
|
+
lines: lines.map((l, i) => ({ type: "add", content: l, oldLine: null, newLine: i + 1 }))
|
|
222
|
+
}
|
|
223
|
+
];
|
|
224
|
+
}
|
|
225
|
+
|
|
205
226
|
// src/git/build.ts
|
|
206
227
|
function git(args, cwd) {
|
|
207
228
|
try {
|
|
@@ -256,6 +277,9 @@ function mergeBaseOf(base, head, opts = {}) {
|
|
|
256
277
|
function fileAt(rev, path, opts = {}) {
|
|
257
278
|
return blobAt(rev, path, opts.cwd);
|
|
258
279
|
}
|
|
280
|
+
function fileOnDisk(path, opts = {}) {
|
|
281
|
+
return fromDisk(path, opts.cwd);
|
|
282
|
+
}
|
|
259
283
|
function isGitRepo(cwd) {
|
|
260
284
|
try {
|
|
261
285
|
git(["rev-parse", "--git-dir"], cwd);
|
|
@@ -306,18 +330,35 @@ function diffWorktree(opts = {}) {
|
|
|
306
330
|
(c) => enrich(c, (p) => blobAt("HEAD", p, opts.cwd), (p) => fromDisk(p, opts.cwd))
|
|
307
331
|
);
|
|
308
332
|
}
|
|
333
|
+
function untrackedAdds(opts = {}, keep, exclude) {
|
|
334
|
+
const others = git(["ls-files", "--others", "--exclude-standard", "-z"], opts.cwd).split("\0").filter(Boolean);
|
|
335
|
+
return others.filter((rel) => !exclude?.has(rel) && (!keep || keep(rel))).map((rel) => {
|
|
336
|
+
const after = fromDisk(rel, opts.cwd);
|
|
337
|
+
return {
|
|
338
|
+
kind: "file",
|
|
339
|
+
path: rel,
|
|
340
|
+
oldPath: null,
|
|
341
|
+
op: "add",
|
|
342
|
+
before: null,
|
|
343
|
+
after,
|
|
344
|
+
binary: false,
|
|
345
|
+
hunks: after == null ? [] : addHunks(after)
|
|
346
|
+
};
|
|
347
|
+
});
|
|
348
|
+
}
|
|
309
349
|
function diffWorktreeWithUntracked(opts = {}) {
|
|
310
350
|
const tracked = diffWorktree(opts);
|
|
311
|
-
const others = git(["ls-files", "--others", "--exclude-standard", "-z"], opts.cwd).split("\0").filter(Boolean);
|
|
312
351
|
const seen = new Set(tracked.filter((c) => c.kind === "file").map((c) => c.path));
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
)
|
|
319
|
-
|
|
320
|
-
|
|
352
|
+
return [...tracked, ...untrackedAdds(opts, void 0, seen)];
|
|
353
|
+
}
|
|
354
|
+
function hiddenTrackedPaths(opts = {}) {
|
|
355
|
+
const out2 = [];
|
|
356
|
+
for (const entry of git(["ls-files", "-v", "-z"], opts.cwd).split("\0")) {
|
|
357
|
+
if (entry.length < 3 || entry[1] !== " ") continue;
|
|
358
|
+
const tag = entry[0];
|
|
359
|
+
if (tag === "S" || tag !== tag.toUpperCase()) out2.push(entry.slice(2));
|
|
360
|
+
}
|
|
361
|
+
return out2;
|
|
321
362
|
}
|
|
322
363
|
|
|
323
364
|
// src/detectors/finding.ts
|
|
@@ -947,8 +988,20 @@ var PATTERNS2 = {
|
|
|
947
988
|
py: [
|
|
948
989
|
// `import pytest as pt` makes the decorator `@pt.mark.skip`; the module alias is free.
|
|
949
990
|
{ re: /@\w+\.mark\.(?:skip|skipif|xfail)\b/, why: "a pytest skip/skipif/xfail marker" },
|
|
991
|
+
// `from pytest import mark` makes it `@mark.skip`; `marks=pytest.mark.skip` is the
|
|
992
|
+
// parametrize form. Both carry the same marker without the `@<module>.` shape.
|
|
993
|
+
{ re: /@mark\.(?:skip|skipif|xfail)\b/, why: "a pytest skip/skipif/xfail marker (from pytest import mark)" },
|
|
994
|
+
{ re: /\bmarks\s*=\s*(?:\w+\.)?mark\.(?:skip|skipif|xfail)\b/, why: "a parametrize marks= skip/skipif/xfail marker" },
|
|
950
995
|
{ re: /\bpytestmark\s*=.*\.mark\.(?:skip|skipif|xfail)\b/, why: "a module-level pytestmark skip (every test in the file is skipped)" },
|
|
951
|
-
{ re: /\bpytest\.(?:skip|xfail|importorskip)\(/, why: "a runtime pytest.skip()/xfail() call" },
|
|
996
|
+
{ re: /\bpytest\.(?:skip|xfail|importorskip)\(/, why: "a runtime pytest.skip()/xfail()/importorskip() call" },
|
|
997
|
+
// Collection-time drops. A conftest.py is protected as a test file, and these
|
|
998
|
+
// are how pytest is told never to collect a test in the first place — no
|
|
999
|
+
// marker ever appears in the test itself. `collect_ignore` / `collect_ignore_glob`
|
|
1000
|
+
// are conftest globals (pytest docs, "Customizing test collection");
|
|
1001
|
+
// `pytest_ignore_collect` is the hook whose only job is to say "skip this path";
|
|
1002
|
+
// `__test__ = False` de-collects a class or function.
|
|
1003
|
+
{ re: /\bcollect_ignore(?:_glob)?\s*(?:=|\+=|\.(?:append|extend)\()/, why: "a collect_ignore / collect_ignore_glob entry (the files are never collected)" },
|
|
1004
|
+
{ re: /\bdef\s+pytest_ignore_collect\s*\(/, why: "a pytest_ignore_collect hook (paths are dropped at collection)" },
|
|
952
1005
|
// `from unittest import skip` makes the decorator bare `@skip`.
|
|
953
1006
|
{ re: /@(?:unittest\.)?(?:skip|skipIf|skipUnless|expectedFailure)\b/, why: "a unittest skip/expectedFailure decorator" },
|
|
954
1007
|
{ re: /\bself\.skipTest\(/, why: "a runtime self.skipTest() call" },
|
|
@@ -1162,12 +1215,12 @@ var coverageLowering = {
|
|
|
1162
1215
|
);
|
|
1163
1216
|
}
|
|
1164
1217
|
for (const l of added) {
|
|
1165
|
-
if (/--passWithNoTests\b/.test(l.content)) {
|
|
1218
|
+
if (/--passWithNoTests\b/.test(l.content) || /\bpassWithNoTests\s*:\s*true\b/.test(l.content)) {
|
|
1166
1219
|
out2.push(
|
|
1167
1220
|
makeFinding(RULE4, policy, {
|
|
1168
1221
|
file: c.path,
|
|
1169
1222
|
line: l.newLine ?? void 0,
|
|
1170
|
-
message: "
|
|
1223
|
+
message: "passWithNoTests added \u2014 the suite can now pass with zero tests.",
|
|
1171
1224
|
evidence: l.content.trim(),
|
|
1172
1225
|
remediation: "Remove --passWithNoTests; an empty suite must not be a green check."
|
|
1173
1226
|
})
|
|
@@ -2476,6 +2529,7 @@ function runCheck(opts) {
|
|
|
2476
2529
|
// src/cli/hook.ts
|
|
2477
2530
|
import { readFileSync as readFileSync9, appendFileSync as appendFileSync4, writeFileSync as writeFileSync4, existsSync as existsSync6, statSync as statSync4 } from "node:fs";
|
|
2478
2531
|
import { createHash as createHash4 } from "node:crypto";
|
|
2532
|
+
import { join as join8 } from "node:path";
|
|
2479
2533
|
|
|
2480
2534
|
// src/adapters/claude/changes.ts
|
|
2481
2535
|
import { execFileSync as execFileSync2 } from "node:child_process";
|
|
@@ -2658,14 +2712,23 @@ function gitDir2(cwd) {
|
|
|
2658
2712
|
return null;
|
|
2659
2713
|
}
|
|
2660
2714
|
}
|
|
2661
|
-
function
|
|
2715
|
+
function statePath(cwd, sessionId, kind) {
|
|
2662
2716
|
if (!sessionId) return null;
|
|
2663
2717
|
const gd = gitDir2(cwd);
|
|
2664
2718
|
if (!gd) return null;
|
|
2665
2719
|
const dir = join6(gd, "tamperward");
|
|
2666
|
-
return join6(dir,
|
|
2720
|
+
return join6(dir, `${kind}-${sessionId.replace(UNSAFE2, "")}.json`);
|
|
2721
|
+
}
|
|
2722
|
+
function ptreePath(cwd, sessionId) {
|
|
2723
|
+
return statePath(cwd, sessionId, "ptree");
|
|
2724
|
+
}
|
|
2725
|
+
function turnTreePath(cwd, sessionId) {
|
|
2726
|
+
return statePath(cwd, sessionId, "turntree");
|
|
2667
2727
|
}
|
|
2668
2728
|
var sha = (buf) => createHash2("sha256").update(buf).digest("hex").slice(0, 16);
|
|
2729
|
+
function contentHash(content) {
|
|
2730
|
+
return sha(Buffer.isBuffer(content) ? content : Buffer.from(content, "utf8"));
|
|
2731
|
+
}
|
|
2669
2732
|
function snapshotProtected(cwd, policy, prev) {
|
|
2670
2733
|
const out2 = {};
|
|
2671
2734
|
const walk = (rel) => {
|
|
@@ -2710,8 +2773,7 @@ function driftBetween(expected, current) {
|
|
|
2710
2773
|
}
|
|
2711
2774
|
return { changed, deleted };
|
|
2712
2775
|
}
|
|
2713
|
-
function
|
|
2714
|
-
const p = ptreePath(cwd, sessionId);
|
|
2776
|
+
function loadTree(p) {
|
|
2715
2777
|
if (!p || !existsSync5(p)) return null;
|
|
2716
2778
|
try {
|
|
2717
2779
|
return JSON.parse(readFileSync6(p, "utf8"));
|
|
@@ -2719,8 +2781,7 @@ function loadPtree(cwd, sessionId) {
|
|
|
2719
2781
|
return null;
|
|
2720
2782
|
}
|
|
2721
2783
|
}
|
|
2722
|
-
function
|
|
2723
|
-
const p = ptreePath(cwd, sessionId);
|
|
2784
|
+
function saveTree(p, tree) {
|
|
2724
2785
|
if (!p) return;
|
|
2725
2786
|
try {
|
|
2726
2787
|
mkdirSync3(join6(p, ".."), { recursive: true });
|
|
@@ -2728,6 +2789,18 @@ function savePtree(cwd, sessionId, tree) {
|
|
|
2728
2789
|
} catch {
|
|
2729
2790
|
}
|
|
2730
2791
|
}
|
|
2792
|
+
function loadPtree(cwd, sessionId) {
|
|
2793
|
+
return loadTree(ptreePath(cwd, sessionId));
|
|
2794
|
+
}
|
|
2795
|
+
function savePtree(cwd, sessionId, tree) {
|
|
2796
|
+
saveTree(ptreePath(cwd, sessionId), tree);
|
|
2797
|
+
}
|
|
2798
|
+
function loadTurnTree(cwd, sessionId) {
|
|
2799
|
+
return loadTree(turnTreePath(cwd, sessionId));
|
|
2800
|
+
}
|
|
2801
|
+
function saveTurnTree(cwd, sessionId, tree) {
|
|
2802
|
+
saveTree(turnTreePath(cwd, sessionId), tree);
|
|
2803
|
+
}
|
|
2731
2804
|
|
|
2732
2805
|
// src/cli/watch.ts
|
|
2733
2806
|
import { appendFileSync as appendFileSync3, mkdirSync as mkdirSync4, readdirSync as readdirSync2, readFileSync as readFileSync7, statSync as statSync2, watch } from "node:fs";
|
|
@@ -2951,12 +3024,107 @@ function verdict(blocks, kind) {
|
|
|
2951
3024
|
} : { decision: "block", reason };
|
|
2952
3025
|
return { exitCode: 0, stdout: JSON.stringify(payload) + "\n" };
|
|
2953
3026
|
}
|
|
3027
|
+
function fileChanges(changes) {
|
|
3028
|
+
return changes.filter((c) => c.kind === "file");
|
|
3029
|
+
}
|
|
3030
|
+
function hashOnDisk(cwd, rel) {
|
|
3031
|
+
try {
|
|
3032
|
+
return contentHash(readFileSync9(join8(cwd, rel)));
|
|
3033
|
+
} catch {
|
|
3034
|
+
return null;
|
|
3035
|
+
}
|
|
3036
|
+
}
|
|
3037
|
+
function gitMode(mode) {
|
|
3038
|
+
return (mode & 64) !== 0 ? "100755" : "100644";
|
|
3039
|
+
}
|
|
3040
|
+
function turnView(cwd, sessionId, policy, base) {
|
|
3041
|
+
const tracked = base ? diffSince(base, { cwd }) : diffWorktree({ cwd });
|
|
3042
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3043
|
+
for (const c of fileChanges(tracked)) {
|
|
3044
|
+
seen.add(c.path);
|
|
3045
|
+
if (c.oldPath) seen.add(c.oldPath);
|
|
3046
|
+
}
|
|
3047
|
+
const turnTree = loadTurnTree(cwd, sessionId);
|
|
3048
|
+
const untracked = untrackedAdds({ cwd }, (rel) => isProtected(rel, policy), seen).filter(
|
|
3049
|
+
(c) => c.kind !== "file" || !turnTree || turnTree[c.path]?.hash !== hashOnDisk(cwd, c.path)
|
|
3050
|
+
);
|
|
3051
|
+
for (const c of fileChanges(untracked)) seen.add(c.path);
|
|
3052
|
+
const trusted = base ?? "HEAD";
|
|
3053
|
+
const hidden = [];
|
|
3054
|
+
for (const rel of hiddenTrackedPaths({ cwd })) {
|
|
3055
|
+
if (seen.has(rel) || !isProtected(rel, policy)) continue;
|
|
3056
|
+
hidden.push(...synthFileChange(rel, fileAt(trusted, rel, { cwd }), fileOnDisk(rel, { cwd })));
|
|
3057
|
+
}
|
|
3058
|
+
return [...tracked, ...untracked, ...hidden];
|
|
3059
|
+
}
|
|
3060
|
+
function uncoveredDrift(cwd, policy, expected, current, covered, base, turnTree) {
|
|
3061
|
+
const d = driftBetween(expected, current);
|
|
3062
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3063
|
+
for (const c of fileChanges(covered)) {
|
|
3064
|
+
seen.add(c.path);
|
|
3065
|
+
if (c.oldPath) seen.add(c.oldPath);
|
|
3066
|
+
}
|
|
3067
|
+
const changes = [];
|
|
3068
|
+
const blocks = [];
|
|
3069
|
+
const trusted = base ?? "HEAD";
|
|
3070
|
+
const candidates = base ? [base, "HEAD", ""] : ["HEAD", ""];
|
|
3071
|
+
for (const rel of [...d.changed, ...d.deleted]) {
|
|
3072
|
+
if (seen.has(rel)) continue;
|
|
3073
|
+
const after = fileOnDisk(rel, { cwd });
|
|
3074
|
+
const trustedBlob = fileAt(trusted, rel, { cwd });
|
|
3075
|
+
const atGitState = after != null && trustedBlob != null && contentHash(after) === contentHash(trustedBlob);
|
|
3076
|
+
const atTurnStart = current[rel] != null && turnTree?.[rel]?.hash === current[rel].hash;
|
|
3077
|
+
if ((atGitState || atTurnStart) && current[rel] && expected[rel] && current[rel].mode === expected[rel].mode) {
|
|
3078
|
+
continue;
|
|
3079
|
+
}
|
|
3080
|
+
const want = expected[rel]?.hash;
|
|
3081
|
+
let before = null;
|
|
3082
|
+
for (const rev of candidates) {
|
|
3083
|
+
const blob = fileAt(rev, rel, { cwd });
|
|
3084
|
+
if (blob != null && contentHash(blob) === want) {
|
|
3085
|
+
before = blob;
|
|
3086
|
+
break;
|
|
3087
|
+
}
|
|
3088
|
+
}
|
|
3089
|
+
if (before == null) {
|
|
3090
|
+
blocks.push({
|
|
3091
|
+
rule: "hidden-drift",
|
|
3092
|
+
severity: "block",
|
|
3093
|
+
file: rel,
|
|
3094
|
+
message: `Protected file ${rel} changed outside git's view, and its sanctioned content cannot be reconstructed to judge the change.`,
|
|
3095
|
+
evidence: rel,
|
|
3096
|
+
remediation: "Restore the file to its last sanctioned content (git restore, or undo the edit) and bring it back into git's view (git update-index --no-skip-worktree / --no-assume-unchanged); make the change through a tool call the gate can see.",
|
|
3097
|
+
signoff: { required: true, command: `tamperward allow hidden-drift --file ${rel} --reason "..."` }
|
|
3098
|
+
});
|
|
3099
|
+
continue;
|
|
3100
|
+
}
|
|
3101
|
+
const synth = synthFileChange(rel, before, after);
|
|
3102
|
+
if (synth.length > 0) {
|
|
3103
|
+
changes.push(...synth);
|
|
3104
|
+
} else if (after != null && current[rel] && expected[rel] && current[rel].mode !== expected[rel].mode) {
|
|
3105
|
+
changes.push({
|
|
3106
|
+
kind: "file",
|
|
3107
|
+
path: rel,
|
|
3108
|
+
oldPath: null,
|
|
3109
|
+
op: "modify",
|
|
3110
|
+
before,
|
|
3111
|
+
after,
|
|
3112
|
+
binary: false,
|
|
3113
|
+
hunks: [],
|
|
3114
|
+
oldMode: gitMode(expected[rel].mode),
|
|
3115
|
+
newMode: gitMode(current[rel].mode)
|
|
3116
|
+
});
|
|
3117
|
+
}
|
|
3118
|
+
}
|
|
3119
|
+
return { changes, blocks };
|
|
3120
|
+
}
|
|
2954
3121
|
function effectDriftBlocks(cwd, sessionId, policy) {
|
|
2955
3122
|
if (!sessionId || !isGitRepo(cwd)) return null;
|
|
2956
3123
|
const expected = loadPtree(cwd, sessionId);
|
|
2957
3124
|
const current = snapshotProtected(cwd, policy, expected ?? void 0);
|
|
2958
3125
|
if (!expected) {
|
|
2959
3126
|
savePtree(cwd, sessionId, current);
|
|
3127
|
+
if (!loadTurnTree(cwd, sessionId)) saveTurnTree(cwd, sessionId, current);
|
|
2960
3128
|
return null;
|
|
2961
3129
|
}
|
|
2962
3130
|
const d = driftBetween(expected, current);
|
|
@@ -2965,12 +3133,13 @@ function effectDriftBlocks(cwd, sessionId, policy) {
|
|
|
2965
3133
|
return null;
|
|
2966
3134
|
}
|
|
2967
3135
|
const base = turnBaseline(cwd, sessionId);
|
|
2968
|
-
const all =
|
|
3136
|
+
const all = turnView(cwd, sessionId, policy, base);
|
|
2969
3137
|
const drifted = /* @__PURE__ */ new Set([...d.changed, ...d.deleted]);
|
|
2970
3138
|
const scoped = all.filter(
|
|
2971
3139
|
(c) => c.kind === "file" && (drifted.has(c.path) || c.oldPath != null && drifted.has(c.oldPath))
|
|
2972
3140
|
);
|
|
2973
|
-
const
|
|
3141
|
+
const gap = uncoveredDrift(cwd, policy, expected, current, scoped, base, loadTurnTree(cwd, sessionId));
|
|
3142
|
+
const blocks = evaluate([...scoped, ...gap.changes], policy, void 0, "turn").filter((f) => f.severity === "block").concat(gap.blocks);
|
|
2974
3143
|
if (blocks.length > 0) return blocks;
|
|
2975
3144
|
savePtree(cwd, sessionId, current);
|
|
2976
3145
|
return null;
|
|
@@ -3071,13 +3240,22 @@ function stopVerdict(input) {
|
|
|
3071
3240
|
try {
|
|
3072
3241
|
const policy = loadPolicy(cwd);
|
|
3073
3242
|
const base = turnBaseline(cwd, input.session_id);
|
|
3074
|
-
|
|
3075
|
-
|
|
3243
|
+
let changes = turnView(cwd, input.session_id, policy, base);
|
|
3244
|
+
const expected = loadPtree(cwd, input.session_id);
|
|
3245
|
+
const current = input.session_id ? snapshotProtected(cwd, policy, expected ?? void 0) : null;
|
|
3246
|
+
let hiddenBlocks = [];
|
|
3247
|
+
if (expected && current) {
|
|
3248
|
+
const gap = uncoveredDrift(cwd, policy, expected, current, changes, base, loadTurnTree(cwd, input.session_id));
|
|
3249
|
+
changes = changes.concat(gap.changes);
|
|
3250
|
+
hiddenBlocks = gap.blocks;
|
|
3251
|
+
}
|
|
3252
|
+
blocks = evaluate(changes, policy, void 0, "turn").filter((f) => f.severity === "block").concat(hiddenBlocks);
|
|
3076
3253
|
const transient = turnTransientBlocks(cwd, input.session_id, policy, changes);
|
|
3077
3254
|
blocks = blocks.concat(transient.blocks);
|
|
3078
3255
|
commitCursor = transient.commit;
|
|
3079
|
-
if (blocks.length === 0 && input.session_id) {
|
|
3080
|
-
savePtree(cwd, input.session_id,
|
|
3256
|
+
if (blocks.length === 0 && input.session_id && current) {
|
|
3257
|
+
savePtree(cwd, input.session_id, current);
|
|
3258
|
+
saveTurnTree(cwd, input.session_id, current);
|
|
3081
3259
|
}
|
|
3082
3260
|
} catch (e) {
|
|
3083
3261
|
return failClosed("Stop", errText(e));
|
|
@@ -3171,7 +3349,7 @@ Honored at LOCAL pre-commit only. The agent-layer hook ignores this file; CI req
|
|
|
3171
3349
|
import { execFileSync as execFileSync4 } from "node:child_process";
|
|
3172
3350
|
import { createHash as createHash5 } from "node:crypto";
|
|
3173
3351
|
import { existsSync as existsSync7, mkdirSync as mkdirSync5, readFileSync as readFileSync10, writeFileSync as writeFileSync5, chmodSync, statSync as statSync5 } from "node:fs";
|
|
3174
|
-
import { basename, dirname as dirname3, isAbsolute as isAbsolute4, join as
|
|
3352
|
+
import { basename, dirname as dirname3, isAbsolute as isAbsolute4, join as join9, relative as relative2, resolve as resolve2 } from "node:path";
|
|
3175
3353
|
import { fileURLToPath } from "node:url";
|
|
3176
3354
|
var errText2 = (e) => (e instanceof Error ? e.message : String(e)).replace(/\s+/g, " ").trim();
|
|
3177
3355
|
var isPlainObject = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
@@ -3236,7 +3414,7 @@ function shippedVersion() {
|
|
|
3236
3414
|
const here = dirname3(fileURLToPath(import.meta.url));
|
|
3237
3415
|
for (const rel of ["../package.json", "../../package.json", "../../../package.json"]) {
|
|
3238
3416
|
try {
|
|
3239
|
-
const pkg = JSON.parse(readFileSync10(
|
|
3417
|
+
const pkg = JSON.parse(readFileSync10(join9(here, rel), "utf8"));
|
|
3240
3418
|
if (pkg.name === "tamperward" && pkg.version) return pkg.version;
|
|
3241
3419
|
} catch {
|
|
3242
3420
|
}
|
|
@@ -3369,7 +3547,7 @@ function hooksShapeError(hooks) {
|
|
|
3369
3547
|
return null;
|
|
3370
3548
|
}
|
|
3371
3549
|
function planPolicy(cwd) {
|
|
3372
|
-
const path =
|
|
3550
|
+
const path = join9(cwd, POLICY_FILE);
|
|
3373
3551
|
if (existsSync7(path)) {
|
|
3374
3552
|
try {
|
|
3375
3553
|
loadPolicy(cwd);
|
|
@@ -3388,7 +3566,7 @@ function planPolicy(cwd) {
|
|
|
3388
3566
|
}
|
|
3389
3567
|
function planClaudeHooks(cwd) {
|
|
3390
3568
|
const rel = ".claude/settings.json";
|
|
3391
|
-
const path =
|
|
3569
|
+
const path = join9(cwd, rel);
|
|
3392
3570
|
let settings = {};
|
|
3393
3571
|
if (existsSync7(path)) {
|
|
3394
3572
|
let parsed;
|
|
@@ -3486,7 +3664,7 @@ function hooksDir(cwd) {
|
|
|
3486
3664
|
if (out2) return resolve2(cwd, out2);
|
|
3487
3665
|
} catch {
|
|
3488
3666
|
}
|
|
3489
|
-
return existsSync7(
|
|
3667
|
+
return existsSync7(join9(cwd, ".git")) ? join9(cwd, ".git", "hooks") : null;
|
|
3490
3668
|
}
|
|
3491
3669
|
function display(cwd, path) {
|
|
3492
3670
|
const rel = relative2(cwd, path);
|
|
@@ -3513,23 +3691,23 @@ function planPreCommit(cwd) {
|
|
|
3513
3691
|
const skip = (detail) => ({ item: "pre-commit", path: "(none)", status: "skip", detail });
|
|
3514
3692
|
let path;
|
|
3515
3693
|
let note;
|
|
3516
|
-
if (existsSync7(
|
|
3517
|
-
path =
|
|
3694
|
+
if (existsSync7(join9(cwd, ".husky"))) {
|
|
3695
|
+
path = join9(cwd, ".husky", "pre-commit");
|
|
3518
3696
|
note = "husky";
|
|
3519
3697
|
} else {
|
|
3520
|
-
const managed = MANAGED_HOOKS.find((m) => m.configs.some((c) => existsSync7(
|
|
3698
|
+
const managed = MANAGED_HOOKS.find((m) => m.configs.some((c) => existsSync7(join9(cwd, c))));
|
|
3521
3699
|
if (managed) {
|
|
3522
3700
|
return skip(`${managed.name} manages this repository's hooks and regenerates them on install, so a line in the hooks directory would not last \u2014 ${managed.hint}`);
|
|
3523
3701
|
}
|
|
3524
3702
|
const dir = hooksDir(cwd);
|
|
3525
3703
|
if (!dir) return skip("not a git repo and no .husky/ \u2014 nothing to wire");
|
|
3526
3704
|
if (basename(dir) === "_" && basename(dirname3(dir)) === ".husky") {
|
|
3527
|
-
path =
|
|
3705
|
+
path = join9(dirname3(dir), "pre-commit");
|
|
3528
3706
|
note = "husky";
|
|
3529
3707
|
} else {
|
|
3530
|
-
path =
|
|
3708
|
+
path = join9(dir, "pre-commit");
|
|
3531
3709
|
const rel2 = display(cwd, dir);
|
|
3532
|
-
note = rel2 ===
|
|
3710
|
+
note = rel2 === join9(".git", "hooks") ? "plain git hook (local-only: .git/hooks is not committed \u2014 consider husky to share it)" : `git hooks directory ${rel2} (core.hooksPath)`;
|
|
3533
3711
|
}
|
|
3534
3712
|
}
|
|
3535
3713
|
const rel = display(cwd, path);
|
|
@@ -3625,9 +3803,9 @@ ${block.join("\n")}
|
|
|
3625
3803
|
function planCodeowners(cwd) {
|
|
3626
3804
|
const rel = ".github/CODEOWNERS";
|
|
3627
3805
|
const existingRel = [".github/CODEOWNERS", "CODEOWNERS", "docs/CODEOWNERS"].find(
|
|
3628
|
-
(r) => existsSync7(
|
|
3806
|
+
(r) => existsSync7(join9(cwd, r))
|
|
3629
3807
|
);
|
|
3630
|
-
const path =
|
|
3808
|
+
const path = join9(cwd, existingRel ?? rel);
|
|
3631
3809
|
const existing = existingRel ? readFileSync10(path, "utf8") : null;
|
|
3632
3810
|
const missing = CODEOWNERS_PATHS.filter((p) => existing === null || !coveredBy(existing, p));
|
|
3633
3811
|
if (missing.length === 0) {
|
|
@@ -3661,7 +3839,7 @@ ${CODEOWNERS_MARK}
|
|
|
3661
3839
|
}
|
|
3662
3840
|
function planWorkflow(cwd, force) {
|
|
3663
3841
|
const rel = ".github/workflows/tamperward.yml";
|
|
3664
|
-
const path =
|
|
3842
|
+
const path = join9(cwd, rel);
|
|
3665
3843
|
const write = () => {
|
|
3666
3844
|
mkdirSync5(dirname3(path), { recursive: true });
|
|
3667
3845
|
writeFileSync5(path, workflowFile(WORKFLOW_CONTENT));
|
|
@@ -3778,13 +3956,13 @@ import { execFileSync as execFileSync7, spawnSync } from "node:child_process";
|
|
|
3778
3956
|
import { createHash as createHash8 } from "node:crypto";
|
|
3779
3957
|
import { chmodSync as chmodSync2, cpSync, lstatSync, mkdirSync as mkdirSync6, mkdtempSync as mkdtempSync2, readFileSync as readFileSync13, readlinkSync as readlinkSync2, rmSync as rmSync2, statSync as statSync7, symlinkSync, writeFileSync as writeFileSync6, existsSync as existsSync9 } from "node:fs";
|
|
3780
3958
|
import { tmpdir as tmpdir2 } from "node:os";
|
|
3781
|
-
import { dirname as dirname4, join as
|
|
3959
|
+
import { dirname as dirname4, join as join12, resolve as resolve3 } from "node:path";
|
|
3782
3960
|
|
|
3783
3961
|
// src/git/trusted.ts
|
|
3784
3962
|
import { execFileSync as execFileSync5 } from "node:child_process";
|
|
3785
3963
|
import { createHash as createHash6 } from "node:crypto";
|
|
3786
3964
|
import { existsSync as existsSync8, readFileSync as readFileSync11 } from "node:fs";
|
|
3787
|
-
import { isAbsolute as isAbsolute5, join as
|
|
3965
|
+
import { isAbsolute as isAbsolute5, join as join10 } from "node:path";
|
|
3788
3966
|
function trustedGitEnv() {
|
|
3789
3967
|
return { ...process.env, GIT_NO_REPLACE_OBJECTS: "1" };
|
|
3790
3968
|
}
|
|
@@ -3797,7 +3975,7 @@ function gitDirOf(cwd) {
|
|
|
3797
3975
|
env: trustedGitEnv()
|
|
3798
3976
|
}).trim();
|
|
3799
3977
|
if (!d) return null;
|
|
3800
|
-
return isAbsolute5(d) ? d :
|
|
3978
|
+
return isAbsolute5(d) ? d : join10(cwd, d);
|
|
3801
3979
|
} catch {
|
|
3802
3980
|
return null;
|
|
3803
3981
|
}
|
|
@@ -3822,7 +4000,7 @@ function objectRewriteState(cwd) {
|
|
|
3822
4000
|
parts.push(`${rel}:<no-git-dir>`);
|
|
3823
4001
|
continue;
|
|
3824
4002
|
}
|
|
3825
|
-
const p =
|
|
4003
|
+
const p = join10(dir, rel);
|
|
3826
4004
|
try {
|
|
3827
4005
|
parts.push(`${rel}:` + (existsSync8(p) ? readFileSync11(p, "utf8").trim() : ""));
|
|
3828
4006
|
} catch {
|
|
@@ -3836,7 +4014,7 @@ function objectRewriteState(cwd) {
|
|
|
3836
4014
|
import { execFileSync as execFileSync6 } from "node:child_process";
|
|
3837
4015
|
import { createHash as createHash7 } from "node:crypto";
|
|
3838
4016
|
import { readdirSync as readdirSync3, readFileSync as readFileSync12, readlinkSync, statSync as statSync6 } from "node:fs";
|
|
3839
|
-
import { join as
|
|
4017
|
+
import { join as join11 } from "node:path";
|
|
3840
4018
|
function treeFingerprint(cwd) {
|
|
3841
4019
|
const files = execFileSync6("git", ["ls-files", "-z", "--cached", "--others", "--exclude-standard"], {
|
|
3842
4020
|
cwd,
|
|
@@ -3849,7 +4027,7 @@ function treeFingerprint(cwd) {
|
|
|
3849
4027
|
h.update(rel);
|
|
3850
4028
|
h.update("\0");
|
|
3851
4029
|
try {
|
|
3852
|
-
h.update(readFileSync12(
|
|
4030
|
+
h.update(readFileSync12(join11(cwd, rel)));
|
|
3853
4031
|
} catch {
|
|
3854
4032
|
h.update("<unreadable>");
|
|
3855
4033
|
}
|
|
@@ -3858,7 +4036,7 @@ function treeFingerprint(cwd) {
|
|
|
3858
4036
|
return h.digest("hex");
|
|
3859
4037
|
}
|
|
3860
4038
|
function depsFingerprint(cwd) {
|
|
3861
|
-
const root =
|
|
4039
|
+
const root = join11(cwd, "node_modules");
|
|
3862
4040
|
const h = createHash7("sha256");
|
|
3863
4041
|
const walk = (dir) => {
|
|
3864
4042
|
let entries;
|
|
@@ -3868,7 +4046,7 @@ function depsFingerprint(cwd) {
|
|
|
3868
4046
|
return;
|
|
3869
4047
|
}
|
|
3870
4048
|
for (const e of [...entries].sort((a, b) => a.name.localeCompare(b.name))) {
|
|
3871
|
-
const p =
|
|
4049
|
+
const p = join11(dir, e.name);
|
|
3872
4050
|
if (e.isDirectory()) walk(p);
|
|
3873
4051
|
else if (e.isFile()) {
|
|
3874
4052
|
h.update(p);
|
|
@@ -3988,7 +4166,7 @@ function baseIsAncestorOfHead(base, cwd) {
|
|
|
3988
4166
|
function materialize(cwd, dest) {
|
|
3989
4167
|
const listed = git2(["ls-files", "-z", "--cached", "--others", "--exclude-standard"], cwd).split("\0").filter(Boolean);
|
|
3990
4168
|
for (const rel of listed) {
|
|
3991
|
-
const src =
|
|
4169
|
+
const src = join12(cwd, rel);
|
|
3992
4170
|
let st;
|
|
3993
4171
|
try {
|
|
3994
4172
|
st = statSync7(src);
|
|
@@ -3996,7 +4174,7 @@ function materialize(cwd, dest) {
|
|
|
3996
4174
|
continue;
|
|
3997
4175
|
}
|
|
3998
4176
|
if (!st.isFile()) continue;
|
|
3999
|
-
const out2 =
|
|
4177
|
+
const out2 = join12(dest, rel);
|
|
4000
4178
|
mkdirSync6(dirname4(out2), { recursive: true });
|
|
4001
4179
|
dropSymlink(out2);
|
|
4002
4180
|
if (lstatSync(src).isSymbolicLink()) {
|
|
@@ -4007,7 +4185,7 @@ function materialize(cwd, dest) {
|
|
|
4007
4185
|
chmodSync2(out2, st.mode);
|
|
4008
4186
|
}
|
|
4009
4187
|
const nm = resolve3(cwd, "node_modules");
|
|
4010
|
-
if (existsSync9(nm)) symlinkSync(nm,
|
|
4188
|
+
if (existsSync9(nm)) symlinkSync(nm, join12(dest, "node_modules"), "dir");
|
|
4011
4189
|
}
|
|
4012
4190
|
function dropSymlink(p) {
|
|
4013
4191
|
try {
|
|
@@ -4040,7 +4218,7 @@ function overlayDigest(dest, paths) {
|
|
|
4040
4218
|
for (const rel of [...paths].sort()) {
|
|
4041
4219
|
h.update(rel);
|
|
4042
4220
|
h.update("\0");
|
|
4043
|
-
const p =
|
|
4221
|
+
const p = join12(dest, rel);
|
|
4044
4222
|
try {
|
|
4045
4223
|
const st = lstatSync(p);
|
|
4046
4224
|
h.update(String(st.mode));
|
|
@@ -4067,7 +4245,7 @@ function overlayPristine(cwd, base, dest, policy, cmd) {
|
|
|
4067
4245
|
throw new Error(`the base's ${e.path} is a submodule; the pristine copy cannot reproduce it`);
|
|
4068
4246
|
}
|
|
4069
4247
|
const content = execFileSync7("git", ["show", `${base}:${e.path}`], { cwd, maxBuffer: 1 << 28, env: trustedGitEnv() });
|
|
4070
|
-
const out2 =
|
|
4248
|
+
const out2 = join12(dest, e.path);
|
|
4071
4249
|
mkdirSync6(dirname4(out2), { recursive: true });
|
|
4072
4250
|
rmSync2(out2, { force: true });
|
|
4073
4251
|
if (e.mode === "120000") {
|
|
@@ -4082,7 +4260,7 @@ function overlayPristine(cwd, base, dest, policy, cmd) {
|
|
|
4082
4260
|
const inCopy = git2(["ls-files", "-z", "--cached", "--others", "--exclude-standard"], cwd).split("\0").filter(Boolean);
|
|
4083
4261
|
for (const rel of inCopy) {
|
|
4084
4262
|
if (!isOverlay(rel) || baseProtected.has(rel)) continue;
|
|
4085
|
-
rmSync2(
|
|
4263
|
+
rmSync2(join12(dest, rel), { force: true });
|
|
4086
4264
|
removed++;
|
|
4087
4265
|
}
|
|
4088
4266
|
return { restored, removed };
|
|
@@ -4124,8 +4302,8 @@ function suiteEnv(scratch) {
|
|
|
4124
4302
|
if (DROPPED_ENV.has(k) || PINNED_NPM.test(k)) continue;
|
|
4125
4303
|
env[k] = v;
|
|
4126
4304
|
}
|
|
4127
|
-
const userRc =
|
|
4128
|
-
const globalRc =
|
|
4305
|
+
const userRc = join12(scratch, "npmrc-user");
|
|
4306
|
+
const globalRc = join12(scratch, "npmrc-global");
|
|
4129
4307
|
for (const f of [userRc, globalRc]) writeFileSync6(f, "", { mode: 292 });
|
|
4130
4308
|
env.npm_config_userconfig = userRc;
|
|
4131
4309
|
env.npm_config_globalconfig = globalRc;
|
|
@@ -4135,8 +4313,8 @@ function suiteEnv(scratch) {
|
|
|
4135
4313
|
}
|
|
4136
4314
|
function runSuite(dir, cmd, budgetSecs) {
|
|
4137
4315
|
const t0 = Date.now();
|
|
4138
|
-
const outDir = mkdtempSync2(
|
|
4139
|
-
const outFile =
|
|
4316
|
+
const outDir = mkdtempSync2(join12(tmpdir2(), "tw-verify-run-"));
|
|
4317
|
+
const outFile = join12(outDir, "result.json");
|
|
4140
4318
|
try {
|
|
4141
4319
|
spawnSync(process.execPath, ["-e", SUPERVISOR, cmd, String(budgetSecs * 1e3), outFile], {
|
|
4142
4320
|
cwd: dir,
|
|
@@ -4200,8 +4378,8 @@ function runVerify(opts) {
|
|
|
4200
4378
|
out2(`verify: cannot resolve base rev "${opts.base ?? "HEAD"}" \u2014 failing closed`);
|
|
4201
4379
|
return 2;
|
|
4202
4380
|
}
|
|
4203
|
-
const visRoot = mkdtempSync2(
|
|
4204
|
-
const visDir =
|
|
4381
|
+
const visRoot = mkdtempSync2(join12(tmpdir2(), "tw-verify-vis-"));
|
|
4382
|
+
const visDir = join12(visRoot, "t");
|
|
4205
4383
|
const cleanup = (dirs) => {
|
|
4206
4384
|
if (opts.keep) return;
|
|
4207
4385
|
for (const d of dirs) rmSync2(d, { recursive: true, force: true });
|
|
@@ -4229,8 +4407,8 @@ function runVerify(opts) {
|
|
|
4229
4407
|
out2("pristine run executes through it too. Failing closed, not open.");
|
|
4230
4408
|
return 2;
|
|
4231
4409
|
}
|
|
4232
|
-
const priRoot = mkdtempSync2(
|
|
4233
|
-
const priDir =
|
|
4410
|
+
const priRoot = mkdtempSync2(join12(tmpdir2(), "tw-verify-pri-"));
|
|
4411
|
+
const priDir = join12(priRoot, "t");
|
|
4234
4412
|
let restored = [];
|
|
4235
4413
|
let removedAdded = 0;
|
|
4236
4414
|
try {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tamperward",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.4.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",
|