plimsoll 在沙箱内运行不受信任的代码,比如 AI 智能体编写的代码,并且每个结果都会说明该沙箱的强度。请求可以要求最低强度;如果沙箱更弱,则什么都不会运行。
这个名字来自Plimsoll 线,即画在船体外侧、任何人都能查看的载重限制线。plimsoll 对隔离层级也做同样的事:一次运行周围那道墙有多强,用四个级别之一来表示,从最弱开始:process,container,kernel,vm。该层级是每个结果上的一个值,而不是数据手册里的一句话。请求可以设置一个下限,即它会接受的最弱层级,官方 Go 客户端会在结果返回时再次检查该层级。
同一套机制让 plimsoll 不仅能隔离智能体编写的代码,还能对它进行考试。内置于模拟镜像中的试验运行器会让智能体的程序针对该程序无法读取的场景运行:每个场景都通过一个在程序启动前就被删除的文件送达运行器。每次运行都会返回所发送请求和所返回结果的 SHA-256 哈希,官方客户端会重新计算(如何)。守护进程之外的一个独立程序,plimsoll-attest,会对已检查的结果签名,拒绝缺少一环的调用链,并重放一次运行。考试本身(模拟系统、场景、及格分数)仍归你所有:plimsoll 提供边界和证据。
查看一次真实运行
一个 AI 智能体编写了一个控制器:一个在每个tick(这里指模拟的一个 10 ms 步长)读取系统状态并决定如何推动它的程序。plimsoll 让它针对一个模拟的倒立摆小车运行,即一辆在轨道上的小车,顶部铰接一根杆,控制器通过左右推动小车使杆保持直立。模拟器被编译为WebAssembly,一种在宿主程序内运行的可移植字节码,并内置到沙箱镜像中。控制器是调用方通过普通项目 API 发送的唯一文件,它运行在container层级。
倒立摆小车是控制工程中的标准教学问题。运行器是内置于镜像中的一个程序,它让控制器针对模拟器运行,记录该次运行的轨迹,即每个 tick 的每个状态,并将其哈希成一个指纹: 一个 SHA-256 哈希,因此相同的指纹意味着相同的数字。运行两次被接受的控制器,两个指纹就匹配。智能体的初稿有两个增益(其公式中的乘数)符号错误:它在 5.96 秒处丢掉极点,其指纹也不同。
打开实时运行报告 ↗在浏览器中重放两次运行,阅读智能体编写的控制器,并查看该页面刻意不声称的内容。示例的一次执行会产生三次沙箱运行:被接受的控制器两次,初稿一次。用以下命令复现它们make docker-images && go run ./examples/oracle。
它做的两件事
它报告墙。每个结果都说明该次运行实际获得的隔离层级。每个请求都可以设置一个下限,而守护进程无法满足其下限的请求会在任何代码运行之前被拒绝:ErrInsufficientIsolation意味着什么都没运行。
res, err := provider.Sandbox.RunJavaScript(ctx, sandbox.Request{ Code: "console.log(6 * 7)", MinimumIsolation: sandbox.IsolationVM, // ErrInsufficientIsolation 意味着什么都没运行 }) // res.Isolation 报告实际运行的边界。
每一个什么都没运行的拒绝都被标记为拒绝,并附有原因(request,permission,protocol,unsupported,isolation,environment,capacity)。sandbox.NotDispatchedReason(err)读取该标记,无论provider(运行代码的后端)是在你自己的进程中还是在守护进程之后。没有该标记的错误可能是在代码启动之后出现的,因此自动重试永远不安全。标记只在一个把客户端请求 ID 带回来的应答上才算数,因此一个把某个请求的应答提供给另一个请求的代理,无法让一个实际运行过的调用读作“什么都没运行”(返回什么,以及它意味着什么)。
该层级是守护进程收集的证据:它的配置及其自身的启动检查。它不是证明,即来自硬件的、关于正在运行什么软件的密码学证明,而这里的任何 provider 都不提供这种证明。docs/isolation-tiers.md列出了每个层级所依赖的内容。
它让智能体编写的代码可以调用你的 API,而无需持有你的凭证、你的 API 地址或开放的网络访问。一个授权授予一次运行调用某个 API 所列路由的权限。代码通过 plimsoll 放入沙箱的一个小型客户端进行调用,而broker,即 plimsoll 位于沙箱之外、发起真实调用的部分,会检查调用方和确切的路由,然后自行附加凭证。该凭证是mintedper run:plimsoll 每次运行只请求一次,因此它可以是一个全新的短期令牌,而且它永远不会进入沙箱。绕过客户端、手动发起调用的代码无法更进一步,因为执行规则的是 broker,而不是客户端。参见docs/capability-grants.md。
在智能体框架中使用它
这些智能体框架可以通过它们自己的工具或代码执行器,将其模型的代码交给 plimsoll(Trigger.dev,其中之一,是一个托管的 TypeScript 任务运行器)。每一行列出需要安装的内容、指南,以及一个页面,该页面回放从该框架的工具到沙箱再返回的已记录调用,包括被拒绝的调用。
| 框架 | 包或附加项 | 指南 | 查看一次运行 |
|---|---|---|---|
| Vercel AI SDK | @plimsollmark/client/ai-sdk |
Vercel AI SDK 指南(内部 · 文档 →) | Vercel AI SDK 页面(内部 · plimsoll 站点 →) |
| Google ADK | plimsoll-client[adk] |
Google ADK 指南(内部 · 文档 →) | Google ADK 页面(内部 · plimsoll 站点 →) |
| Agno | plimsoll-client[agno] |
Python 客户端指南,Agno 和 CrewAI 工具(内部 · 文档 →) | Agno 页面(内部 · plimsoll 站点 →) |
| CrewAI | plimsoll-client[crewai] |
Python 客户端指南,Agno 和 CrewAI 工具(内部 · 文档 →) | CrewAI 页面(内部 · plimsoll 站点 →) |
| Trigger.dev | @plimsollmark/client/trigger |
Trigger.dev 指南(内部 · 文档 →) | Trigger.dev 页面(内部 · plimsoll 站点 →) |
| Mastra | @plimsollmark/client/mastra |
TypeScript 客户端指南,Mastra(内部 · 文档 →) | Mastra 页面(内部 · plimsoll 站点 →) |
给 Trigger.dev 智能体一个代码工具
这个TypeScript 附加组件为 Trigger.dev 上的聊天智能体提供一个executeCode工具。在带有会话的守护进程上,一个沙箱会为多次调用保留,每次调用是一个单元,代码在该对话所保留的运行记录的解释器内运行:变量和文件可以存活到下一次调用。每个工具结果都会说明它们能否存活、本次调用是否全新开始、它运行在哪个隔离层级,以及
的 SHA-256,这是一份关于发送和返回内容的凭据,客户端在返回前已对其进行检查。调用方会设置最低层级,因此较弱的提供方会在运行代码前拒绝。参见功能与部署指南以了解确切的保证和生产底线。这个可部署的 Trigger.dev 起始项目
一分钟内运行点什么
这个示例不需要守护进程、不需要 docker、不需要凭据,也不需要网络:它使用的是wasmprovider,它在QuickJS上运行 JavaScript,这是一个编译为 WebAssembly 的小型 JavaScript 引擎,运行在你自己的进程内。克隆和拉取 Go 模块确实会用到网络。
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}
这个isolation行来自运行的结果,而不是示例自身的文本。process意味着完全没有操作系统层面的隔离墙:不要用它来运行恶意代码。同一段代码会在kernel层级运行,一旦gVisor安装完成。gVisor 是容器与你的机器内核之间的一层,它处理容器对操作系统本身的请求,因此代码永远不会直接与你的内核通信。sudo ./docker/install-gvisor.sh会安装一个固定版本的gVisor 发行版(一个确切的版本,只能通过编辑此仓库来更改),并向 docker 注册其运行时runsc。程序的其他部分没有任何变化:
make docker-images #构建项目镜像(plimsoll/sandbox),docker provider 会检查它是否存在
SANDBOX_PROVIDER=docker SANDBOX_DOCKER_RUNTIME=runsc go run ./examples/minimal当SANDBOX_PROVIDER未设置时,plimsoll 使用 Disabled provider,它什么都不运行,因此代码执行绝不会被意外开启。
接下来: docs/getting-started.md会在同一台机器上走一遍相同的部分:启动守护进程、创建调用方凭据、编写你自己的客户端、观察某个层级拒绝一次运行,然后把守护进程从process迁移到kernel,而无需更改客户端。它还涵盖了嵌入 Go 包而不是运行守护进程的方式。
各部分所处的位置
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 |
QuickJS 运行在wazero(一个用 Go 编写的 WebAssembly 运行时)之上,位于plimsolld自身内部 |
process |
快速的本地开发。引擎中的一次逃逸(一个让代码逃出其沙箱的漏洞)会落在你的守护进程内部。 |
docker搭配runc,docker 的默认运行时 |
一个共享你机器内核的容器 | container |
在你并不打算防范内核漏洞这类攻击时用于自托管。使用项目镜像时它会保持一个会话:一个容器服务多次调用,配有一个 JavaScript 解释器(以及一个以plimsoll/sandbox-python作为项目镜像的 Python 解释器),其变量在调用之间得以保留。 |
docker搭配runsc |
gVisor,一旦启动检查确认 docker 已注册runsc的 |
kernel |
在你自己的机器上运行恶意代码。也会保留会话。 |
e2b |
amicroVM(一个为单次运行而创建、随后销毁的小型虚拟机)来自E2B,一项托管服务,在Firecracker上运行它们,这是 AWS 的开源虚拟机监视器 | vm |
恶意代码,运行在 E2B 的机器上而非你的机器上;按运行计费。也会保留会话,在 microVM 运行期间计费;挂起会将其暂停,这会保留文件但不保留解释器的变量。 |
dockercloud |
一个 microVM,来自Docker Cloud Sandboxes,Docker 的托管沙箱服务 | vm |
恶意代码,运行在 Docker 的机器上;按运行计费。它支持两种 API,由SANDBOX_DOCKERCLOUD_API选择,且两者都在 2026-10-04 通过了实时测试套件:Docker 在发布前推出、现已不再有文档记录的那个,它是默认选项,因为只有它具备授权和镜像证据;以及 Docker 有文档记录的 REST API,作为备用保留,直到它覆盖这些功能(无授权,在PLIMSOLL_HARDENED=1下被拒绝,运行时长最多 270 s)。账户的网络策略必须为deny-all(除非有规则允许,否则不建立连接),且每次运行都会检查这一点。当设置了guard时,授权通过与 E2B 相同的SANDBOX_DOCKERCLOUD_GUARD_URL生效(plimsoll 服务器上的一个地址,是 microVM 唯一可以连接的地方)。与 E2B 不同,guest 持有自己这次运行的、用于 guard 的短期凭证,而那条唯一的网络规则是通过 Docker 在其已发布 API 之外的一次调用设置的。 |
openshell |
一个沙箱,来自OpenShell,NVIDIA 的智能体沙箱运行时,由其网关服务器在 docker 上创建 | container |
已经运行 OpenShell 网关的智能体平台。每次运行都会获得自己的沙箱,且无网络;plimsoll 会读回其设置,若有任何差异则拒绝运行,并在之后将其删除。已针对 2026-09-28 的 v0.1.2 网关进行测试。授权通过沙箱内部的一个中继到达 broker,plimsoll 从外部连接到该中继,因此沙箱不需要网络规则。也会保留会话。 |
| 未设置 | 不运行任何内容 | 不适用 | 默认选项。 |
每一层级都依赖于守护进程的配置、提供方所报告的内容,以及启动时的一次真实测试运行;没有一项是证明。docs/isolation-tiers.md列出了每一层的证据,并展示请求如何设定其下限。会话用速度和保留的状态换取每次调用全新的沙箱:较早调用运行过的代码可以改变后续调用看到的内容,而一次具有 API 访问权限的调用需要一项允许会话的授权(docs/sessions.md)。一个会话就是一个信任域,plimsoll 将其绑定到经过身份验证的调用方,而不是该调用方的客户:一个通过单一凭证运行众多客户代码的服务必须把每个客户放在各自的会话中(谁可以共享一个会话)。docker provider 还可以应用随附的seccomp配置文件,这是一份其容器可以发起的唯一系统调用(对内核的请求,例如打开文件)的列表(docs/seccomp.md)。
这与其他用于智能体代码的自托管沙箱(E2B 的自托管版本、microsandbox、NVIDIA OpenShell、Kubernetes agent-sandbox 以及另外八个)相比如何,每一个都根据其自身文档描述,并附上 plimsoll 的弱点:对比(INTERNAL · plimsoll site →)。
更多真实运行
| 页面 | 它展示了什么 |
|---|---|
| 模拟回放页面,全部八个模拟器 ↗ | 为控制器运行内置于模拟镜像中的八个模拟器:shower、buck converter(一种通过开关切换将电压降下来的电源)、ship heading、black hole orbit、relativistic rocket、satellite clock、double slit 以及 cart-pole swing-up(该镜像还带有模块运行测试所用的模型)。每个页面都在沙箱中运行一个失败的和一个通过的手写控制器,并连同它们的指纹回放两条轨迹。 |
| 一个用 C 编写、在沙箱中编译的控制器 ↗ | 同一个 cart-pole 模拟器,配有一个用 C 编写、由该次运行自身的第一步编译为 WebAssembly 的 swing-up 控制器(它把杆从悬挂状态摆起,然后使其平衡),因此模拟器和控制器都是 WebAssembly。运行报告将其与 JavaScript 版本逐 tick 比较。这两种语言的cos函数在大约百分之一的输入上最后一位不一致,因此在四种场景中的三种里,有一到两个力值不同,相差几个可表示的 double(最多 14)。指纹能捕捉到这一差异,而运动本身逐位完全相同。来源和注意事项见其 README;复现方式:make docker-images && go run ./examples/wasm-controller。 |
| 用 C 写的降压转换器控制器 ↗ | 一个电源的控制律(其控制器在每个 tick 应用的公式)用 C 写成,也就是转换器固件所用的语言。它被编译为 WebAssembly 并在沙箱中针对模拟转换器运行,必须在负载突变时将输出保持在 5 V。它不调用任何库函数,因此在全部四种场景中其轨迹与 JavaScript 版本逐字节相同;页面逐 tick 绘制了其中一种场景的图表。来源和注意事项见其 README;复现方式:make docker-images && go run ./examples/wasm-buck。 |
| 同一次运行,不同的沙箱 ↗ | 本页顶部的倒立摆运行,在本地runc和 gVisor、一个 OpenShell 网关、E2B 以及 Docker Cloud Sandboxes 上:三个隔离层级、两个 Node 版本,以及一个指纹,与第一份报告发布的是同一个。用以下命令复现已配置的行go run ./examples/providers;云端行需要付费。 |
| 一个沙箱,五次调用 ↗ | 一个 OpenShell 沙箱上的会话:一个失败的测试、一个补丁、测试通过且无需再次发送文件,以及一个遗留进程在下一次调用时已消失。每次调用的运行记录(守护进程关于发送了什么、返回了什么以及在哪里运行的陈述)都经过签名并链接到上一条。验证器接受这个签名集合,并在丢弃一次调用或改动一个字节时拒绝它。复现方式:go run ./examples/sessions以及一个网关。 |
| 效率顾问的报告 ↗ | 顾问读取一次运行所做的 API 调用,并指出浪费的模式。一次实测运行:同一个问题对 API 先以 13 次调用提问,再以 1 次调用提问,以及顾问的发现,指出能在一次调用中回答它的路径。 |
| 十二节交互式课程 ↗ | 一次运行如何执行、各提供商、API 代理,以及连接一个 AI 智能体。静态页面:无网络调用、无分析、无第三方脚本。 |
状态,直说
- 1.0 之前,单一作者,尚无外部用户。破坏性变更不带弃用路径,这是有意为之。
- 从未进行过第三方安全审计,作者自己的自审账本也未公开。SECURITY.md说明了存在什么、价值几何,以及你可以自己检查什么。
- CI 公开运行这道关卡,每项检查都说明它运行了什么。该审计工作流有三个作业。
audit在每次推送到make audit时运行普通的main以及每个拉取请求:构建、vet、竞态测试、lint、buf lint、生成代码漂移检查,以及govulncheck。在audit之后,clients运行make clients-suite:npm ci在clients/typescript和examples/trigger-chat中,然后以必需模式运行audit-docker中的 Python 和 TypeScript Go 测试包。缺少运行时、依赖或被跳过的 Go 测试都会使该作业失败。audit也遵循make docker-suite并在镜像已准备好的情况下以必需模式运行audit-docker:缺少守护进程、缺少镜像或被跳过的测试都会使作业失败。因此一个绿色的noexec意味着 docker 套件在 runc 下使用随附的 seccomp 配置文件运行,并证明了只读根目录、大小合适的可写挂载,以及 broker 的拒绝。gvisor 工作流在 runsc(内核层级)下从固定版本的安装器运行同一套件。竞态检测器在普通审计作业和两个 docker 套件作业中运行;客户端作业不运行它。没有任何检查会演练 E2B 或 Docker Cloud Sandboxes:这些套件驱动的是实时付费服务,被刻意从不接入运行器。参见
CONTRIBUTING.md
。了解它如何工作如果你更愿意阅读而不是克隆,交互式课程是最快的入门方式。先从Plain English了解 API 之前的概念,或者用Quick start运行一些东西。这些文档使用的每个术语在术语表中都有一句话定义。课程位于.htmldocs/trainers/
并可离线使用:在浏览器中打开克隆中的任意文件即可。GitHub 会将
文件显示为源码而不是渲染它们,这就是为什么上面的链接指向已发布的副本。
| 文档 | 以下每一项都端到端地回答一个问题。 |
|---|---|
| 文档 | 回答 |
| docs/getting-started.md | 我如何构建它、嵌入它、以带身份验证的服务方式启动它,并观察 floor 拒绝一次运行? |
| docs/example-programs.md | 可运行的示例证明了什么,我应该先读哪一个? |
| docs/isolation-tiers.md | 每个层级依托于什么,我如何按请求要求某一层级? |
| docs/capability-grants.md | 返回什么,以及何时失败是错误而非结果? |
| docs/run-records.md | 每次运行的记录声明了什么,我如何用另一种语言重新计算它,以及我如何在守护进程之外签名、验证和重放记录? |
| docs/sessions.md | 我如何为多次调用保留一个沙箱,调用之间保留什么,包括解释器的变量? |
| clients/python | 我如何从 Python 调用 plimsolld? |
| clients/typescript | 我如何从 TypeScript 调用它,并给 Trigger.dev 或 Mastra 智能体一个保持其状态的代码工具? |
| docs/trigger-dev.md | 复用同一个解释器、隔离底线和经过校验的运行记录,如何在 Trigger.dev 工具中一起出现,以及我如何部署一个任务? |
| docs/ai-sdk.md | 我如何给 Vercel AI SDK 智能体一个executeCode工具,每次调用都新建还是为一个用户的对话保留? |
| docs/google-adk.md | 我如何让 plimsoll 成为 Google ADK 智能体的代码执行器,支持输入文件、输出产物以及低于底线时的拒绝? |
| docs/placement.md | 我运行多个守护进程:我如何为每个请求选择一个,以及何时拒绝可以安全地在别处重试? |
| docs/inner-loop-workflow.md | 我如何在本地快速迭代,而不把薄弱的沙箱发布到生产环境? |
| docs/efficiency-advisor.md | 顾问能看到什么,为什么它记录的内容永远不会包含代码发送或接收的数据,以及我如何选择它输出什么? |
| docs/hardened-mode.md | 我如何让守护进程拒绝启动,除非每一项生产保障措施都已设置? |
| docs/dependencies.md | 哪些依赖必须被信任才能让沙箱成立,以及检查它们的工具是如何被固定的? |
| docs/limitations.md | 它刻意不做什么? |
| docs/dockercloud.md | Docker Cloud Sandboxes 提供方需要运维人员提供什么,以及每次运行检查什么? |
| docs/openshell.md | OpenShell 提供方需要运维人员提供什么,以及每次运行检查什么? |
| docs/releasing.md | 为什么模块路径是公开的,为什么发布从 v0.2.0 开始,以及为什么没有校验和豁免? |
| docs/callers.md | 我如何创建、轮换和撤销调用方凭据? |
| docs/seccomp.md和docs/gvisor.md | seccomp 过滤器和 gVisor 强制执行什么? |
| docs/guest-dependencies.md | 在没有网络的沙箱中,npm 包是如何进入的? |
| docs/architecture/credential-minting.md | 每次运行的凭证从何而来,又保存在哪里? |
| docs/seams.md | 刻意的扩展点在哪里? |
| 术语表 ↗ | 这个词是什么意思?每个术语用一句平实的话说明。 |
| AGENTS.md | 架构参考:提供方、任何改动都不得违反的规则,以及每一个环境变量。 |

