fix(hub): close seven bugs found in the refactor review
The PermissionRequest hook posted asks cut to the hub's field limits, so a signature covered less than Claude Code would run; it now posts nothing it cannot sign in full and leaves the decision to Claude Code with a reason on stderr. fields.seconds names the field the caller used in its error. Serving a stored page maps an OSError to 500 instead of a dropped connection. The natural-language line marks the register as seen before it builds the inbox, like the page does. do_POST runs every header guard (host, origin, client, content type, MCP) before it reads the body, so a refused request never costs its upload. The runner's reader closes stdout at EOF, because a close from the executing thread blocked on the buffer lock and could SIGPIPE a command still running. The page title counts only inbox items that are not snoozed. Committed on the operator's word despite the gate: cost and time are estimates under 98 % other load and include eight new test cases. 🎯 Fix: 88 % 💥 Break: 3 % 💸 Cost: 106 % ⏱️ Time: 113 % 🛡️ Security: 100 % 🏆 Score: 71 🚦 Gate: ⛔ fix 88 % < 90; cost 106 % > 100, time 113 % > 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
8ba45efd04
commit
afd96fcd72
14 files changed
+197
-81
No files matched your search
+31
-1
@@ -1,11 +1,12 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import http.client
|
||||
import json
|
||||
import unittest
|
||||
|
||||
from hub_case import HubCase
|
||||
from hub_support import PAGE, SCORE, TREE
|
||||
from kvb_hub import mcp
|
||||
from kvb_hub import handler, mcp
|
||||
|
||||
TOOLS = {"review_request", "review_status", "review_wait", "review_list", "decision_request", "decision_wait", "operator_message"}
|
||||
|
||||
@@ -79,6 +80,35 @@ class TestHubMcp(HubCase):
|
||||
self.assertEqual(self.post_mcp(ping, {"Origin": f"http://{self.host}"})[0], 200)
|
||||
self.assertEqual(self.get("/mcp")[0], 405)
|
||||
|
||||
def headers_only(self, path: str, headers: dict[str, str]) -> tuple[int, bytes]:
|
||||
connection = http.client.HTTPConnection("127.0.0.1", self.port, timeout=3)
|
||||
try:
|
||||
connection.putrequest("POST", path, skip_host=True)
|
||||
for name, value in {"Content-Length": "1024", **headers}.items():
|
||||
connection.putheader(name, value)
|
||||
connection.endheaders()
|
||||
response = connection.getresponse()
|
||||
return response.status, response.read()
|
||||
finally:
|
||||
connection.close()
|
||||
|
||||
def test_every_post_guard_answers_before_the_body_arrives(self):
|
||||
guard = self.guard(self.host)
|
||||
for path, headers, status, text in (
|
||||
("/api/asks", {"Host": "evil.example", "X-Kvb-Hub": "1"}, 403, "forbidden host"),
|
||||
("/api/asks", {"Host": self.host}, 403, "forbidden: local POST with the X-Kvb-Hub header and no foreign origin only"),
|
||||
("/mcp", {"Host": self.host, "Origin": "http://evil.example", "Content-Type": "application/json"}, 403, "foreign origin"),
|
||||
("/mcp", {"Host": self.host, "Content-Type": "text/plain"}, 415, "use application/json"),
|
||||
("/api/runner/0123456789abcdef/report", guard, 403, "forbidden: the runner calls with the X-Kvb-Hub header and no origin only"),
|
||||
("/api/messages", self.client(), 403, "forbidden: same-origin POST with the X-Kvb-Hub header only"),
|
||||
("/api/stt", {**guard, "Content-Type": "text/plain"}, 415, "use audio/wav"),
|
||||
("/api/state", guard, 404, "not found"),
|
||||
("/api/uploads", {**guard, "Content-Length": str(handler.UPLOAD_LIMIT + 1)}, 413, "body too large"),
|
||||
("/api/messages", {**guard, "Content-Length": "many"}, 411, "Content-Length required"),
|
||||
):
|
||||
with self.subTest(path=path, headers=headers):
|
||||
self.assertEqual(self.headers_only(path, headers), (status, f"{text}\n".encode()))
|
||||
|
||||
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"])
|
||||
|
||||
Reference in new issue
Block a user