tamperward 2.15.1 → 2.16.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 +25 -0
- package/dist/cli/index.js +724 -136
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -167,6 +167,19 @@ observer down deterministically. Observer health remains advisory; transient fin
|
|
|
167
167
|
remain warnings by default and affect the envelope only when the operator explicitly
|
|
168
168
|
sets `TAMPERWARD_TRANSIENT=block`.
|
|
169
169
|
|
|
170
|
+
Since **2.15.2**, Stop-sweep event consumption uses the saved cursor as a real byte
|
|
171
|
+
offset: it performs positioned reads of only new JSONL bytes instead of decoding the
|
|
172
|
+
whole historical log on every turn. Each physical read is capped at **4 MiB**, and one
|
|
173
|
+
authority decision drains at most **16 MiB** in bounded chunks. Only complete
|
|
174
|
+
newline-terminated records advance the cursor, so a torn final watcher write is replayed
|
|
175
|
+
after completion instead of being lost. Stop commits the cursor only after the entire
|
|
176
|
+
bounded telemetry tail has been parsed and classified; malformed records, an oversized
|
|
177
|
+
single record, a torn tail, or telemetry beyond the 16 MiB aggregate ceiling blocks Stop
|
|
178
|
+
and retains the previous cursor rather than certifying unjudged bytes. Supervised
|
|
179
|
+
`run --observe-transients` keeps observer evidence advisory by default, but when
|
|
180
|
+
`TAMPERWARD_TRANSIENT=block` is explicitly enabled, any unclassified observer tail
|
|
181
|
+
also fails the envelope closed.
|
|
182
|
+
|
|
170
183
|
After the runtime exits its exit code is treated as untrusted, and the envelope
|
|
171
184
|
checks that post-agent `HEAD` still descends from the entry commit; the committed
|
|
172
185
|
changes over `entry...HEAD`; staged, unstaged and untracked non-ignored worktree
|
|
@@ -356,6 +369,18 @@ restored `"test": "sh scripts/test.sh"` will happily call a script nothing
|
|
|
356
369
|
restored. It bounds the class rather than closing it — see
|
|
357
370
|
[the threat model](./docs/THREAT-MODEL-pristine-run.md).
|
|
358
371
|
|
|
372
|
+
From **2.16.0**, verifier suite output is diagnostic evidence instead of discarded
|
|
373
|
+
noise. Both visible and pristine stages continuously drain stdout/stderr through a
|
|
374
|
+
trusted supervisor, retain only the final **16 KiB per stream**, and count the total
|
|
375
|
+
bytes observed. Structured output exposes this under
|
|
376
|
+
`visible.diagnostics` / `pristine.diagnostics` with
|
|
377
|
+
`captured_bytes`, `retained_bytes`, `truncated`, and a bounded `tail` on a
|
|
378
|
+
failed stage. Default human output remains quiet on success. On failure, retained
|
|
379
|
+
diagnostics are rendered with every line prefixed and terminal/control characters
|
|
380
|
+
escaped, so candidate output cannot become ANSI control traffic or a GitHub
|
|
381
|
+
`::command::`. These bytes are evidence produced by candidate code; they never own
|
|
382
|
+
the verifier verdict.
|
|
383
|
+
|
|
359
384
|
The four primitives:
|
|
360
385
|
|
|
361
386
|
```bash
|
package/dist/cli/index.js
CHANGED
|
@@ -6829,7 +6829,7 @@ function runCheck(opts) {
|
|
|
6829
6829
|
}
|
|
6830
6830
|
|
|
6831
6831
|
// src/cli/hook.ts
|
|
6832
|
-
import { readFileSync as
|
|
6832
|
+
import { readFileSync as readFileSync10, appendFileSync as appendFileSync4, writeFileSync as writeFileSync5, existsSync as existsSync8 } from "node:fs";
|
|
6833
6833
|
import { createHash as createHash5 } from "node:crypto";
|
|
6834
6834
|
|
|
6835
6835
|
// src/adapters/claude/deny.ts
|
|
@@ -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();
|
|
@@ -7253,26 +7262,200 @@ function runWatch(args) {
|
|
|
7253
7262
|
}
|
|
7254
7263
|
|
|
7255
7264
|
// src/detectors/fs-events.ts
|
|
7256
|
-
import {
|
|
7265
|
+
import { closeSync as closeSync2, openSync as openSync2, readSync as readSync2, statSync } from "node:fs";
|
|
7257
7266
|
var RULE11 = "transient-protected-mutation";
|
|
7258
|
-
|
|
7267
|
+
var MAX_EVENT_READ_BYTES = 4 * 1024 * 1024;
|
|
7268
|
+
var MAX_EVENT_SWEEP_BYTES = 16 * 1024 * 1024;
|
|
7269
|
+
var DEFAULT_EVENT_READ_IO = {
|
|
7270
|
+
read: (fd, buffer, bufferOffset, length, position) => readSync2(fd, buffer, bufferOffset, length, position)
|
|
7271
|
+
};
|
|
7272
|
+
function readEvents(log, offset, maxBytes = MAX_EVENT_READ_BYTES, io = DEFAULT_EVENT_READ_IO) {
|
|
7259
7273
|
let size = 0;
|
|
7260
7274
|
try {
|
|
7261
7275
|
size = statSync(log).size;
|
|
7262
7276
|
} catch {
|
|
7263
|
-
return {
|
|
7277
|
+
return {
|
|
7278
|
+
events: [],
|
|
7279
|
+
newOffset: Math.max(0, Number.isFinite(offset) ? Math.floor(offset) : 0),
|
|
7280
|
+
bytesRead: 0,
|
|
7281
|
+
limitReached: false,
|
|
7282
|
+
malformedLines: 0,
|
|
7283
|
+
incompleteTail: false
|
|
7284
|
+
};
|
|
7285
|
+
}
|
|
7286
|
+
let start = Number.isFinite(offset) ? Math.max(0, Math.floor(offset)) : 0;
|
|
7287
|
+
if (start > size) start = 0;
|
|
7288
|
+
if (size <= start) {
|
|
7289
|
+
return {
|
|
7290
|
+
events: [],
|
|
7291
|
+
newOffset: start,
|
|
7292
|
+
bytesRead: 0,
|
|
7293
|
+
limitReached: false,
|
|
7294
|
+
malformedLines: 0,
|
|
7295
|
+
incompleteTail: false
|
|
7296
|
+
};
|
|
7297
|
+
}
|
|
7298
|
+
const cap = Number.isFinite(maxBytes) && maxBytes > 0 ? Math.max(1, Math.floor(maxBytes)) : MAX_EVENT_READ_BYTES;
|
|
7299
|
+
const available = size - start;
|
|
7300
|
+
const requested = Math.min(available, cap);
|
|
7301
|
+
const buffer = Buffer.allocUnsafe(requested);
|
|
7302
|
+
const fd = openSync2(log, "r");
|
|
7303
|
+
let bytesRead = 0;
|
|
7304
|
+
try {
|
|
7305
|
+
bytesRead = io.read(fd, buffer, 0, requested, start);
|
|
7306
|
+
} finally {
|
|
7307
|
+
closeSync2(fd);
|
|
7308
|
+
}
|
|
7309
|
+
if (bytesRead <= 0) {
|
|
7310
|
+
return {
|
|
7311
|
+
events: [],
|
|
7312
|
+
newOffset: start,
|
|
7313
|
+
bytesRead: 0,
|
|
7314
|
+
limitReached: available > 0,
|
|
7315
|
+
malformedLines: 0,
|
|
7316
|
+
incompleteTail: available > 0
|
|
7317
|
+
};
|
|
7318
|
+
}
|
|
7319
|
+
const chunk = buffer.subarray(0, bytesRead);
|
|
7320
|
+
const lastNewline = chunk.lastIndexOf(10);
|
|
7321
|
+
const limitReached = start + bytesRead < size;
|
|
7322
|
+
if (lastNewline < 0) {
|
|
7323
|
+
return {
|
|
7324
|
+
events: [],
|
|
7325
|
+
newOffset: start,
|
|
7326
|
+
bytesRead,
|
|
7327
|
+
limitReached,
|
|
7328
|
+
malformedLines: 0,
|
|
7329
|
+
incompleteTail: true
|
|
7330
|
+
};
|
|
7264
7331
|
}
|
|
7265
|
-
if (size <= offset) return { events: [], newOffset: offset >= 0 ? Math.min(offset, size) : 0 };
|
|
7266
|
-
const raw = readFileSync10(log, "utf8").slice(offset);
|
|
7267
7332
|
const events = [];
|
|
7268
|
-
|
|
7333
|
+
let malformedLines = 0;
|
|
7334
|
+
const complete = chunk.subarray(0, lastNewline + 1).toString("utf8");
|
|
7335
|
+
for (const line of complete.split("\n")) {
|
|
7269
7336
|
if (!line.trim()) continue;
|
|
7270
7337
|
try {
|
|
7271
7338
|
events.push(JSON.parse(line));
|
|
7272
7339
|
} catch {
|
|
7340
|
+
malformedLines++;
|
|
7341
|
+
}
|
|
7342
|
+
}
|
|
7343
|
+
const newOffset = start + lastNewline + 1;
|
|
7344
|
+
return {
|
|
7345
|
+
events,
|
|
7346
|
+
newOffset,
|
|
7347
|
+
bytesRead,
|
|
7348
|
+
limitReached,
|
|
7349
|
+
malformedLines,
|
|
7350
|
+
incompleteTail: newOffset < start + bytesRead
|
|
7351
|
+
};
|
|
7352
|
+
}
|
|
7353
|
+
function drainEvents(log, offset, maxTotalBytes = MAX_EVENT_SWEEP_BYTES, io = DEFAULT_EVENT_READ_IO) {
|
|
7354
|
+
const start = Number.isFinite(offset) ? Math.max(0, Math.floor(offset)) : 0;
|
|
7355
|
+
const aggregateCap = Number.isFinite(maxTotalBytes) && maxTotalBytes > 0 ? Math.max(1, Math.floor(maxTotalBytes)) : MAX_EVENT_SWEEP_BYTES;
|
|
7356
|
+
const events = [];
|
|
7357
|
+
let cursor = start;
|
|
7358
|
+
let bytesRead = 0;
|
|
7359
|
+
let batches = 0;
|
|
7360
|
+
let malformedLines = 0;
|
|
7361
|
+
while (bytesRead < aggregateCap) {
|
|
7362
|
+
const remainingBudget = aggregateCap - bytesRead;
|
|
7363
|
+
const batch = readEvents(
|
|
7364
|
+
log,
|
|
7365
|
+
cursor,
|
|
7366
|
+
Math.min(MAX_EVENT_READ_BYTES, remainingBudget),
|
|
7367
|
+
io
|
|
7368
|
+
);
|
|
7369
|
+
batches++;
|
|
7370
|
+
bytesRead += batch.bytesRead;
|
|
7371
|
+
malformedLines += batch.malformedLines;
|
|
7372
|
+
events.push(...batch.events);
|
|
7373
|
+
if (batch.malformedLines > 0) {
|
|
7374
|
+
return {
|
|
7375
|
+
events,
|
|
7376
|
+
newOffset: cursor,
|
|
7377
|
+
bytesRead,
|
|
7378
|
+
batches,
|
|
7379
|
+
malformedLines,
|
|
7380
|
+
complete: false,
|
|
7381
|
+
issue: "malformed-record"
|
|
7382
|
+
};
|
|
7383
|
+
}
|
|
7384
|
+
if (batch.bytesRead === 0) {
|
|
7385
|
+
if (batch.limitReached || batch.incompleteTail) {
|
|
7386
|
+
return {
|
|
7387
|
+
events,
|
|
7388
|
+
newOffset: cursor,
|
|
7389
|
+
bytesRead,
|
|
7390
|
+
batches,
|
|
7391
|
+
malformedLines,
|
|
7392
|
+
complete: false,
|
|
7393
|
+
issue: "read-stalled"
|
|
7394
|
+
};
|
|
7395
|
+
}
|
|
7396
|
+
return {
|
|
7397
|
+
events,
|
|
7398
|
+
newOffset: cursor,
|
|
7399
|
+
bytesRead,
|
|
7400
|
+
batches,
|
|
7401
|
+
malformedLines,
|
|
7402
|
+
complete: true
|
|
7403
|
+
};
|
|
7404
|
+
}
|
|
7405
|
+
if (batch.limitReached && batch.newOffset === cursor) {
|
|
7406
|
+
return {
|
|
7407
|
+
events,
|
|
7408
|
+
newOffset: cursor,
|
|
7409
|
+
bytesRead,
|
|
7410
|
+
batches,
|
|
7411
|
+
malformedLines,
|
|
7412
|
+
complete: false,
|
|
7413
|
+
issue: "oversized-record"
|
|
7414
|
+
};
|
|
7415
|
+
}
|
|
7416
|
+
if (!batch.limitReached && batch.incompleteTail) {
|
|
7417
|
+
return {
|
|
7418
|
+
events,
|
|
7419
|
+
newOffset: cursor,
|
|
7420
|
+
bytesRead,
|
|
7421
|
+
batches,
|
|
7422
|
+
malformedLines,
|
|
7423
|
+
complete: false,
|
|
7424
|
+
issue: "incomplete-record"
|
|
7425
|
+
};
|
|
7426
|
+
}
|
|
7427
|
+
cursor = batch.newOffset;
|
|
7428
|
+
if (!batch.limitReached) {
|
|
7429
|
+
return {
|
|
7430
|
+
events,
|
|
7431
|
+
newOffset: cursor,
|
|
7432
|
+
bytesRead,
|
|
7433
|
+
batches,
|
|
7434
|
+
malformedLines,
|
|
7435
|
+
complete: true
|
|
7436
|
+
};
|
|
7437
|
+
}
|
|
7438
|
+
if (bytesRead >= aggregateCap) {
|
|
7439
|
+
return {
|
|
7440
|
+
events,
|
|
7441
|
+
newOffset: cursor,
|
|
7442
|
+
bytesRead,
|
|
7443
|
+
batches,
|
|
7444
|
+
malformedLines,
|
|
7445
|
+
complete: false,
|
|
7446
|
+
issue: "aggregate-limit"
|
|
7447
|
+
};
|
|
7273
7448
|
}
|
|
7274
7449
|
}
|
|
7275
|
-
return {
|
|
7450
|
+
return {
|
|
7451
|
+
events,
|
|
7452
|
+
newOffset: cursor,
|
|
7453
|
+
bytesRead,
|
|
7454
|
+
batches,
|
|
7455
|
+
malformedLines,
|
|
7456
|
+
complete: false,
|
|
7457
|
+
issue: "aggregate-limit"
|
|
7458
|
+
};
|
|
7276
7459
|
}
|
|
7277
7460
|
function transientFindings(events, persistentPaths, policy, finalHash) {
|
|
7278
7461
|
if (!isEnabled(RULE11, policy)) return [];
|
|
@@ -7316,7 +7499,7 @@ var HookInputError = class extends Error {
|
|
|
7316
7499
|
};
|
|
7317
7500
|
function readStdin() {
|
|
7318
7501
|
try {
|
|
7319
|
-
return
|
|
7502
|
+
return readFileSync10(0, "utf8");
|
|
7320
7503
|
} catch (e) {
|
|
7321
7504
|
throw new HookInputError(`cannot read the hook payload on stdin: ${e instanceof Error ? e.message : String(e)}`);
|
|
7322
7505
|
}
|
|
@@ -7564,6 +7747,17 @@ function cursorPath(cwd, sessionId) {
|
|
|
7564
7747
|
const p = ptreePath(cwd, sessionId);
|
|
7565
7748
|
return p ? p.replace(/ptree-/, "fscursor-") : null;
|
|
7566
7749
|
}
|
|
7750
|
+
function observerIntegrityBlock(issue, bytesRead, malformedLines) {
|
|
7751
|
+
const detail = issue === "malformed-record" ? `${malformedLines} malformed complete JSONL record(s)` : issue === "oversized-record" ? `one event record exceeded the ${MAX_EVENT_READ_BYTES}-byte per-record/read ceiling` : issue === "incomplete-record" ? "the event log ended with an incomplete JSONL record" : issue === "aggregate-limit" ? `more than ${MAX_EVENT_SWEEP_BYTES} observer bytes required classification in one Stop sweep` : "the positioned event-log read made no safe progress";
|
|
7752
|
+
return {
|
|
7753
|
+
rule: "tamperward-unavailable",
|
|
7754
|
+
severity: "block",
|
|
7755
|
+
message: `TamperWard could not fully classify transient observer telemetry (${detail}), so Stop is blocked rather than certifying an unjudged tail.`,
|
|
7756
|
+
evidence: `observer_issue=${issue}; bytes_read=${bytesRead}; malformed_lines=${malformedLines}`,
|
|
7757
|
+
remediation: "Let the watcher finish its current record or repair/rotate the malformed observer log, then retry Stop. Do not delete the cursor/log to bypass unjudged telemetry.",
|
|
7758
|
+
signoff: { required: true, command: 'tamperward allow --reason "..."' }
|
|
7759
|
+
};
|
|
7760
|
+
}
|
|
7567
7761
|
function turnTransientBlocks(cwd, sessionId, policy, changes) {
|
|
7568
7762
|
const none = { blocks: [], commit: () => {
|
|
7569
7763
|
} };
|
|
@@ -7576,16 +7770,34 @@ function turnTransientBlocks(cwd, sessionId, policy, changes) {
|
|
|
7576
7770
|
if (!existsSync8(log)) return none;
|
|
7577
7771
|
let offset = 0;
|
|
7578
7772
|
if (cp && existsSync8(cp)) {
|
|
7579
|
-
const n = Number(
|
|
7773
|
+
const n = Number(readFileSync10(cp, "utf8"));
|
|
7580
7774
|
offset = Number.isFinite(n) ? n : 0;
|
|
7581
7775
|
}
|
|
7582
|
-
const
|
|
7583
|
-
if (
|
|
7584
|
-
|
|
7585
|
-
|
|
7586
|
-
|
|
7587
|
-
|
|
7588
|
-
|
|
7776
|
+
const drained = drainEvents(log, offset);
|
|
7777
|
+
if (!drained.complete) {
|
|
7778
|
+
const issue = drained.issue ?? "read-stalled";
|
|
7779
|
+
recordObserverHealth(
|
|
7780
|
+
"degraded",
|
|
7781
|
+
`event telemetry was not fully classified (${issue}); Stop retained the prior cursor`
|
|
7782
|
+
);
|
|
7783
|
+
return {
|
|
7784
|
+
blocks: [observerIntegrityBlock(issue, drained.bytesRead, drained.malformedLines)],
|
|
7785
|
+
commit: () => {
|
|
7786
|
+
}
|
|
7787
|
+
};
|
|
7788
|
+
}
|
|
7789
|
+
const { events, newOffset } = drained;
|
|
7790
|
+
if (events.length === 0) {
|
|
7791
|
+
return {
|
|
7792
|
+
...none,
|
|
7793
|
+
commit: () => {
|
|
7794
|
+
if (cp) try {
|
|
7795
|
+
writeFileSync5(cp, String(newOffset));
|
|
7796
|
+
} catch {
|
|
7797
|
+
}
|
|
7798
|
+
}
|
|
7799
|
+
};
|
|
7800
|
+
}
|
|
7589
7801
|
const persistent = new Set(changes.filter((c) => c.kind === "file").map((c) => c.path));
|
|
7590
7802
|
const finalHash = (path) => {
|
|
7591
7803
|
const e = inspectRel(cwd, path);
|
|
@@ -7742,7 +7954,7 @@ Honored at LOCAL pre-commit only. The agent-layer hook ignores this file; CI req
|
|
|
7742
7954
|
// src/cli/init.ts
|
|
7743
7955
|
import { execFileSync as execFileSync6 } from "node:child_process";
|
|
7744
7956
|
import { createHash as createHash6 } from "node:crypto";
|
|
7745
|
-
import { existsSync as existsSync9, mkdirSync as mkdirSync5, readFileSync as
|
|
7957
|
+
import { existsSync as existsSync9, mkdirSync as mkdirSync5, readFileSync as readFileSync11, writeFileSync as writeFileSync6, chmodSync, statSync as statSync2 } from "node:fs";
|
|
7746
7958
|
import { basename as basename2, dirname as dirname4, isAbsolute as isAbsolute8, join as join12, relative as relative3, resolve as resolve3 } from "node:path";
|
|
7747
7959
|
|
|
7748
7960
|
// src/verifier-limits.ts
|
|
@@ -7986,7 +8198,7 @@ function planClaudeHooks(cwd) {
|
|
|
7986
8198
|
if (existsSync9(path)) {
|
|
7987
8199
|
let parsed;
|
|
7988
8200
|
try {
|
|
7989
|
-
parsed = JSON.parse(
|
|
8201
|
+
parsed = JSON.parse(readFileSync11(path, "utf8"));
|
|
7990
8202
|
} catch {
|
|
7991
8203
|
return {
|
|
7992
8204
|
item: "agent",
|
|
@@ -8132,7 +8344,7 @@ function planPreCommit(cwd) {
|
|
|
8132
8344
|
if (existsSync9(path) && !statSync2(path).isFile()) {
|
|
8133
8345
|
return { item: "pre-commit", path: rel, status: "error", detail: "exists but is not a regular file \u2014 refusing to write" };
|
|
8134
8346
|
}
|
|
8135
|
-
const existing = existsSync9(path) ?
|
|
8347
|
+
const existing = existsSync9(path) ? readFileSync11(path, "utf8") : null;
|
|
8136
8348
|
if (existing === null) {
|
|
8137
8349
|
return {
|
|
8138
8350
|
item: "pre-commit",
|
|
@@ -8224,7 +8436,7 @@ function planCodeowners(cwd) {
|
|
|
8224
8436
|
(r) => existsSync9(join12(cwd, r))
|
|
8225
8437
|
);
|
|
8226
8438
|
const path = join12(cwd, existingRel ?? rel);
|
|
8227
|
-
const existing = existingRel ?
|
|
8439
|
+
const existing = existingRel ? readFileSync11(path, "utf8") : null;
|
|
8228
8440
|
const missing = CODEOWNERS_PATHS.filter((p) => existing === null || !coveredBy(existing, p));
|
|
8229
8441
|
if (missing.length === 0) {
|
|
8230
8442
|
return { item: "codeowners", path: existingRel, status: "ok", detail: "gate paths already have code owners" };
|
|
@@ -8273,7 +8485,7 @@ function planWorkflow(cwd, force) {
|
|
|
8273
8485
|
}
|
|
8274
8486
|
let src;
|
|
8275
8487
|
try {
|
|
8276
|
-
src =
|
|
8488
|
+
src = readFileSync11(path, "utf8");
|
|
8277
8489
|
} catch {
|
|
8278
8490
|
return { item: "ci", path: rel, status: "error", detail: "exists but cannot be read \u2014 refusing to overwrite" };
|
|
8279
8491
|
}
|
|
@@ -8372,7 +8584,7 @@ tamperward init: ${changed} change(s) applied. Commit them so the gate travels w
|
|
|
8372
8584
|
|
|
8373
8585
|
// src/cli/doctor.ts
|
|
8374
8586
|
import { execFileSync as execFileSync7 } from "node:child_process";
|
|
8375
|
-
import { existsSync as existsSync10, readFileSync as
|
|
8587
|
+
import { existsSync as existsSync10, readFileSync as readFileSync12, readdirSync as readdirSync3 } from "node:fs";
|
|
8376
8588
|
import { join as join13, resolve as resolve4 } from "node:path";
|
|
8377
8589
|
import { parse as parse3 } from "yaml";
|
|
8378
8590
|
var VERIFY_COMMAND = /\btamperward(?:@\S+)?\s+verify\b/;
|
|
@@ -8410,7 +8622,7 @@ function pinsInFile(path) {
|
|
|
8410
8622
|
if (!existsSync10(path)) return [];
|
|
8411
8623
|
let src = "";
|
|
8412
8624
|
try {
|
|
8413
|
-
src =
|
|
8625
|
+
src = readFileSync12(path, "utf8");
|
|
8414
8626
|
} catch {
|
|
8415
8627
|
return [];
|
|
8416
8628
|
}
|
|
@@ -8437,7 +8649,7 @@ function workflowPermissionCheck(cwd, workflowRels = [".github/workflows/tamperw
|
|
|
8437
8649
|
}
|
|
8438
8650
|
let doc;
|
|
8439
8651
|
try {
|
|
8440
|
-
doc = parse3(
|
|
8652
|
+
doc = parse3(readFileSync12(path, "utf8"));
|
|
8441
8653
|
} catch (e) {
|
|
8442
8654
|
broken.push(`${rel} is not valid YAML (${e instanceof Error ? e.message : String(e)})`);
|
|
8443
8655
|
continue;
|
|
@@ -8843,7 +9055,7 @@ function runDoctor(opts = {}) {
|
|
|
8843
9055
|
}
|
|
8844
9056
|
let doc;
|
|
8845
9057
|
try {
|
|
8846
|
-
doc = parse3(
|
|
9058
|
+
doc = parse3(readFileSync12(workflowPath, "utf8"));
|
|
8847
9059
|
} catch (e) {
|
|
8848
9060
|
return fail(
|
|
8849
9061
|
"ci-verifier",
|
|
@@ -8949,11 +9161,11 @@ function runDoctor(opts = {}) {
|
|
|
8949
9161
|
}
|
|
8950
9162
|
|
|
8951
9163
|
// src/cli/verify.ts
|
|
8952
|
-
import { execFileSync as execFileSync9, spawnSync as
|
|
9164
|
+
import { execFileSync as execFileSync9, spawnSync as spawnSync3 } from "node:child_process";
|
|
8953
9165
|
import { createHash as createHash10 } from "node:crypto";
|
|
8954
|
-
import { chmodSync as chmodSync2, cpSync, lstatSync as lstatSync5, mkdirSync as mkdirSync6, mkdtempSync as
|
|
8955
|
-
import { tmpdir as
|
|
8956
|
-
import { dirname as dirname7, isAbsolute as isAbsolute10, join as
|
|
9166
|
+
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";
|
|
9167
|
+
import { tmpdir as tmpdir3 } from "node:os";
|
|
9168
|
+
import { dirname as dirname7, isAbsolute as isAbsolute10, join as join17, relative as relative5, resolve as resolve7, sep as sep2 } from "node:path";
|
|
8957
9169
|
|
|
8958
9170
|
// src/fingerprint.ts
|
|
8959
9171
|
import { execFileSync as execFileSync8 } from "node:child_process";
|
|
@@ -8987,7 +9199,7 @@ import {
|
|
|
8987
9199
|
existsSync as existsSync11,
|
|
8988
9200
|
lstatSync as lstatSync4,
|
|
8989
9201
|
readdirSync as readdirSync4,
|
|
8990
|
-
readFileSync as
|
|
9202
|
+
readFileSync as readFileSync13,
|
|
8991
9203
|
readlinkSync as readlinkSync2,
|
|
8992
9204
|
realpathSync as realpathSync3,
|
|
8993
9205
|
statSync as statSync3
|
|
@@ -9044,7 +9256,7 @@ function fingerprintRoot(root, cwd, h) {
|
|
|
9044
9256
|
st = statSync3(path);
|
|
9045
9257
|
if (!st.isFile()) throw new Error("not a regular file");
|
|
9046
9258
|
h.update(label + "\0external-file\0" + path + "\0" + String(st.mode) + "\0");
|
|
9047
|
-
h.update(
|
|
9259
|
+
h.update(readFileSync13(path));
|
|
9048
9260
|
h.update("\0");
|
|
9049
9261
|
} catch (e) {
|
|
9050
9262
|
throw new Unattestable(
|
|
@@ -9078,7 +9290,7 @@ function fingerprintRoot(root, cwd, h) {
|
|
|
9078
9290
|
h.update(rel + "\0" + ident.kind + "\0" + String(ident.mode) + "\0");
|
|
9079
9291
|
if (ident.kind === "file") {
|
|
9080
9292
|
try {
|
|
9081
|
-
h.update(
|
|
9293
|
+
h.update(readFileSync13(path));
|
|
9082
9294
|
h.update("\0");
|
|
9083
9295
|
} catch (e) {
|
|
9084
9296
|
throw new Unattestable(
|
|
@@ -9239,7 +9451,7 @@ function venvProblem(root) {
|
|
|
9239
9451
|
const cfg = join14(root, "pyvenv.cfg");
|
|
9240
9452
|
if (!existsSync11(cfg)) return "VIRTUAL_ENV " + JSON.stringify(root) + " has no pyvenv.cfg";
|
|
9241
9453
|
try {
|
|
9242
|
-
const body =
|
|
9454
|
+
const body = readFileSync13(cfg, "utf8");
|
|
9243
9455
|
if (/^\s*include-system-site-packages\s*=\s*true\s*$/im.test(body)) {
|
|
9244
9456
|
return "Python environment " + JSON.stringify(root) + " enables system site-packages; those external package roots are not bounded";
|
|
9245
9457
|
}
|
|
@@ -9336,9 +9548,321 @@ function dependencyEnvironmentSummary(descriptor) {
|
|
|
9336
9548
|
|
|
9337
9549
|
// src/verifier-backend.ts
|
|
9338
9550
|
import { createHash as createHash9, randomUUID } from "node:crypto";
|
|
9339
|
-
import { spawnSync } from "node:child_process";
|
|
9551
|
+
import { spawnSync as spawnSync2 } from "node:child_process";
|
|
9340
9552
|
import { accessSync, constants as constants2, readFileSync as readFileSync15, realpathSync as realpathSync4, statSync as statSync4 } from "node:fs";
|
|
9341
|
-
import { delimiter as delimiter2, dirname as dirname6, join as
|
|
9553
|
+
import { delimiter as delimiter2, dirname as dirname6, join as join16, resolve as resolve6 } from "node:path";
|
|
9554
|
+
|
|
9555
|
+
// src/suite-diagnostics.ts
|
|
9556
|
+
import { spawnSync } from "node:child_process";
|
|
9557
|
+
import { mkdtempSync as mkdtempSync2, rmSync as rmSync3, writeFileSync as writeFileSync7 } from "node:fs";
|
|
9558
|
+
import { tmpdir as tmpdir2 } from "node:os";
|
|
9559
|
+
import { join as join15 } from "node:path";
|
|
9560
|
+
var DIAGNOSTIC_TAIL_BYTES = 16 * 1024;
|
|
9561
|
+
var EMPTY_STREAM = {
|
|
9562
|
+
captured_bytes: 0,
|
|
9563
|
+
retained_bytes: 0,
|
|
9564
|
+
truncated: false,
|
|
9565
|
+
tail: ""
|
|
9566
|
+
};
|
|
9567
|
+
function emptySuiteDiagnostics() {
|
|
9568
|
+
return {
|
|
9569
|
+
stdout: { ...EMPTY_STREAM },
|
|
9570
|
+
stderr: { ...EMPTY_STREAM }
|
|
9571
|
+
};
|
|
9572
|
+
}
|
|
9573
|
+
function decodeUtf8Tail(tail) {
|
|
9574
|
+
let start = 0;
|
|
9575
|
+
while (start < tail.length && (tail[start] & 192) === 128) start++;
|
|
9576
|
+
return tail.subarray(start).toString("utf8");
|
|
9577
|
+
}
|
|
9578
|
+
function streamFromRaw(raw) {
|
|
9579
|
+
let tail = Buffer.alloc(0);
|
|
9580
|
+
try {
|
|
9581
|
+
tail = Buffer.from(raw?.tail_b64 ?? "", "base64");
|
|
9582
|
+
} catch {
|
|
9583
|
+
tail = Buffer.alloc(0);
|
|
9584
|
+
}
|
|
9585
|
+
const captured = Number.isFinite(raw?.captured_bytes) ? Math.max(0, Number(raw?.captured_bytes)) : tail.length;
|
|
9586
|
+
return {
|
|
9587
|
+
captured_bytes: captured,
|
|
9588
|
+
retained_bytes: tail.length,
|
|
9589
|
+
truncated: captured > tail.length,
|
|
9590
|
+
tail: decodeUtf8Tail(tail)
|
|
9591
|
+
};
|
|
9592
|
+
}
|
|
9593
|
+
function scrubDiagnosticText(input) {
|
|
9594
|
+
let out2 = "";
|
|
9595
|
+
for (const ch of input) {
|
|
9596
|
+
const cp = ch.codePointAt(0);
|
|
9597
|
+
if (ch === "\n" || ch === " ") {
|
|
9598
|
+
out2 += ch;
|
|
9599
|
+
} else if (cp === 13) {
|
|
9600
|
+
out2 += "\\r";
|
|
9601
|
+
} else if (cp < 32 || cp >= 127 && cp <= 159) {
|
|
9602
|
+
out2 += cp <= 255 ? `\\x${cp.toString(16).padStart(2, "0")}` : `\\u{${cp.toString(16)}}`;
|
|
9603
|
+
} else {
|
|
9604
|
+
out2 += ch;
|
|
9605
|
+
}
|
|
9606
|
+
}
|
|
9607
|
+
return out2;
|
|
9608
|
+
}
|
|
9609
|
+
function diagnosticLines(stage, diagnostics) {
|
|
9610
|
+
const lines = [];
|
|
9611
|
+
for (const streamName of ["stdout", "stderr"]) {
|
|
9612
|
+
const stream = diagnostics[streamName];
|
|
9613
|
+
if (stream.captured_bytes === 0) continue;
|
|
9614
|
+
const suffix = stream.truncated ? ` \u2014 tail ${stream.retained_bytes}/${stream.captured_bytes} bytes retained` : ` \u2014 ${stream.captured_bytes} bytes`;
|
|
9615
|
+
lines.push(`${stage} suite ${streamName}${suffix}:`);
|
|
9616
|
+
const scrubbed = scrubDiagnosticText(stream.tail);
|
|
9617
|
+
const body = scrubbed.split("\n");
|
|
9618
|
+
if (body.at(-1) === "") body.pop();
|
|
9619
|
+
for (const line of body) lines.push(` | ${line}`);
|
|
9620
|
+
}
|
|
9621
|
+
return lines;
|
|
9622
|
+
}
|
|
9623
|
+
var CAPTURE_SUPERVISOR = String.raw`
|
|
9624
|
+
const cp = require('node:child_process');
|
|
9625
|
+
const fs = require('node:fs');
|
|
9626
|
+
|
|
9627
|
+
const [configFile] = process.argv.slice(1);
|
|
9628
|
+
const cfg = JSON.parse(fs.readFileSync(configFile, 'utf8'));
|
|
9629
|
+
const CAP = Number(cfg.captureBytes);
|
|
9630
|
+
|
|
9631
|
+
const fresh = () => ({ total: 0, tail: Buffer.alloc(0) });
|
|
9632
|
+
const stdout = fresh();
|
|
9633
|
+
const stderr = fresh();
|
|
9634
|
+
|
|
9635
|
+
// Linux ordinary-exit hardening (#371): process-group ownership alone misses a
|
|
9636
|
+
// descendant that calls setsid(). Track the real descendant tree while the
|
|
9637
|
+
// suite is alive so a reparented session escape is still known when the main
|
|
9638
|
+
// child exits. This supplements, rather than replaces, group termination.
|
|
9639
|
+
const trackedDescendants = new Set();
|
|
9640
|
+
|
|
9641
|
+
function linuxDescendants(rootPid) {
|
|
9642
|
+
if (process.platform !== 'linux' || !rootPid) return [];
|
|
9643
|
+
const byParent = new Map();
|
|
9644
|
+
let names = [];
|
|
9645
|
+
try { names = fs.readdirSync('/proc').filter((x) => /^\d+$/.test(x)); } catch { return []; }
|
|
9646
|
+
for (const name of names) {
|
|
9647
|
+
const pid = Number(name);
|
|
9648
|
+
try {
|
|
9649
|
+
const stat = fs.readFileSync('/proc/' + name + '/stat', 'utf8');
|
|
9650
|
+
const fields = stat.slice(stat.lastIndexOf(')') + 2).split(' ');
|
|
9651
|
+
const ppid = Number(fields[1]);
|
|
9652
|
+
if (!Number.isFinite(ppid)) continue;
|
|
9653
|
+
const kids = byParent.get(ppid) || [];
|
|
9654
|
+
kids.push(pid);
|
|
9655
|
+
byParent.set(ppid, kids);
|
|
9656
|
+
} catch {}
|
|
9657
|
+
}
|
|
9658
|
+
const out = [];
|
|
9659
|
+
const seen = new Set([rootPid]);
|
|
9660
|
+
const stack = [rootPid];
|
|
9661
|
+
while (stack.length) {
|
|
9662
|
+
const parent = stack.pop();
|
|
9663
|
+
for (const pid of (byParent.get(parent) || [])) {
|
|
9664
|
+
if (seen.has(pid)) continue;
|
|
9665
|
+
seen.add(pid);
|
|
9666
|
+
out.push(pid);
|
|
9667
|
+
stack.push(pid);
|
|
9668
|
+
}
|
|
9669
|
+
}
|
|
9670
|
+
return out;
|
|
9671
|
+
}
|
|
9672
|
+
|
|
9673
|
+
function trackDescendants() {
|
|
9674
|
+
if (!child || !child.pid || process.platform !== 'linux') return;
|
|
9675
|
+
for (const pid of linuxDescendants(child.pid)) trackedDescendants.add(pid);
|
|
9676
|
+
}
|
|
9677
|
+
|
|
9678
|
+
function append(state, chunk) {
|
|
9679
|
+
const b = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
9680
|
+
state.total += b.length;
|
|
9681
|
+
if (b.length >= CAP) {
|
|
9682
|
+
state.tail = Buffer.from(b.subarray(b.length - CAP));
|
|
9683
|
+
return;
|
|
9684
|
+
}
|
|
9685
|
+
if (state.tail.length + b.length <= CAP) {
|
|
9686
|
+
state.tail = Buffer.concat([state.tail, b]);
|
|
9687
|
+
return;
|
|
9688
|
+
}
|
|
9689
|
+
const keep = CAP - b.length;
|
|
9690
|
+
state.tail = Buffer.concat([state.tail.subarray(state.tail.length - keep), b]);
|
|
9691
|
+
}
|
|
9692
|
+
|
|
9693
|
+
function diag(state) {
|
|
9694
|
+
return {
|
|
9695
|
+
captured_bytes: state.total,
|
|
9696
|
+
tail_b64: state.tail.toString('base64'),
|
|
9697
|
+
};
|
|
9698
|
+
}
|
|
9699
|
+
|
|
9700
|
+
let child;
|
|
9701
|
+
let done = false;
|
|
9702
|
+
let timedOut = false;
|
|
9703
|
+
let exitInfo = { exit: null, signal: null };
|
|
9704
|
+
let descendantTracker = null;
|
|
9705
|
+
|
|
9706
|
+
function killOwned() {
|
|
9707
|
+
if (!child || !child.pid) return;
|
|
9708
|
+
trackDescendants();
|
|
9709
|
+
if (cfg.detached && process.platform !== 'win32') {
|
|
9710
|
+
try { process.kill(-child.pid, 'SIGKILL'); } catch {}
|
|
9711
|
+
}
|
|
9712
|
+
try { child.kill('SIGKILL'); } catch {}
|
|
9713
|
+
if (process.platform === 'linux') {
|
|
9714
|
+
// Deepest-first is friendlier to short-lived process trees. PIDs are still
|
|
9715
|
+
// rechecked by kill(2); races with natural exit are harmless.
|
|
9716
|
+
for (const pid of Array.from(trackedDescendants).reverse()) {
|
|
9717
|
+
try { process.kill(pid, 'SIGKILL'); } catch {}
|
|
9718
|
+
}
|
|
9719
|
+
}
|
|
9720
|
+
}
|
|
9721
|
+
|
|
9722
|
+
function finish(extra) {
|
|
9723
|
+
if (done) return;
|
|
9724
|
+
done = true;
|
|
9725
|
+
if (descendantTracker) clearInterval(descendantTracker);
|
|
9726
|
+
try {
|
|
9727
|
+
// stdout is reserved for the trusted supervisor result. Candidate suite
|
|
9728
|
+
// stdout/stderr are separate pipes and are never inherited here.
|
|
9729
|
+
process.stdout.write(JSON.stringify({
|
|
9730
|
+
...exitInfo,
|
|
9731
|
+
timedOut,
|
|
9732
|
+
...extra,
|
|
9733
|
+
stdout: diag(stdout),
|
|
9734
|
+
stderr: diag(stderr),
|
|
9735
|
+
}));
|
|
9736
|
+
} catch {}
|
|
9737
|
+
process.exit(0);
|
|
9738
|
+
}
|
|
9739
|
+
|
|
9740
|
+
try {
|
|
9741
|
+
child = cp.spawn(cfg.executable, cfg.args, {
|
|
9742
|
+
cwd: cfg.cwd,
|
|
9743
|
+
env: process.env,
|
|
9744
|
+
detached: Boolean(cfg.detached) && process.platform !== 'win32',
|
|
9745
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
9746
|
+
});
|
|
9747
|
+
} catch (e) {
|
|
9748
|
+
finish({ error: String(e) });
|
|
9749
|
+
}
|
|
9750
|
+
|
|
9751
|
+
if (child) {
|
|
9752
|
+
if (process.platform === 'linux') {
|
|
9753
|
+
trackDescendants();
|
|
9754
|
+
descendantTracker = setInterval(trackDescendants, 5);
|
|
9755
|
+
descendantTracker.unref();
|
|
9756
|
+
}
|
|
9757
|
+
child.stdout.on('data', (b) => append(stdout, b));
|
|
9758
|
+
child.stderr.on('data', (b) => append(stderr, b));
|
|
9759
|
+
|
|
9760
|
+
const timer = setTimeout(() => {
|
|
9761
|
+
timedOut = true;
|
|
9762
|
+
killOwned();
|
|
9763
|
+
// Backstop if a platform fails to emit close after the forced kill.
|
|
9764
|
+
setTimeout(() => finish({}), 1000).unref();
|
|
9765
|
+
}, Number(cfg.timeoutMs));
|
|
9766
|
+
timer.unref();
|
|
9767
|
+
|
|
9768
|
+
child.on('error', (e) => {
|
|
9769
|
+
clearTimeout(timer);
|
|
9770
|
+
killOwned();
|
|
9771
|
+
finish({ error: String(e) });
|
|
9772
|
+
});
|
|
9773
|
+
|
|
9774
|
+
child.on('exit', (code, signal) => {
|
|
9775
|
+
exitInfo = {
|
|
9776
|
+
exit: code == null ? null : Number(code),
|
|
9777
|
+
signal: signal == null ? null : String(signal),
|
|
9778
|
+
};
|
|
9779
|
+
if (cfg.killGroupOnFinish) killOwned();
|
|
9780
|
+
});
|
|
9781
|
+
|
|
9782
|
+
// close fires after the stdio pipes close, so the retained tails include all
|
|
9783
|
+
// output that was drained before owned descendants were terminated.
|
|
9784
|
+
child.on('close', (code, signal) => {
|
|
9785
|
+
clearTimeout(timer);
|
|
9786
|
+
if (exitInfo.exit === null && code != null) exitInfo.exit = Number(code);
|
|
9787
|
+
if (exitInfo.signal === null && signal != null) exitInfo.signal = String(signal);
|
|
9788
|
+
finish({});
|
|
9789
|
+
});
|
|
9790
|
+
}
|
|
9791
|
+
`;
|
|
9792
|
+
function parseCapturedSupervisorResult(stdoutText) {
|
|
9793
|
+
let raw;
|
|
9794
|
+
try {
|
|
9795
|
+
raw = JSON.parse(stdoutText);
|
|
9796
|
+
} catch {
|
|
9797
|
+
return null;
|
|
9798
|
+
}
|
|
9799
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return null;
|
|
9800
|
+
const diagnostics = {
|
|
9801
|
+
stdout: streamFromRaw(raw.stdout),
|
|
9802
|
+
stderr: streamFromRaw(raw.stderr)
|
|
9803
|
+
};
|
|
9804
|
+
return {
|
|
9805
|
+
exit: typeof raw.exit === "number" ? raw.exit : null,
|
|
9806
|
+
signal: typeof raw.signal === "string" ? raw.signal : null,
|
|
9807
|
+
timedOut: Boolean(raw.timedOut),
|
|
9808
|
+
...typeof raw.error === "string" && raw.error ? { error: raw.error } : {},
|
|
9809
|
+
diagnostics
|
|
9810
|
+
};
|
|
9811
|
+
}
|
|
9812
|
+
function runCapturedProcessSync(executable, args, opts) {
|
|
9813
|
+
const stateDir = mkdtempSync2(join15(tmpdir2(), "tw-suite-capture-"));
|
|
9814
|
+
const configFile = join15(stateDir, "config.json");
|
|
9815
|
+
const backstop = opts.backstopMs ?? 3e4;
|
|
9816
|
+
try {
|
|
9817
|
+
writeFileSync7(
|
|
9818
|
+
configFile,
|
|
9819
|
+
JSON.stringify({
|
|
9820
|
+
executable,
|
|
9821
|
+
args,
|
|
9822
|
+
cwd: opts.cwd,
|
|
9823
|
+
timeoutMs: opts.timeoutMs,
|
|
9824
|
+
detached: Boolean(opts.detached),
|
|
9825
|
+
killGroupOnFinish: Boolean(opts.killGroupOnFinish),
|
|
9826
|
+
captureBytes: DIAGNOSTIC_TAIL_BYTES
|
|
9827
|
+
}),
|
|
9828
|
+
{ mode: 384 }
|
|
9829
|
+
);
|
|
9830
|
+
const supervisor = spawnSync(
|
|
9831
|
+
process.execPath,
|
|
9832
|
+
["-e", CAPTURE_SUPERVISOR, configFile],
|
|
9833
|
+
{
|
|
9834
|
+
cwd: opts.cwd,
|
|
9835
|
+
env: opts.env,
|
|
9836
|
+
encoding: "utf8",
|
|
9837
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
9838
|
+
// The trusted result is at most two 16 KiB tails plus small metadata.
|
|
9839
|
+
// A candidate that somehow writes into the supervisor's stdout through
|
|
9840
|
+
// same-UID /proc can only overflow/corrupt this bounded channel and make
|
|
9841
|
+
// parsing fail closed; its suite streams are never inherited here.
|
|
9842
|
+
maxBuffer: 256 * 1024,
|
|
9843
|
+
timeout: opts.timeoutMs + backstop,
|
|
9844
|
+
killSignal: "SIGKILL"
|
|
9845
|
+
}
|
|
9846
|
+
);
|
|
9847
|
+
const parsed = parseCapturedSupervisorResult(supervisor.stdout ?? "");
|
|
9848
|
+
const diagnostics = parsed?.diagnostics ?? emptySuiteDiagnostics();
|
|
9849
|
+
if (!parsed) {
|
|
9850
|
+
const supervisorTimedOut = Boolean(supervisor.error) && supervisor.error.code === "ETIMEDOUT";
|
|
9851
|
+
return {
|
|
9852
|
+
exit: null,
|
|
9853
|
+
signal: supervisor.signal ? String(supervisor.signal) : null,
|
|
9854
|
+
timedOut: supervisorTimedOut,
|
|
9855
|
+
error: supervisor.error?.message ?? "suite capture supervisor did not produce a result",
|
|
9856
|
+
diagnostics
|
|
9857
|
+
};
|
|
9858
|
+
}
|
|
9859
|
+
return parsed;
|
|
9860
|
+
} finally {
|
|
9861
|
+
rmSync3(stateDir, { recursive: true, force: true });
|
|
9862
|
+
}
|
|
9863
|
+
}
|
|
9864
|
+
|
|
9865
|
+
// src/verifier-backend.ts
|
|
9342
9866
|
var DEFAULT_CONTAINER_RESOURCE_LIMITS = Object.freeze({
|
|
9343
9867
|
memoryBytes: 2 * 1024 * 1024 * 1024,
|
|
9344
9868
|
// Docker interprets memory-swap == memory as no additional swap allowance.
|
|
@@ -9381,7 +9905,7 @@ function replaceableByCaller(path) {
|
|
|
9381
9905
|
function findExecutable(name) {
|
|
9382
9906
|
for (const raw of (process.env.PATH ?? "").split(delimiter2)) {
|
|
9383
9907
|
if (!raw) continue;
|
|
9384
|
-
const candidate =
|
|
9908
|
+
const candidate = join16(raw, name);
|
|
9385
9909
|
try {
|
|
9386
9910
|
accessSync(candidate, constants2.X_OK);
|
|
9387
9911
|
return realpathSync4(candidate);
|
|
@@ -9406,7 +9930,7 @@ function dockerArgs(host, args) {
|
|
|
9406
9930
|
}
|
|
9407
9931
|
function dockerAvailable(enginePath, host) {
|
|
9408
9932
|
try {
|
|
9409
|
-
const r =
|
|
9933
|
+
const r = spawnSync2(enginePath, dockerArgs(host, ["version"]), {
|
|
9410
9934
|
stdio: "ignore",
|
|
9411
9935
|
timeout: 5e3,
|
|
9412
9936
|
killSignal: "SIGKILL",
|
|
@@ -9434,7 +9958,7 @@ function declaredImageVolumePaths(metadata) {
|
|
|
9434
9958
|
}
|
|
9435
9959
|
function inspectImageMetadata(enginePath, host, image) {
|
|
9436
9960
|
try {
|
|
9437
|
-
const r =
|
|
9961
|
+
const r = spawnSync2(enginePath, dockerArgs(host, ["image", "inspect", image]), {
|
|
9438
9962
|
encoding: "utf8",
|
|
9439
9963
|
stdio: ["ignore", "pipe", "ignore"],
|
|
9440
9964
|
timeout: 1e4,
|
|
@@ -9609,7 +10133,7 @@ function verifierBackendSummary(backend) {
|
|
|
9609
10133
|
function cleanupContainer(backend, name) {
|
|
9610
10134
|
if (!backend.enginePath || !backend.daemonHost || !engineStable(backend)) return;
|
|
9611
10135
|
try {
|
|
9612
|
-
|
|
10136
|
+
spawnSync2(backend.enginePath, dockerArgs(backend.daemonHost, ["rm", "-f", "-v", name]), {
|
|
9613
10137
|
stdio: "ignore",
|
|
9614
10138
|
timeout: 1e4,
|
|
9615
10139
|
killSignal: "SIGKILL",
|
|
@@ -9720,29 +10244,45 @@ function runContainerStage(backend, dir, command, budgetSecs) {
|
|
|
9720
10244
|
resources: backend.resources ?? DEFAULT_CONTAINER_RESOURCE_LIMITS
|
|
9721
10245
|
});
|
|
9722
10246
|
try {
|
|
9723
|
-
const r =
|
|
10247
|
+
const r = runCapturedProcessSync(
|
|
9724
10248
|
backend.enginePath,
|
|
9725
10249
|
dockerArgs(backend.daemonHost, args),
|
|
9726
10250
|
{
|
|
9727
|
-
|
|
9728
|
-
|
|
9729
|
-
|
|
9730
|
-
|
|
10251
|
+
cwd: dirname6(backend.enginePath),
|
|
10252
|
+
env: engineClientEnv(),
|
|
10253
|
+
timeoutMs: budgetSecs * 1e3,
|
|
10254
|
+
backstopMs: 3e4
|
|
9731
10255
|
}
|
|
9732
10256
|
);
|
|
9733
10257
|
const secs = Math.round((Date.now() - t0) / 1e3);
|
|
10258
|
+
if (r.timedOut) {
|
|
10259
|
+
cleanupContainer(backend, name);
|
|
10260
|
+
return {
|
|
10261
|
+
exit: null,
|
|
10262
|
+
secs,
|
|
10263
|
+
failure: "budget",
|
|
10264
|
+
reason: "verifier stage exceeded its budget",
|
|
10265
|
+
diagnostics: r.diagnostics
|
|
10266
|
+
};
|
|
10267
|
+
}
|
|
9734
10268
|
if (r.error) {
|
|
9735
10269
|
cleanupContainer(backend, name);
|
|
9736
|
-
|
|
9737
|
-
|
|
10270
|
+
return {
|
|
10271
|
+
exit: null,
|
|
10272
|
+
secs,
|
|
10273
|
+
failure: "backend",
|
|
10274
|
+
reason: `Docker client failed: ${r.error}`,
|
|
10275
|
+
diagnostics: r.diagnostics
|
|
10276
|
+
};
|
|
9738
10277
|
}
|
|
9739
|
-
if (r.
|
|
10278
|
+
if (r.exit === null) {
|
|
9740
10279
|
cleanupContainer(backend, name);
|
|
9741
10280
|
return {
|
|
9742
10281
|
exit: null,
|
|
9743
10282
|
secs,
|
|
9744
10283
|
failure: "backend",
|
|
9745
|
-
reason: `Docker client did not return a status${r.signal ? ` (signal ${r.signal})` : ""}
|
|
10284
|
+
reason: `Docker client did not return a status${r.signal ? ` (signal ${r.signal})` : ""}`,
|
|
10285
|
+
diagnostics: r.diagnostics
|
|
9746
10286
|
};
|
|
9747
10287
|
}
|
|
9748
10288
|
if (!engineStable(backend)) {
|
|
@@ -9750,10 +10290,11 @@ function runContainerStage(backend, dir, command, budgetSecs) {
|
|
|
9750
10290
|
exit: null,
|
|
9751
10291
|
secs,
|
|
9752
10292
|
failure: "backend",
|
|
9753
|
-
reason: "Docker engine identity changed before verifier result inspection"
|
|
10293
|
+
reason: "Docker engine identity changed before verifier result inspection",
|
|
10294
|
+
diagnostics: r.diagnostics
|
|
9754
10295
|
};
|
|
9755
10296
|
}
|
|
9756
|
-
const inspected =
|
|
10297
|
+
const inspected = spawnSync2(
|
|
9757
10298
|
backend.enginePath,
|
|
9758
10299
|
dockerArgs(backend.daemonHost, ["inspect", "--format", "{{json .State}}", name]),
|
|
9759
10300
|
{
|
|
@@ -9773,7 +10314,10 @@ function runContainerStage(backend, dir, command, budgetSecs) {
|
|
|
9773
10314
|
}
|
|
9774
10315
|
}
|
|
9775
10316
|
cleanupContainer(backend, name);
|
|
9776
|
-
return
|
|
10317
|
+
return {
|
|
10318
|
+
...containerStateResult(state, secs),
|
|
10319
|
+
diagnostics: r.diagnostics
|
|
10320
|
+
};
|
|
9777
10321
|
} catch (e) {
|
|
9778
10322
|
cleanupContainer(backend, name);
|
|
9779
10323
|
return {
|
|
@@ -9878,7 +10422,7 @@ function resolveBase(base, cwd) {
|
|
|
9878
10422
|
}
|
|
9879
10423
|
}
|
|
9880
10424
|
function baseIsAncestorOfHead(base, cwd) {
|
|
9881
|
-
const r =
|
|
10425
|
+
const r = spawnSync3("git", ["merge-base", "--is-ancestor", base, "HEAD"], { cwd, env: trustedGitEnv() });
|
|
9882
10426
|
return r.status === 0;
|
|
9883
10427
|
}
|
|
9884
10428
|
function inside2(root, path) {
|
|
@@ -9902,7 +10446,7 @@ function safeSymlink(target, out2, root, label) {
|
|
|
9902
10446
|
if (rootedLinkTarget(target) || !inside2(root, resolve7(dirname7(out2), target))) {
|
|
9903
10447
|
throw linkEscape(label, target);
|
|
9904
10448
|
}
|
|
9905
|
-
|
|
10449
|
+
rmSync4(out2, { force: true });
|
|
9906
10450
|
symlinkSync(target, out2);
|
|
9907
10451
|
}
|
|
9908
10452
|
function validateSymlinkGraph(target, out2, root, dependencyRoot, label) {
|
|
@@ -9925,7 +10469,7 @@ function validateSymlinkGraph(target, out2, root, dependencyRoot, label) {
|
|
|
9925
10469
|
continue;
|
|
9926
10470
|
}
|
|
9927
10471
|
const base = domain === "tree" ? root : dependencyRoot;
|
|
9928
|
-
const current =
|
|
10472
|
+
const current = join17(base, ...stack);
|
|
9929
10473
|
let st;
|
|
9930
10474
|
try {
|
|
9931
10475
|
st = lstatSync5(current);
|
|
@@ -9961,7 +10505,7 @@ function rejectLinkedParent(cwd, rel) {
|
|
|
9961
10505
|
const parts = rel.split("/").slice(0, -1);
|
|
9962
10506
|
let current = cwd;
|
|
9963
10507
|
for (const part of parts) {
|
|
9964
|
-
current =
|
|
10508
|
+
current = join17(current, part);
|
|
9965
10509
|
let st;
|
|
9966
10510
|
try {
|
|
9967
10511
|
st = lstatSync5(current);
|
|
@@ -9977,7 +10521,7 @@ function materialize(cwd, dest, dependencyRoot) {
|
|
|
9977
10521
|
const listed = git2(["ls-files", "-z", "--cached", "--others", "--exclude-standard"], cwd).split("\0").filter(Boolean);
|
|
9978
10522
|
const links = [];
|
|
9979
10523
|
for (const rel of listed) {
|
|
9980
|
-
const src =
|
|
10524
|
+
const src = join17(cwd, rel);
|
|
9981
10525
|
rejectLinkedParent(cwd, rel);
|
|
9982
10526
|
let st;
|
|
9983
10527
|
try {
|
|
@@ -9985,7 +10529,7 @@ function materialize(cwd, dest, dependencyRoot) {
|
|
|
9985
10529
|
} catch {
|
|
9986
10530
|
continue;
|
|
9987
10531
|
}
|
|
9988
|
-
const out2 =
|
|
10532
|
+
const out2 = join17(dest, rel);
|
|
9989
10533
|
mkdirSync6(dirname7(out2), { recursive: true });
|
|
9990
10534
|
dropSymlink(out2);
|
|
9991
10535
|
if (st.isSymbolicLink()) {
|
|
@@ -10002,11 +10546,11 @@ function materialize(cwd, dest, dependencyRoot) {
|
|
|
10002
10546
|
for (const link of links) {
|
|
10003
10547
|
validateSymlinkGraph(link.target, link.out, dest, dependencyRoot, link.rel);
|
|
10004
10548
|
}
|
|
10005
|
-
if (dependencyRoot) symlinkSync(dependencyRoot,
|
|
10549
|
+
if (dependencyRoot) symlinkSync(dependencyRoot, join17(dest, "node_modules"), "dir");
|
|
10006
10550
|
}
|
|
10007
10551
|
function dropSymlink(p) {
|
|
10008
10552
|
try {
|
|
10009
|
-
if (lstatSync5(p).isSymbolicLink())
|
|
10553
|
+
if (lstatSync5(p).isSymbolicLink()) rmSync4(p, { force: true });
|
|
10010
10554
|
} catch {
|
|
10011
10555
|
}
|
|
10012
10556
|
}
|
|
@@ -10035,7 +10579,7 @@ function overlayDigest(dest, paths) {
|
|
|
10035
10579
|
for (const rel of [...paths].sort()) {
|
|
10036
10580
|
h.update(rel);
|
|
10037
10581
|
h.update("\0");
|
|
10038
|
-
const p =
|
|
10582
|
+
const p = join17(dest, rel);
|
|
10039
10583
|
try {
|
|
10040
10584
|
const st = lstatSync5(p);
|
|
10041
10585
|
h.update(String(st.mode));
|
|
@@ -10063,15 +10607,15 @@ function overlayPristine(cwd, base, dest, policy, cmd, dependencyRoot) {
|
|
|
10063
10607
|
throw new Error(`the base's ${e.path} is a submodule; the pristine copy cannot reproduce it`);
|
|
10064
10608
|
}
|
|
10065
10609
|
const content = execFileSync9("git", ["show", `${base}:${e.path}`], { cwd, maxBuffer: 1 << 28, env: trustedGitEnv() });
|
|
10066
|
-
const out2 =
|
|
10610
|
+
const out2 = join17(dest, e.path);
|
|
10067
10611
|
mkdirSync6(dirname7(out2), { recursive: true });
|
|
10068
|
-
|
|
10612
|
+
rmSync4(out2, { force: true });
|
|
10069
10613
|
if (e.mode === "120000") {
|
|
10070
10614
|
const target = content.toString("utf8");
|
|
10071
10615
|
safeSymlink(target, out2, dest, `the base's ${e.path}`);
|
|
10072
10616
|
restoredLinks.push({ path: e.path, out: out2, target });
|
|
10073
10617
|
} else {
|
|
10074
|
-
|
|
10618
|
+
writeFileSync8(out2, content);
|
|
10075
10619
|
chmodSync2(out2, parseInt(e.mode.slice(-4), 8) & 511);
|
|
10076
10620
|
}
|
|
10077
10621
|
restored.push(e.path);
|
|
@@ -10080,7 +10624,7 @@ function overlayPristine(cwd, base, dest, policy, cmd, dependencyRoot) {
|
|
|
10080
10624
|
const inCopy = git2(["ls-files", "-z", "--cached", "--others", "--exclude-standard"], cwd).split("\0").filter(Boolean);
|
|
10081
10625
|
for (const rel of inCopy) {
|
|
10082
10626
|
if (!isOverlay(rel) || baseProtected.has(rel)) continue;
|
|
10083
|
-
|
|
10627
|
+
rmSync4(join17(dest, rel), { force: true });
|
|
10084
10628
|
removed++;
|
|
10085
10629
|
}
|
|
10086
10630
|
for (const link of restoredLinks) {
|
|
@@ -10088,27 +10632,6 @@ function overlayPristine(cwd, base, dest, policy, cmd, dependencyRoot) {
|
|
|
10088
10632
|
}
|
|
10089
10633
|
return { restored, removed };
|
|
10090
10634
|
}
|
|
10091
|
-
var SUPERVISOR = `
|
|
10092
|
-
const cp = require('node:child_process');
|
|
10093
|
-
const fs = require('node:fs');
|
|
10094
|
-
const [cmd, budgetMs, outFile] = process.argv.slice(1);
|
|
10095
|
-
const child = cp.spawn('sh', ['-c', cmd], { stdio: 'ignore', detached: process.platform !== 'win32' });
|
|
10096
|
-
const killGroup = () => {
|
|
10097
|
-
try { process.kill(-child.pid, 'SIGKILL'); } catch {}
|
|
10098
|
-
try { child.kill('SIGKILL'); } catch {}
|
|
10099
|
-
};
|
|
10100
|
-
let done = false;
|
|
10101
|
-
const finish = (r) => {
|
|
10102
|
-
if (done) return;
|
|
10103
|
-
done = true;
|
|
10104
|
-
killGroup();
|
|
10105
|
-
fs.writeFileSync(outFile, JSON.stringify(r));
|
|
10106
|
-
process.exit(0);
|
|
10107
|
-
};
|
|
10108
|
-
const timer = setTimeout(() => finish({ timedOut: true }), Number(budgetMs));
|
|
10109
|
-
child.on('error', (e) => { clearTimeout(timer); finish({ error: String(e) }); });
|
|
10110
|
-
child.on('exit', (code, signal) => { clearTimeout(timer); finish({ exit: code, signal }); });
|
|
10111
|
-
`;
|
|
10112
10635
|
var DROPPED_ENV = /* @__PURE__ */ new Set([
|
|
10113
10636
|
"NODE_OPTIONS",
|
|
10114
10637
|
"NODE_PATH",
|
|
@@ -10125,9 +10648,9 @@ function suiteEnv(scratch) {
|
|
|
10125
10648
|
if (DROPPED_ENV.has(k) || PINNED_NPM.test(k)) continue;
|
|
10126
10649
|
env[k] = v;
|
|
10127
10650
|
}
|
|
10128
|
-
const userRc =
|
|
10129
|
-
const globalRc =
|
|
10130
|
-
for (const f of [userRc, globalRc])
|
|
10651
|
+
const userRc = join17(scratch, "npmrc-user");
|
|
10652
|
+
const globalRc = join17(scratch, "npmrc-global");
|
|
10653
|
+
for (const f of [userRc, globalRc]) writeFileSync8(f, "", { mode: 292 });
|
|
10131
10654
|
env.npm_config_userconfig = userRc;
|
|
10132
10655
|
env.npm_config_globalconfig = globalRc;
|
|
10133
10656
|
env.npm_config_node_options = " ";
|
|
@@ -10136,31 +10659,75 @@ function suiteEnv(scratch) {
|
|
|
10136
10659
|
}
|
|
10137
10660
|
function runLocalSuite(dir, cmd, budgetSecs) {
|
|
10138
10661
|
const t0 = Date.now();
|
|
10139
|
-
const
|
|
10140
|
-
const outFile = join16(outDir, "result.json");
|
|
10662
|
+
const scratch = mkdtempSync3(join17(tmpdir3(), "tw-verify-run-"));
|
|
10141
10663
|
try {
|
|
10142
|
-
|
|
10664
|
+
const r = runCapturedProcessSync("sh", ["-c", cmd], {
|
|
10143
10665
|
cwd: dir,
|
|
10144
|
-
env: suiteEnv(
|
|
10145
|
-
|
|
10146
|
-
|
|
10147
|
-
|
|
10148
|
-
|
|
10666
|
+
env: suiteEnv(scratch),
|
|
10667
|
+
timeoutMs: budgetSecs * 1e3,
|
|
10668
|
+
detached: process.platform !== "win32",
|
|
10669
|
+
killGroupOnFinish: true,
|
|
10670
|
+
backstopMs: 3e4
|
|
10149
10671
|
});
|
|
10150
10672
|
const secs = Math.round((Date.now() - t0) / 1e3);
|
|
10151
|
-
|
|
10152
|
-
|
|
10153
|
-
|
|
10154
|
-
|
|
10155
|
-
|
|
10673
|
+
if (r.timedOut) {
|
|
10674
|
+
return {
|
|
10675
|
+
exit: null,
|
|
10676
|
+
secs,
|
|
10677
|
+
failure: "budget",
|
|
10678
|
+
reason: "verifier stage exceeded its budget",
|
|
10679
|
+
diagnostics: r.diagnostics
|
|
10680
|
+
};
|
|
10681
|
+
}
|
|
10682
|
+
if (r.error) {
|
|
10683
|
+
return {
|
|
10684
|
+
exit: null,
|
|
10685
|
+
secs,
|
|
10686
|
+
failure: "backend",
|
|
10687
|
+
reason: r.error,
|
|
10688
|
+
diagnostics: r.diagnostics
|
|
10689
|
+
};
|
|
10690
|
+
}
|
|
10691
|
+
if (r.exit === null) {
|
|
10692
|
+
return {
|
|
10693
|
+
exit: null,
|
|
10694
|
+
secs,
|
|
10695
|
+
failure: "backend",
|
|
10696
|
+
reason: `verifier suite did not return an exit code${r.signal ? ` (signal ${r.signal})` : ""}`,
|
|
10697
|
+
diagnostics: r.diagnostics
|
|
10698
|
+
};
|
|
10156
10699
|
}
|
|
10157
|
-
|
|
10158
|
-
if (r.exit === null || r.exit === void 0) return { exit: null, secs };
|
|
10159
|
-
return { exit: r.exit, secs };
|
|
10700
|
+
return { exit: r.exit, secs, diagnostics: r.diagnostics };
|
|
10160
10701
|
} finally {
|
|
10161
|
-
|
|
10702
|
+
rmSync4(scratch, { recursive: true, force: true });
|
|
10162
10703
|
}
|
|
10163
10704
|
}
|
|
10705
|
+
function diagnosticsJson(diagnostics, includeTail) {
|
|
10706
|
+
if (!diagnostics) return void 0;
|
|
10707
|
+
const stream = (d) => ({
|
|
10708
|
+
captured_bytes: d.captured_bytes,
|
|
10709
|
+
retained_bytes: d.retained_bytes,
|
|
10710
|
+
truncated: d.truncated,
|
|
10711
|
+
...includeTail && d.captured_bytes > 0 ? { tail: d.tail } : {}
|
|
10712
|
+
});
|
|
10713
|
+
return {
|
|
10714
|
+
stdout: stream(diagnostics.stdout),
|
|
10715
|
+
stderr: stream(diagnostics.stderr)
|
|
10716
|
+
};
|
|
10717
|
+
}
|
|
10718
|
+
function stageJson(result) {
|
|
10719
|
+
const includeTail = result.exit !== 0 || result.failure !== void 0;
|
|
10720
|
+
return {
|
|
10721
|
+
exit: result.exit,
|
|
10722
|
+
secs: result.secs,
|
|
10723
|
+
...result.reason ? { reason: result.reason } : {},
|
|
10724
|
+
...result.diagnostics ? { diagnostics: diagnosticsJson(result.diagnostics, includeTail) } : {}
|
|
10725
|
+
};
|
|
10726
|
+
}
|
|
10727
|
+
function renderStageDiagnostics(out2, stage, result) {
|
|
10728
|
+
if (!result.diagnostics) return;
|
|
10729
|
+
for (const line of diagnosticLines(stage, result.diagnostics)) out2(line);
|
|
10730
|
+
}
|
|
10164
10731
|
function runVerify(opts) {
|
|
10165
10732
|
const cwd = opts.cwd ?? process.cwd();
|
|
10166
10733
|
const out2 = (s) => void process.stdout.write(s + "\n");
|
|
@@ -10242,11 +10809,11 @@ function runVerify(opts) {
|
|
|
10242
10809
|
out2(`verify: cannot resolve base rev "${opts.base ?? "HEAD"}" \u2014 failing closed`);
|
|
10243
10810
|
return 2;
|
|
10244
10811
|
}
|
|
10245
|
-
const visRoot =
|
|
10246
|
-
const visDir =
|
|
10812
|
+
const visRoot = mkdtempSync3(join17(tmpdir3(), "tw-verify-vis-"));
|
|
10813
|
+
const visDir = join17(visRoot, "t");
|
|
10247
10814
|
const cleanup = (dirs) => {
|
|
10248
10815
|
if (opts.keep) return;
|
|
10249
|
-
for (const d of dirs)
|
|
10816
|
+
for (const d of dirs) rmSync4(d, { recursive: true, force: true });
|
|
10250
10817
|
};
|
|
10251
10818
|
const protectedIgnored = (rel) => isProtected(rel, policy);
|
|
10252
10819
|
const treeBefore = treeFingerprint(cwd, protectedIgnored);
|
|
@@ -10276,14 +10843,16 @@ function runVerify(opts) {
|
|
|
10276
10843
|
stage: "visible",
|
|
10277
10844
|
...visible.resource ? { resource: visible.resource } : {},
|
|
10278
10845
|
detail: visible.reason,
|
|
10846
|
+
diagnostics: diagnosticsJson(visible.diagnostics, true),
|
|
10279
10847
|
verifier_backend: backendReport(),
|
|
10280
10848
|
oracle_assurance: oracleAssuranceReport()
|
|
10281
10849
|
}));
|
|
10282
10850
|
} else {
|
|
10283
10851
|
out2(
|
|
10284
|
-
exhausted ? "verify: isolated verifier resource envelope was exhausted during the visible stage \u2014 failing closed" : "verify:
|
|
10852
|
+
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"
|
|
10285
10853
|
);
|
|
10286
10854
|
if (visible.reason) out2("verify: " + visible.reason);
|
|
10855
|
+
renderStageDiagnostics(out2, "visible", visible);
|
|
10287
10856
|
}
|
|
10288
10857
|
return 2;
|
|
10289
10858
|
}
|
|
@@ -10300,8 +10869,8 @@ function runVerify(opts) {
|
|
|
10300
10869
|
out2((dependencyAfterVisible.reason ?? "dependency identity changed") + ". Failing closed, not open.");
|
|
10301
10870
|
return 2;
|
|
10302
10871
|
}
|
|
10303
|
-
const priRoot =
|
|
10304
|
-
const priDir =
|
|
10872
|
+
const priRoot = mkdtempSync3(join17(tmpdir3(), "tw-verify-pri-"));
|
|
10873
|
+
const priDir = join17(priRoot, "t");
|
|
10305
10874
|
let restored = [];
|
|
10306
10875
|
let removedAdded = 0;
|
|
10307
10876
|
try {
|
|
@@ -10332,14 +10901,16 @@ function runVerify(opts) {
|
|
|
10332
10901
|
stage: "pristine",
|
|
10333
10902
|
...pristine.resource ? { resource: pristine.resource } : {},
|
|
10334
10903
|
detail: pristine.reason,
|
|
10904
|
+
diagnostics: diagnosticsJson(pristine.diagnostics, true),
|
|
10335
10905
|
verifier_backend: backendReport(),
|
|
10336
10906
|
oracle_assurance: oracleAssuranceReport()
|
|
10337
10907
|
}));
|
|
10338
10908
|
} else {
|
|
10339
10909
|
out2(
|
|
10340
|
-
exhausted ? "verify: isolated verifier resource envelope was exhausted during the pristine stage \u2014 failing closed" : "verify:
|
|
10910
|
+
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"
|
|
10341
10911
|
);
|
|
10342
10912
|
if (pristine.reason) out2("verify: " + pristine.reason);
|
|
10913
|
+
renderStageDiagnostics(out2, "pristine", pristine);
|
|
10343
10914
|
}
|
|
10344
10915
|
return 2;
|
|
10345
10916
|
}
|
|
@@ -10381,8 +10952,8 @@ function runVerify(opts) {
|
|
|
10381
10952
|
base,
|
|
10382
10953
|
command: cmd,
|
|
10383
10954
|
budget_secs: budget,
|
|
10384
|
-
visible:
|
|
10385
|
-
pristine:
|
|
10955
|
+
visible: stageJson(visible),
|
|
10956
|
+
pristine: stageJson(pristine),
|
|
10386
10957
|
protected_restored: restored.length,
|
|
10387
10958
|
added_protected_removed: removedAdded,
|
|
10388
10959
|
verifier_backend: backendReport(),
|
|
@@ -10407,6 +10978,14 @@ function runVerify(opts) {
|
|
|
10407
10978
|
out2(
|
|
10408
10979
|
dependencyEnvironment ? `dependency environment: ${dependencyEnvironmentSummary(dependencyEnvironment)}` + (opts.allowDepDrift && dependencyEnvironment.status === "unattestable" ? " (operator override)" : "") : `dependency environment: verifier-owned by ${verifierBackend.image ?? "isolated image"}`
|
|
10409
10980
|
);
|
|
10981
|
+
if (verdict2 !== "VERIFIED") {
|
|
10982
|
+
if (visible.exit !== 0 || visible.failure !== void 0) {
|
|
10983
|
+
renderStageDiagnostics(out2, "visible", visible);
|
|
10984
|
+
}
|
|
10985
|
+
if (pristine.exit !== 0 || pristine.failure !== void 0) {
|
|
10986
|
+
renderStageDiagnostics(out2, "pristine", pristine);
|
|
10987
|
+
}
|
|
10988
|
+
}
|
|
10410
10989
|
if (signedOff)
|
|
10411
10990
|
out2(
|
|
10412
10991
|
`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.`
|
|
@@ -10451,11 +11030,11 @@ function parseVerify(args) {
|
|
|
10451
11030
|
}
|
|
10452
11031
|
|
|
10453
11032
|
// src/cli/run.ts
|
|
10454
|
-
import { execFileSync as execFileSync10, spawn, spawnSync as
|
|
11033
|
+
import { execFileSync as execFileSync10, spawn, spawnSync as spawnSync4 } from "node:child_process";
|
|
10455
11034
|
import { randomBytes } from "node:crypto";
|
|
10456
|
-
import { mkdtempSync as
|
|
10457
|
-
import { tmpdir as
|
|
10458
|
-
import { join as
|
|
11035
|
+
import { mkdtempSync as mkdtempSync4, readdirSync as readdirSync5, readFileSync as readFileSync17, readlinkSync as readlinkSync4, realpathSync as realpathSync6, rmSync as rmSync5 } from "node:fs";
|
|
11036
|
+
import { tmpdir as tmpdir4 } from "node:os";
|
|
11037
|
+
import { join as join18, resolve as resolve8 } from "node:path";
|
|
10459
11038
|
var out = (s) => process.stdout.write(s + "\n");
|
|
10460
11039
|
var err2 = (s) => process.stderr.write(s + "\n");
|
|
10461
11040
|
var AGENT_SUPERVISOR = String.raw`
|
|
@@ -10603,7 +11182,7 @@ function killAgentTree(pid) {
|
|
|
10603
11182
|
if (!Number.isInteger(pid) || pid <= 1) return;
|
|
10604
11183
|
if (process.platform === "win32") {
|
|
10605
11184
|
try {
|
|
10606
|
-
|
|
11185
|
+
spawnSync4("taskkill", ["/pid", String(pid), "/t", "/f"], { stdio: "ignore", timeout: 5e3 });
|
|
10607
11186
|
} catch {
|
|
10608
11187
|
}
|
|
10609
11188
|
return;
|
|
@@ -10625,11 +11204,11 @@ function killAgentTree(pid) {
|
|
|
10625
11204
|
}
|
|
10626
11205
|
}
|
|
10627
11206
|
function runAgentWithBudget(argv2, cwd, budgetSecs) {
|
|
10628
|
-
const stateDir =
|
|
10629
|
-
const resultFile =
|
|
11207
|
+
const stateDir = mkdtempSync4(join18(tmpdir4(), "tw-agent-supervisor-"));
|
|
11208
|
+
const resultFile = join18(stateDir, "result.json");
|
|
10630
11209
|
let supervisorTimedOut = false;
|
|
10631
11210
|
try {
|
|
10632
|
-
const supervisor =
|
|
11211
|
+
const supervisor = spawnSync4(
|
|
10633
11212
|
process.execPath,
|
|
10634
11213
|
["-e", AGENT_SUPERVISOR, resultFile, String(budgetSecs * 1e3), ...argv2],
|
|
10635
11214
|
{
|
|
@@ -10671,7 +11250,7 @@ function runAgentWithBudget(argv2, cwd, budgetSecs) {
|
|
|
10671
11250
|
failure: state.failure ?? "agent supervisor did not produce a final status"
|
|
10672
11251
|
};
|
|
10673
11252
|
} finally {
|
|
10674
|
-
|
|
11253
|
+
rmSync5(stateDir, { recursive: true, force: true });
|
|
10675
11254
|
}
|
|
10676
11255
|
}
|
|
10677
11256
|
var waitCell = new Int32Array(new SharedArrayBuffer(4));
|
|
@@ -10688,8 +11267,8 @@ function pidAlive2(pid) {
|
|
|
10688
11267
|
}
|
|
10689
11268
|
}
|
|
10690
11269
|
function supervisedObserverLog(cwd) {
|
|
10691
|
-
const gd = gitDir(cwd) ??
|
|
10692
|
-
return
|
|
11270
|
+
const gd = gitDir(cwd) ?? join18(cwd, ".git");
|
|
11271
|
+
return join18(
|
|
10693
11272
|
gd,
|
|
10694
11273
|
"tamperward",
|
|
10695
11274
|
`run-observer-${process.pid}-${Date.now()}-${randomBytes(4).toString("hex")}.jsonl`
|
|
@@ -10758,7 +11337,16 @@ function observerSummary(observer, telemetry) {
|
|
|
10758
11337
|
}
|
|
10759
11338
|
function collectObserverFindings(observer, telemetry, cwd, base, head, policy) {
|
|
10760
11339
|
observerSummary(observer, telemetry);
|
|
10761
|
-
const
|
|
11340
|
+
const strict = process.env.TAMPERWARD_TRANSIENT === "block";
|
|
11341
|
+
const drained = drainEvents(observer.log, 0);
|
|
11342
|
+
const { events } = drained;
|
|
11343
|
+
if (!drained.complete) {
|
|
11344
|
+
const issue = drained.issue ?? "read-stalled";
|
|
11345
|
+
out(
|
|
11346
|
+
`tamperward run \u2014 transient observer: telemetry was not fully classified (${issue}; ${drained.bytesRead}/${MAX_EVENT_SWEEP_BYTES} bytes read). ` + (strict ? "Strict transient policy fails closed." : "Observer telemetry remains advisory, so this degrades evidence but does not independently fail the envelope.")
|
|
11347
|
+
);
|
|
11348
|
+
if (strict) return { blocking: true };
|
|
11349
|
+
}
|
|
10762
11350
|
if (events.length === 0) return { blocking: false };
|
|
10763
11351
|
let persistent = /* @__PURE__ */ new Set();
|
|
10764
11352
|
try {
|
|
@@ -10771,9 +11359,9 @@ function collectObserverFindings(observer, telemetry, cwd, base, head, policy) {
|
|
|
10771
11359
|
);
|
|
10772
11360
|
} catch (e) {
|
|
10773
11361
|
out(
|
|
10774
|
-
`tamperward run \u2014 transient observer: recorded ${events.length} event(s), but could not classify them against the final diff (${e instanceof Error ? e.message : String(e)}).`
|
|
11362
|
+
`tamperward run \u2014 transient observer: recorded ${events.length} event(s), but could not classify them against the final diff (${e instanceof Error ? e.message : String(e)}).` + (strict ? " Strict transient policy fails closed." : "")
|
|
10775
11363
|
);
|
|
10776
|
-
return { blocking:
|
|
11364
|
+
return { blocking: strict };
|
|
10777
11365
|
}
|
|
10778
11366
|
const finalHash = (path) => {
|
|
10779
11367
|
const e = inspectRel(cwd, path);
|
|
@@ -10925,7 +11513,7 @@ function runEnvelope(opts) {
|
|
|
10925
11513
|
const rewritesBefore = objectRewriteState(cwd);
|
|
10926
11514
|
const spawnTicks = nowTicks();
|
|
10927
11515
|
const agentRun = opts.agentBudget === void 0 ? (() => {
|
|
10928
|
-
const agent =
|
|
11516
|
+
const agent = spawnSync4(opts.argv[0], opts.argv.slice(1), { cwd, stdio: "inherit" });
|
|
10929
11517
|
return {
|
|
10930
11518
|
exit: agent.status ?? 1,
|
|
10931
11519
|
timedOut: false,
|
|
@@ -10965,7 +11553,7 @@ tamperward run \u2014 agent exit ${agentExit}; DEPENDENCY_DRIFT \u2192 ENFORCEME
|
|
|
10965
11553
|
err2("(--allow-dep-drift: proceeding anyway, on the operator's judgement.)");
|
|
10966
11554
|
}
|
|
10967
11555
|
const head = git3(["rev-parse", "HEAD"], cwd).trim();
|
|
10968
|
-
const isAncestor =
|
|
11556
|
+
const isAncestor = spawnSync4("git", ["merge-base", "--is-ancestor", base, head], { cwd, env: trustedGitEnv() });
|
|
10969
11557
|
if (isAncestor.status !== 0) {
|
|
10970
11558
|
err2(`tamperward run: HEAD ${head.slice(0, 10)} is not a descendant of the trusted base ${base.slice(0, 10)} \u2014`);
|
|
10971
11559
|
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.0",
|
|
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",
|