Blog · 01 · 2026-10-10 · 5 min
Harbor on denv
Run any Harbor dataset with denv as the environment backend, import Harbor tasks as denv projects, and export denv environments as Harbor task directories.
Harbor is the task format labs mandate. A task is a directory: instruction.md, task.toml, an environment/ with a Dockerfile or a compose file, tests/test.sh that writes /logs/verifier/reward.txt, and a solution/. Datasets are published to Harbor Hub as org/name@tag, and harbor run drives one of about forty agents against them inside an environment backend: Docker, Daytona, Modal, E2B, Runloop, GKE, and others.
denv is one more backend on that list, with one difference. On every other backend an environment is a container that is rebuilt per episode. On denv it is a versioned, snapshotted, forkable system that keeps the five protections regardless of which agent runs inside.
There are three ways to use them together.
1. denv as the Harbor backend
Install the package and point Harbor at it:
pip install "denv[harbor]"
export DENV_SERVER=https://api.denv.ai DENV_API_KEY=dak_…
harbor run --dataset terminal-bench/terminal-bench-2 \
--agent claude-code \
-e denv.harbor:DenvEnvironment
Nothing about the dataset or the agent changes. denv.harbor:DenvEnvironment implements Harbor's BaseEnvironment: start, stop, exec, upload_file, upload_dir, download_file, download_dir. Here is what each does on denv.
start. The task's [environment] config decides the sandbox:
In task.toml or environment/ |
What denv does |
|---|---|
docker_image = "…" |
POST /v1/sandboxes {image} with volumes for /app, /logs, and the task workdir |
docker-compose.yaml present |
the file is deployed as a project under your team; the agent runs in the first service, or the one marked x-denv.agent.service |
only a Dockerfile |
built on the target by the sidecar, cached by content digest, then deployed as above |
allow_internet = true |
egress: allow; otherwise egress is denied and the probe result is recorded |
cpus, memory_mb |
become cpus and mem_limit on the agent service; Harbor's --cpus limit and --memory limit policies are honoured, guarantee is reported as unsupported |
The sandbox returns when every service is healthy. The directories Harbor expects, /logs/agent, /logs/verifier, /logs/artifacts, /tests, /solution, are created before the agent starts.
exec. Harbor hands the adapter a shell string plus cwd, env, user, and a timeout. denv runs it through bash when the image has it and sh otherwise, with env exported first, as the requested user. Harbor's agents, oracle and verifier all pass through this one path. When Harbor sets a default user on the environment, that user is used; it is never root unless Harbor says user="root", which it does for setup and verification.
upload and download. Tar streams through PUT and GET /v1/envs/{id}/files. Directory download copies the directory's contents, the way docker cp src/. does, which is what Harbor's verifier expects when it fetches /logs/verifier into the trial directory.
stop. DELETE /v1/envs/{id} when Harbor asks to delete; otherwise the environment is kept for inspection and reaped by TTL.
Capabilities. The adapter reports disable_internet=True and docker_compose=True. It does not report mounted, so Harbor downloads logs rather than expecting a shared filesystem. GPU and TPU report false until device passthrough ships.
What a Harbor trial gains on denv
- Every protection, without the task author doing anything. The agent service runs as the task's user, never root; caps are enforced; egress is denied and proven before the agent's first command. Harbor's own defaults leave three of Karotte's five protections unenforced; on denv they are properties of the environment.
- Fork instead of rebuild. Harbor rebuilds the environment for each trial. With
-k 5, five attempts means five builds. On denv the first trial deploys a version; the next four fork it in about a second each, data included, becauseDenvEnvironmentkeys the version on the task digest and reuses it. - A verdict log per trial. Each trial's environment has a deployment record: the pinned digests, the snapshot, the egress probe result, the caps. That record is the evidence you attach to a dataset when a lab asks how it was run.
- Multi-service tasks that stay portable. Compose tasks run on denv unchanged. If a dataset must also run on Daytona or Modal, the single-container discipline those providers need is yours to keep; denv does not force it, and
denv export --harborflags tasks that exceed a target's limits.
Limits that are real
Harbor's shared verifier runs tests/test.sh inside the same environment, as root, after the agent's turn. That is Harbor's design and the adapter honours it. If you want the grader as a separate principal on a fork, set [verifier] environment_mode = "separate" in task.toml; denv then forks the agent's environment at the end of the episode, stops the agent's processes in the fork, and runs the verifier there. The agent's environment is untouched. Both modes write the reward where Harbor expects it.
2. Import a Harbor task as a denv project
denv import harbor ./tasks/fix-backup-cron
Reads task.toml and environment/, writes a docker-compose.yaml with an x-denv block, creates a project of kind rl-environment, and deploys alpha. A compose task is used directly. A Dockerfile task becomes one service, built on your target. tests/test.sh becomes the x-denv.verifier command, solution/ is kept out of the image, and the task's allow_internet maps to x-denv.egress.
From there the task is a denv environment: push to redeploy, fork per rollout, snapshot mid-episode, promote through gates. You can keep publishing it to Harbor Hub from the same repo.
3. Export a denv environment as Harbor tasks
denv export --harbor ops-world@ver_01… ./dist
For each task declared in the project (a tasks/ directory with instruction.md and a verifier, or a pipeline that generates them), writes a Harbor task directory: task.toml with docker_image pinned to the version's digest, instruction.md, tests/test.sh wrapping the verifier, solution/ if present. Multi-service versions are exported as compose tasks, with a note of the memory and CPU a single-container target would need.
Export is how a denv environment reaches every trainer that reads Harbor: rLLM, verifiers, OpenEnv's Harbor environment, SkyRL's Harbor generator. You build and calibrate on denv, you publish in the format labs consume.
Conformance
The adapter is proven by running Harbor, not by unit tests of the adapter. make e2e-frameworks in the backend repo boots a control plane and a sidecar on one machine and runs:
harbor run -p tests/fixtures/harbor/hello -a oracle -e denv.harbor:DenvEnvironment
The oracle agent uploads the solution, runs it, the verifier writes reward 1, and Harbor downloads the logs. The run passes on a laptop with Docker and dir storage. We run it on every commit and will run Terminal-Bench 2 through it before calling the backend production-ready.
Status
Ships today. denv.harbor:DenvEnvironment for image-based and compose-based tasks; exec through bash/sh with env, cwd and user; file upload and download; allow_internet to egress; log directories pre-created; conformance with the oracle agent.
Committed by this post. Dockerfile-only tasks built on the target (today they are refused with a message to set docker_image); fork reuse across -k attempts keyed on task digest; cpus and memory_mb mapped to caps with enforcement policies reported; environment_mode = "separate" running the verifier on a fork as a separate principal; denv import harbor; denv export --harbor; Terminal-Bench 2 run through the backend with published results.