"""Attributive emission to audit-core from the local outbox. Trade (statute ยง9.6): mutation evidence is attributive. Emission is atomic with the local outbox (crash between mutation and insert is prevented). Drain to audit-core is after commit and MUST NOT fail a mutation. Completeness is not claimed. See docs/evidence-emission.md. This module has no SQL against audit-core's store and no credential for it beyond a sender token used to POST /v1/events. The external copy cannot be rewritten through tenant-engine's runtime database credential because we do not hold that credential. """ from __future__ import annotations from dataclasses import dataclass from typing import Any from uuid import uuid4 import httpx SCHEMA_VERSION = "audit-core.event.v1alpha1" SOURCE = "tenant-engine" def new_event_id() -> str: return str(uuid4()) def envelope_for( *, event_id: str, event_type: str, tenant_id: str, observed_at: str, payload: dict[str, Any], ) -> dict[str, Any]: return { "schema_version": SCHEMA_VERSION, "event_id": event_id, "observed_at": observed_at, "tenant": tenant_id, "scope": "tenant-engine", "source": SOURCE, "actor": payload.get("actor") or payload.get("granted_by") or payload.get("changed_by"), "action": event_type, "resource": f"tenant:{tenant_id}", "outcome": "recorded", "reason": payload.get("reason") or payload.get("authorization_reason"), "details": payload, } @dataclass(frozen=True, slots=True) class DeliveryResult: event_id: str status: str # delivered | duplicate | retry | dead | skipped http_status: int | None = None detail: str = "" class AuditCoreClient: """POST /v1/events. The only audit-core surface this engine holds.""" def __init__( self, *, base_url: str, timeout_seconds: float = 3.0, token_file: str | None = None, transport: httpx.BaseTransport | None = None, ) -> None: self.base_url = base_url.rstrip("/") self.token_file = token_file self._client = httpx.Client( base_url=self.base_url, timeout=httpx.Timeout(timeout_seconds), transport=transport, ) def post_event(self, envelope: dict[str, Any]) -> DeliveryResult: event_id = str(envelope.get("event_id") or "") headers: dict[str, str] = {"Content-Type": "application/json"} if self.token_file: try: token = open(self.token_file, encoding="utf-8").read().strip() except OSError as exc: return DeliveryResult(event_id, "retry", None, f"token_unreadable:{exc}") if not token: return DeliveryResult(event_id, "retry", None, "token_empty") headers["Authorization"] = f"Bearer {token}" try: response = self._client.post("/v1/events", json=envelope, headers=headers) except (httpx.HTTPError, OSError) as exc: return DeliveryResult(event_id, "retry", None, f"unreachable:{exc.__class__.__name__}") if response.status_code in (200, 202): status = "duplicate" if response.status_code == 200 else "delivered" return DeliveryResult(event_id, status, response.status_code) if response.status_code in (400, 409): return DeliveryResult(event_id, "dead", response.status_code, "rejected") if response.status_code in (401, 403): return DeliveryResult(event_id, "retry", response.status_code, "unauthorized") return DeliveryResult(event_id, "retry", response.status_code, "unavailable") def close(self) -> None: self._client.close()