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
@@ -16,12 +16,17 @@ 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.
|
||||
--json --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 and deletes no remote branch, `pkgmgr branch cleanup
|
||||
--apply --json --branch <name>`. While the tree file says pkgmgr has no GitHub token
|
||||
(delete_ready false), only a branch the dry run shows merged by ancestry
|
||||
into the default branch and without a remote branch is deleted; the
|
||||
others are refused with "token needed". Every other item is refused and
|
||||
nothing changes for it. An applied item is done only when pkgmgr's
|
||||
result for it is done; a skipped one or one without result is failed
|
||||
with pkgmgr's reason. 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
|
||||
@@ -43,6 +48,9 @@ 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
|
||||
DONE = "done"
|
||||
REMOTE_ACTION = "remote-branch:"
|
||||
TOKEN_NEEDED = "token needed: without the GitHub token only a local branch merged by ancestry into the default branch and without a remote branch is deleted"
|
||||
# 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 = (
|
||||
@@ -62,12 +70,18 @@ def git_overrides() -> dict[str, str]:
|
||||
return env
|
||||
|
||||
|
||||
def known_repositories(tree: Path) -> set[str]:
|
||||
def tree_facts(tree: Path) -> tuple[dict[str, str], bool]:
|
||||
"""Returns:
|
||||
The repositories the tree file says pkgmgr knows, each with its
|
||||
default branch, and whether pkgmgr found the GitHub token
|
||||
(delete_ready); nothing and False for an unreadable file.
|
||||
"""
|
||||
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)}
|
||||
known = {entry["path"]: entry.get("default") if isinstance(entry.get("default"), str) else "" for entry in data["repos"] if entry.get("known") is True and isinstance(entry.get("path"), str)}
|
||||
return known, data.get("delete_ready") is True
|
||||
except (OSError, ValueError, KeyError, TypeError, AttributeError):
|
||||
return set()
|
||||
return {}, False
|
||||
|
||||
|
||||
def run(pkgmgr: Path, argv: list[str], cwd: str) -> tuple[int, str, str]:
|
||||
@@ -78,15 +92,32 @@ def run(pkgmgr: Path, argv: list[str], cwd: str) -> tuple[int, str, str]:
|
||||
return done.returncode, done.stdout, done.stderr[-OUTPUT_LIMIT:]
|
||||
|
||||
|
||||
def plan_argv(branch: str, apply: bool) -> list[str]:
|
||||
"""Args:
|
||||
branch: the exact branch name.
|
||||
apply: execute the plan instead of the dry run.
|
||||
|
||||
Returns:
|
||||
The `pkgmgr branch cleanup` arguments for this branch's plan in JSON.
|
||||
"""
|
||||
return ["branch", "cleanup", *(["--apply"] if apply else []), "--json", "--branch", branch]
|
||||
|
||||
|
||||
def plan_items(stdout: str) -> list[dict] | None:
|
||||
"""Returns:
|
||||
Every item of `pkgmgr branch cleanup --json`, which prints
|
||||
{"host_pid_namespace", "repos": [{"repo", "default", "merged_into",
|
||||
"warnings", "items"}]}; None for any other output.
|
||||
"""
|
||||
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):
|
||||
repos = data.get("repos") if isinstance(data, dict) else None
|
||||
if not isinstance(repos, list) or not all(isinstance(repo, dict) and isinstance(repo.get("items"), list) for repo in repos):
|
||||
return None
|
||||
return items
|
||||
items = [entry for repo in repos for entry in repo["items"]]
|
||||
return items if all(isinstance(entry, dict) for entry in items) else None
|
||||
|
||||
|
||||
def shaped(item: object) -> bool:
|
||||
@@ -105,6 +136,9 @@ def mismatch(entry: dict, item: dict) -> str | None:
|
||||
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'}"
|
||||
remote = [str(action) for action in entry.get("actions") or [] if str(action).startswith(REMOTE_ACTION)]
|
||||
if remote:
|
||||
return f"the plan also deletes {', '.join(remote)[:200]}; a signed delete is local only"
|
||||
return None
|
||||
|
||||
|
||||
@@ -114,16 +148,20 @@ class Cleanup:
|
||||
self.tree = tree
|
||||
self.pkgmgr = pkgmgr
|
||||
|
||||
def item(self, item: dict, proof: object, known: set[str], now: float) -> dict:
|
||||
def item(self, item: dict, proof: object, known: dict[str, str], ready: bool, 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.
|
||||
known: the repositories the tree file says pkgmgr knows, each
|
||||
with its default branch.
|
||||
ready: whether pkgmgr holds the GitHub token; without it only a
|
||||
branch signing.token_free() accepts in the dry run is deleted.
|
||||
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.
|
||||
refused items changed nothing, failed ones are those pkgmgr did
|
||||
not report done.
|
||||
"""
|
||||
def refused(reason: str) -> dict:
|
||||
return {**item, "status": "refused", "reason": reason}
|
||||
@@ -136,8 +174,7 @@ class Cleanup:
|
||||
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"])
|
||||
code, stdout, stderr = run(self.pkgmgr, plan_argv(item["branch"], False), 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:]}")
|
||||
@@ -149,10 +186,15 @@ class Cleanup:
|
||||
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 []}
|
||||
if not ready and not signing.token_free(plan[0], known[item["repository"]]):
|
||||
return refused(TOKEN_NEEDED)
|
||||
code, stdout, stderr = run(self.pkgmgr, plan_argv(item["branch"], True), item["repository"])
|
||||
applied = plan_items(stdout) or []
|
||||
outcome = next((entry.get("result") for entry in applied if entry.get("branch") == item["branch"]), None)
|
||||
if code == 0 and isinstance(outcome, str) and outcome.split(";")[0] == DONE:
|
||||
return {**item, "status": "done", "reason": "", "code": code, "applied": applied}
|
||||
reason = outcome if isinstance(outcome, str) and outcome else stderr.strip()[-500:] or f"pkgmgr reported no result for {item['branch']}"
|
||||
return {**item, "status": "failed", "reason": reason[:500], "code": code, "applied": applied}
|
||||
|
||||
def job(self, path: Path, now: float) -> dict:
|
||||
try:
|
||||
@@ -162,8 +204,8 @@ class Cleanup:
|
||||
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]
|
||||
known, ready = tree_facts(self.tree)
|
||||
results = [self.item(item, job.get("proof"), known, ready, 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))
|
||||
|
||||
@@ -10,6 +10,9 @@
|
||||
~/.claude/skills/kvb-org-rasci/scripts/rasci.py; optional.
|
||||
--council: the council directory holding rasci.json, the agents'
|
||||
AGENTS_COUNCIL_ROOT; optional, task changes need both.
|
||||
--gates: the installed gates script, e.g.
|
||||
~/.claude/skills/kvb-check-gate/scripts/gates.py; optional, gate
|
||||
changes need it.
|
||||
--once: handle the queued jobs once and exit.
|
||||
|
||||
Returns:
|
||||
@@ -18,9 +21,12 @@ Returns:
|
||||
in the signed directory, each signature once and before its expiry.
|
||||
Runs every signed task change (priority pin or WSJF estimate) as
|
||||
`python3 -I -B <rasci> <argv>` from / without a shell, after checking the
|
||||
argv shape itself, and kills its process group after 30 s. Refuses task
|
||||
changes without --rasci and --council or into a council that is a
|
||||
symlink or another uid's, and every other job. Posts the last 200 output lines
|
||||
argv shape itself, and kills its process group after 30 s. Runs every
|
||||
signed gate change (rule set|drop, parents add|drop) the same way as
|
||||
`python3 -I -B <gates> <argv>` with the operator's HOME, so gates.py
|
||||
writes its default gates.db. Refuses task changes without --rasci and
|
||||
--council or into a council that is a symlink or another uid's, gate
|
||||
changes without --gates, and every other job. Posts the last 200 output lines
|
||||
back while it runs and the exit code at its end. The exit code is 0.
|
||||
"""
|
||||
|
||||
@@ -52,8 +58,10 @@ BEAT = 2.0
|
||||
SAFE_PATH = "/usr/bin:/bin"
|
||||
TASK_JOB = "task"
|
||||
TASK_LIMIT = 30.0
|
||||
GATE_JOB = "gate"
|
||||
REFUSED = "refused: no valid operator signature for this command"
|
||||
UNREGISTERED = "refused: this runner was started without --rasci and --council; reinstall its unit with make orchestrator-runner"
|
||||
UNGATED = "refused: this runner was started without --gates; reinstall its unit with make orchestrator-runner"
|
||||
|
||||
|
||||
class Register(NamedTuple):
|
||||
@@ -113,6 +121,24 @@ def task_refusal(job: dict, trust: verifier.Trust, now: float) -> str | None:
|
||||
return None
|
||||
|
||||
|
||||
def gate_refusal(job: dict, trust: verifier.Trust, now: float) -> str | None:
|
||||
"""Args:
|
||||
job: the claimed gate change with argv and proof.
|
||||
trust: the pinned keys, the runner's replay store and the origin.
|
||||
now: the current time in epoch seconds.
|
||||
|
||||
Returns:
|
||||
Why the change must not run, or None when signing.gate_argv()
|
||||
accepts its argv and a pinned key signed it.
|
||||
"""
|
||||
try:
|
||||
argv = signing.gate_argv(job["argv"])
|
||||
verifier.check(job.get("proof"), signing.gate_item(argv), "", trust, now)
|
||||
except (signing.Refused, OSError) as error:
|
||||
return f"{REFUSED}: {error}"
|
||||
return None
|
||||
|
||||
|
||||
def council_refusal(council: Path) -> str | None:
|
||||
"""Args:
|
||||
council: the directory rasci.py writes the register into.
|
||||
@@ -161,6 +187,23 @@ def change(job: dict, register: Register, report: Callable[[dict], None]) -> int
|
||||
return spawn([sys.executable, "-I", "-B", str(register.rasci), *argv], "/", env, report, TASK_LIMIT)
|
||||
|
||||
|
||||
def gate(job: dict, gates: Path, report: Callable[[dict], None]) -> int:
|
||||
"""Args:
|
||||
job: the claimed and verified gate change with argv.
|
||||
gates: the gates script.
|
||||
report: called with {"output", "code", "refused"}.
|
||||
|
||||
Returns:
|
||||
The exit code of gates.py, run from / on the argv gate_argv() checked.
|
||||
|
||||
Raises:
|
||||
signing.Refused for an argv of any other shape.
|
||||
"""
|
||||
argv = signing.gate_argv(job["argv"])
|
||||
env = {"PATH": SAFE_PATH, "HOME": str(Path.home())}
|
||||
return spawn([sys.executable, "-I", "-B", str(gates), *argv], "/", env, report, TASK_LIMIT)
|
||||
|
||||
|
||||
def spawn(argv: list[str], cwd: str, env: dict[str, str] | None, report: Callable[[dict], None], limit: float | None = None) -> int:
|
||||
"""Args:
|
||||
argv: the program and its arguments, run without a shell.
|
||||
@@ -224,22 +267,26 @@ def sender(base: str, job_id: str) -> Callable[[dict], None]:
|
||||
return send
|
||||
|
||||
|
||||
def cycle(base: str, trust: verifier.Trust, register: Register | None) -> None:
|
||||
def cycle(base: str, trust: verifier.Trust, register: Register | None, gates: Path | None = None) -> None:
|
||||
for job in call(base, "/api/runner/jobs", None)["jobs"]:
|
||||
try:
|
||||
claimed = call(base, f"/api/runner/{job['id']}/claim", {})
|
||||
except urllib.error.HTTPError:
|
||||
continue
|
||||
send = sender(base, claimed["id"])
|
||||
task = claimed.get("kind") == TASK_JOB
|
||||
if not task:
|
||||
reason = refusal(claimed, trust, time.time())
|
||||
else:
|
||||
kind = claimed.get("kind")
|
||||
if kind == TASK_JOB:
|
||||
reason = UNREGISTERED if register is None else council_refusal(register.council) or task_refusal(claimed, trust, time.time())
|
||||
elif kind == GATE_JOB:
|
||||
reason = UNGATED if gates is None else gate_refusal(claimed, trust, time.time())
|
||||
else:
|
||||
reason = refusal(claimed, trust, time.time())
|
||||
if reason is not None:
|
||||
send({"output": reason, "code": None, "refused": True})
|
||||
elif task:
|
||||
elif kind == TASK_JOB:
|
||||
change(claimed, register, send)
|
||||
elif kind == GATE_JOB:
|
||||
gate(claimed, gates, send)
|
||||
else:
|
||||
execute(claimed, send)
|
||||
|
||||
@@ -252,6 +299,7 @@ def main() -> int:
|
||||
parser.add_argument("--replay", type=Path, required=True)
|
||||
parser.add_argument("--rasci", type=Path)
|
||||
parser.add_argument("--council", type=Path)
|
||||
parser.add_argument("--gates", type=Path)
|
||||
parser.add_argument("--once", action="store_true")
|
||||
args = parser.parse_args()
|
||||
base = hub_api.base_url(args.port)
|
||||
@@ -259,7 +307,7 @@ def main() -> int:
|
||||
register = Register(rasci=args.rasci, council=args.council) if args.rasci and args.council else None
|
||||
while True:
|
||||
try:
|
||||
cycle(base, trust, register)
|
||||
cycle(base, trust, register, args.gates)
|
||||
except (OSError, ValueError, KeyError, TypeError):
|
||||
pass
|
||||
if args.once:
|
||||
|
||||
@@ -27,6 +27,11 @@ PRIO = re.compile(r"[1-9][0-9]{0,2}")
|
||||
FIBONACCI = ("1", "2", "3", "5", "8", "13", "21", "34", "55", "89")
|
||||
ESTIMATOR = "Operator"
|
||||
COMMANDS = "operator/"
|
||||
GATE_LEVELS = ("none", "quick", "test", "quality-high", "e2e")
|
||||
GATE_ACTIONS = ("commit", "merge", "push", "pr")
|
||||
GATE_REPO = re.compile(r"/[^\x00-\x1f\x7f]{0,4095}")
|
||||
GATE_BRANCH = re.compile(r"(?![-/])(?!.*\.\.)(?!.*@\{)(?!.*//)[^\x00-\x20\x7f~^:?*\[\\]{1,255}(?<![/.])")
|
||||
TOKEN_FREE_MERGE = "ancestor"
|
||||
APPROVAL_KEYS = {"v", "kind", "items", "nonce", "expires"}
|
||||
RULE_KEYS = {"v", "kind", "id", "kinds", "tool", "pattern", "scope", "expires"}
|
||||
WARNINGS = (
|
||||
@@ -184,6 +189,72 @@ def task_argv(task: object, argv: object) -> list[str]:
|
||||
raise Refused("the argv is no task priority or WSJF estimate change of this task")
|
||||
|
||||
|
||||
def gate_item(argv: list[str]) -> dict:
|
||||
return {"kind": "gate", "argv": argv}
|
||||
|
||||
|
||||
def gate_argv(argv: object) -> list[str]:
|
||||
"""Args:
|
||||
argv: the gates.py arguments, one of `rule set --repo R --branch B
|
||||
--action A --level L`, `rule drop --repo R --branch B --action A`,
|
||||
`parents add --repo R --branch B --parent P` (source operator) or
|
||||
`parents drop --repo R --branch B --parent P`; R an absolute path,
|
||||
B and P branch names, P not B.
|
||||
|
||||
Returns:
|
||||
argv, the hub and the runner check it the same way.
|
||||
|
||||
Raises:
|
||||
Refused for every other argv.
|
||||
"""
|
||||
if not isinstance(argv, list) or not all(isinstance(part, str) for part in argv):
|
||||
raise Refused("the argv is no list of texts")
|
||||
head, values = argv[:2], dict(zip(argv[2::2], argv[3::2]))
|
||||
names = {("rule", "set"): ("--repo", "--branch", "--action", "--level"), ("rule", "drop"): ("--repo", "--branch", "--action"), ("parents", "add"): ("--repo", "--branch", "--parent"), ("parents", "drop"): ("--repo", "--branch", "--parent")}.get(tuple(head))
|
||||
if names is None or argv[2::2] != list(names) or len(argv) != 2 + 2 * len(names):
|
||||
raise Refused("the argv is no gates.py rule set|drop or parents add|drop of one branch")
|
||||
repo = values["--repo"]
|
||||
if not GATE_REPO.fullmatch(repo) or "/../" in f"{repo}/" or "/./" in f"{repo}/":
|
||||
raise Refused("the repository is no absolute path")
|
||||
branches = [values[name] for name in ("--branch", "--parent") if name in values]
|
||||
if not all(GATE_BRANCH.fullmatch(name) for name in branches) or len(set(branches)) != len(branches):
|
||||
raise Refused("the branch or parent is no branch name, or the branch is its own parent")
|
||||
if values.get("--action", GATE_ACTIONS[0]) not in GATE_ACTIONS or values.get("--level", GATE_LEVELS[0]) not in GATE_LEVELS:
|
||||
raise Refused(f"the action must be one of {', '.join(GATE_ACTIONS)} and the level one of {', '.join(GATE_LEVELS)}")
|
||||
return argv
|
||||
|
||||
|
||||
def names_default(ref: object, default: object) -> bool:
|
||||
"""Args:
|
||||
ref: a ref as pkgmgr names it, bare such as main or behind one
|
||||
remote such as origin/main, the remote-tracking ref it compares
|
||||
against when the remote has a HEAD.
|
||||
default: the repository's default branch as the tree file names it,
|
||||
without the remote (pkgmgr tree.py strips it).
|
||||
|
||||
Returns:
|
||||
True when ref is the default branch.
|
||||
"""
|
||||
if not isinstance(ref, str) or not isinstance(default, str) or not default:
|
||||
return False
|
||||
remote, _, name = ref.partition("/")
|
||||
return ref == default or (bool(remote) and name == default)
|
||||
|
||||
|
||||
def token_free(entry: dict, default: object) -> bool:
|
||||
"""Args:
|
||||
entry: a pkgmgr tree or gc plan item with merged_by, merged_into and
|
||||
remotes.
|
||||
default: the repository's default branch as the tree file names it.
|
||||
|
||||
Returns:
|
||||
True for a local branch merged by ancestry into the default branch
|
||||
(names_default) that has no remote branch: deleting it needs no
|
||||
GitHub token.
|
||||
"""
|
||||
return entry.get("merged_by") == TOKEN_FREE_MERGE and names_default(entry.get("merged_into"), default) and not entry.get("remotes")
|
||||
|
||||
|
||||
def approval_payload(hashes: list[str], nonce: str, expires: int) -> dict:
|
||||
return {"v": VERSION, "kind": "approve", "items": sorted(set(hashes)), "nonce": nonce, "expires": expires}
|
||||
|
||||
|
||||
Reference in new issue
Block a user