"""Severity-based alert dispatcher (``docs/07-risk-controls.md``). Routes anomalies to the right notification channel and, for HIGH and CRITICAL events, arms the kill switch. * ``LOW`` — append to audit log only. * ``MEDIUM`` — audit + ``telegram.notify`` (priority="high"). * ``HIGH`` — audit + ``telegram.notify_alert`` (priority="high") + arm kill switch. * ``CRITICAL``— audit + ``telegram.notify_system_error`` + arm kill switch (already armed → idempotent). """ from __future__ import annotations import logging from enum import StrEnum from cerbero_bite.clients.telegram import TelegramClient from cerbero_bite.safety.audit_log import AuditLog from cerbero_bite.safety.kill_switch import KillSwitch __all__ = ["AlertManager", "Severity"] _log = logging.getLogger("cerbero_bite.runtime.alert_manager") class Severity(StrEnum): LOW = "low" MEDIUM = "medium" HIGH = "high" CRITICAL = "critical" class AlertManager: """Dispatcher used by every runtime module to surface anomalies.""" def __init__( self, *, telegram: TelegramClient, audit_log: AuditLog, kill_switch: KillSwitch, ) -> None: self._telegram = telegram self._audit = audit_log self._kill = kill_switch async def emit( self, severity: Severity, *, source: str, message: str, component: str | None = None, ) -> None: """Emit an alert at the given severity level.""" self._audit.append( event="ALERT", payload={ "severity": severity.value, "source": source, "message": message, "component": component, }, ) if severity == Severity.LOW: _log.info("alert.low source=%s message=%s", source, message) return if severity == Severity.MEDIUM: # The TelegramClient already prefixes [PRIORITY][tag] in the # rendered text, so we pass the raw message and let the # client compose the final form. await self._telegram.notify( message, priority="high", tag=source ) return if severity == Severity.HIGH: await self._telegram.notify_alert( source=source, message=message, priority="high" ) self._kill.arm(reason=message, source=source) return # CRITICAL await self._telegram.notify_system_error( message=message, component=component, priority="critical" ) self._kill.arm(reason=message, source=source) async def low(self, *, source: str, message: str) -> None: await self.emit(Severity.LOW, source=source, message=message) async def medium(self, *, source: str, message: str) -> None: await self.emit(Severity.MEDIUM, source=source, message=message) async def high(self, *, source: str, message: str) -> None: await self.emit(Severity.HIGH, source=source, message=message) async def critical( self, *, source: str, message: str, component: str | None = None ) -> None: await self.emit( Severity.CRITICAL, source=source, message=message, component=component )