tamperward 2.10.8 → 2.11.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/README.md +28 -5
- package/dist/cli/index.js +997 -118
- 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, resolve8) {
|
|
1655
|
+
const obj = resolve8(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(resolve8(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 resolve8 = 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 = resolve8(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, resolve8);
|
|
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, resolve8);
|
|
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 resolve8 = 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(resolve8(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(resolve8(node.initializer));
|
|
1784
1784
|
res.ignore.push(...items);
|
|
1785
1785
|
if (opaque) res.opaqueIgnore = true;
|
|
1786
1786
|
}
|
|
@@ -3064,6 +3064,18 @@ function policyWeakening(before, after) {
|
|
|
3064
3064
|
`verify.inputs narrowed (removed ${droppedInputs.join(", ")}) \u2014 the pristine run would execute the candidate's copy of those files`
|
|
3065
3065
|
);
|
|
3066
3066
|
}
|
|
3067
|
+
const beforeBackend = bv.backend ?? "local";
|
|
3068
|
+
const afterBackend = av.backend ?? "local";
|
|
3069
|
+
if (beforeBackend === "container" && afterBackend !== "container") {
|
|
3070
|
+
reasons.push(
|
|
3071
|
+
`verify.backend lowered container \u2192 ${afterBackend} \u2014 final verification would leave the isolated trust domain`
|
|
3072
|
+
);
|
|
3073
|
+
}
|
|
3074
|
+
if (beforeBackend === "container" && afterBackend === "container" && av.image !== bv.image) {
|
|
3075
|
+
reasons.push(
|
|
3076
|
+
`verify.image changed (${bv.image ?? "<missing>"} \u2192 ${av.image ?? "<missing>"}) \u2014 the trusted verifier image identity changed`
|
|
3077
|
+
);
|
|
3078
|
+
}
|
|
3067
3079
|
}
|
|
3068
3080
|
}
|
|
3069
3081
|
return reasons;
|
|
@@ -6342,6 +6354,21 @@ function validate(r, where) {
|
|
|
6342
6354
|
bad(`verify.budget must be a positive number of seconds, got ${show(v.budget)}`);
|
|
6343
6355
|
}
|
|
6344
6356
|
if (v.inputs !== void 0 && !isStringList(v.inputs)) bad(`verify.inputs must be a list of globs, got ${show(v.inputs)}`);
|
|
6357
|
+
if (v.backend !== void 0 && v.backend !== "local" && v.backend !== "container") {
|
|
6358
|
+
bad(`verify.backend must be "local" or "container", got ${show(v.backend)}`);
|
|
6359
|
+
}
|
|
6360
|
+
if (v.image !== void 0 && typeof v.image !== "string") {
|
|
6361
|
+
bad(`verify.image must be an immutable container image reference, got ${show(v.image)}`);
|
|
6362
|
+
}
|
|
6363
|
+
const backend = v.backend ?? "local";
|
|
6364
|
+
if (backend === "container") {
|
|
6365
|
+
if (typeof v.image !== "string") bad('verify.image is required when verify.backend is "container"');
|
|
6366
|
+
if (!/^(?!-)[^\s@]+@sha256:[0-9a-f]{64}$/i.test(v.image)) {
|
|
6367
|
+
bad(`verify.image must be pinned by sha256 digest (name@sha256:<64 hex>), got ${show(v.image)}`);
|
|
6368
|
+
}
|
|
6369
|
+
} else if (v.image !== void 0) {
|
|
6370
|
+
bad('verify.image is only valid when verify.backend is "container"');
|
|
6371
|
+
}
|
|
6345
6372
|
}
|
|
6346
6373
|
}
|
|
6347
6374
|
function parsePolicy(raw, where = POLICY_FILE) {
|
|
@@ -6374,7 +6401,9 @@ function parsePolicy(raw, where = POLICY_FILE) {
|
|
|
6374
6401
|
// Rebuilding the block field-by-field means a field added later and
|
|
6375
6402
|
// not listed here is SILENTLY DROPPED — which is how `inputs` first
|
|
6376
6403
|
// shipped as a no-op that parsed, validated, and restored nothing.
|
|
6377
|
-
...Array.isArray(r.verify.inputs) ? { inputs: r.verify.inputs.map(String) } : {}
|
|
6404
|
+
...Array.isArray(r.verify.inputs) ? { inputs: r.verify.inputs.map(String) } : {},
|
|
6405
|
+
...r.verify.backend ? { backend: r.verify.backend } : {},
|
|
6406
|
+
...r.verify.image ? { image: r.verify.image } : {}
|
|
6378
6407
|
}
|
|
6379
6408
|
} : {}
|
|
6380
6409
|
};
|
|
@@ -8162,17 +8191,15 @@ tamperward init: ${changed} change(s) applied. Commit them so the gate travels w
|
|
|
8162
8191
|
}
|
|
8163
8192
|
|
|
8164
8193
|
// src/cli/verify.ts
|
|
8165
|
-
import { execFileSync as execFileSync8, spawnSync } from "node:child_process";
|
|
8166
|
-
import { createHash as
|
|
8167
|
-
import { chmodSync as chmodSync2, cpSync, lstatSync as
|
|
8194
|
+
import { execFileSync as execFileSync8, spawnSync as spawnSync2 } from "node:child_process";
|
|
8195
|
+
import { createHash as createHash10 } from "node:crypto";
|
|
8196
|
+
import { chmodSync as chmodSync2, cpSync, lstatSync as lstatSync4, mkdirSync as mkdirSync6, mkdtempSync as mkdtempSync2, readFileSync as readFileSync15, readlinkSync as readlinkSync3, rmSync as rmSync3, symlinkSync, writeFileSync as writeFileSync6 } from "node:fs";
|
|
8168
8197
|
import { tmpdir as tmpdir2 } from "node:os";
|
|
8169
|
-
import { dirname as
|
|
8198
|
+
import { dirname as dirname7, isAbsolute as isAbsolute10, join as join15, relative as relative5, resolve as resolve6, sep as sep2 } from "node:path";
|
|
8170
8199
|
|
|
8171
8200
|
// src/fingerprint.ts
|
|
8172
8201
|
import { execFileSync as execFileSync7 } from "node:child_process";
|
|
8173
8202
|
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
8203
|
function treeFingerprint(cwd, keepIgnored) {
|
|
8177
8204
|
const listed = execFileSync7("git", ["ls-files", "-z", "--cached", "--others", "--exclude-standard"], {
|
|
8178
8205
|
cwd,
|
|
@@ -8195,44 +8222,772 @@ function treeFingerprint(cwd, keepIgnored) {
|
|
|
8195
8222
|
}
|
|
8196
8223
|
return h.digest("hex");
|
|
8197
8224
|
}
|
|
8198
|
-
|
|
8199
|
-
|
|
8200
|
-
|
|
8201
|
-
|
|
8225
|
+
|
|
8226
|
+
// src/dependency-env.ts
|
|
8227
|
+
import { createHash as createHash8 } from "node:crypto";
|
|
8228
|
+
import {
|
|
8229
|
+
existsSync as existsSync10,
|
|
8230
|
+
lstatSync as lstatSync3,
|
|
8231
|
+
readdirSync as readdirSync3,
|
|
8232
|
+
readFileSync as readFileSync13,
|
|
8233
|
+
readlinkSync as readlinkSync2,
|
|
8234
|
+
realpathSync as realpathSync3,
|
|
8235
|
+
statSync as statSync4
|
|
8236
|
+
} from "node:fs";
|
|
8237
|
+
import {
|
|
8238
|
+
basename as basename3,
|
|
8239
|
+
delimiter,
|
|
8240
|
+
dirname as dirname5,
|
|
8241
|
+
isAbsolute as isAbsolute9,
|
|
8242
|
+
join as join13,
|
|
8243
|
+
relative as relative4,
|
|
8244
|
+
resolve as resolve4,
|
|
8245
|
+
sep
|
|
8246
|
+
} from "node:path";
|
|
8247
|
+
var Unattestable = class extends Error {
|
|
8248
|
+
};
|
|
8249
|
+
function inside(root, candidate) {
|
|
8250
|
+
const rel = relative4(resolve4(root), resolve4(candidate));
|
|
8251
|
+
return rel === "" || rel !== ".." && !rel.startsWith(".." + sep) && !isAbsolute9(rel);
|
|
8252
|
+
}
|
|
8253
|
+
function modeKind(path) {
|
|
8254
|
+
const st = lstatSync3(path);
|
|
8255
|
+
if (st.isFile()) return { kind: "file", mode: st.mode };
|
|
8256
|
+
if (st.isDirectory()) return { kind: "dir", mode: st.mode };
|
|
8257
|
+
if (st.isSymbolicLink()) return { kind: "symlink", mode: st.mode };
|
|
8258
|
+
return { kind: "special", mode: st.mode };
|
|
8259
|
+
}
|
|
8260
|
+
function fingerprintRoot(root, cwd, h) {
|
|
8261
|
+
let rootStat;
|
|
8262
|
+
let currentReal;
|
|
8263
|
+
try {
|
|
8264
|
+
rootStat = lstatSync3(root.path);
|
|
8265
|
+
currentReal = realpathSync3(root.path);
|
|
8266
|
+
} catch (e) {
|
|
8267
|
+
throw new Unattestable(
|
|
8268
|
+
root.kind + " root " + JSON.stringify(root.path) + " can no longer be resolved (" + (e instanceof Error ? e.message : String(e)) + ")"
|
|
8269
|
+
);
|
|
8270
|
+
}
|
|
8271
|
+
if (currentReal !== root.realPath) {
|
|
8272
|
+
throw new Unattestable(
|
|
8273
|
+
root.kind + " root moved from " + JSON.stringify(root.realPath) + " to " + JSON.stringify(currentReal)
|
|
8274
|
+
);
|
|
8275
|
+
}
|
|
8276
|
+
if (!statSync4(root.path).isDirectory()) {
|
|
8277
|
+
throw new Unattestable(root.kind + " root " + JSON.stringify(root.path) + " is not a directory");
|
|
8278
|
+
}
|
|
8279
|
+
h.update("root\0" + root.kind + "\0" + root.path + "\0" + root.realPath + "\0" + String(rootStat.mode) + "\0");
|
|
8280
|
+
if (rootStat.isSymbolicLink()) h.update(readlinkSync2(root.path) + "\0");
|
|
8281
|
+
const cwdReal = realpathSync3(cwd);
|
|
8282
|
+
const visited = /* @__PURE__ */ new Set();
|
|
8283
|
+
const hashFile = (path, label) => {
|
|
8284
|
+
let st;
|
|
8285
|
+
try {
|
|
8286
|
+
st = statSync4(path);
|
|
8287
|
+
if (!st.isFile()) throw new Error("not a regular file");
|
|
8288
|
+
h.update(label + "\0external-file\0" + path + "\0" + String(st.mode) + "\0");
|
|
8289
|
+
h.update(readFileSync13(path));
|
|
8290
|
+
h.update("\0");
|
|
8291
|
+
} catch (e) {
|
|
8292
|
+
throw new Unattestable(
|
|
8293
|
+
"dependency link target " + JSON.stringify(path) + " is not attestable (" + (e instanceof Error ? e.message : String(e)) + ")"
|
|
8294
|
+
);
|
|
8295
|
+
}
|
|
8296
|
+
};
|
|
8297
|
+
const walk2 = (dir, namespace) => {
|
|
8298
|
+
const realDir = realpathSync3(dir);
|
|
8299
|
+
if (visited.has(realDir)) return;
|
|
8300
|
+
visited.add(realDir);
|
|
8202
8301
|
let entries;
|
|
8203
8302
|
try {
|
|
8204
8303
|
entries = readdirSync3(dir, { withFileTypes: true });
|
|
8205
|
-
} catch {
|
|
8206
|
-
|
|
8304
|
+
} catch (e) {
|
|
8305
|
+
throw new Unattestable(
|
|
8306
|
+
"dependency directory " + JSON.stringify(dir) + " cannot be read (" + (e instanceof Error ? e.message : String(e)) + ")"
|
|
8307
|
+
);
|
|
8207
8308
|
}
|
|
8208
|
-
for (const
|
|
8209
|
-
const
|
|
8210
|
-
|
|
8211
|
-
|
|
8212
|
-
|
|
8309
|
+
for (const entry of [...entries].sort((a, b) => a.name.localeCompare(b.name))) {
|
|
8310
|
+
const path = join13(dir, entry.name);
|
|
8311
|
+
const rel = namespace ? namespace + "/" + entry.name : entry.name;
|
|
8312
|
+
let ident;
|
|
8313
|
+
try {
|
|
8314
|
+
ident = modeKind(path);
|
|
8315
|
+
} catch (e) {
|
|
8316
|
+
throw new Unattestable(
|
|
8317
|
+
"dependency entry " + JSON.stringify(path) + " disappeared while attesting (" + (e instanceof Error ? e.message : String(e)) + ")"
|
|
8318
|
+
);
|
|
8319
|
+
}
|
|
8320
|
+
h.update(rel + "\0" + ident.kind + "\0" + String(ident.mode) + "\0");
|
|
8321
|
+
if (ident.kind === "file") {
|
|
8213
8322
|
try {
|
|
8214
|
-
h.update(readFileSync13(
|
|
8215
|
-
h.update(
|
|
8216
|
-
} catch {
|
|
8217
|
-
|
|
8323
|
+
h.update(readFileSync13(path));
|
|
8324
|
+
h.update("\0");
|
|
8325
|
+
} catch (e) {
|
|
8326
|
+
throw new Unattestable(
|
|
8327
|
+
"dependency file " + JSON.stringify(path) + " cannot be read (" + (e instanceof Error ? e.message : String(e)) + ")"
|
|
8328
|
+
);
|
|
8329
|
+
}
|
|
8330
|
+
continue;
|
|
8331
|
+
}
|
|
8332
|
+
if (ident.kind === "dir") {
|
|
8333
|
+
walk2(path, rel);
|
|
8334
|
+
continue;
|
|
8335
|
+
}
|
|
8336
|
+
if (ident.kind === "symlink") {
|
|
8337
|
+
let target;
|
|
8338
|
+
try {
|
|
8339
|
+
target = readlinkSync2(path);
|
|
8340
|
+
} catch (e) {
|
|
8341
|
+
throw new Unattestable(
|
|
8342
|
+
"dependency symlink " + JSON.stringify(path) + " cannot be read (" + (e instanceof Error ? e.message : String(e)) + ")"
|
|
8343
|
+
);
|
|
8218
8344
|
}
|
|
8219
|
-
|
|
8220
|
-
|
|
8345
|
+
h.update(target + "\0");
|
|
8346
|
+
let resolved;
|
|
8221
8347
|
try {
|
|
8222
|
-
|
|
8348
|
+
resolved = realpathSync3(path);
|
|
8223
8349
|
} catch {
|
|
8224
|
-
|
|
8350
|
+
continue;
|
|
8225
8351
|
}
|
|
8352
|
+
const followed = statSync4(path);
|
|
8353
|
+
if (followed.isFile()) {
|
|
8354
|
+
hashFile(resolved, "@target:" + rel);
|
|
8355
|
+
continue;
|
|
8356
|
+
}
|
|
8357
|
+
if (followed.isDirectory()) {
|
|
8358
|
+
if (inside(root.realPath, resolved)) {
|
|
8359
|
+
walk2(resolved, "@target:" + rel);
|
|
8360
|
+
continue;
|
|
8361
|
+
}
|
|
8362
|
+
if (inside(cwdReal, resolved)) {
|
|
8363
|
+
throw new Unattestable(
|
|
8364
|
+
"dependency workspace symlink " + JSON.stringify(path) + " points into the candidate worktree at " + JSON.stringify(resolved) + "; faithful per-copy remapping is not available"
|
|
8365
|
+
);
|
|
8366
|
+
}
|
|
8367
|
+
throw new Unattestable(
|
|
8368
|
+
"dependency directory symlink " + JSON.stringify(path) + " escapes the bounded root to " + JSON.stringify(resolved)
|
|
8369
|
+
);
|
|
8370
|
+
}
|
|
8371
|
+
throw new Unattestable("dependency symlink " + JSON.stringify(path) + " resolves to a special file");
|
|
8226
8372
|
}
|
|
8373
|
+
throw new Unattestable("dependency entry " + JSON.stringify(path) + " is a special file");
|
|
8227
8374
|
}
|
|
8228
8375
|
};
|
|
8376
|
+
walk2(root.realPath, "");
|
|
8377
|
+
}
|
|
8378
|
+
function snapshot(roots, probes, cwd) {
|
|
8379
|
+
const h = createHash8("sha256");
|
|
8380
|
+
for (const probe of probes) {
|
|
8381
|
+
h.update(
|
|
8382
|
+
"probe\0" + probe.kind + "\0" + probe.path + "\0" + (existsSync10(probe.path) ? "present" : "absent") + "\0"
|
|
8383
|
+
);
|
|
8384
|
+
}
|
|
8385
|
+
for (const root of roots) fingerprintRoot(root, cwd, h);
|
|
8386
|
+
return h.digest("hex");
|
|
8387
|
+
}
|
|
8388
|
+
function freezeRoot(kind, path) {
|
|
8229
8389
|
try {
|
|
8230
|
-
if (!statSync4(
|
|
8390
|
+
if (!statSync4(path).isDirectory()) {
|
|
8391
|
+
throw new Error("not a directory");
|
|
8392
|
+
}
|
|
8393
|
+
return { kind, path: resolve4(path), realPath: realpathSync3(path) };
|
|
8394
|
+
} catch (e) {
|
|
8395
|
+
throw new Unattestable(
|
|
8396
|
+
kind + " root " + JSON.stringify(path) + " is not resolvable (" + (e instanceof Error ? e.message : String(e)) + ")"
|
|
8397
|
+
);
|
|
8398
|
+
}
|
|
8399
|
+
}
|
|
8400
|
+
function shellTokens(command) {
|
|
8401
|
+
const matches = command.match(/(?:[^\s"'\\]+|"(?:\\.|[^"])*"|'[^']*')+/g) ?? [];
|
|
8402
|
+
return matches.map((token) => {
|
|
8403
|
+
if (token.startsWith("'") && token.endsWith("'") || token.startsWith('"') && token.endsWith('"')) return token.slice(1, -1);
|
|
8404
|
+
return token;
|
|
8405
|
+
});
|
|
8406
|
+
}
|
|
8407
|
+
function firstExecutable(command) {
|
|
8408
|
+
const tokens2 = shellTokens(command);
|
|
8409
|
+
let i = 0;
|
|
8410
|
+
while (i < tokens2.length && /^[A-Za-z_][A-Za-z0-9_]*=/.test(tokens2[i])) i++;
|
|
8411
|
+
if (tokens2[i] === "env") {
|
|
8412
|
+
i++;
|
|
8413
|
+
while (i < tokens2.length && (tokens2[i].startsWith("-") || /^[A-Za-z_][A-Za-z0-9_]*=/.test(tokens2[i]))) i++;
|
|
8414
|
+
}
|
|
8415
|
+
if (tokens2[i] === "command") {
|
|
8416
|
+
i++;
|
|
8417
|
+
while (i < tokens2.length && tokens2[i].startsWith("-")) i++;
|
|
8418
|
+
}
|
|
8419
|
+
if (tokens2[i] === "timeout") {
|
|
8420
|
+
i++;
|
|
8421
|
+
while (i < tokens2.length && tokens2[i].startsWith("-")) i++;
|
|
8422
|
+
if (i < tokens2.length) i++;
|
|
8423
|
+
}
|
|
8424
|
+
return tokens2[i] ?? null;
|
|
8425
|
+
}
|
|
8426
|
+
function resolveExecutable(token, cwd, env) {
|
|
8427
|
+
if (!token) return null;
|
|
8428
|
+
if (token.includes("/") || token.includes("\\")) {
|
|
8429
|
+
const path = isAbsolute9(token) ? token : resolve4(cwd, token);
|
|
8430
|
+
try {
|
|
8431
|
+
return existsSync10(path) ? realpathSync3(path) : null;
|
|
8432
|
+
} catch {
|
|
8433
|
+
return null;
|
|
8434
|
+
}
|
|
8435
|
+
}
|
|
8436
|
+
for (const entry of (env.PATH ?? "").split(delimiter).filter(Boolean)) {
|
|
8437
|
+
const candidate = resolve4(entry, token);
|
|
8438
|
+
try {
|
|
8439
|
+
if (existsSync10(candidate) && statSync4(candidate).isFile()) return realpathSync3(candidate);
|
|
8440
|
+
} catch {
|
|
8441
|
+
}
|
|
8442
|
+
}
|
|
8443
|
+
return null;
|
|
8444
|
+
}
|
|
8445
|
+
function pythonName(name) {
|
|
8446
|
+
return /^(?:python(?:\d+(?:\.\d+)*)?|pytest|py\.test|pip(?:\d+(?:\.\d+)*)?)$/.test(name.toLowerCase());
|
|
8447
|
+
}
|
|
8448
|
+
function looksPython(command, executable, token) {
|
|
8449
|
+
const exe = executable ? basename3(executable) : "";
|
|
8450
|
+
const raw = token ? basename3(token) : "";
|
|
8451
|
+
if (pythonName(exe) || pythonName(raw)) return true;
|
|
8452
|
+
return /(?:^|[\s;&|()])(?:python(?:\d+(?:\.\d+)*)?|pytest|py\.test|pip(?:\d+(?:\.\d+)*)?)(?=$|[\s;&|()])/.test(command);
|
|
8453
|
+
}
|
|
8454
|
+
function inferredVenvFromToken(token, cwd) {
|
|
8455
|
+
if (!token || !token.includes("/") && !token.includes("\\")) return null;
|
|
8456
|
+
const path = isAbsolute9(token) ? token : resolve4(cwd, token);
|
|
8457
|
+
if (!pythonName(basename3(path))) return null;
|
|
8458
|
+
const parent = dirname5(path);
|
|
8459
|
+
const parentName = basename3(parent).toLowerCase();
|
|
8460
|
+
if (parentName !== "bin" && parentName !== "scripts") return null;
|
|
8461
|
+
const root = dirname5(parent);
|
|
8462
|
+
return existsSync10(join13(root, "pyvenv.cfg")) ? root : null;
|
|
8463
|
+
}
|
|
8464
|
+
function unsupportedEcosystem(command, token) {
|
|
8465
|
+
const name = token ? basename3(token).toLowerCase() : "";
|
|
8466
|
+
const raw = command.toLowerCase();
|
|
8467
|
+
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";
|
|
8468
|
+
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";
|
|
8469
|
+
if (name === "dotnet" || /(?:^|[\s;&|()])dotnet(?=$|[\s;&|()])/.test(raw)) return ".NET dependency environment discovery is not yet attestable";
|
|
8470
|
+
return null;
|
|
8471
|
+
}
|
|
8472
|
+
function inferredVenv(executable) {
|
|
8473
|
+
if (!executable) return null;
|
|
8474
|
+
const parent = dirname5(executable);
|
|
8475
|
+
const parentName = basename3(parent).toLowerCase();
|
|
8476
|
+
if (parentName !== "bin" && parentName !== "scripts") return null;
|
|
8477
|
+
const root = dirname5(parent);
|
|
8478
|
+
return existsSync10(join13(root, "pyvenv.cfg")) ? root : null;
|
|
8479
|
+
}
|
|
8480
|
+
function venvProblem(root) {
|
|
8481
|
+
const cfg = join13(root, "pyvenv.cfg");
|
|
8482
|
+
if (!existsSync10(cfg)) return "VIRTUAL_ENV " + JSON.stringify(root) + " has no pyvenv.cfg";
|
|
8483
|
+
try {
|
|
8484
|
+
const body = readFileSync13(cfg, "utf8");
|
|
8485
|
+
if (/^\s*include-system-site-packages\s*=\s*true\s*$/im.test(body)) {
|
|
8486
|
+
return "Python environment " + JSON.stringify(root) + " enables system site-packages; those external package roots are not bounded";
|
|
8487
|
+
}
|
|
8488
|
+
} catch (e) {
|
|
8489
|
+
return "cannot read " + JSON.stringify(cfg) + " (" + (e instanceof Error ? e.message : String(e)) + ")";
|
|
8490
|
+
}
|
|
8491
|
+
return null;
|
|
8492
|
+
}
|
|
8493
|
+
function discoverDependencyEnvironment(cwdInput, command, env = process.env) {
|
|
8494
|
+
const cwd = resolve4(cwdInput);
|
|
8495
|
+
const roots = [];
|
|
8496
|
+
const probes = [];
|
|
8497
|
+
const seen = /* @__PURE__ */ new Set();
|
|
8498
|
+
const addRoot = (kind, path) => {
|
|
8499
|
+
const root = freezeRoot(kind, path);
|
|
8500
|
+
const key2 = kind + ":" + root.path;
|
|
8501
|
+
if (!seen.has(key2)) {
|
|
8502
|
+
roots.push(root);
|
|
8503
|
+
seen.add(key2);
|
|
8504
|
+
}
|
|
8505
|
+
};
|
|
8506
|
+
try {
|
|
8507
|
+
const nodeModules = join13(cwd, "node_modules");
|
|
8508
|
+
if (existsSync10(nodeModules)) addRoot("node_modules", nodeModules);
|
|
8509
|
+
else probes.push({ kind: "node_modules", path: nodeModules });
|
|
8510
|
+
const token = firstExecutable(command);
|
|
8511
|
+
const executable = resolveExecutable(token, cwd, env);
|
|
8512
|
+
let venv = null;
|
|
8513
|
+
if (env.VIRTUAL_ENV?.trim()) {
|
|
8514
|
+
if (!isAbsolute9(env.VIRTUAL_ENV)) {
|
|
8515
|
+
throw new Unattestable("VIRTUAL_ENV is relative (" + JSON.stringify(env.VIRTUAL_ENV) + ")");
|
|
8516
|
+
}
|
|
8517
|
+
venv = resolve4(env.VIRTUAL_ENV);
|
|
8518
|
+
} else {
|
|
8519
|
+
venv = inferredVenv(executable) ?? inferredVenvFromToken(token, cwd);
|
|
8520
|
+
if (!venv && /(?:^|\s)uv\s+run(?:\s|$)/.test(command) && existsSync10(join13(cwd, ".venv", "pyvenv.cfg"))) {
|
|
8521
|
+
venv = join13(cwd, ".venv");
|
|
8522
|
+
}
|
|
8523
|
+
}
|
|
8524
|
+
if (venv) {
|
|
8525
|
+
const problem = venvProblem(venv);
|
|
8526
|
+
if (problem) throw new Unattestable(problem);
|
|
8527
|
+
addRoot("python-venv", venv);
|
|
8528
|
+
} else if (looksPython(command, executable, token)) {
|
|
8529
|
+
throw new Unattestable(
|
|
8530
|
+
"Python verifier selected without an identifiable virtual environment; global/user site-packages are not bounded"
|
|
8531
|
+
);
|
|
8532
|
+
}
|
|
8533
|
+
const unsupported = unsupportedEcosystem(command, token);
|
|
8534
|
+
if (unsupported) throw new Unattestable(unsupported);
|
|
8535
|
+
const status = roots.length ? "attested" : "none";
|
|
8536
|
+
const fingerprint2 = snapshot(roots, probes, cwd);
|
|
8537
|
+
return { status, command, roots, probes, fingerprint: fingerprint2 };
|
|
8538
|
+
} catch (e) {
|
|
8539
|
+
return {
|
|
8540
|
+
status: "unattestable",
|
|
8541
|
+
command,
|
|
8542
|
+
roots,
|
|
8543
|
+
probes,
|
|
8544
|
+
reason: e instanceof Error ? e.message : String(e)
|
|
8545
|
+
};
|
|
8546
|
+
}
|
|
8547
|
+
}
|
|
8548
|
+
function checkDependencyEnvironment(cwdInput, descriptor) {
|
|
8549
|
+
if (descriptor.status === "unattestable") {
|
|
8550
|
+
return { ok: false, reason: descriptor.reason ?? "dependency environment is not attestable" };
|
|
8551
|
+
}
|
|
8552
|
+
try {
|
|
8553
|
+
const fingerprint2 = snapshot(descriptor.roots, descriptor.probes, resolve4(cwdInput));
|
|
8554
|
+
if (fingerprint2 !== descriptor.fingerprint) {
|
|
8555
|
+
return { ok: false, fingerprint: fingerprint2, reason: "dependency environment fingerprint changed" };
|
|
8556
|
+
}
|
|
8557
|
+
return { ok: true, fingerprint: fingerprint2 };
|
|
8558
|
+
} catch (e) {
|
|
8559
|
+
return { ok: false, reason: e instanceof Error ? e.message : String(e) };
|
|
8560
|
+
}
|
|
8561
|
+
}
|
|
8562
|
+
function dependencyEnvironmentReport(descriptor) {
|
|
8563
|
+
return {
|
|
8564
|
+
status: descriptor.status,
|
|
8565
|
+
roots: descriptor.roots.map((root) => ({ kind: root.kind, path: root.path })),
|
|
8566
|
+
...descriptor.fingerprint ? { fingerprint: descriptor.fingerprint } : {},
|
|
8567
|
+
...descriptor.reason ? { reason: descriptor.reason } : {}
|
|
8568
|
+
};
|
|
8569
|
+
}
|
|
8570
|
+
function dependencyEnvironmentSummary(descriptor) {
|
|
8571
|
+
if (descriptor.status === "unattestable") {
|
|
8572
|
+
return "unattestable - " + (descriptor.reason ?? "unknown dependency environment");
|
|
8573
|
+
}
|
|
8574
|
+
if (descriptor.status === "none") return "none detected (candidate node_modules remains absent)";
|
|
8575
|
+
const kinds = [...new Set(descriptor.roots.map((root) => root.kind))].join(", ");
|
|
8576
|
+
return "attested " + kinds + " (sha256 " + (descriptor.fingerprint?.slice(0, 12) ?? "unknown") + "...)";
|
|
8577
|
+
}
|
|
8578
|
+
|
|
8579
|
+
// src/verifier-backend.ts
|
|
8580
|
+
import { createHash as createHash9, randomUUID } from "node:crypto";
|
|
8581
|
+
import { spawnSync } from "node:child_process";
|
|
8582
|
+
import { accessSync, constants as constants2, readFileSync as readFileSync14, realpathSync as realpathSync4, statSync as statSync5 } from "node:fs";
|
|
8583
|
+
import { delimiter as delimiter2, dirname as dirname6, join as join14, resolve as resolve5 } from "node:path";
|
|
8584
|
+
var DIGEST_IMAGE = /^(?!-)[^\s@]+@sha256:[0-9a-f]{64}$/i;
|
|
8585
|
+
var DEFAULT_DOCKER_HOST = "unix:///var/run/docker.sock";
|
|
8586
|
+
function identity() {
|
|
8587
|
+
const uid = typeof process.getuid === "function" ? process.getuid() : null;
|
|
8588
|
+
const groups = typeof process.getgroups === "function" ? process.getgroups() : [];
|
|
8589
|
+
return { uid, groups };
|
|
8590
|
+
}
|
|
8591
|
+
function writableByCaller(path) {
|
|
8592
|
+
try {
|
|
8593
|
+
const st = statSync5(path);
|
|
8594
|
+
const { uid, groups } = identity();
|
|
8595
|
+
if (uid === 0) return true;
|
|
8596
|
+
if ((st.mode & 2) !== 0) return true;
|
|
8597
|
+
if (uid !== null && st.uid === uid && (st.mode & 128) !== 0) return true;
|
|
8598
|
+
if (groups.includes(st.gid) && (st.mode & 16) !== 0) return true;
|
|
8599
|
+
return false;
|
|
8231
8600
|
} catch {
|
|
8232
|
-
return
|
|
8601
|
+
return true;
|
|
8602
|
+
}
|
|
8603
|
+
}
|
|
8604
|
+
function replaceableByCaller(path) {
|
|
8605
|
+
let current = resolve5(path);
|
|
8606
|
+
if (writableByCaller(current)) return true;
|
|
8607
|
+
current = dirname6(current);
|
|
8608
|
+
while (true) {
|
|
8609
|
+
if (writableByCaller(current)) return true;
|
|
8610
|
+
const parent = dirname6(current);
|
|
8611
|
+
if (parent === current) break;
|
|
8612
|
+
current = parent;
|
|
8613
|
+
}
|
|
8614
|
+
return false;
|
|
8615
|
+
}
|
|
8616
|
+
function findExecutable(name) {
|
|
8617
|
+
for (const raw of (process.env.PATH ?? "").split(delimiter2)) {
|
|
8618
|
+
if (!raw) continue;
|
|
8619
|
+
const candidate = join14(raw, name);
|
|
8620
|
+
try {
|
|
8621
|
+
accessSync(candidate, constants2.X_OK);
|
|
8622
|
+
return realpathSync4(candidate);
|
|
8623
|
+
} catch {
|
|
8624
|
+
}
|
|
8625
|
+
}
|
|
8626
|
+
return null;
|
|
8627
|
+
}
|
|
8628
|
+
function fileSha256(path) {
|
|
8629
|
+
return createHash9("sha256").update(readFileSync14(path)).digest("hex");
|
|
8630
|
+
}
|
|
8631
|
+
function engineClientEnv() {
|
|
8632
|
+
return {
|
|
8633
|
+
PATH: "/usr/bin:/bin",
|
|
8634
|
+
HOME: "/nonexistent",
|
|
8635
|
+
LANG: "C",
|
|
8636
|
+
LC_ALL: "C"
|
|
8637
|
+
};
|
|
8638
|
+
}
|
|
8639
|
+
function dockerArgs(host, args) {
|
|
8640
|
+
return ["--host", host, ...args];
|
|
8641
|
+
}
|
|
8642
|
+
function dockerAvailable(enginePath, host) {
|
|
8643
|
+
try {
|
|
8644
|
+
const r = spawnSync(enginePath, dockerArgs(host, ["version"]), {
|
|
8645
|
+
stdio: "ignore",
|
|
8646
|
+
timeout: 5e3,
|
|
8647
|
+
killSignal: "SIGKILL",
|
|
8648
|
+
env: engineClientEnv()
|
|
8649
|
+
});
|
|
8650
|
+
return r.status === 0;
|
|
8651
|
+
} catch {
|
|
8652
|
+
return false;
|
|
8653
|
+
}
|
|
8654
|
+
}
|
|
8655
|
+
function declaredImageVolumePaths(metadata) {
|
|
8656
|
+
if (!metadata || typeof metadata !== "object" || Array.isArray(metadata)) {
|
|
8657
|
+
throw new Error("image metadata must be a Docker image-inspect object");
|
|
8658
|
+
}
|
|
8659
|
+
const config = metadata.Config;
|
|
8660
|
+
if (!config || typeof config !== "object" || Array.isArray(config)) {
|
|
8661
|
+
throw new Error("image metadata Config must be an object");
|
|
8662
|
+
}
|
|
8663
|
+
const volumes = config.Volumes;
|
|
8664
|
+
if (volumes === void 0 || volumes === null) return [];
|
|
8665
|
+
if (typeof volumes !== "object" || Array.isArray(volumes)) {
|
|
8666
|
+
throw new Error("image metadata Config.Volumes must be a mapping or null");
|
|
8667
|
+
}
|
|
8668
|
+
return Object.keys(volumes).sort();
|
|
8669
|
+
}
|
|
8670
|
+
function inspectImageMetadata(enginePath, host, image) {
|
|
8671
|
+
try {
|
|
8672
|
+
const r = spawnSync(enginePath, dockerArgs(host, ["image", "inspect", image]), {
|
|
8673
|
+
encoding: "utf8",
|
|
8674
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
8675
|
+
timeout: 1e4,
|
|
8676
|
+
killSignal: "SIGKILL",
|
|
8677
|
+
env: engineClientEnv()
|
|
8678
|
+
});
|
|
8679
|
+
if (r.error || r.status !== 0) {
|
|
8680
|
+
return {
|
|
8681
|
+
metadata: null,
|
|
8682
|
+
reason: "the digest-pinned verifier image is not present locally; TamperWard refuses to pull during adjudication"
|
|
8683
|
+
};
|
|
8684
|
+
}
|
|
8685
|
+
const parsed = JSON.parse(r.stdout ?? "");
|
|
8686
|
+
if (!Array.isArray(parsed) || parsed.length !== 1) {
|
|
8687
|
+
return { metadata: null, reason: "Docker returned malformed verifier image metadata" };
|
|
8688
|
+
}
|
|
8689
|
+
return { metadata: parsed[0] };
|
|
8690
|
+
} catch {
|
|
8691
|
+
return { metadata: null, reason: "Docker returned unreadable verifier image metadata" };
|
|
8692
|
+
}
|
|
8693
|
+
}
|
|
8694
|
+
function engineStable(backend) {
|
|
8695
|
+
if (!backend.enginePath || !backend.engineSha256) return false;
|
|
8696
|
+
try {
|
|
8697
|
+
if (realpathSync4(backend.enginePath) !== backend.enginePath) return false;
|
|
8698
|
+
if (replaceableByCaller(backend.enginePath)) return false;
|
|
8699
|
+
return fileSha256(backend.enginePath) === backend.engineSha256;
|
|
8700
|
+
} catch {
|
|
8701
|
+
return false;
|
|
8702
|
+
}
|
|
8703
|
+
}
|
|
8704
|
+
function prepareVerifierBackend(verify) {
|
|
8705
|
+
const kind = verify?.backend ?? "local";
|
|
8706
|
+
if (kind === "local") {
|
|
8707
|
+
return { kind: "local", trust: "checkpointed-local", available: true };
|
|
8708
|
+
}
|
|
8709
|
+
const image = verify?.image;
|
|
8710
|
+
if (!image || !DIGEST_IMAGE.test(image)) {
|
|
8711
|
+
return {
|
|
8712
|
+
kind: "container",
|
|
8713
|
+
trust: "isolated-container",
|
|
8714
|
+
available: false,
|
|
8715
|
+
...image ? { image } : {},
|
|
8716
|
+
reason: "container verifier requires an immutable runtime-safe image reference name@sha256:<64 hex>"
|
|
8717
|
+
};
|
|
8718
|
+
}
|
|
8719
|
+
const forced = process.env.TAMPERWARD_CONTAINER_ENGINE;
|
|
8720
|
+
if (forced && forced !== "docker") {
|
|
8721
|
+
return {
|
|
8722
|
+
kind: "container",
|
|
8723
|
+
trust: "isolated-container",
|
|
8724
|
+
available: false,
|
|
8725
|
+
image,
|
|
8726
|
+
reason: `unsupported isolated verifier engine ${JSON.stringify(forced)}; Docker is the tested backend`
|
|
8727
|
+
};
|
|
8728
|
+
}
|
|
8729
|
+
const enginePath = findExecutable("docker");
|
|
8730
|
+
if (!enginePath) {
|
|
8731
|
+
return {
|
|
8732
|
+
kind: "container",
|
|
8733
|
+
trust: "isolated-container",
|
|
8734
|
+
available: false,
|
|
8735
|
+
image,
|
|
8736
|
+
reason: "Docker executable was not found on the operator PATH"
|
|
8737
|
+
};
|
|
8738
|
+
}
|
|
8739
|
+
if (replaceableByCaller(enginePath)) {
|
|
8740
|
+
return {
|
|
8741
|
+
kind: "container",
|
|
8742
|
+
trust: "isolated-container",
|
|
8743
|
+
available: false,
|
|
8744
|
+
image,
|
|
8745
|
+
engine: "docker",
|
|
8746
|
+
enginePath,
|
|
8747
|
+
reason: "Docker engine executable is writable/replaceable by the caller identity; it cannot be verifier authority"
|
|
8748
|
+
};
|
|
8749
|
+
}
|
|
8750
|
+
const daemonHost = DEFAULT_DOCKER_HOST;
|
|
8751
|
+
if (!dockerAvailable(enginePath, daemonHost)) {
|
|
8752
|
+
return {
|
|
8753
|
+
kind: "container",
|
|
8754
|
+
trust: "isolated-container",
|
|
8755
|
+
available: false,
|
|
8756
|
+
image,
|
|
8757
|
+
engine: "docker",
|
|
8758
|
+
enginePath,
|
|
8759
|
+
daemonHost,
|
|
8760
|
+
reason: `Docker is unavailable at the fixed verifier endpoint ${daemonHost}`
|
|
8761
|
+
};
|
|
8762
|
+
}
|
|
8763
|
+
const inspectedImage = inspectImageMetadata(enginePath, daemonHost, image);
|
|
8764
|
+
if (!inspectedImage.metadata) {
|
|
8765
|
+
return {
|
|
8766
|
+
kind: "container",
|
|
8767
|
+
trust: "isolated-container",
|
|
8768
|
+
available: false,
|
|
8769
|
+
image,
|
|
8770
|
+
engine: "docker",
|
|
8771
|
+
enginePath,
|
|
8772
|
+
daemonHost,
|
|
8773
|
+
reason: inspectedImage.reason ?? "Docker could not attest the verifier image metadata"
|
|
8774
|
+
};
|
|
8775
|
+
}
|
|
8776
|
+
let declaredVolumes;
|
|
8777
|
+
try {
|
|
8778
|
+
declaredVolumes = declaredImageVolumePaths(inspectedImage.metadata);
|
|
8779
|
+
} catch (e) {
|
|
8780
|
+
return {
|
|
8781
|
+
kind: "container",
|
|
8782
|
+
trust: "isolated-container",
|
|
8783
|
+
available: false,
|
|
8784
|
+
image,
|
|
8785
|
+
engine: "docker",
|
|
8786
|
+
enginePath,
|
|
8787
|
+
daemonHost,
|
|
8788
|
+
reason: `Docker verifier image metadata is invalid: ${e instanceof Error ? e.message : String(e)}`
|
|
8789
|
+
};
|
|
8790
|
+
}
|
|
8791
|
+
if (declaredVolumes.length > 0) {
|
|
8792
|
+
return {
|
|
8793
|
+
kind: "container",
|
|
8794
|
+
trust: "isolated-container",
|
|
8795
|
+
available: false,
|
|
8796
|
+
image,
|
|
8797
|
+
engine: "docker",
|
|
8798
|
+
enginePath,
|
|
8799
|
+
daemonHost,
|
|
8800
|
+
reason: `the verifier image declares writable Docker VOLUME path(s): ${declaredVolumes.join(", ")}; --read-only does not make image-declared volumes immutable`
|
|
8801
|
+
};
|
|
8802
|
+
}
|
|
8803
|
+
return {
|
|
8804
|
+
kind: "container",
|
|
8805
|
+
trust: "isolated-container",
|
|
8806
|
+
available: true,
|
|
8807
|
+
image,
|
|
8808
|
+
engine: "docker",
|
|
8809
|
+
enginePath,
|
|
8810
|
+
engineSha256: fileSha256(enginePath),
|
|
8811
|
+
daemonHost
|
|
8812
|
+
};
|
|
8813
|
+
}
|
|
8814
|
+
function verifierBackendReport(backend) {
|
|
8815
|
+
return {
|
|
8816
|
+
kind: backend.kind,
|
|
8817
|
+
trust: backend.trust,
|
|
8818
|
+
available: backend.available,
|
|
8819
|
+
...backend.image ? { image: backend.image } : {},
|
|
8820
|
+
...backend.engine ? { engine: backend.engine } : {},
|
|
8821
|
+
...backend.reason ? { reason: backend.reason } : {}
|
|
8822
|
+
};
|
|
8823
|
+
}
|
|
8824
|
+
function verifierBackendSummary(backend) {
|
|
8825
|
+
if (backend.kind === "local") {
|
|
8826
|
+
return "local (checkpointed same-host; weaker trust boundary)";
|
|
8827
|
+
}
|
|
8828
|
+
const identity2 = backend.image ?? "<missing image>";
|
|
8829
|
+
if (!backend.available) {
|
|
8830
|
+
return `container (isolated; unavailable: ${backend.reason ?? "unknown reason"}; image ${identity2})`;
|
|
8831
|
+
}
|
|
8832
|
+
return `container (isolated via Docker; image ${identity2})`;
|
|
8833
|
+
}
|
|
8834
|
+
function cleanupContainer(backend, name) {
|
|
8835
|
+
if (!backend.enginePath || !backend.daemonHost || !engineStable(backend)) return;
|
|
8836
|
+
try {
|
|
8837
|
+
spawnSync(backend.enginePath, dockerArgs(backend.daemonHost, ["rm", "-f", "-v", name]), {
|
|
8838
|
+
stdio: "ignore",
|
|
8839
|
+
timeout: 1e4,
|
|
8840
|
+
killSignal: "SIGKILL",
|
|
8841
|
+
env: engineClientEnv()
|
|
8842
|
+
});
|
|
8843
|
+
} catch {
|
|
8844
|
+
}
|
|
8845
|
+
}
|
|
8846
|
+
function containerRunArgs(input) {
|
|
8847
|
+
return [
|
|
8848
|
+
"run",
|
|
8849
|
+
"--name",
|
|
8850
|
+
input.name,
|
|
8851
|
+
// Keep the stopped container until the trusted host has inspected .State.
|
|
8852
|
+
// Docker CLI exit codes overlap candidate exit codes; container metadata is
|
|
8853
|
+
// the authority that tells us whether the suite actually ran.
|
|
8854
|
+
"--pull",
|
|
8855
|
+
"never",
|
|
8856
|
+
"--network",
|
|
8857
|
+
"none",
|
|
8858
|
+
"--read-only",
|
|
8859
|
+
"--cap-drop",
|
|
8860
|
+
"ALL",
|
|
8861
|
+
"--security-opt",
|
|
8862
|
+
"no-new-privileges",
|
|
8863
|
+
"--pids-limit",
|
|
8864
|
+
"256",
|
|
8865
|
+
"--user",
|
|
8866
|
+
`${input.uid}:${input.gid}`,
|
|
8867
|
+
// Frozen candidate/pristine input is immutable in the verifier. A suite
|
|
8868
|
+
// needing outputs writes to /workspace-out, HOME or /tmp instead.
|
|
8869
|
+
"--mount",
|
|
8870
|
+
`type=bind,src=${input.workspace},dst=/workspace,ro`,
|
|
8871
|
+
"--tmpfs",
|
|
8872
|
+
"/tmp:rw,nosuid,nodev,mode=1777",
|
|
8873
|
+
"--tmpfs",
|
|
8874
|
+
`/home/tamperward:rw,nosuid,nodev,mode=700,uid=${input.uid},gid=${input.gid}`,
|
|
8875
|
+
"--tmpfs",
|
|
8876
|
+
`/workspace-out:rw,nosuid,nodev,mode=700,uid=${input.uid},gid=${input.gid}`,
|
|
8877
|
+
"--workdir",
|
|
8878
|
+
"/workspace",
|
|
8879
|
+
"--env",
|
|
8880
|
+
"HOME=/home/tamperward",
|
|
8881
|
+
"--env",
|
|
8882
|
+
"TMPDIR=/tmp",
|
|
8883
|
+
"--env",
|
|
8884
|
+
"CI=1",
|
|
8885
|
+
"--env",
|
|
8886
|
+
"TAMPERWARD_OUTPUT_DIR=/workspace-out",
|
|
8887
|
+
"--env",
|
|
8888
|
+
"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
|
|
8889
|
+
// The image supplies immutable runtime/dependencies, not command policy.
|
|
8890
|
+
// Docker otherwise appends our command to an image ENTRYPOINT.
|
|
8891
|
+
"--entrypoint",
|
|
8892
|
+
"/bin/sh",
|
|
8893
|
+
input.image,
|
|
8894
|
+
"-c",
|
|
8895
|
+
input.command
|
|
8896
|
+
];
|
|
8897
|
+
}
|
|
8898
|
+
function runContainerStage(backend, dir, command, budgetSecs) {
|
|
8899
|
+
const t0 = Date.now();
|
|
8900
|
+
if (backend.kind !== "container" || !backend.available || !backend.image || !backend.enginePath || !backend.engineSha256 || !backend.daemonHost || !engineStable(backend)) {
|
|
8901
|
+
return {
|
|
8902
|
+
exit: null,
|
|
8903
|
+
secs: 0,
|
|
8904
|
+
failure: "backend",
|
|
8905
|
+
reason: "isolated verifier authority is incomplete or changed before stage execution"
|
|
8906
|
+
};
|
|
8907
|
+
}
|
|
8908
|
+
const name = `tamperward-verify-${process.pid}-${randomUUID().slice(0, 12)}`;
|
|
8909
|
+
const uid = typeof process.getuid === "function" ? process.getuid() : 65534;
|
|
8910
|
+
const gid = typeof process.getgid === "function" ? process.getgid() : 65534;
|
|
8911
|
+
const args = containerRunArgs({
|
|
8912
|
+
image: backend.image,
|
|
8913
|
+
name,
|
|
8914
|
+
workspace: resolve5(dir),
|
|
8915
|
+
command,
|
|
8916
|
+
uid,
|
|
8917
|
+
gid
|
|
8918
|
+
});
|
|
8919
|
+
try {
|
|
8920
|
+
const r = spawnSync(
|
|
8921
|
+
backend.enginePath,
|
|
8922
|
+
dockerArgs(backend.daemonHost, args),
|
|
8923
|
+
{
|
|
8924
|
+
stdio: "ignore",
|
|
8925
|
+
timeout: budgetSecs * 1e3,
|
|
8926
|
+
killSignal: "SIGKILL",
|
|
8927
|
+
env: engineClientEnv()
|
|
8928
|
+
}
|
|
8929
|
+
);
|
|
8930
|
+
const secs = Math.round((Date.now() - t0) / 1e3);
|
|
8931
|
+
if (r.error) {
|
|
8932
|
+
cleanupContainer(backend, name);
|
|
8933
|
+
const code = r.error.code;
|
|
8934
|
+
return code === "ETIMEDOUT" ? { exit: null, secs, failure: "budget", reason: "verifier stage exceeded its budget" } : { exit: null, secs, failure: "backend", reason: `Docker client failed: ${r.error.message}` };
|
|
8935
|
+
}
|
|
8936
|
+
if (r.status === null) {
|
|
8937
|
+
cleanupContainer(backend, name);
|
|
8938
|
+
return {
|
|
8939
|
+
exit: null,
|
|
8940
|
+
secs,
|
|
8941
|
+
failure: "backend",
|
|
8942
|
+
reason: `Docker client did not return a status${r.signal ? ` (signal ${r.signal})` : ""}`
|
|
8943
|
+
};
|
|
8944
|
+
}
|
|
8945
|
+
if (!engineStable(backend)) {
|
|
8946
|
+
return {
|
|
8947
|
+
exit: null,
|
|
8948
|
+
secs,
|
|
8949
|
+
failure: "backend",
|
|
8950
|
+
reason: "Docker engine identity changed before verifier result inspection"
|
|
8951
|
+
};
|
|
8952
|
+
}
|
|
8953
|
+
const inspected = spawnSync(
|
|
8954
|
+
backend.enginePath,
|
|
8955
|
+
dockerArgs(backend.daemonHost, ["inspect", "--format", "{{json .State}}", name]),
|
|
8956
|
+
{
|
|
8957
|
+
encoding: "utf8",
|
|
8958
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
8959
|
+
timeout: 1e4,
|
|
8960
|
+
killSignal: "SIGKILL",
|
|
8961
|
+
env: engineClientEnv()
|
|
8962
|
+
}
|
|
8963
|
+
);
|
|
8964
|
+
let state = null;
|
|
8965
|
+
if (!inspected.error && inspected.status === 0) {
|
|
8966
|
+
try {
|
|
8967
|
+
state = JSON.parse((inspected.stdout ?? "").trim());
|
|
8968
|
+
} catch {
|
|
8969
|
+
state = null;
|
|
8970
|
+
}
|
|
8971
|
+
}
|
|
8972
|
+
cleanupContainer(backend, name);
|
|
8973
|
+
if (!state || state.Status !== "exited" || typeof state.ExitCode !== "number" || state.OOMKilled || (state.Error ?? "") !== "") {
|
|
8974
|
+
return {
|
|
8975
|
+
exit: null,
|
|
8976
|
+
secs,
|
|
8977
|
+
failure: "backend",
|
|
8978
|
+
reason: state?.OOMKilled ? "verifier container was OOM-killed" : state?.Error ? `verifier container runtime error: ${state.Error}` : "Docker did not provide a trustworthy exited-container state"
|
|
8979
|
+
};
|
|
8980
|
+
}
|
|
8981
|
+
return { exit: state.ExitCode, secs };
|
|
8982
|
+
} catch (e) {
|
|
8983
|
+
cleanupContainer(backend, name);
|
|
8984
|
+
return {
|
|
8985
|
+
exit: null,
|
|
8986
|
+
secs: Math.round((Date.now() - t0) / 1e3),
|
|
8987
|
+
failure: "backend",
|
|
8988
|
+
reason: `Docker verifier execution failed: ${e instanceof Error ? e.message : String(e)}`
|
|
8989
|
+
};
|
|
8233
8990
|
}
|
|
8234
|
-
walk2(root);
|
|
8235
|
-
return h.digest("hex");
|
|
8236
8991
|
}
|
|
8237
8992
|
|
|
8238
8993
|
// src/cli/verify.ts
|
|
@@ -8320,28 +9075,28 @@ function resolveBase(base, cwd) {
|
|
|
8320
9075
|
}
|
|
8321
9076
|
}
|
|
8322
9077
|
function baseIsAncestorOfHead(base, cwd) {
|
|
8323
|
-
const r =
|
|
9078
|
+
const r = spawnSync2("git", ["merge-base", "--is-ancestor", base, "HEAD"], { cwd, env: trustedGitEnv() });
|
|
8324
9079
|
return r.status === 0;
|
|
8325
9080
|
}
|
|
8326
|
-
function
|
|
8327
|
-
const rel =
|
|
8328
|
-
return rel === "" || rel !== ".." && !rel.startsWith(`..${
|
|
9081
|
+
function inside2(root, path) {
|
|
9082
|
+
const rel = relative5(resolve6(root), resolve6(path));
|
|
9083
|
+
return rel === "" || rel !== ".." && !rel.startsWith(`..${sep2}`) && !isAbsolute10(rel);
|
|
8329
9084
|
}
|
|
8330
9085
|
function linkParts(path) {
|
|
8331
|
-
const parts =
|
|
9086
|
+
const parts = sep2 === "\\" ? path.split(/[\\/]+/) : path.split("/");
|
|
8332
9087
|
return parts.filter((part) => part !== "" && part !== ".");
|
|
8333
9088
|
}
|
|
8334
9089
|
function linkEscape(label, target) {
|
|
8335
9090
|
return new Error(`${label} is a symlink that escapes the materialised tree (${JSON.stringify(target)})`);
|
|
8336
9091
|
}
|
|
8337
9092
|
function driveRelativeLinkTarget(target) {
|
|
8338
|
-
return
|
|
9093
|
+
return sep2 === "\\" && /^[A-Za-z]:(?![\\/])/.test(target);
|
|
8339
9094
|
}
|
|
8340
9095
|
function rootedLinkTarget(target) {
|
|
8341
|
-
return
|
|
9096
|
+
return isAbsolute10(target) || driveRelativeLinkTarget(target);
|
|
8342
9097
|
}
|
|
8343
9098
|
function safeSymlink(target, out2, root, label) {
|
|
8344
|
-
if (rootedLinkTarget(target) || !
|
|
9099
|
+
if (rootedLinkTarget(target) || !inside2(root, resolve6(dirname7(out2), target))) {
|
|
8345
9100
|
throw linkEscape(label, target);
|
|
8346
9101
|
}
|
|
8347
9102
|
rmSync3(out2, { force: true });
|
|
@@ -8349,7 +9104,7 @@ function safeSymlink(target, out2, root, label) {
|
|
|
8349
9104
|
}
|
|
8350
9105
|
function validateSymlinkGraph(target, out2, root, dependencyRoot, label) {
|
|
8351
9106
|
let domain = "tree";
|
|
8352
|
-
let stack = linkParts(
|
|
9107
|
+
let stack = linkParts(relative5(root, dirname7(out2)));
|
|
8353
9108
|
const pending = linkParts(target);
|
|
8354
9109
|
const seen = /* @__PURE__ */ new Set();
|
|
8355
9110
|
let hops = 0;
|
|
@@ -8367,10 +9122,10 @@ function validateSymlinkGraph(target, out2, root, dependencyRoot, label) {
|
|
|
8367
9122
|
continue;
|
|
8368
9123
|
}
|
|
8369
9124
|
const base = domain === "tree" ? root : dependencyRoot;
|
|
8370
|
-
const current =
|
|
9125
|
+
const current = join15(base, ...stack);
|
|
8371
9126
|
let st;
|
|
8372
9127
|
try {
|
|
8373
|
-
st =
|
|
9128
|
+
st = lstatSync4(current);
|
|
8374
9129
|
} catch {
|
|
8375
9130
|
continue;
|
|
8376
9131
|
}
|
|
@@ -8378,7 +9133,7 @@ function validateSymlinkGraph(target, out2, root, dependencyRoot, label) {
|
|
|
8378
9133
|
if (++hops > 128) {
|
|
8379
9134
|
throw new Error(`${label} contains a symlink cycle while materialising (${JSON.stringify(target)})`);
|
|
8380
9135
|
}
|
|
8381
|
-
const key2 = `${domain}:${current}:${pending.join(
|
|
9136
|
+
const key2 = `${domain}:${current}:${pending.join(sep2)}`;
|
|
8382
9137
|
if (seen.has(key2)) {
|
|
8383
9138
|
throw new Error(`${label} contains a symlink cycle while materialising (${JSON.stringify(target)})`);
|
|
8384
9139
|
}
|
|
@@ -8388,12 +9143,12 @@ function validateSymlinkGraph(target, out2, root, dependencyRoot, label) {
|
|
|
8388
9143
|
if (driveRelativeLinkTarget(next)) {
|
|
8389
9144
|
throw linkEscape(label, target);
|
|
8390
9145
|
}
|
|
8391
|
-
if (
|
|
8392
|
-
if (domain === "tree" || !dependencyRoot || !
|
|
9146
|
+
if (isAbsolute10(next)) {
|
|
9147
|
+
if (domain === "tree" || !dependencyRoot || !inside2(dependencyRoot, next)) {
|
|
8393
9148
|
throw linkEscape(label, target);
|
|
8394
9149
|
}
|
|
8395
9150
|
stack = [];
|
|
8396
|
-
pending.unshift(...linkParts(
|
|
9151
|
+
pending.unshift(...linkParts(relative5(dependencyRoot, resolve6(next))));
|
|
8397
9152
|
} else {
|
|
8398
9153
|
pending.unshift(...linkParts(next));
|
|
8399
9154
|
}
|
|
@@ -8403,32 +9158,32 @@ function rejectLinkedParent(cwd, rel) {
|
|
|
8403
9158
|
const parts = rel.split("/").slice(0, -1);
|
|
8404
9159
|
let current = cwd;
|
|
8405
9160
|
for (const part of parts) {
|
|
8406
|
-
current =
|
|
9161
|
+
current = join15(current, part);
|
|
8407
9162
|
let st;
|
|
8408
9163
|
try {
|
|
8409
|
-
st =
|
|
9164
|
+
st = lstatSync4(current);
|
|
8410
9165
|
} catch {
|
|
8411
9166
|
continue;
|
|
8412
9167
|
}
|
|
8413
9168
|
if (st.isSymbolicLink()) {
|
|
8414
|
-
throw new Error(`${rel} has a symlinked parent directory (${
|
|
9169
|
+
throw new Error(`${rel} has a symlinked parent directory (${relative5(cwd, current)})`);
|
|
8415
9170
|
}
|
|
8416
9171
|
}
|
|
8417
9172
|
}
|
|
8418
|
-
function materialize(cwd, dest) {
|
|
9173
|
+
function materialize(cwd, dest, dependencyRoot) {
|
|
8419
9174
|
const listed = git2(["ls-files", "-z", "--cached", "--others", "--exclude-standard"], cwd).split("\0").filter(Boolean);
|
|
8420
9175
|
const links = [];
|
|
8421
9176
|
for (const rel of listed) {
|
|
8422
|
-
const src =
|
|
9177
|
+
const src = join15(cwd, rel);
|
|
8423
9178
|
rejectLinkedParent(cwd, rel);
|
|
8424
9179
|
let st;
|
|
8425
9180
|
try {
|
|
8426
|
-
st =
|
|
9181
|
+
st = lstatSync4(src);
|
|
8427
9182
|
} catch {
|
|
8428
9183
|
continue;
|
|
8429
9184
|
}
|
|
8430
|
-
const out2 =
|
|
8431
|
-
mkdirSync6(
|
|
9185
|
+
const out2 = join15(dest, rel);
|
|
9186
|
+
mkdirSync6(dirname7(out2), { recursive: true });
|
|
8432
9187
|
dropSymlink(out2);
|
|
8433
9188
|
if (st.isSymbolicLink()) {
|
|
8434
9189
|
const target = readlinkSync3(src);
|
|
@@ -8441,17 +9196,14 @@ function materialize(cwd, dest) {
|
|
|
8441
9196
|
throw new Error(`${rel} is ${st.isDirectory() ? "a directory" : "a special file"} where git expects a file`);
|
|
8442
9197
|
}
|
|
8443
9198
|
}
|
|
8444
|
-
const nm = resolve4(cwd, "node_modules");
|
|
8445
|
-
let dependencyRoot = null;
|
|
8446
|
-
if (existsSync10(nm)) dependencyRoot = realpathSync3(nm);
|
|
8447
9199
|
for (const link of links) {
|
|
8448
9200
|
validateSymlinkGraph(link.target, link.out, dest, dependencyRoot, link.rel);
|
|
8449
9201
|
}
|
|
8450
|
-
if (dependencyRoot) symlinkSync(
|
|
9202
|
+
if (dependencyRoot) symlinkSync(dependencyRoot, join15(dest, "node_modules"), "dir");
|
|
8451
9203
|
}
|
|
8452
9204
|
function dropSymlink(p) {
|
|
8453
9205
|
try {
|
|
8454
|
-
if (
|
|
9206
|
+
if (lstatSync4(p).isSymbolicLink()) rmSync3(p, { force: true });
|
|
8455
9207
|
} catch {
|
|
8456
9208
|
}
|
|
8457
9209
|
}
|
|
@@ -8476,15 +9228,15 @@ function baseEntries(base, cwd) {
|
|
|
8476
9228
|
});
|
|
8477
9229
|
}
|
|
8478
9230
|
function overlayDigest(dest, paths) {
|
|
8479
|
-
const h =
|
|
9231
|
+
const h = createHash10("sha256");
|
|
8480
9232
|
for (const rel of [...paths].sort()) {
|
|
8481
9233
|
h.update(rel);
|
|
8482
9234
|
h.update("\0");
|
|
8483
|
-
const p =
|
|
9235
|
+
const p = join15(dest, rel);
|
|
8484
9236
|
try {
|
|
8485
|
-
const st =
|
|
9237
|
+
const st = lstatSync4(p);
|
|
8486
9238
|
h.update(String(st.mode));
|
|
8487
|
-
h.update(st.isSymbolicLink() ? readlinkSync3(p) :
|
|
9239
|
+
h.update(st.isSymbolicLink() ? readlinkSync3(p) : readFileSync15(p));
|
|
8488
9240
|
} catch {
|
|
8489
9241
|
h.update("<gone>");
|
|
8490
9242
|
}
|
|
@@ -8492,7 +9244,7 @@ function overlayDigest(dest, paths) {
|
|
|
8492
9244
|
}
|
|
8493
9245
|
return h.digest("hex");
|
|
8494
9246
|
}
|
|
8495
|
-
function overlayPristine(cwd, base, dest, policy, cmd) {
|
|
9247
|
+
function overlayPristine(cwd, base, dest, policy, cmd, dependencyRoot) {
|
|
8496
9248
|
const entries = baseEntries(base, cwd);
|
|
8497
9249
|
const atBase = entries.map((e) => e.path);
|
|
8498
9250
|
const verifierOwned = verifierInputs(cmd, atBase, policy);
|
|
@@ -8508,8 +9260,8 @@ function overlayPristine(cwd, base, dest, policy, cmd) {
|
|
|
8508
9260
|
throw new Error(`the base's ${e.path} is a submodule; the pristine copy cannot reproduce it`);
|
|
8509
9261
|
}
|
|
8510
9262
|
const content = execFileSync8("git", ["show", `${base}:${e.path}`], { cwd, maxBuffer: 1 << 28, env: trustedGitEnv() });
|
|
8511
|
-
const out2 =
|
|
8512
|
-
mkdirSync6(
|
|
9263
|
+
const out2 = join15(dest, e.path);
|
|
9264
|
+
mkdirSync6(dirname7(out2), { recursive: true });
|
|
8513
9265
|
rmSync3(out2, { force: true });
|
|
8514
9266
|
if (e.mode === "120000") {
|
|
8515
9267
|
const target = content.toString("utf8");
|
|
@@ -8525,11 +9277,9 @@ function overlayPristine(cwd, base, dest, policy, cmd) {
|
|
|
8525
9277
|
const inCopy = git2(["ls-files", "-z", "--cached", "--others", "--exclude-standard"], cwd).split("\0").filter(Boolean);
|
|
8526
9278
|
for (const rel of inCopy) {
|
|
8527
9279
|
if (!isOverlay(rel) || baseProtected.has(rel)) continue;
|
|
8528
|
-
rmSync3(
|
|
9280
|
+
rmSync3(join15(dest, rel), { force: true });
|
|
8529
9281
|
removed++;
|
|
8530
9282
|
}
|
|
8531
|
-
const nm = resolve4(cwd, "node_modules");
|
|
8532
|
-
const dependencyRoot = existsSync10(nm) ? realpathSync3(nm) : null;
|
|
8533
9283
|
for (const link of restoredLinks) {
|
|
8534
9284
|
validateSymlinkGraph(link.target, link.out, dest, dependencyRoot, `the base's ${link.path}`);
|
|
8535
9285
|
}
|
|
@@ -8572,8 +9322,8 @@ function suiteEnv(scratch) {
|
|
|
8572
9322
|
if (DROPPED_ENV.has(k) || PINNED_NPM.test(k)) continue;
|
|
8573
9323
|
env[k] = v;
|
|
8574
9324
|
}
|
|
8575
|
-
const userRc =
|
|
8576
|
-
const globalRc =
|
|
9325
|
+
const userRc = join15(scratch, "npmrc-user");
|
|
9326
|
+
const globalRc = join15(scratch, "npmrc-global");
|
|
8577
9327
|
for (const f of [userRc, globalRc]) writeFileSync6(f, "", { mode: 292 });
|
|
8578
9328
|
env.npm_config_userconfig = userRc;
|
|
8579
9329
|
env.npm_config_globalconfig = globalRc;
|
|
@@ -8581,12 +9331,12 @@ function suiteEnv(scratch) {
|
|
|
8581
9331
|
env.YARN_IGNORE_PATH = "1";
|
|
8582
9332
|
return env;
|
|
8583
9333
|
}
|
|
8584
|
-
function
|
|
9334
|
+
function runLocalSuite(dir, cmd, budgetSecs) {
|
|
8585
9335
|
const t0 = Date.now();
|
|
8586
|
-
const outDir = mkdtempSync2(
|
|
8587
|
-
const outFile =
|
|
9336
|
+
const outDir = mkdtempSync2(join15(tmpdir2(), "tw-verify-run-"));
|
|
9337
|
+
const outFile = join15(outDir, "result.json");
|
|
8588
9338
|
try {
|
|
8589
|
-
|
|
9339
|
+
spawnSync2(process.execPath, ["-e", SUPERVISOR, cmd, String(budgetSecs * 1e3), outFile], {
|
|
8590
9340
|
cwd: dir,
|
|
8591
9341
|
env: suiteEnv(outDir),
|
|
8592
9342
|
stdio: "ignore",
|
|
@@ -8597,7 +9347,7 @@ function runSuite(dir, cmd, budgetSecs) {
|
|
|
8597
9347
|
const secs = Math.round((Date.now() - t0) / 1e3);
|
|
8598
9348
|
let r;
|
|
8599
9349
|
try {
|
|
8600
|
-
r = JSON.parse(
|
|
9350
|
+
r = JSON.parse(readFileSync15(outFile, "utf8"));
|
|
8601
9351
|
} catch {
|
|
8602
9352
|
return { exit: null, secs };
|
|
8603
9353
|
}
|
|
@@ -8641,6 +9391,45 @@ function runVerify(opts) {
|
|
|
8641
9391
|
return 2;
|
|
8642
9392
|
}
|
|
8643
9393
|
const budget = opts.budget ?? policy.verify?.budget ?? 300;
|
|
9394
|
+
const verifierBackend = opts.verifierBackend ?? prepareVerifierBackend(policy.verify);
|
|
9395
|
+
const backendReport = () => verifierBackendReport(verifierBackend);
|
|
9396
|
+
if (!verifierBackend.available) {
|
|
9397
|
+
if (opts.json) {
|
|
9398
|
+
out2(JSON.stringify({
|
|
9399
|
+
verdict: "CANNOT_VERIFY",
|
|
9400
|
+
reason: "VERIFIER_BACKEND_UNAVAILABLE",
|
|
9401
|
+
verifier_backend: backendReport()
|
|
9402
|
+
}));
|
|
9403
|
+
} else {
|
|
9404
|
+
out2("verify: isolated verifier backend is unavailable \u2014 failing closed");
|
|
9405
|
+
out2("verify: " + (verifierBackend.reason ?? "unknown backend failure"));
|
|
9406
|
+
}
|
|
9407
|
+
return 2;
|
|
9408
|
+
}
|
|
9409
|
+
const isolated = verifierBackend.kind === "container";
|
|
9410
|
+
const dependencyEnvironment = isolated ? null : opts.dependencyEnvironment ?? discoverDependencyEnvironment(cwd, cmd);
|
|
9411
|
+
const dependencyReport = () => dependencyEnvironment ? dependencyEnvironmentReport(dependencyEnvironment) : {
|
|
9412
|
+
status: "verifier-owned",
|
|
9413
|
+
roots: [],
|
|
9414
|
+
image: verifierBackend.image
|
|
9415
|
+
};
|
|
9416
|
+
if (dependencyEnvironment?.status === "unattestable" && !opts.allowDepDrift) {
|
|
9417
|
+
if (opts.json) {
|
|
9418
|
+
out2(JSON.stringify({
|
|
9419
|
+
verdict: "CANNOT_VERIFY",
|
|
9420
|
+
reason: "DEPENDENCY_ENVIRONMENT_UNATTESTABLE",
|
|
9421
|
+
verifier_backend: backendReport(),
|
|
9422
|
+
dependency_environment: dependencyReport()
|
|
9423
|
+
}));
|
|
9424
|
+
} else {
|
|
9425
|
+
out2("verify: dependency environment is not attestable \u2014 failing closed");
|
|
9426
|
+
out2("verify: " + (dependencyEnvironment.reason ?? "unknown dependency environment"));
|
|
9427
|
+
}
|
|
9428
|
+
return 2;
|
|
9429
|
+
}
|
|
9430
|
+
const frozenNodeModules = dependencyEnvironment ? dependencyEnvironment.roots.find((root) => root.kind === "node_modules")?.realPath ?? null : null;
|
|
9431
|
+
const checkDeps = () => dependencyEnvironment ? checkDependencyEnvironment(cwd, dependencyEnvironment) : { ok: true };
|
|
9432
|
+
const runStage = (dir) => isolated ? runContainerStage(verifierBackend, dir, cmd, budget) : runLocalSuite(dir, cmd, budget);
|
|
8644
9433
|
let base;
|
|
8645
9434
|
try {
|
|
8646
9435
|
base = resolveBase(opts.base ?? "HEAD", cwd);
|
|
@@ -8648,58 +9437,105 @@ function runVerify(opts) {
|
|
|
8648
9437
|
out2(`verify: cannot resolve base rev "${opts.base ?? "HEAD"}" \u2014 failing closed`);
|
|
8649
9438
|
return 2;
|
|
8650
9439
|
}
|
|
8651
|
-
const visRoot = mkdtempSync2(
|
|
8652
|
-
const visDir =
|
|
9440
|
+
const visRoot = mkdtempSync2(join15(tmpdir2(), "tw-verify-vis-"));
|
|
9441
|
+
const visDir = join15(visRoot, "t");
|
|
8653
9442
|
const cleanup = (dirs) => {
|
|
8654
9443
|
if (opts.keep) return;
|
|
8655
9444
|
for (const d of dirs) rmSync3(d, { recursive: true, force: true });
|
|
8656
9445
|
};
|
|
8657
9446
|
const protectedIgnored = (rel) => isProtected(rel, policy);
|
|
8658
9447
|
const treeBefore = treeFingerprint(cwd, protectedIgnored);
|
|
8659
|
-
const
|
|
9448
|
+
const dependencyAtEntry = checkDeps();
|
|
9449
|
+
if (!dependencyAtEntry.ok && !opts.allowDepDrift) {
|
|
9450
|
+
cleanup([visRoot]);
|
|
9451
|
+
out2("verify: the frozen dependency environment changed before the visible suite ran \u2014");
|
|
9452
|
+
out2((dependencyAtEntry.reason ?? "dependency identity changed") + ". Failing closed, not open.");
|
|
9453
|
+
return 2;
|
|
9454
|
+
}
|
|
8660
9455
|
try {
|
|
8661
9456
|
mkdirSync6(visDir);
|
|
8662
|
-
materialize(cwd, visDir);
|
|
9457
|
+
materialize(cwd, visDir, frozenNodeModules);
|
|
8663
9458
|
} catch (e) {
|
|
8664
9459
|
cleanup([visRoot]);
|
|
8665
9460
|
out2(`verify: could not materialize (${e instanceof Error ? e.message : String(e)}) \u2014 failing closed`);
|
|
8666
9461
|
return 2;
|
|
8667
9462
|
}
|
|
8668
|
-
const visible =
|
|
9463
|
+
const visible = runStage(visDir);
|
|
9464
|
+
if (visible.failure === "backend") {
|
|
9465
|
+
cleanup([visRoot]);
|
|
9466
|
+
if (opts.json) {
|
|
9467
|
+
out2(JSON.stringify({
|
|
9468
|
+
verdict: "CANNOT_VERIFY",
|
|
9469
|
+
reason: "VERIFIER_BACKEND_RUNTIME_FAILURE",
|
|
9470
|
+
stage: "visible",
|
|
9471
|
+
detail: visible.reason,
|
|
9472
|
+
verifier_backend: backendReport()
|
|
9473
|
+
}));
|
|
9474
|
+
} else {
|
|
9475
|
+
out2("verify: isolated verifier backend failed while running the visible stage \u2014 failing closed");
|
|
9476
|
+
if (visible.reason) out2("verify: " + visible.reason);
|
|
9477
|
+
}
|
|
9478
|
+
return 2;
|
|
9479
|
+
}
|
|
8669
9480
|
if (treeFingerprint(cwd, protectedIgnored) !== treeBefore) {
|
|
8670
9481
|
cleanup([visRoot]);
|
|
8671
9482
|
out2("verify: the working tree changed while the visible suite was running \u2014 the pristine copy");
|
|
8672
9483
|
out2("would be materialised from a tree the candidate just edited. Failing closed, not open.");
|
|
8673
9484
|
return 2;
|
|
8674
9485
|
}
|
|
8675
|
-
|
|
9486
|
+
const dependencyAfterVisible = checkDeps();
|
|
9487
|
+
if (!dependencyAfterVisible.ok && !opts.allowDepDrift) {
|
|
8676
9488
|
cleanup([visRoot]);
|
|
8677
|
-
out2("verify: the
|
|
8678
|
-
out2("
|
|
9489
|
+
out2("verify: the frozen dependency environment changed while the visible suite was running \u2014");
|
|
9490
|
+
out2((dependencyAfterVisible.reason ?? "dependency identity changed") + ". Failing closed, not open.");
|
|
8679
9491
|
return 2;
|
|
8680
9492
|
}
|
|
8681
|
-
const priRoot = mkdtempSync2(
|
|
8682
|
-
const priDir =
|
|
9493
|
+
const priRoot = mkdtempSync2(join15(tmpdir2(), "tw-verify-pri-"));
|
|
9494
|
+
const priDir = join15(priRoot, "t");
|
|
8683
9495
|
let restored = [];
|
|
8684
9496
|
let removedAdded = 0;
|
|
8685
9497
|
try {
|
|
8686
9498
|
mkdirSync6(priDir);
|
|
8687
|
-
materialize(cwd, priDir);
|
|
8688
|
-
({ restored, removed: removedAdded } = overlayPristine(
|
|
9499
|
+
materialize(cwd, priDir, frozenNodeModules);
|
|
9500
|
+
({ restored, removed: removedAdded } = overlayPristine(
|
|
9501
|
+
cwd,
|
|
9502
|
+
base,
|
|
9503
|
+
priDir,
|
|
9504
|
+
policy,
|
|
9505
|
+
cmd,
|
|
9506
|
+
frozenNodeModules
|
|
9507
|
+
));
|
|
8689
9508
|
} catch (e) {
|
|
8690
9509
|
cleanup([visRoot, priRoot]);
|
|
8691
9510
|
out2(`verify: could not materialize (${e instanceof Error ? e.message : String(e)}) \u2014 failing closed`);
|
|
8692
9511
|
return 2;
|
|
8693
9512
|
}
|
|
8694
9513
|
const overlayBefore = overlayDigest(priDir, restored);
|
|
8695
|
-
const pristine =
|
|
9514
|
+
const pristine = runStage(priDir);
|
|
9515
|
+
if (pristine.failure === "backend") {
|
|
9516
|
+
cleanup([visRoot, priRoot]);
|
|
9517
|
+
if (opts.json) {
|
|
9518
|
+
out2(JSON.stringify({
|
|
9519
|
+
verdict: "CANNOT_VERIFY",
|
|
9520
|
+
reason: "VERIFIER_BACKEND_RUNTIME_FAILURE",
|
|
9521
|
+
stage: "pristine",
|
|
9522
|
+
detail: pristine.reason,
|
|
9523
|
+
verifier_backend: backendReport()
|
|
9524
|
+
}));
|
|
9525
|
+
} else {
|
|
9526
|
+
out2("verify: isolated verifier backend failed while running the pristine stage \u2014 failing closed");
|
|
9527
|
+
if (pristine.reason) out2("verify: " + pristine.reason);
|
|
9528
|
+
}
|
|
9529
|
+
return 2;
|
|
9530
|
+
}
|
|
8696
9531
|
const overlayMoved = overlayDigest(priDir, restored) !== overlayBefore;
|
|
8697
9532
|
const treeMoved = treeFingerprint(cwd, protectedIgnored) !== treeBefore;
|
|
8698
|
-
const
|
|
9533
|
+
const dependencyAfterPristine = checkDeps();
|
|
9534
|
+
const depsMoved = !dependencyAfterPristine.ok && !opts.allowDepDrift;
|
|
8699
9535
|
cleanup([visRoot, priRoot]);
|
|
8700
9536
|
if (overlayMoved || treeMoved || depsMoved) {
|
|
8701
9537
|
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
|
|
9538
|
+
"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
9539
|
);
|
|
8704
9540
|
out2("the verdict would describe something other than what ran. Failing closed, not open.");
|
|
8705
9541
|
out2("(A suite that rewrites its own snapshots or test files in place will trip this; run it in");
|
|
@@ -8734,6 +9570,8 @@ function runVerify(opts) {
|
|
|
8734
9570
|
pristine: { exit: pristine.exit, secs: pristine.secs },
|
|
8735
9571
|
protected_restored: restored.length,
|
|
8736
9572
|
added_protected_removed: removedAdded,
|
|
9573
|
+
verifier_backend: backendReport(),
|
|
9574
|
+
dependency_environment: dependencyReport(),
|
|
8737
9575
|
...signedOff ? { oob_signoff: signedOff } : {},
|
|
8738
9576
|
...opts.keep ? { visible_dir: visDir, pristine_dir: priDir } : {}
|
|
8739
9577
|
})
|
|
@@ -8746,6 +9584,10 @@ function runVerify(opts) {
|
|
|
8746
9584
|
BUDGET_EXCEEDED: `budget exceeded (${budget}s): could not verify \u2014 failing closed, not open.`
|
|
8747
9585
|
};
|
|
8748
9586
|
out2(`tamperward verify \u2014 ${lines[verdict2]}`);
|
|
9587
|
+
out2(`verifier backend: ${verifierBackendSummary(verifierBackend)}`);
|
|
9588
|
+
out2(
|
|
9589
|
+
dependencyEnvironment ? `dependency environment: ${dependencyEnvironmentSummary(dependencyEnvironment)}` + (opts.allowDepDrift && dependencyEnvironment.status === "unattestable" ? " (operator override)" : "") : `dependency environment: verifier-owned by ${verifierBackend.image ?? "isolated image"}`
|
|
9590
|
+
);
|
|
8749
9591
|
if (signedOff)
|
|
8750
9592
|
out2(
|
|
8751
9593
|
`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.`
|
|
@@ -8790,9 +9632,9 @@ function parseVerify(args) {
|
|
|
8790
9632
|
}
|
|
8791
9633
|
|
|
8792
9634
|
// src/cli/run.ts
|
|
8793
|
-
import { execFileSync as execFileSync9, spawnSync as
|
|
8794
|
-
import { readdirSync as readdirSync4, readFileSync as
|
|
8795
|
-
import { resolve as
|
|
9635
|
+
import { execFileSync as execFileSync9, spawnSync as spawnSync3 } from "node:child_process";
|
|
9636
|
+
import { readdirSync as readdirSync4, readFileSync as readFileSync16, readlinkSync as readlinkSync4, realpathSync as realpathSync6 } from "node:fs";
|
|
9637
|
+
import { resolve as resolve7 } from "node:path";
|
|
8796
9638
|
var out = (s) => process.stdout.write(s + "\n");
|
|
8797
9639
|
var err = (s) => process.stderr.write(s + "\n");
|
|
8798
9640
|
function git3(args, cwd) {
|
|
@@ -8800,7 +9642,7 @@ function git3(args, cwd) {
|
|
|
8800
9642
|
}
|
|
8801
9643
|
function nowTicks() {
|
|
8802
9644
|
try {
|
|
8803
|
-
return Math.floor(parseFloat(
|
|
9645
|
+
return Math.floor(parseFloat(readFileSync16("/proc/uptime", "utf8").split(" ")[0]) * 100);
|
|
8804
9646
|
} catch {
|
|
8805
9647
|
return Number.POSITIVE_INFINITY;
|
|
8806
9648
|
}
|
|
@@ -8809,7 +9651,7 @@ function survivorsHoldingTree(cwd, spawnedAfterTicks) {
|
|
|
8809
9651
|
const out2 = [];
|
|
8810
9652
|
let real;
|
|
8811
9653
|
try {
|
|
8812
|
-
real =
|
|
9654
|
+
real = realpathSync6(cwd);
|
|
8813
9655
|
} catch {
|
|
8814
9656
|
return out2;
|
|
8815
9657
|
}
|
|
@@ -8823,7 +9665,7 @@ function survivorsHoldingTree(cwd, spawnedAfterTicks) {
|
|
|
8823
9665
|
const n = Number(pid);
|
|
8824
9666
|
if (n === process.pid) continue;
|
|
8825
9667
|
try {
|
|
8826
|
-
const stat =
|
|
9668
|
+
const stat = readFileSync16(`/proc/${pid}/stat`, "utf8");
|
|
8827
9669
|
const startTicks = Number(stat.slice(stat.lastIndexOf(")") + 2).split(" ")[19]);
|
|
8828
9670
|
if (!Number.isFinite(startTicks) || startTicks < spawnedAfterTicks) continue;
|
|
8829
9671
|
} catch {
|
|
@@ -8838,7 +9680,7 @@ function survivorsHoldingTree(cwd, spawnedAfterTicks) {
|
|
|
8838
9680
|
for (const link of procLinks) {
|
|
8839
9681
|
try {
|
|
8840
9682
|
const raw = readlinkSync4(link).replace(/ \(deleted\)$/, "");
|
|
8841
|
-
const held = raw.startsWith("/") ?
|
|
9683
|
+
const held = raw.startsWith("/") ? resolve7(raw) : raw;
|
|
8842
9684
|
if (held === real || held.startsWith(real + "/")) {
|
|
8843
9685
|
holds = true;
|
|
8844
9686
|
break;
|
|
@@ -8851,7 +9693,7 @@ function survivorsHoldingTree(cwd, spawnedAfterTicks) {
|
|
|
8851
9693
|
return out2;
|
|
8852
9694
|
}
|
|
8853
9695
|
function runEnvelope(opts) {
|
|
8854
|
-
const cwd =
|
|
9696
|
+
const cwd = resolve7(opts.cwd ?? process.cwd());
|
|
8855
9697
|
if (!opts.argv.length) {
|
|
8856
9698
|
err("tamperward run: no agent command given (usage: tamperward run [options] -- <command...>)");
|
|
8857
9699
|
return 2;
|
|
@@ -8887,11 +9729,37 @@ function runEnvelope(opts) {
|
|
|
8887
9729
|
return 2;
|
|
8888
9730
|
}
|
|
8889
9731
|
}
|
|
9732
|
+
const verifierBackend = prepareVerifierBackend(frozenPolicy.verify);
|
|
8890
9733
|
out(`tamperward run \u2014 trusted base ${base.slice(0, 10)}; agent exit is untrusted.`);
|
|
8891
|
-
|
|
9734
|
+
out(`tamperward run \u2014 verifier backend: ${verifierBackendSummary(verifierBackend)}`);
|
|
9735
|
+
if (!verifierBackend.available) {
|
|
9736
|
+
err("tamperward run: the requested verifier backend cannot be established \u2014");
|
|
9737
|
+
err(`${verifierBackend.reason ?? "unknown backend failure"}. Failing closed before the agent starts.`);
|
|
9738
|
+
return 2;
|
|
9739
|
+
}
|
|
9740
|
+
if (verifierBackend.kind === "container") {
|
|
9741
|
+
err("tamperward run: container verification requires a frozen candidate handoff from an");
|
|
9742
|
+
err("agent domain that cannot control the verifier engine. `run` executes the agent under");
|
|
9743
|
+
err("this host identity, so it cannot honestly provide that separation. Failing closed");
|
|
9744
|
+
err("before the agent starts; use standalone `tamperward verify` in trusted CI or after");
|
|
9745
|
+
err("an externally isolated agent produces the frozen candidate artifact.");
|
|
9746
|
+
return 2;
|
|
9747
|
+
}
|
|
9748
|
+
const dependencyEnvironment = verifierBackend.kind === "local" ? discoverDependencyEnvironment(cwd, frozenCmd) : null;
|
|
9749
|
+
if (dependencyEnvironment) {
|
|
9750
|
+
out(`tamperward run \u2014 dependency environment: ${dependencyEnvironmentSummary(dependencyEnvironment)}`);
|
|
9751
|
+
if (dependencyEnvironment.status === "unattestable" && !opts.allowDepDrift) {
|
|
9752
|
+
err("tamperward run: the verifier dependency environment cannot be attested \u2014");
|
|
9753
|
+
err(`${dependencyEnvironment.reason ?? "unknown dependency environment"}. Failing closed before the agent starts.`);
|
|
9754
|
+
err("(Pass --allow-dep-drift only if you explicitly own this dependency-integrity risk.)");
|
|
9755
|
+
return 2;
|
|
9756
|
+
}
|
|
9757
|
+
} else {
|
|
9758
|
+
out(`tamperward run \u2014 dependency environment: verifier-owned by ${verifierBackend.image ?? "isolated image"}`);
|
|
9759
|
+
}
|
|
8892
9760
|
const rewritesBefore = objectRewriteState(cwd);
|
|
8893
9761
|
const spawnTicks = nowTicks();
|
|
8894
|
-
const agent =
|
|
9762
|
+
const agent = spawnSync3(opts.argv[0], opts.argv.slice(1), { cwd, stdio: "inherit" });
|
|
8895
9763
|
const agentExit = agent.status ?? 1;
|
|
8896
9764
|
if (objectRewriteState(cwd) !== rewritesBefore) {
|
|
8897
9765
|
err("tamperward run: git object-replacement state changed during this run \u2014 a replace ref, graft");
|
|
@@ -8901,9 +9769,10 @@ function runEnvelope(opts) {
|
|
|
8901
9769
|
tamperward run \u2014 agent exit ${agentExit}; OBJECT_REWRITE \u2192 ENFORCEMENT_FAILED`);
|
|
8902
9770
|
return 1;
|
|
8903
9771
|
}
|
|
8904
|
-
|
|
8905
|
-
|
|
8906
|
-
err("
|
|
9772
|
+
const dependencyBeforeAdjudication = dependencyEnvironment ? checkDependencyEnvironment(cwd, dependencyEnvironment) : { ok: true };
|
|
9773
|
+
if (!dependencyBeforeAdjudication.ok) {
|
|
9774
|
+
err("tamperward run: the frozen dependency environment changed before adjudication began \u2014");
|
|
9775
|
+
err(`${dependencyBeforeAdjudication.reason ?? "dependency identity changed"}.`);
|
|
8907
9776
|
if (!opts.allowDepDrift) {
|
|
8908
9777
|
out(`
|
|
8909
9778
|
tamperward run \u2014 agent exit ${agentExit}; DEPENDENCY_DRIFT \u2192 ENFORCEMENT_FAILED`);
|
|
@@ -8912,7 +9781,7 @@ tamperward run \u2014 agent exit ${agentExit}; DEPENDENCY_DRIFT \u2192 ENFORCEME
|
|
|
8912
9781
|
err("(--allow-dep-drift: proceeding anyway, on the operator's judgement.)");
|
|
8913
9782
|
}
|
|
8914
9783
|
const head = git3(["rev-parse", "HEAD"], cwd).trim();
|
|
8915
|
-
const isAncestor =
|
|
9784
|
+
const isAncestor = spawnSync3("git", ["merge-base", "--is-ancestor", base, head], { cwd, env: trustedGitEnv() });
|
|
8916
9785
|
if (isAncestor.status !== 0) {
|
|
8917
9786
|
err(`tamperward run: HEAD ${head.slice(0, 10)} is not a descendant of the trusted base ${base.slice(0, 10)} \u2014`);
|
|
8918
9787
|
err("the agent rewrote history out from under the anchor. That is the finding.");
|
|
@@ -8931,7 +9800,16 @@ tamperward run \u2014 agent exit ${agentExit}; HISTORY_REWRITE \u2192 ENFORCEMEN
|
|
|
8931
9800
|
out("\n[envelope] policy check over the working tree (frozen policy, untracked included, no local ledger):");
|
|
8932
9801
|
const workCode = runCheck({ worktree: true, cwd, policyOverride: frozenPolicy, includeUntracked: true, ciLayer: true });
|
|
8933
9802
|
out("\n[envelope] pristine verification against the trusted base:");
|
|
8934
|
-
const verifyCode = runVerify({
|
|
9803
|
+
const verifyCode = runVerify({
|
|
9804
|
+
cwd,
|
|
9805
|
+
base,
|
|
9806
|
+
cmd: frozenCmd,
|
|
9807
|
+
budget: frozenBudget,
|
|
9808
|
+
policyOverride: frozenPolicy,
|
|
9809
|
+
...dependencyEnvironment ? { dependencyEnvironment } : {},
|
|
9810
|
+
allowDepDrift: opts.allowDepDrift,
|
|
9811
|
+
verifierBackend
|
|
9812
|
+
});
|
|
8935
9813
|
if (opts.settle && opts.settle > 0) {
|
|
8936
9814
|
try {
|
|
8937
9815
|
execFileSync9("sleep", [String(opts.settle)]);
|
|
@@ -8943,10 +9821,11 @@ tamperward run \u2014 agent exit ${agentExit}; HISTORY_REWRITE \u2192 ENFORCEMEN
|
|
|
8943
9821
|
err("tamperward run: the working tree changed while it was being adjudicated \u2014");
|
|
8944
9822
|
err("the verdict below would describe a tree that no longer exists.");
|
|
8945
9823
|
}
|
|
8946
|
-
const
|
|
9824
|
+
const dependencyAfter = dependencyEnvironment ? checkDependencyEnvironment(cwd, dependencyEnvironment) : { ok: true };
|
|
9825
|
+
const depsDrifted = Boolean(dependencyEnvironment) && !opts.allowDepDrift && !dependencyAfter.ok;
|
|
8947
9826
|
if (depsDrifted) {
|
|
8948
|
-
err("tamperward run: the
|
|
8949
|
-
err(
|
|
9827
|
+
err("tamperward run: the frozen dependency environment changed during this run \u2014");
|
|
9828
|
+
err(`${dependencyAfter.reason ?? "dependency identity changed"}. Both verification runs depend on it.`);
|
|
8950
9829
|
}
|
|
8951
9830
|
const rewrote = objectRewriteState(cwd) !== rewritesBefore;
|
|
8952
9831
|
if (rewrote) {
|