nemo_gym.sandbox.providers.opensandbox.pty

View as Markdown

execd PTY sessions over the OpenSandbox server proxy.

Speaks execd’s documented PTY wire protocol directly: the released OpenSandbox SDKs expose no PTY API. Sessions live at {base}/pty[/{session_id}[/ws]] where base is the sandbox’s execd endpoint as resolved by the SDK (through the server proxy when use_server_proxy is set).

execd sends the exit frame and closes the socket concurrently with its output pumps, so a clean EOF means the socket drained, not that every byte the process wrote was delivered. Callers that need the tail should have the command emit a sentinel and read until it appears.

Module Contents

Classes

NameDescription
OpenSandboxPtySessionOne live PTY WebSocket. Created via :func:open_pty_session.

Functions

NameDescription
_connect_ws-
_effective_commandRewrite env/user into the command: execd’s PTY create accepts only
_start_session-
attach_pty_sessionAttach to an existing execd PTY session. Owns client as above.
open_pty_sessionCreate an execd PTY session and attach its WebSocket.

Data

CHAN_REPLAY

CHAN_STDERR

CHAN_STDIN

CHAN_STDOUT

LOGGER

REPLAY_HEADER_BYTES

SUPPORTED_SIGNALS

WS_CLOSE_POLICY_VIOLATION

WS_CLOSE_TAKEN_OVER

_DEFAULT_SHELL_SNIPPET

_PTY_RETRY_DELAYS

API

class nemo_gym.sandbox.providers.opensandbox.pty.OpenSandboxPtySession(
client: aiohttp.ClientSession,
ws: aiohttp.ClientWebSocketResponse,
session_id: str,
session_url: str,
headers: dict[str, str],
request_timeout_s: float | None,
owned: bool = True
)

One live PTY WebSocket. Created via :func:open_pty_session.

_connected
Future[None] = asyncio.get_running_loop().create_future()
_error
SandboxPtyError | None = None
_exit
Future[int] = asyncio.get_running_loop().create_future()
_output
Queue[bytes | None] = asyncio.Queue()
_pump_task
= asyncio.create_task(self._pump())
_received
= 0
_stderr
Queue[bytes | None] = asyncio.Queue()
closed
bool

True once the session can no longer run commands: after close(), or once the connection pump has ended (process exit, takeover eviction, or connection loss). Resources are released by close().

mode
str | None = None
replay_offset
int | None = None
async
nemo_gym.sandbox.providers.opensandbox.pty.OpenSandboxPtySession.__aexit__(
exc_type: typing.Any,
exc_val: typing.Any,
exc_tb: typing.Any
) -> None
async
nemo_gym.sandbox.providers.opensandbox.pty.OpenSandboxPtySession.__aiter__() -> collections.abc.AsyncIterator[bytes]
nemo_gym.sandbox.providers.opensandbox.pty.OpenSandboxPtySession._close_error() -> nemo_gym.sandbox.providers.base.SandboxPtyError
nemo_gym.sandbox.providers.opensandbox.pty.OpenSandboxPtySession._pump() -> None
async

Sole reader of the session’s sockets; fans frames out to queue/future.

A socket that dies while the process still runs (proxy shed, connection loss) is re-dialed with since=<bytes received>, so pending reads and execs continue across the gap. A deliberate takeover, a protocol error, an exit, or close() ends the session instead.

nemo_gym.sandbox.providers.opensandbox.pty.OpenSandboxPtySession._pump_socket() -> None
async

Drain one WebSocket until it closes, fanning frames out.

nemo_gym.sandbox.providers.opensandbox.pty.OpenSandboxPtySession._read_stream(
queue: asyncio.Queue[bytes | None],
timeout_s: float | None
) -> bytes
async
nemo_gym.sandbox.providers.opensandbox.pty.OpenSandboxPtySession._reattach_socket() -> bool
async

Re-dial the session’s WebSocket, resuming from the last received byte.

nemo_gym.sandbox.providers.opensandbox.pty.OpenSandboxPtySession._send(
frame: bytes | str
) -> None
async
nemo_gym.sandbox.providers.opensandbox.pty.OpenSandboxPtySession._wait_connected(
timeout_s: float | None
) -> None
async
nemo_gym.sandbox.providers.opensandbox.pty.OpenSandboxPtySession.close() -> None
async
nemo_gym.sandbox.providers.opensandbox.pty.OpenSandboxPtySession.read(
timeout_s: float | None = None
) -> bytes
async
nemo_gym.sandbox.providers.opensandbox.pty.OpenSandboxPtySession.read_stderr(
timeout_s: float | None = None
) -> bytes
async
nemo_gym.sandbox.providers.opensandbox.pty.OpenSandboxPtySession.resize(
rows: int,
cols: int
) -> None
async
nemo_gym.sandbox.providers.opensandbox.pty.OpenSandboxPtySession.send_signal(
signal: str
) -> None
async
nemo_gym.sandbox.providers.opensandbox.pty.OpenSandboxPtySession.wait_exit(
timeout_s: float | None = None
) -> int
async
nemo_gym.sandbox.providers.opensandbox.pty.OpenSandboxPtySession.write(
data: bytes
) -> None
async
nemo_gym.sandbox.providers.opensandbox.pty._connect_ws(
client: aiohttp.ClientSession,
base_url: str,
headers: dict[str, str],
session_id: str,
query: dict[str, str],
request_timeout_s: float | None
) -> aiohttp.ClientWebSocketResponse
async
nemo_gym.sandbox.providers.opensandbox.pty._effective_command(
spec: nemo_gym.sandbox.providers.base.SandboxPtySpec
) -> str | None

Rewrite env/user into the command: execd’s PTY create accepts only cwd and command.

nemo_gym.sandbox.providers.opensandbox.pty._start_session(
client: aiohttp.ClientSession,
ws: aiohttp.ClientWebSocketResponse,
base_url: str,
session_id: str,
headers: dict[str, str],
request_timeout_s: float | None,
owned: bool = True
) -> nemo_gym.sandbox.providers.opensandbox.pty.OpenSandboxPtySession
async
nemo_gym.sandbox.providers.opensandbox.pty.attach_pty_session(
client: aiohttp.ClientSession,
base_url: str,
headers: dict[str, str],
session_id: str,
takeover: bool = True,
since: int | None = None,
request_timeout_s: float | None
) -> nemo_gym.sandbox.providers.opensandbox.pty.OpenSandboxPtySession
async

Attach to an existing execd PTY session. Owns client as above.

nemo_gym.sandbox.providers.opensandbox.pty.open_pty_session(
client: aiohttp.ClientSession,
base_url: str,
headers: dict[str, str],
spec: nemo_gym.sandbox.providers.base.SandboxPtySpec,
request_timeout_s: float | None
) -> nemo_gym.sandbox.providers.opensandbox.pty.OpenSandboxPtySession
async

Create an execd PTY session and attach its WebSocket.

Owns client: it is closed on failure and by session.close().

nemo_gym.sandbox.providers.opensandbox.pty.CHAN_REPLAY = 3
nemo_gym.sandbox.providers.opensandbox.pty.CHAN_STDERR = 2
nemo_gym.sandbox.providers.opensandbox.pty.CHAN_STDIN = 0
nemo_gym.sandbox.providers.opensandbox.pty.CHAN_STDOUT = 1
nemo_gym.sandbox.providers.opensandbox.pty.LOGGER = logging.getLogger(__name__)
nemo_gym.sandbox.providers.opensandbox.pty.REPLAY_HEADER_BYTES = 9
nemo_gym.sandbox.providers.opensandbox.pty.SUPPORTED_SIGNALS = frozenset({'SIGINT', 'SIGTERM', 'SIGKILL', 'SIGQUIT', 'SIGHUP'})
nemo_gym.sandbox.providers.opensandbox.pty.WS_CLOSE_POLICY_VIOLATION = 1008
nemo_gym.sandbox.providers.opensandbox.pty.WS_CLOSE_TAKEN_OVER = 4001
nemo_gym.sandbox.providers.opensandbox.pty._DEFAULT_SHELL_SNIPPET = 'exec "$(command -v bash || echo sh)"'
nemo_gym.sandbox.providers.opensandbox.pty._PTY_RETRY_DELAYS = (0.25, 0.5, 1.0, 2.0, 4.0, 8.0)