Blog · 06 · 2026-10-10 · 4 min
Karotte on denv
Karotte's five protections, now with versions, snapshots, forks and multi-service worlds. Run Karotte environments inside denv and import them as denv projects.
Karotte, from Preference Model, is the harness that made protection the default. An environment is one self-contained image; a task is a Python Step with instructions, submission_paths and a judge; karotte run boots the image in a Firecracker microVM on Linux or an Apple container on macOS, runs the student unprivileged, kills every student process, copies the submission to a root-owned location, refuses symlinks and other special files, and only then runs the judge. Memory, process and disk caps are enforced by the VM. The network is off and checked before the run.
Karotte published a table comparing those five protections across Harbor, HUD, AgentEnv, verifiers and Inspect. We agree with the table. denv's position is that the protections belong to the environment runtime, so every framework gets them, and that an environment also needs the things Karotte deliberately leaves out: more than one service, data with versions, a consistent snapshot, a fork, and a place to run that is your own cloud.
There are two ways to use them together.
1. Run Karotte inside a denv environment
A Karotte environment image is a service in a denv compose file. denv supplies the outer protections; Karotte supplies the task semantics.
x-denv:
kind: rl-environment
egress: deny
agent:
service: karotte
verifier:
service: karotte
command: ["karotte", "judge", "--step", "fix_backup"]
collect: ["/home/student/submission"]
services:
karotte:
image: ghcr.io/you/karotte-ops-env:3
user: "1000:1000"
command: ["sleep", "infinity"]
mem_limit: 4g
pids_limit: 1024
volumes: ["student:/home/student"]
db:
image: postgres:16-alpine
volumes: ["pg:/var/lib/postgresql/data"]
healthcheck: { test: ["CMD-SHELL", "pg_isready -U postgres"], interval: 2s, retries: 30 }
volumes: { student: {}, pg: {} }
Deploy it, fork it per episode, and run the student with denv exec --service karotte … -- karotte run --step fix_backup. The judge runs through denv verify: the student's processes are stopped in a fork, /home/student/submission is collected to a root-owned location, special files are refused and scored zero, and the judge command runs there. That is Karotte's sequence, executed by denv, with the database alongside.
What changes relative to karotte run on a laptop:
| Karotte alone | Karotte inside denv | |
|---|---|---|
| Isolation | Firecracker (Linux) or Apple container; Docker fallback without memory limits | Container with caps on your machines today; gVisor runtime on hosts that have it; Firecracker on targets that offer it |
| Services | one image | any number, with healthchecks and data |
| Snapshot and fork | none | consistent snapshot of every volume; fork in about a second on ZFS |
| Versions | image tag | pinned digests plus snapshot, with a deployment record per change |
| Where it runs | your laptop or Linux box | your cloud, provisioned or connected; hosted sandbox targets later |
| Egress | off, checked | off, probed before every episode, result in the record |
The isolation row is the honest one. Karotte alone gives you a microVM. denv on your own machines gives you a container with caps by default, a gVisor runtime where the host has it, and a microVM only on targets that provide one. If VM isolation is a requirement, deploy to a target that offers it, and the console's capability row says which.
2. Import a Karotte environment as a denv project
denv import karotte ./ops-env
Reads the Karotte environment directory, uses the built image (or builds it on the target), and writes a compose file like the one above with an x-denv block derived from the Karotte steps: the student user becomes user, submission_paths become verifier.collect, the judge becomes the verifier command, and each Step becomes a task in the project's tasks/ directory. The project deploys as alpha and from there is a denv environment: push to redeploy, fork per rollout, promote through gates, export as Harbor tasks.
The Karotte source stays authoritative for the task logic. denv does not rewrite steps or judges; it runs them in a world.
What denv adds that Karotte does not want to
Karotte's authors are explicit that the framework is one image, no cloud integrations, no snapshots, binary egress. That is a good harness. The things an environment program needs beyond a harness are what denv is for:
- A version with a snapshot. Two runs disagree; which world were they in? A denv version answers that with digests and a captured dataset.
- Fork mid-episode. Branch an episode eight ways from one state for search or counterfactual grading.
- Promote gates. A new version of the environment passes replay, consistency, invariant and soak gates before
stagingpoints at it. The gate record is the data sheet a lab asks for. - Your cloud. The environment runs in your account, on machines you can delete, with denv holding only the control plane.
Conformance
The committed conformance run builds Karotte's smallest example environment, imports it with denv import karotte, forks the deployed version, runs the student step with karotte run through denv exec, and grades through denv verify, asserting the judge's result equals Karotte's own result on the same image. A second case places a symlink in the submission path and asserts a zero.
Status
Ships today. Running a Karotte image as a service with denv's caps, non-root user and verified egress denial; fork and snapshot of the environment; denv verify with x-denv.verifier.service running the judge inside the fork as root over the collected copy, special files refused (2026-10-11).
Committed by this post. denv import karotte; gVisor runtime selection per host; microVM targets with the capability row; the two conformance cases above; our own column in Karotte's protections table, published with the evidence.