tamperward 2.11.3 → 2.13.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.
Files changed (3) hide show
  1. package/README.md +32 -7
  2. package/dist/cli/index.js +503 -138
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -199,6 +199,16 @@ are involved here, and they are not the same thing.
199
199
  its policy, verification surface and verifier command and budget from that same
200
200
  trusted base — so neither step's verdict is governed by the candidate.
201
201
 
202
+ Since **2.12.0**, the generated job uses GitHub Actions' 360-minute outer timeout and
203
+ runs `tamperward doctor` before verification. The doctor reads the **trusted base**
204
+ policy, finds the job(s) that actually invoke `tamperward verify`, and requires enough
205
+ static outer time for both independently budgeted stages plus a 60-minute authority
206
+ reserve. With the generated 360-minute job that means budgets up to 9,000 seconds per
207
+ stage fit. Larger positive finite policy budgets remain valid — there is no schema cap —
208
+ but this generated GitHub-hosted authority refuses deterministically and tells you how
209
+ much outer time is required, so a custom runner/workflow can provide it instead of
210
+ GitHub killing verification mid-verdict.
211
+
202
212
  Legitimate exceptions are out-of-band PR labels, `tamperward:allow:<rule>@<head-sha>`.
203
213
  The workflow passes the head SHA to both steps through `TAMPERWARD_OOB_HEAD`, so an
204
214
  approval is bound to the exact commit it was granted for and a new push invalidates it.
@@ -244,13 +254,26 @@ CI workflow that runs both the diff-time check and pristine verification, and a
244
254
  `CODEOWNERS` requirement on the paths that decide whether the gate runs at all.
245
255
  It never overwrites anything you wrote; `--dry-run` prints the plan.
246
256
 
247
- **`init` is not sufficient on its own, and it will tell you so.** In your
248
- branch-protection or ruleset settings you must require the check *and* enable
249
- "Require review from Code Owners". A `pull_request` workflow runs from the pull
250
- request's own head and a required check is matched by job name, so without that
251
- a PR can keep the job name, replace the gate with `true`, and present a green
252
- required check over a change the gate would have blocked. That is reproduced on
253
- this project's own CI, not a theoretical concern.
257
+ **`init` is not sufficient on its own, and it will tell you so.** The protected
258
+ branch needs all three repository-authority controls:
259
+
260
+ 1. require the **`tamperward`** status check;
261
+ 2. enable **Require review from Code Owners**; and
262
+ 3. enable **Dismiss stale pull request approvals when new commits are pushed**.
263
+
264
+ The third control is load-bearing: an approval for an older gate-critical diff must
265
+ not authorize a later push. GitHub's "require approval of the most recent reviewable
266
+ push" can be useful in addition, but it is not equivalent here because that fresh
267
+ approver is not necessarily the Code Owner for the gate path. A `pull_request`
268
+ workflow runs from the pull request's own head and a required check is matched by job
269
+ name, so without this human boundary a PR can keep the job name, replace the gate with
270
+ `true`, and present a green required check over a change the gate would have blocked.
271
+ That is reproduced on this project's own CI, not a theoretical concern.
272
+
273
+ After configuring GitHub, verify the boundary with
274
+ `tamperward doctor --github --repo OWNER/REPO --branch <default-branch>`.
275
+ Public rulesets can be read anonymously; set `GH_TOKEN` or `GITHUB_TOKEN` when
276
+ authentication is required.
254
277
 
255
278
  A real deployment needs a verify command configured — the generated CI verify step
256
279
  **fails closed (exit 2) without one** rather than passing quietly. In
@@ -321,6 +344,7 @@ commands ignore what they do not know.
321
344
  | --- | --- |
322
345
  | `check` | one view — `--staged` · `--worktree` · `--diff <base>...<head>` — plus `--format text\|json\|github\|auto` (default `auto`) · `--json` (alias for `--format json`) · `--cwd <dir>` |
323
346
  | `verify` | `--base <rev>` (default `HEAD`) · `--cmd <suite command>` · `--budget <seconds>` · `--json` · `--keep` (keep the two materialised copies and report their paths) · `--require-ancestor` (refuse a base that is not an ancestor of `HEAD`) · `--cwd <dir>` |
347
+ | `doctor` | `--base <rev>` (trusted policy revision) · `--workflow <path>` (default `.github/workflows/tamperward.yml`) · `--cwd <dir>` — validates every job that runs `tamperward verify` has a static outer timeout covering visible + pristine + authority reserve |
324
348
  | `run` | `--base <rev>` · `--cmd <suite command>` · `--budget <seconds>` (per verifier suite) · `--agent-budget <seconds>` (optional wrapped-agent wall clock) · `--allow-dirty` · `--settle <seconds>` (wait before the final quiescence check) · `--allow-dep-drift` · `--cwd <dir>` · then `-- <agent command...>` |
325
349
  | `allow` | `<rule>` · `--file <path>` · `--reason "<why>"` (required) · `--cwd <dir>` |
326
350
  | `init` | `--cwd <dir>` · `--dry-run` · `--force-workflow` |
@@ -333,6 +357,7 @@ commands ignore what they do not know.
333
357
  | --- | --- | --- | --- | --- |
334
358
  | `check` | no blocking finding | at least one blocking finding | cannot evaluate: policy parse error, malformed `--diff` range, no view given, not a git repository, or an unresolvable revision — any failure the gate cannot recover from is one clean `tamperward: …` line on stderr at exit 2, never a stack trace at exit 1 | — |
335
359
  | `verify` | `VERIFIED` — visible and pristine both green; or a `MASKED_FAILURE` cleared by an out-of-band `verify@<head-sha>` approval | `MASKED_FAILURE` (visible green, pristine red) or `SUITE_RED` | cannot verify, failing closed: no suite command, unresolvable base, `--require-ancestor` refused, budget exceeded, or the working or dependency tree moved during the run | — |
360
+ | `doctor` | configured verify job(s) have sufficient static outer time for the trusted policy | — | missing/invalid workflow, no verify job, missing/malformed/insufficient timeout, or trusted policy cannot be loaded | — |
336
361
  | `run` | enforcement clean and the agent exited 0 — another non-zero agent exit is passed through unchanged | any blocking finding or masked failure, including a non-quiescent process after timeout | cannot adjudicate: dirty start, policy error, verify cannot run | `AGENT_TIMEOUT`: `--agent-budget` expired and post-timeout enforcement was clean |
337
362
  | `hook claude` / `sweep claude` | always — a deny is JSON on stdout at exit 0, never exit 2 | — | only for an unsupported agent name | — |
338
363
  | `allow` | sign-off recorded | — | no rule or `--reason`, not a git repo, or no current blocking finding to sign off | — |
package/dist/cli/index.js CHANGED
@@ -674,9 +674,9 @@ function git(args, cwd) {
674
674
  env: trustedGitEnv()
675
675
  });
676
676
  } catch (e) {
677
- const err2 = e;
678
- const detail = String(err2.stderr ?? "").trim();
679
- throw new Error(detail ? `git ${args[0]}: ${detail}` : err2.message ?? `git ${args[0]} failed`);
677
+ const err3 = e;
678
+ const detail = String(err3.stderr ?? "").trim();
679
+ throw new Error(detail ? `git ${args[0]}: ${detail}` : err3.message ?? `git ${args[0]} failed`);
680
680
  }
681
681
  }
682
682
  function blobAt(rev, path, cwd) {
@@ -864,11 +864,11 @@ function synthFileChange(displayPath, before, after) {
864
864
  maxBuffer: 256 * 1024 * 1024
865
865
  });
866
866
  } catch (e) {
867
- const err2 = e;
868
- if (err2.status !== 1) {
869
- throw new Error(`cannot diff the incoming edit to ${displayPath}: ${err2.code ?? err2.message ?? String(e)}`);
867
+ const err3 = e;
868
+ if (err3.status !== 1) {
869
+ throw new Error(`cannot diff the incoming edit to ${displayPath}: ${err3.code ?? err3.message ?? String(e)}`);
870
870
  }
871
- raw = err2.stdout ? String(err2.stdout) : "";
871
+ raw = err3.stdout ? String(err3.stdout) : "";
872
872
  }
873
873
  } finally {
874
874
  rmSync(dir, { recursive: true, force: true });
@@ -1585,7 +1585,7 @@ function branchExists(name, ctx) {
1585
1585
  }
1586
1586
  function revIsHead(rev, ctx) {
1587
1587
  if (!ctx?.cwd || rev.startsWith("-")) return null;
1588
- const parse3 = (r) => {
1588
+ const parse4 = (r) => {
1589
1589
  try {
1590
1590
  return execFileSync4("git", ["rev-parse", "--verify", "--quiet", "--end-of-options", `${r}^{commit}`], {
1591
1591
  cwd: ctx.cwd,
@@ -1596,8 +1596,8 @@ function revIsHead(rev, ctx) {
1596
1596
  return null;
1597
1597
  }
1598
1598
  };
1599
- const head = parse3("HEAD");
1600
- const target = parse3(rev);
1599
+ const head = parse4("HEAD");
1600
+ const target = parse4(rev);
1601
1601
  if (!head || !target) return null;
1602
1602
  return head === target;
1603
1603
  }
@@ -1651,8 +1651,8 @@ function numericOf(e) {
1651
1651
  }
1652
1652
  return void 0;
1653
1653
  }
1654
- function metricsOf(expr, resolve8) {
1655
- const obj = resolve8(expr);
1654
+ function metricsOf(expr, resolve9) {
1655
+ const obj = resolve9(expr);
1656
1656
  if (!ts2.isObjectLiteralExpression(obj)) return void 0;
1657
1657
  const out2 = { values: {}, opaque: false };
1658
1658
  let any = false;
@@ -1666,7 +1666,7 @@ function metricsOf(expr, resolve8) {
1666
1666
  const k = keyName(p.name);
1667
1667
  if (k === null || !isMetric(k)) continue;
1668
1668
  any = true;
1669
- const n = numericOf(resolve8(p.initializer));
1669
+ const n = numericOf(resolve9(p.initializer));
1670
1670
  if (n === void 0) out2.opaque = true;
1671
1671
  else out2.values[k] = n;
1672
1672
  }
@@ -1712,17 +1712,17 @@ function parseThresholds(src) {
1712
1712
  const res = { paths: /* @__PURE__ */ new Map(), present: false, opaque: false };
1713
1713
  try {
1714
1714
  const sf = sourceOf(src);
1715
- const resolve8 = resolverFor(sf);
1715
+ const resolve9 = resolverFor(sf);
1716
1716
  const visit = (node) => {
1717
1717
  if (ts2.isPropertyAssignment(node)) {
1718
1718
  const key2 = keyName(node.name);
1719
1719
  if (key2 === "coverageThreshold" || key2 === "thresholds" && underKey(node, "coverage")) {
1720
1720
  res.present = true;
1721
- const init = resolve8(node.initializer);
1721
+ const init = resolve9(node.initializer);
1722
1722
  if (!ts2.isObjectLiteralExpression(init)) {
1723
1723
  res.opaque = true;
1724
1724
  } else {
1725
- const flat = metricsOf(init, resolve8);
1725
+ const flat = metricsOf(init, resolve9);
1726
1726
  if (flat) res.global = merge(res.global, flat);
1727
1727
  for (const p of init.properties) {
1728
1728
  if (ts2.isSpreadAssignment(p)) {
@@ -1736,7 +1736,7 @@ function parseThresholds(src) {
1736
1736
  res.global = merge(res.global, { values: { branches: 100, functions: 100, lines: 100, statements: 100 }, opaque: false });
1737
1737
  continue;
1738
1738
  }
1739
- const m = metricsOf(p.initializer, resolve8);
1739
+ const m = metricsOf(p.initializer, resolve9);
1740
1740
  if (!m) continue;
1741
1741
  if (k === "global") res.global = merge(res.global, m);
1742
1742
  else res.paths.set(norm(k), m);
@@ -1769,18 +1769,18 @@ function parseLists(src) {
1769
1769
  const res = { collectFrom: null, collectKey: "collectCoverageFrom", ignore: [], opaqueCollect: false, opaqueIgnore: false };
1770
1770
  try {
1771
1771
  const sf = sourceOf(src);
1772
- const resolve8 = resolverFor(sf);
1772
+ const resolve9 = resolverFor(sf);
1773
1773
  const visit = (node) => {
1774
1774
  if (ts2.isPropertyAssignment(node)) {
1775
1775
  const k = keyName(node.name);
1776
1776
  const vitestCoverage = ownerKey(node) === "coverage";
1777
1777
  if (k === "collectCoverageFrom" || k === "include" && vitestCoverage) {
1778
- const { items, opaque } = literals(resolve8(node.initializer));
1778
+ const { items, opaque } = literals(resolve9(node.initializer));
1779
1779
  res.collectFrom = items;
1780
1780
  res.collectKey = k === "include" ? "coverage.include" : k;
1781
1781
  if (opaque) res.opaqueCollect = true;
1782
1782
  } else if (k === "coveragePathIgnorePatterns" || k === "exclude" && vitestCoverage) {
1783
- const { items, opaque } = literals(resolve8(node.initializer));
1783
+ const { items, opaque } = literals(resolve9(node.initializer));
1784
1784
  res.ignore.push(...items);
1785
1785
  if (opaque) res.opaqueIgnore = true;
1786
1786
  }
@@ -7600,6 +7600,36 @@ import { execFileSync as execFileSync6 } from "node:child_process";
7600
7600
  import { createHash as createHash6 } from "node:crypto";
7601
7601
  import { existsSync as existsSync9, mkdirSync as mkdirSync5, readFileSync as readFileSync12, writeFileSync as writeFileSync5, chmodSync, statSync as statSync3 } from "node:fs";
7602
7602
  import { basename as basename2, dirname as dirname4, isAbsolute as isAbsolute8, join as join12, relative as relative3, resolve as resolve3 } from "node:path";
7603
+
7604
+ // src/verifier-limits.ts
7605
+ var VERIFIER_STAGE_COUNT = 2;
7606
+ var GENERATED_CI_TIMEOUT_MINUTES = 360;
7607
+ var GENERATED_CI_MIN_OVERHEAD_RESERVE_SECS = 60 * 60;
7608
+ function assertEnvelopeConstants() {
7609
+ if (!Number.isInteger(VERIFIER_STAGE_COUNT) || VERIFIER_STAGE_COUNT < 1) {
7610
+ throw new Error("TamperWard verifier envelope: stage count must be a positive integer");
7611
+ }
7612
+ if (!Number.isFinite(GENERATED_CI_TIMEOUT_MINUTES) || GENERATED_CI_TIMEOUT_MINUTES <= 0) {
7613
+ throw new Error("TamperWard verifier envelope: generated CI timeout must be positive");
7614
+ }
7615
+ if (!Number.isFinite(GENERATED_CI_MIN_OVERHEAD_RESERVE_SECS) || GENERATED_CI_MIN_OVERHEAD_RESERVE_SECS < 0) {
7616
+ throw new Error("TamperWard verifier envelope: overhead reserve cannot be negative");
7617
+ }
7618
+ if (GENERATED_CI_TIMEOUT_MINUTES * 60 <= GENERATED_CI_MIN_OVERHEAD_RESERVE_SECS) {
7619
+ throw new Error(
7620
+ "TamperWard verifier envelope: generated CI timeout leaves no time for verifier stages"
7621
+ );
7622
+ }
7623
+ }
7624
+ assertEnvelopeConstants();
7625
+ function requiredVerifierAuthoritySeconds(stageBudgetSecs) {
7626
+ if (!Number.isFinite(stageBudgetSecs) || stageBudgetSecs <= 0) {
7627
+ throw new Error("verifier stage budget must be a positive finite number");
7628
+ }
7629
+ return VERIFIER_STAGE_COUNT * stageBudgetSecs + GENERATED_CI_MIN_OVERHEAD_RESERVE_SECS;
7630
+ }
7631
+
7632
+ // src/cli/init.ts
7603
7633
  var errText3 = (e) => (e instanceof Error ? e.message : String(e)).replace(/\s+/g, " ").trim();
7604
7634
  var isPlainObject = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
7605
7635
  function gitConfig(cwd, key2) {
@@ -7690,7 +7720,7 @@ permissions:
7690
7720
  jobs:
7691
7721
  tamperward:
7692
7722
  runs-on: ubuntu-latest
7693
- timeout-minutes: 10
7723
+ timeout-minutes: ${GENERATED_CI_TIMEOUT_MINUTES}
7694
7724
  steps:
7695
7725
  - uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6
7696
7726
  with:
@@ -7708,6 +7738,12 @@ jobs:
7708
7738
  # Candidate code runs later in this job. Do not leave the checkout
7709
7739
  # token in .git/config where a verifier command can read/reuse it.
7710
7740
  persist-credentials: false
7741
+ - name: Tamperward doctor (CI verifier envelope)
7742
+ # Validate the outer GitHub job budget against the TRUSTED base policy
7743
+ # before starting visible/pristine execution. Large policy budgets remain
7744
+ # valid for custom runners; this generated authority refuses if its host
7745
+ # cannot accommodate both stages plus cleanup/reporting reserve.
7746
+ run: tamperward doctor --base "\${{ github.event.pull_request.base.sha }}" --workflow .github/workflows/tamperward.yml
7711
7747
  - name: Resolve out-of-band sign-off from PR labels
7712
7748
  id: oob
7713
7749
  env:
@@ -8185,23 +8221,329 @@ tamperward init: ${changed} change(s) applied. Commit them so the gate travels w
8185
8221
  `
8186
8222
  );
8187
8223
  w.write(
8188
- '\nONE STEP LEFT, AND IT IS NOT OPTIONAL. In your branch-protection or ruleset\nsettings for the default branch, require the tamperward check AND enable\n"Require review from Code Owners".\n\nUntil you do, the CI gate is advisory. A pull request runs the workflow from\nits OWN head, and a required check is matched by job name \u2014 so a pull request\ncan keep the job called `tamperward`, replace the gate with `true`, and present\na green required check over a change the gate would have blocked. That is\nreproduced, not theoretical. CODEOWNERS is what puts a human in front of it;\nbranch protection is what makes CODEOWNERS bind.\n'
8224
+ '\nONE STEP LEFT, AND IT IS NOT OPTIONAL. In your branch-protection or ruleset\nsettings for the default branch, configure ALL THREE repository-authority controls:\n 1. require the tamperward check;\n 2. enable "Require review from Code Owners"; and\n 3. enable "Dismiss stale pull request approvals when new commits are pushed".\n\nThe freshness control is part of the security boundary: an approval for an older\ngate-critical diff must not authorize a later push. "Require approval of the most\nrecent reviewable push" can be useful in addition, but is not a substitute here\nbecause the fresh approver is not necessarily the Code Owner for the gate path.\n\nUntil all three are enforced, the CI gate is advisory. A pull request runs the\nworkflow from its OWN head, and a required check is matched by job name \u2014 so a\npull request can keep the job called `tamperward`, replace the gate with `true`,\nand present a green required check over a change the gate would have blocked.\nCODEOWNERS puts a human in front of gate-critical paths; branch protection or an\nactive ruleset makes that review fresh and binding. Verify with: tamperward doctor\n--github --repo OWNER/REPO --branch <default-branch>.\n'
8225
+ );
8226
+ return 0;
8227
+ }
8228
+
8229
+ // src/cli/doctor.ts
8230
+ import { execFileSync as execFileSync7 } from "node:child_process";
8231
+ import { existsSync as existsSync10, readFileSync as readFileSync13, readdirSync as readdirSync3 } from "node:fs";
8232
+ import { join as join13, resolve as resolve4 } from "node:path";
8233
+ import { parse as parse3 } from "yaml";
8234
+ var VERIFY_COMMAND = /\btamperward(?:@\S+)?\s+verify\b/;
8235
+ function err(message) {
8236
+ process.stderr.write(`tamperward doctor: ${message}
8237
+ `);
8238
+ return 2;
8239
+ }
8240
+ function policyFor(opts, cwd) {
8241
+ if (!opts.base) return loadPolicy(cwd);
8242
+ const atBase = loadPolicyAt(opts.base, cwd);
8243
+ return atBase ?? defaultPolicy();
8244
+ }
8245
+ function asMapping(value) {
8246
+ return typeof value === "object" && value !== null && !Array.isArray(value) ? value : null;
8247
+ }
8248
+ function evaluateGitHubProtection(snapshot2, requiredCheck = "tamperward") {
8249
+ let hasRequiredCheck = false;
8250
+ let hasCodeOwnerReview = false;
8251
+ let dismissesStaleReviews = false;
8252
+ if (Array.isArray(snapshot2.rules)) {
8253
+ for (const raw of snapshot2.rules) {
8254
+ const rule = asMapping(raw);
8255
+ const params = asMapping(rule?.parameters);
8256
+ if (!rule || !params) continue;
8257
+ if (rule.type === "pull_request") {
8258
+ if (params.require_code_owner_review === true) hasCodeOwnerReview = true;
8259
+ if (params.dismiss_stale_reviews_on_push === true) dismissesStaleReviews = true;
8260
+ }
8261
+ if (rule.type === "required_status_checks") {
8262
+ const checks = params.required_status_checks;
8263
+ if (Array.isArray(checks)) {
8264
+ hasRequiredCheck ||= checks.some((rawCheck) => {
8265
+ const check2 = asMapping(rawCheck);
8266
+ return check2?.context === requiredCheck;
8267
+ });
8268
+ }
8269
+ }
8270
+ }
8271
+ }
8272
+ const classic = asMapping(snapshot2.branchProtection);
8273
+ const reviews = asMapping(classic?.required_pull_request_reviews);
8274
+ if (reviews?.require_code_owner_reviews === true) hasCodeOwnerReview = true;
8275
+ if (reviews?.dismiss_stale_reviews === true) dismissesStaleReviews = true;
8276
+ const status = asMapping(classic?.required_status_checks);
8277
+ if (status) {
8278
+ const contexts = status.contexts;
8279
+ if (Array.isArray(contexts)) {
8280
+ hasRequiredCheck ||= contexts.some((x) => x === requiredCheck);
8281
+ }
8282
+ const checks = status.checks;
8283
+ if (Array.isArray(checks)) {
8284
+ hasRequiredCheck ||= checks.some((rawCheck) => {
8285
+ const check2 = asMapping(rawCheck);
8286
+ return check2?.context === requiredCheck;
8287
+ });
8288
+ }
8289
+ }
8290
+ const findings = [];
8291
+ if (!hasRequiredCheck) findings.push("require the tamperward status check");
8292
+ if (!hasCodeOwnerReview) findings.push("require Code Owner review");
8293
+ if (!dismissesStaleReviews) findings.push("dismiss stale pull request approvals on new pushes");
8294
+ return findings;
8295
+ }
8296
+ function gitText(cwd, args) {
8297
+ try {
8298
+ return execFileSync7("git", args, {
8299
+ cwd,
8300
+ encoding: "utf8",
8301
+ stdio: ["ignore", "pipe", "ignore"]
8302
+ }).trim();
8303
+ } catch {
8304
+ return null;
8305
+ }
8306
+ }
8307
+ function githubRepoFromRemote(remote) {
8308
+ const m = remote.match(/github\.com(?::|\/)([^/\s]+)\/([^/\s]+?)(?:\.git)?$/i);
8309
+ return m ? m[1] + "/" + m[2].replace(/\.git$/i, "") : null;
8310
+ }
8311
+ function inferGitHubRepo(cwd) {
8312
+ const envRepo = process.env.GITHUB_REPOSITORY;
8313
+ if (envRepo && /^[^/\s]+\/[^/\s]+$/.test(envRepo)) return envRepo;
8314
+ const remote = gitText(cwd, ["config", "--get", "remote.origin.url"]);
8315
+ return remote ? githubRepoFromRemote(remote) : null;
8316
+ }
8317
+ var GITHUB_API_SCRIPT = [
8318
+ "const endpoint = process.argv[1];",
8319
+ "const token = process.env.TAMPERWARD_GITHUB_TOKEN || '';",
8320
+ "const headers = { Accept: 'application/vnd.github+json', 'X-GitHub-Api-Version': '2026-03-10', 'User-Agent': 'tamperward-doctor' };",
8321
+ "if (token) headers.Authorization = 'Bearer ' + token;",
8322
+ "fetch('https://api.github.com/' + endpoint, { headers }).then(async (r) => {",
8323
+ " const body = await r.text();",
8324
+ " if (!r.ok) { process.stderr.write('HTTP ' + r.status + ': ' + body); process.exit(22); }",
8325
+ " process.stdout.write(body);",
8326
+ "}).catch((e) => { process.stderr.write(String(e)); process.exit(23); });"
8327
+ ].join("\n");
8328
+ function githubApiInvocation(endpoint, token) {
8329
+ return {
8330
+ executable: process.execPath,
8331
+ args: ["-e", GITHUB_API_SCRIPT, endpoint],
8332
+ env: {
8333
+ TAMPERWARD_GITHUB_TOKEN: token,
8334
+ LANG: "C",
8335
+ LC_ALL: "C"
8336
+ }
8337
+ };
8338
+ }
8339
+ function githubApi(cwd, endpoint) {
8340
+ const token = process.env.GH_TOKEN ?? process.env.GITHUB_TOKEN ?? "";
8341
+ const invocation = githubApiInvocation(endpoint, token);
8342
+ try {
8343
+ const stdout = execFileSync7(
8344
+ invocation.executable,
8345
+ invocation.args,
8346
+ {
8347
+ cwd,
8348
+ encoding: "utf8",
8349
+ stdio: ["ignore", "pipe", "pipe"],
8350
+ env: invocation.env,
8351
+ timeout: 15e3
8352
+ }
8353
+ );
8354
+ return JSON.parse(stdout);
8355
+ } catch (e) {
8356
+ const x = e;
8357
+ const detail = x.stderr ? String(x.stderr).replace(/\s+/g, " ").trim() : x.message;
8358
+ throw new Error(
8359
+ "GitHub API " + endpoint + " failed: " + (detail || "unknown error")
8360
+ );
8361
+ }
8362
+ }
8363
+ function githubAuthority(opts, cwd) {
8364
+ const repo = opts.repo ?? inferGitHubRepo(cwd);
8365
+ if (!repo || !/^[^/\s]+\/[^/\s]+$/.test(repo)) {
8366
+ throw new Error(
8367
+ "cannot determine GitHub repository; pass --repo OWNER/REPO or configure a github.com origin"
8368
+ );
8369
+ }
8370
+ let branch = opts.branch ?? process.env.GITHUB_BASE_REF ?? "";
8371
+ if (!branch) {
8372
+ const meta2 = asMapping(githubApi(cwd, "repos/" + repo));
8373
+ if (typeof meta2?.default_branch === "string") branch = meta2.default_branch;
8374
+ }
8375
+ if (!branch) throw new Error("cannot determine protected branch; pass --branch BRANCH");
8376
+ let rules;
8377
+ let rulesError = null;
8378
+ try {
8379
+ rules = githubApi(cwd, "repos/" + repo + "/rules/branches/" + encodeURIComponent(branch));
8380
+ } catch (e) {
8381
+ rulesError = e instanceof Error ? e : new Error(String(e));
8382
+ }
8383
+ let findings = evaluateGitHubProtection({ rules });
8384
+ if (findings.length === 0) return { repo, branch, findings };
8385
+ let branchProtection;
8386
+ let classicError = null;
8387
+ try {
8388
+ branchProtection = githubApi(
8389
+ cwd,
8390
+ "repos/" + repo + "/branches/" + encodeURIComponent(branch) + "/protection"
8391
+ );
8392
+ } catch (e) {
8393
+ classicError = e instanceof Error ? e : new Error(String(e));
8394
+ }
8395
+ findings = evaluateGitHubProtection({ rules, branchProtection });
8396
+ if (findings.length > 0 && rules === void 0 && branchProtection === void 0) {
8397
+ throw new Error(
8398
+ "cannot inspect GitHub repository authority. Rules API: " + (rulesError?.message ?? "unavailable") + ". Classic branch protection API: " + (classicError?.message ?? "unavailable") + ". Authenticate gh with metadata read; classic-protection fallback may also need Administration read."
8399
+ );
8400
+ }
8401
+ if (findings.length > 0 && branchProtection === void 0 && classicError) {
8402
+ findings.push(
8403
+ "classic branch protection could not be inspected; if it supplies a missing requirement, use GH_TOKEN/GITHUB_TOKEN with Administration read"
8404
+ );
8405
+ }
8406
+ return { repo, branch, findings };
8407
+ }
8408
+ function verifyJobs(doc) {
8409
+ const root = asMapping(doc);
8410
+ const jobs = asMapping(root?.jobs);
8411
+ if (!jobs) return [];
8412
+ const found = [];
8413
+ for (const [name, raw] of Object.entries(jobs)) {
8414
+ const job = asMapping(raw);
8415
+ if (!job || !Array.isArray(job.steps)) continue;
8416
+ const hasVerify = job.steps.some((step) => {
8417
+ const item = asMapping(step);
8418
+ return typeof item?.run === "string" && VERIFY_COMMAND.test(item.run);
8419
+ });
8420
+ if (hasVerify) found.push({ name, job });
8421
+ }
8422
+ return found;
8423
+ }
8424
+ function runDoctor(opts = {}) {
8425
+ const cwd = resolve4(opts.cwd ?? process.cwd());
8426
+ let policy;
8427
+ try {
8428
+ policy = policyFor(opts, cwd);
8429
+ } catch (e) {
8430
+ return err(e instanceof PolicyError || e instanceof Error ? e.message : String(e));
8431
+ }
8432
+ if (!policy.verify?.command) {
8433
+ return err("trusted policy has no verify.command; generated CI cannot verify this repository");
8434
+ }
8435
+ const workflowRels = [];
8436
+ if (opts.workflow) {
8437
+ workflowRels.push(opts.workflow);
8438
+ } else {
8439
+ const workflowDirRel = ".github/workflows";
8440
+ const workflowDir = resolve4(cwd, workflowDirRel);
8441
+ if (!existsSync10(workflowDir)) {
8442
+ return err(`${workflowDirRel}: workflow directory does not exist`);
8443
+ }
8444
+ let entries;
8445
+ try {
8446
+ entries = readdirSync3(workflowDir).filter((name) => /\.ya?ml$/i.test(name)).sort();
8447
+ } catch (e) {
8448
+ return err(
8449
+ `${workflowDirRel}: could not enumerate workflows (${e instanceof Error ? e.message : String(e)})`
8450
+ );
8451
+ }
8452
+ for (const name of entries) workflowRels.push(join13(workflowDirRel, name));
8453
+ }
8454
+ const requiredSecs = requiredVerifierAuthoritySeconds(policy.verify.budget);
8455
+ const requiredMinutes = Math.ceil(requiredSecs / 60);
8456
+ let verifyJobCount = 0;
8457
+ for (const workflowRel of workflowRels) {
8458
+ const workflowPath = resolve4(cwd, workflowRel);
8459
+ if (!existsSync10(workflowPath)) {
8460
+ return err(`${workflowRel}: workflow does not exist`);
8461
+ }
8462
+ let doc;
8463
+ try {
8464
+ doc = parse3(readFileSync13(workflowPath, "utf8"));
8465
+ } catch (e) {
8466
+ return err(
8467
+ `${workflowRel}: workflow is not valid YAML (${e instanceof Error ? e.message : String(e)})`
8468
+ );
8469
+ }
8470
+ const jobs = verifyJobs(doc);
8471
+ verifyJobCount += jobs.length;
8472
+ for (const { name, job } of jobs) {
8473
+ const timeout = job["timeout-minutes"];
8474
+ if (timeout === void 0 || timeout === null) {
8475
+ return err(
8476
+ `${workflowRel} job "${name}": timeout-minutes is missing; requires at least ${requiredMinutes} minutes`
8477
+ );
8478
+ }
8479
+ if (typeof timeout === "string" && timeout.includes("${{")) {
8480
+ return err(
8481
+ `${workflowRel} job "${name}": timeout-minutes must be a static numeric value; requires at least ${requiredMinutes} minutes`
8482
+ );
8483
+ }
8484
+ if (typeof timeout !== "number" || !Number.isFinite(timeout)) {
8485
+ return err(
8486
+ `${workflowRel} job "${name}": timeout-minutes must be numeric; got ${JSON.stringify(timeout)}`
8487
+ );
8488
+ }
8489
+ if (timeout <= 0) {
8490
+ return err(
8491
+ `${workflowRel} job "${name}": timeout-minutes must be positive; got ${timeout}`
8492
+ );
8493
+ }
8494
+ if (!Number.isInteger(timeout)) {
8495
+ return err(
8496
+ `${workflowRel} job "${name}": timeout-minutes must be a whole number; got ${timeout}`
8497
+ );
8498
+ }
8499
+ if (timeout < requiredMinutes) {
8500
+ return err(
8501
+ `${workflowRel} job "${name}": timeout-minutes ${timeout} is too small; trusted verify.budget ${policy.verify.budget}s requires at least ${requiredMinutes} minutes for visible + pristine + authority overhead`
8502
+ );
8503
+ }
8504
+ }
8505
+ }
8506
+ if (verifyJobCount === 0) {
8507
+ const scope = opts.workflow ?? ".github/workflows/*.yml|*.yaml";
8508
+ return err(`${scope}: no job contains a tamperward verify step`);
8509
+ }
8510
+ let github = null;
8511
+ if (opts.github) {
8512
+ try {
8513
+ github = githubAuthority(opts, cwd);
8514
+ } catch (e) {
8515
+ return err(e instanceof Error ? e.message : String(e));
8516
+ }
8517
+ if (github.findings.length > 0) {
8518
+ return err(
8519
+ "GitHub repository authority for " + github.repo + "#" + github.branch + " is incomplete: " + github.findings.join("; ")
8520
+ );
8521
+ }
8522
+ }
8523
+ process.stdout.write(
8524
+ `tamperward doctor: CI verifier envelope OK \u2014 ${verifyJobCount} verify job(s), trusted budget ${policy.verify.budget}s/stage, requires >=${requiredMinutes}m outer timeout.
8525
+ `
8189
8526
  );
8527
+ if (github) {
8528
+ process.stdout.write(
8529
+ "tamperward doctor: GitHub repository authority OK \u2014 " + github.repo + "#" + github.branch + " requires tamperward, Code Owner review, and stale-review dismissal on new pushes.\n"
8530
+ );
8531
+ }
8190
8532
  return 0;
8191
8533
  }
8192
8534
 
8193
8535
  // src/cli/verify.ts
8194
- import { execFileSync as execFileSync8, spawnSync as spawnSync2 } from "node:child_process";
8536
+ import { execFileSync as execFileSync9, spawnSync as spawnSync2 } from "node:child_process";
8195
8537
  import { createHash as createHash10 } from "node:crypto";
8196
- import { chmodSync as chmodSync2, cpSync, lstatSync as lstatSync4, mkdirSync as mkdirSync6, mkdtempSync as mkdtempSync2, readFileSync as readFileSync15, readlinkSync as readlinkSync3, rmSync as rmSync3, symlinkSync, writeFileSync as writeFileSync6 } from "node:fs";
8538
+ import { chmodSync as chmodSync2, cpSync, lstatSync as lstatSync4, mkdirSync as mkdirSync6, mkdtempSync as mkdtempSync2, readFileSync as readFileSync16, readlinkSync as readlinkSync3, rmSync as rmSync3, symlinkSync, writeFileSync as writeFileSync6 } from "node:fs";
8197
8539
  import { tmpdir as tmpdir2 } from "node:os";
8198
- import { dirname as dirname7, isAbsolute as isAbsolute10, join as join15, relative as relative5, resolve as resolve6, sep as sep2 } from "node:path";
8540
+ import { dirname as dirname7, isAbsolute as isAbsolute10, join as join16, relative as relative5, resolve as resolve7, sep as sep2 } from "node:path";
8199
8541
 
8200
8542
  // src/fingerprint.ts
8201
- import { execFileSync as execFileSync7 } from "node:child_process";
8543
+ import { execFileSync as execFileSync8 } from "node:child_process";
8202
8544
  import { createHash as createHash7 } from "node:crypto";
8203
8545
  function treeFingerprint(cwd, keepIgnored) {
8204
- const listed = execFileSync7("git", ["ls-files", "-z", "--cached", "--others", "--exclude-standard"], {
8546
+ const listed = execFileSync8("git", ["ls-files", "-z", "--cached", "--others", "--exclude-standard"], {
8205
8547
  cwd,
8206
8548
  encoding: "utf8",
8207
8549
  maxBuffer: 1 << 28,
@@ -8226,10 +8568,10 @@ function treeFingerprint(cwd, keepIgnored) {
8226
8568
  // src/dependency-env.ts
8227
8569
  import { createHash as createHash8 } from "node:crypto";
8228
8570
  import {
8229
- existsSync as existsSync10,
8571
+ existsSync as existsSync11,
8230
8572
  lstatSync as lstatSync3,
8231
- readdirSync as readdirSync3,
8232
- readFileSync as readFileSync13,
8573
+ readdirSync as readdirSync4,
8574
+ readFileSync as readFileSync14,
8233
8575
  readlinkSync as readlinkSync2,
8234
8576
  realpathSync as realpathSync3,
8235
8577
  statSync as statSync4
@@ -8239,15 +8581,15 @@ import {
8239
8581
  delimiter,
8240
8582
  dirname as dirname5,
8241
8583
  isAbsolute as isAbsolute9,
8242
- join as join13,
8584
+ join as join14,
8243
8585
  relative as relative4,
8244
- resolve as resolve4,
8586
+ resolve as resolve5,
8245
8587
  sep
8246
8588
  } from "node:path";
8247
8589
  var Unattestable = class extends Error {
8248
8590
  };
8249
8591
  function inside(root, candidate) {
8250
- const rel = relative4(resolve4(root), resolve4(candidate));
8592
+ const rel = relative4(resolve5(root), resolve5(candidate));
8251
8593
  return rel === "" || rel !== ".." && !rel.startsWith(".." + sep) && !isAbsolute9(rel);
8252
8594
  }
8253
8595
  function modeKind(path) {
@@ -8286,7 +8628,7 @@ function fingerprintRoot(root, cwd, h) {
8286
8628
  st = statSync4(path);
8287
8629
  if (!st.isFile()) throw new Error("not a regular file");
8288
8630
  h.update(label + "\0external-file\0" + path + "\0" + String(st.mode) + "\0");
8289
- h.update(readFileSync13(path));
8631
+ h.update(readFileSync14(path));
8290
8632
  h.update("\0");
8291
8633
  } catch (e) {
8292
8634
  throw new Unattestable(
@@ -8300,14 +8642,14 @@ function fingerprintRoot(root, cwd, h) {
8300
8642
  visited.add(realDir);
8301
8643
  let entries;
8302
8644
  try {
8303
- entries = readdirSync3(dir, { withFileTypes: true });
8645
+ entries = readdirSync4(dir, { withFileTypes: true });
8304
8646
  } catch (e) {
8305
8647
  throw new Unattestable(
8306
8648
  "dependency directory " + JSON.stringify(dir) + " cannot be read (" + (e instanceof Error ? e.message : String(e)) + ")"
8307
8649
  );
8308
8650
  }
8309
8651
  for (const entry of [...entries].sort((a, b) => a.name.localeCompare(b.name))) {
8310
- const path = join13(dir, entry.name);
8652
+ const path = join14(dir, entry.name);
8311
8653
  const rel = namespace ? namespace + "/" + entry.name : entry.name;
8312
8654
  let ident;
8313
8655
  try {
@@ -8320,7 +8662,7 @@ function fingerprintRoot(root, cwd, h) {
8320
8662
  h.update(rel + "\0" + ident.kind + "\0" + String(ident.mode) + "\0");
8321
8663
  if (ident.kind === "file") {
8322
8664
  try {
8323
- h.update(readFileSync13(path));
8665
+ h.update(readFileSync14(path));
8324
8666
  h.update("\0");
8325
8667
  } catch (e) {
8326
8668
  throw new Unattestable(
@@ -8379,7 +8721,7 @@ function snapshot(roots, probes, cwd) {
8379
8721
  const h = createHash8("sha256");
8380
8722
  for (const probe of probes) {
8381
8723
  h.update(
8382
- "probe\0" + probe.kind + "\0" + probe.path + "\0" + (existsSync10(probe.path) ? "present" : "absent") + "\0"
8724
+ "probe\0" + probe.kind + "\0" + probe.path + "\0" + (existsSync11(probe.path) ? "present" : "absent") + "\0"
8383
8725
  );
8384
8726
  }
8385
8727
  for (const root of roots) fingerprintRoot(root, cwd, h);
@@ -8390,7 +8732,7 @@ function freezeRoot(kind, path) {
8390
8732
  if (!statSync4(path).isDirectory()) {
8391
8733
  throw new Error("not a directory");
8392
8734
  }
8393
- return { kind, path: resolve4(path), realPath: realpathSync3(path) };
8735
+ return { kind, path: resolve5(path), realPath: realpathSync3(path) };
8394
8736
  } catch (e) {
8395
8737
  throw new Unattestable(
8396
8738
  kind + " root " + JSON.stringify(path) + " is not resolvable (" + (e instanceof Error ? e.message : String(e)) + ")"
@@ -8426,17 +8768,17 @@ function firstExecutable(command) {
8426
8768
  function resolveExecutable(token, cwd, env) {
8427
8769
  if (!token) return null;
8428
8770
  if (token.includes("/") || token.includes("\\")) {
8429
- const path = isAbsolute9(token) ? token : resolve4(cwd, token);
8771
+ const path = isAbsolute9(token) ? token : resolve5(cwd, token);
8430
8772
  try {
8431
- return existsSync10(path) ? realpathSync3(path) : null;
8773
+ return existsSync11(path) ? realpathSync3(path) : null;
8432
8774
  } catch {
8433
8775
  return null;
8434
8776
  }
8435
8777
  }
8436
8778
  for (const entry of (env.PATH ?? "").split(delimiter).filter(Boolean)) {
8437
- const candidate = resolve4(entry, token);
8779
+ const candidate = resolve5(entry, token);
8438
8780
  try {
8439
- if (existsSync10(candidate) && statSync4(candidate).isFile()) return realpathSync3(candidate);
8781
+ if (existsSync11(candidate) && statSync4(candidate).isFile()) return realpathSync3(candidate);
8440
8782
  } catch {
8441
8783
  }
8442
8784
  }
@@ -8453,13 +8795,13 @@ function looksPython(command, executable, token) {
8453
8795
  }
8454
8796
  function inferredVenvFromToken(token, cwd) {
8455
8797
  if (!token || !token.includes("/") && !token.includes("\\")) return null;
8456
- const path = isAbsolute9(token) ? token : resolve4(cwd, token);
8798
+ const path = isAbsolute9(token) ? token : resolve5(cwd, token);
8457
8799
  if (!pythonName(basename3(path))) return null;
8458
8800
  const parent = dirname5(path);
8459
8801
  const parentName = basename3(parent).toLowerCase();
8460
8802
  if (parentName !== "bin" && parentName !== "scripts") return null;
8461
8803
  const root = dirname5(parent);
8462
- return existsSync10(join13(root, "pyvenv.cfg")) ? root : null;
8804
+ return existsSync11(join14(root, "pyvenv.cfg")) ? root : null;
8463
8805
  }
8464
8806
  function unsupportedEcosystem(command, token) {
8465
8807
  const name = token ? basename3(token).toLowerCase() : "";
@@ -8475,13 +8817,13 @@ function inferredVenv(executable) {
8475
8817
  const parentName = basename3(parent).toLowerCase();
8476
8818
  if (parentName !== "bin" && parentName !== "scripts") return null;
8477
8819
  const root = dirname5(parent);
8478
- return existsSync10(join13(root, "pyvenv.cfg")) ? root : null;
8820
+ return existsSync11(join14(root, "pyvenv.cfg")) ? root : null;
8479
8821
  }
8480
8822
  function venvProblem(root) {
8481
- const cfg = join13(root, "pyvenv.cfg");
8482
- if (!existsSync10(cfg)) return "VIRTUAL_ENV " + JSON.stringify(root) + " has no pyvenv.cfg";
8823
+ const cfg = join14(root, "pyvenv.cfg");
8824
+ if (!existsSync11(cfg)) return "VIRTUAL_ENV " + JSON.stringify(root) + " has no pyvenv.cfg";
8483
8825
  try {
8484
- const body = readFileSync13(cfg, "utf8");
8826
+ const body = readFileSync14(cfg, "utf8");
8485
8827
  if (/^\s*include-system-site-packages\s*=\s*true\s*$/im.test(body)) {
8486
8828
  return "Python environment " + JSON.stringify(root) + " enables system site-packages; those external package roots are not bounded";
8487
8829
  }
@@ -8491,7 +8833,7 @@ function venvProblem(root) {
8491
8833
  return null;
8492
8834
  }
8493
8835
  function discoverDependencyEnvironment(cwdInput, command, env = process.env) {
8494
- const cwd = resolve4(cwdInput);
8836
+ const cwd = resolve5(cwdInput);
8495
8837
  const roots = [];
8496
8838
  const probes = [];
8497
8839
  const seen = /* @__PURE__ */ new Set();
@@ -8504,8 +8846,8 @@ function discoverDependencyEnvironment(cwdInput, command, env = process.env) {
8504
8846
  }
8505
8847
  };
8506
8848
  try {
8507
- const nodeModules = join13(cwd, "node_modules");
8508
- if (existsSync10(nodeModules)) addRoot("node_modules", nodeModules);
8849
+ const nodeModules = join14(cwd, "node_modules");
8850
+ if (existsSync11(nodeModules)) addRoot("node_modules", nodeModules);
8509
8851
  else probes.push({ kind: "node_modules", path: nodeModules });
8510
8852
  const token = firstExecutable(command);
8511
8853
  const executable = resolveExecutable(token, cwd, env);
@@ -8514,11 +8856,11 @@ function discoverDependencyEnvironment(cwdInput, command, env = process.env) {
8514
8856
  if (!isAbsolute9(env.VIRTUAL_ENV)) {
8515
8857
  throw new Unattestable("VIRTUAL_ENV is relative (" + JSON.stringify(env.VIRTUAL_ENV) + ")");
8516
8858
  }
8517
- venv = resolve4(env.VIRTUAL_ENV);
8859
+ venv = resolve5(env.VIRTUAL_ENV);
8518
8860
  } else {
8519
8861
  venv = inferredVenv(executable) ?? inferredVenvFromToken(token, cwd);
8520
- if (!venv && /(?:^|\s)uv\s+run(?:\s|$)/.test(command) && existsSync10(join13(cwd, ".venv", "pyvenv.cfg"))) {
8521
- venv = join13(cwd, ".venv");
8862
+ if (!venv && /(?:^|\s)uv\s+run(?:\s|$)/.test(command) && existsSync11(join14(cwd, ".venv", "pyvenv.cfg"))) {
8863
+ venv = join14(cwd, ".venv");
8522
8864
  }
8523
8865
  }
8524
8866
  if (venv) {
@@ -8550,7 +8892,7 @@ function checkDependencyEnvironment(cwdInput, descriptor) {
8550
8892
  return { ok: false, reason: descriptor.reason ?? "dependency environment is not attestable" };
8551
8893
  }
8552
8894
  try {
8553
- const fingerprint2 = snapshot(descriptor.roots, descriptor.probes, resolve4(cwdInput));
8895
+ const fingerprint2 = snapshot(descriptor.roots, descriptor.probes, resolve5(cwdInput));
8554
8896
  if (fingerprint2 !== descriptor.fingerprint) {
8555
8897
  return { ok: false, fingerprint: fingerprint2, reason: "dependency environment fingerprint changed" };
8556
8898
  }
@@ -8579,8 +8921,8 @@ function dependencyEnvironmentSummary(descriptor) {
8579
8921
  // src/verifier-backend.ts
8580
8922
  import { createHash as createHash9, randomUUID } from "node:crypto";
8581
8923
  import { spawnSync } from "node:child_process";
8582
- import { accessSync, constants as constants2, readFileSync as readFileSync14, realpathSync as realpathSync4, statSync as statSync5 } from "node:fs";
8583
- import { delimiter as delimiter2, dirname as dirname6, join as join14, resolve as resolve5 } from "node:path";
8924
+ import { accessSync, constants as constants2, readFileSync as readFileSync15, realpathSync as realpathSync4, statSync as statSync5 } from "node:fs";
8925
+ import { delimiter as delimiter2, dirname as dirname6, join as join15, resolve as resolve6 } from "node:path";
8584
8926
  var DEFAULT_CONTAINER_RESOURCE_LIMITS = Object.freeze({
8585
8927
  memoryBytes: 2 * 1024 * 1024 * 1024,
8586
8928
  // Docker interprets memory-swap == memory as no additional swap allowance.
@@ -8609,7 +8951,7 @@ function writableByCaller(path) {
8609
8951
  }
8610
8952
  }
8611
8953
  function replaceableByCaller(path) {
8612
- let current = resolve5(path);
8954
+ let current = resolve6(path);
8613
8955
  if (writableByCaller(current)) return true;
8614
8956
  current = dirname6(current);
8615
8957
  while (true) {
@@ -8623,7 +8965,7 @@ function replaceableByCaller(path) {
8623
8965
  function findExecutable(name) {
8624
8966
  for (const raw of (process.env.PATH ?? "").split(delimiter2)) {
8625
8967
  if (!raw) continue;
8626
- const candidate = join14(raw, name);
8968
+ const candidate = join15(raw, name);
8627
8969
  try {
8628
8970
  accessSync(candidate, constants2.X_OK);
8629
8971
  return realpathSync4(candidate);
@@ -8633,7 +8975,7 @@ function findExecutable(name) {
8633
8975
  return null;
8634
8976
  }
8635
8977
  function fileSha256(path) {
8636
- return createHash9("sha256").update(readFileSync14(path)).digest("hex");
8978
+ return createHash9("sha256").update(readFileSync15(path)).digest("hex");
8637
8979
  }
8638
8980
  function engineClientEnv() {
8639
8981
  return {
@@ -8955,7 +9297,7 @@ function runContainerStage(backend, dir, command, budgetSecs) {
8955
9297
  const args = containerRunArgs({
8956
9298
  image: backend.image,
8957
9299
  name,
8958
- workspace: resolve5(dir),
9300
+ workspace: resolve6(dir),
8959
9301
  command,
8960
9302
  uid,
8961
9303
  gid,
@@ -9108,7 +9450,7 @@ var VERIFICATION_SURFACE = [
9108
9450
  "**/*.runsettings"
9109
9451
  ];
9110
9452
  function git2(args, cwd) {
9111
- return execFileSync8("git", args, { cwd, encoding: "utf8", maxBuffer: 1 << 28, env: trustedGitEnv() });
9453
+ return execFileSync9("git", args, { cwd, encoding: "utf8", maxBuffer: 1 << 28, env: trustedGitEnv() });
9112
9454
  }
9113
9455
  function resolveBase(base, cwd) {
9114
9456
  assertRev(base);
@@ -9124,7 +9466,7 @@ function baseIsAncestorOfHead(base, cwd) {
9124
9466
  return r.status === 0;
9125
9467
  }
9126
9468
  function inside2(root, path) {
9127
- const rel = relative5(resolve6(root), resolve6(path));
9469
+ const rel = relative5(resolve7(root), resolve7(path));
9128
9470
  return rel === "" || rel !== ".." && !rel.startsWith(`..${sep2}`) && !isAbsolute10(rel);
9129
9471
  }
9130
9472
  function linkParts(path) {
@@ -9141,7 +9483,7 @@ function rootedLinkTarget(target) {
9141
9483
  return isAbsolute10(target) || driveRelativeLinkTarget(target);
9142
9484
  }
9143
9485
  function safeSymlink(target, out2, root, label) {
9144
- if (rootedLinkTarget(target) || !inside2(root, resolve6(dirname7(out2), target))) {
9486
+ if (rootedLinkTarget(target) || !inside2(root, resolve7(dirname7(out2), target))) {
9145
9487
  throw linkEscape(label, target);
9146
9488
  }
9147
9489
  rmSync3(out2, { force: true });
@@ -9167,7 +9509,7 @@ function validateSymlinkGraph(target, out2, root, dependencyRoot, label) {
9167
9509
  continue;
9168
9510
  }
9169
9511
  const base = domain === "tree" ? root : dependencyRoot;
9170
- const current = join15(base, ...stack);
9512
+ const current = join16(base, ...stack);
9171
9513
  let st;
9172
9514
  try {
9173
9515
  st = lstatSync4(current);
@@ -9193,7 +9535,7 @@ function validateSymlinkGraph(target, out2, root, dependencyRoot, label) {
9193
9535
  throw linkEscape(label, target);
9194
9536
  }
9195
9537
  stack = [];
9196
- pending.unshift(...linkParts(relative5(dependencyRoot, resolve6(next))));
9538
+ pending.unshift(...linkParts(relative5(dependencyRoot, resolve7(next))));
9197
9539
  } else {
9198
9540
  pending.unshift(...linkParts(next));
9199
9541
  }
@@ -9203,7 +9545,7 @@ function rejectLinkedParent(cwd, rel) {
9203
9545
  const parts = rel.split("/").slice(0, -1);
9204
9546
  let current = cwd;
9205
9547
  for (const part of parts) {
9206
- current = join15(current, part);
9548
+ current = join16(current, part);
9207
9549
  let st;
9208
9550
  try {
9209
9551
  st = lstatSync4(current);
@@ -9219,7 +9561,7 @@ function materialize(cwd, dest, dependencyRoot) {
9219
9561
  const listed = git2(["ls-files", "-z", "--cached", "--others", "--exclude-standard"], cwd).split("\0").filter(Boolean);
9220
9562
  const links = [];
9221
9563
  for (const rel of listed) {
9222
- const src = join15(cwd, rel);
9564
+ const src = join16(cwd, rel);
9223
9565
  rejectLinkedParent(cwd, rel);
9224
9566
  let st;
9225
9567
  try {
@@ -9227,7 +9569,7 @@ function materialize(cwd, dest, dependencyRoot) {
9227
9569
  } catch {
9228
9570
  continue;
9229
9571
  }
9230
- const out2 = join15(dest, rel);
9572
+ const out2 = join16(dest, rel);
9231
9573
  mkdirSync6(dirname7(out2), { recursive: true });
9232
9574
  dropSymlink(out2);
9233
9575
  if (st.isSymbolicLink()) {
@@ -9244,7 +9586,7 @@ function materialize(cwd, dest, dependencyRoot) {
9244
9586
  for (const link of links) {
9245
9587
  validateSymlinkGraph(link.target, link.out, dest, dependencyRoot, link.rel);
9246
9588
  }
9247
- if (dependencyRoot) symlinkSync(dependencyRoot, join15(dest, "node_modules"), "dir");
9589
+ if (dependencyRoot) symlinkSync(dependencyRoot, join16(dest, "node_modules"), "dir");
9248
9590
  }
9249
9591
  function dropSymlink(p) {
9250
9592
  try {
@@ -9277,11 +9619,11 @@ function overlayDigest(dest, paths) {
9277
9619
  for (const rel of [...paths].sort()) {
9278
9620
  h.update(rel);
9279
9621
  h.update("\0");
9280
- const p = join15(dest, rel);
9622
+ const p = join16(dest, rel);
9281
9623
  try {
9282
9624
  const st = lstatSync4(p);
9283
9625
  h.update(String(st.mode));
9284
- h.update(st.isSymbolicLink() ? readlinkSync3(p) : readFileSync15(p));
9626
+ h.update(st.isSymbolicLink() ? readlinkSync3(p) : readFileSync16(p));
9285
9627
  } catch {
9286
9628
  h.update("<gone>");
9287
9629
  }
@@ -9304,8 +9646,8 @@ function overlayPristine(cwd, base, dest, policy, cmd, dependencyRoot) {
9304
9646
  if (e.type === "commit") {
9305
9647
  throw new Error(`the base's ${e.path} is a submodule; the pristine copy cannot reproduce it`);
9306
9648
  }
9307
- const content = execFileSync8("git", ["show", `${base}:${e.path}`], { cwd, maxBuffer: 1 << 28, env: trustedGitEnv() });
9308
- const out2 = join15(dest, e.path);
9649
+ const content = execFileSync9("git", ["show", `${base}:${e.path}`], { cwd, maxBuffer: 1 << 28, env: trustedGitEnv() });
9650
+ const out2 = join16(dest, e.path);
9309
9651
  mkdirSync6(dirname7(out2), { recursive: true });
9310
9652
  rmSync3(out2, { force: true });
9311
9653
  if (e.mode === "120000") {
@@ -9322,7 +9664,7 @@ function overlayPristine(cwd, base, dest, policy, cmd, dependencyRoot) {
9322
9664
  const inCopy = git2(["ls-files", "-z", "--cached", "--others", "--exclude-standard"], cwd).split("\0").filter(Boolean);
9323
9665
  for (const rel of inCopy) {
9324
9666
  if (!isOverlay(rel) || baseProtected.has(rel)) continue;
9325
- rmSync3(join15(dest, rel), { force: true });
9667
+ rmSync3(join16(dest, rel), { force: true });
9326
9668
  removed++;
9327
9669
  }
9328
9670
  for (const link of restoredLinks) {
@@ -9367,8 +9709,8 @@ function suiteEnv(scratch) {
9367
9709
  if (DROPPED_ENV.has(k) || PINNED_NPM.test(k)) continue;
9368
9710
  env[k] = v;
9369
9711
  }
9370
- const userRc = join15(scratch, "npmrc-user");
9371
- const globalRc = join15(scratch, "npmrc-global");
9712
+ const userRc = join16(scratch, "npmrc-user");
9713
+ const globalRc = join16(scratch, "npmrc-global");
9372
9714
  for (const f of [userRc, globalRc]) writeFileSync6(f, "", { mode: 292 });
9373
9715
  env.npm_config_userconfig = userRc;
9374
9716
  env.npm_config_globalconfig = globalRc;
@@ -9378,8 +9720,8 @@ function suiteEnv(scratch) {
9378
9720
  }
9379
9721
  function runLocalSuite(dir, cmd, budgetSecs) {
9380
9722
  const t0 = Date.now();
9381
- const outDir = mkdtempSync2(join15(tmpdir2(), "tw-verify-run-"));
9382
- const outFile = join15(outDir, "result.json");
9723
+ const outDir = mkdtempSync2(join16(tmpdir2(), "tw-verify-run-"));
9724
+ const outFile = join16(outDir, "result.json");
9383
9725
  try {
9384
9726
  spawnSync2(process.execPath, ["-e", SUPERVISOR, cmd, String(budgetSecs * 1e3), outFile], {
9385
9727
  cwd: dir,
@@ -9392,7 +9734,7 @@ function runLocalSuite(dir, cmd, budgetSecs) {
9392
9734
  const secs = Math.round((Date.now() - t0) / 1e3);
9393
9735
  let r;
9394
9736
  try {
9395
- r = JSON.parse(readFileSync15(outFile, "utf8"));
9737
+ r = JSON.parse(readFileSync16(outFile, "utf8"));
9396
9738
  } catch {
9397
9739
  return { exit: null, secs };
9398
9740
  }
@@ -9484,8 +9826,8 @@ function runVerify(opts) {
9484
9826
  out2(`verify: cannot resolve base rev "${opts.base ?? "HEAD"}" \u2014 failing closed`);
9485
9827
  return 2;
9486
9828
  }
9487
- const visRoot = mkdtempSync2(join15(tmpdir2(), "tw-verify-vis-"));
9488
- const visDir = join15(visRoot, "t");
9829
+ const visRoot = mkdtempSync2(join16(tmpdir2(), "tw-verify-vis-"));
9830
+ const visDir = join16(visRoot, "t");
9489
9831
  const cleanup = (dirs) => {
9490
9832
  if (opts.keep) return;
9491
9833
  for (const d of dirs) rmSync3(d, { recursive: true, force: true });
@@ -9542,8 +9884,8 @@ function runVerify(opts) {
9542
9884
  out2((dependencyAfterVisible.reason ?? "dependency identity changed") + ". Failing closed, not open.");
9543
9885
  return 2;
9544
9886
  }
9545
- const priRoot = mkdtempSync2(join15(tmpdir2(), "tw-verify-pri-"));
9546
- const priDir = join15(priRoot, "t");
9887
+ const priRoot = mkdtempSync2(join16(tmpdir2(), "tw-verify-pri-"));
9888
+ const priDir = join16(priRoot, "t");
9547
9889
  let restored = [];
9548
9890
  let removedAdded = 0;
9549
9891
  try {
@@ -9693,12 +10035,12 @@ function parseVerify(args) {
9693
10035
  }
9694
10036
 
9695
10037
  // src/cli/run.ts
9696
- import { execFileSync as execFileSync9, spawnSync as spawnSync3 } from "node:child_process";
9697
- import { mkdtempSync as mkdtempSync3, readdirSync as readdirSync4, readFileSync as readFileSync16, readlinkSync as readlinkSync4, realpathSync as realpathSync6, rmSync as rmSync4 } from "node:fs";
10038
+ import { execFileSync as execFileSync10, spawnSync as spawnSync3 } from "node:child_process";
10039
+ import { mkdtempSync as mkdtempSync3, readdirSync as readdirSync5, readFileSync as readFileSync17, readlinkSync as readlinkSync4, realpathSync as realpathSync6, rmSync as rmSync4 } from "node:fs";
9698
10040
  import { tmpdir as tmpdir3 } from "node:os";
9699
- import { join as join16, resolve as resolve7 } from "node:path";
10041
+ import { join as join17, resolve as resolve8 } from "node:path";
9700
10042
  var out = (s) => process.stdout.write(s + "\n");
9701
- var err = (s) => process.stderr.write(s + "\n");
10043
+ var err2 = (s) => process.stderr.write(s + "\n");
9702
10044
  var AGENT_SUPERVISOR = String.raw`
9703
10045
  const { spawn, spawnSync } = require('node:child_process');
9704
10046
  const fs = require('node:fs');
@@ -9809,14 +10151,14 @@ function linuxDescendantPids(rootPid) {
9809
10151
  const byParent = /* @__PURE__ */ new Map();
9810
10152
  let names;
9811
10153
  try {
9812
- names = readdirSync4("/proc").filter((x) => /^\d+$/.test(x));
10154
+ names = readdirSync5("/proc").filter((x) => /^\d+$/.test(x));
9813
10155
  } catch {
9814
10156
  return [];
9815
10157
  }
9816
10158
  for (const name of names) {
9817
10159
  const pid = Number(name);
9818
10160
  try {
9819
- const stat = readFileSync16(`/proc/${name}/stat`, "utf8");
10161
+ const stat = readFileSync17(`/proc/${name}/stat`, "utf8");
9820
10162
  const fields = stat.slice(stat.lastIndexOf(")") + 2).split(" ");
9821
10163
  const ppid = Number(fields[1]);
9822
10164
  if (!Number.isFinite(ppid)) continue;
@@ -9866,8 +10208,8 @@ function killAgentTree(pid) {
9866
10208
  }
9867
10209
  }
9868
10210
  function runAgentWithBudget(argv2, cwd, budgetSecs) {
9869
- const stateDir = mkdtempSync3(join16(tmpdir3(), "tw-agent-supervisor-"));
9870
- const resultFile = join16(stateDir, "result.json");
10211
+ const stateDir = mkdtempSync3(join17(tmpdir3(), "tw-agent-supervisor-"));
10212
+ const resultFile = join17(stateDir, "result.json");
9871
10213
  let supervisorTimedOut = false;
9872
10214
  try {
9873
10215
  const supervisor = spawnSync3(
@@ -9885,7 +10227,7 @@ function runAgentWithBudget(argv2, cwd, budgetSecs) {
9885
10227
  supervisorTimedOut = Boolean(supervisor.error) && supervisor.error.code === "ETIMEDOUT";
9886
10228
  let state = {};
9887
10229
  try {
9888
- state = JSON.parse(readFileSync16(resultFile, "utf8"));
10230
+ state = JSON.parse(readFileSync17(resultFile, "utf8"));
9889
10231
  } catch {
9890
10232
  }
9891
10233
  if (supervisorTimedOut || state.timedOut) {
@@ -9916,11 +10258,11 @@ function runAgentWithBudget(argv2, cwd, budgetSecs) {
9916
10258
  }
9917
10259
  }
9918
10260
  function git3(args, cwd) {
9919
- return execFileSync9("git", args, { cwd, encoding: "utf8", maxBuffer: 1 << 28, env: trustedGitEnv() });
10261
+ return execFileSync10("git", args, { cwd, encoding: "utf8", maxBuffer: 1 << 28, env: trustedGitEnv() });
9920
10262
  }
9921
10263
  function nowTicks() {
9922
10264
  try {
9923
- return Math.floor(parseFloat(readFileSync16("/proc/uptime", "utf8").split(" ")[0]) * 100);
10265
+ return Math.floor(parseFloat(readFileSync17("/proc/uptime", "utf8").split(" ")[0]) * 100);
9924
10266
  } catch {
9925
10267
  return Number.POSITIVE_INFINITY;
9926
10268
  }
@@ -9935,7 +10277,7 @@ function survivorsHoldingTree(cwd, spawnedAfterTicks) {
9935
10277
  }
9936
10278
  let pids;
9937
10279
  try {
9938
- pids = readdirSync4("/proc").filter((d) => /^\d+$/.test(d));
10280
+ pids = readdirSync5("/proc").filter((d) => /^\d+$/.test(d));
9939
10281
  } catch {
9940
10282
  return out2;
9941
10283
  }
@@ -9943,7 +10285,7 @@ function survivorsHoldingTree(cwd, spawnedAfterTicks) {
9943
10285
  const n = Number(pid);
9944
10286
  if (n === process.pid) continue;
9945
10287
  try {
9946
- const stat = readFileSync16(`/proc/${pid}/stat`, "utf8");
10288
+ const stat = readFileSync17(`/proc/${pid}/stat`, "utf8");
9947
10289
  const startTicks = Number(stat.slice(stat.lastIndexOf(")") + 2).split(" ")[19]);
9948
10290
  if (!Number.isFinite(startTicks) || startTicks < spawnedAfterTicks) continue;
9949
10291
  } catch {
@@ -9951,14 +10293,14 @@ function survivorsHoldingTree(cwd, spawnedAfterTicks) {
9951
10293
  }
9952
10294
  const procLinks = [`/proc/${pid}/cwd`, `/proc/${pid}/exe`];
9953
10295
  try {
9954
- for (const fd of readdirSync4(`/proc/${pid}/fd`)) procLinks.push(`/proc/${pid}/fd/${fd}`);
10296
+ for (const fd of readdirSync5(`/proc/${pid}/fd`)) procLinks.push(`/proc/${pid}/fd/${fd}`);
9955
10297
  } catch {
9956
10298
  }
9957
10299
  let holds = false;
9958
10300
  for (const link of procLinks) {
9959
10301
  try {
9960
10302
  const raw = readlinkSync4(link).replace(/ \(deleted\)$/, "");
9961
- const held = raw.startsWith("/") ? resolve7(raw) : raw;
10303
+ const held = raw.startsWith("/") ? resolve8(raw) : raw;
9962
10304
  if (held === real || held.startsWith(real + "/")) {
9963
10305
  holds = true;
9964
10306
  break;
@@ -9971,13 +10313,13 @@ function survivorsHoldingTree(cwd, spawnedAfterTicks) {
9971
10313
  return out2;
9972
10314
  }
9973
10315
  function runEnvelope(opts) {
9974
- const cwd = resolve7(opts.cwd ?? process.cwd());
10316
+ const cwd = resolve8(opts.cwd ?? process.cwd());
9975
10317
  if (!opts.argv.length) {
9976
- err("tamperward run: no agent command given (usage: tamperward run [options] -- <command...>)");
10318
+ err2("tamperward run: no agent command given (usage: tamperward run [options] -- <command...>)");
9977
10319
  return 2;
9978
10320
  }
9979
10321
  if (opts.agentBudget !== void 0 && (!Number.isFinite(opts.agentBudget) || opts.agentBudget <= 0)) {
9980
- err(
10322
+ err2(
9981
10323
  `tamperward run: --agent-budget needs a positive number of seconds (got ${JSON.stringify(opts.agentBudget)}) \u2014 failing closed.`
9982
10324
  );
9983
10325
  return 2;
@@ -9986,7 +10328,7 @@ function runEnvelope(opts) {
9986
10328
  try {
9987
10329
  base = git3(["rev-parse", "--verify", `${opts.base ?? "HEAD"}^{commit}`], cwd).trim();
9988
10330
  } catch {
9989
- err(`tamperward run: cannot resolve trusted base ${opts.base ?? "HEAD"} \u2014 failing closed.`);
10331
+ err2(`tamperward run: cannot resolve trusted base ${opts.base ?? "HEAD"} \u2014 failing closed.`);
9990
10332
  return 2;
9991
10333
  }
9992
10334
  let frozenPolicy;
@@ -9997,19 +10339,19 @@ function runEnvelope(opts) {
9997
10339
  frozenCmd = opts.cmd ?? frozenPolicy.verify?.command;
9998
10340
  frozenBudget = opts.budget ?? frozenPolicy.verify?.budget;
9999
10341
  } catch (e) {
10000
- err(`tamperward run: cannot load the entry-time policy (${e instanceof Error ? e.message : String(e)}) \u2014 failing closed.`);
10342
+ err2(`tamperward run: cannot load the entry-time policy (${e instanceof Error ? e.message : String(e)}) \u2014 failing closed.`);
10001
10343
  return 2;
10002
10344
  }
10003
10345
  if (!frozenCmd) {
10004
- err("tamperward run: no suite command at entry (policy `verify: { command: ... }` or --cmd) \u2014");
10005
- err("refusing to start: a verifier that appears after the agent runs would be agent-authored.");
10346
+ err2("tamperward run: no suite command at entry (policy `verify: { command: ... }` or --cmd) \u2014");
10347
+ err2("refusing to start: a verifier that appears after the agent runs would be agent-authored.");
10006
10348
  return 2;
10007
10349
  }
10008
10350
  if (!opts.allowDirty) {
10009
10351
  const dirty = git3(["status", "--porcelain"], cwd).trim();
10010
10352
  if (dirty) {
10011
- err("tamperward run: working tree is dirty before the agent starts \u2014 the envelope cannot");
10012
- err("attribute changes. Commit or stash first, or pass --allow-dirty to own the risk.");
10353
+ err2("tamperward run: working tree is dirty before the agent starts \u2014 the envelope cannot");
10354
+ err2("attribute changes. Commit or stash first, or pass --allow-dirty to own the risk.");
10013
10355
  return 2;
10014
10356
  }
10015
10357
  }
@@ -10017,25 +10359,25 @@ function runEnvelope(opts) {
10017
10359
  out(`tamperward run \u2014 trusted base ${base.slice(0, 10)}; agent exit is untrusted.`);
10018
10360
  out(`tamperward run \u2014 verifier backend: ${verifierBackendSummary(verifierBackend)}`);
10019
10361
  if (!verifierBackend.available) {
10020
- err("tamperward run: the requested verifier backend cannot be established \u2014");
10021
- err(`${verifierBackend.reason ?? "unknown backend failure"}. Failing closed before the agent starts.`);
10362
+ err2("tamperward run: the requested verifier backend cannot be established \u2014");
10363
+ err2(`${verifierBackend.reason ?? "unknown backend failure"}. Failing closed before the agent starts.`);
10022
10364
  return 2;
10023
10365
  }
10024
10366
  if (verifierBackend.kind === "container") {
10025
- err("tamperward run: container verification requires a frozen candidate handoff from an");
10026
- err("agent domain that cannot control the verifier engine. `run` executes the agent under");
10027
- err("this host identity, so it cannot honestly provide that separation. Failing closed");
10028
- err("before the agent starts; use standalone `tamperward verify` in trusted CI or after");
10029
- err("an externally isolated agent produces the frozen candidate artifact.");
10367
+ err2("tamperward run: container verification requires a frozen candidate handoff from an");
10368
+ err2("agent domain that cannot control the verifier engine. `run` executes the agent under");
10369
+ err2("this host identity, so it cannot honestly provide that separation. Failing closed");
10370
+ err2("before the agent starts; use standalone `tamperward verify` in trusted CI or after");
10371
+ err2("an externally isolated agent produces the frozen candidate artifact.");
10030
10372
  return 2;
10031
10373
  }
10032
10374
  const dependencyEnvironment = verifierBackend.kind === "local" ? discoverDependencyEnvironment(cwd, frozenCmd) : null;
10033
10375
  if (dependencyEnvironment) {
10034
10376
  out(`tamperward run \u2014 dependency environment: ${dependencyEnvironmentSummary(dependencyEnvironment)}`);
10035
10377
  if (dependencyEnvironment.status === "unattestable" && !opts.allowDepDrift) {
10036
- err("tamperward run: the verifier dependency environment cannot be attested \u2014");
10037
- err(`${dependencyEnvironment.reason ?? "unknown dependency environment"}. Failing closed before the agent starts.`);
10038
- err("(Pass --allow-dep-drift only if you explicitly own this dependency-integrity risk.)");
10378
+ err2("tamperward run: the verifier dependency environment cannot be attested \u2014");
10379
+ err2(`${dependencyEnvironment.reason ?? "unknown dependency environment"}. Failing closed before the agent starts.`);
10380
+ err2("(Pass --allow-dep-drift only if you explicitly own this dependency-integrity risk.)");
10039
10381
  return 2;
10040
10382
  }
10041
10383
  } else {
@@ -10055,37 +10397,37 @@ function runEnvelope(opts) {
10055
10397
  const agentExit = agentRun.exit;
10056
10398
  const agentTimedOut = agentRun.timedOut;
10057
10399
  if (agentRun.failure && !agentTimedOut) {
10058
- err(`tamperward run: agent runtime failed to start/report cleanly (${agentRun.failure}) \u2014 continuing to adjudicate the released tree.`);
10400
+ err2(`tamperward run: agent runtime failed to start/report cleanly (${agentRun.failure}) \u2014 continuing to adjudicate the released tree.`);
10059
10401
  }
10060
10402
  if (agentTimedOut) {
10061
- err(
10403
+ err2(
10062
10404
  `tamperward run: AGENT_TIMEOUT \u2014 wrapped agent exceeded ${opts.agentBudget}s; its owned process tree was terminated. Post-timeout adjudication continues.`
10063
10405
  );
10064
10406
  }
10065
10407
  if (objectRewriteState(cwd) !== rewritesBefore) {
10066
- err("tamperward run: git object-replacement state changed during this run \u2014 a replace ref, graft");
10067
- err("or shallow boundary now redirects what the trusted base resolves to. Nothing below could be");
10068
- err("read against the anchor we started from. That is the finding.");
10408
+ err2("tamperward run: git object-replacement state changed during this run \u2014 a replace ref, graft");
10409
+ err2("or shallow boundary now redirects what the trusted base resolves to. Nothing below could be");
10410
+ err2("read against the anchor we started from. That is the finding.");
10069
10411
  out(`
10070
10412
  tamperward run \u2014 agent exit ${agentExit}; OBJECT_REWRITE \u2192 ENFORCEMENT_FAILED`);
10071
10413
  return 1;
10072
10414
  }
10073
10415
  const dependencyBeforeAdjudication = dependencyEnvironment ? checkDependencyEnvironment(cwd, dependencyEnvironment) : { ok: true };
10074
10416
  if (!dependencyBeforeAdjudication.ok) {
10075
- err("tamperward run: the frozen dependency environment changed before adjudication began \u2014");
10076
- err(`${dependencyBeforeAdjudication.reason ?? "dependency identity changed"}.`);
10417
+ err2("tamperward run: the frozen dependency environment changed before adjudication began \u2014");
10418
+ err2(`${dependencyBeforeAdjudication.reason ?? "dependency identity changed"}.`);
10077
10419
  if (!opts.allowDepDrift) {
10078
10420
  out(`
10079
10421
  tamperward run \u2014 agent exit ${agentExit}; DEPENDENCY_DRIFT \u2192 ENFORCEMENT_FAILED`);
10080
10422
  return 1;
10081
10423
  }
10082
- err("(--allow-dep-drift: proceeding anyway, on the operator's judgement.)");
10424
+ err2("(--allow-dep-drift: proceeding anyway, on the operator's judgement.)");
10083
10425
  }
10084
10426
  const head = git3(["rev-parse", "HEAD"], cwd).trim();
10085
10427
  const isAncestor = spawnSync3("git", ["merge-base", "--is-ancestor", base, head], { cwd, env: trustedGitEnv() });
10086
10428
  if (isAncestor.status !== 0) {
10087
- err(`tamperward run: HEAD ${head.slice(0, 10)} is not a descendant of the trusted base ${base.slice(0, 10)} \u2014`);
10088
- err("the agent rewrote history out from under the anchor. That is the finding.");
10429
+ err2(`tamperward run: HEAD ${head.slice(0, 10)} is not a descendant of the trusted base ${base.slice(0, 10)} \u2014`);
10430
+ err2("the agent rewrote history out from under the anchor. That is the finding.");
10089
10431
  out(`
10090
10432
  tamperward run \u2014 agent exit ${agentExit}; HISTORY_REWRITE \u2192 ENFORCEMENT_FAILED`);
10091
10433
  return 1;
@@ -10113,30 +10455,30 @@ tamperward run \u2014 agent exit ${agentExit}; HISTORY_REWRITE \u2192 ENFORCEMEN
10113
10455
  });
10114
10456
  if (opts.settle && opts.settle > 0) {
10115
10457
  try {
10116
- execFileSync9("sleep", [String(opts.settle)]);
10458
+ execFileSync10("sleep", [String(opts.settle)]);
10117
10459
  } catch {
10118
10460
  }
10119
10461
  }
10120
10462
  const mutatedDuringAdjudication = treeFingerprint(cwd, protectedIgnored) !== fpBefore;
10121
10463
  if (mutatedDuringAdjudication) {
10122
- err("tamperward run: the working tree changed while it was being adjudicated \u2014");
10123
- err("the verdict below would describe a tree that no longer exists.");
10464
+ err2("tamperward run: the working tree changed while it was being adjudicated \u2014");
10465
+ err2("the verdict below would describe a tree that no longer exists.");
10124
10466
  }
10125
10467
  const dependencyAfter = dependencyEnvironment ? checkDependencyEnvironment(cwd, dependencyEnvironment) : { ok: true };
10126
10468
  const depsDrifted = Boolean(dependencyEnvironment) && !opts.allowDepDrift && !dependencyAfter.ok;
10127
10469
  if (depsDrifted) {
10128
- err("tamperward run: the frozen dependency environment changed during this run \u2014");
10129
- err(`${dependencyAfter.reason ?? "dependency identity changed"}. Both verification runs depend on it.`);
10470
+ err2("tamperward run: the frozen dependency environment changed during this run \u2014");
10471
+ err2(`${dependencyAfter.reason ?? "dependency identity changed"}. Both verification runs depend on it.`);
10130
10472
  }
10131
10473
  const rewrote = objectRewriteState(cwd) !== rewritesBefore;
10132
10474
  if (rewrote) {
10133
- err("tamperward run: git object-replacement state changed WHILE the tree was being adjudicated \u2014");
10134
- err("the checks above read the trusted base through an object layer that has since moved.");
10475
+ err2("tamperward run: git object-replacement state changed WHILE the tree was being adjudicated \u2014");
10476
+ err2("the checks above read the trusted base through an object layer that has since moved.");
10135
10477
  }
10136
10478
  const survivors = survivorsHoldingTree(cwd, spawnTicks);
10137
10479
  if (survivors.length) {
10138
- err(`tamperward run: ${survivors.length} process(es) started during this run still hold the`);
10139
- err(`working tree (pid ${survivors.join(", ")}). A verdict cannot outlive the tree it describes.`);
10480
+ err2(`tamperward run: ${survivors.length} process(es) started during this run still hold the`);
10481
+ err2(`working tree (pid ${survivors.join(", ")}). A verdict cannot outlive the tree it describes.`);
10140
10482
  }
10141
10483
  const notQuiescent = mutatedDuringAdjudication || survivors.length > 0 || depsDrifted || rewrote;
10142
10484
  const cannot = !notQuiescent && (diffCode === 2 || workCode === 2 || verifyCode === 2);
@@ -10202,6 +10544,19 @@ function parseInit(args) {
10202
10544
  }
10203
10545
  return o;
10204
10546
  }
10547
+ function parseDoctor(args) {
10548
+ const o = {};
10549
+ for (let i = 0; i < args.length; i++) {
10550
+ const a = args[i];
10551
+ if (a === "--cwd") o.cwd = args[++i];
10552
+ else if (a === "--base") o.base = args[++i];
10553
+ else if (a === "--workflow") o.workflow = args[++i];
10554
+ else if (a === "--github") o.github = true;
10555
+ else if (a === "--repo") o.repo = args[++i];
10556
+ else if (a === "--branch") o.branch = args[++i];
10557
+ }
10558
+ return o;
10559
+ }
10205
10560
  function parseCheck(args) {
10206
10561
  const o = {};
10207
10562
  for (let i = 0; i < args.length; i++) {
@@ -10276,6 +10631,14 @@ Formats:
10276
10631
  every tool the gate must see.
10277
10632
  --force-workflow replaces a workflow it
10278
10633
  did not write, or one you have edited.
10634
+ tamperward doctor [--base R] validate the CI verifier's outer-time
10635
+ [--workflow F] [--cwd D] envelope against the trusted policy.
10636
+ [--github] [--repo O/R] --github also validates repository
10637
+ [--branch B] authority: required tamperward status,
10638
+ Code Owner review, and stale-review
10639
+ dismissal after new pushes. Public
10640
+ rules can be read anonymously; set
10641
+ GH_TOKEN/GITHUB_TOKEN when needed.
10279
10642
 
10280
10643
  Exit codes: 0 clean \xB7 1 a blocking finding (check), MASKED_FAILURE or SUITE_RED
10281
10644
  (verify), any blocking finding or masked failure (run) \xB7 2 cannot
@@ -10300,6 +10663,8 @@ function main(argv2) {
10300
10663
  return runAllow(parseAllow(rest));
10301
10664
  case "init":
10302
10665
  return runInit(parseInit(rest));
10666
+ case "doctor":
10667
+ return runDoctor(parseDoctor(rest));
10303
10668
  case "verify":
10304
10669
  return runVerify(parseVerify(rest));
10305
10670
  case "run":
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tamperward",
3
- "version": "2.11.3",
3
+ "version": "2.13.0",
4
4
  "description": "The deterministic agent-integrity gate. One ruleset, evaluated on the actual diff/commands as a verdict, enforced everywhere a change can be made.",
5
5
  "license": "Apache-2.0",
6
6
  "author": "hexrift",