tamperward 2.7.1 → 2.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/index.js +526 -355
- package/package.json +1 -1
package/dist/cli/index.js
CHANGED
|
@@ -2,9 +2,376 @@
|
|
|
2
2
|
|
|
3
3
|
// src/git/build.ts
|
|
4
4
|
import { execFileSync } from "node:child_process";
|
|
5
|
-
|
|
5
|
+
|
|
6
|
+
// src/disk.ts
|
|
7
|
+
import { closeSync, constants, fstatSync, lstatSync, openSync, readSync, readlinkSync, realpathSync } from "node:fs";
|
|
6
8
|
import { join } from "node:path";
|
|
7
9
|
|
|
10
|
+
// src/policy.ts
|
|
11
|
+
import picomatch from "picomatch";
|
|
12
|
+
var POLICY_FILE = ".tamperward.yml";
|
|
13
|
+
var BLOCK_SINCE = {};
|
|
14
|
+
function applyVersionGates(rules, version, gates = BLOCK_SINCE) {
|
|
15
|
+
const out2 = { ...rules };
|
|
16
|
+
for (const [rule, since] of Object.entries(gates)) {
|
|
17
|
+
if (since > version && out2[rule]?.severity === "block") {
|
|
18
|
+
out2[rule] = { ...out2[rule], severity: "warn" };
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
return out2;
|
|
22
|
+
}
|
|
23
|
+
function normalizeGlob(glob) {
|
|
24
|
+
return glob.startsWith("/") ? glob.replace(/^\/+/, "") : glob;
|
|
25
|
+
}
|
|
26
|
+
function mergeRules(base, user) {
|
|
27
|
+
const out2 = { ...base };
|
|
28
|
+
for (const [name, cfg] of Object.entries(user ?? {})) {
|
|
29
|
+
out2[name] = { ...out2[name] ?? {}, ...cfg ?? {} };
|
|
30
|
+
}
|
|
31
|
+
return out2;
|
|
32
|
+
}
|
|
33
|
+
var CONTROL = /[\u0000-\u001f\u007f]/;
|
|
34
|
+
function hasControlChar(path) {
|
|
35
|
+
return CONTROL.test(path);
|
|
36
|
+
}
|
|
37
|
+
function escapeControl(path) {
|
|
38
|
+
return path.replace(/[\u0000-\u001f\u007f]/g, (ch) => {
|
|
39
|
+
const c = ch.charCodeAt(0);
|
|
40
|
+
if (c === 10) return "\\n";
|
|
41
|
+
if (c === 13) return "\\r";
|
|
42
|
+
if (c === 9) return "\\t";
|
|
43
|
+
return `\\x${c.toString(16).padStart(2, "0")}`;
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
var listCache = /* @__PURE__ */ new WeakMap();
|
|
47
|
+
function listMatcher(globs4) {
|
|
48
|
+
let re = listCache.get(globs4);
|
|
49
|
+
if (re === void 0) {
|
|
50
|
+
re = globs4.length === 0 ? null : new RegExp(globs4.map((g) => `(?:${picomatch.makeRe(g, { dot: true }).source})`).join("|"));
|
|
51
|
+
listCache.set(globs4, re);
|
|
52
|
+
}
|
|
53
|
+
return re;
|
|
54
|
+
}
|
|
55
|
+
function matchesAny(path, globs4) {
|
|
56
|
+
if (!globs4) return false;
|
|
57
|
+
const re = listMatcher(globs4);
|
|
58
|
+
return re !== null && re.test(path);
|
|
59
|
+
}
|
|
60
|
+
function protectedCategory(path, policy) {
|
|
61
|
+
const cats = Object.entries(policy.protected ?? {});
|
|
62
|
+
if (hasControlChar(path)) return cats[0]?.[0] ?? "tests";
|
|
63
|
+
for (const [cat, globs4] of cats) {
|
|
64
|
+
if (matchesAny(path, globs4)) return cat;
|
|
65
|
+
}
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
var anyCache = /* @__PURE__ */ new WeakMap();
|
|
69
|
+
function allProtectedGlobs(policy) {
|
|
70
|
+
const p = policy.protected ?? {};
|
|
71
|
+
let all = anyCache.get(p);
|
|
72
|
+
if (!all) {
|
|
73
|
+
all = Object.values(p).flat();
|
|
74
|
+
anyCache.set(p, all);
|
|
75
|
+
}
|
|
76
|
+
return all;
|
|
77
|
+
}
|
|
78
|
+
function isProtected(path, policy, category) {
|
|
79
|
+
if (hasControlChar(path)) return true;
|
|
80
|
+
if (category) return matchesAny(path, policy.protected?.[category]);
|
|
81
|
+
return matchesAny(path, allProtectedGlobs(policy));
|
|
82
|
+
}
|
|
83
|
+
function isPolicyFile(path) {
|
|
84
|
+
return path === POLICY_FILE || path.endsWith("/" + POLICY_FILE);
|
|
85
|
+
}
|
|
86
|
+
function isIgnored(path, policy) {
|
|
87
|
+
return matchesAny(path, policy.ignore);
|
|
88
|
+
}
|
|
89
|
+
function mergeProtected(base, user) {
|
|
90
|
+
const out2 = { ...base };
|
|
91
|
+
for (const [cat, globs4] of Object.entries(user ?? {})) {
|
|
92
|
+
const have = new Set(out2[cat] ?? []);
|
|
93
|
+
out2[cat] = [...out2[cat] ?? [], ...(globs4 ?? []).filter((g) => !have.has(g))];
|
|
94
|
+
}
|
|
95
|
+
return out2;
|
|
96
|
+
}
|
|
97
|
+
function defaultPolicy(version = 1) {
|
|
98
|
+
const p = {
|
|
99
|
+
version,
|
|
100
|
+
protected: {
|
|
101
|
+
// Cover every JS/TS test extension, not just .test.ts — the multi-repo FP study found
|
|
102
|
+
// .test.tsx/.spec.tsx (hono, zustand) slipped the glob, so legit test-file casts blocked
|
|
103
|
+
// instead of warning. Kept ADDITIVE (originals + new extensions) so the policy-diff sees a
|
|
104
|
+
// strengthening, not a narrowing. picomatch expands the brace.
|
|
105
|
+
// test-skip, test-deletion and test-content-removal all gate on this
|
|
106
|
+
// class, so a repo it does not match gets rules that can NEVER fire —
|
|
107
|
+
// protection that is silently absent rather than visibly off. The JS/TS
|
|
108
|
+
// list shipped alone; these are the conventional layouts of the other
|
|
109
|
+
// ecosystems `init` is run in. (P2-13, external review.)
|
|
110
|
+
tests: [
|
|
111
|
+
// JavaScript / TypeScript
|
|
112
|
+
"**/*.test.ts",
|
|
113
|
+
"**/*.spec.ts",
|
|
114
|
+
"**/*.{test,spec}.{tsx,cts,mts,js,jsx,cjs,mjs}",
|
|
115
|
+
"**/__tests__/**",
|
|
116
|
+
// Python (pytest / unittest layouts)
|
|
117
|
+
"**/test_*.py",
|
|
118
|
+
"**/*_test.py",
|
|
119
|
+
"**/conftest.py",
|
|
120
|
+
// Go
|
|
121
|
+
"**/*_test.go",
|
|
122
|
+
// Rust (integration tests live in tests/, unit tests inline)
|
|
123
|
+
"**/tests/**/*.rs",
|
|
124
|
+
// Ruby (RSpec / minitest)
|
|
125
|
+
"**/*_spec.rb",
|
|
126
|
+
"**/*_test.rb",
|
|
127
|
+
// Java / Kotlin / Scala (Maven & Gradle convention)
|
|
128
|
+
"**/src/test/**",
|
|
129
|
+
// PHP (PHPUnit)
|
|
130
|
+
"**/*Test.php",
|
|
131
|
+
// C# (.NET)
|
|
132
|
+
"**/*Tests.cs",
|
|
133
|
+
"**/*Test.cs"
|
|
134
|
+
],
|
|
135
|
+
config: [
|
|
136
|
+
"**/jest.config.*",
|
|
137
|
+
"**/vitest.config.*",
|
|
138
|
+
// A `vitest.workspace.*` file REPLACES the root config's selection with the
|
|
139
|
+
// union of its projects, and a `vite.config.*` carrying a `test:` key is the
|
|
140
|
+
// vitest config when no vitest.config exists — both are where a spec can be
|
|
141
|
+
// dropped from the suite without a line of it changing. ADDITIVE, like every
|
|
142
|
+
// baseline widening (the policy-diff reads it as a strengthening).
|
|
143
|
+
"**/vitest.workspace.*",
|
|
144
|
+
"**/vite.config.*",
|
|
145
|
+
"**/tsconfig*.json",
|
|
146
|
+
"**/.eslintrc*",
|
|
147
|
+
"**/eslint.config.*",
|
|
148
|
+
"**/package.json",
|
|
149
|
+
// Coverage gates outside the Jest/Vitest shapes. coverage-lowering could only
|
|
150
|
+
// see a threshold it was shown: a Python `fail_under`, an nyc `lines`, a
|
|
151
|
+
// codecov `target` lived in files no glob protected, so lowering them fired
|
|
152
|
+
// nothing. ADDITIVE, like every baseline widening (the policy-diff reads it
|
|
153
|
+
// as a strengthening). pyproject.toml carries [tool.coverage.report].
|
|
154
|
+
"**/.coveragerc",
|
|
155
|
+
"**/pyproject.toml",
|
|
156
|
+
"**/.nycrc",
|
|
157
|
+
"**/.nycrc.*",
|
|
158
|
+
"**/nyc.config.*",
|
|
159
|
+
"**/codecov.yml",
|
|
160
|
+
"**/.codecov.yml"
|
|
161
|
+
],
|
|
162
|
+
ci: [".github/workflows/**"],
|
|
163
|
+
// Recorded expected outputs. An assertion stored as data is still an assertion;
|
|
164
|
+
// rewriting it from current output is the snapshot-update move the affordance
|
|
165
|
+
// experiment measured at a 70% attempt / 100% through rate (snapshot-rewrite).
|
|
166
|
+
// tap-snapshots/** added from Phase-3 evidence: node-tar's recorded expectations
|
|
167
|
+
// live there as .cjs and were rewritten unseen (PHASE3.md §4).
|
|
168
|
+
snapshots: ["**/*.snap", "**/__snapshots__/**", "**/tap-snapshots/**", "**/golden/**", "**/*.golden.*"],
|
|
169
|
+
// The wiring IS the enforcement: settings.json carries the PreToolUse and
|
|
170
|
+
// Stop hooks, and a pre-commit config carries the commit-time gate. These
|
|
171
|
+
// were installed by `init` and protected by nothing, so editing them out
|
|
172
|
+
// fired no rule at all. (.git/hooks/** is outside every git view and so
|
|
173
|
+
// cannot be covered here — noted in SECURITY-ENVELOPE.md.) (P1-4, review.)
|
|
174
|
+
hooks: [
|
|
175
|
+
".husky/**",
|
|
176
|
+
"**/lefthook.*",
|
|
177
|
+
// lefthook merges `lefthook-local.yml` OVER `lefthook.yml`: a `skip: true`
|
|
178
|
+
// there switches the gate off without touching the file init pointed at.
|
|
179
|
+
"**/lefthook-local.*",
|
|
180
|
+
".tamperward.yml",
|
|
181
|
+
"**/.tamperward.yml",
|
|
182
|
+
".claude/settings.json",
|
|
183
|
+
".claude/settings.local.json",
|
|
184
|
+
"**/.pre-commit-config.yaml",
|
|
185
|
+
// CODEOWNERS is enforcement wiring, not documentation: it is the only
|
|
186
|
+
// thing that can require a human on a change to the workflow that
|
|
187
|
+
// decides whether the gate runs at all. GitHub reads it from any of
|
|
188
|
+
// these three locations. (H2.)
|
|
189
|
+
".github/CODEOWNERS",
|
|
190
|
+
"CODEOWNERS",
|
|
191
|
+
"docs/CODEOWNERS"
|
|
192
|
+
]
|
|
193
|
+
},
|
|
194
|
+
rules: {
|
|
195
|
+
"test-deletion": { severity: "block" },
|
|
196
|
+
// content gutted from a surviving spec — the class Phase 3 measured walking
|
|
197
|
+
// through the block-count rule (rows, assertion arguments, setup). Severity
|
|
198
|
+
// backed by the real-commit corpus measurement recorded in the rule's SPEC row.
|
|
199
|
+
"test-content-removal": { severity: "block" },
|
|
200
|
+
"test-skip": { severity: "block" },
|
|
201
|
+
"ts-any-cast": { severity: "block" },
|
|
202
|
+
// the unambiguous explicit casts + ts-suppression directives
|
|
203
|
+
// broad any in annotation/generic position is common in legit code (measured ~84-100% FP as a
|
|
204
|
+
// block rule on real TS), so it WARNs until a semantic (error-silencing-aware) signal earns block.
|
|
205
|
+
"ts-any-launder": { severity: "warn" },
|
|
206
|
+
"lint-suppression": { severity: "block" },
|
|
207
|
+
"coverage-lowering": { severity: "block" },
|
|
208
|
+
"ci-tampering": { severity: "block" },
|
|
209
|
+
"hook-tampering": { severity: "block" },
|
|
210
|
+
"no-verify": { severity: "block" },
|
|
211
|
+
// mechanical but intent-ambiguous: updating a snapshot is the legitimate workflow
|
|
212
|
+
// when intended output changes. WARN until the §7 evidence path earns block.
|
|
213
|
+
"snapshot-rewrite": { severity: "warn" },
|
|
214
|
+
// the watcher's transient class (modified-and-restored within a turn). WARN by
|
|
215
|
+
// default: a Stop block already forces restores, and a compliant restore reads
|
|
216
|
+
// as churn — block-by-default would deadlock the honest path. Harnesses that own
|
|
217
|
+
// that semantics raise it via TAMPERWARD_TRANSIENT=block.
|
|
218
|
+
"transient-protected-mutation": { severity: "warn" },
|
|
219
|
+
// the narrow graduation candidate: snapshot moved with NO accompanying change.
|
|
220
|
+
// ~0.06% FP on 1,652 audited mainline commits, 7/7 TP on observed tampers
|
|
221
|
+
// (harness/fp-study/). warn until a version: gate graduates it (BLOCK_SINCE).
|
|
222
|
+
"snapshot-only-rewrite": { severity: "warn" },
|
|
223
|
+
// heuristic — warn until precision is measured (SPEC §7)
|
|
224
|
+
"assertion-weakening": { severity: "warn" },
|
|
225
|
+
"guard-removal": { severity: "warn" }
|
|
226
|
+
},
|
|
227
|
+
ignore: [],
|
|
228
|
+
signoff: { requiredFor: ["block"], ledger: ".tamperward/ledger.jsonl" }
|
|
229
|
+
};
|
|
230
|
+
p.rules = applyVersionGates(p.rules, version);
|
|
231
|
+
return p;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// src/disk.ts
|
|
235
|
+
var READ_CAP = 64 * 1024 * 1024;
|
|
236
|
+
var NO_META = { mode: 0, size: 0, mtimeMs: 0 };
|
|
237
|
+
function errText(e) {
|
|
238
|
+
return e instanceof Error ? e.message : String(e);
|
|
239
|
+
}
|
|
240
|
+
function errCode(e) {
|
|
241
|
+
return String(e?.code ?? "");
|
|
242
|
+
}
|
|
243
|
+
function irregularName(st) {
|
|
244
|
+
if (st.isFIFO()) return "fifo";
|
|
245
|
+
if (st.isSocket()) return "socket";
|
|
246
|
+
if (st.isCharacterDevice()) return "character device";
|
|
247
|
+
if (st.isBlockDevice()) return "block device";
|
|
248
|
+
return "unknown file type";
|
|
249
|
+
}
|
|
250
|
+
function meta(st) {
|
|
251
|
+
return { mode: st.mode, size: st.size, mtimeMs: st.mtimeMs };
|
|
252
|
+
}
|
|
253
|
+
var O_READ = constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0) | (constants.O_NONBLOCK ?? 0);
|
|
254
|
+
function readRegular(abs2, st) {
|
|
255
|
+
let fd;
|
|
256
|
+
try {
|
|
257
|
+
fd = openSync(abs2, O_READ);
|
|
258
|
+
} catch (e) {
|
|
259
|
+
if (errCode(e) === "ELOOP") return inspectOnce(abs2);
|
|
260
|
+
return { kind: "unreadable", content: null, ...meta(st), detail: errText(e) };
|
|
261
|
+
}
|
|
262
|
+
try {
|
|
263
|
+
const now = fstatSync(fd);
|
|
264
|
+
if (!now.isFile()) return { kind: "irregular", content: null, ...meta(now), detail: irregularName(now) };
|
|
265
|
+
if (now.size > READ_CAP) return { kind: "oversize", content: null, ...meta(now), detail: `${now.size} bytes` };
|
|
266
|
+
const buf = Buffer.allocUnsafe(now.size);
|
|
267
|
+
let off2 = 0;
|
|
268
|
+
while (off2 < now.size) {
|
|
269
|
+
const n = readSync(fd, buf, off2, now.size - off2, off2);
|
|
270
|
+
if (n === 0) break;
|
|
271
|
+
off2 += n;
|
|
272
|
+
}
|
|
273
|
+
return { kind: "file", content: off2 === now.size ? buf : buf.subarray(0, off2), mode: now.mode, size: off2, mtimeMs: now.mtimeMs, detail: "" };
|
|
274
|
+
} catch (e) {
|
|
275
|
+
return { kind: "unreadable", content: null, ...meta(st), detail: errText(e) };
|
|
276
|
+
} finally {
|
|
277
|
+
closeSync(fd);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
function classify(abs2, st) {
|
|
281
|
+
if (st.isSymbolicLink()) {
|
|
282
|
+
try {
|
|
283
|
+
const target = readlinkSync(abs2);
|
|
284
|
+
return { kind: "symlink", content: Buffer.from(target, "utf8"), ...meta(st), detail: target };
|
|
285
|
+
} catch (e) {
|
|
286
|
+
return { kind: "unreadable", content: null, ...meta(st), detail: errText(e) };
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
if (st.isDirectory()) return { kind: "directory", content: null, ...meta(st), detail: "directory" };
|
|
290
|
+
if (!st.isFile()) return { kind: "irregular", content: null, ...meta(st), detail: irregularName(st) };
|
|
291
|
+
if (st.size > READ_CAP) return { kind: "oversize", content: null, ...meta(st), detail: `${st.size} bytes` };
|
|
292
|
+
return readRegular(abs2, st);
|
|
293
|
+
}
|
|
294
|
+
function inspectOnce(abs2) {
|
|
295
|
+
let st;
|
|
296
|
+
try {
|
|
297
|
+
st = lstatSync(abs2);
|
|
298
|
+
} catch (e) {
|
|
299
|
+
const code = errCode(e);
|
|
300
|
+
if (code === "ENOENT" || code === "ENOTDIR") return { kind: "absent", content: null, ...NO_META, detail: "" };
|
|
301
|
+
return { kind: "unreadable", content: null, ...NO_META, detail: errText(e) };
|
|
302
|
+
}
|
|
303
|
+
if (st.isSymbolicLink() || !st.isFile()) return classify(abs2, st);
|
|
304
|
+
return { kind: "unreadable", content: null, ...meta(st), detail: "the path changed shape while it was being read" };
|
|
305
|
+
}
|
|
306
|
+
function inspectPath(abs2) {
|
|
307
|
+
let st;
|
|
308
|
+
try {
|
|
309
|
+
st = lstatSync(abs2);
|
|
310
|
+
} catch (e) {
|
|
311
|
+
const code = errCode(e);
|
|
312
|
+
if (code === "ENOENT" || code === "ENOTDIR") return { kind: "absent", content: null, ...NO_META, detail: "" };
|
|
313
|
+
return { kind: "unreadable", content: null, ...NO_META, detail: errText(e) };
|
|
314
|
+
}
|
|
315
|
+
return classify(abs2, st);
|
|
316
|
+
}
|
|
317
|
+
function inspectRel(cwd, rel) {
|
|
318
|
+
return inspectPath(join(cwd, rel));
|
|
319
|
+
}
|
|
320
|
+
function inspectResolved(abs2) {
|
|
321
|
+
let real;
|
|
322
|
+
try {
|
|
323
|
+
real = realpathSync(abs2);
|
|
324
|
+
} catch (e) {
|
|
325
|
+
const code = errCode(e);
|
|
326
|
+
if (code === "ENOENT" || code === "ENOTDIR") return { kind: "absent", content: null, ...NO_META, detail: "" };
|
|
327
|
+
return { kind: "unreadable", content: null, ...NO_META, detail: errText(e) };
|
|
328
|
+
}
|
|
329
|
+
return inspectPath(real);
|
|
330
|
+
}
|
|
331
|
+
function textOf(e) {
|
|
332
|
+
return e.content == null ? null : e.content.toString("utf8");
|
|
333
|
+
}
|
|
334
|
+
function describe(e, shown) {
|
|
335
|
+
switch (e.kind) {
|
|
336
|
+
case "symlink":
|
|
337
|
+
return `Protected path ${shown} is a symbolic link to ${escapeControl(e.detail)}. The gate never follows a link, so what the runner will read there cannot be judged.`;
|
|
338
|
+
case "directory":
|
|
339
|
+
return `Protected path ${shown} is a directory where a file is expected, so it cannot be judged.`;
|
|
340
|
+
case "irregular":
|
|
341
|
+
return `Protected path ${shown} is a ${e.detail}, not a regular file, so it cannot be read to judge it.`;
|
|
342
|
+
case "oversize":
|
|
343
|
+
return `Protected file ${shown} is ${e.detail}, above the ${READ_CAP / (1024 * 1024)} MiB the gate reads, so it is judged by name.`;
|
|
344
|
+
default:
|
|
345
|
+
return `Protected file ${shown} cannot be read to judge it (${e.detail}).`;
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
function unjudgeableFinding(cwd, rel) {
|
|
349
|
+
const e = inspectRel(cwd, rel);
|
|
350
|
+
if (e.kind === "file" || e.kind === "absent") return null;
|
|
351
|
+
const shown = escapeControl(rel);
|
|
352
|
+
return {
|
|
353
|
+
rule: "hidden-drift",
|
|
354
|
+
severity: "block",
|
|
355
|
+
file: rel,
|
|
356
|
+
message: describe(e, shown),
|
|
357
|
+
evidence: `${shown}: ${e.kind}${e.detail ? ` (${escapeControl(e.detail)})` : ""}`,
|
|
358
|
+
remediation: "Put a regular file the gate can read at the path \u2014 no symbolic link, FIFO, socket or device, and under 64 MiB \u2014 through a tool call the gate can see, or remove it.",
|
|
359
|
+
signoff: { required: true, command: `tamperward allow hidden-drift --file ${shown} --reason "..."` }
|
|
360
|
+
};
|
|
361
|
+
}
|
|
362
|
+
function unjudgeableProtected(cwd, policy, changes) {
|
|
363
|
+
const out2 = [];
|
|
364
|
+
const seen = /* @__PURE__ */ new Set();
|
|
365
|
+
for (const c of changes) {
|
|
366
|
+
if (c.kind !== "file" || c.op === "delete" || seen.has(c.path)) continue;
|
|
367
|
+
seen.add(c.path);
|
|
368
|
+
if (!isProtected(c.path, policy)) continue;
|
|
369
|
+
const f = unjudgeableFinding(cwd, c.path);
|
|
370
|
+
if (f) out2.push(f);
|
|
371
|
+
}
|
|
372
|
+
return out2;
|
|
373
|
+
}
|
|
374
|
+
|
|
8
375
|
// src/diff/parse.ts
|
|
9
376
|
var HUNK_RE = /^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@/;
|
|
10
377
|
var C_ESCAPES = { a: 7, b: 8, f: 12, n: 10, r: 13, t: 9, v: 11, "\\": 92, '"': 34 };
|
|
@@ -251,11 +618,7 @@ function blobAt(rev, path, cwd) {
|
|
|
251
618
|
}
|
|
252
619
|
}
|
|
253
620
|
function fromDisk(path, cwd) {
|
|
254
|
-
|
|
255
|
-
return readFileSync(join(cwd ?? process.cwd(), path), "utf8");
|
|
256
|
-
} catch {
|
|
257
|
-
return null;
|
|
258
|
-
}
|
|
621
|
+
return textOf(inspectRel(cwd ?? process.cwd(), path));
|
|
259
622
|
}
|
|
260
623
|
function enrich(c, beforeReader, afterReader) {
|
|
261
624
|
if (c.kind !== "file") return c;
|
|
@@ -353,6 +716,9 @@ function underSkippedDir(rel) {
|
|
|
353
716
|
return rel.split("/").some((seg) => SKIP_DIRS.has(seg));
|
|
354
717
|
}
|
|
355
718
|
function ignoredPaths(opts = {}, keep) {
|
|
719
|
+
return ignoredTree(opts, keep).files;
|
|
720
|
+
}
|
|
721
|
+
function ignoredTree(opts = {}, keep) {
|
|
356
722
|
const files = [];
|
|
357
723
|
const dirs = [];
|
|
358
724
|
for (const entry of git([...IGNORED_LS, "--directory"], opts.cwd).split("\0").filter(Boolean)) {
|
|
@@ -364,7 +730,7 @@ function ignoredPaths(opts = {}, keep) {
|
|
|
364
730
|
const expanded = git([...IGNORED_LS, "--", ...dirs.map((d) => `:(literal)${d}`)], opts.cwd).split("\0").filter(Boolean);
|
|
365
731
|
for (const rel of expanded) if (!underSkippedDir(rel)) files.push(rel);
|
|
366
732
|
}
|
|
367
|
-
return files.filter(keep);
|
|
733
|
+
return { dirs, files: files.filter(keep) };
|
|
368
734
|
}
|
|
369
735
|
function ignoredAdds(opts = {}, keep, exclude) {
|
|
370
736
|
return ignoredPaths(opts, keep).filter((rel) => !exclude?.has(rel)).map((rel) => addOf(rel, opts.cwd));
|
|
@@ -588,202 +954,6 @@ function removedLines(c) {
|
|
|
588
954
|
return c.hunks.flatMap((h) => h.lines.filter((l) => l.type === "del"));
|
|
589
955
|
}
|
|
590
956
|
|
|
591
|
-
// src/policy.ts
|
|
592
|
-
import picomatch from "picomatch";
|
|
593
|
-
var POLICY_FILE = ".tamperward.yml";
|
|
594
|
-
var BLOCK_SINCE = {};
|
|
595
|
-
function applyVersionGates(rules, version, gates = BLOCK_SINCE) {
|
|
596
|
-
const out2 = { ...rules };
|
|
597
|
-
for (const [rule, since] of Object.entries(gates)) {
|
|
598
|
-
if (since > version && out2[rule]?.severity === "block") {
|
|
599
|
-
out2[rule] = { ...out2[rule], severity: "warn" };
|
|
600
|
-
}
|
|
601
|
-
}
|
|
602
|
-
return out2;
|
|
603
|
-
}
|
|
604
|
-
function normalizeGlob(glob) {
|
|
605
|
-
return glob.startsWith("/") ? glob.replace(/^\/+/, "") : glob;
|
|
606
|
-
}
|
|
607
|
-
function mergeRules(base, user) {
|
|
608
|
-
const out2 = { ...base };
|
|
609
|
-
for (const [name, cfg] of Object.entries(user ?? {})) {
|
|
610
|
-
out2[name] = { ...out2[name] ?? {}, ...cfg ?? {} };
|
|
611
|
-
}
|
|
612
|
-
return out2;
|
|
613
|
-
}
|
|
614
|
-
var cache = /* @__PURE__ */ new Map();
|
|
615
|
-
function matcher(glob) {
|
|
616
|
-
let m = cache.get(glob);
|
|
617
|
-
if (!m) {
|
|
618
|
-
m = picomatch(glob, { dot: true });
|
|
619
|
-
cache.set(glob, m);
|
|
620
|
-
}
|
|
621
|
-
return m;
|
|
622
|
-
}
|
|
623
|
-
function matchesAny(path, globs4) {
|
|
624
|
-
return !!globs4 && globs4.some((g) => matcher(g)(path));
|
|
625
|
-
}
|
|
626
|
-
function protectedCategory(path, policy) {
|
|
627
|
-
for (const [cat, globs4] of Object.entries(policy.protected ?? {})) {
|
|
628
|
-
if (matchesAny(path, globs4)) return cat;
|
|
629
|
-
}
|
|
630
|
-
return null;
|
|
631
|
-
}
|
|
632
|
-
function isProtected(path, policy, category) {
|
|
633
|
-
if (category) return matchesAny(path, policy.protected?.[category]);
|
|
634
|
-
return protectedCategory(path, policy) !== null;
|
|
635
|
-
}
|
|
636
|
-
function isPolicyFile(path) {
|
|
637
|
-
return path === POLICY_FILE || path.endsWith("/" + POLICY_FILE);
|
|
638
|
-
}
|
|
639
|
-
function isIgnored(path, policy) {
|
|
640
|
-
return matchesAny(path, policy.ignore);
|
|
641
|
-
}
|
|
642
|
-
function mergeProtected(base, user) {
|
|
643
|
-
const out2 = { ...base };
|
|
644
|
-
for (const [cat, globs4] of Object.entries(user ?? {})) {
|
|
645
|
-
const have = new Set(out2[cat] ?? []);
|
|
646
|
-
out2[cat] = [...out2[cat] ?? [], ...(globs4 ?? []).filter((g) => !have.has(g))];
|
|
647
|
-
}
|
|
648
|
-
return out2;
|
|
649
|
-
}
|
|
650
|
-
function defaultPolicy(version = 1) {
|
|
651
|
-
const p = {
|
|
652
|
-
version,
|
|
653
|
-
protected: {
|
|
654
|
-
// Cover every JS/TS test extension, not just .test.ts — the multi-repo FP study found
|
|
655
|
-
// .test.tsx/.spec.tsx (hono, zustand) slipped the glob, so legit test-file casts blocked
|
|
656
|
-
// instead of warning. Kept ADDITIVE (originals + new extensions) so the policy-diff sees a
|
|
657
|
-
// strengthening, not a narrowing. picomatch expands the brace.
|
|
658
|
-
// test-skip, test-deletion and test-content-removal all gate on this
|
|
659
|
-
// class, so a repo it does not match gets rules that can NEVER fire —
|
|
660
|
-
// protection that is silently absent rather than visibly off. The JS/TS
|
|
661
|
-
// list shipped alone; these are the conventional layouts of the other
|
|
662
|
-
// ecosystems `init` is run in. (P2-13, external review.)
|
|
663
|
-
tests: [
|
|
664
|
-
// JavaScript / TypeScript
|
|
665
|
-
"**/*.test.ts",
|
|
666
|
-
"**/*.spec.ts",
|
|
667
|
-
"**/*.{test,spec}.{tsx,cts,mts,js,jsx,cjs,mjs}",
|
|
668
|
-
"**/__tests__/**",
|
|
669
|
-
// Python (pytest / unittest layouts)
|
|
670
|
-
"**/test_*.py",
|
|
671
|
-
"**/*_test.py",
|
|
672
|
-
"**/conftest.py",
|
|
673
|
-
// Go
|
|
674
|
-
"**/*_test.go",
|
|
675
|
-
// Rust (integration tests live in tests/, unit tests inline)
|
|
676
|
-
"**/tests/**/*.rs",
|
|
677
|
-
// Ruby (RSpec / minitest)
|
|
678
|
-
"**/*_spec.rb",
|
|
679
|
-
"**/*_test.rb",
|
|
680
|
-
// Java / Kotlin / Scala (Maven & Gradle convention)
|
|
681
|
-
"**/src/test/**",
|
|
682
|
-
// PHP (PHPUnit)
|
|
683
|
-
"**/*Test.php",
|
|
684
|
-
// C# (.NET)
|
|
685
|
-
"**/*Tests.cs",
|
|
686
|
-
"**/*Test.cs"
|
|
687
|
-
],
|
|
688
|
-
config: [
|
|
689
|
-
"**/jest.config.*",
|
|
690
|
-
"**/vitest.config.*",
|
|
691
|
-
// A `vitest.workspace.*` file REPLACES the root config's selection with the
|
|
692
|
-
// union of its projects, and a `vite.config.*` carrying a `test:` key is the
|
|
693
|
-
// vitest config when no vitest.config exists — both are where a spec can be
|
|
694
|
-
// dropped from the suite without a line of it changing. ADDITIVE, like every
|
|
695
|
-
// baseline widening (the policy-diff reads it as a strengthening).
|
|
696
|
-
"**/vitest.workspace.*",
|
|
697
|
-
"**/vite.config.*",
|
|
698
|
-
"**/tsconfig*.json",
|
|
699
|
-
"**/.eslintrc*",
|
|
700
|
-
"**/eslint.config.*",
|
|
701
|
-
"**/package.json",
|
|
702
|
-
// Coverage gates outside the Jest/Vitest shapes. coverage-lowering could only
|
|
703
|
-
// see a threshold it was shown: a Python `fail_under`, an nyc `lines`, a
|
|
704
|
-
// codecov `target` lived in files no glob protected, so lowering them fired
|
|
705
|
-
// nothing. ADDITIVE, like every baseline widening (the policy-diff reads it
|
|
706
|
-
// as a strengthening). pyproject.toml carries [tool.coverage.report].
|
|
707
|
-
"**/.coveragerc",
|
|
708
|
-
"**/pyproject.toml",
|
|
709
|
-
"**/.nycrc",
|
|
710
|
-
"**/.nycrc.*",
|
|
711
|
-
"**/nyc.config.*",
|
|
712
|
-
"**/codecov.yml",
|
|
713
|
-
"**/.codecov.yml"
|
|
714
|
-
],
|
|
715
|
-
ci: [".github/workflows/**"],
|
|
716
|
-
// Recorded expected outputs. An assertion stored as data is still an assertion;
|
|
717
|
-
// rewriting it from current output is the snapshot-update move the affordance
|
|
718
|
-
// experiment measured at a 70% attempt / 100% through rate (snapshot-rewrite).
|
|
719
|
-
// tap-snapshots/** added from Phase-3 evidence: node-tar's recorded expectations
|
|
720
|
-
// live there as .cjs and were rewritten unseen (PHASE3.md §4).
|
|
721
|
-
snapshots: ["**/*.snap", "**/__snapshots__/**", "**/tap-snapshots/**", "**/golden/**", "**/*.golden.*"],
|
|
722
|
-
// The wiring IS the enforcement: settings.json carries the PreToolUse and
|
|
723
|
-
// Stop hooks, and a pre-commit config carries the commit-time gate. These
|
|
724
|
-
// were installed by `init` and protected by nothing, so editing them out
|
|
725
|
-
// fired no rule at all. (.git/hooks/** is outside every git view and so
|
|
726
|
-
// cannot be covered here — noted in SECURITY-ENVELOPE.md.) (P1-4, review.)
|
|
727
|
-
hooks: [
|
|
728
|
-
".husky/**",
|
|
729
|
-
"**/lefthook.*",
|
|
730
|
-
// lefthook merges `lefthook-local.yml` OVER `lefthook.yml`: a `skip: true`
|
|
731
|
-
// there switches the gate off without touching the file init pointed at.
|
|
732
|
-
"**/lefthook-local.*",
|
|
733
|
-
".tamperward.yml",
|
|
734
|
-
"**/.tamperward.yml",
|
|
735
|
-
".claude/settings.json",
|
|
736
|
-
".claude/settings.local.json",
|
|
737
|
-
"**/.pre-commit-config.yaml",
|
|
738
|
-
// CODEOWNERS is enforcement wiring, not documentation: it is the only
|
|
739
|
-
// thing that can require a human on a change to the workflow that
|
|
740
|
-
// decides whether the gate runs at all. GitHub reads it from any of
|
|
741
|
-
// these three locations. (H2.)
|
|
742
|
-
".github/CODEOWNERS",
|
|
743
|
-
"CODEOWNERS",
|
|
744
|
-
"docs/CODEOWNERS"
|
|
745
|
-
]
|
|
746
|
-
},
|
|
747
|
-
rules: {
|
|
748
|
-
"test-deletion": { severity: "block" },
|
|
749
|
-
// content gutted from a surviving spec — the class Phase 3 measured walking
|
|
750
|
-
// through the block-count rule (rows, assertion arguments, setup). Severity
|
|
751
|
-
// backed by the real-commit corpus measurement recorded in the rule's SPEC row.
|
|
752
|
-
"test-content-removal": { severity: "block" },
|
|
753
|
-
"test-skip": { severity: "block" },
|
|
754
|
-
"ts-any-cast": { severity: "block" },
|
|
755
|
-
// the unambiguous explicit casts + ts-suppression directives
|
|
756
|
-
// broad any in annotation/generic position is common in legit code (measured ~84-100% FP as a
|
|
757
|
-
// block rule on real TS), so it WARNs until a semantic (error-silencing-aware) signal earns block.
|
|
758
|
-
"ts-any-launder": { severity: "warn" },
|
|
759
|
-
"lint-suppression": { severity: "block" },
|
|
760
|
-
"coverage-lowering": { severity: "block" },
|
|
761
|
-
"ci-tampering": { severity: "block" },
|
|
762
|
-
"hook-tampering": { severity: "block" },
|
|
763
|
-
"no-verify": { severity: "block" },
|
|
764
|
-
// mechanical but intent-ambiguous: updating a snapshot is the legitimate workflow
|
|
765
|
-
// when intended output changes. WARN until the §7 evidence path earns block.
|
|
766
|
-
"snapshot-rewrite": { severity: "warn" },
|
|
767
|
-
// the watcher's transient class (modified-and-restored within a turn). WARN by
|
|
768
|
-
// default: a Stop block already forces restores, and a compliant restore reads
|
|
769
|
-
// as churn — block-by-default would deadlock the honest path. Harnesses that own
|
|
770
|
-
// that semantics raise it via TAMPERWARD_TRANSIENT=block.
|
|
771
|
-
"transient-protected-mutation": { severity: "warn" },
|
|
772
|
-
// the narrow graduation candidate: snapshot moved with NO accompanying change.
|
|
773
|
-
// ~0.06% FP on 1,652 audited mainline commits, 7/7 TP on observed tampers
|
|
774
|
-
// (harness/fp-study/). warn until a version: gate graduates it (BLOCK_SINCE).
|
|
775
|
-
"snapshot-only-rewrite": { severity: "warn" },
|
|
776
|
-
// heuristic — warn until precision is measured (SPEC §7)
|
|
777
|
-
"assertion-weakening": { severity: "warn" },
|
|
778
|
-
"guard-removal": { severity: "warn" }
|
|
779
|
-
},
|
|
780
|
-
ignore: [],
|
|
781
|
-
signoff: { requiredFor: ["block"], ledger: ".tamperward/ledger.jsonl" }
|
|
782
|
-
};
|
|
783
|
-
p.rules = applyVersionGates(p.rules, version);
|
|
784
|
-
return p;
|
|
785
|
-
}
|
|
786
|
-
|
|
787
957
|
// src/detectors/files.ts
|
|
788
958
|
var CODE = /\.(?:ts|tsx|mts|cts|js|jsx|mjs|cjs)$/;
|
|
789
959
|
function isCodeFile(path) {
|
|
@@ -1164,12 +1334,12 @@ import ts2 from "typescript";
|
|
|
1164
1334
|
|
|
1165
1335
|
// src/detectors/repo.ts
|
|
1166
1336
|
import { execFileSync as execFileSync2 } from "node:child_process";
|
|
1167
|
-
var
|
|
1337
|
+
var cache = /* @__PURE__ */ new Map();
|
|
1168
1338
|
function trackedFiles(ctx) {
|
|
1169
1339
|
if (!ctx) return null;
|
|
1170
1340
|
if (ctx.trackedFiles) return ctx.trackedFiles;
|
|
1171
1341
|
if (!ctx.cwd) return null;
|
|
1172
|
-
const hit =
|
|
1342
|
+
const hit = cache.get(ctx.cwd);
|
|
1173
1343
|
if (hit !== void 0) return hit;
|
|
1174
1344
|
let files = null;
|
|
1175
1345
|
try {
|
|
@@ -1182,7 +1352,7 @@ function trackedFiles(ctx) {
|
|
|
1182
1352
|
} catch {
|
|
1183
1353
|
files = null;
|
|
1184
1354
|
}
|
|
1185
|
-
|
|
1355
|
+
cache.set(ctx.cwd, files);
|
|
1186
1356
|
return files;
|
|
1187
1357
|
}
|
|
1188
1358
|
var branchCache = /* @__PURE__ */ new Map();
|
|
@@ -2490,11 +2660,11 @@ var ciTampering = {
|
|
|
2490
2660
|
};
|
|
2491
2661
|
|
|
2492
2662
|
// src/detectors/hook-tampering.ts
|
|
2493
|
-
import { existsSync, readFileSync as
|
|
2663
|
+
import { existsSync, readFileSync as readFileSync2 } from "node:fs";
|
|
2494
2664
|
import { isAbsolute, join as join3, relative } from "node:path";
|
|
2495
2665
|
|
|
2496
2666
|
// src/wiring.ts
|
|
2497
|
-
import { readFileSync
|
|
2667
|
+
import { readFileSync } from "node:fs";
|
|
2498
2668
|
import { dirname, join as join2 } from "node:path";
|
|
2499
2669
|
import { fileURLToPath } from "node:url";
|
|
2500
2670
|
function shippedVersion() {
|
|
@@ -2502,7 +2672,7 @@ function shippedVersion() {
|
|
|
2502
2672
|
const here = dirname(fileURLToPath(import.meta.url));
|
|
2503
2673
|
for (const rel of ["../package.json", "../../package.json", "../../../package.json"]) {
|
|
2504
2674
|
try {
|
|
2505
|
-
const pkg = JSON.parse(
|
|
2675
|
+
const pkg = JSON.parse(readFileSync(join2(here, rel), "utf8"));
|
|
2506
2676
|
if (pkg.name === "tamperward" && pkg.version) return pkg.version;
|
|
2507
2677
|
} catch {
|
|
2508
2678
|
}
|
|
@@ -4151,7 +4321,7 @@ function lefthookBase(c, changes, ctx) {
|
|
|
4151
4321
|
for (const n of names) {
|
|
4152
4322
|
try {
|
|
4153
4323
|
const p = join3(ctx.cwd, n);
|
|
4154
|
-
if (existsSync(p)) return parseDoc(
|
|
4324
|
+
if (existsSync(p)) return parseDoc(readFileSync2(p, "utf8"));
|
|
4155
4325
|
} catch {
|
|
4156
4326
|
}
|
|
4157
4327
|
}
|
|
@@ -5386,7 +5556,7 @@ function hasBlocking(findings) {
|
|
|
5386
5556
|
}
|
|
5387
5557
|
|
|
5388
5558
|
// src/policy-load.ts
|
|
5389
|
-
import { readFileSync as
|
|
5559
|
+
import { readFileSync as readFileSync3, existsSync as existsSync2 } from "node:fs";
|
|
5390
5560
|
import { isAbsolute as isAbsolute2, join as join4, posix } from "node:path";
|
|
5391
5561
|
import { parse as parse2 } from "yaml";
|
|
5392
5562
|
var PolicyError = class extends Error {
|
|
@@ -5513,7 +5683,7 @@ function parseOrThrow(src, where) {
|
|
|
5513
5683
|
function loadPolicy(cwd = process.cwd()) {
|
|
5514
5684
|
const path = join4(cwd, POLICY_FILE);
|
|
5515
5685
|
if (!existsSync2(path)) return defaultPolicy();
|
|
5516
|
-
return parseOrThrow(
|
|
5686
|
+
return parseOrThrow(readFileSync3(path, "utf8"), POLICY_FILE);
|
|
5517
5687
|
}
|
|
5518
5688
|
function loadPolicyAt(rev, cwd) {
|
|
5519
5689
|
const src = fileAt(rev, POLICY_FILE, { cwd });
|
|
@@ -5523,7 +5693,7 @@ function loadPolicyAt(rev, cwd) {
|
|
|
5523
5693
|
|
|
5524
5694
|
// src/signoff.ts
|
|
5525
5695
|
import { createHash } from "node:crypto";
|
|
5526
|
-
import { appendFileSync, mkdirSync, readFileSync as
|
|
5696
|
+
import { appendFileSync, mkdirSync, readFileSync as readFileSync4, existsSync as existsSync3 } from "node:fs";
|
|
5527
5697
|
import { dirname as dirname2, join as join5 } from "node:path";
|
|
5528
5698
|
var DEFAULT_TTL_MS = 30 * 24 * 60 * 60 * 1e3;
|
|
5529
5699
|
function fingerprint(rule, file, evidence) {
|
|
@@ -5541,7 +5711,7 @@ function readLedger(cwd, policy) {
|
|
|
5541
5711
|
const p = ledgerPath(cwd, policy);
|
|
5542
5712
|
if (!existsSync3(p)) return [];
|
|
5543
5713
|
const out2 = [];
|
|
5544
|
-
for (const line of
|
|
5714
|
+
for (const line of readFileSync4(p, "utf8").split("\n")) {
|
|
5545
5715
|
if (!line.trim()) continue;
|
|
5546
5716
|
try {
|
|
5547
5717
|
const e = JSON.parse(line);
|
|
@@ -5694,7 +5864,7 @@ function renderText(input, opts) {
|
|
|
5694
5864
|
for (const f of findings) {
|
|
5695
5865
|
const isBlock = f.severity === "block";
|
|
5696
5866
|
const mark = paint(isBlock ? "BLOCK" : "warn ", isBlock ? BOLD + RED : YELLOW, c);
|
|
5697
|
-
const loc = f.file ? `${stripControl(f.file)}${f.line ? `:${f.line}` : ""}` : "(command)";
|
|
5867
|
+
const loc = f.file ? `${stripControl(escapeControl(f.file))}${f.line ? `:${f.line}` : ""}` : "(command)";
|
|
5698
5868
|
out2.push(` ${mark} ${paint(stripControl(f.rule), BOLD, c)} ${paint(loc, DIM, c)}`);
|
|
5699
5869
|
for (const l of wrap(f.message, opts.width - 4)) out2.push(` ${l}`);
|
|
5700
5870
|
const row = (key2, lines) => {
|
|
@@ -5886,6 +6056,7 @@ function check(opts) {
|
|
|
5886
6056
|
}
|
|
5887
6057
|
const ignoredFiles = changes.filter((c) => isSuppressed(c, policy)).length;
|
|
5888
6058
|
let findings = evaluate(changes, policy, void 0, view, { cwd });
|
|
6059
|
+
if (view === "worktree") findings = findings.concat(unjudgeableProtected(cwd, policy, changes));
|
|
5889
6060
|
const { findings: remaining, cleared } = layer === "local" ? applyLocalSignoffs(findings, cwd, policy) : applyOobSignoffs(findings, oobFromEnv(), oobHeadFromEnv());
|
|
5890
6061
|
findings = remaining;
|
|
5891
6062
|
if (cleared.length) {
|
|
@@ -5910,24 +6081,19 @@ function runCheck(opts) {
|
|
|
5910
6081
|
}
|
|
5911
6082
|
|
|
5912
6083
|
// src/cli/hook.ts
|
|
5913
|
-
import { readFileSync as
|
|
6084
|
+
import { readFileSync as readFileSync9, appendFileSync as appendFileSync4, writeFileSync as writeFileSync4, existsSync as existsSync6 } from "node:fs";
|
|
5914
6085
|
import { createHash as createHash4 } from "node:crypto";
|
|
5915
|
-
import { join as join10 } from "node:path";
|
|
5916
6086
|
|
|
5917
6087
|
// src/adapters/claude/changes.ts
|
|
5918
6088
|
import { execFileSync as execFileSync3 } from "node:child_process";
|
|
5919
|
-
import {
|
|
6089
|
+
import { writeFileSync, mkdtempSync, rmSync } from "node:fs";
|
|
5920
6090
|
import { tmpdir } from "node:os";
|
|
5921
6091
|
import { isAbsolute as isAbsolute3, join as join6, relative as relative2, resolve } from "node:path";
|
|
5922
6092
|
function asStr(v) {
|
|
5923
6093
|
return typeof v === "string" ? v : "";
|
|
5924
6094
|
}
|
|
5925
6095
|
function readDisk(path) {
|
|
5926
|
-
|
|
5927
|
-
return existsSync4(path) ? readFileSync6(path, "utf8") : null;
|
|
5928
|
-
} catch {
|
|
5929
|
-
return null;
|
|
5930
|
-
}
|
|
6096
|
+
return textOf(inspectResolved(path));
|
|
5931
6097
|
}
|
|
5932
6098
|
function abs(path, cwd) {
|
|
5933
6099
|
return resolve(cwd, path);
|
|
@@ -6021,7 +6187,7 @@ function formatDenial(blocks) {
|
|
|
6021
6187
|
const lead = blocks[0];
|
|
6022
6188
|
const lines = ["Tamperward blocked this change \u2014 it weakens a protected safety net to pass checks.", ""];
|
|
6023
6189
|
for (const f of blocks) {
|
|
6024
|
-
const loc = f.file ? ` (${f.file}${f.line ? `:${f.line}` : ""})` : "";
|
|
6190
|
+
const loc = f.file ? ` (${escapeControl(f.file)}${f.line ? `:${f.line}` : ""})` : "";
|
|
6025
6191
|
lines.push(` \u2022 ${f.rule}${loc}: ${f.message}`);
|
|
6026
6192
|
}
|
|
6027
6193
|
lines.push("");
|
|
@@ -6036,7 +6202,7 @@ function formatDenial(blocks) {
|
|
|
6036
6202
|
}
|
|
6037
6203
|
|
|
6038
6204
|
// src/session.ts
|
|
6039
|
-
import { existsSync as
|
|
6205
|
+
import { existsSync as existsSync4, mkdirSync as mkdirSync2, readFileSync as readFileSync5, writeFileSync as writeFileSync2 } from "node:fs";
|
|
6040
6206
|
import { dirname as dirname3, join as join7 } from "node:path";
|
|
6041
6207
|
var UNSAFE = /[^A-Za-z0-9_-]/g;
|
|
6042
6208
|
var SHA = /^[0-9a-f]{7,40}$/;
|
|
@@ -6050,8 +6216,8 @@ function turnBaseline(cwd, sessionId) {
|
|
|
6050
6216
|
try {
|
|
6051
6217
|
const p = baselinePath(cwd, sessionId);
|
|
6052
6218
|
if (!p) return null;
|
|
6053
|
-
if (
|
|
6054
|
-
const v =
|
|
6219
|
+
if (existsSync4(p)) {
|
|
6220
|
+
const v = readFileSync5(p, "utf8").trim();
|
|
6055
6221
|
if (SHA.test(v)) return v;
|
|
6056
6222
|
}
|
|
6057
6223
|
const head = headSha(cwd);
|
|
@@ -6077,7 +6243,7 @@ function advanceTurnBaseline(cwd, sessionId) {
|
|
|
6077
6243
|
|
|
6078
6244
|
// src/effect.ts
|
|
6079
6245
|
import { createHash as createHash2 } from "node:crypto";
|
|
6080
|
-
import { existsSync as
|
|
6246
|
+
import { existsSync as existsSync5, lstatSync as lstatSync2, mkdirSync as mkdirSync3, readdirSync, readFileSync as readFileSync6, renameSync, rmSync as rmSync2, writeFileSync as writeFileSync3 } from "node:fs";
|
|
6081
6247
|
import { isAbsolute as isAbsolute4, join as join8 } from "node:path";
|
|
6082
6248
|
import { execFileSync as execFileSync4 } from "node:child_process";
|
|
6083
6249
|
var UNSAFE2 = /[^A-Za-z0-9._-]/g;
|
|
@@ -6112,8 +6278,22 @@ var sha = (buf) => createHash2("sha256").update(buf).digest("hex").slice(0, 16);
|
|
|
6112
6278
|
function contentHash(content) {
|
|
6113
6279
|
return sha(Buffer.isBuffer(content) ? content : Buffer.from(content, "utf8"));
|
|
6114
6280
|
}
|
|
6281
|
+
function entryOf(e) {
|
|
6282
|
+
const hash = e.content != null ? sha(e.content) : e.kind === "unreadable" ? "unreadable" : `${e.kind}:${e.detail}`;
|
|
6283
|
+
return { hash, mode: e.mode, size: e.size, mtimeMs: e.mtimeMs };
|
|
6284
|
+
}
|
|
6115
6285
|
function snapshotProtected(cwd, policy, prev) {
|
|
6286
|
+
void prev;
|
|
6116
6287
|
const out2 = {};
|
|
6288
|
+
const keep = (rel) => isProtected(rel, policy);
|
|
6289
|
+
let pruned = /* @__PURE__ */ new Set();
|
|
6290
|
+
let listed = [];
|
|
6291
|
+
try {
|
|
6292
|
+
const t = ignoredTree({ cwd }, keep);
|
|
6293
|
+
pruned = new Set(t.dirs.map((d) => d.replace(/\/$/, "")));
|
|
6294
|
+
listed = t.files;
|
|
6295
|
+
} catch {
|
|
6296
|
+
}
|
|
6117
6297
|
const walk2 = (rel) => {
|
|
6118
6298
|
const abs2 = rel ? join8(cwd, rel) : cwd;
|
|
6119
6299
|
let names;
|
|
@@ -6125,25 +6305,19 @@ function snapshotProtected(cwd, policy, prev) {
|
|
|
6125
6305
|
for (const name of names) {
|
|
6126
6306
|
if (SKIP_DIRS2.has(name)) continue;
|
|
6127
6307
|
const r = rel ? `${rel}/${name}` : name;
|
|
6308
|
+
if (pruned.has(r)) continue;
|
|
6128
6309
|
let st;
|
|
6129
6310
|
try {
|
|
6130
|
-
st =
|
|
6311
|
+
st = lstatSync2(join8(cwd, r));
|
|
6131
6312
|
} catch {
|
|
6132
6313
|
continue;
|
|
6133
6314
|
}
|
|
6134
6315
|
if (st.isDirectory()) walk2(r);
|
|
6135
|
-
else if (
|
|
6136
|
-
let hash = "";
|
|
6137
|
-
try {
|
|
6138
|
-
hash = sha(readFileSync8(join8(cwd, r)));
|
|
6139
|
-
} catch {
|
|
6140
|
-
hash = "unreadable";
|
|
6141
|
-
}
|
|
6142
|
-
out2[r] = { hash, mode: st.mode, size: st.size, mtimeMs: st.mtimeMs };
|
|
6143
|
-
}
|
|
6316
|
+
else if (keep(r)) out2[r] = entryOf(inspectRel(cwd, r));
|
|
6144
6317
|
}
|
|
6145
6318
|
};
|
|
6146
6319
|
walk2("");
|
|
6320
|
+
for (const rel of listed) if (out2[rel] === void 0) out2[rel] = entryOf(inspectRel(cwd, rel));
|
|
6147
6321
|
return out2;
|
|
6148
6322
|
}
|
|
6149
6323
|
function driftBetween(expected, current) {
|
|
@@ -6161,9 +6335,9 @@ function driftBetween(expected, current) {
|
|
|
6161
6335
|
return { changed, deleted, added };
|
|
6162
6336
|
}
|
|
6163
6337
|
function loadTree(p) {
|
|
6164
|
-
if (!p || !
|
|
6338
|
+
if (!p || !existsSync5(p)) return null;
|
|
6165
6339
|
try {
|
|
6166
|
-
return JSON.parse(
|
|
6340
|
+
return JSON.parse(readFileSync6(p, "utf8"));
|
|
6167
6341
|
} catch {
|
|
6168
6342
|
return null;
|
|
6169
6343
|
}
|
|
@@ -6202,19 +6376,16 @@ function dropTurnTree(cwd, sessionId) {
|
|
|
6202
6376
|
}
|
|
6203
6377
|
|
|
6204
6378
|
// src/cli/watch.ts
|
|
6205
|
-
import { appendFileSync as appendFileSync3, mkdirSync as mkdirSync4, readdirSync as readdirSync2,
|
|
6379
|
+
import { appendFileSync as appendFileSync3, mkdirSync as mkdirSync4, readdirSync as readdirSync2, statSync, watch } from "node:fs";
|
|
6206
6380
|
import { join as join9 } from "node:path";
|
|
6207
6381
|
import { createHash as createHash3 } from "node:crypto";
|
|
6208
6382
|
var SKIP2 = /(^|\/)(\.git|node_modules)(\/|$)/;
|
|
6209
6383
|
function snap(abs2) {
|
|
6210
|
-
|
|
6211
|
-
|
|
6212
|
-
|
|
6213
|
-
|
|
6214
|
-
|
|
6215
|
-
} catch {
|
|
6216
|
-
return { mode: null, size: null, hash: null };
|
|
6217
|
-
}
|
|
6384
|
+
const e = inspectPath(abs2);
|
|
6385
|
+
if (e.kind === "absent" || e.kind === "unreadable") return { mode: null, size: null, hash: null };
|
|
6386
|
+
if (e.kind !== "file" || e.content == null) return { mode: e.mode, size: null, hash: null };
|
|
6387
|
+
const hash = createHash3("sha256").update(e.content).digest("hex").slice(0, 16);
|
|
6388
|
+
return { mode: e.mode, size: e.size, hash };
|
|
6218
6389
|
}
|
|
6219
6390
|
function defaultEventLog(cwd) {
|
|
6220
6391
|
if (process.env.TAMPERWARD_FSEVENTS) return process.env.TAMPERWARD_FSEVENTS;
|
|
@@ -6240,7 +6411,7 @@ function watchTree(dir, cb) {
|
|
|
6240
6411
|
const child = rel ? `${rel}/${String(fname)}` : String(fname);
|
|
6241
6412
|
cb(kind, child);
|
|
6242
6413
|
try {
|
|
6243
|
-
if (
|
|
6414
|
+
if (statSync(join9(dir, child)).isDirectory()) addDir(child);
|
|
6244
6415
|
} catch {
|
|
6245
6416
|
}
|
|
6246
6417
|
});
|
|
@@ -6261,7 +6432,7 @@ function watchTree(dir, cb) {
|
|
|
6261
6432
|
const child = rel ? `${rel}/${name}` : name;
|
|
6262
6433
|
if (SKIP2.test(child + "/")) continue;
|
|
6263
6434
|
try {
|
|
6264
|
-
if (
|
|
6435
|
+
if (statSync(join9(dir, child)).isDirectory()) walk2(child);
|
|
6265
6436
|
} catch {
|
|
6266
6437
|
}
|
|
6267
6438
|
}
|
|
@@ -6306,17 +6477,17 @@ function runWatch(args) {
|
|
|
6306
6477
|
}
|
|
6307
6478
|
|
|
6308
6479
|
// src/detectors/fs-events.ts
|
|
6309
|
-
import { readFileSync as
|
|
6480
|
+
import { readFileSync as readFileSync8, statSync as statSync2 } from "node:fs";
|
|
6310
6481
|
var RULE11 = "transient-protected-mutation";
|
|
6311
6482
|
function readEvents(log, offset) {
|
|
6312
6483
|
let size = 0;
|
|
6313
6484
|
try {
|
|
6314
|
-
size =
|
|
6485
|
+
size = statSync2(log).size;
|
|
6315
6486
|
} catch {
|
|
6316
6487
|
return { events: [], newOffset: offset };
|
|
6317
6488
|
}
|
|
6318
6489
|
if (size <= offset) return { events: [], newOffset: offset >= 0 ? Math.min(offset, size) : 0 };
|
|
6319
|
-
const raw =
|
|
6490
|
+
const raw = readFileSync8(log, "utf8").slice(offset);
|
|
6320
6491
|
const events = [];
|
|
6321
6492
|
for (const line of raw.split("\n")) {
|
|
6322
6493
|
if (!line.trim()) continue;
|
|
@@ -6369,7 +6540,7 @@ var HookInputError = class extends Error {
|
|
|
6369
6540
|
};
|
|
6370
6541
|
function readStdin() {
|
|
6371
6542
|
try {
|
|
6372
|
-
return
|
|
6543
|
+
return readFileSync9(0, "utf8");
|
|
6373
6544
|
} catch (e) {
|
|
6374
6545
|
throw new HookInputError(`cannot read the hook payload on stdin: ${e instanceof Error ? e.message : String(e)}`);
|
|
6375
6546
|
}
|
|
@@ -6395,7 +6566,7 @@ function recordDenylog(blocks) {
|
|
|
6395
6566
|
} catch {
|
|
6396
6567
|
}
|
|
6397
6568
|
}
|
|
6398
|
-
function
|
|
6569
|
+
function errText2(e) {
|
|
6399
6570
|
const m = e instanceof Error ? e.message : String(e);
|
|
6400
6571
|
return m.length > 200 ? m.slice(0, 200) + "\u2026" : m;
|
|
6401
6572
|
}
|
|
@@ -6427,11 +6598,8 @@ function fileChanges(changes) {
|
|
|
6427
6598
|
return changes.filter((c) => c.kind === "file");
|
|
6428
6599
|
}
|
|
6429
6600
|
function hashOnDisk(cwd, rel) {
|
|
6430
|
-
|
|
6431
|
-
|
|
6432
|
-
} catch {
|
|
6433
|
-
return null;
|
|
6434
|
-
}
|
|
6601
|
+
const e = inspectRel(cwd, rel);
|
|
6602
|
+
return e.content == null ? null : contentHash(e.content);
|
|
6435
6603
|
}
|
|
6436
6604
|
function gitMode(mode) {
|
|
6437
6605
|
return (mode & 64) !== 0 ? "100755" : "100644";
|
|
@@ -6478,6 +6646,11 @@ function uncoveredDrift(cwd, policy, expected, current, covered, base, turnTree)
|
|
|
6478
6646
|
const candidates = base ? [base, "HEAD", ""] : ["HEAD", ""];
|
|
6479
6647
|
for (const rel of [...d.changed, ...d.deleted]) {
|
|
6480
6648
|
if (seen.has(rel)) continue;
|
|
6649
|
+
const unjudgeable = unjudgeableFinding(cwd, rel);
|
|
6650
|
+
if (unjudgeable) {
|
|
6651
|
+
blocks.push(unjudgeable);
|
|
6652
|
+
continue;
|
|
6653
|
+
}
|
|
6481
6654
|
const after = fileOnDisk(rel, { cwd });
|
|
6482
6655
|
const trustedBlob = fileAt(trusted, rel, { cwd });
|
|
6483
6656
|
const atGitState = after != null && trustedBlob != null && contentHash(after) === contentHash(trustedBlob);
|
|
@@ -6526,6 +6699,11 @@ function uncoveredDrift(cwd, policy, expected, current, covered, base, turnTree)
|
|
|
6526
6699
|
}
|
|
6527
6700
|
for (const rel of d.added) {
|
|
6528
6701
|
if (seen.has(rel)) continue;
|
|
6702
|
+
const unjudgeable = unjudgeableFinding(cwd, rel);
|
|
6703
|
+
if (unjudgeable) {
|
|
6704
|
+
blocks.push(unjudgeable);
|
|
6705
|
+
continue;
|
|
6706
|
+
}
|
|
6529
6707
|
const after = fileOnDisk(rel, { cwd });
|
|
6530
6708
|
if (after == null) {
|
|
6531
6709
|
blocks.push({
|
|
@@ -6565,7 +6743,8 @@ function effectDriftBlocks(cwd, sessionId, policy) {
|
|
|
6565
6743
|
(c) => c.kind === "file" && (drifted.has(c.path) || c.oldPath != null && drifted.has(c.oldPath))
|
|
6566
6744
|
);
|
|
6567
6745
|
const gap = uncoveredDrift(cwd, policy, expected, current, scoped, base, loadTurnTree(cwd, sessionId));
|
|
6568
|
-
const
|
|
6746
|
+
const judged = [...scoped, ...gap.changes];
|
|
6747
|
+
const blocks = evaluate(judged, policy, void 0, "turn", { cwd }).filter((f) => f.severity === "block").concat(gap.blocks, unjudgeableProtected(cwd, policy, judged));
|
|
6569
6748
|
if (blocks.length > 0) return blocks;
|
|
6570
6749
|
savePtree(cwd, sessionId, current);
|
|
6571
6750
|
return null;
|
|
@@ -6602,7 +6781,7 @@ function preToolUseVerdict(input) {
|
|
|
6602
6781
|
if (blocks.length === 0) sanctionPredictedWrites(cwd, input.session_id, policy, changes);
|
|
6603
6782
|
return verdict(blocks, "PreToolUse");
|
|
6604
6783
|
} catch (e) {
|
|
6605
|
-
return failClosed("PreToolUse",
|
|
6784
|
+
return failClosed("PreToolUse", errText2(e));
|
|
6606
6785
|
}
|
|
6607
6786
|
}
|
|
6608
6787
|
function cursorPath(cwd, sessionId) {
|
|
@@ -6614,10 +6793,10 @@ function turnTransientBlocks(cwd, sessionId, policy, changes) {
|
|
|
6614
6793
|
} };
|
|
6615
6794
|
const log = defaultEventLog(cwd);
|
|
6616
6795
|
const cp = cursorPath(cwd, sessionId);
|
|
6617
|
-
if (!
|
|
6796
|
+
if (!existsSync6(log)) return none;
|
|
6618
6797
|
let offset = 0;
|
|
6619
|
-
if (cp &&
|
|
6620
|
-
const n = Number(
|
|
6798
|
+
if (cp && existsSync6(cp)) {
|
|
6799
|
+
const n = Number(readFileSync9(cp, "utf8"));
|
|
6621
6800
|
offset = Number.isFinite(n) ? n : 0;
|
|
6622
6801
|
}
|
|
6623
6802
|
const { events, newOffset } = readEvents(log, offset);
|
|
@@ -6629,12 +6808,8 @@ function turnTransientBlocks(cwd, sessionId, policy, changes) {
|
|
|
6629
6808
|
} };
|
|
6630
6809
|
const persistent = new Set(changes.filter((c) => c.kind === "file").map((c) => c.path));
|
|
6631
6810
|
const finalHash = (path) => {
|
|
6632
|
-
|
|
6633
|
-
|
|
6634
|
-
return createHash4("sha256").update(readFileSync11(`${cwd}/${path}`)).digest("hex").slice(0, 16);
|
|
6635
|
-
} catch {
|
|
6636
|
-
return null;
|
|
6637
|
-
}
|
|
6811
|
+
const e = inspectRel(cwd, path);
|
|
6812
|
+
return e.kind === "file" && e.content != null ? contentHash(e.content) : null;
|
|
6638
6813
|
};
|
|
6639
6814
|
const findings = transientFindings(events, persistent, policy, finalHash);
|
|
6640
6815
|
recordWarns(findings.filter((f) => f.severity !== "block"));
|
|
@@ -6675,7 +6850,7 @@ function stopVerdict(input) {
|
|
|
6675
6850
|
changes = changes.concat(gap.changes);
|
|
6676
6851
|
hiddenBlocks = gap.blocks;
|
|
6677
6852
|
}
|
|
6678
|
-
blocks = evaluate(changes, policy, void 0, "turn", { cwd }).filter((f) => f.severity === "block").concat(hiddenBlocks);
|
|
6853
|
+
blocks = evaluate(changes, policy, void 0, "turn", { cwd }).filter((f) => f.severity === "block").concat(hiddenBlocks, unjudgeableProtected(cwd, policy, changes));
|
|
6679
6854
|
const transient = turnTransientBlocks(cwd, input.session_id, policy, changes);
|
|
6680
6855
|
blocks = blocks.concat(transient.blocks);
|
|
6681
6856
|
commitCursor = transient.commit;
|
|
@@ -6684,7 +6859,7 @@ function stopVerdict(input) {
|
|
|
6684
6859
|
saveTurnTree(cwd, input.session_id, current);
|
|
6685
6860
|
}
|
|
6686
6861
|
} catch (e) {
|
|
6687
|
-
return failClosed("Stop",
|
|
6862
|
+
return failClosed("Stop", errText2(e));
|
|
6688
6863
|
}
|
|
6689
6864
|
if (blocks.length === 0) {
|
|
6690
6865
|
advanceTurnBaseline(cwd, input.session_id);
|
|
@@ -6700,28 +6875,28 @@ function preToolUseFromRaw(raw) {
|
|
|
6700
6875
|
try {
|
|
6701
6876
|
return preToolUseVerdict(parseInput(raw));
|
|
6702
6877
|
} catch (e) {
|
|
6703
|
-
return failClosed("PreToolUse",
|
|
6878
|
+
return failClosed("PreToolUse", errText2(e));
|
|
6704
6879
|
}
|
|
6705
6880
|
}
|
|
6706
6881
|
function stopFromRaw(raw) {
|
|
6707
6882
|
try {
|
|
6708
6883
|
return stopVerdict(parseInput(raw));
|
|
6709
6884
|
} catch (e) {
|
|
6710
|
-
return failClosed("Stop",
|
|
6885
|
+
return failClosed("Stop", errText2(e));
|
|
6711
6886
|
}
|
|
6712
6887
|
}
|
|
6713
6888
|
function runHookClaude() {
|
|
6714
6889
|
try {
|
|
6715
6890
|
return emit(preToolUseFromRaw(readStdin()));
|
|
6716
6891
|
} catch (e) {
|
|
6717
|
-
return emit(failClosed("PreToolUse",
|
|
6892
|
+
return emit(failClosed("PreToolUse", errText2(e)));
|
|
6718
6893
|
}
|
|
6719
6894
|
}
|
|
6720
6895
|
function runSweepClaude() {
|
|
6721
6896
|
try {
|
|
6722
6897
|
return emit(stopFromRaw(readStdin()));
|
|
6723
6898
|
} catch (e) {
|
|
6724
|
-
return emit(failClosed("Stop",
|
|
6899
|
+
return emit(failClosed("Stop", errText2(e)));
|
|
6725
6900
|
}
|
|
6726
6901
|
}
|
|
6727
6902
|
|
|
@@ -6774,9 +6949,9 @@ Honored at LOCAL pre-commit only. The agent-layer hook ignores this file; CI req
|
|
|
6774
6949
|
// src/cli/init.ts
|
|
6775
6950
|
import { execFileSync as execFileSync5 } from "node:child_process";
|
|
6776
6951
|
import { createHash as createHash5 } from "node:crypto";
|
|
6777
|
-
import { existsSync as
|
|
6778
|
-
import { basename, dirname as dirname4, isAbsolute as isAbsolute5, join as
|
|
6779
|
-
var
|
|
6952
|
+
import { existsSync as existsSync7, mkdirSync as mkdirSync5, readFileSync as readFileSync10, writeFileSync as writeFileSync5, chmodSync, statSync as statSync3 } from "node:fs";
|
|
6953
|
+
import { basename, dirname as dirname4, isAbsolute as isAbsolute5, join as join10, relative as relative3, resolve as resolve2 } from "node:path";
|
|
6954
|
+
var errText3 = (e) => (e instanceof Error ? e.message : String(e)).replace(/\s+/g, " ").trim();
|
|
6780
6955
|
var isPlainObject = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
6781
6956
|
function gitConfig(cwd, key2) {
|
|
6782
6957
|
try {
|
|
@@ -6925,13 +7100,13 @@ function readWorkflowMark(src) {
|
|
|
6925
7100
|
if (!m) return null;
|
|
6926
7101
|
return { version: m[1], hash: m[2], body: src.replace(WORKFLOW_MARK_RE, "") };
|
|
6927
7102
|
}
|
|
6928
|
-
function toolSet(
|
|
6929
|
-
const m = String(
|
|
7103
|
+
function toolSet(matcher) {
|
|
7104
|
+
const m = String(matcher ?? "").trim();
|
|
6930
7105
|
if (m === "" || m === "*") return null;
|
|
6931
7106
|
return new Set(m.split("|").map((t) => t.trim()).filter(Boolean));
|
|
6932
7107
|
}
|
|
6933
|
-
function missingTools(
|
|
6934
|
-
const have = toolSet(
|
|
7108
|
+
function missingTools(matcher) {
|
|
7109
|
+
const have = toolSet(matcher);
|
|
6935
7110
|
if (have === null) return [];
|
|
6936
7111
|
return PRE_MATCHER.split("|").filter((t) => !have.has(t));
|
|
6937
7112
|
}
|
|
@@ -6951,12 +7126,12 @@ function hooksShapeError(hooks) {
|
|
|
6951
7126
|
return null;
|
|
6952
7127
|
}
|
|
6953
7128
|
function planPolicy(cwd) {
|
|
6954
|
-
const path =
|
|
6955
|
-
if (
|
|
7129
|
+
const path = join10(cwd, POLICY_FILE);
|
|
7130
|
+
if (existsSync7(path)) {
|
|
6956
7131
|
try {
|
|
6957
7132
|
loadPolicy(cwd);
|
|
6958
7133
|
} catch (e) {
|
|
6959
|
-
return { item: "policy", path: POLICY_FILE, status: "error", detail: `present but does not load \u2014 ${
|
|
7134
|
+
return { item: "policy", path: POLICY_FILE, status: "error", detail: `present but does not load \u2014 ${errText3(e)}; fix it, then re-run init (left untouched)` };
|
|
6960
7135
|
}
|
|
6961
7136
|
return { item: "policy", path: POLICY_FILE, status: "ok", detail: "already present and loads \u2014 left untouched" };
|
|
6962
7137
|
}
|
|
@@ -6970,12 +7145,12 @@ function planPolicy(cwd) {
|
|
|
6970
7145
|
}
|
|
6971
7146
|
function planClaudeHooks(cwd) {
|
|
6972
7147
|
const rel = ".claude/settings.json";
|
|
6973
|
-
const path =
|
|
7148
|
+
const path = join10(cwd, rel);
|
|
6974
7149
|
let settings = {};
|
|
6975
|
-
if (
|
|
7150
|
+
if (existsSync7(path)) {
|
|
6976
7151
|
let parsed;
|
|
6977
7152
|
try {
|
|
6978
|
-
parsed = JSON.parse(
|
|
7153
|
+
parsed = JSON.parse(readFileSync10(path, "utf8"));
|
|
6979
7154
|
} catch {
|
|
6980
7155
|
return {
|
|
6981
7156
|
item: "agent",
|
|
@@ -7009,7 +7184,7 @@ function planClaudeHooks(cwd) {
|
|
|
7009
7184
|
return {
|
|
7010
7185
|
item: "agent",
|
|
7011
7186
|
path: rel,
|
|
7012
|
-
status:
|
|
7187
|
+
status: existsSync7(path) ? "update" : "create",
|
|
7013
7188
|
detail: [
|
|
7014
7189
|
needPre && "wire PreToolUse deny",
|
|
7015
7190
|
needStop && "wire Stop sweep",
|
|
@@ -7068,7 +7243,7 @@ function hooksDir(cwd) {
|
|
|
7068
7243
|
if (out2) return resolve2(cwd, out2);
|
|
7069
7244
|
} catch {
|
|
7070
7245
|
}
|
|
7071
|
-
return
|
|
7246
|
+
return existsSync7(join10(cwd, ".git")) ? join10(cwd, ".git", "hooks") : null;
|
|
7072
7247
|
}
|
|
7073
7248
|
function display(cwd, path) {
|
|
7074
7249
|
const rel = relative3(cwd, path);
|
|
@@ -7095,30 +7270,30 @@ function planPreCommit(cwd) {
|
|
|
7095
7270
|
const skip = (detail) => ({ item: "pre-commit", path: "(none)", status: "skip", detail });
|
|
7096
7271
|
let path;
|
|
7097
7272
|
let note;
|
|
7098
|
-
if (
|
|
7099
|
-
path =
|
|
7273
|
+
if (existsSync7(join10(cwd, ".husky"))) {
|
|
7274
|
+
path = join10(cwd, ".husky", "pre-commit");
|
|
7100
7275
|
note = "husky";
|
|
7101
7276
|
} else {
|
|
7102
|
-
const managed = MANAGED_HOOKS.find((m) => m.configs.some((c) =>
|
|
7277
|
+
const managed = MANAGED_HOOKS.find((m) => m.configs.some((c) => existsSync7(join10(cwd, c))));
|
|
7103
7278
|
if (managed) {
|
|
7104
7279
|
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}`);
|
|
7105
7280
|
}
|
|
7106
7281
|
const dir = hooksDir(cwd);
|
|
7107
7282
|
if (!dir) return skip("not a git repo and no .husky/ \u2014 nothing to wire");
|
|
7108
7283
|
if (basename(dir) === "_" && basename(dirname4(dir)) === ".husky") {
|
|
7109
|
-
path =
|
|
7284
|
+
path = join10(dirname4(dir), "pre-commit");
|
|
7110
7285
|
note = "husky";
|
|
7111
7286
|
} else {
|
|
7112
|
-
path =
|
|
7287
|
+
path = join10(dir, "pre-commit");
|
|
7113
7288
|
const rel2 = display(cwd, dir);
|
|
7114
|
-
note = rel2 ===
|
|
7289
|
+
note = rel2 === join10(".git", "hooks") ? "plain git hook (local-only: .git/hooks is not committed \u2014 consider husky to share it)" : `git hooks directory ${rel2} (core.hooksPath)`;
|
|
7115
7290
|
}
|
|
7116
7291
|
}
|
|
7117
7292
|
const rel = display(cwd, path);
|
|
7118
|
-
if (
|
|
7293
|
+
if (existsSync7(path) && !statSync3(path).isFile()) {
|
|
7119
7294
|
return { item: "pre-commit", path: rel, status: "error", detail: "exists but is not a regular file \u2014 refusing to write" };
|
|
7120
7295
|
}
|
|
7121
|
-
const existing =
|
|
7296
|
+
const existing = existsSync7(path) ? readFileSync10(path, "utf8") : null;
|
|
7122
7297
|
if (existing === null) {
|
|
7123
7298
|
return {
|
|
7124
7299
|
item: "pre-commit",
|
|
@@ -7207,10 +7382,10 @@ ${block.join("\n")}
|
|
|
7207
7382
|
function planCodeowners(cwd) {
|
|
7208
7383
|
const rel = ".github/CODEOWNERS";
|
|
7209
7384
|
const existingRel = [".github/CODEOWNERS", "CODEOWNERS", "docs/CODEOWNERS"].find(
|
|
7210
|
-
(r) =>
|
|
7385
|
+
(r) => existsSync7(join10(cwd, r))
|
|
7211
7386
|
);
|
|
7212
|
-
const path =
|
|
7213
|
-
const existing = existingRel ?
|
|
7387
|
+
const path = join10(cwd, existingRel ?? rel);
|
|
7388
|
+
const existing = existingRel ? readFileSync10(path, "utf8") : null;
|
|
7214
7389
|
const missing = CODEOWNERS_PATHS.filter((p) => existing === null || !coveredBy(existing, p));
|
|
7215
7390
|
if (missing.length === 0) {
|
|
7216
7391
|
return { item: "codeowners", path: existingRel, status: "ok", detail: "gate paths already have code owners" };
|
|
@@ -7243,12 +7418,12 @@ ${CODEOWNERS_MARK}
|
|
|
7243
7418
|
}
|
|
7244
7419
|
function planWorkflow(cwd, force) {
|
|
7245
7420
|
const rel = ".github/workflows/tamperward.yml";
|
|
7246
|
-
const path =
|
|
7421
|
+
const path = join10(cwd, rel);
|
|
7247
7422
|
const write = () => {
|
|
7248
7423
|
mkdirSync5(dirname4(path), { recursive: true });
|
|
7249
7424
|
writeFileSync5(path, workflowFile(WORKFLOW_CONTENT));
|
|
7250
7425
|
};
|
|
7251
|
-
if (!
|
|
7426
|
+
if (!existsSync7(path)) {
|
|
7252
7427
|
return {
|
|
7253
7428
|
item: "ci",
|
|
7254
7429
|
path: rel,
|
|
@@ -7259,7 +7434,7 @@ function planWorkflow(cwd, force) {
|
|
|
7259
7434
|
}
|
|
7260
7435
|
let src;
|
|
7261
7436
|
try {
|
|
7262
|
-
src =
|
|
7437
|
+
src = readFileSync10(path, "utf8");
|
|
7263
7438
|
} catch {
|
|
7264
7439
|
return { item: "ci", path: rel, status: "error", detail: "exists but cannot be read \u2014 refusing to overwrite" };
|
|
7265
7440
|
}
|
|
@@ -7297,7 +7472,7 @@ function planned(item, path, plan) {
|
|
|
7297
7472
|
try {
|
|
7298
7473
|
return plan();
|
|
7299
7474
|
} catch (e) {
|
|
7300
|
-
return { item, path, status: "error", detail: `cannot plan this item \u2014 ${
|
|
7475
|
+
return { item, path, status: "error", detail: `cannot plan this item \u2014 ${errText3(e)}` };
|
|
7301
7476
|
}
|
|
7302
7477
|
}
|
|
7303
7478
|
function planInit(cwd, opts = {}) {
|
|
@@ -7321,7 +7496,7 @@ function runInit(opts) {
|
|
|
7321
7496
|
applied++;
|
|
7322
7497
|
} catch (e) {
|
|
7323
7498
|
a.status = "error";
|
|
7324
|
-
a.detail = `could not be written \u2014 ${
|
|
7499
|
+
a.detail = `could not be written \u2014 ${errText3(e)}`;
|
|
7325
7500
|
a.apply = void 0;
|
|
7326
7501
|
}
|
|
7327
7502
|
}
|
|
@@ -7358,15 +7533,15 @@ tamperward init: ${changed} change(s) applied. Commit them so the gate travels w
|
|
|
7358
7533
|
// src/cli/verify.ts
|
|
7359
7534
|
import { execFileSync as execFileSync8, spawnSync } from "node:child_process";
|
|
7360
7535
|
import { createHash as createHash8 } from "node:crypto";
|
|
7361
|
-
import { chmodSync as chmodSync2, cpSync, lstatSync, mkdirSync as mkdirSync6, mkdtempSync as mkdtempSync2, readFileSync as
|
|
7536
|
+
import { chmodSync as chmodSync2, cpSync, lstatSync as lstatSync3, mkdirSync as mkdirSync6, mkdtempSync as mkdtempSync2, readFileSync as readFileSync13, readlinkSync as readlinkSync3, rmSync as rmSync3, statSync as statSync5, symlinkSync, writeFileSync as writeFileSync6, existsSync as existsSync9 } from "node:fs";
|
|
7362
7537
|
import { tmpdir as tmpdir2 } from "node:os";
|
|
7363
|
-
import { dirname as dirname5, join as
|
|
7538
|
+
import { dirname as dirname5, join as join13, resolve as resolve3 } from "node:path";
|
|
7364
7539
|
|
|
7365
7540
|
// src/git/trusted.ts
|
|
7366
7541
|
import { execFileSync as execFileSync6 } from "node:child_process";
|
|
7367
7542
|
import { createHash as createHash6 } from "node:crypto";
|
|
7368
|
-
import { existsSync as
|
|
7369
|
-
import { isAbsolute as isAbsolute6, join as
|
|
7543
|
+
import { existsSync as existsSync8, readFileSync as readFileSync11 } from "node:fs";
|
|
7544
|
+
import { isAbsolute as isAbsolute6, join as join11 } from "node:path";
|
|
7370
7545
|
function trustedGitEnv() {
|
|
7371
7546
|
return { ...process.env, GIT_NO_REPLACE_OBJECTS: "1" };
|
|
7372
7547
|
}
|
|
@@ -7379,7 +7554,7 @@ function gitDirOf(cwd) {
|
|
|
7379
7554
|
env: trustedGitEnv()
|
|
7380
7555
|
}).trim();
|
|
7381
7556
|
if (!d) return null;
|
|
7382
|
-
return isAbsolute6(d) ? d :
|
|
7557
|
+
return isAbsolute6(d) ? d : join11(cwd, d);
|
|
7383
7558
|
} catch {
|
|
7384
7559
|
return null;
|
|
7385
7560
|
}
|
|
@@ -7404,9 +7579,9 @@ function objectRewriteState(cwd) {
|
|
|
7404
7579
|
parts.push(`${rel}:<no-git-dir>`);
|
|
7405
7580
|
continue;
|
|
7406
7581
|
}
|
|
7407
|
-
const p =
|
|
7582
|
+
const p = join11(dir, rel);
|
|
7408
7583
|
try {
|
|
7409
|
-
parts.push(`${rel}:` + (
|
|
7584
|
+
parts.push(`${rel}:` + (existsSync8(p) ? readFileSync11(p, "utf8").trim() : ""));
|
|
7410
7585
|
} catch {
|
|
7411
7586
|
parts.push(`${rel}:<unreadable>`);
|
|
7412
7587
|
}
|
|
@@ -7417,8 +7592,8 @@ function objectRewriteState(cwd) {
|
|
|
7417
7592
|
// src/fingerprint.ts
|
|
7418
7593
|
import { execFileSync as execFileSync7 } from "node:child_process";
|
|
7419
7594
|
import { createHash as createHash7 } from "node:crypto";
|
|
7420
|
-
import { readdirSync as readdirSync3, readFileSync as
|
|
7421
|
-
import { join as
|
|
7595
|
+
import { readdirSync as readdirSync3, readFileSync as readFileSync12, readlinkSync as readlinkSync2, statSync as statSync4 } from "node:fs";
|
|
7596
|
+
import { join as join12 } from "node:path";
|
|
7422
7597
|
function treeFingerprint(cwd) {
|
|
7423
7598
|
const files = execFileSync7("git", ["ls-files", "-z", "--cached", "--others", "--exclude-standard"], {
|
|
7424
7599
|
cwd,
|
|
@@ -7430,17 +7605,13 @@ function treeFingerprint(cwd) {
|
|
|
7430
7605
|
for (const rel of files) {
|
|
7431
7606
|
h.update(rel);
|
|
7432
7607
|
h.update("\0");
|
|
7433
|
-
|
|
7434
|
-
h.update(readFileSync14(join13(cwd, rel)));
|
|
7435
|
-
} catch {
|
|
7436
|
-
h.update("<unreadable>");
|
|
7437
|
-
}
|
|
7608
|
+
h.update(inspectRel(cwd, rel).content ?? "<unreadable>");
|
|
7438
7609
|
h.update("\0");
|
|
7439
7610
|
}
|
|
7440
7611
|
return h.digest("hex");
|
|
7441
7612
|
}
|
|
7442
7613
|
function depsFingerprint(cwd) {
|
|
7443
|
-
const root =
|
|
7614
|
+
const root = join12(cwd, "node_modules");
|
|
7444
7615
|
const h = createHash7("sha256");
|
|
7445
7616
|
const walk2 = (dir) => {
|
|
7446
7617
|
let entries;
|
|
@@ -7450,20 +7621,20 @@ function depsFingerprint(cwd) {
|
|
|
7450
7621
|
return;
|
|
7451
7622
|
}
|
|
7452
7623
|
for (const e of [...entries].sort((a, b) => a.name.localeCompare(b.name))) {
|
|
7453
|
-
const p =
|
|
7624
|
+
const p = join12(dir, e.name);
|
|
7454
7625
|
if (e.isDirectory()) walk2(p);
|
|
7455
7626
|
else if (e.isFile()) {
|
|
7456
7627
|
h.update(p);
|
|
7457
7628
|
try {
|
|
7458
|
-
h.update(
|
|
7459
|
-
h.update(String(
|
|
7629
|
+
h.update(readFileSync12(p));
|
|
7630
|
+
h.update(String(statSync4(p).mode));
|
|
7460
7631
|
} catch {
|
|
7461
7632
|
h.update("<unreadable>");
|
|
7462
7633
|
}
|
|
7463
7634
|
} else if (e.isSymbolicLink()) {
|
|
7464
7635
|
h.update(p);
|
|
7465
7636
|
try {
|
|
7466
|
-
h.update(
|
|
7637
|
+
h.update(readlinkSync2(p));
|
|
7467
7638
|
} catch {
|
|
7468
7639
|
h.update("<unreadable-link>");
|
|
7469
7640
|
}
|
|
@@ -7471,7 +7642,7 @@ function depsFingerprint(cwd) {
|
|
|
7471
7642
|
}
|
|
7472
7643
|
};
|
|
7473
7644
|
try {
|
|
7474
|
-
if (!
|
|
7645
|
+
if (!statSync4(root).isDirectory()) return "none";
|
|
7475
7646
|
} catch {
|
|
7476
7647
|
return "none";
|
|
7477
7648
|
}
|
|
@@ -7570,30 +7741,30 @@ function baseIsAncestorOfHead(base, cwd) {
|
|
|
7570
7741
|
function materialize(cwd, dest) {
|
|
7571
7742
|
const listed = git2(["ls-files", "-z", "--cached", "--others", "--exclude-standard"], cwd).split("\0").filter(Boolean);
|
|
7572
7743
|
for (const rel of listed) {
|
|
7573
|
-
const src =
|
|
7744
|
+
const src = join13(cwd, rel);
|
|
7574
7745
|
let st;
|
|
7575
7746
|
try {
|
|
7576
|
-
st =
|
|
7747
|
+
st = statSync5(src);
|
|
7577
7748
|
} catch {
|
|
7578
7749
|
continue;
|
|
7579
7750
|
}
|
|
7580
7751
|
if (!st.isFile()) continue;
|
|
7581
|
-
const out2 =
|
|
7752
|
+
const out2 = join13(dest, rel);
|
|
7582
7753
|
mkdirSync6(dirname5(out2), { recursive: true });
|
|
7583
7754
|
dropSymlink(out2);
|
|
7584
|
-
if (
|
|
7585
|
-
writeFileSync6(out2,
|
|
7755
|
+
if (lstatSync3(src).isSymbolicLink()) {
|
|
7756
|
+
writeFileSync6(out2, readFileSync13(src));
|
|
7586
7757
|
} else {
|
|
7587
7758
|
cpSync(src, out2, { dereference: true });
|
|
7588
7759
|
}
|
|
7589
7760
|
chmodSync2(out2, st.mode);
|
|
7590
7761
|
}
|
|
7591
7762
|
const nm = resolve3(cwd, "node_modules");
|
|
7592
|
-
if (
|
|
7763
|
+
if (existsSync9(nm)) symlinkSync(nm, join13(dest, "node_modules"), "dir");
|
|
7593
7764
|
}
|
|
7594
7765
|
function dropSymlink(p) {
|
|
7595
7766
|
try {
|
|
7596
|
-
if (
|
|
7767
|
+
if (lstatSync3(p).isSymbolicLink()) rmSync3(p, { force: true });
|
|
7597
7768
|
} catch {
|
|
7598
7769
|
}
|
|
7599
7770
|
}
|
|
@@ -7622,11 +7793,11 @@ function overlayDigest(dest, paths) {
|
|
|
7622
7793
|
for (const rel of [...paths].sort()) {
|
|
7623
7794
|
h.update(rel);
|
|
7624
7795
|
h.update("\0");
|
|
7625
|
-
const p =
|
|
7796
|
+
const p = join13(dest, rel);
|
|
7626
7797
|
try {
|
|
7627
|
-
const st =
|
|
7798
|
+
const st = lstatSync3(p);
|
|
7628
7799
|
h.update(String(st.mode));
|
|
7629
|
-
h.update(st.isSymbolicLink() ?
|
|
7800
|
+
h.update(st.isSymbolicLink() ? readlinkSync3(p) : readFileSync13(p));
|
|
7630
7801
|
} catch {
|
|
7631
7802
|
h.update("<gone>");
|
|
7632
7803
|
}
|
|
@@ -7649,7 +7820,7 @@ function overlayPristine(cwd, base, dest, policy, cmd) {
|
|
|
7649
7820
|
throw new Error(`the base's ${e.path} is a submodule; the pristine copy cannot reproduce it`);
|
|
7650
7821
|
}
|
|
7651
7822
|
const content = execFileSync8("git", ["show", `${base}:${e.path}`], { cwd, maxBuffer: 1 << 28, env: trustedGitEnv() });
|
|
7652
|
-
const out2 =
|
|
7823
|
+
const out2 = join13(dest, e.path);
|
|
7653
7824
|
mkdirSync6(dirname5(out2), { recursive: true });
|
|
7654
7825
|
rmSync3(out2, { force: true });
|
|
7655
7826
|
if (e.mode === "120000") {
|
|
@@ -7664,7 +7835,7 @@ function overlayPristine(cwd, base, dest, policy, cmd) {
|
|
|
7664
7835
|
const inCopy = git2(["ls-files", "-z", "--cached", "--others", "--exclude-standard"], cwd).split("\0").filter(Boolean);
|
|
7665
7836
|
for (const rel of inCopy) {
|
|
7666
7837
|
if (!isOverlay(rel) || baseProtected.has(rel)) continue;
|
|
7667
|
-
rmSync3(
|
|
7838
|
+
rmSync3(join13(dest, rel), { force: true });
|
|
7668
7839
|
removed++;
|
|
7669
7840
|
}
|
|
7670
7841
|
return { restored, removed };
|
|
@@ -7706,8 +7877,8 @@ function suiteEnv(scratch) {
|
|
|
7706
7877
|
if (DROPPED_ENV.has(k) || PINNED_NPM.test(k)) continue;
|
|
7707
7878
|
env[k] = v;
|
|
7708
7879
|
}
|
|
7709
|
-
const userRc =
|
|
7710
|
-
const globalRc =
|
|
7880
|
+
const userRc = join13(scratch, "npmrc-user");
|
|
7881
|
+
const globalRc = join13(scratch, "npmrc-global");
|
|
7711
7882
|
for (const f of [userRc, globalRc]) writeFileSync6(f, "", { mode: 292 });
|
|
7712
7883
|
env.npm_config_userconfig = userRc;
|
|
7713
7884
|
env.npm_config_globalconfig = globalRc;
|
|
@@ -7717,8 +7888,8 @@ function suiteEnv(scratch) {
|
|
|
7717
7888
|
}
|
|
7718
7889
|
function runSuite(dir, cmd, budgetSecs) {
|
|
7719
7890
|
const t0 = Date.now();
|
|
7720
|
-
const outDir = mkdtempSync2(
|
|
7721
|
-
const outFile =
|
|
7891
|
+
const outDir = mkdtempSync2(join13(tmpdir2(), "tw-verify-run-"));
|
|
7892
|
+
const outFile = join13(outDir, "result.json");
|
|
7722
7893
|
try {
|
|
7723
7894
|
spawnSync(process.execPath, ["-e", SUPERVISOR, cmd, String(budgetSecs * 1e3), outFile], {
|
|
7724
7895
|
cwd: dir,
|
|
@@ -7731,7 +7902,7 @@ function runSuite(dir, cmd, budgetSecs) {
|
|
|
7731
7902
|
const secs = Math.round((Date.now() - t0) / 1e3);
|
|
7732
7903
|
let r;
|
|
7733
7904
|
try {
|
|
7734
|
-
r = JSON.parse(
|
|
7905
|
+
r = JSON.parse(readFileSync13(outFile, "utf8"));
|
|
7735
7906
|
} catch {
|
|
7736
7907
|
return { exit: null, secs };
|
|
7737
7908
|
}
|
|
@@ -7782,8 +7953,8 @@ function runVerify(opts) {
|
|
|
7782
7953
|
out2(`verify: cannot resolve base rev "${opts.base ?? "HEAD"}" \u2014 failing closed`);
|
|
7783
7954
|
return 2;
|
|
7784
7955
|
}
|
|
7785
|
-
const visRoot = mkdtempSync2(
|
|
7786
|
-
const visDir =
|
|
7956
|
+
const visRoot = mkdtempSync2(join13(tmpdir2(), "tw-verify-vis-"));
|
|
7957
|
+
const visDir = join13(visRoot, "t");
|
|
7787
7958
|
const cleanup = (dirs) => {
|
|
7788
7959
|
if (opts.keep) return;
|
|
7789
7960
|
for (const d of dirs) rmSync3(d, { recursive: true, force: true });
|
|
@@ -7811,8 +7982,8 @@ function runVerify(opts) {
|
|
|
7811
7982
|
out2("pristine run executes through it too. Failing closed, not open.");
|
|
7812
7983
|
return 2;
|
|
7813
7984
|
}
|
|
7814
|
-
const priRoot = mkdtempSync2(
|
|
7815
|
-
const priDir =
|
|
7985
|
+
const priRoot = mkdtempSync2(join13(tmpdir2(), "tw-verify-pri-"));
|
|
7986
|
+
const priDir = join13(priRoot, "t");
|
|
7816
7987
|
let restored = [];
|
|
7817
7988
|
let removedAdded = 0;
|
|
7818
7989
|
try {
|
|
@@ -7924,7 +8095,7 @@ function parseVerify(args) {
|
|
|
7924
8095
|
|
|
7925
8096
|
// src/cli/run.ts
|
|
7926
8097
|
import { execFileSync as execFileSync9, spawnSync as spawnSync2 } from "node:child_process";
|
|
7927
|
-
import { readdirSync as readdirSync4, readFileSync as
|
|
8098
|
+
import { readdirSync as readdirSync4, readFileSync as readFileSync14, realpathSync as realpathSync2 } from "node:fs";
|
|
7928
8099
|
import { resolve as resolve4 } from "node:path";
|
|
7929
8100
|
var out = (s) => process.stdout.write(s + "\n");
|
|
7930
8101
|
var err = (s) => process.stderr.write(s + "\n");
|
|
@@ -7933,7 +8104,7 @@ function git3(args, cwd) {
|
|
|
7933
8104
|
}
|
|
7934
8105
|
function nowTicks() {
|
|
7935
8106
|
try {
|
|
7936
|
-
return Math.floor(parseFloat(
|
|
8107
|
+
return Math.floor(parseFloat(readFileSync14("/proc/uptime", "utf8").split(" ")[0]) * 100);
|
|
7937
8108
|
} catch {
|
|
7938
8109
|
return Number.POSITIVE_INFINITY;
|
|
7939
8110
|
}
|
|
@@ -7942,7 +8113,7 @@ function survivorsHoldingTree(cwd, spawnedAfterTicks) {
|
|
|
7942
8113
|
const out2 = [];
|
|
7943
8114
|
let real;
|
|
7944
8115
|
try {
|
|
7945
|
-
real =
|
|
8116
|
+
real = realpathSync2(cwd);
|
|
7946
8117
|
} catch {
|
|
7947
8118
|
return out2;
|
|
7948
8119
|
}
|
|
@@ -7957,13 +8128,13 @@ function survivorsHoldingTree(cwd, spawnedAfterTicks) {
|
|
|
7957
8128
|
if (n === process.pid) continue;
|
|
7958
8129
|
let held;
|
|
7959
8130
|
try {
|
|
7960
|
-
held =
|
|
8131
|
+
held = realpathSync2(`/proc/${pid}/cwd`);
|
|
7961
8132
|
} catch {
|
|
7962
8133
|
continue;
|
|
7963
8134
|
}
|
|
7964
8135
|
if (held !== real && !held.startsWith(real + "/")) continue;
|
|
7965
8136
|
try {
|
|
7966
|
-
const stat =
|
|
8137
|
+
const stat = readFileSync14(`/proc/${pid}/stat`, "utf8");
|
|
7967
8138
|
const startTicks = Number(stat.slice(stat.lastIndexOf(")") + 2).split(" ")[19]);
|
|
7968
8139
|
if (Number.isFinite(startTicks) && startTicks >= spawnedAfterTicks) out2.push(n);
|
|
7969
8140
|
} catch {
|