From aaddc6c93a153844baa3b57b46028caeca8b2840 Mon Sep 17 00:00:00 2001 From: Julien Talbot Date: Sat, 13 Jun 2026 22:45:38 +0400 Subject: [PATCH] fix(gateway): deliver MEDIA: tags wrapped in Markdown emphasis MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Models routinely present a file to the user with the delivery tag wrapped in Markdown emphasis — `**MEDIA:/path.pptx**`, `*MEDIA:/path*`, `_MEDIA:/path_`. MEDIA_TAG_CLEANUP_RE only tolerated a single leading/trailing quote or backtick (`[`"']?`), and its closing lookahead set excluded `*` and `_`, so an emphasis-wrapped tag never matched. The file was then silently never delivered and the literal `MEDIA:/path` text leaked into the chat instead — the user sees a path, not the attachment. Allow a short run of emphasis/quote markers (`[`"'*_]{0,3}`) on both sides of the tag and add `*`/`_` to the closing lookahead. Code-block, inline-code and blockquote contexts are still neutralised earlier by `_mask_protected_spans` (#35695), so documentation/example tags remain non-deliverable; the absolute-path anchor still rejects relative paths; `_` inside a filename is unaffected. Adds regression coverage in TestExtractMedia for bold/italic/underscore wrapping, mid-prose bold, emphasis-wrapped .html, underscore-in-filename, and emphasis-wrapped relative-path rejection. Co-Authored-By: Claude Fable 5 --- gateway/platforms/base.py | 17 ++++++++--- tests/gateway/test_platform_base.py | 45 +++++++++++++++++++++++++++++ 2 files changed, 58 insertions(+), 4 deletions(-) diff --git a/gateway/platforms/base.py b/gateway/platforms/base.py index af74265d9a1..660be2e92d5 100644 --- a/gateway/platforms/base.py +++ b/gateway/platforms/base.py @@ -1510,11 +1510,20 @@ _MEDIA_EXT_ALTERNATION = "|".join( # consumer so both behave identically. # Path anchors: ``~/`` (Unix home-relative), ``/`` (Unix absolute), # ``X:\\`` or ``X:/`` (Windows drive-letter absolute — #34632). +# Emphasis tolerance: models routinely wrap the tag in Markdown emphasis +# (``**MEDIA:/x.pdf**``, ``*MEDIA:/x.pdf*``, ``_MEDIA:/x.pdf_``) when they +# present a file to the user. The old single-quote anchor (``[`"']?``) and the +# closing lookahead (which lacked ``*``/``_``) failed to match such tags, so the +# file was silently never delivered and the literal ``MEDIA:`` text leaked into +# the chat. Allow a short run of emphasis/quote markers on both sides so the tag +# is recognised regardless of cosmetic Markdown. Code-block / inline-code / +# blockquote contexts are still neutralised earlier by ``_mask_protected_spans`` +# (#35695), so example tags remain non-deliverable. MEDIA_TAG_CLEANUP_RE = re.compile( - r'''[`"']?MEDIA:\s*''' + r'''[`"'*_]{0,3}MEDIA:\s*''' r'''(?P`[^`\n]+`|"[^"\n]+"|'[^'\n]+'|''' r'''(?:~/|/|[A-Za-z]:[/\\])\S+(?:[^\S\n]+\S+)*?\.(?:''' + _MEDIA_EXT_ALTERNATION + r'''))''' - r'''(?=[\s`"',;:)\]}\[]|$)[`"']?''', + r'''(?=[\s`"'*_,;:)\]}\[]|$)[`"'*_]{0,3}''', re.IGNORECASE, ) @@ -1529,10 +1538,10 @@ MEDIA_TAG_CLEANUP_RE = re.compile( # prompt-injection paths that do not validate are left visible instead of # silently dropped. MEDIA_EXTENSIONLESS_TAG_RE = re.compile( - r'''[`"']?MEDIA:\s*''' + r'''[`"'*_]{0,3}MEDIA:\s*''' r'''(?P`[^`\n]+`|"[^"\n]+"|'[^'\n]+'|''' r'''(?:~/|/|[A-Za-z]:[/\\])[^\s\n`"']+)''' - r'''[`"']?\s*''', + r'''[`"'*_]{0,3}\s*''', re.IGNORECASE, ) diff --git a/tests/gateway/test_platform_base.py b/tests/gateway/test_platform_base.py index 0f1d5c9901d..7b4170f457a 100644 --- a/tests/gateway/test_platform_base.py +++ b/tests/gateway/test_platform_base.py @@ -529,6 +529,51 @@ class TestExtractMedia: assert [p for p, _ in media] == ["/r/a.png"] assert "`MEDIA:/ex/b.png`" in cleaned + # --- Markdown emphasis wrapping tolerance --- + # Models routinely present a file as **MEDIA:/path** / *MEDIA:/path* / + # _MEDIA:/path_. The old pattern only tolerated a single quote/backtick, so + # the emphasis prevented the match and the file was silently never + # delivered (the literal MEDIA: text leaked into the chat instead). + + def test_media_bold_wrapped_extracted(self): + media, cleaned = BasePlatformAdapter.extract_media( + "**MEDIA:/home/u/report.pptx**" + ) + assert media == [("/home/u/report.pptx", False)] + assert "MEDIA:" not in cleaned + + def test_media_italic_asterisk_extracted(self): + media, _ = BasePlatformAdapter.extract_media("*MEDIA:/home/u/report.pdf*") + assert media == [("/home/u/report.pdf", False)] + + def test_media_italic_underscore_extracted(self): + media, _ = BasePlatformAdapter.extract_media("_MEDIA:/home/u/report.pdf_") + assert media == [("/home/u/report.pdf", False)] + + def test_media_bold_mid_prose_extracted_and_stripped(self): + media, cleaned = BasePlatformAdapter.extract_media( + "Voici votre fichier **MEDIA:/tmp/r.pdf** bonne lecture" + ) + assert media == [("/tmp/r.pdf", False)] + assert "MEDIA:" not in cleaned + assert "bonne lecture" in cleaned + + def test_media_bold_wrapped_html_extracted(self): + # .html is a recognised extension; emphasis was the only blocker. + media, _ = BasePlatformAdapter.extract_media("**MEDIA:/srv/page.html**") + assert media == [("/srv/page.html", False)] + + def test_media_underscore_in_filename_unaffected(self): + # Emphasis tolerance must not eat a legitimate '_' inside the path. + media, _ = BasePlatformAdapter.extract_media("MEDIA:/tmp/my_report_v2.pptx") + assert media == [("/tmp/my_report_v2.pptx", False)] + + def test_media_bold_relative_path_still_ignored(self): + # The absolute-path anchor must still reject relative paths even when + # wrapped in emphasis. + media, _ = BasePlatformAdapter.extract_media("**MEDIA:report.html**") + assert media == [] + class TestMediaInsideSerializedJson: """Regression coverage for #34375 — MEDIA: embedded in serialized JSON