#!/usr/bin/env python3 """Strictly preflighted, discoverable, and self-cleaning armed mode.""" from __future__ import annotations import configparser import re from collections.abc import Sequence from dataclasses import dataclass from pathlib import Path from typing import Any from hermes_handoff_exec import Outcome, Runner, Vantage from hermes_handoff_model import EPHEMERAL, FAIL, NOT_RUN, PASS, CheckResult, CheckSpec from hermes_handoff_policy import ( EXPECTED_BASE, EXPECTED_REMOTE, EXPECTED_REPO, GITEA_CLIENT, arm_ephemeral_policy, register_ephemeral_pull, ) from hermes_handoff_rules import strict_json CONFIRMATION = "ARM EPHEMERAL HERMES HANDOFF PUSH" REF_PREFIX = "ephemeral/hermes-handoff-acceptance" REF_RE = re.compile(rf"{REF_PREFIX}/[a-z0-9][a-z0-9-]{{7,63}}\Z") TOKEN_RE = re.compile(r"[a-z0-9][a-z0-9-]{7,63}\Z") SHA_RE = re.compile(r"[0-9a-f]{40}\Z") DRAFT_TITLE_PREFIX = "WIP: " EXPECTED_HEAD_REF = "feature/hermes-full-handoff-acceptance" EXPECTED_REMOTE_URL = "https://scm.bstein.dev/atlas/titan-iac.git" PROTECTED_NAMES = frozenset( { "default", "develop", "head", "main", "master", "prod", "production", "release", "stable", "trunk", } ) class ArmingError(ValueError): """Raised when an ephemeral run is not safe to start.""" @dataclass(frozen=True) class ArmRequest: """Every fixed and caller-confirmed input to one armed run.""" repo: str remote: str token: str confirmation: str base: str = EXPECTED_BASE expected_head: str = "" expected_base_sha: str = "" @property def ref(self) -> str: return f"{REF_PREFIX}/{self.token}" def normalise_branch(name: str) -> str: """Return a branch name stripped of ref decoration, for comparison.""" return name.strip().removeprefix("refs/heads/").strip("/").lower() def assert_push_target_allowed(ref: str) -> None: """Raise unless ``ref`` is the one ephemeral branch grammar.""" if normalise_branch(ref) in PROTECTED_NAMES: raise ArmingError(f"refusing to push to protected branch {ref!r}") if not REF_RE.fullmatch(ref): raise ArmingError(f"push target {ref!r} is not an ephemeral acceptance ref") def _bounded_read(path: Path, limit: int = 1024 * 1024) -> str: data = path.read_bytes() if not data or len(data) > limit: raise ArmingError(f"local Git metadata is empty or exceeds {limit} bytes") return data.decode("utf-8", "strict") def _attest_worktree(root: Path, expected_head: str, expected_base_sha: str) -> None: """Bind cwd, common Git metadata, branch, remote, and exact local HEAD.""" resolved = root.resolve(strict=True) marker = resolved / ".git" if not marker.is_file(): raise ArmingError( "armed mode must run at the root of the existing linked worktree" ) line = _bounded_read(marker, 4096).strip() if not line.startswith("gitdir: "): raise ArmingError("linked-worktree Git metadata is malformed") gitdir = Path(line.removeprefix("gitdir: ")).resolve(strict=True) common = (gitdir / _bounded_read(gitdir / "commondir", 4096).strip()).resolve( strict=True ) head = _bounded_read(gitdir / "HEAD", 4096).strip() expected_ref = f"refs/heads/{EXPECTED_HEAD_REF}" if head != f"ref: {expected_ref}": raise ArmingError("armed mode must run from the existing PR #19 feature branch") ref_path = common / expected_ref actual_head = _bounded_read(ref_path, 4096).strip() if ref_path.is_file() else "" if actual_head != expected_head: raise ArmingError( "local worktree HEAD does not match the exact reviewed PR head" ) remote_ref = common / "refs" / "remotes" / EXPECTED_REMOTE / EXPECTED_BASE remote_main = ( _bounded_read(remote_ref, 4096).strip() if remote_ref.is_file() else "" ) if remote_main != expected_base_sha: raise ArmingError("local origin/main does not match the exact release base SHA") parser = configparser.RawConfigParser(interpolation=None) try: parser.read_string(_bounded_read(common / "config")) remote_url = parser.get(f'remote "{EXPECTED_REMOTE}"', "url") except (configparser.Error, KeyError) as exc: raise ArmingError("local Git remote metadata is malformed") from exc if remote_url != EXPECTED_REMOTE_URL: raise ArmingError("origin is not the fixed Atlas titan-iac HTTPS repository") def preflight( request: ArmRequest, expected_repo: str, worktree: Path | None = None ) -> None: """Validate arming and local provenance before any subprocess/network call.""" if request.confirmation != CONFIRMATION: raise ArmingError("confirmation phrase does not match") if expected_repo != EXPECTED_REPO or request.repo != EXPECTED_REPO: raise ArmingError( "armed repository is not the fixed Atlas titan-iac repository" ) if ( request.remote != EXPECTED_REMOTE or normalise_branch(request.base) != EXPECTED_BASE ): raise ArmingError("armed remote and pull-request base are fixed to origin/main") if not TOKEN_RE.fullmatch(request.token): raise ArmingError( "ephemeral token must be 8-64 lowercase alphanumeric or dash characters" ) if not SHA_RE.fullmatch(request.expected_head) or not SHA_RE.fullmatch( request.expected_base_sha ): raise ArmingError("exact reviewed head and release base SHAs are required") assert_push_target_allowed(request.ref) _attest_worktree( worktree or Path.cwd(), request.expected_head, request.expected_base_sha ) arm_ephemeral_policy(request.ref) def _spec(identifier: str, title: str) -> CheckSpec: return CheckSpec( id=f"ephemeral.{identifier}", title=title, group="ephemeral", rule="armed", steps=(), mandatory=True, scope=EPHEMERAL, ) def _result( identifier: str, title: str, status: str, reason: str, outcomes: Sequence[Outcome] = (), evidence: dict[str, Any] | None = None, ) -> CheckResult: return CheckResult( spec=_spec(identifier, title), status=status, reason=reason, evidence=evidence or {}, outcomes=list(outcomes), ) def _protected_refusal_result() -> CheckResult: accepted: list[str] = [] for candidate in ("main", "master", "refs/heads/main", "HEAD", f"{REF_PREFIX}/x"): try: assert_push_target_allowed(candidate) except ArmingError: continue accepted.append(candidate) return _result( "protected-branch-refusal", "Arming against a protected branch is refused before any network call", FAIL if accepted else PASS, f"guard accepted {accepted}" if accepted else "every protected and malformed target was refused", evidence={"accepted": accepted}, ) def _detail(outcome: Outcome) -> str: return ( outcome.error or outcome.combined[:200] or f"exit status {outcome.returncode}" ) def _pr_problems(payload: Any, request: ArmRequest) -> list[str]: if not isinstance(payload, dict): return ["pull request is not an object"] wanted = { "state": "open", "draft": True, "merged": False, "base": {"ref": EXPECTED_BASE, "sha": request.expected_base_sha}, "head": {"ref": request.ref, "sha": request.expected_head}, } problems: list[str] = [] if ( not isinstance(payload.get("number"), int) or isinstance(payload.get("number"), bool) or payload["number"] <= 0 ): problems.append("number is malformed") for field in ("state", "draft", "merged"): if ( type(payload.get(field)) is not type(wanted[field]) or payload.get(field) != wanted[field] ): problems.append(f"{field} does not match") for group, values in (("base", wanted["base"]), ("head", wanted["head"])): observed = payload.get(group) if not isinstance(observed, dict): problems.append(f"{group} is malformed") continue for field, value in values.items(): if observed.get(field) != value: problems.append(f"{group}.{field} does not match") return problems def _discover(outcome: Outcome, request: ArmRequest) -> tuple[list[int], str]: if not outcome.ok: return ([], f"pull-request discovery failed: {_detail(outcome)}") try: payload = strict_json(outcome.stdout) except ValueError as exc: return ([], f"pull-request discovery returned malformed JSON: {exc}") if not isinstance(payload, list): return ([], "pull-request discovery did not return an array") matches: list[int] = [] for index, item in enumerate(payload): if ( not isinstance(item, dict) or not isinstance(item.get("head"), dict) or not isinstance(item.get("base"), dict) ): return ([], f"pull-request discovery row {index + 1} is malformed") if item["head"].get("ref") != request.ref: continue problems = _pr_problems(item, request) if problems: return ([], f"discovered pull request is invalid: {', '.join(problems)}") matches.append(item["number"]) if len(matches) != 1: return ( matches, f"expected one exact draft pull request, discovered {len(matches)}", ) return (matches, "") def _closed_pr_problems(payload: Any, request: ArmRequest) -> list[str]: """Validate the same exact PR identity with state changed to closed.""" problems = _pr_problems(payload, request) if isinstance(payload, dict) and payload.get("state") == "closed": problems = [ problem for problem in problems if problem != "state does not match" ] elif "state does not match" not in problems: problems.append("state is not closed") return problems def _cleanup( runner: Runner, vantage: Vantage, request: ArmRequest, api: str, numbers: Sequence[int], discovery_error: str = "", ) -> CheckResult: outcomes: list[Outcome] = [] problems = [discovery_error] if discovery_error else [] for number in sorted(set(numbers)): closed = runner.run( (GITEA_CLIENT, "PATCH", f"{api}/pulls/{number}", "--field", "state=closed"), vantage, ) outcomes.append(closed) if not closed.ok: problems.append(f"pull request #{number} close failed") continue try: payload = strict_json(closed.stdout) except ValueError: problems.append(f"pull request #{number} close response is malformed") continue expected = _closed_pr_problems(payload, request) if expected: problems.append(f"pull request #{number} close response does not match") deleted = runner.run( (GITEA_CLIENT, "DELETE", f"{api}/branches/{request.ref}"), vantage ) outcomes.append(deleted) if not deleted.ok: problems.append("ephemeral branch deletion failed") remaining = runner.run( ("git", "ls-remote", request.remote, f"refs/heads/{request.ref}"), vantage ) outcomes.append(remaining) if not remaining.ok or remaining.stdout.strip(): problems.append("ephemeral branch absence could not be verified") for number in sorted(set(numbers)): observed = runner.run((GITEA_CLIENT, "GET", f"{api}/pulls/{number}"), vantage) outcomes.append(observed) if not observed.ok: problems.append(f"pull request #{number} closure could not be verified") continue try: payload = strict_json(observed.stdout) except ValueError: problems.append(f"pull request #{number} verification is malformed") continue expected = _closed_pr_problems(payload, request) if expected: problems.append(f"pull request #{number} is not exactly closed") return _result( "cleanup-verified", "Every ephemeral ref and pull request is removed and the removal is verified", FAIL if problems else PASS, "; ".join(problems) if problems else "the exact branch and draft pull request are gone", outcomes, {"ref": request.ref, "pull_requests": sorted(set(numbers))}, ) def run_armed( runner: Runner, vantage: Vantage, request: ArmRequest ) -> list[CheckResult]: """Create, discover, validate, and clean one exact ephemeral branch/PR.""" guard = _protected_refusal_result() if guard.status != PASS: return [ guard, _result( "feature-branch-push", "A unique ephemeral feature branch can be pushed and removed", NOT_RUN, "protected-ref guard failed", ), _result( "draft-pull-request", "A draft pull request can be opened and closed", NOT_RUN, "protected-ref guard failed", ), _result( "cleanup-verified", "Every ephemeral artifact is removed", NOT_RUN, "nothing was created", ), ] api = f"/api/v1/repos/{request.repo}" reserve = min(6000.0, max(300.0, runner.command_timeout * 10)) existing = runner.run( ("git", "ls-remote", request.remote, f"refs/heads/{request.ref}"), vantage, reserve_seconds=reserve, ) if not existing.ok or existing.stdout.strip(): reason = ( _detail(existing) if not existing.ok else "the ephemeral ref already exists" ) return [ guard, _result( "feature-branch-push", "A unique ephemeral feature branch can be pushed and removed", FAIL, reason, [existing], ), _result( "draft-pull-request", "A draft pull request can be opened and closed", NOT_RUN, "branch preflight failed", ), _result( "cleanup-verified", "Every ephemeral artifact is removed", NOT_RUN, "nothing was created", ), ] local_head = runner.run( ("git", "rev-parse", "HEAD"), vantage, reserve_seconds=reserve ) pushed = ( runner.run( ("git", "push", request.remote, f"HEAD:refs/heads/{request.ref}"), vantage, reserve_seconds=reserve, ) if local_head.ok and local_head.stdout.strip() == request.expected_head else Outcome( argv=("git", "push"), vantage=vantage.name, error="local HEAD changed after static preflight", ) ) verified = runner.run( ("git", "ls-remote", request.remote, f"refs/heads/{request.ref}"), vantage, reserve_seconds=reserve, ) branch_ok = verified.ok and verified.stdout.split() == [ request.expected_head, f"refs/heads/{request.ref}", ] branch_result = _result( "feature-branch-push", "A unique ephemeral feature branch can be pushed and removed", PASS if branch_ok else FAIL, "the exact reviewed HEAD was pushed" if branch_ok else "push could not be verified exactly", [local_head, pushed, verified], {"ref": request.ref, "head": request.expected_head}, ) numbers: list[int] = [] discovery_error = "" create_outcomes: list[Outcome] = [] if branch_ok: created = runner.run( ( GITEA_CLIENT, "POST", f"{api}/pulls", "--field", f"title={DRAFT_TITLE_PREFIX}Hermes handoff acceptance ephemeral probe", "--field", f"head={request.ref}", "--field", f"base={EXPECTED_BASE}", "--field", "body=Ephemeral acceptance probe. Closed and deleted by the harness.", ), vantage, reserve_seconds=reserve, ) discovered = runner.run( (GITEA_CLIENT, "GET", f"{api}/pulls?state=all&limit=50"), vantage, reserve_seconds=reserve, ) create_outcomes = [created, discovered] numbers, discovery_error = _discover(discovered, request) for number in numbers: register_ephemeral_pull(number) else: discovery_error = "branch creation was not verified" draft_result = _result( "draft-pull-request", "A draft pull request can be opened and closed against the ephemeral branch", PASS if len(numbers) == 1 and not discovery_error else FAIL, f"discovered exact draft pull request #{numbers[0]}" if len(numbers) == 1 and not discovery_error else discovery_error, create_outcomes, {"pull_requests": numbers}, ) cleanup = _cleanup(runner, vantage, request, api, numbers, discovery_error) return [guard, branch_result, draft_result, cleanup]