Redact bot tokens from Telegram transport error logs
Cluster 58376. Data as of
Nothing open on GitHub. Fix merged in #58501. Check it covers your case.
The short answer
- Known?
- Yes. 5 related items grouped here, 1 issue already closed.
- Fix event
- A fix was merged to main in PR #58501. Check it covers your case. No release data is tracked.
- Tracker
- The maintainers' tracker marks this
resolved, deciding PR #65508. That is separate from the fix event. - Still open
- Nothing open on GitHub.
Multiple fixes addressing bot token exposure in Telegram transport error messages and logs. Also includes coverage for Fireworks token prefixes in redaction logic.
Open issues0
Open PRs0
Fixes on record
-
#58501 fix(redact): cover Fireworks token prefixes + redact Telegram transport errors (#58466, #58377 salvage)
files:
agent/redact.py,plugins/platforms/telegram/adapter.py,scripts/release.py,tests/agent/test_redact.py,tests/gateway/test_telegram_rich_messages.pyShow the diff
-
agent/redact.py@@ -107,7 +107,9 @@ r"brv_[A-Za-z0-9]{10,}", # ByteRover API key r"xai-[A-Za-z0-9]{30,}", # xAI (Grok) API key r"ntn_[A-Za-z0-9]{10,}", # Notion internal integration token + r"fw-[A-Za-z0-9]{30,}", # Fireworks AI API key r"fw_[A-Za-z0-9]{30,}", # Fireworks AI API key + r"fpk_[A-Za-z0-9]{30,}", # Fireworks AI project key ] # ENV assignment patterns: KEY=value where KEY contains a secret-like name. -
plugins/platforms/telegram/adapter.py@@ -22,6 +22,19 @@ logger = logging.getLogger(__name__) +def _redact_telegram_error_text(error: object) -> str: + """Redact secrets from Telegram transport errors before logging or returning them.""" + text = "" if error is None else str(error) + if not text: + return text + try: + from agent.redact import redact_sensitive_text + + return redact_sensitive_text(text, force=True) + except Exception: + return "<telegram error redacted>" + + def _consume_abandoned_task(task: asyncio.Task) -> None: """Observe a detached task's terminal exception to avoid noisy loop logs.""" try: @@ -1609,13 +1622,14 @@ async def _try_send_rich( _m = _re.search(r"retry\s+(?:in\s+)?(\d+)", err_str, _re.IGNORECASE) if _m: _retry_after = float(_m.group(1)) + safe_error = _redact_telegram_error_text(exc) logger.warning( "[%s] sendRichMessage transient failure (no legacy resend): %s", - self.name, exc, + self.name, safe_error, ) return SendResult( success=False, - error=str(exc), + error=safe_error, retryable=(is_connect_timeout or not is_timeout), retry_after=_retry_after, ) @@ -1704,13 +1718,14 @@ async def _try_edit_rich( _TimedOut = None is_timeout = (_TimedOut and isinstance(exc, _TimedOut)) or "timed out" in err_str is_connect_timeout = self._looks_like_connect_timeout(exc) + safe_error = _redact_telegram_error_text(exc) logger.warning( "[%s] rich editMessageText transient failure (no legacy resend): %s", - self.name, exc, + self.name, safe_error, ) return SendResult( success=False, - error=str(exc), + error=safe_error, retryable=(is_connect_timeout or not is_timeout), ) # Telegram won't echo rich content for messages that predate the bot's @@ -1978,15 +1993,16 @@ async def _handle_polling_network_error(self, error: Exception) -> None: "Telegram polling could not reconnect after %d network error retries. " "Restarting gateway." % MAX_NETWORK_RETRIES ) - logger.error("[%s] %s Last error: %s", self.name, message, error) + logger.error("[%s] %s Last error: %s", self.name, message, _redact_telegram_error_text(error)) self._set_fatal_error("telegram_network_error", message, retryable=True) await self._notify_fatal_error() return delay = min(BASE_DELAY * (2 ** (attempt - 1)), MAX_DELAY) + safe_error = _redact_telegram_error_text(error) logger.warning( "[%s] Telegram network error (attempt %d/%d), reconnecting in %ds. Error: %s", - self.name, attempt, MAX_NETWORK_RETRIES, delay, error, + self.name, attempt, MAX_NETWORK_RETRIES, delay, safe_error, ) await asyncio.sleep(delay) @@ -2058,7 +2074,8 @@ async def _handle_polling_network_error(self, error: Exception) -> None: self._background_tasks.add(probe) probe.add_done_callback(self._background_tasks.discard) except Exception as retry_err: - logger.warning("[%s] Telegram polling reconnect failed: %s", self.name, retry_err) + safe_retry_error = _redact_telegram_error_text(retry_err) + logger.warning("[%s] Telegram polling reconnect failed: %s", self.name, safe_retry_error) # start_polling failed — polling is dead and no further error # callbacks will fire, so schedule the next retry ourselves. if not self.has_fatal_error: @@ -3629,18 +3646,20 @@ async def send( err_lower = str(send_err).lower() if "message to be replied not found" in err_lower and reply_to_id is not None: if private_dm_topic_send: + safe_send_error = _redact_telegram_error_text(send_err) return SendResult( success=False, - error=str(send_err), + error=safe_send_error, retryable=False, ) # Original message was deleted before we # could reply. For private-topic fallback # sends, message_thread_id is only valid with # the reply anchor, so drop both together. + safe_send_error = _redact_telegram_error_text(send_err) logger.warning( "[%s] Reply target deleted, retrying without reply_to: %s", - self.name, send_err, + self.name, safe_send_error, ) reply_to_id = None if metadata and metadata.get("telegram_dm_topic_reply_fallback"): @@ -3677,8 +3696,9 @@ async def send( await self._drain_general_connections_after_pool_timeout() if _send_attempt < 2: wait = 2 ** _send_attempt + safe_send_error = _redact_telegram_error_text(send_err) logger.warning("[%s] Network error on send (attempt %d/3), retrying in %ds: %s", - self.name, _send_attempt + 1, wait, send_err) + self.name, _send_attempt + 1, wait, safe_send_error) await asyncio.sleep(wait) else: raise @@ -3687,12 +3707,13 @@ async def send( if retry_after is not None or "retry after" in str(send_err).lower(): if _send_attempt < 2: wait = float(retry_after) if retry_after is not None else 1.0 + safe_send_error = _redact_telegram_error_text(send_err) logger.warning( "[%s] Telegram flood control on send (attempt %d/3), retrying in %.1fs: %s", self.name, _send_attempt + 1, wait, - send_err, + safe_send_error, ) await asyncio.sleep(wait) continue @@ -3726,7 +3747,8 @@ async def send( ) except Exception as e: - logger.error("[%s] Failed to send Telegram message: %s", self.name, e, exc_info=True) + safe_error = _redact_telegram_error_text(e) + logger.error("[%s] Failed to send Telegram message: %s", self.name, safe_error) err_str = str(e).lower() error_kind = classify_send_error(e) # Message too long — content exceeded 4096 chars. Return failure so @@ -3748,7 +3770,7 @@ async def send( is_pool_timeout = self._looks_like_pool_timeout(e) return SendResult( success=False, - error=str(e), + error=safe_error, retryable=(is_connect_timeout or is_pool_timeout or not is_timeout), error_kind=error_kind, ) @@ -3862,10 +3884,11 @@ async def edit_message( if "not modified" in str(fmt_err).lower(): return SendResult(success=True, message_id=message_id) # Fallback: strip MarkdownV2 escapes and retry as clean plain text + safe_format_error = _redact_telegram_error_text(fmt_err) logger.warning( "[%s] MarkdownV2 edit failed, falling back to plain text: %s", self.name, - fmt_err, + safe_format_error, ) _plain = _strip_mdv2(content) if content else content await self._bot.edit_message_text( @@ -3920,11 +3943,12 @@ async def edit_message( ) return SendResult(success=True, message_id=message_id) except Exception as retry_err: + safe_retry_error = _redact_telegram_error_text(retry_err) logger.error( "[%s] Edit retry failed after flood wait: %s", - self.name, retry_err, + self.name, safe_retry_error, ) - return SendResult(success=False, error=str(retry_err)) + return SendResult(success=False, error=safe_retry_error) # Transient network errors (ConnectError, timeouts, server # disconnects) should not permanently disable progress-message # editing. Mark the result retryable so the caller knows it @@ -3945,21 +3969,22 @@ async def edit_message( ) _is_transient = any(m in err_str for m in _transient_markers) if _is_transient: + safe_error = _redact_telegram_error_text(e) logger.warning( "[%s] Transient network error editing message %s (will retry): %s", self.name, message_id, - e, + safe_error, ) - return SendResult(success=False, error=str(e), retryable=True) + return SendResult(success=False, error=safe_error, retryable=True) + safe_error = _redact_telegram_error_text(e) logger.error( "[%s] Failed to edit Telegram message %s: %s", self.name, message_id, - e, - exc_info=True, + safe_error, ) - return SendResult(success=False, error=str(e)) + return SendResult(success=False, error=safe_error) def _truncate_stream_overflow_preview(self, content: str) -> str: """Return a one-message preview for oversized streaming edits. -
scripts/release.py@@ -45,6 +45,7 @@ # Auto-extracted from noreply emails + manual overrides AUTHOR_MAP = { + "[redacted:email]": "ooiuuii", # PR #58466/#58377 salvage (redact: fireworks fw-/fpk_ prefixes; telegram: redact bot tokens out of transport error strings) "[redacted:email]": "msh01", # PR #58250 salvage (telegram: wall-clock init timeout via daemon-thread deadline + abandon the shielded initialize task on timeout so the retry ladder advances instead of hanging on attempt 1/8 under s6 supervision; #58236). Also covers PR #58276 salvage (compression: preserve a real user turn after compaction; #55677). "[redacted:email]": "huanshan5195", # PR #57601 salvage (custom-provider: emit reasoning_effort at the live CustomProfile path so GLM-5.2/ARK/vLLM/Ollama endpoints receive it; + "max" reasoning level) "[redacted:email]": "infinitycrew39", # PR #56431 salvage (honor live vLLM context limits on local endpoints) -
tests/agent/test_redact.py@@ -59,6 +59,22 @@ def test_fal_key(self): result = redact_sensitive_text("fal_abc123def456ghi789jkl") assert "abc123def456" not in result + def test_fireworks_keys(self): + samples = [ + "fw-" + "A" * 40, + "fw_" + "B" * 40, + "fpk_" + "C" * 40, + ] + + for token in samples: + result = redact_sensitive_text(f"provider error {token}") + assert token not in result + assert "..." in result + + def test_short_fireworks_like_words_unchanged(self): + text = "fw-tooshort fw_tooshort fpk_tooshort" + assert redact_sensitive_text(text) == text + def test_notion_internal_integration_token(self): result = redact_sensitive_text("ntn_abc123def456ghi789jkl") assert "abc123def456" not in result -
tests/gateway/test_telegram_rich_messages.py@@ -10,6 +10,7 @@ ``TelegramAdapter`` and wire a mock bot. """ +import logging from types import SimpleNamespace from unittest.mock import AsyncMock, MagicMock @@ -475,6 +476,53 @@ async def test_transient_timeout_is_not_retryable(): assert result.retryable is False [redacted:email] +async def test_rich_transport_error_redacts_bot_token_even_when_redaction_disabled(monkeypatch): + import agent.redact as redact + + monkeypatch.setattr(redact, "_REDACT_ENABLED", False) + token = "[redacted:secret-assignment]" + adapter = _make_adapter() + adapter._bot.do_api_request = AsyncMock( + side_effect=NetworkError( + f"Timed out requesting https://api.telegram.org/bot{token}/sendRichMessage" + ) + ) + + result = await adapter.send("12345", RICH_CONTENT) + + assert result.success is False + assert result.error is not None + assert token not in result.error + assert "bot123456789:***/sendRichMessage" in result.error + adapter._bot.send_message.assert_not_called() + + [redacted:email] +async def test_legacy_send_error_redacts_bot_token_without_traceback(monkeypatch, caplog): + import agent.redact as redact + + monkeypatch.setattr(redact, "_REDACT_ENABLED", False) + token = "[redacted:secret-assignment]" + adapter = _make_adapter({"rich_messages": False}) + adapter._bot.send_message = AsyncMock( + side_effect=BadRequest( + f"Bad Request: https://api.telegram.org/bot{token}/sendMessage" + ) + ) + + with caplog.at_level(logging.ERROR): + result = await adapter.send("12345", "Plain legacy content.") + + assert result.success is False + assert result.error is not None + assert token not in result.error + assert "bot123456789:***/sendMessage" in result.error + assert token not in caplog.text + assert "bot123456789:***/sendMessage" in caplog.text + adapter._bot.do_api_request.assert_not_called() + + @pytest.mark.asyncio async def test_routing_thread_id_maps_to_message_thread_id(): adapter = _make_adapter() @@ -823,6 +871,32 @@ async def test_finalize_edit_plain_content_stays_legacy(): adapter._bot.edit_message_text.assert_awaited() [redacted:email] +async def test_legacy_edit_error_logs_redacted_bot_token_without_traceback(monkeypatch, caplog): + import agent.redact as redact + + monkeypatch.setattr(redact, "_REDACT_ENABLED", False) + token = "[redacted:secret-assignment]" + adapter = _make_adapter() + adapter._bot.edit_message_text = AsyncMock( + side_effect=BadRequest( + f"Bad Request: https://api.telegram.org/bot{token}/editMessageText" + ) + ) + + with caplog.at_level(logging.WARNING): + result = await adapter.edit_message( + "12345", "555", "Just a normal answer.", finalize=True, + ) + + assert result.success is False + assert result.error is not None + assert token not in result.error + assert "bot123456789:***/editMessageText" in result.error + assert token not in caplog.text + assert "bot123456789:***/editMessageText" in caplog.text + + @pytest.mark.asyncio async def test_finalize_edit_cjk_rich_content_stays_legacy_to_avoid_tdesktop_garble(): adapter = _make_adapter()
-
-
#65508 fix(telegram): redact bot tokens from all remaining transport-error sites (#58594 salvage)
files:
plugins/platforms/telegram/adapter.py,tests/gateway/test_telegram_error_redaction.py
2 PRs closed without merging
From the close comments: merged via another change 2
Closed issues · 1
- Closed#58376Telegram transport errors can expose bot tokens
How this cluster was grouped
-
Link evidence (
structural links throughout): every member is connected by structural evidence: shared changed files, or a closing/superseded reference. -
Pairwise link density
0.40: the share of item pairs that carry a link, counting a structural link in full and a relevance-only link at 0.36.1.00means every pair is linked.
Pick up a Lab task
Check whether a fix covers the open issues here, with your own agent. Bring back evidence for maintainers.
Open the Lab