Docs · reference

Compose file and x-denv

The compose subset denv accepts, what it refuses and why, resource caps, the x-denv block, and the verifier contract.

An environment is described by a Docker Compose file plus an x-denv block. The control plane parses it at deploy time, rejects anything outside the subset below with the reason shown, and pins the result as the version's spec. The sidecar renders a new compose file from that spec; your file is never run verbatim.

Supported keys

Top level: services, volumes, x-denv. Everything else is ignored.

Per service:

Key Handling
image Required.
environment Map or KEY=VALUE list.
ports Container ports only. "8080:80" is reduced to 80; a /tcp suffix is dropped. Host ports are never pinned because forks need unique ports.
volumes "<volume>:<container path>" only, where <volume> is declared at top level.
depends_on List or map (keys only; conditions ignored).
command String (split on whitespace) or list.
healthcheck test (string or list), interval, retries.
user uid[:gid].
mem_limit, pids_limit, cpus Caps; see below.
deploy.resources.limits.{memory,cpus,pids} Alternative spelling of the caps; used only where the shorthand is absent.

Refused

Condition Error
No services compose: no services
Service without image compose: service "x" has no image (build is not supported in v0; push an image)
network_mode: host compose: service "x" uses network_mode: host, which cannot be forked
privileged or cap_add compose: service "x" asks for privileged or cap_add, which is refused
Bind mount (/path:… or ./path:…) compose: service "x" bind-mounts "/path"; only named volumes are forkable
Undeclared volume compose: service "x" uses undeclared volume "v"
Volume without a container path compose: service "x" volume "v" must be <volume>:<path>
x-denv.egress not deny/allow compose: x-denv.egress must be deny or allow, got "…"

build: is not supported: a service needs a pushed image.

Resource caps

Every service gets memory, pid, and CPU caps. Zero in the file means the sidecar default.

Cap Compose keys Default
Memory mem_limit (2g, 512m, 1024k, bytes) or deploy.resources.limits.memory 4096 MiB
Processes pids_limit or deploy.resources.limits.pids 2048
CPUs cpus or deploy.resources.limits.cpus 2.0

The sidecar renders these as mem_limit, pids_limit, cpus on every service of the generated compose file.

Network

With x-denv.egress: deny (the default) the stack's default network is rendered internal: true: no route out, and published ports are not reachable from the host. A probe container on that network tries http://1.1.1.1/ before the environment is reported healthy; the result is denied (verified). If the probe reaches the internet the deploy fails with egress is denied but the stack reached the internet. With allow, ports are published on 127.0.0.1:<random> and reported as the environment's Endpoints.

x-denv

x-denv:
  kind: rl-environment          # informational; the project's kind is authoritative
  egress: deny                  # deny (default) | allow
  warm:                         # argv run in the first service after health, before the snapshot
    - psql
    - -f
    - /seed/orders.sql
  agent:
    service: opsbox             # the service the agent runs in; must not be root
  verifier:
    image: ghcr.io/you/grader:1 # run as its own container on the fork's network …
    # service: opsbox           # … or inside this service of the fork, as root (exactly one of image/service)
    command: ["/grade.sh"]      # required
    collect: ["/home/ubuntu/answer.txt"]   # absolute paths in the agent service
    timeout_sec: 600            # default 600
Key Rule
kind Parsed, not enforced.
egress deny or allow.
warm List of strings; also settable per deploy by the API's warm field, which overrides the file. Runs in the first service (sorted by name), timeout 600 s; a non-zero exit fails the deploy.
agent.service Must name a service. That service must set a non-root user (an empty user counts as root). Error: compose: x-denv.agent.service "x" must set a non-root user (user: "1000:1000"); the agent is never root.
verifier Exactly one of image or service (compose: x-denv.verifier needs exactly one of image or service). command required (compose: x-denv.verifier.command is required). collect paths must be absolute (compose: x-denv.verifier.collect path "x" must be absolute). Requires agent.service (compose: x-denv.verifier requires x-denv.agent.service, the service whose paths are collected).

Services are processed in sorted name order; the "first service" (for warm and for the post-snapshot health check) is the alphabetically first.

Verifier contract

denv verify runs the verifier on a fork of the environment, after the agent service is frozen and the collect paths have been copied to a root-owned directory on the host.

Image verifier. docker run --rm --user 0 on the fork's network, with:

Mount / env Value
/submission the collected copy, read-only; the collected paths keep their absolute layout under it (/submission/home/ubuntu/answer.txt)
/logs/verifier writable; write the reward here
SUBMISSION /submission
DENV_VERIFY_ID the verify deployment id

Service verifier. The copy is placed at /submission inside the named service of the fork, /logs/verifier is created there, and the command runs with docker exec --user 0 and the same two environment variables. reward.txt is copied back out afterwards.

Reward. Read, in order: /logs/verifier/reward.txt parsed as a float; else the last whitespace-separated token of stdout parsed as a float. Neither present: the grade phase fails with verifier wrote no reward (exit N). The verdict records source as reward.txt, stdout, refused, or error.

Refusal. The copy is checked before any grader runs. Anything below sets the reward to 0 with source: refused, drops the whole copy, and skips the grader:

Finding Recorded as
symlink <path>: symlink
FIFO <path>: fifo
device <path>: device
socket <path>: socket
deeper than 32 levels <path>: deeper than 32
more than 256 MiB in total <path>: submission larger than 256 MiB
more than 20,000 files <path>: more than 20000 files

A declared path that does not exist is recorded under missing and does not cause a refusal. The grader's run is limited to timeout_sec (default 10 minutes); exceeding it fails the grade phase.

Example

x-denv:
  kind: rl-environment
  egress: deny
  agent: { service: opsbox }
  verifier:
    image: busybox:1.36
    command: ["sh", "-c", "mkdir -p /logs/verifier; [ \"$(cat /submission/home/agent/answer.txt)\" = hello ] && echo 1 > /logs/verifier/reward.txt || echo 0 > /logs/verifier/reward.txt"]
    collect: ["/home/agent/answer.txt"]
services:
  opsbox:
    image: python:3.12-slim
    user: "1000:1000"
    command: ["sleep", "infinity"]
    environment: { HOME: /home/agent }
    volumes: ["home:/home/agent"]
    mem_limit: 2g
    pids_limit: 512
  db:
    image: redis:7-alpine
    volumes: ["rd:/data"]
    healthcheck: { test: ["CMD", "redis-cli", "ping"], interval: 2s, retries: 15 }
volumes: { home: {}, rd: {} }

Not yet available

x-denv.clock, x-denv.verifier for hosted sandbox providers (E2B, Daytona refuse verify), and host-side image builds are described in the blog but not implemented. A compose file with no declared volumes still deploys; its snapshot is an empty tree.