Skip to main content

04 - 开源产品怎么做

前置02 篇的三条路线。

本篇回答:两个生产级开源 Agent 产品,各自把沙箱边界划在哪里、为什么划在那里。

本篇会用到的词

意思
workspaceOpenHands 的抽象:把「Agent 在哪里干活」定义成一个接口,底下可以换成本机、容器、远端服务等不同实现
apptainer一种免 root 的容器方案,普通用户不需要管理员权限就能跑,在高性能计算集群里用得多
OCIOpen Container Initiative,容器镜像与运行时的标准。符合它的运行时可以互相替换
沙箱内代理服务跑在沙箱里、代表宿主执行操作的那个进程(E2B 叫 envd,OpenHands 叫 sandbox-agent-server)
协议边界宿主与沙箱之间只通过一组明确定义的消息通信,宿主不直接读写沙箱内部。边界越窄,缺口越少

一、OpenHands:把 workspace 做成可替换的抽象

OpenHands/software-agent-sdk(★1,008,MIT)的做法是不选一种沙箱,而是把"Agent 在哪里干活"抽象成 workspace,提供五种实现

不选一种沙箱,而是把「Agent 在哪里干活」抽象成一个接口Agent只依赖 workspaceWorkspace 接口执行命令 · 读写文件管理 git 工作树local   直接在本机跑,没有隔离 —— 只适合本地开发docker  容器隔离,最常用apptainer 免 root 容器,HPC 环境用得多remote_api 调用远端沙箱服务cloud   官方托管这个抽象的价值在于:换沙箱不用改 Agent 代码。本地开发用 local,CI 里用 docker,生产上换成 remote_api,业务逻辑一行都不动。
注意最上面那一行是没有隔离的 —— 它存在的意义是让开发时不必先把沙箱环境搭起来,但一旦跑到线上,它就是一个赤裸的执行面。

对应的源码位置与体量:

后端路径大小
localopenhands-sdk/openhands/sdk/workspace/local.py7 KB
dockeropenhands-workspace/openhands/workspace/docker/workspace.py15 KB
apptaineropenhands-workspace/openhands/workspace/apptainer/workspace.py15.5 KB
remote_apiopenhands-workspace/openhands/workspace/remote_api/workspace.py16 KB
cloudopenhands-workspace/openhands/workspace/cloud/workspace.py37 KB
远端基类openhands-sdk/openhands/sdk/workspace/remote/base.py36 KB

1.1 apptainer 的存在说明了什么

五个后端里最不常见的是 apptainer。它是 HPC(高性能计算)领域的容器运行时,最大特点是不需要 root 权限就能运行容器

这个后端的存在意味着 OpenHands 认真对待一类用户:在大学或研究机构的计算集群上跑 Agent 的人。那些环境里管理员不会给你 Docker daemon 的访问权限,而 Docker 需要一个以 root 运行的守护进程。

可迁移的判断:如果你的沙箱方案强依赖 Docker daemon,就自动排除了所有共享集群场景。免 root 的容器运行时(apptainer、podman rootless)是这类环境的唯一出路。

1.2 local 后端不是遗留代码

local 意味着完全不隔离 —— Agent 直接在你的机器上执行命令。

这看起来与本专题的主题相悖,但它对应的正是 01 篇第四节说的权限路线:本地编码 Agent 要在你真实的项目目录里改文件,隔离了就没有意义。

两条路线在同一个产品里并存,由用户按场景选择。这比"我们只支持沙箱"要诚实。

1.3 cloud 后端最大不是偶然

37 KB,是 docker 后端的两倍多。多出来的部分不是隔离逻辑,而是托管服务才需要的东西:认证、配额、生命周期管理、凭证传递、状态同步。

这个体量差可以作为自建时的成本参照:隔离本身不难,难的是把它变成一个多租户服务。

二、Suna:把代理服务放进沙箱

kortix-ai/suna(★20,119)的架构选择不同。它有一个独立的应用叫 kortix-sandbox-agent-server

文件大小职责
src/main.ts120 KB主服务
src/opencode.ts98 KB代码编辑能力
src/git.ts60 KBgit 操作
src/routes/web-proxy.ts28 KB沙箱内的网页代理
src/monitor-runner.ts27 KB运行监控

宿主侧则有对应的组件:

文件大小职责
apps/api/src/sandbox-proxy/routes/preview.ts83 KB预览流量代理
apps/api/src/platform/services/session-sandbox.ts53 KB会话与沙箱的绑定
apps/api/src/projects/sandbox-turn-lifecycle.ts43 KB按对话轮次管理沙箱生命周期
apps/api/src/projects/lib/sandbox-env-sync.ts57 KB环境变量同步
packages/shared/src/sandbox/dockerfile-layer.ts59 KB镜像分层

2.1 这个架构与 E2B 的 envd 同构

沙箱里跑一个服务、宿主通过明确的协议与它通信 —— 这与 03 篇里 E2B 的 envd 是同一个思路。

两个独立项目做出同样的选择,说明这是这类系统的收敛解

宿主侧沙箱内会话与沙箱绑定生命周期管理:创建 · 暂停 · 销毁流量代理明确定义的协议宿主不直接伸进沙箱沙箱内的代理服务envd / sandbox-agent-server执行命令 · 读写文件git 操作 · 网页代理E2B 和 OpenHands 是两个独立项目,却做出了同样的结构选择 —— 沙箱里跑一个自己的代理服务,宿主只通过协议和它说话。这通常意味着这是这类系统的收敛解。
为什么不让宿主直接操作沙箱内部:一旦宿主需要伸进去(挂载、注入、直接读写),隔离边界就出现了缺口,而这个缺口的宽度等于宿主代码的复杂度。

为什么不让宿主直接操作沙箱? 因为那需要宿主具备"进入任意沙箱执行命令"的能力 —— 这个能力本身就是攻击目标。走协议接口则边界清晰:宿主只能做协议里定义的事。

2.2 sandbox-turn-lifecycle:按对话轮次管理

这个文件名直接回答了 01 篇 3.2 节提的状态保留问题:沙箱的生命周期绑定到对话轮次,而不是绑定到单次代码执行。

这印证了那一节的判断 —— 会话级复用是 Agent 场景的主流选择。

仓库里还有一份 docs/superpowers/plans/2026-08-08-sandbox-mid-session-stop.md(53 KB),专门讨论「会话中途停止沙箱」这个场景。一个 53 KB 的设计文档只为解决"中途停下来怎么办",说明这件事在生产上比想象中麻烦:停的时候有正在跑的进程、有未落盘的文件、有等待中的请求。

2.3 web-proxy 的位置很关键

Suna 在沙箱内部放了一个网页代理(routes/web-proxy.ts),宿主侧还有一个预览代理(sandbox-proxy/routes/preview.ts,83 KB)。

这解决的是一个具体需求:Agent 在沙箱里起了一个开发服务器,用户要能在浏览器里看到它。流量路径是:

用户浏览器 → 宿主预览代理 → 沙箱内 web-proxy → 沙箱内的开发服务器

这也是网络出口管控的天然位置01 篇 3.1 节说沙箱最容易漏掉网络出口 —— 而一旦所有流量都必须经过一个自己实现的代理,白名单和审计就有了落点。

三、两种边界划法的对比

OpenHandsSuna
核心抽象workspace,五种实现可替换沙箱内代理服务
隔离强度由所选后端决定(local 无隔离)容器 + 自建代理层
生命周期跟随会话绑定对话轮次,支持中途停止
网络管控由后端环境决定自建代理,可管控
面向场景本地开发 + 托管两条路并存托管产品

3.1 抽象层的价值与代价

OpenHands 的 workspace 抽象让用户可以从 local 起步、需要隔离时换成 docker、上集群时换成 apptainer、要托管时换成 cloud —— 代码不改

代价是抽象必须取五种后端的能力交集。local 能做到的(直接访问宿主 git 配置、复用已装好的工具链)在 docker 里做不到,抽象就只能取更弱的那一侧,或者引入后端专属的分支。

判断依据:如果你的产品只有一种部署形态,抽象层是纯成本;如果用户会跨形态迁移,它就是核心资产。

下一篇05 - 选型与落地

← 回到 专题索引  ·  Agent Infra 板块总览