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>
This commit is contained in:
DanAsBjorn 2026-06-30 23:35:43 -07:00 committed by Teknium
parent cc1e4c32c0
commit a537baa81d
2 changed files with 18 additions and 13 deletions

View file

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

View file

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