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:
briandevans 2026-06-04 14:22:56 -07:00 committed by kshitij
parent 244a6f2ceb
commit 57864d07ed
2 changed files with 110 additions and 11 deletions

View file

@ -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)

View file

@ -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():