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 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 readFileSync11, appendFileSync as appendFileSync4, writeFileSync as writeFileSync5, existsSync as existsSync8 } from "node:fs";
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 { readFileSync as readFileSync10, statSync } from "node:fs";
7256
+ import { closeSync as closeSync2, openSync as openSync2, readSync as readSync2, statSync } from "node:fs";
7257
7257
  var RULE11 = "transient-protected-mutation";
7258
- function readEvents(log, offset) {
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 { events: [], newOffset: offset };
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
- for (const line of raw.split("\n")) {
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
- return { events, newOffset: size };
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 readFileSync11(0, "utf8");
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(readFileSync11(cp, "utf8"));
7764
+ const n = Number(readFileSync10(cp, "utf8"));
7580
7765
  offset = Number.isFinite(n) ? n : 0;
7581
7766
  }
7582
- const { events, newOffset } = readEvents(log, offset);
7583
- if (events.length === 0) return { ...none, commit: () => {
7584
- if (cp) try {
7585
- writeFileSync5(cp, String(newOffset));
7586
- } catch {
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 readFileSync12, writeFileSync as writeFileSync6, chmodSync, statSync as statSync2 } from "node:fs";
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(readFileSync12(path, "utf8"));
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) ? readFileSync12(path, "utf8") : null;
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 ? readFileSync12(path, "utf8") : null;
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 = readFileSync12(path, "utf8");
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 readFileSync13, readdirSync as readdirSync3 } from "node:fs";
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 = readFileSync13(path, "utf8");
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(readFileSync13(path, "utf8"));
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(readFileSync13(workflowPath, "utf8"));
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 readFileSync16, readlinkSync as readlinkSync3, rmSync as rmSync3, symlinkSync, writeFileSync as writeFileSync7 } from "node:fs";
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 readFileSync14,
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(readFileSync14(path));
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(readFileSync14(path));
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 = readFileSync14(cfg, "utf8");
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 readFileSync15, realpathSync as realpathSync4, statSync as statSync4 } from "node:fs";
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(readFileSync15(path)).digest("hex");
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) : readFileSync16(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(readFileSync16(outFile, "utf8"));
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 readFileSync17, readlinkSync as readlinkSync4, realpathSync as realpathSync6, rmSync as rmSync4 } from "node:fs";
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 = readFileSync17(`/proc/${name}/stat`, "utf8");
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(readFileSync17(resultFile, "utf8"));
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 { events } = readEvents(observer.log, 0);
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: false };
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(readFileSync17("/proc/uptime", "utf8").split(" ")[0]) * 100);
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 = readFileSync17(`/proc/${pid}/stat`, "utf8");
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.1",
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",