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:
Kevin Veen-BirkenbachandClaude Opus 5.5 committed 2026-11-06 18:46:40 +00:00
1 parent 8ba45efd04
commit afd96fcd72
14 files changed
+197 -81

No files matched your search

+12 -12
View File
@@ -13,8 +13,9 @@ Returns:
stdout: the PermissionRequest decision JSON: allow only when a pinned
passkey signed exactly this tool, command, cwd, session and ask id;
deny when the operator denied in the kvb-hub. Nothing on timeout,
error, a missing or invalid signature or a hub that is down, so
Claude Code shows its own dialog. The exit code is 0.
error, a missing or invalid signature, a value longer than the hub
takes or a hub that is down, so Claude Code shows its own dialog.
The exit code is 0.
"""
from __future__ import annotations
@@ -34,6 +35,7 @@ POLL = 25.0
NAME_LIMIT = 200
COMMAND_LIMIT = 4000
PATH_LIMIT = 4096
LIMITS = {"tool": NAME_LIMIT, "command": COMMAND_LIMIT, "cwd": PATH_LIMIT, "session": NAME_LIMIT, "agent": NAME_LIMIT}
DENIED = "The operator denied this in the kvb-hub."
@@ -51,18 +53,12 @@ def facts(hook: dict) -> dict:
"command": summary(hook.get("tool_input")),
"cwd": str(hook.get("cwd") or ""),
"session": str(hook.get("session_id") or ""),
"agent": str(hook.get("agent_type") or ""),
}
def create(base: str, hook: dict, ask: dict, timeout: float) -> str:
payload = {
"tool": ask["tool"][:NAME_LIMIT],
"command": ask["command"][:COMMAND_LIMIT],
"cwd": ask["cwd"][:PATH_LIMIT],
"session": ask["session"][:NAME_LIMIT],
"agent": str(hook.get("agent_type") or "")[:NAME_LIMIT],
"timeout": timeout,
}
def create(base: str, ask: dict, timeout: float) -> str:
payload = {**ask, "timeout": timeout}
headers = {"Content-Type": "application/json", **hub_api.GUARD}
request = urllib.request.Request(f"{base}/api/asks", data=json.dumps(payload).encode(), headers=headers, method="POST")
ask_id = hub_api.call(request, hub_api.SLACK)["id"]
@@ -104,7 +100,11 @@ def main() -> int:
try:
hook = json.load(sys.stdin)
ask = facts(hook)
ask_id = create(base, hook, ask, args.timeout)
too_long = [name for name, limit in LIMITS.items() if len(ask[name]) > limit]
if too_long:
print(f"kvb-hub: {', '.join(too_long)} too long to sign in full, Claude Code asks instead", file=sys.stderr)
return 0
ask_id = create(base, ask, args.timeout)
answer = {"status": "pending"}
while answer["status"] == "pending" and (left := deadline - time.monotonic()) > 0:
answer = status(base, ask_id, min(left, POLL))
+5 -3
View File
@@ -49,9 +49,11 @@ def call(base: str, path: str, payload: dict | None) -> dict:
def collect(stream: object, tail: collections.deque, lock: threading.Lock) -> None:
for line in stream:
with lock:
tail.append(line)
# The reader closes the pipe at EOF: a close in execute() would block on the buffer lock this read holds and SIGPIPE a command still running.
with stream:
for line in stream:
with lock:
tail.append(line)
def refusal(job: dict, trust: verifier.Trust, now: float) -> str | None: