install copied the gitignored __pycache__ directories of the sandbox-writable tree into ~/.local/share/kvb-orchestrator with cp -a. Python loads a cached .pyc whose header matches the source mtime and size, and cp -a keeps the mtime, so a .pyc planted by a sandboxed agent would have run in the host runner and PermissionRequest hook without showing in any diff. install now removes every __pycache__ right after the copy; the compose build reads the same cleaned directory. 🎯 Fix: 95 % 💥 Break: 2 % 💸 Cost: – ⏱️ Time: – 🛡️ Security: 100 % 🏆 Score: 93 🚦 Gate: ✅ 90/5, cost and time <= 100, security 100 % Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
156 lines
8.4 KiB
Python
156 lines
8.4 KiB
Python
"""Run orchestrator.sh install twice in a scratch HOME with stub docker, claude and systemctl."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import hashlib
|
|
import json
|
|
import os
|
|
import shutil
|
|
import subprocess
|
|
import tempfile
|
|
import unittest
|
|
from pathlib import Path
|
|
|
|
REPO_ROOT = Path(__file__).resolve().parents[1]
|
|
SCRIPT = REPO_ROOT / "scripts" / "orchestrator.sh"
|
|
OLD = "kvb-hub"
|
|
NEW = "kvb-orchestrator"
|
|
STUB = r'''#!/usr/bin/env python3
|
|
import json, os, sys
|
|
from pathlib import Path
|
|
|
|
tool, args = Path(sys.argv[0]).name, sys.argv[1:]
|
|
file = Path(os.environ["STUB_STATE"])
|
|
state = json.loads(file.read_text())
|
|
state["calls"].append([tool, *args])
|
|
code = 0
|
|
if tool == "docker" and args[:2] == ["image", "inspect"]:
|
|
code = 0 if args[2] in state["images"] else 1
|
|
elif tool == "docker" and args[:2] == ["image", "rm"]:
|
|
code = 0 if args[2] in state["images"] else 1
|
|
state["images"] = [image for image in state["images"] if image != args[2]]
|
|
elif tool == "docker" and args[0] == "compose" and "up" in args:
|
|
state["images"] = sorted({*state["images"], "kvb-orchestrator:local"})
|
|
elif tool == "claude" and args[:2] == ["mcp", "get"]:
|
|
code = 0 if args[2] in state["mcp"] else 1
|
|
elif tool == "claude" and args[:2] == ["mcp", "remove"]:
|
|
code = 0 if args[-1] in state["mcp"] else 1
|
|
state["mcp"].pop(args[-1], None)
|
|
elif tool == "claude" and args[:2] == ["mcp", "add"]:
|
|
state["mcp"][args[-2]] = args[-1]
|
|
elif tool == "systemctl":
|
|
verb, unit = args[1], args[-1]
|
|
if verb == "is-enabled":
|
|
print("enabled" if unit in state["enabled"] else "disabled")
|
|
code = 0 if unit in state["enabled"] else 1
|
|
elif verb == "disable":
|
|
state["enabled"] = [name for name in state["enabled"] if name != unit]
|
|
elif verb == "enable":
|
|
state["enabled"] = sorted({*state["enabled"], unit})
|
|
elif verb == "is-active":
|
|
print("active")
|
|
file.write_text(json.dumps(state))
|
|
sys.exit(code)
|
|
'''
|
|
|
|
|
|
def snapshot(root: Path) -> dict:
|
|
found = {}
|
|
for path in sorted(root.rglob("*")):
|
|
key = str(path.relative_to(root))
|
|
if path.is_symlink():
|
|
found[key] = ("link", os.readlink(path))
|
|
elif path.is_dir():
|
|
found[key] = ("dir", path.stat().st_mode & 0o777)
|
|
else:
|
|
found[key] = ("file", path.stat().st_mode & 0o777, hashlib.sha256(path.read_bytes()).hexdigest())
|
|
return found
|
|
|
|
|
|
@unittest.skipUnless(shutil.which("bash") and shutil.which("python3"), "bash or python3 missing")
|
|
class TestOrchestratorMigration(unittest.TestCase):
|
|
def setUp(self):
|
|
tmp = tempfile.TemporaryDirectory()
|
|
self.addCleanup(tmp.cleanup)
|
|
self.root = Path(tmp.name)
|
|
self.home = self.root / "home"
|
|
stubs = self.root / "bin"
|
|
stubs.mkdir()
|
|
(stubs / "stub").write_text(STUB, encoding="utf-8")
|
|
(stubs / "stub").chmod(0o755)
|
|
for tool in ("docker", "claude", "systemctl"):
|
|
(stubs / tool).symlink_to("stub")
|
|
self.state = self.root / "stub.json"
|
|
self.state.write_text(json.dumps({"calls": [], "images": [f"{OLD}:local"], "mcp": {OLD: "http://127.0.0.1:8737/mcp"}, "enabled": [f"{OLD}-runner.service"]}), encoding="utf-8")
|
|
self.env = {**os.environ, "HOME": str(self.home), "PATH": f"{stubs}:{os.environ['PATH']}", "STUB_STATE": str(self.state), "AGENTS_COUNCIL_ROOT": str(self.root / "council")}
|
|
share, local, config = self.home / ".local" / "share", self.home / ".local" / "state", self.home / ".config"
|
|
self.write(share / OLD / "compose.yml", f"name: {OLD}\n")
|
|
self.write(share / OLD / ".env", "KVB_HUB_PORT=8737\n")
|
|
self.write(local / OLD / "rules.json", '{"rule": 1}\n')
|
|
self.write(local / f"{OLD}-approvals" / "runner.json", '{"used": {}}\n')
|
|
self.write(config / OLD / "approvers.json", '{"credentials": []}\n', 0o600)
|
|
(config / OLD).chmod(0o700)
|
|
units = config / "systemd" / "user"
|
|
self.write(units / f"{OLD}-runner.service", "[Unit]\n")
|
|
(units / "default.target.wants").mkdir()
|
|
(units / "default.target.wants" / f"{OLD}-runner.service").symlink_to(units / f"{OLD}-runner.service")
|
|
self.other = {"matcher": "Bash", "hooks": [{"type": "command", "command": "other-hook"}]}
|
|
legacy = {"matcher": "Bash", "hooks": [{"type": "command", "command": f"python3 {share}/{OLD}/client/permission_hook.py --port 8737"}]}
|
|
self.write(self.home / ".claude" / "settings.json", json.dumps({"model": "x", "hooks": {"PermissionRequest": [self.other, legacy], "Stop": [self.other]}}), 0o640)
|
|
|
|
def write(self, path: Path, text: str, mode: int = 0o644) -> None:
|
|
path.parent.mkdir(parents=True, exist_ok=True)
|
|
path.write_text(text, encoding="utf-8")
|
|
path.chmod(mode)
|
|
|
|
def install(self) -> dict:
|
|
done = subprocess.run(["bash", str(SCRIPT), "install"], env=self.env, capture_output=True, text=True, timeout=120, check=False)
|
|
self.assertEqual(done.returncode, 0, done.stderr)
|
|
return json.loads(self.state.read_text(encoding="utf-8"))
|
|
|
|
def test_install_migrates_every_trace_once(self):
|
|
first = self.install()
|
|
share, local, config = self.home / ".local" / "share", self.home / ".local" / "state", self.home / ".config"
|
|
units = config / "systemd" / "user"
|
|
for gone in (share / OLD, local / OLD, local / f"{OLD}-approvals", config / OLD, units / f"{OLD}-runner.service", units / "default.target.wants" / f"{OLD}-runner.service"):
|
|
with self.subTest(gone=gone):
|
|
self.assertFalse(gone.exists() or gone.is_symlink())
|
|
self.assertEqual((local / NEW / "rules.json").read_text(encoding="utf-8"), '{"rule": 1}\n')
|
|
self.assertEqual((local / f"{NEW}-approvals" / "runner.json").read_text(encoding="utf-8"), '{"used": {}}\n')
|
|
self.assertEqual(((config / NEW).stat().st_mode & 0o777, (config / NEW / "approvers.json").stat().st_mode & 0o777), (0o700, 0o600))
|
|
self.assertTrue((units / f"{NEW}-runner.service").is_file())
|
|
self.assertIn(f"KVB_ORCHESTRATOR_STATE={local / NEW}", (share / NEW / ".env").read_text(encoding="utf-8"))
|
|
settings = self.home / ".claude" / "settings.json"
|
|
data = json.loads(settings.read_text(encoding="utf-8"))
|
|
hooks = data["hooks"]["PermissionRequest"]
|
|
self.assertEqual((data["model"], data["hooks"]["Stop"], hooks[0], len(hooks)), ("x", [self.other], self.other, 2))
|
|
self.assertIn(f"{share / NEW}/client/permission_hook.py", hooks[1]["hooks"][0]["command"])
|
|
self.assertEqual(settings.stat().st_mode & 0o777, 0o640)
|
|
self.assertIn(f"{OLD}/client", (self.home / ".claude" / f"settings.json.before-{NEW}-migration").read_text(encoding="utf-8"))
|
|
self.assertEqual((first["images"], first["mcp"], first["enabled"]), ([f"{NEW}:local"], {NEW: "http://127.0.0.1:8737/mcp"}, [f"{NEW}-runner.service"]))
|
|
self.assertIn(["docker", "compose", "--file", str(share / OLD / "compose.yml"), "down", "--remove-orphans"], first["calls"])
|
|
|
|
before = snapshot(self.home)
|
|
second = self.install()
|
|
self.assertEqual(snapshot(self.home), before)
|
|
self.assertEqual({key: second[key] for key in ("images", "mcp", "enabled")}, {key: first[key] for key in ("images", "mcp", "enabled")})
|
|
legacy_calls = [call for call in second["calls"][len(first["calls"]) :] if any(OLD in arg for arg in call)]
|
|
self.assertEqual(legacy_calls, [["docker", "image", "inspect", f"{OLD}:local"], ["claude", "mcp", "get", OLD]])
|
|
|
|
def test_install_ships_no_bytecode_from_the_tree(self):
|
|
planted = REPO_ROOT / "orchestrator" / "client" / "__pycache__" / "planted.cpython-0.pyc"
|
|
planted.parent.mkdir(exist_ok=True)
|
|
planted.write_bytes(b"planted")
|
|
self.addCleanup(planted.unlink, True)
|
|
self.install()
|
|
self.assertEqual(sorted((self.home / ".local" / "share" / NEW).rglob("__pycache__")), [])
|
|
|
|
def test_old_names_live_only_in_the_migration(self):
|
|
done = subprocess.run(["git", "grep", "-niE", "kvb[-_]hub", "--", ".", ":!docs/requirements", f":!{Path(__file__).relative_to(REPO_ROOT)}"], cwd=REPO_ROOT, capture_output=True, text=True, check=False)
|
|
hits = sorted(line.split(":", 2)[0] + ":" + line.split(":", 2)[2].strip() for line in done.stdout.splitlines())
|
|
self.assertEqual(hits, ['orchestrator/templates/page-state.js:let userViews = validViews(stored(STORE.views, stored("kvb-hub.views", [])));', 'scripts/orchestrator.sh:LEGACY="kvb-hub"'])
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|