Phase 4 of requirement 001. Agents and Artifacts each offer Orbit, Chord, Swimlane, Tree and 3D: the spawn tree (page-spawn.js) and the artifacts orbit (page-artifacts.js, kvb_orchestrator/artifacts.py) replace the "folgt" placeholders, 3D runs on the vendored 3d-force-graph and three.js (MIT, LICENSE files kept, served from the image under the unchanged CSP), and a Bootstrap Icons sprite (icons.py, MIT) replaces the emoji glyphs, also in the meditation page. Signed branch deletes: the Tree view offers a delete only for merged-clean branches without a gc skip reason; the operator signs the exact item with the passkey, the hub writes a job without any command (kvb_orchestrator/cleanup.py), and the host path unit runs client/cleanup.py, which verifies the signature, dry-runs pkgmgr branch cleanup for that exact branch, refuses patterns, option names, changed tips, other worktrees and plans naming other branches, deletes, and rewrites the branch tree. pkgmgr runs with core.hooksPath=/dev/null, core.fsmonitor=false and the config hook events disabled through GIT_CONFIG_COUNT, because the cwd is a repository agents can write and pkgmgr releases before 4c8b137 delete without scan mode (test with a positive control that both hook kinds fire without it). 3D labels go into the tooltip as a text node: the vendored tooltip renders a string label as HTML. Tests: 503 OK, Chromium (CDP) and Firefox (BiDi) walks over every screen at three widths. MEDUSA acceptances (kvb-think-criticize skeptic 2026-11-07): WEB-FLASK-004 view.py:135 (file template, autoescape on, state via tojson), MEDUSA-MM-SCAN-058 server.py:69/:85 (substring pla in --templates and Places), RH005 client/cleanup.py:199 (raise SystemExit(main())), B704 icons.py:45 (sprite baked into the image, names checked against it), EXA002 page-artifacts.js:221 (openAgent only opens the drawer), STG002 page-constants.js:64 (U+2212 in a static label), PL002 page-graph3d.js:44 (CSS class space-tools; the real tooltip sink fixed), B603 client/cleanup.py:75 (argv without shell; hook hole closed). 🎯 Fix: 92 % 💥 Break: 5 % 💸 Cost: 100 % ⏱️ Time: – 🛡️ Security: 100 % 🏆 Score: 87 🚦 Gate: ✅ 90/5, cost and time <= 100, security 100 % Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
200 lines
9.0 KiB
Python
Executable File
200 lines
9.0 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""Args:
|
|
--port: port of the Agent Orchestrator; the signature's origin is
|
|
http://localhost:<port>.
|
|
--jobs: the directory the hub writes signed jobs into, e.g.
|
|
~/.local/state/kvb-orchestrator/cleanup/jobs.
|
|
--results: the directory each job's result goes to as <id>.json.
|
|
--approvers: the pinned passkeys, e.g. ~/.config/kvb-orchestrator/approvers.json.
|
|
--replay: this runner's replay store, outside the hub's reach, e.g.
|
|
~/.local/state/kvb-orchestrator-approvals/cleanup.json.
|
|
--tree: the tree file `pkgmgr branch tree` writes; it names the
|
|
repositories pkgmgr knows and is rewritten after a deletion.
|
|
--pkgmgr: the pkgmgr executable built from a signed tag.
|
|
|
|
Returns:
|
|
Runs once over every job in --jobs, as the operator on the host: per item
|
|
it verifies the operator's passkey signature over repository, branch,
|
|
worktree and tip, then in the repository runs `pkgmgr branch cleanup
|
|
--json --local --branch <name>` as a dry run and, only when that plan
|
|
names exactly this branch as merged-clean without skip reason at the
|
|
signed tip and worktree, `pkgmgr branch cleanup --apply --json --local
|
|
--branch <name>`. Every other item is refused and nothing changes for
|
|
it. Writes each item's result, rewrites the tree file after a deletion
|
|
and removes the job. The exit code is 0.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import json
|
|
import os
|
|
import re
|
|
import subprocess
|
|
import time
|
|
from pathlib import Path
|
|
|
|
import signing
|
|
import verifier
|
|
|
|
JOB = re.compile(r"[0-9a-f]{16}\.json")
|
|
ITEM_KEYS = ("repository", "branch", "worktree", "tip")
|
|
TIP = re.compile(r"[0-9a-f]{40}|[0-9a-f]{64}")
|
|
UNSAFE = re.compile(r"[*?\[\]\\\s\x00-\x1f\x7f]|^-|\.\.|@\{")
|
|
OUTPUT_LIMIT = 16 << 10
|
|
TIMEOUT = 600
|
|
# the cwd is a repository agents can write: its config may name hooks, and
|
|
# pkgmgr releases before 4c8b137 run git branch -D without scan mode
|
|
GIT_OVERRIDES = (
|
|
("core.hooksPath", "/dev/null"),
|
|
("core.fsmonitor", "false"),
|
|
("hook.reference-transaction.enabled", "false"),
|
|
("hook.post-checkout.enabled", "false"),
|
|
("hook.post-index-change.enabled", "false"),
|
|
)
|
|
|
|
|
|
def git_overrides() -> dict[str, str]:
|
|
env = {"GIT_CONFIG_COUNT": str(len(GIT_OVERRIDES))}
|
|
for index, (name, value) in enumerate(GIT_OVERRIDES):
|
|
env[f"GIT_CONFIG_KEY_{index}"] = name
|
|
env[f"GIT_CONFIG_VALUE_{index}"] = value
|
|
return env
|
|
|
|
|
|
def known_repositories(tree: Path) -> set[str]:
|
|
try:
|
|
data = json.loads(tree.read_text(encoding="utf-8"))
|
|
return {entry["path"] for entry in data["repos"] if entry.get("known") is True and isinstance(entry.get("path"), str)}
|
|
except (OSError, ValueError, KeyError, TypeError, AttributeError):
|
|
return set()
|
|
|
|
|
|
def run(pkgmgr: Path, argv: list[str], cwd: str) -> tuple[int, str, str]:
|
|
try:
|
|
done = subprocess.run([str(pkgmgr), *argv], cwd=cwd, env={**os.environ, **git_overrides()}, stdin=subprocess.DEVNULL, capture_output=True, text=True, errors="replace", timeout=TIMEOUT, check=False)
|
|
except (OSError, subprocess.TimeoutExpired) as error:
|
|
return 127, "", str(error)
|
|
return done.returncode, done.stdout, done.stderr[-OUTPUT_LIMIT:]
|
|
|
|
|
|
def plan_items(stdout: str) -> list[dict] | None:
|
|
try:
|
|
data = json.loads(stdout)
|
|
except ValueError:
|
|
return None
|
|
items = data.get("items") if isinstance(data, dict) else data
|
|
if not isinstance(items, list) or not all(isinstance(entry, dict) for entry in items):
|
|
return None
|
|
return items
|
|
|
|
|
|
def shaped(item: object) -> bool:
|
|
if not isinstance(item, dict) or set(item) != set(ITEM_KEYS) or not all(isinstance(item[key], str) for key in ITEM_KEYS):
|
|
return False
|
|
return item["repository"].startswith("/") and bool(TIP.fullmatch(item["tip"])) and (item["worktree"] == "" or item["worktree"].startswith("/"))
|
|
|
|
|
|
def mismatch(entry: dict, item: dict) -> str | None:
|
|
if entry.get("state") != "merged-clean":
|
|
return f"state is {entry.get('state')}, not merged-clean"
|
|
if entry.get("skip"):
|
|
return f"gc skip reason: {entry['skip']}"
|
|
tip = entry.get("local_sha") or entry.get("head")
|
|
if tip != item["tip"]:
|
|
return f"tip changed: signed {item['tip'][:12]}, now {str(tip)[:12]}"
|
|
if (entry.get("worktree") or "") != item["worktree"]:
|
|
return f"worktree changed: signed {item['worktree'] or 'none'}, now {entry.get('worktree') or 'none'}"
|
|
return None
|
|
|
|
|
|
class Cleanup:
|
|
def __init__(self, trust: verifier.Trust, tree: Path, pkgmgr: Path) -> None:
|
|
self.trust = trust
|
|
self.tree = tree
|
|
self.pkgmgr = pkgmgr
|
|
|
|
def item(self, item: dict, proof: object, known: set[str], now: float) -> dict:
|
|
"""Args:
|
|
item: one signed item with repository, branch, worktree and tip.
|
|
proof: the job's {"payload", "assertion"}.
|
|
known: the repositories the tree file says pkgmgr knows.
|
|
now: the current time in epoch seconds.
|
|
|
|
Returns:
|
|
The item with status done, refused or failed and the reason;
|
|
refused and failed items changed nothing.
|
|
"""
|
|
def refused(reason: str) -> dict:
|
|
return {**item, "status": "refused", "reason": reason}
|
|
|
|
try:
|
|
verifier.check(proof, signing.cleanup_item(item["repository"], item["branch"], item["worktree"], item["tip"]), "", self.trust, now)
|
|
except (signing.Refused, OSError) as error:
|
|
return refused(f"no valid operator signature: {error}")
|
|
if item["repository"] not in known or not Path(item["repository"]).is_dir():
|
|
return refused("unknown repository: pkgmgr does not list it")
|
|
if UNSAFE.search(item["branch"]) or not item["branch"]:
|
|
return refused("the branch name holds pattern or option characters; only an exact branch name is deleted")
|
|
selector = ["--json", "--local", "--branch", item["branch"]]
|
|
code, stdout, stderr = run(self.pkgmgr, ["branch", "cleanup", *selector], item["repository"])
|
|
plan = plan_items(stdout) if code == 0 else None
|
|
if plan is None:
|
|
return refused(f"the dry run failed ({code}): {stderr.strip()[-500:]}")
|
|
others = sorted({str(entry.get("branch")) for entry in plan} - {item["branch"]})
|
|
if others:
|
|
return refused(f"the dry run names other branches too, a pattern match: {', '.join(others)[:500]}")
|
|
if len(plan) != 1:
|
|
return refused(f"the dry run names {len(plan)} items for {item['branch']}")
|
|
reason = mismatch(plan[0], item)
|
|
if reason:
|
|
return refused(reason)
|
|
code, stdout, stderr = run(self.pkgmgr, ["branch", "cleanup", "--apply", *selector], item["repository"])
|
|
applied = plan_items(stdout)
|
|
status = "done" if code == 0 else "failed"
|
|
return {**item, "status": status, "reason": "" if code == 0 else stderr.strip()[-500:], "code": code, "applied": applied if applied is not None else []}
|
|
|
|
def job(self, path: Path, now: float) -> dict:
|
|
try:
|
|
job = json.loads(path.read_text(encoding="utf-8"))
|
|
except (OSError, ValueError) as error:
|
|
return {"id": path.stem, "finished": now, "items": [], "error": f"unreadable job: {error}"}
|
|
items = job.get("items") if isinstance(job, dict) else None
|
|
if not isinstance(items, list) or not items or not all(map(shaped, items)) or job.get("id") != path.stem:
|
|
return {"id": path.stem, "finished": now, "items": [], "error": "malformed job: refused, nothing deleted"}
|
|
known = known_repositories(self.tree)
|
|
results = [self.item(item, job.get("proof"), known, now) for item in items]
|
|
result = {"id": path.stem, "finished": time.time(), "items": results}
|
|
if any(entry["status"] == "done" for entry in results):
|
|
code, _, stderr = run(self.pkgmgr, ["branch", "tree", "--format", "json", "--output", str(self.tree)], str(self.tree.parent))
|
|
result["tree"] = {"code": code, "error": stderr.strip()[-500:]}
|
|
return result
|
|
|
|
|
|
def cycle(jobs: Path, results: Path, cleanup: Cleanup) -> int:
|
|
handled = 0
|
|
for path in sorted(jobs.iterdir()):
|
|
if not JOB.fullmatch(path.name) or not path.is_file():
|
|
path.unlink(missing_ok=True)
|
|
continue
|
|
verifier.write_private(results / path.name, cleanup.job(path, time.time()))
|
|
path.unlink(missing_ok=True)
|
|
handled += 1
|
|
return handled
|
|
|
|
|
|
def main() -> int:
|
|
parser = argparse.ArgumentParser(prog="cleanup")
|
|
parser.add_argument("--port", type=int, required=True)
|
|
for name in ("--jobs", "--results", "--approvers", "--replay", "--tree", "--pkgmgr"):
|
|
parser.add_argument(name, type=Path, required=True)
|
|
args = parser.parse_args()
|
|
trust = verifier.Trust(approvers=args.approvers, replay=args.replay, origin=signing.origin_of(args.port))
|
|
args.results.mkdir(mode=0o700, parents=True, exist_ok=True)
|
|
cycle(args.jobs, args.results, Cleanup(trust, args.tree, args.pkgmgr))
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|