332 lines
11 KiB
Python
332 lines
11 KiB
Python
"""Narrow governed publisher for an already-existing task branch.
|
|
|
|
Its only writing port is a compare-and-swap publication after a complete
|
|
signed Owner approval, a stable remote observation, and a single-use nonce
|
|
claim. It neither owns trust material nor accepts direct reference strings.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
from dataclasses import dataclass
|
|
import re
|
|
from typing import Any, Mapping, Protocol
|
|
|
|
from aios_existing_branch_approval import (
|
|
OwnerApprovalEnvelope,
|
|
OwnerApprovalRejected,
|
|
OwnerApprovalVerifier,
|
|
PublicationApprovalBinding,
|
|
ReplayLedger,
|
|
canonical_json,
|
|
digest_json,
|
|
)
|
|
|
|
|
|
REQUEST_SCHEMA = "aios.existing-branch-publication.v1"
|
|
PUBLISH_OPERATION = "publishExistingBranchGoverned"
|
|
OID_RE = re.compile(r"^[0-9a-f]{40}$")
|
|
BRANCH_RE = re.compile(r"^agent/[A-Za-z0-9][A-Za-z0-9._/-]{2,179}$")
|
|
|
|
|
|
class ExistingBranchPublicationRejected(RuntimeError):
|
|
def __init__(self, code: str) -> None:
|
|
super().__init__(code)
|
|
self.code = code
|
|
|
|
|
|
class RemoteOidConflict(RuntimeError):
|
|
pass
|
|
|
|
|
|
class RemoteObservationAmbiguous(RuntimeError):
|
|
pass
|
|
|
|
|
|
@dataclass(frozen=True, slots=True)
|
|
class ExistingBranchPublicationRequest:
|
|
repository: str
|
|
branch: str
|
|
expected_branch_oid: str
|
|
idempotency_key: str
|
|
payload: Mapping[str, Any]
|
|
|
|
@classmethod
|
|
def from_mapping(cls, value: object) -> "ExistingBranchPublicationRequest":
|
|
if not isinstance(value, Mapping):
|
|
raise ExistingBranchPublicationRejected("PUBLICATION_REQUEST_REJECTED")
|
|
expected_fields = {
|
|
"schema",
|
|
"repository",
|
|
"branch",
|
|
"expected_branch_oid",
|
|
"operation",
|
|
"idempotency_key",
|
|
"payload",
|
|
}
|
|
if set(value) != expected_fields or value.get("schema") != REQUEST_SCHEMA:
|
|
raise ExistingBranchPublicationRejected("PUBLICATION_REQUEST_REJECTED")
|
|
repository = value["repository"]
|
|
branch = value["branch"]
|
|
expected_oid = value["expected_branch_oid"]
|
|
key = value["idempotency_key"]
|
|
payload = value["payload"]
|
|
if (
|
|
not isinstance(repository, str)
|
|
or repository != "aios/aios"
|
|
or not BRANCH_RE.fullmatch(str(branch))
|
|
or not isinstance(expected_oid, str)
|
|
or not OID_RE.fullmatch(expected_oid)
|
|
or value.get("operation") != PUBLISH_OPERATION
|
|
or not isinstance(key, str)
|
|
or not key.strip()
|
|
or not isinstance(payload, Mapping)
|
|
):
|
|
raise ExistingBranchPublicationRejected("PUBLICATION_REQUEST_REJECTED")
|
|
try:
|
|
canonical_json(payload)
|
|
except OwnerApprovalRejected as exc:
|
|
raise ExistingBranchPublicationRejected("PUBLICATION_PAYLOAD_REJECTED") from exc
|
|
return cls(repository, str(branch), expected_oid, key, dict(payload))
|
|
|
|
@property
|
|
def request_digest(self) -> str:
|
|
return digest_json(
|
|
{
|
|
"schema": REQUEST_SCHEMA,
|
|
"repository": self.repository,
|
|
"branch": self.branch,
|
|
"expected_branch_oid": self.expected_branch_oid,
|
|
"operation": PUBLISH_OPERATION,
|
|
"payload": self.payload,
|
|
}
|
|
)
|
|
|
|
@property
|
|
def idempotency_digest(self) -> str:
|
|
return digest_json(
|
|
{
|
|
"idempotency_key": self.idempotency_key,
|
|
"request_digest": self.request_digest,
|
|
}
|
|
)
|
|
|
|
@property
|
|
def approval_binding(self) -> PublicationApprovalBinding:
|
|
return PublicationApprovalBinding(
|
|
repository=self.repository,
|
|
branch=self.branch,
|
|
expected_branch_oid=self.expected_branch_oid,
|
|
operation=PUBLISH_OPERATION,
|
|
request_digest=self.request_digest,
|
|
idempotency_digest=self.idempotency_digest,
|
|
)
|
|
|
|
|
|
@dataclass(frozen=True, slots=True)
|
|
class RemoteBranchObservation:
|
|
state: str
|
|
git_oid: str | None
|
|
api_oid: str | None
|
|
expected_commit_visible: bool | None
|
|
|
|
|
|
@dataclass(frozen=True, slots=True)
|
|
class RemotePublicationEvidence:
|
|
authority_oid: str | None
|
|
gitea_git_oid: str | None
|
|
gitea_api_oid: str | None
|
|
commit_visible: bool | None
|
|
|
|
|
|
class ExistingBranchRemotePort(Protocol):
|
|
def observe(self, branch: str) -> RemoteBranchObservation: ...
|
|
|
|
def publish_compare_and_swap(
|
|
self, branch: str, expected_branch_oid: str, commit_oid: str
|
|
) -> RemotePublicationEvidence: ...
|
|
|
|
|
|
class AuthorityBranchPort(Protocol):
|
|
def read_branch_oid(self, branch: str) -> str | None: ...
|
|
|
|
|
|
class CredentialProjection(Protocol):
|
|
def project(self) -> None: ...
|
|
|
|
def revoke(self) -> None: ...
|
|
|
|
|
|
class PublicationWorkspace(Protocol):
|
|
def head_oid(self) -> str: ...
|
|
|
|
def commit_payload(self, payload: Mapping[str, Any]) -> str: ...
|
|
|
|
|
|
class DetachedWorkspaceFactory(Protocol):
|
|
def open_detached(self, expected_branch_oid: str) -> PublicationWorkspace: ...
|
|
|
|
|
|
class AuditSink(Protocol):
|
|
def record(self, event: Mapping[str, Any]) -> None: ...
|
|
|
|
|
|
class ExistingBranchPublisher:
|
|
"""Validate once, observe once, then make one bounded publication attempt."""
|
|
|
|
def __init__(
|
|
self,
|
|
*,
|
|
verifier: OwnerApprovalVerifier,
|
|
replay_ledger: ReplayLedger,
|
|
authority: AuthorityBranchPort,
|
|
remote: ExistingBranchRemotePort,
|
|
credentials: CredentialProjection,
|
|
workspaces: DetachedWorkspaceFactory,
|
|
audit: AuditSink,
|
|
) -> None:
|
|
self.verifier = verifier
|
|
self.replay_ledger = replay_ledger
|
|
self.authority = authority
|
|
self.remote = remote
|
|
self.credentials = credentials
|
|
self.workspaces = workspaces
|
|
self.audit = audit
|
|
|
|
def _audit(
|
|
self,
|
|
*,
|
|
status: str,
|
|
request: ExistingBranchPublicationRequest,
|
|
approval: OwnerApprovalEnvelope | None = None,
|
|
commit_oid: str | None = None,
|
|
) -> None:
|
|
event: dict[str, Any] = {
|
|
"event": "governed_existing_branch_publication",
|
|
"status": status,
|
|
"repository": request.repository,
|
|
"branch": request.branch,
|
|
"expected_branch_oid": request.expected_branch_oid,
|
|
"operation": PUBLISH_OPERATION,
|
|
"request_digest": request.request_digest,
|
|
"idempotency_digest": request.idempotency_digest,
|
|
}
|
|
if approval is not None:
|
|
event.update(
|
|
{
|
|
"approval_id": approval.approval_id,
|
|
"owner_identity": approval.owner_identity,
|
|
"trust_domain": approval.trust_domain,
|
|
"approval_fingerprint": approval.fingerprint,
|
|
}
|
|
)
|
|
if commit_oid is not None:
|
|
event["commit_oid"] = commit_oid
|
|
self.audit.record(event)
|
|
|
|
def _preflight_remote(
|
|
self, request: ExistingBranchPublicationRequest
|
|
) -> RemoteBranchObservation | dict[str, str]:
|
|
try:
|
|
authority_oid = self.authority.read_branch_oid(request.branch)
|
|
except Exception:
|
|
return {"status": "RECONCILIATION_REQUIRED"}
|
|
if authority_oid != request.expected_branch_oid:
|
|
return {"status": "MANUAL_OWNER_REVIEW_REQUIRED"}
|
|
try:
|
|
observation = self.remote.observe(request.branch)
|
|
except RemoteObservationAmbiguous:
|
|
return {"status": "RECONCILIATION_REQUIRED"}
|
|
if observation.state == "AMBIGUOUS":
|
|
return {"status": "RECONCILIATION_REQUIRED"}
|
|
if observation.state != "PRESENT":
|
|
return {"status": "MANUAL_OWNER_REVIEW_REQUIRED"}
|
|
if (
|
|
observation.git_oid is not None
|
|
and observation.api_oid is not None
|
|
and observation.git_oid != observation.api_oid
|
|
):
|
|
return {"status": "RECONCILIATION_REQUIRED"}
|
|
if (
|
|
observation.git_oid != request.expected_branch_oid
|
|
or observation.api_oid != request.expected_branch_oid
|
|
):
|
|
return {"status": "MANUAL_OWNER_REVIEW_REQUIRED"}
|
|
if observation.expected_commit_visible is not True:
|
|
return {"status": "RECONCILIATION_REQUIRED"}
|
|
return observation
|
|
|
|
def publish(
|
|
self, request_value: object, approval_value: object
|
|
) -> dict[str, str | bool]:
|
|
request = ExistingBranchPublicationRequest.from_mapping(request_value)
|
|
try:
|
|
approval = self.verifier.verify(approval_value, request.approval_binding)
|
|
except OwnerApprovalRejected:
|
|
self._audit(status="REJECTED", request=request)
|
|
raise
|
|
preflight = self._preflight_remote(request)
|
|
if isinstance(preflight, dict):
|
|
self._audit(status=preflight["status"], request=request, approval=approval)
|
|
return preflight
|
|
self.replay_ledger.consume(approval, request.approval_binding)
|
|
workspace = self.workspaces.open_detached(request.expected_branch_oid)
|
|
if workspace.head_oid() != request.expected_branch_oid:
|
|
self._audit(status="REJECTED", request=request, approval=approval)
|
|
raise ExistingBranchPublicationRejected("DETACHED_WORKSPACE_HEAD_REJECTED")
|
|
projected = False
|
|
try:
|
|
self.credentials.project()
|
|
projected = True
|
|
commit_oid = workspace.commit_payload(request.payload)
|
|
if not isinstance(commit_oid, str) or not OID_RE.fullmatch(commit_oid):
|
|
raise ExistingBranchPublicationRejected("PUBLICATION_COMMIT_REJECTED")
|
|
try:
|
|
evidence = self.remote.publish_compare_and_swap(
|
|
request.branch, request.expected_branch_oid, commit_oid
|
|
)
|
|
except RemoteOidConflict:
|
|
self._audit(
|
|
status="MANUAL_OWNER_REVIEW_REQUIRED",
|
|
request=request,
|
|
approval=approval,
|
|
commit_oid=commit_oid,
|
|
)
|
|
return {"status": "MANUAL_OWNER_REVIEW_REQUIRED"}
|
|
except RemoteObservationAmbiguous:
|
|
self._audit(
|
|
status="RECONCILIATION_REQUIRED",
|
|
request=request,
|
|
approval=approval,
|
|
commit_oid=commit_oid,
|
|
)
|
|
return {"status": "RECONCILIATION_REQUIRED"}
|
|
if (
|
|
evidence.authority_oid != commit_oid
|
|
or evidence.gitea_git_oid != commit_oid
|
|
or evidence.gitea_api_oid != commit_oid
|
|
or evidence.commit_visible is not True
|
|
):
|
|
self._audit(
|
|
status="RECONCILIATION_REQUIRED",
|
|
request=request,
|
|
approval=approval,
|
|
commit_oid=commit_oid,
|
|
)
|
|
return {"status": "RECONCILIATION_REQUIRED"}
|
|
self._audit(
|
|
status="PUBLISHED",
|
|
request=request,
|
|
approval=approval,
|
|
commit_oid=commit_oid,
|
|
)
|
|
return {
|
|
"status": "PUBLISHED",
|
|
"commit_oid": commit_oid,
|
|
"authority_oid": evidence.authority_oid,
|
|
"gitea_git_oid": evidence.gitea_git_oid,
|
|
"gitea_api_oid": evidence.gitea_api_oid,
|
|
"commit_visible": evidence.commit_visible,
|
|
}
|
|
finally:
|
|
if projected:
|
|
self.credentials.revoke()
|