Skip to content
围炉聊科技微信公众号二维码关注公众号,获取新文章推送
添加我为微信朋友扫描二维码,添加我为朋友

智能体沙箱是怎么做的:跨框架源码拆解

前不久OpenAI的大模型从沙箱逃逸出来并且攻击了Hugging Face的新闻大家想必是看到了,那么智能体沙箱是怎么做的,我们在做智能体基建的时候是否需要建沙箱、怎么建沙箱?为了回答这个问题,我让AI拉取了 8 个主流框架的源码,逐一检视它们的沙箱实现来学习。


一、Agent 沙箱到底是什么?为什么必须有?

1.1 什么是沙箱

Agent 沙箱,是一层"能挡副作用"的执行环境。 它包住"Agent 干活"这件事本身——Agent 在里面写文件、跑命令、执行代码,但这些操作的作用域被限制在沙箱之内,宿主机器和宿主数据不受影响。

1.2 为什么 LLM 时代它成了刚需

  • 指令与代码的边界模糊:模型的训练目标是"忠实地执行指令",而指令里可能恰好包含"删除这个目录"。它不是故意使坏,而是真的分不清"该做的"和"不该做的";
  • 命令有级联效应rm -rfcurl ... | shchmod -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)。

  • 隔离的是:进程能触达的内核能力面;
  • 优点:即使代码"跑起来了",也不能随意 mountreboot、读内核模块;
  • 短板:配错会误伤正常系统调用导致程序诡异崩溃,生产上通常要"先跑一遍探针、再收紧规则"。通常不单独使用,而是叠在 "进程/容器" 里加一层。

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秒级需 daemonAgentScope、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 三个高频认知误区

  1. "装了 Docker Desktop 就是装好了"——不一定。Docker Desktop(Windows/macOS)底层依赖虚拟化/WSL2,且守护进程默认不随开机自启、不常驻。Windows 上容器是跑在 WSL2 的 Linux 内核里的,这一层没就绪,任何"容器沙箱"都起不来;
  2. "daemon 必须在本机"——不一定。DOCKER_HOST 可以指向远程 daemon(走 Docker API),也就是说沙箱可以在另一台机器上被创建。生产上常有"开发机发指令、构建机建容器"的拓扑;但注意远程模式下镜像/依赖都落在远端;
  3. "装完就能用 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 默认关,想拿宿主权限必须显式打开并连续通过 enabledallowFrom 两道门;出错时错误信息把失败的门逐条列给你:

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 关进容器,只挂载 ~/.openhandsPROJECTS_PATH,让 agent 只能看到项目目录;要省心就上 Cloud 托管(官方声明沙箱永远隔离),前端通过 V1SandboxStatus 状态机(STARTING/RUNNING/PAUSED/ERROR/MISSING)观测沙箱事件。

注意事项

  • 别用历史架构冒充现状:网络上大量 openhands/runtime/docker_runtime.pyDockerRuntime ↔ 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-JavaDocker(默认)Docker / E2B 扩展Sandbox 接口 + 模板方法 + 租约有(persist/hydrate)最完整但最重,绑定本地 daemon
Claude Agent SDK默认关透传 CLI,seccomp 在闭源 CLI 内配置 TypedDict,无运行时抽象有(session resume)接入最省心,深度可控性为零
OpenAI Agents SDK本机直跑(无隔离)显式 SandboxSession / 云端 E2B/ModalBaseSandboxSession + 审批门控有(snapshot)本地/远程可动态切,但默认裸跑
OpenClawDocker/Podman(默认)Docker / Podman 可切换backend 工厂 + 容器引擎 + 提权双门有(会话 → 容器 registry)默认安全+可解释提权,最值得抄
smolagents本地解释器(危险)Docker / E2B / Modal / BlaxelPythonExecutor 抽象 + 远程四件套无(每次新 VM)切换最爽,但本地实现非安全边界
OpenHands (Agent Canvas)默认无沙箱Docker 容器 / Cloud 托管 / 本机前端不负责隔离,边界 = 部署形态有(cloud 状态机)边界随形态变,选形态即选边界
letta-codememory 子代理默认沙箱,shell 默认不沙箱内核 bwrap/seatbelt + Letta CloudFsSandboxPolicy 纯路径规则有(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 依赖备齐:

  1. 先把容器引擎备齐:装好 Docker(CLI + daemon 都在、能 docker ps),离线环境先预拉好底包镜像;不想要 root 权限就把自己加入 docker 组或用 rootless Docker;
  2. 本地默认起一个空壳容器再让代码进去跑,镜像别用大而全的,python:3.12-slim 起步;
  3. 容器加三条硬约束:--network none(禁外联)、--memory 512m --cpus 1(限资源)、--cap-drop ALL(去能力),需要网络再单独加 allowlist;
  4. 不要试图在本地"安全解释器"上硬刚——smolagents 作者都承认它非完备,本地只能当救急;真正不可信代码直接走容器或 microVM;
  5. 沙箱内外分开记账:凭证只放宿主侧,经环境变量按需注入,绝不写死进镜像或工作区文件(这是 letta 和 smolagents 文档共同强调的);
  6. 给 agent 一个独立的 PROJECTS_PATH(借鉴 OpenHands 的做法),让它只能看到该看的项目目录,而不是整个 ~
  7. 多人共机跑多 agent,参考 letta:用内核级文件系统策略把各自的目录互相"屏蔽",而不是指望每个 agent 自觉;
  8. 别忘了"续跑":任何沙箱方案都要同时给出工作区快照/恢复的手段,否则长任务一断就前功尽弃。

九、结论

把这 7 个框架的沙箱源码梳理完,不难得到这样的判断:"沙箱"是解释器级 / 进程级 / 系统调用级 / 文件系统级 / 容器级 / microVM 级这一条技术谱系在不同成本约束下的取舍。而当安全边界真的重要时,大家的收敛方向惊人的一致:容器/microVM 隔离 + 工作区快照续跑 + 凭证不进场。同时,凡是走容器路线的框架,都绕不开那个前置条件——宿主机先把 Docker 引擎备好、把 daemon 跑起来