> 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.providers.enroot.provider

Enroot provider implementation.

Enroot ([https://github.com/NVIDIA/enroot](https://github.com/NVIDIA/enroot)) is an unprivileged container runtime.
Unlike Apptainer it has no persistent daemon/instance concept, so this provider
emulates a long-lived sandbox by launching a detached `enroot start` running a
sleeping init and then re-entering it with `enroot exec &lt;pid&gt;`. See README.md
for the enroot-specific design notes (non-daemonizing `start`, PID-based
liveness, `#`-registry URIs, pinned `ENROOT_*` paths).

## Module Contents

### Classes

| Name                                                                                                         | Description                                                                  |
| ------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------- |
| [`EnrootCreateConfig`](#nemo_gym-sandbox-providers-enroot-provider-EnrootCreateConfig)                       | Settings for creating an Enroot sandbox container.                           |
| [`EnrootCreateError`](#nemo_gym-sandbox-providers-enroot-provider-EnrootCreateError)                         | Raised when Enroot cannot create a sandbox.                                  |
| [`EnrootCreateVerificationError`](#nemo_gym-sandbox-providers-enroot-provider-EnrootCreateVerificationError) | Raised when a newly-created sandbox cannot execute a probe command.          |
| [`EnrootExecConfig`](#nemo_gym-sandbox-providers-enroot-provider-EnrootExecConfig)                           | Settings for running commands inside an Enroot sandbox.                      |
| [`EnrootProbeConfig`](#nemo_gym-sandbox-providers-enroot-provider-EnrootProbeConfig)                         | Post-create probe settings: a test command confirming the sandbox is usable. |
| [`EnrootProvider`](#nemo_gym-sandbox-providers-enroot-provider-EnrootProvider)                               | Sandbox provider backed by the local Enroot CLI.                             |
| [`_EnrootInstance`](#nemo_gym-sandbox-providers-enroot-provider-_EnrootInstance)                             | Provider-private state stashed on SandboxHandle.raw.                         |

### Functions

| Name                                                                                               | Description                                                                      |
| -------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------- |
| [`_coerce_mounts`](#nemo_gym-sandbox-providers-enroot-provider-_coerce_mounts)                     | Normalize `spec.provider_options['mounts']` into a list of enroot fstab entries. |
| [`_find_container_init_pid`](#nemo_gym-sandbox-providers-enroot-provider-_find_container_init_pid) | Find the container init PID by scanning /proc for its unique cmdline.            |
| [`_is_missing_container`](#nemo_gym-sandbox-providers-enroot-provider-_is_missing_container)       | -                                                                                |
| [`_is_runtime_failure`](#nemo_gym-sandbox-providers-enroot-provider-_is_runtime_failure)           | Best-effort: did enroot itself fail to run the command (vs the command failing)? |
| [`_read_proc_cmdline`](#nemo_gym-sandbox-providers-enroot-provider-_read_proc_cmdline)             | Return /proc/\<pid>/cmdline as a space-joined string, or '' if unreadable.       |
| [`_require_enroot`](#nemo_gym-sandbox-providers-enroot-provider-_require_enroot)                   | Return the enroot binary path or hard-error if it is not installed.              |
| [`_resource_gpu_env`](#nemo_gym-sandbox-providers-enroot-provider-_resource_gpu_env)               | Map a neutral GPU request onto NVIDIA\_VISIBLE\_DEVICES for the enroot hook.     |
| [`_translate_docker_uri`](#nemo_gym-sandbox-providers-enroot-provider-_translate_docker_uri)       | Translate a docker image reference into an enroot import URI.                    |

### Data

[`CONTAINER_NAME_PREFIX`](#nemo_gym-sandbox-providers-enroot-provider-CONTAINER_NAME_PREFIX)

[`DEFAULT_INIT_COMMAND`](#nemo_gym-sandbox-providers-enroot-provider-DEFAULT_INIT_COMMAND)

[`DEFAULT_MOUNT_POINT`](#nemo_gym-sandbox-providers-enroot-provider-DEFAULT_MOUNT_POINT)

[`DOCKER_HUB_HOSTS`](#nemo_gym-sandbox-providers-enroot-provider-DOCKER_HUB_HOSTS)

[`ENROOT_MISSING_CONTAINER_MARKERS`](#nemo_gym-sandbox-providers-enroot-provider-ENROOT_MISSING_CONTAINER_MARKERS)

[`ENROOT_RUNTIME_ERROR_MARKERS`](#nemo_gym-sandbox-providers-enroot-provider-ENROOT_RUNTIME_ERROR_MARKERS)

[`LOGGER`](#nemo_gym-sandbox-providers-enroot-provider-LOGGER)

[`READY_PROBE_COMMAND`](#nemo_gym-sandbox-providers-enroot-provider-READY_PROBE_COMMAND)

[`READY_PROBE_EXPECTED`](#nemo_gym-sandbox-providers-enroot-provider-READY_PROBE_EXPECTED)

[`SANDBOX_RUNTIME_RETURN_CODE`](#nemo_gym-sandbox-providers-enroot-provider-SANDBOX_RUNTIME_RETURN_CODE)

### API

```python
class nemo_gym.sandbox.providers.enroot.provider.EnrootCreateConfig(
    mount_point: str = DEFAULT_MOUNT_POINT,
    base_dir: str | None = None,
    data_path: str | None = None,
    cache_path: str | None = None,
    runtime_path: str | None = None,
    sqsh_cache_dir: str | None = None,
    rw: bool = True,
    remap_root: bool = False,
    bypass_entrypoint: bool = True,
    init_command: str = DEFAULT_INIT_COMMAND,
    import_timeout_s: float | None = 1800,
    create_timeout_s: float | None = 600,
    start_timeout_s: float | None = 600,
    start_poll_s: float = 0.5,
    extra_import_args: list[str] = list(),
    extra_create_args: list[str] = list(),
    extra_start_args: list[str] = list()
)
```

Dataclass

Settings for creating an Enroot sandbox container.

**`base_dir`** `str | None = None`

---

**`bypass_entrypoint`** `bool = True`

---

**`cache_path`** `str | None = None`

---

**`create_timeout_s`** `float | None = 600`

---

**`data_path`** `str | None = None`

---

**`extra_create_args`** `list[str] = field(default_factory=list)`

---

**`extra_import_args`** `list[str] = field(default_factory=list)`

---

**`extra_start_args`** `list[str] = field(default_factory=list)`

---

**`import_timeout_s`** `float | None = 1800`

---

**`init_command`** `str = DEFAULT_INIT_COMMAND`

---

**`mount_point`** `str = DEFAULT_MOUNT_POINT`

---

**`remap_root`** `bool = False`

---

**`runtime_path`** `str | None = None`

---

**`rw`** `bool = True`

---

**`sqsh_cache_dir`** `str | None = None`

---

**`start_poll_s`** `float = 0.5`

---

**`start_timeout_s`** `float | None = 600`

---

```python
nemo_gym.sandbox.providers.enroot.provider.EnrootCreateConfig.__post_init__() -> None
```

```python
class nemo_gym.sandbox.providers.enroot.provider.EnrootCreateError()
```

**Bases:** [SandboxCreateError](/nemo-gym/nemo_gym/sandbox/providers/base#nemo_gym-sandbox-providers-base-SandboxCreateError)

Raised when Enroot cannot create a sandbox.

```python
class nemo_gym.sandbox.providers.enroot.provider.EnrootCreateVerificationError()
```

**Bases:** [SandboxCreateVerificationError](/nemo-gym/nemo_gym/sandbox/providers/base#nemo_gym-sandbox-providers-base-SandboxCreateVerificationError)

Raised when a newly-created sandbox cannot execute a probe command.

```python
class nemo_gym.sandbox.providers.enroot.provider.EnrootExecConfig(
    default_timeout_s: float | None = 180,
    default_mounts: list[str] = list(),
    extra_exec_args: list[str] = list(),
    concurrency: int = 32,
    exec_shell: str = 'sh'
)
```

Dataclass

Settings for running commands inside an Enroot sandbox.

**`concurrency`** `int = 32`

---

**`default_mounts`** `list[str] = field(default_factory=list)`

---

**`default_timeout_s`** `float | None = 180`

---

**`exec_shell`** `str = 'sh'`

---

**`extra_exec_args`** `list[str] = field(default_factory=list)`

---

```python
nemo_gym.sandbox.providers.enroot.provider.EnrootExecConfig.__post_init__() -> None
```

```python
class nemo_gym.sandbox.providers.enroot.provider.EnrootProbeConfig(
    command: str | None = READY_PROBE_COMMAND,
    expected_stdout: str | None = READY_PROBE_EXPECTED,
    timeout_s: int = 30,
    deadline_s: float | None = None,
    stable_count: int = 1,
    stable_delay_s: float = 0.0
)
```

Dataclass

Post-create probe settings: a test command confirming the sandbox is usable.

**`command`** `str | None = READY_PROBE_COMMAND`

---

**`deadline_s`** `float | None = None`

---

**`expected_stdout`** `str | None = READY_PROBE_EXPECTED`

---

**`stable_count`** `int = 1`

---

**`stable_delay_s`** `float = 0.0`

---

**`timeout_s`** `int = 30`

---

```python
nemo_gym.sandbox.providers.enroot.provider.EnrootProbeConfig.__post_init__() -> None
```

```python
class nemo_gym.sandbox.providers.enroot.provider.EnrootProvider(
    exec: nemo_gym.sandbox.providers.enroot.provider.EnrootExecConfig | collections.abc.Mapping[str, typing.Any] | None = None,
    create: nemo_gym.sandbox.providers.enroot.provider.EnrootCreateConfig | collections.abc.Mapping[str, typing.Any] | None = None,
    probe: nemo_gym.sandbox.providers.enroot.provider.EnrootProbeConfig | collections.abc.Mapping[str, typing.Any] | None = None
)
```

Sandbox provider backed by the local Enroot CLI.

**`_binary`** `= _require_enroot()`

---

**`_create_config`** `= _coerce_config(create, EnrootCreateConfig)`

---

**`_enroot_env`**

---

**`_exec_config`** `= _coerce_config(exec, EnrootExecConfig)`

---

**`_import_locks`** `dict[str, Lock] = {}`

---

**`_probe`** `= _coerce_config(probe, EnrootProbeConfig)`

---

**`_semaphore`** `= asyncio.Semaphore(self._exec_config.concurrency)`

---

**`_sqsh_cache_dir`** `= Path(cfg.sqsh_cache_dir or base / 'sqsh')`

---

**`name`** `= 'enroot'`

---

```python
nemo_gym.sandbox.providers.enroot.provider.EnrootProvider._await_container_pid(
    instance: nemo_gym.sandbox.providers.enroot.provider._EnrootInstance,
    err_f: typing.IO[bytes]
) -> int
```

async

Poll `enroot list` until the container's init PID appears, or raise.

```python
nemo_gym.sandbox.providers.enroot.provider.EnrootProvider._cleanup_failed_create_handle(
    handle: nemo_gym.sandbox.providers.base.SandboxHandle
) -> None
```

async

Best-effort teardown of a sandbox that failed to start or verify.

```python
nemo_gym.sandbox.providers.enroot.provider.EnrootProvider._ensure_image(
    image: str
) -> pathlib.Path
```

async

Return a local squashfs path for `image`, importing (and caching) if needed.

```python
nemo_gym.sandbox.providers.enroot.provider.EnrootProvider._kill_start_group(
    instance: nemo_gym.sandbox.providers.enroot.provider._EnrootInstance
) -> None
```

Best-effort SIGTERM then SIGKILL of the detached start's process group.

```python
nemo_gym.sandbox.providers.enroot.provider.EnrootProvider._lookup_container(
    name: str
) -> tuple[int | None, bool]
```

async

Return (pid, present). pid is the running init PID or None; present is
whether the rootfs exists at all (running or not).

```python
nemo_gym.sandbox.providers.enroot.provider.EnrootProvider._pid_has_container_marker(
    pid: int,
    name: str
) -> bool
```

staticmethod

Return True if /proc/\<pid>/cmdline still carries the container's unique marker.

This guards against PID reuse: if the init exits and Linux reuses the PID
for an unrelated process, the container marker won't appear in the new
process's cmdline, so exec() returns a sandbox error instead of joining
the wrong namespace.

```python
nemo_gym.sandbox.providers.enroot.provider.EnrootProvider._read_temp(
    handle: typing.IO[bytes]
) -> str
```

staticmethod

```python
nemo_gym.sandbox.providers.enroot.provider.EnrootProvider._resolve_image(
    image: str
) -> tuple[str | None, pathlib.Path | None]
```

Return (import\_uri, sqsh\_path). Exactly one is non-None.

A local `.sqsh` file is used directly (no import). Anything else is
translated to an enroot import URI.

```python
nemo_gym.sandbox.providers.enroot.provider.EnrootProvider._run(
    argv: list[str],
    timeout_s: float | None,
    stdin: bytes | None = None
) -> tuple[int, str, str]
```

async

Run an enroot CLI command. Returns (return\_code, stdout, stderr).

Enforces timeout via asyncio.wait\_for and kills the whole process group
on timeout so child processes do not linger. Bounds concurrency with a
shared semaphore. Decodes output with errors="replace". Every call runs
with the pinned ENROOT\_\* environment.

```python
nemo_gym.sandbox.providers.enroot.provider.EnrootProvider._start_detached(
    argv: list[str]
) -> tuple[typing.Any, typing.IO[bytes], typing.IO[bytes]]
```

async

Launch the long-lived `enroot start` init without awaiting its exit.

`enroot start` does not daemonize — it stays in the foreground for the
whole container lifetime. We launch it detached in its own session
(start\_new\_session=True), capture output to temp files (so an early exit
leaves diagnosable stderr), and return the process handle plus the temp
files. The caller confirms readiness by polling `enroot list`.

```python
nemo_gym.sandbox.providers.enroot.provider.EnrootProvider._verify_created_handle(
    handle: nemo_gym.sandbox.providers.base.SandboxHandle
) -> None
```

async

Run the readiness probe until the sandbox responds, or raise.

* probe.command is None      -> skip (no verification).
* probe.deadline\_s is None   -> single attempt; a failure raises immediately.
* probe.deadline\_s is set    -> poll until the sandbox passes the probe
  `stable_count` consecutive times, or the deadline elapses.

```python
nemo_gym.sandbox.providers.enroot.provider.EnrootProvider.aclose() -> None
```

async

No provider-wide resources to close.

```python
nemo_gym.sandbox.providers.enroot.provider.EnrootProvider.close(
    handle: nemo_gym.sandbox.providers.base.SandboxHandle
) -> None
```

async

Kill the container init, remove the rootfs, and clean up the staging dir.

```python
nemo_gym.sandbox.providers.enroot.provider.EnrootProvider.create(
    spec: nemo_gym.sandbox.providers.base.SandboxSpec
) -> nemo_gym.sandbox.providers.base.SandboxHandle
```

async

Import/create the rootfs, launch a detached init, and return a ready handle.

```python
nemo_gym.sandbox.providers.enroot.provider.EnrootProvider.download_file(
    handle: nemo_gym.sandbox.providers.base.SandboxHandle,
    source_path: str,
    target_path: pathlib.Path
) -> None
```

async

Download one sandbox file to the host.

Fast path (source under the bind mount): read directly from the host side
of the shared folder. Fallback (arbitrary path): cp inside the container
into the shared folder, then read the host side.

```python
nemo_gym.sandbox.providers.enroot.provider.EnrootProvider.exec(
    handle: nemo_gym.sandbox.providers.base.SandboxHandle,
    command: str,
    cwd: str | None = None,
    env: dict[str, str] | None = None,
    timeout_s: int | float | None = None,
    user: str | int | None = None,
    stdin: bytes | None = None
) -> nemo_gym.sandbox.providers.base.SandboxExecResult
```

async

Run a command inside the container via `enroot exec &lt;pid&gt;`.

Maps the neutral `user` parameter onto enroot:

* None            -> run as the default (launching) user, or root if the
  container was started with remap\_root.
* "root" / 0      -> run directly (requires create.remap\_root=true to be
  root inside the container).
* other user/uid  -> wrap in `su` to switch to that user (requires root
  inside the container).

`stdin`, when given, is piped to the command's standard input.

```python
nemo_gym.sandbox.providers.enroot.provider.EnrootProvider.status(
    handle: nemo_gym.sandbox.providers.base.SandboxHandle
) -> nemo_gym.sandbox.providers.base.SandboxStatus
```

async

Return the container's lifecycle status via `enroot list -f`.

Liveness is keyed off the PID column, not name presence: enroot lists
created-but-not-running rootfs with an empty PID. Running -> RUNNING;
present without a PID, or absent -> STOPPED; timeout/parse error -> UNKNOWN.

```python
nemo_gym.sandbox.providers.enroot.provider.EnrootProvider.upload_file(
    handle: nemo_gym.sandbox.providers.base.SandboxHandle,
    source_path: pathlib.Path,
    target_path: str
) -> None
```

async

Upload one host file into the sandbox.

Fast path (target under the bind mount): write directly to the host side
of the shared folder. Fallback (arbitrary path): stage into the shared
folder, then cp inside the container.

```python
class nemo_gym.sandbox.providers.enroot.provider._EnrootInstance(
    name: str,
    sqsh_path: pathlib.Path,
    staging_dir: pathlib.Path,
    mount_point: str,
    image: str,
    env: dict[str, str] = dict(),
    container_pid: int | None = None,
    start_pgid: int | None = None,
    proc: typing.Any = None
)
```

Dataclass

Provider-private state stashed on SandboxHandle.raw.

**`container_pid`** `int | None = None`

---

**`env`** `dict[str, str] = field(default_factory=dict)`

---

**`image`** `str`

---

**`mount_point`** `str`

---

**`name`** `str`

---

**`sqsh_path`** `Path`

---

**`staging_dir`** `Path`

---

**`start_pgid`** `int | None = None`

---

```python
nemo_gym.sandbox.providers.enroot.provider._coerce_mounts(
    value: typing.Any
) -> list[str]
```

Normalize `spec.provider_options['mounts']` into a list of enroot fstab entries.

Accepts a single `"src:dst[:type:opts]"` string or a list of them. These are
extra per-sandbox mounts, added on top of the staging mount and the
provider-level `exec.default_mounts`.

```python
nemo_gym.sandbox.providers.enroot.provider._find_container_init_pid(
    base_init: str,
    marker: str
) -> int | None
```

Find the container init PID by scanning /proc for its unique cmdline.

Nested inside pyxis, enroot runs as real root without a per-container user
namespace, so `enroot list` cannot map a PID to the container name. The init is
also reparented out of the `enroot start` process tree once the container's PID
namespace is set up, so a tree walk misses it. Instead we tag each container's init
with its unique name (`... # &lt;name&gt;`) and find the process whose cmdline is the
init: it starts with `sh -c ` (the provider passes argv0="sh", so this holds even
where /bin/sh is dash) and carries both the base init loop and the unique marker.
Scanning all of /proc is reparent-proof; the marker keeps it unambiguous under
concurrency. The `enroot start` wrapper also carries the marker but its cmdline
starts with the enroot binary, not `sh -c `, so it is excluded.

```python
nemo_gym.sandbox.providers.enroot.provider._is_missing_container(
    stderr: str
) -> bool
```

```python
nemo_gym.sandbox.providers.enroot.provider._is_runtime_failure(
    stderr: str
) -> bool
```

Best-effort: did enroot itself fail to run the command (vs the command failing)?

```python
nemo_gym.sandbox.providers.enroot.provider._read_proc_cmdline(
    pid: int
) -> str
```

Return /proc/\<pid>/cmdline as a space-joined string, or '' if unreadable.

```python
nemo_gym.sandbox.providers.enroot.provider._require_enroot() -> str
```

Return the enroot binary path or hard-error if it is not installed.

```python
nemo_gym.sandbox.providers.enroot.provider._resource_gpu_env(
    resources: nemo_gym.sandbox.providers.base.SandboxResources
) -> dict[str, str]
```

Map a neutral GPU request onto NVIDIA\_VISIBLE\_DEVICES for the enroot hook.

```python
nemo_gym.sandbox.providers.enroot.provider._translate_docker_uri(
    image: str
) -> str
```

Translate a docker image reference into an enroot import URI.

Enroot's docker scheme separates the registry host with `#` rather than
`/` (`docker://[USER@][REGISTRY#]IMAGE[:TAG]`). A leading component that
looks like a registry host (contains a `.` or `:`, or is `localhost`)
is treated as the registry; otherwise the reference is a Docker Hub name.

The Docker Hub canonical hostnames are dropped rather than passed as the
`REGISTRY` fragment: the public registry API is served from
`registry-1.docker.io`, not the literal `docker.io` alias, so forwarding
`docker://docker.io#repo` points enroot at an endpoint that returns
non-JSON and the import fails. Dropping them lets enroot use its configured
Hub default (e.g. `docker://swebench/foo:tag`).

```python
nemo_gym.sandbox.providers.enroot.provider.CONTAINER_NAME_PREFIX = 'nemo-gym-'
```

```python
nemo_gym.sandbox.providers.enroot.provider.DEFAULT_INIT_COMMAND = 'while true; do sleep 86400; done'
```

```python
nemo_gym.sandbox.providers.enroot.provider.DEFAULT_MOUNT_POINT = '/sandbox'
```

```python
nemo_gym.sandbox.providers.enroot.provider.DOCKER_HUB_HOSTS = frozenset({'docker.io', 'index.docker.io', 'registry-1.docker.io'})
```

```python
nemo_gym.sandbox.providers.enroot.provider.ENROOT_MISSING_CONTAINER_MARKERS = ('does not exist', 'no such', 'not found')
```

```python
nemo_gym.sandbox.providers.enroot.provider.ENROOT_RUNTIME_ERROR_MARKERS = ('no such process', 'no such file or directory: /proc', 'nsenter')
```

```python
nemo_gym.sandbox.providers.enroot.provider.LOGGER = logging.getLogger(__name__)
```

```python
nemo_gym.sandbox.providers.enroot.provider.READY_PROBE_COMMAND = f'printf enroot-sandbox-ready > {DEFAULT_MOUNT_POINT}/.nemo-gym-ready && printf ...
```

```python
nemo_gym.sandbox.providers.enroot.provider.READY_PROBE_EXPECTED = 'enroot-sandbox-ready'
```

```python
nemo_gym.sandbox.providers.enroot.provider.SANDBOX_RUNTIME_RETURN_CODE = 125
```