The Agents views gain a Hierarchie view: an org chart of the chair and the line offices read from the council claims. The operator appoints, hands over and recalls an office there; the page signs each change with the passkey and a host unit (client/offices.py, offices path unit) verifies the signature, removes a live holder itself and runs a plain council.py claim for the successor. council.py loses the --operator flag, so no agent can override a live holder. The agent order is now separate from task priority: rasci.py agents order stores it in agent-order.json beside the register through the nofollow helpers and never writes a --prio pin. It only breaks ties of equal task rank, also among the queue grants. The Hierarchie view edits it as a drag list. Host units run the installed skill scripts, so orchestrator.sh keeps the offices unit off while the installed council.py, nofollow.py or rasci.py differ from the tree. Job files, council files and hub records are read with O_NOFOLLOW and O_NONBLOCK and size caps; nofollow.rewrite replaces a claim without creating directories, so a beat never revives a released office. 🎯 Fix: 92 % 💥 Break: 4 % 💸 Cost: 100 % ⏱️ Time: 79 % 🛡️ Security: 100 % 🏆 Score: 100 🚦 Gate: ✅ 90/5, cost and time <= 100, security 100 % Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
310 lines
12 KiB
Python
Executable File
310 lines
12 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 office orders into, e.g.
|
|
~/.local/state/kvb-orchestrator/offices/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/offices.json.
|
|
--council: the council directory every agent shares, e.g.
|
|
/tmp/agents-council.
|
|
--script: the installed council.py of the kvb-org-organize skill.
|
|
|
|
Returns:
|
|
Runs once over every job in --jobs, as the operator on the host: per item
|
|
it verifies the operator's passkey signature over action, office, holder,
|
|
session and work, refuses a council directory others can write and an
|
|
office path holding a link, and checks that the office still has the
|
|
signed holder. A takeover or recall then moves the office aside, reads its
|
|
claim there and removes it only while that claim names the signed holder,
|
|
else it puts the office back; an appoint or takeover ends in a plain
|
|
council.py claim for the successor, run as an argument list, never through
|
|
a shell: a live Claude Code successor goes in as CLAUDE_CODE_SESSION_ID,
|
|
any other as --session. Writes each item's result and removes the job.
|
|
The exit code is 0.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import errno
|
|
import json
|
|
import os
|
|
import re
|
|
import shutil
|
|
import stat
|
|
import subprocess
|
|
import sys
|
|
import time
|
|
from pathlib import Path
|
|
|
|
import signing
|
|
import verifier
|
|
|
|
JOB = re.compile(r"[0-9a-f]{16}\.json")
|
|
SAFE_PATH = "/usr/local/bin:/usr/bin:/bin"
|
|
CLAIM = "claim.json"
|
|
CLAIM_SHAPE = (("session", str), ("runtime", str), ("beat", (int, float)))
|
|
DIRECTORY = os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW | os.O_CLOEXEC
|
|
FILE = os.O_RDONLY | os.O_NOFOLLOW | os.O_NONBLOCK | os.O_CLOEXEC
|
|
READ_LIMIT = 1 << 20
|
|
OUTPUT_LIMIT = 4000
|
|
TIMEOUT = 120
|
|
|
|
|
|
def read_regular(name: str | Path, dir_fd: int | None) -> bytes:
|
|
"""Args:
|
|
name: the file, relative to dir_fd or a path; a link, FIFO or other
|
|
non-regular file raises OSError, so does one over READ_LIMIT bytes.
|
|
dir_fd: the open directory name is relative to, or None.
|
|
|
|
Returns:
|
|
The file's bytes.
|
|
"""
|
|
fd = os.open(name, FILE, dir_fd=dir_fd)
|
|
with open(fd, "rb") as handle:
|
|
if not stat.S_ISREG(os.fstat(fd).st_mode):
|
|
raise OSError(errno.EINVAL, "not a regular file", str(name))
|
|
data = handle.read(READ_LIMIT + 1)
|
|
if len(data) > READ_LIMIT:
|
|
raise OSError(errno.EFBIG, f"larger than {READ_LIMIT} bytes", str(name))
|
|
return data
|
|
|
|
|
|
def claim_in(office_fd: int) -> dict | None:
|
|
"""Returns:
|
|
The claim of the open office directory in the shape council.py
|
|
reads, or None.
|
|
"""
|
|
try:
|
|
claim = json.loads(read_regular(CLAIM, office_fd))
|
|
except (OSError, ValueError, RecursionError):
|
|
return None
|
|
shaped = isinstance(claim, dict) and all(isinstance(claim.get(key), kind) for key, kind in CLAIM_SHAPE)
|
|
return claim if shaped else None
|
|
|
|
|
|
def parent_of(council: Path, office: str) -> tuple[int, str]:
|
|
"""Returns:
|
|
The open directory holding the office, without following a link on
|
|
the way, and the office's last name; the caller closes it.
|
|
"""
|
|
*dirs, name = office.split("/")
|
|
fd = os.open(council, DIRECTORY)
|
|
try:
|
|
for part in dirs:
|
|
child = os.open(part, DIRECTORY, dir_fd=fd)
|
|
os.close(fd)
|
|
fd = child
|
|
except BaseException:
|
|
os.close(fd)
|
|
raise
|
|
return fd, name
|
|
|
|
|
|
def unsafe(council: Path, office: str) -> str | None:
|
|
"""Returns:
|
|
Why the council directory or the office's path below it must not be
|
|
used, or None.
|
|
"""
|
|
try:
|
|
root = council.lstat()
|
|
except OSError as error:
|
|
return f"the council directory is unreadable: {error.strerror}"
|
|
if not stat.S_ISDIR(root.st_mode) or root.st_uid != os.getuid() or root.st_mode & 0o022:
|
|
return f"{council} is no directory of uid {os.getuid()}, or others can write it"
|
|
path = council
|
|
for part in office.split("/"):
|
|
path = path / part
|
|
try:
|
|
found = path.lstat()
|
|
except FileNotFoundError:
|
|
return None
|
|
if not stat.S_ISDIR(found.st_mode):
|
|
return f"{path} is a link or no directory"
|
|
try:
|
|
found = (path / CLAIM).lstat()
|
|
except FileNotFoundError:
|
|
return None
|
|
return None if stat.S_ISREG(found.st_mode) else f"{path / CLAIM} is a link or no file"
|
|
|
|
|
|
def holder_of(council: Path, office: str) -> str | None:
|
|
"""Returns:
|
|
The session holding the office, empty while it is vacant, None while
|
|
its claim is missing, unreadable or not in the shape council.py reads.
|
|
"""
|
|
try:
|
|
parent, name = parent_of(council, office)
|
|
except FileNotFoundError:
|
|
return ""
|
|
except OSError:
|
|
return None
|
|
try:
|
|
fd = os.open(name, DIRECTORY, dir_fd=parent)
|
|
except FileNotFoundError:
|
|
return ""
|
|
except OSError:
|
|
return None
|
|
finally:
|
|
os.close(parent)
|
|
try:
|
|
claim = claim_in(fd)
|
|
finally:
|
|
os.close(fd)
|
|
return claim["session"] if claim else None
|
|
|
|
|
|
def vacate(council: Path, office: str, holder: str) -> str | None:
|
|
"""Args:
|
|
council: the council directory.
|
|
office: the office to empty.
|
|
holder: the session the operator signed as its holder.
|
|
|
|
Returns:
|
|
None once the office is gone, else why nothing changed.
|
|
"""
|
|
parent, name = parent_of(council, office)
|
|
try:
|
|
tomb = f"{name}.recalled-{os.getpid()}-{time.time_ns()}"
|
|
try:
|
|
os.rename(name, tomb, src_dir_fd=parent, dst_dir_fd=parent)
|
|
except FileNotFoundError:
|
|
return f"the holder changed since the signature: signed {holder}, now vacant; nothing changed"
|
|
try:
|
|
fd = os.open(tomb, DIRECTORY, dir_fd=parent)
|
|
except OSError:
|
|
claim = None
|
|
else:
|
|
try:
|
|
claim = claim_in(fd)
|
|
finally:
|
|
os.close(fd)
|
|
if claim is not None and claim["session"] == holder:
|
|
shutil.rmtree(tomb, dir_fd=parent)
|
|
return None
|
|
now_held = "unreadable" if claim is None else claim["session"]
|
|
try:
|
|
os.lstat(name, dir_fd=parent)
|
|
except FileNotFoundError:
|
|
# Python has no RENAME_NOREPLACE: rename(2) replaces an empty office a claimant made after the lstat.
|
|
os.rename(tomb, name, src_dir_fd=parent, dst_dir_fd=parent)
|
|
return f"the holder changed since the signature: signed {holder}, now {now_held}; nothing changed"
|
|
kept = council.joinpath(*office.split("/")[:-1], tomb)
|
|
return f"the holder changed since the signature: signed {holder}, now {now_held}; a new claim arrived meanwhile, the old office waits at {kept}"
|
|
finally:
|
|
os.close(parent)
|
|
|
|
|
|
def live(home: Path, session: str) -> bool:
|
|
for path in (home / ".claude" / "sessions").glob("*.json"):
|
|
try:
|
|
data = json.loads(path.read_text(encoding="utf-8"))
|
|
except (OSError, ValueError):
|
|
continue
|
|
if isinstance(data, dict) and data.get("sessionId") == session:
|
|
return True
|
|
return False
|
|
|
|
|
|
class Offices:
|
|
def __init__(self, trust: verifier.Trust, council: Path, script: Path, home: Path) -> None:
|
|
self.trust = trust
|
|
self.council = council
|
|
self.script = script
|
|
self.home = home
|
|
|
|
def command(self, item: dict) -> tuple[list[str], dict[str, str]]:
|
|
argv = [sys.executable, "-I", "-B", str(self.script), "claim", item["office"], f"--work={item['work']}"]
|
|
env = {"PATH": SAFE_PATH, "HOME": str(self.home), "AGENTS_COUNCIL_ROOT": str(self.council)}
|
|
if live(self.home, item["session"]):
|
|
return argv, {**env, "CLAUDE_CODE_SESSION_ID": item["session"]}
|
|
return [*argv, f"--session={item['session']}"], env
|
|
|
|
def item(self, item: dict, proof: object, now: float) -> dict:
|
|
"""Args:
|
|
item: one well-formed office order.
|
|
proof: the job's {"payload", "assertion"}.
|
|
now: the current time in epoch seconds.
|
|
|
|
Returns:
|
|
The order with status done, failed or refused, the reason, the
|
|
exit code and the output of council.py; a refused order changed
|
|
nothing.
|
|
"""
|
|
def ended(status: str, reason: str, code: int | None, output: str) -> dict:
|
|
return {**item, "status": status, "reason": reason, "code": code, "output": output}
|
|
|
|
try:
|
|
verifier.check(proof, signing.office_item(**item), "", self.trust, now)
|
|
except (signing.Refused, OSError) as error:
|
|
return ended("refused", f"no valid operator signature: {error}", None, "")
|
|
reason = unsafe(self.council, item["office"])
|
|
if reason:
|
|
return ended("refused", reason, None, "")
|
|
current = holder_of(self.council, item["office"])
|
|
if current != item["holder"]:
|
|
now_held = "unreadable" if current is None else current or "vacant"
|
|
return ended("refused", f"the holder changed since the signature: signed {item['holder'] or 'vacant'}, now {now_held}", None, "")
|
|
if item["action"] != "appoint":
|
|
try:
|
|
reason = vacate(self.council, item["office"], item["holder"])
|
|
except OSError as error:
|
|
return ended("failed", f"the office could not be emptied: {error.strerror or error}; nothing changed", None, "")
|
|
if reason:
|
|
return ended("refused", reason, None, "")
|
|
if item["action"] == "recall":
|
|
return ended("done", "", None, f"recalled {item['office']} from {item['holder']}")
|
|
argv, env = self.command(item)
|
|
try:
|
|
done = subprocess.run(argv, cwd="/", env=env, stdin=subprocess.DEVNULL, capture_output=True, text=True, errors="replace", timeout=TIMEOUT, check=False)
|
|
except (OSError, subprocess.TimeoutExpired) as error:
|
|
return ended("failed", f"{error}; the office is vacant now", 127, "")
|
|
output = (done.stdout + done.stderr)[-OUTPUT_LIMIT:]
|
|
if done.returncode == 0:
|
|
return ended("done", "", 0, output)
|
|
vacant = "; the office is vacant now" if item["action"] == "takeover" else ""
|
|
return ended("failed", f"{output.strip()[-500:]}{vacant}", done.returncode, output)
|
|
|
|
def job(self, path: Path, now: float) -> dict:
|
|
try:
|
|
job = json.loads(read_regular(path, None))
|
|
except (OSError, ValueError, RecursionError) 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 any(signing.office_problem(item) for item in items) or job.get("id") != path.stem:
|
|
return {"id": path.stem, "finished": now, "items": [], "error": "malformed job: refused, nothing changed"}
|
|
results = [self.item(item, job.get("proof"), now) for item in items]
|
|
return {"id": path.stem, "finished": time.time(), "items": results}
|
|
|
|
|
|
def cycle(jobs: Path, results: Path, offices: Offices) -> int:
|
|
handled = 0
|
|
for path in sorted(jobs.iterdir()):
|
|
if JOB.fullmatch(path.name):
|
|
verifier.write_private(results / path.name, offices.job(path, time.time()))
|
|
handled += 1
|
|
if path.is_dir() and not path.is_symlink():
|
|
shutil.rmtree(path)
|
|
else:
|
|
path.unlink(missing_ok=True)
|
|
return handled
|
|
|
|
|
|
def main() -> int:
|
|
parser = argparse.ArgumentParser(prog="offices")
|
|
parser.add_argument("--port", type=int, required=True)
|
|
for name in ("--jobs", "--results", "--approvers", "--replay", "--council", "--script"):
|
|
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, Offices(trust, args.council, args.script, Path.home()))
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|