Skip to content

build(sandboxed-gym): register the package and remove the NeMo-Gym dependency - #1400

Draft
SandyChapman wants to merge 3 commits into
mainfrom
sandboxed-gym-integration/schapman
Draft

build(sandboxed-gym): register the package and remove the NeMo-Gym dependency#1400
SandyChapman wants to merge 3 commits into
mainfrom
sandboxed-gym-integration/schapman

Conversation

@SandyChapman

Copy link
Copy Markdown
Contributor

Summary

Makes the extracted sandboxed_gym package from #1154 a workspace member, so it is installed, linted, type-checked and tested by CI. Doing that required removing its hard dependency on NeMo-Gym first — that dependency is unsatisfiable in this workspace, and silently degrades the resolution rather than failing.

Sam's two commits are carried unchanged (cherry-picked onto current main, authorship and sign-off intact); the third commit is the integration work.

Related Issue

Supersedes the integration half of #1154. That PR remains the source of the package itself.

Changes

The NeMo-Gym dependency is removed. Two problems, either of which blocks registration:

  • nemo-gym[sandbox] cannot be resolved here. Gym floors at CPython 3.13.14 and pulls mlflow-skinny>=3.15.1; services/unsloth pins <3.12.0. uv does not fail on this — it resolves backwards to nemo-gym 0.2.1, a release predating the sandbox work entirely.
  • The imports are from nemo_gym.sandbox.broker, which exists only on an unmerged fork (soluwalana/Gym@nmp/customizer, 15 commits ahead of upstream and 180 behind). Upstream NVIDIA-NeMo/Gym has no such module at any release.

So the contract is vendored, not imported:

Module Contents Origin
sandboxed_gym/wire.py 13 request/response models, 9 constants, 8 validators nemo_gym/sandbox/broker/wire.py @ f2a47392
sandboxed_gym/sandbox_types.py SandboxSpec, SandboxHandle, SandboxExecResult, SandboxStatus, SandboxResources nemo_gym/sandbox/providers/base.py (0.5.0)

Both are pure declarations with no Gym imports. BROKER_PROTOCOL_VERSION is the drift detector between the two copies.

Gym remains a runtime import in exactly two places that genuinely drive it: the OpenSandbox provider (lazy, at backend construction) and runtime/gym_host_runtime.py, which runs inside the Gym image. Both are excluded from ty on the same grounds as the GPU training drivers in services/unsloth.

Fixes surfaced once the type checker could see the package:

  • broker.py — the serve closure dereferenced self._server, an optional attribute; it now captures the server directly.
  • orchestrator.py — a # type: ignore[arg-type] in mypy syntax, which ty does not honour.
  • test_episode_broker.py — a generator fixture annotated as its yield type.
  • test_sandboxed_gym_host.py — an assertion that called _as_network() twice and so never narrowed the Optional.

Three ty: ignore comments remain, each with a stated reason.

Also: SPDX copyright headers on 52 files, and ruff check/format across the package.

Type of Change

  • Code change (feature, bug fix, or refactor)
  • Code change with documentation updates
  • Documentation only
  • Contributor tooling or automation
  • CI, build, or test infrastructure

Quality Gates

  • Tests added or updated for changed behavior
  • Existing tests cover changed behavior — justification: the package ships 130 tests that had never executed, because it was not installed. This PR is what makes them run.
  • Tests not applicable — justification:
  • Documentation updated for user-visible behavior
  • Documentation not applicable — justification: no user-facing surface changes; the vendored modules carry their provenance in module docstrings.

Verification

  • Pull request title follows the repository's Conventional Commit format
  • Every commit includes an appropriate Signed-off-by: trailer
  • uv run pre-commit run -a passes, or any blocked checks are identified below
  • Targeted tests pass, or tests are marked not applicable above
  • No secrets, API keys, or credentials are included

Targeted validation:

Command Result
uv run --frozen pytest packages/sandboxed_gym/tests 128 passed, 2 skipped (first execution of this suite)
uv run --frozen ty check packages/sandboxed_gym All checks passed
uv run ruff check packages/sandboxed_gym All checks passed
uv run ruff format --check packages/sandboxed_gym 39 files already formatted
make check-copyright-headers All 8285 files correct
uv lock zero version changes to existing packages; one addition (sandboxed-gym)
uv run --frozen ty check (repo-wide) 0 diagnostics in sandboxed_gym

The two skipped tests construct the real OpenSandboxProvider, so they run only where Gym is installed — in the runtime image, not a workspace checkout.

uv run pre-commit run -a was not run to completion: on this machine the UI hook fails on a missing pnpm shim, and the license hook rewrites files unrelated to this branch. The individual gates it wraps are listed above.

Open questions for @soluwalana

  1. Is vendoring the right call, or should the broker contract be upstreamed to NVIDIA-NeMo/Gym first? Vendoring unblocks this now; upstreaming is the better end state but is a cross-team ask against a fork that is 180 commits behind.
  2. opensandbox (the SDK) is imported by backends/opensandbox.py but declared nowhere. It is currently supplied by the environment. Should it be a declared optional dependency?
  3. The two skipped tests leave the OpenSandbox construction path uncovered in CI. Worth a fixture that fakes the provider?

soluwalana and others added 3 commits August 19, 2026 13:18
Signed-off-by: Sam Oluwalana <soluwalana@nvidia.com>
Copy the implementation of sandbox gym functionality into a package for nemo platform

cd ~/work/nemo-platform-extract/packages/sandboxed_gym
PYTHONPATH=src:/path/to/nemo-gym pytest tests/ -q
PYTHONPATH=src sandboxed-gym serve --help   # after pip install -e .

Signed-off-by: Sam Oluwalana <soluwalana@nvidia.com>
…pendency

Takes the extracted package from #1154 and makes it a workspace member, which
requires removing its hard dependency on NeMo-Gym first.

`nemo-gym[sandbox]` cannot be declared here. Gym floors at CPython 3.13.14 and
pulls `mlflow-skinny>=3.15.1`, which this workspace cannot satisfy --
`services/unsloth` pins `<3.12.0`. uv does not fail on that: it resolves
backwards to nemo-gym 0.2.1, a release predating the sandbox work, which
satisfies neither the package's imports nor anything we plan against.

Nor could the dependency be satisfied as written. The imports are from
`nemo_gym.sandbox.broker`, which exists only on an unmerged fork
(soluwalana/Gym@nmp/customizer, 15 ahead of upstream and 180 behind); upstream
NVIDIA-NeMo/Gym has no such module at any release.

So the broker contract and the sandbox provider types are vendored instead --
290 and 191 lines of Pydantic models, constants and validators, with no Gym
imports of their own. `BROKER_PROTOCOL_VERSION` is what detects drift between
the copies. Gym remains a runtime import in exactly two places that genuinely
drive it: the OpenSandbox provider (lazy, at backend construction) and
`runtime/gym_host_runtime.py`, which runs inside the Gym image. Both are
excluded from `ty` on the same grounds as the GPU training drivers.

With that resolved, the package's 130 tests run for the first time: 128 pass,
2 skip where they construct the real OpenSandbox provider. Also fixes what the
type checker found: the broker's serve closure dereferenced an optional
attribute, a stale mypy-syntax ignore, a generator fixture annotated as its
yield type, and an assertion that called a function twice and so never narrowed.

Workspace impact is nil: no existing package changes version, none are removed.

Co-authored-by: Sam Oluwalana <soluwalana@nvidia.com>
Signed-off-by: Sandy Chapman <schapman@nvidia.com>
@github-actions github-actions Bot added the build conventional-commit type label Aug 19, 2026
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
if self._config.port is not None:
sock.bind(("", self._config.port))
for _ in range(max_retries):
port = random.randint(port_range_low, port_range_high - 1)
try:
sock.bind(("", port))
)
except Exception as exc:
LOGGER.exception("upstream rollout failed")
return JSONResponse(status_code=502, content={"error": str(exc)})
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
try:
sock.bind(("", port))
@github-actions

Copy link
Copy Markdown
Contributor
Suite Lines Covered Line Rate Branch Rate
Unit Tests 34303/43320 79.2% 64.0%
Integration Tests 20256/41119 49.3% 22.0%

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

build conventional-commit type

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants