Files
Daoyuan LiandGitHub 8153e68eb8 fix(channels): escape Slack reserved chars before mrkdwn conversion (#4197)
* fix(channels): escape &/</> in Slack outbound text before mrkdwn conversion

Slack requires callers to replace &, <, and > with their HTML entity
equivalents before sending message text, since an unescaped <...>
triggers Slack's own mention/link syntax (e.g. <@USERID>,
<http://url|label>). SlackChannel.send() ran msg.text through the
markdown-to-mrkdwn converter and sent the result straight to
chat_postMessage with no escaping step, so technical/code content like
"if a < b && b > c:" would arrive as literal Slack markup and render
broken or misinterpreted.

Escaping must happen before the mrkdwn conversion, not after: the
converter emits its own <url|label> syntax for real markdown links
([text](url)), and that generated syntax must reach Slack unescaped.
Escaping raw input first (via html.escape(text, quote=False), which
replaces & before </>) and leaving the converter's output alone
satisfies both requirements.

* fix(channels): preserve a line-leading > as Slack's blockquote marker

_escape_slack_text's html.escape(text, quote=False) replaces every ">"
with "&gt;", including a ">" at the start of a line -- Slack's own
mrkdwn blockquote marker, which the converter otherwise passes through
unchanged. Agent output using markdown blockquotes for quoting/callouts
lost its blockquote styling and arrived as a literal "&gt; " prefix
instead of a rendered blockquote.

Only "&" and "<" neutralize Slack's "<...>" mention/link syntax; ">" is
special to Slack only at the start of a line. Restore a line-leading
">" to a literal character after escaping (re.sub with MULTILINE), so
the mrkdwn converter still renders it as a blockquote; a "<"/"&"
anywhere, and a ">" that is not at a line start, still escape.

New tests cover the line-start preservation, that the exemption does
not widen into "never escape '>'" (a mid-line/non-leading ">" still
escapes), and that restoration applies per line in multiline text; all
three revert cleanly against the unconditional html.escape() to
reproduce the blockquote-corruption bug. Full test_channels.py (259
tests) plus ruff check/format are clean.
2026-07-21 10:02:49 +08:00
..