Plimsoll puts an isolation rating on every sandbox run
When an AI agent writes code, running that code safely becomes the immediate problem. plimsoll runs untrusted code inside a sandbox and attaches an isolation tier to every result. A request can demand a minimum tier; if the sandbox cannot meet it, nothing executes.
Four tiers from process to VM
The isolation scale has four levels, weakest first: process, container, kernel, vm. The tier is a value on the result object, not a claim in documentation. A caller sets a floor — the weakest tier it will accept — and the official Go client rechecks the tier when the result arrives. If the daemon cannot satisfy the floor, the request is refused before any code runs, returning ErrInsufficientIsolation.
Exams not just containment
The same machinery runs an exam on agent-written code. A trial runner baked into the simulation image executes the agent's program against scenarios the program cannot read; each scenario reaches the runner in a file deleted before the program starts. Every run returns SHA-256 hashes of the request as sent and the result as returned, which the official clients recompute. A separate program, plimsoll-attest, signs the checked results, refuses a chain with a missing link, and can replay a run. The exam definition — simulated system, scenarios, pass mark — stays with the user; plimsoll supplies the boundary and the evidence.
Cart-pole controller proves the fingerprint works
An AI agent wrote a controller: a program that reads system state every 10 ms tick and decides how to push a cart-pole, the standard teaching problem in control engineering. The simulator compiles to WebAssembly and lives in the sandbox image. The controller was the only file the caller sent through the project API, and it ran at the container tier. The runner records every state at every tick and hashes the trajectory into a SHA-256 fingerprint. Equal fingerprints mean identical numbers. Running the accepted controller twice produces matching fingerprints. The agent's first draft had two gains with the wrong sign; it dropped the pole at 5.96 seconds and produced a different fingerprint. A live report replays both runs in the browser. One execution makes three sandbox runs: the accepted controller twice and the draft once. Reproduce with make docker-images && go run ./examples/oracle.
Refusals carry reasons and a dispatch mark
Every refused request that ran nothing is marked with a reason: request, permission, protocol, unsupported, isolation, environment, or capacity. The function sandbox.NotDispatchedReason(err) reads the mark whether the provider runs in-process or behind the daemon. An error without the mark may have occurred after code started, so automatic retry is never safe. A mark only counts on an answer carrying the client's request ID back, so a proxy serving one request's answer for another cannot make a call that ran read "nothing ran." The tier is evidence the daemon collected from its configuration and startup checks; it is not cryptographic attestation from hardware, and no provider here offers that.
API calls without credentials inside the sandbox
Agent code can call your API without ever holding your credential, your API's address, or open network access. A grant gives one run permission to call listed routes of one API. The code calls through a small client plimsoll places in the sandbox. The broker — the part of plimsoll outside the sandbox — makes the real call, verifies the caller and exact route, then attaches the credential itself. The credential is minted per run, so it can be a fresh short-lived token, and it never enters the sandbox. Code that bypasses the client and calls out directly gets nowhere because the broker, not the client, enforces the rules.
Six framework integrations with replay pages
Agent frameworks hand their model's code to plimsoll through their own tool or code executor. Each integration has a package, a guide, and a page that replays recorded calls from that framework's tool to a sandbox and back, including refusals.
- Vercel AI SDK —
@plimsollmark/client/ai-sdk - Google ADK —
plimsoll-client[adk] - Agno —
plimsoll-client[agno] - CrewAI —
plimsoll-client[crewai] - Trigger.dev —
@plimsollmark/client/trigger - Mastra —
@plimsollmark/client/mastra
The Trigger.dev TypeScript add-on gives an executeCode tool to a chat agent. On a daemon with a session — one sandbox kept for several calls — each call is a cell; variables and files can survive the next call. Each tool result reports whether state survives, whether the call started fresh, the isolation tier, and the SHA-256 of the run record, a receipt the client verified before returning. The caller sets a minimum tier so a weaker provider refuses before running code.
Seven providers, one per daemon
plimsolld runs exactly one provider chosen by SANDBOX_PROVIDER.
| Provider | Boundary | Tier | Use |
|---|---|---|---|
| wasm | QuickJS on wazero inside plimsolld | process | Fast local development |
| docker with runc | Container sharing host kernel | container | Self-hosting when kernel bugs are not in scope |
| docker with runsc | gVisor after startup checks confirm runsc | kernel | Hostile code on your own machines |
| e2b | MicroVM from E2B on Firecracker | vm | Hostile code on E2B's machines, billed per run |
| dockercloud | MicroVM from Docker Cloud Sandboxes | vm | Hostile code on Docker's machines, billed per run |
| openshell | OpenShell sandbox via gateway on docker | container | Platforms already running an OpenShell gateway |
| unset | Nothing runs | n/a | Default |
Each tier rests on daemon configuration, provider reports, and a real test run at startup; none is attestation. A session trades a fresh sandbox per call for speed and kept state: code from an earlier call can change what later calls see, and API access needs a grant that allows sessions. A session is one trust domain tied to the authenticated caller, not the caller's customers. The docker provider can also apply the shipped seccomp profile, a list of the only system calls its containers may make.
Local start needs no daemon or docker
The minimal example uses the wasm provider, which runs JavaScript on QuickJS — a small JavaScript engine compiled to WebAssembly — inside your own process.
git clone https://github.com/plimsollmark/plimsoll && cd plimsoll
go run ./examples/minimalprovider wasm
isolation process
exit code 0
timed out false
truncated stdout=false stderr=false
duration 304ms
stdout {"Engineering":59000000,"Sales":20300000,"Operations":9800000}
The isolation line comes from the run's result. Process means no operating-system wall at all; do not use it for hostile code. The same snippet runs at the kernel tier once gVisor is installed. gVisor is a layer between a container and the machine's kernel that handles the container's OS requests so the code never talks to the kernel directly. sudo ./docker/install-gvisor.sh installs a pinned gVisor release and registers its runtime, runsc, with docker. Then:
make docker-images
SANDBOX_PROVIDER=docker SANDBOX_DOCKER_RUNTIME=runsc go run ./examples/minimal
With SANDBOX_PROVIDER unset, plimsoll uses the Disabled provider, which runs nothing, so code execution is never on by accident.
What this means for teams shipping agents
You get a single interface that works from local development through production, with the isolation level explicit on every result and enforceable at request time. The exam machinery lets you test agent code against hidden scenarios and prove determinism with cryptographic fingerprints. API grants let agent code call your services without ever seeing credentials. Framework integrations mean you drop plimsoll into existing agent pipelines. The provider model lets you start with zero infrastructure and move to kernel or VM isolation without changing client code. The documentation walks through starting the daemon, creating a credential, writing a client, watching a floor refuse a run, then moving the daemon from process to kernel tier without touching the client — or embedding the Go package directly instead of running the daemon.