All checks were successful
Build and Publish Container Image / build-and-push (push) Successful in 37s
Engine/PIP declaration is now checkable (layer.yaml plus a Tooling-client scan). Writes persist a decision record or the published fail-closed stance, live-lookup freshness is published, events_for is tenant-scoped, and mutation evidence drains to audit-core from a local outbox without blocking the mutation. Sender registration is requested as AUDIT-IN-0002. Boundary-contract amendment is requested as NET-IN-0002. Assistant: grok Assistant-Session: 01a04cea-e5e8-7081-a0fc-808ebbc35fa9
106 lines
3.7 KiB
Python
106 lines
3.7 KiB
Python
"""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()
|