tamperward 2.13.3 → 2.14.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -158,6 +158,15 @@ This distinguishes “healthy observer, zero events” from “observer telemetr
158
158
  unavailable” without turning absence of watcher telemetry into an enforcement pass
159
159
  or failure.
160
160
 
161
+ Since **2.14.0**, `tamperward run --observe-transients -- <agent...>` can supervise
162
+ that observer as part of the envelope lifecycle. It creates a unique session log under
163
+ the repository git directory, pins the watcher's protected-path policy to the same
164
+ trusted base used by the envelope, waits for observer health before launching the
165
+ agent, consumes the temporal events itself after the agent exits, and shuts the
166
+ observer down deterministically. Observer health remains advisory; transient findings
167
+ remain warnings by default and affect the envelope only when the operator explicitly
168
+ sets `TAMPERWARD_TRANSIENT=block`.
169
+
161
170
  After the runtime exits its exit code is treated as untrusted, and the envelope
162
171
  checks that post-agent `HEAD` still descends from the entry commit; the committed
163
172
  changes over `entry...HEAD`; staged, unstaged and untracked non-ignored worktree
@@ -356,7 +365,7 @@ option can never be reinterpreted as the agent command.
356
365
  | `check` | one view — `--staged` · `--worktree` · `--diff <base>...<head>` — plus `--format text\|json\|github\|auto` (default `auto`) · `--json` (alias for `--format json`) · `--cwd <dir>` |
357
366
  | `verify` | `--base <rev>` (default `HEAD`) · `--cmd <suite command>` · `--budget <seconds>` · `--json` · `--keep` (keep the two materialised copies and report their paths) · `--require-ancestor` (refuse a base that is not an ancestor of `HEAD`) · `--cwd <dir>` |
358
367
  | `doctor` | `--base <rev>` (trusted policy revision) · `--workflow <path>` (default `.github/workflows/tamperward.yml`) · `--cwd <dir>` — validates every job that runs `tamperward verify` has a static outer timeout covering visible + pristine + authority reserve |
359
- | `run` | `--base <rev>` · `--cmd <suite command>` · `--budget <seconds>` (per verifier suite) · `--agent-budget <seconds>` (optional wrapped-agent wall clock) · `--allow-dirty` · `--settle <seconds>` (wait before the final quiescence check) · `--allow-dep-drift` · `--cwd <dir>` · then `-- <agent command...>` |
368
+ | `run` | `--base <rev>` · `--cmd <suite command>` · `--budget <seconds>` (per verifier suite) · `--agent-budget <seconds>` (optional wrapped-agent wall clock) · `--observe-transients` (start a session-scoped transient observer) · `--allow-dirty` · `--settle <seconds>` (wait before the final quiescence check) · `--allow-dep-drift` · `--cwd <dir>` · then `-- <agent command...>` |
360
369
  | `allow` | `<rule>` · `--file <path>` · `--reason "<why>"` (required) · `--cwd <dir>` |
361
370
  | `init` | `--cwd <dir>` · `--dry-run` · `--force-workflow` |
362
371
  | `watch` | `--dir <dir>` · `--log <file>` — a daemon; it runs until signalled |
package/dist/cli/index.js CHANGED
@@ -7226,15 +7226,28 @@ function startWatcher(dir, log, policy) {
7226
7226
  function runWatch(args) {
7227
7227
  let dir = process.cwd();
7228
7228
  let log = null;
7229
+ let base = null;
7229
7230
  for (let i = 0; i < args.length; i++) {
7230
7231
  if (args[i] === "--dir" && args[i + 1]) dir = args[++i];
7231
7232
  else if (args[i] === "--log" && args[i + 1]) log = args[++i];
7233
+ else if (args[i] === "--base" && args[i + 1]) base = args[++i];
7232
7234
  }
7233
- const policy = loadPolicy(dir);
7235
+ const policy = base ? loadPolicyAt(base, dir) ?? defaultPolicy() : loadPolicy(dir);
7234
7236
  const out2 = log ?? defaultEventLog(dir);
7235
- startWatcher(dir, out2, policy);
7236
- process.stdout.write(`tamperward watch: recording protected-file events under ${dir} -> ${out2} (health: ${watcherHealthPath(out2)})
7237
- `);
7237
+ const watcher = startWatcher(dir, out2, policy);
7238
+ let closing = false;
7239
+ const shutdown = () => {
7240
+ if (closing) return;
7241
+ closing = true;
7242
+ watcher.close();
7243
+ process.exit(0);
7244
+ };
7245
+ process.once("SIGTERM", shutdown);
7246
+ process.once("SIGINT", shutdown);
7247
+ process.stdout.write(
7248
+ `tamperward watch: recording protected-file events under ${dir} -> ${out2} (health: ${watcherHealthPath(out2)}${base ? `; trusted base: ${base}` : ""})
7249
+ `
7250
+ );
7238
7251
  return -1;
7239
7252
  }
7240
7253
 
@@ -10182,7 +10195,8 @@ function parseVerify(args) {
10182
10195
  }
10183
10196
 
10184
10197
  // src/cli/run.ts
10185
- import { execFileSync as execFileSync10, spawnSync as spawnSync3 } from "node:child_process";
10198
+ import { execFileSync as execFileSync10, spawn, spawnSync as spawnSync3 } from "node:child_process";
10199
+ import { randomBytes } from "node:crypto";
10186
10200
  import { mkdtempSync as mkdtempSync3, readdirSync as readdirSync5, readFileSync as readFileSync17, readlinkSync as readlinkSync4, realpathSync as realpathSync6, rmSync as rmSync4 } from "node:fs";
10187
10201
  import { tmpdir as tmpdir3 } from "node:os";
10188
10202
  import { join as join17, resolve as resolve8 } from "node:path";
@@ -10404,6 +10418,124 @@ function runAgentWithBudget(argv2, cwd, budgetSecs) {
10404
10418
  rmSync4(stateDir, { recursive: true, force: true });
10405
10419
  }
10406
10420
  }
10421
+ var waitCell = new Int32Array(new SharedArrayBuffer(4));
10422
+ function waitMs(ms) {
10423
+ Atomics.wait(waitCell, 0, 0, ms);
10424
+ }
10425
+ function pidAlive2(pid) {
10426
+ if (!pid || !Number.isInteger(pid) || pid <= 0) return false;
10427
+ try {
10428
+ process.kill(pid, 0);
10429
+ return true;
10430
+ } catch (e) {
10431
+ return e.code === "EPERM";
10432
+ }
10433
+ }
10434
+ function supervisedObserverLog(cwd) {
10435
+ const gd = gitDir(cwd) ?? join17(cwd, ".git");
10436
+ return join17(
10437
+ gd,
10438
+ "tamperward",
10439
+ `run-observer-${process.pid}-${Date.now()}-${randomBytes(4).toString("hex")}.jsonl`
10440
+ );
10441
+ }
10442
+ function startSupervisedObserver(cwd, base, entry) {
10443
+ const log = supervisedObserverLog(cwd);
10444
+ const cliEntry = entry ?? process.argv[1];
10445
+ if (!cliEntry) return { log, pid: null, finished: false };
10446
+ let child;
10447
+ try {
10448
+ child = spawn(
10449
+ process.execPath,
10450
+ [resolve8(cliEntry), "watch", "--dir", cwd, "--log", log, "--base", base],
10451
+ {
10452
+ cwd,
10453
+ stdio: ["ignore", "ignore", "inherit"],
10454
+ env: process.env
10455
+ }
10456
+ );
10457
+ } catch {
10458
+ return { log, pid: null, finished: false };
10459
+ }
10460
+ const pid = child.pid ?? null;
10461
+ const deadline = Date.now() + 3e3;
10462
+ while (Date.now() < deadline) {
10463
+ const telemetry = watcherTelemetry(log);
10464
+ if (telemetry.state === "healthy" || telemetry.state === "degraded") break;
10465
+ if (!pidAlive2(pid)) break;
10466
+ waitMs(25);
10467
+ }
10468
+ return { log, pid, finished: false };
10469
+ }
10470
+ function stopObserverProcess(observer) {
10471
+ waitMs(100);
10472
+ const beforeStop = watcherTelemetry(observer.log);
10473
+ const pid = observer.pid;
10474
+ if (pidAlive2(pid)) {
10475
+ try {
10476
+ process.kill(pid, "SIGTERM");
10477
+ } catch {
10478
+ }
10479
+ const deadline = Date.now() + 2e3;
10480
+ while (Date.now() < deadline) {
10481
+ const health = watcherTelemetry(observer.log);
10482
+ if (health.health?.state === "stopped" || !pidAlive2(pid)) break;
10483
+ waitMs(25);
10484
+ }
10485
+ if (pidAlive2(pid)) {
10486
+ try {
10487
+ process.kill(pid, "SIGKILL");
10488
+ } catch {
10489
+ }
10490
+ }
10491
+ }
10492
+ observer.finished = true;
10493
+ return beforeStop;
10494
+ }
10495
+ function observerSummary(observer, telemetry) {
10496
+ const h = telemetry.health;
10497
+ const counts = h ? `${h.event_count} event(s), ${h.dropped_events} dropped, ${h.error_count} error(s)` : "no valid health record";
10498
+ const reason = telemetry.reason ? `; ${telemetry.reason}` : "";
10499
+ out(
10500
+ `tamperward run \u2014 transient observer: ${telemetry.state} (advisory; ${counts}; log ${observer.log}${reason})`
10501
+ );
10502
+ }
10503
+ function collectObserverFindings(observer, telemetry, cwd, base, head, policy) {
10504
+ observerSummary(observer, telemetry);
10505
+ const { events } = readEvents(observer.log, 0);
10506
+ if (events.length === 0) return { blocking: false };
10507
+ let persistent = /* @__PURE__ */ new Set();
10508
+ try {
10509
+ const changes = [
10510
+ ...head !== base ? diffRange(base, head, { cwd }) : [],
10511
+ ...diffWorktreeWithUntracked({ cwd }, (rel) => isProtected(rel, policy))
10512
+ ];
10513
+ persistent = new Set(
10514
+ changes.filter((change) => change.kind === "file").map((change) => change.path)
10515
+ );
10516
+ } catch (e) {
10517
+ out(
10518
+ `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)}).`
10519
+ );
10520
+ return { blocking: false };
10521
+ }
10522
+ const finalHash = (path) => {
10523
+ const e = inspectRel(cwd, path);
10524
+ return e.kind === "file" && e.content != null ? contentHash(e.content) : null;
10525
+ };
10526
+ const findings = transientFindings(events, persistent, policy, finalHash);
10527
+ for (const finding of findings) {
10528
+ out(
10529
+ `[observer] ${finding.severity === "block" ? "BLOCK" : "warn"} ${finding.rule}${finding.file ? " " + finding.file : ""}: ${finding.message}`
10530
+ );
10531
+ }
10532
+ return { blocking: findings.some((finding) => finding.severity === "block") };
10533
+ }
10534
+ function finishObserverAdvisory(observer) {
10535
+ if (!observer || observer.finished) return;
10536
+ const telemetry = stopObserverProcess(observer);
10537
+ observerSummary(observer, telemetry);
10538
+ }
10407
10539
  function git3(args, cwd) {
10408
10540
  return execFileSync10("git", args, { cwd, encoding: "utf8", maxBuffer: 1 << 28, env: trustedGitEnv() });
10409
10541
  }
@@ -10530,6 +10662,10 @@ function runEnvelope(opts) {
10530
10662
  } else {
10531
10663
  out(`tamperward run \u2014 dependency environment: verifier-owned by ${verifierBackend.image ?? "isolated image"}`);
10532
10664
  }
10665
+ const observer = opts.observeTransients ? startSupervisedObserver(cwd, base, opts.observerEntry) : null;
10666
+ if (!observer) {
10667
+ out("tamperward run \u2014 transient observer: disabled (advisory; pass --observe-transients to enable)");
10668
+ }
10533
10669
  const rewritesBefore = objectRewriteState(cwd);
10534
10670
  const spawnTicks = nowTicks();
10535
10671
  const agentRun = opts.agentBudget === void 0 ? (() => {
@@ -10557,6 +10693,7 @@ function runEnvelope(opts) {
10557
10693
  err2("read against the anchor we started from. That is the finding.");
10558
10694
  out(`
10559
10695
  tamperward run \u2014 agent exit ${agentExit}; OBJECT_REWRITE \u2192 ENFORCEMENT_FAILED`);
10696
+ finishObserverAdvisory(observer);
10560
10697
  return 1;
10561
10698
  }
10562
10699
  const dependencyBeforeAdjudication = dependencyEnvironment ? checkDependencyEnvironment(cwd, dependencyEnvironment) : { ok: true };
@@ -10566,6 +10703,7 @@ tamperward run \u2014 agent exit ${agentExit}; OBJECT_REWRITE \u2192 ENFORCEMENT
10566
10703
  if (!opts.allowDepDrift) {
10567
10704
  out(`
10568
10705
  tamperward run \u2014 agent exit ${agentExit}; DEPENDENCY_DRIFT \u2192 ENFORCEMENT_FAILED`);
10706
+ finishObserverAdvisory(observer);
10569
10707
  return 1;
10570
10708
  }
10571
10709
  err2("(--allow-dep-drift: proceeding anyway, on the operator's judgement.)");
@@ -10577,6 +10715,7 @@ tamperward run \u2014 agent exit ${agentExit}; DEPENDENCY_DRIFT \u2192 ENFORCEME
10577
10715
  err2("the agent rewrote history out from under the anchor. That is the finding.");
10578
10716
  out(`
10579
10717
  tamperward run \u2014 agent exit ${agentExit}; HISTORY_REWRITE \u2192 ENFORCEMENT_FAILED`);
10718
+ finishObserverAdvisory(observer);
10580
10719
  return 1;
10581
10720
  }
10582
10721
  const protectedIgnored = (rel) => isProtected(rel, frozenPolicy);
@@ -10627,11 +10766,24 @@ tamperward run \u2014 agent exit ${agentExit}; HISTORY_REWRITE \u2192 ENFORCEMEN
10627
10766
  err2(`tamperward run: ${survivors.length} process(es) started during this run still hold the`);
10628
10767
  err2(`working tree (pid ${survivors.join(", ")}). A verdict cannot outlive the tree it describes.`);
10629
10768
  }
10769
+ let observerBlocked = false;
10770
+ if (observer && !observer.finished) {
10771
+ const telemetry = stopObserverProcess(observer);
10772
+ const observed = collectObserverFindings(
10773
+ observer,
10774
+ telemetry,
10775
+ cwd,
10776
+ base,
10777
+ head,
10778
+ frozenPolicy
10779
+ );
10780
+ observerBlocked = observed.blocking;
10781
+ }
10630
10782
  const notQuiescent = mutatedDuringAdjudication || survivors.length > 0 || depsDrifted || rewrote;
10631
10783
  const cannot = !notQuiescent && (diffCode === 2 || workCode === 2 || verifyCode === 2);
10632
- const blocked = diffCode === 1 || workCode === 1 || verifyCode === 1 || notQuiescent;
10784
+ const blocked = diffCode === 1 || workCode === 1 || verifyCode === 1 || notQuiescent || observerBlocked;
10633
10785
  const enforcement = cannot ? 2 : blocked ? 1 : 0;
10634
- const verdict2 = enforcement === 2 ? "CANNOT_ADJUDICATE (fail closed)" : rewrote ? "OBJECT_REWRITE \u2014 the base the checks read is not the base we anchored to" : depsDrifted ? "DEPENDENCY_DRIFT \u2014 the suite runner changed under the envelope" : notQuiescent ? "NOT_QUIESCENT \u2014 the tree moved, or something still holds it" : enforcement === 1 ? "ENFORCEMENT_FAILED \u2014 the tree the runtime released does not stand" : agentTimedOut ? "AGENT_TIMEOUT \u2014 runtime budget expired; post-timeout enforcement clean" : agentExit !== 0 ? `agent exited ${agentExit} (enforcement clean)` : "GREEN MEANS GREEN";
10786
+ const verdict2 = enforcement === 2 ? "CANNOT_ADJUDICATE (fail closed)" : rewrote ? "OBJECT_REWRITE \u2014 the base the checks read is not the base we anchored to" : depsDrifted ? "DEPENDENCY_DRIFT \u2014 the suite runner changed under the envelope" : notQuiescent ? "NOT_QUIESCENT \u2014 the tree moved, or something still holds it" : observerBlocked ? "TRANSIENT_OBSERVER_BLOCK \u2014 observed temporal weakening under strict transient policy" : enforcement === 1 ? "ENFORCEMENT_FAILED \u2014 the tree the runtime released does not stand" : agentTimedOut ? "AGENT_TIMEOUT \u2014 runtime budget expired; post-timeout enforcement clean" : agentExit !== 0 ? `agent exited ${agentExit} (enforcement clean)` : "GREEN MEANS GREEN";
10635
10787
  const agentSummary = agentTimedOut ? `AGENT_TIMEOUT (${opts.agentBudget}s; exit 124)` : `agent exit ${agentExit}`;
10636
10788
  out(`
10637
10789
  tamperward run \u2014 ${agentSummary}; checks diff=${diffCode} worktree=${workCode} verify=${verifyCode} \u2192 ${verdict2}`);
@@ -10651,6 +10803,7 @@ function parseRun(args) {
10651
10803
  else if (a === "--allow-dirty") o.allowDirty = true;
10652
10804
  else if (a === "--settle") o.settle = Number(args[++i]);
10653
10805
  else if (a === "--allow-dep-drift") o.allowDepDrift = true;
10806
+ else if (a === "--observe-transients") o.observeTransients = true;
10654
10807
  else if (a === "--cwd") o.cwd = args[++i];
10655
10808
  else {
10656
10809
  o.argv = args.slice(i);
@@ -10784,7 +10937,7 @@ function validateCliArgs(cmd, args) {
10784
10937
  const prefix = args.slice(0, delimiter3);
10785
10938
  const command = args.slice(delimiter3 + 1);
10786
10939
  const parsed = validateFlatArgs(prefix, {
10787
- flags: ["--allow-dirty", "--allow-dep-drift"],
10940
+ flags: ["--allow-dirty", "--allow-dep-drift", "--observe-transients"],
10788
10941
  values: {
10789
10942
  "--base": "string",
10790
10943
  "--cmd": "string",
@@ -10851,7 +11004,7 @@ function validateCliArgs(cmd, args) {
10851
11004
  }
10852
11005
  if (cmd === "watch") {
10853
11006
  return validateFlatArgs(args, {
10854
- values: { "--dir": "string", "--log": "string" }
11007
+ values: { "--dir": "string", "--log": "string", "--base": "string" }
10855
11008
  }).error;
10856
11009
  }
10857
11010
  return void 0;
@@ -10878,8 +11031,10 @@ Formats:
10878
11031
  tamperward hook claude PreToolUse gate (reads hook JSON on stdin)
10879
11032
  tamperward sweep claude Stop sweep (re-scan the turn's working tree)
10880
11033
  tamperward watch [--dir D] [--log F] filesystem-event observer daemon: records
10881
- protected-file events so the sweep can
10882
- observe supported transient effects
11034
+ [--base R] protected-file events so the sweep can
11035
+ observe supported transient effects.
11036
+ --base freezes observer policy to a
11037
+ trusted revision
10883
11038
  tamperward verify [--base R] [--cmd C] pristine-suite re-execution: run the suite
10884
11039
  [--budget S] [--json] [--keep] as-is AND with protected files restored
10885
11040
  [--require-ancestor] [--cwd D] from the trusted base; a visible-green /
@@ -10891,9 +11046,13 @@ Formats:
10891
11046
  [--base R] [--cmd C] base, run the agent, treat its exit as
10892
11047
  [--budget S] [--agent-budget S] untrusted, then re-adjudicate the tree it
10893
11048
  [--allow-dirty] [--settle S] left (policy over base...HEAD and the
10894
- [--allow-dep-drift] [--cwd D] worktree, plus verify). --agent-budget
10895
- bounds the agent runtime itself and still
10896
- adjudicates after timeout. Exit: agent's
11049
+ [--allow-dep-drift] [--cwd D] worktree, plus verify).
11050
+ [--observe-transients] --observe-transients starts a session-
11051
+ scoped observer before the agent and
11052
+ reports temporal evidence/health after it.
11053
+ --agent-budget bounds the agent runtime
11054
+ itself and still adjudicates after timeout.
11055
+ Exit: agent's
10897
11056
  code when clean; 124 on clean AGENT_TIMEOUT;
10898
11057
  1 on any blocking finding/masked failure;
10899
11058
  2 when it cannot adjudicate (fails closed)
@@ -10956,7 +11115,12 @@ function main(argv2) {
10956
11115
  case "verify":
10957
11116
  return runVerify(parseVerify(rest));
10958
11117
  case "run":
10959
- return runEnvelope(parseRun(rest));
11118
+ return runEnvelope({
11119
+ ...parseRun(rest),
11120
+ // The supervised observer re-launches this exact TamperWard CLI under
11121
+ // process.execPath rather than resolving another binary from PATH.
11122
+ observerEntry: process.argv[1]
11123
+ });
10960
11124
  case void 0:
10961
11125
  case "-h":
10962
11126
  case "--help":
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tamperward",
3
- "version": "2.13.3",
3
+ "version": "2.14.1",
4
4
  "description": "The deterministic agent-integrity gate. One ruleset, evaluated on the actual diff/commands as a verdict, enforced everywhere a change can be made.",
5
5
  "license": "Apache-2.0",
6
6
  "author": "hexrift",