tamperward 2.15.0 → 2.15.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -229,10 +229,19 @@ Since **2.15.0**, `tamperward doctor` is also the one-shot installation/posture
229
229
  report promised by `init`'s security model. It reuses `init`'s canonical wiring
230
230
  planner rather than maintaining a second definition of “installed correctly”, and
231
231
  reports named `OK`, `WARN`, or `BROKEN` checks for policy/schema, Claude hooks,
232
- pre-commit, generated CI wiring, CODEOWNERS, workflow permissions, binary/pin
232
+ pre-commit, CI authority wiring, CODEOWNERS, workflow permissions, binary/pin
233
233
  alignment, verifier trust mode + declared inputs, platform residuals, CI verifier
234
234
  outer time, observer health, and (with `--github`) repository authority. Use
235
235
  `--json` for one machine-readable document with `authoritative: true|false`.
236
+
237
+ **2.15.1 closes an authority-reporting gap in that surface.** The permission check now
238
+ includes job-level overrides, not only workflow-root permissions; a policy schema newer
239
+ than the running binary is BROKEN rather than certifiable; and the exact workflow(s)
240
+ selected by `--workflow` or discovered as verifier authorities are also the workflow(s)
241
+ used for CI-wiring and token-permission posture. A custom verifier workflow therefore
242
+ does not get judged against an unrelated/missing generated file, and a second verifier
243
+ workflow with a write-scoped job cannot hide behind a safe canonical workflow.
244
+
236
245
  Local early-layer gaps remain posture findings rather than silently changing the
237
246
  existing generated-CI exit contract; hard CI/GitHub validation failures still exit 2.
238
247
 
package/dist/cli/index.js CHANGED
@@ -8416,52 +8416,90 @@ function pinsInFile(path) {
8416
8416
  }
8417
8417
  return Array.from(src.matchAll(/\btamperward@((?:0|[1-9]\d*)\.(?:0|[1-9]\d*)\.(?:0|[1-9]\d*))\b/g), (m) => m[1]);
8418
8418
  }
8419
- function workflowPermissionCheck(cwd) {
8420
- const rel = ".github/workflows/tamperward.yml";
8421
- const path = join13(cwd, rel);
8422
- if (!existsSync10(path)) return { id: "workflow-permissions", state: "BROKEN", detail: `${rel} is missing` };
8423
- let doc;
8424
- try {
8425
- doc = parse3(readFileSync13(path, "utf8"));
8426
- } catch (e) {
8427
- return {
8428
- id: "workflow-permissions",
8429
- state: "BROKEN",
8430
- detail: `${rel} is not valid YAML (${e instanceof Error ? e.message : String(e)})`
8431
- };
8419
+ function permissionWrites(value) {
8420
+ if (typeof value === "string") {
8421
+ if (/write/i.test(value)) return [value];
8422
+ if (/^(?:read-all|none)$/i.test(value)) return [];
8423
+ return null;
8432
8424
  }
8433
- const root = asMapping(doc);
8434
- const permissions = asMapping(root?.permissions);
8435
- if (!permissions) {
8436
- return {
8437
- id: "workflow-permissions",
8438
- state: "BROKEN",
8439
- detail: "workflow does not declare least-privilege permissions; repository defaults could grant write access"
8440
- };
8425
+ const mapping = asMapping(value);
8426
+ if (!mapping) return null;
8427
+ return Object.entries(mapping).filter(([, permission]) => typeof permission === "string" && /write/i.test(permission)).map(([name, permission]) => `${name}=${String(permission)}`);
8428
+ }
8429
+ function workflowPermissionCheck(cwd, workflowRels = [".github/workflows/tamperward.yml"]) {
8430
+ const broken = [];
8431
+ const warnings = [];
8432
+ for (const rel of workflowRels) {
8433
+ const path = resolve4(cwd, rel);
8434
+ if (!existsSync10(path)) {
8435
+ broken.push(`${rel} is missing`);
8436
+ continue;
8437
+ }
8438
+ let doc;
8439
+ try {
8440
+ doc = parse3(readFileSync13(path, "utf8"));
8441
+ } catch (e) {
8442
+ broken.push(`${rel} is not valid YAML (${e instanceof Error ? e.message : String(e)})`);
8443
+ continue;
8444
+ }
8445
+ const root = asMapping(doc);
8446
+ if (!root) {
8447
+ broken.push(`${rel} is not a YAML mapping`);
8448
+ continue;
8449
+ }
8450
+ const rootPermissions = root.permissions;
8451
+ if (rootPermissions === void 0) {
8452
+ broken.push(`${rel}: workflow does not declare least-privilege permissions; repository defaults could grant write access`);
8453
+ } else {
8454
+ const writes = permissionWrites(rootPermissions);
8455
+ if (writes === null) {
8456
+ broken.push(`${rel}: workflow permissions shape is unsupported`);
8457
+ } else if (writes.length) {
8458
+ broken.push(`${rel}: workflow grants write permission: ${writes.join(", ")}`);
8459
+ } else {
8460
+ const mapping = asMapping(rootPermissions);
8461
+ if (mapping && mapping.contents !== "read") {
8462
+ warnings.push(`${rel}: workflow permissions contain no writes but contents: read is not declared`);
8463
+ }
8464
+ }
8465
+ }
8466
+ const jobs = asMapping(root.jobs);
8467
+ if (!jobs) continue;
8468
+ for (const [name, rawJob] of Object.entries(jobs)) {
8469
+ const job = asMapping(rawJob);
8470
+ if (!job || job.permissions === void 0) continue;
8471
+ const writes = permissionWrites(job.permissions);
8472
+ if (writes === null) {
8473
+ broken.push(`${rel} job "${name}": permissions shape is unsupported`);
8474
+ } else if (writes.length) {
8475
+ broken.push(`${rel} job "${name}" grants write permission: ${writes.join(", ")}`);
8476
+ }
8477
+ }
8441
8478
  }
8442
- const writes = Object.entries(permissions).filter(
8443
- ([, value]) => typeof value === "string" && /write/i.test(value)
8444
- );
8445
- if (writes.length) {
8479
+ if (broken.length) {
8446
8480
  return {
8447
8481
  id: "workflow-permissions",
8448
8482
  state: "BROKEN",
8449
- detail: "workflow grants write permission: " + writes.map(([name]) => name).join(", ")
8483
+ detail: broken.join("; ")
8450
8484
  };
8451
8485
  }
8452
- if (permissions.contents !== "read") {
8486
+ if (warnings.length) {
8453
8487
  return {
8454
8488
  id: "workflow-permissions",
8455
8489
  state: "WARN",
8456
- detail: "workflow permissions are explicit but contents: read is not declared"
8490
+ detail: warnings.join("; ")
8457
8491
  };
8458
8492
  }
8459
- return { id: "workflow-permissions", state: "OK", detail: "workflow token is explicitly read-only (contents: read)" };
8493
+ return {
8494
+ id: "workflow-permissions",
8495
+ state: "OK",
8496
+ detail: `${workflowRels.join(", ")}: workflow/job token permissions contain no write grants and workflow contents access is read-only`
8497
+ };
8460
8498
  }
8461
- function collectLocalPosture(cwd, policy) {
8499
+ function collectLocalPosture(cwd, policy, authorityWorkflows, explicitWorkflow = false) {
8462
8500
  const checks = [];
8463
8501
  checks.push(
8464
- policy.version <= POLICY_VERSION ? { id: "policy", state: "OK", detail: `policy version ${policy.version} is understood (current schema ${POLICY_VERSION})` } : { id: "policy", state: "WARN", detail: `policy version ${policy.version} is newer than this binary's schema ${POLICY_VERSION}; only known gates can be evaluated` }
8502
+ policy.version <= POLICY_VERSION ? { id: "policy", state: "OK", detail: `policy version ${policy.version} is understood (current schema ${POLICY_VERSION})` } : { id: "policy", state: "BROKEN", detail: `policy version ${policy.version} is newer than this binary's schema ${POLICY_VERSION}; authority cannot be certified for unknown policy semantics` }
8465
8503
  );
8466
8504
  let plan = [];
8467
8505
  try {
@@ -8488,9 +8526,29 @@ function collectLocalPosture(cwd, policy) {
8488
8526
  };
8489
8527
  addWiring("claude-hooks", "agent", true);
8490
8528
  addWiring("pre-commit", "pre-commit", false);
8491
- addWiring("ci-wiring", "ci", true);
8529
+ const canonicalWorkflow = ".github/workflows/tamperward.yml";
8530
+ if (authorityWorkflows?.length) {
8531
+ const normalized = authorityWorkflows.map((x) => x.replace(/\\/g, "/"));
8532
+ if (explicitWorkflow) {
8533
+ checks.push({
8534
+ id: "ci-wiring",
8535
+ state: "OK",
8536
+ detail: `explicit verifier authority workflow: ${normalized.join(", ")}; verifier step/timeout validated by doctor`
8537
+ });
8538
+ } else if (normalized.includes(canonicalWorkflow)) {
8539
+ addWiring("ci-wiring", "ci", true);
8540
+ } else {
8541
+ checks.push({
8542
+ id: "ci-wiring",
8543
+ state: "OK",
8544
+ detail: `discovered verifier authority workflow(s): ${normalized.join(", ")}; verifier step/timeout validated by doctor`
8545
+ });
8546
+ }
8547
+ } else {
8548
+ addWiring("ci-wiring", "ci", true);
8549
+ }
8492
8550
  addWiring("codeowners", "codeowners", true);
8493
- checks.push(workflowPermissionCheck(cwd));
8551
+ checks.push(workflowPermissionCheck(cwd, authorityWorkflows));
8494
8552
  const pins = [
8495
8553
  ...pinsInFile(join13(cwd, ".claude", "settings.json")),
8496
8554
  ...pinsInFile(join13(cwd, ".git", "hooks", "pre-commit")),
@@ -8744,7 +8802,13 @@ function runDoctor(opts = {}) {
8744
8802
  "policy"
8745
8803
  );
8746
8804
  }
8747
- const fail = (id, message) => err(message, opts, id, collectLocalPosture(cwd, policy));
8805
+ let postureWorkflows = opts.workflow ? [opts.workflow] : void 0;
8806
+ const fail = (id, message) => err(
8807
+ message,
8808
+ opts,
8809
+ id,
8810
+ collectLocalPosture(cwd, policy, postureWorkflows, Boolean(opts.workflow))
8811
+ );
8748
8812
  if (!policy.verify?.command) {
8749
8813
  return fail("verifier", "trusted policy has no verify.command; generated CI cannot verify this repository");
8750
8814
  }
@@ -8771,6 +8835,7 @@ function runDoctor(opts = {}) {
8771
8835
  const requiredSecs = requiredVerifierAuthoritySeconds(policy.verify.budget);
8772
8836
  const requiredMinutes = Math.ceil(requiredSecs / 60);
8773
8837
  let verifyJobCount = 0;
8838
+ const authorityWorkflows = [];
8774
8839
  for (const workflowRel of workflowRels) {
8775
8840
  const workflowPath = resolve4(cwd, workflowRel);
8776
8841
  if (!existsSync10(workflowPath)) {
@@ -8787,6 +8852,10 @@ function runDoctor(opts = {}) {
8787
8852
  }
8788
8853
  const jobs = verifyJobs(doc);
8789
8854
  verifyJobCount += jobs.length;
8855
+ if (jobs.length > 0) {
8856
+ authorityWorkflows.push(workflowRel);
8857
+ postureWorkflows = [...authorityWorkflows];
8858
+ }
8790
8859
  for (const { name, job } of jobs) {
8791
8860
  const timeout = job["timeout-minutes"];
8792
8861
  if (timeout === void 0 || timeout === null) {
@@ -8845,7 +8914,12 @@ function runDoctor(opts = {}) {
8845
8914
  );
8846
8915
  }
8847
8916
  }
8848
- const checks = collectLocalPosture(cwd, policy);
8917
+ const checks = collectLocalPosture(
8918
+ cwd,
8919
+ policy,
8920
+ authorityWorkflows,
8921
+ Boolean(opts.workflow)
8922
+ );
8849
8923
  checks.push({
8850
8924
  id: "ci-verifier",
8851
8925
  state: "OK",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tamperward",
3
- "version": "2.15.0",
3
+ "version": "2.15.1",
4
4
  "description": "The deterministic agent-integrity gate. One ruleset, evaluated on the actual diff/commands as a verdict, enforced everywhere a change can be made.",
5
5
  "license": "Apache-2.0",
6
6
  "author": "hexrift",