fix(slack): truncate inflated original_text in approval/confirm chat.update handlers

Slack re-escapes HTML entities in the interaction payload
(< -> &lt;, > -> &gt;, & -> &amp;), inflating the section text
past the 3000-char Block Kit limit when the button handler
rebuilds updated_blocks for chat.update.

The send path already budget-truncates, but the click handlers
used the echoed original_text verbatim. Cap it to 3000 chars
in both _handle_approval_action and _handle_slash_confirm_action.

Fixes #53693
This commit is contained in:
liuhao1024 2026-06-28 00:05:01 +08:00 committed by Teknium
parent 8fc0c086f5
commit c13af09177
2 changed files with 75 additions and 0 deletions

View file

@ -4300,6 +4300,11 @@ class SlackAdapter(BasePlatformAdapter):
original_text = block.get("text", {}).get("text", "")
break
# Slack re-escapes HTML entities in the interaction payload
# (< → &lt;, > → &gt;, & → &amp;), which can inflate the text
# past the 3000-char section-block limit on chat.update.
original_text = original_text[:3000]
updated_blocks = [
{
"type": "section",
@ -4467,6 +4472,11 @@ class SlackAdapter(BasePlatformAdapter):
original_text = block.get("text", {}).get("text", "")
break
# Slack re-escapes HTML entities in the interaction payload
# (< → &lt;, > → &gt;, & → &amp;), which can inflate the text
# past the 3000-char section-block limit on chat.update.
original_text = original_text[:3000]
updated_blocks = [
{
"type": "section",

View file

@ -275,6 +275,37 @@ class TestSlackApprovalAction:
update_kwargs = mock_client.chat_update.call_args[1]
assert "Denied by alice" in update_kwargs["text"]
@pytest.mark.asyncio
async def test_truncates_inflated_original_text(self):
"""Interaction payload re-escapes HTML entities; text must be capped."""
adapter = _make_adapter()
_attach_auth_runner(adapter)
adapter._approval_resolved["1.2"] = False
# Simulate Slack re-escaping: original was ~2990 chars, but & → &amp;
# etc. inflates it past 3000.
inflated_text = "a" * 2990 + "&amp;" * 10 # 2990 + 50 = 3040 chars
ack = AsyncMock()
body = {
"message": {"ts": "1.2", "blocks": [
{"type": "section", "text": {"type": "mrkdwn", "text": inflated_text}},
]},
"channel": {"id": "C1"},
"user": {"name": "alice", "id": "U_ALICE"},
}
action = {"action_id": "hermes_approve_once", "value": "session-key"}
mock_client = adapter._team_clients["T1"]
mock_client.chat_update = AsyncMock()
with patch("tools.approval.resolve_gateway_approval", return_value=1):
await adapter._handle_approval_action(ack, body, action)
update_kwargs = mock_client.chat_update.call_args[1]
section_text = update_kwargs["blocks"][0]["text"]["text"]
assert len(section_text) <= 3000
@pytest.mark.asyncio
async def test_global_allowlist_blocks_unauthorized_click(self, monkeypatch):
adapter = _make_adapter()
@ -410,6 +441,40 @@ class TestSlackSlashConfirmAction:
secondary_client.chat_postMessage.assert_awaited_once()
adapter._team_clients["T1"].chat_update.assert_not_called()
@pytest.mark.asyncio
async def test_truncates_inflated_original_text(self):
"""Interaction payload re-escapes HTML entities; text must be capped."""
adapter = _make_adapter()
_attach_auth_runner(adapter)
adapter._approval_resolved["2222.3333"] = False
# Simulate Slack re-escaping inflating text past 3000 chars.
inflated_text = "b" * 2990 + "&lt;" * 10 # 2990 + 40 = 3030 chars
ack = AsyncMock()
body = {
"message": {"ts": "2222.3333", "blocks": [
{"type": "section", "text": {"type": "mrkdwn", "text": inflated_text}},
]},
"channel": {"id": "C1"},
"user": {"name": "owner", "id": "U_OWNER"},
}
action = {
"action_id": "hermes_confirm_once",
"value": "agent:main:slack:group:C1:1111|confirm-1",
}
mock_client = adapter._team_clients["T1"]
mock_client.chat_update = AsyncMock()
mock_client.chat_postMessage = AsyncMock()
with patch("tools.slash_confirm.resolve", new=AsyncMock(return_value="ok")):
await adapter._handle_slash_confirm_action(ack, body, action)
update_kwargs = mock_client.chat_update.call_args[1]
section_text = update_kwargs["blocks"][0]["text"]["text"]
assert len(section_text) <= 3000
# ===========================================================================
# _fetch_thread_context