tamperward 2.10.8 → 2.10.9
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 +447 -90
- package/package.json +1 -1
package/dist/cli/index.js
CHANGED
|
@@ -1651,8 +1651,8 @@ function numericOf(e) {
|
|
|
1651
1651
|
}
|
|
1652
1652
|
return void 0;
|
|
1653
1653
|
}
|
|
1654
|
-
function metricsOf(expr,
|
|
1655
|
-
const obj =
|
|
1654
|
+
function metricsOf(expr, resolve7) {
|
|
1655
|
+
const obj = resolve7(expr);
|
|
1656
1656
|
if (!ts2.isObjectLiteralExpression(obj)) return void 0;
|
|
1657
1657
|
const out2 = { values: {}, opaque: false };
|
|
1658
1658
|
let any = false;
|
|
@@ -1666,7 +1666,7 @@ function metricsOf(expr, resolve6) {
|
|
|
1666
1666
|
const k = keyName(p.name);
|
|
1667
1667
|
if (k === null || !isMetric(k)) continue;
|
|
1668
1668
|
any = true;
|
|
1669
|
-
const n = numericOf(
|
|
1669
|
+
const n = numericOf(resolve7(p.initializer));
|
|
1670
1670
|
if (n === void 0) out2.opaque = true;
|
|
1671
1671
|
else out2.values[k] = n;
|
|
1672
1672
|
}
|
|
@@ -1712,17 +1712,17 @@ function parseThresholds(src) {
|
|
|
1712
1712
|
const res = { paths: /* @__PURE__ */ new Map(), present: false, opaque: false };
|
|
1713
1713
|
try {
|
|
1714
1714
|
const sf = sourceOf(src);
|
|
1715
|
-
const
|
|
1715
|
+
const resolve7 = resolverFor(sf);
|
|
1716
1716
|
const visit = (node) => {
|
|
1717
1717
|
if (ts2.isPropertyAssignment(node)) {
|
|
1718
1718
|
const key2 = keyName(node.name);
|
|
1719
1719
|
if (key2 === "coverageThreshold" || key2 === "thresholds" && underKey(node, "coverage")) {
|
|
1720
1720
|
res.present = true;
|
|
1721
|
-
const init =
|
|
1721
|
+
const init = resolve7(node.initializer);
|
|
1722
1722
|
if (!ts2.isObjectLiteralExpression(init)) {
|
|
1723
1723
|
res.opaque = true;
|
|
1724
1724
|
} else {
|
|
1725
|
-
const flat = metricsOf(init,
|
|
1725
|
+
const flat = metricsOf(init, resolve7);
|
|
1726
1726
|
if (flat) res.global = merge(res.global, flat);
|
|
1727
1727
|
for (const p of init.properties) {
|
|
1728
1728
|
if (ts2.isSpreadAssignment(p)) {
|
|
@@ -1736,7 +1736,7 @@ function parseThresholds(src) {
|
|
|
1736
1736
|
res.global = merge(res.global, { values: { branches: 100, functions: 100, lines: 100, statements: 100 }, opaque: false });
|
|
1737
1737
|
continue;
|
|
1738
1738
|
}
|
|
1739
|
-
const m = metricsOf(p.initializer,
|
|
1739
|
+
const m = metricsOf(p.initializer, resolve7);
|
|
1740
1740
|
if (!m) continue;
|
|
1741
1741
|
if (k === "global") res.global = merge(res.global, m);
|
|
1742
1742
|
else res.paths.set(norm(k), m);
|
|
@@ -1769,18 +1769,18 @@ function parseLists(src) {
|
|
|
1769
1769
|
const res = { collectFrom: null, collectKey: "collectCoverageFrom", ignore: [], opaqueCollect: false, opaqueIgnore: false };
|
|
1770
1770
|
try {
|
|
1771
1771
|
const sf = sourceOf(src);
|
|
1772
|
-
const
|
|
1772
|
+
const resolve7 = resolverFor(sf);
|
|
1773
1773
|
const visit = (node) => {
|
|
1774
1774
|
if (ts2.isPropertyAssignment(node)) {
|
|
1775
1775
|
const k = keyName(node.name);
|
|
1776
1776
|
const vitestCoverage = ownerKey(node) === "coverage";
|
|
1777
1777
|
if (k === "collectCoverageFrom" || k === "include" && vitestCoverage) {
|
|
1778
|
-
const { items, opaque } = literals(
|
|
1778
|
+
const { items, opaque } = literals(resolve7(node.initializer));
|
|
1779
1779
|
res.collectFrom = items;
|
|
1780
1780
|
res.collectKey = k === "include" ? "coverage.include" : k;
|
|
1781
1781
|
if (opaque) res.opaqueCollect = true;
|
|
1782
1782
|
} else if (k === "coveragePathIgnorePatterns" || k === "exclude" && vitestCoverage) {
|
|
1783
|
-
const { items, opaque } = literals(
|
|
1783
|
+
const { items, opaque } = literals(resolve7(node.initializer));
|
|
1784
1784
|
res.ignore.push(...items);
|
|
1785
1785
|
if (opaque) res.opaqueIgnore = true;
|
|
1786
1786
|
}
|
|
@@ -8163,16 +8163,14 @@ tamperward init: ${changed} change(s) applied. Commit them so the gate travels w
|
|
|
8163
8163
|
|
|
8164
8164
|
// src/cli/verify.ts
|
|
8165
8165
|
import { execFileSync as execFileSync8, spawnSync } from "node:child_process";
|
|
8166
|
-
import { createHash as
|
|
8167
|
-
import { chmodSync as chmodSync2, cpSync, lstatSync as
|
|
8166
|
+
import { createHash as createHash9 } from "node:crypto";
|
|
8167
|
+
import { chmodSync as chmodSync2, cpSync, lstatSync as lstatSync4, mkdirSync as mkdirSync6, mkdtempSync as mkdtempSync2, readFileSync as readFileSync14, readlinkSync as readlinkSync3, rmSync as rmSync3, symlinkSync, writeFileSync as writeFileSync6 } from "node:fs";
|
|
8168
8168
|
import { tmpdir as tmpdir2 } from "node:os";
|
|
8169
|
-
import { dirname as
|
|
8169
|
+
import { dirname as dirname6, isAbsolute as isAbsolute10, join as join14, relative as relative5, resolve as resolve5, sep as sep2 } from "node:path";
|
|
8170
8170
|
|
|
8171
8171
|
// src/fingerprint.ts
|
|
8172
8172
|
import { execFileSync as execFileSync7 } from "node:child_process";
|
|
8173
8173
|
import { createHash as createHash7 } from "node:crypto";
|
|
8174
|
-
import { readdirSync as readdirSync3, readFileSync as readFileSync13, readlinkSync as readlinkSync2, statSync as statSync4 } from "node:fs";
|
|
8175
|
-
import { join as join13 } from "node:path";
|
|
8176
8174
|
function treeFingerprint(cwd, keepIgnored) {
|
|
8177
8175
|
const listed = execFileSync7("git", ["ls-files", "-z", "--cached", "--others", "--exclude-standard"], {
|
|
8178
8176
|
cwd,
|
|
@@ -8195,45 +8193,359 @@ function treeFingerprint(cwd, keepIgnored) {
|
|
|
8195
8193
|
}
|
|
8196
8194
|
return h.digest("hex");
|
|
8197
8195
|
}
|
|
8198
|
-
|
|
8199
|
-
|
|
8200
|
-
|
|
8201
|
-
|
|
8196
|
+
|
|
8197
|
+
// src/dependency-env.ts
|
|
8198
|
+
import { createHash as createHash8 } from "node:crypto";
|
|
8199
|
+
import {
|
|
8200
|
+
existsSync as existsSync10,
|
|
8201
|
+
lstatSync as lstatSync3,
|
|
8202
|
+
readdirSync as readdirSync3,
|
|
8203
|
+
readFileSync as readFileSync13,
|
|
8204
|
+
readlinkSync as readlinkSync2,
|
|
8205
|
+
realpathSync as realpathSync3,
|
|
8206
|
+
statSync as statSync4
|
|
8207
|
+
} from "node:fs";
|
|
8208
|
+
import {
|
|
8209
|
+
basename as basename3,
|
|
8210
|
+
delimiter,
|
|
8211
|
+
dirname as dirname5,
|
|
8212
|
+
isAbsolute as isAbsolute9,
|
|
8213
|
+
join as join13,
|
|
8214
|
+
relative as relative4,
|
|
8215
|
+
resolve as resolve4,
|
|
8216
|
+
sep
|
|
8217
|
+
} from "node:path";
|
|
8218
|
+
var Unattestable = class extends Error {
|
|
8219
|
+
};
|
|
8220
|
+
function inside(root, candidate) {
|
|
8221
|
+
const rel = relative4(resolve4(root), resolve4(candidate));
|
|
8222
|
+
return rel === "" || rel !== ".." && !rel.startsWith(".." + sep) && !isAbsolute9(rel);
|
|
8223
|
+
}
|
|
8224
|
+
function modeKind(path) {
|
|
8225
|
+
const st = lstatSync3(path);
|
|
8226
|
+
if (st.isFile()) return { kind: "file", mode: st.mode };
|
|
8227
|
+
if (st.isDirectory()) return { kind: "dir", mode: st.mode };
|
|
8228
|
+
if (st.isSymbolicLink()) return { kind: "symlink", mode: st.mode };
|
|
8229
|
+
return { kind: "special", mode: st.mode };
|
|
8230
|
+
}
|
|
8231
|
+
function fingerprintRoot(root, cwd, h) {
|
|
8232
|
+
let rootStat;
|
|
8233
|
+
let currentReal;
|
|
8234
|
+
try {
|
|
8235
|
+
rootStat = lstatSync3(root.path);
|
|
8236
|
+
currentReal = realpathSync3(root.path);
|
|
8237
|
+
} catch (e) {
|
|
8238
|
+
throw new Unattestable(
|
|
8239
|
+
root.kind + " root " + JSON.stringify(root.path) + " can no longer be resolved (" + (e instanceof Error ? e.message : String(e)) + ")"
|
|
8240
|
+
);
|
|
8241
|
+
}
|
|
8242
|
+
if (currentReal !== root.realPath) {
|
|
8243
|
+
throw new Unattestable(
|
|
8244
|
+
root.kind + " root moved from " + JSON.stringify(root.realPath) + " to " + JSON.stringify(currentReal)
|
|
8245
|
+
);
|
|
8246
|
+
}
|
|
8247
|
+
if (!statSync4(root.path).isDirectory()) {
|
|
8248
|
+
throw new Unattestable(root.kind + " root " + JSON.stringify(root.path) + " is not a directory");
|
|
8249
|
+
}
|
|
8250
|
+
h.update("root\0" + root.kind + "\0" + root.path + "\0" + root.realPath + "\0" + String(rootStat.mode) + "\0");
|
|
8251
|
+
if (rootStat.isSymbolicLink()) h.update(readlinkSync2(root.path) + "\0");
|
|
8252
|
+
const cwdReal = realpathSync3(cwd);
|
|
8253
|
+
const visited = /* @__PURE__ */ new Set();
|
|
8254
|
+
const hashFile = (path, label) => {
|
|
8255
|
+
let st;
|
|
8256
|
+
try {
|
|
8257
|
+
st = statSync4(path);
|
|
8258
|
+
if (!st.isFile()) throw new Error("not a regular file");
|
|
8259
|
+
h.update(label + "\0external-file\0" + path + "\0" + String(st.mode) + "\0");
|
|
8260
|
+
h.update(readFileSync13(path));
|
|
8261
|
+
h.update("\0");
|
|
8262
|
+
} catch (e) {
|
|
8263
|
+
throw new Unattestable(
|
|
8264
|
+
"dependency link target " + JSON.stringify(path) + " is not attestable (" + (e instanceof Error ? e.message : String(e)) + ")"
|
|
8265
|
+
);
|
|
8266
|
+
}
|
|
8267
|
+
};
|
|
8268
|
+
const walk2 = (dir, namespace) => {
|
|
8269
|
+
const realDir = realpathSync3(dir);
|
|
8270
|
+
if (visited.has(realDir)) return;
|
|
8271
|
+
visited.add(realDir);
|
|
8202
8272
|
let entries;
|
|
8203
8273
|
try {
|
|
8204
8274
|
entries = readdirSync3(dir, { withFileTypes: true });
|
|
8205
|
-
} catch {
|
|
8206
|
-
|
|
8275
|
+
} catch (e) {
|
|
8276
|
+
throw new Unattestable(
|
|
8277
|
+
"dependency directory " + JSON.stringify(dir) + " cannot be read (" + (e instanceof Error ? e.message : String(e)) + ")"
|
|
8278
|
+
);
|
|
8207
8279
|
}
|
|
8208
|
-
for (const
|
|
8209
|
-
const
|
|
8210
|
-
|
|
8211
|
-
|
|
8212
|
-
|
|
8280
|
+
for (const entry of [...entries].sort((a, b) => a.name.localeCompare(b.name))) {
|
|
8281
|
+
const path = join13(dir, entry.name);
|
|
8282
|
+
const rel = namespace ? namespace + "/" + entry.name : entry.name;
|
|
8283
|
+
let ident;
|
|
8284
|
+
try {
|
|
8285
|
+
ident = modeKind(path);
|
|
8286
|
+
} catch (e) {
|
|
8287
|
+
throw new Unattestable(
|
|
8288
|
+
"dependency entry " + JSON.stringify(path) + " disappeared while attesting (" + (e instanceof Error ? e.message : String(e)) + ")"
|
|
8289
|
+
);
|
|
8290
|
+
}
|
|
8291
|
+
h.update(rel + "\0" + ident.kind + "\0" + String(ident.mode) + "\0");
|
|
8292
|
+
if (ident.kind === "file") {
|
|
8213
8293
|
try {
|
|
8214
|
-
h.update(readFileSync13(
|
|
8215
|
-
h.update(
|
|
8216
|
-
} catch {
|
|
8217
|
-
|
|
8294
|
+
h.update(readFileSync13(path));
|
|
8295
|
+
h.update("\0");
|
|
8296
|
+
} catch (e) {
|
|
8297
|
+
throw new Unattestable(
|
|
8298
|
+
"dependency file " + JSON.stringify(path) + " cannot be read (" + (e instanceof Error ? e.message : String(e)) + ")"
|
|
8299
|
+
);
|
|
8300
|
+
}
|
|
8301
|
+
continue;
|
|
8302
|
+
}
|
|
8303
|
+
if (ident.kind === "dir") {
|
|
8304
|
+
walk2(path, rel);
|
|
8305
|
+
continue;
|
|
8306
|
+
}
|
|
8307
|
+
if (ident.kind === "symlink") {
|
|
8308
|
+
let target;
|
|
8309
|
+
try {
|
|
8310
|
+
target = readlinkSync2(path);
|
|
8311
|
+
} catch (e) {
|
|
8312
|
+
throw new Unattestable(
|
|
8313
|
+
"dependency symlink " + JSON.stringify(path) + " cannot be read (" + (e instanceof Error ? e.message : String(e)) + ")"
|
|
8314
|
+
);
|
|
8218
8315
|
}
|
|
8219
|
-
|
|
8220
|
-
|
|
8316
|
+
h.update(target + "\0");
|
|
8317
|
+
let resolved;
|
|
8221
8318
|
try {
|
|
8222
|
-
|
|
8319
|
+
resolved = realpathSync3(path);
|
|
8223
8320
|
} catch {
|
|
8224
|
-
|
|
8321
|
+
continue;
|
|
8322
|
+
}
|
|
8323
|
+
const followed = statSync4(path);
|
|
8324
|
+
if (followed.isFile()) {
|
|
8325
|
+
hashFile(resolved, "@target:" + rel);
|
|
8326
|
+
continue;
|
|
8327
|
+
}
|
|
8328
|
+
if (followed.isDirectory()) {
|
|
8329
|
+
if (inside(root.realPath, resolved)) {
|
|
8330
|
+
walk2(resolved, "@target:" + rel);
|
|
8331
|
+
continue;
|
|
8332
|
+
}
|
|
8333
|
+
if (inside(cwdReal, resolved)) {
|
|
8334
|
+
throw new Unattestable(
|
|
8335
|
+
"dependency workspace symlink " + JSON.stringify(path) + " points into the candidate worktree at " + JSON.stringify(resolved) + "; faithful per-copy remapping is not available"
|
|
8336
|
+
);
|
|
8337
|
+
}
|
|
8338
|
+
throw new Unattestable(
|
|
8339
|
+
"dependency directory symlink " + JSON.stringify(path) + " escapes the bounded root to " + JSON.stringify(resolved)
|
|
8340
|
+
);
|
|
8225
8341
|
}
|
|
8342
|
+
throw new Unattestable("dependency symlink " + JSON.stringify(path) + " resolves to a special file");
|
|
8226
8343
|
}
|
|
8344
|
+
throw new Unattestable("dependency entry " + JSON.stringify(path) + " is a special file");
|
|
8227
8345
|
}
|
|
8228
8346
|
};
|
|
8229
|
-
|
|
8230
|
-
|
|
8231
|
-
|
|
8232
|
-
|
|
8347
|
+
walk2(root.realPath, "");
|
|
8348
|
+
}
|
|
8349
|
+
function snapshot(roots, probes, cwd) {
|
|
8350
|
+
const h = createHash8("sha256");
|
|
8351
|
+
for (const probe of probes) {
|
|
8352
|
+
h.update(
|
|
8353
|
+
"probe\0" + probe.kind + "\0" + probe.path + "\0" + (existsSync10(probe.path) ? "present" : "absent") + "\0"
|
|
8354
|
+
);
|
|
8233
8355
|
}
|
|
8234
|
-
|
|
8356
|
+
for (const root of roots) fingerprintRoot(root, cwd, h);
|
|
8235
8357
|
return h.digest("hex");
|
|
8236
8358
|
}
|
|
8359
|
+
function freezeRoot(kind, path) {
|
|
8360
|
+
try {
|
|
8361
|
+
if (!statSync4(path).isDirectory()) {
|
|
8362
|
+
throw new Error("not a directory");
|
|
8363
|
+
}
|
|
8364
|
+
return { kind, path: resolve4(path), realPath: realpathSync3(path) };
|
|
8365
|
+
} catch (e) {
|
|
8366
|
+
throw new Unattestable(
|
|
8367
|
+
kind + " root " + JSON.stringify(path) + " is not resolvable (" + (e instanceof Error ? e.message : String(e)) + ")"
|
|
8368
|
+
);
|
|
8369
|
+
}
|
|
8370
|
+
}
|
|
8371
|
+
function shellTokens(command) {
|
|
8372
|
+
const matches = command.match(/(?:[^\s"'\\]+|"(?:\\.|[^"])*"|'[^']*')+/g) ?? [];
|
|
8373
|
+
return matches.map((token) => {
|
|
8374
|
+
if (token.startsWith("'") && token.endsWith("'") || token.startsWith('"') && token.endsWith('"')) return token.slice(1, -1);
|
|
8375
|
+
return token;
|
|
8376
|
+
});
|
|
8377
|
+
}
|
|
8378
|
+
function firstExecutable(command) {
|
|
8379
|
+
const tokens2 = shellTokens(command);
|
|
8380
|
+
let i = 0;
|
|
8381
|
+
while (i < tokens2.length && /^[A-Za-z_][A-Za-z0-9_]*=/.test(tokens2[i])) i++;
|
|
8382
|
+
if (tokens2[i] === "env") {
|
|
8383
|
+
i++;
|
|
8384
|
+
while (i < tokens2.length && (tokens2[i].startsWith("-") || /^[A-Za-z_][A-Za-z0-9_]*=/.test(tokens2[i]))) i++;
|
|
8385
|
+
}
|
|
8386
|
+
if (tokens2[i] === "command") {
|
|
8387
|
+
i++;
|
|
8388
|
+
while (i < tokens2.length && tokens2[i].startsWith("-")) i++;
|
|
8389
|
+
}
|
|
8390
|
+
if (tokens2[i] === "timeout") {
|
|
8391
|
+
i++;
|
|
8392
|
+
while (i < tokens2.length && tokens2[i].startsWith("-")) i++;
|
|
8393
|
+
if (i < tokens2.length) i++;
|
|
8394
|
+
}
|
|
8395
|
+
return tokens2[i] ?? null;
|
|
8396
|
+
}
|
|
8397
|
+
function resolveExecutable(token, cwd, env) {
|
|
8398
|
+
if (!token) return null;
|
|
8399
|
+
if (token.includes("/") || token.includes("\\")) {
|
|
8400
|
+
const path = isAbsolute9(token) ? token : resolve4(cwd, token);
|
|
8401
|
+
try {
|
|
8402
|
+
return existsSync10(path) ? realpathSync3(path) : null;
|
|
8403
|
+
} catch {
|
|
8404
|
+
return null;
|
|
8405
|
+
}
|
|
8406
|
+
}
|
|
8407
|
+
for (const entry of (env.PATH ?? "").split(delimiter).filter(Boolean)) {
|
|
8408
|
+
const candidate = resolve4(entry, token);
|
|
8409
|
+
try {
|
|
8410
|
+
if (existsSync10(candidate) && statSync4(candidate).isFile()) return realpathSync3(candidate);
|
|
8411
|
+
} catch {
|
|
8412
|
+
}
|
|
8413
|
+
}
|
|
8414
|
+
return null;
|
|
8415
|
+
}
|
|
8416
|
+
function pythonName(name) {
|
|
8417
|
+
return /^(?:python(?:\d+(?:\.\d+)*)?|pytest|py\.test|pip(?:\d+(?:\.\d+)*)?)$/.test(name.toLowerCase());
|
|
8418
|
+
}
|
|
8419
|
+
function looksPython(command, executable, token) {
|
|
8420
|
+
const exe = executable ? basename3(executable) : "";
|
|
8421
|
+
const raw = token ? basename3(token) : "";
|
|
8422
|
+
if (pythonName(exe) || pythonName(raw)) return true;
|
|
8423
|
+
return /(?:^|[\s;&|()])(?:python(?:\d+(?:\.\d+)*)?|pytest|py\.test|pip(?:\d+(?:\.\d+)*)?)(?=$|[\s;&|()])/.test(command);
|
|
8424
|
+
}
|
|
8425
|
+
function inferredVenvFromToken(token, cwd) {
|
|
8426
|
+
if (!token || !token.includes("/") && !token.includes("\\")) return null;
|
|
8427
|
+
const path = isAbsolute9(token) ? token : resolve4(cwd, token);
|
|
8428
|
+
if (!pythonName(basename3(path))) return null;
|
|
8429
|
+
const parent = dirname5(path);
|
|
8430
|
+
const parentName = basename3(parent).toLowerCase();
|
|
8431
|
+
if (parentName !== "bin" && parentName !== "scripts") return null;
|
|
8432
|
+
const root = dirname5(parent);
|
|
8433
|
+
return existsSync10(join13(root, "pyvenv.cfg")) ? root : null;
|
|
8434
|
+
}
|
|
8435
|
+
function unsupportedEcosystem(command, token) {
|
|
8436
|
+
const name = token ? basename3(token).toLowerCase() : "";
|
|
8437
|
+
const raw = command.toLowerCase();
|
|
8438
|
+
if (["ruby", "bundle", "bundler", "rspec", "rake"].includes(name) || /(?:^|[\s;&|()])(?:bundle\s+exec|ruby|rspec|rake)(?=$|[\s;&|()])/.test(raw)) return "Ruby dependency environment discovery is not yet attestable";
|
|
8439
|
+
if (["mvn", "mvnw", "gradle", "gradlew", "java"].includes(name) || /(?:^|[\s;&|()])(?:mvn|mvnw|gradle|gradlew|java)(?=$|[\s;&|()])/.test(raw)) return "JVM dependency environment discovery is not yet attestable";
|
|
8440
|
+
if (name === "dotnet" || /(?:^|[\s;&|()])dotnet(?=$|[\s;&|()])/.test(raw)) return ".NET dependency environment discovery is not yet attestable";
|
|
8441
|
+
return null;
|
|
8442
|
+
}
|
|
8443
|
+
function inferredVenv(executable) {
|
|
8444
|
+
if (!executable) return null;
|
|
8445
|
+
const parent = dirname5(executable);
|
|
8446
|
+
const parentName = basename3(parent).toLowerCase();
|
|
8447
|
+
if (parentName !== "bin" && parentName !== "scripts") return null;
|
|
8448
|
+
const root = dirname5(parent);
|
|
8449
|
+
return existsSync10(join13(root, "pyvenv.cfg")) ? root : null;
|
|
8450
|
+
}
|
|
8451
|
+
function venvProblem(root) {
|
|
8452
|
+
const cfg = join13(root, "pyvenv.cfg");
|
|
8453
|
+
if (!existsSync10(cfg)) return "VIRTUAL_ENV " + JSON.stringify(root) + " has no pyvenv.cfg";
|
|
8454
|
+
try {
|
|
8455
|
+
const body = readFileSync13(cfg, "utf8");
|
|
8456
|
+
if (/^\s*include-system-site-packages\s*=\s*true\s*$/im.test(body)) {
|
|
8457
|
+
return "Python environment " + JSON.stringify(root) + " enables system site-packages; those external package roots are not bounded";
|
|
8458
|
+
}
|
|
8459
|
+
} catch (e) {
|
|
8460
|
+
return "cannot read " + JSON.stringify(cfg) + " (" + (e instanceof Error ? e.message : String(e)) + ")";
|
|
8461
|
+
}
|
|
8462
|
+
return null;
|
|
8463
|
+
}
|
|
8464
|
+
function discoverDependencyEnvironment(cwdInput, command, env = process.env) {
|
|
8465
|
+
const cwd = resolve4(cwdInput);
|
|
8466
|
+
const roots = [];
|
|
8467
|
+
const probes = [];
|
|
8468
|
+
const seen = /* @__PURE__ */ new Set();
|
|
8469
|
+
const addRoot = (kind, path) => {
|
|
8470
|
+
const root = freezeRoot(kind, path);
|
|
8471
|
+
const key2 = kind + ":" + root.path;
|
|
8472
|
+
if (!seen.has(key2)) {
|
|
8473
|
+
roots.push(root);
|
|
8474
|
+
seen.add(key2);
|
|
8475
|
+
}
|
|
8476
|
+
};
|
|
8477
|
+
try {
|
|
8478
|
+
const nodeModules = join13(cwd, "node_modules");
|
|
8479
|
+
if (existsSync10(nodeModules)) addRoot("node_modules", nodeModules);
|
|
8480
|
+
else probes.push({ kind: "node_modules", path: nodeModules });
|
|
8481
|
+
const token = firstExecutable(command);
|
|
8482
|
+
const executable = resolveExecutable(token, cwd, env);
|
|
8483
|
+
let venv = null;
|
|
8484
|
+
if (env.VIRTUAL_ENV?.trim()) {
|
|
8485
|
+
if (!isAbsolute9(env.VIRTUAL_ENV)) {
|
|
8486
|
+
throw new Unattestable("VIRTUAL_ENV is relative (" + JSON.stringify(env.VIRTUAL_ENV) + ")");
|
|
8487
|
+
}
|
|
8488
|
+
venv = resolve4(env.VIRTUAL_ENV);
|
|
8489
|
+
} else {
|
|
8490
|
+
venv = inferredVenv(executable) ?? inferredVenvFromToken(token, cwd);
|
|
8491
|
+
if (!venv && /(?:^|\s)uv\s+run(?:\s|$)/.test(command) && existsSync10(join13(cwd, ".venv", "pyvenv.cfg"))) {
|
|
8492
|
+
venv = join13(cwd, ".venv");
|
|
8493
|
+
}
|
|
8494
|
+
}
|
|
8495
|
+
if (venv) {
|
|
8496
|
+
const problem = venvProblem(venv);
|
|
8497
|
+
if (problem) throw new Unattestable(problem);
|
|
8498
|
+
addRoot("python-venv", venv);
|
|
8499
|
+
} else if (looksPython(command, executable, token)) {
|
|
8500
|
+
throw new Unattestable(
|
|
8501
|
+
"Python verifier selected without an identifiable virtual environment; global/user site-packages are not bounded"
|
|
8502
|
+
);
|
|
8503
|
+
}
|
|
8504
|
+
const unsupported = unsupportedEcosystem(command, token);
|
|
8505
|
+
if (unsupported) throw new Unattestable(unsupported);
|
|
8506
|
+
const status = roots.length ? "attested" : "none";
|
|
8507
|
+
const fingerprint2 = snapshot(roots, probes, cwd);
|
|
8508
|
+
return { status, command, roots, probes, fingerprint: fingerprint2 };
|
|
8509
|
+
} catch (e) {
|
|
8510
|
+
return {
|
|
8511
|
+
status: "unattestable",
|
|
8512
|
+
command,
|
|
8513
|
+
roots,
|
|
8514
|
+
probes,
|
|
8515
|
+
reason: e instanceof Error ? e.message : String(e)
|
|
8516
|
+
};
|
|
8517
|
+
}
|
|
8518
|
+
}
|
|
8519
|
+
function checkDependencyEnvironment(cwdInput, descriptor) {
|
|
8520
|
+
if (descriptor.status === "unattestable") {
|
|
8521
|
+
return { ok: false, reason: descriptor.reason ?? "dependency environment is not attestable" };
|
|
8522
|
+
}
|
|
8523
|
+
try {
|
|
8524
|
+
const fingerprint2 = snapshot(descriptor.roots, descriptor.probes, resolve4(cwdInput));
|
|
8525
|
+
if (fingerprint2 !== descriptor.fingerprint) {
|
|
8526
|
+
return { ok: false, fingerprint: fingerprint2, reason: "dependency environment fingerprint changed" };
|
|
8527
|
+
}
|
|
8528
|
+
return { ok: true, fingerprint: fingerprint2 };
|
|
8529
|
+
} catch (e) {
|
|
8530
|
+
return { ok: false, reason: e instanceof Error ? e.message : String(e) };
|
|
8531
|
+
}
|
|
8532
|
+
}
|
|
8533
|
+
function dependencyEnvironmentReport(descriptor) {
|
|
8534
|
+
return {
|
|
8535
|
+
status: descriptor.status,
|
|
8536
|
+
roots: descriptor.roots.map((root) => ({ kind: root.kind, path: root.path })),
|
|
8537
|
+
...descriptor.fingerprint ? { fingerprint: descriptor.fingerprint } : {},
|
|
8538
|
+
...descriptor.reason ? { reason: descriptor.reason } : {}
|
|
8539
|
+
};
|
|
8540
|
+
}
|
|
8541
|
+
function dependencyEnvironmentSummary(descriptor) {
|
|
8542
|
+
if (descriptor.status === "unattestable") {
|
|
8543
|
+
return "unattestable - " + (descriptor.reason ?? "unknown dependency environment");
|
|
8544
|
+
}
|
|
8545
|
+
if (descriptor.status === "none") return "none detected (candidate node_modules remains absent)";
|
|
8546
|
+
const kinds = [...new Set(descriptor.roots.map((root) => root.kind))].join(", ");
|
|
8547
|
+
return "attested " + kinds + " (sha256 " + (descriptor.fingerprint?.slice(0, 12) ?? "unknown") + "...)";
|
|
8548
|
+
}
|
|
8237
8549
|
|
|
8238
8550
|
// src/cli/verify.ts
|
|
8239
8551
|
var OVERLAY_CLASSES = ["tests", "snapshots", "config"];
|
|
@@ -8323,25 +8635,25 @@ function baseIsAncestorOfHead(base, cwd) {
|
|
|
8323
8635
|
const r = spawnSync("git", ["merge-base", "--is-ancestor", base, "HEAD"], { cwd, env: trustedGitEnv() });
|
|
8324
8636
|
return r.status === 0;
|
|
8325
8637
|
}
|
|
8326
|
-
function
|
|
8327
|
-
const rel =
|
|
8328
|
-
return rel === "" || rel !== ".." && !rel.startsWith(`..${
|
|
8638
|
+
function inside2(root, path) {
|
|
8639
|
+
const rel = relative5(resolve5(root), resolve5(path));
|
|
8640
|
+
return rel === "" || rel !== ".." && !rel.startsWith(`..${sep2}`) && !isAbsolute10(rel);
|
|
8329
8641
|
}
|
|
8330
8642
|
function linkParts(path) {
|
|
8331
|
-
const parts =
|
|
8643
|
+
const parts = sep2 === "\\" ? path.split(/[\\/]+/) : path.split("/");
|
|
8332
8644
|
return parts.filter((part) => part !== "" && part !== ".");
|
|
8333
8645
|
}
|
|
8334
8646
|
function linkEscape(label, target) {
|
|
8335
8647
|
return new Error(`${label} is a symlink that escapes the materialised tree (${JSON.stringify(target)})`);
|
|
8336
8648
|
}
|
|
8337
8649
|
function driveRelativeLinkTarget(target) {
|
|
8338
|
-
return
|
|
8650
|
+
return sep2 === "\\" && /^[A-Za-z]:(?![\\/])/.test(target);
|
|
8339
8651
|
}
|
|
8340
8652
|
function rootedLinkTarget(target) {
|
|
8341
|
-
return
|
|
8653
|
+
return isAbsolute10(target) || driveRelativeLinkTarget(target);
|
|
8342
8654
|
}
|
|
8343
8655
|
function safeSymlink(target, out2, root, label) {
|
|
8344
|
-
if (rootedLinkTarget(target) || !
|
|
8656
|
+
if (rootedLinkTarget(target) || !inside2(root, resolve5(dirname6(out2), target))) {
|
|
8345
8657
|
throw linkEscape(label, target);
|
|
8346
8658
|
}
|
|
8347
8659
|
rmSync3(out2, { force: true });
|
|
@@ -8349,7 +8661,7 @@ function safeSymlink(target, out2, root, label) {
|
|
|
8349
8661
|
}
|
|
8350
8662
|
function validateSymlinkGraph(target, out2, root, dependencyRoot, label) {
|
|
8351
8663
|
let domain = "tree";
|
|
8352
|
-
let stack = linkParts(
|
|
8664
|
+
let stack = linkParts(relative5(root, dirname6(out2)));
|
|
8353
8665
|
const pending = linkParts(target);
|
|
8354
8666
|
const seen = /* @__PURE__ */ new Set();
|
|
8355
8667
|
let hops = 0;
|
|
@@ -8370,7 +8682,7 @@ function validateSymlinkGraph(target, out2, root, dependencyRoot, label) {
|
|
|
8370
8682
|
const current = join14(base, ...stack);
|
|
8371
8683
|
let st;
|
|
8372
8684
|
try {
|
|
8373
|
-
st =
|
|
8685
|
+
st = lstatSync4(current);
|
|
8374
8686
|
} catch {
|
|
8375
8687
|
continue;
|
|
8376
8688
|
}
|
|
@@ -8378,7 +8690,7 @@ function validateSymlinkGraph(target, out2, root, dependencyRoot, label) {
|
|
|
8378
8690
|
if (++hops > 128) {
|
|
8379
8691
|
throw new Error(`${label} contains a symlink cycle while materialising (${JSON.stringify(target)})`);
|
|
8380
8692
|
}
|
|
8381
|
-
const key2 = `${domain}:${current}:${pending.join(
|
|
8693
|
+
const key2 = `${domain}:${current}:${pending.join(sep2)}`;
|
|
8382
8694
|
if (seen.has(key2)) {
|
|
8383
8695
|
throw new Error(`${label} contains a symlink cycle while materialising (${JSON.stringify(target)})`);
|
|
8384
8696
|
}
|
|
@@ -8388,12 +8700,12 @@ function validateSymlinkGraph(target, out2, root, dependencyRoot, label) {
|
|
|
8388
8700
|
if (driveRelativeLinkTarget(next)) {
|
|
8389
8701
|
throw linkEscape(label, target);
|
|
8390
8702
|
}
|
|
8391
|
-
if (
|
|
8392
|
-
if (domain === "tree" || !dependencyRoot || !
|
|
8703
|
+
if (isAbsolute10(next)) {
|
|
8704
|
+
if (domain === "tree" || !dependencyRoot || !inside2(dependencyRoot, next)) {
|
|
8393
8705
|
throw linkEscape(label, target);
|
|
8394
8706
|
}
|
|
8395
8707
|
stack = [];
|
|
8396
|
-
pending.unshift(...linkParts(
|
|
8708
|
+
pending.unshift(...linkParts(relative5(dependencyRoot, resolve5(next))));
|
|
8397
8709
|
} else {
|
|
8398
8710
|
pending.unshift(...linkParts(next));
|
|
8399
8711
|
}
|
|
@@ -8406,16 +8718,16 @@ function rejectLinkedParent(cwd, rel) {
|
|
|
8406
8718
|
current = join14(current, part);
|
|
8407
8719
|
let st;
|
|
8408
8720
|
try {
|
|
8409
|
-
st =
|
|
8721
|
+
st = lstatSync4(current);
|
|
8410
8722
|
} catch {
|
|
8411
8723
|
continue;
|
|
8412
8724
|
}
|
|
8413
8725
|
if (st.isSymbolicLink()) {
|
|
8414
|
-
throw new Error(`${rel} has a symlinked parent directory (${
|
|
8726
|
+
throw new Error(`${rel} has a symlinked parent directory (${relative5(cwd, current)})`);
|
|
8415
8727
|
}
|
|
8416
8728
|
}
|
|
8417
8729
|
}
|
|
8418
|
-
function materialize(cwd, dest) {
|
|
8730
|
+
function materialize(cwd, dest, dependencyRoot) {
|
|
8419
8731
|
const listed = git2(["ls-files", "-z", "--cached", "--others", "--exclude-standard"], cwd).split("\0").filter(Boolean);
|
|
8420
8732
|
const links = [];
|
|
8421
8733
|
for (const rel of listed) {
|
|
@@ -8423,12 +8735,12 @@ function materialize(cwd, dest) {
|
|
|
8423
8735
|
rejectLinkedParent(cwd, rel);
|
|
8424
8736
|
let st;
|
|
8425
8737
|
try {
|
|
8426
|
-
st =
|
|
8738
|
+
st = lstatSync4(src);
|
|
8427
8739
|
} catch {
|
|
8428
8740
|
continue;
|
|
8429
8741
|
}
|
|
8430
8742
|
const out2 = join14(dest, rel);
|
|
8431
|
-
mkdirSync6(
|
|
8743
|
+
mkdirSync6(dirname6(out2), { recursive: true });
|
|
8432
8744
|
dropSymlink(out2);
|
|
8433
8745
|
if (st.isSymbolicLink()) {
|
|
8434
8746
|
const target = readlinkSync3(src);
|
|
@@ -8441,17 +8753,14 @@ function materialize(cwd, dest) {
|
|
|
8441
8753
|
throw new Error(`${rel} is ${st.isDirectory() ? "a directory" : "a special file"} where git expects a file`);
|
|
8442
8754
|
}
|
|
8443
8755
|
}
|
|
8444
|
-
const nm = resolve4(cwd, "node_modules");
|
|
8445
|
-
let dependencyRoot = null;
|
|
8446
|
-
if (existsSync10(nm)) dependencyRoot = realpathSync3(nm);
|
|
8447
8756
|
for (const link of links) {
|
|
8448
8757
|
validateSymlinkGraph(link.target, link.out, dest, dependencyRoot, link.rel);
|
|
8449
8758
|
}
|
|
8450
|
-
if (dependencyRoot) symlinkSync(
|
|
8759
|
+
if (dependencyRoot) symlinkSync(dependencyRoot, join14(dest, "node_modules"), "dir");
|
|
8451
8760
|
}
|
|
8452
8761
|
function dropSymlink(p) {
|
|
8453
8762
|
try {
|
|
8454
|
-
if (
|
|
8763
|
+
if (lstatSync4(p).isSymbolicLink()) rmSync3(p, { force: true });
|
|
8455
8764
|
} catch {
|
|
8456
8765
|
}
|
|
8457
8766
|
}
|
|
@@ -8476,13 +8785,13 @@ function baseEntries(base, cwd) {
|
|
|
8476
8785
|
});
|
|
8477
8786
|
}
|
|
8478
8787
|
function overlayDigest(dest, paths) {
|
|
8479
|
-
const h =
|
|
8788
|
+
const h = createHash9("sha256");
|
|
8480
8789
|
for (const rel of [...paths].sort()) {
|
|
8481
8790
|
h.update(rel);
|
|
8482
8791
|
h.update("\0");
|
|
8483
8792
|
const p = join14(dest, rel);
|
|
8484
8793
|
try {
|
|
8485
|
-
const st =
|
|
8794
|
+
const st = lstatSync4(p);
|
|
8486
8795
|
h.update(String(st.mode));
|
|
8487
8796
|
h.update(st.isSymbolicLink() ? readlinkSync3(p) : readFileSync14(p));
|
|
8488
8797
|
} catch {
|
|
@@ -8492,7 +8801,7 @@ function overlayDigest(dest, paths) {
|
|
|
8492
8801
|
}
|
|
8493
8802
|
return h.digest("hex");
|
|
8494
8803
|
}
|
|
8495
|
-
function overlayPristine(cwd, base, dest, policy, cmd) {
|
|
8804
|
+
function overlayPristine(cwd, base, dest, policy, cmd, dependencyRoot) {
|
|
8496
8805
|
const entries = baseEntries(base, cwd);
|
|
8497
8806
|
const atBase = entries.map((e) => e.path);
|
|
8498
8807
|
const verifierOwned = verifierInputs(cmd, atBase, policy);
|
|
@@ -8509,7 +8818,7 @@ function overlayPristine(cwd, base, dest, policy, cmd) {
|
|
|
8509
8818
|
}
|
|
8510
8819
|
const content = execFileSync8("git", ["show", `${base}:${e.path}`], { cwd, maxBuffer: 1 << 28, env: trustedGitEnv() });
|
|
8511
8820
|
const out2 = join14(dest, e.path);
|
|
8512
|
-
mkdirSync6(
|
|
8821
|
+
mkdirSync6(dirname6(out2), { recursive: true });
|
|
8513
8822
|
rmSync3(out2, { force: true });
|
|
8514
8823
|
if (e.mode === "120000") {
|
|
8515
8824
|
const target = content.toString("utf8");
|
|
@@ -8528,8 +8837,6 @@ function overlayPristine(cwd, base, dest, policy, cmd) {
|
|
|
8528
8837
|
rmSync3(join14(dest, rel), { force: true });
|
|
8529
8838
|
removed++;
|
|
8530
8839
|
}
|
|
8531
|
-
const nm = resolve4(cwd, "node_modules");
|
|
8532
|
-
const dependencyRoot = existsSync10(nm) ? realpathSync3(nm) : null;
|
|
8533
8840
|
for (const link of restoredLinks) {
|
|
8534
8841
|
validateSymlinkGraph(link.target, link.out, dest, dependencyRoot, `the base's ${link.path}`);
|
|
8535
8842
|
}
|
|
@@ -8641,6 +8948,22 @@ function runVerify(opts) {
|
|
|
8641
8948
|
return 2;
|
|
8642
8949
|
}
|
|
8643
8950
|
const budget = opts.budget ?? policy.verify?.budget ?? 300;
|
|
8951
|
+
const dependencyEnvironment = opts.dependencyEnvironment ?? discoverDependencyEnvironment(cwd, cmd);
|
|
8952
|
+
const dependencyReport = () => dependencyEnvironmentReport(dependencyEnvironment);
|
|
8953
|
+
if (dependencyEnvironment.status === "unattestable" && !opts.allowDepDrift) {
|
|
8954
|
+
if (opts.json) {
|
|
8955
|
+
out2(JSON.stringify({
|
|
8956
|
+
verdict: "CANNOT_VERIFY",
|
|
8957
|
+
reason: "DEPENDENCY_ENVIRONMENT_UNATTESTABLE",
|
|
8958
|
+
dependency_environment: dependencyReport()
|
|
8959
|
+
}));
|
|
8960
|
+
} else {
|
|
8961
|
+
out2("verify: dependency environment is not attestable \u2014 failing closed");
|
|
8962
|
+
out2("verify: " + (dependencyEnvironment.reason ?? "unknown dependency environment"));
|
|
8963
|
+
}
|
|
8964
|
+
return 2;
|
|
8965
|
+
}
|
|
8966
|
+
const frozenNodeModules = dependencyEnvironment.roots.find((root) => root.kind === "node_modules")?.realPath ?? null;
|
|
8644
8967
|
let base;
|
|
8645
8968
|
try {
|
|
8646
8969
|
base = resolveBase(opts.base ?? "HEAD", cwd);
|
|
@@ -8656,10 +8979,16 @@ function runVerify(opts) {
|
|
|
8656
8979
|
};
|
|
8657
8980
|
const protectedIgnored = (rel) => isProtected(rel, policy);
|
|
8658
8981
|
const treeBefore = treeFingerprint(cwd, protectedIgnored);
|
|
8659
|
-
const
|
|
8982
|
+
const dependencyAtEntry = checkDependencyEnvironment(cwd, dependencyEnvironment);
|
|
8983
|
+
if (!dependencyAtEntry.ok && !opts.allowDepDrift) {
|
|
8984
|
+
cleanup([visRoot]);
|
|
8985
|
+
out2("verify: the frozen dependency environment changed before the visible suite ran \u2014");
|
|
8986
|
+
out2((dependencyAtEntry.reason ?? "dependency identity changed") + ". Failing closed, not open.");
|
|
8987
|
+
return 2;
|
|
8988
|
+
}
|
|
8660
8989
|
try {
|
|
8661
8990
|
mkdirSync6(visDir);
|
|
8662
|
-
materialize(cwd, visDir);
|
|
8991
|
+
materialize(cwd, visDir, frozenNodeModules);
|
|
8663
8992
|
} catch (e) {
|
|
8664
8993
|
cleanup([visRoot]);
|
|
8665
8994
|
out2(`verify: could not materialize (${e instanceof Error ? e.message : String(e)}) \u2014 failing closed`);
|
|
@@ -8672,10 +9001,11 @@ function runVerify(opts) {
|
|
|
8672
9001
|
out2("would be materialised from a tree the candidate just edited. Failing closed, not open.");
|
|
8673
9002
|
return 2;
|
|
8674
9003
|
}
|
|
8675
|
-
|
|
9004
|
+
const dependencyAfterVisible = checkDependencyEnvironment(cwd, dependencyEnvironment);
|
|
9005
|
+
if (!dependencyAfterVisible.ok && !opts.allowDepDrift) {
|
|
8676
9006
|
cleanup([visRoot]);
|
|
8677
|
-
out2("verify: the
|
|
8678
|
-
out2("
|
|
9007
|
+
out2("verify: the frozen dependency environment changed while the visible suite was running \u2014");
|
|
9008
|
+
out2((dependencyAfterVisible.reason ?? "dependency identity changed") + ". Failing closed, not open.");
|
|
8679
9009
|
return 2;
|
|
8680
9010
|
}
|
|
8681
9011
|
const priRoot = mkdtempSync2(join14(tmpdir2(), "tw-verify-pri-"));
|
|
@@ -8684,8 +9014,15 @@ function runVerify(opts) {
|
|
|
8684
9014
|
let removedAdded = 0;
|
|
8685
9015
|
try {
|
|
8686
9016
|
mkdirSync6(priDir);
|
|
8687
|
-
materialize(cwd, priDir);
|
|
8688
|
-
({ restored, removed: removedAdded } = overlayPristine(
|
|
9017
|
+
materialize(cwd, priDir, frozenNodeModules);
|
|
9018
|
+
({ restored, removed: removedAdded } = overlayPristine(
|
|
9019
|
+
cwd,
|
|
9020
|
+
base,
|
|
9021
|
+
priDir,
|
|
9022
|
+
policy,
|
|
9023
|
+
cmd,
|
|
9024
|
+
frozenNodeModules
|
|
9025
|
+
));
|
|
8689
9026
|
} catch (e) {
|
|
8690
9027
|
cleanup([visRoot, priRoot]);
|
|
8691
9028
|
out2(`verify: could not materialize (${e instanceof Error ? e.message : String(e)}) \u2014 failing closed`);
|
|
@@ -8695,11 +9032,12 @@ function runVerify(opts) {
|
|
|
8695
9032
|
const pristine = runSuite(priDir, cmd, budget);
|
|
8696
9033
|
const overlayMoved = overlayDigest(priDir, restored) !== overlayBefore;
|
|
8697
9034
|
const treeMoved = treeFingerprint(cwd, protectedIgnored) !== treeBefore;
|
|
8698
|
-
const
|
|
9035
|
+
const dependencyAfterPristine = checkDependencyEnvironment(cwd, dependencyEnvironment);
|
|
9036
|
+
const depsMoved = !dependencyAfterPristine.ok && !opts.allowDepDrift;
|
|
8699
9037
|
cleanup([visRoot, priRoot]);
|
|
8700
9038
|
if (overlayMoved || treeMoved || depsMoved) {
|
|
8701
9039
|
out2(
|
|
8702
|
-
"verify: " + (overlayMoved ? "a restored file in the pristine copy changed while the pristine suite was running" : treeMoved ? "the working tree changed while the pristine suite was running" : "the
|
|
9040
|
+
"verify: " + (overlayMoved ? "a restored file in the pristine copy changed while the pristine suite was running" : treeMoved ? "the working tree changed while the pristine suite was running" : "the frozen dependency environment changed while the pristine suite was running") + " \u2014"
|
|
8703
9041
|
);
|
|
8704
9042
|
out2("the verdict would describe something other than what ran. Failing closed, not open.");
|
|
8705
9043
|
out2("(A suite that rewrites its own snapshots or test files in place will trip this; run it in");
|
|
@@ -8734,6 +9072,7 @@ function runVerify(opts) {
|
|
|
8734
9072
|
pristine: { exit: pristine.exit, secs: pristine.secs },
|
|
8735
9073
|
protected_restored: restored.length,
|
|
8736
9074
|
added_protected_removed: removedAdded,
|
|
9075
|
+
dependency_environment: dependencyReport(),
|
|
8737
9076
|
...signedOff ? { oob_signoff: signedOff } : {},
|
|
8738
9077
|
...opts.keep ? { visible_dir: visDir, pristine_dir: priDir } : {}
|
|
8739
9078
|
})
|
|
@@ -8746,6 +9085,7 @@ function runVerify(opts) {
|
|
|
8746
9085
|
BUDGET_EXCEEDED: `budget exceeded (${budget}s): could not verify \u2014 failing closed, not open.`
|
|
8747
9086
|
};
|
|
8748
9087
|
out2(`tamperward verify \u2014 ${lines[verdict2]}`);
|
|
9088
|
+
out2(`dependency environment: ${dependencyEnvironmentSummary(dependencyEnvironment)}` + (opts.allowDepDrift && dependencyEnvironment.status === "unattestable" ? " (operator override)" : ""));
|
|
8749
9089
|
if (signedOff)
|
|
8750
9090
|
out2(
|
|
8751
9091
|
`masked failure cleared by out-of-band approval (tamperward:allow:${signedOff}): a reviewer accepted that the original suite no longer applies to this change. Exit 0.`
|
|
@@ -8791,8 +9131,8 @@ function parseVerify(args) {
|
|
|
8791
9131
|
|
|
8792
9132
|
// src/cli/run.ts
|
|
8793
9133
|
import { execFileSync as execFileSync9, spawnSync as spawnSync2 } from "node:child_process";
|
|
8794
|
-
import { readdirSync as readdirSync4, readFileSync as readFileSync15, readlinkSync as readlinkSync4, realpathSync as
|
|
8795
|
-
import { resolve as
|
|
9134
|
+
import { readdirSync as readdirSync4, readFileSync as readFileSync15, readlinkSync as readlinkSync4, realpathSync as realpathSync5 } from "node:fs";
|
|
9135
|
+
import { resolve as resolve6 } from "node:path";
|
|
8796
9136
|
var out = (s) => process.stdout.write(s + "\n");
|
|
8797
9137
|
var err = (s) => process.stderr.write(s + "\n");
|
|
8798
9138
|
function git3(args, cwd) {
|
|
@@ -8809,7 +9149,7 @@ function survivorsHoldingTree(cwd, spawnedAfterTicks) {
|
|
|
8809
9149
|
const out2 = [];
|
|
8810
9150
|
let real;
|
|
8811
9151
|
try {
|
|
8812
|
-
real =
|
|
9152
|
+
real = realpathSync5(cwd);
|
|
8813
9153
|
} catch {
|
|
8814
9154
|
return out2;
|
|
8815
9155
|
}
|
|
@@ -8838,7 +9178,7 @@ function survivorsHoldingTree(cwd, spawnedAfterTicks) {
|
|
|
8838
9178
|
for (const link of procLinks) {
|
|
8839
9179
|
try {
|
|
8840
9180
|
const raw = readlinkSync4(link).replace(/ \(deleted\)$/, "");
|
|
8841
|
-
const held = raw.startsWith("/") ?
|
|
9181
|
+
const held = raw.startsWith("/") ? resolve6(raw) : raw;
|
|
8842
9182
|
if (held === real || held.startsWith(real + "/")) {
|
|
8843
9183
|
holds = true;
|
|
8844
9184
|
break;
|
|
@@ -8851,7 +9191,7 @@ function survivorsHoldingTree(cwd, spawnedAfterTicks) {
|
|
|
8851
9191
|
return out2;
|
|
8852
9192
|
}
|
|
8853
9193
|
function runEnvelope(opts) {
|
|
8854
|
-
const cwd =
|
|
9194
|
+
const cwd = resolve6(opts.cwd ?? process.cwd());
|
|
8855
9195
|
if (!opts.argv.length) {
|
|
8856
9196
|
err("tamperward run: no agent command given (usage: tamperward run [options] -- <command...>)");
|
|
8857
9197
|
return 2;
|
|
@@ -8887,8 +9227,15 @@ function runEnvelope(opts) {
|
|
|
8887
9227
|
return 2;
|
|
8888
9228
|
}
|
|
8889
9229
|
}
|
|
9230
|
+
const dependencyEnvironment = discoverDependencyEnvironment(cwd, frozenCmd);
|
|
8890
9231
|
out(`tamperward run \u2014 trusted base ${base.slice(0, 10)}; agent exit is untrusted.`);
|
|
8891
|
-
|
|
9232
|
+
out(`tamperward run \u2014 dependency environment: ${dependencyEnvironmentSummary(dependencyEnvironment)}`);
|
|
9233
|
+
if (dependencyEnvironment.status === "unattestable" && !opts.allowDepDrift) {
|
|
9234
|
+
err("tamperward run: the verifier dependency environment cannot be attested \u2014");
|
|
9235
|
+
err(`${dependencyEnvironment.reason ?? "unknown dependency environment"}. Failing closed before the agent starts.`);
|
|
9236
|
+
err("(Pass --allow-dep-drift only if you explicitly own this dependency-integrity risk.)");
|
|
9237
|
+
return 2;
|
|
9238
|
+
}
|
|
8892
9239
|
const rewritesBefore = objectRewriteState(cwd);
|
|
8893
9240
|
const spawnTicks = nowTicks();
|
|
8894
9241
|
const agent = spawnSync2(opts.argv[0], opts.argv.slice(1), { cwd, stdio: "inherit" });
|
|
@@ -8901,9 +9248,10 @@ function runEnvelope(opts) {
|
|
|
8901
9248
|
tamperward run \u2014 agent exit ${agentExit}; OBJECT_REWRITE \u2192 ENFORCEMENT_FAILED`);
|
|
8902
9249
|
return 1;
|
|
8903
9250
|
}
|
|
8904
|
-
|
|
8905
|
-
|
|
8906
|
-
err("
|
|
9251
|
+
const dependencyBeforeAdjudication = checkDependencyEnvironment(cwd, dependencyEnvironment);
|
|
9252
|
+
if (!dependencyBeforeAdjudication.ok) {
|
|
9253
|
+
err("tamperward run: the frozen dependency environment changed before adjudication began \u2014");
|
|
9254
|
+
err(`${dependencyBeforeAdjudication.reason ?? "dependency identity changed"}.`);
|
|
8907
9255
|
if (!opts.allowDepDrift) {
|
|
8908
9256
|
out(`
|
|
8909
9257
|
tamperward run \u2014 agent exit ${agentExit}; DEPENDENCY_DRIFT \u2192 ENFORCEMENT_FAILED`);
|
|
@@ -8931,7 +9279,15 @@ tamperward run \u2014 agent exit ${agentExit}; HISTORY_REWRITE \u2192 ENFORCEMEN
|
|
|
8931
9279
|
out("\n[envelope] policy check over the working tree (frozen policy, untracked included, no local ledger):");
|
|
8932
9280
|
const workCode = runCheck({ worktree: true, cwd, policyOverride: frozenPolicy, includeUntracked: true, ciLayer: true });
|
|
8933
9281
|
out("\n[envelope] pristine verification against the trusted base:");
|
|
8934
|
-
const verifyCode = runVerify({
|
|
9282
|
+
const verifyCode = runVerify({
|
|
9283
|
+
cwd,
|
|
9284
|
+
base,
|
|
9285
|
+
cmd: frozenCmd,
|
|
9286
|
+
budget: frozenBudget,
|
|
9287
|
+
policyOverride: frozenPolicy,
|
|
9288
|
+
dependencyEnvironment,
|
|
9289
|
+
allowDepDrift: opts.allowDepDrift
|
|
9290
|
+
});
|
|
8935
9291
|
if (opts.settle && opts.settle > 0) {
|
|
8936
9292
|
try {
|
|
8937
9293
|
execFileSync9("sleep", [String(opts.settle)]);
|
|
@@ -8943,10 +9299,11 @@ tamperward run \u2014 agent exit ${agentExit}; HISTORY_REWRITE \u2192 ENFORCEMEN
|
|
|
8943
9299
|
err("tamperward run: the working tree changed while it was being adjudicated \u2014");
|
|
8944
9300
|
err("the verdict below would describe a tree that no longer exists.");
|
|
8945
9301
|
}
|
|
8946
|
-
const
|
|
9302
|
+
const dependencyAfter = checkDependencyEnvironment(cwd, dependencyEnvironment);
|
|
9303
|
+
const depsDrifted = !opts.allowDepDrift && !dependencyAfter.ok;
|
|
8947
9304
|
if (depsDrifted) {
|
|
8948
|
-
err("tamperward run: the
|
|
8949
|
-
err(
|
|
9305
|
+
err("tamperward run: the frozen dependency environment changed during this run \u2014");
|
|
9306
|
+
err(`${dependencyAfter.reason ?? "dependency identity changed"}. Both verification runs depend on it.`);
|
|
8950
9307
|
}
|
|
8951
9308
|
const rewrote = objectRewriteState(cwd) !== rewritesBefore;
|
|
8952
9309
|
if (rewrote) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tamperward",
|
|
3
|
-
"version": "2.10.
|
|
3
|
+
"version": "2.10.9",
|
|
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",
|