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>
147 lines
6.1 KiB
Python
147 lines
6.1 KiB
Python
from __future__ import annotations
|
|
|
|
import base64
|
|
import fnmatch
|
|
import hashlib
|
|
import json
|
|
import re
|
|
from pathlib import Path
|
|
|
|
RP_ID = "localhost"
|
|
VERSION = 1
|
|
LIFETIME = 900
|
|
RULE_LIFETIME = 366 * 86400
|
|
ALGORITHMS = {-7: "ES256", -8: "EdDSA", -257: "RS256"}
|
|
HASH = re.compile(r"[0-9a-f]{64}")
|
|
NONCE = re.compile(r"[0-9a-f]{32}")
|
|
ID = re.compile(r"[0-9a-f]{16}")
|
|
CHAINED = re.compile(r"[;&|`$<>()\n\r]")
|
|
APPROVAL_KEYS = {"v", "kind", "items", "nonce", "expires"}
|
|
RULE_KEYS = {"v", "kind", "id", "kinds", "tool", "pattern", "scope", "expires"}
|
|
WARNINGS = (
|
|
("löscht rekursiv", re.compile(r"\brm\s+(?:-\S+\s+)*-\S*[rR]")),
|
|
("erzwingt", re.compile(r"--force\b|\bpush\b.*\s-f\b")),
|
|
("pusht", re.compile(r"\bgit\b.*\bpush\b")),
|
|
("root", re.compile(r"\bsudo\b|\bdoas\b")),
|
|
("Pipe in eine Shell", re.compile(r"\|\s*(?:sudo\s+)?(?:ba|z|k|da)?sh\b")),
|
|
("verwirft Änderungen", re.compile(r"\bgit\b.*\b(?:reset\s+--hard|clean\s+-\S*f)")),
|
|
("Docker löscht", re.compile(r"\bdocker\s+(?:system\s+prune|volume\s+(?:rm|prune)|rmi?\b)")),
|
|
)
|
|
|
|
|
|
class Refused(Exception):
|
|
pass
|
|
|
|
|
|
def origin_of(port: int) -> str:
|
|
return f"http://{RP_ID}:{port}"
|
|
|
|
|
|
def b64encode(data: bytes) -> str:
|
|
return base64.urlsafe_b64encode(data).rstrip(b"=").decode("ascii")
|
|
|
|
|
|
def canonical(value: object) -> bytes:
|
|
return json.dumps(value, sort_keys=True, separators=(",", ":"), ensure_ascii=True, allow_nan=False).encode("ascii")
|
|
|
|
|
|
def challenge(payload: dict) -> str:
|
|
return b64encode(hashlib.sha256(canonical(payload)).digest())
|
|
|
|
|
|
def item_hash(item: dict) -> str:
|
|
return hashlib.sha256(canonical(item)).hexdigest()
|
|
|
|
|
|
def ask_item(ask_id: str, tool: str, command: str, cwd: str, session: str) -> dict:
|
|
return {"kind": "ask", "id": ask_id, "tool": tool, "command": command, "cwd": cwd, "session": session}
|
|
|
|
|
|
def review_item(review_id: str, repo: str, worktree: str, branch: str, tree: str) -> dict:
|
|
return {"kind": "review", "id": review_id, "repo": repo, "worktree": worktree, "branch": branch, "tree": tree}
|
|
|
|
|
|
def operator_item(task: str, command: str, directory: str) -> dict:
|
|
return {"kind": "operator", "id": task, "command": command, "directory": directory}
|
|
|
|
|
|
def cleanup_item(repository: str, branch: str, worktree: str, tip: str) -> dict:
|
|
return {"kind": "cleanup", "repository": repository, "branch": branch, "worktree": worktree, "tip": tip}
|
|
|
|
|
|
def approval_payload(hashes: list[str], nonce: str, expires: int) -> dict:
|
|
return {"v": VERSION, "kind": "approve", "items": sorted(set(hashes)), "nonce": nonce, "expires": expires}
|
|
|
|
|
|
def rule_payload(rule_id: str, kinds: list[str], tool: str, pattern: str, scope: str, expires: int) -> dict:
|
|
return {"v": VERSION, "kind": "rule", "id": rule_id, "kinds": kinds, "tool": tool, "pattern": pattern, "scope": scope, "expires": expires}
|
|
|
|
|
|
def warnings(command: str) -> list[str]:
|
|
return [label for label, pattern in WARNINGS if pattern.search(command)]
|
|
|
|
|
|
def expiry(value: object, now: float, cap: int) -> int:
|
|
if isinstance(value, bool) or not isinstance(value, int):
|
|
raise Refused("the expiry is no whole number of epoch seconds")
|
|
if value <= now:
|
|
raise Refused("the signature has expired")
|
|
if value > now + cap:
|
|
raise Refused("the expiry lies too far ahead")
|
|
return value
|
|
|
|
|
|
def checked_approval(payload: object, now: float) -> dict:
|
|
if not isinstance(payload, dict) or set(payload) != APPROVAL_KEYS or payload["v"] != VERSION or payload["kind"] != "approve":
|
|
raise Refused("the payload is no approval")
|
|
items = payload["items"]
|
|
if not isinstance(items, list) or not items or not all(isinstance(entry, str) and HASH.fullmatch(entry) for entry in items):
|
|
raise Refused("the approval lists no item hashes")
|
|
if not isinstance(payload["nonce"], str) or not NONCE.fullmatch(payload["nonce"]):
|
|
raise Refused("the approval holds no nonce")
|
|
expiry(payload["expires"], now, LIFETIME)
|
|
return payload
|
|
|
|
|
|
def checked_rule(payload: object, now: float) -> dict:
|
|
if not isinstance(payload, dict) or set(payload) != RULE_KEYS or payload["v"] != VERSION or payload["kind"] != "rule":
|
|
raise Refused("the payload is no rule")
|
|
if not isinstance(payload["id"], str) or not ID.fullmatch(payload["id"]):
|
|
raise Refused("the rule holds no id")
|
|
kinds = payload["kinds"]
|
|
if not isinstance(kinds, list) or not kinds or not all(isinstance(kind, str) for kind in kinds):
|
|
raise Refused("the rule names no kinds")
|
|
if not isinstance(payload["tool"], str) or not all(isinstance(payload[name], str) and payload[name] for name in ("pattern", "scope")):
|
|
raise Refused("the rule needs a tool text, a pattern and a scope")
|
|
expiry(payload["expires"], now, RULE_LIFETIME)
|
|
return payload
|
|
|
|
|
|
def rule_allows(rule: dict, item: dict, gate: str, now: float) -> bool:
|
|
if rule["expires"] <= now or item["kind"] not in rule["kinds"]:
|
|
return False
|
|
if item["kind"] == "ask":
|
|
named = fnmatch.fnmatchcase(item["tool"], rule["tool"]) and fnmatch.fnmatchcase(item["cwd"], rule["scope"])
|
|
plain = not CHAINED.search(item["command"]) and not warnings(item["command"])
|
|
return named and plain and fnmatch.fnmatchcase(item["command"], rule["pattern"])
|
|
if item["kind"] == "review":
|
|
return gate == "pass" and fnmatch.fnmatchcase(item["branch"], rule["pattern"]) and fnmatch.fnmatchcase(item["worktree"], rule["scope"])
|
|
return False
|
|
|
|
|
|
def load_pinned(path: Path) -> list[dict]:
|
|
try:
|
|
mode = path.stat().st_mode
|
|
data = json.loads(path.read_text(encoding="utf-8"))
|
|
except FileNotFoundError:
|
|
return []
|
|
except (OSError, ValueError, RecursionError) as error:
|
|
raise Refused(f"the pinned keys in {path} are unreadable") from error
|
|
if mode & 0o022:
|
|
raise Refused(f"{path} is writable by others")
|
|
entries = data.get("credentials") if isinstance(data, dict) else None
|
|
shaped = isinstance(entries, list) and all(isinstance(entry, dict) and isinstance(entry.get("id"), str) and entry.get("alg") in ALGORITHMS and isinstance(entry.get("spki"), str) for entry in entries)
|
|
if not shaped:
|
|
raise Refused(f"the pinned keys in {path} are malformed")
|
|
return entries
|