Skip to content

feat(osworld): add OpenSandbox backend with scoped dependency policy - #2308

Merged
ko3n1g merged 12 commits into
NVIDIA-NeMo:mainfrom
JeffPengCoder:feature/sandbox
Aug 13, 2026
Merged

feat(osworld): add OpenSandbox backend with scoped dependency policy#2308
ko3n1g merged 12 commits into
NVIDIA-NeMo:mainfrom
JeffPengCoder:feature/sandbox

Conversation

@JeffPengCoder

@JeffPengCoder JeffPengCoder commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

Summary

This PR adds a production-oriented OpenSandbox Pool backend for the OSWorld benchmark while preserving the existing Gym Docker/KVM path.

It also fixes the root dependency policy that previously made OSWorld's managed-server environment impossible to express cleanly: dependency exclusions are now scoped to the upstream packages that declare the unwanted edges instead of globally removing those package names from every server environment.

Key outcomes:

  • Adds gym_opensandbox as an OSWorld execution backend backed by a pre-provisioned, server-managed KVM Pool.
  • Keeps the OSWorld agent, task setup, action loop, and evaluators identical across Docker and OpenSandbox.
  • Adapts OpenSandbox path-based gateway endpoints, routing headers, and Chrome CDP WebSockets to OSWorld's host-and-port interface.
  • Makes create, cancellation, shutdown, and interrupted-run cleanup bounded and ownership-aware.
  • Moves the OSWorld managed-server dependency declaration to the repository's established requirements.txt convention and pins OSWorld to an immutable archive.
  • Replaces global dependency exclusions with source-package-scoped exclusions and aligns CI on the minimum uv version that supports that policy.

Motivation

The existing OSWorld integration assumes that the Gym client owns a Docker container, mounts a local qcow2 image, and can expose the guest services as plain host:port endpoints. An OpenSandbox Pool changes all three assumptions:

  1. The server-side Pool owns the VM image, QEMU entrypoint, and capacity.
  2. Allocation is image-less from the client perspective and is selected by poolRef.
  3. Externally reachable services may be returned as gateway URLs with path prefixes and required routing headers rather than directly routable Pod addresses.

The goal is therefore not to fork the OSWorld execution path. It is to keep OSWorld's behavior unchanged and translate only the lifecycle and endpoint boundary beneath it.

Architecture

                                  model HTTP
                         +--------------------------+
                         |                          v
Task JSONL -> prepare -> OSWorld agent/control -> Nano Omni endpoint
                         |
                         | DesktopEnv provider contract
                         v
                 GymSandboxDesktopProvider
                         |
                         v
                  Gym Sandbox public API
                    /                 \
                   /                   \
      Docker provider                  OpenSandbox provider
      - client-owned image             - server-owned KVM Pool
      - client-owned qcow2 mount        - image-less allocation by poolRef
      - direct host:port endpoints      - gateway path/header endpoints
                   \                   /
                    \                 /
                     OSWorld guest services
              screenshot | input | file | CDP | VLC

For gateway endpoints, the OSWorld adapter starts one loopback forwarder per guest service:

OSWorld http://127.0.0.1:<local-port>/...
        -> preserve gateway path prefix
        -> inject required routing headers
        -> forward HTTP or Chrome CDP WebSocket
        -> OpenSandbox gateway -> allocated VM

Design principles

Preserve one OSWorld execution contract

GymSandboxDesktopProvider continues to implement OSWorld's existing provider interface. Backend selection changes resource acquisition and endpoint resolution only; task setup, observations, actions, proxy behavior, and evaluators remain on the same code path.

Make resource ownership explicit

  • Docker mode requires the client-owned image and readable qcow2 path, then supplies the QEMU entrypoint and mounts.
  • OpenSandbox Pool mode rejects client images and VM paths. It sends only lifecycle fields, metadata, exposed ports, and provider_options.extensions.poolRef; the Pool owns the image, entrypoint, and compute capacity.

This prevents local Docker defaults from leaking into a server-managed Pool allocation.

Translate endpoints at the narrowest boundary

OSWorld expects one host and integer ports, whereas OpenSandbox can expose URL paths, headers, and WebSocket routes. The local forwarder performs that translation at the adapter boundary, disables ambient proxy inheritance, and leaves OSWorld itself unaware of transport-specific routing.

Bound failure and cancellation

  • Provider operations have explicit timeouts and retry policies.
  • Synchronous Sandbox calls cancel and drain their underlying async task before returning on timeout, interruption, or another BaseException.
  • Pool creates carry a unique marker so an allocation whose response is lost can be found and reaped without matching another run.
  • Startup failures close forwarders and best-effort release the allocated Sandbox.

Clean up only owned resources

Each run carries exact run metadata. Normal shutdown releases its Sandbox; the recovery tool first audits and then optionally terminates only instances whose metadata value exactly matches the requested run ID. It checks both the OSWorld and Gym metadata keys and revalidates the match immediately before deletion.

Credentials remain environment-only and are not written to generated env.yaml files.

Dependency policy

The OSWorld agent now uses responses_api_agents/osworld_agent/requirements.txt, matching the managed-server convention used throughout responses_api_agents.

The prior root exclude-dependencies list acted globally when the editable root project was installed into an isolated server environment. That meant a package excluded only because it was an unwanted dependency of mlflow was also silently unavailable when OSWorld or another component required it directly. Keeping a component-local workaround would preserve that incorrect repository-wide behavior.

This PR fixes the policy at its source:

  • mlflow exclusions apply only to dependency edges declared by mlflow.
  • OSWorld's unavailable optional agp-client edge and conflicting GUI OpenCV edges are scoped to osworld.
  • Repeated GUI OpenCV edges are scoped to paddleocr and albumentations.
  • Direct dependencies such as cryptography remain resolvable for other agents and model servers.
  • uv >= 0.11.25 is required because package-scoped exclusions need that resolver behavior; both test workflows install the same minimum version.
  • OSWorld is installed from immutable revision dc23424e9f6316b181bde149e0dc9bc3c5ff78c9, with the compatible headless OpenCV wheel declared explicitly.

This keeps the root policy composable for every managed server instead of special-casing OSWorld around a global exclusion.

User-facing workflow

benchmarks/osworld/prepare.py accepts --execution-backend gym_opensandbox, validates that --vm-path is absent, and renders the Pool reference from OPENSANDBOX_POOL_REF (default: osworld-kvm). The normal control and evaluation wrappers are unchanged.

The OSWorld README documents:

  • deployment roles and resource ownership for both backends;
  • model endpoint probing and run preparation;
  • OpenSandbox configuration through environment variables;
  • normal shutdown and exact-run recovery cleanup;
  • direct, proxy-required, Chrome CDP, and VLC endpoint behavior.

Validation

All validation below applies to the PR patch now published as commit 435ba3bc38373c3b04ccd685f565b89b7066920a on Linux/x86_64 unless noted otherwise. Live artifacts were initially recorded at 949a5e5963d241d4179712070fe954d6f7f468d5; adding missing DCO trailers and rebasing onto current main changed only commit metadata and the unrelated base. The complete git diff --binary main...HEAD remained byte-for-byte identical, with SHA-256 49e8c919c5c39f4054d8d45398d7ba2c11142eb6344d74d65d26ce4206b6cb6d.

Dependency and test gates

  • Fresh root environment with uv 0.11.25: 178 packages installed.
  • Relevant root, Sandbox, OSWorld agent, and benchmark suite: 2041 passed, 4 skipped, 5 subtests passed.
  • Fresh OSWorld managed-server environment from requirements.txt: 336 packages installed; 157 agent tests + 32 benchmark tests = 189 passed.
  • Fresh vLLM managed-server environment: 175 packages installed; cryptography 49.0.0 resolved without a compatibility cap; 110 passed.
  • Pre-commit hooks passed for every changed file.

Live Nano Omni + OSWorld smoke matrix

Backend Scenario Task ID Reward
Gym local Docker + KVM Direct Chrome 2ad9387a-65d8-4e33-ad5b-7580065a27ca 1.0
Gym local Docker + KVM Proxy-required Chrome 06fe7178-4491-4589-810f-2e2bc9502122 1.0
Gym local Docker + KVM VLC 59f21cfb-0120-4326-b255-a5b827b38967 1.0
OpenSandbox KVM Pool Direct Chrome 2ad9387a-65d8-4e33-ad5b-7580065a27ca 1.0
OpenSandbox KVM Pool Proxy-required Chrome 06fe7178-4491-4589-810f-2e2bc9502122 1.0
OpenSandbox KVM Pool VLC 59f21cfb-0120-4326-b255-a5b827b38967 1.0

The Docker route used a real qcow2 VM with KVM acceleration (accel=kvm, -enable-kvm), not a mocked lifecycle. The first independent Docker/proxy sample completed end-to-end but scored 0.0; a second sample scored 1.0, consistent with model sampling variance rather than a transport or evaluator failure.

No runtime source overlay or PYTHONPATH patch was used. The installed pinned OSWorld package supplied the VLC gateway-auth fallback directly.

Cleanup gate

  • No labeled Docker container remained after the run.
  • Post-run OpenSandbox audits for direct Chrome, proxy-required Chrome, and VLC each returned zero matching instances.

Scope

This branch is based on current main after #1408. It intentionally excludes developer-local ignore rules, local experiment harnesses and paths, .pre-commit-config.yaml changes, and unrelated vLLM component metadata changes.

@JeffPengCoder
JeffPengCoder requested a review from a team as a code owner August 4, 2026 14:18
@copy-pr-bot

copy-pr-bot Bot commented Aug 4, 2026

Copy link
Copy Markdown

This pull request requires additional validation before any workflows can run on NVIDIA's runners.

Pull request vetters can view their responsibilities here.

Contributors can view more details about this message here.

@JeffPengCoder
JeffPengCoder force-pushed the feature/sandbox branch 2 times, most recently from dc38304 to 435ba3b Compare August 4, 2026 14:31
@github-actions github-actions Bot added the sla:triage-overdue Review assignment is over the one-business-day SLA label Aug 5, 2026
@ritaneves
ritaneves requested a review from Glorf August 7, 2026 15:08
@github-actions github-actions Bot added sla:review-overdue Review response is over the one-business-day SLA and removed sla:triage-overdue Review assignment is over the one-business-day SLA labels Aug 7, 2026
@hemildesai

Copy link
Copy Markdown
Contributor

/claude review

@claude

claude Bot commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

SHIP — no reliability concerns that block merge.

Reviewed the full diff. This adds a gym_opensandbox execution backend for OSWorld (server-managed KVM Pool, image-less create, path-based gateway forwarders) and hardens the sync sandbox loop against cancellation. It touches no verify(), scorer, or reward-aggregation code, so there's no silent-score-corruption surface here.

What I checked and found sound:

  • Async correctness. The _AsyncLoopRunner.run() change (nemo_gym/sandbox/api.py:263) is a real fix, not a regression: it now drains the cancelled provider coroutine via a completion Event before returning, so Ctrl-C / timeout no longer leaves a create running invisibly on the sync loop and leaking a remote sandbox. The except Exceptionexcept BaseException widenings in the create paths (provider.py, api.py:AsyncSandbox) correctly route CancelledError/KeyboardInterrupt through cleanup. REST calls go through a dedicated aiohttp.ClientSession (_rest_request, trust_env=False), not httpx — consistent with the aiohttp convention.
  • Cleanup on the interrupted-create path. _reap_pooled_create_marker scans by a per-create UUID marker and deletes only the exact match, with page (100) and deadline caps and best-effort logging. _cleanup_failed_pooled_create and the cleanup_opensandbox_run.py reaper both re-check returned metadata client-side before terminating, and the reaper is read-only without --reap. Good blast-radius discipline.
  • Config conventions. Defaults live in the exemplar YAML (osworld_opensandbox.yaml, osworld_agent.yaml) and prepare.py emits them; the API key stays in env vars and is asserted absent from env.yaml. --vm-path is rejected for gym_opensandbox at both arg-parse and write_env layers.
  • Dependency hygiene. The move from a global exclude-dependencies string list to package-scoped [[tool.uv.exclude-dependencies]] blocks (gated on uv>=0.11.25, with test_dependency_policy.py asserting the CI pins satisfy the floor and that cryptography is no longer globally dropped) is the right fix for the "server venvs silently lose direct deps" foot-gun called out in the old comment. The agent's pyproject.tomlrequirements.txt swap is consistent with setup_command.py's handling.

One NOTE (author's call, not blocking): _PooledRestSandbox (provider.py:75) intentionally implements only get_endpoint/get_info/kill/close — no .commands/.files. The OSWorld adapter never calls exec/upload_file, and the Pool config sets probe.command: null so _verify_created_handle short-circuits, so this is unexercised today. But if a future non-OSWorld env points a Pool-mode OpenSandbox provider at provider.exec() or upload_file(), it'll surface as a bare AttributeError on handle.raw.commands rather than a clear "pooled handle does not support exec" message. Cheap to guard if you expect Pool mode to spread beyond OSWorld.

hemildesai

This comment was marked as outdated.

@hemildesai hemildesai left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Recommended design: keep OpenSandboxProvider SDK-only. OSWorld supplies a non-empty compatibility image (SDK validation only), poolRef, skip_health_check=True, and no execd probe. The returned SDK Sandbox handles connect, status, endpoint lookup, and destroy. After rebasing onto main, the provider should only need a small endpoint() adapter (including server-proxy headers) and trailing-slash normalization; delete the custom REST client, facade, marker reaper, and duplicate lifecycle path.

Minimal E2E using Sandbox.create directly:

import asyncio
import os
from datetime import timedelta
from urllib.parse import urlsplit

import aiohttp
from opensandbox import Sandbox
from opensandbox.config.connection import ConnectionConfig


async def main() -> None:
    domain = os.environ["OPENSANDBOX_BASE_URL"].rstrip("/")
    api_key = os.environ["OPENSANDBOX_API_KEY"]
    connection = ConnectionConfig(
        domain=domain,
        api_key=api_key,
        headers={"OPEN-SANDBOX-API-KEY": api_key},
        use_server_proxy=True,
    )
    sandbox = await Sandbox.create(
        image="busybox:1.36",  # SDK validation only; the Pool supplies the OSWorld VM.
        extensions={"poolRef": "osworld-kvm"},
        timeout=timedelta(minutes=10),
        connection_config=connection,
        skip_health_check=True,  # The Pool VM has no execd.
    )
    try:
        endpoint = await sandbox.get_endpoint(5000)
        url = str(endpoint.endpoint)
        if "://" not in url:
            url = f"{urlsplit(domain).scheme or 'http'}://{url}"
        headers = dict(endpoint.headers or {})
        headers.setdefault("OPEN-SANDBOX-API-KEY", api_key)
        async with aiohttp.ClientSession(headers=headers) as session:
            async with session.get(f"{url.rstrip('/')}/screenshot") as response:
                response.raise_for_status()
                print(f"OSWorld screenshot endpoint is ready: {response.status}")
    finally:
        await sandbox.destroy()


asyncio.run(main())

Comment thread nemo_gym/sandbox/providers/opensandbox/provider.py Outdated
Comment thread nemo_gym/sandbox/providers/opensandbox/provider.py Outdated
Comment thread nemo_gym/sandbox/providers/opensandbox/provider.py Outdated
Comment thread nemo_gym/sandbox/providers/opensandbox/provider.py Outdated
@JeffPengCoder

Copy link
Copy Markdown
Contributor Author

Recommended design: keep OpenSandboxProvider SDK-only. OSWorld supplies a non-empty compatibility image (SDK validation only), poolRef, skip_health_check=True, and no execd probe. The returned SDK Sandbox handles connect, status, endpoint lookup, and destroy. After rebasing onto main, the provider should only need a small endpoint() adapter (including server-proxy headers) and trailing-slash normalization; delete the custom REST client, facade, marker reaper, and duplicate lifecycle path.

Thanks @hemildesai — this is very helpful, especially the validation against the actual osworld-kvm Pool.

The custom REST path was originally introduced for two reasons: OpenSandbox SDK 0.1.15 rejects an image-less Sandbox.create(), while the Pool supplies the actual OSWorld VM image; and we wanted to cover
interrupted creates where the server may have accepted the request before the client received the sandbox ID.

Given that a non-empty compatibility image is accepted and ignored by the Pool, and that skip_health_check=True works for the Pool VM without execd, I agree that keeping OpenSandboxProvider SDK-only is the cleaner design.

I’ll rebase onto the current main implementation and:

  • supply a documented compatibility image from the OSWorld config;
  • keep poolRef, skip_health_check=True, and no execd probe;
  • remove _PooledRestSandbox, _rest_request, the custom Pool create/delete path, and the marker reaper;
  • retain only the small endpoint adapter needed for server-proxy URLs and headers;
  • normalize the domain with rstrip("/") before constructing ConnectionConfig;
  • validate create, connect, status, endpoint lookup, cleanup, timeout, and cancellation against Cell-2.

One small SDK API detail: in 0.1.15 the lifecycle methods are kill() followed by close() rather than destroy(), so I’ll use that pair in the implementation and tests.

I’ll also verify that run-attribution/epilogue cleanup covers the narrow case where a create is accepted server-side but the response never reaches the client. If that is not covered, I’ll treat it as an SDK/server lifecycle issue rather than retain a duplicate REST implementation in Gym.

@JeffPengCoder
JeffPengCoder requested review from a team as code owners August 12, 2026 07:06
@JeffPengCoder
JeffPengCoder force-pushed the feature/sandbox branch 2 times, most recently from ee63cb7 to 4380cfd Compare August 12, 2026 07:53
Comment thread pyproject.toml
kajalj22
kajalj22 previously approved these changes Aug 12, 2026

@hemildesai hemildesai left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The SDK-only Pool direction is much simpler. I found three remaining places where the diff can stay smaller and keep cleanup and endpoint ownership explicit.

Comment thread nemo_gym/sandbox/api.py Outdated
Comment thread nemo_gym/sandbox/providers/opensandbox/provider.py Outdated
Comment thread nemo_gym/sandbox/providers/opensandbox/provider.py Outdated
Comment thread nemo_gym/sandbox/providers/opensandbox/provider.py Outdated
Signed-off-by: Jeff Peng <jepeng@nvidia.com>
Signed-off-by: Jeff Peng <jepeng@nvidia.com>
Remove the duplicate image-less REST lifecycle and allocate OSWorld Pool VMs through the SDK compatibility-image path. Keep excluded runtime dependencies scoped to the agent with overrides and an explicit opt-in installer.

Signed-off-by: Jeff Peng <jepeng@nvidia.com>
Signed-off-by: Jeff Peng <jepeng@nvidia.com>
Signed-off-by: Jeff Peng <jepeng@nvidia.com>
Signed-off-by: Jeff Peng <jepeng@nvidia.com>
Signed-off-by: Jeff Peng <jepeng@nvidia.com>
Signed-off-by: Jeff Peng <jepeng@nvidia.com>
Keep generic sync Sandbox cancellation behavior unchanged, scope health-check bypass to OSWorld Pool specs, and centralize endpoint scheme and header policy in the OpenSandbox provider.

Signed-off-by: Jeff Peng <jepeng@nvidia.com>
Signed-off-by: Jeff Peng <jepeng@nvidia.com>
@ko3n1g
ko3n1g enabled auto-merge (squash) August 13, 2026 15:58
@ko3n1g
ko3n1g merged commit 4558578 into NVIDIA-NeMo:main Aug 13, 2026
15 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

sla:review-overdue Review response is over the one-business-day SLA

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants