tamperward 2.10.4 → 2.10.6
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 +467 -319
- package/package.json +1 -1
package/dist/cli/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/git/build.ts
|
|
4
|
-
import { execFileSync } from "node:child_process";
|
|
4
|
+
import { execFileSync as execFileSync2 } from "node:child_process";
|
|
5
5
|
|
|
6
6
|
// src/disk.ts
|
|
7
7
|
import { closeSync, constants, fstatSync, lstatSync, openSync, readSync, readlinkSync, realpathSync } from "node:fs";
|
|
@@ -602,10 +602,62 @@ function addHunks(content) {
|
|
|
602
602
|
];
|
|
603
603
|
}
|
|
604
604
|
|
|
605
|
+
// src/git/trusted.ts
|
|
606
|
+
import { execFileSync } from "node:child_process";
|
|
607
|
+
import { createHash } from "node:crypto";
|
|
608
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
609
|
+
import { isAbsolute, join as join2 } from "node:path";
|
|
610
|
+
function trustedGitEnv() {
|
|
611
|
+
return { ...process.env, GIT_NO_REPLACE_OBJECTS: "1" };
|
|
612
|
+
}
|
|
613
|
+
function gitDirOf(cwd) {
|
|
614
|
+
try {
|
|
615
|
+
const d = execFileSync("git", ["rev-parse", "--git-dir"], {
|
|
616
|
+
cwd,
|
|
617
|
+
encoding: "utf8",
|
|
618
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
619
|
+
env: trustedGitEnv()
|
|
620
|
+
}).trim();
|
|
621
|
+
if (!d) return null;
|
|
622
|
+
return isAbsolute(d) ? d : join2(cwd, d);
|
|
623
|
+
} catch {
|
|
624
|
+
return null;
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
function objectRewriteState(cwd) {
|
|
628
|
+
const parts = [];
|
|
629
|
+
try {
|
|
630
|
+
parts.push(
|
|
631
|
+
"replace:" + execFileSync("git", ["for-each-ref", "--format=%(refname) %(objectname)", "refs/replace/"], {
|
|
632
|
+
cwd,
|
|
633
|
+
encoding: "utf8",
|
|
634
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
635
|
+
env: trustedGitEnv()
|
|
636
|
+
}).trim()
|
|
637
|
+
);
|
|
638
|
+
} catch {
|
|
639
|
+
parts.push("replace:<unreadable>");
|
|
640
|
+
}
|
|
641
|
+
const dir = gitDirOf(cwd);
|
|
642
|
+
for (const rel of ["info/grafts", "shallow"]) {
|
|
643
|
+
if (dir === null) {
|
|
644
|
+
parts.push(`${rel}:<no-git-dir>`);
|
|
645
|
+
continue;
|
|
646
|
+
}
|
|
647
|
+
const p = join2(dir, rel);
|
|
648
|
+
try {
|
|
649
|
+
parts.push(`${rel}:` + (existsSync(p) ? readFileSync(p, "utf8").trim() : ""));
|
|
650
|
+
} catch {
|
|
651
|
+
parts.push(`${rel}:<unreadable>`);
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
return createHash("sha256").update(parts.join("\n")).digest("hex");
|
|
655
|
+
}
|
|
656
|
+
|
|
605
657
|
// src/git/build.ts
|
|
606
658
|
function git(args, cwd) {
|
|
607
659
|
try {
|
|
608
|
-
return
|
|
660
|
+
return execFileSync2("git", args, {
|
|
609
661
|
cwd,
|
|
610
662
|
encoding: "utf8",
|
|
611
663
|
// 256 MiB, matching the hook adapter. Every diff below runs with `--text`, so a
|
|
@@ -614,7 +666,12 @@ function git(args, cwd) {
|
|
|
614
666
|
// the cap git() throws and the view fails CLOSED (a truncated patch would parse
|
|
615
667
|
// as a smaller change than the one landing), which is the documented stance.
|
|
616
668
|
maxBuffer: 256 * 1024 * 1024,
|
|
617
|
-
stdio: ["ignore", "pipe", "pipe"]
|
|
669
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
670
|
+
// Every object read in this shared adapter can feed a trust decision:
|
|
671
|
+
// policy-at-base, merge-base selection, range enrichment, and the HEAD
|
|
672
|
+
// side of worktree/staged comparisons. A pre-existing replace ref must
|
|
673
|
+
// never make those callers see an attacker-selected object.
|
|
674
|
+
env: trustedGitEnv()
|
|
618
675
|
});
|
|
619
676
|
} catch (e) {
|
|
620
677
|
const err2 = e;
|
|
@@ -765,6 +822,104 @@ function hiddenTrackedPaths(opts = {}) {
|
|
|
765
822
|
return out2;
|
|
766
823
|
}
|
|
767
824
|
|
|
825
|
+
// src/adapters/claude/changes.ts
|
|
826
|
+
import { execFileSync as execFileSync3 } from "node:child_process";
|
|
827
|
+
import { writeFileSync, mkdtempSync, rmSync } from "node:fs";
|
|
828
|
+
import { tmpdir } from "node:os";
|
|
829
|
+
import { isAbsolute as isAbsolute2, join as join3, relative, resolve } from "node:path";
|
|
830
|
+
function asStr(v) {
|
|
831
|
+
return typeof v === "string" ? v : "";
|
|
832
|
+
}
|
|
833
|
+
function readDisk(path) {
|
|
834
|
+
return textOf(inspectResolved(path));
|
|
835
|
+
}
|
|
836
|
+
function abs(path, cwd) {
|
|
837
|
+
return resolve(cwd, path);
|
|
838
|
+
}
|
|
839
|
+
function relForDisplay(path, cwd) {
|
|
840
|
+
const rel = relative(cwd, path);
|
|
841
|
+
return rel && !rel.startsWith("..") && !isAbsolute2(rel) ? rel : path;
|
|
842
|
+
}
|
|
843
|
+
function applyEdit(content, oldStr, newStr) {
|
|
844
|
+
if (content === null) return newStr;
|
|
845
|
+
if (oldStr !== "" && !content.includes(oldStr)) return content + "\n" + newStr;
|
|
846
|
+
return content.replace(oldStr, () => newStr);
|
|
847
|
+
}
|
|
848
|
+
function synthFileChange(displayPath, before, after) {
|
|
849
|
+
if (before === after) return [];
|
|
850
|
+
let op;
|
|
851
|
+
if (before === null) op = "add";
|
|
852
|
+
else if (after === null) op = "delete";
|
|
853
|
+
else op = "modify";
|
|
854
|
+
const dir = mkdtempSync(join3(tmpdir(), "hf-"));
|
|
855
|
+
let raw = "";
|
|
856
|
+
try {
|
|
857
|
+
const a = join3(dir, "a");
|
|
858
|
+
const b = join3(dir, "b");
|
|
859
|
+
writeFileSync(a, before ?? "");
|
|
860
|
+
writeFileSync(b, after ?? "");
|
|
861
|
+
try {
|
|
862
|
+
raw = execFileSync3("git", ["diff", "--no-index", "--no-color", "--text", a, b], {
|
|
863
|
+
encoding: "utf8",
|
|
864
|
+
maxBuffer: 256 * 1024 * 1024
|
|
865
|
+
});
|
|
866
|
+
} catch (e) {
|
|
867
|
+
const err2 = e;
|
|
868
|
+
if (err2.status !== 1) {
|
|
869
|
+
throw new Error(`cannot diff the incoming edit to ${displayPath}: ${err2.code ?? err2.message ?? String(e)}`);
|
|
870
|
+
}
|
|
871
|
+
raw = err2.stdout ? String(err2.stdout) : "";
|
|
872
|
+
}
|
|
873
|
+
} finally {
|
|
874
|
+
rmSync(dir, { recursive: true, force: true });
|
|
875
|
+
}
|
|
876
|
+
const first = parseDiff(raw)[0];
|
|
877
|
+
const hunks = first && first.kind === "file" ? first.hunks : [];
|
|
878
|
+
return [{ kind: "file", path: displayPath, oldPath: null, op, before, after, binary: false, hunks }];
|
|
879
|
+
}
|
|
880
|
+
function changesFromClaudeHook(input, cwd) {
|
|
881
|
+
const ti = input.tool_input ?? {};
|
|
882
|
+
switch (input.tool_name) {
|
|
883
|
+
case "Bash": {
|
|
884
|
+
const raw = asStr(ti.command);
|
|
885
|
+
return raw ? [{ kind: "command", raw, argv: raw.split(/\s+/) }] : [];
|
|
886
|
+
}
|
|
887
|
+
case "Write": {
|
|
888
|
+
const fp = asStr(ti.file_path);
|
|
889
|
+
if (!fp) return [];
|
|
890
|
+
const before = readDisk(abs(fp, cwd));
|
|
891
|
+
return synthFileChange(relForDisplay(abs(fp, cwd), cwd), before, asStr(ti.content));
|
|
892
|
+
}
|
|
893
|
+
case "Edit": {
|
|
894
|
+
const fp = asStr(ti.file_path);
|
|
895
|
+
if (!fp) return [];
|
|
896
|
+
const before = readDisk(abs(fp, cwd));
|
|
897
|
+
const after = applyEdit(before, asStr(ti.old_string), asStr(ti.new_string));
|
|
898
|
+
return synthFileChange(relForDisplay(abs(fp, cwd), cwd), before, after);
|
|
899
|
+
}
|
|
900
|
+
case "MultiEdit": {
|
|
901
|
+
const fp = asStr(ti.file_path);
|
|
902
|
+
if (!fp) return [];
|
|
903
|
+
const before = readDisk(abs(fp, cwd));
|
|
904
|
+
let after = before;
|
|
905
|
+
const edits = Array.isArray(ti.edits) ? ti.edits : [];
|
|
906
|
+
for (const raw of edits) {
|
|
907
|
+
const ed = raw;
|
|
908
|
+
after = applyEdit(after, ed.old_string ?? "", ed.new_string ?? "");
|
|
909
|
+
}
|
|
910
|
+
return synthFileChange(relForDisplay(abs(fp, cwd), cwd), before, after);
|
|
911
|
+
}
|
|
912
|
+
case "NotebookEdit": {
|
|
913
|
+
const fp = asStr(ti.notebook_path);
|
|
914
|
+
const src = asStr(ti.new_source);
|
|
915
|
+
if (!fp || !src) return [];
|
|
916
|
+
return synthFileChange(relForDisplay(abs(fp, cwd), cwd), "", src);
|
|
917
|
+
}
|
|
918
|
+
default:
|
|
919
|
+
return [];
|
|
920
|
+
}
|
|
921
|
+
}
|
|
922
|
+
|
|
768
923
|
// src/detectors/finding.ts
|
|
769
924
|
function severityOf(rule, policy, fallback = "block") {
|
|
770
925
|
return policy.rules?.[rule]?.severity ?? fallback;
|
|
@@ -1360,9 +1515,9 @@ import picomatch2 from "picomatch";
|
|
|
1360
1515
|
import ts2 from "typescript";
|
|
1361
1516
|
|
|
1362
1517
|
// src/detectors/repo.ts
|
|
1363
|
-
import { execFileSync as
|
|
1364
|
-
import { readFileSync } from "node:fs";
|
|
1365
|
-
import { join as
|
|
1518
|
+
import { execFileSync as execFileSync4 } from "node:child_process";
|
|
1519
|
+
import { readFileSync as readFileSync2 } from "node:fs";
|
|
1520
|
+
import { join as join4 } from "node:path";
|
|
1366
1521
|
var cache = /* @__PURE__ */ new Map();
|
|
1367
1522
|
function trackedFiles(ctx) {
|
|
1368
1523
|
if (!ctx) return null;
|
|
@@ -1372,7 +1527,7 @@ function trackedFiles(ctx) {
|
|
|
1372
1527
|
if (hit !== void 0) return hit;
|
|
1373
1528
|
let files = null;
|
|
1374
1529
|
try {
|
|
1375
|
-
files =
|
|
1530
|
+
files = execFileSync4("git", ["ls-files", "-z", "--cached", "--others", "--exclude-standard"], {
|
|
1376
1531
|
cwd: ctx.cwd,
|
|
1377
1532
|
encoding: "utf8",
|
|
1378
1533
|
maxBuffer: 64 * 1024 * 1024,
|
|
@@ -1394,7 +1549,7 @@ function trackedContent(path, ctx) {
|
|
|
1394
1549
|
if (hit !== void 0) return hit;
|
|
1395
1550
|
let src = null;
|
|
1396
1551
|
try {
|
|
1397
|
-
src =
|
|
1552
|
+
src = readFileSync2(join4(ctx.cwd, path), "utf8");
|
|
1398
1553
|
} catch {
|
|
1399
1554
|
src = null;
|
|
1400
1555
|
}
|
|
@@ -1409,7 +1564,7 @@ function branchRefs(ctx) {
|
|
|
1409
1564
|
if (hit !== void 0) return hit;
|
|
1410
1565
|
let refs = null;
|
|
1411
1566
|
try {
|
|
1412
|
-
const out2 =
|
|
1567
|
+
const out2 = execFileSync4("git", ["for-each-ref", "--format=%(refname:short)", "refs/heads", "refs/remotes"], {
|
|
1413
1568
|
cwd: ctx.cwd,
|
|
1414
1569
|
encoding: "utf8",
|
|
1415
1570
|
stdio: ["ignore", "pipe", "pipe"]
|
|
@@ -1432,7 +1587,7 @@ function revIsHead(rev, ctx) {
|
|
|
1432
1587
|
if (!ctx?.cwd || rev.startsWith("-")) return null;
|
|
1433
1588
|
const parse3 = (r) => {
|
|
1434
1589
|
try {
|
|
1435
|
-
return
|
|
1590
|
+
return execFileSync4("git", ["rev-parse", "--verify", "--quiet", "--end-of-options", `${r}^{commit}`], {
|
|
1436
1591
|
cwd: ctx.cwd,
|
|
1437
1592
|
encoding: "utf8",
|
|
1438
1593
|
stdio: ["ignore", "pipe", "pipe"]
|
|
@@ -1452,7 +1607,7 @@ function defaultBranch(ctx) {
|
|
|
1452
1607
|
if (hit !== void 0) return hit;
|
|
1453
1608
|
let name = null;
|
|
1454
1609
|
try {
|
|
1455
|
-
const ref =
|
|
1610
|
+
const ref = execFileSync4("git", ["symbolic-ref", "-q", "--short", "refs/remotes/origin/HEAD"], {
|
|
1456
1611
|
cwd: ctx.cwd,
|
|
1457
1612
|
encoding: "utf8",
|
|
1458
1613
|
stdio: ["ignore", "pipe", "pipe"]
|
|
@@ -2706,20 +2861,20 @@ var ciTampering = {
|
|
|
2706
2861
|
};
|
|
2707
2862
|
|
|
2708
2863
|
// src/detectors/hook-tampering.ts
|
|
2709
|
-
import { existsSync as
|
|
2710
|
-
import { isAbsolute as
|
|
2864
|
+
import { existsSync as existsSync3, readFileSync as readFileSync4 } from "node:fs";
|
|
2865
|
+
import { isAbsolute as isAbsolute5, join as join6, relative as relative2 } from "node:path";
|
|
2711
2866
|
|
|
2712
2867
|
// src/wiring.ts
|
|
2713
|
-
import { readFileSync as
|
|
2868
|
+
import { readFileSync as readFileSync3, realpathSync as realpathSync2 } from "node:fs";
|
|
2714
2869
|
import { homedir } from "node:os";
|
|
2715
|
-
import { basename, dirname, isAbsolute, join as
|
|
2870
|
+
import { basename, dirname, isAbsolute as isAbsolute3, join as join5, posix } from "node:path";
|
|
2716
2871
|
import { fileURLToPath } from "node:url";
|
|
2717
2872
|
function shippedVersion() {
|
|
2718
2873
|
try {
|
|
2719
2874
|
const here = dirname(fileURLToPath(import.meta.url));
|
|
2720
2875
|
for (const rel of ["../package.json", "../../package.json", "../../../package.json"]) {
|
|
2721
2876
|
try {
|
|
2722
|
-
const pkg = JSON.parse(
|
|
2877
|
+
const pkg = JSON.parse(readFileSync3(join5(here, rel), "utf8"));
|
|
2723
2878
|
if (pkg.name === "tamperward" && pkg.version) return pkg.version;
|
|
2724
2879
|
} catch {
|
|
2725
2880
|
}
|
|
@@ -2732,10 +2887,12 @@ var TW_VERSION = shippedVersion();
|
|
|
2732
2887
|
var PRE_TOOLS = ["Bash", "Edit", "Write", "MultiEdit", "NotebookEdit"];
|
|
2733
2888
|
var PRE_MATCHER = PRE_TOOLS.join("|");
|
|
2734
2889
|
var MARKER = "# tamperward: block agent shortcuts before they land";
|
|
2735
|
-
var
|
|
2736
|
-
var
|
|
2737
|
-
var
|
|
2738
|
-
var
|
|
2890
|
+
var NPX_AUTHORITY = "npx --yes --global --userconfig=/dev/null --globalconfig=/dev/null/npmrc-global --registry=https://registry.npmjs.org/ --node-options=' ' --script-shell= --ignore-scripts --offline=false --prefer-online";
|
|
2891
|
+
var AUTHORITY_FAIL_CLOSED = " || (echo tamperward: authority failed to start >&2 && exit 2)";
|
|
2892
|
+
var HOOK_CMD = `${NPX_AUTHORITY} tamperward@${TW_VERSION} hook claude${AUTHORITY_FAIL_CLOSED}`;
|
|
2893
|
+
var SWEEP_CMD = `${NPX_AUTHORITY} tamperward@${TW_VERSION} sweep claude${AUTHORITY_FAIL_CLOSED}`;
|
|
2894
|
+
var PRECOMMIT_CMD = `${NPX_AUTHORITY} tamperward@${TW_VERSION} check --staged`;
|
|
2895
|
+
var OURS = /^\s*npx --yes --global --userconfig=\/dev\/null --globalconfig=\/dev\/null\/npmrc-global --registry=https:\/\/registry\.npmjs\.org\/ --node-options=' ' --script-shell= --ignore-scripts --offline=false --prefer-online tamperward(?:@(\S+))? (?:(hook claude|sweep claude) \|\| \(echo tamperward: authority failed to start >&2 && exit 2\)|(check --staged))\s*$/;
|
|
2739
2896
|
var PLAIN_SEMVER = /^(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)$/;
|
|
2740
2897
|
function compareVersions(a, b) {
|
|
2741
2898
|
if (!PLAIN_SEMVER.test(a) || !PLAIN_SEMVER.test(b)) return null;
|
|
@@ -2749,7 +2906,7 @@ function pinNotBelow(now, floor) {
|
|
|
2749
2906
|
const c = compareVersions(now, floor);
|
|
2750
2907
|
return c !== null && c >= 0;
|
|
2751
2908
|
}
|
|
2752
|
-
var INIT_SCRIPT = /^#!\/bin\/sh\n# tamperward: block agent shortcuts before they land\nnpx --yes tamperward(?:@(\S+))? check --staged\n*$/;
|
|
2909
|
+
var INIT_SCRIPT = /^#!\/bin\/sh\n# tamperward: block agent shortcuts before they land\nnpx --yes --global --userconfig=\/dev\/null --globalconfig=\/dev\/null\/npmrc-global --registry=https:\/\/registry\.npmjs\.org\/ --node-options=' ' --script-shell= --ignore-scripts --offline=false --prefer-online tamperward(?:@(\S+))? check --staged\n*$/;
|
|
2753
2910
|
function initScriptPin(src) {
|
|
2754
2911
|
const m = src.match(INIT_SCRIPT);
|
|
2755
2912
|
return m ? m[1] ?? "" : null;
|
|
@@ -2787,7 +2944,7 @@ function canonicalPath(path) {
|
|
|
2787
2944
|
}
|
|
2788
2945
|
function resolvesToClaudeSettings(path, env = process.env) {
|
|
2789
2946
|
if (isClaudeSettings(path, env)) return true;
|
|
2790
|
-
return
|
|
2947
|
+
return isAbsolute3(path) && isClaudeSettings(canonicalPath(path), env);
|
|
2791
2948
|
}
|
|
2792
2949
|
|
|
2793
2950
|
// src/detectors/policy-diff.ts
|
|
@@ -2915,8 +3072,8 @@ function policyAddWeakening(after) {
|
|
|
2915
3072
|
}
|
|
2916
3073
|
|
|
2917
3074
|
// src/detectors/hook-wiring.ts
|
|
2918
|
-
import { existsSync } from "node:fs";
|
|
2919
|
-
import { isAbsolute as
|
|
3075
|
+
import { existsSync as existsSync2 } from "node:fs";
|
|
3076
|
+
import { isAbsolute as isAbsolute4, resolve as resolve2 } from "node:path";
|
|
2920
3077
|
import { parse as parseYaml } from "yaml";
|
|
2921
3078
|
function stripComment(line) {
|
|
2922
3079
|
let single = false;
|
|
@@ -4030,16 +4187,16 @@ function expandShellPath(t, env = process.env) {
|
|
|
4030
4187
|
}
|
|
4031
4188
|
function hookTests(policy, ctx) {
|
|
4032
4189
|
const cwd = ctx?.cwd;
|
|
4033
|
-
const onDisk = cwd !== void 0 &&
|
|
4190
|
+
const onDisk = cwd !== void 0 && existsSync2(cwd);
|
|
4034
4191
|
const root = onDisk ? canonicalPath(cwd) : cwd;
|
|
4035
4192
|
const memo = /* @__PURE__ */ new Map();
|
|
4036
4193
|
const rel = (t) => {
|
|
4037
4194
|
const hit = memo.get(t);
|
|
4038
4195
|
if (hit !== void 0) return hit;
|
|
4039
4196
|
let p = expandShellPath(t.replace(/^["']|["']$/g, ""));
|
|
4040
|
-
const pathy =
|
|
4041
|
-
if (pathy && (
|
|
4042
|
-
p = canonicalPath(
|
|
4197
|
+
const pathy = isAbsolute4(p) || p.includes("/") && !/^[-:]/.test(p);
|
|
4198
|
+
if (pathy && (isAbsolute4(p) || onDisk)) {
|
|
4199
|
+
p = canonicalPath(isAbsolute4(p) ? p : resolve2(cwd, p));
|
|
4043
4200
|
if (root !== void 0) {
|
|
4044
4201
|
if (p === root) p = ".";
|
|
4045
4202
|
else if (p.startsWith(root + "/")) p = p.slice(root.length + 1);
|
|
@@ -4226,7 +4383,6 @@ var declares = (s, event) => eventEntries(s, event) !== null;
|
|
|
4226
4383
|
var GATE_KEYS = /* @__PURE__ */ new Set(["type", "command", "timeout", "statusMessage"]);
|
|
4227
4384
|
var HOOK_TYPES = /* @__PURE__ */ new Set(["command", "prompt", "agent", "http"]);
|
|
4228
4385
|
var SIBLING_KEYS = GATE_KEYS;
|
|
4229
|
-
var CANONICAL_CMD = /^npx --yes tamperward@(\S+) (hook|sweep) claude$/;
|
|
4230
4386
|
var MENTIONS_GATE = /\btamperward\b.*\b(?:hook|sweep)\b/;
|
|
4231
4387
|
var FORGERY = /hookSpecificOutput|permissionDecision|updatedInput|"decision"|"continue"/;
|
|
4232
4388
|
var ENV_MECHANISM = [
|
|
@@ -4244,10 +4400,11 @@ function analyseGateCommand(cmd, event) {
|
|
|
4244
4400
|
const want = event === "PreToolUse" ? "hook" : "sweep";
|
|
4245
4401
|
const problems = [];
|
|
4246
4402
|
let runs = true;
|
|
4247
|
-
const canonical2 = cmd.match(
|
|
4403
|
+
const canonical2 = cmd.match(OURS);
|
|
4248
4404
|
if (canonical2) {
|
|
4249
|
-
|
|
4250
|
-
|
|
4405
|
+
const kind = canonical2[2];
|
|
4406
|
+
if (kind !== `${want} claude`) {
|
|
4407
|
+
problems.push(`runs \`${kind}\` under ${event}`);
|
|
4251
4408
|
runs = false;
|
|
4252
4409
|
}
|
|
4253
4410
|
if (!PLAIN_SEMVER.test(canonical2[1])) {
|
|
@@ -4597,8 +4754,8 @@ function lefthookBase(c, changes, ctx) {
|
|
|
4597
4754
|
if (!ctx?.cwd) return null;
|
|
4598
4755
|
for (const n of names) {
|
|
4599
4756
|
try {
|
|
4600
|
-
const p =
|
|
4601
|
-
if (
|
|
4757
|
+
const p = join6(ctx.cwd, n);
|
|
4758
|
+
if (existsSync3(p)) return parseDoc(readFileSync4(p, "utf8"));
|
|
4602
4759
|
} catch {
|
|
4603
4760
|
}
|
|
4604
4761
|
}
|
|
@@ -4668,9 +4825,9 @@ function updateIndexHiding(toks, policy) {
|
|
|
4668
4825
|
return null;
|
|
4669
4826
|
}
|
|
4670
4827
|
function repoRelative(path, ctx) {
|
|
4671
|
-
if (!ctx?.cwd || !
|
|
4672
|
-
const rel =
|
|
4673
|
-
return rel && !rel.startsWith("..") && !
|
|
4828
|
+
if (!ctx?.cwd || !isAbsolute5(path)) return path;
|
|
4829
|
+
const rel = relative2(ctx.cwd, path);
|
|
4830
|
+
return rel && !rel.startsWith("..") && !isAbsolute5(rel) ? rel : path;
|
|
4674
4831
|
}
|
|
4675
4832
|
var HUSKY_SHIM = '#!/usr/bin/env sh\n. "$(dirname "$0")/h"';
|
|
4676
4833
|
var HUSKY_SHIM_NAMES = /* @__PURE__ */ new Set(["applypatch-msg", "commit-msg", "post-applypatch", "post-checkout", "post-commit", "post-merge", "post-rewrite", "pre-applypatch", "pre-auto-gc", "pre-commit", "pre-merge-commit", "pre-push", "pre-rebase", "prepare-commit-msg"]);
|
|
@@ -4719,7 +4876,7 @@ function huskyRuntimeWrite(path, content, ctx) {
|
|
|
4719
4876
|
if (!ok) return false;
|
|
4720
4877
|
if (!ctx?.cwd) return false;
|
|
4721
4878
|
const root = path.slice(0, path.length - m[0].length + (m[0].startsWith("/") ? 1 : 0));
|
|
4722
|
-
const entry = inspectRel(
|
|
4879
|
+
const entry = inspectRel(join6(ctx.cwd, root), ".husky/pre-commit");
|
|
4723
4880
|
if (entry.kind !== "file") return false;
|
|
4724
4881
|
const gate = textOf(entry);
|
|
4725
4882
|
if (gate == null) return false;
|
|
@@ -4972,7 +5129,7 @@ var hookTampering = {
|
|
|
4972
5129
|
if (why) {
|
|
4973
5130
|
const target = shellHookTarget(seg, toks, policy, ctx) ?? shellHookTarget("", fed, policy, ctx);
|
|
4974
5131
|
const file = target ? repoRelative(target, ctx) : void 0;
|
|
4975
|
-
const outside = file !== void 0 && (
|
|
5132
|
+
const outside = file !== void 0 && (isAbsolute5(file) || /^[A-Za-z]:\//.test(file)) && isClaudeSettings(file) ? ` \u2014 ${file} is a Claude Code settings file the runtime reads hooks from` : "";
|
|
4976
5133
|
out2.push(
|
|
4977
5134
|
makeFinding(RULE6, policy, {
|
|
4978
5135
|
...file !== void 0 ? { file } : {},
|
|
@@ -6099,8 +6256,8 @@ function hasBlocking(findings) {
|
|
|
6099
6256
|
}
|
|
6100
6257
|
|
|
6101
6258
|
// src/policy-load.ts
|
|
6102
|
-
import { readFileSync as
|
|
6103
|
-
import { isAbsolute as
|
|
6259
|
+
import { readFileSync as readFileSync5, existsSync as existsSync4 } from "node:fs";
|
|
6260
|
+
import { isAbsolute as isAbsolute6, join as join7, posix as posix2 } from "node:path";
|
|
6104
6261
|
import { parse as parse2 } from "yaml";
|
|
6105
6262
|
var PolicyError = class extends Error {
|
|
6106
6263
|
};
|
|
@@ -6117,7 +6274,7 @@ var isStringList = (v) => Array.isArray(v) && v.every((s) => typeof s === "strin
|
|
|
6117
6274
|
var isMapping = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
6118
6275
|
var TOP_LEVEL_KEYS = /* @__PURE__ */ new Set(["version", "protected", "rules", "ignore", "signoff", "verify"]);
|
|
6119
6276
|
function ledgerInsideRepo(ledger) {
|
|
6120
|
-
if (!ledger.trim() ||
|
|
6277
|
+
if (!ledger.trim() || isAbsolute6(ledger) || /^[A-Za-z]:[\\/]/.test(ledger)) return false;
|
|
6121
6278
|
const norm2 = posix2.normalize(ledger.replace(/\\/g, "/"));
|
|
6122
6279
|
return norm2 !== ".." && !norm2.startsWith("../") && !norm2.startsWith("/");
|
|
6123
6280
|
}
|
|
@@ -6224,9 +6381,9 @@ function parseOrThrow(src, where) {
|
|
|
6224
6381
|
return parsePolicy(raw, where);
|
|
6225
6382
|
}
|
|
6226
6383
|
function loadPolicy(cwd = process.cwd()) {
|
|
6227
|
-
const path =
|
|
6228
|
-
if (!
|
|
6229
|
-
return parseOrThrow(
|
|
6384
|
+
const path = join7(cwd, POLICY_FILE);
|
|
6385
|
+
if (!existsSync4(path)) return defaultPolicy();
|
|
6386
|
+
return parseOrThrow(readFileSync5(path, "utf8"), POLICY_FILE);
|
|
6230
6387
|
}
|
|
6231
6388
|
function loadPolicyAt(rev, cwd) {
|
|
6232
6389
|
const src = fileAt(rev, POLICY_FILE, { cwd });
|
|
@@ -6235,12 +6392,12 @@ function loadPolicyAt(rev, cwd) {
|
|
|
6235
6392
|
}
|
|
6236
6393
|
|
|
6237
6394
|
// src/signoff.ts
|
|
6238
|
-
import { createHash } from "node:crypto";
|
|
6239
|
-
import { appendFileSync, mkdirSync, readFileSync as
|
|
6240
|
-
import { dirname as dirname2, join as
|
|
6395
|
+
import { createHash as createHash2 } from "node:crypto";
|
|
6396
|
+
import { appendFileSync, mkdirSync, readFileSync as readFileSync6, existsSync as existsSync5 } from "node:fs";
|
|
6397
|
+
import { dirname as dirname2, join as join8 } from "node:path";
|
|
6241
6398
|
var DEFAULT_TTL_MS = 30 * 24 * 60 * 60 * 1e3;
|
|
6242
6399
|
function fingerprint(rule, file, evidence) {
|
|
6243
|
-
return
|
|
6400
|
+
return createHash2("sha256").update(`${rule}\0${file ?? ""}\0${evidence}`).digest("hex").slice(0, 16);
|
|
6244
6401
|
}
|
|
6245
6402
|
var fingerprintOf = (f) => fingerprint(f.rule, f.file, f.evidence);
|
|
6246
6403
|
function ledgerPath(cwd, policy) {
|
|
@@ -6248,13 +6405,13 @@ function ledgerPath(cwd, policy) {
|
|
|
6248
6405
|
if (!ledgerInsideRepo(rel)) {
|
|
6249
6406
|
throw new PolicyError(`signoff.ledger must be a relative path inside the repository, got ${JSON.stringify(rel)}`);
|
|
6250
6407
|
}
|
|
6251
|
-
return
|
|
6408
|
+
return join8(cwd, rel);
|
|
6252
6409
|
}
|
|
6253
6410
|
function readLedger(cwd, policy) {
|
|
6254
6411
|
const p = ledgerPath(cwd, policy);
|
|
6255
|
-
if (!
|
|
6412
|
+
if (!existsSync5(p)) return [];
|
|
6256
6413
|
const out2 = [];
|
|
6257
|
-
for (const line of
|
|
6414
|
+
for (const line of readFileSync6(p, "utf8").split("\n")) {
|
|
6258
6415
|
if (!line.trim()) continue;
|
|
6259
6416
|
try {
|
|
6260
6417
|
const e = JSON.parse(line);
|
|
@@ -6294,7 +6451,9 @@ function oobToken(want, oob, head) {
|
|
|
6294
6451
|
}
|
|
6295
6452
|
const [rule, sha2] = [t.slice(0, at), t.slice(at + 1)];
|
|
6296
6453
|
if (rule !== want) continue;
|
|
6297
|
-
|
|
6454
|
+
const normalizedHead = head?.trim().toLowerCase();
|
|
6455
|
+
const normalizedSha = sha2.trim().toLowerCase();
|
|
6456
|
+
if (!head || /^[0-9a-f]+$/.test(normalizedHead) && normalizedSha === normalizedHead) return t;
|
|
6298
6457
|
}
|
|
6299
6458
|
return null;
|
|
6300
6459
|
}
|
|
@@ -6310,8 +6469,8 @@ function applyOobSignoffs(findings, oob, head) {
|
|
|
6310
6469
|
return { findings: remaining, cleared };
|
|
6311
6470
|
}
|
|
6312
6471
|
function oobHeadFromEnv(env = process.env) {
|
|
6313
|
-
const v = (env.TAMPERWARD_OOB_HEAD ?? "").trim();
|
|
6314
|
-
return v
|
|
6472
|
+
const v = (env.TAMPERWARD_OOB_HEAD ?? "").trim().toLowerCase();
|
|
6473
|
+
return /^(?:[0-9a-f]{40}|[0-9a-f]{64})$/.test(v) ? v : void 0;
|
|
6315
6474
|
}
|
|
6316
6475
|
function oobFromEnv(env = process.env) {
|
|
6317
6476
|
return (env.TAMPERWARD_OOB_SIGNOFF ?? "").split(",").map((s) => s.trim().replace(/^tamperward:allow:/, "")).filter(Boolean);
|
|
@@ -6573,6 +6732,11 @@ function check(opts) {
|
|
|
6573
6732
|
} else if (opts.worktree) {
|
|
6574
6733
|
const p = policy;
|
|
6575
6734
|
changes = opts.includeUntracked ? diffWorktreeWithUntracked({ cwd: opts.cwd }, (rel) => isProtected(rel, p)) : diffWorktree({ cwd: opts.cwd });
|
|
6735
|
+
const seen = new Set(changes.filter((c) => c.kind === "file").map((c) => c.path));
|
|
6736
|
+
for (const rel of hiddenTrackedPaths({ cwd: opts.cwd })) {
|
|
6737
|
+
if (seen.has(rel) || !isProtected(rel, p)) continue;
|
|
6738
|
+
changes.push(...synthFileChange(rel, fileAt("HEAD", rel, { cwd: opts.cwd }), fileOnDisk(rel, { cwd: opts.cwd })));
|
|
6739
|
+
}
|
|
6576
6740
|
layer = opts.ciLayer ? "ci" : "local";
|
|
6577
6741
|
view = "worktree";
|
|
6578
6742
|
} else if (opts.diff) {
|
|
@@ -6624,106 +6788,8 @@ function runCheck(opts) {
|
|
|
6624
6788
|
}
|
|
6625
6789
|
|
|
6626
6790
|
// src/cli/hook.ts
|
|
6627
|
-
import { readFileSync as
|
|
6628
|
-
import { createHash as
|
|
6629
|
-
|
|
6630
|
-
// src/adapters/claude/changes.ts
|
|
6631
|
-
import { execFileSync as execFileSync3 } from "node:child_process";
|
|
6632
|
-
import { writeFileSync, mkdtempSync, rmSync } from "node:fs";
|
|
6633
|
-
import { tmpdir } from "node:os";
|
|
6634
|
-
import { isAbsolute as isAbsolute5, join as join7, relative as relative2, resolve as resolve2 } from "node:path";
|
|
6635
|
-
function asStr(v) {
|
|
6636
|
-
return typeof v === "string" ? v : "";
|
|
6637
|
-
}
|
|
6638
|
-
function readDisk(path) {
|
|
6639
|
-
return textOf(inspectResolved(path));
|
|
6640
|
-
}
|
|
6641
|
-
function abs(path, cwd) {
|
|
6642
|
-
return resolve2(cwd, path);
|
|
6643
|
-
}
|
|
6644
|
-
function relForDisplay(path, cwd) {
|
|
6645
|
-
const rel = relative2(cwd, path);
|
|
6646
|
-
return rel && !rel.startsWith("..") && !isAbsolute5(rel) ? rel : path;
|
|
6647
|
-
}
|
|
6648
|
-
function applyEdit(content, oldStr, newStr) {
|
|
6649
|
-
if (content === null) return newStr;
|
|
6650
|
-
if (oldStr !== "" && !content.includes(oldStr)) return content + "\n" + newStr;
|
|
6651
|
-
return content.replace(oldStr, () => newStr);
|
|
6652
|
-
}
|
|
6653
|
-
function synthFileChange(displayPath, before, after) {
|
|
6654
|
-
if (before === after) return [];
|
|
6655
|
-
let op;
|
|
6656
|
-
if (before === null) op = "add";
|
|
6657
|
-
else if (after === null) op = "delete";
|
|
6658
|
-
else op = "modify";
|
|
6659
|
-
const dir = mkdtempSync(join7(tmpdir(), "hf-"));
|
|
6660
|
-
let raw = "";
|
|
6661
|
-
try {
|
|
6662
|
-
const a = join7(dir, "a");
|
|
6663
|
-
const b = join7(dir, "b");
|
|
6664
|
-
writeFileSync(a, before ?? "");
|
|
6665
|
-
writeFileSync(b, after ?? "");
|
|
6666
|
-
try {
|
|
6667
|
-
raw = execFileSync3("git", ["diff", "--no-index", "--no-color", "--text", a, b], {
|
|
6668
|
-
encoding: "utf8",
|
|
6669
|
-
maxBuffer: 256 * 1024 * 1024
|
|
6670
|
-
});
|
|
6671
|
-
} catch (e) {
|
|
6672
|
-
const err2 = e;
|
|
6673
|
-
if (err2.status !== 1) {
|
|
6674
|
-
throw new Error(`cannot diff the incoming edit to ${displayPath}: ${err2.code ?? err2.message ?? String(e)}`);
|
|
6675
|
-
}
|
|
6676
|
-
raw = err2.stdout ? String(err2.stdout) : "";
|
|
6677
|
-
}
|
|
6678
|
-
} finally {
|
|
6679
|
-
rmSync(dir, { recursive: true, force: true });
|
|
6680
|
-
}
|
|
6681
|
-
const first = parseDiff(raw)[0];
|
|
6682
|
-
const hunks = first && first.kind === "file" ? first.hunks : [];
|
|
6683
|
-
return [{ kind: "file", path: displayPath, oldPath: null, op, before, after, binary: false, hunks }];
|
|
6684
|
-
}
|
|
6685
|
-
function changesFromClaudeHook(input, cwd) {
|
|
6686
|
-
const ti = input.tool_input ?? {};
|
|
6687
|
-
switch (input.tool_name) {
|
|
6688
|
-
case "Bash": {
|
|
6689
|
-
const raw = asStr(ti.command);
|
|
6690
|
-
return raw ? [{ kind: "command", raw, argv: raw.split(/\s+/) }] : [];
|
|
6691
|
-
}
|
|
6692
|
-
case "Write": {
|
|
6693
|
-
const fp = asStr(ti.file_path);
|
|
6694
|
-
if (!fp) return [];
|
|
6695
|
-
const before = readDisk(abs(fp, cwd));
|
|
6696
|
-
return synthFileChange(relForDisplay(abs(fp, cwd), cwd), before, asStr(ti.content));
|
|
6697
|
-
}
|
|
6698
|
-
case "Edit": {
|
|
6699
|
-
const fp = asStr(ti.file_path);
|
|
6700
|
-
if (!fp) return [];
|
|
6701
|
-
const before = readDisk(abs(fp, cwd));
|
|
6702
|
-
const after = applyEdit(before, asStr(ti.old_string), asStr(ti.new_string));
|
|
6703
|
-
return synthFileChange(relForDisplay(abs(fp, cwd), cwd), before, after);
|
|
6704
|
-
}
|
|
6705
|
-
case "MultiEdit": {
|
|
6706
|
-
const fp = asStr(ti.file_path);
|
|
6707
|
-
if (!fp) return [];
|
|
6708
|
-
const before = readDisk(abs(fp, cwd));
|
|
6709
|
-
let after = before;
|
|
6710
|
-
const edits = Array.isArray(ti.edits) ? ti.edits : [];
|
|
6711
|
-
for (const raw of edits) {
|
|
6712
|
-
const ed = raw;
|
|
6713
|
-
after = applyEdit(after, ed.old_string ?? "", ed.new_string ?? "");
|
|
6714
|
-
}
|
|
6715
|
-
return synthFileChange(relForDisplay(abs(fp, cwd), cwd), before, after);
|
|
6716
|
-
}
|
|
6717
|
-
case "NotebookEdit": {
|
|
6718
|
-
const fp = asStr(ti.notebook_path);
|
|
6719
|
-
const src = asStr(ti.new_source);
|
|
6720
|
-
if (!fp || !src) return [];
|
|
6721
|
-
return synthFileChange(relForDisplay(abs(fp, cwd), cwd), "", src);
|
|
6722
|
-
}
|
|
6723
|
-
default:
|
|
6724
|
-
return [];
|
|
6725
|
-
}
|
|
6726
|
-
}
|
|
6791
|
+
import { readFileSync as readFileSync11, appendFileSync as appendFileSync4, writeFileSync as writeFileSync4, existsSync as existsSync8 } from "node:fs";
|
|
6792
|
+
import { createHash as createHash5 } from "node:crypto";
|
|
6727
6793
|
|
|
6728
6794
|
// src/adapters/claude/deny.ts
|
|
6729
6795
|
function formatDenial(blocks) {
|
|
@@ -6745,22 +6811,22 @@ function formatDenial(blocks) {
|
|
|
6745
6811
|
}
|
|
6746
6812
|
|
|
6747
6813
|
// src/session.ts
|
|
6748
|
-
import { existsSync as
|
|
6749
|
-
import { dirname as dirname3, join as
|
|
6814
|
+
import { existsSync as existsSync6, mkdirSync as mkdirSync2, readFileSync as readFileSync7, writeFileSync as writeFileSync2 } from "node:fs";
|
|
6815
|
+
import { dirname as dirname3, join as join9 } from "node:path";
|
|
6750
6816
|
var UNSAFE = /[^A-Za-z0-9_-]/g;
|
|
6751
6817
|
var SHA = /^[0-9a-f]{7,40}$/;
|
|
6752
6818
|
function baselinePath(cwd, sessionId) {
|
|
6753
6819
|
const gd = gitDir(cwd);
|
|
6754
6820
|
if (!gd) return null;
|
|
6755
|
-
return
|
|
6821
|
+
return join9(gd, "tamperward", `session-${sessionId.replace(UNSAFE, "")}`);
|
|
6756
6822
|
}
|
|
6757
6823
|
function turnBaseline(cwd, sessionId) {
|
|
6758
6824
|
if (!sessionId) return null;
|
|
6759
6825
|
try {
|
|
6760
6826
|
const p = baselinePath(cwd, sessionId);
|
|
6761
6827
|
if (!p) return null;
|
|
6762
|
-
if (
|
|
6763
|
-
const v =
|
|
6828
|
+
if (existsSync6(p)) {
|
|
6829
|
+
const v = readFileSync7(p, "utf8").trim();
|
|
6764
6830
|
if (SHA.test(v)) return v;
|
|
6765
6831
|
}
|
|
6766
6832
|
const head = headSha(cwd);
|
|
@@ -6785,21 +6851,21 @@ function advanceTurnBaseline(cwd, sessionId) {
|
|
|
6785
6851
|
}
|
|
6786
6852
|
|
|
6787
6853
|
// src/effect.ts
|
|
6788
|
-
import { createHash as
|
|
6789
|
-
import { existsSync as
|
|
6790
|
-
import { isAbsolute as
|
|
6791
|
-
import { execFileSync as
|
|
6854
|
+
import { createHash as createHash3 } from "node:crypto";
|
|
6855
|
+
import { existsSync as existsSync7, lstatSync as lstatSync2, mkdirSync as mkdirSync3, readdirSync, readFileSync as readFileSync8, renameSync, rmSync as rmSync2, writeFileSync as writeFileSync3 } from "node:fs";
|
|
6856
|
+
import { isAbsolute as isAbsolute7, join as join10 } from "node:path";
|
|
6857
|
+
import { execFileSync as execFileSync5 } from "node:child_process";
|
|
6792
6858
|
var UNSAFE2 = /[^A-Za-z0-9._-]/g;
|
|
6793
6859
|
var SKIP_DIRS2 = /* @__PURE__ */ new Set([".git", "node_modules", ".hg", ".svn"]);
|
|
6794
6860
|
function gitDir2(cwd) {
|
|
6795
6861
|
try {
|
|
6796
|
-
const d =
|
|
6862
|
+
const d = execFileSync5("git", ["rev-parse", "--git-dir"], {
|
|
6797
6863
|
cwd,
|
|
6798
6864
|
encoding: "utf8",
|
|
6799
6865
|
stdio: ["ignore", "pipe", "pipe"]
|
|
6800
6866
|
}).trim();
|
|
6801
6867
|
if (!d) return null;
|
|
6802
|
-
return
|
|
6868
|
+
return isAbsolute7(d) ? d : join10(cwd, d);
|
|
6803
6869
|
} catch {
|
|
6804
6870
|
return null;
|
|
6805
6871
|
}
|
|
@@ -6808,8 +6874,8 @@ function statePath(cwd, sessionId, kind) {
|
|
|
6808
6874
|
if (!sessionId) return null;
|
|
6809
6875
|
const gd = gitDir2(cwd);
|
|
6810
6876
|
if (!gd) return null;
|
|
6811
|
-
const dir =
|
|
6812
|
-
return
|
|
6877
|
+
const dir = join10(gd, "tamperward");
|
|
6878
|
+
return join10(dir, `${kind}-${sessionId.replace(UNSAFE2, "")}.json`);
|
|
6813
6879
|
}
|
|
6814
6880
|
function ptreePath(cwd, sessionId) {
|
|
6815
6881
|
return statePath(cwd, sessionId, "ptree");
|
|
@@ -6817,7 +6883,7 @@ function ptreePath(cwd, sessionId) {
|
|
|
6817
6883
|
function turnTreePath(cwd, sessionId) {
|
|
6818
6884
|
return statePath(cwd, sessionId, "turntree");
|
|
6819
6885
|
}
|
|
6820
|
-
var sha = (buf) =>
|
|
6886
|
+
var sha = (buf) => createHash3("sha256").update(buf).digest("hex").slice(0, 16);
|
|
6821
6887
|
function contentHash(content) {
|
|
6822
6888
|
return sha(Buffer.isBuffer(content) ? content : Buffer.from(content, "utf8"));
|
|
6823
6889
|
}
|
|
@@ -6838,7 +6904,7 @@ function snapshotProtected(cwd, policy, prev) {
|
|
|
6838
6904
|
} catch {
|
|
6839
6905
|
}
|
|
6840
6906
|
const walk2 = (rel) => {
|
|
6841
|
-
const abs2 = rel ?
|
|
6907
|
+
const abs2 = rel ? join10(cwd, rel) : cwd;
|
|
6842
6908
|
let names;
|
|
6843
6909
|
try {
|
|
6844
6910
|
names = readdirSync(abs2);
|
|
@@ -6851,7 +6917,7 @@ function snapshotProtected(cwd, policy, prev) {
|
|
|
6851
6917
|
if (pruned.has(r)) continue;
|
|
6852
6918
|
let st;
|
|
6853
6919
|
try {
|
|
6854
|
-
st = lstatSync2(
|
|
6920
|
+
st = lstatSync2(join10(cwd, r));
|
|
6855
6921
|
} catch {
|
|
6856
6922
|
continue;
|
|
6857
6923
|
}
|
|
@@ -6878,9 +6944,9 @@ function driftBetween(expected, current) {
|
|
|
6878
6944
|
return { changed, deleted, added };
|
|
6879
6945
|
}
|
|
6880
6946
|
function loadTree(p) {
|
|
6881
|
-
if (!p || !
|
|
6947
|
+
if (!p || !existsSync7(p)) return null;
|
|
6882
6948
|
try {
|
|
6883
|
-
return JSON.parse(
|
|
6949
|
+
return JSON.parse(readFileSync8(p, "utf8"));
|
|
6884
6950
|
} catch {
|
|
6885
6951
|
return null;
|
|
6886
6952
|
}
|
|
@@ -6888,7 +6954,7 @@ function loadTree(p) {
|
|
|
6888
6954
|
function saveTree(p, tree) {
|
|
6889
6955
|
if (!p) return;
|
|
6890
6956
|
try {
|
|
6891
|
-
mkdirSync3(
|
|
6957
|
+
mkdirSync3(join10(p, ".."), { recursive: true });
|
|
6892
6958
|
const tmp = `${p}.${process.pid}.tmp`;
|
|
6893
6959
|
writeFileSync3(tmp, JSON.stringify(tree));
|
|
6894
6960
|
renameSync(tmp, p);
|
|
@@ -6920,19 +6986,19 @@ function dropTurnTree(cwd, sessionId) {
|
|
|
6920
6986
|
|
|
6921
6987
|
// src/cli/watch.ts
|
|
6922
6988
|
import { appendFileSync as appendFileSync3, mkdirSync as mkdirSync4, readdirSync as readdirSync2, statSync, watch } from "node:fs";
|
|
6923
|
-
import { join as
|
|
6924
|
-
import { createHash as
|
|
6989
|
+
import { join as join11 } from "node:path";
|
|
6990
|
+
import { createHash as createHash4 } from "node:crypto";
|
|
6925
6991
|
var SKIP2 = /(^|\/)(\.git|node_modules)(\/|$)/;
|
|
6926
6992
|
function snap(abs2) {
|
|
6927
6993
|
const e = inspectPath(abs2);
|
|
6928
6994
|
if (e.kind === "absent" || e.kind === "unreadable") return { mode: null, size: null, hash: null };
|
|
6929
6995
|
if (e.kind !== "file" || e.content == null) return { mode: e.mode, size: null, hash: null };
|
|
6930
|
-
const hash =
|
|
6996
|
+
const hash = createHash4("sha256").update(e.content).digest("hex").slice(0, 16);
|
|
6931
6997
|
return { mode: e.mode, size: e.size, hash };
|
|
6932
6998
|
}
|
|
6933
6999
|
function defaultEventLog(cwd) {
|
|
6934
7000
|
if (process.env.TAMPERWARD_FSEVENTS) return process.env.TAMPERWARD_FSEVENTS;
|
|
6935
|
-
return
|
|
7001
|
+
return join11(gitDir(cwd) ?? join11(cwd, ".git"), "tamperward", "fsevents.jsonl");
|
|
6936
7002
|
}
|
|
6937
7003
|
function watchTree(dir, cb) {
|
|
6938
7004
|
if (process.env.TAMPERWARD_WATCH_NO_RECURSIVE !== "1") {
|
|
@@ -6949,12 +7015,12 @@ function watchTree(dir, cb) {
|
|
|
6949
7015
|
if (watchers.has(rel) || SKIP2.test(rel + "/")) return;
|
|
6950
7016
|
let w;
|
|
6951
7017
|
try {
|
|
6952
|
-
w = watch(rel ?
|
|
7018
|
+
w = watch(rel ? join11(dir, rel) : dir, (kind, fname) => {
|
|
6953
7019
|
if (!fname) return;
|
|
6954
7020
|
const child = rel ? `${rel}/${String(fname)}` : String(fname);
|
|
6955
7021
|
cb(kind, child);
|
|
6956
7022
|
try {
|
|
6957
|
-
if (statSync(
|
|
7023
|
+
if (statSync(join11(dir, child)).isDirectory()) addDir(child);
|
|
6958
7024
|
} catch {
|
|
6959
7025
|
}
|
|
6960
7026
|
});
|
|
@@ -6967,7 +7033,7 @@ function watchTree(dir, cb) {
|
|
|
6967
7033
|
addDir(rel);
|
|
6968
7034
|
let names = [];
|
|
6969
7035
|
try {
|
|
6970
|
-
names = readdirSync2(rel ?
|
|
7036
|
+
names = readdirSync2(rel ? join11(dir, rel) : dir);
|
|
6971
7037
|
} catch {
|
|
6972
7038
|
return;
|
|
6973
7039
|
}
|
|
@@ -6975,7 +7041,7 @@ function watchTree(dir, cb) {
|
|
|
6975
7041
|
const child = rel ? `${rel}/${name}` : name;
|
|
6976
7042
|
if (SKIP2.test(child + "/")) continue;
|
|
6977
7043
|
try {
|
|
6978
|
-
if (statSync(
|
|
7044
|
+
if (statSync(join11(dir, child)).isDirectory()) walk2(child);
|
|
6979
7045
|
} catch {
|
|
6980
7046
|
}
|
|
6981
7047
|
}
|
|
@@ -6987,13 +7053,13 @@ function watchTree(dir, cb) {
|
|
|
6987
7053
|
}
|
|
6988
7054
|
function startWatcher(dir, log, policy) {
|
|
6989
7055
|
try {
|
|
6990
|
-
mkdirSync4(
|
|
7056
|
+
mkdirSync4(join11(log, ".."), { recursive: true });
|
|
6991
7057
|
} catch {
|
|
6992
7058
|
}
|
|
6993
7059
|
const last = /* @__PURE__ */ new Map();
|
|
6994
7060
|
return watchTree(dir, (kind, rel) => {
|
|
6995
7061
|
if (SKIP2.test(rel) || !isProtected(rel, policy)) return;
|
|
6996
|
-
const s = snap(
|
|
7062
|
+
const s = snap(join11(dir, rel));
|
|
6997
7063
|
const key2 = `${s.hash}:${s.mode}`;
|
|
6998
7064
|
if (last.get(rel) === key2) return;
|
|
6999
7065
|
last.set(rel, key2);
|
|
@@ -7020,7 +7086,7 @@ function runWatch(args) {
|
|
|
7020
7086
|
}
|
|
7021
7087
|
|
|
7022
7088
|
// src/detectors/fs-events.ts
|
|
7023
|
-
import { readFileSync as
|
|
7089
|
+
import { readFileSync as readFileSync10, statSync as statSync2 } from "node:fs";
|
|
7024
7090
|
var RULE11 = "transient-protected-mutation";
|
|
7025
7091
|
function readEvents(log, offset) {
|
|
7026
7092
|
let size = 0;
|
|
@@ -7030,7 +7096,7 @@ function readEvents(log, offset) {
|
|
|
7030
7096
|
return { events: [], newOffset: offset };
|
|
7031
7097
|
}
|
|
7032
7098
|
if (size <= offset) return { events: [], newOffset: offset >= 0 ? Math.min(offset, size) : 0 };
|
|
7033
|
-
const raw =
|
|
7099
|
+
const raw = readFileSync10(log, "utf8").slice(offset);
|
|
7034
7100
|
const events = [];
|
|
7035
7101
|
for (const line of raw.split("\n")) {
|
|
7036
7102
|
if (!line.trim()) continue;
|
|
@@ -7083,7 +7149,7 @@ var HookInputError = class extends Error {
|
|
|
7083
7149
|
};
|
|
7084
7150
|
function readStdin() {
|
|
7085
7151
|
try {
|
|
7086
|
-
return
|
|
7152
|
+
return readFileSync11(0, "utf8");
|
|
7087
7153
|
} catch (e) {
|
|
7088
7154
|
throw new HookInputError(`cannot read the hook payload on stdin: ${e instanceof Error ? e.message : String(e)}`);
|
|
7089
7155
|
}
|
|
@@ -7299,7 +7365,7 @@ function sanctionPredictedWrites(cwd, sessionId, policy, changes) {
|
|
|
7299
7365
|
let dirty = false;
|
|
7300
7366
|
for (const c of changes) {
|
|
7301
7367
|
if (c.kind !== "file" || c.after == null || !isProtected(c.path, policy)) continue;
|
|
7302
|
-
const hash =
|
|
7368
|
+
const hash = createHash5("sha256").update(c.after).digest("hex").slice(0, 16);
|
|
7303
7369
|
const prev = tree[c.path];
|
|
7304
7370
|
tree[c.path] = {
|
|
7305
7371
|
hash,
|
|
@@ -7336,10 +7402,10 @@ function turnTransientBlocks(cwd, sessionId, policy, changes) {
|
|
|
7336
7402
|
} };
|
|
7337
7403
|
const log = defaultEventLog(cwd);
|
|
7338
7404
|
const cp = cursorPath(cwd, sessionId);
|
|
7339
|
-
if (!
|
|
7405
|
+
if (!existsSync8(log)) return none;
|
|
7340
7406
|
let offset = 0;
|
|
7341
|
-
if (cp &&
|
|
7342
|
-
const n = Number(
|
|
7407
|
+
if (cp && existsSync8(cp)) {
|
|
7408
|
+
const n = Number(readFileSync11(cp, "utf8"));
|
|
7343
7409
|
offset = Number.isFinite(n) ? n : 0;
|
|
7344
7410
|
}
|
|
7345
7411
|
const { events, newOffset } = readEvents(log, offset);
|
|
@@ -7490,15 +7556,15 @@ Honored at LOCAL pre-commit only. The agent-layer hook ignores this file; CI req
|
|
|
7490
7556
|
}
|
|
7491
7557
|
|
|
7492
7558
|
// src/cli/init.ts
|
|
7493
|
-
import { execFileSync as
|
|
7494
|
-
import { createHash as
|
|
7495
|
-
import { existsSync as
|
|
7496
|
-
import { basename as basename2, dirname as dirname4, isAbsolute as
|
|
7559
|
+
import { execFileSync as execFileSync6 } from "node:child_process";
|
|
7560
|
+
import { createHash as createHash6 } from "node:crypto";
|
|
7561
|
+
import { existsSync as existsSync9, mkdirSync as mkdirSync5, readFileSync as readFileSync12, writeFileSync as writeFileSync5, chmodSync, statSync as statSync3 } from "node:fs";
|
|
7562
|
+
import { basename as basename2, dirname as dirname4, isAbsolute as isAbsolute8, join as join12, relative as relative3, resolve as resolve3 } from "node:path";
|
|
7497
7563
|
var errText3 = (e) => (e instanceof Error ? e.message : String(e)).replace(/\s+/g, " ").trim();
|
|
7498
7564
|
var isPlainObject = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
7499
7565
|
function gitConfig(cwd, key2) {
|
|
7500
7566
|
try {
|
|
7501
|
-
return
|
|
7567
|
+
return execFileSync6("git", ["config", "--get", key2], { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim() || null;
|
|
7502
7568
|
} catch {
|
|
7503
7569
|
return null;
|
|
7504
7570
|
}
|
|
@@ -7506,6 +7572,8 @@ function gitConfig(cwd, key2) {
|
|
|
7506
7572
|
var HOOK_RE = /\btamperward(?:@\S+)?\s+hook\s+claude\b/;
|
|
7507
7573
|
var SWEEP_RE = /\btamperward(?:@\S+)?\s+sweep\s+claude\b/;
|
|
7508
7574
|
var PRECOMMIT_RE = /\btamperward(?:@\S+)?\s+check\s+--staged\b/;
|
|
7575
|
+
var LEGACY_OURS = /^\s*npx\s+(?:--yes|-y)\s+tamperward(?:@(\S+))?\s+(hook claude|sweep claude|check --staged)\s*$/;
|
|
7576
|
+
var PRE_GLOBAL_OURS = /^\s*npx --yes --registry=https:\/\/registry\.npmjs\.org\/ --node-options=' ' --script-shell= --ignore-scripts --offline=false --prefer-online tamperward(?:@(\S+))? (hook claude|sweep claude|check --staged)\s*$/;
|
|
7509
7577
|
var CODEOWNERS_MARK = "# tamperward: the gate cannot guard the file that defines it";
|
|
7510
7578
|
var CODEOWNERS_PATHS = ["/.github/workflows/", "/.tamperward.yml", "/.github/CODEOWNERS"];
|
|
7511
7579
|
function coveredBy(existing, critical) {
|
|
@@ -7550,12 +7618,16 @@ version: 1
|
|
|
7550
7618
|
# ignore: [] # visible blind spots \u2014 the count is always reported
|
|
7551
7619
|
`;
|
|
7552
7620
|
function pinOf(command) {
|
|
7553
|
-
const m = command
|
|
7621
|
+
const m = ours(command);
|
|
7554
7622
|
return m ? m[1] ?? "" : null;
|
|
7555
7623
|
}
|
|
7624
|
+
function ours(command) {
|
|
7625
|
+
const previous = command.replace(" --global", "").replace(" || (echo tamperward: authority failed to start >&2 && exit 2)", "");
|
|
7626
|
+
return command.match(OURS) ?? previous.match(PRE_GLOBAL_OURS) ?? command.match(LEGACY_OURS);
|
|
7627
|
+
}
|
|
7556
7628
|
function stalePin(command) {
|
|
7557
7629
|
const pin = pinOf(command);
|
|
7558
|
-
return pin !== null && pin !== TW_VERSION;
|
|
7630
|
+
return pin !== null && (pin !== TW_VERSION || !OURS.test(command));
|
|
7559
7631
|
}
|
|
7560
7632
|
var WORKFLOW_CONTENT = `name: tamperward
|
|
7561
7633
|
|
|
@@ -7579,22 +7651,20 @@ jobs:
|
|
|
7579
7651
|
tamperward:
|
|
7580
7652
|
runs-on: ubuntu-latest
|
|
7581
7653
|
timeout-minutes: 10
|
|
7582
|
-
# The gate is fetched from the registry AT GATE TIME, from inside the
|
|
7583
|
-
# checked-out pull request \u2014 and \`npm\` reads \`.npmrc\` from the working
|
|
7584
|
-
# directory. A PR that commits one line, \`registry=https://\u2026\`, chooses where
|
|
7585
|
-
# the authority judging it is downloaded from; the version pin below pins the
|
|
7586
|
-
# STRING, not the SOURCE. An environment variable outranks a project
|
|
7587
|
-
# \`.npmrc\` in npm's config precedence, so this is what makes the pin mean
|
|
7588
|
-
# something.
|
|
7589
|
-
env:
|
|
7590
|
-
NPM_CONFIG_REGISTRY: https://registry.npmjs.org/
|
|
7591
7654
|
steps:
|
|
7592
|
-
- uses: actions/checkout@v5
|
|
7593
|
-
with:
|
|
7594
|
-
fetch-depth: 0 # the range diff needs both endpoints
|
|
7595
7655
|
- uses: actions/setup-node@v6
|
|
7596
7656
|
with:
|
|
7597
7657
|
node-version: 22
|
|
7658
|
+
# Install the authority before candidate files exist in the job workspace.
|
|
7659
|
+
# npm therefore cannot discover a pull-request .npmrc, and every security-
|
|
7660
|
+
# relevant npm setting is fixed on the command line. Later steps invoke the
|
|
7661
|
+
# installed binary directly: npm/npx never starts from candidate-controlled cwd.
|
|
7662
|
+
- name: Install Tamperward authority
|
|
7663
|
+
working-directory: \${{ runner.temp }}
|
|
7664
|
+
run: npm install --global --registry=https://registry.npmjs.org/ --node-options=' ' --script-shell= --ignore-scripts --offline=false --prefer-online tamperward@${TW_VERSION}
|
|
7665
|
+
- uses: actions/checkout@v5
|
|
7666
|
+
with:
|
|
7667
|
+
fetch-depth: 0 # the range diff needs both endpoints
|
|
7598
7668
|
- name: Resolve out-of-band sign-off from PR labels
|
|
7599
7669
|
id: oob
|
|
7600
7670
|
env:
|
|
@@ -7609,7 +7679,7 @@ jobs:
|
|
|
7609
7679
|
# across pushes, so an unbound one would clear every later finding on
|
|
7610
7680
|
# the same PR. Labels must read tamperward:allow:<rule>@<head-sha>.
|
|
7611
7681
|
TAMPERWARD_OOB_HEAD: \${{ github.event.pull_request.head.sha }}
|
|
7612
|
-
run:
|
|
7682
|
+
run: tamperward check --diff "\${{ github.event.pull_request.base.sha }}...\${{ github.event.pull_request.head.sha }}"
|
|
7613
7683
|
# Diff-time detection is spelling-dependent by nature; pristine
|
|
7614
7684
|
# re-execution is not, and it was the layer nothing got past in the
|
|
7615
7685
|
# taskbench sweeps. Shipping a CI template WITHOUT it meant every
|
|
@@ -7627,12 +7697,12 @@ jobs:
|
|
|
7627
7697
|
# stays red whatever the labels say.
|
|
7628
7698
|
TAMPERWARD_OOB_SIGNOFF: \${{ steps.oob.outputs.rules }}
|
|
7629
7699
|
TAMPERWARD_OOB_HEAD: \${{ github.event.pull_request.head.sha }}
|
|
7630
|
-
run:
|
|
7700
|
+
run: tamperward verify --require-ancestor --base "\${{ github.event.pull_request.base.sha }}"
|
|
7631
7701
|
`;
|
|
7632
7702
|
var WORKFLOW_MARK = "# tamperward:generated";
|
|
7633
7703
|
var WORKFLOW_MARK_RE = /^# tamperward:generated v(\S+) sha256:([0-9a-f]+)\n/m;
|
|
7634
7704
|
function bodyHash(body) {
|
|
7635
|
-
return
|
|
7705
|
+
return createHash6("sha256").update(body).digest("hex").slice(0, 16);
|
|
7636
7706
|
}
|
|
7637
7707
|
function workflowFile(body) {
|
|
7638
7708
|
return `${WORKFLOW_MARK} v${TW_VERSION} sha256:${bodyHash(body)}
|
|
@@ -7669,8 +7739,8 @@ function hooksShapeError(hooks) {
|
|
|
7669
7739
|
return null;
|
|
7670
7740
|
}
|
|
7671
7741
|
function planPolicy(cwd) {
|
|
7672
|
-
const path =
|
|
7673
|
-
if (
|
|
7742
|
+
const path = join12(cwd, POLICY_FILE);
|
|
7743
|
+
if (existsSync9(path)) {
|
|
7674
7744
|
try {
|
|
7675
7745
|
loadPolicy(cwd);
|
|
7676
7746
|
} catch (e) {
|
|
@@ -7688,12 +7758,12 @@ function planPolicy(cwd) {
|
|
|
7688
7758
|
}
|
|
7689
7759
|
function planClaudeHooks(cwd) {
|
|
7690
7760
|
const rel = ".claude/settings.json";
|
|
7691
|
-
const path =
|
|
7761
|
+
const path = join12(cwd, rel);
|
|
7692
7762
|
let settings = {};
|
|
7693
|
-
if (
|
|
7763
|
+
if (existsSync9(path)) {
|
|
7694
7764
|
let parsed;
|
|
7695
7765
|
try {
|
|
7696
|
-
parsed = JSON.parse(
|
|
7766
|
+
parsed = JSON.parse(readFileSync12(path, "utf8"));
|
|
7697
7767
|
} catch {
|
|
7698
7768
|
return {
|
|
7699
7769
|
item: "agent",
|
|
@@ -7728,7 +7798,7 @@ function planClaudeHooks(cwd) {
|
|
|
7728
7798
|
return {
|
|
7729
7799
|
item: "agent",
|
|
7730
7800
|
path: rel,
|
|
7731
|
-
status:
|
|
7801
|
+
status: existsSync9(path) ? "update" : "create",
|
|
7732
7802
|
detail: [
|
|
7733
7803
|
needPre && "wire PreToolUse deny",
|
|
7734
7804
|
needStop && "wire Stop sweep",
|
|
@@ -7738,7 +7808,7 @@ function planClaudeHooks(cwd) {
|
|
|
7738
7808
|
].filter(Boolean).join(" + "),
|
|
7739
7809
|
apply: () => {
|
|
7740
7810
|
for (const h of repin) {
|
|
7741
|
-
const kind = String(h.command ?? "")
|
|
7811
|
+
const kind = ours(String(h.command ?? ""))?.[2];
|
|
7742
7812
|
h.command = kind === "hook claude" ? HOOK_CMD : kind === "sweep claude" ? SWEEP_CMD : h.command;
|
|
7743
7813
|
}
|
|
7744
7814
|
if (needPre) {
|
|
@@ -7785,15 +7855,15 @@ var MANAGED_HOOKS = [
|
|
|
7785
7855
|
];
|
|
7786
7856
|
function hooksDir(cwd) {
|
|
7787
7857
|
try {
|
|
7788
|
-
const out2 =
|
|
7858
|
+
const out2 = execFileSync6("git", ["rev-parse", "--git-path", "hooks"], { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
|
|
7789
7859
|
if (out2) return resolve3(cwd, out2);
|
|
7790
7860
|
} catch {
|
|
7791
7861
|
}
|
|
7792
|
-
return
|
|
7862
|
+
return existsSync9(join12(cwd, ".git")) ? join12(cwd, ".git", "hooks") : null;
|
|
7793
7863
|
}
|
|
7794
7864
|
function display(cwd, path) {
|
|
7795
7865
|
const rel = relative3(cwd, path);
|
|
7796
|
-
return rel && !rel.startsWith("..") && !
|
|
7866
|
+
return rel && !rel.startsWith("..") && !isAbsolute8(rel) ? rel : path;
|
|
7797
7867
|
}
|
|
7798
7868
|
function unconditionalExitAt(lines) {
|
|
7799
7869
|
let depth = 0;
|
|
@@ -7816,30 +7886,30 @@ function planPreCommit(cwd) {
|
|
|
7816
7886
|
const skip = (detail) => ({ item: "pre-commit", path: "(none)", status: "skip", detail });
|
|
7817
7887
|
let path;
|
|
7818
7888
|
let note;
|
|
7819
|
-
if (
|
|
7820
|
-
path =
|
|
7889
|
+
if (existsSync9(join12(cwd, ".husky"))) {
|
|
7890
|
+
path = join12(cwd, ".husky", "pre-commit");
|
|
7821
7891
|
note = "husky";
|
|
7822
7892
|
} else {
|
|
7823
|
-
const managed = MANAGED_HOOKS.find((m) => m.configs.some((c) =>
|
|
7893
|
+
const managed = MANAGED_HOOKS.find((m) => m.configs.some((c) => existsSync9(join12(cwd, c))));
|
|
7824
7894
|
if (managed) {
|
|
7825
7895
|
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}`);
|
|
7826
7896
|
}
|
|
7827
7897
|
const dir = hooksDir(cwd);
|
|
7828
7898
|
if (!dir) return skip("not a git repo and no .husky/ \u2014 nothing to wire");
|
|
7829
7899
|
if (basename2(dir) === "_" && basename2(dirname4(dir)) === ".husky") {
|
|
7830
|
-
path =
|
|
7900
|
+
path = join12(dirname4(dir), "pre-commit");
|
|
7831
7901
|
note = "husky";
|
|
7832
7902
|
} else {
|
|
7833
|
-
path =
|
|
7903
|
+
path = join12(dir, "pre-commit");
|
|
7834
7904
|
const rel2 = display(cwd, dir);
|
|
7835
|
-
note = rel2 ===
|
|
7905
|
+
note = rel2 === join12(".git", "hooks") ? "plain git hook (local-only: .git/hooks is not committed \u2014 consider husky to share it)" : `git hooks directory ${rel2} (core.hooksPath)`;
|
|
7836
7906
|
}
|
|
7837
7907
|
}
|
|
7838
7908
|
const rel = display(cwd, path);
|
|
7839
|
-
if (
|
|
7909
|
+
if (existsSync9(path) && !statSync3(path).isFile()) {
|
|
7840
7910
|
return { item: "pre-commit", path: rel, status: "error", detail: "exists but is not a regular file \u2014 refusing to write" };
|
|
7841
7911
|
}
|
|
7842
|
-
const existing =
|
|
7912
|
+
const existing = existsSync9(path) ? readFileSync12(path, "utf8") : null;
|
|
7843
7913
|
if (existing === null) {
|
|
7844
7914
|
return {
|
|
7845
7915
|
item: "pre-commit",
|
|
@@ -7883,8 +7953,8 @@ ${block.join("\n")}
|
|
|
7883
7953
|
};
|
|
7884
7954
|
}
|
|
7885
7955
|
if (at !== -1) {
|
|
7886
|
-
const
|
|
7887
|
-
if (!
|
|
7956
|
+
const ours2 = pinOf(lines[at]) !== null && lines[at - 1]?.trim() === MARKER;
|
|
7957
|
+
if (!ours2) {
|
|
7888
7958
|
return {
|
|
7889
7959
|
item: "pre-commit",
|
|
7890
7960
|
path: rel,
|
|
@@ -7928,10 +7998,10 @@ ${block.join("\n")}
|
|
|
7928
7998
|
function planCodeowners(cwd) {
|
|
7929
7999
|
const rel = ".github/CODEOWNERS";
|
|
7930
8000
|
const existingRel = [".github/CODEOWNERS", "CODEOWNERS", "docs/CODEOWNERS"].find(
|
|
7931
|
-
(r) =>
|
|
8001
|
+
(r) => existsSync9(join12(cwd, r))
|
|
7932
8002
|
);
|
|
7933
|
-
const path =
|
|
7934
|
-
const existing = existingRel ?
|
|
8003
|
+
const path = join12(cwd, existingRel ?? rel);
|
|
8004
|
+
const existing = existingRel ? readFileSync12(path, "utf8") : null;
|
|
7935
8005
|
const missing = CODEOWNERS_PATHS.filter((p) => existing === null || !coveredBy(existing, p));
|
|
7936
8006
|
if (missing.length === 0) {
|
|
7937
8007
|
return { item: "codeowners", path: existingRel, status: "ok", detail: "gate paths already have code owners" };
|
|
@@ -7964,12 +8034,12 @@ ${CODEOWNERS_MARK}
|
|
|
7964
8034
|
}
|
|
7965
8035
|
function planWorkflow(cwd, force) {
|
|
7966
8036
|
const rel = ".github/workflows/tamperward.yml";
|
|
7967
|
-
const path =
|
|
8037
|
+
const path = join12(cwd, rel);
|
|
7968
8038
|
const write = () => {
|
|
7969
8039
|
mkdirSync5(dirname4(path), { recursive: true });
|
|
7970
8040
|
writeFileSync5(path, workflowFile(WORKFLOW_CONTENT));
|
|
7971
8041
|
};
|
|
7972
|
-
if (!
|
|
8042
|
+
if (!existsSync9(path)) {
|
|
7973
8043
|
return {
|
|
7974
8044
|
item: "ci",
|
|
7975
8045
|
path: rel,
|
|
@@ -7980,7 +8050,7 @@ function planWorkflow(cwd, force) {
|
|
|
7980
8050
|
}
|
|
7981
8051
|
let src;
|
|
7982
8052
|
try {
|
|
7983
|
-
src =
|
|
8053
|
+
src = readFileSync12(path, "utf8");
|
|
7984
8054
|
} catch {
|
|
7985
8055
|
return { item: "ci", path: rel, status: "error", detail: "exists but cannot be read \u2014 refusing to overwrite" };
|
|
7986
8056
|
}
|
|
@@ -8079,79 +8149,33 @@ tamperward init: ${changed} change(s) applied. Commit them so the gate travels w
|
|
|
8079
8149
|
// src/cli/verify.ts
|
|
8080
8150
|
import { execFileSync as execFileSync8, spawnSync } from "node:child_process";
|
|
8081
8151
|
import { createHash as createHash8 } from "node:crypto";
|
|
8082
|
-
import { chmodSync as chmodSync2, cpSync, lstatSync as lstatSync3, mkdirSync as mkdirSync6, mkdtempSync as mkdtempSync2, readFileSync as readFileSync14, readlinkSync as readlinkSync3,
|
|
8152
|
+
import { chmodSync as chmodSync2, cpSync, lstatSync as lstatSync3, mkdirSync as mkdirSync6, mkdtempSync as mkdtempSync2, readFileSync as readFileSync14, readlinkSync as readlinkSync3, realpathSync as realpathSync3, rmSync as rmSync3, symlinkSync, writeFileSync as writeFileSync6, existsSync as existsSync10 } from "node:fs";
|
|
8083
8153
|
import { tmpdir as tmpdir2 } from "node:os";
|
|
8084
|
-
import { dirname as dirname5, join as join14, resolve as resolve4 } from "node:path";
|
|
8085
|
-
|
|
8086
|
-
// src/git/trusted.ts
|
|
8087
|
-
import { execFileSync as execFileSync6 } from "node:child_process";
|
|
8088
|
-
import { createHash as createHash6 } from "node:crypto";
|
|
8089
|
-
import { existsSync as existsSync9, readFileSync as readFileSync12 } from "node:fs";
|
|
8090
|
-
import { isAbsolute as isAbsolute8, join as join12 } from "node:path";
|
|
8091
|
-
function trustedGitEnv() {
|
|
8092
|
-
return { ...process.env, GIT_NO_REPLACE_OBJECTS: "1" };
|
|
8093
|
-
}
|
|
8094
|
-
function gitDirOf(cwd) {
|
|
8095
|
-
try {
|
|
8096
|
-
const d = execFileSync6("git", ["rev-parse", "--git-dir"], {
|
|
8097
|
-
cwd,
|
|
8098
|
-
encoding: "utf8",
|
|
8099
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
8100
|
-
env: trustedGitEnv()
|
|
8101
|
-
}).trim();
|
|
8102
|
-
if (!d) return null;
|
|
8103
|
-
return isAbsolute8(d) ? d : join12(cwd, d);
|
|
8104
|
-
} catch {
|
|
8105
|
-
return null;
|
|
8106
|
-
}
|
|
8107
|
-
}
|
|
8108
|
-
function objectRewriteState(cwd) {
|
|
8109
|
-
const parts = [];
|
|
8110
|
-
try {
|
|
8111
|
-
parts.push(
|
|
8112
|
-
"replace:" + execFileSync6("git", ["for-each-ref", "--format=%(refname) %(objectname)", "refs/replace/"], {
|
|
8113
|
-
cwd,
|
|
8114
|
-
encoding: "utf8",
|
|
8115
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
8116
|
-
env: trustedGitEnv()
|
|
8117
|
-
}).trim()
|
|
8118
|
-
);
|
|
8119
|
-
} catch {
|
|
8120
|
-
parts.push("replace:<unreadable>");
|
|
8121
|
-
}
|
|
8122
|
-
const dir = gitDirOf(cwd);
|
|
8123
|
-
for (const rel of ["info/grafts", "shallow"]) {
|
|
8124
|
-
if (dir === null) {
|
|
8125
|
-
parts.push(`${rel}:<no-git-dir>`);
|
|
8126
|
-
continue;
|
|
8127
|
-
}
|
|
8128
|
-
const p = join12(dir, rel);
|
|
8129
|
-
try {
|
|
8130
|
-
parts.push(`${rel}:` + (existsSync9(p) ? readFileSync12(p, "utf8").trim() : ""));
|
|
8131
|
-
} catch {
|
|
8132
|
-
parts.push(`${rel}:<unreadable>`);
|
|
8133
|
-
}
|
|
8134
|
-
}
|
|
8135
|
-
return createHash6("sha256").update(parts.join("\n")).digest("hex");
|
|
8136
|
-
}
|
|
8154
|
+
import { dirname as dirname5, isAbsolute as isAbsolute9, join as join14, relative as relative4, resolve as resolve4, sep } from "node:path";
|
|
8137
8155
|
|
|
8138
8156
|
// src/fingerprint.ts
|
|
8139
8157
|
import { execFileSync as execFileSync7 } from "node:child_process";
|
|
8140
8158
|
import { createHash as createHash7 } from "node:crypto";
|
|
8141
8159
|
import { readdirSync as readdirSync3, readFileSync as readFileSync13, readlinkSync as readlinkSync2, statSync as statSync4 } from "node:fs";
|
|
8142
8160
|
import { join as join13 } from "node:path";
|
|
8143
|
-
function treeFingerprint(cwd) {
|
|
8144
|
-
const
|
|
8161
|
+
function treeFingerprint(cwd, keepIgnored) {
|
|
8162
|
+
const listed = execFileSync7("git", ["ls-files", "-z", "--cached", "--others", "--exclude-standard"], {
|
|
8145
8163
|
cwd,
|
|
8146
8164
|
encoding: "utf8",
|
|
8147
8165
|
maxBuffer: 1 << 28,
|
|
8148
8166
|
env: trustedGitEnv()
|
|
8149
|
-
}).split("\0").filter(Boolean)
|
|
8167
|
+
}).split("\0").filter(Boolean);
|
|
8168
|
+
const files = [.../* @__PURE__ */ new Set([...listed, ...keepIgnored ? ignoredPaths({ cwd }, keepIgnored) : []])].sort();
|
|
8150
8169
|
const h = createHash7("sha256");
|
|
8151
8170
|
for (const rel of files) {
|
|
8152
8171
|
h.update(rel);
|
|
8153
8172
|
h.update("\0");
|
|
8154
|
-
|
|
8173
|
+
const entry = inspectRel(cwd, rel);
|
|
8174
|
+
h.update(entry.kind);
|
|
8175
|
+
h.update("\0");
|
|
8176
|
+
h.update(String(entry.mode));
|
|
8177
|
+
h.update("\0");
|
|
8178
|
+
h.update(entry.content ?? `<${entry.kind}:${entry.detail}>`);
|
|
8155
8179
|
h.update("\0");
|
|
8156
8180
|
}
|
|
8157
8181
|
return h.digest("hex");
|
|
@@ -8284,29 +8308,131 @@ function baseIsAncestorOfHead(base, cwd) {
|
|
|
8284
8308
|
const r = spawnSync("git", ["merge-base", "--is-ancestor", base, "HEAD"], { cwd, env: trustedGitEnv() });
|
|
8285
8309
|
return r.status === 0;
|
|
8286
8310
|
}
|
|
8311
|
+
function inside(root, path) {
|
|
8312
|
+
const rel = relative4(resolve4(root), resolve4(path));
|
|
8313
|
+
return rel === "" || rel !== ".." && !rel.startsWith(`..${sep}`) && !isAbsolute9(rel);
|
|
8314
|
+
}
|
|
8315
|
+
function linkParts(path) {
|
|
8316
|
+
const parts = sep === "\\" ? path.split(/[\\/]+/) : path.split("/");
|
|
8317
|
+
return parts.filter((part) => part !== "" && part !== ".");
|
|
8318
|
+
}
|
|
8319
|
+
function linkEscape(label, target) {
|
|
8320
|
+
return new Error(`${label} is a symlink that escapes the materialised tree (${JSON.stringify(target)})`);
|
|
8321
|
+
}
|
|
8322
|
+
function driveRelativeLinkTarget(target) {
|
|
8323
|
+
return sep === "\\" && /^[A-Za-z]:(?![\\/])/.test(target);
|
|
8324
|
+
}
|
|
8325
|
+
function rootedLinkTarget(target) {
|
|
8326
|
+
return isAbsolute9(target) || driveRelativeLinkTarget(target);
|
|
8327
|
+
}
|
|
8328
|
+
function safeSymlink(target, out2, root, label) {
|
|
8329
|
+
if (rootedLinkTarget(target) || !inside(root, resolve4(dirname5(out2), target))) {
|
|
8330
|
+
throw linkEscape(label, target);
|
|
8331
|
+
}
|
|
8332
|
+
rmSync3(out2, { force: true });
|
|
8333
|
+
symlinkSync(target, out2);
|
|
8334
|
+
}
|
|
8335
|
+
function validateSymlinkGraph(target, out2, root, dependencyRoot, label) {
|
|
8336
|
+
let domain = "tree";
|
|
8337
|
+
let stack = linkParts(relative4(root, dirname5(out2)));
|
|
8338
|
+
const pending = linkParts(target);
|
|
8339
|
+
const seen = /* @__PURE__ */ new Set();
|
|
8340
|
+
let hops = 0;
|
|
8341
|
+
while (pending.length) {
|
|
8342
|
+
const part = pending.shift();
|
|
8343
|
+
if (part === "..") {
|
|
8344
|
+
if (!stack.length) throw linkEscape(label, target);
|
|
8345
|
+
stack.pop();
|
|
8346
|
+
continue;
|
|
8347
|
+
}
|
|
8348
|
+
stack.push(part);
|
|
8349
|
+
if (domain === "tree" && dependencyRoot && stack.length === 1 && stack[0] === "node_modules") {
|
|
8350
|
+
domain = "deps";
|
|
8351
|
+
stack = [];
|
|
8352
|
+
continue;
|
|
8353
|
+
}
|
|
8354
|
+
const base = domain === "tree" ? root : dependencyRoot;
|
|
8355
|
+
const current = join14(base, ...stack);
|
|
8356
|
+
let st;
|
|
8357
|
+
try {
|
|
8358
|
+
st = lstatSync3(current);
|
|
8359
|
+
} catch {
|
|
8360
|
+
continue;
|
|
8361
|
+
}
|
|
8362
|
+
if (!st.isSymbolicLink()) continue;
|
|
8363
|
+
if (++hops > 128) {
|
|
8364
|
+
throw new Error(`${label} contains a symlink cycle while materialising (${JSON.stringify(target)})`);
|
|
8365
|
+
}
|
|
8366
|
+
const key2 = `${domain}:${current}:${pending.join(sep)}`;
|
|
8367
|
+
if (seen.has(key2)) {
|
|
8368
|
+
throw new Error(`${label} contains a symlink cycle while materialising (${JSON.stringify(target)})`);
|
|
8369
|
+
}
|
|
8370
|
+
seen.add(key2);
|
|
8371
|
+
const next = readlinkSync3(current);
|
|
8372
|
+
stack.pop();
|
|
8373
|
+
if (driveRelativeLinkTarget(next)) {
|
|
8374
|
+
throw linkEscape(label, target);
|
|
8375
|
+
}
|
|
8376
|
+
if (isAbsolute9(next)) {
|
|
8377
|
+
if (domain === "tree" || !dependencyRoot || !inside(dependencyRoot, next)) {
|
|
8378
|
+
throw linkEscape(label, target);
|
|
8379
|
+
}
|
|
8380
|
+
stack = [];
|
|
8381
|
+
pending.unshift(...linkParts(relative4(dependencyRoot, resolve4(next))));
|
|
8382
|
+
} else {
|
|
8383
|
+
pending.unshift(...linkParts(next));
|
|
8384
|
+
}
|
|
8385
|
+
}
|
|
8386
|
+
}
|
|
8387
|
+
function rejectLinkedParent(cwd, rel) {
|
|
8388
|
+
const parts = rel.split("/").slice(0, -1);
|
|
8389
|
+
let current = cwd;
|
|
8390
|
+
for (const part of parts) {
|
|
8391
|
+
current = join14(current, part);
|
|
8392
|
+
let st;
|
|
8393
|
+
try {
|
|
8394
|
+
st = lstatSync3(current);
|
|
8395
|
+
} catch {
|
|
8396
|
+
continue;
|
|
8397
|
+
}
|
|
8398
|
+
if (st.isSymbolicLink()) {
|
|
8399
|
+
throw new Error(`${rel} has a symlinked parent directory (${relative4(cwd, current)})`);
|
|
8400
|
+
}
|
|
8401
|
+
}
|
|
8402
|
+
}
|
|
8287
8403
|
function materialize(cwd, dest) {
|
|
8288
8404
|
const listed = git2(["ls-files", "-z", "--cached", "--others", "--exclude-standard"], cwd).split("\0").filter(Boolean);
|
|
8405
|
+
const links = [];
|
|
8289
8406
|
for (const rel of listed) {
|
|
8290
8407
|
const src = join14(cwd, rel);
|
|
8408
|
+
rejectLinkedParent(cwd, rel);
|
|
8291
8409
|
let st;
|
|
8292
8410
|
try {
|
|
8293
|
-
st =
|
|
8411
|
+
st = lstatSync3(src);
|
|
8294
8412
|
} catch {
|
|
8295
8413
|
continue;
|
|
8296
8414
|
}
|
|
8297
|
-
if (!st.isFile()) continue;
|
|
8298
8415
|
const out2 = join14(dest, rel);
|
|
8299
8416
|
mkdirSync6(dirname5(out2), { recursive: true });
|
|
8300
8417
|
dropSymlink(out2);
|
|
8301
|
-
if (
|
|
8302
|
-
|
|
8303
|
-
|
|
8418
|
+
if (st.isSymbolicLink()) {
|
|
8419
|
+
const target = readlinkSync3(src);
|
|
8420
|
+
safeSymlink(target, out2, dest, rel);
|
|
8421
|
+
links.push({ rel, out: out2, target });
|
|
8422
|
+
} else if (st.isFile()) {
|
|
8304
8423
|
cpSync(src, out2, { dereference: true });
|
|
8424
|
+
chmodSync2(out2, st.mode);
|
|
8425
|
+
} else {
|
|
8426
|
+
throw new Error(`${rel} is ${st.isDirectory() ? "a directory" : "a special file"} where git expects a file`);
|
|
8305
8427
|
}
|
|
8306
|
-
chmodSync2(out2, st.mode);
|
|
8307
8428
|
}
|
|
8308
8429
|
const nm = resolve4(cwd, "node_modules");
|
|
8309
|
-
|
|
8430
|
+
let dependencyRoot = null;
|
|
8431
|
+
if (existsSync10(nm)) dependencyRoot = realpathSync3(nm);
|
|
8432
|
+
for (const link of links) {
|
|
8433
|
+
validateSymlinkGraph(link.target, link.out, dest, dependencyRoot, link.rel);
|
|
8434
|
+
}
|
|
8435
|
+
if (dependencyRoot) symlinkSync(nm, join14(dest, "node_modules"), "dir");
|
|
8310
8436
|
}
|
|
8311
8437
|
function dropSymlink(p) {
|
|
8312
8438
|
try {
|
|
@@ -8358,6 +8484,7 @@ function overlayPristine(cwd, base, dest, policy, cmd) {
|
|
|
8358
8484
|
const verifierGlobs = policy.verify?.inputs ?? [];
|
|
8359
8485
|
const isOverlay = (p) => OVERLAY_CLASSES.some((c) => isProtected(p, policy, c)) || matchesAny(p, VERIFICATION_SURFACE) || verifierOwned.has(p) || verifierGlobs.length > 0 && matchesAny(p, verifierGlobs);
|
|
8360
8486
|
const restored = [];
|
|
8487
|
+
const restoredLinks = [];
|
|
8361
8488
|
const baseProtected = /* @__PURE__ */ new Set();
|
|
8362
8489
|
for (const e of entries) {
|
|
8363
8490
|
if (!isOverlay(e.path)) continue;
|
|
@@ -8370,7 +8497,9 @@ function overlayPristine(cwd, base, dest, policy, cmd) {
|
|
|
8370
8497
|
mkdirSync6(dirname5(out2), { recursive: true });
|
|
8371
8498
|
rmSync3(out2, { force: true });
|
|
8372
8499
|
if (e.mode === "120000") {
|
|
8373
|
-
|
|
8500
|
+
const target = content.toString("utf8");
|
|
8501
|
+
safeSymlink(target, out2, dest, `the base's ${e.path}`);
|
|
8502
|
+
restoredLinks.push({ path: e.path, out: out2, target });
|
|
8374
8503
|
} else {
|
|
8375
8504
|
writeFileSync6(out2, content);
|
|
8376
8505
|
chmodSync2(out2, parseInt(e.mode.slice(-4), 8) & 511);
|
|
@@ -8384,6 +8513,11 @@ function overlayPristine(cwd, base, dest, policy, cmd) {
|
|
|
8384
8513
|
rmSync3(join14(dest, rel), { force: true });
|
|
8385
8514
|
removed++;
|
|
8386
8515
|
}
|
|
8516
|
+
const nm = resolve4(cwd, "node_modules");
|
|
8517
|
+
const dependencyRoot = existsSync10(nm) ? realpathSync3(nm) : null;
|
|
8518
|
+
for (const link of restoredLinks) {
|
|
8519
|
+
validateSymlinkGraph(link.target, link.out, dest, dependencyRoot, `the base's ${link.path}`);
|
|
8520
|
+
}
|
|
8387
8521
|
return { restored, removed };
|
|
8388
8522
|
}
|
|
8389
8523
|
var SUPERVISOR = `
|
|
@@ -8505,7 +8639,8 @@ function runVerify(opts) {
|
|
|
8505
8639
|
if (opts.keep) return;
|
|
8506
8640
|
for (const d of dirs) rmSync3(d, { recursive: true, force: true });
|
|
8507
8641
|
};
|
|
8508
|
-
const
|
|
8642
|
+
const protectedIgnored = (rel) => isProtected(rel, policy);
|
|
8643
|
+
const treeBefore = treeFingerprint(cwd, protectedIgnored);
|
|
8509
8644
|
const depsBefore = depsFingerprint(cwd);
|
|
8510
8645
|
try {
|
|
8511
8646
|
mkdirSync6(visDir);
|
|
@@ -8516,7 +8651,7 @@ function runVerify(opts) {
|
|
|
8516
8651
|
return 2;
|
|
8517
8652
|
}
|
|
8518
8653
|
const visible = runSuite(visDir, cmd, budget);
|
|
8519
|
-
if (treeFingerprint(cwd) !== treeBefore) {
|
|
8654
|
+
if (treeFingerprint(cwd, protectedIgnored) !== treeBefore) {
|
|
8520
8655
|
cleanup([visRoot]);
|
|
8521
8656
|
out2("verify: the working tree changed while the visible suite was running \u2014 the pristine copy");
|
|
8522
8657
|
out2("would be materialised from a tree the candidate just edited. Failing closed, not open.");
|
|
@@ -8544,7 +8679,7 @@ function runVerify(opts) {
|
|
|
8544
8679
|
const overlayBefore = overlayDigest(priDir, restored);
|
|
8545
8680
|
const pristine = runSuite(priDir, cmd, budget);
|
|
8546
8681
|
const overlayMoved = overlayDigest(priDir, restored) !== overlayBefore;
|
|
8547
|
-
const treeMoved = treeFingerprint(cwd) !== treeBefore;
|
|
8682
|
+
const treeMoved = treeFingerprint(cwd, protectedIgnored) !== treeBefore;
|
|
8548
8683
|
const depsMoved = depsFingerprint(cwd) !== depsBefore;
|
|
8549
8684
|
cleanup([visRoot, priRoot]);
|
|
8550
8685
|
if (overlayMoved || treeMoved || depsMoved) {
|
|
@@ -8641,7 +8776,7 @@ function parseVerify(args) {
|
|
|
8641
8776
|
|
|
8642
8777
|
// src/cli/run.ts
|
|
8643
8778
|
import { execFileSync as execFileSync9, spawnSync as spawnSync2 } from "node:child_process";
|
|
8644
|
-
import { readdirSync as readdirSync4, readFileSync as readFileSync15, realpathSync as
|
|
8779
|
+
import { readdirSync as readdirSync4, readFileSync as readFileSync15, readlinkSync as readlinkSync4, realpathSync as realpathSync4 } from "node:fs";
|
|
8645
8780
|
import { resolve as resolve5 } from "node:path";
|
|
8646
8781
|
var out = (s) => process.stdout.write(s + "\n");
|
|
8647
8782
|
var err = (s) => process.stderr.write(s + "\n");
|
|
@@ -8659,7 +8794,7 @@ function survivorsHoldingTree(cwd, spawnedAfterTicks) {
|
|
|
8659
8794
|
const out2 = [];
|
|
8660
8795
|
let real;
|
|
8661
8796
|
try {
|
|
8662
|
-
real =
|
|
8797
|
+
real = realpathSync4(cwd);
|
|
8663
8798
|
} catch {
|
|
8664
8799
|
return out2;
|
|
8665
8800
|
}
|
|
@@ -8672,19 +8807,31 @@ function survivorsHoldingTree(cwd, spawnedAfterTicks) {
|
|
|
8672
8807
|
for (const pid of pids) {
|
|
8673
8808
|
const n = Number(pid);
|
|
8674
8809
|
if (n === process.pid) continue;
|
|
8675
|
-
let held;
|
|
8676
8810
|
try {
|
|
8677
|
-
|
|
8811
|
+
const stat = readFileSync15(`/proc/${pid}/stat`, "utf8");
|
|
8812
|
+
const startTicks = Number(stat.slice(stat.lastIndexOf(")") + 2).split(" ")[19]);
|
|
8813
|
+
if (!Number.isFinite(startTicks) || startTicks < spawnedAfterTicks) continue;
|
|
8678
8814
|
} catch {
|
|
8679
8815
|
continue;
|
|
8680
8816
|
}
|
|
8681
|
-
|
|
8817
|
+
const procLinks = [`/proc/${pid}/cwd`, `/proc/${pid}/exe`];
|
|
8682
8818
|
try {
|
|
8683
|
-
const
|
|
8684
|
-
const startTicks = Number(stat.slice(stat.lastIndexOf(")") + 2).split(" ")[19]);
|
|
8685
|
-
if (Number.isFinite(startTicks) && startTicks >= spawnedAfterTicks) out2.push(n);
|
|
8819
|
+
for (const fd of readdirSync4(`/proc/${pid}/fd`)) procLinks.push(`/proc/${pid}/fd/${fd}`);
|
|
8686
8820
|
} catch {
|
|
8687
8821
|
}
|
|
8822
|
+
let holds = false;
|
|
8823
|
+
for (const link of procLinks) {
|
|
8824
|
+
try {
|
|
8825
|
+
const raw = readlinkSync4(link).replace(/ \(deleted\)$/, "");
|
|
8826
|
+
const held = raw.startsWith("/") ? resolve5(raw) : raw;
|
|
8827
|
+
if (held === real || held.startsWith(real + "/")) {
|
|
8828
|
+
holds = true;
|
|
8829
|
+
break;
|
|
8830
|
+
}
|
|
8831
|
+
} catch {
|
|
8832
|
+
}
|
|
8833
|
+
}
|
|
8834
|
+
if (holds) out2.push(n);
|
|
8688
8835
|
}
|
|
8689
8836
|
return out2;
|
|
8690
8837
|
}
|
|
@@ -8758,7 +8905,8 @@ tamperward run \u2014 agent exit ${agentExit}; DEPENDENCY_DRIFT \u2192 ENFORCEME
|
|
|
8758
8905
|
tamperward run \u2014 agent exit ${agentExit}; HISTORY_REWRITE \u2192 ENFORCEMENT_FAILED`);
|
|
8759
8906
|
return 1;
|
|
8760
8907
|
}
|
|
8761
|
-
const
|
|
8908
|
+
const protectedIgnored = (rel) => isProtected(rel, frozenPolicy);
|
|
8909
|
+
const fpBefore = treeFingerprint(cwd, protectedIgnored);
|
|
8762
8910
|
let diffCode = 0;
|
|
8763
8911
|
if (head !== base) {
|
|
8764
8912
|
out(`
|
|
@@ -8775,7 +8923,7 @@ tamperward run \u2014 agent exit ${agentExit}; HISTORY_REWRITE \u2192 ENFORCEMEN
|
|
|
8775
8923
|
} catch {
|
|
8776
8924
|
}
|
|
8777
8925
|
}
|
|
8778
|
-
const mutatedDuringAdjudication = treeFingerprint(cwd) !== fpBefore;
|
|
8926
|
+
const mutatedDuringAdjudication = treeFingerprint(cwd, protectedIgnored) !== fpBefore;
|
|
8779
8927
|
if (mutatedDuringAdjudication) {
|
|
8780
8928
|
err("tamperward run: the working tree changed while it was being adjudicated \u2014");
|
|
8781
8929
|
err("the verdict below would describe a tree that no longer exists.");
|
|
@@ -8796,7 +8944,7 @@ tamperward run \u2014 agent exit ${agentExit}; HISTORY_REWRITE \u2192 ENFORCEMEN
|
|
|
8796
8944
|
err(`working tree (pid ${survivors.join(", ")}). A verdict cannot outlive the tree it describes.`);
|
|
8797
8945
|
}
|
|
8798
8946
|
const notQuiescent = mutatedDuringAdjudication || survivors.length > 0 || depsDrifted || rewrote;
|
|
8799
|
-
const cannot = diffCode === 2 || workCode === 2 || verifyCode === 2;
|
|
8947
|
+
const cannot = !notQuiescent && (diffCode === 2 || workCode === 2 || verifyCode === 2);
|
|
8800
8948
|
const blocked = diffCode === 1 || workCode === 1 || verifyCode === 1 || notQuiescent;
|
|
8801
8949
|
const enforcement = cannot ? 2 : blocked ? 1 : 0;
|
|
8802
8950
|
const verdict2 = enforcement === 2 ? "CANNOT_ADJUDICATE (fail closed)" : rewrote ? "OBJECT_REWRITE \u2014 the base the checks read is not the base we anchored to" : 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";
|