"""Local web UI: pick a pack, fill parameters, walk the steps, record evidence. Localhost only. Server-rendered forms; the only script is small: copy button, fail-comment warning, scroll to current step. """ from __future__ import annotations import html import secrets from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer from pathlib import Path from urllib.parse import parse_qs, urlparse from . import pack as P, run as R CSS = """body{font:15px/1.5 system-ui,sans-serif;max-width:52rem;margin:2rem auto;padding:0 1rem;color:#111;background:#fff} @media(prefers-color-scheme:dark){body{color:#eee;background:#111}pre,input,select{background:#1b1b1b;color:#eee}} h1,h2{font-weight:600}a{color:inherit}hr{border:0;border-top:1px solid #8886} .eyebrow{font:12px ui-monospace,monospace;text-transform:uppercase;letter-spacing:.06em} .step{border:1px solid #8886;padding:.8rem 1rem;margin:.8rem 0}.step.running{border-color:currentColor} .pill{font:12px ui-monospace,monospace;border:1px solid #8886;padding:0 .4rem} pre{border:1px solid #8886;padding:.5rem;overflow:auto;margin:.4rem 0} button,input,select{font:inherit;padding:.25rem .5rem}.head{display:inline-flex;width:calc(100% - 1.6rem);justify-content:space-between;align-items:baseline;gap:1rem;vertical-align:top}summary{cursor:pointer}.row{display:flex;gap:.5rem;align-items:center;flex-wrap:wrap}.grow{flex:1;min-width:12rem}[hidden]{display:none}#current{scroll-margin-top:1rem}.note{opacity:.75}.bad{font-weight:600}""" def page(title: str, body: str) -> bytes: return (f"" f"{html.escape(title)}" f"

runbook-tutorials

{body}" "").encode() def e(s) -> str: return html.escape(str(s)) SMILEYS = (("good", "🙂", "Went well"), ("neutral", "😐", "So-so"), ("poor", "🙁", "Poor")) def score_line(c: dict) -> str: return (f"tried {c['tried']} · succeeded {c['succeeded']} · failed {c['failed']} · " f"abandoned {c['abandoned']} · stalled {c['stalled']}") class App: def __init__(self, roots: list[Path]): self.token = secrets.token_urlsafe(16) self.packs = [P.load(p) for r in roots for p in P.discover(r)] def hidden(self) -> str: return f"" def index(self) -> bytes: rows = [] for i, p in enumerate(self.packs): d = p.data if not p.ok: rows.append(f"
  • {e(p.id)}: invalid ({e(p.errors[0])})
  • ") elif d.get("spec") != "runbook-pack/v0.1" and d["engine"] == "native": rows.append(f"
  • {e(d['title'])} {e(d.get('spec'))} " "cannot be run yet (RBT-WP-0007)
  • ") elif d["engine"] != "native": rows.append(f"
  • {e(d['title'])} legacy console " f"start with: python -m rtut launch {e(p.path)}
  • ") else: rows.append(f"
  • {e(d['title'])} " f"{e(d['exercise_status'])} " f"{e(score_line(R.scorecard(p)))}
  • ") return page("Runbooks", f"

    Runbooks

    ") def pack_page(self, i: int) -> bytes: p = self.packs[i]; d = p.data prereq = "".join(f"
  • [{e(x['owner'])}] {e(x['text'])}
  • " for x in d.get("prerequisites", [])) fields = "".join( f"


    {e(x.get('help', ''))} Never enter secrets.

    " for x in d.get("parameters", [])) return page(d["title"], f"

    {e(d['title'])}

    {e(d['exercise_status'])}

    " f"

    {e(d.get('outcome', ''))}

    {e(score_line(R.scorecard(p)))}

    " f"{self.runs_list(i, p)}

    Prerequisites

    " f"
    {self.hidden()}{fields}" "

    " "
    ") def runs_list(self, i: int, p) -> str: rows = [] for rid in reversed(R.list_runs(p)): r = R.load(p, rid) rows.append(f"
  • {e(rid)} · {e(r.data['operator'])} · " f"{e(R.effective_outcome(r))}
  • ") return f"

    Runs

    " if rows else "" def capture_html(self, url: str, s: dict, st: dict, state: str, finished: bool) -> str: saved = "" for c in st.get("captures", []): head = f"$ {c['command']}\n" if c.get("command") else "" saved += f"
    {e(head + c['output'][:4000])}

    captured {e(c['at'][:19])} ({e(c['source'])})

    " if finished or state == "todo": return f"
    Captured output{saved}
    " if saved else "" paste = "" if state == "running" and s["verify"].get("expect") == "output-contains": paste = f"" return (f"
    Capture command and output (optional)" f"{' - ' + str(len(st.get('captures', []))) + ' saved' if st.get('captures') else ''}{saved}" f"
    {self.hidden()}" "

    " "

    " f"

    {paste}

    " "

    Kept in the run record as documentation. Never paste secrets: obvious markers are " "refused, but not everything can be detected. Use the check button when the command ran on a system " "this runbook cannot reach.

    ") def close_run_form(self, i: int, rid: str, r, p) -> str: outcome = r.data.get("outcome") if outcome: note = f" - {e(r.data['outcome_note'])}" if r.data.get("outcome_note") else "" box = f"

    Run finished: {e(outcome)}{note}

    " if outcome == "succeeded": if r.data.get("exercise_recorded"): box += f"

    Pack recorded as {e(r.data['exercise_recorded'])}. Commit the change in the owning repository.

    " else: old, new = R.exercise_update(r) line = new.split("exercise_status:", 1)[1].split("\n", 1)[0].strip() box += (f"

    Current pack status: {e(p.data['exercise_status'])}. " f"Recording this run would set it to {e(line)} in {e(p.path)} " "(a reviewable edit; commit it in the owning repository).

    " f"
    {self.hidden()}" "
    ") return f"

    Close this run

    {box}
    " states = [v["state"] for v in r.data["steps"].values()] btns = [] if r.complete and all(x == "done" for x in states): btns.append("") if "failed" in states: btns.append("") btns.append("") return (f"

    Close this run

    " "

    Finishing records the outcome in the pack's score. A finished run is read-only. " "Unfinished runs count as stalled, and as abandoned after a week.

    " f"
    {self.hidden()}" "

    " f"

    {' '.join(btns)}

    ") def feedback_form(self, i: int, rid: str, r) -> str: fb = R.latest_feedback(r) btns = " ".join(f"" for k, face, t in SMILEYS) form = (f"
    {self.hidden()}" "

    Practicable? Anything that felt strange or unclear, or an idea to make it better. " "Never include secrets.

    " f"

    {btns}

    ") if fb: # collected: close the section, suggest the next step face = next(f for k, f, _ in SMILEYS if k == fb["rating"]) return ("

    Thank you. Recorded: " f"{face} {e(fb['comment'] or '(no comment)')}

    " "

    ← Back to the runbook list

    " f"
    Change my feedback{form}
    ") return f"

    How was this runbook?

    {form}
    " def run_page(self, i: int, rid: str, msg: str = "") -> bytes: p = self.packs[i]; r = R.load(p, rid); d = p.data out = [f"

    {e(d['title'])}

    {e(d['exercise_status'])}

    " f"

    run {e(rid)} · operator {e(r.data['operator'])}

    "] if msg: out.append(f"

    {e(msg)}

    ") blocked = False waiting_for = "" finished = bool(r.data.get("outcome")) total = len(d["steps"]) done_n = sum(1 for x in d["steps"] if r.state(x["id"])["state"] in ("done", "skipped")) out.append(f"

    {done_n} of {total} steps done

    " f"") current = next((x["id"] for x in d["steps"] if r.state(x["id"])["state"] == "running"), None) if current is None: current = next((x["id"] for x in d["steps"] if r.state(x["id"])["state"] in ("todo", "failed")), None) for n, s in enumerate(d["steps"], 1): st = r.state(s["id"]); state = st["state"] act = "" cmd = R.command_for(r, s["id"]) block = "" if cmd and state == "running": # the command and Copy appear only once the step is started block = f"
    {e(cmd)}
    " elif cmd and state == "done": block = f"
    {e(cmd)}
    " # kept as a record; no Copy once done url = f"/run/{i}/{e(rid)}/{e(s['id'])}" v = st.get("verification") if state == "todo" and not blocked: act = f"
    {self.hidden()}
    " elif state == "todo": act = f"

    Waiting for: {e(waiting_for)}

    " elif state == "running": ver = s["verify"] if ver.get("expect", "manual") == "manual": act = (f"
    {self.hidden()}" f"

    Is this true? {e(ver['done_when'])}

    " "
    ") else: act = (f"
    {self.hidden()}" f"

    Runs: {e(R.render(ver['command'], r.data['params'], p))}

    " f"
    ") if not v: act = "

    1. Run the command, then check  →  2. Mark done to lock it in

    " + act elif v["passed"]: act += "

    ✔ Check passed. Press Mark done to lock it in; the next step opens after that.

    " else: act += "

    ✘ Check not passed. Fix the cause, then check again, or mark the step failed.

    " ev = "" if v and v["passed"]: ev = "".join(f" " for x in s.get("evidence", [])) ev = f"

    {ev}

    " if ev else "" done_btn = f" " if v and v["passed"] else "" act += (f"
    {self.hidden()}{ev}

    {done_btn}" f" " "" "

    ") elif state == "failed": act = f"

    Failed: {e(st.get('note') or '(no comment)')}

    {self.hidden()}
    " elif state == "done" and st.get("note"): act = f"

    Comment: {e(st['note'])}

    " if finished and state != "done": act = "" # a finished run is read-only if state not in ("done", "skipped") and not blocked: blocked = True waiting_for = f"step {n}, {s['title']}" rb = f"

    Rollback: {e(s['rollback'])}

    " if s.get("rollback") and state in ("running", "failed") else "" anchor = " id=current" if s["id"] == current else "" fold = "" if state in ("done", "skipped") else " open" # finished steps start folded; the triangle toggles out.append(f"" f"{n}. {e(s['title'])} " f"[{e(s['owner'])}] {e(s.get('risk', 'read-only'))}" f"{state}{block}" f"{self.capture_html(url, s, st, state, finished)}" f"

    Done when: {e(s['verify']['done_when'])}

    {rb}{act}") if r.complete: out.append("

    All steps done. To record this run as exercising the pack, review and apply: " f"python -m rtut run finish {e(p.path)} {e(rid)} --apply

    ") if r.complete: out.append(self.feedback_form(i, rid, r)) out.append(self.close_run_form(i, rid, r, p)) out.append("

    Threat checks

    ") return page(d["title"], "".join(out)) def make_handler(app: App, port_holder: list): class H(BaseHTTPRequestHandler): def log_message(self, *a): # quiet pass def _send(self, body: bytes, code=200): self.send_response(code) self.send_header("Content-Type", "text/html; charset=utf-8") self.send_header("Content-Security-Policy", "default-src 'none'; style-src 'unsafe-inline'; script-src 'unsafe-inline'; form-action 'self'") self.end_headers(); self.wfile.write(body) def _host_ok(self) -> bool: return (self.headers.get("Host", "").split(":")[0] in ("127.0.0.1", "localhost")) def do_GET(self): if not self._host_ok(): return self._send(b"forbidden", 403) parts = [x for x in urlparse(self.path).path.split("/") if x] try: if not parts: return self._send(app.index()) if parts[0] == "pack": return self._send(app.pack_page(int(parts[1]))) if parts[0] == "run": return self._send(app.run_page(int(parts[1]), parts[2])) except (IndexError, ValueError, R.RunError): pass self._send(page("Not found", "

    Not found.

    "), 404) def do_POST(self): if not self._host_ok(): return self._send(b"forbidden", 403) n = int(self.headers.get("Content-Length", 0)) form = {k: v[0] for k, v in parse_qs(self.rfile.read(n).decode()).items()} if form.get("_t") != app.token: return self._send(b"bad token", 403) parts = [x for x in urlparse(self.path).path.split("/") if x] msg = "" try: if parts[:2] == ["run", "new"]: i = int(parts[2]); p = app.packs[i] params = {k[2:]: v for k, v in form.items() if k.startswith("p_")} r = R.create(p, form.get("operator", ""), params) return self._redirect(f"/run/{i}/{r.data['run_id']}#current") if len(parts) == 4 and parts[0] == "run" and parts[3] == "feedback": i, rid = int(parts[1]), parts[2] R.add_feedback(R.load(app.packs[i], rid), form.get("rating", ""), form.get("comment", "")) return self._redirect(f"/run/{i}/{rid}#feedback") if len(parts) == 4 and parts[0] == "run" and parts[3] in ("finalize", "exercise"): i, rid = int(parts[1]), parts[2] r = R.load(app.packs[i], rid) if parts[3] == "finalize": R.finalize(r, form.get("outcome", ""), form.get("note", "")) else: R.apply_exercise(r) app.packs[i] = P.load(app.packs[i].path) # pick up the new exercise status return self._redirect(f"/run/{i}/{rid}#close") i, rid, sid, action = int(parts[1]), parts[2], parts[3], parts[4] p = app.packs[i]; r = R.load(p, rid) if action == "start": R.start(r, sid) elif action == "verify": R.verify(r, sid, confirm=bool(form.get("yes")), manual_ok=(form.get("ok") == "1") if not form.get("yes") else None) elif action == "done": R.complete(r, sid, {k[2:]: v for k, v in form.items() if k.startswith("e_")}, form.get("note", "")) elif action == "fail": R.fail(r, sid, form.get("note", "")) elif action == "capture": R.add_capture(r, sid, form.get("output", ""), form.get("command", "")) elif action == "verify-pasted": R.verify_pasted(r, sid, form.get("output", ""), form.get("command", "")) elif action == "retry": R.retry(r, sid) return self._redirect(f"/run/{i}/{rid}#current") except (R.RunError, KeyError, ValueError, IndexError) as ex: msg = str(ex) try: if parts[:2] == ["run", "new"]: return self._send(app.pack_page(int(parts[2])).replace(b"

    ", f"

    {e(msg)}

    ".encode(), 1), 400) return self._send(app.run_page(int(parts[1]), parts[2], msg), 400) except Exception: return self._send(page("Error", f"

    {e(msg)}

    "), 400) def _redirect(self, to: str): self.send_response(303); self.send_header("Location", to); self.end_headers() return H def serve(roots: list[Path], port: int = 8877) -> ThreadingHTTPServer: app = App(roots) srv = ThreadingHTTPServer(("127.0.0.1", port), make_handler(app, [])) srv.app = app # type: ignore[attr-defined] return srv