Files
skills/tests/test_hub_mcp.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

98 lines
5.9 KiB
Python

from __future__ import annotations
import json
import unittest
from hub_case import HubCase
from hub_support import PAGE, SCORE, TREE
from kvb_hub import mcp
TOOLS = {"review_request", "review_status", "review_wait", "review_list", "decision_request", "decision_wait", "operator_message"}
class TestHubMcp(HubCase):
def test_mcp_round_trip(self):
init = self.rpc("initialize", {"protocolVersion": "2025-06-18", "capabilities": {}, "clientInfo": {"name": "t", "version": "1"}})
self.assertEqual(init["result"]["protocolVersion"], "2025-06-18")
self.assertIn("tools", init["result"]["capabilities"])
self.assertEqual(init["result"]["serverInfo"]["name"], "kvb-hub")
self.assertEqual(self.rpc("initialize", {"protocolVersion": "1999-01-01"})["result"]["protocolVersion"], mcp.PROTOCOLS[0])
status, _, body = self.post_mcp({"jsonrpc": "2.0", "method": "notifications/initialized"}, {})
self.assertEqual((status, body), (202, b""))
self.assertEqual(self.rpc("ping", {})["result"], {})
tools = self.rpc("tools/list", {})["result"]["tools"]
self.assertEqual({t["name"] for t in tools}, TOOLS)
for entry in tools:
with self.subTest(tool=entry["name"]):
schema = entry["inputSchema"]
self.assertEqual((schema["type"], schema["additionalProperties"]), ("object", False))
self.assertEqual(sorted(schema["required"]), sorted(schema["properties"]))
review_id = self.request("feat: hub", "table")
self.assertRegex(review_id, r"^[0-9a-f]{16}$")
state = self.tool("review_status", {"id": review_id})
self.assertEqual(json.loads(state["content"][0]["text"]), state["structuredContent"])
self.assertEqual(state["structuredContent"], {"id": review_id, "status": "pending", "tree": TREE, "decided": None})
listed = self.tool("review_list", {})["structuredContent"]["reviews"]
self.assertEqual([(r["id"], r["title"], r["tree"]) for r in listed], [(review_id, "feat: hub", TREE)])
self.assertEqual(self.rpc("resources/list", {})["error"]["code"], -32601)
self.assertEqual(self.rpc("tools/call", {"name": "review_approve", "arguments": {"id": review_id}})["error"]["code"], -32602)
status, _, body = self.call("POST", "/mcp", b"{not json", {"Host": self.host, "Content-Type": "application/json"})
self.assertEqual((status, json.loads(body)["error"]["code"]), (400, -32700))
def test_tool_arguments_are_validated(self):
good = self.arguments("feat: x", "", self.page("bundle.html", PAGE))
for name, arguments in (
("review_request", {k: v for k, v in good.items() if k != "notes"}),
("review_request", {k: v for k, v in good.items() if k != "repo"}),
("review_request", {**good, "repo": "relative/.git"}),
("review_request", {**good, "extra": "1"}),
("review_request", {**good, "tree": "HEAD"}),
("review_request", {**good, "title": " "}),
("review_request", {**good, "title": "two\nlines"}),
("review_request", {**good, "worktree": "relative/path"}),
("review_request", {**good, "branch": "has space"}),
("review_request", {**good, "notes": 5}),
("review_request", {k: v for k, v in good.items() if k != "gate"}),
("review_request", {k: v for k, v in good.items() if k != "score"}),
("review_request", {**good, "gate": "maybe"}),
("review_request", {**good, "score": 5}),
("review_request", {**good, "score": "x" * 501}),
("review_status", {"id": "../../etc"}),
("review_status", {"id": "0123456789abcdef"}),
("review_wait", {"id": "0123456789abcdef", "timeout_seconds": 1}),
("review_list", {"extra": 1}),
):
with self.subTest(name=name, arguments=arguments):
self.assertTrue(self.tool(name, arguments)["isError"])
review_id = self.request("feat: x", "")
for timeout in (-1, True, "5", None, float("nan"), float("inf")):
with self.subTest(timeout=timeout):
self.assertTrue(self.tool("review_wait", {"id": review_id, "timeout_seconds": timeout})["isError"])
bad = self.rpc("tools/call", {"name": "review_status", "arguments": ["x"]})
self.assertEqual(bad["error"]["code"], -32602)
def test_mcp_rejects_foreign_origins_hosts_and_simple_requests(self):
ping = {"jsonrpc": "2.0", "id": 1, "method": "ping"}
self.assertEqual(self.post_mcp(ping, {"Origin": "http://evil.example"})[0], 403)
self.assertEqual(self.post_mcp(ping, {"Host": "evil.example"})[0], 403)
self.assertEqual(self.post_mcp(ping, {"Content-Type": "text/plain"})[0], 415)
self.assertEqual(self.post_mcp(ping, {"Origin": f"http://{self.host}"})[0], 200)
self.assertEqual(self.get("/mcp")[0], 405)
def test_mcp_requires_gate_score_and_recommended(self):
tools = {entry["name"]: entry["inputSchema"] for entry in self.rpc("tools/list", {})["result"]["tools"]}
self.assertEqual(tools["review_request"]["properties"]["gate"]["enum"], ["pass", "fail"])
self.assertTrue({"gate", "score"} <= set(tools["review_request"]["required"]))
self.assertIn("recommended", tools["decision_request"]["required"])
review_id = self.request("feat: gated", "")
decision_id = self.decision("Wait")
inbox = {item["id"]: item for item in self.snapshot()["inbox"]}
self.assertEqual((inbox[review_id]["gate"], inbox[review_id]["score"], inbox[review_id]["green"]), ("pass", SCORE, True))
self.assertEqual((inbox[decision_id]["recommended"], inbox[decision_id]["green"]), ("Wait", True))
plain = self.decision("")
self.assertFalse(next(item for item in self.snapshot()["inbox"] if item["id"] == plain)["green"])
if __name__ == "__main__":
unittest.main()