From 0ca0547e908d6f06c50a2b238f4c9a0b07500b08 Mon Sep 17 00:00:00 2001 From: nikhil Date: Mon, 12 Oct 2026 17:36:00 -0700 Subject: [PATCH 1/2] Allow websockets 16 and 17 The sandbox file-watch and terminal connections use only connect(), send(), recv(), close() and ConnectionClosed, which websockets 16 and 17 leave unchanged; websockets 16 only dropped Python 3.9. The <16 bound kept the SDK from installing next to anything that needs websockets 16 or later, such as Anthropic's browser toolset quickstart. websockets 17.1 deprecates calling the synchronous connect() outside a with block, so the sync file-watch and terminal connections pass legacy=True where connect() accepts it, and keep working the same way. Co-Authored-By: Claude Opus 5.5 --- .../client/managers/sandboxes/shared.py | 12 ++++++ .../sync_manager/sandboxes/sandbox_files.py | 3 +- .../sandboxes/sandbox_terminal.py | 5 ++- poetry.lock | 2 +- pyproject.toml | 4 +- tests/test_sandbox_wire_contract.py | 40 ++++++++++++++++++- 6 files changed, 59 insertions(+), 7 deletions(-) diff --git a/hyperbrowser/client/managers/sandboxes/shared.py b/hyperbrowser/client/managers/sandboxes/shared.py index 14a131b7..620561af 100644 --- a/hyperbrowser/client/managers/sandboxes/shared.py +++ b/hyperbrowser/client/managers/sandboxes/shared.py @@ -1,11 +1,14 @@ import base64 import collections.abc +import inspect import posixpath import re from datetime import datetime, timedelta, timezone from typing import Dict, Optional, Union from urllib.parse import urlencode, urlsplit, urlunsplit +from websockets.sync.client import connect as _sync_ws_connect + from ..._request import coerce_request, dump_request from ....exceptions import HyperbrowserError from ....models.sandbox import ( @@ -224,6 +227,15 @@ def _build_query_path(path: str, params: Optional[Dict[str, object]] = None) -> return f"{path}?{urlencode(filtered)}" +# websockets 17.1 deprecates calling the synchronous connect() outside a with block unless it is +# passed legacy=True; earlier releases take no such argument. +SYNC_WEBSOCKET_CONNECT_OPTIONS: Dict[str, bool] = ( + {"legacy": True} + if "legacy" in inspect.signature(_sync_ws_connect).parameters + else {} +) + + def _normalize_websocket_error(error: BaseException) -> HyperbrowserError: if isinstance(error, HyperbrowserError): return error diff --git a/hyperbrowser/client/managers/sync_manager/sandboxes/sandbox_files.py b/hyperbrowser/client/managers/sync_manager/sandboxes/sandbox_files.py index 7fcf97f8..c3464329 100644 --- a/hyperbrowser/client/managers/sync_manager/sandboxes/sandbox_files.py +++ b/hyperbrowser/client/managers/sync_manager/sandboxes/sandbox_files.py @@ -37,6 +37,7 @@ SandboxFileWriteEntry as SandboxFileWriteEntryDict, ) from ...sandboxes.shared import ( + SYNC_WEBSOCKET_CONNECT_OPTIONS, DEFAULT_WATCH_TIMEOUT_MS, _encode_batch_write_entry, _copy_model, @@ -142,7 +143,7 @@ def events( self._runtime_proxy_override, ) headers = build_headers(connection.token, host_header=target.host_header) - connect_kwargs = {} + connect_kwargs: Dict[str, Any] = dict(SYNC_WEBSOCKET_CONNECT_OPTIONS) if target.connect_host is not None and target.connect_port is not None: connect_kwargs["sock"] = socket.create_connection( (target.connect_host, target.connect_port), diff --git a/hyperbrowser/client/managers/sync_manager/sandboxes/sandbox_terminal.py b/hyperbrowser/client/managers/sync_manager/sandboxes/sandbox_terminal.py index 014e54b0..18f8700c 100644 --- a/hyperbrowser/client/managers/sync_manager/sandboxes/sandbox_terminal.py +++ b/hyperbrowser/client/managers/sync_manager/sandboxes/sandbox_terminal.py @@ -1,7 +1,7 @@ import base64 import json import socket -from typing import Dict, Optional, Union +from typing import Any, Dict, Optional, Union from urllib.parse import urlencode from websockets.exceptions import ConnectionClosed @@ -20,6 +20,7 @@ SandboxTerminalCreateParams as SandboxTerminalCreateParamsDict, ) from ...sandboxes.shared import ( + SYNC_WEBSOCKET_CONNECT_OPTIONS, _copy_model, _normalize_terminal_output_chunk, _normalize_terminal_status, @@ -182,7 +183,7 @@ def attach(self, cursor: Optional[int] = None) -> SandboxTerminalConnection: self._runtime_proxy_override, ) headers = build_headers(connection.token, host_header=target.host_header) - connect_kwargs = {} + connect_kwargs: Dict[str, Any] = dict(SYNC_WEBSOCKET_CONNECT_OPTIONS) if target.connect_host is not None and target.connect_port is not None: connect_kwargs["sock"] = socket.create_connection( (target.connect_host, target.connect_port), diff --git a/poetry.lock b/poetry.lock index f4b7d84f..f84b178c 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1097,4 +1097,4 @@ files = [ [metadata] lock-version = "2.1" python-versions = "^3.8" -content-hash = "f46d962831b02c3a3ba8c9a3eb95c2caed6b3ec73c2513f702894a4b59b45b6f" +content-hash = "706a645b82e60cfdf8bd6e0d009a8491b2217f582cb2ee5f08a1a4b946efbf6d" diff --git a/pyproject.toml b/pyproject.toml index 48b31363..572581b1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "hyperbrowser" -version = "1.11.1" +version = "1.11.2" description = "Python SDK for hyperbrowser" authors = ["Nikhil Shahi "] license = "MIT" @@ -17,7 +17,7 @@ python = "^3.8" pydantic = ">=2.0,<3" httpx = ">=0.23.0,<1" jsonref = ">=1.1.0" -websockets = ">=13,<16" +websockets = ">=13,<18" typing-extensions = ">=4.0,<5" cryptography = ">=41.0.0" diff --git a/tests/test_sandbox_wire_contract.py b/tests/test_sandbox_wire_contract.py index 8d3b11ff..e72f336a 100644 --- a/tests/test_sandbox_wire_contract.py +++ b/tests/test_sandbox_wire_contract.py @@ -1,7 +1,14 @@ +import inspect import io +import threading +import warnings + import httpx import pytest from types import SimpleNamespace + +from websockets.sync.client import connect as sync_ws_connect +from websockets.sync.server import serve as sync_ws_serve import hyperbrowser.client.managers.async_manager.sandboxes.sandbox_terminal as async_terminal_module import hyperbrowser.client.managers.sync_manager.sandboxes.sandbox_terminal as sync_terminal_module @@ -33,7 +40,10 @@ from hyperbrowser.client.managers.sync_manager.sandboxes.sandbox_terminal import ( SandboxTerminalApi, ) -from hyperbrowser.client.managers.sandboxes.shared import _build_sandbox_exposed_url +from hyperbrowser.client.managers.sandboxes.shared import ( + SYNC_WEBSOCKET_CONNECT_OPTIONS, + _build_sandbox_exposed_url, +) from hyperbrowser.exceptions import HyperbrowserError from hyperbrowser.models import ( CompleteSandboxImageBuildParams, @@ -2230,6 +2240,7 @@ def fake_target(base_url, path, runtime_proxy_override): def fake_connect(url, additional_headers=None, open_timeout=None, **kwargs): captured["url"] = url + captured["kwargs"] = kwargs return object() monkeypatch.setattr( @@ -2255,6 +2266,33 @@ def fake_connect(url, additional_headers=None, open_timeout=None, **kwargs): assert "cursor=7" in captured["path"] assert "cursor=7" in captured["url"] + assert captured["kwargs"].items() >= SYNC_WEBSOCKET_CONNECT_OPTIONS.items() + + +def test_sync_websocket_connect_options_connect_without_a_deprecation_warning(): + supports_legacy = "legacy" in inspect.signature(sync_ws_connect).parameters + assert SYNC_WEBSOCKET_CONNECT_OPTIONS == ( + {"legacy": True} if supports_legacy else {} + ) + + def echo(websocket): + for message in websocket: + websocket.send(message) + + with sync_ws_serve(echo, "127.0.0.1", 0) as server: + threading.Thread(target=server.serve_forever, daemon=True).start() + port = server.socket.getsockname()[1] + with warnings.catch_warnings(): + warnings.simplefilter("error", DeprecationWarning) + websocket = sync_ws_connect( + f"ws://127.0.0.1:{port}", **SYNC_WEBSOCKET_CONNECT_OPTIONS + ) + try: + websocket.send("ping") + assert websocket.recv() == "ping" + finally: + websocket.close() + server.shutdown() @pytest.mark.anyio From e1d478b4d15291681855d8be93d400cba6ef43e8 Mon Sep 17 00:00:00 2001 From: nikhil Date: Mon, 12 Oct 2026 17:50:15 -0700 Subject: [PATCH 2/2] Bump version to 1.12.0 Co-Authored-By: Claude Opus 5.5 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 572581b1..0221793d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "hyperbrowser" -version = "1.11.2" +version = "1.12.0" description = "Python SDK for hyperbrowser" authors = ["Nikhil Shahi "] license = "MIT"