Skip to content

Add sandbox providers for CoreWeave and Wandb - #890

Open
matthoare117-wandb wants to merge 11 commits into
openenv:mainfrom
matthoare117-wandb:hoare/cw-wandb-sandbox-providers
Open

matthoare117-wandb wants to merge 11 commits into
openenv:mainfrom
matthoare117-wandb:hoare/cw-wandb-sandbox-providers

Conversation

@matthoare117-wandb

@matthoare117-wandb matthoare117-wandb commented Jun 30, 2026 •

Copy link
Copy Markdown

Summary

Adds OpenEnv support for running environments in both CoreWeave Sandboxes and W&B Serverless Sandboxes.

Includes optional cwsandbox and wandb extras, a W&B sandbox echo example, and unit tests for the new sandbox providers.

Type of Change

  • Bug fix
  • New feature
  • Breaking change
  • Documentation
  • New environment
  • Refactoring

Alignment Checklist

Before submitting, verify:

  • I have read .claude/docs/PRINCIPLES.md and this PR aligns with our principles

  • I have checked .claude/docs/INVARIANTS.md and no invariants are violated

  • I have run /pre-submit-pr (or bash .claude/hooks/lint.sh and tests) and addressed all issues

    Note: /pre-submit-pr was run via its underlying checks. The only remaining repo-wide formatting failures (uv run usort check src/ tests/ and uv run ruff format src/ tests/ envs/ --check) reproduce on origin/main in a separate worktree, so they are pre-existing unrelated formatting drift and not new issues introduced by this PR.

RFC Status

  • Not required (adds optional runtime provider integrations; no core API contract changes)
  • RFC exists: #___
  • RFC needed (will create before merge)

Test Plan

  • Provider unit tests - 26 passed
  • Full non-integration test suite - 1398 passed, 92 skipped, 36 deselected
  • Lint/debug checks passed for this PR’s changed files
  • Manual sandbox smoke suite passed for Hosted Echo, Echo/CodingEnv via CWSandbox, and Echo/CodingEnv via W&B Sandbox

Claude Code Review

N/A


Note

Medium Risk
New remote runtime paths depend on correct HTTPS ingress and sandbox auth; public ingress is explicit, but misconfiguration could expose workloads or leak secrets if surface_server_logs is enabled.

Overview
Adds CoreWeave Sandbox and W&B Serverless Sandbox runtime providers so OpenEnv can provision remote sandboxes, launch the environment server inside the container, and connect via GenericEnvClient.

CWSandboxProvider drives the cwsandbox SDK lifecycle (run → wait → validate URL → exec server start → /health readiness). WandbSandboxProvider reuses that behavior through wandb.sandbox with W&B auth and optional integration metadata. New optional cwsandbox and wandb extras in pyproject.toml, plus docs on explicit ingress_mode, HTTPS-only service_address, and that bare SDK addresses / url_scheme do not imply TLS.

Startup and readiness are fail-closed: missing image or ingress, non-HTTPS addresses, or startup errors trigger cleanup (stop/delete per policy) before clients connect; server crash logs stay hidden by default with best-effort redaction of injected env secrets. A conditional examples/wandb_sandbox_echo_env.py and broad mocked unit tests cover provisioning, GenericEnvClient wiring, and cleanup/ownership edge cases.

Reviewed by Cursor Bugbot for commit 6716be6. Bugbot is set up for automated code reviews on this repo. Configure here.

@matthoare117-wandb matthoare117-wandb changed the title Hoare/cw wandb sandbox providers Add sandbox providers for CoreWeave and Wandb Jun 30, 2026
@matthoare117-wandb
matthoare117-wandb marked this pull request as ready for review June 30, 2026 16:26
@burtenshaw

Copy link
Copy Markdown
Collaborator

Cool pull request. Thanks. Reviewing now.

@sergiopaniego sergiopaniego left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Thanks for the PR! Some comments from a first pass:

Transport security (S1)

Both ModalProvider and ACASandboxProvider enforce https/wss transport via _require_secure_url() (RFC 002 invariant S1), because EnvClient derives the WebSocket URL from base_url (http:// becomes cleartext ws:// in convert_to_ws_url). CWSandboxProvider defaults to url_scheme="http" and returns http://{service_address} when the address has no scheme. Combined with ingress_mode="public" by default, client traffic can travel in cleartext against a public endpoint. EnvClient does not block this on its own (it only skips the proxy for localhost, no cleartext warning for remote hosts). Could we add the same _require_secure_url guard (or force/validate https) before returning the URL?

Public ingress opt-in (S2)

ACASandboxProvider requires an explicit anonymous_port=True (RFC 002 invariant S2, "public exposure is never implicit"). Here ingress_mode="public" is the default with no explicit opt-in or warning. Could we require an explicit opt-in (or at least warn) to match the ACA pattern?

Docs

The provider table and "Per-provider setup" section in docs/source/guides/runtime-providers.md, plus the container-providers section in docs/source/reference/core.md, are not updated, so these two providers are currently undocumented. Adding table rows and setup blocks for CWSandbox and W&B (with a "Full example" link, like Daytona/Modal have) would keep the providers doc complete.

Examples

The PR adds an example for W&B (the subclass) but not for CWSandboxProvider, which is the base provider holding all the logic. It would be good to add a CWSandbox example (or generalize the current one to cover both). A couple of style nits to align the W&B example with modal_echo_env.py:

  • The try/except Exception: provider.stop_container(); raise around from_docker_image plus a separate async with env mixes two cleanup styles. Modal uses the cleaner with Provider() as provider: start_container / wait_for_ready / stop pattern.
  • Prefer logging over print to match the other examples.
  • The example references registry.hf.space/openenv-echo-env:latest with a hardcoded SERVER_CMD, whereas Modal/Daytona build the image with Provider.image_from_dockerfile(...). A short note on where that image comes from would help.

RFC status

The PR marks RFC as "Not required", but Modal and ACA landed under the proposed "Cloud Sandbox Providers" amendment to RFC 002, which is where the S1/S2 invariants come from. A new cloud provider with public ingress should reference and satisfy those invariants rather than opt out (ties into the two points above).

Minor

  • wait_for_ready docstring says "respond to /health and /ws" but only polls /health.
  • The two new provider files and the two test files are missing the BSD copyright header that all other providers (and __init__.py) carry.
  • If S1/S2 get implemented, a test asserting the resulting URL is secure and that implicit public ingress is rejected would be worth adding.

@burtenshaw

Copy link
Copy Markdown
Collaborator

Adding one stack-compatibility note, and +1 to the S1/S2 feedback above.

  • [P1] The public/plain defaults should match the safer provider posture before merge: public ingress should be explicit, and public remote URLs should not fall back to cleartext http / ws. This is the same issue called out above under transport security and public ingress.
  • [P2] This provider should also line up with Provider-owned startup #880/Add HF sandbox provider #841's provider-owned startup contract. EnvClient(provider=...) starts a provider by calling provider.start_container() with no image, and HF Sandbox / Modal / Daytona handle that by accepting constructor-owned image / env_vars while keeping explicit start_container(image, ...). CWSandboxProvider still requires image, so CoreWeave/W&B will not compose cleanly once Provider-owned startup #880 lands. I would either add constructor image / env_vars support here or track a clear follow-up before stacking this with Add HF sandbox provider #841.

@burtenshaw burtenshaw added feature size: large Large pull request labels Jul 1, 2026
@matthoare117-wandb

Copy link
Copy Markdown
Author

🙏 Thanks so much for the thoughtful review. I really appreciate you taking the time, and I’ll address the feedback.

@burtenshaw

Copy link
Copy Markdown
Collaborator

Hey Matt, how's this one going?

@matthoare117-wandb

Copy link
Copy Markdown
Author

Hey! Thanks for following up. We’re currently implementing HTTPS endpoint support in CoreWeave Sandboxes so it’ll work with this integration. We’re hoping to have it ready soon, and once it lands we’ll update this PR. Looking forward to getting this landed!

@burtenshaw

Copy link
Copy Markdown
Collaborator

Fixed in ebad7d7. Retain the sandbox handle, base URL and redaction values when stop/delete fails, so close can retry and start_container cannot orphan the old sandbox by replacing it. Clear ownership after successful cleanup. Three failure/retry regressions failed before the fix; all 29 CoreWeave/W&B mocked-provider tests now pass. No live SDK/provider validation. Public ingress and HTTP defaults (S1/S2), startup contract and documentation still require review; this is not merge signoff.

@cursor cursor Bot 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.

Stale Bugbot comment from a previous run.

@cursor cursor Bot 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.

Cursor Bugbot has reviewed your changes using default effort and found 3 potential issues.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit f50057f. Configure here.

@burtenshaw burtenshaw left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

NEEDS AUTHOR CHANGES

Reviewed 2df6b809b5f91d5a6295d174952079b3161a157c. Pushed the mechanical URL normalization fix 6716be606e46904fcfe7e0a4de7ce79b333e75f2; the three blockers below remain at that head.

2df6b809 addressed the earlier default transport, ingress and image startup findings on the author's behalf:

  • src/openenv/core/containers/runtime/cwsandbox_provider.py:47 now declares url_scheme: str = "https"; line 53 rejects other schemes. At line 366, parsed.scheme != "https" rejects plaintext and bare service addresses before server commands run. Both providers reject HTTP, WS, bare host:port and credential-bearing addresses and clean up the sandbox. Synthetic HTTPS endpoints reach EnvClient as HTTPS and its connection function as wss://.../ws, through both provider-owned startup and from_docker_image.
  • At cwsandbox_provider.py:36, ingress_mode: str | None = None; line 142 rejects omitted/empty ingress before Sandbox.run. Public ingress requires an explicit ingress_mode="public". wandb_sandbox_provider.py:10 inherits this behavior. This resolves the implicit-public finding, but full S2/S4 capability hygiene and S6 lifetime requirements still have the failures below.
  • The startup-contract finding was concrete: EnvClient(provider=...) calls start_container() without an image. Constructor image at line 34 and fallback at line 136 now support that call for both providers. An explicit start image overrides it; missing images fail before provisioning. Constructor env_vars parity remains optional follow-up, not a startup blocker.
  • Cleanup regressions confirm ownership survives failed deletion and blocks replacement, while if stop_error is None or deleted: at line 239 releases ownership after successful deletion. Failed SDK wait, command discovery, command launch and client readiness clean up; failed cleanup retains the handle for retry.

One additional S1 case failed at 2df6b809: the accepted address HTTPS://sandbox.example.test:8000 reached the client unchanged and became ws://HTTPS://sandbox.example.test:8000/ws. Commit 6716be60 returns parsed.geturl() at cwsandbox_provider.py:377, normalizing the scheme. The regression at tests/test_core/test_cwsandbox_provider.py:212 failed before this fix and passes afterward for CoreWeave and W&B. The checked transport cases now remain HTTPS/WSS.

Bugbot reconciliation: “Public ingress is the silent default”, “Provider-owned startup is unsupported”, and “Stop error retains ownership after delete” are stale and were already resolved when this review started. The unresolved “Remote URLs default to cleartext HTTP” claim is also stale: the current default is HTTPS and insecure addresses are rejected. Its October 26 inline maintainer reply explicitly kept the hosted TLS integration question open. That question remains in blocker 1; it is separate from the fixed cleartext-default bug.

Blocking findings:

  1. [P1] Make a supported SDK and HTTPS endpoint path work. pyproject.toml:54 and pyproject.toml:57 admit the PyPI releases selected by a fresh uv sync --all-extras: cwsandbox 1.25.0 and wandb 0.31.0. For either provider, Provider(image="echo:stored", ingress_mode="public").start_container() fails at cwsandbox_provider.py:161 with TypeError: NetworkOptions.__init__() got an unexpected keyword argument 'ingress_mode', before provisioning. The real SDK has also replaced service_address with service_urls. The minimum releases, cwsandbox 0.23.3 and wandb 0.27.0, exist and satisfy the Python markers, but their documented bare-address path is deliberately rejected at line 366. Pinning to those minima alone does not establish a working hosted HTTPS deployment. The current SDK supplies HTTPS endpoint configuration through Service/Endpoint; this adapter does not request it. Smallest complete fix: map one supported SDK generation to an explicit HTTPS endpoint and its returned URL, constrain both extras to the verified compatible range, and add strict SDK contract coverage plus evidence that the exposed /ws upgrades successfully. Keep the insecure-address rejection. Sources: PyPI cwsandbox, PyPI W&B, current endpoint types, minimum service-address contract.
  2. [P1] Remove capability URLs and raw SDK errors from diagnostics. cwsandbox_provider.py:276 inserts the ingress URL into every readiness timeout; line 274 also includes raw request errors. Calling wait_for_ready("https://sensitive-capability.example.test", timeout_s=0) reproduces the URL disclosure for both providers. Authentication errors containing a synthetic token are interpolated at cwsandbox_provider.py:92 and wandb_sandbox_provider.py:31. A failed-start cleanup warning at cwsandbox_provider.py:205 logs the raw exception traceback through exc_info=cleanup_exc. Tests reproduce all three disclosures for both providers. Smallest fix: use fixed or sanitized messages at these boundaries, omit raw exception chaining/tracebacks, and keep regression assertions on both exception text and captured logs. Default server-output suppression already passes; it does not cover these paths.
  3. [P2] Require or set a finite sandbox lifetime. cwsandbox_provider.py:40 defaults max_lifetime_seconds to None, and line 184 removes that argument from Sandbox.run. Both providers' default calls omit it. Constructing the real minimum SDK with the captured kwargs leaves its effective lifetime None; W&B 0.27.0 re-exports the same SDK. The configured request timeout does not impose a sandbox lifetime. This leaves time boxing to an unspecified runner policy. Smallest fix: require a finite positive max_lifetime_seconds before provisioning, or supply and document a finite default. Sources: minimum SDK lifetime defaults, W&B minimum SDK wrapper.

Documentation was a concrete request for provider table rows, per-provider install/auth/setup blocks, API-reference entries and an example link. docs/source/guides/runtime-providers.md:322 now explains image ownership, explicit ingress and the unsupported bare-address path; examples/wandb_sandbox_echo_env.py:1 now states its TLS prerequisite. The table at guide line 56, setup section at line 159 and reference at docs/source/reference/core.md:273 still omit these providers. Completing those entries and a CoreWeave/shared example is a nonblocking suggestion. Also correct the /ws readiness claim at cwsandbox_provider.py:252, document URL lifetime/reconnect behavior and the egress_mode="internet" posture, and state the required runner resource/IMDS restrictions. No live metadata access or runner-policy violation was demonstrated, so those remain suggestions. The proposed RFC 002 amendment covers these providers; a separate protocol RFC is not needed for the existing adapter contract.

Validation:

  • Provider suites plus tests/test_core/test_client_provider_cleanup.py: 131 passed, 4 skipped at the original head; 132 passed, 4 skipped after the pushed regression/fix. Changed-file usort, Ruff formatting, Ruff lint and git diff --check pass.
  • Additional offline audit: 32 passed and 10 intentional failing requirement tests covering the three blockers for both providers. The audit tests are in the collapsed block below. Save them as tests/test_core/test_pr890_audit.py and run PYTHONPATH=src:envs uv run pytest tests/test_core/test_pr890_audit.py -q --tb=short. No live sandbox was provisioned.
  • Both provider modules import with SDK imports blocked. Construction without the optional SDK raises the existing actionable installation/auth error. Core installation remains free of these optional SDK imports. The existing provider tests are offline, but their permissive fakes missed the installed SDK incompatibility.
  • PYTHONPATH=src:envs uv run pytest tests/core -q: 873 passed, 9 skipped, 5 failed in unchanged harness-process tests with macOS os.killpg(..., 0) PermissionError. Three of those failures reproduce at merge-base f26cb132; the other two pass in isolation. They are not provider findings. Repository Ruff lint passes; repository formatting checks flag unchanged files, including the two known usort failures.
  • The original head's recorded CI checks passed, with Bugbot neutral. The PR is behind current main.
Audit tests (tests/test_core/test_pr890_audit.py)
"""Offline PR 890 review checks. Synthetic credentials only; never provision."""

import asyncio
import builtins
import importlib
import json
import logging
import sys
import traceback
from pathlib import Path
from types import SimpleNamespace
from unittest.mock import AsyncMock, patch

import pytest
import requests

sys.path.insert(0, str(Path.cwd() / 'tests/test_core'))
import test_cwsandbox_provider as cw_tests
import test_wandb_sandbox_provider as wb_tests

from openenv.core.containers.runtime.cwsandbox_provider import CWSandboxProvider
from openenv.core.containers.runtime.wandb_sandbox_provider import WandbSandboxProvider
from openenv.core.generic_client import GenericEnvClient


@pytest.fixture(params=['coreweave', 'wandb'])
def provider_case(request):
    if request.param == 'coreweave':
        return CWSandboxProvider, cw_tests.fake_sdk.__wrapped__()
    return WandbSandboxProvider, wb_tests.fake_wandb_sdk.__wrapped__()


@pytest.mark.parametrize('mode', ['omitted', None, ''])
def test_ingress_opt_in_precedes_provisioning(provider_case, mode):
    provider_cls, sdk = provider_case
    kwargs = {} if mode == 'omitted' else {'ingress_mode': mode}
    provider = provider_cls(image='echo:stored', sdk=sdk, **kwargs)
    with pytest.raises(ValueError, match='explicit ingress_mode'):
        provider.start_container()
    assert sdk.Sandbox.run_count == 0


@pytest.mark.parametrize('scheme', ['http', 'ws', 'wss'])
def test_constructor_rejects_insecure_or_unsupported_scheme(provider_case, scheme):
    provider_cls, sdk = provider_case
    with pytest.raises(ValueError, match='HTTPS'):
        provider_cls(sdk=sdk, url_scheme=scheme)
    assert sdk.Sandbox.run_count == 0


@pytest.mark.parametrize('address', [
    'http://sandbox.example.test:8000',
    'ws://sandbox.example.test:8000',
    'sandbox.example.test:443',
    'https://user:dummy-secret@sandbox.example.test',
])
def test_unacceptable_endpoint_is_cleaned_before_commands(provider_case, address):
    provider_cls, sdk = provider_case
    sdk.Sandbox.instance.service_address = address
    provider = provider_cls(sdk=sdk, ingress_mode='public')
    with pytest.raises(RuntimeError, match='HTTPS service_address') as error:
        provider.start_container('echo:latest')
    assert address not in str(error.value)
    assert sdk.Sandbox.instance.stopped
    assert len(sdk.Sandbox.deleted) == 1
    assert provider.sandbox_id is None
    assert not sdk.Sandbox.instance.exec_calls


@pytest.mark.parametrize('flow', ['provider_owned', 'from_docker_image'])
def test_envclient_receives_https_and_opens_wss(provider_case, flow):
    provider_cls, sdk = provider_case
    provider = provider_cls(sdk=sdk, image='echo:stored', ingress_mode='public')
    address = sdk.Sandbox.instance.service_address
    with patch.object(provider, 'wait_for_ready'):
        if flow == 'provider_owned':
            client = GenericEnvClient(provider=provider)
        else:
            client = GenericEnvClient.from_docker_image(
                'echo:explicit', provider=provider
            )._consume()
        ws = SimpleNamespace(state=None, send=AsyncMock(), close=AsyncMock())
        with patch('openenv.core.env_client.ws_connect', new_callable=AsyncMock,
                   return_value=ws) as connect:
            async def exercise():
                await client.connect()
                assert client.base_url == address
                assert connect.call_args.args[0] == address.replace('https://', 'wss://', 1) + '/ws'
                assert sdk.Sandbox.last_run_kwargs['container_image'] == (
                    'echo:stored' if flow == 'provider_owned' else 'echo:explicit'
                )
                await client.close()
            asyncio.run(exercise())
    assert sdk.Sandbox.instance.stopped
    assert len(sdk.Sandbox.deleted) == 1


@pytest.mark.parametrize('failure', ['wait', 'discovery', 'exec', 'readiness'])
def test_client_failed_start_releases_sandbox(provider_case, failure):
    provider_cls, sdk = provider_case
    provider = provider_cls(sdk=sdk, image='echo:stored', ingress_mode='public')
    client = GenericEnvClient(provider=provider)
    owner, name = {
        'wait': (sdk.Sandbox.instance, 'wait'),
        'discovery': (provider, '_discover_server_cmd'),
        'exec': (provider, '_start_server'),
        'readiness': (provider, 'wait_for_ready'),
    }[failure]
    with patch.object(owner, name, side_effect=ValueError('synthetic startup failure')):
        async def exercise():
            with pytest.raises(ValueError, match='synthetic startup failure'):
                await client.connect()
        asyncio.run(exercise())
    assert sdk.Sandbox.instance.stopped
    assert len(sdk.Sandbox.deleted) == 1
    assert provider.sandbox_id is None


# These tests express requirements the current head violates.

def test_readiness_errors_do_not_expose_ingress_url(provider_case):
    provider_cls, sdk = provider_case
    provider = provider_cls(sdk=sdk, ingress_mode='public')
    address = 'https://sensitive-capability.example.test'
    with patch('openenv.core.containers.runtime.cwsandbox_provider.time.time',
               side_effect=[0, 1]):
        with pytest.raises(TimeoutError) as error:
            provider.wait_for_ready(address, timeout_s=0)
    assert address not in str(error.value)


def test_auth_errors_do_not_expose_tokens_or_urls(provider_case):
    provider_cls, sdk = provider_case
    marker = 'DUMMY_REVIEW_TOKEN https://sensitive-capability.example.test'
    sdk.Sandbox.list_result = cw_tests._FakeRef(sdk.CWSandboxAuthenticationError(marker))
    with pytest.raises(SystemExit) as error:
        provider_cls.preflight(sdk=sdk)
    assert marker not in str(error.value)
    assert marker not in ''.join(traceback.format_exception(error.value))


def test_cleanup_warning_does_not_expose_sdk_error(provider_case, caplog):
    provider_cls, sdk = provider_case
    provider = provider_cls(sdk=sdk, ingress_mode='public')
    marker = 'DUMMY_REVIEW_TOKEN https://sensitive-capability.example.test'
    with (
        patch.object(sdk.Sandbox.instance, 'wait', side_effect=ValueError('startup failed')),
        patch.object(provider, 'stop_container', side_effect=RuntimeError(marker)),
        caplog.at_level(logging.WARNING),
        pytest.raises(ValueError, match='startup failed'),
    ):
        provider.start_container('echo:latest')
    assert marker not in caplog.text


def test_a_finite_positive_lifetime_is_sent_by_default(provider_case):
    provider_cls, sdk = provider_case
    provider = provider_cls(sdk=sdk, ingress_mode='public')
    try:
        provider.start_container('echo:latest')
        lifetime = sdk.Sandbox.last_run_kwargs.get('max_lifetime_seconds')
        assert lifetime is not None and 0 < lifetime < float('inf')
    finally:
        provider.close()


def test_installed_sdk_network_contract_is_compatible(provider_case):
    provider_cls, _ = provider_case
    module_name = 'cwsandbox' if provider_cls is CWSandboxProvider else 'wandb.sandbox'
    sdk = importlib.import_module(module_name)
    with patch.object(sdk.Sandbox, 'run', side_effect=RuntimeError('provisioning intercepted')) as run:
        with pytest.raises(RuntimeError, match='provisioning intercepted'):
            provider_cls(ingress_mode='public', image='echo:stored').start_container()
        assert run.call_count == 1

This branch has not been deployed

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

Labels

feature size: large Large pull request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants