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>
210 lines
9.9 KiB
JavaScript
210 lines
9.9 KiB
JavaScript
const skyMoment = () => (live() ? Date.now() : (ui.asOf === null ? state.until : ui.asOf) * 1000);
|
||
|
||
function applyTheme(sun) {
|
||
const custom = settings.theme === "custom";
|
||
const mode = custom ? Ambience.modeOf(settings.colors.top, settings.colors.bottom) : settings.theme === "sky" ? Ambience.modeAt(sun.alt) : settings.theme;
|
||
const glass = Boolean(window.CSS && CSS.supports("(backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))")) && !window.matchMedia("(prefers-reduced-transparency: reduce)").matches;
|
||
const key = [mode, custom ? settings.colors.accent : "", glass].join("|");
|
||
if (key === sky.mode) {
|
||
return;
|
||
}
|
||
sky.mode = key;
|
||
const root = document.documentElement;
|
||
const tokens = Ambience.THEMES[mode];
|
||
for (const [name, value] of Object.entries(tokens)) {
|
||
root.style.setProperty(`--${name}`, value);
|
||
}
|
||
if (custom) {
|
||
root.style.setProperty("--accent", settings.colors.accent);
|
||
root.style.setProperty("--accent-soft", Ambience.css(Ambience.mix(Ambience.hex(settings.colors.accent), Ambience.hex(tokens.panel), 0.84)));
|
||
}
|
||
root.style.setProperty("--veil", `${glass ? Ambience.GLASS * 100 : 100}%`);
|
||
root.style.setProperty("--card", `${glass ? Ambience.CARD * 100 : 100}%`);
|
||
root.style.colorScheme = mode;
|
||
root.dataset.mode = mode;
|
||
requestSpace();
|
||
}
|
||
|
||
function drawMoon(paint, at, radius, moon) {
|
||
const tilt = (moon.tilt * Math.PI) / 180;
|
||
paint.save();
|
||
paint.translate(at.x, at.y);
|
||
paint.rotate(Math.atan2(-Math.cos(tilt), -Math.sin(tilt)));
|
||
const halo = paint.createRadialGradient(0, 0, radius, 0, 0, radius * 4);
|
||
halo.addColorStop(0, `rgb(235 240 255 / ${(0.28 * moon.lit).toFixed(3)})`);
|
||
halo.addColorStop(1, "rgb(235 240 255 / 0)");
|
||
paint.fillStyle = halo;
|
||
paint.fillRect(-radius * 4, -radius * 4, radius * 8, radius * 8);
|
||
paint.fillStyle = "rgb(60 68 92 / 0.55)";
|
||
paint.beginPath();
|
||
paint.arc(0, 0, radius, 0, Math.PI * 2);
|
||
paint.fill();
|
||
paint.fillStyle = "#f3f0e4";
|
||
paint.beginPath();
|
||
paint.arc(0, 0, radius, -Math.PI / 2, Math.PI / 2, false);
|
||
paint.ellipse(0, 0, Math.abs(2 * moon.lit - 1) * radius, radius, 0, Math.PI / 2, -Math.PI / 2, moon.lit < 0.5);
|
||
paint.closePath();
|
||
paint.fill();
|
||
paint.restore();
|
||
}
|
||
|
||
function drawSky() {
|
||
const ms = skyMoment();
|
||
const sun = Ambience.sun(ms, settings.latitude, settings.longitude);
|
||
const moon = Ambience.moon(ms, settings.latitude, settings.longitude);
|
||
applyTheme(sun);
|
||
const view = seenView("sky");
|
||
const facing = COMPASS[Space.compass(view.azimuth)];
|
||
const turned = ["azimuth", "altitude", "fov"].some((key) => Math.abs(view[key] - Space.DEFAULTS.sky[key]) >= 0.5);
|
||
const moonText = moon.alt > 0 ? ` · Mond ${Math.round(moon.lit * 100)} % ${moon.waxing ? "zunehmend" : "abnehmend"}` : "";
|
||
document.getElementById("sky-note").textContent = `${sun.alt > 0 ? "☀" : "☾"} ${STAGE_LABELS[Ambience.stage(sun.alt)]}${moonText}${turned ? ` · Blick ${facing[0]}` : ""}`;
|
||
if (space.look) {
|
||
const words = `Blick nach ${facing[1]} · ${((Math.round(view.azimuth) % 360) + 360) % 360}° · Höhe ${Math.round(view.altitude)}° · Sichtfeld ${Math.round(view.fov)}°`;
|
||
space.compass.textContent = words;
|
||
sky.canvas.setAttribute("aria-label", `Himmel, ${words}. Pfeiltasten drehen den Blick, Plus und Minus zoomen, 0 setzt zurück, Esc beendet.`);
|
||
}
|
||
const canvas = sky.canvas;
|
||
const paint = canvas.getContext("2d");
|
||
if (!paint) {
|
||
return;
|
||
}
|
||
const ratio = Math.min(window.devicePixelRatio || 1, 2);
|
||
const width = window.innerWidth;
|
||
const height = window.innerHeight;
|
||
if (canvas.width !== Math.round(width * ratio) || canvas.height !== Math.round(height * ratio)) {
|
||
canvas.width = Math.round(width * ratio);
|
||
canvas.height = Math.round(height * ratio);
|
||
}
|
||
paint.setTransform(ratio, 0, 0, ratio, 0, 0);
|
||
const sight = Space.lens(view, width, height);
|
||
const place = (alt, az) => Space.place(sight, alt, az);
|
||
const inside = (at, margin) => at.x >= -margin && at.x <= width + margin && at.y >= -margin && at.y <= height + margin;
|
||
const seen = (body, at) => body.alt > -2 && Math.abs(at.lon) <= view.fov / 2 + 10 && inside(at, 100);
|
||
const colors = settings.theme === "custom" ? {top: Ambience.hex(settings.colors.top), bottom: Ambience.hex(settings.colors.bottom)} : Ambience.skyAt(sun.alt);
|
||
const low = place(0, view.azimuth);
|
||
const high = place(90, view.azimuth);
|
||
const air = paint.createLinearGradient(low.x + (high.x - low.x) / Space.REACH, low.y + (high.y - low.y) / Space.REACH, low.x, low.y);
|
||
air.addColorStop(0, Ambience.css(colors.top));
|
||
air.addColorStop(1, Ambience.css(colors.bottom));
|
||
paint.fillStyle = air;
|
||
paint.fillRect(0, 0, width, height);
|
||
const dusk = Math.max(0, 1 - Math.abs(sun.alt) / 14);
|
||
const glowAt = place(Math.max(sun.alt, -3), sun.az);
|
||
if (dusk > 0 && Math.abs(glowAt.lon) <= 135) {
|
||
const x = Math.min(Math.max(glowAt.x, sight.cx - sight.kx * 100), sight.cx + sight.kx * 100);
|
||
const glow = paint.createRadialGradient(x, glowAt.y, 0, x, glowAt.y, width * 0.5);
|
||
glow.addColorStop(0, `rgb(255 186 128 / ${(0.45 * dusk).toFixed(3)})`);
|
||
glow.addColorStop(1, "rgb(255 186 128 / 0)");
|
||
paint.fillStyle = glow;
|
||
paint.fillRect(0, 0, width, height);
|
||
}
|
||
const light = Ambience.starlight(sun.alt);
|
||
if (light > 0) {
|
||
const spin = Ambience.sidereal(ms) + settings.longitude;
|
||
paint.fillStyle = "#ffffff";
|
||
for (const star of sky.stars) {
|
||
const spot = Ambience.horizontal(spin - star.ra, star.dec, settings.latitude);
|
||
const at = spot.alt > 1 ? place(spot.alt, spot.az) : null;
|
||
if (at && Math.abs(at.lon) <= view.fov / 2 && inside(at, 4)) {
|
||
paint.globalAlpha = light * (0.25 + 0.75 * star.glow);
|
||
paint.beginPath();
|
||
paint.arc(at.x, at.y, 0.5 + star.glow * 1.2, 0, Math.PI * 2);
|
||
paint.fill();
|
||
}
|
||
}
|
||
paint.globalAlpha = 1;
|
||
}
|
||
const sunAt = place(sun.alt, sun.az);
|
||
if (seen(sun, sunAt)) {
|
||
const at = sunAt;
|
||
const halo = paint.createRadialGradient(at.x, at.y, 0, at.x, at.y, 90);
|
||
halo.addColorStop(0, "rgb(255 236 200 / 0.55)");
|
||
halo.addColorStop(1, "rgb(255 236 200 / 0)");
|
||
paint.fillStyle = halo;
|
||
paint.fillRect(at.x - 90, at.y - 90, 180, 180);
|
||
paint.fillStyle = Ambience.css(Ambience.mix([255, 170, 90], [255, 246, 220], Math.min(Math.max(sun.alt / 20, 0), 1)));
|
||
paint.beginPath();
|
||
paint.arc(at.x, at.y, 15, 0, Math.PI * 2);
|
||
paint.fill();
|
||
}
|
||
const moonAt = place(moon.alt, moon.az);
|
||
if (seen(moon, moonAt)) {
|
||
drawMoon(paint, moonAt, 12, moon);
|
||
}
|
||
const edge = Space.horizon(sight, 96);
|
||
const bottom = Math.max(height, ...edge.map((at) => at.y)) + 2;
|
||
const trace = (lift) => edge.forEach((at, index) => (index ? paint.lineTo(at.x, at.y + lift) : paint.moveTo(at.x, at.y + lift)));
|
||
const ground = paint.createLinearGradient(0, low.y, 0, low.y + height - sight.cy);
|
||
ground.addColorStop(0, Ambience.css(Ambience.mix(colors.bottom, [4, 6, 10], 0.72)));
|
||
ground.addColorStop(1, Ambience.css(Ambience.mix(colors.bottom, [4, 6, 10], 0.9)));
|
||
paint.fillStyle = ground;
|
||
paint.beginPath();
|
||
trace(0);
|
||
paint.lineTo(edge[edge.length - 1].x, bottom);
|
||
paint.lineTo(edge[0].x, bottom);
|
||
paint.closePath();
|
||
paint.fill();
|
||
paint.strokeStyle = "rgb(255 255 255 / 0.18)";
|
||
paint.lineWidth = 1;
|
||
paint.beginPath();
|
||
trace(0.5);
|
||
paint.stroke();
|
||
paint.fillStyle = "rgb(255 255 255 / 0.5)";
|
||
paint.font = "600 11px system-ui, sans-serif";
|
||
paint.textAlign = "center";
|
||
COMPASS.forEach(([label], index) => {
|
||
const at = place(0, index * 45);
|
||
if (Math.abs(at.lon) <= view.fov / 2 + 0.5) {
|
||
paint.fillText(label, Math.min(Math.max(at.x, 14), width - 14), at.y + 16);
|
||
}
|
||
});
|
||
if (space.glides.sky) {
|
||
redrawSky();
|
||
}
|
||
}
|
||
|
||
function redrawSky() {
|
||
window.cancelAnimationFrame(sky.frame);
|
||
sky.frame = window.requestAnimationFrame(drawSky);
|
||
}
|
||
|
||
function skyLook(on) {
|
||
const canvas = sky.canvas;
|
||
const button = document.getElementById("sky-look");
|
||
space.look = on;
|
||
document.body.classList.toggle("sky-look", on);
|
||
button.setAttribute("aria-pressed", String(on));
|
||
document.getElementById("sky-panel").hidden = !on;
|
||
if (on) {
|
||
canvas.removeAttribute("aria-hidden");
|
||
canvas.setAttribute("tabindex", "0");
|
||
canvas.setAttribute("role", "application");
|
||
canvas.setAttribute("aria-roledescription", "Himmel");
|
||
drawSky();
|
||
canvas.focus();
|
||
return;
|
||
}
|
||
for (const name of ["tabindex", "role", "aria-roledescription", "aria-label"]) {
|
||
canvas.removeAttribute(name);
|
||
}
|
||
canvas.setAttribute("aria-hidden", "true");
|
||
button.focus();
|
||
}
|
||
|
||
function skyPanel() {
|
||
const panel = document.getElementById("sky-panel");
|
||
const step = (text, label, run) => h("button", {type: "button", class: "btn small", title: label, "aria-label": label, on: {click: run}}, text);
|
||
space.compass = h("span", {class: "compass", "aria-live": "polite"});
|
||
panel.append(space.compass,
|
||
step("◀", "Blick nach links drehen (Pfeil links)", () => skyHooks.turnStep(-1, 0)),
|
||
step("▶", "Blick nach rechts drehen (Pfeil rechts)", () => skyHooks.turnStep(1, 0)),
|
||
step("▲", "Blick heben (Pfeil hoch)", () => skyHooks.turnStep(0, -1)),
|
||
step("▼", "Blick senken (Pfeil runter)", () => skyHooks.turnStep(0, 1)),
|
||
step("−", "Sichtfeld weiten (Minus)", () => skyHooks.zoomStep(1 / ZOOM)),
|
||
step("+", "Hineinzoomen (Plus)", () => skyHooks.zoomStep(ZOOM)),
|
||
step("⟲ Zurücksetzen", "Blick zurücksetzen: Süden, Horizont, Sichtfeld 180° (Taste 0)", () => resetView("sky")),
|
||
step("Fertig", "Himmel verlassen (Esc)", () => skyLook(false)));
|
||
steer(sky.canvas, "sky", skyHooks);
|
||
document.getElementById("sky-look").addEventListener("click", () => skyLook(!space.look));
|
||
}
|