#!/usr/bin/env python3 """CKBunker HSM production validator — CLI entrypoint. Runs a short, structured sequence of tests against a live CKBunker + Coldcard deployment and exits non-zero if anything fails. Safe to run in CI or as a periodic monitor; all signing uses pre-crafted test PSBTs that you supply. Usage: ./hsm_validate.py # env/.env only ./hsm_validate.py --config config.yaml ./hsm_validate.py --url http://10.x.y.z:9823 --tests connectivity message_signing Exits: 0 all enabled tests passed (or were skipped) 1 at least one test failed 2 configuration error """ from __future__ import annotations import argparse import sys from pathlib import Path from ckbunker_hsm_sign import Harness, load_config from ckbunker_hsm_sign.harness import Verdict def parse_args() -> argparse.Namespace: p = argparse.ArgumentParser( description="Validate a CKBunker + Coldcard HSM deployment", formatter_class=argparse.RawDescriptionHelpFormatter, epilog=__doc__.split("Usage:")[1], ) p.add_argument("--config", type=Path, default=None, help="YAML configuration file (see config.example.yaml)") p.add_argument("--env", type=Path, default=Path(".env"), help="dotenv file to read (default: .env)") p.add_argument("--url", default=None, help="override CKBunker URL") p.add_argument("--tests", nargs="+", default=None, help="only run these tests (by name)") p.add_argument("--skip", nargs="+", default=None, help="skip these tests (by name)") p.add_argument("--verbose", "-v", action="store_true", help="dump every WebSocket frame") p.add_argument("--save-signed", default=None, help="write signed PSBTs from sign tests into this directory") p.add_argument("--list-tests", action="store_true", help="print test names and exit") return p.parse_args() def main() -> int: args = parse_args() try: overrides = { "url": args.url, "verbose": args.verbose, "save_signed_dir": args.save_signed, } cfg = load_config( yaml_path=args.config, dotenv_path=args.env if args.env.exists() else None, overrides={k: v for k, v in overrides.items() if v is not None}, ) except SystemExit as e: print(f"configuration error: {e}", file=sys.stderr) return 2 if args.list_tests: for name in cfg.tests: print(name) return 0 if args.tests: for k in cfg.tests: cfg.tests[k] = k in args.tests if args.skip: for k in args.skip: if k in cfg.tests: cfg.tests[k] = False harness = Harness(cfg) outcomes = harness.run_all() if any(o.verdict == Verdict.FAIL for o in outcomes): return 1 return 0 if __name__ == "__main__": sys.exit(main())