From a537baa81dcd239286cdab0511a6ece07724f3cc Mon Sep 17 00:00:00 2001 From: DanAsBjorn <4164761+DanAsBjorn@users.noreply.github.com> Date: Tue, 30 Jun 2026 23:35:43 -0700 Subject: [PATCH] fix(matrix): route text-only send_message through adapter for E2EE support Text-only Matrix messages sent via the send_message engine (hermes send, cron deliver: matrix) arrived unencrypted (red padlock) in E2EE rooms. Media sends already routed through the mautrix adapter and encrypted fine, but text-only sends took the raw-HTTP standalone_sender_fn path, which never encrypts. Route ALL Matrix sends through _send_matrix_via_adapter so text is encrypted too. The adapter reuses the live gateway's E2EE session when available (#46310) and falls back to an encryption-aware ephemeral adapter for standalone/cron contexts. The registry standalone_sender_fn stays registered for the contract; it is simply no longer reached for Matrix. Salvaged from PR #20259 onto current main (the original patched the pre-#41112 _send_matrix branch, which had since moved to the plugin's standalone path). Co-authored-by: Teknium <127238744+teknium1@users.noreply.github.com> --- tests/tools/test_send_message_tool.py | 21 ++++++++++++--------- tools/send_message_tool.py | 10 ++++++---- 2 files changed, 18 insertions(+), 13 deletions(-) diff --git a/tests/tools/test_send_message_tool.py b/tests/tools/test_send_message_tool.py index 32be68405..5d28b8b20 100644 --- a/tests/tools/test_send_message_tool.py +++ b/tests/tools/test_send_message_tool.py @@ -879,19 +879,22 @@ class TestSendToPlatformChunking: finally: doc_path.unlink(missing_ok=True) - def test_matrix_text_only_uses_lightweight_path(self): - """Text-only Matrix sends should NOT go through the heavy adapter path. + def test_matrix_text_only_uses_adapter_path(self): + """Text-only Matrix sends must go through the E2EE-capable adapter. - Post-#41112 the lightweight text path flows through the matrix plugin's - registry standalone_sender_fn (not the via-adapter media path).""" + The raw-HTTP standalone path (registry standalone_sender_fn) sends + cleartext, so in an E2EE room text-only messages arrived with a red + padlock. All Matrix sends now route through _send_matrix_via_adapter, + which encrypts via the mautrix adapter (live gateway session when + available, encryption-aware ephemeral adapter otherwise).""" from hermes_cli.plugins import discover_plugins from gateway.platform_registry import platform_registry discover_plugins() - helper = AsyncMock() - lightweight = AsyncMock(return_value={"success": True, "platform": "matrix", "chat_id": "!room:ex.com", "message_id": "$txt"}) + helper = AsyncMock(return_value={"success": True, "platform": "matrix", "chat_id": "!room:ex.com", "message_id": "$txt"}) + standalone = AsyncMock() matrix_entry = platform_registry.get("matrix") original_sender = matrix_entry.standalone_sender_fn - matrix_entry.standalone_sender_fn = lightweight + matrix_entry.standalone_sender_fn = standalone try: with patch("tools.send_message_tool._send_matrix_via_adapter", helper): result = asyncio.run( @@ -906,8 +909,8 @@ class TestSendToPlatformChunking: matrix_entry.standalone_sender_fn = original_sender assert result["success"] is True - helper.assert_not_awaited() - lightweight.assert_awaited_once() + helper.assert_awaited_once() + standalone.assert_not_awaited() def test_send_matrix_via_adapter_sends_document(self, tmp_path): file_path = tmp_path / "report.pdf" diff --git a/tools/send_message_tool.py b/tools/send_message_tool.py index b5a3dfe2d..a6c629260 100644 --- a/tools/send_message_tool.py +++ b/tools/send_message_tool.py @@ -829,8 +829,12 @@ async def _send_to_platform(platform, pconfig, chat_id, message, thread_id=None, last_result = result return last_result - # --- Matrix: use the native adapter helper when media is present --- - if platform == Platform.MATRIX and media_files: + # --- Matrix: route ALL sends through the native adapter so text is + # encrypted in E2EE rooms too (issue: text-only sends arrived with a red + # padlock because they took the raw-HTTP standalone path). The adapter + # reuses the live gateway's E2EE session when available (#46310) and falls + # back to an encryption-aware ephemeral adapter for standalone/cron. --- + if platform == Platform.MATRIX: last_result = None for i, chunk in enumerate(chunks): is_last = (i == len(chunks) - 1) @@ -965,8 +969,6 @@ async def _send_to_platform(platform, pconfig, chat_id, message, thread_id=None, result = await _registry_standalone_send("email", pconfig, chat_id, chunk, thread_id) elif platform == Platform.SMS: result = await _registry_standalone_send("sms", pconfig, chat_id, chunk, thread_id) - elif platform == Platform.MATRIX: - result = await _registry_standalone_send("matrix", pconfig, chat_id, chunk, thread_id) elif platform == Platform.DINGTALK: result = await _registry_standalone_send("dingtalk", pconfig, chat_id, chunk, thread_id) elif platform == Platform.FEISHU: