verify.py python
214 lines 7.5 KB
Raw
sha256:cf6265cea8c21d9228d90dec13ef6ec2dab5103d466db9cc4590681832de4bf8 docs(KD-STAGING): sync governance after KD-6b DONE Human 13 days ago
1 """``muse verify`` — whole-repository integrity check.
2
3 Walks every reachable commit from every branch ref and performs a five-tier
4 integrity check:
5
6 1. Every branch ref points to an existing, well-formed commit.
7 2. Every commit's snapshot exists.
8 3. Every object referenced by every snapshot exists, and (unless
9 ``--no-objects``) its SHA-256 is recomputed to detect silent corruption.
10 4. For every commit that carries an HMAC signature, the signature is verified
11 against the agent key stored under ``.muse/keys/<agent_id>.key``.
12 5. Missing agent keys are reported separately as ``kind="key_missing"`` so
13 agents can distinguish key-rotation events from genuine tamper detection
14 (``kind="signature"``).
15
16 This is Muse's equivalent of ``git fsck``. Run it periodically on long-lived
17 agent repositories or after recovering from a storage failure.
18
19 Usage::
20
21 muse verify # full check — re-hashes all objects
22 muse verify --no-objects # existence check only (faster)
23 muse verify --branch feat/x # check one branch only
24 muse verify --fail-fast # stop on first failure (CI-friendly)
25 muse verify --quiet # no output — exit code only
26 muse verify --json # machine-readable report
27
28 JSON output schema::
29
30 {
31 "repo_id": "<str>",
32 "refs_checked": <int>,
33 "commits_checked": <int>,
34 "snapshots_checked": <int>,
35 "objects_checked": <int>,
36 "signatures_checked": <int>,
37 "all_ok": <bool>,
38 "check_objects": <bool>,
39 "branch": "<str | null>",
40 "fail_fast": <bool>,
41 "failures": [
42 {
43 "kind": "ref|commit|snapshot|object|signature|key_missing",
44 "id": "<str>",
45 "error": "<str>"
46 }
47 ]
48 }
49
50 Exit codes::
51
52 0 — all checks passed
53 1 — one or more integrity failures detected
54 3 — I/O error reading repository files
55 """
56
57 from __future__ import annotations
58
59 import argparse
60 import json
61 import logging
62 import sys
63 from typing import TypedDict
64
65 from muse.core.errors import ExitCode
66 from muse.core.repo import read_repo_id, require_repo
67 from muse.core.validation import sanitize_display
68 from muse.core.verify import VerifyFailure, VerifyResult, run_verify
69
70 logger = logging.getLogger(__name__)
71
72
73 class _VerifyJson(TypedDict):
74 """JSON wire format for ``muse verify --json``."""
75
76 repo_id: str
77 refs_checked: int
78 commits_checked: int
79 snapshots_checked: int
80 objects_checked: int
81 signatures_checked: int
82 all_ok: bool
83 check_objects: bool
84 branch: str | None
85 fail_fast: bool
86 failures: list[VerifyFailure]
87
88
89 def register(subparsers: "argparse._SubParsersAction[argparse.ArgumentParser]") -> None:
90 """Register the verify subcommand."""
91 parser = subparsers.add_parser(
92 "verify",
93 help="Check repository integrity — commits, snapshots, and objects.",
94 description=__doc__,
95 formatter_class=argparse.RawDescriptionHelpFormatter,
96 )
97 parser.add_argument(
98 "--quiet", "-q", action="store_true",
99 help="No output — exit code only.",
100 )
101 parser.add_argument(
102 "--no-objects", "-O", action="store_true", dest="no_objects",
103 help="Existence check only — skip object re-hashing (faster).",
104 )
105 parser.add_argument(
106 "--branch", "-b", metavar="BRANCH", default=None,
107 help="Verify only the named branch instead of all branches.",
108 )
109 parser.add_argument(
110 "--fail-fast", "-F", action="store_true", dest="fail_fast",
111 help="Stop on the first failure (useful in CI pipelines).",
112 )
113 parser.add_argument(
114 "--json", action="store_true", dest="json_out",
115 help="Emit a machine-readable JSON report on stdout.",
116 )
117 parser.set_defaults(func=run)
118
119
120 def run(args: argparse.Namespace) -> None:
121 """Check repository integrity — commits, snapshots, and objects.
122
123 Walks every reachable commit from every branch ref. For each commit,
124 verifies that the snapshot exists. For each snapshot, verifies that every
125 object file exists and (by default) re-hashes it to detect bit-rot.
126
127 JSON output includes ``repo_id``, per-tier counters, and the full
128 ``failures`` list with ``kind``, ``id``, and ``error`` for each failure.
129 Failures with ``kind="key_missing"`` indicate that an agent key is absent
130 (possible key rotation) and should not be treated as hard corruption.
131
132 Exit code is 0 when all checks pass, 1 when any failure is found.
133 Use ``--quiet`` in scripts that only care about the exit code.
134 Use ``--json`` for agent pipelines that parse the failure list.
135
136 Examples::
137
138 muse verify # full integrity check
139 muse verify --no-objects # fast existence-only check
140 muse verify --branch feat/x # check one branch only
141 muse verify --fail-fast # abort on first failure
142 muse verify --quiet && echo "healthy"
143 muse verify --json | jq '.failures'
144 """
145 quiet: bool = args.quiet
146 no_objects: bool = args.no_objects
147 json_out: bool = args.json_out
148 branch: str | None = args.branch
149 fail_fast: bool = args.fail_fast
150
151 root = require_repo()
152
153 try:
154 result = run_verify(
155 root,
156 check_objects=not no_objects,
157 branch=branch,
158 fail_fast=fail_fast,
159 )
160 except OSError as exc:
161 if not quiet:
162 print(f"❌ I/O error during verify: {exc}", file=sys.stderr)
163 raise SystemExit(ExitCode.INTERNAL_ERROR) from exc
164
165 if quiet:
166 raise SystemExit(0 if result["all_ok"] else ExitCode.USER_ERROR)
167
168 if json_out:
169 repo_id = read_repo_id(root) or ""
170 payload: _VerifyJson = {
171 "repo_id": repo_id,
172 "refs_checked": result["refs_checked"],
173 "commits_checked": result["commits_checked"],
174 "snapshots_checked": result["snapshots_checked"],
175 "objects_checked": result["objects_checked"],
176 "signatures_checked": result["signatures_checked"],
177 "all_ok": result["all_ok"],
178 "check_objects": not no_objects,
179 "branch": branch,
180 "fail_fast": fail_fast,
181 "failures": result["failures"],
182 }
183 print(json.dumps(payload, indent=2))
184 else:
185 _print_text(result, no_objects=no_objects, branch=branch)
186
187 raise SystemExit(0 if result["all_ok"] else ExitCode.USER_ERROR)
188
189
190 def _print_text(
191 result: VerifyResult,
192 *,
193 no_objects: bool,
194 branch: str | None,
195 ) -> None:
196 """Render a human-readable summary of the verify result."""
197 if branch:
198 print(f"Scope: branch '{sanitize_display(branch)}'")
199 print(f"Checking refs... {result['refs_checked']} ref(s)")
200 print(f"Checking commits... {result['commits_checked']} commit(s)")
201 print(f"Checking snapshots... {result['snapshots_checked']} snapshot(s)")
202 action = "existence only" if no_objects else "re-hashed"
203 print(f"Checking objects... {result['objects_checked']} object(s) [{action}]")
204 print(f"Checking signatures... {result['signatures_checked']} signed commit(s)")
205
206 if result["all_ok"]:
207 print("✅ Repository is healthy.")
208 else:
209 failure_count = len(result["failures"])
210 print(f"\n❌ {failure_count} integrity failure(s):")
211 for f in result["failures"]:
212 kind = sanitize_display(f["kind"])
213 err = sanitize_display(f["error"])
214 print(f" {kind:<12} {f['id'][:24]} {err}")
File History 1 commit
sha256:cf6265cea8c21d9228d90dec13ef6ec2dab5103d466db9cc4590681832de4bf8 docs(KD-STAGING): sync governance after KD-6b DONE Human 13 days ago