155 lines
6.5 KiB
Python
155 lines
6.5 KiB
Python
"""Campaign-aware continuation leases for an existing Git branch.
|
|
|
|
This module is deliberately transport-neutral. It validates branch lineage
|
|
and hands ref mutation to an injected compare-and-swap broker; it does not run
|
|
Git, inspect credentials, or create a branch itself.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from dataclasses import dataclass, replace
|
|
from datetime import datetime, timedelta, timezone
|
|
import re
|
|
from typing import Protocol
|
|
|
|
|
|
OID_RE = re.compile(r"^[0-9a-f]{40}$")
|
|
TARGET_BRANCH = "agent/autonomous-release-campaign-v1-20260807"
|
|
LINEAGE_ANCHOR = "69b45e81dcd2c3707c367003b08c55fd361faae8"
|
|
CAMPAIGN_HEAD = "bba675fab3b7e0c5fba7066720252b3c91b9f20d"
|
|
|
|
|
|
class ContinuationError(RuntimeError):
|
|
"""Stable fail-closed error for lease or branch binding violations."""
|
|
|
|
|
|
class BranchProbe(Protocol):
|
|
def branch_exists(self, branch: str) -> bool: ...
|
|
def branch_head(self, branch: str) -> str: ...
|
|
def is_ancestor(self, older: str, newer: str) -> bool: ...
|
|
|
|
|
|
class RefCAS(Protocol):
|
|
def compare_and_swap_branch(self, branch: str, expected: str, new: str) -> str: ...
|
|
|
|
|
|
def _oid(value: str, field: str) -> str:
|
|
if not isinstance(value, str) or not OID_RE.fullmatch(value):
|
|
raise ContinuationError(f"{field}_invalid")
|
|
return value
|
|
|
|
|
|
@dataclass(frozen=True, slots=True)
|
|
class WriterLeaseRequestV2:
|
|
task_id: str
|
|
repository: str
|
|
branch_mode: str
|
|
base_oid: str
|
|
expected_branch_oid: str
|
|
target_branch: str
|
|
generation: int
|
|
lease_seconds: int = 900
|
|
|
|
schema = "aios.task-writer-lease.v2"
|
|
accepted_schemas = frozenset({"aios.task-writer-lease.v2", "aios.task-writer-lease-request.v2"})
|
|
|
|
def validate(self, probe: BranchProbe) -> None:
|
|
if not self.task_id or self.repository != "aios/aios":
|
|
raise ContinuationError("request_identity_invalid")
|
|
if self.branch_mode not in {"CREATE_NEW", "CONTINUE_EXISTING"}:
|
|
raise ContinuationError("branch_mode_invalid")
|
|
_oid(self.base_oid, "base_oid")
|
|
_oid(self.expected_branch_oid, "expected_branch_oid")
|
|
if self.target_branch in {"main", "master"} or self.target_branch.startswith(("refs/", "tags/")):
|
|
raise ContinuationError("protected_branch_rejected")
|
|
if not isinstance(self.generation, int) or isinstance(self.generation, bool) or self.generation < 1:
|
|
raise ContinuationError("generation_invalid")
|
|
if not isinstance(self.lease_seconds, int) or not 1 <= self.lease_seconds <= 86_400:
|
|
raise ContinuationError("lease_duration_invalid")
|
|
if self.branch_mode == "CONTINUE_EXISTING":
|
|
if not probe.branch_exists(self.target_branch):
|
|
raise ContinuationError("EXISTING_BRANCH_MISSING")
|
|
actual = _oid(probe.branch_head(self.target_branch), "actual_branch_oid")
|
|
if actual != self.expected_branch_oid:
|
|
raise ContinuationError("EXISTING_BRANCH_OID_CONFLICT")
|
|
if not probe.is_ancestor(self.base_oid, actual):
|
|
raise ContinuationError("BASE_NOT_ANCESTOR_OF_BRANCH")
|
|
elif hasattr(probe, "commit_exists") and not probe.commit_exists(self.base_oid):
|
|
raise ContinuationError("BASE_COMMIT_NOT_FOUND")
|
|
elif self.branch_mode == "CREATE_NEW" and probe.branch_exists(self.target_branch):
|
|
raise ContinuationError("TARGET_BRANCH_ALREADY_EXISTS")
|
|
elif self.branch_mode == "CREATE_NEW" and self.base_oid != self.expected_branch_oid:
|
|
raise ContinuationError("CREATE_NEW_BASE_EXPECTATION_MISMATCH")
|
|
|
|
|
|
@dataclass(frozen=True, slots=True)
|
|
class CampaignWriterLease:
|
|
task_id: str
|
|
repository: str
|
|
branch: str
|
|
lineage_anchor_oid: str
|
|
current_head_oid: str
|
|
generation: int
|
|
lease_expires_at: datetime
|
|
append_only: bool = True
|
|
renewable: bool = True
|
|
multi_commit: bool = True
|
|
recoverable: bool = True
|
|
state: str = "ACTIVE"
|
|
branch_bound: bool = True
|
|
|
|
def __post_init__(self) -> None:
|
|
_oid(self.lineage_anchor_oid, "lineage_anchor_oid")
|
|
_oid(self.current_head_oid, "current_head_oid")
|
|
if not all((self.append_only, self.renewable, self.multi_commit, self.recoverable, self.branch_bound)):
|
|
raise ContinuationError("lease_capability_downgrade")
|
|
|
|
|
|
class ContinuationLeaseManager:
|
|
"""State machine for a multi-commit lease with CAS-only ref updates."""
|
|
|
|
def __init__(self, probe: BranchProbe, refs: RefCAS, *, now=datetime.now) -> None:
|
|
self.probe = probe
|
|
self.refs = refs
|
|
self.now = now
|
|
|
|
def open(self, request: WriterLeaseRequestV2) -> CampaignWriterLease:
|
|
request.validate(self.probe)
|
|
if request.branch_mode != "CONTINUE_EXISTING":
|
|
raise ContinuationError("CREATE_NEW_REQUIRES_SEPARATE_PROVISIONER")
|
|
return CampaignWriterLease(
|
|
task_id=request.task_id,
|
|
repository=request.repository,
|
|
branch=request.target_branch,
|
|
lineage_anchor_oid=request.base_oid,
|
|
current_head_oid=request.expected_branch_oid,
|
|
generation=request.generation,
|
|
lease_expires_at=self.now(timezone.utc) + timedelta(seconds=request.lease_seconds),
|
|
)
|
|
|
|
def renew(self, lease: CampaignWriterLease, seconds: int) -> CampaignWriterLease:
|
|
if lease.state != "ACTIVE" or not lease.renewable or seconds < 1:
|
|
raise ContinuationError("lease_not_renewable")
|
|
return replace(lease, lease_expires_at=self.now(timezone.utc) + timedelta(seconds=seconds))
|
|
|
|
def append_commit(self, lease: CampaignWriterLease, new_head_oid: str) -> CampaignWriterLease:
|
|
if lease.state != "ACTIVE":
|
|
raise ContinuationError("lease_not_active")
|
|
_oid(new_head_oid, "new_head_oid")
|
|
if not self.probe.is_ancestor(lease.current_head_oid, new_head_oid):
|
|
raise ContinuationError("NON_FAST_FORWARD_COMMIT")
|
|
try:
|
|
actual = self.refs.compare_and_swap_branch(
|
|
lease.branch, lease.current_head_oid, new_head_oid
|
|
)
|
|
except Exception as exc:
|
|
raise ContinuationError("BRANCH_LEASE_STALE") from exc
|
|
if actual != new_head_oid:
|
|
raise ContinuationError("BRANCH_LEASE_STALE")
|
|
return replace(lease, current_head_oid=new_head_oid)
|
|
|
|
def supersede(self, lease: CampaignWriterLease) -> CampaignWriterLease:
|
|
if lease.state not in {"ACTIVE", "RECOVERABLE_FAILURE", "RECONCILIATION_REQUIRED"}:
|
|
raise ContinuationError("lease_not_supersedable")
|
|
return replace(lease, generation=lease.generation + 1, state="SUPERSEDED")
|