mirror of
https://github.com/turnstonelabs/turnstone.git
synced 2026-08-12 23:12:23 -06:00
fix(preview): fetch ceiling tracks the widest kind cap, not a flat 10 MB
Review feedback (PR #800): the URL lane hard-capped fetched bodies at 10 MB before kind resolution, making the 32 MiB pdf cap unreachable for URL targets while path targets honored it. The flat pre-check is gone; the guarded fetch's max_bytes now tracks max(PREVIEW_SIZE_CAPS.values()) - mirroring the path lane's stat pre-check - and the per-kind caps after resolution stay authoritative. Also drops a redundant function-local asyncio import in test_console.py.
This commit is contained in:
@@ -2699,7 +2699,6 @@ class TestProxyGetHeaderPassThrough:
|
||||
finding, preview-pane branch)."""
|
||||
|
||||
def test_security_headers_forwarded(self, monkeypatch):
|
||||
import asyncio
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
|
||||
@@ -183,6 +183,29 @@ class TestExecOpenPreview:
|
||||
assert msg.startswith("Error:")
|
||||
assert "too large" in msg
|
||||
|
||||
def test_url_pdf_over_10mb_previews_to_kind_cap(self, monkeypatch):
|
||||
# Review finding (PR #800): a flat 10 MB URL pre-check rejected PDFs
|
||||
# the 32 MiB pdf kind cap allows — the fetch ceiling must track the
|
||||
# widest kind cap and leave the per-kind caps as the authority.
|
||||
from turnstone.core.preview import PREVIEW_SIZE_CAPS
|
||||
|
||||
s = _make_session()
|
||||
body = b"%PDF-1.7\n" + b"a" * (12 * 1024 * 1024)
|
||||
seen = {}
|
||||
|
||||
def _capture(url, **kw):
|
||||
seen.update(kw)
|
||||
return _fake_response(url, body, "application/pdf")
|
||||
|
||||
monkeypatch.setattr("turnstone.core.session.fetch_with_ssrf_guard", _capture)
|
||||
item = s._prepare_open_preview("c1", {"target": "https://acme.com/report.pdf"})
|
||||
_, msg = s._exec_open_preview(item)
|
||||
assert not msg.startswith("Error:")
|
||||
descriptor, _ = s._tool_previews["c1"]
|
||||
assert descriptor["kind"] == "pdf"
|
||||
assert descriptor["size"] == len(body)
|
||||
assert seen["max_bytes"] == max(PREVIEW_SIZE_CAPS.values())
|
||||
|
||||
def test_path_image(self, tmp_path):
|
||||
s = _make_session()
|
||||
p = tmp_path / "chart.png"
|
||||
|
||||
@@ -16159,11 +16159,14 @@ class ChatSession:
|
||||
try:
|
||||
# Every redirect hop is SSRF-screened BEFORE its request goes
|
||||
# out — the pre-approval screen covers only the URL the model
|
||||
# named, not where it 302s.
|
||||
# named, not where it 302s. The fetch ceiling tracks the most
|
||||
# permissive kind cap (like the path lane's stat pre-check);
|
||||
# the per-kind cap after resolution is the authority.
|
||||
resp = fetch_with_ssrf_guard(
|
||||
url,
|
||||
timeout=self.tool_timeout,
|
||||
allow_private_origin=item.get("allow_private_origin", False),
|
||||
max_bytes=max(PREVIEW_SIZE_CAPS.values()),
|
||||
)
|
||||
resp.raise_for_status()
|
||||
except httpx.HTTPStatusError as e:
|
||||
@@ -16177,10 +16180,6 @@ class ChatSession:
|
||||
if resp.url.username or resp.url.password:
|
||||
final_url = str(resp.url.copy_with(username=None, password=None))
|
||||
body = resp.content
|
||||
if len(body) > 10 * 1024 * 1024:
|
||||
return _fail(
|
||||
f"Error: response too large to preview ({len(body):,} bytes; cap 10 MB)"
|
||||
)
|
||||
mime_hint = resp.headers.get("content-type", "")
|
||||
name_hint = final_url
|
||||
source = final_url
|
||||
|
||||
Reference in New Issue
Block a user