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>
132 lines
4.7 KiB
JavaScript
132 lines
4.7 KiB
JavaScript
function windowQuery() {
|
||
if (ui.preset === -1) {
|
||
return `from=${Math.floor(ui.from)}&to=${Math.ceil(ui.to)}`;
|
||
}
|
||
const end = ui.asOf === null ? nowSeconds() : ui.asOf;
|
||
const begin = ui.preset === 0 ? 0 : end - ui.preset;
|
||
return `from=${Math.floor(begin)}${ui.asOf === null ? "" : `&to=${Math.ceil(end)}`}`;
|
||
}
|
||
|
||
const localInput = (ts) => {
|
||
const date = stampOf(ts);
|
||
return new Date(date.getTime() - date.getTimezoneOffset() * 60000).toISOString().slice(0, 16);
|
||
};
|
||
|
||
function timeBar() {
|
||
const bar = document.getElementById("timebar");
|
||
const presets = h("div", {class: "segmented", role: "group", "aria-label": "Zeitraum"});
|
||
const from = h("input", {type: "datetime-local", "aria-label": "Von"});
|
||
const to = h("input", {type: "datetime-local", "aria-label": "Bis"});
|
||
const custom = h("span", {class: "custom", hidden: true}, from, h("span", {class: "muted"}, "bis"), to,
|
||
h("button", {type: "button", class: "btn small soft", on: {click: () => {
|
||
const begin = Date.parse(from.value) / 1000;
|
||
const end = Date.parse(to.value) / 1000;
|
||
if (!Number.isFinite(begin) || !Number.isFinite(end) || begin >= end) {
|
||
toast("Zeitraum ungültig", ["„Von“ muss vor „Bis“ liegen."], false);
|
||
return;
|
||
}
|
||
ui.from = begin;
|
||
ui.to = end;
|
||
reload();
|
||
}}}, "Anwenden"));
|
||
const slider = h("input", {type: "range", min: 0, max: 1000, step: 1, value: 1000, "aria-label": "Stand der Wiedergabe"});
|
||
const play = h("button", {type: "button", class: "btn small soft"}, "▶ Wiedergabe");
|
||
const liveButton = h("button", {type: "button", class: "btn small ghost"}, "Live");
|
||
const label = h("span", {class: "window-label"});
|
||
for (const [seconds, name] of PRESETS) {
|
||
presets.append(h("button", {type: "button", "data-preset": seconds, on: {click: () => choosePreset(seconds)}}, name));
|
||
}
|
||
slider.addEventListener("input", () => {
|
||
stopReplay();
|
||
const span = state.now - state.first;
|
||
ui.asOf = Number(slider.value) >= 1000 ? null : state.first + (span * Number(slider.value)) / 1000;
|
||
reload();
|
||
});
|
||
play.addEventListener("click", () => (ui.playing ? stopReplay() : startReplay()));
|
||
liveButton.addEventListener("click", () => {
|
||
stopReplay();
|
||
ui.asOf = null;
|
||
if (ui.preset === -1) {
|
||
ui.preset = DAY;
|
||
}
|
||
reload();
|
||
});
|
||
bar.append(presets, custom, h("div", {class: "replay"}, play, slider, liveButton, label));
|
||
timeDom = {presets, custom, from, to, slider, play, live: liveButton, label};
|
||
}
|
||
|
||
function reload() {
|
||
load();
|
||
if (ui.screen === "stream") {
|
||
fetchEvents();
|
||
}
|
||
}
|
||
|
||
function choosePreset(seconds) {
|
||
stopReplay();
|
||
ui.preset = seconds;
|
||
if (seconds === -1) {
|
||
ui.from = ui.from === null ? state.since : ui.from;
|
||
ui.to = ui.to === null ? state.until : ui.to;
|
||
timeDom.from.value = localInput(ui.from);
|
||
timeDom.to.value = localInput(ui.to);
|
||
}
|
||
reload();
|
||
}
|
||
|
||
function drawTime() {
|
||
if (!timeDom) {
|
||
return;
|
||
}
|
||
for (const button of timeDom.presets.children) {
|
||
button.setAttribute("aria-pressed", String(Number(button.dataset.preset) === ui.preset));
|
||
}
|
||
timeDom.custom.hidden = ui.preset !== -1;
|
||
timeDom.slider.disabled = ui.preset === -1;
|
||
timeDom.play.disabled = ui.preset === -1;
|
||
timeDom.live.disabled = live();
|
||
const span = Math.max(state.now - state.first, 1);
|
||
if (!ui.playing) {
|
||
timeDom.slider.value = String(ui.asOf === null ? 1000 : Math.round((1000 * (ui.asOf - state.first)) / span));
|
||
}
|
||
timeDom.label.textContent = `${absolute.format(stampOf(state.since))} – ${absolute.format(stampOf(state.until))}${live() ? " · live" : ""}`;
|
||
}
|
||
|
||
function startReplay() {
|
||
if (ui.preset === -1) {
|
||
return;
|
||
}
|
||
const began = state.first;
|
||
const span = Math.max(state.now - began, 1);
|
||
const resume = ui.asOf !== null && ui.asOf < state.now - 60;
|
||
let progress = resume ? (ui.asOf - began) / span : 0;
|
||
ui.playing = true;
|
||
timeDom.play.textContent = "❚❚ Pause";
|
||
const advance = async () => {
|
||
if (!ui.playing) {
|
||
return;
|
||
}
|
||
progress = Math.min(progress + 1 / 48, 1);
|
||
ui.asOf = progress >= 1 ? null : began + span * progress;
|
||
timeDom.slider.value = String(Math.round(progress * 1000));
|
||
await load();
|
||
if (ui.screen === "stream") {
|
||
fetchEvents();
|
||
}
|
||
if (progress >= 1) {
|
||
stopReplay();
|
||
} else if (ui.playing) {
|
||
replay = window.setTimeout(advance, 500);
|
||
}
|
||
};
|
||
advance();
|
||
}
|
||
|
||
function stopReplay() {
|
||
ui.playing = false;
|
||
window.clearTimeout(replay);
|
||
if (timeDom) {
|
||
timeDom.play.textContent = "▶ Wiedergabe";
|
||
}
|
||
}
|