tamperward 2.17.1 → 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
@@ -2010,8 +2010,8 @@ function numericOf(e) {
2010
2010
  }
2011
2011
  return void 0;
2012
2012
  }
2013
- function metricsOf(expr, resolve9) {
2014
- const obj = resolve9(expr);
2013
+ function metricsOf(expr, resolve10) {
2014
+ const obj = resolve10(expr);
2015
2015
  if (!ts3.isObjectLiteralExpression(obj)) return void 0;
2016
2016
  const out2 = { values: {}, opaque: false };
2017
2017
  let any = false;
@@ -2025,7 +2025,7 @@ function metricsOf(expr, resolve9) {
2025
2025
  const k = keyName(p.name);
2026
2026
  if (k === null || !isMetric(k)) continue;
2027
2027
  any = true;
2028
- const n = numericOf(resolve9(p.initializer));
2028
+ const n = numericOf(resolve10(p.initializer));
2029
2029
  if (n === void 0) out2.opaque = true;
2030
2030
  else out2.values[k] = n;
2031
2031
  }
@@ -2071,17 +2071,17 @@ function parseThresholds(src) {
2071
2071
  const res = { paths: /* @__PURE__ */ new Map(), present: false, opaque: false };
2072
2072
  try {
2073
2073
  const sf = sourceOf(src);
2074
- const resolve9 = resolverFor(sf);
2074
+ const resolve10 = resolverFor(sf);
2075
2075
  const visit = (node) => {
2076
2076
  if (ts3.isPropertyAssignment(node)) {
2077
2077
  const key2 = keyName(node.name);
2078
2078
  if (key2 === "coverageThreshold" || key2 === "thresholds" && underKey(node, "coverage")) {
2079
2079
  res.present = true;
2080
- const init = resolve9(node.initializer);
2080
+ const init = resolve10(node.initializer);
2081
2081
  if (!ts3.isObjectLiteralExpression(init)) {
2082
2082
  res.opaque = true;
2083
2083
  } else {
2084
- const flat = metricsOf(init, resolve9);
2084
+ const flat = metricsOf(init, resolve10);
2085
2085
  if (flat) res.global = merge(res.global, flat);
2086
2086
  for (const p of init.properties) {
2087
2087
  if (ts3.isSpreadAssignment(p)) {
@@ -2095,7 +2095,7 @@ function parseThresholds(src) {
2095
2095
  res.global = merge(res.global, { values: { branches: 100, functions: 100, lines: 100, statements: 100 }, opaque: false });
2096
2096
  continue;
2097
2097
  }
2098
- const m = metricsOf(p.initializer, resolve9);
2098
+ const m = metricsOf(p.initializer, resolve10);
2099
2099
  if (!m) continue;
2100
2100
  if (k === "global") res.global = merge(res.global, m);
2101
2101
  else res.paths.set(norm(k), m);
@@ -2128,18 +2128,18 @@ function parseLists(src) {
2128
2128
  const res = { collectFrom: null, collectKey: "collectCoverageFrom", ignore: [], opaqueCollect: false, opaqueIgnore: false };
2129
2129
  try {
2130
2130
  const sf = sourceOf(src);
2131
- const resolve9 = resolverFor(sf);
2131
+ const resolve10 = resolverFor(sf);
2132
2132
  const visit = (node) => {
2133
2133
  if (ts3.isPropertyAssignment(node)) {
2134
2134
  const k = keyName(node.name);
2135
2135
  const vitestCoverage = ownerKey(node) === "coverage";
2136
2136
  if (k === "collectCoverageFrom" || k === "include" && vitestCoverage) {
2137
- const { items, opaque } = literals(resolve9(node.initializer));
2137
+ const { items, opaque } = literals(resolve10(node.initializer));
2138
2138
  res.collectFrom = items;
2139
2139
  res.collectKey = k === "include" ? "coverage.include" : k;
2140
2140
  if (opaque) res.opaqueCollect = true;
2141
2141
  } else if (k === "coveragePathIgnorePatterns" || k === "exclude" && vitestCoverage) {
2142
- const { items, opaque } = literals(resolve9(node.initializer));
2142
+ const { items, opaque } = literals(resolve10(node.initializer));
2143
2143
  res.ignore.push(...items);
2144
2144
  if (opaque) res.opaqueIgnore = true;
2145
2145
  }
@@ -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) {
@@ -10695,6 +10703,17 @@ function verifierInputs(cmd, atBase, policy) {
10695
10703
  }
10696
10704
  return picked;
10697
10705
  }
10706
+ function verifierCoveredInputs(cmd, atBase, policy) {
10707
+ const verifierOwned = verifierInputs(cmd, atBase, policy);
10708
+ const verifierGlobs = policy.verify?.inputs ?? [];
10709
+ const covered = /* @__PURE__ */ new Set();
10710
+ for (const path of atBase) {
10711
+ if (OVERLAY_CLASSES.some((category) => isProtected(path, policy, category)) || matchesAny(path, VERIFICATION_SURFACE) || verifierOwned.has(path) || verifierGlobs.length > 0 && matchesAny(path, verifierGlobs)) {
10712
+ covered.add(path);
10713
+ }
10714
+ }
10715
+ return covered;
10716
+ }
10698
10717
  function baseEntries(base, cwd) {
10699
10718
  return git2(["ls-tree", "-r", "-z", base], cwd).split("\0").filter(Boolean).map((rec) => {
10700
10719
  const tab = rec.indexOf(" ");
@@ -10722,9 +10741,12 @@ function overlayDigest(dest, paths) {
10722
10741
  function overlayPristine(cwd, base, dest, policy, cmd, dependencyRoot) {
10723
10742
  const entries = baseEntries(base, cwd);
10724
10743
  const atBase = entries.map((e) => e.path);
10725
- const verifierOwned = verifierInputs(cmd, atBase, policy);
10726
10744
  const verifierGlobs = policy.verify?.inputs ?? [];
10727
- const isOverlay = (p) => OVERLAY_CLASSES.some((c) => isProtected(p, policy, c)) || matchesAny(p, VERIFICATION_SURFACE) || verifierOwned.has(p) || verifierGlobs.length > 0 && matchesAny(p, verifierGlobs);
10745
+ const coveredBaseInputs = verifierCoveredInputs(cmd, atBase, policy);
10746
+ const isOverlay = (p) => coveredBaseInputs.has(p) || // Candidate-added files do not exist in atBase and therefore cannot appear
10747
+ // in the precomputed set. They still must be removed when they land on a
10748
+ // protected class or the built-in verification surface.
10749
+ OVERLAY_CLASSES.some((category) => isProtected(p, policy, category)) || matchesAny(p, VERIFICATION_SURFACE) || verifierGlobs.length > 0 && matchesAny(p, verifierGlobs);
10728
10750
  const restored = [];
10729
10751
  const restoredLinks = [];
10730
10752
  const baseProtected = /* @__PURE__ */ new Set();
@@ -10880,10 +10902,24 @@ function renderStageDiagnostics(out2, stage, result) {
10880
10902
  }
10881
10903
  function runVerify(opts) {
10882
10904
  const cwd = opts.cwd ?? process.cwd();
10883
- const out2 = (s) => void process.stdout.write(s + "\n");
10884
- if (opts.invalid) {
10885
- 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
+ }
10886
10919
  return 2;
10920
+ };
10921
+ if (opts.invalid) {
10922
+ return cannotVerify("INVALID_ARGUMENTS", opts.invalid);
10887
10923
  }
10888
10924
  let policy;
10889
10925
  try {
@@ -10891,24 +10927,24 @@ function runVerify(opts) {
10891
10927
  else if (opts.base) policy = loadPolicyAt(resolveBase(opts.base, cwd), cwd) ?? defaultPolicy();
10892
10928
  else policy = loadPolicy(cwd);
10893
10929
  } catch (e) {
10894
- out2(`verify: cannot load policy (${e instanceof Error ? e.message : String(e)}) \u2014 failing closed`);
10895
- return 2;
10930
+ return cannotVerify("POLICY_ERROR", `cannot load policy (${e instanceof Error ? e.message : String(e)})`);
10896
10931
  }
10897
10932
  if (opts.requireAncestor) {
10898
10933
  const requested = git2(["rev-parse", "--verify", `${opts.base ?? "HEAD"}^{commit}`], cwd).trim();
10899
10934
  if (!baseIsAncestorOfHead(requested, cwd)) {
10900
- out2(
10901
- `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 }
10902
10939
  );
10903
- return 2;
10904
10940
  }
10905
10941
  }
10906
10942
  const cmd = opts.cmd ?? policy.verify?.command;
10907
10943
  if (!cmd) {
10908
- out2(
10909
- 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"
10910
10947
  );
10911
- return 2;
10912
10948
  }
10913
10949
  const budget = opts.budget ?? policy.verify?.budget ?? 300;
10914
10950
  const verifierBackend = opts.verifierBackend ?? prepareVerifierBackend(policy.verify);
@@ -10916,6 +10952,7 @@ function runVerify(opts) {
10916
10952
  if (!verifierBackend.available) {
10917
10953
  if (opts.json) {
10918
10954
  out2(JSON.stringify({
10955
+ schema_version: MACHINE_SCHEMA_VERSION,
10919
10956
  verdict: "CANNOT_VERIFY",
10920
10957
  reason: "VERIFIER_BACKEND_UNAVAILABLE",
10921
10958
  verifier_backend: backendReport(),
@@ -10931,6 +10968,7 @@ function runVerify(opts) {
10931
10968
  if (!isolated && !localVerifierShell(process.platform, cmd)) {
10932
10969
  if (opts.json) {
10933
10970
  out2(JSON.stringify({
10971
+ schema_version: MACHINE_SCHEMA_VERSION,
10934
10972
  verdict: "CANNOT_VERIFY",
10935
10973
  reason: "LOCAL_VERIFIER_UNSUPPORTED_PLATFORM",
10936
10974
  platform: process.platform,
@@ -10953,6 +10991,7 @@ function runVerify(opts) {
10953
10991
  if (dependencyEnvironment?.status === "unattestable" && !opts.allowDepDrift) {
10954
10992
  if (opts.json) {
10955
10993
  out2(JSON.stringify({
10994
+ schema_version: MACHINE_SCHEMA_VERSION,
10956
10995
  verdict: "CANNOT_VERIFY",
10957
10996
  reason: "DEPENDENCY_ENVIRONMENT_UNATTESTABLE",
10958
10997
  verifier_backend: backendReport(),
@@ -10972,8 +11011,7 @@ function runVerify(opts) {
10972
11011
  try {
10973
11012
  base = resolveBase(opts.base ?? "HEAD", cwd);
10974
11013
  } catch {
10975
- out2(`verify: cannot resolve base rev "${opts.base ?? "HEAD"}" \u2014 failing closed`);
10976
- return 2;
11014
+ return cannotVerify("BASE_UNRESOLVABLE", `cannot resolve base rev "${opts.base ?? "HEAD"}"`);
10977
11015
  }
10978
11016
  const visRoot = mkdtempSync3(join16(tmpdir3(), "tw-verify-vis-"));
10979
11017
  const visDir = join16(visRoot, "t");
@@ -10990,17 +11028,22 @@ function runVerify(opts) {
10990
11028
  ) ?? checkDeps() : { ok: true };
10991
11029
  if (!dependencyAtEntry.ok && !opts.allowDepDrift) {
10992
11030
  cleanup([visRoot]);
10993
- out2("verify: the frozen dependency environment changed before the visible suite ran \u2014");
10994
- out2((dependencyAtEntry.reason ?? "dependency identity changed") + ". Failing closed, not open.");
10995
- 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
+ );
10996
11036
  }
10997
11037
  try {
10998
11038
  mkdirSync6(visDir);
10999
11039
  materialize(cwd, visDir, frozenNodeModules);
11000
11040
  } catch (e) {
11001
11041
  cleanup([visRoot]);
11002
- out2(`verify: could not materialize (${e instanceof Error ? e.message : String(e)}) \u2014 failing closed`);
11003
- 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
+ );
11004
11047
  }
11005
11048
  const visible = runStage(visDir);
11006
11049
  if (visible.failure === "backend" || visible.failure === "resource") {
@@ -11008,6 +11051,7 @@ function runVerify(opts) {
11008
11051
  const exhausted = visible.failure === "resource";
11009
11052
  if (opts.json) {
11010
11053
  out2(JSON.stringify({
11054
+ schema_version: MACHINE_SCHEMA_VERSION,
11011
11055
  verdict: "CANNOT_VERIFY",
11012
11056
  reason: exhausted ? "VERIFIER_RESOURCE_EXHAUSTED" : "VERIFIER_BACKEND_RUNTIME_FAILURE",
11013
11057
  stage: "visible",
@@ -11028,16 +11072,20 @@ function runVerify(opts) {
11028
11072
  }
11029
11073
  if (treeFingerprint(cwd, protectedIgnored) !== treeBefore) {
11030
11074
  cleanup([visRoot]);
11031
- out2("verify: the working tree changed while the visible suite was running \u2014 the pristine copy");
11032
- out2("would be materialised from a tree the candidate just edited. Failing closed, not open.");
11033
- 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
+ );
11034
11080
  }
11035
11081
  const dependencyAfterVisible = checkDeps();
11036
11082
  if (!dependencyAfterVisible.ok && !opts.allowDepDrift) {
11037
11083
  cleanup([visRoot]);
11038
- out2("verify: the frozen dependency environment changed while the visible suite was running \u2014");
11039
- out2((dependencyAfterVisible.reason ?? "dependency identity changed") + ". Failing closed, not open.");
11040
- 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
+ );
11041
11089
  }
11042
11090
  const priRoot = mkdtempSync3(join16(tmpdir3(), "tw-verify-pri-"));
11043
11091
  const priDir = join16(priRoot, "t");
@@ -11056,8 +11104,11 @@ function runVerify(opts) {
11056
11104
  ));
11057
11105
  } catch (e) {
11058
11106
  cleanup([visRoot, priRoot]);
11059
- out2(`verify: could not materialize (${e instanceof Error ? e.message : String(e)}) \u2014 failing closed`);
11060
- 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
+ );
11061
11112
  }
11062
11113
  const overlayBefore = overlayDigest(priDir, restored);
11063
11114
  const pristine = runStage(priDir);
@@ -11066,6 +11117,7 @@ function runVerify(opts) {
11066
11117
  const exhausted = pristine.failure === "resource";
11067
11118
  if (opts.json) {
11068
11119
  out2(JSON.stringify({
11120
+ schema_version: MACHINE_SCHEMA_VERSION,
11069
11121
  verdict: "CANNOT_VERIFY",
11070
11122
  reason: exhausted ? "VERIFIER_RESOURCE_EXHAUSTED" : "VERIFIER_BACKEND_RUNTIME_FAILURE",
11071
11123
  stage: "pristine",
@@ -11094,13 +11146,14 @@ function runVerify(opts) {
11094
11146
  const depsMoved = !dependencyAfterPristine.ok && !opts.allowDepDrift;
11095
11147
  cleanup([visRoot, priRoot]);
11096
11148
  if (overlayMoved || treeMoved || depsMoved) {
11097
- out2(
11098
- "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"
11099
- );
11100
- out2("the verdict would describe something other than what ran. Failing closed, not open.");
11101
- out2("(A suite that rewrites its own snapshots or test files in place will trip this; run it in");
11102
- out2("whatever mode your runner calls CI, so verification observes rather than updates.)");
11103
- 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
+ });
11104
11157
  }
11105
11158
  let verdict2;
11106
11159
  let code2;
@@ -11122,6 +11175,7 @@ function runVerify(opts) {
11122
11175
  if (opts.json) {
11123
11176
  out2(
11124
11177
  JSON.stringify({
11178
+ schema_version: MACHINE_SCHEMA_VERSION,
11125
11179
  verdict: verdict2,
11126
11180
  base,
11127
11181
  command: cmd,
@@ -11285,8 +11339,9 @@ var AGENT_SUPERVISOR = String.raw`
11285
11339
  const { spawn, spawnSync } = require('node:child_process');
11286
11340
  const fs = require('node:fs');
11287
11341
 
11288
- const [resultFile, budgetRaw, command, ...args] = process.argv.slice(1);
11342
+ const [resultFile, budgetRaw, machineRaw, command, ...args] = process.argv.slice(1);
11289
11343
  const budgetMs = budgetRaw === '' ? null : Number(budgetRaw);
11344
+ const machineMode = machineRaw === '1';
11290
11345
  let child;
11291
11346
  let timedOut = false;
11292
11347
  let finished = false;
@@ -11312,7 +11367,9 @@ function killTree(pid) {
11312
11367
 
11313
11368
  try {
11314
11369
  child = spawn(command, args, {
11315
- 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',
11316
11373
  detached: process.platform !== 'win32',
11317
11374
  });
11318
11375
  } catch (e) {
@@ -11349,8 +11406,9 @@ child.once('exit', (code, signal) => finish(code, signal));
11349
11406
  var LINUX_SUBREAPER_SUPERVISOR = String.raw`
11350
11407
  import ctypes, json, os, signal, subprocess, sys, time
11351
11408
 
11352
- 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:]
11353
11410
  budget = None if budget_raw == "" else float(budget_raw)
11411
+ machine_mode = machine_raw == "1"
11354
11412
  libc = ctypes.CDLL(None, use_errno=True)
11355
11413
  PR_SET_DUMPABLE = 4
11356
11414
  PR_SET_CHILD_SUBREAPER = 36
@@ -11468,6 +11526,10 @@ try:
11468
11526
  cwd=agent_cwd,
11469
11527
  env=agent_env,
11470
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,
11471
11533
  )
11472
11534
  except Exception as exc:
11473
11535
  # No candidate process exists, so the lifecycle domain is trivially empty.
@@ -11497,7 +11559,7 @@ write_result({
11497
11559
  })
11498
11560
  raise SystemExit(0)
11499
11561
  `;
11500
- function runAgentSupervised(argv2, cwd, budgetSecs, linuxPythonCandidates, lifecycleTestMode) {
11562
+ function runAgentSupervised(argv2, cwd, budgetSecs, linuxPythonCandidates, lifecycleTestMode, machineMode = false) {
11501
11563
  const stateDir = mkdtempSync4(join17(tmpdir4(), "tw-agent-supervisor-"));
11502
11564
  const resultFile = join17(stateDir, "result.json");
11503
11565
  const agentEnvFile = join17(stateDir, "agent-env.json");
@@ -11530,6 +11592,7 @@ function runAgentSupervised(argv2, cwd, budgetSecs, linuxPythonCandidates, lifec
11530
11592
  cwd,
11531
11593
  budgetSecs === void 0 ? "" : String(budgetSecs),
11532
11594
  lifecycleTestMode ?? "",
11595
+ machineMode ? "1" : "0",
11533
11596
  ...argv2
11534
11597
  ];
11535
11598
  supervisorCwd = "/";
@@ -11541,6 +11604,7 @@ function runAgentSupervised(argv2, cwd, budgetSecs, linuxPythonCandidates, lifec
11541
11604
  AGENT_SUPERVISOR,
11542
11605
  resultFile,
11543
11606
  budgetSecs === void 0 ? "" : String(budgetSecs * 1e3),
11607
+ machineMode ? "1" : "0",
11544
11608
  ...argv2
11545
11609
  ];
11546
11610
  }
@@ -11660,8 +11724,7 @@ function stopObserverProcess(observer) {
11660
11724
  }
11661
11725
  const deadline = Date.now() + 2e3;
11662
11726
  while (Date.now() < deadline) {
11663
- const health = watcherTelemetry(observer.log);
11664
- if (health.health?.state === "stopped" || !pidAlive2(pid)) break;
11727
+ if (!pidAlive2(pid)) break;
11665
11728
  waitMs(25);
11666
11729
  }
11667
11730
  if (pidAlive2(pid)) {
@@ -11674,22 +11737,22 @@ function stopObserverProcess(observer) {
11674
11737
  observer.finished = true;
11675
11738
  return beforeStop;
11676
11739
  }
11677
- function observerSummary(observer, telemetry) {
11740
+ function observerSummary(observer, telemetry, write = out) {
11678
11741
  const h = telemetry.health;
11679
11742
  const counts = h ? `${h.event_count} event(s), ${h.dropped_events} dropped, ${h.error_count} error(s)` : "no valid health record";
11680
11743
  const reason = telemetry.reason ? `; ${telemetry.reason}` : "";
11681
- out(
11744
+ write(
11682
11745
  `tamperward run \u2014 transient observer: ${telemetry.state} (advisory; ${counts}; log ${observer.log}${reason})`
11683
11746
  );
11684
11747
  }
11685
- function collectObserverFindings(observer, telemetry, cwd, base, head, policy) {
11686
- observerSummary(observer, telemetry);
11748
+ function collectObserverFindings(observer, telemetry, cwd, base, head, policy, write = out) {
11749
+ observerSummary(observer, telemetry, write);
11687
11750
  const strict = process.env.TAMPERWARD_TRANSIENT === "block";
11688
11751
  const drained = drainEvents(observer.log, 0);
11689
11752
  const { events } = drained;
11690
11753
  if (!drained.complete) {
11691
11754
  const issue = drained.issue ?? "read-stalled";
11692
- out(
11755
+ write(
11693
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.")
11694
11757
  );
11695
11758
  if (strict) return { blocking: true };
@@ -11705,7 +11768,7 @@ function collectObserverFindings(observer, telemetry, cwd, base, head, policy) {
11705
11768
  changes.filter((change) => change.kind === "file").map((change) => change.path)
11706
11769
  );
11707
11770
  } catch (e) {
11708
- out(
11771
+ write(
11709
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." : "")
11710
11773
  );
11711
11774
  return { blocking: strict };
@@ -11716,16 +11779,16 @@ function collectObserverFindings(observer, telemetry, cwd, base, head, policy) {
11716
11779
  };
11717
11780
  const findings = transientFindings(events, persistent, policy, finalHash);
11718
11781
  for (const finding of findings) {
11719
- out(
11782
+ write(
11720
11783
  `[observer] ${finding.severity === "block" ? "BLOCK" : "warn"} ${finding.rule}${finding.file ? " " + finding.file : ""}: ${finding.message}`
11721
11784
  );
11722
11785
  }
11723
11786
  return { blocking: findings.some((finding) => finding.severity === "block") };
11724
11787
  }
11725
- function finishObserverAdvisory(observer) {
11788
+ function finishObserverAdvisory(observer, write = out) {
11726
11789
  if (!observer || observer.finished) return;
11727
11790
  const telemetry = stopObserverProcess(observer);
11728
- observerSummary(observer, telemetry);
11791
+ observerSummary(observer, telemetry, write);
11729
11792
  }
11730
11793
  function git3(args, cwd) {
11731
11794
  return execFileSync9("git", args, { cwd, encoding: "utf8", maxBuffer: 1 << 28, env: trustedGitEnv() });
@@ -11784,6 +11847,8 @@ function survivorsHoldingTree(cwd, spawnedAfterTicks) {
11784
11847
  }
11785
11848
  function runEnvelope(opts) {
11786
11849
  const cwd = resolve7(opts.cwd ?? process.cwd());
11850
+ const say = opts.json ? (_s) => {
11851
+ } : out;
11787
11852
  if (!opts.argv.length) {
11788
11853
  err("tamperward run: no agent command given (usage: tamperward run [options] -- <command...>)");
11789
11854
  return 2;
@@ -11834,8 +11899,8 @@ function runEnvelope(opts) {
11834
11899
  return 2;
11835
11900
  }
11836
11901
  const verifierBackend = prepareVerifierBackend(frozenPolicy.verify);
11837
- out(`tamperward run \u2014 trusted base ${base.slice(0, 10)}; agent exit is untrusted.`);
11838
- 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)}`);
11839
11904
  if (!verifierBackend.available) {
11840
11905
  err("tamperward run: the requested verifier backend cannot be established \u2014");
11841
11906
  err(`${verifierBackend.reason ?? "unknown backend failure"}. Failing closed before the agent starts.`);
@@ -11851,7 +11916,7 @@ function runEnvelope(opts) {
11851
11916
  }
11852
11917
  const dependencyEnvironment = verifierBackend.kind === "local" ? discoverDependencyEnvironment(cwd, frozenCmd) : null;
11853
11918
  if (dependencyEnvironment) {
11854
- out(`tamperward run \u2014 dependency environment: ${dependencyEnvironmentSummary(dependencyEnvironment)}`);
11919
+ say(`tamperward run \u2014 dependency environment: ${dependencyEnvironmentSummary(dependencyEnvironment)}`);
11855
11920
  if (dependencyEnvironment.status === "unattestable" && !opts.allowDepDrift) {
11856
11921
  err("tamperward run: the verifier dependency environment cannot be attested \u2014");
11857
11922
  err(`${dependencyEnvironment.reason ?? "unknown dependency environment"}. Failing closed before the agent starts.`);
@@ -11859,11 +11924,11 @@ function runEnvelope(opts) {
11859
11924
  return 2;
11860
11925
  }
11861
11926
  } else {
11862
- 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"}`);
11863
11928
  }
11864
11929
  const observer = opts.observeTransients ? startSupervisedObserver(cwd, base, opts.observerEntry) : null;
11865
11930
  if (!observer) {
11866
- 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)");
11867
11932
  }
11868
11933
  const rewritesBefore = objectRewriteState(cwd);
11869
11934
  const spawnTicks = nowTicks();
@@ -11872,16 +11937,38 @@ function runEnvelope(opts) {
11872
11937
  cwd,
11873
11938
  opts.agentBudget,
11874
11939
  opts.linuxPythonCandidates,
11875
- opts.lifecycleTestMode
11940
+ opts.lifecycleTestMode,
11941
+ opts.json === true
11876
11942
  );
11877
11943
  const agentExit = agentRun.exit;
11878
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
+ };
11879
11963
  if (agentRun.failure && !agentTimedOut) {
11880
11964
  err(`tamperward run: agent runtime failed to start/report cleanly (${agentRun.failure}).`);
11881
11965
  }
11882
- if (process.platform === "linux" && !agentRun.lifecycleOwned) {
11883
- err("tamperward run: the Linux agent lifecycle boundary was not established/drained \u2014 failing closed before adjudication.");
11884
- 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
+ });
11885
11972
  return 2;
11886
11973
  }
11887
11974
  if (agentTimedOut) {
@@ -11894,9 +11981,10 @@ function runEnvelope(opts) {
11894
11981
  err("tamperward run: git object-replacement state changed during this run \u2014 a replace ref, graft");
11895
11982
  err("or shallow boundary now redirects what the trusted base resolves to. Nothing below could be");
11896
11983
  err("read against the anchor we started from. That is the finding.");
11897
- out(`
11984
+ say(`
11898
11985
  tamperward run \u2014 agent exit ${agentExit}; OBJECT_REWRITE \u2192 ENFORCEMENT_FAILED`);
11899
- finishObserverAdvisory(observer);
11986
+ finishObserverAdvisory(observer, say);
11987
+ emitRunJson("OBJECT_REWRITE", 1);
11900
11988
  return 1;
11901
11989
  }
11902
11990
  const head = git3(["rev-parse", "HEAD"], cwd).trim();
@@ -11904,36 +11992,41 @@ tamperward run \u2014 agent exit ${agentExit}; OBJECT_REWRITE \u2192 ENFORCEMENT
11904
11992
  if (isAncestor.status !== 0) {
11905
11993
  err(`tamperward run: HEAD ${head.slice(0, 10)} is not a descendant of the trusted base ${base.slice(0, 10)} \u2014`);
11906
11994
  err("the agent rewrote history out from under the anchor. That is the finding.");
11907
- out(`
11995
+ say(`
11908
11996
  tamperward run \u2014 agent exit ${agentExit}; HISTORY_REWRITE \u2192 ENFORCEMENT_FAILED`);
11909
- finishObserverAdvisory(observer);
11997
+ finishObserverAdvisory(observer, say);
11998
+ emitRunJson("HISTORY_REWRITE", 1, { head });
11910
11999
  return 1;
11911
12000
  }
11912
12001
  const protectedIgnored = (rel) => isProtected(rel, frozenPolicy);
11913
12002
  const fpBefore = treeFingerprint(cwd, protectedIgnored);
11914
12003
  let diffCode = 0;
11915
12004
  if (head !== base) {
11916
- out(`
12005
+ say(`
11917
12006
  [envelope] policy check over committed changes (${base.slice(0, 10)}...${head.slice(0, 10)}):`);
11918
- diffCode = runCheck({ diff: `${base}...${head}`, cwd, policyOverride: frozenPolicy });
12007
+ diffCode = runCheck({ diff: `${base}...${head}`, cwd, policyOverride: frozenPolicy, silent: opts.json });
11919
12008
  }
11920
- out("\n[envelope] policy check over the working tree (frozen policy, untracked included, no local ledger):");
11921
- 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 });
11922
12011
  const dependencyBeforeVerificationAttestation = dependencyEnvironment ? attestDependencyEnvironment(cwd, dependencyEnvironment) : void 0;
11923
12012
  const dependencyBeforeVerification = dependencyBeforeVerificationAttestation?.check ?? { ok: true };
11924
12013
  if (!dependencyBeforeVerification.ok) {
11925
12014
  err("tamperward run: the frozen dependency environment changed before verification began \u2014");
11926
12015
  err(`${dependencyBeforeVerification.reason ?? "dependency identity changed"}.`);
11927
12016
  if (!opts.allowDepDrift) {
11928
- out(`
12017
+ say(`
11929
12018
  tamperward run \u2014 agent exit ${agentExit}; DEPENDENCY_DRIFT \u2192 ENFORCEMENT_FAILED`);
11930
- finishObserverAdvisory(observer);
12019
+ finishObserverAdvisory(observer, say);
12020
+ emitRunJson("DEPENDENCY_DRIFT", 1, {
12021
+ head,
12022
+ checks: { diff: diffCode, worktree: workCode }
12023
+ });
11931
12024
  return 1;
11932
12025
  }
11933
12026
  err("(--allow-dep-drift: proceeding anyway, on the operator's judgement.)");
11934
12027
  }
11935
12028
  let verifierFinalDependencyAttestation;
11936
- out("\n[envelope] pristine verification against the trusted base:");
12029
+ say("\n[envelope] pristine verification against the trusted base:");
11937
12030
  const verifyCode = runVerify({
11938
12031
  cwd,
11939
12032
  base,
@@ -11948,7 +12041,8 @@ tamperward run \u2014 agent exit ${agentExit}; DEPENDENCY_DRIFT \u2192 ENFORCEME
11948
12041
  }
11949
12042
  } : {},
11950
12043
  allowDepDrift: opts.allowDepDrift,
11951
- verifierBackend
12044
+ verifierBackend,
12045
+ silent: opts.json
11952
12046
  });
11953
12047
  if (opts.settle && opts.settle > 0) {
11954
12048
  waitForSettleSync(opts.settle);
@@ -11961,7 +12055,7 @@ tamperward run \u2014 agent exit ${agentExit}; DEPENDENCY_DRIFT \u2192 ENFORCEME
11961
12055
  const dependencyAfter = dependencyEnvironment ? checkDependencyEnvironment(cwd, dependencyEnvironment) : { ok: true };
11962
12056
  if (dependencyEnvironment && process.env.TAMPERWARD_DIAGNOSTICS === "1") {
11963
12057
  const metrics = dependencyEnvironmentDiagnostics(dependencyEnvironment);
11964
- out(
12058
+ say(
11965
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"}`
11966
12060
  );
11967
12061
  }
@@ -11989,7 +12083,8 @@ tamperward run \u2014 agent exit ${agentExit}; DEPENDENCY_DRIFT \u2192 ENFORCEME
11989
12083
  cwd,
11990
12084
  base,
11991
12085
  head,
11992
- frozenPolicy
12086
+ frozenPolicy,
12087
+ say
11993
12088
  );
11994
12089
  observerBlocked = observed.blocking;
11995
12090
  }
@@ -11998,10 +12093,25 @@ tamperward run \u2014 agent exit ${agentExit}; DEPENDENCY_DRIFT \u2192 ENFORCEME
11998
12093
  const blocked = diffCode === 1 || workCode === 1 || verifyCode === 1 || notQuiescent || observerBlocked;
11999
12094
  const enforcement = cannot ? 2 : blocked ? 1 : 0;
12000
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";
12001
12097
  const agentSummary = agentTimedOut ? `AGENT_TIMEOUT (${opts.agentBudget}s; exit 124)` : `agent exit ${agentExit}`;
12002
- 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(`
12003
12112
  tamperward run \u2014 ${agentSummary}; checks diff=${diffCode} worktree=${workCode} verify=${verifyCode} \u2192 ${verdict2}`);
12004
- return enforcement !== 0 ? enforcement : agentTimedOut ? 124 : agentExit;
12113
+ }
12114
+ return exitCode;
12005
12115
  }
12006
12116
  function parseRun(args) {
12007
12117
  const o = { argv: [] };
@@ -12013,6 +12123,7 @@ function parseRun(args) {
12013
12123
  } else if (a === "--base") o.base = args[++i];
12014
12124
  else if (a === "--cmd") o.cmd = args[++i];
12015
12125
  else if (a === "--budget") o.budget = Number(args[++i]);
12126
+ else if (a === "--json") o.json = true;
12016
12127
  else if (a === "--agent-budget") o.agentBudget = Number(args[++i]);
12017
12128
  else if (a === "--allow-dirty") o.allowDirty = true;
12018
12129
  else if (a === "--settle") o.settle = Number(args[++i]);
@@ -12059,14 +12170,14 @@ function lifecyclePlatformCheck(platform = process.platform, linuxPython = platf
12059
12170
  var VERIFY_COMMAND = /\btamperward(?:@\S+)?\s+verify\b/;
12060
12171
  function err2(message, opts, id = "doctor", checks = []) {
12061
12172
  if (opts?.json) {
12062
- const report2 = {
12173
+ const report2 = machineOutput({
12063
12174
  command: "doctor",
12064
12175
  authoritative: false,
12065
12176
  checks: [
12066
12177
  ...checks.filter((check2) => check2.id !== id),
12067
12178
  { id, state: "BROKEN", detail: message }
12068
12179
  ]
12069
- };
12180
+ });
12070
12181
  process.stdout.write(JSON.stringify(report2) + "\n");
12071
12182
  } else {
12072
12183
  process.stderr.write(`tamperward doctor: ${message}
@@ -12279,7 +12390,7 @@ function observerCheck(cwd) {
12279
12390
  function emitReport(opts, checks) {
12280
12391
  const authoritative = !checks.some((x) => x.state === "BROKEN");
12281
12392
  if (opts.json) {
12282
- const report2 = { command: "doctor", authoritative, checks };
12393
+ const report2 = machineOutput({ command: "doctor", authoritative, checks });
12283
12394
  process.stdout.write(JSON.stringify(report2) + "\n");
12284
12395
  return;
12285
12396
  }
@@ -12627,6 +12738,384 @@ function runDoctor(opts = {}) {
12627
12738
  return 0;
12628
12739
  }
12629
12740
 
12741
+ // src/cli/trace-verify.ts
12742
+ import { execFileSync as execFileSync11, spawnSync as spawnSync5 } from "node:child_process";
12743
+ import {
12744
+ existsSync as existsSync13,
12745
+ mkdtempSync as mkdtempSync5,
12746
+ readFileSync as readFileSync18,
12747
+ readdirSync as readdirSync6,
12748
+ realpathSync as realpathSync7,
12749
+ rmSync as rmSync6,
12750
+ symlinkSync as symlinkSync2
12751
+ } from "node:fs";
12752
+ import { tmpdir as tmpdir5 } from "node:os";
12753
+ import { isAbsolute as isAbsolute11, join as join19, relative as relative6, resolve as resolve9, sep as sep3 } from "node:path";
12754
+ var TRACE_ROOT = "/__tamperward_trusted_base__";
12755
+ function unescapeStraceString(s) {
12756
+ return s.replace(/\\([0-7]{1,3})/g, (_m, oct) => String.fromCharCode(parseInt(oct, 8))).replace(/\\x([0-9a-fA-F]{2})/g, (_m, hex) => String.fromCharCode(parseInt(hex, 16))).replace(/\\n/g, "\n").replace(/\\r/g, "\r").replace(/\\t/g, " ").replace(/\\"/g, '"').replace(/\\\\/g, "\\");
12757
+ }
12758
+ function parseStraceFileAccess(raw) {
12759
+ const out2 = [];
12760
+ const seen = /* @__PURE__ */ new Set();
12761
+ for (const line of raw.split("\n")) {
12762
+ if (!line.trim() || /\)\s+=\s+-1\b/.test(line)) continue;
12763
+ const call = line.match(/\b(execveat|execve|openat2|openat|open|newfstatat|fstatat|statx|lstat|stat|access|readlinkat|readlink)\s*\(/);
12764
+ if (!call) continue;
12765
+ const afterCall = line.slice((call.index ?? 0) + call[0].length);
12766
+ const quoted = afterCall.match(/"((?:\\.|[^"])*)"/);
12767
+ if (!quoted) continue;
12768
+ const path = unescapeStraceString(quoted[1]);
12769
+ if (!path) continue;
12770
+ const access = call[1].startsWith("execve") ? "exec" : "read";
12771
+ const key2 = `${access}\0${path}`;
12772
+ if (seen.has(key2)) continue;
12773
+ seen.add(key2);
12774
+ out2.push({ path, access });
12775
+ }
12776
+ return out2;
12777
+ }
12778
+ function inside3(root, path) {
12779
+ const rel = relative6(resolve9(root), resolve9(path));
12780
+ return rel === "" || rel !== ".." && !rel.startsWith(`..${sep3}`) && !isAbsolute11(rel);
12781
+ }
12782
+ function normalRepoPath(root, raw) {
12783
+ const abs2 = isAbsolute11(raw) ? resolve9(raw) : resolve9(root, raw);
12784
+ if (!inside3(root, abs2)) return null;
12785
+ const rel = relative6(root, abs2).split(sep3).join("/");
12786
+ return rel && rel !== ".." ? rel : null;
12787
+ }
12788
+ var CONFIG_BASENAMES = /* @__PURE__ */ new Set([
12789
+ "package.json",
12790
+ "pytest.ini",
12791
+ ".pytest.ini",
12792
+ "setup.cfg",
12793
+ "setup.py",
12794
+ "tox.ini",
12795
+ "pyproject.toml",
12796
+ ".npmrc",
12797
+ ".yarnrc",
12798
+ ".yarnrc.yml",
12799
+ "pnpm-workspace.yaml",
12800
+ ".pnpmfile.cjs",
12801
+ ".pnp.cjs",
12802
+ ".pnp.js",
12803
+ ".pnp.loader.mjs",
12804
+ ".nvmrc",
12805
+ ".node-version",
12806
+ ".python-version",
12807
+ ".gitattributes",
12808
+ ".rspec",
12809
+ "phpunit.xml",
12810
+ "phpunit.xml.dist"
12811
+ ]);
12812
+ function likelyConfig(path) {
12813
+ const base = path.split("/").at(-1) ?? path;
12814
+ return CONFIG_BASENAMES.has(base) || /(?:^|\/)(?:config|configs)\//.test(path) || /^\.env(?:\.|$)/.test(base) || /^(?:jest|vitest|playwright|cypress|vite|babel|karma|ava)\.config\./.test(base) || /^tsconfig(?:\..+)?\.json$/.test(base) || /^\.?(?:babelrc|swcrc|nycrc|c8rc)/.test(base) || /(?:^|\/)conftest\.py$/.test(path);
12815
+ }
12816
+ function sortAccesses(values) {
12817
+ return [...values].sort((a, b) => a.localeCompare(b));
12818
+ }
12819
+ function summarizeTraceRuns(opts) {
12820
+ const repo = /* @__PURE__ */ new Map();
12821
+ const external = /* @__PURE__ */ new Map();
12822
+ opts.runs.forEach((run, index) => {
12823
+ const perRun = /* @__PURE__ */ new Set();
12824
+ for (const item of run) {
12825
+ const rel = normalRepoPath(opts.root, item.path);
12826
+ const isTracked = rel !== null && opts.tracked.has(rel);
12827
+ const keyPath = isTracked ? rel : rel === null ? resolve9(opts.root, item.path) : null;
12828
+ if (keyPath === null) continue;
12829
+ const target = isTracked ? repo : external;
12830
+ const key2 = `${isTracked ? "repo" : "external"}\0${keyPath}`;
12831
+ let acc2 = target.get(keyPath);
12832
+ if (!acc2) {
12833
+ acc2 = { accesses: /* @__PURE__ */ new Set(), runs: /* @__PURE__ */ new Set() };
12834
+ target.set(keyPath, acc2);
12835
+ }
12836
+ acc2.accesses.add(item.access);
12837
+ if (!perRun.has(key2)) {
12838
+ acc2.runs.add(index);
12839
+ perRun.add(key2);
12840
+ }
12841
+ }
12842
+ });
12843
+ const totalRuns = opts.runs.length;
12844
+ const repository_inputs = [...repo.entries()].map(([path, acc2]) => ({
12845
+ path,
12846
+ accesses: sortAccesses(acc2.accesses),
12847
+ config: likelyConfig(path),
12848
+ covered: opts.covered.has(path),
12849
+ observed_runs: acc2.runs.size,
12850
+ dynamic: acc2.runs.size < totalRuns
12851
+ })).sort((a, b) => a.path.localeCompare(b.path));
12852
+ const external_inputs = [...external.entries()].map(([path, acc2]) => ({
12853
+ path,
12854
+ accesses: sortAccesses(acc2.accesses),
12855
+ observed_runs: acc2.runs.size,
12856
+ dynamic: acc2.runs.size < totalRuns
12857
+ })).sort((a, b) => a.path.localeCompare(b.path));
12858
+ const uncovered_repository_inputs = repository_inputs.filter((x) => !x.covered).map((x) => x.path);
12859
+ return {
12860
+ repository_inputs,
12861
+ external_inputs,
12862
+ uncovered_repository_inputs,
12863
+ // Exact paths are intentionally conservative. A maintainer may widen them to
12864
+ // reviewed globs, but TamperWard never invents a broader trust surface.
12865
+ suggested_verify_inputs: [...uncovered_repository_inputs]
12866
+ };
12867
+ }
12868
+ function git4(args, cwd) {
12869
+ return execFileSync11("git", args, {
12870
+ cwd,
12871
+ encoding: "utf8",
12872
+ maxBuffer: 256 * 1024 * 1024,
12873
+ env: trustedGitEnv(),
12874
+ stdio: ["ignore", "pipe", "pipe"]
12875
+ });
12876
+ }
12877
+ function commandExists(command) {
12878
+ const r = spawnSync5(command, ["--version"], { stdio: "ignore" });
12879
+ return !r.error && r.status === 0;
12880
+ }
12881
+ function trackedAt(base, cwd) {
12882
+ return git4(["ls-tree", "-r", "-z", "--name-only", base], cwd).split("\0").filter(Boolean);
12883
+ }
12884
+ function materializeBase(base, cwd, dest) {
12885
+ const archive = spawnSync5("git", ["archive", "--format=tar", base], {
12886
+ cwd,
12887
+ env: trustedGitEnv(),
12888
+ maxBuffer: 256 * 1024 * 1024
12889
+ });
12890
+ if (archive.error || archive.status !== 0 || !archive.stdout) {
12891
+ throw new Error(
12892
+ `git archive failed${archive.stderr?.length ? `: ${String(archive.stderr).trim()}` : ""}`
12893
+ );
12894
+ }
12895
+ const untar = spawnSync5("tar", ["-xf", "-", "-C", dest], {
12896
+ input: archive.stdout,
12897
+ maxBuffer: 256 * 1024 * 1024
12898
+ });
12899
+ if (untar.error || untar.status !== 0) {
12900
+ throw new Error(
12901
+ `tar extraction failed${untar.stderr?.length ? `: ${String(untar.stderr).trim()}` : ""}`
12902
+ );
12903
+ }
12904
+ const hostModules = join19(cwd, "node_modules");
12905
+ const tracedModules = join19(dest, "node_modules");
12906
+ if (existsSync13(hostModules) && !existsSync13(tracedModules)) {
12907
+ symlinkSync2(resolve9(hostModules), tracedModules, "dir");
12908
+ }
12909
+ }
12910
+ function traceFiles(prefix) {
12911
+ const dir = resolve9(prefix, "..");
12912
+ const base = prefix.split(sep3).at(-1);
12913
+ return readdirSync6(dir).filter((name) => name === base || name.startsWith(base + ".")).map((name) => join19(dir, name)).sort();
12914
+ }
12915
+ function rewriteTraceRoot(accesses, actualRoot) {
12916
+ return accesses.map((item) => {
12917
+ const abs2 = isAbsolute11(item.path) ? resolve9(item.path) : resolve9(actualRoot, item.path);
12918
+ if (!inside3(actualRoot, abs2)) return { ...item, path: abs2 };
12919
+ const rel = relative6(actualRoot, abs2).split(sep3).join("/");
12920
+ try {
12921
+ const real = resolve9(realpathSync7(abs2));
12922
+ if (!inside3(actualRoot, real)) return { ...item, path: real };
12923
+ } catch {
12924
+ }
12925
+ return { ...item, path: join19(TRACE_ROOT, ...rel.split("/")) };
12926
+ });
12927
+ }
12928
+ function runOneTrace(cwd, base, command, budget) {
12929
+ const root = mkdtempSync5(join19(tmpdir5(), "tw-trace-base-"));
12930
+ const traceDir = mkdtempSync5(join19(tmpdir5(), "tw-trace-log-"));
12931
+ try {
12932
+ materializeBase(base, cwd, root);
12933
+ const prefix = join19(traceDir, "trace");
12934
+ const traced = spawnSync5(
12935
+ "strace",
12936
+ [
12937
+ "-ff",
12938
+ "-qq",
12939
+ "-s",
12940
+ "4096",
12941
+ "-e",
12942
+ "trace=%file",
12943
+ "-o",
12944
+ prefix,
12945
+ "--",
12946
+ "timeout",
12947
+ "--signal=TERM",
12948
+ "--kill-after=2s",
12949
+ `${budget}s`,
12950
+ "sh",
12951
+ "-c",
12952
+ command
12953
+ ],
12954
+ {
12955
+ cwd: root,
12956
+ env: process.env,
12957
+ encoding: "utf8",
12958
+ maxBuffer: 64 * 1024 * 1024
12959
+ }
12960
+ );
12961
+ if (traced.error) throw traced.error;
12962
+ const logs = traceFiles(prefix);
12963
+ if (logs.length === 0) {
12964
+ throw new Error("strace produced no trace files");
12965
+ }
12966
+ const accesses = logs.flatMap((path) => parseStraceFileAccess(readFileSync18(path, "utf8")));
12967
+ return {
12968
+ exit: traced.status ?? 1,
12969
+ accesses: rewriteTraceRoot(accesses, root)
12970
+ };
12971
+ } finally {
12972
+ rmSync6(root, { recursive: true, force: true });
12973
+ rmSync6(traceDir, { recursive: true, force: true });
12974
+ }
12975
+ }
12976
+ function renderText2(report2) {
12977
+ const out2 = (s = "") => void process.stdout.write(s + "\n");
12978
+ out2("tamperward trace-verify \u2014 ADVISORY observation, not an enforcement verdict");
12979
+ out2(`trusted base: ${report2.base}`);
12980
+ out2(`command: ${report2.command}`);
12981
+ out2(`runs: ${report2.runs_completed}/${report2.runs_requested}; exits: ${report2.run_exits.join(", ")}`);
12982
+ out2();
12983
+ out2("repository inputs observed:");
12984
+ if (!report2.repository_inputs.length) out2(" (none)");
12985
+ for (const item of report2.repository_inputs) {
12986
+ const flags = [
12987
+ item.covered ? "covered" : "UNCOVERED",
12988
+ item.config ? "config" : null,
12989
+ item.dynamic ? `dynamic ${item.observed_runs}/${report2.runs_completed}` : null,
12990
+ item.accesses.join("+")
12991
+ ].filter(Boolean).join(", ");
12992
+ out2(` ${item.path} [${flags}]`);
12993
+ }
12994
+ out2();
12995
+ out2("external dependency/runtime paths observed:");
12996
+ if (!report2.external_inputs.length) out2(" (none)");
12997
+ for (const item of report2.external_inputs) {
12998
+ out2(
12999
+ ` ${item.path} [${item.accesses.join("+")}${item.dynamic ? `, dynamic ${item.observed_runs}/${report2.runs_completed}` : ""}]`
13000
+ );
13001
+ }
13002
+ out2();
13003
+ out2("candidate verify.inputs entries for HUMAN REVIEW:");
13004
+ if (!report2.suggested_verify_inputs.length) out2(" (none \u2014 all observed tracked inputs are already covered)");
13005
+ for (const path of report2.suggested_verify_inputs) out2(` - ${JSON.stringify(path)}`);
13006
+ out2();
13007
+ for (const note of report2.notes) out2(`note: ${note}`);
13008
+ }
13009
+ function runTraceVerify(opts = {}) {
13010
+ const cwd = resolve9(opts.cwd ?? process.cwd());
13011
+ if (process.platform !== "linux") {
13012
+ process.stderr.write("tamperward trace-verify: Linux only in this release; unsupported platform \u2014 no parity is implied.\n");
13013
+ return 2;
13014
+ }
13015
+ if (!commandExists("strace")) {
13016
+ process.stderr.write("tamperward trace-verify: strace is required on Linux but was not found.\n");
13017
+ return 2;
13018
+ }
13019
+ if (!commandExists("tar")) {
13020
+ process.stderr.write("tamperward trace-verify: tar is required to materialize the trusted base but was not found.\n");
13021
+ return 2;
13022
+ }
13023
+ if (!commandExists("timeout")) {
13024
+ process.stderr.write("tamperward trace-verify: GNU timeout is required to bound traced verifier runs but was not found.\n");
13025
+ return 2;
13026
+ }
13027
+ const runs = opts.runs ?? 2;
13028
+ if (!Number.isInteger(runs) || runs <= 0) {
13029
+ process.stderr.write(`tamperward trace-verify: --runs needs a positive integer (got ${JSON.stringify(opts.runs)}).
13030
+ `);
13031
+ return 2;
13032
+ }
13033
+ const requestedBase = opts.base ?? "HEAD";
13034
+ let base;
13035
+ try {
13036
+ const baseArg = assertRev(requestedBase);
13037
+ base = git4(["rev-parse", "--verify", `${baseArg}^{commit}`], cwd).trim();
13038
+ } catch (e) {
13039
+ process.stderr.write(`tamperward trace-verify: cannot resolve trusted base ${JSON.stringify(requestedBase)} (${e instanceof Error ? e.message : String(e)}).
13040
+ `);
13041
+ return 2;
13042
+ }
13043
+ let policy;
13044
+ try {
13045
+ policy = loadPolicyAt(base, cwd) ?? defaultPolicy();
13046
+ } catch (e) {
13047
+ process.stderr.write(`tamperward trace-verify: cannot load policy at trusted base (${e instanceof Error ? e.message : String(e)}).
13048
+ `);
13049
+ return 2;
13050
+ }
13051
+ const command = opts.cmd ?? policy.verify?.command;
13052
+ if (!command) {
13053
+ process.stderr.write("tamperward trace-verify: no verifier command; set verify.command at the base or pass --cmd.\n");
13054
+ return 2;
13055
+ }
13056
+ const budget = opts.budget ?? policy.verify?.budget ?? 300;
13057
+ if (!Number.isFinite(budget) || budget <= 0) {
13058
+ process.stderr.write(`tamperward trace-verify: --budget needs a positive number of seconds (got ${JSON.stringify(budget)}).
13059
+ `);
13060
+ return 2;
13061
+ }
13062
+ const atBase = trackedAt(base, cwd);
13063
+ const tracked = new Set(atBase);
13064
+ const covered = verifierCoveredInputs(command, atBase, policy);
13065
+ const observed = [];
13066
+ const runExits = [];
13067
+ try {
13068
+ for (let i = 0; i < runs; i++) {
13069
+ const result = runOneTrace(cwd, base, command, budget);
13070
+ observed.push(result.accesses);
13071
+ runExits.push(result.exit);
13072
+ }
13073
+ } catch (e) {
13074
+ process.stderr.write(`tamperward trace-verify: tracing failed (${e instanceof Error ? e.message : String(e)}).
13075
+ `);
13076
+ return 2;
13077
+ }
13078
+ const summary = summarizeTraceRuns({
13079
+ root: TRACE_ROOT,
13080
+ runs: observed,
13081
+ tracked,
13082
+ covered
13083
+ });
13084
+ const report2 = {
13085
+ advisory: true,
13086
+ platform: process.platform,
13087
+ base,
13088
+ command,
13089
+ runs_requested: runs,
13090
+ runs_completed: observed.length,
13091
+ run_exits: runExits,
13092
+ trace_complete: runExits.every((code2) => code2 === 0),
13093
+ ...summary,
13094
+ notes: [
13095
+ "Observed reads are evidence from these runs, not proof that an unobserved path can never be read.",
13096
+ "Dynamic paths are those observed in fewer than all repeated traces.",
13097
+ "Suggestions are exact tracked paths only; TamperWard never edits .tamperward.yml or widens a glob automatically.",
13098
+ "Review every suggestion before adding it to verify.inputs."
13099
+ ]
13100
+ };
13101
+ if (opts.json) process.stdout.write(JSON.stringify(report2) + "\n");
13102
+ else renderText2(report2);
13103
+ return report2.trace_complete ? 0 : 1;
13104
+ }
13105
+ function parseTraceVerify(args) {
13106
+ const out2 = {};
13107
+ for (let i = 0; i < args.length; i++) {
13108
+ const a = args[i];
13109
+ if (a === "--json") out2.json = true;
13110
+ else if (a === "--base") out2.base = args[++i];
13111
+ else if (a === "--cmd") out2.cmd = args[++i];
13112
+ else if (a === "--cwd") out2.cwd = args[++i];
13113
+ else if (a === "--budget") out2.budget = Number(args[++i]);
13114
+ else if (a === "--runs") out2.runs = Number(args[++i]);
13115
+ }
13116
+ return out2;
13117
+ }
13118
+
12630
13119
  // src/cli/main.ts
12631
13120
  function parseAllow(args) {
12632
13121
  const o = {};
@@ -12715,6 +13204,11 @@ function validateFlatArgs(args, grammar) {
12715
13204
  if (!Number.isFinite(n) || n <= 0) {
12716
13205
  return { error: `${a} needs a positive number (got "${v}")`, seen, positionals };
12717
13206
  }
13207
+ } else if (rule === "positive-integer") {
13208
+ const n = Number(v);
13209
+ if (!Number.isFinite(n) || !Number.isInteger(n) || n <= 0) {
13210
+ return { error: `${a} needs a positive integer (got "${v}")`, seen, positionals };
13211
+ }
12718
13212
  } else if (rule === "non-negative") {
12719
13213
  const n = Number(v);
12720
13214
  if (!Number.isFinite(n) || n < 0) {
@@ -12752,7 +13246,7 @@ function validateCliArgs(cmd, args) {
12752
13246
  const prefix = args.slice(0, delimiter3);
12753
13247
  const command = args.slice(delimiter3 + 1);
12754
13248
  const parsed = validateFlatArgs(prefix, {
12755
- flags: ["--allow-dirty", "--allow-dep-drift", "--observe-transients"],
13249
+ flags: ["--allow-dirty", "--allow-dep-drift", "--observe-transients", "--json"],
12756
13250
  values: {
12757
13251
  "--base": "string",
12758
13252
  "--cmd": "string",
@@ -12817,6 +13311,18 @@ function validateCliArgs(cmd, args) {
12817
13311
  }
12818
13312
  }).error;
12819
13313
  }
13314
+ if (cmd === "trace-verify") {
13315
+ return validateFlatArgs(args, {
13316
+ flags: ["--json"],
13317
+ values: {
13318
+ "--base": "string",
13319
+ "--cmd": "string",
13320
+ "--cwd": "string",
13321
+ "--budget": "positive",
13322
+ "--runs": "positive-integer"
13323
+ }
13324
+ }).error;
13325
+ }
12820
13326
  if (cmd === "watch") {
12821
13327
  return validateFlatArgs(args, {
12822
13328
  values: { "--dir": "string", "--log": "string", "--base": "string" }
@@ -12841,7 +13347,8 @@ Formats:
12841
13347
  github GitHub Actions view: one inline annotation per finding (so it shows up
12842
13348
  on the line in "Files changed") plus a job-summary table on the run
12843
13349
  page, alongside the same text output in the log.
12844
- 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/.
12845
13352
  auto github when GITHUB_ACTIONS=true, otherwise text.
12846
13353
  tamperward hook claude PreToolUse gate (reads hook JSON on stdin)
12847
13354
  tamperward sweep claude Stop sweep (re-scan the turn's working tree)
@@ -12857,16 +13364,26 @@ Formats:
12857
13364
  (exit 1, or 0 under an out-of-band
12858
13365
  verify@<head-sha> approval); cannot-verify
12859
13366
  fails closed (2)
13367
+ tamperward trace-verify [--base R] advisory Linux verifier-input discovery:
13368
+ [--cmd C] [--budget S] [--runs N] trace a trusted/known-good base with
13369
+ [--json] [--cwd D] strace, union repeated observations,
13370
+ mark dynamic/config/external reads,
13371
+ and suggest uncovered exact paths for
13372
+ HUMAN review as verify.inputs. Never
13373
+ edits policy; unsupported platforms
13374
+ fail explicitly rather than claiming parity.
12860
13375
  tamperward run [opts] -- <agent cmd...> enforcement envelope: record the trusted
12861
13376
  [--base R] [--cmd C] base, run the agent, treat its exit as
12862
13377
  [--budget S] [--agent-budget S] untrusted, then re-adjudicate the tree it
12863
13378
  [--allow-dirty] [--settle S] left (policy over base...HEAD and the
12864
13379
  [--allow-dep-drift] [--cwd D] worktree, plus verify).
12865
- [--observe-transients] --observe-transients starts a session-
13380
+ [--observe-transients] [--json] --observe-transients starts a session-
12866
13381
  scoped observer before the agent and
12867
13382
  reports temporal evidence/health after it.
12868
13383
  --agent-budget bounds the agent runtime
12869
13384
  itself and still adjudicates after timeout.
13385
+ --json emits one final versioned run verdict
13386
+ document on stdout after agent adjudication.
12870
13387
  Exit: agent's
12871
13388
  code when clean; 124 on clean AGENT_TIMEOUT;
12872
13389
  1 on any blocking finding/masked failure;
@@ -12930,6 +13447,8 @@ function main(argv2) {
12930
13447
  return runWatch(rest);
12931
13448
  case "verify":
12932
13449
  return runVerify(parseVerify(rest));
13450
+ case "trace-verify":
13451
+ return runTraceVerify(parseTraceVerify(rest));
12933
13452
  case "run":
12934
13453
  return runEnvelope({
12935
13454
  ...parseRun(rest),