feat(harness): allocate canonical issue-scoped broker runtime

This commit is contained in:
2026-10-09 16:11:52 +00:00
parent 3df85090ff
commit ac3cfdaeb9
13 changed files with 713 additions and 69 deletions
+42 -13
View File
@@ -17,6 +17,7 @@ if __package__ in (None, ""):
sys.path.insert(0, str(Path(__file__).resolve().parents[2]))
from harness.lib.managed_lock import LockTimeoutError, ManagedLockSet, probe_free
from harness.lib.runtime_root import issue_runtime_path, prepare_issue_runtime
ROLES = {"gitea-impl-agent", "gitea-spec-agent", "gitea-review-agent"}
MAX_TTL = 3600
@@ -184,13 +185,18 @@ class Broker:
raise ValueError("issue must be positive")
if not 1 <= args.ttl_seconds <= MAX_TTL:
raise ValueError("ttl_seconds out of bounds")
runtime_path = issue_runtime_path(args.root, args.owner, args.repository, args.issue, args.run_id)
self.root = _abs(args.root, "root")
self.handle = _abs(args.handle, "handle")
self.sock_path = _abs(args.socket, "socket")
self.handle = runtime_path / "broker.handle"
self.sock_path = self.handle.parent / "broker.sock"
supplied_handle = getattr(args, "handle", None)
supplied_socket = getattr(args, "socket", None)
if supplied_handle is not None and os.fspath(supplied_handle) != str(self.handle):
raise ValueError("handle path must be canonical")
if supplied_socket is not None and os.fspath(supplied_socket) != str(self.sock_path):
raise ValueError("socket path must be canonical")
locks = self.root / "repositories" / args.owner / args.repository / "locks"
_safe_dir(locks, "repository locks")
_safe_dir(self.handle.parent, "handle")
_safe_dir(self.sock_path.parent, "socket")
self.token = secrets.token_hex(32)
self.deadline = time.monotonic() + args.ttl_seconds
self.set = ManagedLockSet.for_issue(self.root, args.owner, args.repository, args.issue, timeout=args.acquire_timeout)
@@ -337,6 +343,14 @@ class Broker:
signal.signal(signal.SIGTERM, lambda *_: setattr(self, "stopping", True))
signal.signal(signal.SIGINT, lambda *_: setattr(self, "stopping", True))
self.set.issue.write_metadata(self.meta)
with self.set.repository_scope():
runtime = prepare_issue_runtime(self.root, self.meta["owner"], self.meta["repository"],
self.meta["issue"], self.meta["run_id"], lock_set=self.set)
try:
runtime_fd = runtime.fd
self.socket_parent_fd = os.dup(runtime_fd)
finally:
os.close(runtime.fd)
try:
_write_handle(self.handle, self.state, exclusive=True,
on_created=self._record_handle_identity)
@@ -346,7 +360,9 @@ class Broker:
if (st.st_dev, st.st_ino) != self.handle_identity:
raise ValueError("created handle identity changed")
self.handle_created = True
self.socket_parent_fd, socket_name = _open_parent(self.sock_path)
if self.socket_parent_fd is None:
raise ValueError("runtime directory unavailable")
socket_name = self.sock_path.name
try:
os.stat(socket_name, dir_fd=self.socket_parent_fd, follow_symlinks=False)
raise ValueError("socket path already exists")
@@ -404,13 +420,26 @@ def request_handle(handle_path: str | Path, action: str, ttl: float | None = Non
handle = _abs(str(handle_path), "handle")
data = _read_handle(handle)
sock_path = _abs(data["socket"], "socket")
req = {"action": action, "token": data["token"]}
if ttl is not None:
req["ttl_seconds"] = ttl
with socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) as sock:
sock.settimeout(3); sock.connect(str(sock_path)); sock.sendall((json.dumps(req) + "\n").encode())
with sock.makefile("rb") as response_file:
response = json.loads(response_file.readline())
parent_fd, name = _open_parent(sock_path)
try:
parent_st = os.fstat(parent_fd)
if (parent_st.st_uid != os.getuid() or stat.S_IMODE(parent_st.st_mode) != 0o700):
raise ValueError("unsafe socket parent")
st = os.stat(name, dir_fd=parent_fd, follow_symlinks=False)
if (not stat.S_ISSOCK(st.st_mode) or st.st_uid != os.getuid()
or stat.S_IMODE(st.st_mode) != 0o600):
raise ValueError("unsafe socket")
req = {"action": action, "token": data["token"]}
if ttl is not None:
req["ttl_seconds"] = ttl
with socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) as sock:
sock.settimeout(3)
sock.connect(f"/proc/self/fd/{parent_fd}/{name}")
sock.sendall((json.dumps(req) + "\n").encode())
with sock.makefile("rb") as response_file:
response = json.loads(response_file.readline())
finally:
os.close(parent_fd)
if not isinstance(response, dict):
raise ValueError("invalid broker response")
return response
@@ -444,7 +473,7 @@ def parser() -> argparse.ArgumentParser:
p = argparse.ArgumentParser()
sub = p.add_subparsers(dest="command", required=True)
s = sub.add_parser("serve")
s.add_argument("--root", required=True); s.add_argument("--owner", required=True); s.add_argument("--repository", required=True); s.add_argument("--issue", required=True, type=int); s.add_argument("--role", required=True); s.add_argument("--run-id", required=True); s.add_argument("--handle", required=True); s.add_argument("--socket", required=True); s.add_argument("--ttl-seconds", required=True, type=float); s.add_argument("--acquire-timeout", type=float, default=0.0)
s.add_argument("--root", required=True); s.add_argument("--owner", required=True); s.add_argument("--repository", required=True); s.add_argument("--issue", required=True, type=int); s.add_argument("--role", required=True); s.add_argument("--run-id", required=True); s.add_argument("--handle"); s.add_argument("--socket"); s.add_argument("--ttl-seconds", required=True, type=float); s.add_argument("--acquire-timeout", type=float, default=0.0)
r = sub.add_parser("request")
r.add_argument("--handle", required=True); r.add_argument("--action", required=True, choices=["status", "renew", "acquire_repository", "release_repository", "release"]); r.add_argument("--ttl-seconds", type=float)
check = sub.add_parser("probe")