cockpit: UX law — the cockpit never leaves a human in the dark

Operator verdict on the first cockpit: unusable, jargon-walled. The
evidence layer was honest but the presentation assumed the reader
already lived inside warden's head. This rebuilds the presentation
layer around a testable UX contract, additive to the design law:

- every page: verdict IN WORDS first (CUSTODY HEALTHY / CUSTODY FROZEN
  (LATCHED) / CUSTODY EVIDENCE BROKEN), then evidence, then provenance
- every page: plain-language lead saying what the page answers
- every panel: 'How to read this panel' expander interpreting every
  column and pill; jargon carries a ? linking to the glossary
- new /guide view: what warden is, how to read any page, color code,
  five-minute tour, 12-term glossary, honest 'what this cockpit cannot
  tell you'
- navigation: two-line tabs stating the question each view answers,
  present on every page; /estate gets a back-to-cockpit chip
- explained empty states (incidents: 'empty is the good state')
- narrow-viewport fix: tables/pre scroll in their own containers
- estate fact chip updated 130->135 green (both renderings)

UX contract is test-enforced (guide terms, lead+nav+explainers on every
view, empty states); read-only byte-guarantee sweep now covers /guide.
Verified by looking: served --demo, walked all six views in a browser.
Suite 131 -> 135 green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
mrwulf 2026-07-21 16:15:00 +02:00
parent b3239dba45
commit 00be6a396e
7 changed files with 579 additions and 79 deletions

View file

@ -9,7 +9,7 @@ machinery hub and the only repo that changes freely.
State snapshot (2026-07-19): log **13 leaves**, root `3488a2d0…`, key
fingerprint `874c8a00…`, paper **v0.9 camera-ready (23 pp)**, five
attested components, pacta suite 130 green.
attested components, pacta suite 135 green.
```mermaid
flowchart LR

View file

@ -145,14 +145,19 @@ pacta wallet mcp --wallet ./my-warden # stdio JSON-RPC MCP server
## The custody cockpit (human surface, read-only)
`pacta wallet cockpit --wallet <dir>` serves a local web UI for the
operator: posture (latch, ledger chain re-verified, pinned quorum),
the airgap signature queue (observed, never operated), the incident and
refusal browser, and a receipt inspector driven by the deployed
verifier. Design law: it renders recomputed evidence with provenance
lines, never cached status; it cannot approve, sign, unlatch, or modify
custody state (byte-level read-only guarantee in
`tests/test_walletui.py`). Details: [docs/cockpit.md](docs/cockpit.md).
`pacta wallet cockpit --wallet <dir>` (or `--demo` for a throwaway,
custody-inert wallet to explore from zero) serves a local web UI for the
operator: posture (verdict banner, latch, ledger chain re-verified,
pinned quorum), the airgap signature queue (observed, never operated),
the incident and refusal browser, a receipt inspector driven by the
deployed verifier, the estate map, and a plain-language guide with a
full glossary. Design law: it renders recomputed evidence with
provenance lines, never cached status; it cannot approve, sign, unlatch,
or modify custody state (byte-level read-only guarantee in
`tests/test_walletui.py`). UX law: every page explains itself — verdict
in words, per-panel "how to read this" expanders, jargon linked to the
`/guide` glossary (also test-enforced). Details:
[docs/cockpit.md](docs/cockpit.md).
## Agent-native surface (MCP)

View file

@ -16,6 +16,36 @@ recomputed renders as a loud red FAILED-TO-VERIFY panel. There is no
cached green and no neutral gray — a cockpit that shows unverified green
lights would be the anti-warden.
## The UX law (the design law's twin)
**The cockpit never leaves a human in the dark.** A person who has never
heard of warden must be able to read every screen. Concretely, every
page is built from the same anatomy, top to bottom:
1. **Verdict in words** — e.g. CUSTODY HEALTHY / CUSTODY FROZEN
(LATCHED) / CUSTODY EVIDENCE BROKEN — before any evidence, with one
sentence saying what that means and what to do.
2. **A plain-language lead** stating what the page shows and what it
cannot do.
3. **Panels that explain themselves**: each opens with a plain sentence,
carries a "How to read this panel" expander interpreting every column
and every pill, and links each jargon term to the glossary via a
small `?`.
4. **Explained empty states** — an empty list says what empty means and
whether it is good news (for incidents, it is).
5. **The provenance line** — the dashed footer naming the exact function
and timestamp that recomputed the panel.
The `/guide` view is the manual: what warden is, how to read any page,
the color code, a five-minute tour, a glossary of every term (capsule,
member, pinning, evidence grades R0R5, ledger, latch, incident, refusal
receipt, air-gap, attestation/receipt, provenance, DEMO), and an honest
"what this cockpit cannot tell you" section. Navigation tabs state the
question each view answers. This contract is enforced by tests
(`test_guide_view_explains_every_term`,
`test_every_view_carries_lead_nav_and_explainers`,
`test_empty_states_are_explained`).
## The read-only guarantee
The cockpit cannot approve, sign, unlatch, or modify custody state. It
@ -27,15 +57,16 @@ wallet directory hash-identical. Human approve/deny is deliberately NOT
here — that would be a custody-semantics change, which belongs to a
separate, explicitly reviewed milestone.
## The four views
## The six views
| view | shows | recomputed by |
| view | answers | recomputed by |
|---|---|---|
| **Posture** (`/`) | custody latch state, ledger head with full hash-chain re-verification, the pinned quorum members (backend, component, tier, source commit, binary hash), spending policy verbatim, incident/refusal counts | `Wallet.posture()` / `Wallet.verify_ledger()` |
| **Signature queue** (`/queue`) | parked airgap signing requests (outbox) and whether the device has answered (inbox) — observed, never operated | airgap outbox/inbox listing |
| **Incidents & refusals** (`/incidents`) | incident records and signed refusal receipts, verbatim, newest first | `incidents/*.json`, `receipts/*.json` |
| **Estate map** (`/estate`) | the whole endeavour — every repo, service, mirror, loop — with RUNTIME on every entity (always-on / on-demand / not-running / static) | rendering of ESTATE.md's model (drift-guarded by test) |
| **Receipt inspector** (`/inspect`) | paste an attestation + transparency receipt + log public key; the verdict, per-signature results, and diagnostics come verbatim from the deployed verifier | `pacta.transparency.verify_receipt` |
| **Posture** (`/`) | *Is custody healthy right now?* Verdict banner, then: custody latch, ledger with full hash-chain re-verification, the pinned quorum members (backend, component, evidence grade, source commit, binary fingerprint), signing rules verbatim, incident/refusal counts | `Wallet.posture()` / `Wallet.verify_ledger()` |
| **Queue** (`/queue`) | *What awaits the offline signer?* Parked air-gap signing requests (outbox) and whether the device has answered (inbox) — observed, never operated | airgap outbox/inbox listing |
| **Incidents** (`/incidents`) | *What has ever gone wrong?* Incident records and signed refusal receipts, verbatim, newest first — with the page explaining why empty is the good state | `incidents/*.json`, `receipts/*.json` |
| **Inspect** (`/inspect`) | *Can I check a receipt myself?* Paste an attestation + transparency receipt + log public key; the verdict, per-signature results, and diagnostics come verbatim from the deployed verifier | `pacta.transparency.verify_receipt` |
| **Estate map** (`/estate`) | *Where does this wallet sit in the whole endeavour?* Every repo, service, mirror, loop — with RUNTIME on every entity (always-on / on-demand / not-running / static) | rendering of ESTATE.md's model (drift-guarded by test) |
| **Guide** (`/guide`) | *What does any of this mean?* The manual: plain-language explanations, color code, tour, full glossary, honest limits — static, no live data | — |
Every panel also states what it does **not** prove (e.g. the quorum
table says binary hashes are pinned but source-to-binary correspondence

View file

@ -693,6 +693,7 @@ def cmd_wallet_cockpit(args: argparse.Namespace) -> int:
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")
print(f" first time? start at http://{host}:{port}/guide - every term explained")
try:
server.serve_forever()
except KeyboardInterrupt:

View file

@ -123,7 +123,7 @@ ESTATE_HTML = r'''<title>LTL estate map — repos, services, loops</title>
<span class="fact">key <b class="mono">874c8a00</b></span>
<span class="fact">paper <b>v0.9 · 23 pp · camera-ready</b></span>
<span class="fact">attested components <b>5</b></span>
<span class="fact">pacta suite <b>130 green</b></span>
<span class="fact">pacta suite <b>135 green</b></span>
<span class="fact">state as of <b>2026-07-20</b></span>
</div>
<div class="facts" style="padding-top:.15rem">

View file

@ -1,9 +1,9 @@
"""walletui - the warden custody cockpit (local, read-only).
A localhost web surface over an existing wallet directory, for the human
operator who ultimately answers for the money. Four views: posture, the
operator who ultimately answers for the money. Six views: posture, the
pending-signature queue (airgap outbox), the incident & refusal browser,
and a receipt inspector.
a receipt inspector, the estate map, and a plain-language guide.
Design law: THE COCKPIT RENDERS EVIDENCE, IT NEVER ASSERTS IT. Every
panel is recomputed from wallet state or submitted artifacts at request
@ -12,6 +12,13 @@ the function and timestamp that produced it. Anything that cannot be
recomputed renders as a loud FAILED-TO-VERIFY panel - there is no cached
green and no neutral gray.
UX law (the design law's twin): THE COCKPIT NEVER LEAVES A HUMAN IN THE
DARK. Every page opens with a plain-language statement of what it shows;
every verdict is stated in words, not just color; every panel carries a
"how to read this" explainer; every piece of jargon links to the /guide
glossary. A person who has never heard of warden must be able to read
every screen.
Read-only guarantee: this module calls only read paths (``Wallet.posture``,
``verify_ledger``, directory listings) and ``transparency.verify_receipt``
on submitted artifacts (parsed in memory / temp files outside the wallet).
@ -48,12 +55,23 @@ _STYLE = """
h2{font-size:1.05rem;margin:1.6rem 0 .5rem}
code{font-family:ui-monospace,Menlo,Consolas,monospace;background:#eef0f3;
border-radius:4px;padding:.08rem .3rem;font-size:.88em}
.sub{color:var(--ink2);font-size:.85rem;margin:.3rem 0 .6rem}
nav{margin:.7rem 0 1rem;display:flex;gap:.5rem;flex-wrap:wrap}
nav a{color:var(--accent);text-decoration:none;border:1px solid var(--line);
background:#fff;border-radius:6px;padding:.25rem .7rem;font-size:.85rem}
nav a.here{border-color:var(--accent);font-weight:600}
background:#fff;border-radius:6px;padding:.3rem .7rem;font-size:.85rem;
display:flex;flex-direction:column;line-height:1.25;min-width:7.5rem}
nav a.here{border-color:var(--accent);font-weight:600;background:var(--accentbg)}
nav a .navsub{font-size:.67rem;color:var(--ink2);font-weight:400}
.banner{background:var(--warnbg);border:1px solid var(--warn);color:var(--warn);
border-radius:6px;padding:.45rem .8rem;font-size:.82rem;font-weight:600}
.banner a{color:var(--warn)}
.lead{font-size:.92rem;margin:.9rem 0 .2rem}
.verdict{border-radius:8px;padding:.8rem 1.1rem;margin:.8rem 0;border:1px solid}
.verdict strong{font-size:1.05rem;letter-spacing:.02em}
.verdict p{margin:.3rem 0 0;font-size:.88rem;font-weight:400}
.verdict.ok{background:var(--okbg);border-color:var(--ok);color:var(--ok)}
.verdict.warn{background:var(--warnbg);border-color:var(--warn);color:var(--warn)}
.verdict.bad{background:var(--badbg);border-color:var(--bad);color:var(--bad)}
.panel{background:#fff;border:1px solid var(--line);border-radius:8px;
padding:.9rem 1.1rem;margin:.7rem 0}
.panel.bad{border-color:var(--bad);background:var(--badbg)}
@ -64,11 +82,27 @@ _STYLE = """
.pill.ok{background:var(--okbg);color:var(--ok)}
.pill.bad{background:var(--badbg);color:var(--bad)}
.pill.warn{background:var(--warnbg);color:var(--warn)}
a.help{display:inline-block;width:1.05rem;height:1.05rem;line-height:1.05rem;text-align:center;
border-radius:50%;background:var(--accentbg);color:var(--accent);font-size:.72rem;
font-weight:700;text-decoration:none;vertical-align:.15em}
details.explain{margin-top:.55rem;font-size:.82rem}
details.explain summary{cursor:pointer;color:var(--accent);font-weight:600;font-size:.78rem}
details.explain .expl{color:var(--ink2);margin:.4rem 0 0;padding:.5rem .7rem;
background:var(--accentbg);border-radius:6px}
details.explain .expl ul{margin:.3rem 0;padding-left:1.1rem}
details.explain .expl li{margin:.15rem 0}
.plain{font-size:.88rem;margin:.3rem 0 .6rem}
.empty{color:var(--ink2);font-size:.88rem;background:var(--accentbg);border-radius:6px;
padding:.5rem .8rem}
pre{overflow-x:auto}
.tablewrap{overflow-x:auto}
table{border-collapse:collapse;width:100%;font-size:.88rem;background:#fff}
td,th{border:1px solid var(--line);padding:.4rem .6rem;text-align:left;vertical-align:top}
th{background:var(--accentbg)}
ul.diag{margin:.4rem 0 0;padding-left:1.2rem}
ul.diag li{font-size:.85rem;margin:.2rem 0}
dl.gloss dt{font-weight:700;margin-top:.8rem}
dl.gloss dd{margin:.15rem 0 0 0;font-size:.88rem;color:var(--ink)}
textarea{width:100%;min-height:7.5rem;font-family:ui-monospace,monospace;font-size:.8rem;
border:1px solid var(--line);border-radius:6px;padding:.5rem}
button{background:var(--accent);color:#fff;border:0;border-radius:6px;
@ -86,6 +120,18 @@ def _esc(value: Any) -> str:
return html.escape(str(value))
def _help(anchor: str) -> str:
"""A small ? that jumps to the glossary entry for a term."""
return (f'<a class="help" href="/guide#{anchor}" '
f'title="what does this mean? — explained in the guide">?</a>')
def _explain(body: str) -> str:
"""The per-panel interpretation aid: always present, opt-in detail."""
return (f'<details class="explain"><summary>How to read this panel</summary>'
f'<div class="expl">{body}</div></details>')
def _provenance(via: str) -> str:
return f'<div class="prov">recomputed {_esc(_now())} via <code>{_esc(via)}</code> — nothing on this panel is cached or asserted.</div>'
@ -95,7 +141,10 @@ def _failed_panel(what: str, via: str, error: Exception) -> str:
f'<div class="panel bad"><span class="pill bad">FAILED TO VERIFY</span> '
f"<strong>{_esc(what)}</strong> could not be recomputed: "
f"<code>{_esc(f'{type(error).__name__}: {error}')}</code>. "
f"A cockpit that cannot verify shows red, never a stale green."
f"A cockpit that cannot verify shows red, never a stale green. "
f"<span class='muted'>What to do: check that the wallet directory still exists and is "
f"readable, then reload. If this persists, inspect from the command line with "
f"<code>pacta wallet posture</code>.</span>"
f"{_provenance(via)}</div>"
)
@ -184,29 +233,94 @@ def inspect_receipt(attestation_text: str, receipt_text: str,
# ---------------------------------------------------------------------------
# renderers - pure string builders over collector output
# page shell - one navigation, one lead paragraph, on every view
# ---------------------------------------------------------------------------
_VIEWS = [("/", "Posture"), ("/queue", "Signature queue"),
("/incidents", "Incidents & refusals"), ("/inspect", "Receipt inspector"),
("/estate", "Estate map")]
_VIEWS = [
("/", "Posture", "is custody healthy right now?"),
("/queue", "Queue", "what awaits the offline signer?"),
("/incidents", "Incidents", "what has ever gone wrong?"),
("/inspect", "Inspect", "check a receipt yourself"),
("/estate", "Estate map", "the whole system, drawn"),
("/guide", "Guide", "every term, explained"),
]
_LEADS = {
"/": ('This page answers one question: <strong>is custody healthy right now?</strong> '
'The verdict comes first, the evidence behind it below. Every panel ends with a '
'dashed provenance line naming the exact function that just recomputed it — '
'and every small <a class="help" href="/guide#glossary">?</a> jumps to a '
'plain-language explanation.'),
"/queue": ('The wallets signing key can live on an <em>air-gapped</em> device — a '
'computer with no network connection. To get something signed, the wallet '
'parks a request file in an outbox; a human carries it to the device; the '
'answer lands in an inbox. This page watches those two folders. It cannot '
'approve, refuse, or move anything.'),
"/incidents": ('The wallets paper trail. An <strong>incident</strong> is the wallet '
'noticing something wrong and writing it down on the spot. A '
'<strong>refusal receipt</strong> is the wallet saying <em>no</em> in '
'writing — with the rule it applied and what would fix the request. '
'An empty page here is good news.'),
"/inspect": ('Paste verification artifacts below and this page re-runs the wallets '
'own verifier on them, on your machine, without writing anything to the '
'wallet. Use it to check evidence somebody handed you before trusting it.'),
"/guide": ('Plain-language explanations for everything this cockpit shows. Nothing on '
'this page is live data — this is the manual. The other five tabs are the '
'instruments.'),
}
def _page(title: str, active: str, body: str, wallet_dir: str) -> str:
nav = "".join(
f'<a href="{href}"{" class=here" if href == active else ""}>{label}</a>'
for href, label in _VIEWS)
f'<a href="{href}"{" class=here" if href == active else ""}>{label}'
f'<span class="navsub">{sub}</span></a>'
for href, label, sub in _VIEWS)
demo_badge = ('<span class="pill warn" title="sealed by --demo; fake members; can sign '
'nothing real">DEMO WALLET — custody-inert</span> '
if "DEMO" in wallet_dir else "")
lead = _LEADS.get(active, "")
lead_html = f'<p class="lead">{lead}</p>' if lead else ""
return (
"<!doctype html><html><head><meta charset='utf-8'>"
"<meta name='viewport' content='width=device-width,initial-scale=1'>"
f"<title>warden cockpit — {_esc(title)}</title>"
f"<style>{_STYLE}</style></head><body>"
f"<h1>warden cockpit <span class='muted mono'>{_esc(wallet_dir)}</span></h1>"
f"<h1>warden custody cockpit {demo_badge}</h1>"
f"<p class='sub'>Watching wallet <code>{_esc(wallet_dir)}</code> — everything below is "
"recomputed live from that directory each time a page loads; nothing is cached, "
"nothing is taken on trust.</p>"
"<div class='banner'>READ-ONLY. This cockpit observes and recomputes; it cannot "
"approve, sign, unlatch, or modify custody state.</div>"
f"<nav>{nav}</nav>{body}</body></html>"
"approve, sign, unlatch, or modify custody state. First time here? Start with the "
"<a href='/guide'>Guide</a> — every term on these pages is explained there.</div>"
f"<nav>{nav}</nav>{lead_html}{body}</body></html>"
)
# ---------------------------------------------------------------------------
# renderers - pure string builders over collector output
# ---------------------------------------------------------------------------
def _posture_verdict(p: dict[str, Any]) -> str:
"""The one-glance answer, in words, before any evidence."""
ledger = p["ledger"]
if not ledger["chain_ok"] or ledger["problems"]:
return ("<div class='verdict bad'><strong>CUSTODY EVIDENCE BROKEN</strong>"
"<p>The ledger — the wallet's tamper-evident history — did not recompute "
"cleanly. Until you know why, treat this wallet as compromised: do not "
"trust its history and do not sign with it. The Ledger panel below shows "
"exactly which check failed.</p></div>")
if p["latch"].get("latched"):
return ("<div class='verdict warn'><strong>CUSTODY FROZEN (LATCHED)</strong>"
"<p>The wallet detected a problem and pulled its own emergency brake: all "
"outbound signing is frozen until a human investigates and deliberately "
"clears the latch. Nothing can be signed right now. The latch panel below "
"names the trigger; <code>docs/runbook-latch.md</code> is the step-by-step "
"recovery guide.</p></div>")
return ("<div class='verdict ok'><strong>CUSTODY HEALTHY</strong>"
"<p>History intact, quorum sealed, signing unfrozen. Each of those three claims "
"is re-checked — not remembered — in the panels below.</p></div>")
def render_posture(posture: dict[str, Any]) -> str:
if not posture["ok"]:
return _failed_panel("Custody posture", posture["via"], posture["error"])
@ -224,38 +338,98 @@ def render_posture(posture: dict[str, Any]) -> str:
f"<td class='mono'>{_esc(m['source_commit'][:12])}…</td>"
f"<td class='mono'>{_esc(m['binary_sha256'][:16])}…</td></tr>"
for m in p["members"])
problems = "".join(f"<li>{_esc(x)}</li>" for x in ledger["problems"]) or "<li>none</li>"
problems = "".join(f"<li>{_esc(x)}</li>" for x in ledger["problems"]) or (
"<li>none — every link in the chain held</li>")
latch_detail = ""
if latch.get("latched"):
latch_detail = (f"<p>reason: <code>{_esc(latch.get('reason'))}</code> · "
f"incident: <code>{_esc(latch.get('incident'))}</code> · "
f"since {_esc(latch.get('at'))} — see the "
f"<a href='/incidents'>incident browser</a> and docs/runbook-latch.md.</p>")
latch_detail = (f"<p class='plain'>Trigger: <code>{_esc(latch.get('reason'))}</code> · "
f"recorded as incident <code>{_esc(latch.get('incident'))}</code> · "
f"frozen since {_esc(latch.get('at'))}. Read the incident in the "
f"<a href='/incidents'>incident browser</a>, then follow "
f"<code>docs/runbook-latch.md</code> to recover.</p>")
spending = p.get("spending_policy") or {}
no_rules = (not spending) or ("note" in spending and len(spending) == 1)
spending_note = (
"<p class='empty'>No spending rules are configured for this wallet: beyond the "
"quorum gate and the latch, outbound signing is unrestricted — the wallet's own "
"words below say so. A real deployment would define limits and allowlists in "
"<code>policy.json</code>.</p>" if no_rules else
"<p class='plain'>These rules are enforced by the signing firewall before anything "
"is signed. Shown verbatim from <code>policy.json</code>.</p>")
return (
f"<div class='panel'><h2 style='margin-top:0'>Custody latch {latch_pill}</h2>"
f"{latch_detail}{_provenance('Wallet.latch_state()')}</div>"
f"<div class='panel'><h2 style='margin-top:0'>Ledger {chain_pill}</h2>"
f"<p>{ledger['entries']} entries · head <code>{_esc(ledger['head'][:24])}…</code></p>"
_posture_verdict(p)
# --- latch ---------------------------------------------------------
+ f"<div class='panel'><h2 style='margin-top:0'>Custody latch {_help('latch')} {latch_pill}</h2>"
"<p class='plain'>The latch is the wallet's emergency brake. It trips when the "
"quorum disagrees or tampering is suspected, and freezes all outbound signing "
"until an operator clears it through the wallet's own channels — never from "
"this page.</p>"
f"{latch_detail}"
+ _explain(
"<ul><li><span class='pill ok'>unlatched</span> — the brake is off; signing is "
"allowed (subject to quorum and policy).</li>"
"<li><span class='pill bad'>LATCHED</span> — the brake is on; every signing "
"request is refused with a receipt until a human resolves the trigger. "
"Recovery steps live in <code>docs/runbook-latch.md</code>.</li></ul>")
+ f"{_provenance('Wallet.latch_state()')}</div>"
# --- ledger --------------------------------------------------------
f"<div class='panel'><h2 style='margin-top:0'>Ledger — has history been tampered with? {_help('ledger')} {chain_pill}</h2>"
"<p class='plain'>The ledger is the wallet's append-only journal: every custody "
"event, in order, each entry carrying the hash of the one before it. Editing, "
"deleting, or reordering anything in the past breaks the chain visibly.</p>"
f"<p>{ledger['entries']} entries · newest-entry hash (the «head»): "
f"<code>{_esc(ledger['head'][:24])}…</code></p>"
f"<ul class='diag'>{problems}</ul>"
f"{_provenance('Wallet.verify_ledger() — full hash-chain recomputation')}</div>"
f"<div class='panel'><h2 style='margin-top:0'>Quorum members "
+ _explain(
"<ul><li><span class='pill ok'>chain verified</span> — the cockpit just "
"re-hashed every entry from the first to the newest and every link held. This "
"happens again on every reload.</li>"
"<li><span class='pill bad'>CHAIN BROKEN</span> — at least one link failed: "
"history was altered, truncated, or corrupted. The list above names the first "
"entry that failed.</li></ul>")
+ f"{_provenance('Wallet.verify_ledger() — full hash-chain recomputation')}</div>"
# --- quorum --------------------------------------------------------
f"<div class='panel'><h2 style='margin-top:0'>Quorum — who must agree before anything is trusted? {_help('member')} "
f"<span class='pill ok'>{len(p['members'])} pinned</span></h2>"
"<table><tr><th>backend</th><th>component</th><th>tier</th>"
"<th>source commit</th><th>binary sha256</th></tr>"
f"{members}</table>"
"<p class='muted'>Every member is pinned by binary hash in the capsule; the capsule "
f"hash is <code>{_esc(p['capsule_sha256'][:24])}…</code>. What this table does NOT "
f"<p class='plain'>These are the {len(p['members'])} verifier programs this wallet "
"trusts, each built from a <em>different</em> formally verified codebase. Before "
"the wallet accepts a cryptographic component, every member must independently "
"reach the same verdict — a single dissenter freezes custody instead.</p>"
"<div class='tablewrap'><table><tr><th>member</th><th>built from</th>"
f"<th>evidence grade {_help('tier')}</th><th>source commit</th>"
f"<th>binary fingerprint {_help('pinned')}</th></tr>"
f"{members}</table></div>"
+ _explain(
"<ul><li><strong>member</strong> — short name of the verifier backend.</li>"
"<li><strong>built from</strong> — the formally verified repository the member "
"binary was compiled from.</li>"
"<li><strong>evidence grade</strong> — R0R5 scale of the formal evidence behind "
"the member: R0 = no usable evidence, R4 = machine-checked proofs covering the "
"full documented boundary, R5 would add reproducible builds and side-channel "
"assurance. This capsule requires R4.</li>"
"<li><strong>source commit</strong> — the exact git commit of those verified "
"sources (first 12 characters shown).</li>"
"<li><strong>binary fingerprint</strong> — SHA-256 hash of the member "
"executable (first 16 characters shown). The capsule pins the full value; a "
"swapped or modified binary fails the comparison and is rejected.</li></ul>")
+ "<p class='muted'>Everything above is sealed in the "
f"<a href='/guide#capsule'>custody capsule</a> — the wallet's founding document, "
f"fingerprint <code>{_esc(p['capsule_sha256'][:24])}…</code>, anchored in the "
"ledger's first entry so it cannot be quietly swapped. What this table does NOT "
"prove: that the binaries correspond to the attested sources (reproducible builds "
"are out of scope, stated in the paper and the claim cards)."
"are out of scope — that gap is grade R5 — stated in the paper and the claim cards)."
f"{_provenance('Wallet.capsule() / Wallet.posture()')}</div>"
f"<div class='panel'><h2 style='margin-top:0'>Spending policy</h2>"
# --- signing rules -------------------------------------------------
f"<div class='panel'><h2 style='margin-top:0'>Signing rules (spending policy)</h2>"
f"{spending_note}"
f"<pre style='margin:0;font-size:.8rem'>{_esc(json.dumps(spending, indent=2, sort_keys=True))}</pre>"
f"{_provenance('Wallet.policy() (policy.json, verbatim)')}</div>"
f"<div class='panel'><h2 style='margin-top:0'>Counters</h2>"
f"<p>incidents: <strong>{p['incidents']}</strong> · refusal receipts: "
f"<strong>{p['refusal_receipts']}</strong> — browse them under "
"<a href='/incidents'>Incidents &amp; refusals</a>.</p>"
# --- recorded history ----------------------------------------------
f"<div class='panel'><h2 style='margin-top:0'>Recorded history</h2>"
f"<p class='plain'>Incidents on file: <strong>{p['incidents']}</strong> · refusal "
f"receipts on file: <strong>{p['refusal_receipts']}</strong> — read every one, "
"verbatim, under <a href='/incidents'>Incidents</a>. An incident is the wallet "
"noticing something wrong; a refusal receipt is the wallet saying no, in writing.</p>"
f"{_provenance('directory counts, recomputed')}</div>"
)
@ -269,13 +443,23 @@ def render_queue(airgap: dict[str, Any]) -> str:
f"<td class='mono'>{_esc((r.get('payload_sha256') or '')[:24])}…</td>"
f"<td>{'<span class=\"pill ok\">answered</span>' if r['answered'] else '<span class=\"pill warn\">awaiting device</span>'}</td></tr>"
for r in airgap["data"])
body = (f"<table><tr><th>request</th><th>created</th><th>payload sha256</th>"
f"<th>state</th></tr>{rows}</table>" if airgap["data"]
else "<p class='muted'>No parked signing requests.</p>")
body = (f"<div class='tablewrap'><table><tr><th>request</th><th>created</th>"
f"<th>payload fingerprint</th><th>state</th></tr>{rows}</table></div>"
if airgap["data"]
else "<p class='empty'>No signing requests are waiting. The moment the wallet "
"parks one for the offline signer, it appears here.</p>")
return (
"<div class='panel'><h2 style='margin-top:0'>Pending airgap signatures</h2>"
+ body +
"<p class='muted'>This queue is OBSERVED, not operated: completing or refusing a "
f"<div class='panel'><h2 style='margin-top:0'>Waiting for the offline signer {_help('airgap')}</h2>"
+ body
+ _explain(
"<ul><li><span class='pill warn'>awaiting device</span> — the request file is "
"parked in the outbox; the offline signer has not answered yet.</li>"
"<li><span class='pill ok'>answered</span> — a response file for this request "
"has arrived in the inbox.</li>"
"<li><strong>payload fingerprint</strong> — SHA-256 of the exact bytes to be "
"signed (first 24 characters shown). The device signs those bytes and nothing "
"else, so you can compare fingerprints on both machines before approving.</li></ul>")
+ "<p class='muted'>This queue is OBSERVED, not operated: completing or refusing a "
"request happens through the wallet's own channels (<code>request_signature</code> "
"over MCP, or the airgap device flow), never from this page.</p>"
+ _provenance("airgap outbox/inbox listing") + "</div>"
@ -283,23 +467,55 @@ def render_queue(airgap: dict[str, Any]) -> str:
def render_incidents(incidents: dict[str, Any], refusals: dict[str, Any]) -> str:
def block(title: str, coll: dict[str, Any], via_note: str) -> str:
def block(title: str, coll: dict[str, Any], intro: str, empty: str,
explain: str, via_note: str) -> str:
if not coll["ok"]:
return _failed_panel(title, coll["via"], coll["error"])
items = coll["data"]
if not items:
body = "<p class='muted'>none recorded</p>"
body = f"<p class='empty'>{empty}</p>"
else:
body = "".join(
f"<div class='panel' style='margin:.5rem 0'><code>{_esc(i['_file'])}</code>"
f"<div class='panel' style='margin:.5rem 0'>"
f"<span class='muted'>file</span> <code>{_esc(i['_file'])}</code>"
f"<span class='muted'> — shown verbatim:</span>"
f"<pre style='font-size:.76rem;overflow-x:auto'>{_esc(json.dumps({k: v for k, v in i.items() if k != '_file'}, indent=2, sort_keys=True))}</pre></div>"
for i in items[:50])
return (f"<div class='panel'><h2 style='margin-top:0'>{_esc(title)}</h2>{body}"
return (f"<div class='panel'><h2 style='margin-top:0'>{title}</h2>"
f"<p class='plain'>{intro}</p>{body}{_explain(explain)}"
f"{_provenance(via_note)}</div>")
return (block("Incidents (quorum divergences, quarantines)", incidents,
"incidents/*.json, verbatim, newest first")
+ block("Refusal receipts (signed, machine-actionable)", refusals,
"receipts/*.json, verbatim, newest first"))
return (
block(f"Incidents — what the wallet noticed {_help('incident')}", incidents,
"Each file below is the wallet recording, at the moment it happened, that "
"something did not add up. The most serious kind is a <em>quorum "
"divergence</em>: the verifier members disagreed about the same input, "
"which must never happen if all of them are honest and intact.",
"None recorded — the wallet has never detected a divergence or tamper "
"event. Empty is the good state here.",
"<ul><li><strong>severity</strong> — how bad: a <code>divergence</code> or "
"<code>tamper</code> incident also trips the custody latch.</li>"
"<li><strong>detail</strong> — what exactly was observed, in the wallet's "
"own words.</li>"
"<li><strong>at</strong> — when it was recorded (UTC).</li>"
"<li><strong>payload_sha256</strong> — fingerprint of the input the members "
"disagreed about, so the case can be replayed later.</li>"
"<li>Incident files are never deleted; they are the permanent record.</li></ul>",
"incidents/*.json, verbatim, newest first")
+ block(f"Refusal receipts — every «no», in writing {_help('refusal')}", refusals,
"When the wallet declines to do something, it answers with a signed, "
"machine-readable receipt instead of a bare error: the rule it applied "
"(<code>code</code>), what was missing, and what would fix it "
"(<code>remediation</code>). An agent — or you — can read it, correct the "
"problem, and retry. No guessing.",
"None recorded — nothing has been refused yet.",
"<ul><li><strong>code</strong> — the rule that fired, e.g. "
"<code>POLICY_DENIED</code> (a spending rule) or <code>CUSTODY_LATCHED</code> "
"(the emergency brake is on).</li>"
"<li><strong>missing</strong> — the concrete precondition that was not "
"met.</li>"
"<li><strong>remediation</strong> — what would make the same request "
"succeed.</li></ul>",
"receipts/*.json, verbatim, newest first"))
def render_inspect(result: dict[str, Any] | None,
@ -318,23 +534,181 @@ def render_inspect(result: dict[str, Any] | None,
diags = "".join(f"<li>{_esc(x)}</li>" for x in result["diagnostics"]) or "<li>none</li>"
verdict = (
f"<div class='panel'><h2 style='margin-top:0'>Verdict {pill}</h2>"
f"<table><tr><th>signature check</th><th>result</th></tr>{sigs}</table>"
"<p class='plain'>A green verdict means every check passed: the "
"signatures are valid and the attestation really is recorded in the log "
"under the signed head. A red verdict means at least one check failed — "
"the diagnostics below name each check verbatim, so you can see exactly "
"which one.</p>"
f"<div class='tablewrap'><table><tr><th>signature check</th>"
f"<th>result</th></tr>{sigs}</table></div>"
f"<h2>Diagnostics</h2><ul class='diag'>{diags}</ul>"
f"{_provenance(result['via'])}</div>")
return (
verdict +
"<div class='panel'><h2 style='margin-top:0'>Inspect a receipt</h2>"
"<p class='muted'>Paste an attestation, its transparency receipt, and the log's "
"public key. The verdict is produced by the wallet's own deployed verifier — "
"this page adds nothing and hides nothing; the diagnostics list is verbatim.</p>"
"<form method='post' action='/inspect'>"
f"<p><strong>attestation.json</strong><br><textarea name='attestation'>{_esc(d.get('attestation', ''))}</textarea></p>"
f"<p><strong>receipt.json</strong><br><textarea name='receipt'>{_esc(d.get('receipt', ''))}</textarea></p>"
f"<p><strong>log public key (PEM)</strong><br><textarea name='pubkey' style='min-height:4rem'>{_esc(d.get('pubkey', ''))}</textarea></p>"
f"<div class='panel'><h2 style='margin-top:0'>Check a receipt yourself {_help('attestation')}</h2>"
"<p class='plain'>An <strong>attestation</strong> is a signed statement that a "
"verification run happened — on an exact commit, with an exact toolchain, with a "
"stated result. Its <strong>transparency receipt</strong> proves that statement "
"is permanently recorded in the public Lean Transparency Log. The <strong>log "
"public key</strong> is what the log signs with. The verdict is produced by the "
"wallet's own deployed verifier — this page adds nothing and hides nothing.</p>"
+ _explain(
"<ul><li><strong>Where do I get these?</strong> For the live log: attestation "
"and receipt from <code>ltl.zkdefi.org/v1/attestation?component=…</code>, the "
"public key (<code>log.pub</code>) from the log's mirror repository. This "
"repository also ships samples under <code>examples/wallet-evidence/</code> — "
"paste those to see the verifier work.</li>"
"<li><strong>Is this safe?</strong> Yes: nothing you paste is stored. The "
"artifacts live in a throwaway temp folder outside the wallet for the "
"duration of the check, and the wallet directory is never written.</li>"
"<li><strong>If the input is malformed</strong>, the page shows FAILED TO "
"VERIFY rather than guessing.</li></ul>")
+ "<form method='post' action='/inspect'>"
f"<p><strong>attestation.json</strong> <span class='muted'>— the signed verification statement</span><br>"
f"<textarea name='attestation'>{_esc(d.get('attestation', ''))}</textarea></p>"
f"<p><strong>receipt.json</strong> <span class='muted'>— the log's proof that the statement is recorded</span><br>"
f"<textarea name='receipt'>{_esc(d.get('receipt', ''))}</textarea></p>"
f"<p><strong>log public key (PEM)</strong> <span class='muted'>— starts with "
f"<code>-----BEGIN PUBLIC KEY-----</code></span><br>"
f"<textarea name='pubkey' style='min-height:4rem'>{_esc(d.get('pubkey', ''))}</textarea></p>"
"<button type='submit'>Verify (read-only)</button></form></div>"
)
def render_guide() -> str:
"""The manual: static plain-language explanations, no live data."""
return (
# --- what is warden ------------------------------------------------
"<div class='panel'><h2 style='margin-top:0' id='what'>What is warden?</h2>"
"<p class='plain'>warden is a prototype custody wallet built on one idea: "
"<strong>no single program is trusted</strong>. Four verifier programs, each "
"built from a different formally verified codebase, must independently agree "
"before the wallet trusts a cryptographic component. If they ever disagree, the "
"wallet freezes itself and writes down what happened. Every decision leaves a "
"tamper-evident trace.</p></div>"
# --- what is the cockpit -------------------------------------------
"<div class='panel'><h2 style='margin-top:0' id='cockpit'>What is this cockpit?</h2>"
"<p class='plain'>A local, read-only window onto one wallet directory, for the "
"human who ultimately answers for the money. Its design law: <strong>the cockpit "
"renders evidence, it never asserts it</strong>. Every page is recomputed from "
"the wallet's files at the moment you load it, by the same functions the wallet "
"itself uses. It cannot approve, sign, unlatch, or change anything — the server "
"has no writing routes, and the test suite proves a full click-through changes "
"not one byte of wallet state.</p></div>"
# --- how to read ---------------------------------------------------
"<div class='panel'><h2 style='margin-top:0' id='reading'>How to read any page here</h2>"
"<ol>"
"<li><strong>Verdict first.</strong> The top of a page states the conclusion in "
"words — e.g. CUSTODY HEALTHY or CUSTODY FROZEN. If you read nothing else, read "
"that.</li>"
"<li><strong>Evidence below.</strong> Each panel shows the recomputed facts "
"behind the verdict. Every panel has a «How to read this panel» expander, and "
"every jargon term carries a small <a class='help' href='#glossary'>?</a> that "
"jumps here.</li>"
"<li><strong>Provenance last.</strong> The dashed line at the bottom of every "
"panel names the exact function and time that produced it — your proof that "
"nothing was cached.</li>"
"</ol>"
"<p class='plain'>Colors mean one thing each: "
"<span class='pill ok'>green</span> = re-checked just now and passed · "
"<span class='pill warn'>amber</span> = waiting, or needs your attention · "
"<span class='pill bad'>red</span> = re-checked and failed, or could not be "
"checked at all. A red <strong>FAILED TO VERIFY</strong> panel is the cockpit "
"being honest: it refuses to show a green it cannot back up right now. There is "
"no neutral gray anywhere.</p></div>"
# --- tour ----------------------------------------------------------
"<div class='panel'><h2 style='margin-top:0' id='tour'>A five-minute tour</h2>"
"<ol>"
"<li>Open <a href='/'>Posture</a> — read the verdict banner, then the panels top "
"to bottom: latch, ledger, quorum, signing rules, recorded history.</li>"
"<li>Open <a href='/incidents'>Incidents</a> — on a healthy wallet both lists are "
"empty, and the page says why that is the good state.</li>"
"<li>Open <a href='/queue'>Queue</a> — empty unless a signature is waiting for "
"the offline device.</li>"
"<li>Open <a href='/inspect'>Inspect</a> — paste the sample artifacts from "
"<code>examples/wallet-evidence/</code> and watch the deployed verifier run.</li>"
"<li>Open the <a href='/estate'>Estate map</a> — where this wallet sits in the "
"wider verified-crypto estate, and what is actually running where.</li>"
"</ol></div>"
# --- glossary ------------------------------------------------------
"<div class='panel'><h2 style='margin-top:0' id='glossary'>Glossary</h2>"
"<dl class='gloss'>"
"<dt id='capsule'>custody capsule</dt>"
"<dd>The wallet's founding document: which verifier members it trusts, their "
"pinned binary fingerprints, and the policy (unanimity, minimum members, required "
"evidence grade). Sealed when the wallet is created; its SHA-256 is anchored in "
"the ledger's first entry, so it cannot be quietly swapped later.</dd>"
"<dt id='member'>quorum member</dt>"
"<dd>One of the verifier programs the capsule names. Each is built from a "
"different formally verified Ed25519 codebase (dalek, anza, risc0, betrusted), so "
"a bug — or a backdoor — would have to exist in all of them independently for a "
"wrong verdict to slip through unanimously.</dd>"
"<dt id='pinned'>pinned (binary fingerprint)</dt>"
"<dd>The capsule stores the SHA-256 hash of each member executable. Before use, "
"the file on disk is re-hashed and compared; a modified or swapped binary fails "
"the comparison and is rejected. Pinning proves the file is <em>unchanged</em> — "
"not that it was honestly <em>built</em> (see the limits section below).</dd>"
"<dt id='tier'>evidence grade (R0R5)</dt>"
"<dd>This project's scale for how strong the formal evidence behind a component "
"is. R0 = no usable evidence; R4 = machine-checked proofs covering the component's "
"full documented boundary; R5 would add reproducible builds and side-channel "
"assurance — nothing holds R5 yet, and the gap is stated rather than hidden. This "
"wallet's capsule requires R4 of every member.</dd>"
"<dt id='ledger'>ledger / hash chain</dt>"
"<dd>The wallet's append-only journal of custody events. Every entry contains the "
"hash of the previous entry; the newest hash is called the «head». Rewriting, "
"deleting, or reordering anything in the past changes the hashes and breaks the "
"chain visibly — that is what «chain verified» re-checks on every page load.</dd>"
"<dt id='latch'>custody latch</dt>"
"<dd>The wallet's emergency brake. It trips on quorum divergence or suspected "
"tampering; while latched, all outbound signing is frozen and every request is "
"refused with a receipt. Only a deliberate operator action through the wallet's "
"own channels can clear it — never this cockpit. Recovery steps: "
"<code>docs/runbook-latch.md</code>.</dd>"
"<dt id='incident'>incident</dt>"
"<dd>A file the wallet writes the moment it notices something wrong — for "
"example, one member answering INVALID while the others answer OK. Incidents are "
"never deleted; serious ones also trip the latch.</dd>"
"<dt id='refusal'>refusal receipt</dt>"
"<dd>When the wallet declines to act, it answers in writing: a machine-readable "
"receipt naming the rule (<code>code</code>), what was missing, and what would "
"fix it (<code>remediation</code>). An agent can read it, correct the problem, "
"and retry — no guessing at error messages.</dd>"
"<dt id='airgap'>air-gap outbox / inbox</dt>"
"<dd>The signing key may live on a device that never touches a network. Signing "
"requests are parked as files in an outbox and carried across by a human; "
"responses come back through an inbox. The Queue page watches both folders and "
"touches neither.</dd>"
"<dt id='attestation'>attestation &amp; transparency receipt</dt>"
"<dd>An attestation is a signed statement that a verification run happened: which "
"repository, which exact commit, which toolchain, what result. Its transparency "
"receipt proves the statement is permanently recorded in the public Lean "
"Transparency Log — so it can never be quietly edited, backdated, or denied "
"later. The Inspect tab re-verifies both.</dd>"
"<dt id='provenance'>provenance line</dt>"
"<dd>The dashed footer on every panel, naming the exact function that recomputed "
"the panel and when. It is the cockpit's signature move: evidence of freshness "
"attached to every claim.</dd>"
"<dt id='demo'>DEMO wallet</dt>"
"<dd>A throwaway wallet sealed by <code>pacta wallet cockpit --demo</code> so you "
"can explore this cockpit before creating a real wallet. Its members are fake "
"shell stubs, every label says DEMO, and it can sign nothing real. Real wallets "
"are sealed with <code>pacta wallet init</code>.</dd>"
"</dl></div>"
# --- limits --------------------------------------------------------
"<div class='panel'><h2 style='margin-top:0' id='limits'>What this cockpit cannot tell you</h2>"
"<ul class='diag'>"
"<li>Whether the pinned binaries were honestly <em>built</em> from their attested "
"sources — reproducible builds are out of scope (the R5 gap), stated in the paper "
"and the claim cards rather than hidden.</li>"
"<li>Whether the machine this cockpit runs on is itself clean — a compromised "
"operating system can lie to any dashboard, including this one.</li>"
"<li>Anything it could not recompute just now — that renders as a red FAILED TO "
"VERIFY panel, never as a guess and never as a stale green.</li>"
"</ul></div>"
)
# ---------------------------------------------------------------------------
# demo wallet - custody-inert, for exploring the cockpit from zero
# ---------------------------------------------------------------------------
@ -415,6 +789,13 @@ def seal_demo_wallet(root: str | Path | None = None) -> Path:
# server
# ---------------------------------------------------------------------------
_ESTATE_BACK_CHIP = (
'<a href="/" style="position:fixed;left:14px;bottom:14px;z-index:999;'
'background:#1c2430;color:#fff;padding:.45rem .8rem;border-radius:8px;'
'font:600 .8rem system-ui;text-decoration:none;'
'box-shadow:0 2px 8px rgba(0,0,0,.25)">← Back to cockpit</a>')
def make_handler(wallet_dir: Path):
class CockpitHandler(BaseHTTPRequestHandler):
server_version = "warden-cockpit/1"
@ -447,11 +828,15 @@ def make_handler(wallet_dir: Path):
self._send(_page("incidents", "/incidents", body, wd))
elif route == "/inspect":
self._send(_page("receipt inspector", "/inspect", render_inspect(None), wd))
elif route == "/guide":
self._send(_page("guide", "/guide", render_guide(), wd))
elif route == "/estate":
from .estateview import ESTATE_HTML
self._send(ESTATE_HTML)
self._send(ESTATE_HTML + _ESTATE_BACK_CHIP)
else:
self._send(_page("not found", "", "<div class='panel bad'>No such view.</div>", wd), 404)
self._send(_page("not found", "",
"<div class='panel bad'>No such view. The tabs above list "
"everything this cockpit can show.</div>", wd), 404)
def do_POST(self) -> None: # noqa: N802
route = urllib.parse.urlparse(self.path).path

View file

@ -82,6 +82,7 @@ def _dir_fingerprint(root: Path) -> dict[str, str]:
def test_posture_renders_recomputed_evidence(tmp_path):
wallet = _seal_wallet(tmp_path)
html = render_posture(collect("Wallet.posture()", wallet.posture))
assert "CUSTODY HEALTHY" in html # the verdict, in words, before any evidence
assert "chain verified" in html
assert "unlatched" in html
assert "dalek-ed25519-verified" in html and "anza-ed25519-verified" in html
@ -96,6 +97,7 @@ def test_broken_ledger_renders_red(tmp_path):
ledger.write_text(ledger.read_text().replace("genesis", "gene-sis"), encoding="utf-8")
html = render_posture(collect("Wallet.posture()", wallet.posture))
assert "CHAIN BROKEN" in html
assert "CUSTODY EVIDENCE BROKEN" in html # verdict banner states it in words
def test_collector_failure_is_loud_not_gray(tmp_path):
@ -115,6 +117,7 @@ def test_latched_state_renders_frozen():
"ledger": {"entries": 1, "head": "cd" * 32, "chain_ok": True, "problems": []},
"incidents": 1, "refusal_receipts": 0, "generated_at": "now"}}
html = render_posture(posture)
assert "CUSTODY FROZEN" in html # verdict banner states it in words
assert "LATCHED" in html and "outbound custody frozen" in html
assert "quorum divergence" in html and "runbook-latch" in html
@ -175,7 +178,7 @@ def test_server_routes_and_read_only_guarantee(tmp_path):
thread = threading.Thread(target=server.serve_forever, daemon=True)
thread.start()
try:
for route in ("/", "/queue", "/incidents", "/inspect"):
for route in ("/", "/queue", "/incidents", "/inspect", "/guide"):
with urllib.request.urlopen(f"http://127.0.0.1:{port}{route}") as resp:
body = resp.read().decode()
assert resp.status == 200
@ -248,3 +251,78 @@ def test_demo_wallet_seals_and_serves(tmp_path):
finally:
server.shutdown()
thread.join(timeout=5)
# ---------------------------------------------------------------------------
# UX contract: the cockpit never leaves a human in the dark. Every view must
# explain itself in plain language, every verdict must be stated in words,
# every page must carry navigation and interpretation help.
# ---------------------------------------------------------------------------
def _serve(wallet_dir):
server = serve(wallet_dir, host="127.0.0.1", port=0)
thread = threading.Thread(target=server.serve_forever, daemon=True)
thread.start()
return server, thread, server.server_address[1]
def _get(port, route):
with urllib.request.urlopen(f"http://127.0.0.1:{port}{route}") as resp:
return resp.status, resp.read().decode()
def test_guide_view_explains_every_term(tmp_path):
wallet = _seal_wallet(tmp_path)
server, thread, port = _serve(wallet.dir)
try:
status, body = _get(port, "/guide")
assert status == 200
assert "renders evidence" in body # the design law, in the user's face
for term in ("custody capsule", "quorum member", "hash chain",
"custody latch", "refusal receipt", "air-gap",
"evidence grade", "DEMO wallet",
"What this cockpit cannot tell you",
"A five-minute tour"):
assert term in body, f"guide is missing: {term}"
finally:
server.shutdown()
thread.join(timeout=5)
def test_every_view_carries_lead_nav_and_explainers(tmp_path):
wallet = _seal_wallet(tmp_path)
server, thread, port = _serve(wallet.dir)
try:
for route in ("/", "/queue", "/incidents", "/inspect"):
status, body = _get(port, route)
assert status == 200
assert 'href="/guide"' in body, f"{route}: no path to the guide"
assert 'class="lead"' in body, f"{route}: no plain-language lead"
assert "<details" in body, f"{route}: no interpretation expander"
assert 'class="navsub"' in body, f"{route}: tabs do not say what they answer"
finally:
server.shutdown()
thread.join(timeout=5)
def test_empty_states_are_explained(tmp_path):
"""A blank table is a UX failure; every empty list must say what empty
means and whether it is good news."""
wallet = _seal_wallet(tmp_path)
q = render_queue(collect_airgap(wallet))
assert "No signing requests are waiting" in q
inc = render_incidents(collect_incidents(wallet), collect_refusals(wallet))
assert "Empty is the good state" in inc
assert "nothing has been refused" in inc
def test_estate_page_links_back_to_cockpit(tmp_path):
wallet = _seal_wallet(tmp_path)
server, thread, port = _serve(wallet.dir)
try:
status, body = _get(port, "/estate")
assert status == 200
assert "Back to cockpit" in body
finally:
server.shutdown()
thread.join(timeout=5)