fix(deps): promote markdown to a core dependency so rich delivery works out of the box (#32486) (#38649)

`markdown` was declared only in the `matrix` optional extra, and the
official Docker image installs `--extra all --extra messaging --extra
anthropic --extra bedrock --extra azure-identity --extra hindsight` —
notably NOT `--extra matrix` (the matrix extra is deliberately routed to
lazy-install because `mautrix[encryption]`/`python-olm` can't build on
Windows/macOS — see the 2026-05-12 policy comment in `[all]`).

Result: `markdown` never lands in the image venv, so the Markdown->HTML
conversion on the DEFAULT delivery path silently falls back to plain
text. Cron/agent deliveries render raw `##`/`**`/tables in clients like
Element (no `formatted_body`). The conversion is now used by BOTH
`gateway/platforms/matrix.py` and `tools/send_message_tool.py`, so it is
no longer matrix-specific.

`markdown` is a pure-Python `py3-none-any` wheel (~108KB, no compiled
extensions, no platform constraints), so none of the reasons the matrix
extra was lazy-routed apply to it. Promote it to a core dependency so it
ships in the wheel, the Docker image, and every install; drop the now
redundant copies from the `matrix` extra and the `platform.matrix`
lazy-deps group; refresh the stale "installed with the matrix extra"
docstring.

Verified against a real build: ran the image's exact `uv sync` command
(same extras, no `--extra matrix`) in a clean container off the new
lockfile -> `import markdown` succeeds (3.10.2). On `origin/main` the
same command leaves markdown absent. 223 targeted tests pass
(test_matrix.py + test_lazy_deps.py).

Closes #32486.
This commit is contained in:
Ben Barclay
2026-06-05 09:46:36 +10:00
committed by GitHub
parent 495c3733d8
commit b434f8c3e0
4 changed files with 18 additions and 9 deletions

View File

@ -2799,11 +2799,11 @@ class MatrixAdapter(BasePlatformAdapter):
def _markdown_to_html(self, text: str) -> str: def _markdown_to_html(self, text: str) -> str:
"""Convert Markdown to Matrix-compatible HTML (org.matrix.custom.html). """Convert Markdown to Matrix-compatible HTML (org.matrix.custom.html).
Uses the ``markdown`` library when available (installed with the Uses the ``markdown`` library (a core dependency) when available.
``matrix`` extra). Falls back to a comprehensive regex converter Falls back to a comprehensive regex converter that handles fenced
that handles fenced code blocks, inline code, headers, bold, code blocks, inline code, headers, bold, italic, strikethrough,
italic, strikethrough, links, blockquotes, lists, and horizontal links, blockquotes, lists, and horizontal rules — everything the
rules — everything the Matrix HTML spec allows. Matrix HTML spec allows.
""" """
try: try:
import markdown as _md import markdown as _md

View File

@ -61,6 +61,16 @@ dependencies = [
"prompt_toolkit==3.0.52", "prompt_toolkit==3.0.52",
# Cron scheduler (built-in feature — scheduled cron/interval jobs use croniter). # Cron scheduler (built-in feature — scheduled cron/interval jobs use croniter).
"croniter==6.0.0", "croniter==6.0.0",
# Markdown -> HTML conversion for rich message delivery (Matrix
# `formatted_body`, and the `send_message` tool's HTML path). Now on the
# DEFAULT delivery path, not matrix-specific: without it both
# gateway/platforms/matrix.py and tools/send_message_tool.py silently fall
# back to plain text, so cron/agent deliveries render raw `##`/`**`/tables
# in clients like Element (see #32486). Pure-Python py3-none-any wheel
# (~108KB, no compiled extensions, no platform constraints), so unlike the
# matrix extra's `mautrix`/`python-olm` it's safe to ship everywhere — keeps
# it out of the lazy-install path that exists only for the heavy matrix deps.
"Markdown==3.10.2",
# Skills Hub (GitHub App JWT auth — optional, only needed for bot identity) # Skills Hub (GitHub App JWT auth — optional, only needed for bot identity)
"PyJWT[crypto]==2.12.1", # CVE-2026-32597 "PyJWT[crypto]==2.12.1", # CVE-2026-32597
# Windows has no IANA tzdata shipped with the OS, so Python's ``zoneinfo`` # Windows has no IANA tzdata shipped with the OS, so Python's ``zoneinfo``
@ -104,7 +114,7 @@ dev = ["debugpy==1.8.20", "pytest==9.0.2", "pytest-asyncio==1.3.0", "pytest-time
messaging = ["python-telegram-bot[webhooks]==22.6", "discord.py[voice]==2.7.1", "aiohttp==3.13.3", "brotlicffi==1.2.0.1", "slack-bolt==1.27.0", "slack-sdk==3.40.1", "qrcode==7.4.2"] messaging = ["python-telegram-bot[webhooks]==22.6", "discord.py[voice]==2.7.1", "aiohttp==3.13.3", "brotlicffi==1.2.0.1", "slack-bolt==1.27.0", "slack-sdk==3.40.1", "qrcode==7.4.2"]
cron = [] # croniter is now a core dependency; this extra kept for back-compat cron = [] # croniter is now a core dependency; this extra kept for back-compat
slack = ["slack-bolt==1.27.0", "slack-sdk==3.40.1", "aiohttp==3.13.3"] slack = ["slack-bolt==1.27.0", "slack-sdk==3.40.1", "aiohttp==3.13.3"]
matrix = ["mautrix[encryption]==0.21.0", "Markdown==3.10.2", "aiosqlite==0.22.1", "asyncpg==0.31.0", "aiohttp-socks==0.11.0"] matrix = ["mautrix[encryption]==0.21.0", "aiosqlite==0.22.1", "asyncpg==0.31.0", "aiohttp-socks==0.11.0"]
# WeCom callback-mode adapter — parses untrusted XML POST bodies from # WeCom callback-mode adapter — parses untrusted XML POST bodies from
# WeCom-controlled callback endpoints, so we use defusedxml (drop-in # WeCom-controlled callback endpoints, so we use defusedxml (drop-in
# replacement for stdlib xml.etree.ElementTree) to block billion-laughs # replacement for stdlib xml.etree.ElementTree) to block billion-laughs

View File

@ -135,7 +135,6 @@ LAZY_DEPS: dict[str, tuple[str, ...]] = {
), ),
"platform.matrix": ( "platform.matrix": (
"mautrix[encryption]==0.21.0", "mautrix[encryption]==0.21.0",
"Markdown==3.10.2",
"aiosqlite==0.22.1", "aiosqlite==0.22.1",
"asyncpg==0.31.0", "asyncpg==0.31.0",
"aiohttp-socks==0.11.0", "aiohttp-socks==0.11.0",

4
uv.lock generated
View File

@ -1398,6 +1398,7 @@ dependencies = [
{ name = "fire" }, { name = "fire" },
{ name = "httpx", extra = ["socks"] }, { name = "httpx", extra = ["socks"] },
{ name = "jinja2" }, { name = "jinja2" },
{ name = "markdown" },
{ name = "openai" }, { name = "openai" },
{ name = "pathspec" }, { name = "pathspec" },
{ name = "prompt-toolkit" }, { name = "prompt-toolkit" },
@ -1502,7 +1503,6 @@ matrix = [
{ name = "aiohttp-socks" }, { name = "aiohttp-socks" },
{ name = "aiosqlite" }, { name = "aiosqlite" },
{ name = "asyncpg" }, { name = "asyncpg" },
{ name = "markdown" },
{ name = "mautrix", extra = ["encryption"] }, { name = "mautrix", extra = ["encryption"] },
] ]
mcp = [ mcp = [
@ -1642,7 +1642,7 @@ requires-dist = [
{ name = "httpx", extras = ["socks"], specifier = "==0.28.1" }, { name = "httpx", extras = ["socks"], specifier = "==0.28.1" },
{ name = "jinja2", specifier = "==3.1.6" }, { name = "jinja2", specifier = "==3.1.6" },
{ name = "lark-oapi", marker = "extra == 'feishu'", specifier = "==1.5.3" }, { name = "lark-oapi", marker = "extra == 'feishu'", specifier = "==1.5.3" },
{ name = "markdown", marker = "extra == 'matrix'", specifier = "==3.10.2" }, { name = "markdown", specifier = "==3.10.2" },
{ name = "mautrix", extras = ["encryption"], marker = "extra == 'matrix'", specifier = "==0.21.0" }, { name = "mautrix", extras = ["encryption"], marker = "extra == 'matrix'", specifier = "==0.21.0" },
{ name = "mcp", marker = "extra == 'computer-use'", specifier = "==1.26.0" }, { name = "mcp", marker = "extra == 'computer-use'", specifier = "==1.26.0" },
{ name = "mcp", marker = "extra == 'dev'", specifier = "==1.26.0" }, { name = "mcp", marker = "extra == 'dev'", specifier = "==1.26.0" },