Docs · reference
Protections
The five protections an rl-environment gets by default — what enforces each, where the evidence is, and the exact strings you will see.
These are the five checks from Karotte's public framework comparison, as denv implements them. Each is a property of the environment runtime, not of the harness running inside, so Harbor, Inspect, OpenEnv, or a custom harness gets the same behaviour. They apply to environments on sidecar hosts; hosted sandbox providers refuse verify and enforce caps and egress their own way (see Targets).
1. Reading the answer key
Rule. The agent is never root. With x-denv.agent.service set, the compose parser refuses a service whose user is empty, 0, or root.
Where. compose.Parse, at the resolve phase of a deploy (the API returns 400 before any deployment starts).
You will see. compose: x-denv.agent.service "opsbox" must set a non-root user (user: "1000:1000"); the agent is never root
Limits. Without x-denv.agent.service nothing enforces the user; the rl-environment-ops template sets user: "1000:1000". Grading data is kept out of the agent's reach only insofar as the verifier runs in its own image or on a separate fork; a service verifier runs inside the fork's copy of a service.
2. Tampering with grading
Rule. The grader never shares a process tree with the agent. verify pauses the agent service (docker compose pause <service>) before anything is copied, copies the declared paths to a root-owned directory on the host (<data>/verify/<deployment>/submission, mode 0700), snapshots the environment with the agent still frozen, forks it, and runs the verifier on the fork as uid 0, over the root-owned copy mounted read-only at /submission.
Where. verify.collect and verify.run on the sidecar; phases stop → collect → snapshot → clone → grade of the verify deployment.
You will see. Progress lines freezing agent service <name>, checking the copy, grading in <image> or grading inside service <name>; verdict evidence {"reward": 1, "source": "reward.txt", "agent_frozen": true, …}. After verify without resume, denv exec into the agent service fails with Docker's is paused error.
Limits. The verifier image you supply is yours; denv does not inspect it. PATH/LD_PRELOAD scrubbing inside the agent container is a harness concern (Karotte does it; denv does not).
3. Breaking grading with a weird answer file
Rule. The collected copy is walked with lstat before any grader runs. Symlinks, FIFOs, devices, and sockets are refused; so are trees over 256 MiB, over 20,000 files, or deeper than 32 levels. On any refusal the whole copy is deleted, the grader is not run, and the reward is 0. docker cp is invoked with -a=false, so a symlink is copied as a symlink and caught, not followed.
Where. checkTree in the sidecar's verify.collect; verify.run with refused: true short-circuits.
You will see. In the collect phase evidence and the verdict: "refused": ["home/agent/answer.txt: symlink"], "… : fifo", "… : device", "… : socket", "… : deeper than 32", "… : submission larger than 256 MiB", "… : more than 20000 files"; and "source": "refused", "reward": 0.
4. Crashing the run by using up resources
Rule. Every service gets mem_limit, pids_limit, and cpus in the rendered compose file: the values from the compose file, or 4096 MiB, 2048 pids, 2.0 CPUs when unset. A fork bomb hits the pid cap inside its own cgroup; memory pressure kills the service, not the host or the sidecar.
Where. stack.render on the sidecar, from the resources carried in the spec.
You will see. The caps in <data>/envs/<env>/compose.yaml; the Docker error for the container when it is hit.
Limits. No disk quota on dir storage; ZFS datasets have no quota set either. The host itself is not isolated from the sidecar beyond cgroups (no gVisor, no microVM).
5. Looking up the answer online
Rule. x-denv.egress defaults to deny. The stack's default network is rendered internal: true. After the stack is healthy, and again after every fork, a busybox container on that network attempts wget http://1.1.1.1/ with a 4 s timeout. The environment is reported healthy only if the probe prints BLOCKED.
Where. stack.verifyEgress in env.create and env.fork on the sidecar.
You will see. Progress line healthy; egress denied (verified) (or healthy; egress allowed); the env.create result's "egress": "denied (verified)"; denv deploy output alpha · egress denied (verified) in the console. If the probe reaches out, the deploy fails with egress is denied but the stack reached the internet and the stack is stopped. If the probe cannot run at all: egress probe failed to run: ….
Limits. A denied network cannot publish ports, so Endpoints is empty; set egress: allow to publish, and then nothing is denied. DNS is not separately blocked; with internal: true there is no route to a resolver either.
Where the evidence lives
| Protection | Evidence |
|---|---|
| Non-root agent | The version's spec (GET /v1/envs/{id}/versions, Spec.services[].user, Spec.agent). |
| Separate grader, refused files | The verify deployment's collect, grade, and verdict phases (GET /v1/deployments/{id}). |
| Caps | The version's spec (resources) and the rendered compose on the host. |
| Egress | Progress events on cmd:<id> and the prepare phase's command result; the sidecar's env.create result carries egress. |
Progress lines are stored as events with subject cmd:<command id>; the command id is on the commands row the control plane recorded for the phase.