cockpit: --demo flag — one command from zero, custody-inert

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 <noreply@anthropic.com>
This commit is contained in:
mrwulf 2026-07-21 15:09:26 +02:00
parent 1acbaa1a76
commit b3239dba45
4 changed files with 111 additions and 3 deletions

View file

@ -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.

View file

@ -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:

View file

@ -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
# ---------------------------------------------------------------------------

View file

@ -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)