Show HN:Plimsoll,自托管的 AI Agent 代码沙箱
英文标题:🔶 Show HN: Plimsoll, a self-hosted sandbox for code your AI agent writes
Plimsoll 是一个用于运行 AI Agent 等来源生成的不可信代码的沙箱,支持在每次请求中设置最低隔离等级,并在结果中报告实际等级;达不到要求时,代码不会运行。它提供从进程、容器、内核到虚拟机的四级隔离,并支持通过代理让沙箱内代码调用指定 API,而不暴露凭证或开放网络访问。项目还提供运行记录哈希、结果签名与重放能力,并以倒立摆控制器演示如何测试 Agent 编写的代码。开发者可以通过 Agent 框架集成或 Go 客户端使用它,但项目强调,隔离等级来自守护进程的配置和启动检查,并非硬件级密码学证明。
1.5 KB
271.4 KB
97.9 KB
165.2 KB
中文
plimsoll 在沙箱中运行不可信代码,例如 AI Agent 编写的代码,并在每次结果中说明该沙箱的隔离强度。请求可以要求最低隔离强度;如果沙箱不满足要求,代码就不会运行。
项目名称来自 Plimsoll line(载重线):它画在船体外侧,标明载重限度,任何人都可以查看。plimsoll 对_隔离等级_做了类似的事:它用四个等级说明一次运行周围的隔离边界有多强,从弱到强分别是 process、container、kernel 和 vm。隔离等级是每次结果中的一个值,而不是产品说明书里的一句话。请求可以设置可接受的最低等级,官方 Go 客户端在收到结果后还会再次检查该等级。
同一套机制不仅能隔离 Agent 编写的代码,还能对其进行考核。模拟镜像内置的试运行器会让 Agent 的程序应对它无法读取的场景:每个场景通过文件传递给运行器,而文件会在程序启动前删除。每次运行都会返回实际发送的请求和实际返回的结果的 SHA-256 哈希,官方客户端会重新计算这些哈希(实现方式)。守护进程之外的独立程序 plimsoll-attest 会对检查过的结果签名,拒绝存在缺失环节的调用链,并可重放一次运行。考核本身——模拟系统、场景和合格标准——仍由你决定;plimsoll 提供的是边界和证据。
一个 AI Agent 编写了一个_控制器_:它在每个 tick(这里是模拟中的一个 10 毫秒步长)读取系统状态,并决定如何施加作用。plimsoll 让它控制一个模拟的_倒立摆_:轨道上的小车顶部铰接着一根杆,控制器通过向左、向右推动小车,使杆保持直立。模拟器被编译为 WebAssembly——一种在宿主程序中运行的可移植字节码——并内置于沙箱镜像中。调用方通过普通项目 API 只发送了控制器这一个文件,它以 container 等级运行。
倒立摆是控制工程中的标准教学问题。运行器是内置于镜像中的程序,负责让控制器与模拟器对接;它会记录运行的_轨迹_,即每个 tick 的完整状态,并将其哈希为_指纹_:一个 SHA-256 哈希值。指纹相同意味着数值完全一致。将通过测试的控制器运行两次,两次指纹一致。Agent 的初稿有两个增益值(公式中的乘数)符号错误:它在 5.96 秒时让杆倒下,指纹也不同。
打开交互式运行报告 ↗,即可在浏览器中重放这两种运行、阅读 Agent 编写的控制器,并查看该页面有意没有作出哪些声明。执行一次示例会产生三次沙箱运行:通过测试的控制器运行两次,初稿运行一次。使用 make docker-images && go run ./examples/oracle 可以复现。
**它会报告隔离边界。**每个结果都说明运行实际获得的隔离等级。每个请求都可以设置最低等级;如果守护进程无法满足要求,请求会在任何代码运行之前被拒绝:ErrInsufficientIsolation 意味着没有运行任何代码。
go res, err := provider.Sandbox.RunJavaScript(ctx, sandbox.Request{ Code: "console.log(6 * 7)", MinimumIsolation: sandbox.IsolationVM, // ErrInsufficientIsolation means nothing ran }) // res.Isolation reports the boundary that actually ran.
每次未运行任何代码的拒绝都会被标记,并附有原因(request、permission、protocol、unsupported、isolation、environment、capacity)。无论 provider(运行代码的后端)位于你的进程内还是守护进程之后,sandbox.NotDispatchedReason(err) 都可以读取该标记。没有标记的错误可能是在代码开始运行之后产生的,因此绝不能安全地自动重试。只有响应带回客户端的请求 ID 时,该标记才有效,因此即便代理把一个请求的响应返回给另一个请求,也无法让已经运行过的调用被误读为“没有运行任何代码”(返回内容及其含义)。
隔离等级是守护进程收集的证据:其配置及自身启动检查。它不是_证明(attestation)_,即由硬件提供的、关于正在运行何种软件的密码学证明;这里没有任何 provider 提供这种证明。docs/isolation-tiers.md 列出了每个等级的依据。
**它让 Agent 编写的代码调用你的 API,同时从不持有你的凭证、API 地址或开放的网络访问权限。**一项_授权(grant)_允许单次运行调用某个 API 的指定路由。代码通过 plimsoll 放入沙箱的小型客户端发起调用;_代理(broker)是沙箱外 plimsoll 的组成部分,负责发起实际调用,它会检查调用方和精确路由,再自行附加凭证。凭证按运行_签发:plimsoll 每次运行请求一次凭证,因此它可以是新生成的短期令牌,而且永远不会进入沙箱。绕过客户端、手动向外发起调用的代码也无法越权,因为执行规则的是代理,而不是客户端。参见 docs/capability-grants.md。
从 Agent 框架中使用
这些 Agent 框架可以通过各自的工具或代码执行器,将模型生成的代码交给 plimsoll(其中的 Trigger.dev 是托管式 TypeScript 作业运行器)。每一行都列出了需要安装的内容、使用指南,以及一个页面:该页面可重放从相应框架工具到沙箱再返回的调用记录,包括被拒绝的调用。
为 Trigger.dev Agent 提供代码工具
TypeScript 插件为 Trigger.dev 上的聊天 Agent 提供了 executeCode 工具。在支持_会话_的守护进程上,一个沙箱可供多次调用使用;每次调用是一个 cell,即在为该对话保留的解释器中运行的一段代码:变量和文件可以保留到下一次调用。每个工具结果都会说明它们能否保留、此次调用是否从全新状态开始、运行所处的隔离等级,以及_运行记录_的 SHA-256。运行记录是发送和返回内容的凭据,客户端会在返回前进行检查。调用方设置最低等级,因此较弱的 provider 会在运行代码之前拒绝请求。
具体保证和生产环境的最低要求请参阅功能与部署指南。可部署的 Trigger.dev 入门项目包含一个聊天 Agent 和一项任务,后者通过真实守护进程证明两个 cell 共享同一个解释器,无需调用 AI 模型。
一分钟内运行示例
该示例不需要守护进程、docker、凭证或网络:它使用 wasm provider,在你自己的进程内通过 QuickJS 运行 JavaScript。QuickJS 是一个被编译为 WebAssembly 的小型 JavaScript 引擎。克隆仓库和获取 Go 模块仍需要网络。
text git clone https://github.com/plimsollmark/plimsoll && cd plimsoll go run ./examples/minimal
text provider wasm isolation process exit code 0 timed out false truncated stdout=false stderr=false duration 304ms stdout {"Engineering":59000000,"Sales":20300000,"Operations":9800000}
isolation 行来自运行结果,而不是示例自行编写的文本。**process 意味着完全没有操作系统级隔离边界:不要用它运行恶意代码。**安装 gVisor 后,同一段代码可以在 kernel 等级运行。gVisor 是容器与机器内核之间的一层,它自行处理容器发给操作系统的请求,因此代码不会直接与机器内核交互。sudo ./docker/install-gvisor.sh 会安装一个_固定版本_的 gVisor(只有编辑此仓库才会更改的精确版本),并将其运行时 runsc 注册到 docker。程序的其他部分不需要改变:
text make docker-images # builds the project image (plimsoll/sandbox) the docker provider checks for SANDBOX_PROVIDER=docker SANDBOX_DOCKER_RUNTIME=runsc go run ./examples/minimal
如果未设置 SANDBOX_PROVIDER,plimsoll 会使用 Disabled provider,它不运行任何代码,因此不会意外启用代码执行。
接下来:docs/getting-started.md 介绍如何在一台机器上完成这些步骤:启动守护进程、创建调用方凭证、编写自己的客户端、观察最低等级要求如何拒绝运行,然后在不更改客户端的情况下将守护进程从 process 迁移到 kernel。文档还介绍如何嵌入 Go 包,而不是运行守护进程。
text flowchart LR A["Agent or MCP gateway (MCP: how an AI app offers tools to a model)"] -->|"Run: one payload, a protocol number, minimum_isolation"| AU
subgraph D["plimsolld"] AU["authenticate the caller"] --> FL["compare the floor with current provider evidence"] FL --> DI["dispatch"] BR["broker: holds the minted credential, matches the exact route, counts every call"] end
DI --> W["wasm: QuickJS on wazero (process tier)"] DI --> K["docker with runsc (kernel tier)"] DI --> V["e2b Firecracker (VM tier)"]
W -. "host.get / host.post" .-> BR K -. "per-run Unix socket" .-> BR V -. "authenticated guard" .-> BR BR ==> |"your credential, attached host-side"| API["Your API"]
沙箱内的代码,即_客户代码(guest)_,从不持有凭证,也不会直接连接网络。没有授权的运行完全无法访问网络。
plimsolld 是 plimsoll 服务器。每个实例恰好运行一个 provider,由 SANDBOX_PROVIDER 选择:
| Provider | 隔离边界 | 报告等级 | 用途 |
|---|---|---|---|
wasm |
plimsolld 自身内部、运行在 wazero(用 Go 编写的 WebAssembly 运行时)上的 QuickJS |
process |
快速本地开发。引擎发生_逃逸_(让代码逃出沙箱的漏洞)时,代码会进入你的守护进程。 |
使用 runc(docker 默认运行时)的 docker |
与机器共享内核的容器 | container |
适用于未将内核漏洞列入威胁模型的自托管场景。使用项目镜像时可保持会话:一个容器用于多次调用,并提供变量可在调用之间保留的 JavaScript 解释器;使用 plimsoll/sandbox-python 作为项目镜像时还提供 Python 解释器。 |
使用 runsc 的 docker |
gVisor,启动检查确认 docker 已注册 runsc 后启用 |
kernel |
在自己的机器上运行恶意代码。也支持保持会话。 |
e2b |
来自 E2B 的_微型虚拟机(microVM)_;E2B 是托管服务,在 AWS 开源虚拟机监控器 Firecracker 上运行微型虚拟机。微型虚拟机为单次运行创建,之后销毁 | vm |
在 E2B 的机器而非自己的机器上运行恶意代码,按运行计费。也支持保持会话,微型虚拟机运行期间计费;挂起会暂停它并保留文件,但不会保留解释器变量。 |
dockercloud |
来自 Docker 托管沙箱服务 Docker Cloud Sandboxes 的微型虚拟机 | vm |
在 Docker 的机器上运行恶意代码,按运行计费。它支持两个 API,由 SANDBOX_DOCKERCLOUD_API 选择,两者均于 2026-10-04 通过实时测试套件:一个是 Docker 在正式推出前发布、目前不再提供文档的 API,因只有它支持授权和镜像证据而成为默认选项;另一个是 Docker 有文档的 REST API,在支持上述功能之前作为备用选项保留(不支持授权,在 PLIMSOLL_HARDENED=1 时会被拒绝,单次运行最长 270 秒)。账户的网络策略必须是_默认全部拒绝_(除非规则允许,否则不建立连接),每次运行都会检查。当设置 SANDBOX_DOCKERCLOUD_GUARD_URL 时,授权通过与 E2B 相同的 guard 工作:它是 plimsoll 服务器上的一个地址,也是微型虚拟机唯一可以连接的位置。与 E2B 不同,客户代码持有自己的…… |
English / 原文
plimsoll runs untrusted code, such as code an AI agent wrote, inside a sandbox, and every result says how strong that sandbox was. A request can demand a minimum strength; if the sandbox is weaker, nothing runs.
The name comes from the Plimsoll line , the load limit painted on the outside of a ship's hull where anyone can check it. plimsoll does the same for the isolation tier : how strong the wall around a run is, stated as one of four levels, weakest first: process, container, kernel, vm. The tier is a value on every result, not a sentence in a datasheet. A request can set a floor , the weakest tier it will accept, and the official Go client checks the tier again when the result comes back.
The same machinery lets plimsoll run an exam on code an agent wrote, not only contain it. A trial runner built into the simulation image runs the agent's program against scenarios that program cannot read: each one 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 (how). A separate program outside the daemon, plimsoll-attest, signs the checked results, refuses a chain of calls with one missing, and replays a run. The exam itself (the simulated system, the scenarios, the pass mark) stays yours: plimsoll supplies the boundary and the evidence.
An AI agent wrote a controller : a program that reads a system's state at every tick (here, one 10 ms step of the simulation) and decides how to push it. plimsoll ran it against a simulated cart-pole , a cart on a rail with a pole hinged on top, which the controller keeps upright by pushing the cart left and right. The simulator is compiled to WebAssembly , a portable bytecode that runs inside a host program, and built into the sandbox image. The controller was the only file the caller sent, through the ordinary project API, and it ran at the container tier.
The cart-pole is the standard teaching problem in control engineering. The runner, a program built into the image that runs the controller against the simulator, records the run's trajectory , every state at every tick, and hashes it into a fingerprint : a SHA-256 hash, so equal fingerprints mean identical numbers. Run the accepted controller twice and the two fingerprints match. The agent's first draft had two gains (multipliers in its formula) with the wrong sign: it drops the pole at 5.96 seconds, and its fingerprint differs.
Open the live run report ↗ to replay both runs in the browser, read the controller the agent wrote, and see what the page deliberately does not claim. One execution of the example makes three sandbox runs: the accepted controller twice and the draft once. Reproduce them with make docker-images && go run ./examples/oracle.
It reports the wall. Every result states the isolation tier the run actually got. Every request can set a floor, and a request whose floor the daemon cannot meet is refused before any code runs: ErrInsufficientIsolation means nothing ran.
res, err := provider.Sandbox.RunJavaScript(ctx, sandbox.Request{
Code: "console.log(6 * 7)",
MinimumIsolation: sandbox.IsolationVM, // ErrInsufficientIsolation means nothing ran
})
// res.Isolation reports the boundary that actually ran.
Every refusal that ran nothing is marked as one, with a reason (request, permission, protocol, unsupported, isolation, environment, capacity). sandbox.NotDispatchedReason(err) reads the mark, whether the provider (the backend that runs the code) is in your own process or behind the daemon. An error without the mark may have come after the code started, so it is never safe to retry automatically. A mark counts only on an answer that carries the client's request ID back, so a proxy that serves one request's answer for another cannot make a call that ran read "nothing ran" (what comes back, and what it means).
The tier is evidence the daemon collected: its configuration and its own startup checks. It is not attestation , cryptographic proof from the hardware of what software is running, and no provider here offers that. docs/isolation-tiers.md lists what each tier rests on.
It lets agent-written code 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 puts in the sandbox, and the broker , the part of plimsoll outside the sandbox that makes the real call, checks the caller and the exact route, then attaches the credential itself. The credential is minted per run: plimsoll asks for it once per run, so it can be a fresh short-lived token, and it never enters the sandbox. Code that skips the client and calls out by hand gets no further, because the broker, not the client, enforces the rules. See docs/capability-grants.md.
Use it from an agent framework
These agent frameworks can hand their model's code to plimsoll through a tool or code executor of their own (Trigger.dev , one of them, is a hosted TypeScript job runner). Each row names what to install, the guide, and a page that replays recorded calls from that framework's tool to a sandbox and back, refusals included.
Give a Trigger.dev agent a code tool
The TypeScript add-on gives an executeCode tool to a chat agent on Trigger.dev. On a daemon with a session , one sandbox kept for several calls, each call is a cell , code run inside the interpreter kept for that conversation: variables and files can survive the next call. Each tool result says whether they can survive, whether this call started fresh, the isolation tier it ran behind, and the SHA-256 of the run record , a receipt for what was sent and returned that the client checked before returning. The caller sets a minimum tier, so a weaker provider refuses before running code.
See the feature and deployment guide for the exact guarantees and production floor. The deployable Trigger.dev starter has a chat agent and a task that proves two cells share one interpreter through a real daemon, without making an AI model call.
Run something in one minute
The example needs no daemon, no docker, no credentials and no network: it uses the wasm provider, which runs JavaScript on QuickJS , a small JavaScript engine compiled to WebAssembly, inside your own process. Cloning and fetching Go modules do use the network.
git clone https://github.com/plimsollmark/plimsoll && cd plimsoll
go run ./examples/minimal
provider 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, not from the example's own text. process means there is 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 your machine's kernel that handles the container's requests to the operating system itself, so the code never talks to your kernel directly. sudo ./docker/install-gvisor.sh installs a pinned gVisor release (one exact version, changed only by editing this repository) and registers its runtime, runsc , with docker. Nothing else about the program changes:
make docker-images # builds the project image (plimsoll/sandbox) the docker provider checks for
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.
Next: docs/getting-started.md walks through the same pieces on one machine: start the daemon, create a caller credential, write your own client, watch a floor refuse a run, then move the daemon from process to kernel without changing the client. It also covers embedding the Go package instead of running the daemon.
flowchart LR
A["Agent or MCP gateway (MCP: how an AI app offers tools to a model)"] -->|"Run: one payload, a protocol number, minimum_isolation"| AU
subgraph D["plimsolld"]
AU["authenticate the caller"] --> FL["compare the floor with current provider evidence"]
FL --> DI["dispatch"]
BR["broker: holds the minted credential, matches the exact route, counts every call"]
end
DI --> W["wasm: QuickJS on wazero (process tier)"]
DI --> K["docker with runsc (kernel tier)"]
DI --> V["e2b Firecracker (VM tier)"]
W -. "host.get / host.post" .-> BR
K -. "per-run Unix socket" .-> BR
V -. "authenticated guard" .-> BR
BR ==> |"your credential, attached host-side"| API["Your API"]
Loading
The code inside the sandbox, the guest , never holds the credential and never reaches the network directly. A run with no grant has no network at all.
plimsolld is the plimsoll server. Each one runs exactly one provider, chosen by SANDBOX_PROVIDER:
| Provider | Boundary | Tier reported | Use |
|---|---|---|---|
wasm |
QuickJS on wazero (a WebAssembly runtime written in Go), inside plimsolld itself |
process |
Fast local development. An escape (a bug that lets code out of its sandbox) in the engine lands inside your daemon. |
docker with runc , docker's default runtime |
a container sharing your machine's kernel | container |
Self-hosting when a kernel bug is not one of the attacks you plan for. With a project image it keeps a session: one container for many calls, with a JavaScript interpreter (and a Python one with plimsoll/sandbox-python as the project image) whose variables survive between calls. |
docker with runsc |
gVisor, once the startup checks confirm docker has runsc registered |
kernel |
Hostile code on your own machines. Keeps sessions too. |
e2b |
a microVM (a small virtual machine made for one run, then destroyed) from E2B , a hosted service that runs them on Firecracker , AWS's open-source VM monitor | vm |
Hostile code, on E2B's machines rather than yours; billed per run. Keeps sessions too, billed while the microVM runs; a suspend pauses it, which keeps the files but not an interpreter's variables. |
dockercloud |
a microVM from Docker Cloud Sandboxes , Docker's hosted sandbox service | vm |
Hostile code, on Docker's machines; billed per run. It speaks two APIs, chosen by SANDBOX_DOCKERCLOUD_API, and both passed the live suite on 2026-10-04: the one Docker released before launch, no longer documented, which is the default because only it has grants and image evidence, and the REST API Docker documents, kept as a backup until it covers those (no grants, refused under PLIMSOLL_HARDENED=1, runs of at most 270 s). The account's network policy must be deny-all (no connection unless a rule allows it), and every run checks that. Grants work through the same guard as E2B (an address on the plimsoll server, the only place the microVM may connect to) when SANDBOX_DOCKERCLOUD_GUARD_URL is set. Unlike E2B, the guest holds its own run's short-lived credential for the guard, and the one network rule is set through a Docker call outside its published API. |
openshell |
a sandbox from OpenShell , NVIDIA's agent sandbox runtime, created by its gateway server on docker | container |
Agent platforms that already run an OpenShell gateway. Each run gets its own sandbox with no network; plimsoll reads its settings back, refuses to run on any difference, and deletes it afterwards. Tested against a v0.1.2 gateway on 2026-09-28. Grants reach the broker through a relay inside the sandbox that plimsoll connects to from outside, so the sandbox needs no network rules. Keeps sessions too. |
| unset | nothing runs | n/a | The default. |
Each tier rests on the daemon's configuration, what the provider reports, and a real test run at startup; none is attestation. docs/isolation-tiers.md lists the evidence for each tier and shows how a request sets its floor. A session trades the fresh sandbox of every call for speed and kept state: code an earlier call ran can change what later calls see, and a call with API access needs a grant that allows sessions (docs/sessions.md). A session is one trust domain, and plimsoll ties it to the authenticated caller, not to that caller's customers: a service that runs many customers' code through one credential must keep each customer in a session of their own (who may share a session). The docker provider can also apply the shipped seccomp profile, a list of the only system calls (requests to the kernel, such as opening a file) its containers may make (docs/seccomp.md).
How this compares with the other self-hosted sandboxes for agent code (E2B's self-hosted version, microsandbox, NVIDIA OpenShell, Kubernetes agent-sandbox and eight more), each described from its own documentation, with plimsoll's weak points beside them: the comparison (INTERNAL · plimsoll site →).
| Page | What it shows |
|---|---|
| Simulation replay pages, all eight simulators ↗ | The eight simulators built into the simulation image for controller runs: shower, buck converter (a power supply that steps a voltage down by switching it on and off), ship heading, black hole orbit, relativistic rocket, satellite clock, double slit, and cart-pole swing-up (the image also carries the models the module-run tests use). Each page runs a failing and a passing hand-written controller through the sandbox and replays both trajectories with their fingerprints. |
| A controller in C, compiled in the sandbox ↗ | The same cart-pole simulator, with a swing-up controller (it swings the pole up from hanging, then balances it) written in C and compiled to WebAssembly by the run's own first step, so the simulator and the controller are both WebAssembly. The run report compares it with the JavaScript version tick by tick. The two languages' cos functions disagree in the last bit on about one input in a hundred, so in three of the four scenarios one or two force values differ, by a few representable doubles (at most 14). The fingerprint catches that difference, and the motion itself is identical to the bit. Source and caveats in its README; reproduce with make docker-images && go run ./examples/wasm-controller. |
| A buck converter controller in C ↗ | A power supply's control law (the formula its controller applies at each tick) written in C, the language converter firmware is written in. It is compiled to WebAssembly in the sandbox and run against a simulated converter, whose output it must hold at 5 V while the load changes suddenly. It calls no library function, so its trajectory equals the JavaScript version's byte for byte in all four scenarios; the page charts one of them tick by tick. Source and caveats in its README; reproduce with make docker-images && go run ./examples/wasm-buck. |
| Same run, different sandboxes ↗ | The cart-pole run from the top of this page, on local runc and gVisor, an OpenShell gateway, E2B and Docker Cloud Sandboxes: three isolation tiers, two Node versions, and one fingerprint, the same one the first report published. Reproduce the configured rows with go run ./examples/providers; the cloud rows are billed. |
| One sandbox, five calls ↗ | A session on an OpenShell sandbox: a failing test, a patch, the test passing without the files being sent again, and a leftover process that is gone by the next call. Each call's run record (the daemon's statement of what was sent, what came back and where it ran) is signed and linked to the previous one. The verifier accepts the signed set, and refuses it when one call is dropped or one byte is changed. Reproduce with go run ./examples/sessions and a gateway. |
| The efficiency advisor's report ↗ | The advisor reads the API calls a run made and points out wasteful patterns. One measured run: the same question asked of an API as 13 calls, then as 1, and the advisor's finding that names the route that answers it in one call. |
| Twelve interactive lessons ↗ | How a run is executed, the providers, the API broker, and connecting an AI agent. Static pages: no network calls, no analytics, no third-party scripts. |
- Pre-1.0 , single author, no external users yet. Breaking changes land without a deprecation path, on purpose.
- No third-party security audit has ever been performed , and the author's own self-review ledgers are not published either. SECURITY.md says what exists, what it is worth, and what you can check yourself instead.
- CI runs the gate, in the open, and each check means what it ran. The audit workflow has three jobs.
auditruns plainmake auditon every push tomainand every pull request: build, vet, race tests, lint,buf lint, a generated-code drift check, andgovulncheck. Afteraudit,clientsrunsmake clients-suite:npm ciinclients/typescriptandexamples/trigger-chat, then the Python and TypeScript Go test packages in required mode. A missing runtime, dependency or skipped Go test fails that job.audit-dockeralso followsauditand runsmake docker-suitewith the images prepared, in required mode: a missing daemon, a missing image or a skipped test fails the job. A greenaudit-dockertherefore means the docker suite ran under runc with the shipped seccomp profile and proved a read-only root, sizednoexecwritable mounts, and the broker's refusals. The gvisor workflow runs the same suite under runsc, the kernel tier, from the pinned installer. The race detector runs in the plain audit job and both docker-suite jobs; the client job runs without it. No check exercises E2B or Docker Cloud Sandboxes: those suites drive live paid services and are deliberately never wired to a runner. See CONTRIBUTING.md.
The interactive lessons are the fastest way in if you would rather read than clone. Start with Plain English for the idea before the API, or Quick start to run something. Every term these docs use has a one-sentence definition in the glossary. The lessons live in docs/trainers/ and work offline: open any file from a clone in a browser. GitHub shows .html files as source instead of rendering them, which is why the links above point at the published copy.
Each of these answers one question, end to end.
| Document | Answers |
|---|---|
| docs/getting-started.md | How do I build it, embed it, start it as a service with authentication, and watch a floor refuse a run? |
| docs/example-programs.md | What do the runnable examples prove, and which should I read first? |
| docs/isolation-tiers.md | What does each tier rest on, and how do I demand one per request? |
| docs/capability-grants.md | How does agent code call my API without ever holding my credential? |
| docs/run-results.md | What comes back, and when is a failure an error rather than a result? |
| docs/run-records.md | What does each run's record state, how do I recompute it in another language, and how do I sign, verify and replay records outside the daemon? |
| docs/sessions.md | How do I keep one sandbox for many calls, and what holds between the calls, an interpreter's variables included? |
| clients/python | How do I call plimsolld from Python? |
| clients/typescript | How do I call it from TypeScript, and give a Trigger.dev or Mastra agent a code tool that keeps its state? |
| docs/trigger-dev.md | How do calls that reuse one interpreter, an isolation floor and checked run records appear together in a Trigger.dev tool, and how do I deploy a task? |
| docs/ai-sdk.md | How do I give a Vercel AI SDK agent an executeCode tool, fresh per call or kept for one user's conversation? |
| docs/google-adk.md | How do I make plimsoll the code executor of a Google ADK agent, with input files, output artifacts and a refusal below the floor? |
| docs/placement.md | I run several daemons: how do I pick one per request, and when is a refusal safe to retry elsewhere? |
| docs/inner-loop-workflow.md | How do I iterate fast locally without shipping a weak sandbox to production? |
| docs/efficiency-advisor.md | What does the advisor see, why can what it records never include the data the code sent or received, and how do I choose what it emits? |
| docs/hardened-mode.md | How do I make the daemon refuse to start unless every production safeguard is set? |
| docs/dependencies.md | Which dependencies must be trusted for the sandbox to hold, and how are the tools that check them pinned? |
| docs/limitations.md | What does this deliberately not do? |
| docs/dockercloud.md | What does the Docker Cloud Sandboxes provider need from the operator, and what does each run check? |
| docs/openshell.md | What does the OpenShell provider need from the operator, and what does each run check? |
| docs/releasing.md | Why is the module path public, why do releases start at v0.2.0, and why is there no checksum exemption? |
| docs/callers.md | How do I create, rotate and revoke caller credentials? |
| docs/seccomp.md and docs/gvisor.md | What do the seccomp filter and gVisor enforce? |
| docs/guest-dependencies.md | How do npm packages get into a sandbox that has no network? |
| docs/architecture/credential-minting.md | Where does a per-run credential come from, and where does it stay? |
| docs/seams.md | Where are the deliberate extension points? |
| Glossary ↗ | What does this word mean? One plain sentence per term. |
| AGENTS.md | The architecture reference: providers, the rules no change may break, and every environment variable. |

