feat(orchestrator): inbox console, deny by channel and agent rule requests

The inbox shows the console output of a command under its "Ausführen"
row, kept to the same tail the job list shows. A deny picks its radio
channel (post by default, call, newsletter, broadcast, pan-pan,
security) and an optional reason; the passkey signs denial, channel and
reason, the hub verifies the proof before it stores or sends anything,
and the PermissionRequest hook quotes a reason only when it verifies.

Agents ask for an auto-approval rule through the MCP tool rule_request;
the request shows in the inbox with a dry run over the last seven days
and is accepted only with a passkey assertion the hub verifies against
the pinned keys and the hub origin, for every rule kind. Requests lapse
with the ask lifetime and are capped per agent and in total.

The image runs as nobody by default and carries the health check
command; compose keeps the timing.

🎯 Fix: 93 %
💥 Break: 4 %
💸 Cost: 98 %
⏱️ Time: 75 %
🛡️ Security: 100 %
🏆 Score: 100
🚦 Gate: ✅ 90/5, cost and time <= 100, security 100 %

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Kevin Veen-BirkenbachandClaude Opus 5.5 committed 2026-11-07 18:14:30 +00:00
1 parent 1d311a3382
commit 58187e8323
37 files changed
+1387 -100

No files matched your search

+170
View File
@@ -0,0 +1,170 @@
from __future__ import annotations
import json
import time
import unittest
from unittest import mock
from hub_case import HubCase
from hub_support import ASK, PAGE, PRESENT, key, signing
from kvb_orchestrator import batches, denying, requesting, skill_scripts
class TestHubRuleRequests(HubCase):
def ask_rule(self, **change: object) -> dict:
return {"kind": "ask", "tool": "Bash", "pattern": "make test*", "scope": "/repo*", "expires": int(time.time()) + 86400, "agent": "Skills", "why": "the gate runs make test all day", **change}
def requested(self, arguments: dict) -> str:
result = self.tool("rule_request", arguments)
self.assertFalse(result["isError"], result)
return result["structuredContent"]["id"]
def waited(self, request_id: str) -> dict:
return self.tool("rule_request_wait", {"id": request_id, "timeout_seconds": 0})["structuredContent"]
def item(self, request_id: str) -> dict | None:
return next((item for item in self.snapshot()["inbox"] if item["kind"] == "rule" and item["id"] == request_id), None)
def action(self, request_id: str, verb: str, payload: object, headers: dict[str, str]) -> tuple[int, dict]:
status, _, body = self.post_json(f"/api/rule-requests/{request_id}/{verb}", payload, headers)
return status, json.loads(body) if status < 300 else {}
def test_a_requested_rule_is_shown_with_its_dry_run_and_stored_only_signed(self):
now = time.time()
for change in (
{"expires": int(now) - 1},
{"expires": int(now) + 400 * 86400},
{"tool": ""},
{"kind": "review"},
{"kind": "decision", "tool": "", "scope": "/repo"},
{"kind": "radio"},
{"pattern": " "},
{"scope": "two\nlines"},
{"why": " "},
{"agent": ""},
{"extra": 1},
):
with self.subTest(change=change):
self.assertTrue(self.tool("rule_request", self.ask_rule(**change))["isError"])
for command, cwd in (("make test", "/repo"), ("make test-unit", "/repo/sub"), ("make deploy", "/repo"), ("make test && rm -rf /", "/repo"), ("make test", "/elsewhere")):
self.create_ask({**ASK, "command": command, "cwd": cwd})
request_id = self.requested(self.ask_rule())
item = self.item(request_id)
arguments = self.ask_rule()
self.assertEqual({name: item["rule"][name] for name in ("kind", "tool", "pattern", "scope")}, {name: arguments[name] for name in ("kind", "tool", "pattern", "scope")})
self.assertEqual((item["agent"], item["detail"], item["green"], item["deniable"], item["dry_run"]), ("Skills", "the gate runs make test all day", False, True, {"matched": 2, "total": 5, "days": 7}))
self.assertEqual(self.waited(request_id), {"id": request_id, "status": "pending", "rule": None, "reason": "", "decided": None})
self.assertEqual(self.approve([self.entry("rule", request_id, "", "")], None)[0], 400)
self.assertEqual(self.action(request_id, "sign", {}, self.client())[0], 403)
self.assertEqual(self.action("0123456789abcdef", "sign", {}, self.guard(self.host))[0], 404)
self.assertEqual(self.get(f"/api/rule-requests/{request_id}/sign")[0], 405)
signer = self.pin()
status, prepared = self.action(request_id, "sign", {}, self.guard(self.host))
self.assertEqual(status, 200)
rule = prepared["payload"]
self.assertEqual((rule["kinds"], rule["tool"], rule["pattern"], rule["scope"], rule["expires"]), (["ask"], "Bash", "make test*", "/repo*", arguments["expires"]))
self.assertEqual((prepared["challenge"], prepared["credentials"], prepared["sign"]), (signing.challenge(rule), [signer.id], True))
signed = signer.assertion(rule, self.origin, "localhost", PRESENT, 0)
wider = {**rule, "pattern": "*"}
for payload in ({"rule": rule, "assertion": None}, {"rule": wider, "assertion": signer.assertion(wider, self.origin, "localhost", PRESENT, 0)}, {"rule": rule}, {"rule": {**rule, "kinds": ["ask", "review"]}, "assertion": signed}):
with self.subTest(payload=str(payload)[:80]):
self.assertEqual(self.action(request_id, "accept", payload, self.guard(self.host))[0], 400)
self.assertEqual(self.action(request_id, "accept", {"rule": rule, "assertion": signed}, self.client())[0], 403)
status, accepted = self.action(request_id, "accept", {"rule": rule, "assertion": signed}, self.guard(self.host))
self.assertEqual((status, accepted["status"], accepted["rule"]), (201, "accepted", rule["id"]))
stored = self.httpd.hub.rules.get(rule["id"])
self.assertEqual({name: stored[name] for name in ("kind", "tool", "pattern", "scope", "expires", "enabled")}, {"kind": "ask", "tool": "Bash", "pattern": "make test*", "scope": "/repo*", "expires": float(arguments["expires"]), "enabled": True})
self.assertEqual(stored["proof"], {"payload": rule, "assertion": signed})
form = self.post_json("/api/rules/sign", {name: arguments[name] for name in ("kind", "tool", "pattern", "scope", "expires")}, self.guard(self.host))
formed = json.loads(form[2])["payload"]
self.post_json("/api/rules", {"rule": formed, "assertion": signer.assertion(formed, self.origin, "localhost", PRESENT, 0), "enabled": True}, self.guard(self.host))
self.assertEqual(set(self.httpd.hub.rules.get(formed["id"])), set(stored))
self.assertEqual(self.action(request_id, "accept", {"rule": rule, "assertion": signed}, self.guard(self.host))[0], 409)
self.assertIsNone(self.item(request_id))
answer = self.waited(request_id)
self.assertEqual((answer["status"], answer["rule"]), ("accepted", rule["id"]))
with mock.patch.object(batches, "UNDO", 0.05):
matched = self.create_ask({**ASK, "command": "make test"})
self.settle(lambda: self.ask_status(matched, "0") == "allowed")
def test_review_and_decision_requests_count_their_own_history(self):
self.request("feat: green", "")
self.tool("review_request", {**self.arguments("feat: red", "", self.page("red.html", PAGE)), "gate": "fail"})
self.decision("Merge now")
self.decision("")
review = self.requested(self.ask_rule(kind="review", tool="", pattern="feature/*", scope="/repo/*"))
choice = self.requested(self.ask_rule(kind="decision", tool="", pattern="Sk*", scope="*"))
self.assertEqual(self.item(review)["dry_run"], {"matched": 1, "total": 2, "days": 7})
self.assertEqual(self.item(choice)["dry_run"], {"matched": 1, "total": 2, "days": 7})
signer = self.pin()
_, prepared = self.action(choice, "sign", {}, self.guard(self.host))
self.assertEqual(self.action(choice, "accept", {"rule": prepared["payload"], "assertion": None}, self.guard(self.host))[0], 400)
status, _ = self.action(choice, "accept", {"rule": prepared["payload"], "assertion": signer.assertion(prepared["payload"], self.origin, "localhost", PRESENT, 0)}, self.guard(self.host))
self.assertEqual(status, 201)
def test_an_assertion_the_hub_cannot_verify_stores_no_rule(self):
signer = self.pin()
request_id = self.requested(self.ask_rule())
_, prepared = self.action(request_id, "sign", {}, self.guard(self.host))
rule = prepared["payload"]
for name, assertion in (("empty", {}), ("unpinned key", key(-8, "stranger").assertion(rule, self.origin, "localhost", PRESENT, 0)), ("other origin", signer.assertion(rule, "http://localhost:1", "localhost", PRESENT, 0)), ("other rule", signer.assertion({**rule, "pattern": "*"}, self.origin, "localhost", PRESENT, 0))):
with self.subTest(name=name):
status, _, body = self.post_json(f"/api/rule-requests/{request_id}/accept", {"rule": rule, "assertion": assertion}, self.guard(self.host))
self.assertEqual(status, 400, body)
self.assertIn("nicht signiert", body.decode())
self.assertEqual((self.waited(request_id)["status"], self.httpd.hub.rules.listed()), ("pending", []))
self.assertIsNotNone(self.item(request_id))
now = int(time.time())
for fields in ({"kind": "ask", "tool": "Bash", "pattern": "make *", "scope": "/repo*"}, {"kind": "review", "tool": "", "pattern": "feature/*", "scope": "/repo/*"}, {"kind": "decision", "tool": "", "pattern": "*", "scope": "*"}):
offer = json.loads(self.post_json("/api/rules/sign", {**fields, "expires": now + 3600}, self.guard(self.host))[2])
self.assertTrue(offer["sign"])
for assertion in (None, {}, key(-8, "stranger").assertion(offer["payload"], self.origin, "localhost", PRESENT, 0)):
with self.subTest(kind=fields["kind"], assertion=str(assertion)[:40]):
self.assertEqual(self.post_json("/api/rules", {"rule": offer["payload"], "assertion": assertion, "enabled": True}, self.guard(self.host))[0], 400)
self.assertEqual(self.httpd.hub.rules.listed(), [])
with mock.patch.object(batches, "UNDO", 0.05):
asked = self.create_ask({**ASK, "command": "make test"})
time.sleep(0.3)
self.assertEqual(self.ask_status(asked, "0"), "pending")
def test_a_request_lapses_with_the_ask_lifetime_and_frees_its_cap(self):
self.pin()
first = [self.requested(self.ask_rule(pattern=f"make test{index}*")) for index in range(requesting.AGENT_CAP)]
self.assertTrue(self.tool("rule_request", self.ask_rule(pattern="make lint*"))["isError"])
with mock.patch.object(denying, "ASK_LIFETIME", 0):
self.assertEqual(self.waited(first[0])["status"], "expired")
self.assertIsNone(self.item(first[0]))
self.assertEqual(self.action(first[0], "sign", {}, self.guard(self.host))[0], 409)
entry = {**self.entry("rule", first[0], "", ""), "verb": "reject", "channel": "post", "reason": ""}
self.assertEqual(self.post_json("/api/sign", {"items": [entry]}, self.guard(self.host))[0], 409)
self.requested(self.ask_rule(pattern="make lint*"))
self.assertEqual(self.waited(first[0])["status"], "pending")
def test_pending_requests_are_capped_and_a_denial_reaches_the_agent(self):
self.claim("s1", "Skills session")
first = [self.requested(self.ask_rule(pattern=f"make test{index}*")) for index in range(requesting.AGENT_CAP)]
refused = self.tool("rule_request", self.ask_rule(pattern="make lint*"))
self.assertTrue(refused["isError"])
self.assertIn("pending", refused["content"][0]["text"])
other = self.requested(self.ask_rule(agent="Docs"))
entry = {**self.entry("rule", first[0], "", ""), "verb": "reject", "channel": "post", "reason": "too broad"}
self.assertEqual(self.prepare([entry])["payload"]["items"], [signing.item_hash(signing.denial_item(signing.rule_request_item(first[0], "ask", "Bash", "make test0*", "/repo*", self.httpd.hub.rule_requests.get(first[0])["expires"]), "post", "too broad"))])
with mock.patch.object(batches, "UNDO", 0.3):
self.assertEqual(self.approve([entry], self.signed([entry], self.pin()))[0], 202)
self.assertEqual(self.action(first[0], "sign", {}, self.guard(self.host))[0], 409)
self.settle(lambda: self.waited(first[0])["status"] == "denied")
answer = self.waited(first[0])
self.assertEqual((answer["reason"], answer["rule"]), ("too broad", None))
radio = [args for script, args in self.scripts.calls if script == skill_scripts.RADIO]
self.assertEqual(radio[-1][:4], ["post", "--to", "Skills session", "--session"])
self.assertTrue(radio[-1][-1].endswith("Denied by the operator: rule request ask Bash make test0* in /repo* (from Skills). Reason: too broad"), radio[-1])
self.assertEqual(self.action(first[0], "sign", {}, self.guard(self.host))[0], 409)
self.requested(self.ask_rule(pattern="make lint*"))
with mock.patch.object(requesting, "TOTAL_CAP", len(first) + 1):
self.assertTrue(self.tool("rule_request", self.ask_rule(agent="Third"))["isError"])
self.assertIsNotNone(self.item(other))
self.assertTrue(self.tool("rule_request_wait", {"id": "0123456789abcdef", "timeout_seconds": 0})["isError"])
if __name__ == "__main__":
unittest.main()