Environment Engine
A standalone, ROCK-based sandbox service: a REST admin server on 127.0.0.1:8080, Ray-scheduled GPU/CPU allocation, and Docker or Podman containers that execute agent actions for interactive RL.
1 Overview
The Environment Engine (env_engine/) is the execution backend for agentic rollouts — a self-contained service the training pipeline reaches only over a small HTTP API, sending a request and reading back a result rather than touching container logic or the scheduler directly.
It is built on ROCK (Reinforcement Open Construction Kit), a client–server framework that owns the container lifecycle. On the reference 8-GPU node (see Getting Started), the ROCK admin server runs co-located on 127.0.0.1:8080 and orchestrates one SWE-Bench container per task.
Decoupled service
Runs as an independent admin server, isolated from the trainer. Swap Docker for Podman, add nodes, or change task logic without editing a line of training code.
Ray-scheduled concurrency
Each sandbox is a Ray actor reserving CPU/GPU. The scheduler tracks capacity, queues when exhausted, and reclaims on completion — hundreds of concurrent tasks, jobs lasting days.
Docker and Podman
Containers are managed through a pluggable deployment backend, so the same task graph runs on cloud Docker hosts or rootless Podman without changes.
This page covers the engine architecture — layers, REST surface, and how rollouts consume it. For the per-container lifecycle (start → session → exec → stop, leases, failure recovery), see Sandbox Management.
2 Layered architecture
A request travels down four layers; the result returns up:
Every layer maps to a concrete module under env_engine/rock/:
| Layer | Module | Role |
|---|---|---|
| Client | rl_engine/rollout/sandbox/rock.pyrock/sdk/sandbox/client.py |
The rollout's RockSandbox wrapper and the ROCK Sandbox SDK — the only surfaces training code touches. |
| Admin | rock/admin/entrypoints/sandbox_api.pyrock/admin/main.py |
FastAPI router mounted at /apis/envs/sandbox/v1; validates requests and delegates to the SandboxManager. |
| Scheduler | rock/admin/core/ray_service.pyrock/deployments/ray.py |
RayService calls ray.init(resources=…); each sandbox is a Ray actor reserving actor_resource units, so Ray queues when capacity is full. |
| Sandbox mgr | rock/sandbox/sandbox_manager.pyrock/deployments/manager.py |
Owns start / session / exec / stop and container placement; DeploymentManager selects the Docker/Podman/Ray deployment. |
| Runtime | rock/deployments/docker.pyrock/rocklet/ |
Launches the container and starts the in-container rocklet agent (rocklet/server.py), which runs commands and streams back stdout / exit code. |
The rocklet
The rocklet is a lightweight execution agent inside each container (based on SWE-ReX, per env_engine/README_ROCK.md) that owns persistent bash sessions, file I/O, and process management.
3 The sandbox REST API
All sandbox verbs are registered on sandbox_router (rock/admin/entrypoints/sandbox_api.py) and mounted with a version prefix in rock/admin/main.py:
# rock/admin/main.py
app.include_router(sandbox_router, prefix="/apis/envs/sandbox/v1", tags=["sandbox"])
app.include_router(warmup_router, prefix="/apis/envs/sandbox/v1", tags=["warmup"])
app.include_router(gem_router, prefix="/apis/v1/envs/gem", tags=["gem"])
Every handler is wrapped by @handle_exceptions(…) and returns a RockResponse[T] envelope whose status field is "Success" or "Failed". The core lifecycle verbs:
| Method & path | Handler | Purpose |
|---|---|---|
POST /start | start | Launch a container from an image; returns a sandbox_id. |
POST /start_async | start_async | Non-blocking start for warmup / pre-provisioning. |
POST /create_session | create_session | Open a persistent bash session inside the container. |
POST /run_in_session | run | Run a command in a session (shell state persists); returns a BashObservation. |
POST /execute | execute | One-shot stateless command; returns a CommandResponse. |
POST /close_session | close_session | Tear down a single session. |
POST /read_file · /write_file · /upload | read_file… | File transfer in and out of the container. |
GET /is_alive · /get_status | is_alive | Health / liveness probe (see §8). |
GET /get_sandbox_statistics | get_sandbox_statistics | Per-sandbox resource / usage stats. |
POST /stop | close | Terminate the container and release its Ray slot. |
Two sibling routers share the same prefix: warmup_api.py pre-pulls task images so the first task avoids full cold-start, and sandbox_proxy_api.py routes each request to the node owning that sandbox. A separate GEM router (rock/admin/gem/api.py, /apis/v1/envs/gem) exposes the Gym-style make / reset / step protocol for turn-based environments like the Sokoban demo.
4 Python SDK
Callers use the async SDK in rock/sdk/sandbox/: Sandbox (in client.py) wraps every REST verb as a coroutine, and SandboxConfig (in config.py) declares the container spec. The canonical smoke test from env_engine/README_ROCK.md:
import asyncio
from rock.actions import CreateBashSessionRequest
from rock.sdk.sandbox.client import Sandbox
from rock.sdk.sandbox.config import SandboxConfig
async def run_sandbox():
cfg = SandboxConfig(image="python:3.11", memory="8g", cpus=2.0)
sb = Sandbox(cfg)
await sb.start() # POST /start
await sb.create_session(CreateBashSessionRequest(session="bash-1"))
r = await sb.arun(cmd="echo Hello Rock", session="bash-1") # /run_in_session
await sb.stop() # POST /stop
asyncio.run(run_sandbox())
Sandbox also exposes execute, run_nohup_and_wait, write_file/read_file, upload, commit, and close; SandboxGroup starts a pool of identical sandboxes with bounded start concurrency and retry. The config surface:
| Field | Default | Meaning |
|---|---|---|
image | "python:3.11" | Container image; per-instance override in SWE (see §5). |
memory | "8g" | Container memory limit. |
cpus | 2 | CPU limit. |
startup_timeout | ROCK_SANDBOX_STARTUP_TIMEOUT_SECONDS (180) | Max wait for the container to become ready. |
auto_clear_seconds | 14400 (4 h) | Idle lease; ROCK reclaims the container if untouched this long. |
route_key | None | Sticky key to pin related sandboxes to one node. |
cluster | "zb" | Target cluster label for placement. |
base_url | ROCK_BASE_URL (http://localhost:8080) | Admin server the client talks to. |
5 How training consumes it
The in-house SWE agent skips the high-level SDK: rl_engine/rollout/sandbox/rock.py defines a thin RockSandbox wrapper that POSTs to the same /apis/envs/sandbox/v1 endpoints with aiohttp — deliberately dependency-free ("ROCK API is NOT mandatory"), so the rollout never imports ROCK:
| RockSandbox method | REST call | Notes |
|---|---|---|
start(image, cpus, memory) | /start | Up to 10 retries with backoff; returns sandbox_id. |
execute_in_session(cmd, session) | /run_in_session | Persistent shell; scrubs prompt / broken-pipe artifacts from output. |
execute(cmd) | /execute | Stateless one-shot command. |
upload_dir(local, container) | /upload | Tars a host dir and extracts it inside the container. |
stop() · remove_image() | /stop · /remove_image | Teardown; remove_image reclaims host disk (logs a LEAKED IMAGE warning on failure). |
The SWE-Bench per-step flow
Task configuration lives in rl_engine/examples/swe_agent/environment/swe_sandbox.py (SWE_TOOL_CONFIGS). For each task the agent:
- resolves the per-instance image with
get_swe_bench_image(instance_id)— e.g.swebench/sweb.eval.x86_64.django_1776_django-16911:latest; - starts a container (
cpus=4,memory="16g") with the editor entrypoint bind-mounted read-only (SWE_ENTRYPOINT_LOCAL; mount details in Sandbox Management §5); - dispatches each tool call into a bash session (through
run_in_session) underconda activate testbed— file-edit actions runpython3 /home/swe_execute_sandbox.py --action … --params …; bash actions run the command directly.
One env.step round trip against an already-started sandbox; container start is a separate call (timing below).
Startup timing & image supply
Warm start
With the SWE-Bench image already in the local Docker cache, start() returns in ~15–25 s; later env.step RPCs are dominated by command runtime.
Cold start
The first time an instance is hit, ROCK pulls a 1–8 GB image from Docker Hub (several minutes); startup_timeout defaults to 300 s, overridable via STACX_SANDBOX_STARTUP_TIMEOUT.
Docker Hub auth
Anonymous pulls are rate-limited (100 / 6 h / IP) and a run may touch 200+ unique images, so docker login first (AGENTS.md); ROCK inherits the daemon login.
Offline tar-cache fallback
Before pulling from a registry, DockerDeployment checks DOCKER_IMAGE_CACHE_DIR (rock/deployments/docker.py); a matching tar is loaded with docker load instead, letting air-gapped or tmpfs-backed hosts re-hydrate images without network access.
6 Deployments & configuration
The bottom layer is a family of deployment backends under rock/deployments/, selected by DeploymentManager (manager.py) and sharing the AbstractDeployment contract (abstract.py):
| Module | Backend |
|---|---|
deployments/docker.py | DockerDeployment — local Docker (or Podman) container; hosts the tar-cache fallback. |
deployments/ray.py | RayDeployment(DockerDeployment) — wraps the Docker deployment as a Ray actor with resources={actor_resource: actor_resource_num} reservations. |
deployments/local.py | In-process / local runtime for tests and demos. |
deployments/remote.py | Attach to a container on a remote node. |
Podman is not a separate module — DockerDeployment shells out through a container provider (rock/utils/providers/) that abstracts over both runtimes, so the same task graph runs unchanged on either.
Configuration files
Runtime config lives in env_engine/rock-conf/: rock-local.yml drives single-node development, rock-test.yml targets the test suite:
# env_engine/rock-conf/rock-local.yml (excerpt)
ray:
runtime_env:
working_dir: ./
pip: ["apscheduler", "opentelemetry-api", "gem-llm", ...]
namespace: "rock-sandbox-local"
warmup:
images: ["python:3.11"]
runtime:
enable_auto_clear: true
The rock CLI
The rock command (rock/cli/main.py, subcommands under rock/cli/command/) manages the admin process via AdminCommand, with two actions:
# start the admin server (default 127.0.0.1:8080)
rock admin start
# graceful SIGTERM to running admin processes
rock admin stop
Install independently of the trainer: build the ROCK venv per env_engine/README_ROCK.md (uv-managed Python recommended), then rock admin start; a source/pip install also needs ROCK_WORKER_ENV_TYPE=pip. Launch scripts health-check this server before every run and abort if it is down (AGENTS.md).
7 Directory tour
| Path | Contents |
|---|---|
rock/actions/ | Action schemas — sandbox/ (bash, file, config requests/responses) and envs/ (GEM); CreateBashSessionRequest is re-exported from rock.actions. |
rock/admin/ | Admin server: main.py, entrypoints/ (sandbox / proxy / warmup APIs), core/ (ray_service.py, tables), gem/, metrics/, proto/. |
rock/sandbox/ | Sandbox abstraction: sandbox_manager.py, sandbox_actor.py, gem_manager.py, remote_sandbox.py, job/, service/. |
rock/rocklet/ | In-container agent: server.py, local_sandbox.py, local_api.py, __main__.py. |
rock/sdk/ | Python client: sandbox/ (Sandbox, SandboxConfig), envs/ (Gym registration), envhub/, builder/ (image builders). |
rock/deployments/ | Backends: docker.py, ray.py, local.py, remote.py, manager.py, abstract.py. |
rock/envhub/ | Environment registry service: server.py, api/, core/, database/. |
rock/cli/ | CLI: main.py, command/ (admin.py), loader.py, config.py. |
rock/utils/ | Shared: container providers/, docker.py, http.py, retry, DB, concurrency helpers. |
rock/config.py · rock/env_vars.py | Typed config model and the ROCK_* environment-variable registry. |
rock-conf/ | rock-local.yml, rock-test.yml. |
examples/ | sandbox_demo.py, sandbox_files_demo.py, sokoban_demo.py. |
tests/ | unit/ (admin, sandbox, rocklet, cli, utils) and integration/. |
pyproject.toml · LICENSE | Package metadata / dependencies; ROCK ships under its own Apache-2.0 license. |
8 Operations reference
Ports & health
| Endpoint | Default | Purpose |
|---|---|---|
| Admin REST | 127.0.0.1:8080 | Sandbox API (ROCK_BASE_URL); the trainer's ROCK_PORT. |
| EnvHub | 127.0.0.1:8081 | Environment registry (ROCK_ENVHUB_BASE_URL), SQLite-backed (ROCK_ENVHUB_DB_URL, default ~/.rock/rock_envs.db). |
| Health probe | GET /apis/envs/sandbox/v1/get_status?sandbox_id=… | Returns SandboxStatusResponse{is_alive, host_name}; is_alive() wraps it. |
Key environment variables
The full registry is rock/env_vars.py; the knobs that matter most in training:
| Variable | Default | Effect |
|---|---|---|
ROCK_BASE_URL | http://localhost:8080 | Admin server the SDK / client targets. |
ROCK_SANDBOX_STARTUP_TIMEOUT_SECONDS | 180 | SDK-level SandboxConfig.startup_timeout default. |
STACX_SANDBOX_STARTUP_TIMEOUT | 300 | SWE-agent start timeout (swe_sandbox.py); raise for slow networks. |
DOCKER_IMAGE_CACHE_DIR | unset | Directory of image tars loaded before any registry pull (§5). |
ROCK_DEFAULT_AUTO_CLEAR_TIME_MINUTES | 360 | Idle-container reclaim window (6 h). |
ROCK_RAY_NAMESPACE | xrl-sandbox | Ray namespace for sandbox actors (overridden by rock-local.yml). |
ROCK_WORKER_ENV_TYPE | local | Runtime env type; set to pip for PyPI installs. |
ROCK_MONITOR_ENABLE | false | Toggle metrics collection (rock/admin/metrics/). |
EnvHub
EnvHub (rock/envhub/server.py) is a standalone, SQLite-backed registry for reproducible environment definitions. The SWE lane bypasses it (images come from Docker Hub by instance id); Gym-style environments resolve their images through it.
Where to look next
Sandbox Management
Container lifecycle, sessions, auto-clear leases, and multi-node failure recovery.
Rollout Architecture
How evaluators drive env.step() and fold observations back into samples.
env_engine/README_ROCK.md
ROCK install, admin startup, GEM protocol, and the SDK smoke test.