gabriel / muse public
verify.py python
216 lines 7.5 KB
Raw
sha256:2fa778aba8ab0ec15295b8624c6480a573482ffc9c206a6d9546f1c41d2c2b7b feat: supercharge muse blame + remove --porcelain everywhere Human patch 164 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 nothing_checked: bool
84 check_objects: bool
85 branch: str | None
86 fail_fast: bool
87 failures: list[VerifyFailure]
88
89
90 def register(subparsers: "argparse._SubParsersAction[argparse.ArgumentParser]") -> None:
91 """Register the verify subcommand."""
92 parser = subparsers.add_parser(
93 "verify",
94 help="Check repository integrity — commits, snapshots, and objects.",
95 description=__doc__,
96 formatter_class=argparse.RawDescriptionHelpFormatter,
97 )
98 parser.add_argument(
99 "--quiet", "-q", action="store_true",
100 help="No output — exit code only.",
101 )
102 parser.add_argument(
103 "--no-objects", "-O", action="store_true", dest="no_objects",
104 help="Existence check only — skip object re-hashing (faster).",
105 )
106 parser.add_argument(
107 "--branch", "-b", metavar="BRANCH", default=None,
108 help="Verify only the named branch instead of all branches.",
109 )
110 parser.add_argument(
111 "--fail-fast", "-F", action="store_true", dest="fail_fast",
112 help="Stop on the first failure (useful in CI pipelines).",
113 )
114 parser.add_argument(
115 "--json", action="store_true", dest="json_out",
116 help="Emit a machine-readable JSON report on stdout.",
117 )
118 parser.set_defaults(func=run)
119
120
121 def run(args: argparse.Namespace) -> None:
122 """Check repository integrity — commits, snapshots, and objects.
123
124 Walks every reachable commit from every branch ref. For each commit,
125 verifies that the snapshot exists. For each snapshot, verifies that every
126 object file exists and (by default) re-hashes it to detect bit-rot.
127
128 JSON output includes ``repo_id``, per-tier counters, and the full
129 ``failures`` list with ``kind``, ``id``, and ``error`` for each failure.
130 Failures with ``kind="key_missing"`` indicate that an agent key is absent
131 (possible key rotation) and should not be treated as hard corruption.
132
133 Exit code is 0 when all checks pass, 1 when any failure is found.
134 Use ``--quiet`` in scripts that only care about the exit code.
135 Use ``--json`` for agent pipelines that parse the failure list.
136
137 Examples::
138
139 muse verify # full integrity check
140 muse verify --no-objects # fast existence-only check
141 muse verify --branch feat/x # check one branch only
142 muse verify --fail-fast # abort on first failure
143 muse verify --quiet && echo "healthy"
144 muse verify --json | jq '.failures'
145 """
146 quiet: bool = args.quiet
147 no_objects: bool = args.no_objects
148 json_out: bool = args.json_out
149 branch: str | None = args.branch
150 fail_fast: bool = args.fail_fast
151
152 root = require_repo()
153
154 try:
155 result = run_verify(
156 root,
157 check_objects=not no_objects,
158 branch=branch,
159 fail_fast=fail_fast,
160 )
161 except OSError as exc:
162 if not quiet:
163 print(f"❌ I/O error during verify: {exc}", file=sys.stderr)
164 raise SystemExit(ExitCode.INTERNAL_ERROR) from exc
165
166 if quiet:
167 raise SystemExit(0 if result["all_ok"] else ExitCode.USER_ERROR)
168
169 if json_out:
170 repo_id = read_repo_id(root) or ""
171 payload: _VerifyJson = {
172 "repo_id": repo_id,
173 "refs_checked": result["refs_checked"],
174 "commits_checked": result["commits_checked"],
175 "snapshots_checked": result["snapshots_checked"],
176 "objects_checked": result["objects_checked"],
177 "signatures_checked": result["signatures_checked"],
178 "all_ok": result["all_ok"],
179 "nothing_checked": result["nothing_checked"],
180 "check_objects": not no_objects,
181 "branch": branch,
182 "fail_fast": fail_fast,
183 "failures": result["failures"],
184 }
185 print(json.dumps(payload, indent=2))
186 else:
187 _print_text(result, no_objects=no_objects, branch=branch)
188
189 raise SystemExit(0 if result["all_ok"] else ExitCode.USER_ERROR)
190
191
192 def _print_text(
193 result: VerifyResult,
194 *,
195 no_objects: bool,
196 branch: str | None,
197 ) -> None:
198 """Render a human-readable summary of the verify result."""
199 if branch:
200 print(f"Scope: branch '{sanitize_display(branch)}'")
201 print(f"Checking refs... {result['refs_checked']} ref(s)")
202 print(f"Checking commits... {result['commits_checked']} commit(s)")
203 print(f"Checking snapshots... {result['snapshots_checked']} snapshot(s)")
204 action = "existence only" if no_objects else "re-hashed"
205 print(f"Checking objects... {result['objects_checked']} object(s) [{action}]")
206 print(f"Checking signatures... {result['signatures_checked']} signed commit(s)")
207
208 if result["all_ok"]:
209 print("✅ Repository is healthy.")
210 else:
211 failure_count = len(result["failures"])
212 print(f"\n❌ {failure_count} integrity failure(s):")
213 for f in result["failures"]:
214 kind = sanitize_display(f["kind"])
215 err = sanitize_display(f["error"])
216 print(f" {kind:<12} {f['id'][:24]} {err}")
File History 1 commit
sha256:2fa778aba8ab0ec15295b8624c6480a573482ffc9c206a6d9546f1c41d2c2b7b feat: supercharge muse blame + remove --porcelain everywhere Human patch 164 days ago