Files
skills/hub/templates/page-screens.js
T
Kevin Veen-BirkenbachandClaude Opus 5.5 8ba45efd04 feat(hub): kvb-hub review hub with passkey-signed approvals
The kvb-hub is a local review hub in a container bound to 127.0.0.1: one page, JSON APIs and an MCP endpoint for the operator's inbox, asks, decisions, staged-commit reviews, the radio stream, the agent graph, the worktree tree, push-to-talk and read-aloud radio (Piper, whisper.cpp) and an optional LLM command line. The hub is only a relay. The host executors in hub/client (the runner user service, the PermissionRequest hook and the approval CLI the commit skills call) verify a WebAuthn passkey signature against keys pinned outside every sandbox, refuse replays and fail closed. scripts/hub.sh with make hub-install, hub-check, hub-pin, hub-runner and hub-hook installs and checks the hub; hub-hook writes ~/.claude/settings.json after a backup. make watch-check shows a stuck watch hook. The new kvb-git-diff skill and the commit skills route reviews through the hub; the test_hub_* suites replace test_hub.py.

Security review (kvb-think-criticize, three skeptics) fixed four holes: signed rules auto-approved chained shell commands; the client and LLM openers followed redirects, the LLM one with its bearer key; the worktree scan ran git fsmonitor and filter commands from agent-writable repos; compose set no memory, pids or health limits. Documented until core requirement 093, on the operator's word: the hub page is trusted while signing and pinning, local processes can forge the unsigned operator actions, and the container reads the host /tmp. Committed on the operator's word despite the gate: the passkey path is not yet verified live.

Accepted MEDUSA false positives (137 findings on 115 lines, rows in security-accepted.jsonl): EXA002 x17, MEDUSA-MM-SCAN-058 x12, B603 x8, MEDUSA-FTSC-SCAN-089 x7, MEDUSA-PRIV-SCAN-762 x6, AIC-EXFIL-005 x5, EXA006 x5, MEDUSA-FTSCAN-231 x5, WEB-SSRF x5, MEDUSA-INF-SCAN-2057 x3, RH005 x3, TC001 x3, LO002 x2, MEDUSA-AEXP-SCAN-096 x2, MEDUSA-CGS-SCAN-028 x2, MEDUSA-INF-SCAN-1350 x2, MEDUSA-INF-SCAN-2369 x2, MEDUSA-INF-SCAN-2440 x2, MEDUSA-INF-SCAN-2638 x2, MLSC-SERIAL-002 x2, PL002 x2, PQC005 x2, SAST-PATH-001 x2, TC004 x2, TC005 x2, TC006 x2, TC007 x2, AA-146, EXA007, FINE-050, GB-ENCODE-001, GB-ENCODE-003, MEDUSA-CGS-SCAN-029, MEDUSA-CGS-SCAN-103, MEDUSA-DP-SCAN-1034, MEDUSA-DP-SCAN-1916, MEDUSA-DP-SCAN-954, MEDUSA-EXF26-TOOL-004, MEDUSA-FT-TRIGGER-005, MEDUSA-INF-SCAN-2206, MEDUSA-INF-SCAN-2457, MEDUSA-INF-WEB-003, MEDUSA-INFRA-SCAN-030, MEDUSA-MP-SCAN-1135, MEDUSA-RLHF-POISON-002, MEDUSA-RT-SCAN-006, MM-ENCODE-001, MP-124, PQC001, PQC002, PQC006, RP001, TC009, TC010, WEB-FLASK-004.

🎯 Fix: 80 %
💥 Break: 4 %
💸 Cost: 129 %
⏱️ Time: –
🛡️ Security: 100 %
💰 Value: ?
🏆 Score: 60
🚦 Gate: ⛔ fix 80 % < 90; cost 129 % > 100: ask the operator before the commit

```
/home/kevinveenbirkenbach/Repositories/github.com/kevinveenbirkenbach/skills/.worktrees/feature/kvb-hub
```

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-06 18:46:40 +00:00

94 lines
3.6 KiB
JavaScript

const nav = document.getElementById("tabs");
const screenButtons = new Map(SCREENS.map(([id, label], index) => {
const button = h("button", {type: "button", role: "tab", id: `tab-${id}`, "aria-controls": "main", title: `${label} (Taste ${index + 1})`, on: {click: () => go(id, null)}}, label);
nav.append(button);
return [id, button];
}));
function measure() {
document.documentElement.style.setProperty("--head", `${document.getElementById("head").offsetHeight}px`);
}
function go(screen, sub) {
const hash = screen === "graph" ? `#graph/${sub || ui.graph}` : `#${screen}`;
if (window.location.hash === hash) {
apply();
} else {
window.location.hash = hash;
}
}
function apply() {
const [screen, sub] = decodeURIComponent(window.location.hash.slice(1)).split("/");
ui.screen = SCREENS.some(([id]) => id === screen) ? screen : "stream";
if (GRAPHS.some(([id]) => id === sub)) {
ui.graph = sub;
}
if (document.startViewTransition && drawn.screen && !calm.matches) {
document.startViewTransition(() => render(true));
} else {
render(true);
}
}
function signature() {
return JSON.stringify(ui.screen === "graph" ? [ui.graph, space.views.graph.axis] : []);
}
function render(force) {
if (ui.node !== null && !state.graph.nodes.some((node) => node.id === ui.node)) {
ui.node = null;
}
for (const [id, button] of screenButtons) {
button.setAttribute("aria-selected", String(ui.screen === id));
}
const sig = signature();
if (force || drawn.screen !== ui.screen || drawn.sig !== sig) {
graphDom = null;
stream.dom = null;
tree.dom = null;
const main = document.getElementById("main");
main.setAttribute("aria-labelledby", `tab-${ui.screen}`);
main.replaceChildren({stream: streamScreen, graph: graphScreen, tree: treeScreen}[ui.screen]());
drawn.screen = ui.screen;
drawn.sig = sig;
}
stage();
drawSide();
drawInbox();
drawStatus();
tick();
}
function drawStatus() {
const alarms = alarmRows().length;
const fresh = state.now - ACTIVE;
const agents = new Set(state.claims.filter((claim) => claim.beat !== null && claim.beat >= fresh).map((claim) => claim.agent)).size;
const jobs = state.jobs.filter((job) => job.status === "queued" || job.status === "running").length;
const [mode] = NIGHT[state.night] || [state.night];
const waiting = waitingCount();
const box = (value, label, tone) => h("div", {class: `stat-box${tone ? ` ${tone}` : ""}`}, h("span", {class: "num"}, String(value)), h("span", {class: "lbl"}, label));
document.getElementById("status").replaceChildren(
box(waiting, "wartet auf dich", waiting ? "urgent" : ""),
box(alarms, "Mayday", alarms ? "alarm" : ""),
box(agents, "Agenten aktiv", ""),
box(jobs, "Runner", ""),
box(mode, "Modus", ""));
const badge = document.getElementById("inbox-count");
badge.textContent = String(waiting);
badge.className = `badge${alarms ? " alarm" : waiting ? " urgent" : ""}`;
document.title = `${waiting ? `(${waiting}) ` : ""}kvb-hub`;
}
function toggleLeitstand() {
const on = !document.body.classList.contains("leitstand");
document.body.classList.toggle("leitstand", on);
if (on && document.documentElement.requestFullscreen && !document.fullscreenElement) {
document.documentElement.requestFullscreen().catch(() => null);
}
if (!on && document.fullscreenElement) {
document.exitFullscreen().catch(() => null);
}
window.setTimeout(measure, 50);
}