Files
skills/hub/templates/space-paint.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

192 lines
6.8 KiB
JavaScript

const SpacePaint = (() => {
"use strict";
const {clamp, project, depthSort} = typeof SpaceView === "object" ? SpaceView : require("./space-view.js");
const LOOKS = {
guide: ["line", 1, 1],
"line:operator": ["accent", 2.4, 0.9],
"line:chair": ["accent", 2, 0.85],
"line:session": ["muted", 1.6, 0.8],
"line:subagent": ["violet", 1.2, 0.75],
"line:twig": ["violet", 1, 0.6],
"line:selected": ["accent", 3.2, 1],
"arc:direct": ["direct", 1.3, 0.6],
"arc:broadcast": ["broadcast", 1.2, 0.55],
"arc:alarm": ["alarm", 2, 0.9],
"arc:security": ["security", 1.5, 0.75],
"arc:grant": ["grant", 1.6, 0.75],
"arc:spawn": ["violet", 1.4, 0.8],
};
const DISCS = {operator: 9, chair: 7, session: 5.5, subagent: 3.5, twig: 3};
const RIM = 330;
const HALO = 14;
const SLICE = 25;
const ROUND = 48;
const MARGIN = 40;
function ring(center, radius, count) {
return Array.from({length: count}, (_, index) => {
const angle = (2 * Math.PI * index) / count;
return {x: center.x + radius * Math.cos(angle), y: center.y + radius * Math.sin(angle), z: center.z};
});
}
function arcPoints(arc) {
return arc.kind === "broadcast" ? ring(arc.from, HALO, 12) : [arc.from, arc.via, arc.to];
}
function visible(cam, marks) {
let [left, right, upper, lower] = [Infinity, -Infinity, Infinity, -Infinity];
for (const mark of marks) {
if (mark === null) {
return false;
}
left = Math.min(left, mark.x);
right = Math.max(right, mark.x);
upper = Math.min(upper, mark.y);
lower = Math.max(lower, mark.y);
}
return right >= -MARGIN && left <= cam.width + MARGIN && lower >= -MARGIN && upper <= cam.height + MARGIN;
}
function mean(marks) {
return marks.reduce((sum, mark) => sum + mark.depth, 0) / marks.length;
}
function pen(ctx, colors, look) {
const [role, width, alpha] = LOOKS[look];
ctx.strokeStyle = colors[role];
ctx.lineWidth = width;
ctx.globalAlpha = alpha;
}
function paint(ctx, built, cam, style) {
const colors = style.colors;
const stats = {items: 0, arcs: 0, culled: 0, batches: 0, labels: 0};
const buckets = new Map(Object.keys(LOOKS).map((look) => [look, []]));
const others = [];
const marksOf = (points) => points.map((entry) => project(cam, entry));
const stroke = (look, marks, type, closed) => {
if (!visible(cam, marks)) {
stats.culled += 1;
return false;
}
buckets.get(look).push({type, look, marks, closed, depth: Math.round(mean(marks) / SLICE) * SLICE});
return true;
};
const label = (at, text, tone, strong) => {
const mark = project(cam, at);
if (mark && visible(cam, [mark])) {
others.push({type: "label", mark, text, tone, strong, depth: mark.depth - 1});
}
};
for (const guide of built.guides) {
const marks = marksOf(ring({x: 0, y: 0, z: guide.z}, RIM, ROUND));
if (stroke("guide", marks, "path", true)) {
label({x: 0, y: -RIM, z: guide.z}, built.axis === "time" ? style.format(guide.ts) : `${style.level} ${guide.level}`, "muted", false);
}
}
if (built.plane) {
const marks = marksOf(ring({x: 0, y: 0, z: built.plane.z}, RIM, ROUND));
if (visible(cam, marks)) {
others.push({type: "plane", marks, depth: mean(marks)});
label({x: RIM * Math.SQRT1_2, y: -RIM * Math.SQRT1_2, z: built.plane.z}, `${style.cursor} ${style.format(built.plane.ts)}`, "accent", true);
}
}
built.lines.forEach((line) => {
const marks = marksOf(line.points);
const look = line.node !== null && line.node === style.selected ? "line:selected" : `line:${line.kind}`;
for (let index = 1; index < marks.length; index += 1) {
stroke(look, [marks[index - 1], marks[index]], "path", false);
}
const tail = marks[marks.length - 1];
if ((line.alive || built.axis !== "time") && tail && visible(cam, [tail])) {
others.push({type: "disc", mark: tail, look, radius: DISCS[line.kind] * clamp(tail.size, 0.5, 2.5), dim: !line.alive, depth: tail.depth - 0.5});
}
});
for (const arc of built.arcs) {
if (stroke(`arc:${arc.tone}`, marksOf(arcPoints(arc)), arc.kind === "broadcast" ? "path" : "curve", arc.kind === "broadcast")) {
stats.arcs += 1;
}
}
for (const entry of built.labels) {
label(entry.at, entry.text, "text", built.lines[entry.line].node !== null && built.lines[entry.line].node === style.selected);
}
const items = depthSort([...[...buckets.values()].flat(), ...others]);
stats.items = items.length;
ctx.clearRect(0, 0, cam.width, cam.height);
ctx.lineCap = "round";
ctx.lineJoin = "round";
ctx.textAlign = "center";
let open = "";
const flush = () => {
if (open) {
ctx.stroke();
stats.batches += 1;
open = "";
}
};
for (const item of items) {
const marks = item.marks;
if (item.type === "path" || item.type === "curve") {
if (item.look !== open) {
flush();
pen(ctx, colors, item.look);
ctx.beginPath();
open = item.look;
}
ctx.moveTo(marks[0].x, marks[0].y);
if (item.type === "curve") {
ctx.quadraticCurveTo(marks[1].x, marks[1].y, marks[2].x, marks[2].y);
} else {
for (let index = 1; index < marks.length; index += 1) {
ctx.lineTo(marks[index].x, marks[index].y);
}
if (item.closed) {
ctx.closePath();
}
}
continue;
}
flush();
if (item.type === "plane") {
ctx.beginPath();
marks.forEach((mark, index) => (index ? ctx.lineTo(mark.x, mark.y) : ctx.moveTo(mark.x, mark.y)));
ctx.closePath();
ctx.globalAlpha = 0.1;
ctx.fillStyle = colors.accent;
ctx.fill();
ctx.globalAlpha = 0.55;
ctx.strokeStyle = colors.accent;
ctx.lineWidth = 1.2;
ctx.stroke();
} else if (item.type === "disc") {
ctx.beginPath();
ctx.arc(item.mark.x, item.mark.y, item.radius, 0, 2 * Math.PI);
ctx.globalAlpha = item.dim ? 0.5 : 1;
ctx.fillStyle = colors.panel;
ctx.fill();
pen(ctx, colors, item.look);
ctx.globalAlpha = item.dim ? 0.5 : 1;
ctx.stroke();
} else {
stats.labels += 1;
ctx.globalAlpha = 1;
ctx.font = item.strong ? style.strong : style.font;
ctx.lineWidth = 4;
ctx.strokeStyle = colors.panel;
ctx.strokeText(item.text, item.mark.x, item.mark.y - 10);
ctx.fillStyle = colors[item.tone];
ctx.fillText(item.text, item.mark.x, item.mark.y - 10);
}
}
flush();
ctx.globalAlpha = 1;
return stats;
}
return {arcPoints, paint};
})();
if (typeof module === "object" && module.exports) {
module.exports = SpacePaint;
}