Files
skills/hub/kvb_hub/council.py
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

116 lines
3.9 KiB
Python

from __future__ import annotations
from pathlib import Path
from .records import epoch, read_json, shown
NEWS = "news.jsonl"
HUMAN = "Operator"
SESSION = " session"
ALARMS = (("MAYDAY", "mayday"), ("PAN-PAN", "pan-pan"))
CLASSES = ("broadcast", "direct", "security", "pan-pan", "mayday")
MODES = ("evening", "sunset")
RADIO_LIMIT = 5000
GRANT_LIMIT = 50
MAYDAY_ACTIVE = 7200
def agent(name: object) -> str:
if not isinstance(name, str):
return ""
text = name.strip()
if text.lower().endswith(SESSION):
return text[: -len(SESSION)].strip()
return text
def subagent(name: str) -> bool:
return name.lower().startswith("subagent")
def ordered(names: set[str]) -> list[str]:
return sorted(names, key=lambda name: (name.lower(), name))
def kind(row: dict, names: list[str]) -> str:
if row.get("class") in CLASSES:
return row["class"]
text = row.get("text")
for prefix, alarm in ALARMS:
if isinstance(text, str) and text.lstrip().startswith(prefix):
return alarm
return "direct" if names else "broadcast"
def radio_row(row: dict, ts: float) -> dict:
to = row.get("to")
names = [name for name in map(agent, to) if name] if isinstance(to, list) else []
return {"ts": ts, "from": agent(row.get("from")), "to": names, "class": kind(row, names), "text": shown(row.get("text"))}
def radio_rows(rows: list[dict], begin: float, end: float) -> list[dict]:
found = [radio_row(row, ts) for row in rows if (ts := epoch(row.get("ts"))) is not None and begin <= ts <= end]
return sorted(found, key=lambda row: row["ts"])[-RADIO_LIMIT:]
def claim_row(data: dict) -> dict:
name = data.get("name")
return {
"office": shown(data.get("office")),
"agent": agent(name),
"address": name.strip() if isinstance(name, str) else "",
"session": shown(data.get("session")),
"runtime": shown(data.get("runtime")),
"since": shown(data.get("since")),
"beat": epoch(data.get("beat")),
"work": shown(data.get("work")),
}
def read_claims(council: Path) -> list[dict]:
paths = sorted([*council.glob("cluster/*/claim.json"), *council.glob("work/*/claim.json")])
return [claim_row(data) for data in map(read_json, paths) if isinstance(data, dict)]
def sessions(claims: list[dict]) -> dict[str, str]:
return {claim["session"]: claim["agent"] for claim in claims if claim["agent"] and claim["session"] != "-"}
def home(workdir: object, homes: dict[str, str]) -> str:
if not isinstance(workdir, str):
return ""
matches = [path for path in homes if workdir == path or workdir.startswith(f"{path}/")]
return homes[max(matches, key=len)] if matches else ""
def grants(rows: list[dict], window: tuple[float, float], claims: list[dict], tasks: dict[str, dict]) -> list[dict]:
begin, end = window
homes = {}
for claim in claims:
workdir = tasks.get(claim["office"], {}).get("workdir")
if claim["agent"] and isinstance(workdir, str) and workdir.rstrip("/"):
homes[workdir.rstrip("/")] = claim["agent"]
found = []
for row in rows:
start = epoch(row.get("start"))
holder = home(row.get("workdir"), homes)
if start is not None and begin <= start <= end and holder:
found.append({"ts": start, "to": holder, "kind": shown(row.get("kind")), "id": shown(row.get("id"))})
return found[-GRANT_LIMIT:]
def night_mode(council: Path) -> str:
data = read_json(council / "night.json")
mode = data.get("mode") if isinstance(data, dict) else None
return mode if mode in MODES else "day"
def silence_of(council: Path, now: float) -> dict | None:
data = read_json(council / "radio-silence.json")
if not isinstance(data, dict):
return None
until = epoch(data.get("until"))
if until is None or until <= now:
return None
return {"by": shown(data.get("by")), "text": shown(data.get("text")), "until": until}