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