tamperward 2.5.0 → 2.5.1
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 +150 -54
- package/package.json +1 -1
package/dist/cli/index.js
CHANGED
|
@@ -330,26 +330,52 @@ function diffWorktree(opts = {}) {
|
|
|
330
330
|
(c) => enrich(c, (p) => blobAt("HEAD", p, opts.cwd), (p) => fromDisk(p, opts.cwd))
|
|
331
331
|
);
|
|
332
332
|
}
|
|
333
|
+
function addOf(rel, cwd) {
|
|
334
|
+
const after = fromDisk(rel, cwd);
|
|
335
|
+
return {
|
|
336
|
+
kind: "file",
|
|
337
|
+
path: rel,
|
|
338
|
+
oldPath: null,
|
|
339
|
+
op: "add",
|
|
340
|
+
before: null,
|
|
341
|
+
after,
|
|
342
|
+
binary: false,
|
|
343
|
+
hunks: after == null ? [] : addHunks(after)
|
|
344
|
+
};
|
|
345
|
+
}
|
|
333
346
|
function untrackedAdds(opts = {}, keep, exclude) {
|
|
334
347
|
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
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
op: "add",
|
|
342
|
-
before: null,
|
|
343
|
-
after,
|
|
344
|
-
binary: false,
|
|
345
|
-
hunks: after == null ? [] : addHunks(after)
|
|
346
|
-
};
|
|
347
|
-
});
|
|
348
|
+
return others.filter((rel) => !exclude?.has(rel) && (!keep || keep(rel))).map((rel) => addOf(rel, opts.cwd));
|
|
349
|
+
}
|
|
350
|
+
var SKIP_DIRS = /* @__PURE__ */ new Set([".git", "node_modules", ".hg", ".svn"]);
|
|
351
|
+
var IGNORED_LS = ["ls-files", "--others", "--ignored", "--exclude-standard", "-z"];
|
|
352
|
+
function underSkippedDir(rel) {
|
|
353
|
+
return rel.split("/").some((seg) => SKIP_DIRS.has(seg));
|
|
348
354
|
}
|
|
349
|
-
function
|
|
355
|
+
function ignoredPaths(opts = {}, keep) {
|
|
356
|
+
const files = [];
|
|
357
|
+
const dirs = [];
|
|
358
|
+
for (const entry of git([...IGNORED_LS, "--directory"], opts.cwd).split("\0").filter(Boolean)) {
|
|
359
|
+
if (underSkippedDir(entry)) continue;
|
|
360
|
+
if (entry.endsWith("/")) dirs.push(entry);
|
|
361
|
+
else files.push(entry);
|
|
362
|
+
}
|
|
363
|
+
if (dirs.length > 0) {
|
|
364
|
+
const expanded = git([...IGNORED_LS, "--", ...dirs.map((d) => `:(literal)${d}`)], opts.cwd).split("\0").filter(Boolean);
|
|
365
|
+
for (const rel of expanded) if (!underSkippedDir(rel)) files.push(rel);
|
|
366
|
+
}
|
|
367
|
+
return files.filter(keep);
|
|
368
|
+
}
|
|
369
|
+
function ignoredAdds(opts = {}, keep, exclude) {
|
|
370
|
+
return ignoredPaths(opts, keep).filter((rel) => !exclude?.has(rel)).map((rel) => addOf(rel, opts.cwd));
|
|
371
|
+
}
|
|
372
|
+
function diffWorktreeWithUntracked(opts = {}, keepIgnored) {
|
|
350
373
|
const tracked = diffWorktree(opts);
|
|
351
374
|
const seen = new Set(tracked.filter((c) => c.kind === "file").map((c) => c.path));
|
|
352
|
-
|
|
375
|
+
const untracked = untrackedAdds(opts, void 0, seen);
|
|
376
|
+
for (const c of untracked) if (c.kind === "file") seen.add(c.path);
|
|
377
|
+
const ignored = keepIgnored ? ignoredAdds(opts, keepIgnored, seen) : [];
|
|
378
|
+
return [...tracked, ...untracked, ...ignored];
|
|
353
379
|
}
|
|
354
380
|
function hiddenTrackedPaths(opts = {}) {
|
|
355
381
|
const out2 = [];
|
|
@@ -1985,7 +2011,7 @@ var ciTampering = {
|
|
|
1985
2011
|
|
|
1986
2012
|
// src/detectors/hook-tampering.ts
|
|
1987
2013
|
import { existsSync, readFileSync as readFileSync2 } from "node:fs";
|
|
1988
|
-
import { join as join2 } from "node:path";
|
|
2014
|
+
import { isAbsolute, join as join2, relative } from "node:path";
|
|
1989
2015
|
|
|
1990
2016
|
// src/detectors/policy-diff.ts
|
|
1991
2017
|
import { parse } from "yaml";
|
|
@@ -2495,9 +2521,22 @@ function chmodDropsExec(toks) {
|
|
|
2495
2521
|
return false;
|
|
2496
2522
|
}
|
|
2497
2523
|
var WRAPPERS = /^(?:sudo|command|exec|time|nice|env|[A-Za-z_][A-Za-z0-9_]*=\S*)$/;
|
|
2524
|
+
var REDIRECT_TARGET = /(?:^|\s)\d*>{1,2}\|?\s*(\S+)/g;
|
|
2525
|
+
var hookPath = (t) => t.replace(/^["']|["']$/g, "").replace(/^\.\//, "");
|
|
2526
|
+
function shellHookTarget(seg, toks, policy) {
|
|
2527
|
+
for (const m of seg.matchAll(REDIRECT_TARGET)) {
|
|
2528
|
+
const t = hookPath(m[1]);
|
|
2529
|
+
if (isProtected(t, policy, "hooks")) return t;
|
|
2530
|
+
}
|
|
2531
|
+
for (const tok of toks) {
|
|
2532
|
+
const t = hookPath(tok.replace(/^of=/, ""));
|
|
2533
|
+
if (isProtected(t, policy, "hooks")) return t;
|
|
2534
|
+
}
|
|
2535
|
+
return null;
|
|
2536
|
+
}
|
|
2498
2537
|
function shellWritesHook(seg, toks, policy) {
|
|
2499
2538
|
const hook = (t) => isProtected(t.replace(/^\.\//, ""), policy, "hooks");
|
|
2500
|
-
for (const m of seg.matchAll(
|
|
2539
|
+
for (const m of seg.matchAll(REDIRECT_TARGET)) {
|
|
2501
2540
|
if (hook(m[1].replace(/^["']|["']$/g, ""))) return "a redirect empties or rewrites a protected hook";
|
|
2502
2541
|
}
|
|
2503
2542
|
let at = 0;
|
|
@@ -2708,14 +2747,24 @@ function configFindings(c, policy) {
|
|
|
2708
2747
|
function updateIndexHiding(toks, policy) {
|
|
2709
2748
|
if (!toks.some((t) => t === "git" || t.endsWith("/git")) || !toks.includes("update-index")) return null;
|
|
2710
2749
|
const hide = toks.find((t) => /^--(?:skip-worktree|assume-unchanged)$/.test(t));
|
|
2711
|
-
|
|
2712
|
-
|
|
2750
|
+
const hidden = hide ? toks.find((t) => isProtected(t, policy)) : void 0;
|
|
2751
|
+
if (hide && hidden) {
|
|
2752
|
+
return {
|
|
2753
|
+
reason: `git update-index ${hide} hides a protected file from every git diff \u2014 edits to it no longer reach the sweep or the pre-commit gate`,
|
|
2754
|
+
file: hidden
|
|
2755
|
+
};
|
|
2713
2756
|
}
|
|
2714
|
-
|
|
2715
|
-
|
|
2757
|
+
const hook = toks.some((t) => /^--chmod=-x$/.test(t)) ? toks.find((t) => isProtected(t, policy, "hooks")) : void 0;
|
|
2758
|
+
if (hook) {
|
|
2759
|
+
return { reason: "git update-index --chmod=-x clears the execute bit on a protected hook through the index, which disables it", file: hook };
|
|
2716
2760
|
}
|
|
2717
2761
|
return null;
|
|
2718
2762
|
}
|
|
2763
|
+
function repoRelative(path, ctx) {
|
|
2764
|
+
if (!ctx?.cwd || !isAbsolute(path)) return path;
|
|
2765
|
+
const rel = relative(ctx.cwd, path);
|
|
2766
|
+
return rel && !rel.startsWith("..") && !isAbsolute(rel) ? rel : path;
|
|
2767
|
+
}
|
|
2719
2768
|
var hookTampering = {
|
|
2720
2769
|
id: RULE6,
|
|
2721
2770
|
surface: ["file", "command"],
|
|
@@ -2865,7 +2914,8 @@ var hookTampering = {
|
|
|
2865
2914
|
if (hiding) {
|
|
2866
2915
|
out2.push(
|
|
2867
2916
|
makeFinding(RULE6, policy, {
|
|
2868
|
-
|
|
2917
|
+
file: repoRelative(hiding.file, ctx),
|
|
2918
|
+
message: `Hook tampering via shell: ${hiding.reason}.`,
|
|
2869
2919
|
evidence: seg,
|
|
2870
2920
|
remediation: "Leave protected files visible to git. Hiding one from the diff hides it from the gate, which is the tamper \u2014 use `--no-skip-worktree` / `--no-assume-unchanged` to undo it."
|
|
2871
2921
|
})
|
|
@@ -2874,8 +2924,10 @@ var hookTampering = {
|
|
|
2874
2924
|
}
|
|
2875
2925
|
const why = shellWritesHook(seg, toks, policy);
|
|
2876
2926
|
if (why) {
|
|
2927
|
+
const target = shellHookTarget(seg, toks, policy);
|
|
2877
2928
|
out2.push(
|
|
2878
2929
|
makeFinding(RULE6, policy, {
|
|
2930
|
+
...target ? { file: repoRelative(target, ctx) } : {},
|
|
2879
2931
|
message: `Hook tampering via shell: ${why}.`,
|
|
2880
2932
|
evidence: seg,
|
|
2881
2933
|
remediation: "Leave the hooks in place. Mutating them from the shell is still tampering."
|
|
@@ -3572,7 +3624,7 @@ function hasBlocking(findings) {
|
|
|
3572
3624
|
|
|
3573
3625
|
// src/policy-load.ts
|
|
3574
3626
|
import { readFileSync as readFileSync3, existsSync as existsSync2 } from "node:fs";
|
|
3575
|
-
import { isAbsolute, join as join3, posix } from "node:path";
|
|
3627
|
+
import { isAbsolute as isAbsolute2, join as join3, posix } from "node:path";
|
|
3576
3628
|
import { parse as parse2 } from "yaml";
|
|
3577
3629
|
var PolicyError = class extends Error {
|
|
3578
3630
|
};
|
|
@@ -3589,7 +3641,7 @@ var isStringList = (v) => Array.isArray(v) && v.every((s) => typeof s === "strin
|
|
|
3589
3641
|
var isMapping = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
3590
3642
|
var TOP_LEVEL_KEYS = /* @__PURE__ */ new Set(["version", "protected", "rules", "ignore", "signoff", "verify"]);
|
|
3591
3643
|
function ledgerInsideRepo(ledger) {
|
|
3592
|
-
if (!ledger.trim() ||
|
|
3644
|
+
if (!ledger.trim() || isAbsolute2(ledger) || /^[A-Za-z]:[\\/]/.test(ledger)) return false;
|
|
3593
3645
|
const norm2 = posix.normalize(ledger.replace(/\\/g, "/"));
|
|
3594
3646
|
return norm2 !== ".." && !norm2.startsWith("../") && !norm2.startsWith("/");
|
|
3595
3647
|
}
|
|
@@ -4043,7 +4095,8 @@ function check(opts) {
|
|
|
4043
4095
|
layer = "local";
|
|
4044
4096
|
view = "staged";
|
|
4045
4097
|
} else if (opts.worktree) {
|
|
4046
|
-
|
|
4098
|
+
const p = policy;
|
|
4099
|
+
changes = opts.includeUntracked ? diffWorktreeWithUntracked({ cwd: opts.cwd }, (rel) => isProtected(rel, p)) : diffWorktree({ cwd: opts.cwd });
|
|
4047
4100
|
layer = opts.ciLayer ? "ci" : "local";
|
|
4048
4101
|
view = "worktree";
|
|
4049
4102
|
} else if (opts.diff) {
|
|
@@ -4102,7 +4155,7 @@ import { join as join9 } from "node:path";
|
|
|
4102
4155
|
import { execFileSync as execFileSync3 } from "node:child_process";
|
|
4103
4156
|
import { readFileSync as readFileSync5, existsSync as existsSync4, writeFileSync, mkdtempSync, rmSync } from "node:fs";
|
|
4104
4157
|
import { tmpdir } from "node:os";
|
|
4105
|
-
import { isAbsolute as
|
|
4158
|
+
import { isAbsolute as isAbsolute3, join as join5, relative as relative2, resolve } from "node:path";
|
|
4106
4159
|
function asStr(v) {
|
|
4107
4160
|
return typeof v === "string" ? v : "";
|
|
4108
4161
|
}
|
|
@@ -4117,8 +4170,8 @@ function abs(path, cwd) {
|
|
|
4117
4170
|
return resolve(cwd, path);
|
|
4118
4171
|
}
|
|
4119
4172
|
function relForDisplay(path, cwd) {
|
|
4120
|
-
const rel =
|
|
4121
|
-
return rel && !rel.startsWith("..") && !
|
|
4173
|
+
const rel = relative2(cwd, path);
|
|
4174
|
+
return rel && !rel.startsWith("..") && !isAbsolute3(rel) ? rel : path;
|
|
4122
4175
|
}
|
|
4123
4176
|
function applyEdit(content, oldStr, newStr) {
|
|
4124
4177
|
if (content === null) return newStr;
|
|
@@ -4261,11 +4314,11 @@ function advanceTurnBaseline(cwd, sessionId) {
|
|
|
4261
4314
|
|
|
4262
4315
|
// src/effect.ts
|
|
4263
4316
|
import { createHash as createHash2 } from "node:crypto";
|
|
4264
|
-
import { existsSync as existsSync6, mkdirSync as mkdirSync3, readdirSync, readFileSync as readFileSync7, statSync, writeFileSync as writeFileSync3 } from "node:fs";
|
|
4265
|
-
import { isAbsolute as
|
|
4317
|
+
import { existsSync as existsSync6, mkdirSync as mkdirSync3, readdirSync, readFileSync as readFileSync7, renameSync, rmSync as rmSync2, statSync, writeFileSync as writeFileSync3 } from "node:fs";
|
|
4318
|
+
import { isAbsolute as isAbsolute4, join as join7 } from "node:path";
|
|
4266
4319
|
import { execFileSync as execFileSync4 } from "node:child_process";
|
|
4267
4320
|
var UNSAFE2 = /[^A-Za-z0-9._-]/g;
|
|
4268
|
-
var
|
|
4321
|
+
var SKIP_DIRS2 = /* @__PURE__ */ new Set([".git", "node_modules", ".hg", ".svn"]);
|
|
4269
4322
|
function gitDir2(cwd) {
|
|
4270
4323
|
try {
|
|
4271
4324
|
const d = execFileSync4("git", ["rev-parse", "--git-dir"], {
|
|
@@ -4274,7 +4327,7 @@ function gitDir2(cwd) {
|
|
|
4274
4327
|
stdio: ["ignore", "pipe", "pipe"]
|
|
4275
4328
|
}).trim();
|
|
4276
4329
|
if (!d) return null;
|
|
4277
|
-
return
|
|
4330
|
+
return isAbsolute4(d) ? d : join7(cwd, d);
|
|
4278
4331
|
} catch {
|
|
4279
4332
|
return null;
|
|
4280
4333
|
}
|
|
@@ -4307,7 +4360,7 @@ function snapshotProtected(cwd, policy, prev) {
|
|
|
4307
4360
|
return;
|
|
4308
4361
|
}
|
|
4309
4362
|
for (const name of names) {
|
|
4310
|
-
if (
|
|
4363
|
+
if (SKIP_DIRS2.has(name)) continue;
|
|
4311
4364
|
const r = rel ? `${rel}/${name}` : name;
|
|
4312
4365
|
let st;
|
|
4313
4366
|
try {
|
|
@@ -4333,12 +4386,16 @@ function snapshotProtected(cwd, policy, prev) {
|
|
|
4333
4386
|
function driftBetween(expected, current) {
|
|
4334
4387
|
const changed = [];
|
|
4335
4388
|
const deleted = [];
|
|
4389
|
+
const added = [];
|
|
4336
4390
|
for (const [path, e] of Object.entries(expected)) {
|
|
4337
4391
|
const c = current[path];
|
|
4338
4392
|
if (!c) deleted.push(path);
|
|
4339
4393
|
else if (c.hash !== e.hash || c.mode !== e.mode) changed.push(path);
|
|
4340
4394
|
}
|
|
4341
|
-
|
|
4395
|
+
for (const path of Object.keys(current)) {
|
|
4396
|
+
if (!expected[path]) added.push(path);
|
|
4397
|
+
}
|
|
4398
|
+
return { changed, deleted, added };
|
|
4342
4399
|
}
|
|
4343
4400
|
function loadTree(p) {
|
|
4344
4401
|
if (!p || !existsSync6(p)) return null;
|
|
@@ -4352,7 +4409,16 @@ function saveTree(p, tree) {
|
|
|
4352
4409
|
if (!p) return;
|
|
4353
4410
|
try {
|
|
4354
4411
|
mkdirSync3(join7(p, ".."), { recursive: true });
|
|
4355
|
-
|
|
4412
|
+
const tmp = `${p}.${process.pid}.tmp`;
|
|
4413
|
+
writeFileSync3(tmp, JSON.stringify(tree));
|
|
4414
|
+
renameSync(tmp, p);
|
|
4415
|
+
} catch {
|
|
4416
|
+
}
|
|
4417
|
+
}
|
|
4418
|
+
function dropTree(p) {
|
|
4419
|
+
if (!p) return;
|
|
4420
|
+
try {
|
|
4421
|
+
rmSync2(p, { force: true });
|
|
4356
4422
|
} catch {
|
|
4357
4423
|
}
|
|
4358
4424
|
}
|
|
@@ -4368,6 +4434,9 @@ function loadTurnTree(cwd, sessionId) {
|
|
|
4368
4434
|
function saveTurnTree(cwd, sessionId, tree) {
|
|
4369
4435
|
saveTree(turnTreePath(cwd, sessionId), tree);
|
|
4370
4436
|
}
|
|
4437
|
+
function dropTurnTree(cwd, sessionId) {
|
|
4438
|
+
dropTree(turnTreePath(cwd, sessionId));
|
|
4439
|
+
}
|
|
4371
4440
|
|
|
4372
4441
|
// src/cli/watch.ts
|
|
4373
4442
|
import { appendFileSync as appendFileSync3, mkdirSync as mkdirSync4, readdirSync as readdirSync2, readFileSync as readFileSync8, statSync as statSync2, watch } from "node:fs";
|
|
@@ -4612,17 +4681,26 @@ function turnView(cwd, sessionId, policy, base) {
|
|
|
4612
4681
|
if (c.oldPath) seen.add(c.oldPath);
|
|
4613
4682
|
}
|
|
4614
4683
|
const turnTree = loadTurnTree(cwd, sessionId);
|
|
4615
|
-
const
|
|
4616
|
-
|
|
4617
|
-
);
|
|
4684
|
+
const ptree = loadPtree(cwd, sessionId);
|
|
4685
|
+
const keep = (rel) => isProtected(rel, policy);
|
|
4686
|
+
const untouched = (rel, disk) => disk != null && turnTree?.[rel]?.hash === disk && ptree?.[rel]?.hash === disk;
|
|
4687
|
+
const untracked = untrackedAdds({ cwd }, keep, seen).filter((c) => c.kind !== "file" || !untouched(c.path, hashOnDisk(cwd, c.path)));
|
|
4618
4688
|
for (const c of fileChanges(untracked)) seen.add(c.path);
|
|
4689
|
+
const ignored = ignoredAdds({ cwd }, keep, seen).filter((c) => {
|
|
4690
|
+
if (c.kind !== "file") return false;
|
|
4691
|
+
const disk = hashOnDisk(cwd, c.path);
|
|
4692
|
+
if (untouched(c.path, disk)) return false;
|
|
4693
|
+
const sanctioned = ptree?.[c.path];
|
|
4694
|
+
return !(sanctioned && disk != null && sanctioned.hash !== disk);
|
|
4695
|
+
});
|
|
4696
|
+
for (const c of fileChanges(ignored)) seen.add(c.path);
|
|
4619
4697
|
const trusted = base ?? "HEAD";
|
|
4620
4698
|
const hidden = [];
|
|
4621
4699
|
for (const rel of hiddenTrackedPaths({ cwd })) {
|
|
4622
4700
|
if (seen.has(rel) || !isProtected(rel, policy)) continue;
|
|
4623
4701
|
hidden.push(...synthFileChange(rel, fileAt(trusted, rel, { cwd }), fileOnDisk(rel, { cwd })));
|
|
4624
4702
|
}
|
|
4625
|
-
return [...tracked, ...untracked, ...hidden];
|
|
4703
|
+
return [...tracked, ...untracked, ...ignored, ...hidden];
|
|
4626
4704
|
}
|
|
4627
4705
|
function uncoveredDrift(cwd, policy, expected, current, covered, base, turnTree) {
|
|
4628
4706
|
const d = driftBetween(expected, current);
|
|
@@ -4640,7 +4718,7 @@ function uncoveredDrift(cwd, policy, expected, current, covered, base, turnTree)
|
|
|
4640
4718
|
const after = fileOnDisk(rel, { cwd });
|
|
4641
4719
|
const trustedBlob = fileAt(trusted, rel, { cwd });
|
|
4642
4720
|
const atGitState = after != null && trustedBlob != null && contentHash(after) === contentHash(trustedBlob);
|
|
4643
|
-
const atTurnStart = current[rel] != null && turnTree?.[rel]?.hash === current[rel].hash;
|
|
4721
|
+
const atTurnStart = current[rel] != null && expected[rel]?.mtimeMs === -1 && turnTree?.[rel]?.hash === current[rel].hash;
|
|
4644
4722
|
if ((atGitState || atTurnStart) && current[rel] && expected[rel] && current[rel].mode === expected[rel].mode) {
|
|
4645
4723
|
continue;
|
|
4646
4724
|
}
|
|
@@ -4683,6 +4761,23 @@ function uncoveredDrift(cwd, policy, expected, current, covered, base, turnTree)
|
|
|
4683
4761
|
});
|
|
4684
4762
|
}
|
|
4685
4763
|
}
|
|
4764
|
+
for (const rel of d.added) {
|
|
4765
|
+
if (seen.has(rel)) continue;
|
|
4766
|
+
const after = fileOnDisk(rel, { cwd });
|
|
4767
|
+
if (after == null) {
|
|
4768
|
+
blocks.push({
|
|
4769
|
+
rule: "hidden-drift",
|
|
4770
|
+
severity: "block",
|
|
4771
|
+
file: rel,
|
|
4772
|
+
message: `Protected file ${rel} appeared outside git's view and cannot be read to judge it.`,
|
|
4773
|
+
evidence: rel,
|
|
4774
|
+
remediation: "Remove the file, or make it readable and bring it into git's view; create protected files through a tool call the gate can see.",
|
|
4775
|
+
signoff: { required: true, command: `tamperward allow hidden-drift --file ${rel} --reason "..."` }
|
|
4776
|
+
});
|
|
4777
|
+
continue;
|
|
4778
|
+
}
|
|
4779
|
+
changes.push(...synthFileChange(rel, null, after));
|
|
4780
|
+
}
|
|
4686
4781
|
return { changes, blocks };
|
|
4687
4782
|
}
|
|
4688
4783
|
function effectDriftBlocks(cwd, sessionId, policy) {
|
|
@@ -4691,17 +4786,18 @@ function effectDriftBlocks(cwd, sessionId, policy) {
|
|
|
4691
4786
|
const current = snapshotProtected(cwd, policy, expected ?? void 0);
|
|
4692
4787
|
if (!expected) {
|
|
4693
4788
|
savePtree(cwd, sessionId, current);
|
|
4694
|
-
if (
|
|
4789
|
+
if (loadTurnTree(cwd, sessionId)) dropTurnTree(cwd, sessionId);
|
|
4790
|
+
else saveTurnTree(cwd, sessionId, current);
|
|
4695
4791
|
return null;
|
|
4696
4792
|
}
|
|
4697
4793
|
const d = driftBetween(expected, current);
|
|
4698
|
-
if (d.changed.length === 0 && d.deleted.length === 0) {
|
|
4794
|
+
if (d.changed.length === 0 && d.deleted.length === 0 && d.added.length === 0) {
|
|
4699
4795
|
savePtree(cwd, sessionId, current);
|
|
4700
4796
|
return null;
|
|
4701
4797
|
}
|
|
4702
4798
|
const base = turnBaseline(cwd, sessionId);
|
|
4703
4799
|
const all = turnView(cwd, sessionId, policy, base);
|
|
4704
|
-
const drifted = /* @__PURE__ */ new Set([...d.changed, ...d.deleted]);
|
|
4800
|
+
const drifted = /* @__PURE__ */ new Set([...d.changed, ...d.deleted, ...d.added]);
|
|
4705
4801
|
const scoped = all.filter(
|
|
4706
4802
|
(c) => c.kind === "file" && (drifted.has(c.path) || c.oldPath != null && drifted.has(c.oldPath))
|
|
4707
4803
|
);
|
|
@@ -4916,7 +5012,7 @@ Honored at LOCAL pre-commit only. The agent-layer hook ignores this file; CI req
|
|
|
4916
5012
|
import { execFileSync as execFileSync5 } from "node:child_process";
|
|
4917
5013
|
import { createHash as createHash5 } from "node:crypto";
|
|
4918
5014
|
import { existsSync as existsSync8, mkdirSync as mkdirSync5, readFileSync as readFileSync11, writeFileSync as writeFileSync5, chmodSync, statSync as statSync5 } from "node:fs";
|
|
4919
|
-
import { basename, dirname as dirname3, isAbsolute as
|
|
5015
|
+
import { basename, dirname as dirname3, isAbsolute as isAbsolute5, join as join10, relative as relative3, resolve as resolve2 } from "node:path";
|
|
4920
5016
|
import { fileURLToPath } from "node:url";
|
|
4921
5017
|
var errText2 = (e) => (e instanceof Error ? e.message : String(e)).replace(/\s+/g, " ").trim();
|
|
4922
5018
|
var isPlainObject = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
@@ -5234,8 +5330,8 @@ function hooksDir(cwd) {
|
|
|
5234
5330
|
return existsSync8(join10(cwd, ".git")) ? join10(cwd, ".git", "hooks") : null;
|
|
5235
5331
|
}
|
|
5236
5332
|
function display(cwd, path) {
|
|
5237
|
-
const rel =
|
|
5238
|
-
return rel && !rel.startsWith("..") && !
|
|
5333
|
+
const rel = relative3(cwd, path);
|
|
5334
|
+
return rel && !rel.startsWith("..") && !isAbsolute5(rel) ? rel : path;
|
|
5239
5335
|
}
|
|
5240
5336
|
function unconditionalExitAt(lines) {
|
|
5241
5337
|
let depth = 0;
|
|
@@ -5521,7 +5617,7 @@ tamperward init: ${changed} change(s) applied. Commit them so the gate travels w
|
|
|
5521
5617
|
// src/cli/verify.ts
|
|
5522
5618
|
import { execFileSync as execFileSync8, spawnSync } from "node:child_process";
|
|
5523
5619
|
import { createHash as createHash8 } from "node:crypto";
|
|
5524
|
-
import { chmodSync as chmodSync2, cpSync, lstatSync, mkdirSync as mkdirSync6, mkdtempSync as mkdtempSync2, readFileSync as readFileSync14, readlinkSync as readlinkSync2, rmSync as
|
|
5620
|
+
import { chmodSync as chmodSync2, cpSync, lstatSync, mkdirSync as mkdirSync6, mkdtempSync as mkdtempSync2, readFileSync as readFileSync14, readlinkSync as readlinkSync2, rmSync as rmSync3, statSync as statSync7, symlinkSync, writeFileSync as writeFileSync6, existsSync as existsSync10 } from "node:fs";
|
|
5525
5621
|
import { tmpdir as tmpdir2 } from "node:os";
|
|
5526
5622
|
import { dirname as dirname4, join as join13, resolve as resolve3 } from "node:path";
|
|
5527
5623
|
|
|
@@ -5529,7 +5625,7 @@ import { dirname as dirname4, join as join13, resolve as resolve3 } from "node:p
|
|
|
5529
5625
|
import { execFileSync as execFileSync6 } from "node:child_process";
|
|
5530
5626
|
import { createHash as createHash6 } from "node:crypto";
|
|
5531
5627
|
import { existsSync as existsSync9, readFileSync as readFileSync12 } from "node:fs";
|
|
5532
|
-
import { isAbsolute as
|
|
5628
|
+
import { isAbsolute as isAbsolute6, join as join11 } from "node:path";
|
|
5533
5629
|
function trustedGitEnv() {
|
|
5534
5630
|
return { ...process.env, GIT_NO_REPLACE_OBJECTS: "1" };
|
|
5535
5631
|
}
|
|
@@ -5542,7 +5638,7 @@ function gitDirOf(cwd) {
|
|
|
5542
5638
|
env: trustedGitEnv()
|
|
5543
5639
|
}).trim();
|
|
5544
5640
|
if (!d) return null;
|
|
5545
|
-
return
|
|
5641
|
+
return isAbsolute6(d) ? d : join11(cwd, d);
|
|
5546
5642
|
} catch {
|
|
5547
5643
|
return null;
|
|
5548
5644
|
}
|
|
@@ -5756,7 +5852,7 @@ function materialize(cwd, dest) {
|
|
|
5756
5852
|
}
|
|
5757
5853
|
function dropSymlink(p) {
|
|
5758
5854
|
try {
|
|
5759
|
-
if (lstatSync(p).isSymbolicLink())
|
|
5855
|
+
if (lstatSync(p).isSymbolicLink()) rmSync3(p, { force: true });
|
|
5760
5856
|
} catch {
|
|
5761
5857
|
}
|
|
5762
5858
|
}
|
|
@@ -5814,7 +5910,7 @@ function overlayPristine(cwd, base, dest, policy, cmd) {
|
|
|
5814
5910
|
const content = execFileSync8("git", ["show", `${base}:${e.path}`], { cwd, maxBuffer: 1 << 28, env: trustedGitEnv() });
|
|
5815
5911
|
const out2 = join13(dest, e.path);
|
|
5816
5912
|
mkdirSync6(dirname4(out2), { recursive: true });
|
|
5817
|
-
|
|
5913
|
+
rmSync3(out2, { force: true });
|
|
5818
5914
|
if (e.mode === "120000") {
|
|
5819
5915
|
symlinkSync(content.toString("utf8"), out2);
|
|
5820
5916
|
} else {
|
|
@@ -5827,7 +5923,7 @@ function overlayPristine(cwd, base, dest, policy, cmd) {
|
|
|
5827
5923
|
const inCopy = git2(["ls-files", "-z", "--cached", "--others", "--exclude-standard"], cwd).split("\0").filter(Boolean);
|
|
5828
5924
|
for (const rel of inCopy) {
|
|
5829
5925
|
if (!isOverlay(rel) || baseProtected.has(rel)) continue;
|
|
5830
|
-
|
|
5926
|
+
rmSync3(join13(dest, rel), { force: true });
|
|
5831
5927
|
removed++;
|
|
5832
5928
|
}
|
|
5833
5929
|
return { restored, removed };
|
|
@@ -5902,7 +5998,7 @@ function runSuite(dir, cmd, budgetSecs) {
|
|
|
5902
5998
|
if (r.exit === null || r.exit === void 0) return { exit: null, secs };
|
|
5903
5999
|
return { exit: r.exit, secs };
|
|
5904
6000
|
} finally {
|
|
5905
|
-
|
|
6001
|
+
rmSync3(outDir, { recursive: true, force: true });
|
|
5906
6002
|
}
|
|
5907
6003
|
}
|
|
5908
6004
|
function runVerify(opts) {
|
|
@@ -5949,7 +6045,7 @@ function runVerify(opts) {
|
|
|
5949
6045
|
const visDir = join13(visRoot, "t");
|
|
5950
6046
|
const cleanup = (dirs) => {
|
|
5951
6047
|
if (opts.keep) return;
|
|
5952
|
-
for (const d of dirs)
|
|
6048
|
+
for (const d of dirs) rmSync3(d, { recursive: true, force: true });
|
|
5953
6049
|
};
|
|
5954
6050
|
const treeBefore = treeFingerprint(cwd);
|
|
5955
6051
|
const depsBefore = depsFingerprint(cwd);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tamperward",
|
|
3
|
-
"version": "2.5.
|
|
3
|
+
"version": "2.5.1",
|
|
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",
|