Why agents need a different sandbox than CI runners
There is a temptation, when you first wire an LLM agent up to a shell, to reach for the sandbox you already know. Docker, a fresh container per run, a clean filesystem, the same rig you use for CI. It works, in the demo sense of works. It will keep working until the agent starts doing useful work, at which point it becomes the wrong tool for reasons that are easier to feel than to explain.
agentvfs was built on the bet that agent sandboxes and CI sandboxes are different products. This post is the long version of why.
CI runners are optimised for hermetic single-pass builds
A CI runner has a job description: take a git ref, produce an artifact, exit. The whole apparatus is designed around that arc. Containers are ephemeral on purpose. State that survives a build is a bug. The runner sandbox is permissive within the build — your cargo build can do almost anything — because the blast radius is bounded by the container’s lifetime. Whatever it touches gets thrown away. The container is the rollback.
The CI sandbox optimises for three properties:
- Reproducibility. A fresh container for every job means yesterday’s build cannot poison today’s.
- Throughput. The runner pool churns through jobs; provisioning cost matters per job.
- Hermeticity. A build that depends on hidden state on the host is the bug to design against.
Inside that frame, kernel-level isolation is a feature. seccomp, namespaces, read-only root filesystems, all of it points the same way: the sandbox is a process boundary that disappears when the process does.
Agents do not look like that
An agent is not a single-pass build. An agent runs a loop. It reads the workspace, decides on a step, runs a command, looks at the result, and decides on the next step. The workspace is not an artifact the agent is producing; it is the agent’s desk. It needs to survive across turns. It needs to remember what the agent did three steps ago. It needs to be the same workspace the agent will pick up tomorrow.
That changes everything about what the sandbox has to provide.
Persistence is now a feature, not a bug. The CI sandbox throws state away on purpose. An agent’s vault has to keep it. agentvfs makes the vault a single SQLite file — ~/.avfs/vaults/myproject.avfs — that holds files, version history, metadata, snapshots, and an audit log. You can copy it. You can ship it to a colleague. It survives reboots.
Cheap forks become the unit of work. When an agent attempts a risky step, it wants a workspace to attempt it in, separate from the canonical state, easy to throw away. CI’s answer is “another container”, which is fine when you have minutes and a registry. An agent has neither. agentvfs answers with vault fork: clone the vault into a task workspace in milliseconds, work inside the fork, discard or merge when done.
Rollback is the primary recovery tool. CI’s recovery tool is “rerun the job”. You cannot rerun an agent turn; the LLM call has happened, the cost is paid, the prior turns assumed the failed step’s side effects exist or don’t. agentvfs’s answer is checkpoint save before a risky command and checkpoint restore after a bad one. Policy can auto-checkpoint. The workspace snaps back; the agent’s history doesn’t have to.
Policy lives at the command, not at the syscall. A CI runner’s threat model is “an attacker submits a malicious PR”. Defense is kernel-level isolation against arbitrary syscalls. An agent’s threat model is “the model proposed a top-level command that, on reflection, is a bad idea”. Defense is a step that classifies the command before it runs. agentvfs’s PolicyEngine returns allow, allow_with_checkpoint, deny, or require_approval for the top-level command, evaluated before the workspace is mounted. It is cheap, side-effect-free, and lives in the right place: at the line you actually want to gate.
The workspace looks like a directory because tools are directory-shaped. CI runners give a container, and most tools just work because they were written to work in a directory. Agents need the same. agentvfs mounts the vault via FUSE, so git, cargo, npm, jq, python see a normal path. There is no special SDK to learn; the agent already knows how to call these.
The “honest-but-fallible” agent
The most important question a sandbox designer answers, implicitly, is what threat are you defending against? agentvfs is explicit: the runtime is built for an honest-but-fallible agent, not a malicious one.
That sounds like a hedge. It is, in fact, the calibration that makes the rest of the design coherent. If you assume the agent is malicious, every primitive has to defend against arbitrary syscalls, and you are back to writing a microVM. If you assume the agent is honest-but-fallible, you can deliver something cheaper and more useful: a top-level command boundary that catches the bad commands you can characterize and lets the rest run normally.
The docs say it plainly: “The proxy is a top-level command boundary, not a syscall monitor.” Full syscall tracing, complete subprocess visibility, kernel-level isolation, and multi-tenant permission systems are listed in the roadmap as explicit non-goals for the current phase. That is not a limitation hidden in the footnotes. It is the design.
If you do need defense against a hostile subprocess, the answer is to compose: run agentvfs inside a container or a microVM, get OS isolation for the bad cases, and use the proxy boundary for the agent-aware ones (policy, checkpoints, change summaries). The layers stack cleanly because they are aimed at different threats.
The structured-output question
There is one more thing the agent sandbox has to provide that the CI sandbox does not: machine-readable output, by default, on every operation.
CI runners produce logs. Logs are fine for a human reading a build. They are a disaster for an agent trying to decide what to do next. Every --json mode on every agentvfs command returns typed JSON. Errors come back as { "error": "QuotaExceeded", "message": "...", "type": "max_file_size_mb", "requested": 15, "limit": 10 }. The proxy’s execution result comes back as a versioned ExecutionEnvelope with stdout, stderr, exit code, duration, the policy decision, and the list of changed files. The agent does not need to parse du -h output; it queries the structured fields.
This is the part that, more than anything else, makes the agent loop tractable. A bad CI job logs its way to a fix. A bad agent step needs a contract.
What you give up
This design is not free.
You give up kernel-level isolation against a malicious binary. agentvfs cannot stop a process that wants to make syscalls from making them. If the agent installs a malicious package and runs it, the proxy did not stop that.
You give up cross-runtime portability. A vault is not an OCI image. You cannot point an arbitrary Kubernetes cluster at one.
You give up a polished hosted runtime. agentvfs is a library and a CLI; you run it, you operate it, you debug it.
In exchange, you get the workspace-shaped primitives an agent loop actually uses: durable vaults, cheap forks, checkpoint rollback, command-level policy, structured execution results, and standard CLI tooling that runs unchanged. For the case where the agent is yours, the threat is misbehavior rather than malice, and the workspace is something the agent has to come back to tomorrow, that trade is the right one.
Where the boundary belongs
The temptation, again, is to draw the boundary at the syscall. It feels rigorous. It also makes the wrong things expensive: simple tool invocations require sandbox infrastructure, rollback becomes container manipulation, policy becomes seccomp rules you write in YAML.
The agentvfs bet is that the right boundary, for an agent that means well and sometimes gets it wrong, is the top-level command. Cheap to enforce. Cheap to undo. Easy to log. Composable with the lower-level boundary when you need one.
If your agent looks more like a CI job — single-pass, hermetic, no state to preserve — keep using a CI runner. If it looks like a teammate who needs a desk to come back to, give it a workspace runtime instead.