155 lines
6.2 KiB
Python
155 lines
6.2 KiB
Python
#!/usr/bin/env python3
|
|
"""Canonical Atlas project and Git-ref policy for distributed execution."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import os
|
|
import re
|
|
import stat
|
|
import subprocess
|
|
from pathlib import Path
|
|
from typing import Any
|
|
|
|
|
|
DATA_ROOT = Path(os.environ.get("HERMES_HOME", "/opt/data"))
|
|
PROJECT_ROOT = DATA_ROOT / "workspace/projects"
|
|
BOARD_ROOT = DATA_ROOT / "kanban/boards"
|
|
ATLAS_REPO = re.compile(
|
|
r"https://scm\.bstein\.dev/titan/(?P<repo>[A-Za-z0-9][A-Za-z0-9_.-]{0,99})\.git\Z"
|
|
)
|
|
SAFE_PREFIXES = frozenset(
|
|
{"feature", "fix", "chore", "docs", "test", "refactor", "wt", "review", "hermes", "handoff"}
|
|
)
|
|
IDENTIFIER = re.compile(r"[A-Za-z0-9][A-Za-z0-9_.-]{0,127}\Z")
|
|
GIT = "/usr/bin/git"
|
|
MAX_BOARD_BYTES = 64 * 1024
|
|
REPOSITORY_ALIASES = {"titan-iac": "atlas-iac"}
|
|
|
|
|
|
class ProjectPolicyError(ValueError):
|
|
"""Canonical project metadata or a requested ref failed closed."""
|
|
|
|
|
|
def _task_value(task: Any, name: str, default: Any = None) -> Any:
|
|
return getattr(task, name, default)
|
|
|
|
|
|
def _run_git(workdir: Path, *arguments: str) -> str:
|
|
completed = subprocess.run(
|
|
[GIT, "-C", str(workdir), *arguments],
|
|
check=False,
|
|
stdin=subprocess.DEVNULL,
|
|
capture_output=True,
|
|
text=True,
|
|
timeout=30,
|
|
env={
|
|
"HOME": "/nonexistent",
|
|
"PATH": "/usr/bin:/bin",
|
|
"GIT_CONFIG_NOSYSTEM": "1",
|
|
"GIT_TERMINAL_PROMPT": "0",
|
|
},
|
|
)
|
|
if completed.returncode:
|
|
raise ProjectPolicyError("canonical project Git metadata is unavailable")
|
|
return completed.stdout.strip()
|
|
|
|
|
|
def validate_branch(value: object, *, feature: bool) -> str:
|
|
"""Validate a complete local branch name with Git and a reviewed namespace."""
|
|
if not isinstance(value, str) or not value or len(value) > 200:
|
|
raise ProjectPolicyError("branch name exceeds the safe limit")
|
|
if not value.isascii() or value.startswith("-"):
|
|
raise ProjectPolicyError("branch name must be canonical ASCII")
|
|
prefix = value.split("/", 1)[0]
|
|
if feature and ("/" not in value or prefix not in SAFE_PREFIXES):
|
|
raise ProjectPolicyError("task branch is outside the reviewed namespace")
|
|
completed = subprocess.run(
|
|
[GIT, "check-ref-format", f"refs/heads/{value}"],
|
|
check=False,
|
|
stdin=subprocess.DEVNULL,
|
|
stdout=subprocess.DEVNULL,
|
|
stderr=subprocess.DEVNULL,
|
|
timeout=10,
|
|
env={"PATH": "/usr/bin:/bin", "GIT_CONFIG_NOSYSTEM": "1"},
|
|
)
|
|
if completed.returncode:
|
|
raise ProjectPolicyError("branch name is not a valid Git ref")
|
|
return value
|
|
|
|
|
|
def _read_board(board: str) -> dict[str, Any]:
|
|
if not IDENTIFIER.fullmatch(board):
|
|
raise ProjectPolicyError("board slug is invalid")
|
|
path = BOARD_ROOT / board / "board.json"
|
|
descriptor = os.open(path, os.O_RDONLY | getattr(os, "O_NOFOLLOW", 0))
|
|
try:
|
|
info = os.fstat(descriptor)
|
|
if not stat.S_ISREG(info.st_mode) or not 0 < info.st_size <= MAX_BOARD_BYTES:
|
|
raise ProjectPolicyError("board registry entry is not a bounded regular file")
|
|
raw = os.read(descriptor, MAX_BOARD_BYTES + 1)
|
|
finally:
|
|
os.close(descriptor)
|
|
try:
|
|
value = json.loads(raw)
|
|
except (UnicodeError, json.JSONDecodeError) as error:
|
|
raise ProjectPolicyError("board registry entry is malformed") from error
|
|
if not isinstance(value, dict) or value.get("slug") != board or value.get("archived") is True:
|
|
raise ProjectPolicyError("board registry identity is unavailable or archived")
|
|
return value
|
|
|
|
|
|
def resolve_project(board: str) -> tuple[str, str, Path]:
|
|
"""Resolve repo and base exclusively from the canonical board registry.
|
|
|
|
A board may be created before its primary checkout. In that case the
|
|
Atlas repository naming contract and ``main`` are the registry defaults;
|
|
once a checkout exists, its credential-free origin and remote HEAD must
|
|
agree with that identity. This keeps a missing checkout from silently
|
|
routing work to some other project's repository.
|
|
"""
|
|
entry = _read_board(board)
|
|
raw_workdir = entry.get("default_workdir")
|
|
if not isinstance(raw_workdir, str) or not Path(raw_workdir).is_absolute():
|
|
raise ProjectPolicyError("board default_workdir is missing")
|
|
workdir = Path(raw_workdir).resolve(strict=False)
|
|
project_root = PROJECT_ROOT.resolve(strict=True)
|
|
try:
|
|
workdir.relative_to(project_root)
|
|
except ValueError as error:
|
|
raise ProjectPolicyError("board workdir is outside the Atlas project registry") from error
|
|
repository = REPOSITORY_ALIASES.get(board, board)
|
|
remote = f"https://scm.bstein.dev/titan/{repository}.git"
|
|
if not ATLAS_REPO.fullmatch(remote):
|
|
raise ProjectPolicyError("board repository identity is invalid")
|
|
if not workdir.exists():
|
|
return remote, "main", workdir
|
|
if not workdir.is_dir():
|
|
raise ProjectPolicyError("board checkout is not a directory")
|
|
checkout_remote = _run_git(workdir, "remote", "get-url", "origin")
|
|
if "@" in checkout_remote or checkout_remote != remote:
|
|
raise ProjectPolicyError("board origin disagrees with the Atlas registry")
|
|
try:
|
|
base = _run_git(workdir, "symbolic-ref", "--short", "refs/remotes/origin/HEAD")
|
|
if not base.startswith("origin/"):
|
|
raise ProjectPolicyError("origin HEAD is not canonical")
|
|
base = base.removeprefix("origin/")
|
|
except ProjectPolicyError:
|
|
base = "main"
|
|
return remote, validate_branch(base, feature=False), workdir
|
|
|
|
|
|
def resolve_assignment(board: str, task: Any) -> tuple[str, str, str]:
|
|
"""Resolve the exact repo/base and safe task branch for one board task."""
|
|
repo, base, _workdir = resolve_project(board)
|
|
task_id = str(_task_value(task, "id", "") or "")
|
|
if not IDENTIFIER.fullmatch(task_id):
|
|
raise ProjectPolicyError("task identity is invalid")
|
|
branch = str(_task_value(task, "branch_name", "") or f"wt/{task_id}")
|
|
return repo, validate_branch(branch, feature=True), base
|
|
|
|
|
|
def distributed_workspace_eligible(task: Any) -> bool:
|
|
"""Only pathless tasks migrate; an existing worktree stays with its local owner."""
|
|
return not str(_task_value(task, "workspace_path", "") or "").strip()
|