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>
79 lines
4.0 KiB
Python
79 lines
4.0 KiB
Python
from __future__ import annotations
|
|
|
|
import fnmatch
|
|
import time
|
|
|
|
from .errors import Conflict, Missing, ToolError
|
|
from .fields import COMMAND_LIMIT, PATH_LIMIT, bounded, decode, expect
|
|
from .signed import signing
|
|
|
|
RULE_KINDS = ("ask", "review", "decision")
|
|
|
|
|
|
def rule_matches(rule: dict, kind_name: str, subject: dict, now: float) -> bool:
|
|
if not rule["enabled"] or rule["expires"] <= now or rule["kind"] != kind_name:
|
|
return False
|
|
if kind_name == "decision":
|
|
return subject["recommended"] != "" and fnmatch.fnmatchcase(subject["agent"], rule["pattern"])
|
|
proof = rule.get("proof")
|
|
return proof is not None and signing.rule_allows(proof["payload"], subject["item"], subject["gate"], now)
|
|
|
|
|
|
def rule_fields(raw: dict, now: float) -> dict:
|
|
kind_name, expires = raw["kind"], raw["expires"]
|
|
if kind_name not in RULE_KINDS:
|
|
raise ToolError(f"kind must be one of {', '.join(RULE_KINDS)}")
|
|
tool_name = bounded(raw, "tool", 200).strip()
|
|
if (kind_name == "ask") != bool(tool_name):
|
|
raise ToolError("an ask rule names a tool; review and decision rules leave tool empty")
|
|
pattern, scope = bounded(raw, "pattern", COMMAND_LIMIT), bounded(raw, "scope", PATH_LIMIT)
|
|
if not pattern.strip() or not scope.strip() or "\n" in pattern or "\n" in scope:
|
|
raise ToolError("pattern and scope must be one non-empty line each")
|
|
if isinstance(expires, bool) or not isinstance(expires, int | float) or not now + 1 < expires <= now + signing.RULE_LIFETIME:
|
|
raise ToolError("expires must be a moment within the next 366 days")
|
|
return {"kind": kind_name, "tool": tool_name, "pattern": pattern, "scope": scope, "expires": int(expires)}
|
|
|
|
|
|
class Rules:
|
|
def sign_rule(self, body: bytes) -> dict:
|
|
payload = decode(body)
|
|
expect(payload, ("kind", "tool", "pattern", "scope", "expires"))
|
|
fields = rule_fields(payload, time.time())
|
|
rule = signing.rule_payload(self.rules.fresh(), [fields["kind"]], fields["tool"], fields["pattern"], fields["scope"], fields["expires"])
|
|
credentials = [key["id"] for key in self.keys()]
|
|
return {"payload": rule, "challenge": signing.challenge(rule), "sign": fields["kind"] != "decision", "credentials": credentials}
|
|
|
|
def add_rule(self, body: bytes) -> dict:
|
|
payload = decode(body)
|
|
expect(payload, ("rule", "assertion", "enabled"))
|
|
rule, assertion = payload["rule"], payload["assertion"]
|
|
now = time.time()
|
|
try:
|
|
signing.checked_rule(rule, now)
|
|
except signing.Refused as error:
|
|
raise ToolError(f"rule: {error}") from None
|
|
if len(rule["kinds"]) != 1:
|
|
raise ToolError("a rule names exactly one kind")
|
|
fields = rule_fields({**{name: rule[name] for name in ("tool", "pattern", "scope", "expires")}, "kind": rule["kinds"][0]}, now)
|
|
if fields["tool"] != rule["tool"]:
|
|
raise ToolError("tool must not carry surrounding blanks")
|
|
signed = fields["kind"] != "decision"
|
|
if not (isinstance(assertion, dict) if signed else assertion is None):
|
|
raise ToolError("an ask or review rule carries the passkey assertion; a decision rule carries null")
|
|
if not isinstance(payload["enabled"], bool):
|
|
raise ToolError("enabled must be true or false")
|
|
if self.rules.get(rule["id"]) is not None:
|
|
raise Conflict({"id": rule["id"], "status": "exists"})
|
|
proof = {"payload": rule, "assertion": assertion} if signed else None
|
|
return self.rules.insert(rule["id"], {**fields, "expires": float(fields["expires"]), "enabled": payload["enabled"], "created_ts": now, "proof": proof})
|
|
|
|
def rule_action(self, rule_id: str, verb: str) -> dict:
|
|
if verb == "delete":
|
|
if not self.rules.remove(rule_id):
|
|
raise Missing(rule_id)
|
|
return {"id": rule_id, "deleted": True}
|
|
record = self.rules.change(rule_id, {"enabled": verb == "enable"})
|
|
if record is None:
|
|
raise Missing(rule_id)
|
|
return record
|