tamperward 2.15.2 → 2.16.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +20 -2
- package/dist/cli/index.js +561 -109
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -325,8 +325,14 @@ Public rulesets can be read anonymously; set `GH_TOKEN` or `GITHUB_TOKEN` when
|
|
|
325
325
|
authentication is required.
|
|
326
326
|
|
|
327
327
|
A real deployment needs a verify command configured — the generated CI verify step
|
|
328
|
-
**fails closed (exit 2) without one** rather than passing quietly.
|
|
329
|
-
|
|
328
|
+
**fails closed (exit 2) without one** rather than passing quietly. Since 2.16.1,
|
|
329
|
+
`tamperward init` ends with a separate **VERIFICATION SETUP** status: it says either
|
|
330
|
+
`verification configured — <command>` or prominently reports
|
|
331
|
+
`INCOMPLETE: verification not configured — CI will fail closed`. It can suggest a
|
|
332
|
+
single high-confidence command such as `npm test`, `pytest`, `tox`, `cargo test`
|
|
333
|
+
or `go test ./...`; multiple candidates are listed without choosing one. Suggestions
|
|
334
|
+
are advisory only — init never writes an inferred verifier command because that command
|
|
335
|
+
is part of the trust anchor. In `.tamperward.yml`:
|
|
330
336
|
|
|
331
337
|
```yaml
|
|
332
338
|
verify:
|
|
@@ -369,6 +375,18 @@ restored `"test": "sh scripts/test.sh"` will happily call a script nothing
|
|
|
369
375
|
restored. It bounds the class rather than closing it — see
|
|
370
376
|
[the threat model](./docs/THREAT-MODEL-pristine-run.md).
|
|
371
377
|
|
|
378
|
+
From **2.16.0**, verifier suite output is diagnostic evidence instead of discarded
|
|
379
|
+
noise. Both visible and pristine stages continuously drain stdout/stderr through a
|
|
380
|
+
trusted supervisor, retain only the final **16 KiB per stream**, and count the total
|
|
381
|
+
bytes observed. Structured output exposes this under
|
|
382
|
+
`visible.diagnostics` / `pristine.diagnostics` with
|
|
383
|
+
`captured_bytes`, `retained_bytes`, `truncated`, and a bounded `tail` on a
|
|
384
|
+
failed stage. Default human output remains quiet on success. On failure, retained
|
|
385
|
+
diagnostics are rendered with every line prefixed and terminal/control characters
|
|
386
|
+
escaped, so candidate output cannot become ANSI control traffic or a GitHub
|
|
387
|
+
`::command::`. These bytes are evidence produced by candidate code; they never own
|
|
388
|
+
the verifier verdict.
|
|
389
|
+
|
|
372
390
|
The four primitives:
|
|
373
391
|
|
|
374
392
|
```bash
|
package/dist/cli/index.js
CHANGED
|
@@ -7085,13 +7085,14 @@ function watchTree(dir, cb, onState, onError) {
|
|
|
7085
7085
|
}
|
|
7086
7086
|
}
|
|
7087
7087
|
const watchers = /* @__PURE__ */ new Map();
|
|
7088
|
+
let closed = false;
|
|
7088
7089
|
onState("fallback", 0);
|
|
7089
7090
|
const addDir = (rel) => {
|
|
7090
|
-
if (watchers.has(rel) || SKIP2.test(rel + "/")) return;
|
|
7091
|
+
if (closed || watchers.has(rel) || SKIP2.test(rel + "/")) return;
|
|
7091
7092
|
let w;
|
|
7092
7093
|
try {
|
|
7093
7094
|
w = watch(rel ? join11(dir, rel) : dir, (kind, fname) => {
|
|
7094
|
-
if (!fname) return;
|
|
7095
|
+
if (closed || !fname) return;
|
|
7095
7096
|
const child = rel ? `${rel}/${String(fname)}` : String(fname);
|
|
7096
7097
|
cb(kind, child);
|
|
7097
7098
|
try {
|
|
@@ -7129,9 +7130,14 @@ function watchTree(dir, cb, onState, onError) {
|
|
|
7129
7130
|
}
|
|
7130
7131
|
};
|
|
7131
7132
|
walk2("");
|
|
7132
|
-
return {
|
|
7133
|
-
|
|
7134
|
-
|
|
7133
|
+
return {
|
|
7134
|
+
close: () => {
|
|
7135
|
+
if (closed) return;
|
|
7136
|
+
closed = true;
|
|
7137
|
+
for (const w of watchers.values()) w.close();
|
|
7138
|
+
watchers.clear();
|
|
7139
|
+
}
|
|
7140
|
+
};
|
|
7135
7141
|
}
|
|
7136
7142
|
function startWatcher(dir, log, policy) {
|
|
7137
7143
|
try {
|
|
@@ -7215,8 +7221,11 @@ function startWatcher(dir, log, policy) {
|
|
|
7215
7221
|
},
|
|
7216
7222
|
(detail) => degrade(detail)
|
|
7217
7223
|
);
|
|
7224
|
+
let closed = false;
|
|
7218
7225
|
return {
|
|
7219
7226
|
close: () => {
|
|
7227
|
+
if (closed) return;
|
|
7228
|
+
closed = true;
|
|
7220
7229
|
tree.close();
|
|
7221
7230
|
health.state = "stopped";
|
|
7222
7231
|
health.stopped_at = (/* @__PURE__ */ new Date()).toISOString();
|
|
@@ -8164,6 +8173,78 @@ function hooksShapeError(hooks) {
|
|
|
8164
8173
|
}
|
|
8165
8174
|
return null;
|
|
8166
8175
|
}
|
|
8176
|
+
function verifierCandidates(cwd) {
|
|
8177
|
+
const candidates = [];
|
|
8178
|
+
const add = (command) => {
|
|
8179
|
+
if (!candidates.includes(command)) candidates.push(command);
|
|
8180
|
+
};
|
|
8181
|
+
try {
|
|
8182
|
+
const pkg = JSON.parse(readFileSync11(join12(cwd, "package.json"), "utf8"));
|
|
8183
|
+
const test = pkg.scripts?.test;
|
|
8184
|
+
if (typeof test === "string" && test.trim() !== "" && !/error:\s*no test specified/i.test(test)) add("npm test");
|
|
8185
|
+
} catch {
|
|
8186
|
+
}
|
|
8187
|
+
try {
|
|
8188
|
+
const pyproject = readFileSync11(join12(cwd, "pyproject.toml"), "utf8");
|
|
8189
|
+
if (/^\s*\[tool\.pytest(?:\.ini_options)?\]\s*$/m.test(pyproject)) add("pytest");
|
|
8190
|
+
} catch {
|
|
8191
|
+
}
|
|
8192
|
+
if (existsSync9(join12(cwd, "pytest.ini"))) add("pytest");
|
|
8193
|
+
try {
|
|
8194
|
+
const setupCfg = readFileSync11(join12(cwd, "setup.cfg"), "utf8");
|
|
8195
|
+
if (/^\s*\[tool:pytest\]\s*$/m.test(setupCfg)) add("pytest");
|
|
8196
|
+
} catch {
|
|
8197
|
+
}
|
|
8198
|
+
try {
|
|
8199
|
+
const tox = readFileSync11(join12(cwd, "tox.ini"), "utf8");
|
|
8200
|
+
if (/^\s*\[tox\]\s*$/m.test(tox)) add("tox");
|
|
8201
|
+
} catch {
|
|
8202
|
+
}
|
|
8203
|
+
try {
|
|
8204
|
+
const cargo = readFileSync11(join12(cwd, "Cargo.toml"), "utf8");
|
|
8205
|
+
if (/^\s*\[(?:package|workspace)\]\s*(?:#.*)?$/m.test(cargo)) add("cargo test");
|
|
8206
|
+
} catch {
|
|
8207
|
+
}
|
|
8208
|
+
try {
|
|
8209
|
+
const goMod = readFileSync11(join12(cwd, "go.mod"), "utf8");
|
|
8210
|
+
if (/^\s*module\s+\S+\s*(?:\/\/.*)?$/m.test(goMod)) add("go test ./...");
|
|
8211
|
+
} catch {
|
|
8212
|
+
}
|
|
8213
|
+
return candidates;
|
|
8214
|
+
}
|
|
8215
|
+
function verifierSetupMessage(cwd) {
|
|
8216
|
+
try {
|
|
8217
|
+
const policy = loadPolicy(cwd);
|
|
8218
|
+
const command = policy.verify?.command?.trim();
|
|
8219
|
+
if (command) return `verification configured \u2014 ${command}`;
|
|
8220
|
+
} catch (e) {
|
|
8221
|
+
return `INCOMPLETE: verification not configured \u2014 policy cannot currently be loaded; CI will fail closed until the policy is fixed (${errText3(e)}).`;
|
|
8222
|
+
}
|
|
8223
|
+
const candidates = verifierCandidates(cwd);
|
|
8224
|
+
const lines = [
|
|
8225
|
+
"INCOMPLETE: verification not configured \u2014 CI will fail closed until .tamperward.yml names a trusted suite command."
|
|
8226
|
+
];
|
|
8227
|
+
if (candidates.length === 1) {
|
|
8228
|
+
lines.push(`Suggested verifier command: ${candidates[0]}`);
|
|
8229
|
+
lines.push("Review it, then add:");
|
|
8230
|
+
lines.push("verify:");
|
|
8231
|
+
lines.push(` command: "${candidates[0]}"`);
|
|
8232
|
+
lines.push(" budget: 300");
|
|
8233
|
+
} else if (candidates.length > 1) {
|
|
8234
|
+
lines.push(`Detected verifier candidates: ${candidates.join(", ")}`);
|
|
8235
|
+
lines.push("Choose the suite that defines merge authority, then add for example:");
|
|
8236
|
+
lines.push("verify:");
|
|
8237
|
+
lines.push(' command: "<reviewed suite command>"');
|
|
8238
|
+
lines.push(" budget: 300");
|
|
8239
|
+
} else {
|
|
8240
|
+
lines.push("Add the reviewed suite command to .tamperward.yml, for example:");
|
|
8241
|
+
lines.push("verify:");
|
|
8242
|
+
lines.push(' command: "<your test command>"');
|
|
8243
|
+
lines.push(" budget: 300");
|
|
8244
|
+
}
|
|
8245
|
+
lines.push("TamperWard does not write an inferred verifier command automatically because it is part of the trust anchor.");
|
|
8246
|
+
return lines.join("\n");
|
|
8247
|
+
}
|
|
8167
8248
|
function planPolicy(cwd) {
|
|
8168
8249
|
const path = join12(cwd, POLICY_FILE);
|
|
8169
8250
|
if (existsSync9(path)) {
|
|
@@ -8554,6 +8635,10 @@ WARNING (${a.item}): ${a.warning}
|
|
|
8554
8635
|
}
|
|
8555
8636
|
const errors = plan.filter((a) => a.status === "error");
|
|
8556
8637
|
const changed = opts.dryRun ? plan.filter((a) => a.apply).length : applied;
|
|
8638
|
+
w.write(`
|
|
8639
|
+
VERIFICATION SETUP
|
|
8640
|
+
${verifierSetupMessage(cwd)}
|
|
8641
|
+
`);
|
|
8557
8642
|
if (errors.length) {
|
|
8558
8643
|
w.write(`
|
|
8559
8644
|
tamperward init: ${errors.length} item(s) need your attention above; the rest ${opts.dryRun ? "are planned" : "were applied"}.
|
|
@@ -9152,11 +9237,11 @@ function runDoctor(opts = {}) {
|
|
|
9152
9237
|
}
|
|
9153
9238
|
|
|
9154
9239
|
// src/cli/verify.ts
|
|
9155
|
-
import { execFileSync as execFileSync9, spawnSync as
|
|
9240
|
+
import { execFileSync as execFileSync9, spawnSync as spawnSync3 } from "node:child_process";
|
|
9156
9241
|
import { createHash as createHash10 } from "node:crypto";
|
|
9157
|
-
import { chmodSync as chmodSync2, cpSync, lstatSync as lstatSync5, mkdirSync as mkdirSync6, mkdtempSync as
|
|
9158
|
-
import { tmpdir as
|
|
9159
|
-
import { dirname as dirname7, isAbsolute as isAbsolute10, join as
|
|
9242
|
+
import { chmodSync as chmodSync2, cpSync, lstatSync as lstatSync5, mkdirSync as mkdirSync6, mkdtempSync as mkdtempSync3, readFileSync as readFileSync16, readlinkSync as readlinkSync3, rmSync as rmSync4, symlinkSync, writeFileSync as writeFileSync8 } from "node:fs";
|
|
9243
|
+
import { tmpdir as tmpdir3 } from "node:os";
|
|
9244
|
+
import { dirname as dirname7, isAbsolute as isAbsolute10, join as join17, relative as relative5, resolve as resolve7, sep as sep2 } from "node:path";
|
|
9160
9245
|
|
|
9161
9246
|
// src/fingerprint.ts
|
|
9162
9247
|
import { execFileSync as execFileSync8 } from "node:child_process";
|
|
@@ -9539,9 +9624,321 @@ function dependencyEnvironmentSummary(descriptor) {
|
|
|
9539
9624
|
|
|
9540
9625
|
// src/verifier-backend.ts
|
|
9541
9626
|
import { createHash as createHash9, randomUUID } from "node:crypto";
|
|
9627
|
+
import { spawnSync as spawnSync2 } from "node:child_process";
|
|
9628
|
+
import { accessSync, constants as constants2, readFileSync as readFileSync15, realpathSync as realpathSync4, statSync as statSync4 } from "node:fs";
|
|
9629
|
+
import { delimiter as delimiter2, dirname as dirname6, join as join16, resolve as resolve6 } from "node:path";
|
|
9630
|
+
|
|
9631
|
+
// src/suite-diagnostics.ts
|
|
9542
9632
|
import { spawnSync } from "node:child_process";
|
|
9543
|
-
import {
|
|
9544
|
-
import {
|
|
9633
|
+
import { mkdtempSync as mkdtempSync2, rmSync as rmSync3, writeFileSync as writeFileSync7 } from "node:fs";
|
|
9634
|
+
import { tmpdir as tmpdir2 } from "node:os";
|
|
9635
|
+
import { join as join15 } from "node:path";
|
|
9636
|
+
var DIAGNOSTIC_TAIL_BYTES = 16 * 1024;
|
|
9637
|
+
var EMPTY_STREAM = {
|
|
9638
|
+
captured_bytes: 0,
|
|
9639
|
+
retained_bytes: 0,
|
|
9640
|
+
truncated: false,
|
|
9641
|
+
tail: ""
|
|
9642
|
+
};
|
|
9643
|
+
function emptySuiteDiagnostics() {
|
|
9644
|
+
return {
|
|
9645
|
+
stdout: { ...EMPTY_STREAM },
|
|
9646
|
+
stderr: { ...EMPTY_STREAM }
|
|
9647
|
+
};
|
|
9648
|
+
}
|
|
9649
|
+
function decodeUtf8Tail(tail) {
|
|
9650
|
+
let start = 0;
|
|
9651
|
+
while (start < tail.length && (tail[start] & 192) === 128) start++;
|
|
9652
|
+
return tail.subarray(start).toString("utf8");
|
|
9653
|
+
}
|
|
9654
|
+
function streamFromRaw(raw) {
|
|
9655
|
+
let tail = Buffer.alloc(0);
|
|
9656
|
+
try {
|
|
9657
|
+
tail = Buffer.from(raw?.tail_b64 ?? "", "base64");
|
|
9658
|
+
} catch {
|
|
9659
|
+
tail = Buffer.alloc(0);
|
|
9660
|
+
}
|
|
9661
|
+
const captured = Number.isFinite(raw?.captured_bytes) ? Math.max(0, Number(raw?.captured_bytes)) : tail.length;
|
|
9662
|
+
return {
|
|
9663
|
+
captured_bytes: captured,
|
|
9664
|
+
retained_bytes: tail.length,
|
|
9665
|
+
truncated: captured > tail.length,
|
|
9666
|
+
tail: decodeUtf8Tail(tail)
|
|
9667
|
+
};
|
|
9668
|
+
}
|
|
9669
|
+
function scrubDiagnosticText(input) {
|
|
9670
|
+
let out2 = "";
|
|
9671
|
+
for (const ch of input) {
|
|
9672
|
+
const cp = ch.codePointAt(0);
|
|
9673
|
+
if (ch === "\n" || ch === " ") {
|
|
9674
|
+
out2 += ch;
|
|
9675
|
+
} else if (cp === 13) {
|
|
9676
|
+
out2 += "\\r";
|
|
9677
|
+
} else if (cp < 32 || cp >= 127 && cp <= 159) {
|
|
9678
|
+
out2 += cp <= 255 ? `\\x${cp.toString(16).padStart(2, "0")}` : `\\u{${cp.toString(16)}}`;
|
|
9679
|
+
} else {
|
|
9680
|
+
out2 += ch;
|
|
9681
|
+
}
|
|
9682
|
+
}
|
|
9683
|
+
return out2;
|
|
9684
|
+
}
|
|
9685
|
+
function diagnosticLines(stage, diagnostics) {
|
|
9686
|
+
const lines = [];
|
|
9687
|
+
for (const streamName of ["stdout", "stderr"]) {
|
|
9688
|
+
const stream = diagnostics[streamName];
|
|
9689
|
+
if (stream.captured_bytes === 0) continue;
|
|
9690
|
+
const suffix = stream.truncated ? ` \u2014 tail ${stream.retained_bytes}/${stream.captured_bytes} bytes retained` : ` \u2014 ${stream.captured_bytes} bytes`;
|
|
9691
|
+
lines.push(`${stage} suite ${streamName}${suffix}:`);
|
|
9692
|
+
const scrubbed = scrubDiagnosticText(stream.tail);
|
|
9693
|
+
const body = scrubbed.split("\n");
|
|
9694
|
+
if (body.at(-1) === "") body.pop();
|
|
9695
|
+
for (const line of body) lines.push(` | ${line}`);
|
|
9696
|
+
}
|
|
9697
|
+
return lines;
|
|
9698
|
+
}
|
|
9699
|
+
var CAPTURE_SUPERVISOR = String.raw`
|
|
9700
|
+
const cp = require('node:child_process');
|
|
9701
|
+
const fs = require('node:fs');
|
|
9702
|
+
|
|
9703
|
+
const [configFile] = process.argv.slice(1);
|
|
9704
|
+
const cfg = JSON.parse(fs.readFileSync(configFile, 'utf8'));
|
|
9705
|
+
const CAP = Number(cfg.captureBytes);
|
|
9706
|
+
|
|
9707
|
+
const fresh = () => ({ total: 0, tail: Buffer.alloc(0) });
|
|
9708
|
+
const stdout = fresh();
|
|
9709
|
+
const stderr = fresh();
|
|
9710
|
+
|
|
9711
|
+
// Linux ordinary-exit hardening (#371): process-group ownership alone misses a
|
|
9712
|
+
// descendant that calls setsid(). Track the real descendant tree while the
|
|
9713
|
+
// suite is alive so a reparented session escape is still known when the main
|
|
9714
|
+
// child exits. This supplements, rather than replaces, group termination.
|
|
9715
|
+
const trackedDescendants = new Set();
|
|
9716
|
+
|
|
9717
|
+
function linuxDescendants(rootPid) {
|
|
9718
|
+
if (process.platform !== 'linux' || !rootPid) return [];
|
|
9719
|
+
const byParent = new Map();
|
|
9720
|
+
let names = [];
|
|
9721
|
+
try { names = fs.readdirSync('/proc').filter((x) => /^\d+$/.test(x)); } catch { return []; }
|
|
9722
|
+
for (const name of names) {
|
|
9723
|
+
const pid = Number(name);
|
|
9724
|
+
try {
|
|
9725
|
+
const stat = fs.readFileSync('/proc/' + name + '/stat', 'utf8');
|
|
9726
|
+
const fields = stat.slice(stat.lastIndexOf(')') + 2).split(' ');
|
|
9727
|
+
const ppid = Number(fields[1]);
|
|
9728
|
+
if (!Number.isFinite(ppid)) continue;
|
|
9729
|
+
const kids = byParent.get(ppid) || [];
|
|
9730
|
+
kids.push(pid);
|
|
9731
|
+
byParent.set(ppid, kids);
|
|
9732
|
+
} catch {}
|
|
9733
|
+
}
|
|
9734
|
+
const out = [];
|
|
9735
|
+
const seen = new Set([rootPid]);
|
|
9736
|
+
const stack = [rootPid];
|
|
9737
|
+
while (stack.length) {
|
|
9738
|
+
const parent = stack.pop();
|
|
9739
|
+
for (const pid of (byParent.get(parent) || [])) {
|
|
9740
|
+
if (seen.has(pid)) continue;
|
|
9741
|
+
seen.add(pid);
|
|
9742
|
+
out.push(pid);
|
|
9743
|
+
stack.push(pid);
|
|
9744
|
+
}
|
|
9745
|
+
}
|
|
9746
|
+
return out;
|
|
9747
|
+
}
|
|
9748
|
+
|
|
9749
|
+
function trackDescendants() {
|
|
9750
|
+
if (!child || !child.pid || process.platform !== 'linux') return;
|
|
9751
|
+
for (const pid of linuxDescendants(child.pid)) trackedDescendants.add(pid);
|
|
9752
|
+
}
|
|
9753
|
+
|
|
9754
|
+
function append(state, chunk) {
|
|
9755
|
+
const b = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
9756
|
+
state.total += b.length;
|
|
9757
|
+
if (b.length >= CAP) {
|
|
9758
|
+
state.tail = Buffer.from(b.subarray(b.length - CAP));
|
|
9759
|
+
return;
|
|
9760
|
+
}
|
|
9761
|
+
if (state.tail.length + b.length <= CAP) {
|
|
9762
|
+
state.tail = Buffer.concat([state.tail, b]);
|
|
9763
|
+
return;
|
|
9764
|
+
}
|
|
9765
|
+
const keep = CAP - b.length;
|
|
9766
|
+
state.tail = Buffer.concat([state.tail.subarray(state.tail.length - keep), b]);
|
|
9767
|
+
}
|
|
9768
|
+
|
|
9769
|
+
function diag(state) {
|
|
9770
|
+
return {
|
|
9771
|
+
captured_bytes: state.total,
|
|
9772
|
+
tail_b64: state.tail.toString('base64'),
|
|
9773
|
+
};
|
|
9774
|
+
}
|
|
9775
|
+
|
|
9776
|
+
let child;
|
|
9777
|
+
let done = false;
|
|
9778
|
+
let timedOut = false;
|
|
9779
|
+
let exitInfo = { exit: null, signal: null };
|
|
9780
|
+
let descendantTracker = null;
|
|
9781
|
+
|
|
9782
|
+
function killOwned() {
|
|
9783
|
+
if (!child || !child.pid) return;
|
|
9784
|
+
trackDescendants();
|
|
9785
|
+
if (cfg.detached && process.platform !== 'win32') {
|
|
9786
|
+
try { process.kill(-child.pid, 'SIGKILL'); } catch {}
|
|
9787
|
+
}
|
|
9788
|
+
try { child.kill('SIGKILL'); } catch {}
|
|
9789
|
+
if (process.platform === 'linux') {
|
|
9790
|
+
// Deepest-first is friendlier to short-lived process trees. PIDs are still
|
|
9791
|
+
// rechecked by kill(2); races with natural exit are harmless.
|
|
9792
|
+
for (const pid of Array.from(trackedDescendants).reverse()) {
|
|
9793
|
+
try { process.kill(pid, 'SIGKILL'); } catch {}
|
|
9794
|
+
}
|
|
9795
|
+
}
|
|
9796
|
+
}
|
|
9797
|
+
|
|
9798
|
+
function finish(extra) {
|
|
9799
|
+
if (done) return;
|
|
9800
|
+
done = true;
|
|
9801
|
+
if (descendantTracker) clearInterval(descendantTracker);
|
|
9802
|
+
try {
|
|
9803
|
+
// stdout is reserved for the trusted supervisor result. Candidate suite
|
|
9804
|
+
// stdout/stderr are separate pipes and are never inherited here.
|
|
9805
|
+
process.stdout.write(JSON.stringify({
|
|
9806
|
+
...exitInfo,
|
|
9807
|
+
timedOut,
|
|
9808
|
+
...extra,
|
|
9809
|
+
stdout: diag(stdout),
|
|
9810
|
+
stderr: diag(stderr),
|
|
9811
|
+
}));
|
|
9812
|
+
} catch {}
|
|
9813
|
+
process.exit(0);
|
|
9814
|
+
}
|
|
9815
|
+
|
|
9816
|
+
try {
|
|
9817
|
+
child = cp.spawn(cfg.executable, cfg.args, {
|
|
9818
|
+
cwd: cfg.cwd,
|
|
9819
|
+
env: process.env,
|
|
9820
|
+
detached: Boolean(cfg.detached) && process.platform !== 'win32',
|
|
9821
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
9822
|
+
});
|
|
9823
|
+
} catch (e) {
|
|
9824
|
+
finish({ error: String(e) });
|
|
9825
|
+
}
|
|
9826
|
+
|
|
9827
|
+
if (child) {
|
|
9828
|
+
if (process.platform === 'linux') {
|
|
9829
|
+
trackDescendants();
|
|
9830
|
+
descendantTracker = setInterval(trackDescendants, 5);
|
|
9831
|
+
descendantTracker.unref();
|
|
9832
|
+
}
|
|
9833
|
+
child.stdout.on('data', (b) => append(stdout, b));
|
|
9834
|
+
child.stderr.on('data', (b) => append(stderr, b));
|
|
9835
|
+
|
|
9836
|
+
const timer = setTimeout(() => {
|
|
9837
|
+
timedOut = true;
|
|
9838
|
+
killOwned();
|
|
9839
|
+
// Backstop if a platform fails to emit close after the forced kill.
|
|
9840
|
+
setTimeout(() => finish({}), 1000).unref();
|
|
9841
|
+
}, Number(cfg.timeoutMs));
|
|
9842
|
+
timer.unref();
|
|
9843
|
+
|
|
9844
|
+
child.on('error', (e) => {
|
|
9845
|
+
clearTimeout(timer);
|
|
9846
|
+
killOwned();
|
|
9847
|
+
finish({ error: String(e) });
|
|
9848
|
+
});
|
|
9849
|
+
|
|
9850
|
+
child.on('exit', (code, signal) => {
|
|
9851
|
+
exitInfo = {
|
|
9852
|
+
exit: code == null ? null : Number(code),
|
|
9853
|
+
signal: signal == null ? null : String(signal),
|
|
9854
|
+
};
|
|
9855
|
+
if (cfg.killGroupOnFinish) killOwned();
|
|
9856
|
+
});
|
|
9857
|
+
|
|
9858
|
+
// close fires after the stdio pipes close, so the retained tails include all
|
|
9859
|
+
// output that was drained before owned descendants were terminated.
|
|
9860
|
+
child.on('close', (code, signal) => {
|
|
9861
|
+
clearTimeout(timer);
|
|
9862
|
+
if (exitInfo.exit === null && code != null) exitInfo.exit = Number(code);
|
|
9863
|
+
if (exitInfo.signal === null && signal != null) exitInfo.signal = String(signal);
|
|
9864
|
+
finish({});
|
|
9865
|
+
});
|
|
9866
|
+
}
|
|
9867
|
+
`;
|
|
9868
|
+
function parseCapturedSupervisorResult(stdoutText) {
|
|
9869
|
+
let raw;
|
|
9870
|
+
try {
|
|
9871
|
+
raw = JSON.parse(stdoutText);
|
|
9872
|
+
} catch {
|
|
9873
|
+
return null;
|
|
9874
|
+
}
|
|
9875
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return null;
|
|
9876
|
+
const diagnostics = {
|
|
9877
|
+
stdout: streamFromRaw(raw.stdout),
|
|
9878
|
+
stderr: streamFromRaw(raw.stderr)
|
|
9879
|
+
};
|
|
9880
|
+
return {
|
|
9881
|
+
exit: typeof raw.exit === "number" ? raw.exit : null,
|
|
9882
|
+
signal: typeof raw.signal === "string" ? raw.signal : null,
|
|
9883
|
+
timedOut: Boolean(raw.timedOut),
|
|
9884
|
+
...typeof raw.error === "string" && raw.error ? { error: raw.error } : {},
|
|
9885
|
+
diagnostics
|
|
9886
|
+
};
|
|
9887
|
+
}
|
|
9888
|
+
function runCapturedProcessSync(executable, args, opts) {
|
|
9889
|
+
const stateDir = mkdtempSync2(join15(tmpdir2(), "tw-suite-capture-"));
|
|
9890
|
+
const configFile = join15(stateDir, "config.json");
|
|
9891
|
+
const backstop = opts.backstopMs ?? 3e4;
|
|
9892
|
+
try {
|
|
9893
|
+
writeFileSync7(
|
|
9894
|
+
configFile,
|
|
9895
|
+
JSON.stringify({
|
|
9896
|
+
executable,
|
|
9897
|
+
args,
|
|
9898
|
+
cwd: opts.cwd,
|
|
9899
|
+
timeoutMs: opts.timeoutMs,
|
|
9900
|
+
detached: Boolean(opts.detached),
|
|
9901
|
+
killGroupOnFinish: Boolean(opts.killGroupOnFinish),
|
|
9902
|
+
captureBytes: DIAGNOSTIC_TAIL_BYTES
|
|
9903
|
+
}),
|
|
9904
|
+
{ mode: 384 }
|
|
9905
|
+
);
|
|
9906
|
+
const supervisor = spawnSync(
|
|
9907
|
+
process.execPath,
|
|
9908
|
+
["-e", CAPTURE_SUPERVISOR, configFile],
|
|
9909
|
+
{
|
|
9910
|
+
cwd: opts.cwd,
|
|
9911
|
+
env: opts.env,
|
|
9912
|
+
encoding: "utf8",
|
|
9913
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
9914
|
+
// The trusted result is at most two 16 KiB tails plus small metadata.
|
|
9915
|
+
// A candidate that somehow writes into the supervisor's stdout through
|
|
9916
|
+
// same-UID /proc can only overflow/corrupt this bounded channel and make
|
|
9917
|
+
// parsing fail closed; its suite streams are never inherited here.
|
|
9918
|
+
maxBuffer: 256 * 1024,
|
|
9919
|
+
timeout: opts.timeoutMs + backstop,
|
|
9920
|
+
killSignal: "SIGKILL"
|
|
9921
|
+
}
|
|
9922
|
+
);
|
|
9923
|
+
const parsed = parseCapturedSupervisorResult(supervisor.stdout ?? "");
|
|
9924
|
+
const diagnostics = parsed?.diagnostics ?? emptySuiteDiagnostics();
|
|
9925
|
+
if (!parsed) {
|
|
9926
|
+
const supervisorTimedOut = Boolean(supervisor.error) && supervisor.error.code === "ETIMEDOUT";
|
|
9927
|
+
return {
|
|
9928
|
+
exit: null,
|
|
9929
|
+
signal: supervisor.signal ? String(supervisor.signal) : null,
|
|
9930
|
+
timedOut: supervisorTimedOut,
|
|
9931
|
+
error: supervisor.error?.message ?? "suite capture supervisor did not produce a result",
|
|
9932
|
+
diagnostics
|
|
9933
|
+
};
|
|
9934
|
+
}
|
|
9935
|
+
return parsed;
|
|
9936
|
+
} finally {
|
|
9937
|
+
rmSync3(stateDir, { recursive: true, force: true });
|
|
9938
|
+
}
|
|
9939
|
+
}
|
|
9940
|
+
|
|
9941
|
+
// src/verifier-backend.ts
|
|
9545
9942
|
var DEFAULT_CONTAINER_RESOURCE_LIMITS = Object.freeze({
|
|
9546
9943
|
memoryBytes: 2 * 1024 * 1024 * 1024,
|
|
9547
9944
|
// Docker interprets memory-swap == memory as no additional swap allowance.
|
|
@@ -9584,7 +9981,7 @@ function replaceableByCaller(path) {
|
|
|
9584
9981
|
function findExecutable(name) {
|
|
9585
9982
|
for (const raw of (process.env.PATH ?? "").split(delimiter2)) {
|
|
9586
9983
|
if (!raw) continue;
|
|
9587
|
-
const candidate =
|
|
9984
|
+
const candidate = join16(raw, name);
|
|
9588
9985
|
try {
|
|
9589
9986
|
accessSync(candidate, constants2.X_OK);
|
|
9590
9987
|
return realpathSync4(candidate);
|
|
@@ -9594,7 +9991,7 @@ function findExecutable(name) {
|
|
|
9594
9991
|
return null;
|
|
9595
9992
|
}
|
|
9596
9993
|
function fileSha256(path) {
|
|
9597
|
-
return createHash9("sha256").update(
|
|
9994
|
+
return createHash9("sha256").update(readFileSync15(path)).digest("hex");
|
|
9598
9995
|
}
|
|
9599
9996
|
function engineClientEnv() {
|
|
9600
9997
|
return {
|
|
@@ -9609,7 +10006,7 @@ function dockerArgs(host, args) {
|
|
|
9609
10006
|
}
|
|
9610
10007
|
function dockerAvailable(enginePath, host) {
|
|
9611
10008
|
try {
|
|
9612
|
-
const r =
|
|
10009
|
+
const r = spawnSync2(enginePath, dockerArgs(host, ["version"]), {
|
|
9613
10010
|
stdio: "ignore",
|
|
9614
10011
|
timeout: 5e3,
|
|
9615
10012
|
killSignal: "SIGKILL",
|
|
@@ -9637,7 +10034,7 @@ function declaredImageVolumePaths(metadata) {
|
|
|
9637
10034
|
}
|
|
9638
10035
|
function inspectImageMetadata(enginePath, host, image) {
|
|
9639
10036
|
try {
|
|
9640
|
-
const r =
|
|
10037
|
+
const r = spawnSync2(enginePath, dockerArgs(host, ["image", "inspect", image]), {
|
|
9641
10038
|
encoding: "utf8",
|
|
9642
10039
|
stdio: ["ignore", "pipe", "ignore"],
|
|
9643
10040
|
timeout: 1e4,
|
|
@@ -9812,7 +10209,7 @@ function verifierBackendSummary(backend) {
|
|
|
9812
10209
|
function cleanupContainer(backend, name) {
|
|
9813
10210
|
if (!backend.enginePath || !backend.daemonHost || !engineStable(backend)) return;
|
|
9814
10211
|
try {
|
|
9815
|
-
|
|
10212
|
+
spawnSync2(backend.enginePath, dockerArgs(backend.daemonHost, ["rm", "-f", "-v", name]), {
|
|
9816
10213
|
stdio: "ignore",
|
|
9817
10214
|
timeout: 1e4,
|
|
9818
10215
|
killSignal: "SIGKILL",
|
|
@@ -9923,29 +10320,45 @@ function runContainerStage(backend, dir, command, budgetSecs) {
|
|
|
9923
10320
|
resources: backend.resources ?? DEFAULT_CONTAINER_RESOURCE_LIMITS
|
|
9924
10321
|
});
|
|
9925
10322
|
try {
|
|
9926
|
-
const r =
|
|
10323
|
+
const r = runCapturedProcessSync(
|
|
9927
10324
|
backend.enginePath,
|
|
9928
10325
|
dockerArgs(backend.daemonHost, args),
|
|
9929
10326
|
{
|
|
9930
|
-
|
|
9931
|
-
|
|
9932
|
-
|
|
9933
|
-
|
|
10327
|
+
cwd: dirname6(backend.enginePath),
|
|
10328
|
+
env: engineClientEnv(),
|
|
10329
|
+
timeoutMs: budgetSecs * 1e3,
|
|
10330
|
+
backstopMs: 3e4
|
|
9934
10331
|
}
|
|
9935
10332
|
);
|
|
9936
10333
|
const secs = Math.round((Date.now() - t0) / 1e3);
|
|
10334
|
+
if (r.timedOut) {
|
|
10335
|
+
cleanupContainer(backend, name);
|
|
10336
|
+
return {
|
|
10337
|
+
exit: null,
|
|
10338
|
+
secs,
|
|
10339
|
+
failure: "budget",
|
|
10340
|
+
reason: "verifier stage exceeded its budget",
|
|
10341
|
+
diagnostics: r.diagnostics
|
|
10342
|
+
};
|
|
10343
|
+
}
|
|
9937
10344
|
if (r.error) {
|
|
9938
10345
|
cleanupContainer(backend, name);
|
|
9939
|
-
|
|
9940
|
-
|
|
10346
|
+
return {
|
|
10347
|
+
exit: null,
|
|
10348
|
+
secs,
|
|
10349
|
+
failure: "backend",
|
|
10350
|
+
reason: `Docker client failed: ${r.error}`,
|
|
10351
|
+
diagnostics: r.diagnostics
|
|
10352
|
+
};
|
|
9941
10353
|
}
|
|
9942
|
-
if (r.
|
|
10354
|
+
if (r.exit === null) {
|
|
9943
10355
|
cleanupContainer(backend, name);
|
|
9944
10356
|
return {
|
|
9945
10357
|
exit: null,
|
|
9946
10358
|
secs,
|
|
9947
10359
|
failure: "backend",
|
|
9948
|
-
reason: `Docker client did not return a status${r.signal ? ` (signal ${r.signal})` : ""}
|
|
10360
|
+
reason: `Docker client did not return a status${r.signal ? ` (signal ${r.signal})` : ""}`,
|
|
10361
|
+
diagnostics: r.diagnostics
|
|
9949
10362
|
};
|
|
9950
10363
|
}
|
|
9951
10364
|
if (!engineStable(backend)) {
|
|
@@ -9953,10 +10366,11 @@ function runContainerStage(backend, dir, command, budgetSecs) {
|
|
|
9953
10366
|
exit: null,
|
|
9954
10367
|
secs,
|
|
9955
10368
|
failure: "backend",
|
|
9956
|
-
reason: "Docker engine identity changed before verifier result inspection"
|
|
10369
|
+
reason: "Docker engine identity changed before verifier result inspection",
|
|
10370
|
+
diagnostics: r.diagnostics
|
|
9957
10371
|
};
|
|
9958
10372
|
}
|
|
9959
|
-
const inspected =
|
|
10373
|
+
const inspected = spawnSync2(
|
|
9960
10374
|
backend.enginePath,
|
|
9961
10375
|
dockerArgs(backend.daemonHost, ["inspect", "--format", "{{json .State}}", name]),
|
|
9962
10376
|
{
|
|
@@ -9976,7 +10390,10 @@ function runContainerStage(backend, dir, command, budgetSecs) {
|
|
|
9976
10390
|
}
|
|
9977
10391
|
}
|
|
9978
10392
|
cleanupContainer(backend, name);
|
|
9979
|
-
return
|
|
10393
|
+
return {
|
|
10394
|
+
...containerStateResult(state, secs),
|
|
10395
|
+
diagnostics: r.diagnostics
|
|
10396
|
+
};
|
|
9980
10397
|
} catch (e) {
|
|
9981
10398
|
cleanupContainer(backend, name);
|
|
9982
10399
|
return {
|
|
@@ -10081,7 +10498,7 @@ function resolveBase(base, cwd) {
|
|
|
10081
10498
|
}
|
|
10082
10499
|
}
|
|
10083
10500
|
function baseIsAncestorOfHead(base, cwd) {
|
|
10084
|
-
const r =
|
|
10501
|
+
const r = spawnSync3("git", ["merge-base", "--is-ancestor", base, "HEAD"], { cwd, env: trustedGitEnv() });
|
|
10085
10502
|
return r.status === 0;
|
|
10086
10503
|
}
|
|
10087
10504
|
function inside2(root, path) {
|
|
@@ -10105,7 +10522,7 @@ function safeSymlink(target, out2, root, label) {
|
|
|
10105
10522
|
if (rootedLinkTarget(target) || !inside2(root, resolve7(dirname7(out2), target))) {
|
|
10106
10523
|
throw linkEscape(label, target);
|
|
10107
10524
|
}
|
|
10108
|
-
|
|
10525
|
+
rmSync4(out2, { force: true });
|
|
10109
10526
|
symlinkSync(target, out2);
|
|
10110
10527
|
}
|
|
10111
10528
|
function validateSymlinkGraph(target, out2, root, dependencyRoot, label) {
|
|
@@ -10128,7 +10545,7 @@ function validateSymlinkGraph(target, out2, root, dependencyRoot, label) {
|
|
|
10128
10545
|
continue;
|
|
10129
10546
|
}
|
|
10130
10547
|
const base = domain === "tree" ? root : dependencyRoot;
|
|
10131
|
-
const current =
|
|
10548
|
+
const current = join17(base, ...stack);
|
|
10132
10549
|
let st;
|
|
10133
10550
|
try {
|
|
10134
10551
|
st = lstatSync5(current);
|
|
@@ -10164,7 +10581,7 @@ function rejectLinkedParent(cwd, rel) {
|
|
|
10164
10581
|
const parts = rel.split("/").slice(0, -1);
|
|
10165
10582
|
let current = cwd;
|
|
10166
10583
|
for (const part of parts) {
|
|
10167
|
-
current =
|
|
10584
|
+
current = join17(current, part);
|
|
10168
10585
|
let st;
|
|
10169
10586
|
try {
|
|
10170
10587
|
st = lstatSync5(current);
|
|
@@ -10180,7 +10597,7 @@ function materialize(cwd, dest, dependencyRoot) {
|
|
|
10180
10597
|
const listed = git2(["ls-files", "-z", "--cached", "--others", "--exclude-standard"], cwd).split("\0").filter(Boolean);
|
|
10181
10598
|
const links = [];
|
|
10182
10599
|
for (const rel of listed) {
|
|
10183
|
-
const src =
|
|
10600
|
+
const src = join17(cwd, rel);
|
|
10184
10601
|
rejectLinkedParent(cwd, rel);
|
|
10185
10602
|
let st;
|
|
10186
10603
|
try {
|
|
@@ -10188,7 +10605,7 @@ function materialize(cwd, dest, dependencyRoot) {
|
|
|
10188
10605
|
} catch {
|
|
10189
10606
|
continue;
|
|
10190
10607
|
}
|
|
10191
|
-
const out2 =
|
|
10608
|
+
const out2 = join17(dest, rel);
|
|
10192
10609
|
mkdirSync6(dirname7(out2), { recursive: true });
|
|
10193
10610
|
dropSymlink(out2);
|
|
10194
10611
|
if (st.isSymbolicLink()) {
|
|
@@ -10205,11 +10622,11 @@ function materialize(cwd, dest, dependencyRoot) {
|
|
|
10205
10622
|
for (const link of links) {
|
|
10206
10623
|
validateSymlinkGraph(link.target, link.out, dest, dependencyRoot, link.rel);
|
|
10207
10624
|
}
|
|
10208
|
-
if (dependencyRoot) symlinkSync(dependencyRoot,
|
|
10625
|
+
if (dependencyRoot) symlinkSync(dependencyRoot, join17(dest, "node_modules"), "dir");
|
|
10209
10626
|
}
|
|
10210
10627
|
function dropSymlink(p) {
|
|
10211
10628
|
try {
|
|
10212
|
-
if (lstatSync5(p).isSymbolicLink())
|
|
10629
|
+
if (lstatSync5(p).isSymbolicLink()) rmSync4(p, { force: true });
|
|
10213
10630
|
} catch {
|
|
10214
10631
|
}
|
|
10215
10632
|
}
|
|
@@ -10238,11 +10655,11 @@ function overlayDigest(dest, paths) {
|
|
|
10238
10655
|
for (const rel of [...paths].sort()) {
|
|
10239
10656
|
h.update(rel);
|
|
10240
10657
|
h.update("\0");
|
|
10241
|
-
const p =
|
|
10658
|
+
const p = join17(dest, rel);
|
|
10242
10659
|
try {
|
|
10243
10660
|
const st = lstatSync5(p);
|
|
10244
10661
|
h.update(String(st.mode));
|
|
10245
|
-
h.update(st.isSymbolicLink() ? readlinkSync3(p) :
|
|
10662
|
+
h.update(st.isSymbolicLink() ? readlinkSync3(p) : readFileSync16(p));
|
|
10246
10663
|
} catch {
|
|
10247
10664
|
h.update("<gone>");
|
|
10248
10665
|
}
|
|
@@ -10266,15 +10683,15 @@ function overlayPristine(cwd, base, dest, policy, cmd, dependencyRoot) {
|
|
|
10266
10683
|
throw new Error(`the base's ${e.path} is a submodule; the pristine copy cannot reproduce it`);
|
|
10267
10684
|
}
|
|
10268
10685
|
const content = execFileSync9("git", ["show", `${base}:${e.path}`], { cwd, maxBuffer: 1 << 28, env: trustedGitEnv() });
|
|
10269
|
-
const out2 =
|
|
10686
|
+
const out2 = join17(dest, e.path);
|
|
10270
10687
|
mkdirSync6(dirname7(out2), { recursive: true });
|
|
10271
|
-
|
|
10688
|
+
rmSync4(out2, { force: true });
|
|
10272
10689
|
if (e.mode === "120000") {
|
|
10273
10690
|
const target = content.toString("utf8");
|
|
10274
10691
|
safeSymlink(target, out2, dest, `the base's ${e.path}`);
|
|
10275
10692
|
restoredLinks.push({ path: e.path, out: out2, target });
|
|
10276
10693
|
} else {
|
|
10277
|
-
|
|
10694
|
+
writeFileSync8(out2, content);
|
|
10278
10695
|
chmodSync2(out2, parseInt(e.mode.slice(-4), 8) & 511);
|
|
10279
10696
|
}
|
|
10280
10697
|
restored.push(e.path);
|
|
@@ -10283,7 +10700,7 @@ function overlayPristine(cwd, base, dest, policy, cmd, dependencyRoot) {
|
|
|
10283
10700
|
const inCopy = git2(["ls-files", "-z", "--cached", "--others", "--exclude-standard"], cwd).split("\0").filter(Boolean);
|
|
10284
10701
|
for (const rel of inCopy) {
|
|
10285
10702
|
if (!isOverlay(rel) || baseProtected.has(rel)) continue;
|
|
10286
|
-
|
|
10703
|
+
rmSync4(join17(dest, rel), { force: true });
|
|
10287
10704
|
removed++;
|
|
10288
10705
|
}
|
|
10289
10706
|
for (const link of restoredLinks) {
|
|
@@ -10291,27 +10708,6 @@ function overlayPristine(cwd, base, dest, policy, cmd, dependencyRoot) {
|
|
|
10291
10708
|
}
|
|
10292
10709
|
return { restored, removed };
|
|
10293
10710
|
}
|
|
10294
|
-
var SUPERVISOR = `
|
|
10295
|
-
const cp = require('node:child_process');
|
|
10296
|
-
const fs = require('node:fs');
|
|
10297
|
-
const [cmd, budgetMs, outFile] = process.argv.slice(1);
|
|
10298
|
-
const child = cp.spawn('sh', ['-c', cmd], { stdio: 'ignore', detached: process.platform !== 'win32' });
|
|
10299
|
-
const killGroup = () => {
|
|
10300
|
-
try { process.kill(-child.pid, 'SIGKILL'); } catch {}
|
|
10301
|
-
try { child.kill('SIGKILL'); } catch {}
|
|
10302
|
-
};
|
|
10303
|
-
let done = false;
|
|
10304
|
-
const finish = (r) => {
|
|
10305
|
-
if (done) return;
|
|
10306
|
-
done = true;
|
|
10307
|
-
killGroup();
|
|
10308
|
-
fs.writeFileSync(outFile, JSON.stringify(r));
|
|
10309
|
-
process.exit(0);
|
|
10310
|
-
};
|
|
10311
|
-
const timer = setTimeout(() => finish({ timedOut: true }), Number(budgetMs));
|
|
10312
|
-
child.on('error', (e) => { clearTimeout(timer); finish({ error: String(e) }); });
|
|
10313
|
-
child.on('exit', (code, signal) => { clearTimeout(timer); finish({ exit: code, signal }); });
|
|
10314
|
-
`;
|
|
10315
10711
|
var DROPPED_ENV = /* @__PURE__ */ new Set([
|
|
10316
10712
|
"NODE_OPTIONS",
|
|
10317
10713
|
"NODE_PATH",
|
|
@@ -10328,9 +10724,9 @@ function suiteEnv(scratch) {
|
|
|
10328
10724
|
if (DROPPED_ENV.has(k) || PINNED_NPM.test(k)) continue;
|
|
10329
10725
|
env[k] = v;
|
|
10330
10726
|
}
|
|
10331
|
-
const userRc =
|
|
10332
|
-
const globalRc =
|
|
10333
|
-
for (const f of [userRc, globalRc])
|
|
10727
|
+
const userRc = join17(scratch, "npmrc-user");
|
|
10728
|
+
const globalRc = join17(scratch, "npmrc-global");
|
|
10729
|
+
for (const f of [userRc, globalRc]) writeFileSync8(f, "", { mode: 292 });
|
|
10334
10730
|
env.npm_config_userconfig = userRc;
|
|
10335
10731
|
env.npm_config_globalconfig = globalRc;
|
|
10336
10732
|
env.npm_config_node_options = " ";
|
|
@@ -10339,31 +10735,75 @@ function suiteEnv(scratch) {
|
|
|
10339
10735
|
}
|
|
10340
10736
|
function runLocalSuite(dir, cmd, budgetSecs) {
|
|
10341
10737
|
const t0 = Date.now();
|
|
10342
|
-
const
|
|
10343
|
-
const outFile = join16(outDir, "result.json");
|
|
10738
|
+
const scratch = mkdtempSync3(join17(tmpdir3(), "tw-verify-run-"));
|
|
10344
10739
|
try {
|
|
10345
|
-
|
|
10740
|
+
const r = runCapturedProcessSync("sh", ["-c", cmd], {
|
|
10346
10741
|
cwd: dir,
|
|
10347
|
-
env: suiteEnv(
|
|
10348
|
-
|
|
10349
|
-
|
|
10350
|
-
|
|
10351
|
-
|
|
10742
|
+
env: suiteEnv(scratch),
|
|
10743
|
+
timeoutMs: budgetSecs * 1e3,
|
|
10744
|
+
detached: process.platform !== "win32",
|
|
10745
|
+
killGroupOnFinish: true,
|
|
10746
|
+
backstopMs: 3e4
|
|
10352
10747
|
});
|
|
10353
10748
|
const secs = Math.round((Date.now() - t0) / 1e3);
|
|
10354
|
-
|
|
10355
|
-
|
|
10356
|
-
|
|
10357
|
-
|
|
10358
|
-
|
|
10749
|
+
if (r.timedOut) {
|
|
10750
|
+
return {
|
|
10751
|
+
exit: null,
|
|
10752
|
+
secs,
|
|
10753
|
+
failure: "budget",
|
|
10754
|
+
reason: "verifier stage exceeded its budget",
|
|
10755
|
+
diagnostics: r.diagnostics
|
|
10756
|
+
};
|
|
10757
|
+
}
|
|
10758
|
+
if (r.error) {
|
|
10759
|
+
return {
|
|
10760
|
+
exit: null,
|
|
10761
|
+
secs,
|
|
10762
|
+
failure: "backend",
|
|
10763
|
+
reason: r.error,
|
|
10764
|
+
diagnostics: r.diagnostics
|
|
10765
|
+
};
|
|
10359
10766
|
}
|
|
10360
|
-
if (r.
|
|
10361
|
-
|
|
10362
|
-
|
|
10767
|
+
if (r.exit === null) {
|
|
10768
|
+
return {
|
|
10769
|
+
exit: null,
|
|
10770
|
+
secs,
|
|
10771
|
+
failure: "backend",
|
|
10772
|
+
reason: `verifier suite did not return an exit code${r.signal ? ` (signal ${r.signal})` : ""}`,
|
|
10773
|
+
diagnostics: r.diagnostics
|
|
10774
|
+
};
|
|
10775
|
+
}
|
|
10776
|
+
return { exit: r.exit, secs, diagnostics: r.diagnostics };
|
|
10363
10777
|
} finally {
|
|
10364
|
-
|
|
10778
|
+
rmSync4(scratch, { recursive: true, force: true });
|
|
10365
10779
|
}
|
|
10366
10780
|
}
|
|
10781
|
+
function diagnosticsJson(diagnostics, includeTail) {
|
|
10782
|
+
if (!diagnostics) return void 0;
|
|
10783
|
+
const stream = (d) => ({
|
|
10784
|
+
captured_bytes: d.captured_bytes,
|
|
10785
|
+
retained_bytes: d.retained_bytes,
|
|
10786
|
+
truncated: d.truncated,
|
|
10787
|
+
...includeTail && d.captured_bytes > 0 ? { tail: d.tail } : {}
|
|
10788
|
+
});
|
|
10789
|
+
return {
|
|
10790
|
+
stdout: stream(diagnostics.stdout),
|
|
10791
|
+
stderr: stream(diagnostics.stderr)
|
|
10792
|
+
};
|
|
10793
|
+
}
|
|
10794
|
+
function stageJson(result) {
|
|
10795
|
+
const includeTail = result.exit !== 0 || result.failure !== void 0;
|
|
10796
|
+
return {
|
|
10797
|
+
exit: result.exit,
|
|
10798
|
+
secs: result.secs,
|
|
10799
|
+
...result.reason ? { reason: result.reason } : {},
|
|
10800
|
+
...result.diagnostics ? { diagnostics: diagnosticsJson(result.diagnostics, includeTail) } : {}
|
|
10801
|
+
};
|
|
10802
|
+
}
|
|
10803
|
+
function renderStageDiagnostics(out2, stage, result) {
|
|
10804
|
+
if (!result.diagnostics) return;
|
|
10805
|
+
for (const line of diagnosticLines(stage, result.diagnostics)) out2(line);
|
|
10806
|
+
}
|
|
10367
10807
|
function runVerify(opts) {
|
|
10368
10808
|
const cwd = opts.cwd ?? process.cwd();
|
|
10369
10809
|
const out2 = (s) => void process.stdout.write(s + "\n");
|
|
@@ -10445,11 +10885,11 @@ function runVerify(opts) {
|
|
|
10445
10885
|
out2(`verify: cannot resolve base rev "${opts.base ?? "HEAD"}" \u2014 failing closed`);
|
|
10446
10886
|
return 2;
|
|
10447
10887
|
}
|
|
10448
|
-
const visRoot =
|
|
10449
|
-
const visDir =
|
|
10888
|
+
const visRoot = mkdtempSync3(join17(tmpdir3(), "tw-verify-vis-"));
|
|
10889
|
+
const visDir = join17(visRoot, "t");
|
|
10450
10890
|
const cleanup = (dirs) => {
|
|
10451
10891
|
if (opts.keep) return;
|
|
10452
|
-
for (const d of dirs)
|
|
10892
|
+
for (const d of dirs) rmSync4(d, { recursive: true, force: true });
|
|
10453
10893
|
};
|
|
10454
10894
|
const protectedIgnored = (rel) => isProtected(rel, policy);
|
|
10455
10895
|
const treeBefore = treeFingerprint(cwd, protectedIgnored);
|
|
@@ -10479,14 +10919,16 @@ function runVerify(opts) {
|
|
|
10479
10919
|
stage: "visible",
|
|
10480
10920
|
...visible.resource ? { resource: visible.resource } : {},
|
|
10481
10921
|
detail: visible.reason,
|
|
10922
|
+
diagnostics: diagnosticsJson(visible.diagnostics, true),
|
|
10482
10923
|
verifier_backend: backendReport(),
|
|
10483
10924
|
oracle_assurance: oracleAssuranceReport()
|
|
10484
10925
|
}));
|
|
10485
10926
|
} else {
|
|
10486
10927
|
out2(
|
|
10487
|
-
exhausted ? "verify: isolated verifier resource envelope was exhausted during the visible stage \u2014 failing closed" : "verify:
|
|
10928
|
+
exhausted ? "verify: isolated verifier resource envelope was exhausted during the visible stage \u2014 failing closed" : "verify: verifier execution backend failed while running the visible stage \u2014 failing closed"
|
|
10488
10929
|
);
|
|
10489
10930
|
if (visible.reason) out2("verify: " + visible.reason);
|
|
10931
|
+
renderStageDiagnostics(out2, "visible", visible);
|
|
10490
10932
|
}
|
|
10491
10933
|
return 2;
|
|
10492
10934
|
}
|
|
@@ -10503,8 +10945,8 @@ function runVerify(opts) {
|
|
|
10503
10945
|
out2((dependencyAfterVisible.reason ?? "dependency identity changed") + ". Failing closed, not open.");
|
|
10504
10946
|
return 2;
|
|
10505
10947
|
}
|
|
10506
|
-
const priRoot =
|
|
10507
|
-
const priDir =
|
|
10948
|
+
const priRoot = mkdtempSync3(join17(tmpdir3(), "tw-verify-pri-"));
|
|
10949
|
+
const priDir = join17(priRoot, "t");
|
|
10508
10950
|
let restored = [];
|
|
10509
10951
|
let removedAdded = 0;
|
|
10510
10952
|
try {
|
|
@@ -10535,14 +10977,16 @@ function runVerify(opts) {
|
|
|
10535
10977
|
stage: "pristine",
|
|
10536
10978
|
...pristine.resource ? { resource: pristine.resource } : {},
|
|
10537
10979
|
detail: pristine.reason,
|
|
10980
|
+
diagnostics: diagnosticsJson(pristine.diagnostics, true),
|
|
10538
10981
|
verifier_backend: backendReport(),
|
|
10539
10982
|
oracle_assurance: oracleAssuranceReport()
|
|
10540
10983
|
}));
|
|
10541
10984
|
} else {
|
|
10542
10985
|
out2(
|
|
10543
|
-
exhausted ? "verify: isolated verifier resource envelope was exhausted during the pristine stage \u2014 failing closed" : "verify:
|
|
10986
|
+
exhausted ? "verify: isolated verifier resource envelope was exhausted during the pristine stage \u2014 failing closed" : "verify: verifier execution backend failed while running the pristine stage \u2014 failing closed"
|
|
10544
10987
|
);
|
|
10545
10988
|
if (pristine.reason) out2("verify: " + pristine.reason);
|
|
10989
|
+
renderStageDiagnostics(out2, "pristine", pristine);
|
|
10546
10990
|
}
|
|
10547
10991
|
return 2;
|
|
10548
10992
|
}
|
|
@@ -10584,8 +11028,8 @@ function runVerify(opts) {
|
|
|
10584
11028
|
base,
|
|
10585
11029
|
command: cmd,
|
|
10586
11030
|
budget_secs: budget,
|
|
10587
|
-
visible:
|
|
10588
|
-
pristine:
|
|
11031
|
+
visible: stageJson(visible),
|
|
11032
|
+
pristine: stageJson(pristine),
|
|
10589
11033
|
protected_restored: restored.length,
|
|
10590
11034
|
added_protected_removed: removedAdded,
|
|
10591
11035
|
verifier_backend: backendReport(),
|
|
@@ -10610,6 +11054,14 @@ function runVerify(opts) {
|
|
|
10610
11054
|
out2(
|
|
10611
11055
|
dependencyEnvironment ? `dependency environment: ${dependencyEnvironmentSummary(dependencyEnvironment)}` + (opts.allowDepDrift && dependencyEnvironment.status === "unattestable" ? " (operator override)" : "") : `dependency environment: verifier-owned by ${verifierBackend.image ?? "isolated image"}`
|
|
10612
11056
|
);
|
|
11057
|
+
if (verdict2 !== "VERIFIED") {
|
|
11058
|
+
if (visible.exit !== 0 || visible.failure !== void 0) {
|
|
11059
|
+
renderStageDiagnostics(out2, "visible", visible);
|
|
11060
|
+
}
|
|
11061
|
+
if (pristine.exit !== 0 || pristine.failure !== void 0) {
|
|
11062
|
+
renderStageDiagnostics(out2, "pristine", pristine);
|
|
11063
|
+
}
|
|
11064
|
+
}
|
|
10613
11065
|
if (signedOff)
|
|
10614
11066
|
out2(
|
|
10615
11067
|
`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.`
|
|
@@ -10654,11 +11106,11 @@ function parseVerify(args) {
|
|
|
10654
11106
|
}
|
|
10655
11107
|
|
|
10656
11108
|
// src/cli/run.ts
|
|
10657
|
-
import { execFileSync as execFileSync10, spawn, spawnSync as
|
|
11109
|
+
import { execFileSync as execFileSync10, spawn, spawnSync as spawnSync4 } from "node:child_process";
|
|
10658
11110
|
import { randomBytes } from "node:crypto";
|
|
10659
|
-
import { mkdtempSync as
|
|
10660
|
-
import { tmpdir as
|
|
10661
|
-
import { join as
|
|
11111
|
+
import { mkdtempSync as mkdtempSync4, readdirSync as readdirSync5, readFileSync as readFileSync17, readlinkSync as readlinkSync4, realpathSync as realpathSync6, rmSync as rmSync5 } from "node:fs";
|
|
11112
|
+
import { tmpdir as tmpdir4 } from "node:os";
|
|
11113
|
+
import { join as join18, resolve as resolve8 } from "node:path";
|
|
10662
11114
|
var out = (s) => process.stdout.write(s + "\n");
|
|
10663
11115
|
var err2 = (s) => process.stderr.write(s + "\n");
|
|
10664
11116
|
var AGENT_SUPERVISOR = String.raw`
|
|
@@ -10778,7 +11230,7 @@ function linuxDescendantPids(rootPid) {
|
|
|
10778
11230
|
for (const name of names) {
|
|
10779
11231
|
const pid = Number(name);
|
|
10780
11232
|
try {
|
|
10781
|
-
const stat =
|
|
11233
|
+
const stat = readFileSync17(`/proc/${name}/stat`, "utf8");
|
|
10782
11234
|
const fields = stat.slice(stat.lastIndexOf(")") + 2).split(" ");
|
|
10783
11235
|
const ppid = Number(fields[1]);
|
|
10784
11236
|
if (!Number.isFinite(ppid)) continue;
|
|
@@ -10806,7 +11258,7 @@ function killAgentTree(pid) {
|
|
|
10806
11258
|
if (!Number.isInteger(pid) || pid <= 1) return;
|
|
10807
11259
|
if (process.platform === "win32") {
|
|
10808
11260
|
try {
|
|
10809
|
-
|
|
11261
|
+
spawnSync4("taskkill", ["/pid", String(pid), "/t", "/f"], { stdio: "ignore", timeout: 5e3 });
|
|
10810
11262
|
} catch {
|
|
10811
11263
|
}
|
|
10812
11264
|
return;
|
|
@@ -10828,11 +11280,11 @@ function killAgentTree(pid) {
|
|
|
10828
11280
|
}
|
|
10829
11281
|
}
|
|
10830
11282
|
function runAgentWithBudget(argv2, cwd, budgetSecs) {
|
|
10831
|
-
const stateDir =
|
|
10832
|
-
const resultFile =
|
|
11283
|
+
const stateDir = mkdtempSync4(join18(tmpdir4(), "tw-agent-supervisor-"));
|
|
11284
|
+
const resultFile = join18(stateDir, "result.json");
|
|
10833
11285
|
let supervisorTimedOut = false;
|
|
10834
11286
|
try {
|
|
10835
|
-
const supervisor =
|
|
11287
|
+
const supervisor = spawnSync4(
|
|
10836
11288
|
process.execPath,
|
|
10837
11289
|
["-e", AGENT_SUPERVISOR, resultFile, String(budgetSecs * 1e3), ...argv2],
|
|
10838
11290
|
{
|
|
@@ -10847,7 +11299,7 @@ function runAgentWithBudget(argv2, cwd, budgetSecs) {
|
|
|
10847
11299
|
supervisorTimedOut = Boolean(supervisor.error) && supervisor.error.code === "ETIMEDOUT";
|
|
10848
11300
|
let state = {};
|
|
10849
11301
|
try {
|
|
10850
|
-
state = JSON.parse(
|
|
11302
|
+
state = JSON.parse(readFileSync17(resultFile, "utf8"));
|
|
10851
11303
|
} catch {
|
|
10852
11304
|
}
|
|
10853
11305
|
if (supervisorTimedOut || state.timedOut) {
|
|
@@ -10874,7 +11326,7 @@ function runAgentWithBudget(argv2, cwd, budgetSecs) {
|
|
|
10874
11326
|
failure: state.failure ?? "agent supervisor did not produce a final status"
|
|
10875
11327
|
};
|
|
10876
11328
|
} finally {
|
|
10877
|
-
|
|
11329
|
+
rmSync5(stateDir, { recursive: true, force: true });
|
|
10878
11330
|
}
|
|
10879
11331
|
}
|
|
10880
11332
|
var waitCell = new Int32Array(new SharedArrayBuffer(4));
|
|
@@ -10891,8 +11343,8 @@ function pidAlive2(pid) {
|
|
|
10891
11343
|
}
|
|
10892
11344
|
}
|
|
10893
11345
|
function supervisedObserverLog(cwd) {
|
|
10894
|
-
const gd = gitDir(cwd) ??
|
|
10895
|
-
return
|
|
11346
|
+
const gd = gitDir(cwd) ?? join18(cwd, ".git");
|
|
11347
|
+
return join18(
|
|
10896
11348
|
gd,
|
|
10897
11349
|
"tamperward",
|
|
10898
11350
|
`run-observer-${process.pid}-${Date.now()}-${randomBytes(4).toString("hex")}.jsonl`
|
|
@@ -11009,7 +11461,7 @@ function git3(args, cwd) {
|
|
|
11009
11461
|
}
|
|
11010
11462
|
function nowTicks() {
|
|
11011
11463
|
try {
|
|
11012
|
-
return Math.floor(parseFloat(
|
|
11464
|
+
return Math.floor(parseFloat(readFileSync17("/proc/uptime", "utf8").split(" ")[0]) * 100);
|
|
11013
11465
|
} catch {
|
|
11014
11466
|
return Number.POSITIVE_INFINITY;
|
|
11015
11467
|
}
|
|
@@ -11032,7 +11484,7 @@ function survivorsHoldingTree(cwd, spawnedAfterTicks) {
|
|
|
11032
11484
|
const n = Number(pid);
|
|
11033
11485
|
if (n === process.pid) continue;
|
|
11034
11486
|
try {
|
|
11035
|
-
const stat =
|
|
11487
|
+
const stat = readFileSync17(`/proc/${pid}/stat`, "utf8");
|
|
11036
11488
|
const startTicks = Number(stat.slice(stat.lastIndexOf(")") + 2).split(" ")[19]);
|
|
11037
11489
|
if (!Number.isFinite(startTicks) || startTicks < spawnedAfterTicks) continue;
|
|
11038
11490
|
} catch {
|
|
@@ -11137,7 +11589,7 @@ function runEnvelope(opts) {
|
|
|
11137
11589
|
const rewritesBefore = objectRewriteState(cwd);
|
|
11138
11590
|
const spawnTicks = nowTicks();
|
|
11139
11591
|
const agentRun = opts.agentBudget === void 0 ? (() => {
|
|
11140
|
-
const agent =
|
|
11592
|
+
const agent = spawnSync4(opts.argv[0], opts.argv.slice(1), { cwd, stdio: "inherit" });
|
|
11141
11593
|
return {
|
|
11142
11594
|
exit: agent.status ?? 1,
|
|
11143
11595
|
timedOut: false,
|
|
@@ -11177,7 +11629,7 @@ tamperward run \u2014 agent exit ${agentExit}; DEPENDENCY_DRIFT \u2192 ENFORCEME
|
|
|
11177
11629
|
err2("(--allow-dep-drift: proceeding anyway, on the operator's judgement.)");
|
|
11178
11630
|
}
|
|
11179
11631
|
const head = git3(["rev-parse", "HEAD"], cwd).trim();
|
|
11180
|
-
const isAncestor =
|
|
11632
|
+
const isAncestor = spawnSync4("git", ["merge-base", "--is-ancestor", base, head], { cwd, env: trustedGitEnv() });
|
|
11181
11633
|
if (isAncestor.status !== 0) {
|
|
11182
11634
|
err2(`tamperward run: HEAD ${head.slice(0, 10)} is not a descendant of the trusted base ${base.slice(0, 10)} \u2014`);
|
|
11183
11635
|
err2("the agent rewrote history out from under the anchor. That is the finding.");
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tamperward",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.16.1",
|
|
4
4
|
"description": "The deterministic agent-integrity gate. One ruleset, evaluated on the actual diff/commands as a verdict, enforced everywhere a change can be made.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"author": "hexrift",
|