tamperward 2.23.0 → 2.23.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/dist/cli/index.js +267 -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,172 @@ 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;
|
|
14788
|
-
if (!head)
|
|
14789
|
-
|
|
14790
|
-
|
|
14791
|
-
const status = git5(cwd, ["status", "--porcelain", "--untracked-files=all"]) ?? "";
|
|
14792
|
-
const dirty = status.split("\n").filter(Boolean).map((line) => line.slice(3).split(" -> ").at(-1) ?? "");
|
|
14821
|
+
if (!head) status("ACTION", "No commit yet. Commit once before pristine verification or the safe demo.", "warn");
|
|
14822
|
+
const treeStatus = git5(cwd, ["status", "--porcelain", "--untracked-files=all"]) ?? "";
|
|
14823
|
+
const dirty = treeStatus.split("\n").filter(Boolean).map((line) => line.slice(3).split(" -> ").at(-1) ?? "");
|
|
14793
14824
|
const owned = new Set(planInit(cwd).map((a) => a.path));
|
|
14794
14825
|
const foreign = dirty.filter((p) => !owned.has(p));
|
|
14795
|
-
if (dirty.length
|
|
14796
|
-
|
|
14797
|
-
} else if (
|
|
14798
|
-
|
|
14799
|
-
|
|
14800
|
-
|
|
14801
|
-
if (!await confirm("Continue with
|
|
14802
|
-
|
|
14826
|
+
if (dirty.length === 0) {
|
|
14827
|
+
status("OK", "Working tree is clean.", "ok");
|
|
14828
|
+
} else if (foreign.length === 0) {
|
|
14829
|
+
status("ACTION", dirty.length + " uncommitted setup file(s) from TamperWard; commit them when setup is complete.", "warn");
|
|
14830
|
+
} else {
|
|
14831
|
+
status("ACTION", dirty.length + " existing changed/untracked path(s). TamperWard will not stash or reset them.", "warn");
|
|
14832
|
+
if (!await confirm("Continue with the existing working-tree changes?", false, true)) {
|
|
14833
|
+
status("STOPPED", "Commit or stash your work, then re-run `tamperward onboard`.", "warn");
|
|
14803
14834
|
return 2;
|
|
14804
14835
|
}
|
|
14805
|
-
} else {
|
|
14806
|
-
out3("working tree: clean");
|
|
14807
14836
|
}
|
|
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.");
|
|
14837
|
+
section(2);
|
|
14820
14838
|
const plan = planInit(cwd);
|
|
14839
|
+
const names = {
|
|
14840
|
+
policy: "Policy",
|
|
14841
|
+
agent: "Claude hooks",
|
|
14842
|
+
"pre-commit": "Pre-commit",
|
|
14843
|
+
ci: "CI workflow",
|
|
14844
|
+
codeowners: "CODEOWNERS"
|
|
14845
|
+
};
|
|
14846
|
+
const renderPlan = (a) => {
|
|
14847
|
+
const name = (names[a.item] ?? a.item).padEnd(13);
|
|
14848
|
+
if (a.status === "ok") status("OK", name + " " + a.path, "ok");
|
|
14849
|
+
else if (a.status === "create") status("ADD", name + " " + a.path, "info");
|
|
14850
|
+
else if (a.status === "update") status("UPDATE", name + " " + a.path, "info");
|
|
14851
|
+
else if (a.status === "skip") status("ACTION", name + " " + a.path + " \u2014 " + a.detail, "warn");
|
|
14852
|
+
else status("ERROR", name + " " + a.path + " \u2014 " + a.detail, "bad");
|
|
14853
|
+
if (a.warning) status("ACTION", a.warning, "warn");
|
|
14854
|
+
};
|
|
14855
|
+
plan.forEach(renderPlan);
|
|
14821
14856
|
const pending = plan.filter((a) => a.apply).length;
|
|
14822
|
-
const
|
|
14857
|
+
const planAttention = plan.filter((a) => a.status === "error" || a.status === "skip").length;
|
|
14823
14858
|
if (pending === 0) {
|
|
14824
|
-
|
|
14825
|
-
|
|
14826
|
-
|
|
14827
|
-
|
|
14828
|
-
|
|
14829
|
-
|
|
14830
|
-
|
|
14831
|
-
|
|
14832
|
-
|
|
14859
|
+
status(
|
|
14860
|
+
planAttention ? "ACTION" : "OK",
|
|
14861
|
+
planAttention ? "Local protection still needs the item(s) above." : "Local protection is already wired.",
|
|
14862
|
+
planAttention ? "warn" : "ok"
|
|
14863
|
+
);
|
|
14864
|
+
} else if (await confirm("Apply " + pending + " setup change(s)?", true, true)) {
|
|
14865
|
+
const code2 = runners.init({ cwd, quiet: true });
|
|
14866
|
+
const afterPlan = planInit(cwd);
|
|
14867
|
+
const remainingItems = new Set(afterPlan.filter((a) => a.status !== "ok").map((a) => a.item));
|
|
14868
|
+
wrote.push(
|
|
14869
|
+
...plan.filter((a) => a.apply && !remainingItems.has(a.item)).map((a) => a.path)
|
|
14870
|
+
);
|
|
14871
|
+
const remaining = afterPlan.filter((a) => a.status !== "ok");
|
|
14872
|
+
if (code2 === 0 && remaining.length === 0) {
|
|
14873
|
+
status("OK", "Applied " + pending + " setup change(s).", "ok");
|
|
14874
|
+
} else {
|
|
14875
|
+
status("ACTION", "Applied what was safe; some setup items still need attention.", "warn");
|
|
14876
|
+
remaining.forEach(renderPlan);
|
|
14833
14877
|
}
|
|
14878
|
+
} else {
|
|
14879
|
+
declined = true;
|
|
14880
|
+
status("SKIP", "No setup files were changed.", "dim");
|
|
14834
14881
|
}
|
|
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;
|
|
14882
|
+
section(3);
|
|
14883
|
+
let configured;
|
|
14884
|
+
try {
|
|
14885
|
+
configured = loadPolicy(cwd).verify?.command?.trim() || void 0;
|
|
14886
|
+
} catch (e) {
|
|
14887
|
+
status("ERROR", POLICY_FILE + " does not load \u2014 " + errorMessage(e), "bad");
|
|
14888
|
+
}
|
|
14889
|
+
if (configured) {
|
|
14890
|
+
verifyCommand = configured;
|
|
14891
|
+
status("OK", "Trusted test command: " + configured, "ok");
|
|
14892
|
+
} else if (!declined) {
|
|
14893
|
+
const candidates = verifierCandidates(cwd);
|
|
14894
|
+
let chosen;
|
|
14895
|
+
if (scripted) {
|
|
14896
|
+
if (opts.verifyCommand?.trim()) {
|
|
14897
|
+
chosen = opts.verifyCommand.trim();
|
|
14874
14898
|
} else {
|
|
14875
|
-
|
|
14876
|
-
chosen = await ask("Enter the suite command to trust (empty to skip): ") || void 0;
|
|
14899
|
+
status("ACTION", 'No trusted test command configured. Pass --verify-command "<suite command>".', "warn");
|
|
14877
14900
|
}
|
|
14878
|
-
|
|
14879
|
-
|
|
14880
|
-
|
|
14881
|
-
|
|
14882
|
-
} else {
|
|
14883
|
-
out3(`Wrote verify.command: ${chosen} (budget 300s) to ${POLICY_FILE}. Commit it: with a --base, verify reads`);
|
|
14884
|
-
out3("the policy from that commit, so an uncommitted command is invisible to CI.");
|
|
14885
|
-
verifyCommand = chosen;
|
|
14886
|
-
wrote.push(POLICY_FILE);
|
|
14887
|
-
}
|
|
14901
|
+
} else if (candidates.length === 1) {
|
|
14902
|
+
status("FOUND", candidates[0], "info");
|
|
14903
|
+
if (await confirm("Trust `" + candidates[0] + "` as the verifier command?", false, false)) {
|
|
14904
|
+
chosen = candidates[0];
|
|
14888
14905
|
} else {
|
|
14889
|
-
|
|
14890
|
-
out3(`${POLICY_FILE} under verify: and re-run \`tamperward doctor\`.`);
|
|
14906
|
+
chosen = await ask("Test command to trust (Enter to skip): ") || void 0;
|
|
14891
14907
|
}
|
|
14892
|
-
}
|
|
14893
|
-
|
|
14894
|
-
|
|
14895
|
-
|
|
14896
|
-
|
|
14897
|
-
|
|
14898
|
-
if (!verifyCommand) {
|
|
14899
|
-
out3("Skipped: no verify.command. Standalone `tamperward verify` would report CANNOT_VERIFY");
|
|
14900
|
-
out3("(exit 2) here \u2014 it fails closed rather than pretending to verify.");
|
|
14901
|
-
} else if (!localVerifySupported) {
|
|
14902
|
-
out3(`Skipped: checkpointed-local verify is unsupported on ${platform}; \`tamperward verify\` fails closed`);
|
|
14903
|
-
out3("(exit 2) before running anything. Configure a digest-pinned container verifier for this platform.");
|
|
14904
|
-
} else if (!head) {
|
|
14905
|
-
out3("Skipped: no commit to anchor the pristine copy to. Commit, then run: tamperward verify");
|
|
14906
|
-
} else if (await confirm(`Run the first verification now (${verifyCommand}, twice)?`, true, true)) {
|
|
14907
|
-
let summary = null;
|
|
14908
|
-
const verifyOpts = { cwd, onVerdict: (v) => {
|
|
14909
|
-
summary = v;
|
|
14910
|
-
} };
|
|
14911
|
-
if (opts.base) verifyOpts.base = opts.base;
|
|
14912
|
-
const code2 = runners.verify(verifyOpts);
|
|
14913
|
-
verifyResult = { code: code2, summary };
|
|
14914
|
-
out3("");
|
|
14915
|
-
for (const line of explainVerdict(summary, code2)) out3(line);
|
|
14916
|
-
out3(`(verify exited ${code2}; onboarding reports it and changes nothing about it.)`);
|
|
14908
|
+
} else if (candidates.length > 1) {
|
|
14909
|
+
status("ACTION", "Choose the command that decides whether this repository passes:", "warn");
|
|
14910
|
+
candidates.forEach((candidate, i) => out3(" " + (i + 1) + ". " + terminalText(candidate)));
|
|
14911
|
+
const a = await ask("Choose 1-" + candidates.length + ", type a command, or press Enter to skip: ");
|
|
14912
|
+
const n = Number(a);
|
|
14913
|
+
chosen = a === "" ? void 0 : Number.isInteger(n) && n >= 1 && n <= candidates.length ? candidates[n - 1] : a;
|
|
14917
14914
|
} else {
|
|
14918
|
-
|
|
14915
|
+
status("ACTION", "No test command was detected automatically.", "warn");
|
|
14916
|
+
chosen = await ask("Test command to trust (Enter to skip): ") || void 0;
|
|
14917
|
+
}
|
|
14918
|
+
if (chosen) {
|
|
14919
|
+
const problem = writeVerifyCommand(cwd, chosen);
|
|
14920
|
+
if (problem) {
|
|
14921
|
+
status("ERROR", "verify.command was not written \u2014 " + problem, "bad");
|
|
14922
|
+
} else {
|
|
14923
|
+
verifyCommand = chosen;
|
|
14924
|
+
wrote.push(POLICY_FILE);
|
|
14925
|
+
status("OK", "Saved verify.command: " + chosen + " (budget 300s).", "ok");
|
|
14926
|
+
}
|
|
14919
14927
|
}
|
|
14920
14928
|
}
|
|
14921
|
-
if (!
|
|
14922
|
-
|
|
14923
|
-
out3(
|
|
14924
|
-
|
|
14925
|
-
|
|
14926
|
-
|
|
14927
|
-
|
|
14928
|
-
|
|
14929
|
-
|
|
14930
|
-
|
|
14931
|
-
|
|
14932
|
-
|
|
14929
|
+
if (!verifyCommand) {
|
|
14930
|
+
status("ACTION", "Verification is not configured; CI will fail closed until it is.", "warn");
|
|
14931
|
+
out3(paint2(' Set it later with: tamperward onboard --verify-command "<suite command>"', DIM2, colour));
|
|
14932
|
+
} else if (!localVerifySupported) {
|
|
14933
|
+
status("LIMITED", "Local verification is unavailable on " + platformLabel(platform) + "; use a digest-pinned container verifier.", "warn");
|
|
14934
|
+
} else if (!head) {
|
|
14935
|
+
status("ACTION", "Commit once, then run `tamperward verify`.", "warn");
|
|
14936
|
+
} else if (await confirm("Run the first verification now? (the suite runs twice)", true, true)) {
|
|
14937
|
+
let summary = null;
|
|
14938
|
+
const verifyOpts = {
|
|
14939
|
+
cwd,
|
|
14940
|
+
silent: true,
|
|
14941
|
+
onVerdict: (v) => {
|
|
14942
|
+
summary = v;
|
|
14943
|
+
}
|
|
14944
|
+
};
|
|
14945
|
+
if (opts.base) verifyOpts.base = opts.base;
|
|
14946
|
+
const code2 = runners.verify(verifyOpts);
|
|
14947
|
+
verifyResult = { code: code2, summary };
|
|
14948
|
+
status(code2 === 0 ? "OK" : code2 === 1 ? "ACTION" : "ERROR", explainVerdict(summary, code2), code2 === 0 ? "ok" : code2 === 1 ? "warn" : "bad");
|
|
14949
|
+
} else {
|
|
14950
|
+
status("SKIP", "Verification skipped. Run `tamperward verify` when ready.", "dim");
|
|
14951
|
+
}
|
|
14952
|
+
if (!opts.skipDemo) {
|
|
14953
|
+
const wanted = scripted ? Boolean(opts.demo) : opts.demo ? true : await confirm("Run the optional safe tamper demo? (temporary worktree only)", false, false);
|
|
14954
|
+
if (wanted && head) {
|
|
14955
|
+
runDemo(cwd, head, runners, (line) => out3(terminalText(line)));
|
|
14956
|
+
} else if (wanted) {
|
|
14957
|
+
status("SKIP", "Demo needs at least one commit.", "dim");
|
|
14933
14958
|
}
|
|
14934
14959
|
}
|
|
14935
|
-
|
|
14936
|
-
out3("Local wiring steers the agent and gates commits; only the repository can make CI binding.");
|
|
14960
|
+
section(4);
|
|
14937
14961
|
repo = opts.repo ?? inferGitHubRepo(cwd);
|
|
14938
|
-
const doctorCommand =
|
|
14962
|
+
const doctorCommand = "tamperward doctor --github --repo " + (repo ?? "OWNER/REPO") + " --branch " + (opts.branch ?? "<default-branch>");
|
|
14939
14963
|
if (opts.noGithub) {
|
|
14940
|
-
|
|
14941
|
-
for (const line of MANUAL_CONTROLS) out3(line);
|
|
14942
|
-
out3(`Then verify it with: ${doctorCommand}`);
|
|
14964
|
+
status("SKIP", "GitHub repository controls were not checked.", "dim");
|
|
14943
14965
|
} else if (!repo) {
|
|
14944
|
-
|
|
14945
|
-
for (const line of MANUAL_CONTROLS) out3(line);
|
|
14946
|
-
out3(`Then verify it with: ${doctorCommand}`);
|
|
14966
|
+
status("ACTION", "No github.com origin detected. Pass --repo OWNER/REPO to check repository protection.", "warn");
|
|
14947
14967
|
} 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
|
-
}
|
|
14968
|
+
status("INFO", repo + (opts.branch ? "#" + opts.branch : ""), "info");
|
|
14969
|
+
githubChecked = await confirm("Check GitHub branch protection now?", true, true);
|
|
14970
|
+
if (!githubChecked) status("SKIP", "GitHub repository controls were not checked.", "dim");
|
|
14958
14971
|
}
|
|
14959
|
-
|
|
14972
|
+
section(5);
|
|
14960
14973
|
const doctorOpts = { cwd };
|
|
14961
14974
|
if (githubChecked && repo) {
|
|
14962
14975
|
doctorOpts.github = true;
|
|
@@ -14964,48 +14977,51 @@ async function runOnboard(opts, io = {}) {
|
|
|
14964
14977
|
if (opts.branch) doctorOpts.branch = opts.branch;
|
|
14965
14978
|
}
|
|
14966
14979
|
doctorOutcome = runners.doctor(doctorOpts);
|
|
14967
|
-
|
|
14968
|
-
|
|
14969
|
-
|
|
14980
|
+
const seenFailure = /* @__PURE__ */ new Set();
|
|
14981
|
+
for (const check2 of doctorOutcome.checks) {
|
|
14982
|
+
if (check2.state === "OK" || check2.id === "observer") continue;
|
|
14983
|
+
seenFailure.add(check2.id);
|
|
14984
|
+
if (check2.id === "platform" && platform !== "linux") {
|
|
14985
|
+
const detail = localVerifySupported ? platformLabel(platform) + ": `tamperward run` requires Linux; check + verify remain available." : platformLabel(platform) + ": `tamperward run` and local verify are unavailable; check remains available.";
|
|
14986
|
+
status("LIMITED", detail, "warn");
|
|
14987
|
+
} else {
|
|
14988
|
+
renderCheck(check2, out3, colour);
|
|
14989
|
+
}
|
|
14990
|
+
}
|
|
14991
|
+
if (doctorOutcome.failure && !seenFailure.has(doctorOutcome.failure.id)) {
|
|
14992
|
+
status("ACTION", doctorOutcome.failure.message, "warn");
|
|
14993
|
+
}
|
|
14970
14994
|
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
|
-
);
|
|
14995
|
+
if (posture === "READY") status("READY", "TamperWard is configured for this repository.", "ok");
|
|
14996
|
+
else if (posture === "READY WITH WARNINGS") status("READY", "Configured with the limitation(s) above.", "warn");
|
|
14997
|
+
else if (posture === "INCOMPLETE") status("INCOMPLETE", "Setup needs the action(s) above.", "warn");
|
|
14998
|
+
else status("BLOCKED", "Fix the broken item(s) above before relying on the gate.", "bad");
|
|
14977
14999
|
if (doctorOutcome.github) {
|
|
14978
|
-
|
|
14979
|
-
out3(" requires the tamperward check, Code Owner review, and stale-approval dismissal.");
|
|
15000
|
+
status("OK", "GitHub authority enforced for " + doctorOutcome.github.repo + "#" + doctorOutcome.github.branch + ".", "ok");
|
|
14980
15001
|
} else {
|
|
14981
|
-
|
|
14982
|
-
out3(
|
|
14983
|
-
|
|
14984
|
-
|
|
14985
|
-
|
|
15002
|
+
status("ACTION", "GitHub authority is not verified yet.", "warn");
|
|
15003
|
+
out3(paint2(" " + terminalText(doctorCommand), DIM2, colour));
|
|
15004
|
+
for (const line of MANUAL_CONTROLS) out3(paint2(" " + line, DIM2, colour));
|
|
15005
|
+
}
|
|
15006
|
+
const commitPaths = [...new Set(wrote)].filter((path) => committableSetupPath(cwd, path));
|
|
15007
|
+
if (commitPaths.length) {
|
|
15008
|
+
status("NEXT", "Commit repository setup files: " + commitPaths.join(", "), "info");
|
|
15009
|
+
}
|
|
15010
|
+
if (!verifyCommand) {
|
|
15011
|
+
status("NEXT", "Choose the test command TamperWard should trust for verification.", "info");
|
|
14986
15012
|
}
|
|
14987
|
-
if (
|
|
14988
|
-
|
|
14989
|
-
|
|
14990
|
-
out3("
|
|
14991
|
-
out3(" tamperward check --worktree the stop-sweep view of everything changed since HEAD");
|
|
14992
|
-
out3(` tamperward verify --base ${opts.base ?? "<base>"}${opts.base ? "" : " "}pristine re-execution against a base the agent cannot rewrite`);
|
|
14993
|
-
out3(` tamperward run --base ${opts.base ?? "<base>"} -- <agent...> the enforcement envelope (Linux; fails closed elsewhere)`);
|
|
14994
|
-
out3(` ${doctorCommand}`);
|
|
14995
|
-
out3("When the final verification matters most \u2014 a release, a merge into the protected branch \u2014 prefer");
|
|
14996
|
-
out3("the digest-pinned container verifier (verify.backend: container, image: <ref>@sha256:<digest>):");
|
|
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.`);
|
|
15013
|
+
if (verifyResult) {
|
|
15014
|
+
status("VERIFY", verifyResult.summary?.verdict ?? "exit " + verifyResult.code, verifyResult.code === 0 ? "ok" : "warn");
|
|
15015
|
+
}
|
|
15016
|
+
out3(paint2(" Daily: tamperward check --worktree \xB7 tamperward verify --base <base>", DIM2, colour));
|
|
15000
15017
|
return posture === "READY" || posture === "READY WITH WARNINGS" ? 0 : 1;
|
|
15001
15018
|
} catch (e) {
|
|
15002
15019
|
if (e instanceof Aborted) {
|
|
15003
15020
|
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.");
|
|
15021
|
+
status("STOPPED", "Onboarding was cancelled during " + (SECTIONS[sectionNo - 1] ?? "setup") + ".", "warn");
|
|
15022
|
+
if (wrote.length) status("INFO", "Already written: " + [...new Set(wrote)].join(", ") + ". Re-running is safe.", "dim");
|
|
15023
|
+
else status("INFO", "Nothing was written.", "dim");
|
|
15024
|
+
status("NEXT", "Re-run `tamperward onboard` to continue, or `tamperward doctor` to inspect the current setup.", "info");
|
|
15009
15025
|
return 2;
|
|
15010
15026
|
}
|
|
15011
15027
|
throw e;
|
|
@@ -15013,14 +15029,18 @@ async function runOnboard(opts, io = {}) {
|
|
|
15013
15029
|
prompt.asker?.close();
|
|
15014
15030
|
}
|
|
15015
15031
|
}
|
|
15016
|
-
function renderCheck(check2, out3) {
|
|
15017
|
-
|
|
15018
|
-
|
|
15032
|
+
function renderCheck(check2, out3, colour) {
|
|
15033
|
+
const broken = check2.state === "BROKEN";
|
|
15034
|
+
const label = broken ? "ERROR" : "WARN";
|
|
15035
|
+
const code2 = broken ? BOLD2 + RED2 : YELLOW2;
|
|
15036
|
+
out3(paint2(label.padEnd(8), code2, colour) + " " + terminalText(check2.id + " \u2014 " + check2.detail));
|
|
15019
15037
|
}
|
|
15020
15038
|
function postureOf(outcome) {
|
|
15021
15039
|
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"
|
|
15040
|
+
if (outcome.checks.some((c) => c.state === "BROKEN" && c.id !== "platform")) return "BROKEN";
|
|
15041
|
+
if (outcome.checks.some((c) => c.state === "WARN" || c.state === "BROKEN" && c.id === "platform")) {
|
|
15042
|
+
return "READY WITH WARNINGS";
|
|
15043
|
+
}
|
|
15024
15044
|
return "READY";
|
|
15025
15045
|
}
|
|
15026
15046
|
function runDemo(cwd, head, runners, out3) {
|
|
@@ -15092,7 +15112,7 @@ function runDemo(cwd, head, runners, out3) {
|
|
|
15092
15112
|
out3("changed it while the demo ran; inspect with `git status`.");
|
|
15093
15113
|
}
|
|
15094
15114
|
}
|
|
15095
|
-
var Aborted,
|
|
15115
|
+
var Aborted, SECTIONS, MANUAL_CONTROLS, ESC2, RESET2, BOLD2, DIM2, RED2, YELLOW2, GREEN2, CYAN, JS_TEST_PATH, JS_TEST_BLOCK;
|
|
15096
15116
|
var init_onboard = __esm({
|
|
15097
15117
|
"src/cli/onboard.ts"() {
|
|
15098
15118
|
"use strict";
|
|
@@ -15100,7 +15120,7 @@ var init_onboard = __esm({
|
|
|
15100
15120
|
init_verify();
|
|
15101
15121
|
init_doctor();
|
|
15102
15122
|
init_check();
|
|
15103
|
-
|
|
15123
|
+
init_text();
|
|
15104
15124
|
init_fingerprint();
|
|
15105
15125
|
init_policy();
|
|
15106
15126
|
init_policy_load();
|
|
@@ -15108,26 +15128,27 @@ var init_onboard = __esm({
|
|
|
15108
15128
|
init_wiring();
|
|
15109
15129
|
Aborted = class extends Error {
|
|
15110
15130
|
};
|
|
15111
|
-
|
|
15112
|
-
"
|
|
15113
|
-
"
|
|
15114
|
-
"
|
|
15115
|
-
"
|
|
15116
|
-
"
|
|
15117
|
-
"Safe demonstration",
|
|
15118
|
-
"GitHub repository authority",
|
|
15119
|
-
"Posture",
|
|
15120
|
-
"Next steps"
|
|
15131
|
+
SECTIONS = [
|
|
15132
|
+
"Environment",
|
|
15133
|
+
"Local protection",
|
|
15134
|
+
"Verification",
|
|
15135
|
+
"GitHub protection",
|
|
15136
|
+
"Summary"
|
|
15121
15137
|
];
|
|
15122
15138
|
MANUAL_CONTROLS = [
|
|
15123
|
-
"
|
|
15124
|
-
"
|
|
15125
|
-
|
|
15126
|
-
|
|
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."
|
|
15139
|
+
"GitHub still needs three repository controls:",
|
|
15140
|
+
" required check: tamperward",
|
|
15141
|
+
" Code Owner review",
|
|
15142
|
+
" dismiss stale approvals when new commits are pushed"
|
|
15130
15143
|
];
|
|
15144
|
+
ESC2 = "\x1B";
|
|
15145
|
+
RESET2 = `${ESC2}[0m`;
|
|
15146
|
+
BOLD2 = `${ESC2}[1m`;
|
|
15147
|
+
DIM2 = `${ESC2}[2m`;
|
|
15148
|
+
RED2 = `${ESC2}[31m`;
|
|
15149
|
+
YELLOW2 = `${ESC2}[33m`;
|
|
15150
|
+
GREEN2 = `${ESC2}[32m`;
|
|
15151
|
+
CYAN = `${ESC2}[36m`;
|
|
15131
15152
|
JS_TEST_PATH = /(?:^|\/)(?:[^/]+\.(?:test|spec)\.[cm]?[jt]sx?|__tests__\/[^/]+\.[cm]?[jt]sx?)$/;
|
|
15132
15153
|
JS_TEST_BLOCK = /^(\s*)(it|test|describe)(\s*\()/m;
|
|
15133
15154
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tamperward",
|
|
3
|
-
"version": "2.23.
|
|
3
|
+
"version": "2.23.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",
|