fix(gateway): suppress operational status/error noise on all chat gateways, not just Telegram (#39293)
The Telegram noise/secret filter added in #28533 gated its work on `_gateway_platform_value(platform) != "telegram"`, so `_sanitize_gateway_final_response` and `_prepare_gateway_status_message` only ran for Telegram. Every other human-facing chat surface (WhatsApp, Discord, Slack, Signal, Matrix, plugin platforms, etc.) received raw provider-error bodies verbatim — including any leaked credentials the secret-redaction pass (`sk-…`, `Bearer …`, `gh[pousr]_…`, `xox[baprs]-…`, `hf_…`, `glpat-…`) was meant to strip. Invert the gate from a one-platform allowlist into a small programmatic-surface denylist: only `local`, `api_server`, `webhook`, and `msgraph_webhook` consume gateway text programmatically and keep raw status/error text. Every other (chat) surface — including unknown/empty platform values and on-demand plugin pseudo-members — fails closed to the redacted, noise-filtered, sanitized path. This widens the same root-cause fix to both call sites: status callbacks and final replies.
This commit is contained in:
parent
244a6f2ceb
commit
57864d07ed
2 changed files with 110 additions and 11 deletions
|
|
@ -87,6 +87,22 @@ _TELEGRAM_NOISY_STATUS_RE = re.compile(
|
|||
re.IGNORECASE | re.DOTALL,
|
||||
)
|
||||
|
||||
# Surfaces that consume gateway text programmatically (CLI/TUI "local"
|
||||
# diagnostics, API JSON, webhook payloads) and therefore must keep RAW
|
||||
# status/error text. EVERY other platform is a human-facing chat surface
|
||||
# where operational lifecycle/provider-error noise (and any secrets in it)
|
||||
# must be suppressed or sanitized. Widens #28533's Telegram-only filter to
|
||||
# all chat gateways (#39293). Fail-closed: unknown/empty platform -> chat.
|
||||
_GATEWAY_RAW_TEXT_PLATFORMS = frozenset(
|
||||
{"local", "api_server", "webhook", "msgraph_webhook"}
|
||||
)
|
||||
|
||||
|
||||
def _gateway_surface_passes_raw_text(platform: Any) -> bool:
|
||||
"""True only for programmatic/local surfaces that must keep raw text."""
|
||||
return _gateway_platform_value(platform) in _GATEWAY_RAW_TEXT_PLATFORMS
|
||||
|
||||
|
||||
_GATEWAY_PROVIDER_ERROR_RE = re.compile(
|
||||
r"(" # infrastructure/provider error preambles, not ordinary assistant prose
|
||||
r"api\s+(?:call\s+)?failed"
|
||||
|
|
@ -370,15 +386,18 @@ def _looks_like_gateway_provider_error(text: str) -> bool:
|
|||
|
||||
|
||||
def _sanitize_gateway_final_response(platform: Any, text: str) -> str:
|
||||
"""Sanitize final gateway replies before sending them to high-noise chats.
|
||||
"""Sanitize final gateway replies before sending them to chat surfaces.
|
||||
|
||||
Telegram is Bob's mobile inbox, so it should receive concise, safe provider
|
||||
failure categories instead of raw HTTP bodies, request IDs, or policy text.
|
||||
Other platforms keep the existing behaviour for now.
|
||||
Every human-facing chat surface (Telegram, WhatsApp, Discord, Slack,
|
||||
Signal, Matrix, plugin platforms, etc.) should receive concise, safe
|
||||
provider failure categories with secrets redacted instead of raw HTTP
|
||||
bodies, request IDs, leaked credentials, or policy text. Only programmatic
|
||||
surfaces in ``_GATEWAY_RAW_TEXT_PLATFORMS`` (CLI/TUI ``local`` diagnostics,
|
||||
API JSON, webhook payloads) keep the raw text unchanged.
|
||||
"""
|
||||
if not text:
|
||||
return text
|
||||
if _gateway_platform_value(platform) != "telegram":
|
||||
if _gateway_surface_passes_raw_text(platform):
|
||||
return text
|
||||
|
||||
redacted = _redact_gateway_user_facing_secrets(str(text))
|
||||
|
|
@ -392,7 +411,7 @@ def _prepare_gateway_status_message(platform: Any, event_type: str, message: str
|
|||
text = str(message or "").strip()
|
||||
if not text:
|
||||
return None
|
||||
if _gateway_platform_value(platform) != "telegram":
|
||||
if _gateway_surface_passes_raw_text(platform):
|
||||
return text
|
||||
|
||||
text = _redact_gateway_user_facing_secrets(text)
|
||||
|
|
|
|||
|
|
@ -1,4 +1,6 @@
|
|||
"""Telegram-specific gateway filtering for noisy status/error output."""
|
||||
"""Gateway noise/secret filtering across chat surfaces (Telegram + siblings)."""
|
||||
|
||||
import pytest
|
||||
|
||||
from gateway.config import Platform
|
||||
from gateway.run import (
|
||||
|
|
@ -6,6 +8,34 @@ from gateway.run import (
|
|||
_sanitize_gateway_final_response,
|
||||
)
|
||||
|
||||
# Every human-facing chat surface that must receive noise-filtered,
|
||||
# secret-redacted, provider-error-sanitized output (not just Telegram).
|
||||
CHAT_PLATFORMS = [
|
||||
"telegram",
|
||||
"whatsapp",
|
||||
"discord",
|
||||
"slack",
|
||||
"signal",
|
||||
"matrix",
|
||||
"mattermost",
|
||||
"dingtalk",
|
||||
"feishu",
|
||||
"wecom",
|
||||
"weixin",
|
||||
"bluebubbles",
|
||||
"qqbot",
|
||||
"homeassistant",
|
||||
"sms",
|
||||
]
|
||||
|
||||
NOISY_STATUS_MESSAGES = [
|
||||
"🗜️ Preflight compression check before sending...",
|
||||
"🗜️ Compacting context — summarizing earlier conversation so I can continue...",
|
||||
"⚠ Compression summary failed: upstream error. Inserted a fallback context marker.",
|
||||
"⏱️ Rate limited. Waiting 30.0s (attempt 2/3)...",
|
||||
"⏳ Retrying in 4.2s (attempt 1/3)...",
|
||||
]
|
||||
|
||||
|
||||
def test_telegram_status_suppresses_auxiliary_and_retry_noise():
|
||||
"""Auxiliary failures and retry backoff chatter should not hit Telegram."""
|
||||
|
|
@ -23,12 +53,62 @@ def test_telegram_status_suppresses_auxiliary_and_retry_noise():
|
|||
assert _prepare_gateway_status_message(Platform.TELEGRAM, "warn", message) is None
|
||||
|
||||
|
||||
def test_non_telegram_status_is_unchanged():
|
||||
"""The Telegram quieting policy must not hide CLI/Discord diagnostics."""
|
||||
def test_programmatic_surfaces_keep_raw_status():
|
||||
"""Programmatic surfaces (local/api/webhook) must keep raw diagnostics.
|
||||
|
||||
Negative case for the invariant: the chat-noise filter must not touch
|
||||
CLI/TUI diagnostics, API JSON, or webhook payloads.
|
||||
"""
|
||||
message = "⏳ Retrying in 4.2s (attempt 1/3)..."
|
||||
|
||||
assert _prepare_gateway_status_message(Platform.DISCORD, "lifecycle", message) == message
|
||||
assert _prepare_gateway_status_message("local", "lifecycle", message) == message
|
||||
for platform in ("local", "api_server", "webhook", "msgraph_webhook"):
|
||||
assert (
|
||||
_prepare_gateway_status_message(platform, "lifecycle", message) == message
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("platform", CHAT_PLATFORMS)
|
||||
@pytest.mark.parametrize("message", NOISY_STATUS_MESSAGES)
|
||||
def test_all_chat_gateways_suppress_noise(platform, message):
|
||||
"""Operational lifecycle/retry noise must be suppressed on every chat surface."""
|
||||
assert _prepare_gateway_status_message(platform, "warn", message) is None
|
||||
|
||||
|
||||
@pytest.mark.parametrize("platform", ["whatsapp", "slack", "signal", "matrix"])
|
||||
def test_chat_gateways_redact_secret_in_provider_error(platform):
|
||||
"""Provider-error bodies carrying secrets must never reach chat users.
|
||||
|
||||
THE security invariant being widened from Telegram (#28533) to all chat
|
||||
surfaces (#39293): a leaked bearer token in a provider error body must be
|
||||
redacted/replaced before delivery on any chat platform.
|
||||
"""
|
||||
raw = (
|
||||
"API call failed after 3 retries: HTTP 401 Unauthorized — "
|
||||
"Authorization: Bearer sk-ABCDEF0123456789abcdef0123"
|
||||
)
|
||||
|
||||
sanitized = _sanitize_gateway_final_response(platform, raw)
|
||||
|
||||
assert "sk-ABCDEF0123456789abcdef0123" not in sanitized
|
||||
assert "sk-ABCDEF" not in sanitized
|
||||
assert "HTTP 401" not in sanitized
|
||||
# The user gets the safe provider-error category instead of the raw body.
|
||||
assert "provider" in sanitized.lower()
|
||||
|
||||
|
||||
def test_plugin_platform_string_suppresses_noise():
|
||||
"""Unknown/plugin chat platforms fail closed to the chat-filter path."""
|
||||
message = "⏳ Retrying in 4.2s (attempt 1/3)..."
|
||||
|
||||
assert _prepare_gateway_status_message("irc", "warn", message) is None
|
||||
|
||||
|
||||
@pytest.mark.parametrize("platform", CHAT_PLATFORMS)
|
||||
def test_chat_gateways_keep_normal_answers(platform):
|
||||
"""Normal assistant content must pass through unchanged on chat surfaces."""
|
||||
answer = "Here is the clean summary you asked for."
|
||||
|
||||
assert _sanitize_gateway_final_response(platform, answer) == answer
|
||||
|
||||
|
||||
def test_telegram_status_sanitizes_raw_provider_security_errors():
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue