#!/usr/bin/env python3 """ Red Queen — A3: Telegram Safety Notification Interface (§19). KEIN Daemon, KEIN autonomer Sender. Dieses Modul liefert nur einen deterministischen Formatter + Payload-Bausteine fuer relevante Safety-Events. Der spaetere Red Queen Lead wuerde diese Payloads ueber einen existierenden Telegram-Kanal senden. Anti-Spam-Regeln (TELEGRAM_MISSION_CONTROL): * NUR signifikante Events: CIRCUIT_OPENED, CRITICAL-Severity, ESCALATION_REQUIRED, HUMAN_DECISION_REQUIRED, RETRY_LIMIT_REACHED, OSCILLATION_DETECTED. * Nicht jeder Retry braucht Telegram. RETRY_ALLOWED/DEBUG/info-Events -> kein Alert. Payload ist deterministisch und grundsaetzlich REDACTED (keine Secrets, §28). Test-Konvention: Tests pruefen Formatter-/Payload-Ausgabe (deterministisch), senden aber NICHT real. """ from __future__ import annotations from typing import Any, Dict from rq_safety import ( EVENT_CIRCUIT_OPENED, EVENT_CIRCUIT_RESET_REQUESTED, EVENT_ESCALATION_REQUIRED, EVENT_HUMAN_DECISION_REQUIRED, EVENT_RETRY_LIMIT_REACHED, EVENT_OSCILLATION_DETECTED, EVENT_SAFETY_STATE_ERROR, redact_secret, ) # Meldungs-Praefixe (TELEGRAM_MISSION_CONTROL §5) PREFIX_CRITICAL = "[CRITICAL]" PREFIX_BLOCKER = "[BLOCKER]" PREFIX_DECISION = "[DECISION REQUIRED]" # Events, die einen Telegram-Alert ausloesen (signifikant, kein Spam). ALERT_TYPES = frozenset( { EVENT_CIRCUIT_OPENED, EVENT_CIRCUIT_RESET_REQUESTED, EVENT_ESCALATION_REQUIRED, EVENT_HUMAN_DECISION_REQUIRED, EVENT_RETRY_LIMIT_REACHED, EVENT_OSCILLATION_DETECTED, EVENT_SAFETY_STATE_ERROR, } ) def should_notify(event_type: str, severity: str) -> bool: """Deterministische Spam-Regel: nur signifikante Events melden. CRITICAL-Severity wird immer gemeldet; ansonsten nur in ALERT_TYPES gelistete Event-Typen. RETRY_ALLOWED / INFO / gewoehnliche Events -> kein Alert. """ if severity == "CRITICAL": return True return event_type in ALERT_TYPES def format_alert( event_type: str, severity: str, *, mission_id: Any = None, wp_id: Any = None, scope_type: Any = None, scope_id: Any = None, reason: str = "", reason_code: str = "", ) -> str: """Baue eine deterministische, maschinenlesbare Telegram-Nachricht. Ohne Secrets (redacted). Gibt einen menschenlesbaren Block zurueck, der mit einem festen Praefix beginnt. Kein Alert bei nicht-signifikanten Events -> "". """ if not should_notify(event_type, severity): return "" # kein Alert -> keine Nachricht (Anti-Spam) prefix = PREFIX_CRITICAL if severity == "CRITICAL" else ( PREFIX_DECISION if event_type in (EVENT_HUMAN_DECISION_REQUIRED, EVENT_ESCALATION_REQUIRED) else PREFIX_BLOCKER ) scope = f"scope={scope_type}/{scope_id or '-'}" if scope_type else "scope=n/a" lines = [ f"{prefix} Red Queen Safety", f"EVENT={event_type} SEVERITY={severity}", f"REASON_CODE={reason_code or 'n/a'}", scope, f"mission={mission_id or 'n/a'}" + (f" wp={wp_id}" if wp_id else ""), f"reason={redact_secret(reason) or 'n/a'}", ] return "\n".join(lines) def build_payload( event_type: str, severity: str, *, reason: str = "", reason_code: str = "", scope_type: Any = None, scope_id: Any = None, mission_id: Any = None, wp_id: Any = None, evidence_ref: Any = None, ) -> Dict[str, Any]: """Strukturierte Payload fuer einen Telegram-Sender (deterministisch, redacted). Return-Format ist stabil; ein spaeterer Sender muss nur diesen Dict-Payload an den konfigurierten Messenger senden. """ return { "notify": should_notify(event_type, severity), "text": format_alert( event_type, severity, reason=reason, reason_code=reason_code, scope_type=scope_type, scope_id=scope_id, mission_id=mission_id, wp_id=wp_id, ), "event_type": event_type, "severity": severity, "reason_code": reason_code, "reason": redact_secret(reason), "scope_type": scope_type, "scope_id": scope_id, "mission_id": mission_id, "wp_id": wp_id, "evidence_ref": evidence_ref, }