tamperward 2.10.7 → 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 +452 -92
- 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
|
}
|
|
@@ -7663,7 +7663,7 @@ jobs:
|
|
|
7663
7663
|
runs-on: ubuntu-latest
|
|
7664
7664
|
timeout-minutes: 10
|
|
7665
7665
|
steps:
|
|
7666
|
-
- uses: actions/setup-node@v6
|
|
7666
|
+
- uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6
|
|
7667
7667
|
with:
|
|
7668
7668
|
node-version: 22
|
|
7669
7669
|
# Install the authority before candidate files exist in the job workspace.
|
|
@@ -7673,9 +7673,12 @@ jobs:
|
|
|
7673
7673
|
- name: Install Tamperward authority
|
|
7674
7674
|
working-directory: \${{ runner.temp }}
|
|
7675
7675
|
run: npm install --global --registry=https://registry.npmjs.org/ --node-options=' ' --script-shell= --ignore-scripts --offline=false --prefer-online tamperward@${TW_VERSION}
|
|
7676
|
-
- uses: actions/checkout@v5
|
|
7676
|
+
- uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5
|
|
7677
7677
|
with:
|
|
7678
7678
|
fetch-depth: 0 # the range diff needs both endpoints
|
|
7679
|
+
# Candidate code runs later in this job. Do not leave the checkout
|
|
7680
|
+
# token in .git/config where a verifier command can read/reuse it.
|
|
7681
|
+
persist-credentials: false
|
|
7679
7682
|
- name: Resolve out-of-band sign-off from PR labels
|
|
7680
7683
|
id: oob
|
|
7681
7684
|
env:
|
|
@@ -8160,16 +8163,14 @@ tamperward init: ${changed} change(s) applied. Commit them so the gate travels w
|
|
|
8160
8163
|
|
|
8161
8164
|
// src/cli/verify.ts
|
|
8162
8165
|
import { execFileSync as execFileSync8, spawnSync } from "node:child_process";
|
|
8163
|
-
import { createHash as
|
|
8164
|
-
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";
|
|
8165
8168
|
import { tmpdir as tmpdir2 } from "node:os";
|
|
8166
|
-
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";
|
|
8167
8170
|
|
|
8168
8171
|
// src/fingerprint.ts
|
|
8169
8172
|
import { execFileSync as execFileSync7 } from "node:child_process";
|
|
8170
8173
|
import { createHash as createHash7 } from "node:crypto";
|
|
8171
|
-
import { readdirSync as readdirSync3, readFileSync as readFileSync13, readlinkSync as readlinkSync2, statSync as statSync4 } from "node:fs";
|
|
8172
|
-
import { join as join13 } from "node:path";
|
|
8173
8174
|
function treeFingerprint(cwd, keepIgnored) {
|
|
8174
8175
|
const listed = execFileSync7("git", ["ls-files", "-z", "--cached", "--others", "--exclude-standard"], {
|
|
8175
8176
|
cwd,
|
|
@@ -8192,45 +8193,359 @@ function treeFingerprint(cwd, keepIgnored) {
|
|
|
8192
8193
|
}
|
|
8193
8194
|
return h.digest("hex");
|
|
8194
8195
|
}
|
|
8195
|
-
|
|
8196
|
-
|
|
8197
|
-
|
|
8198
|
-
|
|
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);
|
|
8199
8272
|
let entries;
|
|
8200
8273
|
try {
|
|
8201
8274
|
entries = readdirSync3(dir, { withFileTypes: true });
|
|
8202
|
-
} catch {
|
|
8203
|
-
|
|
8275
|
+
} catch (e) {
|
|
8276
|
+
throw new Unattestable(
|
|
8277
|
+
"dependency directory " + JSON.stringify(dir) + " cannot be read (" + (e instanceof Error ? e.message : String(e)) + ")"
|
|
8278
|
+
);
|
|
8204
8279
|
}
|
|
8205
|
-
for (const
|
|
8206
|
-
const
|
|
8207
|
-
|
|
8208
|
-
|
|
8209
|
-
|
|
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") {
|
|
8210
8293
|
try {
|
|
8211
|
-
h.update(readFileSync13(
|
|
8212
|
-
h.update(
|
|
8213
|
-
} catch {
|
|
8214
|
-
|
|
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
|
+
);
|
|
8215
8315
|
}
|
|
8216
|
-
|
|
8217
|
-
|
|
8316
|
+
h.update(target + "\0");
|
|
8317
|
+
let resolved;
|
|
8218
8318
|
try {
|
|
8219
|
-
|
|
8319
|
+
resolved = realpathSync3(path);
|
|
8220
8320
|
} catch {
|
|
8221
|
-
|
|
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
|
+
);
|
|
8222
8341
|
}
|
|
8342
|
+
throw new Unattestable("dependency symlink " + JSON.stringify(path) + " resolves to a special file");
|
|
8223
8343
|
}
|
|
8344
|
+
throw new Unattestable("dependency entry " + JSON.stringify(path) + " is a special file");
|
|
8224
8345
|
}
|
|
8225
8346
|
};
|
|
8226
|
-
|
|
8227
|
-
|
|
8228
|
-
|
|
8229
|
-
|
|
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
|
+
);
|
|
8230
8355
|
}
|
|
8231
|
-
|
|
8356
|
+
for (const root of roots) fingerprintRoot(root, cwd, h);
|
|
8232
8357
|
return h.digest("hex");
|
|
8233
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
|
+
}
|
|
8234
8549
|
|
|
8235
8550
|
// src/cli/verify.ts
|
|
8236
8551
|
var OVERLAY_CLASSES = ["tests", "snapshots", "config"];
|
|
@@ -8320,25 +8635,25 @@ function baseIsAncestorOfHead(base, cwd) {
|
|
|
8320
8635
|
const r = spawnSync("git", ["merge-base", "--is-ancestor", base, "HEAD"], { cwd, env: trustedGitEnv() });
|
|
8321
8636
|
return r.status === 0;
|
|
8322
8637
|
}
|
|
8323
|
-
function
|
|
8324
|
-
const rel =
|
|
8325
|
-
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);
|
|
8326
8641
|
}
|
|
8327
8642
|
function linkParts(path) {
|
|
8328
|
-
const parts =
|
|
8643
|
+
const parts = sep2 === "\\" ? path.split(/[\\/]+/) : path.split("/");
|
|
8329
8644
|
return parts.filter((part) => part !== "" && part !== ".");
|
|
8330
8645
|
}
|
|
8331
8646
|
function linkEscape(label, target) {
|
|
8332
8647
|
return new Error(`${label} is a symlink that escapes the materialised tree (${JSON.stringify(target)})`);
|
|
8333
8648
|
}
|
|
8334
8649
|
function driveRelativeLinkTarget(target) {
|
|
8335
|
-
return
|
|
8650
|
+
return sep2 === "\\" && /^[A-Za-z]:(?![\\/])/.test(target);
|
|
8336
8651
|
}
|
|
8337
8652
|
function rootedLinkTarget(target) {
|
|
8338
|
-
return
|
|
8653
|
+
return isAbsolute10(target) || driveRelativeLinkTarget(target);
|
|
8339
8654
|
}
|
|
8340
8655
|
function safeSymlink(target, out2, root, label) {
|
|
8341
|
-
if (rootedLinkTarget(target) || !
|
|
8656
|
+
if (rootedLinkTarget(target) || !inside2(root, resolve5(dirname6(out2), target))) {
|
|
8342
8657
|
throw linkEscape(label, target);
|
|
8343
8658
|
}
|
|
8344
8659
|
rmSync3(out2, { force: true });
|
|
@@ -8346,7 +8661,7 @@ function safeSymlink(target, out2, root, label) {
|
|
|
8346
8661
|
}
|
|
8347
8662
|
function validateSymlinkGraph(target, out2, root, dependencyRoot, label) {
|
|
8348
8663
|
let domain = "tree";
|
|
8349
|
-
let stack = linkParts(
|
|
8664
|
+
let stack = linkParts(relative5(root, dirname6(out2)));
|
|
8350
8665
|
const pending = linkParts(target);
|
|
8351
8666
|
const seen = /* @__PURE__ */ new Set();
|
|
8352
8667
|
let hops = 0;
|
|
@@ -8367,7 +8682,7 @@ function validateSymlinkGraph(target, out2, root, dependencyRoot, label) {
|
|
|
8367
8682
|
const current = join14(base, ...stack);
|
|
8368
8683
|
let st;
|
|
8369
8684
|
try {
|
|
8370
|
-
st =
|
|
8685
|
+
st = lstatSync4(current);
|
|
8371
8686
|
} catch {
|
|
8372
8687
|
continue;
|
|
8373
8688
|
}
|
|
@@ -8375,7 +8690,7 @@ function validateSymlinkGraph(target, out2, root, dependencyRoot, label) {
|
|
|
8375
8690
|
if (++hops > 128) {
|
|
8376
8691
|
throw new Error(`${label} contains a symlink cycle while materialising (${JSON.stringify(target)})`);
|
|
8377
8692
|
}
|
|
8378
|
-
const key2 = `${domain}:${current}:${pending.join(
|
|
8693
|
+
const key2 = `${domain}:${current}:${pending.join(sep2)}`;
|
|
8379
8694
|
if (seen.has(key2)) {
|
|
8380
8695
|
throw new Error(`${label} contains a symlink cycle while materialising (${JSON.stringify(target)})`);
|
|
8381
8696
|
}
|
|
@@ -8385,12 +8700,12 @@ function validateSymlinkGraph(target, out2, root, dependencyRoot, label) {
|
|
|
8385
8700
|
if (driveRelativeLinkTarget(next)) {
|
|
8386
8701
|
throw linkEscape(label, target);
|
|
8387
8702
|
}
|
|
8388
|
-
if (
|
|
8389
|
-
if (domain === "tree" || !dependencyRoot || !
|
|
8703
|
+
if (isAbsolute10(next)) {
|
|
8704
|
+
if (domain === "tree" || !dependencyRoot || !inside2(dependencyRoot, next)) {
|
|
8390
8705
|
throw linkEscape(label, target);
|
|
8391
8706
|
}
|
|
8392
8707
|
stack = [];
|
|
8393
|
-
pending.unshift(...linkParts(
|
|
8708
|
+
pending.unshift(...linkParts(relative5(dependencyRoot, resolve5(next))));
|
|
8394
8709
|
} else {
|
|
8395
8710
|
pending.unshift(...linkParts(next));
|
|
8396
8711
|
}
|
|
@@ -8403,16 +8718,16 @@ function rejectLinkedParent(cwd, rel) {
|
|
|
8403
8718
|
current = join14(current, part);
|
|
8404
8719
|
let st;
|
|
8405
8720
|
try {
|
|
8406
|
-
st =
|
|
8721
|
+
st = lstatSync4(current);
|
|
8407
8722
|
} catch {
|
|
8408
8723
|
continue;
|
|
8409
8724
|
}
|
|
8410
8725
|
if (st.isSymbolicLink()) {
|
|
8411
|
-
throw new Error(`${rel} has a symlinked parent directory (${
|
|
8726
|
+
throw new Error(`${rel} has a symlinked parent directory (${relative5(cwd, current)})`);
|
|
8412
8727
|
}
|
|
8413
8728
|
}
|
|
8414
8729
|
}
|
|
8415
|
-
function materialize(cwd, dest) {
|
|
8730
|
+
function materialize(cwd, dest, dependencyRoot) {
|
|
8416
8731
|
const listed = git2(["ls-files", "-z", "--cached", "--others", "--exclude-standard"], cwd).split("\0").filter(Boolean);
|
|
8417
8732
|
const links = [];
|
|
8418
8733
|
for (const rel of listed) {
|
|
@@ -8420,12 +8735,12 @@ function materialize(cwd, dest) {
|
|
|
8420
8735
|
rejectLinkedParent(cwd, rel);
|
|
8421
8736
|
let st;
|
|
8422
8737
|
try {
|
|
8423
|
-
st =
|
|
8738
|
+
st = lstatSync4(src);
|
|
8424
8739
|
} catch {
|
|
8425
8740
|
continue;
|
|
8426
8741
|
}
|
|
8427
8742
|
const out2 = join14(dest, rel);
|
|
8428
|
-
mkdirSync6(
|
|
8743
|
+
mkdirSync6(dirname6(out2), { recursive: true });
|
|
8429
8744
|
dropSymlink(out2);
|
|
8430
8745
|
if (st.isSymbolicLink()) {
|
|
8431
8746
|
const target = readlinkSync3(src);
|
|
@@ -8438,17 +8753,14 @@ function materialize(cwd, dest) {
|
|
|
8438
8753
|
throw new Error(`${rel} is ${st.isDirectory() ? "a directory" : "a special file"} where git expects a file`);
|
|
8439
8754
|
}
|
|
8440
8755
|
}
|
|
8441
|
-
const nm = resolve4(cwd, "node_modules");
|
|
8442
|
-
let dependencyRoot = null;
|
|
8443
|
-
if (existsSync10(nm)) dependencyRoot = realpathSync3(nm);
|
|
8444
8756
|
for (const link of links) {
|
|
8445
8757
|
validateSymlinkGraph(link.target, link.out, dest, dependencyRoot, link.rel);
|
|
8446
8758
|
}
|
|
8447
|
-
if (dependencyRoot) symlinkSync(
|
|
8759
|
+
if (dependencyRoot) symlinkSync(dependencyRoot, join14(dest, "node_modules"), "dir");
|
|
8448
8760
|
}
|
|
8449
8761
|
function dropSymlink(p) {
|
|
8450
8762
|
try {
|
|
8451
|
-
if (
|
|
8763
|
+
if (lstatSync4(p).isSymbolicLink()) rmSync3(p, { force: true });
|
|
8452
8764
|
} catch {
|
|
8453
8765
|
}
|
|
8454
8766
|
}
|
|
@@ -8473,13 +8785,13 @@ function baseEntries(base, cwd) {
|
|
|
8473
8785
|
});
|
|
8474
8786
|
}
|
|
8475
8787
|
function overlayDigest(dest, paths) {
|
|
8476
|
-
const h =
|
|
8788
|
+
const h = createHash9("sha256");
|
|
8477
8789
|
for (const rel of [...paths].sort()) {
|
|
8478
8790
|
h.update(rel);
|
|
8479
8791
|
h.update("\0");
|
|
8480
8792
|
const p = join14(dest, rel);
|
|
8481
8793
|
try {
|
|
8482
|
-
const st =
|
|
8794
|
+
const st = lstatSync4(p);
|
|
8483
8795
|
h.update(String(st.mode));
|
|
8484
8796
|
h.update(st.isSymbolicLink() ? readlinkSync3(p) : readFileSync14(p));
|
|
8485
8797
|
} catch {
|
|
@@ -8489,7 +8801,7 @@ function overlayDigest(dest, paths) {
|
|
|
8489
8801
|
}
|
|
8490
8802
|
return h.digest("hex");
|
|
8491
8803
|
}
|
|
8492
|
-
function overlayPristine(cwd, base, dest, policy, cmd) {
|
|
8804
|
+
function overlayPristine(cwd, base, dest, policy, cmd, dependencyRoot) {
|
|
8493
8805
|
const entries = baseEntries(base, cwd);
|
|
8494
8806
|
const atBase = entries.map((e) => e.path);
|
|
8495
8807
|
const verifierOwned = verifierInputs(cmd, atBase, policy);
|
|
@@ -8506,7 +8818,7 @@ function overlayPristine(cwd, base, dest, policy, cmd) {
|
|
|
8506
8818
|
}
|
|
8507
8819
|
const content = execFileSync8("git", ["show", `${base}:${e.path}`], { cwd, maxBuffer: 1 << 28, env: trustedGitEnv() });
|
|
8508
8820
|
const out2 = join14(dest, e.path);
|
|
8509
|
-
mkdirSync6(
|
|
8821
|
+
mkdirSync6(dirname6(out2), { recursive: true });
|
|
8510
8822
|
rmSync3(out2, { force: true });
|
|
8511
8823
|
if (e.mode === "120000") {
|
|
8512
8824
|
const target = content.toString("utf8");
|
|
@@ -8525,8 +8837,6 @@ function overlayPristine(cwd, base, dest, policy, cmd) {
|
|
|
8525
8837
|
rmSync3(join14(dest, rel), { force: true });
|
|
8526
8838
|
removed++;
|
|
8527
8839
|
}
|
|
8528
|
-
const nm = resolve4(cwd, "node_modules");
|
|
8529
|
-
const dependencyRoot = existsSync10(nm) ? realpathSync3(nm) : null;
|
|
8530
8840
|
for (const link of restoredLinks) {
|
|
8531
8841
|
validateSymlinkGraph(link.target, link.out, dest, dependencyRoot, `the base's ${link.path}`);
|
|
8532
8842
|
}
|
|
@@ -8638,6 +8948,22 @@ function runVerify(opts) {
|
|
|
8638
8948
|
return 2;
|
|
8639
8949
|
}
|
|
8640
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;
|
|
8641
8967
|
let base;
|
|
8642
8968
|
try {
|
|
8643
8969
|
base = resolveBase(opts.base ?? "HEAD", cwd);
|
|
@@ -8653,10 +8979,16 @@ function runVerify(opts) {
|
|
|
8653
8979
|
};
|
|
8654
8980
|
const protectedIgnored = (rel) => isProtected(rel, policy);
|
|
8655
8981
|
const treeBefore = treeFingerprint(cwd, protectedIgnored);
|
|
8656
|
-
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
|
+
}
|
|
8657
8989
|
try {
|
|
8658
8990
|
mkdirSync6(visDir);
|
|
8659
|
-
materialize(cwd, visDir);
|
|
8991
|
+
materialize(cwd, visDir, frozenNodeModules);
|
|
8660
8992
|
} catch (e) {
|
|
8661
8993
|
cleanup([visRoot]);
|
|
8662
8994
|
out2(`verify: could not materialize (${e instanceof Error ? e.message : String(e)}) \u2014 failing closed`);
|
|
@@ -8669,10 +9001,11 @@ function runVerify(opts) {
|
|
|
8669
9001
|
out2("would be materialised from a tree the candidate just edited. Failing closed, not open.");
|
|
8670
9002
|
return 2;
|
|
8671
9003
|
}
|
|
8672
|
-
|
|
9004
|
+
const dependencyAfterVisible = checkDependencyEnvironment(cwd, dependencyEnvironment);
|
|
9005
|
+
if (!dependencyAfterVisible.ok && !opts.allowDepDrift) {
|
|
8673
9006
|
cleanup([visRoot]);
|
|
8674
|
-
out2("verify: the
|
|
8675
|
-
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.");
|
|
8676
9009
|
return 2;
|
|
8677
9010
|
}
|
|
8678
9011
|
const priRoot = mkdtempSync2(join14(tmpdir2(), "tw-verify-pri-"));
|
|
@@ -8681,8 +9014,15 @@ function runVerify(opts) {
|
|
|
8681
9014
|
let removedAdded = 0;
|
|
8682
9015
|
try {
|
|
8683
9016
|
mkdirSync6(priDir);
|
|
8684
|
-
materialize(cwd, priDir);
|
|
8685
|
-
({ 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
|
+
));
|
|
8686
9026
|
} catch (e) {
|
|
8687
9027
|
cleanup([visRoot, priRoot]);
|
|
8688
9028
|
out2(`verify: could not materialize (${e instanceof Error ? e.message : String(e)}) \u2014 failing closed`);
|
|
@@ -8692,11 +9032,12 @@ function runVerify(opts) {
|
|
|
8692
9032
|
const pristine = runSuite(priDir, cmd, budget);
|
|
8693
9033
|
const overlayMoved = overlayDigest(priDir, restored) !== overlayBefore;
|
|
8694
9034
|
const treeMoved = treeFingerprint(cwd, protectedIgnored) !== treeBefore;
|
|
8695
|
-
const
|
|
9035
|
+
const dependencyAfterPristine = checkDependencyEnvironment(cwd, dependencyEnvironment);
|
|
9036
|
+
const depsMoved = !dependencyAfterPristine.ok && !opts.allowDepDrift;
|
|
8696
9037
|
cleanup([visRoot, priRoot]);
|
|
8697
9038
|
if (overlayMoved || treeMoved || depsMoved) {
|
|
8698
9039
|
out2(
|
|
8699
|
-
"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"
|
|
8700
9041
|
);
|
|
8701
9042
|
out2("the verdict would describe something other than what ran. Failing closed, not open.");
|
|
8702
9043
|
out2("(A suite that rewrites its own snapshots or test files in place will trip this; run it in");
|
|
@@ -8731,6 +9072,7 @@ function runVerify(opts) {
|
|
|
8731
9072
|
pristine: { exit: pristine.exit, secs: pristine.secs },
|
|
8732
9073
|
protected_restored: restored.length,
|
|
8733
9074
|
added_protected_removed: removedAdded,
|
|
9075
|
+
dependency_environment: dependencyReport(),
|
|
8734
9076
|
...signedOff ? { oob_signoff: signedOff } : {},
|
|
8735
9077
|
...opts.keep ? { visible_dir: visDir, pristine_dir: priDir } : {}
|
|
8736
9078
|
})
|
|
@@ -8743,6 +9085,7 @@ function runVerify(opts) {
|
|
|
8743
9085
|
BUDGET_EXCEEDED: `budget exceeded (${budget}s): could not verify \u2014 failing closed, not open.`
|
|
8744
9086
|
};
|
|
8745
9087
|
out2(`tamperward verify \u2014 ${lines[verdict2]}`);
|
|
9088
|
+
out2(`dependency environment: ${dependencyEnvironmentSummary(dependencyEnvironment)}` + (opts.allowDepDrift && dependencyEnvironment.status === "unattestable" ? " (operator override)" : ""));
|
|
8746
9089
|
if (signedOff)
|
|
8747
9090
|
out2(
|
|
8748
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.`
|
|
@@ -8788,8 +9131,8 @@ function parseVerify(args) {
|
|
|
8788
9131
|
|
|
8789
9132
|
// src/cli/run.ts
|
|
8790
9133
|
import { execFileSync as execFileSync9, spawnSync as spawnSync2 } from "node:child_process";
|
|
8791
|
-
import { readdirSync as readdirSync4, readFileSync as readFileSync15, readlinkSync as readlinkSync4, realpathSync as
|
|
8792
|
-
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";
|
|
8793
9136
|
var out = (s) => process.stdout.write(s + "\n");
|
|
8794
9137
|
var err = (s) => process.stderr.write(s + "\n");
|
|
8795
9138
|
function git3(args, cwd) {
|
|
@@ -8806,7 +9149,7 @@ function survivorsHoldingTree(cwd, spawnedAfterTicks) {
|
|
|
8806
9149
|
const out2 = [];
|
|
8807
9150
|
let real;
|
|
8808
9151
|
try {
|
|
8809
|
-
real =
|
|
9152
|
+
real = realpathSync5(cwd);
|
|
8810
9153
|
} catch {
|
|
8811
9154
|
return out2;
|
|
8812
9155
|
}
|
|
@@ -8835,7 +9178,7 @@ function survivorsHoldingTree(cwd, spawnedAfterTicks) {
|
|
|
8835
9178
|
for (const link of procLinks) {
|
|
8836
9179
|
try {
|
|
8837
9180
|
const raw = readlinkSync4(link).replace(/ \(deleted\)$/, "");
|
|
8838
|
-
const held = raw.startsWith("/") ?
|
|
9181
|
+
const held = raw.startsWith("/") ? resolve6(raw) : raw;
|
|
8839
9182
|
if (held === real || held.startsWith(real + "/")) {
|
|
8840
9183
|
holds = true;
|
|
8841
9184
|
break;
|
|
@@ -8848,7 +9191,7 @@ function survivorsHoldingTree(cwd, spawnedAfterTicks) {
|
|
|
8848
9191
|
return out2;
|
|
8849
9192
|
}
|
|
8850
9193
|
function runEnvelope(opts) {
|
|
8851
|
-
const cwd =
|
|
9194
|
+
const cwd = resolve6(opts.cwd ?? process.cwd());
|
|
8852
9195
|
if (!opts.argv.length) {
|
|
8853
9196
|
err("tamperward run: no agent command given (usage: tamperward run [options] -- <command...>)");
|
|
8854
9197
|
return 2;
|
|
@@ -8884,8 +9227,15 @@ function runEnvelope(opts) {
|
|
|
8884
9227
|
return 2;
|
|
8885
9228
|
}
|
|
8886
9229
|
}
|
|
9230
|
+
const dependencyEnvironment = discoverDependencyEnvironment(cwd, frozenCmd);
|
|
8887
9231
|
out(`tamperward run \u2014 trusted base ${base.slice(0, 10)}; agent exit is untrusted.`);
|
|
8888
|
-
|
|
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
|
+
}
|
|
8889
9239
|
const rewritesBefore = objectRewriteState(cwd);
|
|
8890
9240
|
const spawnTicks = nowTicks();
|
|
8891
9241
|
const agent = spawnSync2(opts.argv[0], opts.argv.slice(1), { cwd, stdio: "inherit" });
|
|
@@ -8898,9 +9248,10 @@ function runEnvelope(opts) {
|
|
|
8898
9248
|
tamperward run \u2014 agent exit ${agentExit}; OBJECT_REWRITE \u2192 ENFORCEMENT_FAILED`);
|
|
8899
9249
|
return 1;
|
|
8900
9250
|
}
|
|
8901
|
-
|
|
8902
|
-
|
|
8903
|
-
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"}.`);
|
|
8904
9255
|
if (!opts.allowDepDrift) {
|
|
8905
9256
|
out(`
|
|
8906
9257
|
tamperward run \u2014 agent exit ${agentExit}; DEPENDENCY_DRIFT \u2192 ENFORCEMENT_FAILED`);
|
|
@@ -8928,7 +9279,15 @@ tamperward run \u2014 agent exit ${agentExit}; HISTORY_REWRITE \u2192 ENFORCEMEN
|
|
|
8928
9279
|
out("\n[envelope] policy check over the working tree (frozen policy, untracked included, no local ledger):");
|
|
8929
9280
|
const workCode = runCheck({ worktree: true, cwd, policyOverride: frozenPolicy, includeUntracked: true, ciLayer: true });
|
|
8930
9281
|
out("\n[envelope] pristine verification against the trusted base:");
|
|
8931
|
-
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
|
+
});
|
|
8932
9291
|
if (opts.settle && opts.settle > 0) {
|
|
8933
9292
|
try {
|
|
8934
9293
|
execFileSync9("sleep", [String(opts.settle)]);
|
|
@@ -8940,10 +9299,11 @@ tamperward run \u2014 agent exit ${agentExit}; HISTORY_REWRITE \u2192 ENFORCEMEN
|
|
|
8940
9299
|
err("tamperward run: the working tree changed while it was being adjudicated \u2014");
|
|
8941
9300
|
err("the verdict below would describe a tree that no longer exists.");
|
|
8942
9301
|
}
|
|
8943
|
-
const
|
|
9302
|
+
const dependencyAfter = checkDependencyEnvironment(cwd, dependencyEnvironment);
|
|
9303
|
+
const depsDrifted = !opts.allowDepDrift && !dependencyAfter.ok;
|
|
8944
9304
|
if (depsDrifted) {
|
|
8945
|
-
err("tamperward run: the
|
|
8946
|
-
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.`);
|
|
8947
9307
|
}
|
|
8948
9308
|
const rewrote = objectRewriteState(cwd) !== rewritesBefore;
|
|
8949
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",
|