Skip to content

Websocket Client

A pool of remote monty workers reached over a WebSocket instead of local subprocesses — the intended peer is monty-server. See running monty-server and the remote-worker trust boundary. A remote peer may be CPython rather than a Monty sandbox; the transport does not provide isolation.

Connection loss and shutdown

MontyDisconnectError means the connection closed mid-session. It does not distinguish a worker crash from a server policy drop; check out a new session.

MontyShutdown means the server declined the next request because it is shutting down. That request did not run. If the exception includes a dump, restore it into a fresh session using the appropriate snapshot loader. Restoring a suspended call re-announces it even if the host already executed the callback, so callback side effects can happen twice. Neither exception occurs on the local subprocess transport.

Dependencies

install_dependencies() is supported only by embedded-CPython workers. It installs PEP 508 requirements using uv, within the pool’s request_timeout. Those workers also install PEP 723 inline dependencies before running a feed. A Monty sandbox worker rejects non-empty installation requests with MontyRuntimeError and ignores PEP 723 comments. install_dependencies([]) is a no-op on either worker.

API

AsyncMontyWebsocket

Async context manager owning a pool of remote monty workers reached over a WebSocket. The intended peer is monty-server (the production server: one monty subprocess child per connection, plus capacity/timeout policy and graceful drain), but any server that accepts the connection and bridges to a worker fits — a relay pairing it with a child that dialed in from the other end, or the dev relay scripts/websocket_relay.py.

Like AsyncMonty, but instead of spawning local subprocesses each checkout dials the configured URL. There is no sync counterpart — remote turns are network-bound. checkout() yields the same AsyncMontySession.

A monty-server enforces its own policy on top of the pool’s. On SIGTERM drain it answers the session’s next request with MontyShutdown, whose dump restores the session onto another server; every other server-side drop (idle, session or turn timeout, capacity) closes the connection and raises MontyDisconnectError.

async with AsyncMontyWebsocket('ws://127.0.0.1:8799') as pool:
    async with pool.checkout() as session:
        result = await session.feed_run('1 + 1')

Methods

__new__
def __new__(
    cls,
    url: str,
    *,
    max_processes: int | None = None,
    checkout_timeout: float | None = None,
    request_timeout: float | None = 10.0,
    connect_headers: Callable[[], Mapping[str, str]] | None = None,
    feed_duration_limit_grace: float | None = 1.0,
    turn_duration_limit_grace: float | None = 1.0,
) -> Self

Configure a remote worker pool; connections are made by async with and each checkout (no workers are pre-warmed).

Returns

Self

Parameters

url : str

ws:///wss:// URL to dial — a relay, or any server that bridges to a worker. Dialed verbatim; any session/rendezvous routing the URL needs (e.g. a /<uuid>/parent path for a relay) must already be in it.

max_processes : int | None Default: None

Cap on concurrent connections (defaults to the CPU count); checkouts beyond it wait.

checkout_timeout : float | None Default: None

Seconds checkout() waits for capacity before raising TimeoutError. None waits forever.

request_timeout : float | None Default: 10.0

Hard per-turn deadline in seconds (default 10.0) — a worker that exceeds it has its connection killed and the call raises MontyCrashedError with timed_out=True. This also bounds the wait when a relay accepts the connection but never produces a worker. Pass None to wait indefinitely.

Note that install_dependencies is a turn too, so the default 10.0 is often too low for it — a real uv pip install can exceed it. Raise request_timeout (or pass None) when installing dependencies over the WebSocket transport.

connect_headers : Callable[[], Mapping[str, str]] | None Default: None

Called once per session, as it is entered and before any wait for pool capacity, to produce extra str to str headers for that connection’s WebSocket upgrade request — e.g. a token for infrastructure in front of the worker. It runs synchronously on the checking-out task, so it sees that task’s contextvars and must not block. Monty never interprets the values; duplicate names are last-wins, also over the default user-agent and the traceparent the Logfire integration adds, and a malformed name or value raises RuntimeError as the session is entered.

feed_duration_limit_grace : float | None Default: 1.0

Seconds the parent waits past a feed’s max_feed_duration_secs before killing the worker, giving the sandbox time to raise TimeoutError itself rather than the session dying with its worker. None disables this backstop.

turn_duration_limit_grace : float | None Default: 1.0

The same, for max_turn_duration_secs.

checkout
def checkout(
    *,
    script_name: str = 'main.py',
    limits: ResourceLimits | None = None,
    type_check: bool = False,
    type_check_stubs: str | None = None,
    type_check_format: TypeCheckFormat | None = None,
    type_check_color: bool = False,
    assert_message_annotations: bool | int = ...,
    print_flush_interval: float | None = None,
    os_policy: OSPolicy | None = None,
) -> AsyncMontySession

Prepare a REPL session served by a dedicated remote connection.

Identical to AsyncMonty.checkout; the connection is opened by async with on the returned session.

Returns

AsyncMontySession