mirror of
https://github.com/saymrwulf/proof-aware-crypto-tooling-agent.git
synced 2026-09-04 20:03:40 +00:00
Operator asked for a real big fat user manual: read on one monitor with the deck on the other, worked like a lab from beginning to end, with highest didactical skill — the goal being to educate ONE person in ALL roles, as if to BECOME each role. Shipped as a full course: - docs/warden-lab-manual.md (canonical, ~7100 words): 8 sessions + capstone. Fixed didactic ritual per role-session: why the role exists -> the mind of the role -> concepts -> LAB (numbered machine exercises with checkpoint rails) -> never-list with whys + thought experiment -> handoff artifact -> self-test -> recap card. Notation: do / checkpoint / write / optional. Capstone 'incident day': one incident walked through all six chairs producing seven written artifacts; graduation session maps the path to a real wallet; Appendix A command reference by chair, Appendix B full self-test answers. - Session 4's tamper drill: copy the demo wallet, flip one character of ledger history, watch verify-ledger AND a second cockpit catch it. Every checkpoint in the manual was executed by the author before commit: status fields, intact-chain exit 0, tampered-chain 'entry 0: hash mismatch / BROKEN' exit 1, CUSTODY EVIDENCE BROKEN banner. - src/pacta/mdlite.py: deliberately small stdlib Markdown renderer (headings/anchors+TOC, bold/italic/code with placeholder stashing, fences, lists, quotes, tables, links); tested against the real manual for zero artifact leaks. - /manual route with syllabus TOC; manual typography (checkpoint boxes, session borders); nav tab, deck wizard cross-link, cli hint. Suite 142 -> 144 green; read-only sweep covers /manual. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
285 lines
15 KiB
Python
285 lines
15 KiB
Python
"""deck - the ops deck: every role station live in one tmux-style grid,
|
|
with the guided wizard rail.
|
|
|
|
The deck is the crew law made physical: six panes, one per role, all
|
|
live at the same time - because in real life the roles act in parallel,
|
|
they do not take turns existing. Each pane is an independent viewport
|
|
(iframe) onto that role's station in chrome-stripped "pane mode"; panes
|
|
reload and zoom independently, tmux-style.
|
|
|
|
On the right rides the WIZARD: a guided first watch that takes a
|
|
newcomer by the hand through every role's real actions on the live DEMO
|
|
wallet. Each step card is camouflaged in the color of the role being
|
|
lived, and the matching pane glows - instruction and instrument are
|
|
bound by hue, so the learner always knows where to act.
|
|
|
|
Pure presentation: this module renders strings from the station model;
|
|
all live evidence stays inside the panes, which are ordinary cockpit
|
|
routes and therefore inherit the read-only guarantee wholesale. The
|
|
~60 lines of vanilla JS here do layout only (zoom, reload, step
|
|
navigation, glow) - they never touch wallet data.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
from .stations import STATIONS
|
|
from .uikit import STYLE, esc
|
|
|
|
_DECK_EXTRA_STYLE = """
|
|
body.deckbody{max-width:none;margin:0;padding:0;height:100vh;display:flex;
|
|
flex-direction:column;overflow:hidden}
|
|
.deckbar{display:flex;align-items:center;gap:.7rem;flex-wrap:wrap;
|
|
padding:.45rem .9rem;background:#1c2430;color:#e8ecf2;font-size:.82rem}
|
|
.deckbar a{color:#aebcf0;text-decoration:none}
|
|
.deckbar .mono{opacity:.75}
|
|
.deckgrid{flex:1;display:grid;grid-template-columns:minmax(0,1fr) 21.5rem;
|
|
min-height:0}
|
|
.deckmain{display:grid;grid-template-columns:1fr 1fr;grid-auto-rows:1fr;
|
|
gap:6px;padding:6px;min-height:0;overflow:auto}
|
|
@media(min-width:1500px){.deckmain{grid-template-columns:1fr 1fr 1fr}}
|
|
.pane{display:flex;flex-direction:column;border:1px solid var(--line);
|
|
border-left:4px solid var(--role);border-radius:6px;background:#fff;
|
|
min-height:13rem;min-width:0}
|
|
.pane header{display:flex;align-items:center;gap:.45rem;padding:.22rem .5rem;
|
|
font-size:.78rem;background:var(--roletint);border-radius:0 5px 0 0}
|
|
.pane header .q{color:var(--ink2);font-size:.7rem;font-style:italic;
|
|
overflow:hidden;text-overflow:ellipsis;white-space:nowrap;flex:1}
|
|
.pane header .monogram{min-width:1.5rem;height:1.5rem;line-height:1.5rem;
|
|
font-size:.68rem}
|
|
.paneacts{display:flex;gap:.25rem}
|
|
.paneacts button,.paneacts a{border:1px solid var(--line);background:#fff;
|
|
color:var(--ink);border-radius:4px;font-size:.72rem;line-height:1.25;
|
|
padding:.05rem .4rem;cursor:pointer;text-decoration:none}
|
|
.pane iframe{flex:1;border:0;width:100%;min-height:0;border-radius:0 0 5px 5px}
|
|
.pane.focus{outline:3px solid var(--role);outline-offset:-1px}
|
|
.deckmain.zoom .pane{display:none}
|
|
.deckmain.zoom .pane.zoomed{display:flex;grid-column:1/-1;grid-row:1/-1}
|
|
.wizard{border-left:1px solid var(--line);background:#fff;overflow-y:auto;
|
|
padding:.8rem .9rem;min-height:0}
|
|
.wizard h2{margin:.1rem 0 .3rem;font-size:1rem}
|
|
.wizlegend{display:flex;gap:.3rem;flex-wrap:wrap;margin:.4rem 0 .7rem}
|
|
.wizlegend span{font-size:.66rem;font-weight:700;border-radius:5px;
|
|
padding:.1rem .4rem;background:var(--roletint);color:var(--role)}
|
|
.wizstep{display:none;border-left:4px solid var(--role);
|
|
background:var(--roletint);border-radius:6px;padding:.65rem .8rem;
|
|
font-size:.86rem}
|
|
.wizstep.on{display:block}
|
|
.wizstep .rolechip{display:inline-block;font-weight:800;font-size:.7rem;
|
|
letter-spacing:.04em;color:var(--role);margin-bottom:.25rem}
|
|
.wizstep h3{margin:.1rem 0 .35rem;font-size:.92rem}
|
|
.wizstep .succ{margin:.45rem 0 0;font-size:.8rem}
|
|
.wizstep .succ b{color:var(--ok)}
|
|
.wizstep .learn{margin:.35rem 0 0;font-size:.8rem;color:var(--ink2);
|
|
font-style:italic}
|
|
.wizctl{display:flex;align-items:center;gap:.6rem;margin:.7rem 0 0}
|
|
.wizctl button{padding:.35rem .8rem;font-size:.84rem}
|
|
.wizctl button:disabled{opacity:.4;cursor:default}
|
|
#wizprog{font-size:.78rem;color:var(--ink2)}
|
|
@media(max-width:999px){
|
|
body.deckbody{overflow:auto;height:auto}
|
|
.deckgrid{display:flex;flex-direction:column-reverse}
|
|
.deckmain{grid-template-columns:1fr}
|
|
.pane{min-height:22rem}
|
|
.wizard{border-left:0;border-bottom:2px solid var(--line)}
|
|
}
|
|
"""
|
|
|
|
# The guided first watch. Each step: (role_id, title, do_html, success_html,
|
|
# learn_html). The step card wears the role's colors; the matching pane glows.
|
|
WIZARD_STEPS: list[tuple[str, str, str, str, str]] = [
|
|
("newcomer", "Welcome to the deck",
|
|
"Six panes, six roles, all live at the same time — a real crew works in "
|
|
"parallel, and so does this deck. The colors ARE the roles; this wizard "
|
|
"wears the color of whoever you are acting as, and that role's pane "
|
|
"glows. Nothing you do here can sign or change anything — every pane is "
|
|
"read-only evidence.",
|
|
"You can point at each pane and say what its role is for (the italic "
|
|
"line in each pane header helps).",
|
|
"One system, six distinct jobs — never one blurred job."),
|
|
("operator", "Morning watch: probe everything",
|
|
"Act as the <strong>Operator</strong> (green pane): press "
|
|
"<strong>«Probe now»</strong> on the liveness board. Watch the rows fill "
|
|
"with live facts. Find the <code>log head</code> row and read its "
|
|
"<code>tree_size</code> — that number is the public transparency log "
|
|
"answering you, right now.",
|
|
"Every service row says <b>alive</b> with observed facts and latency; "
|
|
"the repo rows show HEAD commits.",
|
|
"Liveness is checked on demand, never assumed — and never silently in "
|
|
"the background."),
|
|
("operator", "Read the wallet's own pulse",
|
|
"Still in green: scroll to <strong>Custody latch</strong> — it says "
|
|
"<em>unlatched</em>, and the panel explains what the latch would do. "
|
|
"Then <strong>Recorded history</strong>: this demo wallet carries "
|
|
"<em>1 incident</em> and <em>1 refusal receipt</em> on file. You will "
|
|
"meet both in the next steps.",
|
|
"You found the latch state and the two counters without leaving the "
|
|
"green pane.",
|
|
"The operator reads state from evidence panels, not from memory."),
|
|
("proposer", "Live the proposer: find your request",
|
|
"Switch hats — <strong>amber pane</strong>. One signing request sits in "
|
|
"the queue, <em>awaiting device</em>. Read its <strong>payload "
|
|
"fingerprint</strong>: that is the SHA-256 of the exact bytes the "
|
|
"offline signer would sign — and nothing else.",
|
|
"You can quote the first characters of the fingerprint of what would be "
|
|
"signed.",
|
|
"A proposal is precise, or it is nothing."),
|
|
("proposer", "Read a refusal like a to-do list",
|
|
"Still amber: open <strong>Incidents (refusals)</strong> from the "
|
|
"station's instrument links. The demo refusal says "
|
|
"<code>POLICY_DENIED</code>, missing <em>allowlisted destination</em>, "
|
|
"with a <code>remediation</code> naming exactly what would make the "
|
|
"same request succeed.",
|
|
"You can say, in one sentence, what the proposer would fix before "
|
|
"retrying.",
|
|
"warden never says a bare «no» — every refusal is machine-readable "
|
|
"instructions."),
|
|
("quorum", "Sit on the bench: find the dissenter",
|
|
"Now the <strong>indigo pane</strong>. Count the seats: four, each "
|
|
"built from a different verified codebase. The demo incident (you saw "
|
|
"its counter in step 3) records that <code>risc0</code> answered "
|
|
"INVALID while the other three said OK — in a real wallet, that single "
|
|
"dissent freezes custody on the spot.",
|
|
"You found all four seats and can name the dissenting member.",
|
|
"One honest dissenter beats three comfortable agreements — that is the "
|
|
"whole bench."),
|
|
("cryptographer", "Recompute — never trust",
|
|
"The <strong>purple pane</strong>: click <strong>«load the sample "
|
|
"evidence»</strong>, then <strong>Verify (read-only)</strong>. The "
|
|
"deployed verifier re-checks the signatures and the log inclusion in "
|
|
"front of you. Then delete one character from the receipt box and "
|
|
"verify again — watch it refuse, loudly.",
|
|
"First run: <b>ACCEPTED</b> with a diagnostics list. Broken run: "
|
|
"REJECTED, and the diagnostics name the exact failing check.",
|
|
"You never trusted this page — you recomputed it. That is the "
|
|
"cryptographer's entire posture."),
|
|
("architect", "Zoom out: map versus territory",
|
|
"The <strong>slate pane</strong>: read the <strong>drift "
|
|
"tripwire</strong> — it just compared the two renderings of the estate "
|
|
"map, name by name, and they agree. Then open the <strong>Estate "
|
|
"map</strong> link and find this wallet's place among the repos, "
|
|
"services, and the two self-referential loops.",
|
|
"You can say where warden sits on the map, and what the tripwire would "
|
|
"catch.",
|
|
"The map is recomputed, never remembered — or it lies."),
|
|
("operator", "The handoff lap — the team in one incident",
|
|
"Watch the whole crew move once, in your head, across the panes: the "
|
|
"<em>proposer</em> escalates a refusal → the <em>operator</em> "
|
|
"investigates and probes → the <em>bench</em> defends its dissent → "
|
|
"the <em>cryptographer</em> re-verifies the evidence → the "
|
|
"<em>architect</em> records what changed. Five roles touched one "
|
|
"incident — and not one of them did another's job.",
|
|
"You can retell the lap naming who hands what to whom.",
|
|
"Teamwork here is handoffs between distinct roles — never a blur."),
|
|
("newcomer", "Graduation",
|
|
"Pick the station that felt most like you and open it full-size (the "
|
|
"↗ in its pane header). Read its Mission, its Duties — every one a "
|
|
"real command — and its «This station never…» list; the never-list is "
|
|
"the fastest way to understand a role. When you are ready for a real "
|
|
"wallet: <code>pacta wallet init</code>.",
|
|
"You have a station — and you know what it never does.",
|
|
"If any page confused you on this watch, that is a bug in the page, "
|
|
"not in you. Report it — that is the newcomer's superpower."),
|
|
]
|
|
|
|
|
|
def render_deck(wallet_dir: str) -> str:
|
|
"""The full deck document: bar, pane grid, wizard rail, layout JS."""
|
|
demo_badge = (' <span class="pill warn">DEMO WALLET — custody-inert</span>'
|
|
if "DEMO" in wallet_dir else "")
|
|
panes = "".join(
|
|
f'<section class="pane" data-role="{s["id"]}" '
|
|
f'style="--role:{s["hue"]};--roletint:{s["tint"]}">'
|
|
f'<header><span class="monogram">{s["monogram"]}</span>'
|
|
f'<strong>{esc(s["name"])}</strong>'
|
|
f'<span class="q">{esc(s["question"])}</span>'
|
|
f'<span class="paneacts">'
|
|
f'<button data-act="reload" title="reload this pane">⟳</button>'
|
|
f'<button data-act="zoom" title="zoom this pane (tmux-style)">⤢</button>'
|
|
f'<a href="/station/{s["id"]}" target="_top" title="open full page">↗</a>'
|
|
f'</span></header>'
|
|
f'<iframe src="/station/{s["id"]}?pane=1" loading="lazy" '
|
|
f'title="{esc(s["name"])} station"></iframe>'
|
|
f'</section>'
|
|
for s in STATIONS)
|
|
legend = "".join(
|
|
f'<span style="--role:{s["hue"]};--roletint:{s["tint"]}">'
|
|
f'{s["monogram"]} {esc(s["name"])}</span>'
|
|
for s in STATIONS)
|
|
role_by_id = {s["id"]: s for s in STATIONS}
|
|
steps = "".join(
|
|
f'<div class="wizstep" data-role="{role_id}" '
|
|
f'style="--role:{role_by_id[role_id]["hue"]};'
|
|
f'--roletint:{role_by_id[role_id]["tint"]}">'
|
|
f'<span class="rolechip">{role_by_id[role_id]["monogram"]} · YOU ARE THE '
|
|
f'{esc(role_by_id[role_id]["name"]).upper()}</span>'
|
|
f'<h3>{title}</h3>'
|
|
f'<div>{do}</div>'
|
|
f'<p class="succ"><b>Success looks like:</b> {success}</p>'
|
|
f'<p class="learn">{learn}</p>'
|
|
f'</div>'
|
|
for role_id, title, do, success, learn in WIZARD_STEPS)
|
|
return (
|
|
"<!doctype html><html><head><meta charset='utf-8'>"
|
|
"<meta name='viewport' content='width=device-width,initial-scale=1'>"
|
|
"<title>warden deck — all stations live</title>"
|
|
f"<style>{STYLE}{_DECK_EXTRA_STYLE}</style></head>"
|
|
"<body class='deckbody'>"
|
|
"<div class='deckbar'><strong>warden deck</strong>"
|
|
"<span>six stations, live in parallel</span>"
|
|
"<a href='/'>← Bridge</a>"
|
|
"<span class='pill warn'>READ-ONLY</span>"
|
|
f"{demo_badge}"
|
|
f"<span class='mono'>{esc(wallet_dir)}</span></div>"
|
|
"<div class='deckgrid'>"
|
|
f"<div class='deckmain'>{panes}</div>"
|
|
"<aside class='wizard'>"
|
|
"<h2>The wizard — your first watch</h2>"
|
|
"<p class='plain' style='font-size:.8rem'>A guided lap through every "
|
|
"role's real actions, on this live demo wallet. Each step wears the "
|
|
"color of the role you are living, and that pane glows.</p>"
|
|
f"<div class='wizlegend'>{legend}</div>"
|
|
f"{steps}"
|
|
"<div class='wizctl'>"
|
|
"<button id='wizprev'>← Back</button>"
|
|
"<button id='wiznext'>Next →</button>"
|
|
"<span id='wizprog'></span></div>"
|
|
"<p class='muted' style='margin-top:.8rem;font-size:.74rem'>Pane "
|
|
"controls, tmux-style: ⟳ reload · ⤢ zoom one pane · ↗ open the full "
|
|
"station page. Your step is remembered for this browser session.</p>"
|
|
"<p class='muted' style='font-size:.74rem'>Want the whole course, not "
|
|
"just the tour? <a href='/manual' target='_top'>Open the lab "
|
|
"manual</a> — sessions, drills, self-tests, capstone. Built to sit "
|
|
"on one monitor while this deck flies on the other.</p>"
|
|
"</aside></div>"
|
|
"<script>"
|
|
"(function(){"
|
|
"var panes=[].slice.call(document.querySelectorAll('.pane'));"
|
|
"var main=document.querySelector('.deckmain');"
|
|
"[].forEach.call(document.querySelectorAll('[data-act=reload]'),function(b){"
|
|
"b.onclick=function(){var f=b.closest('.pane').querySelector('iframe');"
|
|
"f.src=f.src;};});"
|
|
"[].forEach.call(document.querySelectorAll('[data-act=zoom]'),function(b){"
|
|
"b.onclick=function(){var p=b.closest('.pane');"
|
|
"var was=p.classList.contains('zoomed');"
|
|
"panes.forEach(function(x){x.classList.remove('zoomed');});"
|
|
"main.classList.toggle('zoom',!was);"
|
|
"if(!was){p.classList.add('zoomed');}};});"
|
|
"var steps=[].slice.call(document.querySelectorAll('.wizstep'));"
|
|
"var prev=document.getElementById('wizprev');"
|
|
"var next=document.getElementById('wiznext');"
|
|
"var prog=document.getElementById('wizprog');"
|
|
"var i=parseInt(sessionStorage.getItem('warden-wiz')||'0',10)||0;"
|
|
"function show(n){i=Math.max(0,Math.min(steps.length-1,n));"
|
|
"sessionStorage.setItem('warden-wiz',String(i));"
|
|
"steps.forEach(function(s,k){s.classList.toggle('on',k===i);});"
|
|
"prog.textContent=(i+1)+' / '+steps.length;"
|
|
"var role=steps[i].getAttribute('data-role');"
|
|
"panes.forEach(function(p){"
|
|
"p.classList.toggle('focus',p.getAttribute('data-role')===role);});"
|
|
"prev.disabled=(i===0);"
|
|
"next.textContent=(i===steps.length-1)?'Start over':'Next →';}"
|
|
"prev.onclick=function(){show(i-1);};"
|
|
"next.onclick=function(){show(i===steps.length-1?0:i+1);};"
|
|
"show(i);"
|
|
"})();"
|
|
"</script></body></html>"
|
|
)
|