Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 17 additions & 1 deletion gateway/platforms/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -1177,7 +1177,23 @@ def _log_safe_path(path: str) -> str:
# Archives
".zip", ".tar", ".gz", ".tgz", ".bz2", ".xz", ".7z", ".rar", ".apk", ".ipa",
# Web / rendered output
".html", ".htm",
".html", ".htm", ".css",
# Source code (delivered as file attachments — non-credential code review,
# snippet sharing, log triage). Sensitive config (.env, .conf, .ini) is
# intentionally excluded; the credential-path denylist in
# ``_media_delivery_denied_paths`` is a defence-in-depth backstop, but
# keeping these out of the extension allowlist also prevents accidental
# delivery from non-denied locations.
".py", ".js", ".ts", ".tsx", ".jsx", ".mjs", ".cjs",
".go", ".rs", ".sh", ".bash", ".zsh", ".fish",
".sol", ".cpp", ".cc", ".cxx", ".c", ".h", ".hpp",
".java", ".kt", ".scala",
".rb", ".php", ".swift",
".lua", ".pl", ".r",
# Build / project metadata (non-credential)
".toml",
# Logs
".log",
)

# Regex alternation fragment of bare extensions (no leading dot), e.g.
Expand Down
14 changes: 6 additions & 8 deletions gateway/run.py
Original file line number Diff line number Diff line change
Expand Up @@ -17485,11 +17485,10 @@ def _clarify_callback_sync(question: str, choices) -> str:
if _hm.get("role") in {"tool", "function"}:
_hc = _hm.get("content", "")
if "MEDIA:" in _hc:
from gateway.platforms.base import _MEDIA_EXT_ALTERNATION
_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))',
r'MEDIA:((?:[A-Za-z]:[/\\]|/|~\/)\S+\.(?:'
+ _MEDIA_EXT_ALTERNATION + r'))',
re.IGNORECASE
)
for _match in _TOOL_MEDIA_RE.finditer(_hc):
Expand Down Expand Up @@ -17811,11 +17810,10 @@ def _approval_notify_sync(approval_data: dict) -> None:
if msg.get("role") in {"tool", "function"}:
content = msg.get("content", "")
if "MEDIA:" in content:
from gateway.platforms.base import _MEDIA_EXT_ALTERNATION
_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))',
r'MEDIA:((?:[A-Za-z]:[/\\]|/|~\/)\S+\.(?:'
+ _MEDIA_EXT_ALTERNATION + r'))',
re.IGNORECASE
)
for match in _TOOL_MEDIA_RE.finditer(content):
Expand Down
48 changes: 48 additions & 0 deletions tests/gateway/test_platform_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -449,6 +449,54 @@ def test_known_extension_tag_is_stripped_from_body(self):
assert "Here is your report:" in stripped


class TestSourceCodeMediaDelivery:
"""Source code, build manifests, and log files are deliverable as MEDIA:.

Code review, snippet sharing, and log triage are common chat-driven
workflows; without ``.py``/``.js``/``.ts``/``.log`` etc. in the allowlist
the agent silently dropped the attachment and the user only saw the prose
around it. Sensitive single-purpose config formats (``.env``, ``.conf``,
``.ini``) are intentionally excluded — the credential-path denylist is a
backstop, but keeping these out of the extension allowlist is the cheaper
layer of defence.
"""

SOURCE_CODE_EXTS = [
# Python / shell / Web
"py", "js", "ts", "tsx", "jsx", "mjs", "cjs",
"sh", "bash", "zsh", "fish",
# Systems / native
"go", "rs", "sol", "cpp", "cc", "cxx", "c", "h", "hpp",
# JVM / mobile
"java", "kt", "scala", "swift",
# Scripting
"rb", "php", "lua", "pl", "r",
# Build / metadata / web
"toml", "css",
# Logs
"log",
]

def test_source_code_extensions_extract_via_media_tag(self):
for ext in self.SOURCE_CODE_EXTS:
path = f"/home/u/snippet.{ext}"
media, _ = BasePlatformAdapter.extract_media(f"See MEDIA:{path}")
assert media == [(path, False)], (
f".{ext} should extract via MEDIA: tag"
)

def test_sensitive_config_extensions_are_NOT_in_allowlist(self):
"""``.env``/``.conf``/``.ini`` are deliberately omitted — adding them
would let a prompt-injected agent run exfiltrate credentials by
emitting ``MEDIA:~/.env`` even on hosts where the path-denylist
somehow doesn't cover the location."""
from gateway.platforms.base import MEDIA_DELIVERY_EXTS
for ext in (".env", ".conf", ".ini"):
assert ext not in MEDIA_DELIVERY_EXTS, (
f"{ext} must stay out of the MEDIA allowlist"
)


class TestMediaDeliveryPathValidation:
def _patch_roots(self, monkeypatch, *roots):
monkeypatch.setattr(
Expand Down