"""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