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>
194 lines
13 KiB
Python
194 lines
13 KiB
Python
from __future__ import annotations
|
|
|
|
import dataclasses
|
|
import hashlib
|
|
import io
|
|
import json
|
|
import os
|
|
import subprocess
|
|
import sys
|
|
import unittest
|
|
import wave
|
|
from unittest import mock
|
|
|
|
from hub_case import HubCase
|
|
from hub_engines import OPERATOR_VOICE, VOICES, log_rows, wav_bytes
|
|
from hub_support import HOST_STATE, HUB, server
|
|
from kvb_hub import council, engines, exchange, handler, phrasing
|
|
|
|
|
|
class TestHubSpeech(HubCase):
|
|
def test_voices_come_from_the_models_and_stick_to_their_session(self):
|
|
speech = self.httpd.hub.speech
|
|
self.assertEqual(list(speech.voices), VOICES)
|
|
self.assertEqual((speech.voices["de_DE-solo-medium:0"], speech.voices["en_US-test-high:3"]), (("de_DE-solo-medium", None), ("en_US-test-high", 3)))
|
|
pool = [voice for voice in VOICES if voice != OPERATOR_VOICE]
|
|
self.assertEqual((speech.pool, speech.operator), (pool, OPERATOR_VOICE))
|
|
keys = [f"session-{n}" for n in range(40)] + ["s1", "name:Docs"]
|
|
picked = [engines.voice_of(key, pool, OPERATOR_VOICE) for key in keys]
|
|
self.assertEqual(picked, [pool[int(hashlib.sha256(key.encode()).hexdigest(), 16) % len(pool)] for key in keys])
|
|
self.assertEqual(picked, [speech.voice_of(key) for key in keys])
|
|
self.assertEqual(picked, [engines.voice_of(key, list(pool), OPERATOR_VOICE) for key in keys])
|
|
self.assertEqual(set(picked), set(pool))
|
|
self.assertEqual(speech.voice_of("Operator"), OPERATOR_VOICE)
|
|
self.assertEqual(engines.voice_of("s1", [], OPERATOR_VOICE), OPERATOR_VOICE)
|
|
self.assertEqual(engines.voice_of("s1", [], ""), "")
|
|
with self.assertRaises(server.ToolError):
|
|
engines.Speech(dataclasses.replace(self.engines, operator_voice="en_US-test-high:4"), self.state / "other")
|
|
silent = engines.Speech(dataclasses.replace(self.engines, voices=self.outside, operator_voice="anything"), self.state / "silent")
|
|
self.assertEqual((silent.speaks(), silent.voice_of("s1"), silent.describe()["operator"], silent.describe()["tts"]), (False, "", "", False))
|
|
deaf = engines.Speech(dataclasses.replace(self.engines, whisper=self.outside / "missing"), self.state / "deaf")
|
|
self.assertFalse(deaf.hears())
|
|
|
|
def test_speech_output_is_guarded_whitelisted_capped_and_cached(self):
|
|
good = self.guard(self.host)
|
|
calls = self.engines.scratch / "piper.jsonl"
|
|
cache = self.state / "tts"
|
|
request = {"text": "all stations, this is Docs, board, out", "voice": "en_US-test-high:2"}
|
|
for headers in ({k: v for k, v in good.items() if k != "X-Kvb-Hub"}, {k: v for k, v in good.items() if k != "Origin"}, {**good, "Origin": "http://evil.example"}):
|
|
with self.subTest(headers=headers):
|
|
self.assertEqual(self.post_json("/api/tts", request, headers)[0], 403)
|
|
for broken in (
|
|
{**request, "voice": "en_US-test-high:4"},
|
|
{**request, "voice": "zero:0"},
|
|
{**request, "voice": "bad name!:0"},
|
|
{**request, "voice": "broken:0"},
|
|
{**request, "voice": ["en_US-test-high:2"]},
|
|
{**request, "text": "x" * (phrasing.SPEECH_LIMIT + 1)},
|
|
{**request, "text": " \n\t "},
|
|
{**request, "text": "bell \x07 here"},
|
|
{**request, "text": 7},
|
|
{**request, "extra": 1},
|
|
{"text": request["text"]},
|
|
):
|
|
with self.subTest(broken=broken):
|
|
self.assertEqual(self.post_json("/api/tts", broken, good)[0], 400)
|
|
self.assertEqual(log_rows(calls), [])
|
|
status, headers, body = self.post_json("/api/tts", request, good)
|
|
self.assertEqual((status, headers["content-type"], headers["content-security-policy"]), (200, "audio/wav", exchange.PLAIN_CSP), body)
|
|
with wave.open(io.BytesIO(body)) as reader:
|
|
self.assertEqual((reader.getnchannels(), reader.getframerate(), reader.getnframes()), (1, 22050, len(request["text"]) + 1))
|
|
key = hashlib.sha256(f"{request['voice']}\n{request['text']}".encode()).hexdigest()
|
|
self.assertEqual(sorted(path.name for path in cache.iterdir()), [f"{key}.wav"])
|
|
self.assertEqual(self.post_json("/api/tts", {**request, "text": " all stations,\nthis is Docs, board, out "}, good)[2], body)
|
|
self.assertEqual(self.post_json("/api/tts", {**request, "text": "x" * phrasing.SPEECH_LIMIT}, good)[0], 200)
|
|
rows = log_rows(calls)
|
|
model = self.engines.voices / "en_US-test-high.onnx"
|
|
self.assertEqual(len(rows), 2)
|
|
self.assertEqual(rows[0]["argv"][:5], ["--model", str(model), "--config", f"{model}.json", "--output-file"])
|
|
self.assertEqual(rows[0]["argv"][6:], ["--speaker", "2"])
|
|
self.assertTrue(rows[0]["argv"][5].startswith(f"{cache}/.{key}-"))
|
|
self.assertEqual((rows[0]["text"], rows[0]["tmp"], rows[0]["cwd"]), (f"{request['text']}\n", str(self.engines.scratch), str(self.engines.scratch)))
|
|
self.assertEqual(self.post_json("/api/tts", {"text": "solo", "voice": "de_DE-solo-medium:0"}, good)[0], 200)
|
|
self.assertNotIn("--speaker", log_rows(calls)[-1]["argv"])
|
|
status, _, body = self.post_json("/api/tts", {**request, "text": "FAIL now"}, good)
|
|
self.assertEqual(status, 502, body)
|
|
self.assertIn(b"exit code 3", body)
|
|
self.assertEqual([path.name for path in cache.iterdir() if path.name.startswith(".")], [])
|
|
with mock.patch.object(engines, "TTS_KEEP", 2):
|
|
for number in range(3):
|
|
self.assertEqual(self.post_json("/api/tts", {**request, "text": f"line {number}"}, good)[0], 200)
|
|
kept = {hashlib.sha256(f"{request['voice']}\nline {number}".encode()).hexdigest() + ".wav" for number in (1, 2)}
|
|
self.assertEqual({path.name for path in cache.iterdir()}, kept)
|
|
with mock.patch.object(self.httpd.hub.speech, "voices", {}):
|
|
self.assertEqual(self.post_json("/api/tts", request, good)[0], 503)
|
|
self.assertEqual(self.scripts.calls, [])
|
|
|
|
def test_speech_input_checks_the_guard_type_size_and_format(self):
|
|
good = self.guard(self.host)
|
|
calls = self.engines.scratch / "whisper.jsonl"
|
|
clip = wav_bytes(1, 2, engines.STT_RATE, b"\x01\x00" * engines.STT_RATE)
|
|
|
|
def post(body: bytes, headers: dict[str, str]) -> tuple[int, dict, bytes]:
|
|
return self.call("POST", "/api/stt", body, {**headers, "Content-Type": "audio/wav"})
|
|
|
|
for headers in ({k: v for k, v in good.items() if k != "X-Kvb-Hub"}, {k: v for k, v in good.items() if k != "Origin"}, {**good, "Origin": "http://evil.example"}):
|
|
with self.subTest(headers=headers):
|
|
self.assertEqual(post(clip, headers)[0], 403)
|
|
for content_type in ("application/octet-stream", "audio/x-wav", "text/plain", "application/json"):
|
|
with self.subTest(content_type=content_type):
|
|
self.assertEqual(self.call("POST", "/api/stt", clip, {**good, "Content-Type": content_type})[0], 415)
|
|
self.assertEqual(self.call("POST", "/api/stt", clip, good)[0], 415)
|
|
self.assertEqual(self.call("POST", "/api/stt", b"RIFF", {**good, "Content-Type": "audio/wav", "Content-Length": str(engines.STT_LIMIT + 1)})[0], 413)
|
|
for broken in (
|
|
b"not a wav",
|
|
wav_bytes(1, 2, 44100, b"\x00\x00" * 100),
|
|
wav_bytes(2, 2, engines.STT_RATE, b"\x00\x00" * 200),
|
|
wav_bytes(1, 1, engines.STT_RATE, b"\x00" * 100),
|
|
wav_bytes(1, 2, engines.STT_RATE, b""),
|
|
):
|
|
with self.subTest(broken=broken[:40]):
|
|
self.assertEqual(post(broken, good)[0], 400)
|
|
with mock.patch.object(engines, "STT_SECONDS", 1):
|
|
self.assertEqual(post(wav_bytes(1, 2, engines.STT_RATE, b"\x00\x00" * (engines.STT_RATE + 1)), good)[0], 400)
|
|
self.assertEqual(log_rows(calls), [])
|
|
status, headers, body = post(clip, good)
|
|
self.assertEqual((status, headers["content-type"]), (200, "application/json"), body)
|
|
self.assertEqual(json.loads(body), {"text": "hello from the mic second line"})
|
|
argv = ["--model", str(self.engines.whisper_model), "--file", "-", "--output-txt", "--language", "auto", "--no-prints", "--suppress-nst"]
|
|
self.assertEqual(log_rows(calls), [{"argv": argv, "sha256": hashlib.sha256(clip).hexdigest()}])
|
|
long = wav_bytes(1, 2, engines.STT_RATE, b"\x00\x00" * engines.STT_RATE * 40)
|
|
self.assertGreater(len(long), handler.BODY_LIMIT)
|
|
self.assertEqual(post(long, good)[0], 200)
|
|
status, _, body = post(wav_bytes(1, 2, engines.STT_RATE, b"FAIL" * 100), good)
|
|
self.assertEqual(status, 502, body)
|
|
with mock.patch.object(self.httpd.hub.speech, "hears", return_value=False):
|
|
self.assertEqual(post(clip, good)[0], 503)
|
|
self.assertEqual(self.scripts.calls, [])
|
|
self.assertEqual(self.get("/api/stt")[0], 405)
|
|
|
|
def test_the_server_requires_every_engine_argument(self):
|
|
args = {
|
|
"--bind": "127.0.0.1", "--listen-port": "0", "--public-port": "1", "--council": str(self.council), "--pages": str(self.pages), "--state": str(self.state),
|
|
"--host-state": str(HOST_STATE), "--skills": str(self.outside), "--transcripts": str(self.projects), "--templates": str(HUB / "templates"), "--repos": str(self.repos),
|
|
"--worktrees": str(self.worktrees), "--timers": str(self.timers), "--llm-key-file": os.devnull, "--llm-url": "", "--llm-model": "",
|
|
"--piper": str(self.engines.piper), "--voices": str(self.engines.voices), "--operator-voice": "en_US-test-high:9", "--whisper": str(self.engines.whisper),
|
|
"--whisper-model": str(self.engines.whisper_model), "--scratch": str(self.engines.scratch), "--approvers": str(self.approvers),
|
|
}
|
|
|
|
def run(given: dict[str, str]) -> subprocess.CompletedProcess:
|
|
argv = [part for pair in given.items() for part in pair]
|
|
return subprocess.run([sys.executable, str(HUB / "server.py"), *argv], capture_output=True, text=True, timeout=60, check=False)
|
|
|
|
for name in ("--piper", "--voices", "--operator-voice", "--whisper", "--whisper-model", "--scratch", "--approvers"):
|
|
with self.subTest(name=name):
|
|
done = run({key: value for key, value in args.items() if key != name})
|
|
self.assertEqual(done.returncode, 2, done.stderr)
|
|
self.assertIn(f"required: {name}", done.stderr)
|
|
done = run(args)
|
|
self.assertEqual(done.returncode, 2, done.stderr)
|
|
self.assertIn("the operator voice 'en_US-test-high:9' is not installed", done.stderr)
|
|
|
|
|
|
class TestPhraseology(unittest.TestCase):
|
|
def test_every_class_speaks_in_maritime_phraseology(self):
|
|
cases = (
|
|
(("mayday", "Docs", [], "MAYDAY disk full"), "MAYDAY MAYDAY MAYDAY, this is Docs Docs Docs, disk full, over"),
|
|
(("mayday", "Docs", ["Skills"], "mayday, mayday: disk full"), "MAYDAY MAYDAY MAYDAY, this is Docs Docs Docs, disk full, over"),
|
|
(("pan-pan", "Docs", ["Skills"], "PAN-PAN: slow"), "PAN-PAN PAN-PAN PAN-PAN, all stations, this is Docs, slow, over"),
|
|
(("security", "Maintainer", ["Skills", "Docs"], "SECURITE rotate"), "SECURITE SECURITE SECURITE, all stations, this is Maintainer, rotate, over"),
|
|
(("direct", "Skills", ["Maintainer"], "ready"), "Maintainer, Maintainer, this is Skills, ready, over"),
|
|
(("direct", "Skills", ["Alpha", "Beta"], "ready"), "Alpha and Beta, Alpha and Beta, this is Skills, ready, over"),
|
|
(("direct", "Docs", ["Ops"], "MAYDAY is only a word here"), "Ops, Ops, this is Docs, MAYDAY is only a word here, over"),
|
|
(("direct", "Skills", [], "lost target"), "all stations, this is Skills, lost target, out"),
|
|
(("broadcast", "Maintainer", [], "board"), "all stations, this is Maintainer, board, out"),
|
|
(("broadcast", "Operator", [], "[kvb-hub msg 0123456789abcdef] deploy now file: /home/op/x.txt"), "all stations, this is Operator, deploy now, out"),
|
|
(("broadcast", "Docs", [], " two\n lines\t here "), "all stations, this is Docs, two lines here, out"),
|
|
(("broadcast", "", [], "-"), "all stations, this is unknown station, out"),
|
|
)
|
|
for arguments, expected in cases:
|
|
with self.subTest(arguments=arguments):
|
|
self.assertEqual(phrasing.phrase(*arguments), expected)
|
|
|
|
def test_a_phrase_fits_the_speech_limit_on_one_line(self):
|
|
for cls in council.CLASSES:
|
|
with self.subTest(cls=cls):
|
|
spoken = phrasing.phrase(cls, "n" * 900, ["a\nb " * 300] * 9, "word\n" * 9000)
|
|
self.assertLessEqual(len(spoken), phrasing.SPEECH_LIMIT)
|
|
self.assertTrue(phrasing.SPOKEN_LINE.fullmatch(spoken))
|
|
self.assertTrue(phrasing.phrase("broadcast", "Docs", [], "word " * 300).endswith("word, out"))
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|