tamperward 2.23.0 → 2.23.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/index.js +285 -246
- package/package.json +1 -1
package/dist/cli/index.js
CHANGED
|
@@ -10436,7 +10436,7 @@ function planInit(cwd, opts = {}) {
|
|
|
10436
10436
|
function runInit(opts) {
|
|
10437
10437
|
const cwd = opts.cwd ?? process.cwd();
|
|
10438
10438
|
const plan = planInit(cwd, { forceWorkflow: opts.forceWorkflow });
|
|
10439
|
-
const w = process.stdout;
|
|
10439
|
+
const w = opts.quiet ? { write: (_text) => true } : process.stdout;
|
|
10440
10440
|
let applied = 0;
|
|
10441
10441
|
for (const a of plan) {
|
|
10442
10442
|
if (!opts.dryRun && a.apply) {
|
|
@@ -14621,7 +14621,7 @@ import { execFileSync as execFileSync12 } from "node:child_process";
|
|
|
14621
14621
|
import { randomBytes as randomBytes2 } from "node:crypto";
|
|
14622
14622
|
import { lstatSync as lstatSync9, mkdtempSync as mkdtempSync6, readFileSync as readFileSync20, realpathSync as realpathSync9, renameSync as renameSync2, rmSync as rmSync8, writeFileSync as writeFileSync11 } from "node:fs";
|
|
14623
14623
|
import { tmpdir as tmpdir7 } from "node:os";
|
|
14624
|
-
import { join as join22, resolve as resolve10, sep as sep5 } from "node:path";
|
|
14624
|
+
import { join as join22, relative as relative7, resolve as resolve10, sep as sep5 } from "node:path";
|
|
14625
14625
|
import { createInterface } from "node:readline";
|
|
14626
14626
|
import { parseDocument } from "yaml";
|
|
14627
14627
|
function readlineAsker(input, output) {
|
|
@@ -14651,6 +14651,23 @@ function readlineAsker(input, output) {
|
|
|
14651
14651
|
};
|
|
14652
14652
|
return ask;
|
|
14653
14653
|
}
|
|
14654
|
+
function paint2(text, code2, on) {
|
|
14655
|
+
return on ? code2 + text + RESET2 : text;
|
|
14656
|
+
}
|
|
14657
|
+
function terminalText(text) {
|
|
14658
|
+
return stripControl(text).replace(/\s+/g, " ").trim();
|
|
14659
|
+
}
|
|
14660
|
+
function committableSetupPath(cwd, path) {
|
|
14661
|
+
const rel = relative7(cwd, resolve10(cwd, path));
|
|
14662
|
+
if (!rel || rel === ".." || rel.startsWith(".." + sep5)) return false;
|
|
14663
|
+
return rel !== ".git" && !rel.startsWith(".git" + sep5);
|
|
14664
|
+
}
|
|
14665
|
+
function platformLabel(platform) {
|
|
14666
|
+
if (platform === "darwin") return "macOS";
|
|
14667
|
+
if (platform === "win32") return "Windows";
|
|
14668
|
+
if (platform === "linux") return "Linux";
|
|
14669
|
+
return platform;
|
|
14670
|
+
}
|
|
14654
14671
|
function git5(cwd, args) {
|
|
14655
14672
|
try {
|
|
14656
14673
|
return execFileSync12("git", args, { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
|
|
@@ -14662,27 +14679,13 @@ function explainVerdict(v, code2) {
|
|
|
14662
14679
|
const verdict2 = v?.verdict ?? (code2 === 0 ? "VERIFIED" : code2 === 1 ? "SUITE_RED" : "CANNOT_VERIFY");
|
|
14663
14680
|
switch (verdict2) {
|
|
14664
14681
|
case "VERIFIED":
|
|
14665
|
-
return
|
|
14666
|
-
"VERIFIED: your suite passes, and still passes with every protected test, snapshot and",
|
|
14667
|
-
"config file restored from the base. Nothing was masked. Exit 0."
|
|
14668
|
-
];
|
|
14682
|
+
return "Verification passed \u2014 visible and pristine suites are green.";
|
|
14669
14683
|
case "SUITE_RED":
|
|
14670
|
-
return
|
|
14671
|
-
"SUITE_RED: your suite fails as it is. That is not a TamperWard finding \u2014 fix the code",
|
|
14672
|
-
"(or the suite) first; verify exits 1 until the visible run is green."
|
|
14673
|
-
];
|
|
14684
|
+
return "Your test suite is red. Fix it, then run verification again.";
|
|
14674
14685
|
case "MASKED_FAILURE":
|
|
14675
|
-
return
|
|
14676
|
-
"MASKED_FAILURE: your suite passes as it is, but FAILS once the protected files are restored",
|
|
14677
|
-
"from the base. Something weakened the checks; the code does not pass the original suite.",
|
|
14678
|
-
"Exit 1. In CI only a reviewer label (`tamperward:allow:verify@<head-sha>`) clears it."
|
|
14679
|
-
];
|
|
14686
|
+
return "Verification blocked \u2014 the visible suite passes, but the pristine suite fails.";
|
|
14680
14687
|
default:
|
|
14681
|
-
return
|
|
14682
|
-
`CANNOT VERIFY${v?.reason ? ` (${v.reason})` : ""}: TamperWard could not verify this repository and fails closed`,
|
|
14683
|
-
"(exit 2) rather than reporting success. Nothing above is a verdict about your code.",
|
|
14684
|
-
...v?.detail ? [` ${v.detail}`] : []
|
|
14685
|
-
];
|
|
14688
|
+
return `Could not verify${v?.reason ? ` (${v.reason})` : ""} \u2014 TamperWard failed closed${v?.detail ? `: ${v.detail}` : "."}`;
|
|
14686
14689
|
}
|
|
14687
14690
|
}
|
|
14688
14691
|
function atomicReplaceFile(path, content, mode) {
|
|
@@ -14724,10 +14727,11 @@ function writeVerifyCommand(cwd, command) {
|
|
|
14724
14727
|
}
|
|
14725
14728
|
}
|
|
14726
14729
|
async function runOnboard(opts, io = {}) {
|
|
14727
|
-
const
|
|
14730
|
+
const requestedCwd = resolve10(opts.cwd ?? process.cwd());
|
|
14728
14731
|
const out3 = io.out ?? ((line) => void process.stdout.write(line + "\n"));
|
|
14729
|
-
const
|
|
14732
|
+
const rawErr = (line) => void process.stderr.write(terminalText(line) + "\n");
|
|
14730
14733
|
const platform = io.platform ?? process.platform;
|
|
14734
|
+
const colour = io.colour ?? colourEnabled(process.env, process.stdout);
|
|
14731
14735
|
const runners = {
|
|
14732
14736
|
init: runInit,
|
|
14733
14737
|
verify: runVerify,
|
|
@@ -14737,35 +14741,60 @@ async function runOnboard(opts, io = {}) {
|
|
|
14737
14741
|
};
|
|
14738
14742
|
const interactive = io.interactive ?? (Boolean(process.stdin.isTTY) && !process.env.CI && !process.env.GITHUB_ACTIONS);
|
|
14739
14743
|
const scripted = Boolean(opts.yes);
|
|
14744
|
+
const tone = (kind) => {
|
|
14745
|
+
if (kind === "ok") return GREEN2;
|
|
14746
|
+
if (kind === "warn") return YELLOW2;
|
|
14747
|
+
if (kind === "bad") return RED2;
|
|
14748
|
+
if (kind === "info") return CYAN;
|
|
14749
|
+
return DIM2;
|
|
14750
|
+
};
|
|
14751
|
+
const status = (label, text, kind = "info") => {
|
|
14752
|
+
out3(paint2(label.padEnd(8), (kind === "bad" ? BOLD2 : "") + tone(kind), colour) + " " + terminalText(text));
|
|
14753
|
+
};
|
|
14754
|
+
const fail = (text) => rawErr(paint2("ERROR ", BOLD2 + RED2, colour) + " " + text);
|
|
14740
14755
|
if (!interactive && !scripted) {
|
|
14741
|
-
|
|
14742
|
-
|
|
14743
|
-
|
|
14744
|
-
err3("or run the deterministic primitive directly: tamperward init.");
|
|
14756
|
+
fail("onboard needs an interactive terminal.");
|
|
14757
|
+
rawErr('Use `tamperward onboard --yes --verify-command "<suite command>"` for scripted setup,');
|
|
14758
|
+
rawErr("or run `tamperward init` for the deterministic wiring step only.");
|
|
14745
14759
|
return 2;
|
|
14746
14760
|
}
|
|
14747
|
-
|
|
14748
|
-
|
|
14761
|
+
const rootText = git5(requestedCwd, ["rev-parse", "--show-toplevel"])?.trim() ?? "";
|
|
14762
|
+
if (!rootText) {
|
|
14763
|
+
fail(requestedCwd + " is not inside a Git repository. Run `git init` first.");
|
|
14764
|
+
return 2;
|
|
14765
|
+
}
|
|
14766
|
+
const cwd = resolve10(rootText);
|
|
14767
|
+
let sameRoot = cwd === requestedCwd;
|
|
14768
|
+
try {
|
|
14769
|
+
sameRoot = realpathSync9(cwd) === realpathSync9(requestedCwd);
|
|
14770
|
+
} catch {
|
|
14771
|
+
}
|
|
14772
|
+
if (!sameRoot) {
|
|
14773
|
+
fail("current directory is not the Git repository root.");
|
|
14774
|
+
rawErr("Current: " + requestedCwd);
|
|
14775
|
+
rawErr("Git root: " + cwd);
|
|
14776
|
+
rawErr("TamperWard installs repository-wide hooks and CI, so it will not mix a child directory with its parent repository.");
|
|
14777
|
+
rawErr("Run from the Git root, or run `git init` in the child directory if it should be a separate repository.");
|
|
14749
14778
|
return 2;
|
|
14750
14779
|
}
|
|
14751
14780
|
const prompt = { asker: null };
|
|
14752
14781
|
const rawAsk = io.ask ?? ((q) => (prompt.asker ??= readlineAsker(process.stdin, process.stdout))(q));
|
|
14753
14782
|
const ask = async (question) => {
|
|
14754
|
-
const answer = await rawAsk(question);
|
|
14783
|
+
const answer = await rawAsk(terminalText(question) + (question.endsWith(" ") ? " " : ""));
|
|
14755
14784
|
if (answer === null) throw new Aborted();
|
|
14756
14785
|
return answer.trim();
|
|
14757
14786
|
};
|
|
14758
14787
|
const confirm = async (question, fallback, scriptedAnswer) => {
|
|
14759
14788
|
if (scripted) return scriptedAnswer;
|
|
14760
|
-
const a = (await ask(
|
|
14789
|
+
const a = (await ask(question + " " + (fallback ? "[Y/n]" : "[y/N]") + " ")).toLowerCase();
|
|
14761
14790
|
if (a === "") return fallback;
|
|
14762
14791
|
return a === "y" || a === "yes";
|
|
14763
14792
|
};
|
|
14764
|
-
let
|
|
14765
|
-
const
|
|
14766
|
-
|
|
14793
|
+
let sectionNo = 0;
|
|
14794
|
+
const section = (n) => {
|
|
14795
|
+
sectionNo = n;
|
|
14767
14796
|
out3("");
|
|
14768
|
-
out3(
|
|
14797
|
+
out3(paint2(n + "/" + SECTIONS.length + " " + SECTIONS[n - 1], BOLD2 + CYAN, colour));
|
|
14769
14798
|
};
|
|
14770
14799
|
const wrote = [];
|
|
14771
14800
|
let verifyCommand;
|
|
@@ -14775,188 +14804,180 @@ async function runOnboard(opts, io = {}) {
|
|
|
14775
14804
|
let declined = false;
|
|
14776
14805
|
let doctorOutcome = null;
|
|
14777
14806
|
try {
|
|
14778
|
-
|
|
14779
|
-
out3(
|
|
14807
|
+
out3(paint2("TamperWard onboarding", BOLD2, colour));
|
|
14808
|
+
out3(paint2("v" + TW_VERSION + " \xB7 Node " + process.versions.node + " \xB7 " + platformLabel(platform) + "/" + process.arch, DIM2, colour));
|
|
14809
|
+
out3(paint2(terminalText(cwd), DIM2, colour));
|
|
14810
|
+
section(1);
|
|
14780
14811
|
const lifecycle = lifecyclePlatformCheck(platform);
|
|
14781
|
-
out3(`platform: [${lifecycle.state}] ${lifecycle.detail}`);
|
|
14782
14812
|
const localVerifySupported = localVerifierShell(platform, "true") !== null;
|
|
14783
|
-
if (
|
|
14784
|
-
|
|
14785
|
-
|
|
14813
|
+
if (platform === "linux" && lifecycle.state === "OK") {
|
|
14814
|
+
status("OK", "Full check / verify / run support is available.", "ok");
|
|
14815
|
+
} else if (localVerifySupported) {
|
|
14816
|
+
status("LIMITED", platformLabel(platform) + ": check + verify work here; `tamperward run` requires Linux in this release.", "warn");
|
|
14817
|
+
} else {
|
|
14818
|
+
status("LIMITED", platformLabel(platform) + ": check works here; local verify and `run` are unavailable. Use the container verifier for final verification.", "warn");
|
|
14786
14819
|
}
|
|
14787
14820
|
const head = git5(cwd, ["rev-parse", "--verify", "--quiet", "HEAD^{commit}"])?.trim() ?? null;
|
|
14821
|
+
const treeStatus = git5(cwd, ["status", "--porcelain", "--untracked-files=all"]) ?? "";
|
|
14822
|
+
const dirty = treeStatus.split("\n").filter(Boolean).map((line) => line.slice(3).split(" -> ").at(-1) ?? "");
|
|
14788
14823
|
if (!head) {
|
|
14789
|
-
|
|
14790
|
-
|
|
14791
|
-
|
|
14792
|
-
|
|
14793
|
-
|
|
14794
|
-
|
|
14795
|
-
if (dirty.length && foreign.length === 0) {
|
|
14796
|
-
out3(`working tree: ${dirty.length} uncommitted path(s), all written by init \u2014 commit them so the gate travels with the repository.`);
|
|
14797
|
-
} else if (dirty.length) {
|
|
14798
|
-
out3(`working tree is not clean: ${dirty.length} changed or untracked path(s). Onboarding never stashes,`);
|
|
14799
|
-
out3("resets or edits your changes, but a clean tree makes the first verification easier to read:");
|
|
14800
|
-
out3("commit or stash your work first for the cleanest result.");
|
|
14801
|
-
if (!await confirm("Continue with a dirty working tree?", false, true)) {
|
|
14802
|
-
out3("Stopped before writing anything. Commit or stash your work, then re-run: tamperward onboard");
|
|
14803
|
-
return 2;
|
|
14804
|
-
}
|
|
14824
|
+
const count = dirty.length;
|
|
14825
|
+
status(
|
|
14826
|
+
"ACTION",
|
|
14827
|
+
count ? "No first commit yet; " + count + " path(s) are not part of a committed baseline. Create the initial commit before pristine verification or the safe demo." : "No first commit yet. Create the initial commit before pristine verification or the safe demo.",
|
|
14828
|
+
"warn"
|
|
14829
|
+
);
|
|
14805
14830
|
} else {
|
|
14806
|
-
|
|
14831
|
+
const owned = new Set(planInit(cwd).map((a) => a.path));
|
|
14832
|
+
const foreign = dirty.filter((p) => !owned.has(p));
|
|
14833
|
+
if (dirty.length === 0) {
|
|
14834
|
+
status("OK", "Working tree is clean.", "ok");
|
|
14835
|
+
} else if (foreign.length === 0) {
|
|
14836
|
+
status("ACTION", dirty.length + " uncommitted setup file(s) from TamperWard; commit them when setup is complete.", "warn");
|
|
14837
|
+
} else {
|
|
14838
|
+
status("ACTION", dirty.length + " existing changed/untracked path(s). TamperWard will not stash or reset them.", "warn");
|
|
14839
|
+
if (!await confirm("Continue with the existing working-tree changes?", false, true)) {
|
|
14840
|
+
status("STOPPED", "Commit or stash your work, then re-run `tamperward onboard`.", "warn");
|
|
14841
|
+
return 2;
|
|
14842
|
+
}
|
|
14843
|
+
}
|
|
14807
14844
|
}
|
|
14808
|
-
|
|
14809
|
-
out3("This is the plan `tamperward init` would apply (the same planner as `init --dry-run`):");
|
|
14810
|
-
out3("");
|
|
14811
|
-
runners.init({ cwd, dryRun: true });
|
|
14812
|
-
out3("");
|
|
14813
|
-
out3("What each item is for:");
|
|
14814
|
-
out3(` policy \u2014 ${POLICY_FILE} holds your overrides; the baseline rules apply even without it.`);
|
|
14815
|
-
out3(" agent hooks \u2014 Claude Code PreToolUse deny + Stop sweep: the steering layer, judged in the loop.");
|
|
14816
|
-
out3(" pre-commit \u2014 the same engine over the staged diff, so a shortcut cannot be committed quietly.");
|
|
14817
|
-
out3(" CI \u2014 the authority for the protected branch: check --diff over the PR range plus");
|
|
14818
|
-
out3(" pristine verify; cleared only by a reviewer label bound to the head SHA.");
|
|
14819
|
-
out3(" CODEOWNERS \u2014 a human requirement on the paths that decide whether the gate runs at all.");
|
|
14845
|
+
section(2);
|
|
14820
14846
|
const plan = planInit(cwd);
|
|
14847
|
+
const names = {
|
|
14848
|
+
policy: "Policy",
|
|
14849
|
+
agent: "Claude hooks",
|
|
14850
|
+
"pre-commit": "Pre-commit",
|
|
14851
|
+
ci: "CI workflow",
|
|
14852
|
+
codeowners: "CODEOWNERS"
|
|
14853
|
+
};
|
|
14854
|
+
const renderPlan = (a) => {
|
|
14855
|
+
const name = (names[a.item] ?? a.item).padEnd(13);
|
|
14856
|
+
if (a.status === "ok") status("OK", name + " " + a.path, "ok");
|
|
14857
|
+
else if (a.status === "create") status("ADD", name + " " + a.path, "info");
|
|
14858
|
+
else if (a.status === "update") status("UPDATE", name + " " + a.path, "info");
|
|
14859
|
+
else if (a.status === "skip") status("ACTION", name + " " + a.path + " \u2014 " + a.detail, "warn");
|
|
14860
|
+
else status("ERROR", name + " " + a.path + " \u2014 " + a.detail, "bad");
|
|
14861
|
+
if (a.warning) status("ACTION", a.warning, "warn");
|
|
14862
|
+
};
|
|
14863
|
+
plan.forEach(renderPlan);
|
|
14821
14864
|
const pending = plan.filter((a) => a.apply).length;
|
|
14822
|
-
const
|
|
14865
|
+
const planAttention = plan.filter((a) => a.status === "error" || a.status === "skip").length;
|
|
14823
14866
|
if (pending === 0) {
|
|
14824
|
-
|
|
14825
|
-
|
|
14826
|
-
|
|
14827
|
-
|
|
14828
|
-
|
|
14829
|
-
|
|
14830
|
-
|
|
14831
|
-
|
|
14832
|
-
|
|
14867
|
+
status(
|
|
14868
|
+
planAttention ? "ACTION" : "OK",
|
|
14869
|
+
planAttention ? "Local protection still needs the item(s) above." : "Local protection is already wired.",
|
|
14870
|
+
planAttention ? "warn" : "ok"
|
|
14871
|
+
);
|
|
14872
|
+
} else if (await confirm("Apply " + pending + " setup change(s)?", true, true)) {
|
|
14873
|
+
const code2 = runners.init({ cwd, quiet: true });
|
|
14874
|
+
const afterPlan = planInit(cwd);
|
|
14875
|
+
const remainingItems = new Set(afterPlan.filter((a) => a.status !== "ok").map((a) => a.item));
|
|
14876
|
+
wrote.push(
|
|
14877
|
+
...plan.filter((a) => a.apply && !remainingItems.has(a.item)).map((a) => a.path)
|
|
14878
|
+
);
|
|
14879
|
+
const remaining = afterPlan.filter((a) => a.status !== "ok");
|
|
14880
|
+
if (code2 === 0 && remaining.length === 0) {
|
|
14881
|
+
status("OK", "Applied " + pending + " setup change(s).", "ok");
|
|
14882
|
+
} else {
|
|
14883
|
+
status("ACTION", "Applied what was safe; some setup items still need attention.", "warn");
|
|
14884
|
+
remaining.forEach(renderPlan);
|
|
14833
14885
|
}
|
|
14886
|
+
} else {
|
|
14887
|
+
declined = true;
|
|
14888
|
+
status("SKIP", "No setup files were changed.", "dim");
|
|
14834
14889
|
}
|
|
14835
|
-
|
|
14836
|
-
|
|
14837
|
-
|
|
14838
|
-
|
|
14839
|
-
|
|
14840
|
-
|
|
14841
|
-
|
|
14842
|
-
|
|
14843
|
-
|
|
14844
|
-
|
|
14845
|
-
|
|
14846
|
-
|
|
14847
|
-
|
|
14848
|
-
|
|
14849
|
-
|
|
14850
|
-
|
|
14851
|
-
verifyCommand = configured;
|
|
14852
|
-
} else {
|
|
14853
|
-
out3("The verifier command is part of the trust anchor: `verify` re-runs it against a pristine copy of");
|
|
14854
|
-
out3("your suite, and CI fails closed without it. TamperWard never chooses it for you.");
|
|
14855
|
-
const candidates = verifierCandidates(cwd);
|
|
14856
|
-
let chosen;
|
|
14857
|
-
if (scripted) {
|
|
14858
|
-
if (opts.verifyCommand?.trim()) {
|
|
14859
|
-
chosen = opts.verifyCommand.trim();
|
|
14860
|
-
} else {
|
|
14861
|
-
out3(candidates.length ? `Detected candidate(s): ${candidates.join(", ")}.` : "No suite command was detected.");
|
|
14862
|
-
out3('Scripted mode: verify.command was not written. Pass --verify-command "<suite command>" to set it explicitly.');
|
|
14863
|
-
}
|
|
14864
|
-
} else if (candidates.length === 1) {
|
|
14865
|
-
out3(`Detected one high-confidence suite command: ${candidates[0]}`);
|
|
14866
|
-
if (await confirm(`Use \`${candidates[0]}\` as verify.command?`, false, false)) chosen = candidates[0];
|
|
14867
|
-
else chosen = await ask("Enter the suite command to trust (empty to skip): ") || void 0;
|
|
14868
|
-
} else if (candidates.length > 1) {
|
|
14869
|
-
out3("Several suite commands were detected; choose the one that defines merge authority:");
|
|
14870
|
-
candidates.forEach((c, i) => out3(` ${i + 1}. ${c}`));
|
|
14871
|
-
const a = await ask(`Choose 1-${candidates.length} (${candidates.join(" / ")}), type a command, or press Enter to skip: `);
|
|
14872
|
-
const n = Number(a);
|
|
14873
|
-
chosen = a === "" ? void 0 : Number.isInteger(n) && n >= 1 && n <= candidates.length ? candidates[n - 1] : a;
|
|
14890
|
+
section(3);
|
|
14891
|
+
let configured;
|
|
14892
|
+
try {
|
|
14893
|
+
configured = loadPolicy(cwd).verify?.command?.trim() || void 0;
|
|
14894
|
+
} catch (e) {
|
|
14895
|
+
status("ERROR", POLICY_FILE + " does not load \u2014 " + errorMessage(e), "bad");
|
|
14896
|
+
}
|
|
14897
|
+
if (configured) {
|
|
14898
|
+
verifyCommand = configured;
|
|
14899
|
+
status("OK", "Trusted test command: " + configured, "ok");
|
|
14900
|
+
} else if (!declined) {
|
|
14901
|
+
const candidates = verifierCandidates(cwd);
|
|
14902
|
+
let chosen;
|
|
14903
|
+
if (scripted) {
|
|
14904
|
+
if (opts.verifyCommand?.trim()) {
|
|
14905
|
+
chosen = opts.verifyCommand.trim();
|
|
14874
14906
|
} else {
|
|
14875
|
-
|
|
14876
|
-
chosen = await ask("Enter the suite command to trust (empty to skip): ") || void 0;
|
|
14907
|
+
status("ACTION", 'No trusted test command configured. Pass --verify-command "<suite command>".', "warn");
|
|
14877
14908
|
}
|
|
14878
|
-
|
|
14879
|
-
|
|
14880
|
-
|
|
14881
|
-
|
|
14882
|
-
|
|
14883
|
-
|
|
14884
|
-
|
|
14885
|
-
|
|
14886
|
-
|
|
14887
|
-
|
|
14909
|
+
} else if (candidates.length === 1) {
|
|
14910
|
+
status("FOUND", candidates[0], "info");
|
|
14911
|
+
if (await confirm("Trust `" + candidates[0] + "` as the verifier command?", false, false)) {
|
|
14912
|
+
chosen = candidates[0];
|
|
14913
|
+
} else {
|
|
14914
|
+
chosen = await ask("Test command to trust (Enter to skip): ") || void 0;
|
|
14915
|
+
}
|
|
14916
|
+
} else if (candidates.length > 1) {
|
|
14917
|
+
status("ACTION", "Choose the command that decides whether this repository passes:", "warn");
|
|
14918
|
+
candidates.forEach((candidate, i) => out3(" " + (i + 1) + ". " + terminalText(candidate)));
|
|
14919
|
+
const a = await ask("Choose 1-" + candidates.length + ", type a command, or press Enter to skip: ");
|
|
14920
|
+
const n = Number(a);
|
|
14921
|
+
chosen = a === "" ? void 0 : Number.isInteger(n) && n >= 1 && n <= candidates.length ? candidates[n - 1] : a;
|
|
14922
|
+
} else {
|
|
14923
|
+
status("ACTION", "No test command was detected automatically.", "warn");
|
|
14924
|
+
chosen = await ask("Test command to trust (Enter to skip): ") || void 0;
|
|
14925
|
+
}
|
|
14926
|
+
if (chosen) {
|
|
14927
|
+
const problem = writeVerifyCommand(cwd, chosen);
|
|
14928
|
+
if (problem) {
|
|
14929
|
+
status("ERROR", "verify.command was not written \u2014 " + problem, "bad");
|
|
14888
14930
|
} else {
|
|
14889
|
-
|
|
14890
|
-
|
|
14931
|
+
verifyCommand = chosen;
|
|
14932
|
+
wrote.push(POLICY_FILE);
|
|
14933
|
+
status("OK", "Saved verify.command: " + chosen + " (budget 300s).", "ok");
|
|
14891
14934
|
}
|
|
14892
14935
|
}
|
|
14893
14936
|
}
|
|
14894
|
-
if (!
|
|
14895
|
-
|
|
14896
|
-
out3(
|
|
14897
|
-
|
|
14898
|
-
|
|
14899
|
-
|
|
14900
|
-
|
|
14901
|
-
|
|
14902
|
-
|
|
14903
|
-
|
|
14904
|
-
|
|
14905
|
-
|
|
14906
|
-
|
|
14907
|
-
let summary = null;
|
|
14908
|
-
const verifyOpts = { cwd, onVerdict: (v) => {
|
|
14937
|
+
if (!verifyCommand) {
|
|
14938
|
+
status("ACTION", "Verification is not configured; CI will fail closed until it is.", "warn");
|
|
14939
|
+
out3(paint2(' Set it later with: tamperward onboard --verify-command "<suite command>"', DIM2, colour));
|
|
14940
|
+
} else if (!localVerifySupported) {
|
|
14941
|
+
status("LIMITED", "Local verification is unavailable on " + platformLabel(platform) + "; use a digest-pinned container verifier.", "warn");
|
|
14942
|
+
} else if (!head) {
|
|
14943
|
+
status("ACTION", "Commit once, then run `tamperward verify`.", "warn");
|
|
14944
|
+
} else if (await confirm("Run the first verification now? (the suite runs twice)", true, true)) {
|
|
14945
|
+
let summary = null;
|
|
14946
|
+
const verifyOpts = {
|
|
14947
|
+
cwd,
|
|
14948
|
+
silent: true,
|
|
14949
|
+
onVerdict: (v) => {
|
|
14909
14950
|
summary = v;
|
|
14910
|
-
}
|
|
14911
|
-
|
|
14912
|
-
|
|
14913
|
-
|
|
14914
|
-
|
|
14915
|
-
|
|
14916
|
-
|
|
14917
|
-
|
|
14918
|
-
out3(`Skipped. Run it any time: tamperward verify${opts.base ? ` --base ${opts.base}` : ""}`);
|
|
14919
|
-
}
|
|
14951
|
+
}
|
|
14952
|
+
};
|
|
14953
|
+
if (opts.base) verifyOpts.base = opts.base;
|
|
14954
|
+
const code2 = runners.verify(verifyOpts);
|
|
14955
|
+
verifyResult = { code: code2, summary };
|
|
14956
|
+
status(code2 === 0 ? "OK" : code2 === 1 ? "ACTION" : "ERROR", explainVerdict(summary, code2), code2 === 0 ? "ok" : code2 === 1 ? "warn" : "bad");
|
|
14957
|
+
} else {
|
|
14958
|
+
status("SKIP", "Verification skipped. Run `tamperward verify` when ready.", "dim");
|
|
14920
14959
|
}
|
|
14921
|
-
if (!
|
|
14922
|
-
|
|
14923
|
-
|
|
14924
|
-
out3("worktree of HEAD, adds `.skip` to one test block THERE, runs `check --worktree` on it, then");
|
|
14925
|
-
out3("removes the worktree. Your working tree is not touched; its fingerprint is shown before and after.");
|
|
14926
|
-
const wanted = scripted ? Boolean(opts.demo) : opts.demo ? true : await confirm("Run the demonstration?", false, false);
|
|
14927
|
-
if (!wanted) {
|
|
14928
|
-
out3("Demo skipped.");
|
|
14929
|
-
} else if (!head) {
|
|
14930
|
-
out3("Demo skipped: no commit to build disposable state from.");
|
|
14960
|
+
if (!opts.skipDemo) {
|
|
14961
|
+
if (!head) {
|
|
14962
|
+
status("SKIP", "Safe demo becomes available after the first commit.", "dim");
|
|
14931
14963
|
} else {
|
|
14932
|
-
|
|
14964
|
+
const wanted = scripted ? Boolean(opts.demo) : opts.demo ? true : await confirm("Run the optional safe tamper demo? (temporary worktree only)", false, false);
|
|
14965
|
+
if (wanted) runDemo(cwd, head, runners, (line) => out3(terminalText(line)));
|
|
14933
14966
|
}
|
|
14934
14967
|
}
|
|
14935
|
-
|
|
14936
|
-
out3("Local wiring steers the agent and gates commits; only the repository can make CI binding.");
|
|
14968
|
+
section(4);
|
|
14937
14969
|
repo = opts.repo ?? inferGitHubRepo(cwd);
|
|
14938
|
-
const doctorCommand =
|
|
14970
|
+
const doctorCommand = "tamperward doctor --github --repo " + (repo ?? "OWNER/REPO") + " --branch " + (opts.branch ?? "<default-branch>");
|
|
14939
14971
|
if (opts.noGithub) {
|
|
14940
|
-
|
|
14941
|
-
for (const line of MANUAL_CONTROLS) out3(line);
|
|
14942
|
-
out3(`Then verify it with: ${doctorCommand}`);
|
|
14972
|
+
status("SKIP", "GitHub repository controls were not checked.", "dim");
|
|
14943
14973
|
} else if (!repo) {
|
|
14944
|
-
|
|
14945
|
-
for (const line of MANUAL_CONTROLS) out3(line);
|
|
14946
|
-
out3(`Then verify it with: ${doctorCommand}`);
|
|
14974
|
+
status("ACTION", "No github.com origin detected. Pass --repo OWNER/REPO to check repository protection.", "warn");
|
|
14947
14975
|
} else {
|
|
14948
|
-
|
|
14949
|
-
githubChecked = await confirm(
|
|
14950
|
-
|
|
14951
|
-
true,
|
|
14952
|
-
true
|
|
14953
|
-
);
|
|
14954
|
-
if (!githubChecked) {
|
|
14955
|
-
for (const line of MANUAL_CONTROLS) out3(line);
|
|
14956
|
-
out3(`Then verify it with: ${doctorCommand}`);
|
|
14957
|
-
}
|
|
14976
|
+
status("INFO", repo + (opts.branch ? "#" + opts.branch : ""), "info");
|
|
14977
|
+
githubChecked = await confirm("Check GitHub branch protection now?", true, true);
|
|
14978
|
+
if (!githubChecked) status("SKIP", "GitHub repository controls were not checked.", "dim");
|
|
14958
14979
|
}
|
|
14959
|
-
|
|
14980
|
+
section(5);
|
|
14960
14981
|
const doctorOpts = { cwd };
|
|
14961
14982
|
if (githubChecked && repo) {
|
|
14962
14983
|
doctorOpts.github = true;
|
|
@@ -14964,48 +14985,62 @@ async function runOnboard(opts, io = {}) {
|
|
|
14964
14985
|
if (opts.branch) doctorOpts.branch = opts.branch;
|
|
14965
14986
|
}
|
|
14966
14987
|
doctorOutcome = runners.doctor(doctorOpts);
|
|
14967
|
-
|
|
14968
|
-
|
|
14969
|
-
|
|
14988
|
+
const seenFailure = /* @__PURE__ */ new Set();
|
|
14989
|
+
for (const check2 of doctorOutcome.checks) {
|
|
14990
|
+
if (check2.state === "OK" || check2.id === "observer") continue;
|
|
14991
|
+
seenFailure.add(check2.id);
|
|
14992
|
+
if (check2.id === "platform" && platform !== "linux") {
|
|
14993
|
+
continue;
|
|
14994
|
+
}
|
|
14995
|
+
if (check2.id === "verifier" && !verifyCommand && check2.state === "BROKEN") {
|
|
14996
|
+
status("ACTION", "Verification is not configured; choose the trusted test command.", "warn");
|
|
14997
|
+
} else {
|
|
14998
|
+
renderCheck(check2, out3, colour);
|
|
14999
|
+
}
|
|
15000
|
+
}
|
|
15001
|
+
if (doctorOutcome.failure && !seenFailure.has(doctorOutcome.failure.id)) {
|
|
15002
|
+
status("ACTION", doctorOutcome.failure.message, "warn");
|
|
15003
|
+
}
|
|
14970
15004
|
const posture = postureOf(doctorOutcome);
|
|
14971
|
-
|
|
14972
|
-
|
|
14973
|
-
|
|
14974
|
-
|
|
14975
|
-
`POSTURE: ${posture}` + (posture === "READY WITH WARNINGS" ? ` \u2014 ${warnings} warning(s) above` : "") + (posture === "BROKEN" ? ` \u2014 ${broken.map((c) => c.id).join(", ")}` : "") + (posture === "INCOMPLETE" && doctorOutcome.failure ? ` \u2014 doctor could not certify: ${doctorOutcome.failure.id}` : "")
|
|
14976
|
-
);
|
|
15005
|
+
if (posture === "READY") status("READY", "TamperWard is configured for this repository.", "ok");
|
|
15006
|
+
else if (posture === "READY WITH WARNINGS") status("READY", "Configured with the limitation(s) above.", "warn");
|
|
15007
|
+
else if (posture === "INCOMPLETE") status("INCOMPLETE", "Setup needs the action(s) above.", "warn");
|
|
15008
|
+
else status("BLOCKED", "Fix the broken item(s) above before relying on the gate.", "bad");
|
|
14977
15009
|
if (doctorOutcome.github) {
|
|
14978
|
-
|
|
14979
|
-
|
|
15010
|
+
status("OK", "GitHub authority enforced for " + doctorOutcome.github.repo + "#" + doctorOutcome.github.branch + ".", "ok");
|
|
15011
|
+
} else if (!repo) {
|
|
15012
|
+
status("ACTION", "GitHub authority is not configured for this repository.", "warn");
|
|
15013
|
+
out3(paint2(" If GitHub will provide repository authority, run: " + terminalText(doctorCommand), DIM2, colour));
|
|
15014
|
+
out3(paint2(" If this repository will use GitHub CI authority, configure:", DIM2, colour));
|
|
15015
|
+
for (const line of MANUAL_CONTROLS) out3(paint2(" " + line, DIM2, colour));
|
|
14980
15016
|
} else {
|
|
14981
|
-
|
|
14982
|
-
out3(
|
|
14983
|
-
|
|
14984
|
-
|
|
14985
|
-
|
|
15017
|
+
status("ACTION", "GitHub authority is not verified yet.", "warn");
|
|
15018
|
+
out3(paint2(" " + terminalText(doctorCommand), DIM2, colour));
|
|
15019
|
+
out3(paint2(" GitHub authority requires all three controls:", DIM2, colour));
|
|
15020
|
+
for (const line of MANUAL_CONTROLS) out3(paint2(" " + line, DIM2, colour));
|
|
15021
|
+
}
|
|
15022
|
+
const commitPaths = [...new Set(wrote)].filter((path) => committableSetupPath(cwd, path));
|
|
15023
|
+
if (commitPaths.length) {
|
|
15024
|
+
status("NEXT", "Commit repository setup files: " + commitPaths.join(", "), "info");
|
|
14986
15025
|
}
|
|
14987
|
-
if (
|
|
14988
|
-
|
|
14989
|
-
|
|
14990
|
-
|
|
14991
|
-
|
|
14992
|
-
|
|
14993
|
-
|
|
14994
|
-
|
|
14995
|
-
|
|
14996
|
-
out3("
|
|
14997
|
-
out3("it owns the runtime and dependencies, mounts the tree read-only and disables the network, so the");
|
|
14998
|
-
out3("candidate cannot reach the verifier from the same host. `run` deliberately does not offer it.");
|
|
14999
|
-
if (wrote.length) out3(`Commit what onboarding wrote (${[...new Set(wrote)].join(", ")}) so the gate travels with the repository.`);
|
|
15026
|
+
if (!head) {
|
|
15027
|
+
status("NEXT", "Create the initial repository commit before verify or the safe demo.", "info");
|
|
15028
|
+
}
|
|
15029
|
+
if (!verifyCommand) {
|
|
15030
|
+
status("NEXT", "Choose the test command TamperWard should trust for verification.", "info");
|
|
15031
|
+
}
|
|
15032
|
+
if (verifyResult) {
|
|
15033
|
+
status("VERIFY", verifyResult.summary?.verdict ?? "exit " + verifyResult.code, verifyResult.code === 0 ? "ok" : "warn");
|
|
15034
|
+
}
|
|
15035
|
+
out3(paint2(" Daily: tamperward check --worktree \xB7 tamperward verify --base <base>", DIM2, colour));
|
|
15000
15036
|
return posture === "READY" || posture === "READY WITH WARNINGS" ? 0 : 1;
|
|
15001
15037
|
} catch (e) {
|
|
15002
15038
|
if (e instanceof Aborted) {
|
|
15003
15039
|
out3("");
|
|
15004
|
-
|
|
15005
|
-
|
|
15006
|
-
|
|
15007
|
-
);
|
|
15008
|
-
out3("Re-run `tamperward onboard` to continue from the same place, or `tamperward doctor` to see the current posture.");
|
|
15040
|
+
status("STOPPED", "Onboarding was cancelled during " + (SECTIONS[sectionNo - 1] ?? "setup") + ".", "warn");
|
|
15041
|
+
if (wrote.length) status("INFO", "Already written: " + [...new Set(wrote)].join(", ") + ". Re-running is safe.", "dim");
|
|
15042
|
+
else status("INFO", "Nothing was written.", "dim");
|
|
15043
|
+
status("NEXT", "Re-run `tamperward onboard` to continue, or `tamperward doctor` to inspect the current setup.", "info");
|
|
15009
15044
|
return 2;
|
|
15010
15045
|
}
|
|
15011
15046
|
throw e;
|
|
@@ -15013,14 +15048,18 @@ async function runOnboard(opts, io = {}) {
|
|
|
15013
15048
|
prompt.asker?.close();
|
|
15014
15049
|
}
|
|
15015
15050
|
}
|
|
15016
|
-
function renderCheck(check2, out3) {
|
|
15017
|
-
|
|
15018
|
-
|
|
15051
|
+
function renderCheck(check2, out3, colour) {
|
|
15052
|
+
const broken = check2.state === "BROKEN";
|
|
15053
|
+
const label = broken ? "ERROR" : "WARN";
|
|
15054
|
+
const code2 = broken ? BOLD2 + RED2 : YELLOW2;
|
|
15055
|
+
out3(paint2(label.padEnd(8), code2, colour) + " " + terminalText(check2.id + " \u2014 " + check2.detail));
|
|
15019
15056
|
}
|
|
15020
15057
|
function postureOf(outcome) {
|
|
15021
15058
|
if (outcome.failure) return "INCOMPLETE";
|
|
15022
|
-
if (outcome.checks.some((c) => c.state === "BROKEN")) return "BROKEN";
|
|
15023
|
-
if (outcome.checks.some((c) => c.state === "WARN"
|
|
15059
|
+
if (outcome.checks.some((c) => c.state === "BROKEN" && c.id !== "platform")) return "BROKEN";
|
|
15060
|
+
if (outcome.checks.some((c) => c.state === "WARN" || c.state === "BROKEN" && c.id === "platform")) {
|
|
15061
|
+
return "READY WITH WARNINGS";
|
|
15062
|
+
}
|
|
15024
15063
|
return "READY";
|
|
15025
15064
|
}
|
|
15026
15065
|
function runDemo(cwd, head, runners, out3) {
|
|
@@ -15092,7 +15131,7 @@ function runDemo(cwd, head, runners, out3) {
|
|
|
15092
15131
|
out3("changed it while the demo ran; inspect with `git status`.");
|
|
15093
15132
|
}
|
|
15094
15133
|
}
|
|
15095
|
-
var Aborted,
|
|
15134
|
+
var Aborted, SECTIONS, MANUAL_CONTROLS, ESC2, RESET2, BOLD2, DIM2, RED2, YELLOW2, GREEN2, CYAN, JS_TEST_PATH, JS_TEST_BLOCK;
|
|
15096
15135
|
var init_onboard = __esm({
|
|
15097
15136
|
"src/cli/onboard.ts"() {
|
|
15098
15137
|
"use strict";
|
|
@@ -15100,7 +15139,7 @@ var init_onboard = __esm({
|
|
|
15100
15139
|
init_verify();
|
|
15101
15140
|
init_doctor();
|
|
15102
15141
|
init_check();
|
|
15103
|
-
|
|
15142
|
+
init_text();
|
|
15104
15143
|
init_fingerprint();
|
|
15105
15144
|
init_policy();
|
|
15106
15145
|
init_policy_load();
|
|
@@ -15108,26 +15147,26 @@ var init_onboard = __esm({
|
|
|
15108
15147
|
init_wiring();
|
|
15109
15148
|
Aborted = class extends Error {
|
|
15110
15149
|
};
|
|
15111
|
-
|
|
15112
|
-
"
|
|
15113
|
-
"
|
|
15114
|
-
"
|
|
15115
|
-
"
|
|
15116
|
-
"
|
|
15117
|
-
"Safe demonstration",
|
|
15118
|
-
"GitHub repository authority",
|
|
15119
|
-
"Posture",
|
|
15120
|
-
"Next steps"
|
|
15150
|
+
SECTIONS = [
|
|
15151
|
+
"Environment",
|
|
15152
|
+
"Local protection",
|
|
15153
|
+
"Verification",
|
|
15154
|
+
"GitHub protection",
|
|
15155
|
+
"Summary"
|
|
15121
15156
|
];
|
|
15122
15157
|
MANUAL_CONTROLS = [
|
|
15123
|
-
"
|
|
15124
|
-
"
|
|
15125
|
-
|
|
15126
|
-
' 3. enable "Dismiss stale pull request approvals when new commits are pushed".',
|
|
15127
|
-
"Until all three are enforced the generated CI gate is advisory: a pull request runs the",
|
|
15128
|
-
"workflow from its own head and a required check is matched by job name, so it could keep",
|
|
15129
|
-
"the job called `tamperward`, replace the gate with `true`, and report green."
|
|
15158
|
+
"required check: tamperward",
|
|
15159
|
+
"Code Owner review",
|
|
15160
|
+
"dismiss stale approvals when new commits are pushed"
|
|
15130
15161
|
];
|
|
15162
|
+
ESC2 = "\x1B";
|
|
15163
|
+
RESET2 = `${ESC2}[0m`;
|
|
15164
|
+
BOLD2 = `${ESC2}[1m`;
|
|
15165
|
+
DIM2 = `${ESC2}[2m`;
|
|
15166
|
+
RED2 = `${ESC2}[31m`;
|
|
15167
|
+
YELLOW2 = `${ESC2}[33m`;
|
|
15168
|
+
GREEN2 = `${ESC2}[32m`;
|
|
15169
|
+
CYAN = `${ESC2}[36m`;
|
|
15131
15170
|
JS_TEST_PATH = /(?:^|\/)(?:[^/]+\.(?:test|spec)\.[cm]?[jt]sx?|__tests__\/[^/]+\.[cm]?[jt]sx?)$/;
|
|
15132
15171
|
JS_TEST_BLOCK = /^(\s*)(it|test|describe)(\s*\()/m;
|
|
15133
15172
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tamperward",
|
|
3
|
-
"version": "2.23.
|
|
3
|
+
"version": "2.23.2",
|
|
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",
|