feat(orchestrator): W3 artifacts tree, ancestry, gates and token-free cleanup
Artifacts show the branch tree with ancestry and gate marks. The cleanup runner calls pkgmgr branch cleanup with its real argv and parses its repos[*].items output, refuses any plan with a remote-branch action, marks an item done only when pkgmgr reports it done, and offers no delete for a branch a worktree holds. Every text states what the pkgmgr command does to volumes. Committed below the bar on the operator's word 2026-11-12 (no further per-bundle tests, one gate after the merge): the W3 e2e never ran; the client switches to --local once pkgmgr ships it. 🎯 Fix: 90 % 💥 Break: 6 % 💸 Cost: 30 % ⏱️ Time: 40 % 🛡️ Security: 100 % 🏆 Score: 100 🚦 Gate: ⛔ break 6 % > 5 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
fd8fc08603
commit
f7a59d43a5
33 files changed
+1998
-107
No files matched your search
@@ -0,0 +1,131 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import functools
|
||||
import importlib.util
|
||||
import sqlite3
|
||||
import time
|
||||
from contextlib import closing
|
||||
from pathlib import Path
|
||||
from types import ModuleType
|
||||
from urllib.parse import quote
|
||||
|
||||
from .errors import ToolError
|
||||
from .fields import decode, expect
|
||||
from .jobs import GATE_JOB
|
||||
from .signed import signing
|
||||
|
||||
GATES_SCRIPT = "kvb-check-gate/scripts/gates.py"
|
||||
GATES_DB = "gates.db"
|
||||
GATE_TASK = "gates"
|
||||
CHANGE_LIMIT = 100
|
||||
CHANGE_KEYS = ("kind", "argv")
|
||||
|
||||
|
||||
def gate_request(raw: object) -> bool:
|
||||
return isinstance(raw, list) and bool(raw) and all(isinstance(entry, dict) and entry.get("kind") == GATE_JOB for entry in raw)
|
||||
|
||||
|
||||
def shown_command(argv: list[str]) -> str:
|
||||
return " ".join(["gates.py", *argv])
|
||||
|
||||
|
||||
def gates_module(script: Path) -> ModuleType | None:
|
||||
"""Returns:
|
||||
The installed gates.py as a module, for its resolve() and
|
||||
parents_of(), which read only the database; loaded again after an
|
||||
install changed it, None when it is missing or does not load.
|
||||
"""
|
||||
try:
|
||||
changed = script.stat().st_mtime_ns
|
||||
except OSError:
|
||||
return None
|
||||
return loaded(script, changed)
|
||||
|
||||
|
||||
@functools.cache
|
||||
def loaded(script: Path, changed: int) -> ModuleType | None:
|
||||
spec = importlib.util.spec_from_file_location(f"kvb_orchestrator_gates_{changed}", script)
|
||||
if spec is None:
|
||||
return None
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
try:
|
||||
spec.loader.exec_module(module)
|
||||
except (OSError, ImportError, RuntimeError, SyntaxError):
|
||||
return None
|
||||
return module
|
||||
|
||||
|
||||
def branch_gates(gates: ModuleType, db: sqlite3.Connection, repo: str, branch: str) -> dict:
|
||||
return {
|
||||
"parents": [{"parent": parent, "source": source} for parent, source in gates.parents_of(db, repo, branch)],
|
||||
"rules": dict(db.execute("SELECT action, level FROM rules WHERE repo = ? AND branch = ?", (repo, branch))),
|
||||
"resolved": {action: dict(zip(("level", "source"), gates.resolve(db, repo, branch, action))) for action in gates.ACTIONS},
|
||||
}
|
||||
|
||||
|
||||
def read_gates(gates: ModuleType | None, db: Path, scan: dict) -> dict:
|
||||
"""Args:
|
||||
gates: the gates.py module, or None.
|
||||
db: gates.db under the read-only mount of ~/.local/state/agents-council.
|
||||
scan: the tree; its repositories and branches are the ones shown.
|
||||
|
||||
Returns:
|
||||
{repo: {branch: {parents, rules, resolved}}} as `gates.py show --json`
|
||||
prints it, read with the database opened read-only and without git;
|
||||
{} without the script or a readable database.
|
||||
"""
|
||||
if gates is None or not db.is_file():
|
||||
return {}
|
||||
try:
|
||||
with closing(sqlite3.connect(f"file:{quote(str(db))}?mode=ro", uri=True)) as connection:
|
||||
return {repo["path"]: {entry["name"]: branch_gates(gates, connection, repo["path"], entry["name"]) for entry in repo["branches"]} for repo in scan["repos"]}
|
||||
except sqlite3.Error:
|
||||
return {}
|
||||
|
||||
|
||||
class Gating:
|
||||
def gate_view(self, scan: dict) -> dict:
|
||||
try:
|
||||
return read_gates(gates_module(self.scripts.skills / GATES_SCRIPT), self.kept / GATES_DB, scan)
|
||||
except Exception: # noqa: BLE001
|
||||
return {}
|
||||
|
||||
def gate_changes(self, raw: object) -> list[dict]:
|
||||
"""Args:
|
||||
raw: the page's items, each {"kind": "gate", "argv"} with the
|
||||
gates.py arguments of one rule set|drop or parents add|drop;
|
||||
the staged changes go under one passkey assertion.
|
||||
|
||||
Returns:
|
||||
The signed items, each with an argv signing.gate_argv() accepts.
|
||||
"""
|
||||
if not isinstance(raw, list) or not 1 <= len(raw) <= CHANGE_LIMIT:
|
||||
raise ToolError(f"items must be a list of 1 to {CHANGE_LIMIT} gate changes")
|
||||
signed: list[dict] = []
|
||||
for entry in raw:
|
||||
if not isinstance(entry, dict):
|
||||
raise ToolError("every item must be an object with kind and argv")
|
||||
expect(entry, CHANGE_KEYS)
|
||||
try:
|
||||
item = signing.gate_item(signing.gate_argv(entry["argv"]))
|
||||
except signing.Refused as error:
|
||||
raise ToolError(f"gate change: {error}") from None
|
||||
if item in signed:
|
||||
raise ToolError("items must be distinct")
|
||||
signed.append(item)
|
||||
return signed
|
||||
|
||||
def change_gates(self, body: bytes) -> dict:
|
||||
payload = decode(body)
|
||||
expect(payload, ("items", "proof"))
|
||||
items = self.gate_changes(payload["items"])
|
||||
proof = self.covered(payload["proof"], items, time.time())
|
||||
for item in items:
|
||||
self.operator_proven(proof, item)
|
||||
ids = []
|
||||
for item in items:
|
||||
fields = {"kind": GATE_JOB, "task": GATE_TASK, "argv": item["argv"], "command": shown_command(item["argv"]), "directory": "", "sha256": "", "status": "queued", "proof": proof}
|
||||
ids.append(self.jobs.add({**fields, "code": None, "output": "", "created_ts": time.time(), "started": None, "finished": None})["id"])
|
||||
commands = [shown_command(item["argv"]) for item in items]
|
||||
self.log.add({"kind": "run", "title": f"Gate change signed: {len(items)}", "text": "\n".join(commands), "ref": {"jobs": ids, "status": "queued"}})
|
||||
return {"jobs": ids, "status": "queued"}
|
||||
Reference in new issue
Block a user