atlas-iac/services/hermes/scripts/execution_pool_server.py
Hermes Agent 7a55b259bf hermes: add the fenced three-node distributed execution pool
Three fenced worker Pods claim Hermes Kanban runs through a coordinator that
owns every state transition, with per-ordinal HMAC authority, a mediated
broker-only SCM path, and durable per-ordinal workspaces.

Content is the reviewed head of PR #18 (689bcb6e) with PR 16's and PR 19's
contributions removed: they were merged in only to validate co-existence and are
not prerequisites, so this branch no longer carries them as ancestors. Only PR 14
and PR 15 remain, because the broker boundary and the cli_lane_* decomposition
are load-bearing for two of the fixed P0 boundaries.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-17 16:31:15 +00:00

131 lines
4.3 KiB
Python

#!/usr/bin/env python3
"""Versioned HTTP and maintenance loop for the execution-pool coordinator."""
from __future__ import annotations
import argparse
import os
import sqlite3
import sys
import threading
import time
from http.server import BaseHTTPRequestHandler
from pathlib import Path
from typing import Any
from execution_pool_protocol import (
MAX_WIRE_BYTES,
PROTOCOL_VERSION,
BoundedHTTPServer,
PoolStore,
ProtocolError,
canonical_json,
parse_wire,
read_key,
)
DATA_ROOT = Path(os.environ.get("HERMES_HOME", "/opt/data"))
STATE_ROOT = DATA_ROOT / "execution-pool"
KEY_PATH = Path(
os.environ.get(
"HERMES_EXECUTION_POOL_KEY_FILE", "/runtime-access/execution-pool-key"
)
)
PORT = int(os.environ.get("HERMES_EXECUTION_POOL_PORT", "9007"))
RETENTION_SECONDS = int(
os.environ.get("HERMES_EXECUTION_POOL_RETENTION_SECONDS", "1209600")
)
def handler_factory(coordinator: Any) -> type[BaseHTTPRequestHandler]:
class Handler(BaseHTTPRequestHandler):
server_version = f"hermes-execution-pool/{PROTOCOL_VERSION}"
def _reply(self, status: int, value: dict[str, Any]) -> None:
body = canonical_json(value)
self.send_response(status)
self.send_header("Content-Type", "application/json")
self.send_header("Content-Length", str(len(body)))
self.send_header("Cache-Control", "no-store")
self.end_headers()
self.wfile.write(body)
def do_GET(self) -> None: # noqa: N802
if self.path != "/ready":
self._reply(404, {"error": "not found"})
return
try:
coordinator.store.available_ordinals()
self._reply(200, {"ready": True, "version": PROTOCOL_VERSION})
except (OSError, sqlite3.Error):
self._reply(503, {"ready": False, "version": PROTOCOL_VERSION})
def do_POST(self) -> None: # noqa: N802
try:
length = int(self.headers.get("Content-Length", "0"))
if length <= 0 or length > MAX_WIRE_BYTES:
raise ProtocolError("invalid content length")
envelope = parse_wire(self.rfile.read(length))
routes = {
"/v1/poll": coordinator.poll,
"/v1/heartbeat": coordinator.heartbeat,
"/v1/result": coordinator.result,
}
if self.path not in routes:
self._reply(404, {"error": "not found"})
return
self._reply(200, routes[self.path](envelope))
except ProtocolError as error:
self._reply(409, {"error": str(error)})
except Exception as error:
print(
f"pool request failed: {type(error).__name__}: {error}",
file=sys.stderr,
flush=True,
)
self._reply(503, {"error": "coordinator unavailable"})
def log_message(self, _format: str, *_arguments: Any) -> None:
return
return Handler
def run(coordinator_type: type[Any]) -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--once", action="store_true")
args = parser.parse_args()
store = PoolStore(STATE_ROOT / "assignments.db")
coordinator = coordinator_type(read_key(KEY_PATH), store)
for operation in (
coordinator.expire_leases,
coordinator.recover_results,
coordinator.reconcile,
coordinator.dispatch,
):
operation()
if args.once:
return 0
server = BoundedHTTPServer(
("0.0.0.0", PORT), handler_factory(coordinator), max_workers=8
)
threading.Thread(target=server.serve_forever, daemon=True).start()
while True:
for operation in (
coordinator.expire_leases,
coordinator.recover_results,
coordinator.reconcile,
coordinator.dispatch,
lambda: store.garbage_collect(RETENTION_SECONDS),
):
try:
operation()
except Exception as error:
print(
f"pool maintenance deferred: {type(error).__name__}: {error}",
file=sys.stderr,
flush=True,
)
time.sleep(5)