"""Closed runtime configuration and event sink for Brain-to-Sense export.""" from __future__ import annotations from pathlib import Path from typing import Mapping from yovision_brain.events import InternalEventCandidate from yovision_brain.integration.machine_identity import ( Signer, TransportPolicy, load_private_key, ) from .client import HTTPSMachineIdentitySender from .mapper import canonical_json_bytes, export_event class EventExportSink: def __init__( self, sender: HTTPSMachineIdentitySender, *, producer_id: str, site_ref: str, severity: str, region_refs: Mapping[str, str], crossing_directions: Mapping[str, str], ) -> None: self._sender = sender self._producer_id = producer_id self._site_ref = site_ref self._severity = severity self._region_refs = dict(region_refs) self._crossing_directions = dict(crossing_directions) def write(self, candidate: InternalEventCandidate) -> None: event = export_event( candidate, producer_id=self._producer_id, site_ref=self._site_ref, severity=self._severity, region_ref=self._region_refs.get(candidate.rule_id), crossing_direction=self._crossing_directions.get(candidate.rule_id), ) # Mapping and serialization happen before minting, so the exact bytes are # bound to the machine token and remain unchanged for this delivery. self._sender.send(canonical_json_bytes(event)) def build_event_export_sink(raw: object, *, base_dir: Path) -> EventExportSink | None: if raw is None: return None config = _object("event_export", raw) _closed( "event_export", config, { "enabled", "endpoint", "producer_id", "site_ref", "severity", "region_refs", "crossing_directions", "machine_identity", "transport", }, ) enabled = config.get("enabled", False) if not isinstance(enabled, bool): raise ValueError("event_export.enabled must be a boolean") if not enabled: if set(config) != {"enabled"}: raise ValueError("disabled event_export may only contain enabled") return None identity = _object("event_export.machine_identity", config.get("machine_identity")) _closed( "event_export.machine_identity", identity, {"principal", "key_id", "private_key_path"}, ) transport_raw = _object("event_export.transport", config.get("transport")) _closed( "event_export.transport", transport_raw, { "tls_min_version", "verify_certificate", "verify_hostname", "connect_timeout_ms", "response_header_timeout_ms", "request_timeout_ms", "max_request_bytes", }, ) policy = TransportPolicy(**transport_raw) # type: ignore[arg-type] policy.validate() key_path = _string("private_key_path", identity.get("private_key_path")) resolved_key_path = Path(key_path) if not resolved_key_path.is_absolute(): resolved_key_path = base_dir / resolved_key_path signer = Signer( _string("principal", identity.get("principal")), _string("key_id", identity.get("key_id")), load_private_key(resolved_key_path), ) return EventExportSink( HTTPSMachineIdentitySender( _string("endpoint", config.get("endpoint")), signer, policy ), producer_id=_string("producer_id", config.get("producer_id")), site_ref=_string("site_ref", config.get("site_ref")), severity=_string("severity", config.get("severity")), region_refs=_string_map("region_refs", config.get("region_refs", {})), crossing_directions=_string_map( "crossing_directions", config.get("crossing_directions", {}) ), ) def _object(name: str, value: object) -> Mapping[str, object]: if not isinstance(value, Mapping) or any(not isinstance(key, str) for key in value): raise ValueError(f"{name} must be an object") return value def _closed(name: str, value: Mapping[str, object], allowed: set[str]) -> None: unknown = set(value) - allowed if unknown: raise ValueError(f"{name} contains unsupported fields") def _string(name: str, value: object) -> str: if not isinstance(value, str) or not value.strip(): raise ValueError(f"event_export.{name} must be a non-empty string") return value def _string_map(name: str, value: object) -> Mapping[str, str]: mapping = _object(f"event_export.{name}", value) if any(not isinstance(item, str) or not item for item in mapping.values()): raise ValueError(f"event_export.{name} values must be non-empty strings") return mapping # type: ignore[return-value]