Files
turnstone/scripts/livepass.py
T
Patrick Buckley 9102f858a4 chore(scripts): commit the livepass harness generator
The livepass harness — the headless-render rig that verified every
converted modal surface and click-drives submits (the dead-Save bug
class) — lived as ad hoc files in /tmp and got wiped once already.
The durable piece is the GENERATOR: the markup is extracted fresh from
the index files at build time (a committed snapshot would drift) and
the stylesheets/scripts are symlinked so edits are live on refresh.

scripts/livepass.py builds both harnesses into /tmp/livepass/ (ui:
all six dialog-tier surfaces incl. the real cards.js batch controller
drive; console: the admin-pane fragment hosting the shelves, with
schedule/model/policy/confirm/token fixtures and the model-save click
drive that flips document.title to PUT-OK-<n>). --serve included;
the chrome screenshot incantation and the ?open= registry are in the
module docstring. Governance fixtures (roles/HR/OGP/memory/skill) are
documented seams for when those surfaces need driving.
2026-06-10 13:31:53 -07:00

413 lines
17 KiB
Python
Executable File

#!/usr/bin/env python3
"""Build the livepass harnesses — render real hatch dialogs/shelves headlessly.
The livepass is how converted modal surfaces get verified without booting a
server: a minimal page that symlinks the REAL stylesheets and scripts, embeds
the REAL markup (extracted fresh from the index files at build time), stubs
``window.authFetch`` with canned fixtures, and drives surfaces via ``?open=``
query params — including click-driving submits so dead buttons can't hide
(the model-Save bug class).
Usage:
python3 scripts/livepass.py # build into /tmp/livepass/
python3 scripts/livepass.py --out DIR # build elsewhere
python3 scripts/livepass.py --serve 8950 # build + serve (Ctrl+C stops)
Then screenshot states (file:// blocks ES modules — always serve over http;
the reduced-motion flag is REQUIRED, entrance animations race the capture):
google-chrome --headless --disable-gpu --hide-scrollbars \\
--force-prefers-reduced-motion --window-size=1440,900 \\
--virtual-time-budget=9000 --screenshot=out.png \\
"http://localhost:8950/ui/livepass.html?open=new-ws&theme=light"
UI harness (?open=): new-ws · new-ws-fork · edit-title · delete-ws ·
revoke-mcp · ws-delete · ws-delete-results (+ &theme=light, &busy=1)
Console harness (?open=): schedule-create · schedule-edit · model-create ·
model-edit · model-save (drives a Save click; document.title becomes
PUT-OK-<n> on success) · policy · confirm · token
Governance surfaces (roles/HR/OGP/memory/skill) need fixtures that are not
canned yet — add a fixture + driver branch below when you need one.
Rebuild after ANY markup change: the dialog blocks are embedded at build
time. Assets are symlinked, so CSS/JS edits are live on refresh.
"""
from __future__ import annotations
import argparse
import re
from pathlib import Path
ROOT = Path(__file__).resolve().parent.parent
UI_INDEX = ROOT / "turnstone/ui/static/index.html"
CONSOLE_INDEX = ROOT / "turnstone/console/static/index.html"
def extract_dialogs(index: Path, only_id: str | None = None) -> list[str]:
"""Every <dialog class="hatch ..."> block, verbatim from the tree."""
html = index.read_text(encoding="utf-8")
blocks = []
for m in re.finditer(r"[ \t]*<dialog\s[^>]*class=\"[^\"]*\bhatch\b[^\"]*\"", html):
end = html.index("</dialog>", m.start()) + len("</dialog>")
block = html[m.start() : end]
if only_id and f'id="{only_id}"' not in block:
continue
blocks.append(block)
if not blocks:
raise SystemExit(f"no dialog.hatch blocks found in {index}")
return blocks
def extract_admin_fragment() -> str:
"""The console admin pane — the hatch-host all shelves live inside."""
html = CONSOLE_INDEX.read_text(encoding="utf-8")
start = html.index('<div id="admin-layout"')
end = html.index("<!-- /admin-layout -->") + len("<!-- /admin-layout -->")
return html[start:end]
def inject(template: str, marker: str, payload: str) -> str:
begin = template.index(f"<!-- {marker}:BEGIN -->") + len(f"<!-- {marker}:BEGIN -->")
end = template.index(f"<!-- {marker}:END -->")
return template[:begin] + "\n" + payload + "\n" + template[end:]
def symlink(link: Path, target: Path) -> None:
if link.is_symlink() or link.exists():
link.unlink()
link.symlink_to(target)
# --------------------------------------------------------------------------
# UI harness — the standalone app's dialog tier. Drives the REAL cards.js
# controller for the batch surfaces so the production code path renders.
# --------------------------------------------------------------------------
UI_TEMPLATE = """<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>ui livepass</title>
<link rel="stylesheet" href="shared/base.css" />
<link rel="stylesheet" href="shared/ui-base.css" />
<link rel="stylesheet" href="shared/chat.css" />
<link rel="stylesheet" href="shared/conversation.css" />
<link rel="stylesheet" href="shared/cards.css" />
<link rel="stylesheet" href="static/style.css" />
<link rel="stylesheet" href="shared/shell.css" />
<link rel="stylesheet" href="shared/interactive.css" />
<link rel="stylesheet" href="shared/hatch.css" />
</head>
<body>
<!-- DIALOGS:BEGIN -->
<!-- DIALOGS:END -->
<div id="toast" role="status" aria-live="polite"></div>
<script>
window.authFetch = function (url) {
// One canned failure so the results view shows the mixed state.
var fail = url && url.indexOf("c3d4e5f6a1b2") !== -1;
return Promise.resolve({
ok: !fail,
status: fail ? 409 : 200,
headers: { get: function () { return "application/json"; } },
json: function () { return Promise.resolve({}); },
text: function () {
return Promise.resolve(
fail ? '{"error": "workstream is still running"}' : "",
);
},
});
};
window.showToast = function (msg) { console.log("toast:", msg); };
</script>
<script type="module">
import { openDialog, setBusy } from "./shared/hatch.js";
const q = new URLSearchParams(location.search);
if (q.get("theme") === "light")
document.documentElement.dataset.theme = "light";
const open = q.get("open") || "";
function fill(id, text) {
const el = document.getElementById(id);
if (el) el.textContent = text;
}
if (open === "new-ws" || open === "new-ws-fork") {
const dlg = document.getElementById("new-ws-dialog");
const canned = {
"new-ws-model": ["sonnet-4-6", "gpt-5-2", "qwen3-32b"],
"new-ws-judge-model": ["sonnet-4-6", "qwen3-32b"],
"new-ws-skill": ["code-review (default)", "deep-research"],
};
for (const id in canned) {
const s = document.getElementById(id);
for (const n of canned[id]) {
const o = document.createElement("option");
o.value = n;
o.textContent = n;
s.appendChild(o);
}
}
if (open === "new-ws-fork") {
fill("new-ws-title", "Fork workstream");
fill("new-ws-tag", "WS-FORK");
document.getElementById("new-ws-submit").textContent = "Fork";
const skillLabel = document.querySelector('label[for="new-ws-skill"]');
if (skillLabel) skillLabel.hidden = true;
document.getElementById("new-ws-skill").hidden = true;
document.getElementById("new-ws-attach-row").hidden = true;
}
openDialog(dlg);
} else if (open === "edit-title") {
document.getElementById("edit-title-input").value =
"lshell renovation pass 3";
openDialog(document.getElementById("edit-title-dialog"));
} else if (open === "delete-ws") {
fill(
"delete-ws-message",
'Delete "lshell renovation pass 3"? This cannot be undone.',
);
openDialog(document.getElementById("delete-ws-dialog"));
} else if (open === "revoke-mcp") {
fill(
"revoke-mcp-message",
"Revoke the connection to github? Tools that need this server will require re-consent.",
);
openDialog(document.getElementById("revoke-mcp-dialog"));
} else if (open === "ws-delete" || open === "ws-delete-results") {
// Drive the REAL shared controller so the dialog renders through
// the production code path (cards.js confirmSelection/confirm).
const mod = await import("./shared/cards.js");
const c = mod.createSavedCardsController({
idPrefix: "ws-delete",
buttonId: "ws-delete-btn",
noun: "workstream",
activateLabel: (s) => "Resume: " + (s.title || s.ws_id),
render: () => {},
buildDeleteRequest: (wsId) => ({
url: "/v1/api/workstreams/" + encodeURIComponent(wsId) + "/delete",
options: { method: "POST" },
}),
});
c.setItems([
{ ws_id: "a1b2c3d4e5f6", title: "lshell renovation pass 3" },
{ ws_id: "b2c3d4e5f6a1", title: "canonical trajectory spike" },
{
ws_id: "c3d4e5f6a1b2",
title:
"a very long workstream title that should wrap rather than punch out of the dialog box entirely",
},
]);
c.toggleAll();
c.confirmSelection();
if (open === "ws-delete-results") c.confirm();
}
if (q.get("busy")) {
const d = document.querySelector("dialog[open]");
if (d) setBusy(d, true);
}
</script>
</body>
</html>
"""
# --------------------------------------------------------------------------
# Console harness — the admin pane fragment hosts the shelves; body-level
# hatch dialogs (confirm/token/install/batch) ride along inside it.
# model-save click-drives the submit: document.title flips to PUT-OK-<n>.
# --------------------------------------------------------------------------
CONSOLE_TEMPLATE = """<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>console livepass</title>
<link rel="stylesheet" href="shared/base.css" />
<link rel="stylesheet" href="console-static/style.css" />
<link rel="stylesheet" href="shared/hatch.css" />
<style>
body { padding: 0; }
#view-admin { padding: 20px; height: 100vh; box-sizing: border-box; }
#admin-layout { height: 100%; }
</style>
</head>
<body>
<div id="view-admin">
<!-- FRAGMENT:BEGIN -->
<!-- FRAGMENT:END -->
</div>
<div id="toast" role="status" aria-live="polite"></div>
<script>
(function () {
function reply(data) {
return Promise.resolve({
ok: true,
status: 200,
headers: { get: function () { return "application/json"; } },
json: function () { return Promise.resolve(data); },
text: function () { return Promise.resolve(JSON.stringify(data)); },
});
}
var SCHED = {
task_id: "t1", name: "nightly-digest", description: "Morning digest",
schedule_type: "cron", cron_expr: "0 6 * * 1,3,5", at_time: "",
target_mode: "auto", model: "fable-5", skill: "daily-digest",
initial_message: "Summarize overnight cluster activity.",
auto_approve: false, enabled: true,
notify_targets: [{ channel_type: "discord", channel_id: "8675309" }],
next_run: "2026-06-10T06:00:00",
};
var MODEL = {
definition_id: "def1", alias: "fable-5", model: "claude-fable-5",
provider: "anthropic", base_url: "", context_window: 200000,
capabilities: JSON.stringify({ supports_vision: true }),
enabled: true, temperature: null, max_tokens: null,
reasoning_effort: null, surface_persisted_reasoning: true,
replay_reasoning_to_model: false,
};
window.__putCount = 0;
window.authFetch = function (url, opts) {
var method = (opts && opts.method) || "GET";
if (method === "PUT" && url.indexOf("/model-definitions/def1") >= 0) {
window.__putCount++;
document.title = "PUT-OK-" + window.__putCount;
return reply({ ok: true });
}
if (url.indexOf("/schedules/preview") >= 0)
return reply({
valid: true, error: "",
next: [
"2026-06-10T06:00:00+00:00",
"2026-06-12T06:00:00+00:00",
"2026-06-15T06:00:00+00:00",
],
});
if (url.indexOf("/schedules/t1") >= 0) return reply(SCHED);
if (url.indexOf("/schedules") >= 0) return reply({ schedules: [SCHED] });
if (url.indexOf("/model-capabilities/known") >= 0)
return reply({ models: ["claude-fable-5", "claude-opus-4-8"] });
if (url.indexOf("/model-capabilities?") >= 0)
return reply({
known: true,
capabilities: {
context_window: 200000, supports_tools: true,
supports_streaming: true, supports_vision: true,
supports_web_search: true, supports_temperature: true,
supports_effort: true,
},
});
if (url.indexOf("/model-definitions/def1") >= 0) return reply(MODEL);
if (url.indexOf("/model-definitions") >= 0) return reply({ models: [] });
if (url.indexOf("/api/models") >= 0)
return reply({ models: [
{ alias: "fable-5", model: "claude-fable-5" },
{ alias: "gpt-5.2", model: "gpt-5.2" },
] });
if (url.indexOf("/skills") >= 0)
return reply({ skills: [{ name: "daily-digest" }, { name: "ops-runbook" }] });
if (url.indexOf("/policies") >= 0)
return reply({ policies: [
{ policy_id: "p1", name: "deny-rm", tool_pattern: "bash*rm*",
action: "deny", priority: 900, enabled: true },
{ policy_id: "p2", name: "default-ask", tool_pattern: "*",
action: "ask", priority: 0, enabled: true },
] });
return reply({});
};
window.showToast = function (m) {
console.log("toast:", m);
var t = document.getElementById("toast");
t.textContent = m;
t.classList.add("show");
};
})();
</script>
<script type="module" src="shared/utils.js"></script>
<script type="module" src="shared/hatch.js"></script>
<script src="console-static/admin.js"></script>
<script src="console-static/governance.js"></script>
<script>
window.addEventListener("load", function () {
var q = new URLSearchParams(location.search);
if (q.get("theme") === "light")
document.documentElement.dataset.theme = "light";
var open = q.get("open") || "";
setTimeout(function () {
if (open === "schedule-create") showCreateScheduleModal();
else if (open === "schedule-edit") showEditScheduleModal("t1");
else if (open === "model-create") showCreateModelModal();
else if (open === "model-edit" || open === "model-save")
showEditModelModal("def1");
else if (open === "policy") {
window._govPolicies && _govPolicies.length === 0 &&
loadGovPolicies && loadGovPolicies();
showCreatePolicyModal();
} else if (open === "confirm")
showConfirmModal(
"Delete schedule",
"Delete nightly-digest? Its run history is removed with it. This cannot be undone.",
"Delete",
function () {},
);
else if (open === "token")
showTokenCreatedModal(
"tsk_9f2e41c7a8b35d60e1f4a2b89c7d3e5f6a1b0c9d8e7f6a5b4c3d2e1f0a9b8c7d",
);
if (open === "model-save")
setTimeout(function () {
document.getElementById("model-create-submit").click();
}, 900);
if (q.get("busy"))
setTimeout(function () {
var d = document.querySelector("dialog[open]");
if (d) window.TurnstoneHatch.setBusy(d, true);
}, 400);
}, 150);
});
</script>
</body>
</html>
"""
def build(out: Path) -> None:
ui = out / "ui"
con = out / "console"
ui.mkdir(parents=True, exist_ok=True)
con.mkdir(parents=True, exist_ok=True)
symlink(ui / "shared", ROOT / "turnstone/shared_static")
symlink(ui / "static", ROOT / "turnstone/ui/static")
blocks = extract_dialogs(UI_INDEX)
# the coordinator batch dialog shares the cards.js builder — ride along
blocks += extract_dialogs(CONSOLE_INDEX, only_id="coord-delete-dialog")
(ui / "livepass.html").write_text(
inject(UI_TEMPLATE, "DIALOGS", "\n".join(blocks)), encoding="utf-8"
)
print(f"{ui}/livepass.html — {len(blocks)} dialogs")
symlink(con / "shared", ROOT / "turnstone/shared_static")
symlink(con / "console-static", ROOT / "turnstone/console/static")
(con / "livepass.html").write_text(
inject(CONSOLE_TEMPLATE, "FRAGMENT", extract_admin_fragment()),
encoding="utf-8",
)
print(f"{con}/livepass.html — admin fragment embedded")
def main() -> None:
ap = argparse.ArgumentParser(description=__doc__.splitlines()[0])
ap.add_argument("--out", type=Path, default=Path("/tmp/livepass"))
ap.add_argument("--serve", type=int, metavar="PORT")
args = ap.parse_args()
build(args.out)
if args.serve:
import functools
import http.server
handler = functools.partial(http.server.SimpleHTTPRequestHandler, directory=str(args.out))
print(f"serving {args.out} on http://localhost:{args.serve}/ — Ctrl+C stops")
http.server.ThreadingHTTPServer(("127.0.0.1", args.serve), handler).serve_forever()
if __name__ == "__main__":
main()