"""Founder interaction surface — ``warden desk`` (WARDEN-WP-0029 T03). Build-phase localhost page for the rare founder acts emitted by ``warden plan``: approve/deny, OIDC login launch, paste-once provision into OpenBao. Pattern: stdlib ``ThreadingHTTPServer`` on 127.0.0.1 only (see net-kingdom security-bootstrap-console). No multi-user auth; OS session is trust boundary. Never logs secret values. """ from __future__ import annotations import html import json import secrets import subprocess import threading import webbrowser from dataclasses import dataclass, field as dc_field from http import HTTPStatus from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer from pathlib import Path from typing import Any, Callable, Dict, Optional from urllib.parse import parse_qs, urlparse # Acts the desk can render. Keep in sync with plan.FOUNDER_ACT_KINDS. DESK_ACTS = ("approve", "oidc_login", "paste_once_provision") class DeskError(Exception): """Raised when desk session setup fails.""" @dataclass class DeskSession: """In-memory founder act session — metadata only (no secret values stored).""" token: str act: str summary: str lane_id: str = "" path: str = "" kv_field: str = "value" oidc_command: str = "" result: str = "pending" # pending | approved | denied | provisioned | launched | error message: str = "" extra: Dict[str, Any] = dc_field(default_factory=dict) def new_session( *, act: str, summary: str, lane_id: str = "", path: str = "", kv_field: str = "value", oidc_command: str = "", extra: Optional[dict] = None, ) -> DeskSession: if act not in DESK_ACTS: raise DeskError(f"unknown desk act {act!r}; expected one of {DESK_ACTS}") if act == "paste_once_provision" and not path: raise DeskError("paste_once_provision requires --path (concrete OpenBao path)") return DeskSession( token=secrets.token_urlsafe(24), act=act, summary=summary, lane_id=lane_id, path=path, kv_field=kv_field or "value", oidc_command=oidc_command, extra=dict(extra or {}), ) def session_from_plan_dict(plan: dict) -> DeskSession: """Build a desk session from a ``warden plan --json`` payload.""" act_raw = plan.get("founder_act") or {} if not act_raw or plan.get("verdict") != "founder_required": raise DeskError( "plan verdict is not founder_required or founder_act is missing — " "nothing for the desk to render" ) kind = str(act_raw.get("kind") or "") details = act_raw.get("details") or {} path = str(details.get("path_template") or details.get("path") or "") if kind == "paste_once_provision" and ("<" in path or ">" in path): raise DeskError( f"path_template still has placeholders ({path!r}); pass a concrete " "--path to warden desk" ) return new_session( act=kind, summary=str(act_raw.get("summary") or plan.get("need") or "founder act"), lane_id=str(details.get("lane_id") or plan.get("lane_id") or ""), path=path if "<" not in path else "", oidc_command=str(details.get("fetch_command") or ""), extra={"need": plan.get("need"), "organization_posture": plan.get("organization_posture")}, ) def _page(title: str, body: str) -> bytes: doc = f""" {html.escape(title)}

warden desk · build phase · localhost only

{body}
""" return doc.encode("utf-8") def _render_home(session: DeskSession) -> bytes: summary = html.escape(session.summary) lane = html.escape(session.lane_id or "—") act = html.escape(session.act) if session.result != "pending": cls = "ok" if session.result in ("approved", "provisioned", "launched") else "bad" return _page( "Desk result", f"

Act complete

" f"

Result: {html.escape(session.result)}

" f"

{html.escape(session.message or '')}

" f"

You can close this tab. Server will shut down shortly.

", ) if session.act == "approve": body = f"""

Founder approval

{summary}

lane: {lane}
act: {act}

Metadata-only — no secrets transit this form.

""" elif session.act == "oidc_login": cmd = html.escape(session.oidc_command or "bao login -method=oidc") body = f"""

OIDC / identity login

{summary}

{cmd}

Run the command in your own terminal (browser OIDC). ops-warden never captures the token.

""" else: # paste_once_provision path = html.escape(session.path) kv_field = html.escape(session.kv_field) body = f"""

Paste-once provision

{summary}

OpenBao path: {path}
field: {kv_field}
lane: {lane}

Paste the secret once. It is written to OpenBao via bao kv put and never shown in the terminal or audit log.

Build-phase desk: localhost only, OS session trust.

""" return _page("warden desk", body) def _provision_to_openbao(path: str, field: str, value: str) -> None: """Write one field to OpenBao without putting the value on argv.""" # bao kv put path field=- reads value from stdin proc = subprocess.run( ["bao", "kv", "put", path, f"{field}=-"], input=value.encode("utf-8"), capture_output=True, timeout=60, check=False, ) if proc.returncode != 0: err = (proc.stderr or proc.stdout or b"").decode("utf-8", errors="replace") # scrub accidental value echo if value and value in err: err = err.replace(value, "") raise DeskError(f"bao kv put failed (exit {proc.returncode}): {err[:300]}") def make_handler( session: DeskSession, *, on_done: Optional[Callable[[DeskSession], None]] = None, dry_run: bool = False, ) -> type: """Build a request handler class closed over *session*.""" class Handler(BaseHTTPRequestHandler): def log_message(self, fmt: str, *args: Any) -> None: # noqa: A003 # Avoid logging POST bodies / query secrets line = f"[desk] {self.address_string()} {fmt % args}" if session.token in line: line = line.replace(session.token, "") print(line, flush=True) def _deny(self, code: int = 404) -> None: self.send_response(code) self.send_header("Content-Type", "text/plain; charset=utf-8") self.end_headers() self.wfile.write(b"not found\n") def do_GET(self) -> None: # noqa: N802 parsed = urlparse(self.path) qs = parse_qs(parsed.query) token = (qs.get("t") or [""])[0] if parsed.path not in ("/", "/index.html") or token != session.token: self._deny() return body = _render_home(session) self.send_response(HTTPStatus.OK) self.send_header("Content-Type", "text/html; charset=utf-8") self.send_header("Cache-Control", "no-store") self.send_header("Content-Length", str(len(body))) self.end_headers() self.wfile.write(body) def do_POST(self) -> None: # noqa: N802 parsed = urlparse(self.path) if parsed.path != "/act": self._deny() return length = int(self.headers.get("Content-Length") or "0") raw = self.rfile.read(length) if length else b"" form = parse_qs(raw.decode("utf-8", errors="replace"), keep_blank_values=True) token = (form.get("token") or [""])[0] if token != session.token: self._deny(403) return decision = (form.get("decision") or [""])[0] try: if session.act == "approve": if decision == "approve": session.result = "approved" session.message = "Approved (metadata only)." else: session.result = "denied" session.message = "Denied." elif session.act == "oidc_login": if decision == "launched": session.result = "launched" session.message = "Operator confirmed OIDC login completed." else: session.result = "denied" session.message = "Cancelled." elif session.act == "paste_once_provision": if decision == "deny": session.result = "denied" session.message = "Cancelled — nothing written." else: secret = (form.get("secret") or [""])[0] if not secret: raise DeskError("empty secret value") if dry_run: session.result = "provisioned" session.message = ( f"dry-run: would write field {session.kv_field!r} " f"to {session.path}" ) else: _provision_to_openbao(session.path, session.kv_field, secret) session.result = "provisioned" session.message = ( f"Wrote field {session.kv_field!r} to {session.path} " "(value not logged)." ) # drop reference promptly secret = "" form.pop("secret", None) else: raise DeskError(f"unhandled act {session.act}") except DeskError as e: session.result = "error" session.message = str(e) body = _render_home(session) self.send_response(HTTPStatus.OK) self.send_header("Content-Type", "text/html; charset=utf-8") self.send_header("Cache-Control", "no-store") self.send_header("Content-Length", str(len(body))) self.end_headers() self.wfile.write(body) if on_done and session.result != "pending": on_done(session) return Handler def run_desk( session: DeskSession, *, host: str = "127.0.0.1", port: int = 0, open_browser: bool = True, dry_run: bool = False, shutdown_after_done: bool = True, ) -> DeskSession: """Serve the desk until the act completes (or the process is interrupted). Binds *host* (default loopback only). *port* 0 picks an ephemeral port. """ if host not in ("127.0.0.1", "localhost", "::1"): raise DeskError( f"desk refuses non-loopback bind {host!r} in build phase " "(set host only for tests with 127.0.0.1)" ) done = threading.Event() def _on_done(_s: DeskSession) -> None: if shutdown_after_done: done.set() handler = make_handler(session, on_done=_on_done, dry_run=dry_run) server = ThreadingHTTPServer((host, port), handler) bound_port = server.server_address[1] url = f"http://{host}:{bound_port}/?t={session.token}" thread = threading.Thread(target=server.serve_forever, daemon=True) thread.start() print(f"warden desk listening on {url}", flush=True) print(f"act={session.act} lane={session.lane_id or '—'} (token not for logs elsewhere)", flush=True) if open_browser: try: webbrowser.open(url) except Exception: # noqa: BLE001 pass try: done.wait() except KeyboardInterrupt: session.result = session.result if session.result != "pending" else "denied" session.message = session.message or "interrupted" finally: server.shutdown() thread.join(timeout=2) return session def load_plan_json(path: Path) -> dict: data = json.loads(path.read_text(encoding="utf-8")) if not isinstance(data, dict): raise DeskError("plan JSON must be an object") return data