Implement AUDIT-WP-0006 honest operational custody.
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 2s

Postgres now reports custody_class=operational with a cited 30-day
recoverable window. Join ITC-CAP operations.audit at D4, publish the
interface card, and overlay user-engine tenants [*] from Git so an
ExternalSecret refresh cannot shrink it.
This commit is contained in:
tegwick 2026-08-16 00:24:33 +02:00
parent 0a3d05ff1c
commit ded432a63f
25 changed files with 832 additions and 94 deletions

View file

@ -13,7 +13,23 @@ from uuid import uuid4
SCHEMA_VERSION_V1ALPHA1 = "audit-core.event.v1alpha1"
CustodyClass = Literal["development", "archive", "hot_search"]
CustodyClass = Literal["development", "operational", "archive", "hot_search"]
# Production Postgres reports ``operational``. Manifests written before
# AUDIT-WP-0006 required ``archive``. The two are aliases for one deploy so
# a mixed rollout cannot refuse to start. ``development`` is never an alias.
_PRODUCTION_CUSTODY_CLASSES = frozenset({"operational", "archive"})
def custody_class_satisfies(actual: str, required: str) -> bool:
"""Whether a backend's class meets a startup requirement.
``operational`` and ``archive`` satisfy each other. A development
backend satisfies only ``development``.
"""
if actual == required:
return True
return {actual, required} <= _PRODUCTION_CUSTODY_CLASSES
_REQUIRED_STRING_FIELDS = (
"schema_version",
@ -41,6 +57,24 @@ class RetentionPolicy:
immutable: bool
tamper_evidence: bool
durable: bool
# Recoverable history is the platform backup window, not a deletion
# policy. ``None`` means not declared (development backends).
recoverable_days: int | None = None
recoverable_source: str | None = None
recoverable_basis: str | None = None
def as_readiness(self) -> dict[str, Any]:
"""Sender-visible /readyz body. Keeps ``custody_class`` and adds recovery."""
payload: dict[str, Any] = {
"status": "ok",
"custody_class": self.custody_class,
"durable": self.durable,
}
if self.recoverable_days is not None or self.recoverable_source:
payload["recoverable_days"] = self.recoverable_days
payload["recoverable_source"] = self.recoverable_source
payload["recoverable_basis"] = self.recoverable_basis
return payload
@dataclass(frozen=True)
@ -142,7 +176,7 @@ def validate_event(event: AuditEvent) -> None:
class AuditBackend(Protocol):
"""Protocol implemented by replaceable audit sinks.
Production backends provide durable archive or hot-search custody.
Production backends provide durable operational or archive custody.
Development backends (such as :class:`~audit_core.mock_file_backend.MockFileAuditBackend`)
are for wiring only and must not be treated as audit custody.
"""