Add sandbox API and mini swe agent 2 resource agent - #1377
Conversation
ananthsub
left a comment
There was a problem hiding this comment.
looks great! a few clarifying questions added inline
|
can we add sandbox api docs draft in a separate PR |
|
and if there is a simpler example, such as hello world (math + sandboxed python tool?), would be great |
| """Provider-neutral sandbox creation request.""" | ||
|
|
||
| image: str | None = None | ||
| snapshot_id: str | None = None |
There was a problem hiding this comment.
Good point. snapshot_id is not a provider-neutral sandbox concept. It is an OpenSandbox-specific create option for starting from a pre-existing snapshot, so I moved it out of SandboxSpec and into provider_options["snapshot_id"]. Generic callers no longer see it as part of the public API. The relevant change is in a93f703.
|
|
||
| name: str | ||
|
|
||
| async def create(self, spec: SandboxSpec) -> SandboxHandle: |
There was a problem hiding this comment.
I'd appreciate to precise the contract here - will the sandbox handle be returned only when the sandbox is fully up & healthy, or do we expect it to just "create" the sandbox with some status param which can be eg. "STARTING". I mean sth like a pod definition. The later could be better for observability - if the sandbox fails to start what do we know about the failure type?
There was a problem hiding this comment.
The intended contract is that create returns only when the sandbox is ready enough to run commands and transfer files, otherwise it raises a create error. I kept STARTING as a status value for providers that expose lifecycle status after start, but not as a successful create result. Startup failures should be surfaced as typed create or verification errors with provider detail rather than returning a half-ready handle. The contract text and status support are in a93f703.
| should pass it back to the provider through this handle rather than | ||
| inspecting or mutating it directly. | ||
| """ | ||
|
|
There was a problem hiding this comment.
could we have some status marker? either as a property or a function, see https://github.com/NVIDIA-NeMo/Evaluator/blob/main/src/nemo_evaluator/sandbox/base.py#L178 for ref
There was a problem hiding this comment.
Yes. I added a provider-neutral SandboxStatus enum and a minimal public status() method on both AsyncSandbox and Sandbox. I also removed the extra is_running convenience wrapper so the public surface stays small while still allowing callers to inspect lifecycle state. The status API was added in a93f703 and then minimized in 97b3210.
| """ | ||
| ... | ||
|
|
||
| async def connect(self, sandbox_id: str) -> SandboxHandle: |
There was a problem hiding this comment.
what does it mean? should it be more of an internal primitive perhaps, preceeding other operations like exec or upload?
There was a problem hiding this comment.
Agreed. Connect should not be part of the public sandbox API. It is only an OpenSandbox provider implementation detail for obtaining a loop-local SDK object before provider operations. I removed the public attach and connect methods entirely, so callers now start a sandbox and then use exec, upload, download, status, and stop on that object. The public API trim is in 97b3210.
| def register_provider(name: str, provider_class: ProviderClass, *, override: bool = False) -> None: | ||
| """Register a sandbox provider class.""" | ||
| if not name: | ||
| raise ValueError("Provider name must be non-empty") | ||
| if not override and (name in _PROVIDER_REGISTRY or name in _BUILTIN_PROVIDER_LOADERS): | ||
| raise ValueError(f"Sandbox provider {name!r} is already registered") | ||
| _PROVIDER_REGISTRY[name] = provider_class | ||
|
|
There was a problem hiding this comment.
(not blocking) but for users to plug in their own providers, we should also support an entry_points configuration
There was a problem hiding this comment.
given the time pressure for 0.4. id recommend we create a separate issue to track this targeted for 0.5
| sandbox_provider: | ||
| opensandbox: | ||
| connection: | ||
| domain: opensandbox-server.opensandbox-system.svc.cluster.local | ||
| api_key: ${oc.env:OPENSANDBOX_API_KEY} | ||
| protocol: http | ||
| request_timeout_s: 300 | ||
| use_server_proxy: true | ||
| create: | ||
| request_timeout_s: 1200 | ||
| timeout_s: 1200 | ||
| skip_health_check: true | ||
| retries: 10 | ||
| retry_delay_s: 5.0 | ||
| retry_max_delay_s: 90.0 | ||
| probe: | ||
| timeout_s: 60 | ||
| deadline_s: 180 | ||
| stable_count: 2 | ||
| stable_delay_s: 1.0 | ||
| operations: | ||
| retries: 5 | ||
| retry_delay_s: 1.0 | ||
| retry_max_delay_s: 45.0 | ||
| command_retries: 3 | ||
| close_timeout_s: 30 |
There was a problem hiding this comment.
this config shape makes it hard to swap out for new providers. ideally we should provide an example of each of these provider configs as their own yamls and make it easy to swap in for different agent implementations.
e.g.
# nemo_gym/sandbox/providers/opensandbox/configs/opensandbox.yaml
sandbox_main: # the name the agent references
opensandbox: # registry key -> provider class
connection:
domain: ${oc.env:OPENSANDBOX_DOMAIN}
api_key: ${oc.env:OPENSANDBOX_API_KEY}
create: { timeout_s: 1200, skip_health_check: true, retries: 10 }
probe: { timeout_s: 60, deadline_s: 180, stable_count: 2 }
operations: { retries: 5, command_retries: 3, close_timeout_s: 30 }
options: { platform: { os: linux, arch: amd64 } } # run-level provider-specific defaults
then users could have other sandbox configs like
# nemo_gym/sandbox/providers/ecs/configs/ecs_fargate.yaml
sandbox_main:
ecs_fargate:
# define provider specific configs
the agent config can reference this in its config:
env: sandbox
sandbox_provider: sandbox_main # inlined from the reference above, makes it easy to swap out for different providers
sandbox_spec:
timeout_s: 18000
ready_timeout_s: 1200
resources:
cpu: "2"
memory: 8Gi
ephemeral-storage: 20Gi
metadata:
benchmark: swebench-verified
harness: mini-swe-agent
sandbox-api: opensandbox-sdk
sandbox_environment_kwargs:
cwd: /testbed
conda_env: testbed
activate_conda: true
user: root
delete: true
run_golden: false
step_timeout: 600
eval_timeout: 1800
skip_if_exists: false
step_limit: 250
AGENT=responses_api_agents/mini_swe_agent_2/configs/mini_swe_agent_opensandbox.yaml
MODEL=responses_api_models/vllm_model/configs/vllm_model.yaml
# OpenSandbox
ng_run "+config_paths=[$AGENT, nemo_gym/sandbox/providers/opensandbox/configs/opensandbox.yaml, $MODEL]"
# ECS: swap one path
ng_run "+config_paths=[$AGENT, nemo_gym/sandbox/providers/runpod/configs/ecs_fargate.yaml, $MODEL]"
There was a problem hiding this comment.
@ananthsub agreed on the config direction. I am going to keep this PR scoped to the sandbox API/provider lifecycle cleanup, and follow up in a separate PR with the swappable provider config YAMLs and agent config wiring so providers can be exchanged by swapping a config path. Leaving this thread open for that follow-up rather than treating it as addressed here.
There was a problem hiding this comment.
strong agree on this, I would like to decouple sandbox and agent config as much as possible
| if self._create.image_pull_policy is None: | ||
| return spec | ||
|
|
||
| provider_options = dict(spec.provider_options) |
There was a problem hiding this comment.
should the individual provider options configs be represented with frozen dataclasses here? otherwise it's hard for users to see the supported options + any validation logic for it
cd9e198 to
0906e13
Compare
| config_output_dir = Path(output_file_dir) / "_configs" | ||
| config_output_dir.mkdir(parents=True, exist_ok=True) | ||
| config_path = config_output_dir / f"{instance_id}.sandbox.yaml" | ||
| config_path.write_text(yaml.safe_dump(config, sort_keys=False)) |
There was a problem hiding this comment.
This dumps the fully-resolved agent config to disk per instance, and self.config.sandbox_provider includes connection.api_key. Server configs are built with OmegaConf.to_container(..., resolve=True) (see server_utils.py), so ${oc.env:OPENSANDBOX_API_KEY} is already the real key by the time we get here — yaml.safe_dump writes the secret in cleartext to results/<subset>/<model>/_configs/<instance_id>.sandbox.yaml, once per task. This also bypasses the framework's own _recursively_hide_secrets masking. Could we mask/strip connection.api_key before dumping and re-inject it in the Ray worker from the env var (or pass it via Ray runtime_env.env_vars)? It's gitignored, but it's still cleartext on disk and could leak via artifact upload.
| timeout_s: int | float | None = 180, | ||
| user: str | int | None = None, | ||
| ) -> SandboxExecResult: | ||
| return await self._provider.exec( |
There was a problem hiding this comment.
exec() retries through the same policy as create/file ops, and _is_retryable_sdk_operation_error treats ConnectionError/OSError as retryable (the shipped config sets command_retries: 3). For arbitrary agent commands that isn't safe: if the connection drops after the command already ran server-side, the retry re-executes it and duplicates side effects (file writes, git apply, etc.). Suggest defaulting command_retries to 0 (so only create/connect/file ops retry) and/or documenting that command retries assume idempotency.
| result = self._sandbox.exec( | ||
| self._command(command, exec_cwd), | ||
| timeout_s=timeout_s, | ||
| cwd="/", |
There was a problem hiding this comment.
cwd is hardcoded to /, and the configured working dir is only applied via the cd <cwd> that _command() injects — but _command() returns the bare command when activate_conda is off (lines 124–125). So with conda disabled, exec_cwd is silently dropped and every command runs in /. The default SWE-bench config uses conda so it works today, but it's a latent footgun. Suggest passing cwd=exec_cwd to exec() and letting the conda branch skip its own cd.
| self._closed = True | ||
| if not self._loop.is_closed(): | ||
| self._loop.call_soon_threadsafe(self._loop.stop) | ||
| self._thread.join(timeout=5) |
There was a problem hiding this comment.
If a provider call hangs longer than 5s, join(timeout=5) returns while the loop is still running and self._loop.close() then raises RuntimeError: Cannot close a running event loop from inside Sandbox.stop()'s finally. Also call()/run() block on future.result() with no timeout (lines 174/179), so a wedged loop thread hangs the caller indefinitely. Consider only calling loop.close() when the thread actually joined, and/or a bounded future.result(timeout=...).
8127481 to
428238f
Compare
|
@hemildesai can we merge? WDYT? |
…VIDIA-NeMo#1249) First increment of NVIDIA-NeMo#1249 on top of the NVIDIA-NeMo#1377 Sandbox API: a provider-neutral responses_api_agents/swe_env library (provisioning + exec + per-family harness recipes + grading) and a required, stateless, fresh-only resources_servers/swe_env verifier with a server-private verify_task orchestrator. - swe_env library: SweTask/harness contract (provisioning vs server-private grading split), AsyncSweEnvironment over nemo_gym.sandbox, registry, pure grading helpers, swe-bench-ext reference harness. - providers: DockerSandboxProvider (real/local, enables end-to-end testing without apptainer) + ApptainerSandboxProvider (ports the legacy .sif path; mocked-tested). - verifier: SweEnvVerifier.verify() extracts the patch from the response, grades in its own fresh sandbox, masks infra failures as reward=0.0 (never None). - 25 tests incl. an env-gated real docker-backed end-to-end (gold patch -> resolved). See SWE_ENV_DECOUPLE_STATUS.md for scope, what's tested, and follow-ups. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Signed-off-by: adil-a <adil.asif2000@hotmail.com>
Port NEL's ECS Fargate sandboxing into Gym as a third sandbox provider, stacked on the provider framework from #1377. - nemo_gym/sandbox/providers/ecs_fargate/{engine,provider}.py — NEL's engine lifted (task-def registration + SSM caching, RunTask with capacity retries, SSH sidecar + reverse-tunnel outside-endpoint routing, in-container exec server, CodeBuild->ECR image build) behind a thin adapter that keeps per-sandbox state in SandboxHandle.raw. - registry loader + `sandbox-ecs` extra (boto3, lazy-imported). - region-only config via SSM autodiscovery (/<ssm_project>/ecs-sandbox/config), matching NEL. - mini_swe_agent_2 ecs_fargate config + 14 unit tests (AWS/SSH mocked). Fast-follow P0 (separate PR): route the model endpoint over Teleport as an internal option instead of the globally-exposed SSH reverse tunnel that security flagged; SSH tunneling stays available for community use. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Signed-off-by: Michal Bien <mbien@nvidia.com>
Two installed distributions publishing the same provider entry-point name now raise a clear error naming both packages, instead of silently picking one nondeterministically. An entry point shadowed by a higher-precedence built-in or registered provider is logged as a warning and ignored. Refs #1377 Signed-off-by: Ananth Subramaniam <ansubramania@nvidia.com>
## Summary Parent PR: #1368 Refs #1337 This is the first smaller PR split out from #1368. It keeps the scope to the provider-neutral sandbox API, the OpenSandbox provider, and the Mini SWE Agent 2 evaluation integration. Observability is intentionally left out for a follow-up PR. ### Features - Adds the public `nemo_gym.sandbox` facade with async and sync sandbox clients, provider registration, image rewrite support, sandbox specs/handles, and batch create support. - Adds the OpenSandbox provider with create/connect/exec/file/close operations, SDK pool-backed batch creation, retry handling, create probes, direct exec endpoint support, and nested provider configuration sections. - Adds `responses_api_agents/mini_swe_agent_2`, a sandbox-backed mini-swe-agent v2 integration for SWE-bench style evals, including sandbox resource profiles, task metadata propagation, reward aggregation, and `ng_collect_rollouts` usage docs. - Adds focused unit coverage for the sandbox facade, provider registry, OpenSandbox provider behavior, Mini SWE Agent 2 run/aggregation behavior, and sandbox environment adapter. - Moves sandbox-related dependencies behind the `nemo-gym[sandbox]` optional extra. ### Notes - This PR does not include the sandbox observability module from #1368. - The Mini SWE Agent 2 README avoids internal deployment names and user-specific paths; examples use placeholders and local `data/` / `results/` paths. ## Validation Completed on the squashed commit: ```bash uv run ruff check nemo_gym/sandbox responses_api_agents/mini_swe_agent_2 tests/unit_tests/test_sandbox.py tests/unit_tests/test_opensandbox_provider.py ``` Result: `All checks passed!` ```bash uv run pytest tests/unit_tests/test_sandbox.py tests/unit_tests/test_opensandbox_provider.py responses_api_agents/mini_swe_agent_2/tests/test_app.py responses_api_agents/mini_swe_agent_2/tests/test_sandbox_environment.py -q ``` Result: `44 passed, 2 warnings` ```bash uv run coverage run --source=nemo_gym.sandbox,responses_api_agents.mini_swe_agent_2 -m pytest tests/unit_tests/test_sandbox.py tests/unit_tests/test_opensandbox_provider.py responses_api_agents/mini_swe_agent_2/tests/test_app.py responses_api_agents/mini_swe_agent_2/tests/test_sandbox_environment.py -q uv run coverage combine results uv run coverage report --include='nemo_gym/sandbox/*,responses_api_agents/mini_swe_agent_2/*' --fail-under=90 ``` Result: focused coverage `92%`. Kubernetes smoke validation: - Model: `Qwen/Qwen3.5-27B`, served by SGLang with DFLASH draft model `z-lab/Qwen3.5-27B-DFlash`. - Launched the Mini SWE Agent 2 stack through the documented `ng_collect_rollouts` path. - Ran 8 SWE-bench Verified samples with 8 repeats and 64-way rollout concurrency against the OpenSandbox internal service path. - Result: 64/64 rollout rows, `pass@8=0.875`, 7/8 tasks resolved, mean reward `0.765625`, eval error rate `0.0`, reward profile completion `100%`. - Cleanup completed with no leftover sandboxes for the successful internal-service run. Full SWE-bench Verified validation: - Model: `Qwen/Qwen3.5-27B`, served by SGLang with DFLASH draft model `z-lab/Qwen3.5-27B-DFlash`. - Ran 500 SWE-bench Verified samples with pass@1, 500-way rollout concurrency, `step_limit=250`, and OpenSandbox cleanup metadata. - Result: 500/500 rollout rows, `pass@1=0.698`, 349/500 tasks resolved, mean reward `0.698`, eval error rate `0.6`, tests status rate `99.0`, reward profile completion `100%`. - Job duration: `4h6m`; rollout collection duration: `4h04m`. - Cleanup left no sandboxes with the run labels `run_family=mini-swe2-firstpr-q35-cell-full-p1-r9` or `cleanup_id=full-p1-r9-single-20260521-053410`. --------- Signed-off-by: Hemil Desai <hemild@nvidia.com> Signed-off-by: Rita Fernandes Neves <rfernandesne@nvidia.com>
Port NEL's ECS Fargate sandboxing into Gym as a third sandbox provider, stacked on the provider framework from #1377. - nemo_gym/sandbox/providers/ecs_fargate/{engine,provider}.py — NEL's engine lifted (task-def registration + SSM caching, RunTask with capacity retries, SSH sidecar + reverse-tunnel outside-endpoint routing, in-container exec server, CodeBuild->ECR image build) behind a thin adapter that keeps per-sandbox state in SandboxHandle.raw. - registry loader + `sandbox-ecs` extra (boto3, lazy-imported). - region-only config via SSM autodiscovery (/<ssm_project>/ecs-sandbox/config), matching NEL. - mini_swe_agent_2 ecs_fargate config + 14 unit tests (AWS/SSH mocked). Fast-follow P0 (separate PR): route the model endpoint over Teleport as an internal option instead of the globally-exposed SSH reverse tunnel that security flagged; SSH tunneling stays available for community use. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Signed-off-by: Michal Bien <mbien@nvidia.com>
…aclass Represent the recognized per-sandbox create options (spec.provider_options) as a frozen OpenSandboxProviderOptions dataclass with a validating from_mapping, so the supported options and their types are discoverable in one place and unknown keys are rejected with a clear error. The create path now reads typed attributes instead of scattered dict lookups. SDK-owned nested structures (platform, volumes) stay pass-through mappings so their inner fields remain validated by the OpenSandbox SDK rather than over-constrained here. Refs #1377 Signed-off-by: Ananth Subramaniam <ansubramania@nvidia.com>
Sandbox providers are now defined as named blocks in their own config files (e.g. nemo_gym/sandbox/providers/opensandbox/configs/opensandbox.yaml) that agents reference by name (sandbox_provider: sandbox). Swapping providers becomes swapping one config path in +config_paths, with no edits to the agent config. - Add resolve_provider_config to resolve a sandbox name (or an inline single-key mapping) to a single provider config. - Make mini_swe_agent_2's config provider-neutral and resolve the reference at runtime. - Document single / swap / multiple-sandbox usage, including distinct instance names for mixing providers or running two configs of the same provider type. Refs #1377 Signed-off-by: Ananth Subramaniam <ansubramania@nvidia.com>
…aclass Represent the recognized per-sandbox create options (spec.provider_options) as a frozen OpenSandboxProviderOptions dataclass with a validating from_mapping, so the supported options and their types are discoverable in one place and unknown keys are rejected with a clear error. The create path now reads typed attributes instead of scattered dict lookups. SDK-owned nested structures (platform, volumes) stay pass-through mappings so their inner fields remain validated by the OpenSandbox SDK rather than over-constrained here. Refs #1377 Signed-off-by: Ananth Subramaniam <ansubramania@nvidia.com>
…aclass Represent the recognized per-sandbox create options (spec.provider_options) as a frozen OpenSandboxProviderOptions dataclass with a validating from_mapping, so the supported options and their types are discoverable in one place and unknown keys are rejected with a clear error. The create path now reads typed attributes instead of scattered dict lookups. SDK-owned nested structures (platform, volumes) stay pass-through mappings so their inner fields remain validated by the OpenSandbox SDK rather than over-constrained here. Refs #1377 Signed-off-by: Ananth Subramaniam <ansubramania@nvidia.com>
) ## Summary Follow-up to #1377 addressing the config-usability feedback ([thread](#1377 (comment))): the inline `sandbox_provider:` block welded provider connection/lifecycle config (and its secret) into the agent config, making it hard to swap providers. This decouples the two. A sandbox is now a **named block** — `<name>: { <provider>: {config} }` — defined in its own provider config file, and an agent points at it by name (`sandbox_provider: sandbox`). The framework only ever resolves *a name → one provider config*. - **Single (default):** ship a `sandbox` block; the agent defaults to `sandbox_provider: sandbox`. - **Swap providers (no agent edit):** every shipped provider config binds the same name `sandbox`, so swapping providers is swapping one `+config_paths` entry. - **Multiple / mixed / same-type:** give blocks distinct instance names and reference each explicitly — no framework change. ### Changes - Add `nemo_gym.sandbox.resolve_provider_config` — resolves a sandbox name (from the merged config) or an inline single-key mapping to a single `{provider: config}` dict for `create_provider`. - Add `nemo_gym/sandbox/providers/opensandbox/configs/opensandbox.yaml` defining the named `sandbox` block. - Make `responses_api_agents/mini_swe_agent_2` provider-neutral: `sandbox_provider` accepts a name (default) or inline mapping; the reference is resolved at runtime. Renamed `mini_swe_agent_opensandbox.yaml` → `mini_swe_agent_2.yaml`. - README documents the single concept plus single / swap / multi-sandbox usage. - Unit tests for the resolver and the named-reference agent path. Bottom of a 2-PR stack; the follow-up adds provider-contributed default sandbox metadata. Refs #1377 ## Test plan - [x] `uv run pytest tests/unit_tests/test_sandbox.py tests/unit_tests/test_opensandbox_provider.py responses_api_agents/mini_swe_agent_2/tests/` - [x] `uv run ruff check` + `ruff format --check` clean - [x] End-to-end config composition verified through `GlobalConfigDictParser` Signed-off-by: Ananth Subramaniam <ansubramania@nvidia.com> Signed-off-by: Hemil Desai <hemild@nvidia.com> Co-authored-by: Hemil Desai <hemild@nvidia.com>
…aclass Represent the recognized per-sandbox create options (spec.provider_options) as a frozen OpenSandboxProviderOptions dataclass with a validating from_mapping, so the supported options and their types are discoverable in one place and unknown keys are rejected with a clear error. The create path now reads typed attributes instead of scattered dict lookups. SDK-owned nested structures (platform, volumes) stay pass-through mappings so their inner fields remain validated by the OpenSandbox SDK rather than over-constrained here. Refs #1377 Signed-off-by: Ananth Subramaniam <ansubramania@nvidia.com>
…1709) ## Summary Stacked on top of #1708. Lets a sandbox provider config contribute default sandbox metadata* so provider-identifying tags live with the provider rather than the provider-neutral agent config. A sandbox block may carry an optional `default_metadata` key: ```yaml sandbox: default_metadata: { sandbox-api: opensandbox-sdk } opensandbox: connection: { ... } ``` Its entries are merged into each sandbox's `SandboxSpec.metadata`; the agent's own `sandbox_spec.metadata` overrides them on conflict. This restores the `sandbox-api: opensandbox-sdk` label that #1708 dropped from the agent config, now sourced from the provider config instead. ### Changes - Add `nemo_gym.sandbox.resolve_provider_metadata`; exclude reserved keys (`default_metadata`) from `resolve_provider_config`. - `mini_swe_agent_2` merges provider `default_metadata` into the sandbox spec metadata at runtime. - Add `default_metadata: { sandbox-api: opensandbox-sdk }` to the opensandbox provider config; document it in the README. - Unit tests for `resolve_provider_metadata` and the agent merge path. Refs #1377 ## Test plan - [x] `uv run pytest tests/unit_tests/test_sandbox.py responses_api_agents/mini_swe_agent_2/tests/test_app.py` (38 passed, 10 skipped) - [x] `uv run ruff check` + `ruff format --check` clean - [x] End-to-end resolution verified through `GlobalConfigDictParser` (provider key + default_metadata) --------- Signed-off-by: Ananth Subramaniam <ansubramania@nvidia.com> Signed-off-by: Hemil Desai <hemild@nvidia.com> Signed-off-by: Kajal Jain <kajalj@nvidia.com> Co-authored-by: Hemil Desai <hemild@nvidia.com> Co-authored-by: Kajal Jain <kajalj@nvidia.com>
## Summary Stacked on top of #1709. Addresses the open review thread on #1377 ([here](#1377 (comment))) asking to support an entry-points configuration so users can plug in their own providers. Adds a `nemo_gym.sandbox_providers` entry point group. A separate package can publish a sandbox provider that becomes available on install/import — no edits to the registry: ```toml [project.entry-points."nemo_gym.sandbox_providers"] my_provider = "my_pkg.provider:MyProvider" ``` Lookup precedence is **explicit `register_provider` > built-in loaders > entry points**; discovery is cached. `list_providers()` now unions all three sources. ### Changes - `nemo_gym/sandbox/providers/registry.py`: add `ENTRY_POINT_GROUP`, cached `_entry_point_loaders()`, and fold entry points into `get_provider_class` / `list_providers`. - Unit test for discovery + built-in precedence (mocked entry points). - README: document registering a custom provider via entry points. Refs #1377 ## Test plan - [x] `uv run pytest tests/unit_tests/test_sandbox.py tests/unit_tests/test_opensandbox_provider.py` (23 passed, 11 skipped) - [x] `uv run ruff check` clean --------- Signed-off-by: Ananth Subramaniam <ansubramania@nvidia.com>
…aclass (#1713) ## Summary Stacked on top of #1712. Addresses the open review thread on #1377 ([here](#1377 (comment))) asking whether the individual provider options should be frozen dataclasses so the supported options + validation are discoverable. Represents the recognized per-sandbox create options (`SandboxSpec.provider_options`) as a frozen `OpenSandboxProviderOptions` dataclass with a validating `from_mapping`: - Supported options and their types now live in one place (`platform`, `snapshot_id`, `volumes`, `skip_health_check`, `extensions`). - Unknown keys and wrong types are rejected with clear errors. - The create path reads typed attributes instead of scattered `provider_options.get(...)` lookups, replacing the ad-hoc `_spec_extensions` / `_provider_option_bool` / `_spec_volumes` helpers. ### Design note SDK-owned nested structures (`platform`, `volumes`) are kept as pass-through mappings rather than strictly-typed sub-dataclasses, so their inner fields stay validated by the OpenSandbox SDK and we don't over-constrain options we don't own. Refs #1377 ## Test plan - [x] `uv run pytest tests/unit_tests/test_opensandbox_provider.py tests/unit_tests/test_sandbox.py responses_api_agents/mini_swe_agent_2/tests/test_app.py` (40 passed, 11 skipped) - [x] `uv run ruff check` + `ruff format --check` clean - [x] Create-path behavior (platform passthrough, image-pull-policy extensions, skip_health_check) preserved under the fake-SDK tests Signed-off-by: Ananth Subramaniam <ansubramania@nvidia.com>
Port NEL's ECS Fargate sandboxing into Gym as a third sandbox provider, stacked on the provider framework from #1377. - nemo_gym/sandbox/providers/ecs_fargate/{engine,provider}.py — NEL's engine lifted (task-def registration + SSM caching, RunTask with capacity retries, SSH sidecar + reverse-tunnel outside-endpoint routing, in-container exec server, CodeBuild->ECR image build) behind a thin adapter that keeps per-sandbox state in SandboxHandle.raw. - registry loader + `sandbox-ecs` extra (boto3, lazy-imported). - region-only config via SSM autodiscovery (/<ssm_project>/ecs-sandbox/config), matching NEL. - mini_swe_agent_2 ecs_fargate config + 14 unit tests (AWS/SSH mocked). Fast-follow P0 (separate PR): route the model endpoint over Teleport as an internal option instead of the globally-exposed SSH reverse tunnel that security flagged; SSH tunneling stays available for community use. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Signed-off-by: Michal Bien <mbien@nvidia.com>
ECS Fargate `SandboxProvider` on top of the sandbox API (#1377, now merged to `main`). Auto-mirrors public images to ECR on demand; SSH reverse-tunnel for exec / file-transfer / model egress. Rebased onto `main` now that the sandbox base (#1377) has landed — this PR stands on its own and is ready for review. **Sandbox-bound agents line of work:** **ECS Fargate (this PR)** → adapter middleware base (#1646) → sandbox-bound CLI agents + capture (#1647). Interceptor follow-ups on the adapter base: #1649 (caching), #1650 (observability), #1651 (rewrites). 3 commits, +5974: ECS provider + engine, on-demand ECR mirroring + conda activation fix, and 233 unit tests (engine.py to 99%). `uv.lock` reconciled with main's security upgrades (#1657). --------- Signed-off-by: Michal Bien <mbien@nvidia.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Stacked on #1377. Closes #1687 Adds a Daytona-backed sandbox provider to the sandbox API provider registry, including SDK dependency wiring and provider lifecycle/exec/file helpers. Smoke notes: - Provider import and server startup passed against the rebased sandbox API branch. - The latest 16-sample Daytona SWE smoke completed at the Gym layer, but 12/16 rows hit Daytona org disk quota during sandbox creation (`Total disk limit exceeded. Maximum allowed: 30GiB`), so the observed 1/16 reward is not a meaningful model-quality pass rate. - Successful live sandbox labels were observed during the run with `sandbox-api: daytona-sdk`. ### Daytona full SWE-bench Verified validation - Run: Kubernetes job `hemild-daytona-full-500-c64-014139` completed successfully (`1/1`, pod `0` restarts). - Artifact: `/mnt/rl-workspace/hemild/gym_eval/refactor/runs/mini_swe_agent_2_ng_collect_rollouts/20260624-014139-daytona-full-verified-c64-step1800/results/mini_swe_agent_2_sglang_qwen35_dflash_daytona_full500.jsonl` - Scale/result: `500/500` rollout rows, `324/500` solved, pass@1 / avg rollout reward `0.648` (`64.8%`). - Key config: SWE-bench Verified, Daytona provider, Qwen 3.5 27B via SGLang, max concurrency `64`, `enable_thinking=true`, `temperature=0.6`, `top_p=0.95`, `max_output_tokens=32768`, step timeout `1800s`, per-command timeout `1500s`, Daytona command retries `0`. - Infra/model/Gym error summary: `31/500` runtime error rows (`6.2%` eval error rate): `10` Daytona blank `/process/execute` HTTP/request timeouts, `9` Daytona command-timeout `408`s, `10` Daytona container-IP resolution failures, `1` Daytona `502 Bad Gateway`, and `1` model `litellm`/API timeout. No separate Gym schema/config failure class was observed; remaining failures were normal model/test failures. --------- Signed-off-by: Hemil Desai <hemild@nvidia.com>
Summary
Parent PR: #1368
Refs #1337
This is the first smaller PR split out from #1368. It keeps the scope to the provider-neutral sandbox API, the OpenSandbox provider, and the Mini SWE Agent 2 evaluation integration. Observability is intentionally left out for a follow-up PR.
Features
nemo_gym.sandboxfacade with async and sync sandbox clients, provider registration, image rewrite support, sandbox specs/handles, and batch create support.responses_api_agents/mini_swe_agent_2, a sandbox-backed mini-swe-agent v2 integration for SWE-bench style evals, including sandbox resource profiles, task metadata propagation, reward aggregation, andng_collect_rolloutsusage docs.nemo-gym[sandbox]optional extra.Notes
data//results/paths.Validation
Completed on the squashed commit:
Result:
All checks passed!Result:
44 passed, 2 warningsuv run coverage run --source=nemo_gym.sandbox,responses_api_agents.mini_swe_agent_2 -m pytest tests/unit_tests/test_sandbox.py tests/unit_tests/test_opensandbox_provider.py responses_api_agents/mini_swe_agent_2/tests/test_app.py responses_api_agents/mini_swe_agent_2/tests/test_sandbox_environment.py -q uv run coverage combine results uv run coverage report --include='nemo_gym/sandbox/*,responses_api_agents/mini_swe_agent_2/*' --fail-under=90Result: focused coverage
92%.Kubernetes smoke validation:
Qwen/Qwen3.5-27B, served by SGLang with DFLASH draft modelz-lab/Qwen3.5-27B-DFlash.ng_collect_rolloutspath.pass@8=0.875, 7/8 tasks resolved, mean reward0.765625, eval error rate0.0, reward profile completion100%.Full SWE-bench Verified validation:
Qwen/Qwen3.5-27B, served by SGLang with DFLASH draft modelz-lab/Qwen3.5-27B-DFlash.step_limit=250, and OpenSandbox cleanup metadata.pass@1=0.698, 349/500 tasks resolved, mean reward0.698, eval error rate0.6, tests status rate99.0, reward profile completion100%.4h6m; rollout collection duration:4h04m.run_family=mini-swe2-firstpr-q35-cell-full-p1-r9orcleanup_id=full-p1-r9-single-20260521-053410.