Gateway MEDIA tag Windows file path parsing fixes
Cluster 35270 · gateway. Data as of
Nothing open on GitHub. Fix merged in #35388 on 30 May 2026. Check it covers your case.
The short answer
- Known?
- Yes. 3 related items grouped here, 1 issue already closed.
- Fix event
- A fix was merged to main in PR #35388. Check it covers your case. No release data is tracked.
- Tracker
- The maintainers' tracker marks this
resolved, deciding PR #35388. That is separate from the fix event. - Still open
- Nothing open on GitHub.
Multiple bug reports and fixes addressing failures in gateway regex logic to correctly parse Windows-style absolute paths, including drive letters and backslashes, in MEDIA tags.
Open issues0
Open PRs0
Fixes on record
-
#35388 fix(gateway): support Windows absolute paths in MEDIA tag regexes
files:
gateway/platforms/base.py,gateway/run.py,tests/gateway/test_extract_local_files.py,tests/gateway/test_platform_base.py,tests/gateway/test_run_tool_media_re.pyShow the diff
-
gateway/platforms/base.py@@ -1191,10 +1191,12 @@ def _log_safe_path(path: str) -> str: # bare-path detector (extract_local_files) downstream rather than silently # deleted. Shared by the non-streaming dispatch path and the streaming # consumer so both behave identically. +# Path anchors: ``~/`` (Unix home-relative), ``/`` (Unix absolute), +# ``X:\\`` or ``X:/`` (Windows drive-letter absolute — #34632). MEDIA_TAG_CLEANUP_RE = re.compile( r'''[`"']?MEDIA:\s*''' r'''(?P<path>`[^`\n]+`|"[^"\n]+"|'[^'\n]+'|''' - r'''(?:~/|/)\S+(?:[^\S\n]+\S+)*?\.(?:''' + _MEDIA_EXT_ALTERNATION + r'''))''' + r'''(?:~/|/|[A-Za-z]:[/\\])\S+(?:[^\S\n]+\S+)*?\.(?:''' + _MEDIA_EXT_ALTERNATION + r'''))''' r'''(?=[\s`"',;:)\]}]|$)[`"']?''', re.IGNORECASE, ) @@ -2665,9 +2667,10 @@ def extract_local_files(content: str) -> Tuple[List[str], str]: # (?<![/:\w.]) prevents matching inside URLs (e.g. https://…/img.png) # and relative paths (./foo.png) - # (?:~/|/) anchors to absolute or home-relative paths + # (?:~/|/) anchors to absolute or home-relative Unix paths + # (?:[A-Za-z]:[/\\]) anchors to Windows drive-letter paths (#34632) path_re = re.compile( - r'(?<![/:\w.])(?:~/|/)(?:[\w.\-]+/)*[\w.\-]+\.(?:' + ext_part + r')\b', + r'(?<![/:\w.])(?:~/|/|[A-Za-z]:[/\\])(?:[\w.\-]+[/\\])*[\w.\-]+\.(?:' + ext_part + r')\b', re.IGNORECASE, ) -
gateway/run.py@@ -17281,7 +17281,7 @@ def _clarify_callback_sync(question: str, choices) -> str: _hc = _hm.get("content", "") if "MEDIA:" in _hc: _TOOL_MEDIA_RE = re.compile( - r'MEDIA:((?:/|~\/)\S+\.(?:png|jpe?g|gif|webp|' + r'MEDIA:((?:[A-Za-z]:[/\\]|/|~\/)\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))', @@ -17607,7 +17607,7 @@ def _approval_notify_sync(approval_data: dict) -> None: content = msg.get("content", "") if "MEDIA:" in content: _TOOL_MEDIA_RE = re.compile( - r'MEDIA:((?:/|~\/)\S+\.(?:png|jpe?g|gif|webp|' + r'MEDIA:((?:[A-Za-z]:[/\\]|/|~\/)\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))', -
tests/gateway/test_extract_local_files.py@@ -336,9 +336,35 @@ def test_path_with_spaces_not_matched(self): paths, _ = _extract("File at /tmp/my file.png here") assert paths == [] - def test_windows_path_not_matched(self): - """Windows-style paths should not match.""" - paths, _ = _extract("See C:\\Users\\test\\image.png") + @pytest.mark.parametrize( + "content,expected", + [ + # Backslash separators (native Windows style) + ("See C:\\Users\\test\\image.png here", "C:\\Users\\test\\image.png"), + # Forward slashes with drive letter (common in cross-platform code) + ("See C:/Users/test/image.png here", "C:/Users/test/image.png"), + # Non-C: drive + ("Video at D:/data/clip.mp4 ready", "D:/data/clip.mp4"), + # Lowercase drive letter + ("Path e:/audio/track.mp3 done", "e:/audio/track.mp3"), + ], + ) + def test_windows_drive_letter_paths_matched(self, content, expected): + """Windows drive-letter paths (C:/..., C:\\...) must be detected (#34632). + + Prior behavior anchored on (?:~/|/) only, which silently dropped + Windows absolute paths so the agent's bare-path references were + sent as text instead of native uploads. + """ + paths, cleaned = _extract(content) + assert paths == [expected] + assert expected not in cleaned + + def test_relative_windows_path_not_matched(self): + """A bare Windows-style filename without a drive letter must still + not match (e.g. ``foo\\bar.png`` is treated as relative, like its + Unix sibling ``foo/bar.png``).""" + paths, _ = _extract("File at foo\\bar.png here") assert paths == [] def test_relative_path_not_matched(self): -
tests/gateway/test_platform_base.py@@ -361,6 +361,45 @@ def test_both_directives_can_coexist(self): assert "[[audio_as_voice]]" not in cleaned assert "[[as_document]]" not in cleaned + # Windows path support — regression coverage for #34632 + + def test_media_tag_windows_backslash_path(self): + """extract_media should recognise Windows backslash paths.""" + media, cleaned = BasePlatformAdapter.extract_media( + r"MEDIA:C:\Users\kotsu\file.pdf" + ) + assert len(media) == 1 + assert media[0][0].endswith("file.pdf") + + def test_media_tag_windows_forward_slash_path(self): + """extract_media should recognise Windows forward-slash paths.""" + media, cleaned = BasePlatformAdapter.extract_media( + "MEDIA:C:/Users/kotsu/file.pdf" + ) + assert len(media) == 1 + assert media[0][0].endswith("file.pdf") + + def test_media_tag_windows_drive_root(self): + """extract_media should recognise a path at the drive root.""" + media, cleaned = BasePlatformAdapter.extract_media( + r"MEDIA:D:\report.md" + ) + assert len(media) == 1 + assert media[0][0].endswith("report.md") + + def test_media_tag_unix_paths_still_work(self): + """Unix absolute and tilde paths must still extract after Windows change.""" + for content in ["MEDIA:/tmp/audio.ogg", r"MEDIA:~/docs/notes.md"]: + media, _ = BasePlatformAdapter.extract_media(content) + assert len(media) == 1, f"Failed for: {content}" + + def test_relative_path_still_ignored(self): + """Relative Windows-style paths (no drive letter) must not match.""" + media, _ = BasePlatformAdapter.extract_media( + r"MEDIA:Users\kotsu\file.pdf" + ) + assert media == [] + class TestMediaExtensionAllowlistParity: """Regression coverage for issue #34517 — the MEDIA: extension black hole. -
tests/gateway/test_run_tool_media_re.py@@ -0,0 +1,147 @@ +r"""Tests for _TOOL_MEDIA_RE regex patterns in gateway/run.py. + +Issue #34632: The _TOOL_MEDIA_RE patterns in GatewayRunner used (?:/|~\/) to +anchor paths, which only matched Unix-style absolute and home-relative paths. +Windows absolute paths (C:\\Users\\..., D:/...) were silently ignored, causing +MEDIA directive delivery to fail on Windows. + +Fix: Add [A-Za-z]:[/\\\\] as a third anchor alternative in both patterns. + +Two identical _TOOL_MEDIA_RE patterns exist in run.py: +1. History scanning (~L17223): collects already-seen media paths +2. Result scanning (~L17549): extracts new media tags from agent output + +This test file validates that both equivalent regex patterns correctly match +Windows paths while preserving existing Unix path matching behavior. +""" + +import re + +import pytest + + +# Reconstruct the exact _TOOL_MEDIA_RE pattern from gateway/run.py +# The pattern is built by concatenating raw string parts: +# r'MEDIA:((?:[A-Za-z]:[/\\]|/|~\/)\S+\.(?:png|...))' +_TOOL_MEDIA_RE = re.compile( + r'MEDIA:((?:[A-Za-z]:[/\\]|/|~\/)\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))', + re.IGNORECASE, +) + + +# Reconstruct the pre-fix pattern (without Windows anchor) for regression proof +_TOOL_MEDIA_RE_PRE_FIX = re.compile( + 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))', + re.IGNORECASE, +) + + +class TestToolMediaReWindowsPaths: + """Issue #34632: _TOOL_MEDIA_RE must match Windows absolute paths.""" + + # ── Positive: Windows paths now match ────────────────────────── + + @pytest.mark.parametrize("media_tag, expected_path", [ + # Windows backslash paths + ("MEDIA:C:\\Users\\test\\image.png", "C:\\Users\\test\\image.png"), + ("MEDIA:D:\\data\\report.pdf", "D:\\data\\report.pdf"), + ("MEDIA:E:\\Photos\\vacation.jpg", "E:\\Photos\\vacation.jpg"), + # Windows forward-slash paths + ("MEDIA:C:/Users/test/image.png", "C:/Users/test/image.png"), + ("MEDIA:D:/data/report.pdf", "D:/data/report.pdf"), + # Mixed separators + ("MEDIA:C:\\Users/test\\image.webp", "C:\\Users/test\\image.webp"), + # Various extensions + ("MEDIA:F:\\videos\\clip.mp4", "F:\\videos\\clip.mp4"), + ("MEDIA:G:\\audio\\song.mp3", "G:\\audio\\song.mp3"), + ("MEDIA:H:\\docs\\sheet.xlsx", "H:\\docs\\sheet.xlsx"), + ("MEDIA:Z:\\archive\\backup.zip", "Z:\\archive\\backup.zip"), + ]) + def test_windows_paths_match(self, media_tag, expected_path): + """Windows absolute paths with drive letters are matched.""" + match = _TOOL_MEDIA_RE.search(media_tag) + assert match is not None, f"Should match: {media_tag}" + assert match.group(1) == expected_path + + # ── Positive: Unix paths still match ─────────────────────────── + + @pytest.mark.parametrize("media_tag, expected_path", [ + ("MEDIA:/tmp/output.png", "/tmp/output.png"), + ("MEDIA:/var/log/report.pdf", "/var/log/report.pdf"), + ("MEDIA:/home/user/docs/file.txt", "/home/user/docs/file.txt"), + # Home-relative + ("MEDIA:~/Downloads/image.jpg", "~/Downloads/image.jpg"), + ("MEDIA:~/Documents/report.pdf", "~/Documents/report.pdf"), + ]) + def test_unix_paths_still_match(self, media_tag, expected_path): + """Unix-style absolute and home-relative paths still match.""" + match = _TOOL_MEDIA_RE.search(media_tag) + assert match is not None, f"Should match: {media_tag}" + assert match.group(1) == expected_path + + # ── Negative: invalid paths don't match ──────────────────────── + + @pytest.mark.parametrize("text", [ + "No MEDIA tag here", + "MEDIA:relative/path/file.png", # relative path, no anchor + "MEDIA:file.png", # no directory + "MEDIA:C:file.png", # drive letter but no separator + "MEDIA:/path/to/file.unknown", # unsupported extension + "MEDIA:/path/to/file", # no extension + "MEDIA:", # empty path + ]) + def test_invalid_paths_dont_match(self, text): + """Non-MEDIA text, relative paths, and unsupported extensions are ignored.""" + match = _TOOL_MEDIA_RE.search(text) + assert match is None, f"Should NOT match: {text}" + + # ── Negative/preserved: old pattern rejects Windows paths ────── + + @pytest.mark.parametrize("media_tag", [ + "MEDIA:C:\\Users\\test\\image.png", + "MEDIA:D:/data/report.pdf", + "MEDIA:C:\\path\\file.jpg", + ]) + def test_pre_fix_pattern_rejects_windows(self, media_tag): + """The pre-fix pattern (without Windows anchor) does NOT match Windows paths. + This proves the fix is necessary — without it, these paths are silently ignored.""" + match = _TOOL_MEDIA_RE_PRE_FIX.search(media_tag) + assert match is None, f"Pre-fix pattern should NOT match: {media_tag}" + + # ── Edge cases ───────────────────────────────────────────────── + + def test_multiple_media_tags_in_content(self): + """Multiple MEDIA tags in the same content are all found.""" + content = ( + "Some text MEDIA:C:\\path\\img.png and more MEDIA:/tmp/out.pdf trailing" + ) + matches = list(_TOOL_MEDIA_RE.finditer(content)) + assert len(matches) == 2 + paths = [m.group(1) for m in matches] + assert "C:\\path\\img.png" in paths + assert "/tmp/out.pdf" in paths + + def test_case_insensitive_drive_letter(self): + """Drive letters are case-insensitive due to re.IGNORECASE.""" + match_lower = _TOOL_MEDIA_RE.search("MEDIA:c:\\path\\file.png") + match_upper = _TOOL_MEDIA_RE.search("MEDIA:C:\\path\\file.png") + assert match_lower is not None + assert match_upper is not None + assert match_lower.group(1).lower() == match_upper.group(1).lower() + + @pytest.mark.parametrize("media_tag", [ + "MEDIA:C:\\path\\file.jpeg", + "MEDIA:C:\\path\\file.JPG", + "MEDIA:C:\\path\\file.GIF", + "MEDIA:C:\\path\\file.MP4", + ]) + def test_case_insensitive_extensions(self, media_tag): + """File extensions are matched case-insensitively.""" + match = _TOOL_MEDIA_RE.search(media_tag) + assert match is not None, f"Should match: {media_tag}"
-
1 PR closed without merging
From the close comments: done elsewhere 1
-
#35328 fix(gateway): support Windows drive letters and backslashes in file path regex (fixes #35270) · done elsewhere →
#35388,#35161
Closed issues · 1
- Closed#35270[Bug]: Gateway extract_media and extract_local_files fail to parse Windows file paths
How this cluster was grouped
-
Link evidence (
structural links throughout): every member is connected by structural evidence: shared changed files, or a closing/superseded reference. -
Pairwise link density
0.67: the share of item pairs that carry a link, counting a structural link in full and a relevance-only link at 0.36.1.00means every pair is linked.
Pick up a Lab task
Check whether a fix covers the open issues here, with your own agent. Bring back evidence for maintainers.
Open the LabAt a glance
- Subsystem
- gateway
- Merged PRs
- #35388
- Closed issues
- 1
- Attempts
- 1 PR closed without merging
- Fix link checked
- 30 May 2026