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>
This commit is contained in:
1 parent
305ccdb1c2
commit
8ba45efd04
119 files changed
+13672
-4
No files matched your search
@@ -0,0 +1,145 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import math
|
||||
import os
|
||||
import time
|
||||
import unittest
|
||||
from unittest import mock
|
||||
|
||||
from hub_case import HubCase
|
||||
from hub_support import iso
|
||||
from kvb_hub import lifetimes, names, orbits
|
||||
|
||||
|
||||
class TestHubGraph(HubCase):
|
||||
def test_affinity_pulls_talking_agents_together(self):
|
||||
now = 1_000_000.0
|
||||
nodes = [{"id": name, "kind": kind, "parent": None} for name, kind in (("Operator", "operator"), ("Maintainer", "chair"), ("Alpha", "session"), ("Beta", "session"), ("Gamma", "session"))]
|
||||
talk = [{"ts": now - n * 60, "source": "Alpha" if n % 2 else "Beta", "targets": ["Beta" if n % 2 else "Alpha"], "class": "direct", "text": "x"} for n in range(20)]
|
||||
|
||||
def spot(layout: dict, name: str) -> tuple[float, float]:
|
||||
place = layout["orbit"][name]
|
||||
return place["radius"] * math.cos(place["angle"]), place["radius"] * math.sin(place["angle"])
|
||||
|
||||
layout = orbits.affinity(nodes, talk, {}, [], now, str)
|
||||
self.assertEqual(layout, orbits.affinity(nodes, talk, {}, [], now, str))
|
||||
self.assertEqual(set(layout["order"]), {"Maintainer", "Alpha", "Beta", "Gamma"})
|
||||
self.assertLess(math.dist(spot(layout, "Alpha"), spot(layout, "Beta")), math.dist(spot(layout, "Alpha"), spot(layout, "Gamma")))
|
||||
self.assertLess(math.dist(spot(layout, "Alpha"), spot(layout, "Beta")), math.dist(spot(layout, "Beta"), spot(layout, "Gamma")))
|
||||
self.assertLess(layout["orbit"]["Alpha"]["radius"], layout["orbit"]["Gamma"]["radius"])
|
||||
self.assertEqual(layout["orbit"]["Gamma"]["radius"], orbits.OUTER)
|
||||
old = [{**row, "ts": now - 86400} for row in talk]
|
||||
fresh = [{"ts": now - 60, "source": "Gamma", "targets": ["Beta"], "class": "direct", "text": "x"}] * 3
|
||||
later = orbits.affinity(nodes, old + fresh, {}, [], now, str)
|
||||
self.assertLess(math.dist(spot(later, "Beta"), spot(later, "Gamma")), math.dist(spot(later, "Alpha"), spot(later, "Beta")))
|
||||
shared = orbits.affinity(nodes, [], {"work/x": {"status": "running", "worker": "Alpha", "roles": {"Gamma": "C"}}}, [], now, str)
|
||||
self.assertLess(math.dist(spot(shared, "Alpha"), spot(shared, "Gamma")), math.dist(spot(shared, "Alpha"), spot(shared, "Beta")))
|
||||
self.assertIn("layout", self.snapshot()["graph"])
|
||||
|
||||
def test_the_track_samples_the_affinity_over_the_window_on_request(self):
|
||||
now = time.time()
|
||||
news = [{"ts": now - 3000 + n * 60, "from": "Beta" if n % 2 else "Alpha", "to": ["Alpha" if n % 2 else "Beta"], "text": "x"} for n in range(30)]
|
||||
news.append({"ts": now - 100, "from": "Gamma", "to": [], "text": "here"})
|
||||
self.write_council({"news.jsonl": self.jsonl(news)})
|
||||
self.assertEqual(self.snapshot()["graph"]["track"], [])
|
||||
status, _, body = self.get(f"/api/state?from={int(now - 3600)}&track=1")
|
||||
self.assertEqual(status, 200, body)
|
||||
state = json.loads(body)
|
||||
graph, track = state["graph"], state["graph"]["track"]
|
||||
stamps = [sample["ts"] for sample in track]
|
||||
self.assertEqual(len(track), orbits.TRACK_STEPS + 1)
|
||||
self.assertEqual((stamps[0], stamps[-1]), (state["first"], state["until"]))
|
||||
self.assertEqual(stamps, sorted(stamps))
|
||||
self.assertEqual(track[-1]["orbit"], graph["layout"]["orbit"])
|
||||
for sample in track:
|
||||
self.assertEqual(set(sample["orbit"]), set(graph["layout"]["order"]))
|
||||
keys = {node["label"]: node["id"] for node in graph["nodes"]}
|
||||
self.assertEqual([sample["orbit"][keys["Gamma"]]["radius"] for sample in track[:-1]], [orbits.OUTER] * orbits.TRACK_STEPS)
|
||||
self.assertLess(track[0]["orbit"][keys["Alpha"]]["radius"], orbits.OUTER)
|
||||
self.assertLess(track[-1]["orbit"][keys["Alpha"]]["radius"], track[-1]["orbit"][keys["Gamma"]]["radius"])
|
||||
for broken in ("0", "yes", "1&track=1"):
|
||||
with self.subTest(broken=broken):
|
||||
self.assertEqual(self.get(f"/api/state?track={broken}")[0], 400)
|
||||
|
||||
def test_a_rename_keeps_the_session_and_shows_the_name_of_its_moment(self):
|
||||
now = time.time()
|
||||
before = {"ts": now - 100, "from": "Skills", "to": ["Docs"], "text": "before"}
|
||||
claim = {"office": "work/s1", "session": "s1", "runtime": "claude", "name": "Skills", "since": iso(now - 200), "beat": now, "work": "x"}
|
||||
self.write_council({"work/s1/claim.json": claim, "news.jsonl": self.jsonl([before])})
|
||||
self.assertEqual([row["agent"] for row in self.events("kinds=radio")["events"]], ["Skills"])
|
||||
folder = self.projects / "-repo" / "s1"
|
||||
folder.mkdir(parents=True)
|
||||
title = folder / "custom-title.json"
|
||||
title.write_text(json.dumps({"customTitle": "IAM"}), encoding="utf-8")
|
||||
os.utime(title, (now - 50, now - 50))
|
||||
self.write_council({"news.jsonl": self.jsonl([before, {"ts": now - 20, "from": "IAM", "to": ["Docs"], "text": "after"}])})
|
||||
with mock.patch.object(names, "TITLE_SCAN", 0):
|
||||
rows = self.events("kinds=radio,rename")["events"]
|
||||
self.assertEqual([(row["kind"], row["session"], row["agent"], row["now"]) for row in rows], [
|
||||
("radio", "s1", "Skills", "IAM"),
|
||||
("rename", "s1", "IAM", "IAM"),
|
||||
("radio", "s1", "IAM", "IAM"),
|
||||
])
|
||||
self.assertEqual(rows[1]["title"], "Skills heißt jetzt IAM")
|
||||
self.assertAlmostEqual(rows[1]["ts"], now - 50, places=3)
|
||||
for name in ("Skills", "IAM"):
|
||||
with self.subTest(name=name):
|
||||
self.assertEqual([row["text"] for row in self.events(f"kinds=radio&agent={name}")["events"]], ["before", "after"])
|
||||
snapshot = self.snapshot()
|
||||
self.assertEqual([row["speech"] for row in snapshot["radio"]], ["Docs, Docs, this is Skills, before, over", "Docs, Docs, this is IAM, after, over"])
|
||||
self.assertEqual({row["voice"] for row in snapshot["radio"]}, {self.httpd.hub.speech.voice_of("s1")})
|
||||
live = {node["id"]: node for node in snapshot["graph"]["nodes"]}
|
||||
self.assertEqual((live["s1"]["label"], live["s1"]["names"]), ("IAM", ["Skills", "IAM"]))
|
||||
self.assertEqual([entry["name"] for entry in live["s1"]["timeline"]], ["Skills", "IAM"])
|
||||
for entry, since in zip(live["s1"]["timeline"], (now - 200, now - 50), strict=True):
|
||||
self.assertAlmostEqual(entry["since"], since, places=3)
|
||||
self.assertEqual(live["Operator"]["timeline"], [{"name": "Operator", "since": snapshot["until"]}])
|
||||
status, _, body = self.get(f"/api/state?from={int(now - 300)}&to={int(now - 80)}")
|
||||
self.assertEqual(status, 200, body)
|
||||
replay = {node["id"]: node for node in json.loads(body)["graph"]["nodes"]}
|
||||
self.assertEqual(replay["s1"]["label"], "Skills")
|
||||
self.assertEqual(replay["s1"]["timeline"], live["s1"]["timeline"])
|
||||
stored = json.loads((self.state / "names.json").read_text(encoding="utf-8"))["entries"]
|
||||
self.assertEqual([(entry["session"], entry["name"], entry["source"]) for entry in stored], [("s1", "Skills", "claim"), ("s1", "IAM", "title")])
|
||||
|
||||
def test_birth_and_death_dates_follow_the_transcripts(self):
|
||||
self.assertEqual((lifetimes.lifetime_stamp(1_700_000_000.0), lifetimes.lifetime_stamp(None)), ("2023-11-14T22:13Z", ""))
|
||||
now = time.time()
|
||||
self.claim("s1", "Skills")
|
||||
main = self.projects / "-repo" / "s1.jsonl"
|
||||
subagents = self.projects / "-repo" / "s1" / "subagents"
|
||||
subagents.mkdir(parents=True)
|
||||
|
||||
def line(ts: float) -> dict:
|
||||
return {"type": "user", "sessionId": "s1", "timestamp": iso(ts), "message": {"role": "user", "content": "x"}}
|
||||
|
||||
main.write_text(self.jsonl([line(now - 3 * 86400), line(now - 2 * 86400)]), encoding="utf-8")
|
||||
for ident, finished in (("abc", True), ("def", False)):
|
||||
(subagents / f"agent-{ident}.meta.json").write_text(json.dumps({"agentType": "Explore", "description": ident, "spawnDepth": 1, "finished": finished}), encoding="utf-8")
|
||||
(subagents / f"agent-{ident}.jsonl").write_text(self.jsonl([line(now - 3 * 86400 + 60), line(now - 3 * 86400 + 120)]), encoding="utf-8")
|
||||
|
||||
def life() -> dict:
|
||||
status, _, body = self.get("/api/subagents?session=s1")
|
||||
self.assertEqual(status, 200, body)
|
||||
return json.loads(body)
|
||||
|
||||
born, ended = lifetimes.lifetime_stamp(now - 3 * 86400), lifetimes.lifetime_stamp(now - 2 * 86400)
|
||||
sub_born, sub_died = lifetimes.lifetime_stamp(now - 3 * 86400 + 60), lifetimes.lifetime_stamp(now - 3 * 86400 + 120)
|
||||
data = life()
|
||||
self.assertEqual((data["born"], data["died"], data["until"]), (born, "", None))
|
||||
self.assertAlmostEqual(data["since"], now - 3 * 86400, places=3)
|
||||
self.assertEqual([(row["id"], row["born"], row["died"]) for row in data["subagents"]], [("abc", sub_born, sub_died), ("def", sub_born, "")])
|
||||
self.assertAlmostEqual(data["subagents"][0]["until"], now - 3 * 86400 + 120, places=3)
|
||||
self.assertEqual(data["subagents"][1]["until"], None)
|
||||
for row in data["subagents"]:
|
||||
self.assertAlmostEqual(row["since"], now - 3 * 86400 + 60, places=3)
|
||||
os.utime(main, (now - 2 * 86400, now - 2 * 86400))
|
||||
self.assertEqual(life()["died"], "")
|
||||
self.write_council({"work/s1/claim.json": {"office": "work/s1", "session": "s1", "runtime": "claude", "name": "Skills", "since": iso(now - 4 * 86400), "beat": now - 2 * 86400, "work": "x"}})
|
||||
self.assertEqual((life()["born"], life()["died"]), (born, ended))
|
||||
self.assertAlmostEqual(life()["until"], now - 2 * 86400, places=3)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in new issue
Block a user