tamperward 2.15.1 → 2.15.2
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 +13 -0
- package/dist/cli/index.js +255 -43
- 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
|
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
|
|
@@ -7253,26 +7253,200 @@ function runWatch(args) {
|
|
|
7253
7253
|
}
|
|
7254
7254
|
|
|
7255
7255
|
// src/detectors/fs-events.ts
|
|
7256
|
-
import {
|
|
7256
|
+
import { closeSync as closeSync2, openSync as openSync2, readSync as readSync2, statSync } from "node:fs";
|
|
7257
7257
|
var RULE11 = "transient-protected-mutation";
|
|
7258
|
-
|
|
7258
|
+
var MAX_EVENT_READ_BYTES = 4 * 1024 * 1024;
|
|
7259
|
+
var MAX_EVENT_SWEEP_BYTES = 16 * 1024 * 1024;
|
|
7260
|
+
var DEFAULT_EVENT_READ_IO = {
|
|
7261
|
+
read: (fd, buffer, bufferOffset, length, position) => readSync2(fd, buffer, bufferOffset, length, position)
|
|
7262
|
+
};
|
|
7263
|
+
function readEvents(log, offset, maxBytes = MAX_EVENT_READ_BYTES, io = DEFAULT_EVENT_READ_IO) {
|
|
7259
7264
|
let size = 0;
|
|
7260
7265
|
try {
|
|
7261
7266
|
size = statSync(log).size;
|
|
7262
7267
|
} catch {
|
|
7263
|
-
return {
|
|
7268
|
+
return {
|
|
7269
|
+
events: [],
|
|
7270
|
+
newOffset: Math.max(0, Number.isFinite(offset) ? Math.floor(offset) : 0),
|
|
7271
|
+
bytesRead: 0,
|
|
7272
|
+
limitReached: false,
|
|
7273
|
+
malformedLines: 0,
|
|
7274
|
+
incompleteTail: false
|
|
7275
|
+
};
|
|
7276
|
+
}
|
|
7277
|
+
let start = Number.isFinite(offset) ? Math.max(0, Math.floor(offset)) : 0;
|
|
7278
|
+
if (start > size) start = 0;
|
|
7279
|
+
if (size <= start) {
|
|
7280
|
+
return {
|
|
7281
|
+
events: [],
|
|
7282
|
+
newOffset: start,
|
|
7283
|
+
bytesRead: 0,
|
|
7284
|
+
limitReached: false,
|
|
7285
|
+
malformedLines: 0,
|
|
7286
|
+
incompleteTail: false
|
|
7287
|
+
};
|
|
7288
|
+
}
|
|
7289
|
+
const cap = Number.isFinite(maxBytes) && maxBytes > 0 ? Math.max(1, Math.floor(maxBytes)) : MAX_EVENT_READ_BYTES;
|
|
7290
|
+
const available = size - start;
|
|
7291
|
+
const requested = Math.min(available, cap);
|
|
7292
|
+
const buffer = Buffer.allocUnsafe(requested);
|
|
7293
|
+
const fd = openSync2(log, "r");
|
|
7294
|
+
let bytesRead = 0;
|
|
7295
|
+
try {
|
|
7296
|
+
bytesRead = io.read(fd, buffer, 0, requested, start);
|
|
7297
|
+
} finally {
|
|
7298
|
+
closeSync2(fd);
|
|
7299
|
+
}
|
|
7300
|
+
if (bytesRead <= 0) {
|
|
7301
|
+
return {
|
|
7302
|
+
events: [],
|
|
7303
|
+
newOffset: start,
|
|
7304
|
+
bytesRead: 0,
|
|
7305
|
+
limitReached: available > 0,
|
|
7306
|
+
malformedLines: 0,
|
|
7307
|
+
incompleteTail: available > 0
|
|
7308
|
+
};
|
|
7309
|
+
}
|
|
7310
|
+
const chunk = buffer.subarray(0, bytesRead);
|
|
7311
|
+
const lastNewline = chunk.lastIndexOf(10);
|
|
7312
|
+
const limitReached = start + bytesRead < size;
|
|
7313
|
+
if (lastNewline < 0) {
|
|
7314
|
+
return {
|
|
7315
|
+
events: [],
|
|
7316
|
+
newOffset: start,
|
|
7317
|
+
bytesRead,
|
|
7318
|
+
limitReached,
|
|
7319
|
+
malformedLines: 0,
|
|
7320
|
+
incompleteTail: true
|
|
7321
|
+
};
|
|
7264
7322
|
}
|
|
7265
|
-
if (size <= offset) return { events: [], newOffset: offset >= 0 ? Math.min(offset, size) : 0 };
|
|
7266
|
-
const raw = readFileSync10(log, "utf8").slice(offset);
|
|
7267
7323
|
const events = [];
|
|
7268
|
-
|
|
7324
|
+
let malformedLines = 0;
|
|
7325
|
+
const complete = chunk.subarray(0, lastNewline + 1).toString("utf8");
|
|
7326
|
+
for (const line of complete.split("\n")) {
|
|
7269
7327
|
if (!line.trim()) continue;
|
|
7270
7328
|
try {
|
|
7271
7329
|
events.push(JSON.parse(line));
|
|
7272
7330
|
} catch {
|
|
7331
|
+
malformedLines++;
|
|
7273
7332
|
}
|
|
7274
7333
|
}
|
|
7275
|
-
|
|
7334
|
+
const newOffset = start + lastNewline + 1;
|
|
7335
|
+
return {
|
|
7336
|
+
events,
|
|
7337
|
+
newOffset,
|
|
7338
|
+
bytesRead,
|
|
7339
|
+
limitReached,
|
|
7340
|
+
malformedLines,
|
|
7341
|
+
incompleteTail: newOffset < start + bytesRead
|
|
7342
|
+
};
|
|
7343
|
+
}
|
|
7344
|
+
function drainEvents(log, offset, maxTotalBytes = MAX_EVENT_SWEEP_BYTES, io = DEFAULT_EVENT_READ_IO) {
|
|
7345
|
+
const start = Number.isFinite(offset) ? Math.max(0, Math.floor(offset)) : 0;
|
|
7346
|
+
const aggregateCap = Number.isFinite(maxTotalBytes) && maxTotalBytes > 0 ? Math.max(1, Math.floor(maxTotalBytes)) : MAX_EVENT_SWEEP_BYTES;
|
|
7347
|
+
const events = [];
|
|
7348
|
+
let cursor = start;
|
|
7349
|
+
let bytesRead = 0;
|
|
7350
|
+
let batches = 0;
|
|
7351
|
+
let malformedLines = 0;
|
|
7352
|
+
while (bytesRead < aggregateCap) {
|
|
7353
|
+
const remainingBudget = aggregateCap - bytesRead;
|
|
7354
|
+
const batch = readEvents(
|
|
7355
|
+
log,
|
|
7356
|
+
cursor,
|
|
7357
|
+
Math.min(MAX_EVENT_READ_BYTES, remainingBudget),
|
|
7358
|
+
io
|
|
7359
|
+
);
|
|
7360
|
+
batches++;
|
|
7361
|
+
bytesRead += batch.bytesRead;
|
|
7362
|
+
malformedLines += batch.malformedLines;
|
|
7363
|
+
events.push(...batch.events);
|
|
7364
|
+
if (batch.malformedLines > 0) {
|
|
7365
|
+
return {
|
|
7366
|
+
events,
|
|
7367
|
+
newOffset: cursor,
|
|
7368
|
+
bytesRead,
|
|
7369
|
+
batches,
|
|
7370
|
+
malformedLines,
|
|
7371
|
+
complete: false,
|
|
7372
|
+
issue: "malformed-record"
|
|
7373
|
+
};
|
|
7374
|
+
}
|
|
7375
|
+
if (batch.bytesRead === 0) {
|
|
7376
|
+
if (batch.limitReached || batch.incompleteTail) {
|
|
7377
|
+
return {
|
|
7378
|
+
events,
|
|
7379
|
+
newOffset: cursor,
|
|
7380
|
+
bytesRead,
|
|
7381
|
+
batches,
|
|
7382
|
+
malformedLines,
|
|
7383
|
+
complete: false,
|
|
7384
|
+
issue: "read-stalled"
|
|
7385
|
+
};
|
|
7386
|
+
}
|
|
7387
|
+
return {
|
|
7388
|
+
events,
|
|
7389
|
+
newOffset: cursor,
|
|
7390
|
+
bytesRead,
|
|
7391
|
+
batches,
|
|
7392
|
+
malformedLines,
|
|
7393
|
+
complete: true
|
|
7394
|
+
};
|
|
7395
|
+
}
|
|
7396
|
+
if (batch.limitReached && batch.newOffset === cursor) {
|
|
7397
|
+
return {
|
|
7398
|
+
events,
|
|
7399
|
+
newOffset: cursor,
|
|
7400
|
+
bytesRead,
|
|
7401
|
+
batches,
|
|
7402
|
+
malformedLines,
|
|
7403
|
+
complete: false,
|
|
7404
|
+
issue: "oversized-record"
|
|
7405
|
+
};
|
|
7406
|
+
}
|
|
7407
|
+
if (!batch.limitReached && batch.incompleteTail) {
|
|
7408
|
+
return {
|
|
7409
|
+
events,
|
|
7410
|
+
newOffset: cursor,
|
|
7411
|
+
bytesRead,
|
|
7412
|
+
batches,
|
|
7413
|
+
malformedLines,
|
|
7414
|
+
complete: false,
|
|
7415
|
+
issue: "incomplete-record"
|
|
7416
|
+
};
|
|
7417
|
+
}
|
|
7418
|
+
cursor = batch.newOffset;
|
|
7419
|
+
if (!batch.limitReached) {
|
|
7420
|
+
return {
|
|
7421
|
+
events,
|
|
7422
|
+
newOffset: cursor,
|
|
7423
|
+
bytesRead,
|
|
7424
|
+
batches,
|
|
7425
|
+
malformedLines,
|
|
7426
|
+
complete: true
|
|
7427
|
+
};
|
|
7428
|
+
}
|
|
7429
|
+
if (bytesRead >= aggregateCap) {
|
|
7430
|
+
return {
|
|
7431
|
+
events,
|
|
7432
|
+
newOffset: cursor,
|
|
7433
|
+
bytesRead,
|
|
7434
|
+
batches,
|
|
7435
|
+
malformedLines,
|
|
7436
|
+
complete: false,
|
|
7437
|
+
issue: "aggregate-limit"
|
|
7438
|
+
};
|
|
7439
|
+
}
|
|
7440
|
+
}
|
|
7441
|
+
return {
|
|
7442
|
+
events,
|
|
7443
|
+
newOffset: cursor,
|
|
7444
|
+
bytesRead,
|
|
7445
|
+
batches,
|
|
7446
|
+
malformedLines,
|
|
7447
|
+
complete: false,
|
|
7448
|
+
issue: "aggregate-limit"
|
|
7449
|
+
};
|
|
7276
7450
|
}
|
|
7277
7451
|
function transientFindings(events, persistentPaths, policy, finalHash) {
|
|
7278
7452
|
if (!isEnabled(RULE11, policy)) return [];
|
|
@@ -7316,7 +7490,7 @@ var HookInputError = class extends Error {
|
|
|
7316
7490
|
};
|
|
7317
7491
|
function readStdin() {
|
|
7318
7492
|
try {
|
|
7319
|
-
return
|
|
7493
|
+
return readFileSync10(0, "utf8");
|
|
7320
7494
|
} catch (e) {
|
|
7321
7495
|
throw new HookInputError(`cannot read the hook payload on stdin: ${e instanceof Error ? e.message : String(e)}`);
|
|
7322
7496
|
}
|
|
@@ -7564,6 +7738,17 @@ function cursorPath(cwd, sessionId) {
|
|
|
7564
7738
|
const p = ptreePath(cwd, sessionId);
|
|
7565
7739
|
return p ? p.replace(/ptree-/, "fscursor-") : null;
|
|
7566
7740
|
}
|
|
7741
|
+
function observerIntegrityBlock(issue, bytesRead, malformedLines) {
|
|
7742
|
+
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";
|
|
7743
|
+
return {
|
|
7744
|
+
rule: "tamperward-unavailable",
|
|
7745
|
+
severity: "block",
|
|
7746
|
+
message: `TamperWard could not fully classify transient observer telemetry (${detail}), so Stop is blocked rather than certifying an unjudged tail.`,
|
|
7747
|
+
evidence: `observer_issue=${issue}; bytes_read=${bytesRead}; malformed_lines=${malformedLines}`,
|
|
7748
|
+
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.",
|
|
7749
|
+
signoff: { required: true, command: 'tamperward allow --reason "..."' }
|
|
7750
|
+
};
|
|
7751
|
+
}
|
|
7567
7752
|
function turnTransientBlocks(cwd, sessionId, policy, changes) {
|
|
7568
7753
|
const none = { blocks: [], commit: () => {
|
|
7569
7754
|
} };
|
|
@@ -7576,16 +7761,34 @@ function turnTransientBlocks(cwd, sessionId, policy, changes) {
|
|
|
7576
7761
|
if (!existsSync8(log)) return none;
|
|
7577
7762
|
let offset = 0;
|
|
7578
7763
|
if (cp && existsSync8(cp)) {
|
|
7579
|
-
const n = Number(
|
|
7764
|
+
const n = Number(readFileSync10(cp, "utf8"));
|
|
7580
7765
|
offset = Number.isFinite(n) ? n : 0;
|
|
7581
7766
|
}
|
|
7582
|
-
const
|
|
7583
|
-
if (
|
|
7584
|
-
|
|
7585
|
-
|
|
7586
|
-
|
|
7587
|
-
|
|
7588
|
-
|
|
7767
|
+
const drained = drainEvents(log, offset);
|
|
7768
|
+
if (!drained.complete) {
|
|
7769
|
+
const issue = drained.issue ?? "read-stalled";
|
|
7770
|
+
recordObserverHealth(
|
|
7771
|
+
"degraded",
|
|
7772
|
+
`event telemetry was not fully classified (${issue}); Stop retained the prior cursor`
|
|
7773
|
+
);
|
|
7774
|
+
return {
|
|
7775
|
+
blocks: [observerIntegrityBlock(issue, drained.bytesRead, drained.malformedLines)],
|
|
7776
|
+
commit: () => {
|
|
7777
|
+
}
|
|
7778
|
+
};
|
|
7779
|
+
}
|
|
7780
|
+
const { events, newOffset } = drained;
|
|
7781
|
+
if (events.length === 0) {
|
|
7782
|
+
return {
|
|
7783
|
+
...none,
|
|
7784
|
+
commit: () => {
|
|
7785
|
+
if (cp) try {
|
|
7786
|
+
writeFileSync5(cp, String(newOffset));
|
|
7787
|
+
} catch {
|
|
7788
|
+
}
|
|
7789
|
+
}
|
|
7790
|
+
};
|
|
7791
|
+
}
|
|
7589
7792
|
const persistent = new Set(changes.filter((c) => c.kind === "file").map((c) => c.path));
|
|
7590
7793
|
const finalHash = (path) => {
|
|
7591
7794
|
const e = inspectRel(cwd, path);
|
|
@@ -7742,7 +7945,7 @@ Honored at LOCAL pre-commit only. The agent-layer hook ignores this file; CI req
|
|
|
7742
7945
|
// src/cli/init.ts
|
|
7743
7946
|
import { execFileSync as execFileSync6 } from "node:child_process";
|
|
7744
7947
|
import { createHash as createHash6 } from "node:crypto";
|
|
7745
|
-
import { existsSync as existsSync9, mkdirSync as mkdirSync5, readFileSync as
|
|
7948
|
+
import { existsSync as existsSync9, mkdirSync as mkdirSync5, readFileSync as readFileSync11, writeFileSync as writeFileSync6, chmodSync, statSync as statSync2 } from "node:fs";
|
|
7746
7949
|
import { basename as basename2, dirname as dirname4, isAbsolute as isAbsolute8, join as join12, relative as relative3, resolve as resolve3 } from "node:path";
|
|
7747
7950
|
|
|
7748
7951
|
// src/verifier-limits.ts
|
|
@@ -7986,7 +8189,7 @@ function planClaudeHooks(cwd) {
|
|
|
7986
8189
|
if (existsSync9(path)) {
|
|
7987
8190
|
let parsed;
|
|
7988
8191
|
try {
|
|
7989
|
-
parsed = JSON.parse(
|
|
8192
|
+
parsed = JSON.parse(readFileSync11(path, "utf8"));
|
|
7990
8193
|
} catch {
|
|
7991
8194
|
return {
|
|
7992
8195
|
item: "agent",
|
|
@@ -8132,7 +8335,7 @@ function planPreCommit(cwd) {
|
|
|
8132
8335
|
if (existsSync9(path) && !statSync2(path).isFile()) {
|
|
8133
8336
|
return { item: "pre-commit", path: rel, status: "error", detail: "exists but is not a regular file \u2014 refusing to write" };
|
|
8134
8337
|
}
|
|
8135
|
-
const existing = existsSync9(path) ?
|
|
8338
|
+
const existing = existsSync9(path) ? readFileSync11(path, "utf8") : null;
|
|
8136
8339
|
if (existing === null) {
|
|
8137
8340
|
return {
|
|
8138
8341
|
item: "pre-commit",
|
|
@@ -8224,7 +8427,7 @@ function planCodeowners(cwd) {
|
|
|
8224
8427
|
(r) => existsSync9(join12(cwd, r))
|
|
8225
8428
|
);
|
|
8226
8429
|
const path = join12(cwd, existingRel ?? rel);
|
|
8227
|
-
const existing = existingRel ?
|
|
8430
|
+
const existing = existingRel ? readFileSync11(path, "utf8") : null;
|
|
8228
8431
|
const missing = CODEOWNERS_PATHS.filter((p) => existing === null || !coveredBy(existing, p));
|
|
8229
8432
|
if (missing.length === 0) {
|
|
8230
8433
|
return { item: "codeowners", path: existingRel, status: "ok", detail: "gate paths already have code owners" };
|
|
@@ -8273,7 +8476,7 @@ function planWorkflow(cwd, force) {
|
|
|
8273
8476
|
}
|
|
8274
8477
|
let src;
|
|
8275
8478
|
try {
|
|
8276
|
-
src =
|
|
8479
|
+
src = readFileSync11(path, "utf8");
|
|
8277
8480
|
} catch {
|
|
8278
8481
|
return { item: "ci", path: rel, status: "error", detail: "exists but cannot be read \u2014 refusing to overwrite" };
|
|
8279
8482
|
}
|
|
@@ -8372,7 +8575,7 @@ tamperward init: ${changed} change(s) applied. Commit them so the gate travels w
|
|
|
8372
8575
|
|
|
8373
8576
|
// src/cli/doctor.ts
|
|
8374
8577
|
import { execFileSync as execFileSync7 } from "node:child_process";
|
|
8375
|
-
import { existsSync as existsSync10, readFileSync as
|
|
8578
|
+
import { existsSync as existsSync10, readFileSync as readFileSync12, readdirSync as readdirSync3 } from "node:fs";
|
|
8376
8579
|
import { join as join13, resolve as resolve4 } from "node:path";
|
|
8377
8580
|
import { parse as parse3 } from "yaml";
|
|
8378
8581
|
var VERIFY_COMMAND = /\btamperward(?:@\S+)?\s+verify\b/;
|
|
@@ -8410,7 +8613,7 @@ function pinsInFile(path) {
|
|
|
8410
8613
|
if (!existsSync10(path)) return [];
|
|
8411
8614
|
let src = "";
|
|
8412
8615
|
try {
|
|
8413
|
-
src =
|
|
8616
|
+
src = readFileSync12(path, "utf8");
|
|
8414
8617
|
} catch {
|
|
8415
8618
|
return [];
|
|
8416
8619
|
}
|
|
@@ -8437,7 +8640,7 @@ function workflowPermissionCheck(cwd, workflowRels = [".github/workflows/tamperw
|
|
|
8437
8640
|
}
|
|
8438
8641
|
let doc;
|
|
8439
8642
|
try {
|
|
8440
|
-
doc = parse3(
|
|
8643
|
+
doc = parse3(readFileSync12(path, "utf8"));
|
|
8441
8644
|
} catch (e) {
|
|
8442
8645
|
broken.push(`${rel} is not valid YAML (${e instanceof Error ? e.message : String(e)})`);
|
|
8443
8646
|
continue;
|
|
@@ -8843,7 +9046,7 @@ function runDoctor(opts = {}) {
|
|
|
8843
9046
|
}
|
|
8844
9047
|
let doc;
|
|
8845
9048
|
try {
|
|
8846
|
-
doc = parse3(
|
|
9049
|
+
doc = parse3(readFileSync12(workflowPath, "utf8"));
|
|
8847
9050
|
} catch (e) {
|
|
8848
9051
|
return fail(
|
|
8849
9052
|
"ci-verifier",
|
|
@@ -8951,7 +9154,7 @@ function runDoctor(opts = {}) {
|
|
|
8951
9154
|
// src/cli/verify.ts
|
|
8952
9155
|
import { execFileSync as execFileSync9, spawnSync as spawnSync2 } from "node:child_process";
|
|
8953
9156
|
import { createHash as createHash10 } from "node:crypto";
|
|
8954
|
-
import { chmodSync as chmodSync2, cpSync, lstatSync as lstatSync5, mkdirSync as mkdirSync6, mkdtempSync as mkdtempSync2, readFileSync as
|
|
9157
|
+
import { chmodSync as chmodSync2, cpSync, lstatSync as lstatSync5, mkdirSync as mkdirSync6, mkdtempSync as mkdtempSync2, readFileSync as readFileSync15, readlinkSync as readlinkSync3, rmSync as rmSync3, symlinkSync, writeFileSync as writeFileSync7 } from "node:fs";
|
|
8955
9158
|
import { tmpdir as tmpdir2 } from "node:os";
|
|
8956
9159
|
import { dirname as dirname7, isAbsolute as isAbsolute10, join as join16, relative as relative5, resolve as resolve7, sep as sep2 } from "node:path";
|
|
8957
9160
|
|
|
@@ -8987,7 +9190,7 @@ import {
|
|
|
8987
9190
|
existsSync as existsSync11,
|
|
8988
9191
|
lstatSync as lstatSync4,
|
|
8989
9192
|
readdirSync as readdirSync4,
|
|
8990
|
-
readFileSync as
|
|
9193
|
+
readFileSync as readFileSync13,
|
|
8991
9194
|
readlinkSync as readlinkSync2,
|
|
8992
9195
|
realpathSync as realpathSync3,
|
|
8993
9196
|
statSync as statSync3
|
|
@@ -9044,7 +9247,7 @@ function fingerprintRoot(root, cwd, h) {
|
|
|
9044
9247
|
st = statSync3(path);
|
|
9045
9248
|
if (!st.isFile()) throw new Error("not a regular file");
|
|
9046
9249
|
h.update(label + "\0external-file\0" + path + "\0" + String(st.mode) + "\0");
|
|
9047
|
-
h.update(
|
|
9250
|
+
h.update(readFileSync13(path));
|
|
9048
9251
|
h.update("\0");
|
|
9049
9252
|
} catch (e) {
|
|
9050
9253
|
throw new Unattestable(
|
|
@@ -9078,7 +9281,7 @@ function fingerprintRoot(root, cwd, h) {
|
|
|
9078
9281
|
h.update(rel + "\0" + ident.kind + "\0" + String(ident.mode) + "\0");
|
|
9079
9282
|
if (ident.kind === "file") {
|
|
9080
9283
|
try {
|
|
9081
|
-
h.update(
|
|
9284
|
+
h.update(readFileSync13(path));
|
|
9082
9285
|
h.update("\0");
|
|
9083
9286
|
} catch (e) {
|
|
9084
9287
|
throw new Unattestable(
|
|
@@ -9239,7 +9442,7 @@ function venvProblem(root) {
|
|
|
9239
9442
|
const cfg = join14(root, "pyvenv.cfg");
|
|
9240
9443
|
if (!existsSync11(cfg)) return "VIRTUAL_ENV " + JSON.stringify(root) + " has no pyvenv.cfg";
|
|
9241
9444
|
try {
|
|
9242
|
-
const body =
|
|
9445
|
+
const body = readFileSync13(cfg, "utf8");
|
|
9243
9446
|
if (/^\s*include-system-site-packages\s*=\s*true\s*$/im.test(body)) {
|
|
9244
9447
|
return "Python environment " + JSON.stringify(root) + " enables system site-packages; those external package roots are not bounded";
|
|
9245
9448
|
}
|
|
@@ -9337,7 +9540,7 @@ function dependencyEnvironmentSummary(descriptor) {
|
|
|
9337
9540
|
// src/verifier-backend.ts
|
|
9338
9541
|
import { createHash as createHash9, randomUUID } from "node:crypto";
|
|
9339
9542
|
import { spawnSync } from "node:child_process";
|
|
9340
|
-
import { accessSync, constants as constants2, readFileSync as
|
|
9543
|
+
import { accessSync, constants as constants2, readFileSync as readFileSync14, realpathSync as realpathSync4, statSync as statSync4 } from "node:fs";
|
|
9341
9544
|
import { delimiter as delimiter2, dirname as dirname6, join as join15, resolve as resolve6 } from "node:path";
|
|
9342
9545
|
var DEFAULT_CONTAINER_RESOURCE_LIMITS = Object.freeze({
|
|
9343
9546
|
memoryBytes: 2 * 1024 * 1024 * 1024,
|
|
@@ -9391,7 +9594,7 @@ function findExecutable(name) {
|
|
|
9391
9594
|
return null;
|
|
9392
9595
|
}
|
|
9393
9596
|
function fileSha256(path) {
|
|
9394
|
-
return createHash9("sha256").update(
|
|
9597
|
+
return createHash9("sha256").update(readFileSync14(path)).digest("hex");
|
|
9395
9598
|
}
|
|
9396
9599
|
function engineClientEnv() {
|
|
9397
9600
|
return {
|
|
@@ -10039,7 +10242,7 @@ function overlayDigest(dest, paths) {
|
|
|
10039
10242
|
try {
|
|
10040
10243
|
const st = lstatSync5(p);
|
|
10041
10244
|
h.update(String(st.mode));
|
|
10042
|
-
h.update(st.isSymbolicLink() ? readlinkSync3(p) :
|
|
10245
|
+
h.update(st.isSymbolicLink() ? readlinkSync3(p) : readFileSync15(p));
|
|
10043
10246
|
} catch {
|
|
10044
10247
|
h.update("<gone>");
|
|
10045
10248
|
}
|
|
@@ -10150,7 +10353,7 @@ function runLocalSuite(dir, cmd, budgetSecs) {
|
|
|
10150
10353
|
const secs = Math.round((Date.now() - t0) / 1e3);
|
|
10151
10354
|
let r;
|
|
10152
10355
|
try {
|
|
10153
|
-
r = JSON.parse(
|
|
10356
|
+
r = JSON.parse(readFileSync15(outFile, "utf8"));
|
|
10154
10357
|
} catch {
|
|
10155
10358
|
return { exit: null, secs };
|
|
10156
10359
|
}
|
|
@@ -10453,7 +10656,7 @@ function parseVerify(args) {
|
|
|
10453
10656
|
// src/cli/run.ts
|
|
10454
10657
|
import { execFileSync as execFileSync10, spawn, spawnSync as spawnSync3 } from "node:child_process";
|
|
10455
10658
|
import { randomBytes } from "node:crypto";
|
|
10456
|
-
import { mkdtempSync as mkdtempSync3, readdirSync as readdirSync5, readFileSync as
|
|
10659
|
+
import { mkdtempSync as mkdtempSync3, readdirSync as readdirSync5, readFileSync as readFileSync16, readlinkSync as readlinkSync4, realpathSync as realpathSync6, rmSync as rmSync4 } from "node:fs";
|
|
10457
10660
|
import { tmpdir as tmpdir3 } from "node:os";
|
|
10458
10661
|
import { join as join17, resolve as resolve8 } from "node:path";
|
|
10459
10662
|
var out = (s) => process.stdout.write(s + "\n");
|
|
@@ -10575,7 +10778,7 @@ function linuxDescendantPids(rootPid) {
|
|
|
10575
10778
|
for (const name of names) {
|
|
10576
10779
|
const pid = Number(name);
|
|
10577
10780
|
try {
|
|
10578
|
-
const stat =
|
|
10781
|
+
const stat = readFileSync16(`/proc/${name}/stat`, "utf8");
|
|
10579
10782
|
const fields = stat.slice(stat.lastIndexOf(")") + 2).split(" ");
|
|
10580
10783
|
const ppid = Number(fields[1]);
|
|
10581
10784
|
if (!Number.isFinite(ppid)) continue;
|
|
@@ -10644,7 +10847,7 @@ function runAgentWithBudget(argv2, cwd, budgetSecs) {
|
|
|
10644
10847
|
supervisorTimedOut = Boolean(supervisor.error) && supervisor.error.code === "ETIMEDOUT";
|
|
10645
10848
|
let state = {};
|
|
10646
10849
|
try {
|
|
10647
|
-
state = JSON.parse(
|
|
10850
|
+
state = JSON.parse(readFileSync16(resultFile, "utf8"));
|
|
10648
10851
|
} catch {
|
|
10649
10852
|
}
|
|
10650
10853
|
if (supervisorTimedOut || state.timedOut) {
|
|
@@ -10758,7 +10961,16 @@ function observerSummary(observer, telemetry) {
|
|
|
10758
10961
|
}
|
|
10759
10962
|
function collectObserverFindings(observer, telemetry, cwd, base, head, policy) {
|
|
10760
10963
|
observerSummary(observer, telemetry);
|
|
10761
|
-
const
|
|
10964
|
+
const strict = process.env.TAMPERWARD_TRANSIENT === "block";
|
|
10965
|
+
const drained = drainEvents(observer.log, 0);
|
|
10966
|
+
const { events } = drained;
|
|
10967
|
+
if (!drained.complete) {
|
|
10968
|
+
const issue = drained.issue ?? "read-stalled";
|
|
10969
|
+
out(
|
|
10970
|
+
`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.")
|
|
10971
|
+
);
|
|
10972
|
+
if (strict) return { blocking: true };
|
|
10973
|
+
}
|
|
10762
10974
|
if (events.length === 0) return { blocking: false };
|
|
10763
10975
|
let persistent = /* @__PURE__ */ new Set();
|
|
10764
10976
|
try {
|
|
@@ -10771,9 +10983,9 @@ function collectObserverFindings(observer, telemetry, cwd, base, head, policy) {
|
|
|
10771
10983
|
);
|
|
10772
10984
|
} catch (e) {
|
|
10773
10985
|
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)}).`
|
|
10986
|
+
`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
10987
|
);
|
|
10776
|
-
return { blocking:
|
|
10988
|
+
return { blocking: strict };
|
|
10777
10989
|
}
|
|
10778
10990
|
const finalHash = (path) => {
|
|
10779
10991
|
const e = inspectRel(cwd, path);
|
|
@@ -10797,7 +11009,7 @@ function git3(args, cwd) {
|
|
|
10797
11009
|
}
|
|
10798
11010
|
function nowTicks() {
|
|
10799
11011
|
try {
|
|
10800
|
-
return Math.floor(parseFloat(
|
|
11012
|
+
return Math.floor(parseFloat(readFileSync16("/proc/uptime", "utf8").split(" ")[0]) * 100);
|
|
10801
11013
|
} catch {
|
|
10802
11014
|
return Number.POSITIVE_INFINITY;
|
|
10803
11015
|
}
|
|
@@ -10820,7 +11032,7 @@ function survivorsHoldingTree(cwd, spawnedAfterTicks) {
|
|
|
10820
11032
|
const n = Number(pid);
|
|
10821
11033
|
if (n === process.pid) continue;
|
|
10822
11034
|
try {
|
|
10823
|
-
const stat =
|
|
11035
|
+
const stat = readFileSync16(`/proc/${pid}/stat`, "utf8");
|
|
10824
11036
|
const startTicks = Number(stat.slice(stat.lastIndexOf(")") + 2).split(" ")[19]);
|
|
10825
11037
|
if (!Number.isFinite(startTicks) || startTicks < spawnedAfterTicks) continue;
|
|
10826
11038
|
} catch {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tamperward",
|
|
3
|
-
"version": "2.15.
|
|
3
|
+
"version": "2.15.2",
|
|
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",
|