MEDIA tag regex missing file extensions in gateway
NousResearch/hermes-agent · cluster 29582 · gateway
Data as of
Nothing open on GitHub. Fix merged in #34844. Check it covers your case.
The short answer
- Known?
- Yes. 37 related items grouped here, 10 issues already closed.
- Fix event
- A fix was merged to main in PR #34844. Check it covers your case. No release data is tracked.
- Tracker
- The maintainers' tracker marks this
partially resolved, deciding PR #34844. That is separate from the fix event. - Still open
- Nothing open on GitHub.
Multiple bug reports and fix attempts address the gateway's MEDIA: tag extraction regex failing to recognize common file extensions such as .md, .html, .py, and others. A secondary concern is misalignment between the extension lists used by extract_media() and extract_local_files().
Open issues0
Open PRs0
Fixes on record
-
#34844 fix(gateway): unify MEDIA: extraction extension set + close the unknown-ext black hole (#34517)
files:
gateway/platforms/base.py,gateway/run.py,gateway/stream_consumer.py,scripts/release.py,tests/gateway/test_platform_base.pyShow the diff
-
gateway/platforms/base.py@@ -1131,6 +1131,75 @@ def _log_safe_path(path: str) -> str: } +# --------------------------------------------------------------------------- +# Media-delivery extension allowlist — SINGLE SOURCE OF TRUTH +# +# Both extractors that turn response text into native attachments derive their +# extension set from this tuple: +# * ``extract_media()`` — explicit ``MEDIA:<path>`` tags +# * ``extract_local_files()`` — bare absolute/home paths the agent mentions +# +# Historically these two carried independently-maintained extension lists. +# ``extract_media`` had a narrow list (no .md/.json/.yaml/.xml/.html/...) while +# ``extract_local_files`` had a broad one. Combined with the unconditional +# ``MEDIA:\\s*\\S+`` cleanup at the dispatch sites, that mismatch created a +# silent black hole: a ``MEDIA:/report.md`` tag failed the narrow extract_media +# match, got stripped from the body by the loose cleanup regex, and was then +# invisible to extract_local_files — the file was never delivered (issue +# #34517). Keeping one list eliminates the drift; building the cleanup regexes +# from the same set means a tag is only stripped when its extension is one we +# can actually deliver, so an unknown-extension path survives in the body +# instead of vanishing. +# +# Covers images (inline), video (inline where supported), audio (voice/audio), +# documents/spreadsheets/presentations (send_document), archives, and rendered +# web output. The dispatch partition (image vs video vs document) lives in +# ``gateway/run.py``. +# --------------------------------------------------------------------------- + +MEDIA_DELIVERY_EXTS: Tuple[str, ...] = ( + # Images (embed inline) + ".png", ".jpg", ".jpeg", ".gif", ".webp", ".bmp", ".tiff", ".svg", + # Video (embed inline where supported) + ".mp4", ".mov", ".avi", ".mkv", ".webm", + # Audio (delivered as voice/audio where supported) + ".mp3", ".wav", ".ogg", ".opus", ".m4a", ".flac", + # Documents (uploaded as file attachments) + ".pdf", ".docx", ".doc", ".odt", ".rtf", ".txt", ".md", ".epub", + # Spreadsheets / data + ".xlsx", ".xls", ".ods", ".csv", ".tsv", ".json", ".xml", ".yaml", ".yml", + # Presentations + ".pptx", ".ppt", ".odp", ".key", + # Archives + ".zip", ".tar", ".gz", ".tgz", ".bz2", ".xz", ".7z", ".rar", ".apk", ".ipa", + # Web / rendered output + ".html", ".htm", +) + +# Regex alternation fragment of bare extensions (no leading dot), e.g. +# ``png|jpe?g|...``. ``jpe?g`` collapses jpg/jpeg into one branch. Sorted +# longest-first so the alternation never matches a shorter ext as a prefix of +# a longer one (e.g. ``.tar`` before ``.tar.gz`` components). +_MEDIA_EXT_ALTERNATION = "|".join( + sorted((e.lstrip(".") for e in MEDIA_DELIVERY_EXTS), key=len, reverse=True) +) + +# Anchored ``MEDIA:<path>`` cleanup pattern. Unlike the old loose +# ``MEDIA:\\s*\\S+``, this only strips a tag whose path ends in a known +# deliverable extension (optionally quoted/backticked). A ``MEDIA:`` tag with +# an unknown extension is left in the text so it can still be picked up by the +# 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. +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'''(?=[\s`"',;:)\]}]|$)[`"']?''', + re.IGNORECASE, +) + + def get_document_cache_dir() -> Path: """Return the document cache directory, creating it if it doesn't exist.""" DOCUMENT_CACHE_DIR.mkdir(parents=True, exist_ok=True) @@ -2542,10 +2611,10 @@ def extract_media(content: str) -> Tuple[List[Tuple[str, bool]], str]: cleaned = cleaned.replace("[[as_document]]", "") # Extract MEDIA:<path> tags, allowing optional whitespace after the colon - # and quoted/backticked paths for LLM-formatted outputs. - media_pattern = re.compile( - r'''[`"']?MEDIA:\s*(?P<path>`[^`\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`"',;:)\]}]|$))[`"']?''' - ) + # and quoted/backticked paths for LLM-formatted outputs. The extension + # set is the shared MEDIA_DELIVERY_EXTS source of truth (built once into + # MEDIA_TAG_CLEANUP_RE) so it can never drift from extract_local_files. + media_pattern = MEDIA_TAG_CLEANUP_RE for match in media_pattern.finditer(content): path = match.group("path").strip() if len(path) >= 2 and path[0] == path[-1] and path[0] in "`\"'": @@ -2591,24 +2660,7 @@ def extract_local_files(content: str) -> Tuple[List[str], str]: Tuple of (list of expanded file paths, cleaned text with the raw path strings removed). """ - _LOCAL_MEDIA_EXTS = ( - # Images (embed inline) - '.png', '.jpg', '.jpeg', '.gif', '.webp', '.bmp', '.tiff', '.svg', - # Video (embed inline where supported) - '.mp4', '.mov', '.avi', '.mkv', '.webm', - # Audio (delivered as voice/audio where supported) - '.mp3', '.wav', '.ogg', '.m4a', '.flac', - # Documents (uploaded as file attachments) - '.pdf', '.docx', '.doc', '.odt', '.rtf', '.txt', '.md', - # Spreadsheets / data - '.xlsx', '.xls', '.ods', '.csv', '.tsv', '.json', '.xml', '.yaml', '.yml', - # Presentations - '.pptx', '.ppt', '.odp', '.key', - # Archives - '.zip', '.tar', '.gz', '.tgz', '.bz2', '.xz', '.7z', '.rar', - # Web / rendered output - '.html', '.htm', - ) + _LOCAL_MEDIA_EXTS = MEDIA_DELIVERY_EXTS ext_part = '|'.join(e.lstrip('.') for e in _LOCAL_MEDIA_EXTS) # (?<![/:\w.]) prevents matching inside URLs (e.g. https://…/img.png) @@ -3729,7 +3781,12 @@ async def _stop_typing_task() -> None: # Strip any remaining internal directives from message body (fixes #1561) text_content = text_content.replace("[[audio_as_voice]]", "").strip() text_content = text_content.replace("[[as_document]]", "").strip() - text_content = re.sub(r"MEDIA:\s*\S+", "", text_content).strip() + # Strip only MEDIA: tags whose path has a deliverable extension + # (shared MEDIA_TAG_CLEANUP_RE). A MEDIA: tag with an unknown + # extension is intentionally left in the body so extract_local_files + # below can still pick up the bare path — otherwise the file would + # be silently dropped (issue #34517). + text_content = MEDIA_TAG_CLEANUP_RE.sub("", text_content).strip() if images: logger.info("[%s] extract_images found %d image(s) in response (%d chars)", self.name, len(images), len(response)) -
gateway/run.py@@ -11743,9 +11743,16 @@ async def _deliver_media_from_response( from gateway.platforms.base import BasePlatformAdapter, should_send_media_as_audio - media_files, _ = adapter.extract_media(response) + media_files, cleaned = adapter.extract_media(response) media_files = BasePlatformAdapter.filter_media_delivery_paths(media_files) - _, cleaned = adapter.extract_images(response) + # Chain the cleaned text through each extractor (extract_media → + # extract_images → extract_local_files) so MEDIA: tags and image URLs + # are removed before the bare-path auto-detect runs. Previously the + # cleaned text from extract_media was dropped (``_``) and + # extract_local_files scanned text that still contained MEDIA: tags, + # producing false-positive bare-path matches with the MEDIA: prefix + # glued on. This matches the chain order in gateway/platforms/base.py. + _, cleaned = adapter.extract_images(cleaned) local_files, _ = adapter.extract_local_files(cleaned) local_files = BasePlatformAdapter.filter_local_delivery_paths(local_files) -
gateway/stream_consumer.py@@ -26,6 +26,7 @@ from gateway.platforms.base import BasePlatformAdapter as _BasePlatformAdapter from gateway.platforms.base import _custom_unit_to_cp +from gateway.platforms.base import MEDIA_TAG_CLEANUP_RE from gateway.config import ( DEFAULT_STREAMING_EDIT_INTERVAL as _DEFAULT_STREAMING_EDIT_INTERVAL, DEFAULT_STREAMING_BUFFER_THRESHOLD as _DEFAULT_STREAMING_BUFFER_THRESHOLD, @@ -645,10 +646,13 @@ async def run(self) -> None: except Exception as e: logger.error("Stream consumer error: %s", e) - # Pattern to strip MEDIA:<path> tags (including optional surrounding quotes). - # Matches the simple cleanup regex used by the non-streaming path in - # gateway/platforms/base.py for post-processing. - _MEDIA_RE = re.compile(r'''[`"']?MEDIA:\s*\S+[`"']?''') + # Strip MEDIA:<path> tags before display. Uses the shared anchored + # MEDIA_TAG_CLEANUP_RE from gateway/platforms/base.py — only tags whose + # path ends in a deliverable extension are removed, so an unknown-extension + # path stays visible instead of being silently dropped (issue #34517). + # Streaming and non-streaming paths share the same regex, so a tag is + # treated identically whichever path delivered the text. + _MEDIA_RE = MEDIA_TAG_CLEANUP_RE @staticmethod def _clean_for_display(text: str) -> str: -
scripts/release.py@@ -523,6 +523,8 @@ "[redacted:email]": "JezzaHehn", "[redacted:email]": "dodo-reach", "[redacted:email]": "Bartok9", + "[redacted:email]": "banditburai", + "[redacted:email]": "Kyzcreig", "[redacted:email]": "crayfish-ai", "[redacted:email]": "MestreY0d4-Uninter", "[redacted:email]": "Roy-oss1", -
tests/gateway/test_platform_base.py@@ -362,6 +362,54 @@ def test_both_directives_can_coexist(self): assert "[[as_document]]" not in cleaned +class TestMediaExtensionAllowlistParity: + """Regression coverage for issue #34517 — the MEDIA: extension black hole. + + extract_media used to carry a narrow extension allowlist that omitted + .md/.json/.yaml/.xml/.html etc., while extract_local_files had a broad one. + Combined with an unconditional ``MEDIA:\\s*\\S+`` strip at the dispatch + sites, an unmatched MEDIA: tag for one of those extensions was deleted from + the body before extract_local_files could pick up the bare path — the file + was silently dropped. Both extractors now derive from the single + MEDIA_DELIVERY_EXTS source of truth, and the strip is anchored to that set. + """ + + DROPPED_BEFORE = ["md", "json", "yaml", "yml", "xml", "html", "htm", + "tsv", "svg"] + + def test_previously_dropped_extensions_now_extract(self): + for ext in self.DROPPED_BEFORE: + path = f"/tmp/report.{ext}" + media, _ = BasePlatformAdapter.extract_media(f"Here: MEDIA:{path}") + assert media == [(path, False)], f".{ext} should extract via MEDIA:" + + def test_extract_media_and_local_files_share_one_extension_set(self): + from gateway.platforms.base import MEDIA_DELIVERY_EXTS + # Both functions reference MEDIA_DELIVERY_EXTS; assert the documents + # that motivated the bug are present in the shared set. + for ext in (".md", ".json", ".yaml", ".yml", ".xml", ".html", ".htm"): + assert ext in MEDIA_DELIVERY_EXTS + + def test_unknown_extension_not_black_holed_by_cleanup(self): + """A MEDIA: tag with an unknown extension is NOT stripped from the + body — it survives so extract_local_files can still see the bare path, + rather than vanishing entirely (the core of issue #34517).""" + from gateway.platforms.base import MEDIA_TAG_CLEANUP_RE + text = "Saved to MEDIA:/tmp/data.weirdext done" + media, _ = BasePlatformAdapter.extract_media(text) + assert media == [] # unknown extension is not a deliverable MEDIA tag + stripped = MEDIA_TAG_CLEANUP_RE.sub("", text) + assert "/tmp/data.weirdext" in stripped # path preserved, not dropped + + def test_known_extension_tag_is_stripped_from_body(self): + from gateway.platforms.base import MEDIA_TAG_CLEANUP_RE + text = "Here is your report: MEDIA:/tmp/report.md" + stripped = MEDIA_TAG_CLEANUP_RE.sub("", text).strip() + assert "MEDIA:" not in stripped + assert "/tmp/report.md" not in stripped + assert "Here is your report:" in stripped + + class TestMediaDeliveryPathValidation: def _patch_roots(self, monkeypatch, *roots): monkeypatch.setattr(
-
-
#65510 fix(gateway): deliver MEDIA: tags for every file type via validated egress
files:
gateway/platforms/base.py,tests/gateway/test_platform_base.pyShow the diff
-
gateway/platforms/base.py@@ -1504,12 +1504,16 @@ def _log_safe_path(path: str) -> str: re.IGNORECASE, ) -# Extension-less absolute paths (e.g. Caddyfile, Dockerfile, Makefile) are -# intentionally excluded from MEDIA_TAG_CLEANUP_RE — they are validated and -# delivered via MEDIA_EXTENSIONLESS_TAG_RE so prompt-injection paths that do -# not exist on disk are left visible instead of silently dropped. Paths with -# an unknown but present extension (e.g. .weirdext) stay on the #34517 -# bare-path fallback and are not handled here. +# Paths NOT covered by MEDIA_TAG_CLEANUP_RE's extension alternation — both +# extension-less files (Caddyfile, Dockerfile, Makefile) and files with an +# unknown extension (.py, .log, .weirdext, ...) — are validated and delivered +# via MEDIA_EXTENSIONLESS_TAG_RE. Every ``MEDIA:`` path is therefore +# deliverable regardless of file type (#36060): known extensions extract +# unconditionally via the anchored pattern above, everything else extracts +# only after ``validate_media_delivery_path`` accepts it (exists on disk, not +# under the credential/system denylist, strict-mode rules honored), so +# prompt-injection paths that do not validate are left visible instead of +# silently dropped. MEDIA_EXTENSIONLESS_TAG_RE = re.compile( r'''[`"']?MEDIA:\s*''' r'''(?P<path>`[^`\n]+`|"[^"\n]+"|'[^'\n]+'|''' @@ -1527,8 +1531,16 @@ def _normalize_media_tag_path(raw: str) -> str: def _path_lacks_deliverable_extension(path: str) -> bool: - """True only when the basename has no extension (Caddyfile, Makefile, …).""" - return not Path(path).suffix + """True when MEDIA_TAG_CLEANUP_RE's extension alternation does not cover + ``path`` — either the basename has no extension at all (Caddyfile, + Makefile, …) or the extension is not in MEDIA_DELIVERY_EXTS (.py, .log, + .weirdext, …). Such paths route through the validated delivery pass + (``validate_media_delivery_path``) instead of the unconditional one, so + every file type is deliverable (#36060) while nonexistent / denylisted + paths stay visible in the text. + """ + suffix = Path(path).suffix.lower() + return not suffix or suffix not in MEDIA_DELIVERY_EXTS def _strip_media_tag_directives(text: str) -> str: -
tests/gateway/test_platform_base.py@@ -631,12 +631,15 @@ def test_extract_media_and_local_files_share_one_extension_set(self): def test_unknown_extension_not_black_holed_by_cleanup(self): """A MEDIA: tag with an unknown extension is NOT stripped from the - body — it survives so extract_local_files can still see the bare path, - rather than vanishing entirely (the core of issue #34517).""" + body by the extension-anchored cleanup — and when the path does not + validate (nonexistent file here), it is not delivered either, so the + tag survives visibly instead of vanishing (the core of issue #34517). + Validated unknown-extension paths DO deliver — see + TestUniversalMediaEgress (#36060).""" from gateway.platforms.base import MEDIA_TAG_CLEANUP_RE text = "Saved to MEDIA:/tmp/data.weirdext done" media, _ = BasePlatformAdapter.extract_media(text) - assert media == [] # unknown extension is not a deliverable MEDIA tag + assert media == [] # nonexistent path fails validation, not delivered stripped = MEDIA_TAG_CLEANUP_RE.sub("", text) assert "/tmp/data.weirdext" in stripped # path preserved, not dropped @@ -712,6 +715,104 @@ def test_as_document_directive_stripped_without_media_tag(self): ) +class TestUniversalMediaEgress: + """#36060: every MEDIA: path is deliverable regardless of file type. + + Known extensions extract unconditionally (MEDIA_TAG_CLEANUP_RE); unknown + extensions and extension-less files extract via the validated pass — + delivered when validate_media_delivery_path accepts them, left visible + when it does not (nonexistent, denylisted). + """ + + def _patch_allow_root(self, monkeypatch, root): + monkeypatch.setattr( + "gateway.platforms.base.MEDIA_DELIVERY_SAFE_ROOTS", + (str(root),), + ) + monkeypatch.delenv("HERMES_MEDIA_DELIVERY_STRICT", raising=False) + + @pytest.mark.parametrize("name", [ + "script.py", "server.log", "notes.weirdext", "app.ts", "run.sh", + "config.toml", "styles.css", "contract.sol", + ]) + def test_unknown_extension_delivered_when_file_validates( + self, tmp_path, monkeypatch, name, + ): + root = tmp_path / "output" + root.mkdir() + f = root / name + f.write_text("content", encoding="utf-8") + self._patch_allow_root(monkeypatch, root) + + content = f"Here you go:\nMEDIA:{f}\nDone." + media, cleaned = BasePlatformAdapter.extract_media(content) + assert len(media) == 1 + assert media[0][0] == str(f.resolve()) + assert "MEDIA:" not in cleaned + assert "Done." in cleaned + + def test_unknown_extension_left_visible_when_not_on_disk( + self, tmp_path, monkeypatch, + ): + root = tmp_path / "output" + root.mkdir() + self._patch_allow_root(monkeypatch, root) + + content = "MEDIA:/nonexistent/script.py" + media, cleaned = BasePlatformAdapter.extract_media(content) + assert media == [] + assert "MEDIA:/nonexistent/script.py" in cleaned + + def test_denylisted_paths_still_rejected_regardless_of_extension( + self, tmp_path, monkeypatch, + ): + # A denylisted path must not deliver even though .py/.log/etc now + # route through the validated pass. _media_delivery_denied_paths() + # reads _MEDIA_DELIVERY_DENIED_PREFIXES at call time, so patching the + # tuple exercises the real denylist logic. + secret_dir = [redacted:secret-assignment] / "secrets" + secret_dir.mkdir() + f = secret_dir / "creds.py" + f.write_text("TOKEN = 'x'", encoding="utf-8") + monkeypatch.setattr( + "gateway.platforms.base._MEDIA_DELIVERY_DENIED_PREFIXES", + (str(secret_dir),), + ) + monkeypatch.delenv("HERMES_MEDIA_DELIVERY_STRICT", raising=False) + + content = f"MEDIA:{f}" + media, cleaned = BasePlatformAdapter.extract_media(content) + assert media == [] + assert "MEDIA:" in cleaned # rejected tag stays visible + + def test_strip_for_display_strips_validated_unknown_extension( + self, tmp_path, monkeypatch, + ): + root = tmp_path / "output" + root.mkdir() + f = root / "server.log" + f.write_text("x", encoding="utf-8") + self._patch_allow_root(monkeypatch, root) + + text = f"MEDIA:{f}" + stripped = BasePlatformAdapter.strip_media_directives_for_display(text) + assert "MEDIA:" not in stripped + + def test_strip_for_display_keeps_unvalidated_unknown_extension(self): + text = "MEDIA:/nonexistent/server.log" + stripped = BasePlatformAdapter.strip_media_directives_for_display(text) + assert "MEDIA:/nonexistent/server.log" in stripped + + def test_known_extension_still_unconditional(self): + # Known extensions keep the pre-#36060 behavior: extracted (and the + # tag stripped) even when the file does not exist — downstream + # delivery surfaces the failure. + content = "MEDIA:/nonexistent/report.pdf" + media, cleaned = BasePlatformAdapter.extract_media(content) + assert media == [("/nonexistent/report.pdf", False)] + assert "MEDIA:" not in cleaned + + class TestMediaDeliveryPathValidation: def _patch_roots(self, monkeypatch, *roots): monkeypatch.setattr(
-
25 PRs closed without merging
From the close comments: superseded by another PR 16 · done elsewhere 3 · duplicate 3 · merged via another change 2 · closed without comment 1
-
#29592 fix(gateway): allow html media attachments · superseded by another PR →
#34844 -
#29609 fix(gateway): sync MEDIA regex extension allowlist with SUPPORTED_DOCUMENT_TYPES · superseded by another PR →
#34844 -
#29710 fix(gateway): include html in MEDIA: extraction allowlist · superseded by another PR →
#34844 -
#29829 fix: add html? to MEDIA extension allowlist in extract_media · superseded by another PR →
#29592,#29710,#29609 -
#30192 fix(gateway): restore .md extension to MEDIA extraction allowlist · superseded by another PR →
#34844
Show 20 more
-
#30193 fix(#30186): re-include .md in MEDIA extraction regex · superseded by another PR →
#34844 -
#31138 fix(gateway): match extract_media extension list to extract_local_files · superseded by another PR →
#34844 -
#31150 fix(gateway): unify media extension list between extract_media and extract_local_files · duplicate →
#31138,#29609,#22492,#30588 -
#31159 fix(gateway): derive MEDIA extensions dynamically from SUPPORTED_DOCUMENT_TYPES · superseded by another PR →
#34844 -
#31561 fix(gateway): recognize markdown media attachments · superseded by another PR →
#34844 -
#31754 fix(gateway): extract markdown media attachments · superseded by another PR →
#31561,#31560 -
#31822 fix: add md, html, and htm to MEDIA: extractor and make extension matching case-insensitive · superseded by another PR →
#34844 -
#32472 fix(gateway): derive MEDIA attachment extensions · superseded by another PR →
#34844 -
#32604 fix(gateway,weixin): extend MEDIA regex whitelist + add retry on session errors · superseded by another PR →
#34844 -
#32611 fix(weixin): MEDIA tag regex now covers .md, .json, .yaml, .yml, .toml, .log · superseded by another PR →
#34844 -
#32751 fix(gateway): accept text/config extensions in MEDIA tag regex · superseded by another PR →
#32604 -
#33522 fix(gateway): sync MEDIA: directive extension whitelist with extract_local_files · duplicate →
#29609,#31138,#32995,#33089,#31137 -
#34345 fix(gateway): unify extract_media + extract_local_files extension lists (#34321) · merged via another change →
#34844,#34517 -
#34518 fix(gateway): align MEDIA: regex whitelist with extract_local_files · superseded by another PR →
#34844 -
#34656 fix(gateway): unify MEDIA extraction onto one curated extension set · merged via another change →
#34844,#34517 -
#35477 fix(gateway): add .markdown to MEDIA_DELIVERY_EXTS for outbound document delivery · done elsewhere →
#65510 -
#37342 fix(media): add missing extensions to MEDIA delivery whitelist (#37318) · done elsewhere →
#65510 -
#37347 fix(gateway): build _TOOL_MEDIA_RE from shared MEDIA_EXT_ALTERNATION (#37318) · duplicate →
#37342,#37318,#29609 - #37395 fix(gateway): extend MEDIA: regex whitelist to include common document/code extensions (#37318) · closed without comment
-
#60069 fix(gateway): allow .py/.markdown/.stl/.3mf media & document attachments · done elsewhere →
#65510
Closed issues · 10
- Closed#29582[Bug]: WeChat gateway fails to send .html files due to MEDIA extension allowlist
- Closed#30186MEDIA extraction regex missing .md extension after ea49b3862
- Closed#30518[Bug]: Telegram failed to receive .html file.
- Closed#31137fix(gateway): MEDIA: directive silently drops .html and other extensions due to regex/whitelist drift
- Closed#31560Gateway MEDIA tags do not recognize markdown attachments
- Closed#32601fix(weixin): MEDIA tag leaks for .md/.json/.yaml/.toml/.log files + no retry on session expiry
- Closed#34321`extract_media()` regex missing `.md` and other text-based extensions, mismatch with `extract_local_files()`
- Closed#34517MEDIA: tag silently drops .md (and other) files due to regex whitelist mismatch
- Closed#35474[Bug]: Outbound MEDIA extraction misses .md/.markdown document not sent as file
- Closed#37318MEDIA: tag fails silently for .md files — extract_media() regex whitelist missing common extensions
How this cluster was grouped
-
Link evidence (
partly structural links): some members are connected by structural evidence (shared changed files, or a closing/superseded reference); the rest are joined by an automated relevance judgement over their text. - Members outside that structural core may belong to a different underlying problem.
-
Pairwise link density
0.07: 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 Lab