tamperward 2.14.1 → 2.15.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/README.md +12 -1
- package/dist/cli/index.js +224 -40
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -225,6 +225,17 @@ but this generated GitHub-hosted authority refuses deterministically and tells y
|
|
|
225
225
|
much outer time is required, so a custom runner/workflow can provide it instead of
|
|
226
226
|
GitHub killing verification mid-verdict.
|
|
227
227
|
|
|
228
|
+
Since **2.15.0**, `tamperward doctor` is also the one-shot installation/posture
|
|
229
|
+
report promised by `init`'s security model. It reuses `init`'s canonical wiring
|
|
230
|
+
planner rather than maintaining a second definition of “installed correctly”, and
|
|
231
|
+
reports named `OK`, `WARN`, or `BROKEN` checks for policy/schema, Claude hooks,
|
|
232
|
+
pre-commit, generated CI wiring, CODEOWNERS, workflow permissions, binary/pin
|
|
233
|
+
alignment, verifier trust mode + declared inputs, platform residuals, CI verifier
|
|
234
|
+
outer time, observer health, and (with `--github`) repository authority. Use
|
|
235
|
+
`--json` for one machine-readable document with `authoritative: true|false`.
|
|
236
|
+
Local early-layer gaps remain posture findings rather than silently changing the
|
|
237
|
+
existing generated-CI exit contract; hard CI/GitHub validation failures still exit 2.
|
|
238
|
+
|
|
228
239
|
Legitimate exceptions are out-of-band PR labels, `tamperward:allow:<rule>@<head-sha>`.
|
|
229
240
|
The workflow passes the head SHA to both steps through `TAMPERWARD_OOB_HEAD`, so an
|
|
230
241
|
approval is bound to the exact commit it was granted for and a new push invalidates it.
|
|
@@ -364,7 +375,7 @@ option can never be reinterpreted as the agent command.
|
|
|
364
375
|
| --- | --- |
|
|
365
376
|
| `check` | one view — `--staged` · `--worktree` · `--diff <base>...<head>` — plus `--format text\|json\|github\|auto` (default `auto`) · `--json` (alias for `--format json`) · `--cwd <dir>` |
|
|
366
377
|
| `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>` |
|
|
367
|
-
| `doctor` | `--base <rev>` (trusted policy revision) · `--workflow <path>`
|
|
378
|
+
| `doctor` | `--base <rev>` (trusted policy revision) · `--workflow <path>` · `--cwd <dir>` · `--json` · `--github` · `--repo <owner/repo>` · `--branch <name>` — read-only installation/authority posture plus CI verifier outer-time validation |
|
|
368
379
|
| `run` | `--base <rev>` · `--cmd <suite command>` · `--budget <seconds>` (per verifier suite) · `--agent-budget <seconds>` (optional wrapped-agent wall clock) · `--observe-transients` (start a session-scoped transient observer) · `--allow-dirty` · `--settle <seconds>` (wait before the final quiescence check) · `--allow-dep-drift` · `--cwd <dir>` · then `-- <agent command...>` |
|
|
369
380
|
| `allow` | `<rule>` · `--file <path>` · `--reason "<why>"` (required) · `--cwd <dir>` |
|
|
370
381
|
| `init` | `--cwd <dir>` · `--dry-run` · `--force-workflow` |
|
package/dist/cli/index.js
CHANGED
|
@@ -10,6 +10,7 @@ import { join } from "node:path";
|
|
|
10
10
|
// src/policy.ts
|
|
11
11
|
import picomatch from "picomatch";
|
|
12
12
|
var POLICY_FILE = ".tamperward.yml";
|
|
13
|
+
var POLICY_VERSION = 1;
|
|
13
14
|
var BLOCK_SINCE = {};
|
|
14
15
|
function applyVersionGates(rules, version, gates = BLOCK_SINCE) {
|
|
15
16
|
const out2 = { ...rules };
|
|
@@ -8375,9 +8376,21 @@ import { existsSync as existsSync10, readFileSync as readFileSync13, readdirSync
|
|
|
8375
8376
|
import { join as join13, resolve as resolve4 } from "node:path";
|
|
8376
8377
|
import { parse as parse3 } from "yaml";
|
|
8377
8378
|
var VERIFY_COMMAND = /\btamperward(?:@\S+)?\s+verify\b/;
|
|
8378
|
-
function err(message) {
|
|
8379
|
-
|
|
8379
|
+
function err(message, opts, id = "doctor", checks = []) {
|
|
8380
|
+
if (opts?.json) {
|
|
8381
|
+
const report2 = {
|
|
8382
|
+
command: "doctor",
|
|
8383
|
+
authoritative: false,
|
|
8384
|
+
checks: [
|
|
8385
|
+
...checks.filter((check2) => check2.id !== id),
|
|
8386
|
+
{ id, state: "BROKEN", detail: message }
|
|
8387
|
+
]
|
|
8388
|
+
};
|
|
8389
|
+
process.stdout.write(JSON.stringify(report2) + "\n");
|
|
8390
|
+
} else {
|
|
8391
|
+
process.stderr.write(`tamperward doctor: ${message}
|
|
8380
8392
|
`);
|
|
8393
|
+
}
|
|
8381
8394
|
return 2;
|
|
8382
8395
|
}
|
|
8383
8396
|
function policyFor(opts, cwd) {
|
|
@@ -8388,6 +8401,161 @@ function policyFor(opts, cwd) {
|
|
|
8388
8401
|
function asMapping(value) {
|
|
8389
8402
|
return typeof value === "object" && value !== null && !Array.isArray(value) ? value : null;
|
|
8390
8403
|
}
|
|
8404
|
+
function wiringState(status, authority) {
|
|
8405
|
+
if (status === "ok") return "OK";
|
|
8406
|
+
if (status === "skip") return "WARN";
|
|
8407
|
+
return authority ? "BROKEN" : "WARN";
|
|
8408
|
+
}
|
|
8409
|
+
function pinsInFile(path) {
|
|
8410
|
+
if (!existsSync10(path)) return [];
|
|
8411
|
+
let src = "";
|
|
8412
|
+
try {
|
|
8413
|
+
src = readFileSync13(path, "utf8");
|
|
8414
|
+
} catch {
|
|
8415
|
+
return [];
|
|
8416
|
+
}
|
|
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
|
+
}
|
|
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
|
+
};
|
|
8432
|
+
}
|
|
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
|
+
};
|
|
8441
|
+
}
|
|
8442
|
+
const writes = Object.entries(permissions).filter(
|
|
8443
|
+
([, value]) => typeof value === "string" && /write/i.test(value)
|
|
8444
|
+
);
|
|
8445
|
+
if (writes.length) {
|
|
8446
|
+
return {
|
|
8447
|
+
id: "workflow-permissions",
|
|
8448
|
+
state: "BROKEN",
|
|
8449
|
+
detail: "workflow grants write permission: " + writes.map(([name]) => name).join(", ")
|
|
8450
|
+
};
|
|
8451
|
+
}
|
|
8452
|
+
if (permissions.contents !== "read") {
|
|
8453
|
+
return {
|
|
8454
|
+
id: "workflow-permissions",
|
|
8455
|
+
state: "WARN",
|
|
8456
|
+
detail: "workflow permissions are explicit but contents: read is not declared"
|
|
8457
|
+
};
|
|
8458
|
+
}
|
|
8459
|
+
return { id: "workflow-permissions", state: "OK", detail: "workflow token is explicitly read-only (contents: read)" };
|
|
8460
|
+
}
|
|
8461
|
+
function collectLocalPosture(cwd, policy) {
|
|
8462
|
+
const checks = [];
|
|
8463
|
+
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` }
|
|
8465
|
+
);
|
|
8466
|
+
let plan = [];
|
|
8467
|
+
try {
|
|
8468
|
+
plan = planInit(cwd);
|
|
8469
|
+
} catch (e) {
|
|
8470
|
+
checks.push({
|
|
8471
|
+
id: "installation-plan",
|
|
8472
|
+
state: "BROKEN",
|
|
8473
|
+
detail: `canonical wiring could not be evaluated: ${e instanceof Error ? e.message : String(e)}`
|
|
8474
|
+
});
|
|
8475
|
+
}
|
|
8476
|
+
const byItem = new Map(plan.map((x) => [x.item, x]));
|
|
8477
|
+
const addWiring = (id, item, authority) => {
|
|
8478
|
+
const action = byItem.get(item);
|
|
8479
|
+
if (!action) {
|
|
8480
|
+
checks.push({ id, state: authority ? "BROKEN" : "WARN", detail: `${item} wiring could not be evaluated` });
|
|
8481
|
+
return;
|
|
8482
|
+
}
|
|
8483
|
+
checks.push({
|
|
8484
|
+
id,
|
|
8485
|
+
state: wiringState(action.status, authority),
|
|
8486
|
+
detail: action.status === "ok" ? action.detail : `${action.status}: ${action.detail}`
|
|
8487
|
+
});
|
|
8488
|
+
};
|
|
8489
|
+
addWiring("claude-hooks", "agent", true);
|
|
8490
|
+
addWiring("pre-commit", "pre-commit", false);
|
|
8491
|
+
addWiring("ci-wiring", "ci", true);
|
|
8492
|
+
addWiring("codeowners", "codeowners", true);
|
|
8493
|
+
checks.push(workflowPermissionCheck(cwd));
|
|
8494
|
+
const pins = [
|
|
8495
|
+
...pinsInFile(join13(cwd, ".claude", "settings.json")),
|
|
8496
|
+
...pinsInFile(join13(cwd, ".git", "hooks", "pre-commit")),
|
|
8497
|
+
...pinsInFile(join13(cwd, ".husky", "pre-commit"))
|
|
8498
|
+
];
|
|
8499
|
+
const below = pins.filter((pin) => (compareVersions(pin, TW_VERSION) ?? 0) < 0);
|
|
8500
|
+
const above = pins.filter((pin) => (compareVersions(TW_VERSION, pin) ?? 0) < 0);
|
|
8501
|
+
checks.push(
|
|
8502
|
+
below.length ? { id: "binary-version", state: "BROKEN", detail: `wiring pins older TamperWard version(s): ${Array.from(new Set(below)).join(", ")}; running binary is ${TW_VERSION}` } : above.length ? { id: "binary-version", state: "WARN", detail: `repository wiring targets newer TamperWard version(s): ${Array.from(new Set(above)).join(", ")}; running binary is ${TW_VERSION}` } : pins.length ? { id: "binary-version", state: "OK", detail: `wiring pins agree with running TamperWard ${TW_VERSION}` } : { id: "binary-version", state: "WARN", detail: `no canonical TamperWard version pin was found; running binary is ${TW_VERSION}` }
|
|
8503
|
+
);
|
|
8504
|
+
if (!policy.verify?.command) {
|
|
8505
|
+
checks.push({ id: "verifier", state: "BROKEN", detail: "verify.command is not configured" });
|
|
8506
|
+
} else {
|
|
8507
|
+
const backend = policy.verify.backend ?? "local";
|
|
8508
|
+
const inputs = policy.verify.inputs?.length ?? 0;
|
|
8509
|
+
checks.push({
|
|
8510
|
+
id: "verifier",
|
|
8511
|
+
state: backend === "container" ? "OK" : "WARN",
|
|
8512
|
+
detail: backend === "container" ? `isolated-container verifier; budget ${policy.verify.budget}s/stage; ${inputs} declared verify.inputs glob(s); runtime/image availability is checked by verify` : `checkpointed-local verifier; budget ${policy.verify.budget}s/stage; ${inputs} declared verify.inputs glob(s); same-host/self-restoring mutation remains a documented residual`
|
|
8513
|
+
});
|
|
8514
|
+
}
|
|
8515
|
+
checks.push(
|
|
8516
|
+
process.platform === "linux" ? { id: "platform", state: "OK", detail: "Linux: POSIX generated wiring and /proc runtime/quiescence controls are available" } : process.platform === "win32" ? { id: "platform", state: "WARN", detail: "Windows: generated shell wiring has POSIX assumptions and Linux /proc survivor controls are unavailable" } : { id: "platform", state: "WARN", detail: `${process.platform}: POSIX wiring is available, but Linux /proc survivor controls are unavailable` }
|
|
8517
|
+
);
|
|
8518
|
+
return checks;
|
|
8519
|
+
}
|
|
8520
|
+
function observerCheck(cwd) {
|
|
8521
|
+
const observer = watcherTelemetry(defaultEventLog(cwd));
|
|
8522
|
+
if (observer.state === "healthy" && observer.health) {
|
|
8523
|
+
return {
|
|
8524
|
+
id: "observer",
|
|
8525
|
+
state: "OK",
|
|
8526
|
+
detail: `healthy ${observer.health.backend}; ${observer.health.watched_dirs} watched dir(s), ${observer.health.event_count} event(s); advisory telemetry only`
|
|
8527
|
+
};
|
|
8528
|
+
}
|
|
8529
|
+
if (observer.state === "degraded" && observer.health) {
|
|
8530
|
+
return {
|
|
8531
|
+
id: "observer",
|
|
8532
|
+
state: "WARN",
|
|
8533
|
+
detail: `degraded: ${observer.health.error_count} error(s), ${observer.health.dropped_events} dropped event(s); ${observer.reason ?? "telemetry may be incomplete"}; advisory only`
|
|
8534
|
+
};
|
|
8535
|
+
}
|
|
8536
|
+
return {
|
|
8537
|
+
id: "observer",
|
|
8538
|
+
state: "WARN",
|
|
8539
|
+
detail: `unavailable${observer.reason ? ": " + observer.reason : ""}; optional/advisory, and zero events are not evidence of no transient activity`
|
|
8540
|
+
};
|
|
8541
|
+
}
|
|
8542
|
+
function emitReport(opts, checks) {
|
|
8543
|
+
const authoritative = !checks.some((x) => x.state === "BROKEN");
|
|
8544
|
+
if (opts.json) {
|
|
8545
|
+
const report2 = { command: "doctor", authoritative, checks };
|
|
8546
|
+
process.stdout.write(JSON.stringify(report2) + "\n");
|
|
8547
|
+
return;
|
|
8548
|
+
}
|
|
8549
|
+
for (const check2 of checks) {
|
|
8550
|
+
if (check2.id === "observer") {
|
|
8551
|
+
process.stdout.write(`tamperward doctor: transient observer: ${check2.detail}
|
|
8552
|
+
`);
|
|
8553
|
+
} else {
|
|
8554
|
+
process.stdout.write(`tamperward doctor: [${check2.state}] ${check2.id} \u2014 ${check2.detail}
|
|
8555
|
+
`);
|
|
8556
|
+
}
|
|
8557
|
+
}
|
|
8558
|
+
}
|
|
8391
8559
|
function evaluateGitHubProtection(snapshot2, requiredCheck = "tamperward") {
|
|
8392
8560
|
let hasRequiredCheck = false;
|
|
8393
8561
|
let hasCodeOwnerReview = false;
|
|
@@ -8570,10 +8738,15 @@ function runDoctor(opts = {}) {
|
|
|
8570
8738
|
try {
|
|
8571
8739
|
policy = policyFor(opts, cwd);
|
|
8572
8740
|
} catch (e) {
|
|
8573
|
-
return err(
|
|
8741
|
+
return err(
|
|
8742
|
+
e instanceof PolicyError || e instanceof Error ? e.message : String(e),
|
|
8743
|
+
opts,
|
|
8744
|
+
"policy"
|
|
8745
|
+
);
|
|
8574
8746
|
}
|
|
8747
|
+
const fail = (id, message) => err(message, opts, id, collectLocalPosture(cwd, policy));
|
|
8575
8748
|
if (!policy.verify?.command) {
|
|
8576
|
-
return
|
|
8749
|
+
return fail("verifier", "trusted policy has no verify.command; generated CI cannot verify this repository");
|
|
8577
8750
|
}
|
|
8578
8751
|
const workflowRels = [];
|
|
8579
8752
|
if (opts.workflow) {
|
|
@@ -8582,13 +8755,14 @@ function runDoctor(opts = {}) {
|
|
|
8582
8755
|
const workflowDirRel = ".github/workflows";
|
|
8583
8756
|
const workflowDir = resolve4(cwd, workflowDirRel);
|
|
8584
8757
|
if (!existsSync10(workflowDir)) {
|
|
8585
|
-
return
|
|
8758
|
+
return fail("ci-verifier", `${workflowDirRel}: workflow directory does not exist`);
|
|
8586
8759
|
}
|
|
8587
8760
|
let entries;
|
|
8588
8761
|
try {
|
|
8589
8762
|
entries = readdirSync3(workflowDir).filter((name) => /\.ya?ml$/i.test(name)).sort();
|
|
8590
8763
|
} catch (e) {
|
|
8591
|
-
return
|
|
8764
|
+
return fail(
|
|
8765
|
+
"ci-verifier",
|
|
8592
8766
|
`${workflowDirRel}: could not enumerate workflows (${e instanceof Error ? e.message : String(e)})`
|
|
8593
8767
|
);
|
|
8594
8768
|
}
|
|
@@ -8600,13 +8774,14 @@ function runDoctor(opts = {}) {
|
|
|
8600
8774
|
for (const workflowRel of workflowRels) {
|
|
8601
8775
|
const workflowPath = resolve4(cwd, workflowRel);
|
|
8602
8776
|
if (!existsSync10(workflowPath)) {
|
|
8603
|
-
return
|
|
8777
|
+
return fail("ci-verifier", `${workflowRel}: workflow does not exist`);
|
|
8604
8778
|
}
|
|
8605
8779
|
let doc;
|
|
8606
8780
|
try {
|
|
8607
8781
|
doc = parse3(readFileSync13(workflowPath, "utf8"));
|
|
8608
8782
|
} catch (e) {
|
|
8609
|
-
return
|
|
8783
|
+
return fail(
|
|
8784
|
+
"ci-verifier",
|
|
8610
8785
|
`${workflowRel}: workflow is not valid YAML (${e instanceof Error ? e.message : String(e)})`
|
|
8611
8786
|
);
|
|
8612
8787
|
}
|
|
@@ -8615,32 +8790,38 @@ function runDoctor(opts = {}) {
|
|
|
8615
8790
|
for (const { name, job } of jobs) {
|
|
8616
8791
|
const timeout = job["timeout-minutes"];
|
|
8617
8792
|
if (timeout === void 0 || timeout === null) {
|
|
8618
|
-
return
|
|
8793
|
+
return fail(
|
|
8794
|
+
"ci-verifier",
|
|
8619
8795
|
`${workflowRel} job "${name}": timeout-minutes is missing; requires at least ${requiredMinutes} minutes`
|
|
8620
8796
|
);
|
|
8621
8797
|
}
|
|
8622
8798
|
if (typeof timeout === "string" && timeout.includes("${{")) {
|
|
8623
|
-
return
|
|
8799
|
+
return fail(
|
|
8800
|
+
"ci-verifier",
|
|
8624
8801
|
`${workflowRel} job "${name}": timeout-minutes must be a static numeric value; requires at least ${requiredMinutes} minutes`
|
|
8625
8802
|
);
|
|
8626
8803
|
}
|
|
8627
8804
|
if (typeof timeout !== "number" || !Number.isFinite(timeout)) {
|
|
8628
|
-
return
|
|
8805
|
+
return fail(
|
|
8806
|
+
"ci-verifier",
|
|
8629
8807
|
`${workflowRel} job "${name}": timeout-minutes must be numeric; got ${JSON.stringify(timeout)}`
|
|
8630
8808
|
);
|
|
8631
8809
|
}
|
|
8632
8810
|
if (timeout <= 0) {
|
|
8633
|
-
return
|
|
8811
|
+
return fail(
|
|
8812
|
+
"ci-verifier",
|
|
8634
8813
|
`${workflowRel} job "${name}": timeout-minutes must be positive; got ${timeout}`
|
|
8635
8814
|
);
|
|
8636
8815
|
}
|
|
8637
8816
|
if (!Number.isInteger(timeout)) {
|
|
8638
|
-
return
|
|
8817
|
+
return fail(
|
|
8818
|
+
"ci-verifier",
|
|
8639
8819
|
`${workflowRel} job "${name}": timeout-minutes must be a whole number; got ${timeout}`
|
|
8640
8820
|
);
|
|
8641
8821
|
}
|
|
8642
8822
|
if (timeout < requiredMinutes) {
|
|
8643
|
-
return
|
|
8823
|
+
return fail(
|
|
8824
|
+
"ci-verifier",
|
|
8644
8825
|
`${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`
|
|
8645
8826
|
);
|
|
8646
8827
|
}
|
|
@@ -8648,47 +8829,48 @@ function runDoctor(opts = {}) {
|
|
|
8648
8829
|
}
|
|
8649
8830
|
if (verifyJobCount === 0) {
|
|
8650
8831
|
const scope = opts.workflow ?? ".github/workflows/*.yml|*.yaml";
|
|
8651
|
-
return
|
|
8832
|
+
return fail("ci-verifier", `${scope}: no job contains a tamperward verify step`);
|
|
8652
8833
|
}
|
|
8653
8834
|
let github = null;
|
|
8654
8835
|
if (opts.github) {
|
|
8655
8836
|
try {
|
|
8656
8837
|
github = githubAuthority(opts, cwd);
|
|
8657
8838
|
} catch (e) {
|
|
8658
|
-
return
|
|
8839
|
+
return fail("github-authority", e instanceof Error ? e.message : String(e));
|
|
8659
8840
|
}
|
|
8660
8841
|
if (github.findings.length > 0) {
|
|
8661
|
-
return
|
|
8842
|
+
return fail(
|
|
8843
|
+
"github-authority",
|
|
8662
8844
|
"GitHub repository authority for " + github.repo + "#" + github.branch + " is incomplete: " + github.findings.join("; ")
|
|
8663
8845
|
);
|
|
8664
8846
|
}
|
|
8665
8847
|
}
|
|
8666
|
-
|
|
8667
|
-
|
|
8668
|
-
|
|
8669
|
-
|
|
8848
|
+
const checks = collectLocalPosture(cwd, policy);
|
|
8849
|
+
checks.push({
|
|
8850
|
+
id: "ci-verifier",
|
|
8851
|
+
state: "OK",
|
|
8852
|
+
detail: `${verifyJobCount} verify job(s); trusted budget ${policy.verify.budget}s/stage requires >=${requiredMinutes}m outer timeout`
|
|
8853
|
+
});
|
|
8670
8854
|
if (github) {
|
|
8671
|
-
|
|
8672
|
-
|
|
8673
|
-
|
|
8855
|
+
checks.push({
|
|
8856
|
+
id: "github-authority",
|
|
8857
|
+
state: "OK",
|
|
8858
|
+
detail: github.repo + "#" + github.branch + " requires tamperward status, Code Owner review, and stale-review dismissal on new pushes"
|
|
8859
|
+
});
|
|
8674
8860
|
}
|
|
8675
|
-
|
|
8676
|
-
if (
|
|
8861
|
+
checks.push(observerCheck(cwd));
|
|
8862
|
+
if (!opts.json) {
|
|
8677
8863
|
process.stdout.write(
|
|
8678
|
-
`tamperward doctor:
|
|
8679
|
-
`
|
|
8680
|
-
);
|
|
8681
|
-
} else if (observer.state === "degraded" && observer.health) {
|
|
8682
|
-
process.stdout.write(
|
|
8683
|
-
`tamperward doctor: transient observer: degraded \u2014 ${observer.health.error_count} error(s), ${observer.health.dropped_events} dropped event(s); ${observer.reason ?? "telemetry may be incomplete"}. Advisory only; zero events are not evidence of no transient activity.
|
|
8684
|
-
`
|
|
8685
|
-
);
|
|
8686
|
-
} else {
|
|
8687
|
-
process.stdout.write(
|
|
8688
|
-
`tamperward doctor: transient observer: unavailable${observer.reason ? " \u2014 " + observer.reason : ""}. Observer telemetry is optional/advisory; zero events are not evidence of no transient activity.
|
|
8864
|
+
`tamperward doctor: CI verifier envelope OK \u2014 ${verifyJobCount} verify job(s), trusted budget ${policy.verify.budget}s/stage, requires >=${requiredMinutes}m outer timeout.
|
|
8689
8865
|
`
|
|
8690
8866
|
);
|
|
8867
|
+
if (github) {
|
|
8868
|
+
process.stdout.write(
|
|
8869
|
+
"tamperward doctor: GitHub repository authority OK \u2014 " + github.repo + "#" + github.branch + " requires tamperward, Code Owner review, and stale-review dismissal on new pushes.\n"
|
|
8870
|
+
);
|
|
8871
|
+
}
|
|
8691
8872
|
}
|
|
8873
|
+
emitReport(opts, checks);
|
|
8692
8874
|
return 0;
|
|
8693
8875
|
}
|
|
8694
8876
|
|
|
@@ -10852,6 +11034,7 @@ function parseDoctor(args) {
|
|
|
10852
11034
|
else if (a === "--base") o.base = args[++i];
|
|
10853
11035
|
else if (a === "--workflow") o.workflow = args[++i];
|
|
10854
11036
|
else if (a === "--github") o.github = true;
|
|
11037
|
+
else if (a === "--json") o.json = true;
|
|
10855
11038
|
else if (a === "--repo") o.repo = args[++i];
|
|
10856
11039
|
else if (a === "--branch") o.branch = args[++i];
|
|
10857
11040
|
}
|
|
@@ -10981,7 +11164,7 @@ function validateCliArgs(cmd, args) {
|
|
|
10981
11164
|
}
|
|
10982
11165
|
if (cmd === "doctor") {
|
|
10983
11166
|
return validateFlatArgs(args, {
|
|
10984
|
-
flags: ["--github"],
|
|
11167
|
+
flags: ["--github", "--json"],
|
|
10985
11168
|
values: {
|
|
10986
11169
|
"--cwd": "string",
|
|
10987
11170
|
"--base": "string",
|
|
@@ -11068,8 +11251,9 @@ Formats:
|
|
|
11068
11251
|
every tool the gate must see.
|
|
11069
11252
|
--force-workflow replaces a workflow it
|
|
11070
11253
|
did not write, or one you have edited.
|
|
11071
|
-
tamperward doctor [--base R]
|
|
11072
|
-
[--workflow F] [--cwd D]
|
|
11254
|
+
tamperward doctor [--base R] report installation + authority posture
|
|
11255
|
+
[--workflow F] [--cwd D] and validate the CI verifier's outer-time
|
|
11256
|
+
[--json] envelope against the trusted policy.
|
|
11073
11257
|
[--github] [--repo O/R] --github also validates repository
|
|
11074
11258
|
[--branch B] authority: required tamperward status,
|
|
11075
11259
|
Code Owner review, and stale-review
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tamperward",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.15.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",
|