> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://docs.nvidia.com/nemo/gym/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://docs.nvidia.com/nemo/gym/_mcp/server.

# nemo_gym.sandbox.api

Provider-neutral public sandbox API.

## Module Contents

### Classes

| Name                                                         | Description                                                            |
| ------------------------------------------------------------ | ---------------------------------------------------------------------- |
| [`AsyncSandbox`](#nemo_gym-sandbox-api-AsyncSandbox)         | Async sandbox object backed by a runtime provider.                     |
| [`Sandbox`](#nemo_gym-sandbox-api-Sandbox)                   | Synchronous wrapper around `AsyncSandbox`.                             |
| [`SandboxPty`](#nemo_gym-sandbox-api-SandboxPty)             | PTY namespace of a sandbox: `await sandbox.pty.create(...)` for a live |
| [`_AsyncLoopRunner`](#nemo_gym-sandbox-api-_AsyncLoopRunner) | Run async sandbox operations for sync callers.                         |

### Functions

| Name                                                               | Description                                                                 |
| ------------------------------------------------------------------ | --------------------------------------------------------------------------- |
| [`_pty_timeout_result`](#nemo_gym-sandbox-api-_pty_timeout_result) | -                                                                           |
| [`_run_in_pty_session`](#nemo_gym-sandbox-api-_run_in_pty_session) | Run `command` in a live session, delimited by unique markers.               |
| [`_sandbox_id`](#nemo_gym-sandbox-api-_sandbox_id)                 | The provider-neutral id of a handle, or None for a handle-less test double. |

### Data

[`SANDBOX_PTY_RUNTIME_RETURN_CODE`](#nemo_gym-sandbox-api-SANDBOX_PTY_RUNTIME_RETURN_CODE)

[`SYNC_LOOP_CLOSE_TIMEOUT_S`](#nemo_gym-sandbox-api-SYNC_LOOP_CLOSE_TIMEOUT_S)

[`SYNC_OPERATION_TIMEOUT_S`](#nemo_gym-sandbox-api-SYNC_OPERATION_TIMEOUT_S)

[`T`](#nemo_gym-sandbox-api-T)

### API

```python
class nemo_gym.sandbox.AsyncSandbox(
    provider: collections.abc.Mapping[str, typing.Any] | nemo_gym.sandbox.providers.SandboxProvider,
    spec: nemo_gym.sandbox.providers.SandboxSpec | None = None,
    owns_provider: bool = True
)
```

Async sandbox object backed by a runtime provider.

With `owns_provider=False`, the caller closes the shared provider after
all of its sandboxes have stopped.

**`_handle`** `SandboxHandle | None = None`

---

**`_provider`**

---

**`pty`** `= SandboxPty(self)`

---

```python
nemo_gym.sandbox.AsyncSandbox.__aenter__() -> nemo_gym.sandbox.api.AsyncSandbox
```

async

```python
nemo_gym.sandbox.AsyncSandbox.__aexit__(
    exc_type: typing.Any,
    exc_val: typing.Any,
    exc_tb: typing.Any
) -> None
```

async

```python
nemo_gym.sandbox.AsyncSandbox._exec_uninstrumented(
    command: str,
    cwd: str | None = None,
    env: dict[str, str] | None = None,
    timeout_s: int | float | None = 180,
    user: str | int | None = None,
    preserve_background_services: bool = False
) -> nemo_gym.sandbox.providers.SandboxExecResult
```

async

```python
nemo_gym.sandbox.AsyncSandbox._require_handle() -> nemo_gym.sandbox.providers.SandboxHandle
```

```python
nemo_gym.sandbox.AsyncSandbox._telemetry_provider_name() -> str
```

Provider name for span attributes (`docker`, `daytona`, `opensandbox`, ...).

```python
nemo_gym.sandbox.AsyncSandbox.connect(
    descriptor: collections.abc.Mapping[str, typing.Any] | typing.Any,
    provider: nemo_gym.sandbox.providers.SandboxProvider,
    owns_provider: bool = True
) -> nemo_gym.sandbox.api.AsyncSandbox
```

async

classmethod

Rebuild a sandbox in this process from a descriptor produced by
`serialize`, using `provider` (which must support connect).

```python
nemo_gym.sandbox.AsyncSandbox.disconnect() -> None
```

async

Release this client without stopping a borrowed sandbox.

Use this only for a sandbox rebuilt with `connect`.
The component that created the sandbox remains responsible for stopping it.

```python
nemo_gym.sandbox.AsyncSandbox.download(
    remote_path: str,
    local_path: pathlib.Path | str
) -> None
```

async

```python
nemo_gym.sandbox.AsyncSandbox.endpoint(
    port: int
) -> nemo_gym.sandbox.providers.SandboxEndpoint
```

async

Resolve a declared sandbox service port without exposing provider state.

```python
nemo_gym.sandbox.AsyncSandbox.exec(
    command: str,
    cwd: str | None = None,
    env: dict[str, str] | None = None,
    timeout_s: int | float | None = 180,
    user: str | int | None = None,
    preserve_background_services: bool = False
) -> nemo_gym.sandbox.providers.SandboxExecResult
```

async

Run a command, optionally preserving services needed by later commands.

`preserve_background_services` selects a provider's service-preserving
execution when available; other providers use ordinary exec. Services
must redirect stdout and stderr. This mode does not accept per-command
`env` or `user` overrides on providers with a service-preserving path.

```python
nemo_gym.sandbox.AsyncSandbox.pause() -> None
```

async

Pause this sandbox while preserving its state.

Open PTY sessions are detached; whether processes survive and sessions
can be re-attached after `resume()` depends on the provider backend.

```python
nemo_gym.sandbox.AsyncSandbox.resume() -> None
```

async

Resume this sandbox and wait until it is ready.

On timeout the server-side state is unknown: reconnect and check
`status()` before retrying.

```python
nemo_gym.sandbox.AsyncSandbox.serialize(
    scope: str | None = None
) -> dict[str, typing.Any]
```

async

Return a JSON descriptor another process can rebuild this box from.

Requires a provider that supports the connect capability (the remote
provider, or an external-control-plane provider such as OpenSandbox). For
the remote provider, `scope` mints a co-lease (`scope="operate"`).

```python
nemo_gym.sandbox.AsyncSandbox.start(
    spec: nemo_gym.sandbox.providers.SandboxSpec | None = None
) -> nemo_gym.sandbox.api.AsyncSandbox
```

async

```python
nemo_gym.sandbox.AsyncSandbox.start_with_setup(
    spec: nemo_gym.sandbox.providers.SandboxSpec | None,
    setup: collections.abc.Callable[[AsyncSandbox], collections.abc.Awaitable[None]]
) -> nemo_gym.sandbox.api.AsyncSandbox
```

async

Start the sandbox, then run `setup` against it.

If `setup` raises, the sandbox is stopped before the exception
propagates.

```python
nemo_gym.sandbox.AsyncSandbox.status() -> nemo_gym.sandbox.providers.SandboxStatus
```

async

```python
nemo_gym.sandbox.AsyncSandbox.stop() -> None
```

async

```python
nemo_gym.sandbox.AsyncSandbox.upload(
    local_path: pathlib.Path | str,
    remote_path: str
) -> None
```

async

```python
class nemo_gym.sandbox.Sandbox(
    provider: collections.abc.Mapping[str, typing.Any] | nemo_gym.sandbox.providers.SandboxProvider,
    spec: nemo_gym.sandbox.providers.SandboxSpec | None = None
)
```

Synchronous wrapper around `AsyncSandbox`.

`pty`, `serialize` and `connect` are async-only; use `AsyncSandbox`
for those.

**`_async_sandbox`**

---

**`_runner`** `= _AsyncLoopRunner()`

---

```python
nemo_gym.sandbox.Sandbox.__del__() -> None
```

```python
nemo_gym.sandbox.Sandbox.__enter__() -> nemo_gym.sandbox.api.Sandbox
```

```python
nemo_gym.sandbox.Sandbox.__exit__(
    exc_type: typing.Any,
    exc_val: typing.Any,
    exc_tb: typing.Any
) -> None
```

```python
nemo_gym.sandbox.Sandbox.download(
    remote_path: str,
    local_path: pathlib.Path | str
) -> None
```

```python
nemo_gym.sandbox.Sandbox.endpoint(
    port: int
) -> nemo_gym.sandbox.providers.SandboxEndpoint
```

```python
nemo_gym.sandbox.Sandbox.exec(
    command: str,
    cwd: str | None = None,
    env: dict[str, str] | None = None,
    timeout_s: int | float | None = 180,
    user: str | int | None = None,
    preserve_background_services: bool = False
) -> nemo_gym.sandbox.providers.SandboxExecResult
```

```python
nemo_gym.sandbox.Sandbox.pause() -> None
```

```python
nemo_gym.sandbox.Sandbox.resume() -> None
```

```python
nemo_gym.sandbox.Sandbox.start(
    spec: nemo_gym.sandbox.providers.SandboxSpec | None = None
) -> nemo_gym.sandbox.api.Sandbox
```

```python
nemo_gym.sandbox.Sandbox.status() -> nemo_gym.sandbox.providers.SandboxStatus
```

```python
nemo_gym.sandbox.Sandbox.stop() -> None
```

```python
nemo_gym.sandbox.Sandbox.upload(
    local_path: pathlib.Path | str,
    remote_path: str
) -> None
```

```python
class nemo_gym.sandbox.SandboxPty(
    sandbox: nemo_gym.sandbox.api.AsyncSandbox
)
```

PTY namespace of a sandbox: `await sandbox.pty.create(...)` for a live
session, `await sandbox.pty.exec(...)` for one-shot run-and-collect.

**`_default_session`** `SandboxPtySession | None = None`

---

**`_session_exec_lock`** `= asyncio.Lock()`

---

```python
nemo_gym.sandbox.SandboxPty._exec_detached(
    command: str,
    session: nemo_gym.sandbox.providers.SandboxPtySession | None,
    cwd: str | None,
    env: dict[str, str] | None,
    user: str | int | None,
    rows: int,
    cols: int,
    pty: bool,
    timeout_s: int | float | None,
    poll_interval_s: float
) -> nemo_gym.sandbox.providers.SandboxExecResult
```

async

`exec(detach=True)`: hand the command to the session's detached
runner, which holds the socket only for brief completion polls.

```python
nemo_gym.sandbox.SandboxPty.attach(
    session_id: str,
    takeover: bool = True,
    since: int | None = None
) -> nemo_gym.sandbox.providers.SandboxPtySession
```

async

Re-attach to a session opened earlier, here or in another process.

Sessions outlive the client that opened them, so `session.session_id`
is all another process needs. `takeover` evicts the current holder,
whose session then fails with `SandboxPtyError`; without it,
attaching to a held session fails. `since` replays retained output
from that byte offset first (`0` replays all of it).

```python
nemo_gym.sandbox.SandboxPty.create(
    command: str | None = None,
    cwd: str | None = None,
    env: dict[str, str] | None = None,
    rows: int = 24,
    cols: int = 80,
    user: str | int | None = None,
    pty: bool = True
) -> nemo_gym.sandbox.providers.SandboxPtySession
```

async

Open an interactive terminal; the returned session carries
`read`/`read_stderr`/`write`/`resize`/`send_signal`/
`wait_exit`/`close` and is an async context manager.

`pty=False` selects pipe mode: no TTY, stdout/stderr split across
`read()`/`read_stderr()`. `rows`/`cols` are applied right after
the terminal connects, because the backend has no spawn-time size, so a
`command` that reads the size in its first moments can still see the
80x24 default; programs that honor SIGWINCH pick up the real size.
Close the session before the sandbox stops: a session that outlives its
sandbox fails subsequent reads with `SandboxPtyError`. Async-only;
the sync `Sandbox` facade does not mirror it.

```python
nemo_gym.sandbox.SandboxPty.exec(
    command: str,
    session: nemo_gym.sandbox.providers.SandboxPtySession | None = None,
    cwd: str | None = None,
    env: dict[str, str] | None = None,
    timeout_s: int | float | None = 180,
    user: str | int | None = None,
    rows: int = 24,
    cols: int = 80,
    pty: bool = True,
    detach: bool = False,
    poll_interval_s: float = 15.0
) -> nemo_gym.sandbox.providers.SandboxExecResult
```

async

Run one command in a terminal session and collect its output.

Without `session`, the sandbox's default-shell session — the oldest
live one opened by `create()` with no `command` — is reused,
provided the call sets none of the session-shaping arguments
(`cwd`/`env`/`user`, non-default `rows`/`cols`, or
`pty=False`), since those are fixed at `create()`. Custom-command
and attached sessions run arbitrary programs, so they are only used
when passed explicitly. When no default-shell session exists (or
shaping arguments are given) a private session is opened for the
command, drained and closed. Session-mode execs are serialized per
sandbox: concurrent calls into one shared stream would corrupt it.
With `session` the command runs in that live session, which stays open
and keeps its shell state. In a live session the output also contains
the shell's echo of the command, `stderr` is best-effort (pipe mode
only), and a command that ends the shell (`exit`) raises
`SandboxPtyError`.

With `detach=True` the command runs without holding a connection
while it works: it starts in a session, the socket is dropped, and the
session is briefly re-attached every `poll_interval_s` to drain
output and check for completion, so a long command occupies a
connection for milliseconds per poll instead of its whole runtime
(completion latency is bounded by `poll_interval_s`). Nothing is
written to the sandbox filesystem; output rides the server's retained
window (\~1 MiB) between polls, comes back as one merged stream
(`stderr` is `None`), and exceeding the window raises rather than
returning truncated output — run bulk-output commands attached or via
the exec API instead. A detached exec never reuses the default-shell
session implicitly: without `session` it opens a private one. With
`session`, the session is detached while the command works, must
not be used concurrently, and is attached and reusable again when
this returns.

PTY mode returns all output on `stdout` and `None` stderr; pipe mode
splits the two. A command that outlives `timeout_s` returns
`error_type="timeout"` like `sandbox.exec()` rather than raising;
in an explicitly passed session that command keeps running and leaves
unread output behind, so discard the session rather than reusing it (an
implicitly reused session is retired automatically).

```python
class nemo_gym.sandbox.api._AsyncLoopRunner(
    wait_timeout_s: float = SYNC_OPERATION_TIMEOUT_S,
    close_timeout_s: float = SYNC_LOOP_CLOSE_TIMEOUT_S
)
```

Run async sandbox operations for sync callers.

**`_loop`** `= asyncio.new_event_loop()`

---

**`_ready`** `= threading.Event()`

---

**`_thread`**

---

```python
nemo_gym.sandbox.api._AsyncLoopRunner._ensure_can_block(
    operation: str
) -> None
```

```python
nemo_gym.sandbox.api._AsyncLoopRunner._run_loop() -> None
```

```python
nemo_gym.sandbox.api._AsyncLoopRunner._wait_for_result(
    operation: str,
    future: concurrent.futures.Future[nemo_gym.sandbox.api.T]
) -> nemo_gym.sandbox.api.T
```

```python
nemo_gym.sandbox.api._AsyncLoopRunner.call(
    operation: str,
    func: collections.abc.Callable[[], nemo_gym.sandbox.api.T]
) -> nemo_gym.sandbox.api.T
```

```python
nemo_gym.sandbox.api._AsyncLoopRunner.close() -> None
```

```python
nemo_gym.sandbox.api._AsyncLoopRunner.run(
    operation: str,
    awaitable_factory: collections.abc.Callable[[], collections.abc.Awaitable[nemo_gym.sandbox.api.T]]
) -> nemo_gym.sandbox.api.T
```

```python
nemo_gym.sandbox.api._pty_timeout_result(
    command: str,
    timeout_s: float | int | None,
    reusable: bool
) -> nemo_gym.sandbox.providers.SandboxExecResult
```

```python
nemo_gym.sandbox.api._run_in_pty_session(
    session: nemo_gym.sandbox.providers.SandboxPtySession,
    command: str
) -> nemo_gym.sandbox.providers.SandboxExecResult
```

async

Run `command` in a live session, delimited by unique markers.

```python
nemo_gym.sandbox.api._sandbox_id(
    handle: typing.Any
) -> str | None
```

The provider-neutral id of a handle, or None for a handle-less test double.

```python
nemo_gym.sandbox.api.SANDBOX_PTY_RUNTIME_RETURN_CODE = 125
```

```python
nemo_gym.sandbox.api.SYNC_LOOP_CLOSE_TIMEOUT_S = 5.0
```

```python
nemo_gym.sandbox.api.SYNC_OPERATION_TIMEOUT_S = 3600.0
```

```python
nemo_gym.sandbox.api.T = TypeVar('T')
```