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
458 changes: 207 additions & 251 deletions gateway/platforms/base.py

Large diffs are not rendered by default.

325 changes: 100 additions & 225 deletions gateway/platforms/discord.py

Large diffs are not rendered by default.

263 changes: 67 additions & 196 deletions gateway/platforms/slack.py

Large diffs are not rendered by default.

667 changes: 42 additions & 625 deletions gateway/platforms/telegram.py

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions model_tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,7 @@ def _discover_tools():
"tools.web_tools",
"tools.terminal_tool",
"tools.file_tools",
"tools.archive_tool",
"tools.vision_tools",
"tools.mixture_of_agents_tool",
"tools.image_generation_tool",
Expand Down
13 changes: 8 additions & 5 deletions tests/gateway/test_discord_document_handling.py
Original file line number Diff line number Diff line change
Expand Up @@ -227,16 +227,19 @@ async def test_oversized_document_skipped(self, adapter):
adapter.handle_message.assert_called_once()

@pytest.mark.asyncio
async def test_unsupported_type_skipped(self, adapter):
"""An unsupported file type (.zip) should be skipped silently."""
async def test_zip_document_cached(self, adapter):
"""A .zip file should be cached as a supported document."""
msg = make_message([
make_attachment(filename="archive.zip", content_type="application/zip")
])
await adapter._handle_message(msg)

with _mock_aiohttp_download(b"PK\x03\x04test"):
await adapter._handle_message(msg)

event = adapter.handle_message.call_args[0][0]
assert event.media_urls == []
assert event.message_type == MessageType.TEXT
assert len(event.media_urls) == 1
assert event.media_types == ["application/zip"]
assert event.message_type == MessageType.DOCUMENT

@pytest.mark.asyncio
async def test_download_error_handled(self, adapter):
Expand Down
2 changes: 1 addition & 1 deletion tests/gateway/test_document_cache.py
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,7 @@ def test_all_extensions_have_mime_types(self):

@pytest.mark.parametrize(
"ext",
[".pdf", ".md", ".txt", ".docx", ".xlsx", ".pptx"],
[".pdf", ".md", ".txt", ".zip", ".docx", ".xlsx", ".pptx"],
)
def test_expected_extensions_present(self, ext):
assert ext in SUPPORTED_DOCUMENT_TYPES
25 changes: 14 additions & 11 deletions tests/gateway/test_slack.py
Original file line number Diff line number Diff line change
Expand Up @@ -408,19 +408,22 @@ async def test_large_txt_not_injected(self, adapter):
assert "[Content of" not in (msg_event.text or "")

@pytest.mark.asyncio
async def test_unsupported_file_type_skipped(self, adapter):
"""A .zip file should be silently skipped."""
event = self._make_event(files=[{
"mimetype": "application/zip",
"name": "archive.zip",
"url_private_download": "https://files.slack.com/archive.zip",
"size": 1024,
}])
await adapter._handle_slack_message(event)
async def test_zip_file_cached(self, adapter):
"""A .zip file should be cached as a supported document."""
with patch.object(adapter, "_download_slack_file_bytes", new_callable=AsyncMock) as dl:
dl.return_value = b"PK\x03\x04zip"
event = self._make_event(files=[{
"mimetype": "application/zip",
"name": "archive.zip",
"url_private_download": "https://files.slack.com/archive.zip",
"size": 1024,
}])
await adapter._handle_slack_message(event)

msg_event = adapter.handle_message.call_args[0][0]
assert msg_event.message_type == MessageType.TEXT
assert len(msg_event.media_urls) == 0
assert msg_event.message_type == MessageType.DOCUMENT
assert len(msg_event.media_urls) == 1
assert msg_event.media_types == ["application/zip"]

@pytest.mark.asyncio
async def test_oversized_document_skipped(self, adapter):
Expand Down
6 changes: 3 additions & 3 deletions tests/gateway/test_telegram_documents.py
Original file line number Diff line number Diff line change
Expand Up @@ -236,15 +236,15 @@ async def test_caption_preserved_with_injection(self, adapter):
assert "Please summarize" in event.text

@pytest.mark.asyncio
async def test_unsupported_type_rejected(self, adapter):
async def test_zip_document_cached(self, adapter):
doc = _make_document(file_name="archive.zip", mime_type="application/zip", file_size=100)
msg = _make_message(document=doc)
update = _make_update(msg)

await adapter._handle_media_message(update, MagicMock())
event = adapter.handle_message.call_args[0][0]
assert "Unsupported document type" in event.text
assert ".zip" in event.text
assert event.media_urls and event.media_urls[0].endswith("archive.zip")
assert event.media_types == ["application/zip"]

@pytest.mark.asyncio
async def test_oversized_file_rejected(self, adapter):
Expand Down
98 changes: 98 additions & 0 deletions tests/tools/test_archive_tool.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import io
import json
import os
import tarfile
import zipfile
from pathlib import Path

from tools.archive_tool import extract_archive_tool


def _make_zip(path: Path, members: dict[str, bytes]) -> None:
with zipfile.ZipFile(path, "w") as zf:
for name, data in members.items():
zf.writestr(name, data)


def _make_zip_with_symlink(path: Path, link_name: str, target: str) -> None:
info = zipfile.ZipInfo(link_name)
info.create_system = 3
info.external_attr = 0o120777 << 16
with zipfile.ZipFile(path, "w") as zf:
zf.writestr(info, target)


def _make_tar_gz(path: Path, members: dict[str, bytes]) -> None:
with tarfile.open(path, "w:gz") as tf:
for name, data in members.items():
info = tarfile.TarInfo(name)
info.size = len(data)
tf.addfile(info, io.BytesIO(data))


def test_extract_archive_tool_extracts_zip(tmp_path):
archive = tmp_path / "sample.zip"
_make_zip(archive, {"nested/hello.txt": b"hi"})

result = json.loads(extract_archive_tool(str(archive)))

assert result["success"] is True
output_dir = Path(result["output_dir"])
assert (output_dir / "nested" / "hello.txt").read_text() == "hi"
assert "nested/hello.txt" in result["extracted_files"]


def test_extract_archive_tool_extracts_tar_gz(tmp_path):
archive = tmp_path / "sample.tar.gz"
_make_tar_gz(archive, {"hello.txt": b"hi"})

result = json.loads(extract_archive_tool(str(archive)))

assert result["success"] is True
output_dir = Path(result["output_dir"])
assert (output_dir / "hello.txt").read_text() == "hi"


def test_extract_archive_tool_blocks_zip_slip(tmp_path):
archive = tmp_path / "escape.zip"
_make_zip(archive, {"../../escape.txt": b"pwnd"})

result = json.loads(extract_archive_tool(str(archive)))

assert result["success"] is False
assert "unsafe archive member path" in result["error"].lower()
assert not (tmp_path / "escape.txt").exists()


def test_extract_archive_tool_rejects_zip_symlink(tmp_path):
archive = tmp_path / "symlink.zip"
_make_zip_with_symlink(archive, "link", "target.txt")

result = json.loads(extract_archive_tool(str(archive)))

assert result["success"] is False
assert "unsupported archive member type" in result["error"].lower()


def test_extract_archive_tool_rejects_symlinked_destination(tmp_path):
archive = tmp_path / "sample.zip"
_make_zip(archive, {"nested/hello.txt": b"hi"})
real_dir = tmp_path / "real"
real_dir.mkdir()
symlink_dir = tmp_path / "linked-out"
os.symlink(real_dir, symlink_dir)

result = json.loads(extract_archive_tool(str(archive), output_dir=str(symlink_dir)))

assert result["success"] is False
assert "symlinked destination" in result["error"].lower()


def test_extract_archive_tool_rejects_unsupported_extension(tmp_path):
archive = tmp_path / "sample.bin"
archive.write_bytes(b"not an archive")

result = json.loads(extract_archive_tool(str(archive)))

assert result["success"] is False
assert "unsupported archive type" in result["error"].lower()
6 changes: 3 additions & 3 deletions tests/tools/test_modal_sandbox_fixes.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,15 +36,15 @@ class TestToolResolution:
"""Verify get_tool_definitions returns all expected tools for eval."""

def test_terminal_and_file_toolsets_resolve_all_tools(self):
"""enabled_toolsets=['terminal', 'file'] should produce 6 tools."""
"""enabled_toolsets=['terminal', 'file'] should include terminal and file tools."""
from model_tools import get_tool_definitions
tools = get_tool_definitions(
enabled_toolsets=["terminal", "file"],
quiet_mode=True,
)
names = {t["function"]["name"] for t in tools}
expected = {"terminal", "process", "read_file", "write_file", "search_files", "patch"}
assert expected == names, f"Expected {expected}, got {names}"
expected_subset = {"terminal", "process", "read_file", "write_file", "search_files", "patch", "extract_archive"}
assert expected_subset.issubset(names), f"Expected at least {expected_subset}, got {names}"

def test_terminal_tool_present(self):
"""The terminal tool must be present (not silently dropped)."""
Expand Down
Loading
Loading