Phase 3 of requirement 001. The orchestrator no longer starts git: orchestrator/kvb_orchestrator/tree.py maps the document that pkgmgr branch tree writes (repos, branches, worktrees, detached heads, delete_ready) onto the Tree view, re-reads it when mtime or size change, and reports a missing or broken file as an empty tree with the error. compose.yml mounts the tree directory read-only (a file mount would keep the replaced inode). /api/state touches state/tree-heartbeat; kvb-orchestrator-tree.timer runs every minute and its oneshot service runs only while that heartbeat is younger than 5 minutes, with PATH=/usr/bin:/bin and the pkgmgr venv that the install builds from the newest v* tag a fully trusted GPG key signed, cloned fresh from PKGMGR_REPO because the local checkout's config is sandbox-writable. Dialectic verdict 2026-11-06 (2 rounds, 5 skeptics, ~85 %): hardened git runner in pkgmgr, gc classification shared with the tree scan, forge facts only from an unlocked Secret Service token with a 600 s cache, atomic tree output refreshed by open/close/drop/gc, host units outside sandbox-writable paths, orchestrator reads the file only. Losing objections: extending the host runner (it would run bash -c on signed text), a new payload kind, copying the pkgmgr checkout, a tree limited to .worktrees. Operator answers: signed-tag venv, UI delete keeps volumes and is local only, ignored files beyond known caches block a delete, scan only while the page polls. Delete jobs, gc_item signing and the UI delete path follow in phase 4. MEDUSA acceptances (kvb-think-criticize 2026-11-07, 1 skeptic): MEDUSA-MM-SCAN-058 orchestrator/server.py:64 and :80 false positives (case-insensitive substring pla in --templates and Places; --tree-file is a read-only JSON file, no model loading). 🎯 Fix: 93 % 💥 Break: 4 % 💸 Cost: – ⏱️ Time: – 🛡️ Security: 100 % 🏆 Score: 89 🚦 Gate: ✅ 90/5, cost and time <= 100, security 100 % Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
76 lines
4.0 KiB
Python
76 lines
4.0 KiB
Python
from __future__ import annotations
|
|
|
|
import ast
|
|
import datetime
|
|
import json
|
|
import os
|
|
import unittest
|
|
|
|
from hub_case import HubCase
|
|
from hub_support import HUB, PAGE
|
|
|
|
GENERATED = datetime.datetime(2026, 11, 6, 12, tzinfo=datetime.UTC).timestamp()
|
|
|
|
|
|
def starts_git(path) -> list[int]:
|
|
lines = []
|
|
for node in ast.walk(ast.parse(path.read_text(encoding="utf-8"))):
|
|
if isinstance(node, ast.Constant) and isinstance(node.value, str) and node.value.split(" ")[0] in ("git", "/usr/bin/git"):
|
|
lines.append(node.lineno)
|
|
return lines
|
|
|
|
|
|
class TestHubTree(HubCase):
|
|
def test_tree_serves_the_pkgmgr_file_with_workers_and_reviews(self):
|
|
root = self.write_tree()
|
|
demo, feature = f"{root}/repos/github.com/me/demo", f"{root}/worktrees/github.com/me/demo/feature"
|
|
self.write_council({"rasci.json": {"work/feature": {"title": "F", "status": "running", "worker": "Skills session", "workdir": feature}}})
|
|
self.tool("review_request", {**self.arguments("feat: tree", "", self.page("tree.html", PAGE)), "worktree": feature, "branch": "feature"})
|
|
status, _, body = self.get("/api/tree")
|
|
self.assertEqual(status, 200, body)
|
|
data = json.loads(body)
|
|
repos = {entry["path"]: entry for entry in data["repos"]}
|
|
self.assertEqual((repos[demo]["name"], repos[demo]["known"], repos[f"{root}/outside/elsewhere"]["known"]), ("github.com/me/demo", True, False))
|
|
self.assertEqual((data["scanned"], data["delete_ready"], data["delete_reason"]), (GENERATED, False, "Secret Service locked"))
|
|
branches = {entry["name"]: entry for entry in repos[demo]["branches"]}
|
|
main = branches["main"]
|
|
self.assertEqual((main["upstream"], main["ahead"], main["behind"], main["gone"], main["state"]), ("origin/main", 1, 0, False, "protected"))
|
|
self.assertEqual((branches["feature"]["state"], branches["feature"]["reason"]), ("dirty", "dirty: 1 paths"))
|
|
trees = [(tree["path"], tree["dirty"], tree["untracked"], tree["workers"], tree["review"]) for tree in main["worktrees"] + branches["feature"]["worktrees"]]
|
|
self.assertEqual(trees, [(demo, False, 0, [], None), (feature, True, 1, ["Skills"], {"status": "pending", "gate": "pass", "title": "feat: tree"})])
|
|
self.assertEqual([(tree["path"], tree["workers"]) for tree in repos[demo]["detached"]], [(f"{root}/worktrees/github.com/me/demo/detached", [])])
|
|
|
|
def test_tree_reads_the_file_again_once_it_changes(self):
|
|
self.write_tree()
|
|
self.assertEqual(len(json.loads(self.get("/api/tree")[2])["repos"]), 2)
|
|
data = json.loads(self.tree_file.read_text(encoding="utf-8"))
|
|
data["repos"] = data["repos"][:1]
|
|
self.tree_file.write_text(json.dumps(data), encoding="utf-8")
|
|
self.assertEqual(len(json.loads(self.get("/api/tree")[2])["repos"]), 1)
|
|
|
|
def test_a_missing_or_broken_file_answers_with_the_reason(self):
|
|
status, _, body = self.get("/api/tree")
|
|
self.assertEqual(status, 200, body)
|
|
self.assertEqual((json.loads(body)["repos"], json.loads(body)["scanned"]), ([], None))
|
|
self.assertIn("no pkgmgr tree file", json.loads(body)["error"])
|
|
self.tree_file.write_text('{"repos": [{"path": "/x"}]}', encoding="utf-8")
|
|
self.assertIn("unreadable", json.loads(self.get("/api/tree")[2])["error"])
|
|
|
|
def test_the_state_poll_beats_the_heartbeat_the_tree_timer_waits_for(self):
|
|
heartbeat = self.state / "tree-heartbeat"
|
|
self.assertFalse(heartbeat.exists())
|
|
self.snapshot()
|
|
self.assertTrue(heartbeat.is_file())
|
|
os.utime(heartbeat, (0, 0))
|
|
self.snapshot()
|
|
self.assertGreater(heartbeat.stat().st_mtime, 0)
|
|
|
|
def test_no_module_of_the_orchestrator_starts_git(self):
|
|
sources = [HUB / "server.py", *sorted((HUB / "kvb_orchestrator").glob("*.py"))]
|
|
found = {path.name: starts_git(path) for path in sources}
|
|
self.assertEqual({name: lines for name, lines in found.items() if lines}, {})
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|