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

140 lines
5.6 KiB
Python

from __future__ import annotations
import itertools
import json
import math
import re
import threading
from pathlib import Path
from .council import HUMAN, agent
from .events import event
from .records import read_json, write_atomic
from .transcripts import STREAM_AGE
PSEUDO = "name:"
TITLE_SCAN = 10.0
SESSION_ID = re.compile(r"[A-Za-z0-9_-]{1,128}")
class Directory:
def __init__(self, entries: list[dict]) -> None:
self.by_session: dict[str, list[dict]] = {}
self.by_name: dict[str, list[str]] = {}
for entry in entries:
self.by_session.setdefault(entry["session"], []).append(entry)
holders = self.by_name.setdefault(entry["name"], [])
if entry["session"] in holders:
holders.remove(entry["session"])
holders.append(entry["session"])
def at(self, session: str, ts: float) -> str:
own = self.by_session.get(session)
if not own:
return ""
valid = [entry for entry in own if entry["first_seen"] <= ts]
return (valid[-1] if valid else own[0])["name"]
def now(self, session: str) -> str:
own = self.by_session.get(session)
return own[-1]["name"] if own else ""
def find(self, name: str, ts: float) -> str:
holders = self.by_name.get(name, [])
real = [session for session in holders if not session.startswith(PSEUDO)]
valid = [session for session in real if self.at(session, ts) == name]
chosen = valid or real or holders
return chosen[-1] if chosen else ""
def key(self, name: str, ts: float) -> str:
if not name or name == HUMAN:
return name
return self.find(name, ts) or f"{PSEUDO}{name}"
def label(self, key: str, ts: float) -> str:
return self.at(key, ts) or key.removeprefix(PSEUDO)
def names(self, key: str) -> list[str]:
return list(dict.fromkeys(entry["name"] for entry in self.by_session.get(key, [])))
def timeline(self, key: str) -> list[dict]:
return [{"name": entry["name"], "since": entry["first_seen"]} for entry in self.by_session.get(key, [])]
def aliases(self, name: str) -> set[str]:
return set(self.by_name.get(name, []))
def renames(self, begin: float, end: float) -> list[dict]:
found = []
for session, own in self.by_session.items():
for before, after in itertools.pairwise(own):
if before["name"] != after["name"] and begin <= after["first_seen"] <= end:
found.append(event({
"id": f"rename:{session}:{after['first_seen']}", "ts": after["first_seen"], "kind": "rename", "agent": after["name"],
"now": self.now(session), "session": session, "title": f"{before['name']} heißt jetzt {after['name']}",
"text": f"Quelle: {after['source']}", "ref": {"before": before["name"], "after": after["name"], "source": after["source"]},
}))
return found
class Names:
def __init__(self, file: Path) -> None:
self.file = file
self.lock = threading.Lock()
self.titled = -math.inf
data = read_json(file)
data = data if isinstance(data, dict) else {}
entries, observed = data.get("entries"), data.get("observed")
keys = {"session", "name", "first_seen", "source"}
self.entries = [entry for entry in entries if isinstance(entry, dict) and set(entry) == keys] if isinstance(entries, list) else []
self.observed: dict[str, str] = observed if isinstance(observed, dict) else {}
def observe(self, sightings: list[tuple[str, str, float | None, str]], now: float) -> None:
with self.lock:
changed = False
for session, name, seen, source in sightings:
key = f"{session}\x00{source}"
if not session or not name or self.observed.get(key) == name:
continue
self.observed[key] = name
changed = True
history = [entry for entry in self.entries if entry["session"] == session]
if history and history[-1]["name"] == name:
continue
floor = history[-1]["first_seen"] if history else -math.inf
first = seen if seen is not None and seen > floor else max(now, floor)
self.entries.append({"session": session, "name": name, "first_seen": first, "source": source})
if changed:
write_atomic(self.file, json.dumps({"entries": self.entries, "observed": self.observed}, sort_keys=True).encode("utf-8"))
def listed(self) -> list[dict]:
with self.lock:
return [dict(entry) for entry in self.entries]
def due(self, now: float) -> bool:
with self.lock:
if now - self.titled < TITLE_SCAN:
return False
self.titled = now
return True
def title_sightings(root: Path, claimed: set[str], now: float) -> list[tuple[str, str, float | None, str]]:
try:
paths = sorted(root.glob("*/*/custom-title.json"))
except OSError:
return []
found = []
for path in paths:
session = path.parent.name
try:
changed = path.stat().st_mtime
except OSError:
continue
if not SESSION_ID.fullmatch(session) or (session not in claimed and changed < now - STREAM_AGE):
continue
data = read_json(path)
name = agent(data.get("customTitle") if isinstance(data, dict) else None)
if name:
found.append((session, name, changed, "title"))
return found