tamperward 2.18.0 → 2.19.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/dist/cli/index.js CHANGED
@@ -7084,6 +7084,12 @@ function oobFromEnv(env = process.env) {
7084
7084
  return (env.TAMPERWARD_OOB_SIGNOFF ?? "").split(",").map((s) => s.trim().replace(/^tamperward:allow:/, "")).filter(Boolean);
7085
7085
  }
7086
7086
 
7087
+ // src/machine-output.ts
7088
+ var MACHINE_SCHEMA_VERSION = 1;
7089
+ function machineOutput(payload) {
7090
+ return { ...payload, schema_version: MACHINE_SCHEMA_VERSION };
7091
+ }
7092
+
7087
7093
  // src/cli/render/text.ts
7088
7094
  var ESC = "\x1B";
7089
7095
  var RESET = `${ESC}[0m`;
@@ -7303,7 +7309,7 @@ function report(input) {
7303
7309
  JSON.stringify(
7304
7310
  // `findings`, `scanned` and `ignoredFiles` keep their exact prior shape; `summary`
7305
7311
  // is additive, so an existing consumer reading only the old keys is unaffected.
7306
- { findings, scanned, ignoredFiles, summary: { block: blocks2, warn: findings.length - blocks2 } },
7312
+ machineOutput({ findings, scanned, ignoredFiles, summary: { block: blocks2, warn: findings.length - blocks2 } }),
7307
7313
  null,
7308
7314
  2
7309
7315
  ) + "\n"
@@ -7379,7 +7385,9 @@ function check(opts) {
7379
7385
  process.stderr.write(`tamperward: ${cleared.length} blocking finding(s) cleared by ${how}: ${cleared.map((f) => f.rule + (f.file ? `(${f.file})` : "")).join(", ")}
7380
7386
  `);
7381
7387
  }
7382
- report({ findings, scanned: changes.length, ignoredFiles, json: opts.json, format: opts.format });
7388
+ if (!opts.silent) {
7389
+ report({ findings, scanned: changes.length, ignoredFiles, json: opts.json, format: opts.format });
7390
+ }
7383
7391
  return hasBlocking(findings) ? 1 : 0;
7384
7392
  }
7385
7393
  function runCheck(opts) {
@@ -10894,10 +10902,24 @@ function renderStageDiagnostics(out2, stage, result) {
10894
10902
  }
10895
10903
  function runVerify(opts) {
10896
10904
  const cwd = opts.cwd ?? process.cwd();
10897
- const out2 = (s) => void process.stdout.write(s + "\n");
10898
- if (opts.invalid) {
10899
- out2(`verify: ${opts.invalid} \u2014 failing closed`);
10905
+ const out2 = opts.silent ? (_s) => {
10906
+ } : (s) => void process.stdout.write(s + "\n");
10907
+ const cannotVerify = (reason, detail, extra = {}) => {
10908
+ if (opts.json) {
10909
+ out2(JSON.stringify({
10910
+ schema_version: MACHINE_SCHEMA_VERSION,
10911
+ verdict: "CANNOT_VERIFY",
10912
+ reason,
10913
+ ...detail ? { detail } : {},
10914
+ ...extra
10915
+ }));
10916
+ } else if (detail) {
10917
+ out2(`verify: ${detail} \u2014 failing closed`);
10918
+ }
10900
10919
  return 2;
10920
+ };
10921
+ if (opts.invalid) {
10922
+ return cannotVerify("INVALID_ARGUMENTS", opts.invalid);
10901
10923
  }
10902
10924
  let policy;
10903
10925
  try {
@@ -10905,24 +10927,24 @@ function runVerify(opts) {
10905
10927
  else if (opts.base) policy = loadPolicyAt(resolveBase(opts.base, cwd), cwd) ?? defaultPolicy();
10906
10928
  else policy = loadPolicy(cwd);
10907
10929
  } catch (e) {
10908
- out2(`verify: cannot load policy (${e instanceof Error ? e.message : String(e)}) \u2014 failing closed`);
10909
- return 2;
10930
+ return cannotVerify("POLICY_ERROR", `cannot load policy (${e instanceof Error ? e.message : String(e)})`);
10910
10931
  }
10911
10932
  if (opts.requireAncestor) {
10912
10933
  const requested = git2(["rev-parse", "--verify", `${opts.base ?? "HEAD"}^{commit}`], cwd).trim();
10913
10934
  if (!baseIsAncestorOfHead(requested, cwd)) {
10914
- out2(
10915
- `verify: --require-ancestor: ${requested.slice(0, 10)} is not an ancestor of HEAD \u2014 the anchor would silently resolve to an older commit. Failing closed.`
10935
+ return cannotVerify(
10936
+ "BASE_NOT_ANCESTOR",
10937
+ `--require-ancestor: ${requested.slice(0, 10)} is not an ancestor of HEAD; the anchor would silently resolve to an older commit`,
10938
+ { base: requested }
10916
10939
  );
10917
- return 2;
10918
10940
  }
10919
10941
  }
10920
10942
  const cmd = opts.cmd ?? policy.verify?.command;
10921
10943
  if (!cmd) {
10922
- out2(
10923
- opts.base ? "verify: no suite command in the policy at the trusted base \u2014 the base governs the verifier, so a `verify:` block added only on the candidate is not used. Add it at the base, or pass --cmd explicitly." : "verify: no suite command \u2014 set policy `verify: { command: ... }` or pass --cmd"
10944
+ return cannotVerify(
10945
+ "NO_SUITE_COMMAND",
10946
+ opts.base ? "no suite command in the policy at the trusted base; add it at the base or pass --cmd explicitly" : "no suite command; set policy verify.command or pass --cmd"
10924
10947
  );
10925
- return 2;
10926
10948
  }
10927
10949
  const budget = opts.budget ?? policy.verify?.budget ?? 300;
10928
10950
  const verifierBackend = opts.verifierBackend ?? prepareVerifierBackend(policy.verify);
@@ -10930,6 +10952,7 @@ function runVerify(opts) {
10930
10952
  if (!verifierBackend.available) {
10931
10953
  if (opts.json) {
10932
10954
  out2(JSON.stringify({
10955
+ schema_version: MACHINE_SCHEMA_VERSION,
10933
10956
  verdict: "CANNOT_VERIFY",
10934
10957
  reason: "VERIFIER_BACKEND_UNAVAILABLE",
10935
10958
  verifier_backend: backendReport(),
@@ -10945,6 +10968,7 @@ function runVerify(opts) {
10945
10968
  if (!isolated && !localVerifierShell(process.platform, cmd)) {
10946
10969
  if (opts.json) {
10947
10970
  out2(JSON.stringify({
10971
+ schema_version: MACHINE_SCHEMA_VERSION,
10948
10972
  verdict: "CANNOT_VERIFY",
10949
10973
  reason: "LOCAL_VERIFIER_UNSUPPORTED_PLATFORM",
10950
10974
  platform: process.platform,
@@ -10967,6 +10991,7 @@ function runVerify(opts) {
10967
10991
  if (dependencyEnvironment?.status === "unattestable" && !opts.allowDepDrift) {
10968
10992
  if (opts.json) {
10969
10993
  out2(JSON.stringify({
10994
+ schema_version: MACHINE_SCHEMA_VERSION,
10970
10995
  verdict: "CANNOT_VERIFY",
10971
10996
  reason: "DEPENDENCY_ENVIRONMENT_UNATTESTABLE",
10972
10997
  verifier_backend: backendReport(),
@@ -10986,8 +11011,7 @@ function runVerify(opts) {
10986
11011
  try {
10987
11012
  base = resolveBase(opts.base ?? "HEAD", cwd);
10988
11013
  } catch {
10989
- out2(`verify: cannot resolve base rev "${opts.base ?? "HEAD"}" \u2014 failing closed`);
10990
- return 2;
11014
+ return cannotVerify("BASE_UNRESOLVABLE", `cannot resolve base rev "${opts.base ?? "HEAD"}"`);
10991
11015
  }
10992
11016
  const visRoot = mkdtempSync3(join16(tmpdir3(), "tw-verify-vis-"));
10993
11017
  const visDir = join16(visRoot, "t");
@@ -11004,17 +11028,22 @@ function runVerify(opts) {
11004
11028
  ) ?? checkDeps() : { ok: true };
11005
11029
  if (!dependencyAtEntry.ok && !opts.allowDepDrift) {
11006
11030
  cleanup([visRoot]);
11007
- out2("verify: the frozen dependency environment changed before the visible suite ran \u2014");
11008
- out2((dependencyAtEntry.reason ?? "dependency identity changed") + ". Failing closed, not open.");
11009
- return 2;
11031
+ return cannotVerify(
11032
+ "DEPENDENCY_DRIFT",
11033
+ dependencyAtEntry.reason ?? "dependency identity changed before the visible suite ran",
11034
+ { dependency_environment: dependencyReport(), verifier_backend: backendReport(), oracle_assurance: oracleAssuranceReport() }
11035
+ );
11010
11036
  }
11011
11037
  try {
11012
11038
  mkdirSync6(visDir);
11013
11039
  materialize(cwd, visDir, frozenNodeModules);
11014
11040
  } catch (e) {
11015
11041
  cleanup([visRoot]);
11016
- out2(`verify: could not materialize (${e instanceof Error ? e.message : String(e)}) \u2014 failing closed`);
11017
- return 2;
11042
+ return cannotVerify(
11043
+ "MATERIALIZATION_FAILED",
11044
+ `could not materialize (${e instanceof Error ? e.message : String(e)})`,
11045
+ { verifier_backend: backendReport(), dependency_environment: dependencyReport(), oracle_assurance: oracleAssuranceReport() }
11046
+ );
11018
11047
  }
11019
11048
  const visible = runStage(visDir);
11020
11049
  if (visible.failure === "backend" || visible.failure === "resource") {
@@ -11022,6 +11051,7 @@ function runVerify(opts) {
11022
11051
  const exhausted = visible.failure === "resource";
11023
11052
  if (opts.json) {
11024
11053
  out2(JSON.stringify({
11054
+ schema_version: MACHINE_SCHEMA_VERSION,
11025
11055
  verdict: "CANNOT_VERIFY",
11026
11056
  reason: exhausted ? "VERIFIER_RESOURCE_EXHAUSTED" : "VERIFIER_BACKEND_RUNTIME_FAILURE",
11027
11057
  stage: "visible",
@@ -11042,16 +11072,20 @@ function runVerify(opts) {
11042
11072
  }
11043
11073
  if (treeFingerprint(cwd, protectedIgnored) !== treeBefore) {
11044
11074
  cleanup([visRoot]);
11045
- out2("verify: the working tree changed while the visible suite was running \u2014 the pristine copy");
11046
- out2("would be materialised from a tree the candidate just edited. Failing closed, not open.");
11047
- return 2;
11075
+ return cannotVerify(
11076
+ "WORKTREE_CHANGED",
11077
+ "the working tree changed while the visible suite was running",
11078
+ { stage: "visible", verifier_backend: backendReport(), dependency_environment: dependencyReport(), oracle_assurance: oracleAssuranceReport() }
11079
+ );
11048
11080
  }
11049
11081
  const dependencyAfterVisible = checkDeps();
11050
11082
  if (!dependencyAfterVisible.ok && !opts.allowDepDrift) {
11051
11083
  cleanup([visRoot]);
11052
- out2("verify: the frozen dependency environment changed while the visible suite was running \u2014");
11053
- out2((dependencyAfterVisible.reason ?? "dependency identity changed") + ". Failing closed, not open.");
11054
- return 2;
11084
+ return cannotVerify(
11085
+ "DEPENDENCY_DRIFT",
11086
+ dependencyAfterVisible.reason ?? "dependency identity changed while the visible suite was running",
11087
+ { stage: "visible", dependency_environment: dependencyReport(), verifier_backend: backendReport(), oracle_assurance: oracleAssuranceReport() }
11088
+ );
11055
11089
  }
11056
11090
  const priRoot = mkdtempSync3(join16(tmpdir3(), "tw-verify-pri-"));
11057
11091
  const priDir = join16(priRoot, "t");
@@ -11070,8 +11104,11 @@ function runVerify(opts) {
11070
11104
  ));
11071
11105
  } catch (e) {
11072
11106
  cleanup([visRoot, priRoot]);
11073
- out2(`verify: could not materialize (${e instanceof Error ? e.message : String(e)}) \u2014 failing closed`);
11074
- return 2;
11107
+ return cannotVerify(
11108
+ "MATERIALIZATION_FAILED",
11109
+ `could not materialize pristine copy (${e instanceof Error ? e.message : String(e)})`,
11110
+ { stage: "pristine", verifier_backend: backendReport(), dependency_environment: dependencyReport(), oracle_assurance: oracleAssuranceReport() }
11111
+ );
11075
11112
  }
11076
11113
  const overlayBefore = overlayDigest(priDir, restored);
11077
11114
  const pristine = runStage(priDir);
@@ -11080,6 +11117,7 @@ function runVerify(opts) {
11080
11117
  const exhausted = pristine.failure === "resource";
11081
11118
  if (opts.json) {
11082
11119
  out2(JSON.stringify({
11120
+ schema_version: MACHINE_SCHEMA_VERSION,
11083
11121
  verdict: "CANNOT_VERIFY",
11084
11122
  reason: exhausted ? "VERIFIER_RESOURCE_EXHAUSTED" : "VERIFIER_BACKEND_RUNTIME_FAILURE",
11085
11123
  stage: "pristine",
@@ -11108,13 +11146,14 @@ function runVerify(opts) {
11108
11146
  const depsMoved = !dependencyAfterPristine.ok && !opts.allowDepDrift;
11109
11147
  cleanup([visRoot, priRoot]);
11110
11148
  if (overlayMoved || treeMoved || depsMoved) {
11111
- out2(
11112
- "verify: " + (overlayMoved ? "a restored file in the pristine copy changed while the pristine suite was running" : treeMoved ? "the working tree changed while the pristine suite was running" : "the frozen dependency environment changed while the pristine suite was running") + " \u2014"
11113
- );
11114
- out2("the verdict would describe something other than what ran. Failing closed, not open.");
11115
- out2("(A suite that rewrites its own snapshots or test files in place will trip this; run it in");
11116
- out2("whatever mode your runner calls CI, so verification observes rather than updates.)");
11117
- return 2;
11149
+ const reason = overlayMoved ? "PRISTINE_INTEGRITY_CHANGED" : treeMoved ? "WORKTREE_CHANGED" : "DEPENDENCY_DRIFT";
11150
+ const detail = overlayMoved ? "a restored file in the pristine copy changed while the pristine suite was running" : treeMoved ? "the working tree changed while the pristine suite was running" : dependencyAfterPristine.reason ?? "the frozen dependency environment changed while the pristine suite was running";
11151
+ return cannotVerify(reason, detail, {
11152
+ stage: "pristine",
11153
+ verifier_backend: backendReport(),
11154
+ dependency_environment: dependencyReport(),
11155
+ oracle_assurance: oracleAssuranceReport()
11156
+ });
11118
11157
  }
11119
11158
  let verdict2;
11120
11159
  let code2;
@@ -11136,6 +11175,7 @@ function runVerify(opts) {
11136
11175
  if (opts.json) {
11137
11176
  out2(
11138
11177
  JSON.stringify({
11178
+ schema_version: MACHINE_SCHEMA_VERSION,
11139
11179
  verdict: verdict2,
11140
11180
  base,
11141
11181
  command: cmd,
@@ -11299,8 +11339,9 @@ var AGENT_SUPERVISOR = String.raw`
11299
11339
  const { spawn, spawnSync } = require('node:child_process');
11300
11340
  const fs = require('node:fs');
11301
11341
 
11302
- const [resultFile, budgetRaw, command, ...args] = process.argv.slice(1);
11342
+ const [resultFile, budgetRaw, machineRaw, command, ...args] = process.argv.slice(1);
11303
11343
  const budgetMs = budgetRaw === '' ? null : Number(budgetRaw);
11344
+ const machineMode = machineRaw === '1';
11304
11345
  let child;
11305
11346
  let timedOut = false;
11306
11347
  let finished = false;
@@ -11326,7 +11367,9 @@ function killTree(pid) {
11326
11367
 
11327
11368
  try {
11328
11369
  child = spawn(command, args, {
11329
- stdio: 'inherit',
11370
+ // Machine mode owns stdout for the final envelope document. Preserve agent
11371
+ // diagnostics by routing both child streams to the parent's stderr.
11372
+ stdio: machineMode ? ['inherit', 2, 2] : 'inherit',
11330
11373
  detached: process.platform !== 'win32',
11331
11374
  });
11332
11375
  } catch (e) {
@@ -11363,8 +11406,9 @@ child.once('exit', (code, signal) => finish(code, signal));
11363
11406
  var LINUX_SUBREAPER_SUPERVISOR = String.raw`
11364
11407
  import ctypes, json, os, signal, subprocess, sys, time
11365
11408
 
11366
- result_file, agent_env_file, agent_cwd, budget_raw, test_mode, command, *args = sys.argv[1:]
11409
+ result_file, agent_env_file, agent_cwd, budget_raw, test_mode, machine_raw, command, *args = sys.argv[1:]
11367
11410
  budget = None if budget_raw == "" else float(budget_raw)
11411
+ machine_mode = machine_raw == "1"
11368
11412
  libc = ctypes.CDLL(None, use_errno=True)
11369
11413
  PR_SET_DUMPABLE = 4
11370
11414
  PR_SET_CHILD_SUBREAPER = 36
@@ -11482,6 +11526,10 @@ try:
11482
11526
  cwd=agent_cwd,
11483
11527
  env=agent_env,
11484
11528
  start_new_session=True,
11529
+ # In machine mode stdout is reserved for TamperWard's one final JSON
11530
+ # document. Agent stdout is retained as diagnostics on stderr.
11531
+ stdout=sys.stderr if machine_mode else None,
11532
+ stderr=None,
11485
11533
  )
11486
11534
  except Exception as exc:
11487
11535
  # No candidate process exists, so the lifecycle domain is trivially empty.
@@ -11511,7 +11559,7 @@ write_result({
11511
11559
  })
11512
11560
  raise SystemExit(0)
11513
11561
  `;
11514
- function runAgentSupervised(argv2, cwd, budgetSecs, linuxPythonCandidates, lifecycleTestMode) {
11562
+ function runAgentSupervised(argv2, cwd, budgetSecs, linuxPythonCandidates, lifecycleTestMode, machineMode = false) {
11515
11563
  const stateDir = mkdtempSync4(join17(tmpdir4(), "tw-agent-supervisor-"));
11516
11564
  const resultFile = join17(stateDir, "result.json");
11517
11565
  const agentEnvFile = join17(stateDir, "agent-env.json");
@@ -11544,6 +11592,7 @@ function runAgentSupervised(argv2, cwd, budgetSecs, linuxPythonCandidates, lifec
11544
11592
  cwd,
11545
11593
  budgetSecs === void 0 ? "" : String(budgetSecs),
11546
11594
  lifecycleTestMode ?? "",
11595
+ machineMode ? "1" : "0",
11547
11596
  ...argv2
11548
11597
  ];
11549
11598
  supervisorCwd = "/";
@@ -11555,6 +11604,7 @@ function runAgentSupervised(argv2, cwd, budgetSecs, linuxPythonCandidates, lifec
11555
11604
  AGENT_SUPERVISOR,
11556
11605
  resultFile,
11557
11606
  budgetSecs === void 0 ? "" : String(budgetSecs * 1e3),
11607
+ machineMode ? "1" : "0",
11558
11608
  ...argv2
11559
11609
  ];
11560
11610
  }
@@ -11674,8 +11724,7 @@ function stopObserverProcess(observer) {
11674
11724
  }
11675
11725
  const deadline = Date.now() + 2e3;
11676
11726
  while (Date.now() < deadline) {
11677
- const health = watcherTelemetry(observer.log);
11678
- if (health.health?.state === "stopped" || !pidAlive2(pid)) break;
11727
+ if (!pidAlive2(pid)) break;
11679
11728
  waitMs(25);
11680
11729
  }
11681
11730
  if (pidAlive2(pid)) {
@@ -11688,22 +11737,22 @@ function stopObserverProcess(observer) {
11688
11737
  observer.finished = true;
11689
11738
  return beforeStop;
11690
11739
  }
11691
- function observerSummary(observer, telemetry) {
11740
+ function observerSummary(observer, telemetry, write = out) {
11692
11741
  const h = telemetry.health;
11693
11742
  const counts = h ? `${h.event_count} event(s), ${h.dropped_events} dropped, ${h.error_count} error(s)` : "no valid health record";
11694
11743
  const reason = telemetry.reason ? `; ${telemetry.reason}` : "";
11695
- out(
11744
+ write(
11696
11745
  `tamperward run \u2014 transient observer: ${telemetry.state} (advisory; ${counts}; log ${observer.log}${reason})`
11697
11746
  );
11698
11747
  }
11699
- function collectObserverFindings(observer, telemetry, cwd, base, head, policy) {
11700
- observerSummary(observer, telemetry);
11748
+ function collectObserverFindings(observer, telemetry, cwd, base, head, policy, write = out) {
11749
+ observerSummary(observer, telemetry, write);
11701
11750
  const strict = process.env.TAMPERWARD_TRANSIENT === "block";
11702
11751
  const drained = drainEvents(observer.log, 0);
11703
11752
  const { events } = drained;
11704
11753
  if (!drained.complete) {
11705
11754
  const issue = drained.issue ?? "read-stalled";
11706
- out(
11755
+ write(
11707
11756
  `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.")
11708
11757
  );
11709
11758
  if (strict) return { blocking: true };
@@ -11719,7 +11768,7 @@ function collectObserverFindings(observer, telemetry, cwd, base, head, policy) {
11719
11768
  changes.filter((change) => change.kind === "file").map((change) => change.path)
11720
11769
  );
11721
11770
  } catch (e) {
11722
- out(
11771
+ write(
11723
11772
  `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." : "")
11724
11773
  );
11725
11774
  return { blocking: strict };
@@ -11730,16 +11779,16 @@ function collectObserverFindings(observer, telemetry, cwd, base, head, policy) {
11730
11779
  };
11731
11780
  const findings = transientFindings(events, persistent, policy, finalHash);
11732
11781
  for (const finding of findings) {
11733
- out(
11782
+ write(
11734
11783
  `[observer] ${finding.severity === "block" ? "BLOCK" : "warn"} ${finding.rule}${finding.file ? " " + finding.file : ""}: ${finding.message}`
11735
11784
  );
11736
11785
  }
11737
11786
  return { blocking: findings.some((finding) => finding.severity === "block") };
11738
11787
  }
11739
- function finishObserverAdvisory(observer) {
11788
+ function finishObserverAdvisory(observer, write = out) {
11740
11789
  if (!observer || observer.finished) return;
11741
11790
  const telemetry = stopObserverProcess(observer);
11742
- observerSummary(observer, telemetry);
11791
+ observerSummary(observer, telemetry, write);
11743
11792
  }
11744
11793
  function git3(args, cwd) {
11745
11794
  return execFileSync9("git", args, { cwd, encoding: "utf8", maxBuffer: 1 << 28, env: trustedGitEnv() });
@@ -11798,6 +11847,8 @@ function survivorsHoldingTree(cwd, spawnedAfterTicks) {
11798
11847
  }
11799
11848
  function runEnvelope(opts) {
11800
11849
  const cwd = resolve7(opts.cwd ?? process.cwd());
11850
+ const say = opts.json ? (_s) => {
11851
+ } : out;
11801
11852
  if (!opts.argv.length) {
11802
11853
  err("tamperward run: no agent command given (usage: tamperward run [options] -- <command...>)");
11803
11854
  return 2;
@@ -11848,8 +11899,8 @@ function runEnvelope(opts) {
11848
11899
  return 2;
11849
11900
  }
11850
11901
  const verifierBackend = prepareVerifierBackend(frozenPolicy.verify);
11851
- out(`tamperward run \u2014 trusted base ${base.slice(0, 10)}; agent exit is untrusted.`);
11852
- out(`tamperward run \u2014 verifier backend: ${verifierBackendSummary(verifierBackend)}`);
11902
+ say(`tamperward run \u2014 trusted base ${base.slice(0, 10)}; agent exit is untrusted.`);
11903
+ say(`tamperward run \u2014 verifier backend: ${verifierBackendSummary(verifierBackend)}`);
11853
11904
  if (!verifierBackend.available) {
11854
11905
  err("tamperward run: the requested verifier backend cannot be established \u2014");
11855
11906
  err(`${verifierBackend.reason ?? "unknown backend failure"}. Failing closed before the agent starts.`);
@@ -11865,7 +11916,7 @@ function runEnvelope(opts) {
11865
11916
  }
11866
11917
  const dependencyEnvironment = verifierBackend.kind === "local" ? discoverDependencyEnvironment(cwd, frozenCmd) : null;
11867
11918
  if (dependencyEnvironment) {
11868
- out(`tamperward run \u2014 dependency environment: ${dependencyEnvironmentSummary(dependencyEnvironment)}`);
11919
+ say(`tamperward run \u2014 dependency environment: ${dependencyEnvironmentSummary(dependencyEnvironment)}`);
11869
11920
  if (dependencyEnvironment.status === "unattestable" && !opts.allowDepDrift) {
11870
11921
  err("tamperward run: the verifier dependency environment cannot be attested \u2014");
11871
11922
  err(`${dependencyEnvironment.reason ?? "unknown dependency environment"}. Failing closed before the agent starts.`);
@@ -11873,11 +11924,11 @@ function runEnvelope(opts) {
11873
11924
  return 2;
11874
11925
  }
11875
11926
  } else {
11876
- out(`tamperward run \u2014 dependency environment: verifier-owned by ${verifierBackend.image ?? "isolated image"}`);
11927
+ say(`tamperward run \u2014 dependency environment: verifier-owned by ${verifierBackend.image ?? "isolated image"}`);
11877
11928
  }
11878
11929
  const observer = opts.observeTransients ? startSupervisedObserver(cwd, base, opts.observerEntry) : null;
11879
11930
  if (!observer) {
11880
- out("tamperward run \u2014 transient observer: disabled (advisory; pass --observe-transients to enable)");
11931
+ say("tamperward run \u2014 transient observer: disabled (advisory; pass --observe-transients to enable)");
11881
11932
  }
11882
11933
  const rewritesBefore = objectRewriteState(cwd);
11883
11934
  const spawnTicks = nowTicks();
@@ -11886,16 +11937,38 @@ function runEnvelope(opts) {
11886
11937
  cwd,
11887
11938
  opts.agentBudget,
11888
11939
  opts.linuxPythonCandidates,
11889
- opts.lifecycleTestMode
11940
+ opts.lifecycleTestMode,
11941
+ opts.json === true
11890
11942
  );
11891
11943
  const agentExit = agentRun.exit;
11892
11944
  const agentTimedOut = agentRun.timedOut;
11945
+ const emitRunJson = (verdict3, exitCode2, extra = {}, complete = false) => {
11946
+ if (!opts.json) return;
11947
+ out(JSON.stringify(machineOutput({
11948
+ verdict: verdict3,
11949
+ exit_code: exitCode2,
11950
+ complete,
11951
+ base,
11952
+ agent: {
11953
+ exit_code: agentExit,
11954
+ timed_out: agentTimedOut,
11955
+ lifecycle_owned: agentRun.lifecycleOwned,
11956
+ ...opts.agentBudget !== void 0 ? { budget_secs: opts.agentBudget } : {}
11957
+ },
11958
+ verifier_backend: verifierBackendReport(verifierBackend),
11959
+ dependency_environment: dependencyEnvironment ? dependencyEnvironmentReport(dependencyEnvironment) : { status: "verifier-owned", roots: [], image: verifierBackend.image },
11960
+ ...extra
11961
+ })));
11962
+ };
11893
11963
  if (agentRun.failure && !agentTimedOut) {
11894
11964
  err(`tamperward run: agent runtime failed to start/report cleanly (${agentRun.failure}).`);
11895
11965
  }
11896
- if (process.platform === "linux" && !agentRun.lifecycleOwned) {
11897
- err("tamperward run: the Linux agent lifecycle boundary was not established/drained \u2014 failing closed before adjudication.");
11898
- finishObserverAdvisory(observer);
11966
+ if (!agentRun.lifecycleOwned) {
11967
+ err("tamperward run: the agent lifecycle boundary was not established/drained \u2014 failing closed before adjudication.");
11968
+ finishObserverAdvisory(observer, say);
11969
+ emitRunJson("CANNOT_ADJUDICATE", 2, {
11970
+ reason: "AGENT_LIFECYCLE_NOT_OWNED"
11971
+ });
11899
11972
  return 2;
11900
11973
  }
11901
11974
  if (agentTimedOut) {
@@ -11908,9 +11981,10 @@ function runEnvelope(opts) {
11908
11981
  err("tamperward run: git object-replacement state changed during this run \u2014 a replace ref, graft");
11909
11982
  err("or shallow boundary now redirects what the trusted base resolves to. Nothing below could be");
11910
11983
  err("read against the anchor we started from. That is the finding.");
11911
- out(`
11984
+ say(`
11912
11985
  tamperward run \u2014 agent exit ${agentExit}; OBJECT_REWRITE \u2192 ENFORCEMENT_FAILED`);
11913
- finishObserverAdvisory(observer);
11986
+ finishObserverAdvisory(observer, say);
11987
+ emitRunJson("OBJECT_REWRITE", 1);
11914
11988
  return 1;
11915
11989
  }
11916
11990
  const head = git3(["rev-parse", "HEAD"], cwd).trim();
@@ -11918,36 +11992,41 @@ tamperward run \u2014 agent exit ${agentExit}; OBJECT_REWRITE \u2192 ENFORCEMENT
11918
11992
  if (isAncestor.status !== 0) {
11919
11993
  err(`tamperward run: HEAD ${head.slice(0, 10)} is not a descendant of the trusted base ${base.slice(0, 10)} \u2014`);
11920
11994
  err("the agent rewrote history out from under the anchor. That is the finding.");
11921
- out(`
11995
+ say(`
11922
11996
  tamperward run \u2014 agent exit ${agentExit}; HISTORY_REWRITE \u2192 ENFORCEMENT_FAILED`);
11923
- finishObserverAdvisory(observer);
11997
+ finishObserverAdvisory(observer, say);
11998
+ emitRunJson("HISTORY_REWRITE", 1, { head });
11924
11999
  return 1;
11925
12000
  }
11926
12001
  const protectedIgnored = (rel) => isProtected(rel, frozenPolicy);
11927
12002
  const fpBefore = treeFingerprint(cwd, protectedIgnored);
11928
12003
  let diffCode = 0;
11929
12004
  if (head !== base) {
11930
- out(`
12005
+ say(`
11931
12006
  [envelope] policy check over committed changes (${base.slice(0, 10)}...${head.slice(0, 10)}):`);
11932
- diffCode = runCheck({ diff: `${base}...${head}`, cwd, policyOverride: frozenPolicy });
12007
+ diffCode = runCheck({ diff: `${base}...${head}`, cwd, policyOverride: frozenPolicy, silent: opts.json });
11933
12008
  }
11934
- out("\n[envelope] policy check over the working tree (frozen policy, untracked included, no local ledger):");
11935
- const workCode = runCheck({ worktree: true, cwd, policyOverride: frozenPolicy, includeUntracked: true, ciLayer: true });
12009
+ say("\n[envelope] policy check over the working tree (frozen policy, untracked included, no local ledger):");
12010
+ const workCode = runCheck({ worktree: true, cwd, policyOverride: frozenPolicy, includeUntracked: true, ciLayer: true, silent: opts.json });
11936
12011
  const dependencyBeforeVerificationAttestation = dependencyEnvironment ? attestDependencyEnvironment(cwd, dependencyEnvironment) : void 0;
11937
12012
  const dependencyBeforeVerification = dependencyBeforeVerificationAttestation?.check ?? { ok: true };
11938
12013
  if (!dependencyBeforeVerification.ok) {
11939
12014
  err("tamperward run: the frozen dependency environment changed before verification began \u2014");
11940
12015
  err(`${dependencyBeforeVerification.reason ?? "dependency identity changed"}.`);
11941
12016
  if (!opts.allowDepDrift) {
11942
- out(`
12017
+ say(`
11943
12018
  tamperward run \u2014 agent exit ${agentExit}; DEPENDENCY_DRIFT \u2192 ENFORCEMENT_FAILED`);
11944
- finishObserverAdvisory(observer);
12019
+ finishObserverAdvisory(observer, say);
12020
+ emitRunJson("DEPENDENCY_DRIFT", 1, {
12021
+ head,
12022
+ checks: { diff: diffCode, worktree: workCode }
12023
+ });
11945
12024
  return 1;
11946
12025
  }
11947
12026
  err("(--allow-dep-drift: proceeding anyway, on the operator's judgement.)");
11948
12027
  }
11949
12028
  let verifierFinalDependencyAttestation;
11950
- out("\n[envelope] pristine verification against the trusted base:");
12029
+ say("\n[envelope] pristine verification against the trusted base:");
11951
12030
  const verifyCode = runVerify({
11952
12031
  cwd,
11953
12032
  base,
@@ -11962,7 +12041,8 @@ tamperward run \u2014 agent exit ${agentExit}; DEPENDENCY_DRIFT \u2192 ENFORCEME
11962
12041
  }
11963
12042
  } : {},
11964
12043
  allowDepDrift: opts.allowDepDrift,
11965
- verifierBackend
12044
+ verifierBackend,
12045
+ silent: opts.json
11966
12046
  });
11967
12047
  if (opts.settle && opts.settle > 0) {
11968
12048
  waitForSettleSync(opts.settle);
@@ -11975,7 +12055,7 @@ tamperward run \u2014 agent exit ${agentExit}; DEPENDENCY_DRIFT \u2192 ENFORCEME
11975
12055
  const dependencyAfter = dependencyEnvironment ? checkDependencyEnvironment(cwd, dependencyEnvironment) : { ok: true };
11976
12056
  if (dependencyEnvironment && process.env.TAMPERWARD_DIAGNOSTICS === "1") {
11977
12057
  const metrics = dependencyEnvironmentDiagnostics(dependencyEnvironment);
11978
- out(
12058
+ say(
11979
12059
  `tamperward run \u2014 dependency attestation diagnostics: full_snapshots=${metrics.fullSnapshots} reused_snapshots=${metrics.reusedSnapshots} total_ms=${Number(metrics.totalMs.toFixed(3))} verifier_final_attestation=${verifierFinalDependencyAttestation ? "yes" : "no"} run_lifecycle_owned=${agentRun.lifecycleOwned ? "yes" : "no"} entry_reuse=${canReuseAdjacentDependencyAttestation(agentRun.lifecycleOwned) ? "yes" : "no"}`
11980
12060
  );
11981
12061
  }
@@ -12003,7 +12083,8 @@ tamperward run \u2014 agent exit ${agentExit}; DEPENDENCY_DRIFT \u2192 ENFORCEME
12003
12083
  cwd,
12004
12084
  base,
12005
12085
  head,
12006
- frozenPolicy
12086
+ frozenPolicy,
12087
+ say
12007
12088
  );
12008
12089
  observerBlocked = observed.blocking;
12009
12090
  }
@@ -12012,10 +12093,25 @@ tamperward run \u2014 agent exit ${agentExit}; DEPENDENCY_DRIFT \u2192 ENFORCEME
12012
12093
  const blocked = diffCode === 1 || workCode === 1 || verifyCode === 1 || notQuiescent || observerBlocked;
12013
12094
  const enforcement = cannot ? 2 : blocked ? 1 : 0;
12014
12095
  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";
12096
+ const machineVerdict = enforcement === 2 ? "CANNOT_ADJUDICATE" : rewrote ? "OBJECT_REWRITE" : depsDrifted ? "DEPENDENCY_DRIFT" : notQuiescent ? "NOT_QUIESCENT" : observerBlocked ? "TRANSIENT_OBSERVER_BLOCK" : enforcement === 1 ? "ENFORCEMENT_FAILED" : agentTimedOut ? "AGENT_TIMEOUT" : agentExit !== 0 ? "AGENT_FAILED" : "VERIFIED";
12015
12097
  const agentSummary = agentTimedOut ? `AGENT_TIMEOUT (${opts.agentBudget}s; exit 124)` : `agent exit ${agentExit}`;
12016
- out(`
12098
+ const exitCode = enforcement !== 0 ? enforcement : agentTimedOut ? 124 : agentExit;
12099
+ if (opts.json) {
12100
+ const cannotReason = enforcement !== 2 ? null : verifyCode === 2 ? "VERIFY_CANNOT_VERIFY" : diffCode === 2 ? "CHECK_DIFF_UNJUDGEABLE" : "CHECK_WORKTREE_UNJUDGEABLE";
12101
+ emitRunJson(machineVerdict, exitCode, {
12102
+ head,
12103
+ checks: { diff: diffCode, worktree: workCode, verify: verifyCode },
12104
+ observer: {
12105
+ enabled: Boolean(opts.observeTransients),
12106
+ blocking: observerBlocked
12107
+ },
12108
+ ...cannotReason ? { reason: cannotReason } : {}
12109
+ }, true);
12110
+ } else {
12111
+ say(`
12017
12112
  tamperward run \u2014 ${agentSummary}; checks diff=${diffCode} worktree=${workCode} verify=${verifyCode} \u2192 ${verdict2}`);
12018
- return enforcement !== 0 ? enforcement : agentTimedOut ? 124 : agentExit;
12113
+ }
12114
+ return exitCode;
12019
12115
  }
12020
12116
  function parseRun(args) {
12021
12117
  const o = { argv: [] };
@@ -12027,6 +12123,7 @@ function parseRun(args) {
12027
12123
  } else if (a === "--base") o.base = args[++i];
12028
12124
  else if (a === "--cmd") o.cmd = args[++i];
12029
12125
  else if (a === "--budget") o.budget = Number(args[++i]);
12126
+ else if (a === "--json") o.json = true;
12030
12127
  else if (a === "--agent-budget") o.agentBudget = Number(args[++i]);
12031
12128
  else if (a === "--allow-dirty") o.allowDirty = true;
12032
12129
  else if (a === "--settle") o.settle = Number(args[++i]);
@@ -12073,14 +12170,14 @@ function lifecyclePlatformCheck(platform = process.platform, linuxPython = platf
12073
12170
  var VERIFY_COMMAND = /\btamperward(?:@\S+)?\s+verify\b/;
12074
12171
  function err2(message, opts, id = "doctor", checks = []) {
12075
12172
  if (opts?.json) {
12076
- const report2 = {
12173
+ const report2 = machineOutput({
12077
12174
  command: "doctor",
12078
12175
  authoritative: false,
12079
12176
  checks: [
12080
12177
  ...checks.filter((check2) => check2.id !== id),
12081
12178
  { id, state: "BROKEN", detail: message }
12082
12179
  ]
12083
- };
12180
+ });
12084
12181
  process.stdout.write(JSON.stringify(report2) + "\n");
12085
12182
  } else {
12086
12183
  process.stderr.write(`tamperward doctor: ${message}
@@ -12293,7 +12390,7 @@ function observerCheck(cwd) {
12293
12390
  function emitReport(opts, checks) {
12294
12391
  const authoritative = !checks.some((x) => x.state === "BROKEN");
12295
12392
  if (opts.json) {
12296
- const report2 = { command: "doctor", authoritative, checks };
12393
+ const report2 = machineOutput({ command: "doctor", authoritative, checks });
12297
12394
  process.stdout.write(JSON.stringify(report2) + "\n");
12298
12395
  return;
12299
12396
  }
@@ -13149,7 +13246,7 @@ function validateCliArgs(cmd, args) {
13149
13246
  const prefix = args.slice(0, delimiter3);
13150
13247
  const command = args.slice(delimiter3 + 1);
13151
13248
  const parsed = validateFlatArgs(prefix, {
13152
- flags: ["--allow-dirty", "--allow-dep-drift", "--observe-transients"],
13249
+ flags: ["--allow-dirty", "--allow-dep-drift", "--observe-transients", "--json"],
13153
13250
  values: {
13154
13251
  "--base": "string",
13155
13252
  "--cmd": "string",
@@ -13250,7 +13347,8 @@ Formats:
13250
13347
  github GitHub Actions view: one inline annotation per finding (so it shows up
13251
13348
  on the line in "Files changed") plus a job-summary table on the run
13252
13349
  page, alongside the same text output in the log.
13253
- json the findings verbatim, plus a summary count.
13350
+ json versioned machine-readable output. Top-level schema_version identifies
13351
+ the public JSON-schema major; published schemas live under schemas/.
13254
13352
  auto github when GITHUB_ACTIONS=true, otherwise text.
13255
13353
  tamperward hook claude PreToolUse gate (reads hook JSON on stdin)
13256
13354
  tamperward sweep claude Stop sweep (re-scan the turn's working tree)
@@ -13279,11 +13377,13 @@ Formats:
13279
13377
  [--budget S] [--agent-budget S] untrusted, then re-adjudicate the tree it
13280
13378
  [--allow-dirty] [--settle S] left (policy over base...HEAD and the
13281
13379
  [--allow-dep-drift] [--cwd D] worktree, plus verify).
13282
- [--observe-transients] --observe-transients starts a session-
13380
+ [--observe-transients] [--json] --observe-transients starts a session-
13283
13381
  scoped observer before the agent and
13284
13382
  reports temporal evidence/health after it.
13285
13383
  --agent-budget bounds the agent runtime
13286
13384
  itself and still adjudicates after timeout.
13385
+ --json emits one final versioned run verdict
13386
+ document on stdout after agent adjudication.
13287
13387
  Exit: agent's
13288
13388
  code when clean; 124 on clean AGENT_TIMEOUT;
13289
13389
  1 on any blocking finding/masked failure;