diff --git a/hermes_cli/web_server.py b/hermes_cli/web_server.py index 572ee5a38eee7..8a0cc48e179a7 100644 --- a/hermes_cli/web_server.py +++ b/hermes_cli/web_server.py @@ -318,6 +318,13 @@ def _apply_ssh_owner_nonce(nonce: Optional[str]) -> None: # injection share a single, testable seam. _DASHBOARD_EMBEDDED_CHAT_ENABLED = True +# Desktop's file.attach compatibility transport sends a complete base64 data +# URL in one JSON-RPC frame. Uvicorn defaults to 16 MiB, which rejects files at +# the preview ceiling before the dispatcher sees them. Keep the gateway +# finite while allowing the 256 MiB raw Desktop attach cap plus base64/JSON +# overhead. +_DESKTOP_ATTACHMENT_WS_MAX_BYTES = 384 * 1024 * 1024 + # Simple rate limiter for the reveal endpoint _reveal_timestamps: List[float] = [] _REVEAL_MAX_PER_WINDOW = 5 @@ -20290,6 +20297,7 @@ def start_server( # reaped via the WebSocketDisconnect → disconnect/reap path. ws_ping_interval=None if _is_loopback else 20.0, ws_ping_timeout=None if _is_loopback else 20.0, + ws_max_size=_DESKTOP_ATTACHMENT_WS_MAX_BYTES, ) server = uvicorn.Server(config) diff --git a/tests/test_web_server.py b/tests/test_web_server.py index eee5973879e98..e14d3151d916d 100644 --- a/tests/test_web_server.py +++ b/tests/test_web_server.py @@ -107,6 +107,20 @@ def test_start_server_disables_ws_ping_on_loopback(monkeypatch): assert captured["ws_ping_timeout"] is None +def test_start_server_accepts_base64_desktop_attachments_above_preview_limit(monkeypatch): + """The gateway frame cap must fit the Desktop attachment default after + base64 expansion and JSON framing; uvicorn's 16 MiB default would reject + the request before ``file.attach`` can stage it. + """ + captured = _stub_uvicorn(monkeypatch) + + web_server.start_server(host="127.0.0.1", port=0, open_browser=False) + + raw_attachment_bytes = 256 * 1024 * 1024 + base64_bytes = ((raw_attachment_bytes + 2) // 3) * 4 + assert captured["ws_max_size"] > base64_bytes + + def test_start_server_enables_ws_ping_for_half_open_detection(monkeypatch): """Non-loopback (public) binds MUST keep the ws ping enabled so half-open connections (reverse-proxy 524, dropped Cloudflare Tunnel) raise diff --git a/tui_gateway/server.py b/tui_gateway/server.py index b88d77b4fbe6c..a6faccf7d87c0 100644 --- a/tui_gateway/server.py +++ b/tui_gateway/server.py @@ -4528,7 +4528,8 @@ def _current_profile_name() -> str: # v2: adds the file.attach RPC (remote-gateway non-image file upload). # v3: adds approvals.mode config RPCs and session.info reconciliation. # v4: session.create fast=false is an explicit per-session normal-tier override. -DESKTOP_BACKEND_CONTRACT = 4 +# v5: uvicorn ws_max_size raised for one-shot base64 file.attach frames (>16 MiB). +DESKTOP_BACKEND_CONTRACT = 5 def _session_usage_snapshot(session: dict | None) -> dict: