From c43dd4127932665142e82882d05b5c2818f2b5f3 Mon Sep 17 00:00:00 2001 From: ZMGID Date: Wed, 27 May 2026 15:16:10 +0800 Subject: [PATCH 1/3] fix(gateway): add json|md to MEDIA tag extension whitelist extract_media regex (base.py) and the two GatewayRunner tool-result MEDIA regexes (run.py) listed common document extensions but were missing .json and .md, so MEDIA:/path/x.{json,md} emitted by the agent was never extracted and got delivered as raw text instead of as a native attachment. Whitelist already contained txt/csv/docx/pdf/zip/etc., so other document types worked; only json and md were affected. --- gateway/platforms/base.py | 2 +- gateway/run.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/gateway/platforms/base.py b/gateway/platforms/base.py index d396015468862..4a525a299e7d6 100644 --- a/gateway/platforms/base.py +++ b/gateway/platforms/base.py @@ -2413,7 +2413,7 @@ def extract_media(content: str) -> Tuple[List[Tuple[str, bool]], str]: # Extract MEDIA: tags, allowing optional whitespace after the colon # and quoted/backticked paths for LLM-formatted outputs. media_pattern = re.compile( - r'''[`"']?MEDIA:\s*(?P`[^`\n]+`|"[^"\n]+"|'[^'\n]+'|(?:~/|/)\S+(?:[^\S\n]+\S+)*?\.(?:png|jpe?g|gif|webp|mp4|mov|avi|mkv|webm|ogg|opus|mp3|wav|m4a|flac|epub|pdf|zip|rar|7z|docx?|xlsx?|pptx?|txt|csv|apk|ipa)(?=[\s`"',;:)\]}]|$))[`"']?''' + r'''[`"']?MEDIA:\s*(?P`[^`\n]+`|"[^"\n]+"|'[^'\n]+'|(?:~/|/)\S+(?:[^\S\n]+\S+)*?\.(?:png|jpe?g|gif|webp|mp4|mov|avi|mkv|webm|ogg|opus|mp3|wav|m4a|flac|epub|pdf|zip|rar|7z|docx?|xlsx?|pptx?|txt|csv|json|md|apk|ipa)(?=[\s`"',;:)\]}]|$))[`"']?''' ) for match in media_pattern.finditer(content): path = match.group("path").strip() diff --git a/gateway/run.py b/gateway/run.py index a2e41c6090f8b..3e57c1e0ac16e 100644 --- a/gateway/run.py +++ b/gateway/run.py @@ -16853,7 +16853,7 @@ def _clarify_callback_sync(question: str, choices) -> str: r'MEDIA:((?:/|~\/)\S+\.(?:png|jpe?g|gif|webp|' r'mp4|mov|avi|mkv|webm|ogg|opus|mp3|wav|m4a|' r'flac|epub|pdf|zip|rar|7z|docx?|xlsx?|pptx?|' - r'txt|csv|apk|ipa))', + r'txt|csv|json|md|apk|ipa))', re.IGNORECASE ) for _match in _TOOL_MEDIA_RE.finditer(_hc): @@ -17159,7 +17159,7 @@ def _approval_notify_sync(approval_data: dict) -> None: r'MEDIA:((?:/|~\/)\S+\.(?:png|jpe?g|gif|webp|' r'mp4|mov|avi|mkv|webm|ogg|opus|mp3|wav|m4a|' r'flac|epub|pdf|zip|rar|7z|docx?|xlsx?|pptx?|' - r'txt|csv|apk|ipa))', + r'txt|csv|json|md|apk|ipa))', re.IGNORECASE ) for match in _TOOL_MEDIA_RE.finditer(content): From 36774e94077ee2c8e6228928ccd8f283182abdf8 Mon Sep 17 00:00:00 2001 From: ZMGID Date: Wed, 27 May 2026 15:22:46 +0800 Subject: [PATCH 2/3] test(gateway): cover json|md MEDIA tag extraction Regression test for extract_media now accepting MEDIA:/path.{json,md}. --- tests/gateway/test_platform_base.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/tests/gateway/test_platform_base.py b/tests/gateway/test_platform_base.py index b7d96d4dc3ee4..338dfa0413930 100644 --- a/tests/gateway/test_platform_base.py +++ b/tests/gateway/test_platform_base.py @@ -330,6 +330,14 @@ def test_media_tag_supports_unquoted_flac_paths_with_spaces(self): assert media == [("/tmp/Jane Doe/speech.flac", False)] assert cleaned == "" + def test_media_tag_supports_json_and_md_documents(self): + # Regression: .json and .md were missing from the extension whitelist, + # so these tags were never extracted and got delivered as raw text. + for path in ("/tmp/data.json", "/tmp/notes.md"): + media, cleaned = BasePlatformAdapter.extract_media(f"MEDIA:{path}") + assert media == [(path, False)] + assert cleaned == "" + def test_as_document_directive_stripped_from_cleaned_text(self): """[[as_document]] is a routing directive — strip it from user-visible text just like [[audio_as_voice]]. Callers detect the From f7f062818a748d2c444cfed43316130c9d96d905 Mon Sep 17 00:00:00 2001 From: ZMGID Date: Wed, 27 May 2026 16:33:58 +0800 Subject: [PATCH 3/3] fix(gateway): add html|htm to MEDIA tag extension whitelist Same gap as json/md: .html and .htm were missing from the extract_media regex (base.py) and the two GatewayRunner tool-result MEDIA regexes (run.py), so MEDIA:/path/file.html was delivered as raw text instead of a native attachment. extract_local_files already accepted .html/.htm for the platform-reply path, so this aligns the MEDIA-tag path with it. --- gateway/platforms/base.py | 2 +- gateway/run.py | 4 ++-- tests/gateway/test_platform_base.py | 7 +++++++ 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/gateway/platforms/base.py b/gateway/platforms/base.py index 4a525a299e7d6..0f4dcee60bc5d 100644 --- a/gateway/platforms/base.py +++ b/gateway/platforms/base.py @@ -2413,7 +2413,7 @@ def extract_media(content: str) -> Tuple[List[Tuple[str, bool]], str]: # Extract MEDIA: tags, allowing optional whitespace after the colon # and quoted/backticked paths for LLM-formatted outputs. media_pattern = re.compile( - r'''[`"']?MEDIA:\s*(?P`[^`\n]+`|"[^"\n]+"|'[^'\n]+'|(?:~/|/)\S+(?:[^\S\n]+\S+)*?\.(?:png|jpe?g|gif|webp|mp4|mov|avi|mkv|webm|ogg|opus|mp3|wav|m4a|flac|epub|pdf|zip|rar|7z|docx?|xlsx?|pptx?|txt|csv|json|md|apk|ipa)(?=[\s`"',;:)\]}]|$))[`"']?''' + r'''[`"']?MEDIA:\s*(?P`[^`\n]+`|"[^"\n]+"|'[^'\n]+'|(?:~/|/)\S+(?:[^\S\n]+\S+)*?\.(?:png|jpe?g|gif|webp|mp4|mov|avi|mkv|webm|ogg|opus|mp3|wav|m4a|flac|epub|pdf|zip|rar|7z|docx?|xlsx?|pptx?|txt|csv|json|md|html?|apk|ipa)(?=[\s`"',;:)\]}]|$))[`"']?''' ) for match in media_pattern.finditer(content): path = match.group("path").strip() diff --git a/gateway/run.py b/gateway/run.py index 3e57c1e0ac16e..b40e354145126 100644 --- a/gateway/run.py +++ b/gateway/run.py @@ -16853,7 +16853,7 @@ def _clarify_callback_sync(question: str, choices) -> str: r'MEDIA:((?:/|~\/)\S+\.(?:png|jpe?g|gif|webp|' r'mp4|mov|avi|mkv|webm|ogg|opus|mp3|wav|m4a|' r'flac|epub|pdf|zip|rar|7z|docx?|xlsx?|pptx?|' - r'txt|csv|json|md|apk|ipa))', + r'txt|csv|json|md|html?|apk|ipa))', re.IGNORECASE ) for _match in _TOOL_MEDIA_RE.finditer(_hc): @@ -17159,7 +17159,7 @@ def _approval_notify_sync(approval_data: dict) -> None: r'MEDIA:((?:/|~\/)\S+\.(?:png|jpe?g|gif|webp|' r'mp4|mov|avi|mkv|webm|ogg|opus|mp3|wav|m4a|' r'flac|epub|pdf|zip|rar|7z|docx?|xlsx?|pptx?|' - r'txt|csv|json|md|apk|ipa))', + r'txt|csv|json|md|html?|apk|ipa))', re.IGNORECASE ) for match in _TOOL_MEDIA_RE.finditer(content): diff --git a/tests/gateway/test_platform_base.py b/tests/gateway/test_platform_base.py index 338dfa0413930..f6cff4f082df7 100644 --- a/tests/gateway/test_platform_base.py +++ b/tests/gateway/test_platform_base.py @@ -338,6 +338,13 @@ def test_media_tag_supports_json_and_md_documents(self): assert media == [(path, False)] assert cleaned == "" + def test_media_tag_supports_html_documents(self): + # Regression: .html/.htm were missing from the extension whitelist. + for path in ("/tmp/report.html", "/tmp/page.htm"): + media, cleaned = BasePlatformAdapter.extract_media(f"MEDIA:{path}") + assert media == [(path, False)] + assert cleaned == "" + def test_as_document_directive_stripped_from_cleaned_text(self): """[[as_document]] is a routing directive — strip it from user-visible text just like [[audio_as_voice]]. Callers detect the