Docs · tutorials
Quickstart
Deploy an environment, fork it, and grade it on your own machine in about five minutes.
By the end of this page you will have a running environment with a database and an agent box, a fork of it made in about a second, and a reward produced by a verifier that ran apart from the agent. Everything runs on one Linux machine with Docker; a laptop works for the walkthrough.
You need: a Linux host (or a Mac for the control plane with a Linux box for the sidecar), Docker, and Go 1.22+ to build the binaries until the hosted release is live.
1. Get the binaries
git clone https://github.com/opencrew-ai/denv-backend
cd denv-backend && make build
export PATH="$PWD/bin:$PATH"
You now have denv-cp (the control plane), denvd (the sidecar that runs on each machine), and denv (the CLI).
2. Start a control plane
For the managed service you would skip this step and run denv login. For a local walkthrough, run your own:
DENV_BOOTSTRAP_KEY=dak_local denv-cp -addr :8080 -db ./denv.db -public-url http://127.0.0.1:8080 &
export DENV_SERVER=http://127.0.0.1:8080 DENV_API_KEY=dak_local
denv whoami
whoami prints the team the key belongs to. Keys that start with dak_ are team API keys.
3. Connect a machine
A target is where environments run. The simplest target is the machine you are on. Mint a join token and start the sidecar with it:
TOKEN=$(denv host join-token | python3 -c 'import json,sys;print(json.load(sys.stdin)["token"])')
denvd join --server $DENV_SERVER --token $TOKEN --state ./host.json --data ./data &
denv host list
The host shows "connected": true and reports its storage: dir on a plain disk, zfs when you pass --zfs-pool. Forks are copies on dir and clones on zfs; both are correct, one is about a second.
On a real server you would run the one-line installer instead, which does the same and installs a service:
curl -fsSL https://get.denv.ai | sh -s -- --token $TOKEN --server $DENV_SERVER
4. Describe the environment
Create a project and write a compose file. This one is a world: Postgres with data, Redis, and an agent box that is never root, with a verifier that grades a file the agent writes.
denv project create --kind rl-environment quickstart
cat > compose.yaml <<'EOF'
x-denv:
kind: rl-environment
agent:
service: box
verifier:
image: busybox:1.36
command: ["sh", "-c", "[ \"$(cat /submission/home/agent/answer.txt)\" = 42 ] && echo 1 > /logs/verifier/reward.txt || echo 0 > /logs/verifier/reward.txt"]
collect: ["/home/agent/answer.txt"]
services:
db:
image: postgres:16-alpine
environment: { POSTGRES_PASSWORD: world }
volumes: ["pg:/var/lib/postgresql/data"]
healthcheck: { test: ["CMD-SHELL", "pg_isready -U postgres"], interval: 2s, retries: 30 }
box:
image: python:3.12-slim
user: "1000:1000"
command: ["sleep", "infinity"]
environment: { HOME: /home/agent, PGHOST: db, PGPASSWORD: world }
volumes: ["home:/home/agent"]
depends_on: [db]
volumes: { pg: {}, home: {} }
EOF
Three things in this file are denv's: x-denv.agent.service names the box the agent gets, and it must set a non-root user; x-denv.verifier is the grader, which runs after the agent is stopped on a copy of the collect paths; everything else is ordinary compose. Egress is denied by default.
5. Deploy
denv deploy --project quickstart -f compose.yaml --wait
You will see the phases run: resolve, place, prepare, start, health, snapshot, verdict. The snapshot phase freezes writers, checkpoints Postgres, and captures every volume at the same instant. The result is a version; the environment alpha now points at it.
ENV=$(denv project envs quickstart | python3 -c 'import json,sys;print([e for e in json.load(sys.stdin)["environments"] if e["Name"]=="alpha"][0]["ID"])')
denv env get $ENV
6. Act as the agent, then fork
Write something as the agent (note the uid: the box is not root) and leave a row in the database:
denv exec --service box $ENV -- sh -c 'id -u; echo 42 > /home/agent/answer.txt'
denv exec --service db $ENV -- psql -U postgres -c "create table t(x int); insert into t values (1)"
Now capture the live state and fork it:
denv snapshot $ENV
denv fork --name branch-1 $ENV
BRANCH=$(denv project envs quickstart | python3 -c 'import json,sys;print([e for e in json.load(sys.stdin)["environments"] if e["Name"]=="branch-1"][0]["ID"])')
denv exec --service db $BRANCH -- psql -U postgres -tAc "select count(*) from t"
The fork has the row. It has its own network and its own copies of every volume; nothing you do in it reaches alpha.
7. Grade
denv verify $ENV
Watch the phases: stop freezes the agent box, collect copies /home/agent/answer.txt to a root-owned directory and refuses symlinks, pipes and devices, snapshot and clone make a fork with the agent still frozen, grade runs the busybox verifier there, verdict records reward: 1. The agent's box stays frozen so you can inspect what it did; pass --resume to unfreeze it.
Try the attack a reinforcement-learning policy would find:
denv verify --resume $ENV >/dev/null
denv exec --service box $ENV -- sh -c 'ln -sf /etc/passwd /home/agent/answer.txt'
denv verify --resume $ENV
The verdict shows reward: 0, source: refused, and the reason home/agent/answer.txt: symlink. The grader never ran.
8. Clean up
denv env destroy $BRANCH
denv env destroy $ENV
Where next
- Run a Harbor task on denv uses the same environment as a Harbor backend.
- Connect a target covers Hetzner, AWS and hosted sandbox providers.
- Concepts explains projects, environments, deployments and versions.
- CLI reference lists every command.