213 lines
6.5 KiB
Python
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
|