Implement approval engine production readiness
Assistant: codex Assistant-Model: gpt-5.6-sol Assistant-Session: 01a05e2e-805b-7042-a750-71f473bceea2
This commit is contained in:
parent
ebce5abb27
commit
2bd2d19a98
30 changed files with 1679 additions and 53 deletions
100
approval_engine/audit.py
Normal file
100
approval_engine/audit.py
Normal file
|
|
@ -0,0 +1,100 @@
|
|||
"""Asynchronous delivery of the transactional outbox to audit-core."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import threading
|
||||
from pathlib import Path
|
||||
from typing import Any, Callable
|
||||
from urllib.error import HTTPError, URLError
|
||||
from urllib.request import Request, urlopen
|
||||
|
||||
from .store import Engine
|
||||
|
||||
|
||||
class AuditDeliveryError(RuntimeError):
|
||||
"""Bounded delivery failure; details deliberately exclude response bodies."""
|
||||
|
||||
|
||||
def audit_envelope(payload: dict[str, Any]) -> dict[str, Any]:
|
||||
details = dict(payload.get("details") or {})
|
||||
return {
|
||||
"id": payload["event_id"],
|
||||
"type": payload["action"],
|
||||
"source": payload["source"],
|
||||
"subject": payload["resource"],
|
||||
"tenant": payload["tenant"],
|
||||
"correlation_id": details.get("approval_id") or payload["event_id"],
|
||||
"occurred_at": payload["observed_at"],
|
||||
"data": {
|
||||
"schema_version": payload["schema_version"],
|
||||
"scope": payload["scope"],
|
||||
"actor": payload.get("actor"),
|
||||
"resource": payload["resource"],
|
||||
"outcome": payload["outcome"],
|
||||
"reason": payload.get("reason"),
|
||||
"details": details,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
class AuditCoreSink:
|
||||
def __init__(
|
||||
self,
|
||||
base_url: str,
|
||||
token_file: str | Path,
|
||||
*,
|
||||
timeout_seconds: float = 5,
|
||||
opener: Callable[..., Any] = urlopen,
|
||||
) -> None:
|
||||
self.url = base_url.rstrip("/") + "/v1/events"
|
||||
self.token_file = Path(token_file)
|
||||
self.timeout_seconds = timeout_seconds
|
||||
self.opener = opener
|
||||
|
||||
def __call__(self, payload: dict[str, Any]) -> None:
|
||||
token = self.token_file.read_text(encoding="utf-8").strip()
|
||||
if not token or any(ch.isspace() for ch in token):
|
||||
raise AuditDeliveryError("audit credential is unavailable")
|
||||
body = json.dumps(audit_envelope(payload), sort_keys=True).encode("utf-8")
|
||||
request = Request(
|
||||
self.url,
|
||||
data=body,
|
||||
method="POST",
|
||||
headers={
|
||||
"Authorization": f"Bearer {token}",
|
||||
"Content-Type": "application/json",
|
||||
"Idempotency-Key": payload["event_id"],
|
||||
},
|
||||
)
|
||||
try:
|
||||
response = self.opener(request, timeout=self.timeout_seconds)
|
||||
status = int(response.getcode())
|
||||
response.close()
|
||||
except (HTTPError, URLError, OSError) as exc:
|
||||
raise AuditDeliveryError(type(exc).__name__) from exc
|
||||
if status not in {200, 202}:
|
||||
raise AuditDeliveryError(f"audit ingest returned status {status}")
|
||||
|
||||
|
||||
class OutboxWorker:
|
||||
def __init__(
|
||||
self,
|
||||
engine: Engine,
|
||||
sink: Callable[[dict[str, Any]], None],
|
||||
*,
|
||||
heartbeat_interval_seconds: int = 300,
|
||||
) -> None:
|
||||
self.engine = engine
|
||||
self.sink = sink
|
||||
self.heartbeat_interval_seconds = heartbeat_interval_seconds
|
||||
|
||||
def run_once(self) -> dict[str, int]:
|
||||
if self.engine.heartbeat_due(self.heartbeat_interval_seconds):
|
||||
self.engine.emit_heartbeat()
|
||||
return self.engine.drain(self.sink)
|
||||
|
||||
def run_forever(self, stop: threading.Event, poll_seconds: float = 5) -> None:
|
||||
while not stop.is_set():
|
||||
self.run_once()
|
||||
stop.wait(poll_seconds)
|
||||
Loading…
Add table
Add a link
Reference in a new issue