134 lines
4.2 KiB
Python
134 lines
4.2 KiB
Python
#!/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,
|
|
}
|