240 lines
8.9 KiB
Python
240 lines
8.9 KiB
Python
"""Machine-readable Telegram inline-button contracts.
|
|
|
|
Only callback actions that resolve to one explicit handler contract may be
|
|
rendered. Transport readiness is evaluated separately by TelegramGateway;
|
|
this registry prevents an enabled transport from reviving legacy ghost
|
|
buttons.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import re
|
|
from dataclasses import asdict, dataclass
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class TelegramButtonActionContract:
|
|
action: str
|
|
callback_format: str
|
|
handler: str
|
|
risk: str
|
|
executor: str
|
|
verifier: str
|
|
state: str = "active"
|
|
|
|
|
|
_STATIC_ACTIONS: dict[str, TelegramButtonActionContract] = {
|
|
"detail": TelegramButtonActionContract(
|
|
"detail", "info", "TelegramGateway._send_incident_detail", "low",
|
|
"telegram_info_query", "telegram_callback_reply_receipt",
|
|
),
|
|
"history": TelegramButtonActionContract(
|
|
"history", "info", "TelegramGateway._send_incident_history", "low",
|
|
"telegram_info_query", "telegram_callback_reply_receipt",
|
|
),
|
|
"reanalyze": TelegramButtonActionContract(
|
|
"reanalyze", "info", "TelegramGateway._send_reanalyze_result", "low",
|
|
"telegram_reanalysis_query", "telegram_callback_reply_receipt",
|
|
),
|
|
"drift_view_page": TelegramButtonActionContract(
|
|
"drift_view_page", "info", "TelegramGateway._send_drift_diff_detail", "low",
|
|
"telegram_info_query", "telegram_callback_reply_receipt",
|
|
),
|
|
"approve": TelegramButtonActionContract(
|
|
"approve", "nonce", "TelegramGateway.handle_callback", "high",
|
|
"approval_control_plane", "approval_and_execution_receipt",
|
|
state="blocked_missing_controlled_apply_contract",
|
|
),
|
|
"reject": TelegramButtonActionContract(
|
|
"reject", "nonce", "TelegramGateway.handle_callback", "medium",
|
|
"approval_control_plane", "approval_state_receipt",
|
|
state="blocked_missing_controlled_apply_contract",
|
|
),
|
|
"silence": TelegramButtonActionContract(
|
|
"silence", "nonce", "TelegramGateway._handle_silence", "medium",
|
|
"telegram_silence_store", "telegram_callback_reply_receipt",
|
|
state="blocked_missing_controlled_apply_contract",
|
|
),
|
|
"snooze": TelegramButtonActionContract(
|
|
"snooze", "nonce", "TelegramGateway._handle_snooze", "low",
|
|
"telegram_snooze_store", "telegram_callback_reply_receipt",
|
|
state="blocked_missing_controlled_apply_contract",
|
|
),
|
|
"tune": TelegramButtonActionContract(
|
|
"tune", "nonce", "TelegramGateway._handle_auto_tuning", "low",
|
|
"shadow_candidate_recorder", "candidate_record_receipt",
|
|
state="shadow_candidate_only",
|
|
),
|
|
"log_manual_fix": TelegramButtonActionContract(
|
|
"log_manual_fix", "nonce", "TelegramGateway.handle_callback:log_manual_fix", "low",
|
|
"incident_timeline", "timeline_write_receipt",
|
|
state="blocked_missing_controlled_apply_contract",
|
|
),
|
|
"drift_view": TelegramButtonActionContract(
|
|
"drift_view", "nonce", "TelegramGateway._handle_drift_action", "low",
|
|
"drift_readback", "telegram_callback_reply_receipt",
|
|
state="blocked_missing_controlled_apply_contract",
|
|
),
|
|
"drift_adopt": TelegramButtonActionContract(
|
|
"drift_adopt", "nonce", "TelegramGateway._handle_drift_action", "high",
|
|
"drift_control_plane", "drift_post_verifier",
|
|
state="blocked_missing_controlled_apply_contract",
|
|
),
|
|
"drift_revert": TelegramButtonActionContract(
|
|
"drift_revert", "nonce", "TelegramGateway._handle_drift_action", "high",
|
|
"drift_control_plane", "drift_post_verifier",
|
|
state="blocked_missing_controlled_apply_contract",
|
|
),
|
|
"la": TelegramButtonActionContract(
|
|
"la", "llm_short_id", "TelegramGateway._handle_llm_action_callback", "dynamic",
|
|
"callback_dispatcher", "callback_dispatch_receipt",
|
|
state="blocked_missing_controlled_apply_contract",
|
|
),
|
|
}
|
|
|
|
_AI_ADVISORY_ACTIONS = {
|
|
"ai_advisory_handled",
|
|
"ai_advisory_snooze",
|
|
"ai_advisory_view",
|
|
"ai_advisory_produce_cmd",
|
|
}
|
|
_LLM_SHORT_ID_RE = re.compile(r"^[0-9a-f]{16}$")
|
|
|
|
|
|
def _callback_action(callback_data: str) -> str:
|
|
return str(callback_data or "").split(":", 1)[0].strip()
|
|
|
|
|
|
def _registry_action_contract(action: str) -> TelegramButtonActionContract | None:
|
|
contract = _STATIC_ACTIONS.get(action)
|
|
if contract is not None:
|
|
return contract if contract.state == "active" else None
|
|
if action in _AI_ADVISORY_ACTIONS:
|
|
return None
|
|
|
|
# The YAML registry is the existing source of truth for category actions.
|
|
# Import lazily so registry validation never creates a module cycle.
|
|
from src.services.callback_dispatcher import get_action_spec
|
|
|
|
spec = get_action_spec(action)
|
|
if spec is None or spec.callback_format != "info" or spec.risk != "low":
|
|
return None
|
|
return TelegramButtonActionContract(
|
|
action=spec.name,
|
|
callback_format=spec.callback_format,
|
|
handler="callback_dispatcher.dispatch_action",
|
|
risk=spec.risk,
|
|
executor=f"mcp:{spec.mcp_provider}/{spec.mcp_tool}",
|
|
verifier="callback_dispatch_receipt",
|
|
)
|
|
|
|
|
|
def resolve_callback_action_contract(
|
|
callback_data: str,
|
|
) -> TelegramButtonActionContract | None:
|
|
"""Resolve and validate a callback payload without persisting its nonce."""
|
|
raw = str(callback_data or "").strip()
|
|
if not raw or len(raw.encode("utf-8")) > 64:
|
|
return None
|
|
parts = raw.split(":")
|
|
action = _callback_action(raw)
|
|
contract = _registry_action_contract(action)
|
|
if contract is None:
|
|
return None
|
|
|
|
if contract.callback_format == "info":
|
|
return contract if len(parts) == 2 and bool(parts[1]) else None
|
|
if contract.callback_format == "nonce":
|
|
return contract if len(parts) in {4, 5} and all(parts[1:4]) else None
|
|
if contract.callback_format == "llm_short_id":
|
|
return (
|
|
contract
|
|
if len(parts) == 2 and _LLM_SHORT_ID_RE.fullmatch(parts[1])
|
|
else None
|
|
)
|
|
if contract.callback_format == "advisory":
|
|
return contract if len(parts) == 3 and all(parts[1:]) else None
|
|
return None
|
|
|
|
|
|
def callback_action_is_registered(callback_data: str) -> bool:
|
|
return resolve_callback_action_contract(callback_data) is not None
|
|
|
|
|
|
def registered_info_callback_actions() -> frozenset[str]:
|
|
"""Return every two-part callback action accepted by the dispatcher."""
|
|
from src.services.callback_dispatcher import load_action_registry
|
|
|
|
names = {
|
|
action
|
|
for action, contract in _STATIC_ACTIONS.items()
|
|
if contract.callback_format == "info" and contract.state == "active"
|
|
}
|
|
names.update(
|
|
spec.name
|
|
for spec in load_action_registry().values()
|
|
if spec.callback_format == "info" and spec.risk == "low"
|
|
)
|
|
return frozenset(names)
|
|
|
|
|
|
def telegram_button_registry_snapshot() -> dict[str, object]:
|
|
"""Return a redaction-safe registry readback for health and tests."""
|
|
from src.services.callback_dispatcher import load_action_registry
|
|
|
|
contracts = list(_STATIC_ACTIONS.values())
|
|
contracts.extend(
|
|
TelegramButtonActionContract(
|
|
action=action,
|
|
callback_format="advisory",
|
|
handler="TelegramGateway._handle_ai_advisory_action",
|
|
risk="low",
|
|
executor="ai_advisory_control_plane",
|
|
verifier="telegram_callback_reply_receipt",
|
|
state="blocked_missing_controlled_apply_contract",
|
|
)
|
|
for action in sorted(_AI_ADVISORY_ACTIONS)
|
|
)
|
|
contracts.extend(
|
|
TelegramButtonActionContract(
|
|
action=spec.name,
|
|
callback_format=spec.callback_format,
|
|
handler="callback_dispatcher.dispatch_action",
|
|
risk=spec.risk,
|
|
executor=f"mcp:{spec.mcp_provider}/{spec.mcp_tool}",
|
|
verifier="callback_dispatch_receipt",
|
|
state=(
|
|
"active"
|
|
if spec.callback_format == "info" and spec.risk == "low"
|
|
else "break_glass_only"
|
|
if spec.risk == "critical"
|
|
else "blocked_missing_controlled_apply_contract"
|
|
),
|
|
)
|
|
for spec in load_action_registry().values()
|
|
if spec.name not in _STATIC_ACTIONS
|
|
)
|
|
rows = sorted((asdict(item) for item in contracts), key=lambda item: item["action"])
|
|
return {
|
|
"schema_version": "telegram_button_action_registry_v1",
|
|
"declared_action_count": len(rows),
|
|
"registered_action_count": sum(
|
|
1 for item in rows if item["state"] == "active"
|
|
),
|
|
"blocked_action_count": sum(
|
|
1 for item in rows if item["state"] != "active"
|
|
),
|
|
"unknown_action_policy": "do_not_render",
|
|
"write_action_policy": "fail_closed_until_controlled_apply_contract",
|
|
"actions": rows,
|
|
}
|
|
|
|
|
|
__all__ = [
|
|
"TelegramButtonActionContract",
|
|
"callback_action_is_registered",
|
|
"registered_info_callback_actions",
|
|
"resolve_callback_action_contract",
|
|
"telegram_button_registry_snapshot",
|
|
]
|