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>
94 lines
3.6 KiB
JavaScript
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);
|
|
}
|