切换主题
智能体沙箱是怎么做的:跨框架源码拆解
前不久OpenAI的大模型从沙箱逃逸出来并且攻击了Hugging Face的新闻大家想必是看到了,那么智能体沙箱是怎么做的,我们在做智能体基建的时候是否需要建沙箱、怎么建沙箱?为了回答这个问题,我让AI拉取了 8 个主流框架的源码,逐一检视它们的沙箱实现来学习。
一、Agent 沙箱到底是什么?为什么必须有?
1.1 什么是沙箱
Agent 沙箱,是一层"能挡副作用"的执行环境。 它包住"Agent 干活"这件事本身——Agent 在里面写文件、跑命令、执行代码,但这些操作的作用域被限制在沙箱之内,宿主机器和宿主数据不受影响。
1.2 为什么 LLM 时代它成了刚需
- 指令与代码的边界模糊:模型的训练目标是"忠实地执行指令",而指令里可能恰好包含"删除这个目录"。它不是故意使坏,而是真的分不清"该做的"和"不该做的";
- 命令有级联效应:
rm -rf、curl ... | sh、chmod -R 777这类命令的后果是一瞬间且不可逆的,人写错还有 review 环节,模型写错直接执行; - 凭证与隐私需要防御:Agent 进程中往往有 API key、内网地址、个人数据。沙箱是"模型读不到不该读的"这一诉求的执行层;
- 多 Agent 互相污染:一个 Agent 的失败(比如写坏了环境变量、装坏了 Python 包)不该传染给其他 Agent;
- 可回滚、可重试:长任务中断后,沙箱把"现场"存下来(快照),下次接着跑,而不是从头再来。
一句话:没有沙箱,你不敢让 Agent"放手干活";有了沙箱,"干砸了"的成本才变得可承受。
二、沙箱一般用哪些技术实现?
沙箱按"隔离的程度"从轻到重,大致是一条技术谱系。组织上可以理解为:越靠近"轻",启动越快、集成越容易,但边界越脆弱;越靠近"重",隔离越彻底,但资源开销与部署成本越高。
mermaid
flowchart LR
subgraph L1[轻 · 软件层]
A[解释器级软沙箱<br/>AST 解析 + import 白名单]
B[进程级隔离<br/>受限权限子进程/子用户]
end
subgraph L2[中 · 内核能力]
C[系统调用过滤<br/>seccomp / Landlock / seatbelt]
D[文件系统视图隔离<br/>bubblewrap / chroot]
end
subgraph L3[重 · 虚拟化层]
E[容器隔离<br/>Docker / Podman]
F[microVM<br/>Firecracker / Kata / gVisor]
end
L1 --> L2 --> L3下面逐个展开,重点说"它隔离了什么、代价是什么"。
2.1 解释器级软沙箱(AST 解析 + 白名单)——最轻,但"非安全边界"
代表:smolagents (Hugging Face 团队开发的开源极简 AI Agent 框架) 的本地执行器、各类 "Restricted Python"。做法是把代码解析成 AST,逐节点检查,只放行白名单内的模块和内置函数,其余一律拒绝。
- 隔离的是:模块与内置函数的联合面;
- 优点:零依赖、毫秒级、随开随用,适合"防小白误操作";
- 致命短板:它不是真正的安全边界——绕过解释器(比如利用对象内省、GC、星号导入的
__all__缺口)的手段多到数不清,官方文档自己都标注"This is not a security sandbox"。
结论:它适合当"护栏",不适合当"城墙"。 任何"代码会执行、且执行的是不可信输入"的场景,都不能只靠它。
2.2 进程级隔离——隔离最粗、最省事
把 Agent 的命令放进一个权限受限的子进程(低权限用户 / 独立 user),通过操作系统的权限模型来切断它的大多数能力。
- 隔离的是:可写路径(受 OS 账户权限限制);
- 优点:实现成本极低,系统自带;
- 短板:权限模型是"全局"的、粒度粗,进程里能做的事(读 /proc、发网络请求、占用端口)依然很多。更多是"降权"而非"隔离"。
2.3 系统调用过滤——seccomp / Landlock / seatbelt
seccomp / Landlock /seatbelt 三者都是操作系统内核层面的进程约束机制。在操作系统层面拦截进程发起的系统调用(syscall),只放行白名单内的调用。Linux 上 seccomp 是容器化之外最常用的"无侵入"过滤手段;Landlock 则提供文件系统层面的访问控制;macOS 上对应的机制叫 seatbelt(sandbox-exec)。
- 隔离的是:进程能触达的内核能力面;
- 优点:即使代码"跑起来了",也不能随意
mount、reboot、读内核模块; - 短板:配错会误伤正常系统调用导致程序诡异崩溃,生产上通常要"先跑一遍探针、再收紧规则"。通常不单独使用,而是叠在 "进程/容器" 里加一层。
2.4 文件系统视图隔离——bubblewrap(bwrap)
Linux 上最优雅的"文件系统级"沙箱思路:用 mount namespace + 只读绑定/遮蔽,给子进程一个"虚拟的根文件系统视图"——它以为自己在访问 /,其实只是看到了经你裁剪过的一份镜像。关键手法:
--ro-bind / /:把宿主根目录只读地绑进来;--tmpfs <path>:在某个目录上盖一张空内存盘——目录不是不可写,而是直接"缺席";--bind-try <path> <path>:再在遮蔽之上恢复精确的可写 carve-out。优点:不需要 root、不需要 daemon、单进程毫秒级拉起,非常适合"本机多 Agent 各管各的目录";
短板:只隔离文件系统视图,网络、进程、资源仍可共享宿主;macOS/Windows 场景没有 bwrap,需要 seatbelt 等替代。
2.5 容器隔离——Docker / Podman,当前的主流通用解
容器 = namespace(隔离视图:PID/网络/挂载/UTS/IPC)+ cgroup(隔离资源:CPU/内存/IO)+ overlayfs(隔离文件层)+ seccomp(隔离系统调用)。四个维度一起上,得到的是一个"像进程一样快、但拥有近乎独立操作系统视图"的执行单元。
- 优点:资源开销小、启动快(毫秒~秒级)、生态成熟(镜像即插即用)、宿主与容器天然分层;
- 短板:容器共享宿主机内核——内核漏洞可逃逸;镜像/运行时依赖要预置,离线环境麻烦;
- 谁是它的下一级?——microVM。
2.6 microVM——Firecracker / Kata / gVisor,隔离等级的"顶配"
microVM 是"瘦身到只剩内核 + 最小设备"的虚拟机:每个沙箱一个独立内核、独立内存地址空间,即使容器逃逸,也只是逃到一个"空壳"。AWS 的开源项目 Firecracker 就是为此而生,E2B 等专业沙箱厂商的底座就是它。
- 优点:虚拟机级的隔离强度 + 接近容器的启动速度(百毫秒级),隔离、资源、崩溃互不影响都是单点级;
- 短板:部署链路长(要镜像模板、要管理端),本机自托管成本高,通常是以"云服务"形态提供。
2.7 技术谱系速查表
| 技术 | 隔离的层面 | 启动速度 | 是否需要特权/daemon | 典型代表 |
|---|---|---|---|---|
| 解释器级(AST 白名单) | 模块/内置函数 | 极快 | 否 | smolagents Local、RestrictedPython |
| 进程级(受限用户) | OS 权限面 | 极快 | 视实现 | 各类子进程执行器 |
| seccomp/Landlock/seatbelt | 系统调用面 | 快 | 需内核支持 | Claude Code CLI、seatbelt |
| bubblewrap | 文件系统视图 | 快 | 否(user ns) | letta-code |
| 容器(Docker/Podman) | 视图+资源+文件层+syscall | 秒级 | 需 daemon | AgentScope、OpenClaw、smolagents |
| microVM(Firecracker) | 完整内核隔离 | 百毫秒级 | 需宿主管理面 | E2B、gVisor 类服务 |
给你的一句话选型:本地开发/自托管 → 容器打底;多 Agent 共机 → 容器 + 文件系统策略叠加;生产级不可信代码 → 直接上 microVM 服务,别自己造轮子。
三、用 Docker 做沙箱,宿主机要装好什么?先搞懂 Docker 的架构
用容器做沙箱的框架占了一大半(AgentScope、OpenClaw、smolagents、OpenHands 的 Docker 形态),它们"能不能跑起来",完全取决于宿主机。所以这一节把 Docker 依赖讲透。
3.1 Docker 是"Client-Server"架构,不是单个程序
一套 Docker 环境由两层组成:
mermaid
flowchart LR
SDK[Agent 框架<br/>docker API/SDK] --> CLI[`docker` CLI 客户端]
CLI --> DAEMON[`dockerd` 守护进程]
DAEMON --> RUNTIME[containerd + runc<br/>拉镜像/起容器/管理生命周期]
RUNTIME --> KERNEL[宿主机内核<br/>namespace cgroup overlayfs seccomp]- CLI(
docker命令):你敲命令用的客户端,它本身不干任何隔离的活; - 守护进程(
dockerd):真正创建/管理容器的服务,常驻后台; - containerd + runc:守护进程之下的容器运行时层,负责跟内核打交道。
关键含义:Agent 框架(或你)通过 docker CLI / SDK 只是发指令,真正"造出容器"的是守护进程。 也就是说:要跑容器沙箱,宿主机必须装好完整的 Docker 引擎(CLI + daemon),并确保 daemon 正在运行、当前账户能访问它。 只有 docker CLI 而没有 daemon,命令会直接报 Cannot connect to the Docker daemon。
3.2 三个高频认知误区
- "装了 Docker Desktop 就是装好了"——不一定。Docker Desktop(Windows/macOS)底层依赖虚拟化/WSL2,且守护进程默认不随开机自启、不常驻。Windows 上容器是跑在 WSL2 的 Linux 内核里的,这一层没就绪,任何"容器沙箱"都起不来;
- "daemon 必须在本机"——不一定。
DOCKER_HOST可以指向远程 daemon(走 Docker API),也就是说沙箱可以在另一台机器上被创建。生产上常有"开发机发指令、构建机建容器"的拓扑;但注意远程模式下镜像/依赖都落在远端; - "装完就能用 rootful"——不必然。daemon 通常需要较高权限;把用户加进
docker组是常见做法(等价于授予 root 级能力,安全性有争议),不想这样就用 rootless Docker(无特权用户模式)。部分 Agent 框架(如 Claude SDK)在"无特权的 Docker 环境"里会退而用一个更弱的嵌套沙箱,隔离强度打折——这是需要知道的隐含降级。
3.3 几个实操层面的依赖
- 镜像:容器沙箱启动要"拉镜像"或"用本地镜像"。离线/内网/自托管环境必须先预拉好镜像(
docker pull或自建 registry),否则框架会卡在"等镜像就绪"; - 版本:主流框架普遍要求较新的 Docker(老版本的 seccomp/overlayfs 能力不全);
- 资源:一个 Agent 容器可能同时开多个沙箱,内存/磁盘要按"峰值并发 × 沙箱占用"预留;
- 网络:默认容器可出网,若只想让沙箱内不可信代码不联网,需在创建参数里加
--network none(后面零成本清单会展开)。
结论就一句话:凡是用 Docker 做沙箱的框架,宿主机必须先装好并运行着 docker 引擎(守护进程 + CLI),保证当前账户可访问;否则容器起不来,框架要么报错、要么悄悄退回"无沙箱"裸跑——这可能比报错更危险。
四、框架怎么用沙箱?
下面进入 7 个框架的源码拆解。每个框架都按"它把沙箱抽象成什么 → 怎么用 → 有哪些注意 → 方案优劣"来展开,方便对照自己的场景做取舍。
4.1 AgentScope(Java):沙箱 = "可编程资源"
抽象:AgentScope 不给你一个开关,而是给你一套完整的生命周期接口。Sandbox 接口定义了一个沙箱"能启停、能执行命令、能打包/恢复工作区":
java
public interface Sandbox extends AutoCloseable {
void start() throws Exception; // 启动沙箱
void stop() throws Exception; // 停止沙箱
default void shutdown() throws Exception {
// 默认空实现:仅"自管理"沙箱需要销毁后端资源
}
boolean isRunning();
SandboxState getState(); // 可序列化的状态,支持快照
// 在沙箱工作区内执行一条 shell 命令
ExecResult exec(RuntimeContext runtimeContext,
String command,
Integer timeoutSeconds)
throws Exception;
InputStream persistWorkspace() throws Exception; // 工作区打包
void hydrateWorkspace(InputStream archive) throws Exception; // 恢复工作区
}怎么用:它默认实现 DockerSandbox,工作区就是容器内挂载的目录,docker exec 执行命令;App 内通过中间件感知任务生命周期,沙箱启停由框架自动编排,不用你手动记"什么时候 start、什么时候 stop"。工作区可以用 persistWorkspace/hydrateWorkspace 归档与恢复,配合一个约定好的阻塞接口 tryEnter(SandboxIsolationKey) 返回租约,做多 Agent 进场的并发控制:
java
@Override
public SandboxLease tryEnter(SandboxIsolationKey key) {
return SandboxLease.noop();
}注意事项:
- 它是"沙箱 = 资源"的完整工程范式,但默认绑定本地 Docker 守护进程——宿主机没装 Docker(见第三节),整个阶段直接跑不起来;
- 并发租约是并发控制,不是安全过滤,别把它当安全凭据用;
- 快照/恢复的价值要在"长任务"场景才兑现,短问答场景是纯开销。
优劣:优——生命周期+快照+租约齐全,是最完整的"沙箱当资源管"参考;劣——工程最重、部署成本高(要 Docker 引擎就绪),上手曲线陡。
mermaid
sequenceDiagram
participant A as Agent
participant M as SandboxLifecycleMiddleware
participant S as Sandbox
participant D as Docker 引擎
A->>M: 触发需要沙箱的任务
M->>S: start()
S->>D: 创建并启动容器
S-->>M: running
M->>S: exec(cmd)
S->>D: docker exec
D-->>S: stdout/stderr/exit
S-->>A: ExecResult
A->>M: 任务结束
M->>S: stop() / shutdown()适合谁:长会话、可恢复工作区型产品(Coding Agent、数据分析 Agent),愿意为"沙箱工程化"付部署成本。
4.2 Claude Agent SDK:沙箱 = "透传给 CLI 的开关"
抽象:它自己不实现任何沙箱运行时,只提供一个配置结构体,把"要不要沙箱、哪些命令例外"透传给一个独立进程:
python
class SandboxSettings(TypedDict, total=False):
"""Sandbox settings configuration.
控制 Claude Code 如何对 bash 命令做文件系统与网络隔离。
"""
enabled: bool # 是否启用 bash 沙箱(macOS/Linux)。默认 False
autoAllowBashIfSandboxed: bool # 沙箱内 bash 是否自动放行。默认 True
excludedCommands: list[str] # 沙箱外执行的命令,如 ["git", "docker"]
allowUnsandboxedCommands: bool # 是否允许绕过沙箱。默认 True
network: dict # 网络访问配置
ignoreViolations: list # 忽略的违规项
enableWeakerNestedSandbox: bool # 无特权的 Docker 环境用较弱沙箱(Linux)。默认 False怎么用:把 SandboxSettings 塞进 ClientOptions,SDK 用 subprocess 拉一个独立的 claude CLI 进程,把配置以 JSON 透传过去;SDK 层用 _ACTIVE_CHILDREN 跟踪子进程 + atexit 兜底回收孤儿 CLI。真正的 seccomp/网络隔离在闭源 CLI 内部完成。
注意事项:
- 默认
enabled=False——不显式开,等于本机裸跑; - 它 docstring 里有一句很诚实的话:“文件系统与网络限制实际上是通过 permission rules 配置的,不是通过这套 sandbox settings”——也就是说 SDK 发过去的是一组"意图",最终解释权在 CLI 黑盒;
- SDK 对"某条具体命令是否在沙箱里跑"没有代码级控制权,只能控制"整体开不开"、"哪些命令排除"。
优劣:优——接入成本最低、生态跟随官方 CLI 演进;劣——隔离能力完全依赖官方 CLI、离线/自托管受限、深度可控性为零。
mermaid
flowchart LR
APP[你的应用] -->|ClientOptions.sandbox| SDK[Claude Agent SDK]
SDK -->|subprocess 拉起独立 CLI| CLI[claude 进程]
CLI -->|seccomp / 网络策略| SH[沙箱化 bash]
SDK -->|_ACTIVE_CHILDREN + atexit| RC[兜底回收孤儿进程]适合谁:只想快速接官方生态、愿意接受"配置级控制"的集成方。
4.3 OpenAI Agents SDK:默认直跑 + 可插拔托管沙箱
抽象:把"本地直跑"和"沙箱执行"做成两条显式路径,中间夹一道审批门控。本地快、沙箱稳,由产品逻辑决定谁牛刀。
怎么用:函数调用分发处两分支——LocalShellAction(同进程直接执行、无隔离)走本地;ShellAction 才进沙箱会话。进沙箱后,SandboxSession 提供完整的会话抽象(exec / PTY 交互 / 端口暴露 / 路径校验 / 快照持久化恢复),后端可插拔:
python
@dataclass
class SandboxSessionState:
"""对话级沙箱的状态:session_id、workspace_id、snapshot_id、资源……"""
...
class BaseSandboxSession:
async def exec(self, *command, timeout=None, shell=True, user=None) -> ExecResult: ...
async def pty_exec_start(self, *command, ..., tty=False, ...) -> PtyExecUpdate: ...
async def pty_write_stdin(self, *, session_id, chars) -> None: ...
# 路径安全校验、快照持久化/恢复、工作区清单等默认后端是 DockerSandboxSession,也可以换 E2B / Modal / Daytona / Cloudflare 等云端后端(把 SandboxSession 换成对应云沙箱的适配实现):
python
class DockerSandboxSession(BaseSandboxSession):
_docker_client: DockerSDKClient
_container: Container
state: DockerSandboxSessionState
# exec / pty / snapshot / workspace 挂载 全部基于 container 实现注意事项:
- 零配置 ≠ 零风险,零配置 = 无沙箱。
LocalShellAction默认就是本机裸跑,不要把"默认"默认为安全; - 审批门控(
needs_approval / approval)是行为控制,决定"哪些命令值得进沙箱",它不做环境隔离——只开审批、不开沙箱,命令还是在本机执行; - Docker 后端同样吃第三节说的宿主依赖;改云端后端则把镜像/网络职责交给了云厂商。
优劣:优——"低延迟本地"与"高隔离远程"可动态切换,架构上把"到底隔不隔离"上升为可编程决策;劣——默认裸跑,安全需要产品侧主动配置,网关逻辑(什么时候该进沙箱)要你自己维护。
mermaid
flowchart LR
LLM[模型] -->|shell 工具调用| RUN[run loop]
RUN --> GATE{审批门控<br/>needs_approval / approval}
GATE -->|本地| LSA[LocalShellAction<br/>同进程直跑 · 无隔离]
GATE -->|沙箱| SA[ShellAction]
SA -->|exec / pty| SESS[SandboxSession 会话]
SESS -->|Docker| DOCK[DockerSandboxSession]
SESS -->|云端| CLOUD[E2B / Modal / Daytona / Cloudflare]
SESS -->|snapshot| SNAP[快照持久化 / 恢复]适合谁:需要在"本地低延迟"和"远程高隔离"之间动态取舍的产品,能接受"安全靠配置、不靠默认"的产品团队。
4.4 OpenClaw:沙箱 = "agent 的默认住所"
抽象:一个开源助手运行时,把沙箱从"可选"提升为"默认"——bash/文件操作默认关进容器,宿主执行反而是需要显式提权的"后门"。
怎么用:沙箱是可注册的 backend 工厂 + 可插拔容器引擎(Docker / Podman)。核心原语 ensureSandboxContainer 语义是"容器不存在就创建、存在就复用"。执行目标由 resolveExecTarget 决定:requestElevated 默认关,想拿宿主权限必须显式打开并连续通过 enabled、allowFrom 两道门;出错时错误信息把失败的门逐条列给你:
ts
const gates: string[] = [];
if (!elevatedDefaults?.enabled) {
gates.push("enabled (tools.elevated.enabled / agents.entries.*.tools.elevated.enabled)");
} else {
gates.push("allowFrom (tools.elevated.allowFrom.<provider> / ...)");
}
throw new Error([
`elevated is not available right now (runtime=${runtime}).`,
`Failing gates: ${gates.join(", ")}`,
...
].filter(Boolean).join("\n"));运行时标签本身就是立场声明:
ts
const runtime = defaults?.sandbox ? "sandboxed" : "direct";注意事项:
- "默认进容器"意味着出厂就在吃 Docker 依赖——宿主机引擎不就绪,这个框架的多数功能直接不可用;
- 提权双门(enabled + allowFrom)是它最重要的安全细节,别为了省事把
elevated默认打开; - Podman 与 Docker 引擎各有生态差异(socket、卷格式),切换引擎前先确认宿主机装的是哪个。
优劣:优——"默认安全 + 可解释的显式提权"是终端产品(命令行助手类)最值得抄的模板;劣——默认容器化对轻量场景偏重,非容器环境的集成成本高。
mermaid
flowchart TD
CMD[bash / 文件工具调用] --> ELEV{requestElevated?}
ELEV -->|是| GATE{门控全过?<br/>enabled + allowFrom}
GATE -->|否| ERR[抛错:elevated 不可用<br/>并提示修复 key]
GATE -->|是| TGT[resolveExecTarget]
ELEV -->|否| TGT
TGT -->|sandboxRequired 或 sandboxAvailable| SBX[容器引擎<br/>Docker / Podman]
TGT -->|否则| LCL[宿主直接执行]适合谁:追求"默认安全"的终端用户产品,特别是命令行助手、本机 agent 类应用。
4.5 smolagents:沙箱 = "可换的旋钮"
抽象:HuggingFace 的 smolagents 把代码执行边界做成一个统一抽象 PythonExecutor,本地与远程各有一片实现,"换沙箱"= 改一个枚举值:
python
class PythonExecutor(ABC):
@abstractmethod
def send_tools(self, tools): ...
@abstractmethod
def send_variables(self, variables): ...
@abstractmethod
def __call__(self, code_action: str) -> CodeOutput: ...怎么用:本地是 LocalPythonExecutor(解释器级软沙箱:AST 逐节点解释 + import 白名单);远程是四件套 DockerExecutor / E2BExecutor / ModalExecutor / BlaxelExecutor,一行 executor_type 切换。DockerExecutor 会完整走一遍"构建镜像 → 映射 8888 端口 → 注入随机鉴权 token(secrets.token_urlsafe(16),不是写死)→ 启动容器 → 等 Jupyter 就绪 → HTTP 建 kernel → websocket 传代码"的链路:
python
try:
from e2b_code_interpreter import Sandbox
except ModuleNotFoundError:
raise ModuleNotFoundError("Please install 'smolagents[e2b]' to use E2BExecutor")
# Support both e2b v1 and v2 constructors
# v2 exposes Sandbox.create(...), while v1 uses Sandbox(...)
if hasattr(Sandbox, "create"):
self.sandbox = Sandbox.create(**kwargs)
else:
self.sandbox = Sandbox(**kwargs)注意事项:
- LocalPythonExecutor 不是安全沙箱——它 docstring 自我声明:"This executor evaluates Python code with restricted access to imports and built-in functions. It is not a security sandbox"(不是安全沙箱,要隔离不可信代码请用远程执行器)。它拦截的是"别让小白误操作",不是"挡住恶意代码";
- 远程 .env 里的 API key 别传进沙箱/镜像——smolagents 官方文档明确"key 不进沙箱";
- 它对 import 白名单外直接抛
InterpreterError(而非SecurityError)——读代码时看到这个类名就应明白定位了。
优劣:优——隔离成本最低、切换粒度最爽(本地/容器/microVM 一个枚举搞定)、DockerExecutor 的工程细节(随机 token、独立 kernel)是教科书;劣——本地执行根本不可信,远程模式才安全,容易"图省事用了本地模式还以为是安全的"。
mermaid
flowchart TD
SRC[Python 源码] --> AST[AST 解析为语法树]
AST --> OP{逐个语法节点}
OP -->|Import| CHK{模块在<br/>authorized_imports?}
CHK -->|是| MOD[拷贝为安全模块<br/>递归处理子模块]
CHK -->|否| BLK[抛 InterpreterError<br/>拒绝执行]
OP -->|内置函数| SAFE[受限 builtin 集]
OP -->|其它操作| EXEC[受限执行 + 超时]适合谁:要在"最低成本"和"可切换隔离"之间平衡的项目;把"本地救急 + 远程兜底"理解到位再用。
4.6 OpenHands:2026 已是 Agent Canvas,沙箱是"后端黑盒"
抽象:2026 年的 OpenHands (原 OpenDevin)已经重构为 Agent Canvas(React + TS 前端 + 外部 Agent Server),架构文档明确写:Agent Canvas 不负责"提供沙箱或工作区隔离层"。也就是说——这个仓库根本不承担沙箱实现,隔离边界完全由部署形态决定:
mermaid
flowchart TB
subgraph D1[Option 1: 无沙箱]
S1[agent-server 直接在主机运行]
S2[官方警告:agent 将完全访问你的文件系统]
end
subgraph D2[Option 2: Docker 沙箱]
S3[docker run 挂载 ~/.openhands<br/>和 PROJECTS_PATH]
S4[容器 = 沙箱边界<br/>agent 只能看到 PROJECTS_PATH]
end
subgraph D3[Cloud 托管]
S5[前端 V1SandboxStatus 状态机<br/>STARTING / RUNNING / PAUSED / ERROR / MISSING]
S6[官方断言:Cloud sandboxes are always isolated]
end
OH[OpenHands Agent Canvas] --> D1
OH --> D2
OH --> D3怎么用:想本地隔离,就别用"无沙箱"形态——用 docker run 把 Agent Server 关进容器,只挂载 ~/.openhands 和 PROJECTS_PATH,让 agent 只能看到项目目录;要省心就上 Cloud 托管(官方声明沙箱永远隔离),前端通过 V1SandboxStatus 状态机(STARTING/RUNNING/PAUSED/ERROR/MISSING)观测沙箱事件。
注意事项:
- 别用历史架构冒充现状:网络上大量
openhands/runtime/docker_runtime.py、DockerRuntime ↔ ActionExecutionServer的分析都是过时的,2026 年仓库顶层已经是 Canvas 结构; - "无沙箱"形态是存在且有官方警告的——你选了什么部署形态,隔离边界就是什么,没有中间态;
- 容器形态下隔离只覆盖到挂载点,
PROJECTS_PATH之外(如网络、其它卷)是否隔离取决于你的 docker 命令参数,别默认全会隔离。
优劣:优——部署形态决定边界的模型很清晰,Cloud 形态隔离由托管侧提供;劣——开源形态的"自带沙箱"承诺很弱,真正拿到强隔离要么自己配 Docker、要么上托管,自托管方对 Agent Server 内部运行的代码没有逐条拦截能力。
适合谁:能接受"选形态即选边界",或者愿意用托管服务的团队。
4.7 letta-code:罕见的"OS 内核级"文件系统沙箱
抽象:Letta 的 V1 Python 仓库已归档,现在是 TS 的 letta-code。它的沙箱思路是八个框架里最独特的——不靠 Docker、不靠 E2B,直接在 OS 内核层面做文件系统策略隔离。核心是一个纯路径策略结构:
ts
export interface FsSandboxPolicy {
baseWritableRoots: string[]; // 基础可写根(大范围)
deniedRoots: string[]; // 禁止根(被遮蔽,读不到也枚举不到)
readonlyRoots: string[]; // 只读根
writableRoots: string[]; // 可写根(最精确)
restrictWrites: boolean; // 是否全局只读根视图
}怎么用:策略优先级是"大范围放行 → 被嵌套禁止覆盖 → 又被更精确的自画像覆盖"(官方注释:broad base carve is overridden by a nested deny, which is in turn overridden by a still-more-specific self carve)。执行层在 Linux 上翻译成 bubblewrap 参数,buildBwrapArgs 把策略落到系统调用:
ts
export function buildBwrapArgs(policy: FsSandboxPolicy): string[] {
const args: string[] = [];
// 根视图:写作用户只读绑定,跨 agent 可写
args.push(policy.restrictWrites ? "--ro-bind" : "--bind", "/", "/");
// 最小可写 /dev 与全新 /proc,避免泄漏主机进程状态
args.push("--dev", "/dev");
args.push("--proc", "/proc");
// 基础可写根:在只读根上重新绑定(位于遮蔽之前)
for (const root of policy.baseWritableRoots) {
args.push("--bind-try", root, root);
}
// 用空 tmpfs 遮蔽每个 deniedRoot —— 目录不仅不可写,而是"缺席"
for (const root of policy.deniedRoots) {
args.push("--tmpfs", root);
}
// 在遮蔽之上恢复精确的只读/可写 carve-out(-try 容忍目录尚未创建)
for (const root of policy.readonlyRoots) {
args.push("--ro-bind-try", root, root);
}
for (const root of policy.writableRoots) {
args.push("--bind-try", root, root);
}
// 沙箱随父进程一起消亡
args.push("--die-with-parent");
return args;
}注意事项:
- bwrap 是 last-mount-wins:carve-out 必须是 denied root 的后代或不相交,绝不能是祖先,否则会把整个被遮蔽的子树重新绑回去、静默暴露 deniedRoots(源码注释里原话点破:An ancestor carve-out ... re-binds the whole subtree on top of the mask and silently re-exposes the denied roots)。这是"写一次就踩坑"才知道的;
- 哨兵环境变量
LETTA_SANDBOX:已入沙箱的子进程借此识别"自己已被包过",re-exec 时避免二次包裹——对嵌套 Agent 系统至关重要; - 默认值不统一:
memory-subagent(记忆子代理)默认沙箱化(isFsSandboxEnabled默认 true),而 agent 自身 shell 默认不沙箱(LETTA_FS_SANDBOX=1才开)。同一套机制,按数据敏感度给不同默认值——这是很值得抄的设计; - 只做文件系统视图隔离:网络、CPU、内存仍共享宿主,别拿它当"完整沙箱"。
优劣:优——本机多 Agent 场景的"目录级缺席"方案成本最低(无 root、无 daemon、毫秒级),--die-with-parent+哨兵是任何认真做沙箱的实现都不该省略的保险丝;劣——Linux 专属(bwrap),macOS 需 seatbelt,Windows 无对应;隔离维度只有文件系统。
mermaid
flowchart LR
CFG[配置] --> POL[FsSandboxPolicy<br/>baseWritable / denied / readonly / writable]
POL --> BACK{后端}
BACK -->|Linux| BW[buildBwrapArgs<br/>组装 bwrap 参数]
BACK -->|macOS| SB[buildSeatbeltProfile]
BW --> ARGS[--ro-bind / /<br/>--tmpfs 遮蔽 denied<br/>--bind-try 恢复 carve]
SB --> ARGS2[seatbelt profile]
ARGS --> PROC[隔离子进程]
ARGS2 --> PROC
PROC --> ENV[哨兵环境变量<br/>LETTA_SANDBOX]适合谁:多 Agent 共宿一机的自托管服务,追求低成本的目录互相隔离;以及追求"策略即配置、透明可审计"的团队。
六、对比总表
| 框架 | 默认隔离 | 隔离运行时 | 抽象模型 | 快照/续跑 | 方案优劣一句话 |
|---|---|---|---|---|---|
| AgentScope-Java | Docker(默认) | Docker / E2B 扩展 | Sandbox 接口 + 模板方法 + 租约 | 有(persist/hydrate) | 最完整但最重,绑定本地 daemon |
| Claude Agent SDK | 默认关 | 透传 CLI,seccomp 在闭源 CLI 内 | 配置 TypedDict,无运行时抽象 | 有(session resume) | 接入最省心,深度可控性为零 |
| OpenAI Agents SDK | 本机直跑(无隔离) | 显式 SandboxSession / 云端 E2B/Modal | BaseSandboxSession + 审批门控 | 有(snapshot) | 本地/远程可动态切,但默认裸跑 |
| OpenClaw | Docker/Podman(默认) | Docker / Podman 可切换 | backend 工厂 + 容器引擎 + 提权双门 | 有(会话 → 容器 registry) | 默认安全+可解释提权,最值得抄 |
| smolagents | 本地解释器(危险) | Docker / E2B / Modal / Blaxel | PythonExecutor 抽象 + 远程四件套 | 无(每次新 VM) | 切换最爽,但本地实现非安全边界 |
| OpenHands (Agent Canvas) | 默认无沙箱 | Docker 容器 / Cloud 托管 / 本机 | 前端不负责隔离,边界 = 部署形态 | 有(cloud 状态机) | 边界随形态变,选形态即选边界 |
| letta-code | memory 子代理默认沙箱,shell 默认不沙箱 | 内核 bwrap/seatbelt + Letta Cloud | FsSandboxPolicy 纯路径规则 | 有(cloud 沙箱) | 目录级隔离成本最低,但只隔文件系统 |
七、批判性视角:剥开源码后得到的五个结论
1. "默认危险"才是常态
别被宣传话术骗了。smolagents / AutoGen / LangChain / OpenAI SDK 的零配置默认都是本机或解释器直接执行,官方文档措辞再委婉,翻译过来都是"在你机器上裸奔"。真正默认容器化的只有 AgentScope、OpenClaw 和 OpenHands 的 Docker 部署形态。任何让 AI 跑代码的产品,先问一句:它默认关在笼子里吗?
2. 隔离粒度分两派,文档要写清"隔离的是哪一层"
有的隔离"整个执行环境"(OpenHands 容器、整个 Agent 系统入沙箱),有的只隔离"一段代码"(smolagents 档 1、AutoGen 代码块),有的只隔离"文件系统 + 快照"(AgentScope、letta)。粒度不同,安全假设就不同——只隔离代码块的方案,工具调用本身还是在你机器上跑。如果你做产品,请在文档里写清楚你隔离的是哪一层。
3. 凭证泄漏是第二战场
沙箱隔开了代码,但没隔开 API key。Letta 用运行时注入 env 最小化泄漏面、smolagents 文档明确"key 不进沙箱"、而整系统沙箱反而可能需要把凭证传进沙箱——这是最容易被忽略的泄露通道。沙箱内部的 Agent,不该知道你的 OpenAI key。
4. 云端底座正在收敛到 Firecracker
本地测完再看市场:E2B/Firecracker 几乎是所有"生产级沙箱"的公共底座。这不是巧合,是"隔离强度"和"启动速度"两个指标下 microVM 的最优解。自己造沙箱之前,先确认能不能直接用 E2B 或等价的 microVM 底座;自托管的,建议先落到容器,别一上来就造轮子。
5. 沙箱价值的另一半是"续跑"
几乎所有框架都实现了会话级工作区持久化/恢复:AgentScope 的 persist/hydrate、OpenAI 的 snapshot、Claude SDK 的 session resume、OpenClaw 的会话→容器 registry、OpenHands 的 cloud 状态机。因为 Agent 任务是长周期的,沙箱不光要"关住 AI",还要保证"AI 重启后还能想起自己干到哪"。忘了续跑能力的沙箱,只完成了一半工作。
八、零成本落地:本地最小安全配置清单
看完源码,给想自己搭 Agent 的读者一份不花钱的兜底配置——注意第一步就是先把第三节说的 Docker 依赖备齐:
- 先把容器引擎备齐:装好 Docker(CLI + daemon 都在、能
docker ps),离线环境先预拉好底包镜像;不想要 root 权限就把自己加入docker组或用 rootless Docker; - 本地默认起一个空壳容器再让代码进去跑,镜像别用大而全的,
python:3.12-slim起步; - 容器加三条硬约束:
--network none(禁外联)、--memory 512m --cpus 1(限资源)、--cap-drop ALL(去能力),需要网络再单独加 allowlist; - 不要试图在本地"安全解释器"上硬刚——smolagents 作者都承认它非完备,本地只能当救急;真正不可信代码直接走容器或 microVM;
- 沙箱内外分开记账:凭证只放宿主侧,经环境变量按需注入,绝不写死进镜像或工作区文件(这是 letta 和 smolagents 文档共同强调的);
- 给 agent 一个独立的
PROJECTS_PATH(借鉴 OpenHands 的做法),让它只能看到该看的项目目录,而不是整个~; - 多人共机跑多 agent,参考 letta:用内核级文件系统策略把各自的目录互相"屏蔽",而不是指望每个 agent 自觉;
- 别忘了"续跑":任何沙箱方案都要同时给出工作区快照/恢复的手段,否则长任务一断就前功尽弃。
九、结论
把这 7 个框架的沙箱源码梳理完,不难得到这样的判断:"沙箱"是解释器级 / 进程级 / 系统调用级 / 文件系统级 / 容器级 / microVM 级这一条技术谱系在不同成本约束下的取舍。而当安全边界真的重要时,大家的收敛方向惊人的一致:容器/microVM 隔离 + 工作区快照续跑 + 凭证不进场。同时,凡是走容器路线的框架,都绕不开那个前置条件——宿主机先把 Docker 引擎备好、把 daemon 跑起来。


