Blog · 03 · 2026-10-10 · 4 min
OpenEnv on denv
Run OpenEnv environment images on denv with DenvProvider, fork them mid-episode, and export a denv version as an OpenEnv environment so every trainer that speaks OpenEnv can use it.
OpenEnv is the wire protocol for RL environments: reset, step, state over HTTP and WebSocket, tools over MCP, one Docker image per environment, distributed on Hugging Face Spaces. Its committee spans Meta, Hugging Face, Nvidia, Mercor, Fleet and Microsoft, and eight-plus trainers consume it directly, including TRL, SkyRL, torchforge and Unsloth. OpenEnv's own harbor environment wraps any Harbor dataset, which is how most coding tasks reach it.
Where an OpenEnv environment runs is a provider: LocalDockerProvider, DockerSwarmProvider, KubernetesProvider, or a hosted runtime. OpenEnv's RFC 002 leaves suspend, resume and snapshot "provider-local". denv is the provider that makes them real.
pip install "denv[openenv]"
export DENV_SERVER=https://api.denv.ai DENV_API_KEY=dak_…
Running an OpenEnv image on denv
from denv.openenv import DenvProvider
from my_env import MyEnv # an OpenEnv client class
env = MyEnv.from_docker_image("ghcr.io/you/my-env:v1", provider=DenvProvider())
obs = env.reset()
obs = env.step(action)
env.close()
DenvProvider implements OpenEnv's ContainerProvider:
- start_container(image, port, env_vars).
POST /v1/sandboxes {image, ports:[port], env}. The environment's HTTP port is published and the base URL returned. The provider waits for the environment to be healthy before returning it. - wait_for_ready(base_url). Polls the OpenEnv
/healthroute. - stop_container / close.
DELETE /v1/envs/{id}.
Published ports require egress: allow today, because a denied network is internal and cannot publish. DenvProvider(egress="allow") makes that explicit. When the ingress proxy ships, a denied environment can still be reached on its published port through denv, and deny becomes the default here too.
Forking an episode
This is the part no other provider has:
root = MyEnv.from_docker_image("ghcr.io/you/my-env:v1", provider=DenvProvider())
root.reset()
for a in prefix:
root.step(a)
branches = [root.provider.fork() for _ in range(8)]
fork() snapshots the environment's live state as a new version, then creates a new environment from it, and returns a provider bound to the child with its own base URL. Each branch continues from the same state; none shares memory with the root. For tree search, best-of-N from a common prefix, or counterfactual evaluation, this replaces replaying the prefix eight times.
What persists across the fork is what the environment keeps in its volumes (default /work) and in any declared service volumes. Process memory does not survive; the OpenEnv server restarts in the fork and must rebuild its in-memory state from disk on startup, which well-behaved OpenEnv environments already do for reset idempotence. The child's state() after fork should equal the parent's; the conformance suite checks this for the reference environments.
Multi-service OpenEnv environments
An OpenEnv image is one container, but an OpenEnv environment often needs a database or a browser next to it. On denv, put the OpenEnv server in a compose file with its dependencies and x-denv.agent.service pointing at it; deploy it as a project; then create sandboxes from the version:
env = MyEnv.from_denv_version("ver_01M4…", provider=DenvProvider())
from_denv_version is the one addition to the client surface, provided by denv.openenv. It forks the version and connects to the OpenEnv server's published port. The database, the queue and the seeded data are in the fork.
Exporting a denv version as an OpenEnv environment
denv export --openenv ops-world@ver_01… ./dist
Writes an OpenEnv environment package: a server that exposes reset, step, state and the declared MCP tools, and a Dockerfile whose base is the version's agent service image, pinned by digest. reset forks the version through denv rather than restarting a container, so an exported environment keeps denv's semantics when it runs under a trainer that only knows OpenEnv. Credentials are read from DENV_API_KEY in the trainer's environment.
For environments that are a Harbor dataset, prefer denv export --harbor and OpenEnv's own harbor wrapper; it already handles sixteen validated agents and token capture.
Token and logprob capture
OpenEnv's capture proxy returns prompt_token_ids, completion_token_ids and per-token logprobs from vLLM or SGLang so a trainer can do policy gradient on an agent's actual tokens. denv does not touch the model side. The proxy runs where the trainer runs; denv runs the environment. When the agent inside the environment calls a model, point it at the proxy as you would with any provider.
Conformance
make e2e-frameworks starts python -m http.server as a stand-in OpenEnv image through DenvProvider, waits for ready, and stops it. The committed conformance run adds OpenEnv's reference echo environment: reset, step, fork, compare state() across root and branch, close.
Status
Ships today. DenvProvider with start_container, wait_for_ready, stop_container, close; fork() returning a child provider; conformance with a stand-in image.
Committed by this post. egress: deny with published ports through the ingress proxy; from_denv_version; denv export --openenv; conformance against OpenEnv's reference environments with state() equality across fork; listing of denv as a provider in OpenEnv's documentation.