Surface (requirement 001 phase 2): navigation Stream (Inbox, Regeln) / Agents / Artifacts with keys 1-3 and hash routes (#graph and #tree redirect), header with title, command palette and settings only, server-side badges (unsnoozed inbox, beats within 900 s, open jobs), a Mayday banner with "aufheben", four stream chips with agent:/art:/seit: search, Filter and time menus, the inbox in main, an agents toolbar (Funk an alle with Newsletter, Broadcast, Pan-Pan, Security, Mayday; Priorität; Tag/Nacht), Post or Call in the agent panel, Ambiente in the settings and read-aloud over the seven classes with Mayday locked. Rename (decision 9): hub/ is orchestrator/, the package kvb_orchestrator, the unit kvb-orchestrator-runner.service, scripts/orchestrator.sh, environment KVB_ORCHESTRATOR_*, make targets orchestrator-*, the MCP server, image and compose project kvb-orchestrator, the guard header X-Kvb-Orchestrator and the message marker [kvb-orchestrator msg <id>]. install runs migrate first: it stops the old container, removes its image and code, moves state, approvals and pinned keys, swaps the MCP registration, the PermissionRequest hook and the runner unit, and changes nothing on a second run (scratch-HOME test with stub docker, claude and systemctl). Saved views fall back to the old localStorage key once. MEDUSA acceptances carried to the renamed paths: 82 rows whose lines are unchanged (reason prefixed "renamed from <old path>, line unchanged"), and 7 rows whose text changed only in the product name, the package name, a temp prefix or a new class argument, each upheld by one kvb-think-criticize skeptic (approval.py:60, messaging.py:53, server.py:56, tests/hub_support.py:64, passkey.py:205, page-agent.js:75, language.py:46). Cost 101 %: the longer name adds 15 tokens of skill text; the operator approved it with the commit. 🎯 Fix: 92 % 💥 Break: 4 % 💸 Cost: 101 % ⏱️ Time: – 🛡️ Security: 100 % 💰 Value: ? 🏆 Score: 87 🚦 Gate: ⚠️ cost 101 % > 100: ask the operator before the commit Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
84 lines
4.3 KiB
Python
84 lines
4.3 KiB
Python
from __future__ import annotations
|
|
|
|
import errno
|
|
import json
|
|
import re
|
|
import subprocess
|
|
import time
|
|
import unittest
|
|
from pathlib import Path
|
|
from unittest import mock
|
|
|
|
from hub_case import HubCase
|
|
from hub_support import AMBIENCE, NAV, SPACE, VOICE
|
|
from kvb_orchestrator import exchange
|
|
|
|
|
|
class TestHubPage(HubCase):
|
|
def test_review_page_is_sandboxed_and_the_index_allows_only_its_own_inline_code(self):
|
|
review_id = self.request("feat: csp", "")
|
|
status, headers, _ = self.get(f"/reviews/{review_id}/page")
|
|
self.assertEqual(status, 200)
|
|
self.assertEqual(headers["content-security-policy"], "sandbox allow-scripts")
|
|
self.assertEqual(headers["x-content-type-options"], "nosniff")
|
|
status, headers, body = self.get("/")
|
|
csp = headers["content-security-policy"]
|
|
nonce = re.search(r"script-src 'nonce-([^']+)'", csp).group(1)
|
|
self.assertIn(f"style-src 'nonce-{nonce}'", csp)
|
|
self.assertNotIn("unsafe", csp)
|
|
self.assertIn("default-src 'none'", csp)
|
|
html = body.decode("utf-8")
|
|
tags = re.findall(r"<(script|style)\b([^>]*)>", html)
|
|
self.assertEqual(sorted({tag for tag, _ in tags}), ["script", "style"])
|
|
self.assertGreaterEqual(len(tags), 3)
|
|
for tag, attributes in tags:
|
|
with self.subTest(tag=tag, attributes=attributes):
|
|
self.assertIn(f'nonce="{nonce}"', attributes)
|
|
self.assertNotIn(nonce, self.get("/")[1]["content-security-policy"])
|
|
status, headers, _ = self.get(f"/reviews/{review_id}/files")
|
|
self.assertEqual((status, headers["content-type"]), (200, "application/json"))
|
|
self.assertEqual(headers["content-security-policy"], exchange.PLAIN_CSP)
|
|
|
|
def test_a_stored_file_that_cannot_be_read_answers_500(self):
|
|
review_id = self.request("feat: io", "")
|
|
for name in ("page", "files"):
|
|
with self.subTest(name=name), mock.patch.object(Path, "read_bytes", side_effect=OSError(errno.EIO, "Input/output error")):
|
|
status, _, body = self.get(f"/reviews/{review_id}/{name}")
|
|
self.assertEqual((status, body), (500, b"storage failed: Input/output error\n"))
|
|
|
|
def test_markup_from_agents_is_escaped(self):
|
|
self.request("<script>alert(1)</script>", "</pre><script>alert(2)</script>")
|
|
register = {"work/x": {"title": "<script>alert(3)</script>", "status": "running", "roles": {"<b>One</b>": "RA"}}}
|
|
news = {"ts": time.time(), "from": "<img src=x>", "to": [], "text": "<script>alert(4)</script>"}
|
|
self.write_council({"rasci.json": register, "news.jsonl": self.jsonl([news])})
|
|
html = self.index()
|
|
for n in range(1, 5):
|
|
with self.subTest(n=n):
|
|
self.assertNotIn(f"<script>alert({n})", html)
|
|
self.assertNotIn("<img src=x>", html)
|
|
self.assertNotIn("<b>One</b>", html)
|
|
embedded = re.search(r'<script nonce="[^"]+" id="state" type="application/json">(.*?)</script>', html, re.DOTALL).group(1)
|
|
state = json.loads(embedded)
|
|
self.assertEqual((state["reviews"][0]["title"], state["reviews"][0]["notes"]), ("<script>alert(1)</script>", "</pre><script>alert(2)</script>"))
|
|
self.assertEqual((state["register"][0]["title"], state["radio"][0]["from"]), ("<script>alert(3)</script>", "<img src=x>"))
|
|
|
|
def test_the_index_scripts_parse_and_embed_the_ambience_voice_nav_and_space_modules(self):
|
|
html = self.index()
|
|
for module in (AMBIENCE, VOICE, NAV, SPACE):
|
|
with self.subTest(module=module.name):
|
|
self.assertIn(module.read_text(encoding="utf-8").strip(), html)
|
|
scripts = re.findall(r'<script nonce="[^"]+">(.*?)</script>', html, re.DOTALL)
|
|
self.assertEqual(len(scripts), 4)
|
|
page = self.state / "page.js"
|
|
page.write_text("\n;\n".join(scripts), encoding="utf-8")
|
|
done = subprocess.run(["node", "--check", str(page)], capture_output=True, text=True, timeout=60, check=False)
|
|
self.assertEqual(done.returncode, 0, done.stderr)
|
|
for module in (AMBIENCE, VOICE, NAV, SPACE):
|
|
with self.subTest(module=module.name):
|
|
done = subprocess.run(["node", "--check", str(module)], capture_output=True, text=True, timeout=60, check=False)
|
|
self.assertEqual(done.returncode, 0, done.stderr)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|