feat(providers): add NovitaSandboxProvider for Novita AI sandboxes - #1191
Conversation
Runs an OpenEnv server in a Novita sandbox and returns an https base_url that EnvClient connects to over wss://, matching the ContainerProvider contract. start_container accepts either source form: a registry image reference (resolved and cached by the SDK), or a "template:<path>" reference from image_from_dockerfile, which builds a Novita template from a local Dockerfile. Novita's template parser rejects multi-stage build definitions, which is the layout every in-repo environment uses, so image_from_dockerfile rewrites the Dockerfile before handing it over: BuildKit --mount flags are stripped, ARG/--platform in FROM lines are resolved, and a two-stage build whose stages share one base image is replayed as a single stage (COPY --from=builder becomes an in-place cp). A Dockerfile that does not fit those rules raises with the registry route as the alternative. Two properties the provider pins that the image does not: - Runs as root. Novita's parser rewrites USER to a non-root "user" when the Dockerfile declares none, which cannot write the root-owned /app the server installs into -- observed as "Permission denied" on /app/resources and the task's git work tree. Providers that do not rewrite USER (Daytona) keep the image default and never hit this. - Pins the template start command to a keepalive, leaving port 8000 free for the server the provider launches itself. That launch writes a PID file, so wait_for_ready reports a crashed server immediately instead of waiting out the timeout. Secure by default: enforces https/wss transport and withholds captured sandbox output from raised errors unless surface_server_logs=True. The SDK is an optional extra (openenv[novita]) imported lazily; the provider talks to it through a private adapter, so the behavior tests inject a duck-typed fake and need no account, and a separate fake novita_sandbox module pins the real SDK surface. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…der-independent Two defects found in alignment review. The Dockerfile registry was snapshotted into an instance attribute in __init__, so a provider constructed before image_from_dockerfile() saw an empty registry and failed with a misleading "call image_from_dockerfile() first" error. The registry is class-level, so reading it through self works via the MRO -- which is what ModalProvider and DaytonaProvider already do. Drop the copy. Rejecting unknown start_container kwargs made the entire AutoEnv path unusable: AutoEnv.from_env() forwards wait_timeout (default 30.0) unconditionally through from_docker_image -> _bootstrap_container. The value is inert -- _bootstrap_container calls wait_for_ready(base_url) without a timeout, so no provider ever sees it. Accept and drop it, keeping the typo guard for genuinely unknown options. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Remove `allow_internet_access` from NovitaSandboxProvider. DaytonaProvider sets no network policy when calling its SDK, so OpenEnv does not configure egress for either provider -- the sandbox takes the vendor default. Passing `True` explicitly was redundant (it is the SDK default) and implied OpenEnv tunes network posture when it does not. Also wrap an over-long line in examples/novita_tbench2_simple.py, the one formatting failure introduced by the Novita provider commits. The remaining lint.sh failures are pre-existing and unrelated. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
cursor review |
There was a problem hiding this comment.
Stale comment
Release-manager review at head
69e910ff(Ben asked for cursor review).Keep out of the 0.5.0 cut (#1190). Optional
openenv[novita]surface, ~1.1k LOC provider + Dockerfile rewrite engine, and open RFC/security questions — not release cargo. 0.5.0 is already held for Harbor/run_batchauth and the version call.What looks good
- Matches the
ContainerProvidershape used by Daytona/Modal/ACA (https base URL → EnvClientwss://, lazy optional SDK, private adapter,surface_server_logsdefault-off).- No
server/imports — client/runtime boundary is clean.- Focused suite is strong: 87 tests passed locally (
PYTHONPATH=src:envs pytest tests/test_core/test_novita_provider.py); required CI lint/package/docs/locks jobs are green (tests were still finishing when I started).- Secret hygiene direction is right (env redaction, withheld sandbox output by default).
Tier 1 — please fix before merge
- Unquoted paths in Dockerfile flatten (
novita_provider.py~221):COPY --frombecomesRUN mkdir -p $(dirname {dst}) && cp -a {src} {dst}without shell quoting. DockerCOPYdoes not expand shell metacharacters; the rewrittenRUNdoes. Quote withshlex.quoteor reject unsafe paths.- Host URL construction (~899–900): always prefixes
https://. Ifget_hostever returns a scheme-bearing string you gethttps://https://…, which still passes_require_secure_url(startswith("https://")) and can point at the wrong endpoint. Require a bare host (reject embedded://).- Narrow
COPY --fromregex (~55): only matches--fromas the first flag with exactly two path tokens. Forms likeCOPY --chown=… --from=…fall through and can leave--from=in a “single-stage” template. Fail closed on unmatchedCOPY --from.- Misleading test (
test_novita_provider.py~404):test_plaintext_host_rejectedcurrently asserts https prepending succeeds — it does not reject plaintext. Add a real rejection case for scheme-bearing hosts.Tier 2 — alignment (needs a human call, not silent merge)
- RFC required. This adds a public core provider and Dockerfile-rewrite semantics. The module itself says the RFC 002 “Cloud Sandbox Providers” amendment is proposed / unratified, but the PR checklist says “RFC not required.” That checkbox is wrong — open or update the RFC before treating this as supported public surface. Suggested: @Darktex + RFC 002 authors.
- Forced
rootviabuilder.set_user("root"): documented Novita-parser workaround, but it overrides a Dockerfile that already declares a non-rootUSER. Prefer “set root only when Novita would inject defaultuser,” or accept this as an explicit security trade-off in the RFC.- Network posture left to SDK default (parity with Daytona). Still weaker than INVARIANTS “network access must be explicitly configured” for a new public provider — call it out in the RFC/docs.
- Auth model vs Daytona/Modal: Daytona defaults to signed preview; Modal treats the tunnel URL as bearer. Novita returns an account-scoped public
httpshost with no OpenEnv-level auth story for/ws— document whether the platform enforces that.Verdict
Request changes on the Tier 1 items + RFC checkbox. Not blocking Thursday’s delayed 0.5.0 either way — this stays outside the candidate until those land and someone signs the RFC/security trade-offs.
Sent by Cursor Automation: Release
| connects to over ``wss://``. | ||
|
|
||
| Note: the ``RFC 002 security invariant S<n>`` references in this module point to | ||
| the **proposed** "Cloud Sandbox Providers" amendment to RFC 002, which is pending |
There was a problem hiding this comment.
Tier 2 / RFC: This module already states the Cloud Sandbox Providers amendment is proposed/unratified, but the PR checklist marks “RFC not required.” Adding a public core provider + Dockerfile-rewrite engine needs that RFC (or an update to it) before this is treated as supported surface — please fix the checkbox and link the RFC.
There was a problem hiding this comment.
For the Dockerfile rewrite, I’d like to provide some background. image_from_dockerfile creates a Novita Template, similar to Daytona’s Snapshot flow. Since Novita does not currently support multi-stage Dockerfiles, I added a rewrite step to adapt them before building the Template.
I’d also like to confirm whether this change, together with the new Novita provider, requires a separate RFC.
@Darktex @thegovind
There was a problem hiding this comment.
Corrected release-manager review at exact head 69e910ff, after testing the real SDK rather than only the fake adapter.
Automated checks
- All substantive exact-head CI checks pass.
- 87/87 focused provider tests pass.
- Focused usort/Ruff format/check pass.
- The fake tests do not model the real failures below.
Tier 1 — fixes required
- The declared
novita-sandbox>=2.1.0floor is incompatible. In 2.1.0,Sandbox.createhas noimage/buildparameters; the advertised registry path passes them through**optsand raisesTypeError: ConnectionConfig.__init__() got an unexpected keyword argument 'image'. Require at least 2.1.1 and add a minimum-version contract test. - Registry images still fail on 2.1.1 before network access.
wait_for_timeout()returns aReadyCmd, but SDKoci_fingerprint()JSON-serializes thebuildmapping first, producingTypeError: Object of type ReadyCmd is not JSON serializable. Passready_cmd.get_cmd(). - Dockerfile builds discard explicit credentials and region. The adapter configures
self._novita, then bypasses it with staticTemplate.build(...); the real SDK createsConnectionConfig(**{}). Useself._novita.template.build(...)so its namespace mergesapi_key/domain. - The Dockerfile rewrite is not fail-closed or semantics-preserving as documented: it strips secret/SSH/bind mounts along with cache mounts; overlapping ARG names corrupt
FROM $BASE_IMAGE; unsupportedCOPY --fromforms survive after their stage is removed; generated shell paths are unquoted; and replaying the builder stage retains builder-only packages/files/secrets in the runtime filesystem. Narrow the supported grammar, reject everything else, and add real transform regressions—or remove the local-Dockerfile route in favor of prebuilt registry images. novita_tbench2_simple.pystarts readiness polling before its cleanuptry; a timeout leaks the sandbox until its hard lifetime expires.
Security/alignment gates
- The real SDK defaults omitted
allow_internet_accesstoTrue. That is unrestricted egress for untrusted code, not neutral omission, and conflicts with the repository invariant that network access be explicitly configured plus proposed RFC 002 S3. Make the policy explicit/configurable and default-deny unless the RFC owners approve a different posture. - The examples log the returned public URL even though proposed RFC 002 S2 treats it as a bearer capability that must never be logged.
wait_for_ready()checks only HTTP/healthwhile RFC 002 explicitly requires a WebSocket conformance check before claiming provider support.- Always forcing
rootoverrides an explicit non-root DockerfileUSER; this needs an accepted RFC/security decision. - The Cloud Sandbox Providers amendment is still proposed/unratified. Obtain RFC-author/security review before exposing this as supported public core API.
Correction to my earlier review: the nested-scheme host scenario is defensive hardening, not a demonstrated 2.1.1 mismatch—the real SDK’s get_host() contract returns a bare host. The mislabeled plaintext-host test is therefore not itself a merge blocker. The fail-closed COPY/path concerns remain valid correctness issues.
Verdict
Keep CHANGES_REQUESTED and keep this out of the 0.5.0 candidate. Re-review after real-SDK regressions, an explicit ingress/egress/log policy, a fail-closed build route, and RFC-owner sign-off.
Sent by Cursor Automation: Release
Quote flattened COPY paths before embedding them in shell RUN commands. Reject scheme-bearing sandbox
hosts before adding the HTTPS prefix, with regression tests for both cases.
Quote flattened COPY paths before embedding them in shell RUN commands. Reject scheme-bearing sandbox hosts before adding the HTTPS prefix, and preserve explicit Dockerfile USER directives.
Fail closed when Dockerfile COPY --from options cannot be flattened, including reordered flags and multiple sources. Add regression coverage for each unsupported form.
Require novita-sandbox 2.1.1 and serialize readiness commands for OCI image fingerprints. Build Dockerfile templates through the configured Novita namespace to retain credentials and region settings, with regression coverage.
Strip only explicit cache mounts during Dockerfile rewriting. Reject secret, ssh, bind, missing-type, and otherwise unsupported mounts instead of silently changing build semantics.
Run OCI image server commands through the SDK background API as root, use the same user for liveness probes, and tolerate the PID file startup race. Document explicit OCI startup commands, preserve template users, and avoid logging bearer URLs.
There was a problem hiding this comment.
@CeerDecy on your RFC question (Dockerfile rewrite + new Novita provider):
Verdict: RFC REQUIRED for the provider addition; the Dockerfile rewrite needs to be in that RFC (or a linked amendment), not a silent implementation detail.
Why (from rfc-check + RFC 002):
- New public surface under
src/openenv/core/(NovitaSandboxProvider) is a new provider abstraction — that is explicitly RFC-required territory. - RFC 002 already covers provider contracts (S3 egress / least privilege, readiness including WebSocket conformance, secret hygiene). Shipping Novita without an amendment leaves those gates unratified for this path (egress default, root user, health-only readiness, logging of URLs/credentials).
- A Dockerfile rewrite that flattens multi-stage builds, strips BuildKit mounts, and rewrites
ARG/COPY --fromis a new semantics-changing pattern, not a private helper. It needs documented invariants: fail-closed vs best-effort, which grammar is supported, and what is rejected.
Suggested path: open or extend an RFC 002 provider amendment (owners @Darktex / @thegovind) that covers:
- Novita as a first-class provider and its readiness/egress/user/logging contract.
- Whether local Dockerfile → Template builds are in scope at all; if yes, the rewrite must be fail-closed and semantics-preserving (or only prebuilt registry images).
Until that lands, please keep treating the Tier-1 SDK bugs from the earlier CHANGES_REQUESTED review as merge blockers independently of the RFC — the RFC does not waive those.
Sent by Cursor Automation: Release
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 3cb2b7e. Configure here.
| print("Command output:") | ||
| print(result.observation.output) | ||
| finally: | ||
| provider.stop_container() |
There was a problem hiding this comment.
Sandbox leaked on readiness failure
Medium Severity
wait_for_ready runs after start_container but outside the try/finally that calls stop_container. A crash or timeout during readiness never releases the sandbox, so it keeps running until Novita's hard lifetime expires. The other Novita examples in this PR put readiness inside that try.
Reviewed by Cursor Bugbot for commit 3cb2b7e. Configure here.
|
The docs for this PR live here. All of your documentation changes will be reflected on that endpoint. The docs are available until 30 days after the last update. |




Summary
Runs an OpenEnv server in a Novita sandbox and returns an https base_url that EnvClient connects to over wss://, matching the ContainerProvider contract.
start_container accepts either source form: a registry image reference (resolved and cached by the SDK), or a "template:" reference from image_from_dockerfile, which builds a Novita template from a local Dockerfile.
Novita's template parser rejects multi-stage build definitions, which is the layout every in-repo environment uses, so image_from_dockerfile rewrites the Dockerfile before handing it over: BuildKit --mount flags are stripped, ARG/--platform in FROM lines are resolved, and a two-stage build whose stages share one base image is replayed as a single stage (COPY --from=builder becomes an in-place cp). A Dockerfile that does not fit those rules raises with the registry route as the alternative.
Two properties the provider pins that the image does not:
Secure by default: enforces https/wss transport and withholds captured sandbox output from raised errors unless surface_server_logs=True.
The SDK is an optional extra (openenv[novita]) imported lazily; the provider talks to it through a private adapter, so the behavior tests inject a duck-typed fake and need no account, and a separate fake novita_sandbox module pins the real SDK surface.
Type of Change
Alignment Checklist
Before submitting, verify:
.claude/docs/PRINCIPLES.mdand this PR aligns with our principles.claude/docs/INVARIANTS.mdand no invariants are violated/pre-submit-pr(orbash .claude/hooks/lint.shand tests) and addressed all issuesThere are many lint warnings that this PR did not introduce.
RFC Status
Test Plan
Unit tests: 87 in
tests/test_core/test_novita_provider.py, no network or credentials needed.All SDK calls are faked, so the suite runs anywhere. It covers: source resolution (registry image vs image_from_dockerfile), port and lifecycle guards, command discovery and background launch, /health polling with early crash detection, https/secret-hygiene enforcement, the Dockerfile rewrite transforms (buildkit strips, ARG resolution, multi-stage flattening), and the real SDK's method names and kwargs.
Examples:
novita_echo_env.py— fastest check that the provider works end to end; echo is the smallest env (no task data, noexternal services, no model calls).
novita_tbench2_simple.py— boots a TB2 task in the sandbox and runs commands through it.novita_tbench2_e2e_eval.py— full loop: an LLM agent works a real TB2 task, then the environment's own verifier scoresit. Verified end to end against a live Novita sandbox (this is the one that exercised Template.build(), the flattened
multi-stage image, and the WebSocket session).
Claude Code Review
Flags Raised and Resolved
Seven flags came up across the review; none survived.
Five were withdrawn after verification — four of them because your end-to-end run had already proven what I claimed was
unverified:
(env_client.py:379-382, :558), which is exactly what the invariant demands.
both providers identically.
same pattern as Daytona's on_snapshot_create_logs, which is precisely S4's "withheld by default."
One it turned out I had no business raising:
imported_environment) the tests check the contract they depend on, not the script. The three daytona_* examples are
equally uncovered. Convention, not deviation.
And one you had already adjudicated:
OpenEnv images install into a root-owned /app, so every in-process command fails. The fix restores what a plain docker
run of the same image gives. Going least-privilege instead would mean injecting chown into the Dockerfile-transformation
code and maintaining it — a cost you weighed and declined. The reasoning is recorded in the code comment at
novita_provider.py:404-415, so the decision is closed.
Summary
Note
Medium Risk
New optional cloud provider touches remote sandbox lifecycle, Dockerfile rewriting, and transport/security invariants; behavior is heavily unit-tested but live Novita/API-key paths are integration-dependent.
Overview
Adds Novita AI as a cloud runtime via new
NovitaSandboxProvider, installable aspip install openenv[novita]. The provider implementsContainerProvider: it creates a Novita sandbox, launches the OpenEnv server on port 8000 (keepalive template + background start with PID-based crash detection), and returns an httpsbase_urlforEnvClient.image_from_dockerfilerewrites in-repo multi-stage Dockerfiles for Novita’s template parser (strip BuildKit cache mounts, resolveFROMARG/platform, flatten two-stage same-base builds); unsupported layouts fail with guidance to use a registry image instead.Security defaults align with other cloud providers: enforce HTTPS/WSS, redact/withhold sandbox logs unless
surface_server_logs=True, single active sandbox per provider instance, and cleanup on failed start.Docs list Novita in getting-started, runtime-providers, and core API autodoc. Examples: echo smoke test, minimal TB2 sandbox run, and full TB2 agent + verifier e2e eval. Tests: large
test_novita_provider.pysuite with injected fake adapter (no live Novita account).Reviewed by Cursor Bugbot for commit 3cb2b7e. Bugbot is set up for automated code reviews on this repo. Configure here.