451 lines
19 KiB
Python
451 lines
19 KiB
Python
#!/usr/bin/env python3
|
|
"""Ordinal-scoped Atlas SCM boundary; the model container never gets its token."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import configparser
|
|
import json
|
|
import os
|
|
import re
|
|
import shutil
|
|
import stat
|
|
import subprocess
|
|
import threading
|
|
import urllib.error
|
|
import urllib.parse
|
|
import urllib.request
|
|
import uuid
|
|
from http.server import BaseHTTPRequestHandler
|
|
from pathlib import Path
|
|
from typing import Any
|
|
|
|
from execution_pool_protocol import (
|
|
MAX_WIRE_BYTES,
|
|
BoundedHTTPServer,
|
|
ProtocolError,
|
|
parse_wire,
|
|
read_key,
|
|
verify_envelope,
|
|
)
|
|
|
|
|
|
WORKSPACE_ROOT = Path(os.environ.get("HERMES_WORKER_ROOT", "/workspace"))
|
|
SCM_ROOT = Path(os.environ.get("HERMES_SCM_STATE_ROOT", "/scm-state"))
|
|
KEY_PATH = Path(os.environ.get("HERMES_EXECUTION_POOL_KEY_FILE", "/pool-access/execution-pool-key"))
|
|
TOKEN_PATH = Path(os.environ.get("HERMES_GITEA_TOKEN_FILE", "/vault/secrets/gitea-token"))
|
|
USERNAME_PATH = Path(os.environ.get("HERMES_GITEA_USERNAME_FILE", "/vault/secrets/gitea-username"))
|
|
ASKPASS = os.environ.get(
|
|
"HERMES_GITEA_ASKPASS", "/opt/coordinator/execution_pool_askpass.sh"
|
|
)
|
|
ORDINAL = int(os.environ.get("HERMES_WORKER_ORDINAL", "-1"))
|
|
PORT = int(os.environ.get("HERMES_SCM_BOUNDARY_PORT", "9008"))
|
|
REPO = re.compile(r"^https://scm\.bstein\.dev/atlas/([A-Za-z0-9_.-]+)\.git$")
|
|
BRANCH = re.compile(r"^(?:feature|fix|chore|docs|test|refactor)/[A-Za-z0-9][A-Za-z0-9._/-]{0,119}$")
|
|
MAX_BUNDLE_BYTES = 128 * 1024 * 1024
|
|
|
|
|
|
def _private_text(path: Path) -> str:
|
|
descriptor = os.open(path, os.O_RDONLY | getattr(os, "O_NOFOLLOW", 0))
|
|
try:
|
|
value = os.read(descriptor, 65537).decode("utf-8").strip()
|
|
finally:
|
|
os.close(descriptor)
|
|
if not value or len(value) > 65536:
|
|
raise ProtocolError(f"SCM credential is missing or invalid: {path.name}")
|
|
return value
|
|
|
|
|
|
def _git_env(authenticated: bool) -> dict[str, str]:
|
|
"""Expose credential paths only to explicit boundary-owned network calls."""
|
|
sensitive = {
|
|
"GIT_ASKPASS", "SSH_ASKPASS", "HERMES_SCM_PASSWORD_FILE",
|
|
"HERMES_SCM_USERNAME_FILE",
|
|
}
|
|
environment = {
|
|
name: value for name, value in os.environ.items()
|
|
if name not in sensitive and not name.startswith("GIT_CONFIG_")
|
|
}
|
|
environment["GIT_TERMINAL_PROMPT"] = "0"
|
|
if authenticated:
|
|
environment.update(
|
|
{
|
|
"GIT_ASKPASS": ASKPASS,
|
|
"HERMES_SCM_PASSWORD_FILE": str(TOKEN_PATH),
|
|
"HERMES_SCM_USERNAME_FILE": str(USERNAME_PATH),
|
|
"GIT_CONFIG_COUNT": "1",
|
|
"GIT_CONFIG_KEY_0": (
|
|
"url.http://gitea.gitea.svc.cluster.local:3000/.insteadOf"
|
|
),
|
|
"GIT_CONFIG_VALUE_0": "https://scm.bstein.dev/",
|
|
}
|
|
)
|
|
return environment
|
|
|
|
|
|
def _run(
|
|
*arguments: str,
|
|
cwd: Path | None = None,
|
|
timeout: int = 300,
|
|
authenticated: bool = False,
|
|
) -> str:
|
|
completed = subprocess.run(
|
|
list(arguments), cwd=cwd, env=_git_env(authenticated), stdin=subprocess.DEVNULL,
|
|
text=True, capture_output=True, timeout=timeout, check=False,
|
|
)
|
|
if completed.returncode:
|
|
message = (completed.stderr or completed.stdout or "SCM operation failed")[-2000:]
|
|
raise RuntimeError(message.strip())
|
|
return completed.stdout.strip()
|
|
|
|
|
|
def _regular_text(path: Path, limit: int, encoding: str = "utf-8") -> str:
|
|
"""Read bounded model-controlled metadata without following or blocking."""
|
|
flags = os.O_RDONLY | os.O_NONBLOCK | getattr(os, "O_NOFOLLOW", 0)
|
|
try:
|
|
descriptor = os.open(path, flags)
|
|
except OSError as error:
|
|
raise ProtocolError(f"workspace metadata is unavailable: {path.name}") from error
|
|
try:
|
|
info = os.fstat(descriptor)
|
|
if not stat.S_ISREG(info.st_mode) or info.st_size > limit:
|
|
raise ProtocolError(f"workspace metadata is invalid: {path.name}")
|
|
raw = os.read(descriptor, limit + 1)
|
|
if len(raw) > limit:
|
|
raise ProtocolError(f"workspace metadata is oversized: {path.name}")
|
|
return raw.decode(encoding)
|
|
except UnicodeError as error:
|
|
raise ProtocolError(f"workspace metadata is malformed: {path.name}") from error
|
|
finally:
|
|
os.close(descriptor)
|
|
|
|
|
|
def _binding(envelope: dict[str, Any]) -> tuple[dict[str, Any], str, str, str]:
|
|
if envelope["kind"] != "assignment" or int(envelope["worker_ordinal"]) != ORDINAL:
|
|
raise ProtocolError("assignment does not belong to this worker ordinal")
|
|
payload = envelope["payload"]
|
|
if not isinstance(payload, dict):
|
|
raise ProtocolError("assignment payload must be an object")
|
|
repo = str(payload.get("repo_url") or "")
|
|
branch = str(payload.get("branch") or "")
|
|
base_branch = str(payload.get("base_branch") or "main")
|
|
match = REPO.fullmatch(repo)
|
|
if (
|
|
not match
|
|
or not BRANCH.fullmatch(branch)
|
|
or not re.fullmatch(r"[A-Za-z0-9][A-Za-z0-9._/-]{0,119}", base_branch)
|
|
or ".." in branch
|
|
or "//" in branch
|
|
or ".." in base_branch
|
|
or "//" in base_branch
|
|
):
|
|
raise ProtocolError("assignment is outside the Atlas SCM policy")
|
|
run_name = f"{envelope['task_id']}-{envelope['run_id']}"
|
|
return payload, repo, branch, match.group(1) + ":" + run_name
|
|
|
|
|
|
def workspace_path(envelope: dict[str, Any]) -> Path:
|
|
"""Derive a contained path; no caller-provided filesystem path is accepted."""
|
|
parts = (str(envelope["board"]), str(envelope["task_id"]), str(envelope["run_id"]))
|
|
if any(not re.fullmatch(r"[A-Za-z0-9][A-Za-z0-9_.-]{0,127}", part) for part in parts):
|
|
raise ProtocolError("assignment path binding is invalid")
|
|
workspace_root = WORKSPACE_ROOT.resolve()
|
|
root = WORKSPACE_ROOT / "runs"
|
|
if root.is_symlink():
|
|
raise ProtocolError("workspace run root must not be a symlink")
|
|
root.mkdir(mode=0o700, parents=True, exist_ok=True)
|
|
root = root.resolve()
|
|
root.relative_to(workspace_root)
|
|
candidate = root.joinpath(*parts)
|
|
if candidate.is_symlink():
|
|
raise ProtocolError("workspace must not be a symlink")
|
|
current = root
|
|
for part in parts[:-1]:
|
|
current = current / part
|
|
if current.is_symlink():
|
|
raise ProtocolError("workspace parent must not be a symlink")
|
|
current.mkdir(mode=0o700, exist_ok=True)
|
|
if current.is_symlink():
|
|
raise ProtocolError("workspace parent must not be a symlink")
|
|
try:
|
|
candidate.resolve(strict=False).relative_to(root)
|
|
except ValueError as error:
|
|
raise ProtocolError("workspace escaped its ordinal root") from error
|
|
return candidate
|
|
|
|
|
|
def _workspace_identity(destination: Path, repo: str, branch: str) -> str:
|
|
"""Read identity as data; never execute Git in a model-controlled checkout."""
|
|
git_dir = destination / ".git"
|
|
if git_dir.is_symlink() or not git_dir.is_dir():
|
|
raise ProtocolError("workspace Git metadata must be a private directory")
|
|
raw_config = _regular_text(git_dir / "config", 64 * 1024)
|
|
parser = configparser.ConfigParser(interpolation=None, strict=True)
|
|
try:
|
|
parser.read_string(raw_config)
|
|
origin = parser.get('remote "origin"', "url")
|
|
except (configparser.Error, KeyError, UnicodeError) as error:
|
|
raise ProtocolError("workspace Git config cannot prove its origin") from error
|
|
if origin != repo:
|
|
raise ProtocolError("durable workspace origin does not match assignment")
|
|
head = _regular_text(git_dir / "HEAD", 4096).strip()
|
|
if head != f"ref: refs/heads/{branch}":
|
|
raise ProtocolError("durable workspace branch does not match assignment")
|
|
ref_path = git_dir / "refs/heads" / Path(branch)
|
|
if ref_path.is_symlink():
|
|
raise ProtocolError("workspace branch ref must not be a symlink")
|
|
if ref_path.exists():
|
|
commit = _regular_text(ref_path, 128, "ascii").strip()
|
|
else:
|
|
packed = _regular_text(git_dir / "packed-refs", 1024 * 1024, "ascii")
|
|
matches = [
|
|
line.split(" ", 1)[0]
|
|
for line in packed.splitlines()
|
|
if line.endswith(f" refs/heads/{branch}")
|
|
]
|
|
if len(matches) != 1:
|
|
raise ProtocolError("workspace branch ref is unavailable")
|
|
commit = matches[0]
|
|
if not re.fullmatch(r"[0-9a-f]{40,64}", commit):
|
|
raise ProtocolError("workspace branch ref is invalid")
|
|
return commit
|
|
|
|
|
|
def _private_repo(envelope: dict[str, Any]) -> Path:
|
|
root = SCM_ROOT.resolve()
|
|
root.mkdir(mode=0o700, parents=True, exist_ok=True)
|
|
name = f"{envelope['board']}-{envelope['task_id']}-{envelope['run_id']}.git"
|
|
path = root / name
|
|
path.resolve(strict=False).relative_to(root)
|
|
if path.is_symlink():
|
|
raise ProtocolError("private SCM state must not be a symlink")
|
|
return path
|
|
|
|
|
|
def _copy_bundle(source: Path, destination: Path) -> None:
|
|
"""Copy a bounded regular bundle into boundary-private storage."""
|
|
source_fd = os.open(
|
|
source, os.O_RDONLY | os.O_NONBLOCK | getattr(os, "O_NOFOLLOW", 0)
|
|
)
|
|
temporary = destination.with_name(f".{destination.name}.{uuid.uuid4().hex}.tmp")
|
|
destination.parent.mkdir(mode=0o700, parents=True, exist_ok=True)
|
|
target_fd = -1
|
|
try:
|
|
info = os.fstat(source_fd)
|
|
if not stat.S_ISREG(info.st_mode) or not 0 < info.st_size <= MAX_BUNDLE_BYTES:
|
|
raise ProtocolError("submission bundle is empty, oversized, or not regular")
|
|
target_fd = os.open(
|
|
temporary,
|
|
os.O_WRONLY | os.O_CREAT | os.O_EXCL | getattr(os, "O_NOFOLLOW", 0),
|
|
0o600,
|
|
)
|
|
remaining = info.st_size
|
|
while remaining:
|
|
chunk = os.read(source_fd, min(1024 * 1024, remaining))
|
|
if not chunk:
|
|
raise ProtocolError("submission bundle ended early")
|
|
view = memoryview(chunk)
|
|
while view:
|
|
view = view[os.write(target_fd, view) :]
|
|
remaining -= len(chunk)
|
|
os.fsync(target_fd)
|
|
except Exception:
|
|
temporary.unlink(missing_ok=True)
|
|
raise
|
|
finally:
|
|
os.close(source_fd)
|
|
if target_fd >= 0:
|
|
os.close(target_fd)
|
|
try:
|
|
os.replace(temporary, destination)
|
|
finally:
|
|
temporary.unlink(missing_ok=True)
|
|
|
|
|
|
class Boundary:
|
|
def __init__(self, key: bytes):
|
|
self.key = key
|
|
self.lock = threading.RLock()
|
|
|
|
def verify(self, raw: Any) -> dict[str, Any]:
|
|
return verify_envelope(self.key, raw, expected_kind="assignment")
|
|
|
|
def checkout(self, envelope: dict[str, Any]) -> dict[str, Any]:
|
|
"""Clone/fetch exactly one assigned branch into its private durable path."""
|
|
payload, repo, branch, _ = _binding(envelope)
|
|
base_branch = str(payload.get("base_branch") or "main")
|
|
destination = workspace_path(envelope)
|
|
with self.lock:
|
|
if (destination / ".git").exists():
|
|
return {
|
|
"workspace": str(destination), "preserved_dirty_state": True,
|
|
"baseline_sha": _workspace_identity(destination, repo, branch),
|
|
}
|
|
if destination.exists() and any(destination.iterdir()):
|
|
raise ProtocolError("workspace is non-empty and unmanaged")
|
|
destination.parent.mkdir(parents=True, exist_ok=True)
|
|
try:
|
|
_run(
|
|
"git", "clone", "--single-branch", "--branch", branch,
|
|
"--no-tags", repo, str(destination), timeout=900,
|
|
authenticated=True,
|
|
)
|
|
except RuntimeError as error:
|
|
if destination.exists() and any(destination.iterdir()):
|
|
raise RuntimeError(
|
|
"assigned branch clone failed and left state for review"
|
|
) from error
|
|
_run(
|
|
"git", "clone", "--single-branch", "--branch", base_branch,
|
|
"--no-tags", repo, str(destination), timeout=900,
|
|
authenticated=True,
|
|
)
|
|
_run("git", "checkout", "-b", branch, cwd=destination)
|
|
_run("git", "config", "user.name", "Hermes Execution Worker", cwd=destination)
|
|
_run("git", "config", "user.email", "hermes@bstein.dev", cwd=destination)
|
|
return {
|
|
"workspace": str(destination), "preserved_dirty_state": False,
|
|
"baseline_sha": _run("git", "rev-parse", "HEAD", cwd=destination),
|
|
}
|
|
|
|
def submit(self, envelope: dict[str, Any], request: dict[str, Any]) -> dict[str, Any]:
|
|
"""Push only the assignment branch and create/reuse its draft pull request."""
|
|
_payload, repo, branch, repo_binding = _binding(envelope)
|
|
repo_name, _ = repo_binding.split(":", 1)
|
|
destination = workspace_path(envelope)
|
|
title = str(request.get("title") or f"Hermes task {envelope['task_id']}").strip()[:240]
|
|
body = str(request.get("body") or "Automated draft from Hermes execution pool.")[:12000]
|
|
if not title or not (destination / ".git").exists():
|
|
raise ProtocolError("submission workspace or title is invalid")
|
|
with self.lock:
|
|
_workspace_identity(destination, repo, branch)
|
|
private = _private_repo(envelope)
|
|
bundle = private.with_suffix(".bundle")
|
|
_copy_bundle(destination / ".git/hermes-submit.bundle", bundle)
|
|
if not private.exists():
|
|
_run("git", "init", "--bare", str(private))
|
|
_run("git", "--git-dir", str(private), "remote", "add", "origin", repo)
|
|
else:
|
|
_run("git", "--git-dir", str(private), "remote", "set-url", "origin", repo)
|
|
try:
|
|
_run(
|
|
"git", "--git-dir", str(private), "fetch", "origin",
|
|
f"+refs/heads/{branch}:refs/remotes/origin/{branch}", timeout=900,
|
|
authenticated=True,
|
|
)
|
|
except RuntimeError as error:
|
|
if "couldn't find remote ref" not in str(error).lower():
|
|
raise
|
|
_run(
|
|
"git", "--git-dir", str(private), "fetch", str(bundle),
|
|
"HEAD:refs/pool/candidate", timeout=900,
|
|
)
|
|
_run(
|
|
"git", "--git-dir", str(private), "push", "origin",
|
|
f"refs/pool/candidate:refs/heads/{branch}", timeout=900,
|
|
authenticated=True,
|
|
)
|
|
pull = self._draft_pull(
|
|
repo_name, branch, str(_payload.get("base_branch") or "main"), title, body
|
|
)
|
|
bundle.unlink(missing_ok=True)
|
|
shutil.rmtree(private)
|
|
return {"workspace": str(destination), "branch": branch, "pull_request": pull}
|
|
|
|
@staticmethod
|
|
def _api(path: str, data: dict[str, Any] | None = None) -> Any:
|
|
token = _private_text(TOKEN_PATH)
|
|
request = urllib.request.Request(
|
|
"http://gitea.gitea.svc.cluster.local:3000" + path,
|
|
data=json.dumps(data, separators=(",", ":")).encode() if data else None,
|
|
method="POST" if data else "GET",
|
|
headers={
|
|
"Authorization": f"token {token}", "Accept": "application/json",
|
|
"Content-Type": "application/json", "User-Agent": "hermes-scm-boundary/1",
|
|
},
|
|
)
|
|
with urllib.request.urlopen(request, timeout=30) as response:
|
|
body = response.read(1024 * 1024 + 1)
|
|
if len(body) > 1024 * 1024:
|
|
raise ProtocolError("Gitea response exceeds the SCM boundary limit")
|
|
value = json.loads(body)
|
|
if not isinstance(value, (dict, list)):
|
|
raise ProtocolError("Gitea response has an invalid shape")
|
|
return value
|
|
|
|
def _draft_pull(
|
|
self, repo: str, branch: str, base_branch: str, title: str, body: str
|
|
) -> str:
|
|
query = urllib.parse.urlencode({"state": "open", "head": f"atlas:{branch}", "limit": 10})
|
|
existing = self._api(f"/api/v1/repos/atlas/{repo}/pulls?{query}")
|
|
if isinstance(existing, list) and existing:
|
|
return str(existing[0].get("html_url") or "")
|
|
created = self._api(
|
|
f"/api/v1/repos/atlas/{repo}/pulls",
|
|
{
|
|
"base": base_branch,
|
|
"head": branch, "title": title, "body": body, "draft": True,
|
|
},
|
|
)
|
|
return str(created.get("html_url") or "")
|
|
|
|
def garbage_collect(self, envelope: dict[str, Any]) -> dict[str, Any]:
|
|
"""Online requests never delete model-controlled or private SCM state."""
|
|
return {"removed": False, "eligible": False, "workspace": str(workspace_path(envelope))}
|
|
|
|
|
|
def handler_factory(boundary: Boundary) -> type[BaseHTTPRequestHandler]:
|
|
class Handler(BaseHTTPRequestHandler):
|
|
server_version = "hermes-scm-boundary/1"
|
|
|
|
def _reply(self, status: int, value: dict[str, Any]) -> None:
|
|
body = json.dumps(value, separators=(",", ":"), sort_keys=True).encode()
|
|
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
|
|
self._reply(200, {"ready": True}) if self.path == "/ready" else self._reply(404, {"error": "not found"})
|
|
|
|
def do_POST(self) -> None: # noqa: N802
|
|
try:
|
|
length = int(self.headers.get("Content-Length", "0"))
|
|
request = parse_wire(self.rfile.read(length)) if 0 < length <= MAX_WIRE_BYTES else None
|
|
if not request or set(request) - {"operation", "assignment", "title", "body"}:
|
|
raise ProtocolError("invalid SCM request")
|
|
envelope = boundary.verify(request.get("assignment"))
|
|
operations = {
|
|
"checkout": lambda: boundary.checkout(envelope),
|
|
"submit": lambda: boundary.submit(envelope, request),
|
|
"gc-check": lambda: boundary.garbage_collect(envelope),
|
|
}
|
|
operation = str(request.get("operation") or "")
|
|
if operation not in operations:
|
|
raise ProtocolError("unsupported SCM operation")
|
|
self._reply(200, operations[operation]())
|
|
except (ProtocolError, RuntimeError, OSError, urllib.error.URLError) as error:
|
|
self._reply(409, {"error": str(error)[:2000]})
|
|
|
|
def log_message(self, _format: str, *_arguments: Any) -> None:
|
|
return
|
|
|
|
return Handler
|
|
|
|
|
|
def main() -> int:
|
|
if ORDINAL not in range(3):
|
|
raise SystemExit("HERMES_WORKER_ORDINAL must be 0, 1, or 2")
|
|
key = read_key(KEY_PATH)
|
|
_private_text(TOKEN_PATH)
|
|
server = BoundedHTTPServer(
|
|
("0.0.0.0", PORT), handler_factory(Boundary(key)), max_workers=4
|
|
)
|
|
server.serve_forever()
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|