From b3239dba450a410dc3f0f7738f9f3aa662467149 Mon Sep 17 00:00:00 2001 From: mrwulf Date: Tue, 21 Jul 2026 15:09:26 +0200 Subject: [PATCH] =?UTF-8?q?cockpit:=20--demo=20flag=20=E2=80=94=20one=20co?= =?UTF-8?q?mmand=20from=20zero,=20custody-inert?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The operator ran the documented command and hit the fail-fast (no wallet exists on a fresh machine) — correct behavior, useless instruction. seal_demo_wallet() now seals a throwaway DEMO wallet (fake shell-stub members, DEMO-labeled fields, temp dir named warden-DEMO-*, sample incident/refusal/airgap so every view has content); 'pacta wallet cockpit --demo' serves it. Exactly one of --wallet/--demo required. Verified by running the literal command: all five views 200, demo quorum renders. Suite 130 -> 131. Co-Authored-By: Claude Fable 5 --- docs/cockpit.md | 5 +++ src/pacta/cli.py | 15 +++++++-- src/pacta/walletui.py | 76 ++++++++++++++++++++++++++++++++++++++++++ tests/test_walletui.py | 18 ++++++++++ 4 files changed, 111 insertions(+), 3 deletions(-) diff --git a/docs/cockpit.md b/docs/cockpit.md index 83d0aab..48312a6 100644 --- a/docs/cockpit.md +++ b/docs/cockpit.md @@ -44,9 +44,14 @@ is out of scope until reproducible builds). ## Serving ```bash +pacta wallet cockpit --demo # no wallet yet? throwaway + # DEMO wallet, custody-inert pacta wallet cockpit --wallet ~/my-wallet # 127.0.0.1:8471 pacta wallet cockpit --wallet ~/my-wallet --port 9000 ``` +(Uninstalled, from the repo root: +`PYTHONPATH=src:provider/src python3 -m pacta wallet cockpit --demo`.) + The server binds localhost by default and is not meant to be exposed; there is no authentication because there is nothing to operate. diff --git a/src/pacta/cli.py b/src/pacta/cli.py index 86eaaf6..acf6bed 100644 --- a/src/pacta/cli.py +++ b/src/pacta/cli.py @@ -236,7 +236,8 @@ def build_parser() -> argparse.ArgumentParser: w_mcp.set_defaults(func=cmd_wallet_mcp) w_cockpit = wsub.add_parser("cockpit", help="Serve the read-only custody cockpit (local web UI) for the human operator.") - w_cockpit.add_argument("--wallet", required=True) + w_cockpit.add_argument("--wallet", help="Path to an existing wallet directory.") + w_cockpit.add_argument("--demo", action="store_true", help="No wallet yet? Serve a throwaway DEMO wallet (fake members, custody-inert) to explore the views.") w_cockpit.add_argument("--host", default="127.0.0.1", help="Bind address (default localhost; the cockpit is not meant to be exposed).") w_cockpit.add_argument("--port", type=int, default=8471) w_cockpit.set_defaults(func=cmd_wallet_cockpit) @@ -680,8 +681,16 @@ def cmd_wallet_card(args: argparse.Namespace) -> int: def cmd_wallet_cockpit(args: argparse.Namespace) -> int: - from .walletui import serve - server = serve(args.wallet, host=args.host, port=args.port) + from .walletui import seal_demo_wallet, serve + if bool(args.wallet) == bool(args.demo): + print("error: pass exactly one of --wallet DIR or --demo") + return 2 + wallet_dir = args.wallet + if args.demo: + wallet_dir = seal_demo_wallet() + print("DEMO wallet (throwaway, fake members, custody-inert):") + print(f" {wallet_dir}") + server = serve(wallet_dir, host=args.host, port=args.port) host, port = server.server_address[0], server.server_address[1] print(f"warden cockpit (READ-ONLY) on http://{host}:{port} - Ctrl-C to stop") try: diff --git a/src/pacta/walletui.py b/src/pacta/walletui.py index c98a390..bf38964 100644 --- a/src/pacta/walletui.py +++ b/src/pacta/walletui.py @@ -335,6 +335,82 @@ def render_inspect(result: dict[str, Any] | None, ) +# --------------------------------------------------------------------------- +# demo wallet - custody-inert, for exploring the cockpit from zero +# --------------------------------------------------------------------------- + +def seal_demo_wallet(root: str | Path | None = None) -> Path: + """Seal a throwaway DEMO wallet: four fake quorum members (shell stubs, + NOT the verified binaries), a genesis ledger, a fresh keypair, and one + sample incident / refusal / airgap request so every view has content. + + Custody-inert by construction: nothing here can sign anything real, + and the wallet lives in a throwaway directory whose name says DEMO. + """ + import stat + + from .quorum import binary_path + from .signing import generate_ed25519_keypair + + if root is None: + root = Path(tempfile.mkdtemp(prefix="warden-DEMO-")) + root = Path(root) + state_dir = root / "state" + state_dir.mkdir(parents=True, exist_ok=True) + + def _sha(data: bytes) -> str: + import hashlib + return hashlib.sha256(data).hexdigest() + + members = [] + for name in ("dalek", "anza", "risc0", "betrusted"): + binary = binary_path(name, state_dir) + binary.write_text("#!/bin/sh\necho OK\nexit 0\n") + binary.chmod(binary.stat().st_mode | stat.S_IEXEC) + members.append({ + "backend": name, "component": f"{name}-ed25519-verified", + "semantics": "DEMO", "entry_point": "DEMO", + "source_commit": "deadbeef" * 5, "repo_commit": "cafe" * 10, + "binary_sha256": _sha(binary.read_bytes()), + "backend_cfg": "DEMO", "risk_tier": "R4", + "evidence": {"leaf_hash": "00", "leaf_index": 0, "tree_size": 1, + "inclusion_proof": [], + "sth": {"timestamp": "2099-01-01T00:00:00Z"}}, + }) + wallet = Wallet(root / "wallet") + for sub in (wallet.keys_dir, wallet.incidents_dir, wallet.receipts_dir, + wallet.quarantine_dir, wallet.airgap_dir / "outbox", + wallet.airgap_dir / "inbox"): + sub.mkdir(parents=True, exist_ok=True) + capsule = { + "type": "pacta.wallet.custody_capsule.v1", + "created_at": _now(), "members": members, + "policy": {"require_unanimity": True, "min_members": 4, + "require_tier": "R4", "freshness_max_age_days": 0}, + "signing": {"backend": "DEMO"}, "problems_at_init": [], + } + wallet.capsule_path.write_text(json.dumps(capsule, indent=2, sort_keys=True) + "\n") + wallet._append_ledger("genesis", { + "type": "pacta.wallet.ledger_genesis.v1", + "capsule_sha256": _sha(json.dumps(capsule, sort_keys=True, + separators=(",", ":")).encode())}) + generate_ed25519_keypair(wallet.keys_dir / "warden.key.pem", + wallet.keys_dir / "warden.pub.pem") + (wallet.incidents_dir / "incident-0001.json").write_text(json.dumps( + {"type": "pacta.wallet.incident.v1", "severity": "divergence", + "detail": "DEMO sample: member risc0 returned INVALID where the " + "other three returned OK", + "at": _now(), "payload_sha256": "ab" * 32}, indent=2)) + (wallet.receipts_dir / "refusal-0001.json").write_text(json.dumps( + {"type": "pacta.wallet.refusal.v1", "code": "POLICY_DENIED", + "missing": ["allowlisted destination"], + "remediation": "DEMO sample: add destination to policy.json allowlist", + "at": _now()}, indent=2)) + (wallet.airgap_dir / "outbox" / "req-demo.request.json").write_text(json.dumps( + {"created_at": _now(), "payload_sha256": "cd" * 32})) + return wallet.dir + + # --------------------------------------------------------------------------- # server # --------------------------------------------------------------------------- diff --git a/tests/test_walletui.py b/tests/test_walletui.py index 46df3bd..b67833c 100644 --- a/tests/test_walletui.py +++ b/tests/test_walletui.py @@ -230,3 +230,21 @@ def test_estate_view_and_estate_md_do_not_drift(): # the runtime dimension must exist in BOTH renderings assert "What is running" in estate_md assert "ALWAYS ON" in ESTATE_HTML + + +def test_demo_wallet_seals_and_serves(tmp_path): + from pacta.walletui import seal_demo_wallet + wallet_dir = seal_demo_wallet(tmp_path) + server = serve(wallet_dir, host="127.0.0.1", port=0) + port = server.server_address[1] + thread = threading.Thread(target=server.serve_forever, daemon=True) + thread.start() + try: + with urllib.request.urlopen(f"http://127.0.0.1:{port}/") as resp: + body = resp.read().decode() + assert resp.status == 200 + assert "dalek-ed25519-verified" in body and "4 pinned" in body + assert "chain verified" in body + finally: + server.shutdown() + thread.join(timeout=5)