fix(telegram): require durable callback receipts

This commit is contained in:
ogt
2026-07-16 23:49:23 +08:00
parent cb7c775d12
commit 84a3fa17a9
4 changed files with 186 additions and 34 deletions

View File

@@ -43,18 +43,22 @@ _STATIC_ACTIONS: dict[str, TelegramButtonActionContract] = {
"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",
@@ -64,22 +68,27 @@ _STATIC_ACTIONS: dict[str, TelegramButtonActionContract] = {
"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",
),
}
@@ -99,23 +108,16 @@ def _callback_action(callback_data: str) -> str:
def _registry_action_contract(action: str) -> TelegramButtonActionContract | None:
contract = _STATIC_ACTIONS.get(action)
if contract is not None:
return contract
return contract if contract.state == "active" else None
if action in _AI_ADVISORY_ACTIONS:
return TelegramButtonActionContract(
action,
"advisory",
"TelegramGateway._handle_ai_advisory_action",
"low",
"ai_advisory_control_plane",
"telegram_callback_reply_receipt",
)
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:
if spec is None or spec.callback_format != "info" or spec.risk != "low":
return None
return TelegramButtonActionContract(
action=spec.name,
@@ -166,12 +168,12 @@ def registered_info_callback_actions() -> frozenset[str]:
names = {
action
for action, contract in _STATIC_ACTIONS.items()
if contract.callback_format == "info"
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"
if spec.callback_format == "info" and spec.risk == "low"
)
return frozenset(names)
@@ -189,6 +191,7 @@ def telegram_button_registry_snapshot() -> dict[str, object]:
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)
)
@@ -200,6 +203,13 @@ def telegram_button_registry_snapshot() -> dict[str, object]:
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
@@ -207,8 +217,15 @@ def telegram_button_registry_snapshot() -> dict[str, object]:
rows = sorted((asdict(item) for item in contracts), key=lambda item: item["action"])
return {
"schema_version": "telegram_button_action_registry_v1",
"registered_action_count": len(rows),
"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,
}