Files
aios/active-runtime/app/task_plane/site_adapters.py
T

213 lines
6.5 KiB
Python

"""S5.1 live authority adapters with exact caller/candidate binding.
These adapters are projections over existing authorities. They do not create
tasks, grants, leases, branches, or recovery authority.
"""
from __future__ import annotations
from dataclasses import dataclass, field
from typing import Any, Callable, Mapping, Protocol
from .governance_admission import AdmissionOperation, GovernanceFacts
from .governance_recovery_controller import GovernanceEvent
from .governance_resolver import ExactTaskResolver, ResolutionQuery, ResolutionResult
class LiveSurfaceUnavailable(RuntimeError):
def __init__(self, role: str, reason: str) -> None:
self.role = role
self.reason = reason
super().__init__(f"LIVE_SURFACE_UNAVAILABLE[{role}]: {reason}")
class LiveBindingViolation(RuntimeError):
def __init__(self, role: str, reason: str) -> None:
self.role = role
self.reason = reason
super().__init__(f"LIVE_BINDING_VIOLATION[{role}]: {reason}")
class TaskPlaneReadClient(Protocol):
def get_task(
self, task_id: str, *, actor_identity: str
) -> Mapping[str, Any] | None: ...
def list_tasks(
self, *, actor_identity: str
) -> list[Mapping[str, Any]]: ...
class LiveTaskResolverAdapter:
"""Bind ExactTaskResolver to one caller-authorized TaskPlane projection."""
def __init__(
self,
*,
client: TaskPlaneReadClient | None = None,
actor_identity: str | None = None,
bound_repository: str | None = None,
) -> None:
self._client = client
self._actor_identity = actor_identity
self._bound_repository = bound_repository
@property
def live(self) -> bool:
return self._client is not None and bool(self._actor_identity)
def resolve(
self, query: ResolutionQuery, *, actor_identity: str
) -> ResolutionResult:
if not self.live:
raise LiveSurfaceUnavailable(
"A1",
"no caller-authorized TaskPlaneReadClient and bound identity",
)
if actor_identity != self._actor_identity:
raise LiveBindingViolation(
"A1",
"controller actor_identity does not match bound read principal",
)
if (
self._bound_repository
and query.repository != self._bound_repository
):
raise LiveBindingViolation(
"A1",
"query repository is outside the bound authority projection",
)
return ExactTaskResolver(self._client).resolve(
query, actor_identity=self._actor_identity
)
@dataclass(frozen=True)
class GovernedAuthoritySnapshot:
"""One revision-consistent, candidate-specific governed read."""
task_id: str
actor_identity: str
operation: AdmissionOperation
snapshot_revision: str
facts: GovernanceFacts
class GovernedAuthorityReadClient(Protocol):
def read_snapshot(
self,
*,
task_id: str,
actor_identity: str,
operation: AdmissionOperation,
) -> GovernedAuthoritySnapshot: ...
class LiveGovernanceFactsAdapter:
"""Materialize GovernanceFacts from one atomic/revision-consistent read."""
def __init__(
self,
*,
client: GovernedAuthorityReadClient | None = None,
actor_identity: str | None = None,
) -> None:
self._client = client
self._actor_identity = actor_identity
@property
def live(self) -> bool:
return self._client is not None and bool(self._actor_identity)
def snapshot(
self,
*,
task_id: str | None,
actor_identity: str,
operation: AdmissionOperation,
) -> GovernanceFacts:
if not self.live:
raise LiveSurfaceUnavailable(
"A2",
"no candidate-specific governed authority reader and identity",
)
if task_id is None:
raise LiveBindingViolation("A2", "exact task_id required")
if actor_identity != self._actor_identity:
raise LiveBindingViolation(
"A2",
"controller actor_identity does not match bound governed principal",
)
snapshot = self._client.read_snapshot(
task_id=task_id,
actor_identity=actor_identity,
operation=operation,
)
if not snapshot.snapshot_revision:
raise LiveBindingViolation(
"A2", "authority snapshot revision is missing"
)
if snapshot.task_id != task_id:
raise LiveBindingViolation("A2", "authority snapshot task mismatch")
if snapshot.actor_identity != actor_identity:
raise LiveBindingViolation("A2", "authority snapshot actor mismatch")
if snapshot.operation is not operation:
raise LiveBindingViolation("A2", "authority snapshot operation mismatch")
return snapshot.facts
@dataclass(frozen=True)
class LiveGovernanceEvent:
kind: str
revision: str
occurred_at: str
payload: Mapping[str, Any] = field(default_factory=dict)
class GovernanceEventSubscriber(Protocol):
def subscribe(
self, on_event: Callable[[LiveGovernanceEvent], object]
) -> None: ...
def close(self) -> None: ...
class LiveEventSubscriptionAdapter:
"""Translate site events into the core GovernanceEvent contract."""
def __init__(
self,
sink: Callable[[GovernanceEvent], object],
subscriber: GovernanceEventSubscriber | None = None,
) -> None:
self._sink = sink
self._subscriber = subscriber
self._active = False
@property
def live(self) -> bool:
return self._subscriber is not None
def _on_live_event(self, event: LiveGovernanceEvent):
if not isinstance(event, LiveGovernanceEvent):
raise LiveBindingViolation(
"A3", "subscriber emitted an unsupported event type"
)
return self._sink(GovernanceEvent(
kind=event.kind,
revision=event.revision,
occurred_at=event.occurred_at,
))
def start(self) -> None:
if not self.live:
raise LiveSurfaceUnavailable(
"A3",
"no governance event subscriber injected; polling remains fallback",
)
self._subscriber.subscribe(self._on_live_event)
self._active = True
def close(self) -> None:
if self._subscriber is not None:
self._subscriber.close()
self._active = False