跳到主要内容

OpenSandbox — 架构与原理

30 秒导读: OpenSandbox 是阿里开源的通用沙箱平台,给 AI 应用(编码 agent、GUI agent、代码执行、RL 训练)提供「安全、可编程、可大规模调度的一次性电脑」。它的核心是两个平面的分离:一套 OpenAPI 协议把「谁来管容器的生老病死」(控制面)和「怎么在容器里跑命令/读写文件/执行代码」(数据面)彻底切开——你换掉底层是 Docker 还是 Kubernetes,上层的执行 API 一字不变。


1. 这是什么(零基础也能懂)

一句话定义: OpenSandbox 是一个「沙箱即服务」平台——你发一个 HTTP 请求,它给你一台隔离的、临时的、装好环境的容器;你再发请求,就能在里面跑 shell 命令、执行 Python、读写文件、开终端(PTY)。

解决什么问题 / 给谁用: 假设你在写一个 AI agent,它会生成代码并想「真的跑一下」。你绝不敢让模型生成的代码直接在你的服务器上执行——它可能 rm -rf、可能偷你的密钥、可能挖矿。于是你需要一个用完即弃的隔离环境。OpenSandbox 就是把「造这样一台环境、在里面执行、然后销毁」这件事标准化成协议 + 运行时,让你不用自己去拼 Docker API、iptables、seccomp 这些底层。

它能做什么(功能):

  • 用统一 API 创建 / 暂停 / 恢复 / 快照 / 销毁沙箱,底层可选 Docker(本地)或 Kubernetes(大规模分布式调度)。
  • 在沙箱里执行命令、跑代码解释器(Jupyter 内核)、读写/搜索/替换文件、开交互式 PTY 终端
  • 给沙箱套更强的隔离:既能用 gVisor / Kata / Firecracker 这类安全容器运行时,也能在容器内部再用 bubblewrap 套一层「可回滚」的嵌套沙箱。
  • 网络进出:统一入站网关(ingress)按沙箱 ID 路由,逐沙箱出站策略(egress)按域名放行/拦截,还能用「凭证保险箱」把真密钥注入到出站请求里而不让工作负载看到明文
  • 配套多语言 SDK(Python / Java / JS / .NET / Go)、osb CLI、MCP server。

用起来什么样: 一段最小的 osb CLI 交互(取自 README.md):

osb sandbox create --image python:3.12 --timeout 30m -o json
# → 返回一个 sandbox-id,容器已在后台异步provision
osb command run <sandbox-id> -o raw -- python -c "print(1 + 1)"
# → 2

第一条走控制面(造出容器),第二条走数据面(在容器里执行)——这正是理解 OpenSandbox 的钥匙。

一句话直觉/类比: 把它想成「云函数的孪生兄弟」,但反过来:云函数给你无状态的一次调用,OpenSandbox 给你一台有状态、可交互、能装任何东西、随时能拍快照冻结再解冻的一次性电脑。控制面像酒店前台(发钥匙、开房、退房),数据面像房间里的服务电话(点餐、打扫、叫醒)。

本节不谈实现。记住一句话:「管容器的」和「在容器里干活的」是两拨代码、两套 API,故意分开。


2. 顶层全景(它大概怎么转)

OpenSandbox 是个多组件系统。先看谁是谁,再看一次请求怎么流

2.1 部件一句话职责

部件干什么语言 / 位置
协议 specs用 OpenAPI 定义「生命周期 API」「执行 API」「出站 API」「诊断 API」的契约specs/*.yml *.yaml
server(控制面)FastAPI 服务,收生命周期请求,决定用 Docker 还是 K8s 去创建/暂停/快照/销毁容器Python · server/opensandbox_server/
execd(数据面)每个沙箱容器里都跑的 Go 守护进程,监听 44772 端口,提供命令/文件/PTY/代码解释器/嵌套隔离Go · components/execd/
ingress(入站网关)反向代理,按 sandbox-id 从 header 或 URI 路由到对应容器,校验安全访问签名Go · components/ingress/
egress(出站网关)每沙箱一个 sidecar,透明拦截出站流量,按域名策略放行/拦截,内含凭证保险箱Go · components/egress/
SDK / CLI / MCP多语言客户端、osb 终端工具、MCP server,封装上面两套 APIsdks/ cli/

控制面 = Python,数据面/网络面 = Go。 这不是随意的:控制面要跟 Docker SDK、Kubernetes client、编排逻辑打交道(Python 生态顺手);数据面要塞进每个容器、常驻、低开销、直接摸 Linux namespace/seccomp(Go 单二进制顺手)。

2.2 顶层图(怎么读:上半是「管容器」的控制面,下半是「进容器」的数据面,左边是客户端)

┌──────────────── 客户端(SDK / osb CLI / MCP / agent)────────────────┐
│ │
①管理请求 │ 创建/暂停/快照/销毁 ②执行请求 跑命令/读写文件/开终端
▼ ▼
┌───────────────────────────┐ ┌──────────────────────────────┐
│ SERVER 控制面 (Python) │ │ INGRESS 入站网关 (Go) │
│ FastAPI /v1/sandboxes ... │ │ 按 sandbox-id 路由 + 签名校验 │
│ api/lifecycle.py │ │ proxy/host.go │
└────────────┬──────────────┘ └───────────────┬──────────────┘
│ 按 config.runtime.type 选后端 │ 转发到容器 44772
┌────────────┴───────────────┐ ▼
▼ ▼ ┌───────────────────────────────────────┐
┌─────────────┐ ┌────────────────┐ │ 沙箱容器 (一个/多个) │
│ Docker 后端 │ │ Kubernetes 后端│ ──创建/调度──▶ │ ┌─────────────────────────────────┐ │
│ docker_svc │ │ k8s workload │ │ │ EXECD 数据面 (Go, :44772) │ │
└─────────────┘ └────────────────┘ │ │ 命令/文件/PTY/代码解释器 │ │
│ │ + 嵌套隔离 bwrap+overlay+seccomp│ │
│ └─────────────────────────────────┘ │
│ │ 出站流量 │
│ ▼ │
│ ┌─────────────────────────────────┐ │
│ │ EGRESS sidecar (Go) │ │
│ │ 域名策略 + 凭证保险箱 + mitmproxy│ │
│ └─────────────────────────────────┘ │
└───────────────────────────────────────┘

2.3 主线走一遍(高层,不进代码)

造一台沙箱(控制面):

  1. 客户端 POST /sandboxes,body 里写镜像、超时、网络策略等 → server/opensandbox_server/api/lifecycle.py
  2. server 按配置 config.runtime.type 选后端(docker 或 kubernetes)—— services/factory.pycreate_sandbox_service
  3. 后端异步 provision:创建容器/Pod、注入 execd、按需拉起 egress sidecar,先返回 Pending,provision 完转 Running

在沙箱里干活(数据面):

  1. 客户端把执行请求发给 ingress(或经 server 的代理路由)→ ingress 按 sandbox-id 找到容器端点、校验访问签名。
  2. 请求打到容器里的 execd(:44772)→ execd 跑命令 / 执行代码 / 读写文件,结果流式返回。
  3. 若代码要访问外网,流量先过 egress sidecar,按域名策略放行,必要时由凭证保险箱注入真密钥。

收尾: 暂停(冻结进程与内存态)、快照(把 rootfs 存成可复用镜像)、或直接销毁(TTL 到期 / 主动 kill)。


3. 阅读地图(建议顺序)

这套文档拆成 7 章,由浅入深。按顺序读能从「协议契约」一路走到「最底层的 namespace/seccomp」;也可按任务直接跳章。

顺序章节讲什么适合谁
0OpenSandbox — 这是什么 / 全景 / 阅读地图(本页)零基础入门 + 顶层全景 + 导航所有人先读
1协议契约与沙箱生命周期状态机四份 OpenAPI 契约、沙箱状态机(Pending→Running→Paused→Terminated)、鉴权想懂「对外长什么样」
2控制面:从一次 create 请求到容器落地FastAPI 路由 → service 抽象 → 异步 provision 全链路改控制面 / 接新后端
3运行时后端:Docker 与 Kubernetes、池化、快照、暂停恢复两套后端实现、预热池、rootfs 快照、pause/resume、安全容器运行时做部署 / 调度 / 扩容
4数据面:execd 在沙箱里跑命令、文件、PTY 与代码解释器execd 的路由表、命令/会话/PTY/Jupyter 内核、SSE 流式想懂「怎么在容器里执行」
5嵌套隔离:容器内再用 bubblewrap+overlay 套一层沙箱bwrap 参数编排、seccomp 系统调用黑名单、overlay 可回滚工作区、diff/commit关心安全边界 / 可回滚执行
6网络平面:入站路由、出站策略与凭证保险箱ingress header/uri 路由 + 签名、egress 透明代理 + 域名策略、凭证保险箱注入做网络隔离 / 密钥安全

若你只有 10 分钟: 读本页 §2(全景)+ §4(巧妙之处),就能讲清「它是什么、妙在哪」。


4. 巧妙之处(可借鉴的技术)

这一节提炼读完要带走的精华——那些不显然、值得抄的设计决策。

4.1 控制面/数据面彻底分离,后端可插拔

「造容器」和「用容器」是两套完全独立的 API 与代码。控制面 SandboxService 是抽象基类(server/opensandbox_server/services/sandbox_service.py:41),Docker 与 Kubernetes 各自实现;工厂按配置一行选型(services/factory.pycreate_sandbox_service,selected_type = config.runtime.type)。

妙在: 你从本地 Docker 换到生产 K8s,执行 API(execd)、SDK、CLI 全都不用改——因为它们只跟数据面协议打交道,跟「谁在底层造容器」解耦。这是整个平台可扩展性的根。

4.2 创建是异步的:先给 ID,再后台 provision

POST /sandboxes 立刻返回 202 Accepted + Pending 状态,真正的容器创建在后台 worker 里跑(docker_service.py:673 _async_provision_worker:790 _provision_sandbox)。客户端轮询状态直到 Running

妙在: provision 可能要拉镜像、建卷、起 sidecar,耗时几秒到几十秒。同步阻塞会拖垮连接;异步 + 状态机让「慢创建」变成「快返回 + 可观测的状态流转」。

4.3 嵌套隔离 + 可回滚工作区:执行完能「反悔」

除了外层容器,execd 还能在容器内部用 bubblewrap 再套一层沙箱(components/execd/pkg/isolation/),工作区用 overlay 模式挂载(WorkspaceOverlay,isolator.go)——所有写入落在 upper 层。执行后可以 diff 看改了什么,commit 才落盘,否则整层丢弃(路由 /v1/isolated/session/:id/diff/commit,web/router.go)。

妙在: 对「让 AI 试着改代码,不满意就回滚」这种场景是天然契合——一次执行就是一个可审计、可丢弃的事务。

4.4 seccomp 黑名单锁死「越狱」系统调用

嵌套隔离生成一份 seccomp BPF,直接封掉 mount/umount2/chroot/pivot_root/ptrace/bpf/seccomp/init_module 等危险调用(isolation/seccomp_gen.godenylistSyscalls)。注释里还写明为何故意保留 setresuid/setresgid——因为 setpriv 要靠它从 root 降权,降权后进程已无 CAP_SETUID,无法夺回权限(seccomp_gen.go:46 附近)。

妙在: 不是无脑封所有,而是精确到「封掉能重新提权/逃逸的路径,留下正常降权所需」,并把理由写在代码里。

4.5 凭证保险箱:密钥进得了出站请求,进不了工作负载

egress 的凭证保险箱(components/egress/pkg/credentialvault/vault.go)让你把真 API key 存在 sidecar 里,通过绑定(binding)在出站请求里由 mitmproxy 注入对应 header,而沙箱内的代码永远拿不到明文。它还硬性拒绝往 hostauthorization 之外的保留 header 上乱注入(reservedHeaderNames)。

妙在: 解决了「agent 要调第三方 API 但你不敢把密钥交给它」的经典两难——密钥的「使用权」和「知情权」被分开了。

4.6 出站默认拒绝 + 按域名放行

egress 策略是 deny-by-default(policy.goDefaultDenyPolicy),Evaluate(domain) 按域名规则判 allow/deny(policy.go:82),并把域名规则编成 IP 集合下发给 nftables/iptables 做透明重定向到 mitmproxy(egress/nft.gosetupNftegress/main.go 引入 iptables/dnsproxy/mitmproxy)。

妙在: 网络策略以「人能读的域名」表达,底层自动翻译成内核级 IP 规则——策略语义和执行机制解耦。

4.7 入站访问要签名,防止「猜 ID 就能进别人沙箱」

ingress 路由支持 header 或 URI 两种模式(components/ingress/pkg/proxy/host.goModeHeader/ModeURI),但拿到 sandbox-id 后还要过安全访问校验(signature.CheckIngressSecureAccess),用访问令牌 + 签名 + 过期时间验证。

妙在: 沙箱端点暴露在网关后面,光知道 ID 不够——必须带对应签名,天然防横向越权。


5. 代码地图(导航索引)

一张表,agent/人可用符号名 grep 直接跳源码(比行号抗漂移)。路径相对克隆根,as-of sourceCommit

协议契约

主题文件关键符号 / 锚点
生命周期 OpenAPI(状态机、CRUD、暂停/快照)specs/sandbox-lifecycle.ymltitle: OpenSandbox Lifecycle API;状态说明在 :1031 附近
数据面执行 OpenAPI(命令/代码/文件/会话/PTY)specs/execd-api.yamlpaths /code /command /session /files /pty
出站 OpenAPI(策略 + 凭证保险箱)specs/egress-api.yamlpaths /policy /credential-vault
诊断 OpenAPIspecs/diagnostic-api.yml
设计提案(池化/安全运行时/快照等)oseps/000*.md0005-client-side-sandbox-pool0008-pause-resume-rootfs-snapshot

控制面(server · Python)

主题文件关键符号
生命周期路由入口server/opensandbox_server/api/lifecycle.pycreate_sandbox(POST /sandboxes,202)
服务抽象基类server/opensandbox_server/services/sandbox_service.py:41SandboxServicegenerate_sandbox_id
后端选型工厂server/opensandbox_server/services/factory.pycreate_sandbox_service(按 runtime.type)
Docker 后端server/opensandbox_server/services/docker/docker_service.py:135DockerSandboxServicecreate_sandbox:613_async_provision_worker:673_provision_sandbox:790
暂停 / 恢复server/opensandbox_server/services/docker/docker_service.pypause_sandbox:1177resume_sandbox:1210
Kubernetes 后端server/opensandbox_server/services/k8s/kubernetes_service.pyworkload_provider.pyWorkloadProvider(agent/batch 两种 provider)
预热池server/opensandbox_server/api/pool.pyservices/k8s/pool_service.pycreate_poolPoolService
快照server/opensandbox_server/services/snapshot_service.py:93PersistedSnapshotServicecreate_snapshot
安全容器运行时映射server/opensandbox_server/services/runtime_resolver.py:62get_docker_runtimeget_k8s_runtime_class(gVisor/Kata/Firecracker)
反代到容器内 execdserver/opensandbox_server/api/proxy.py_proxy_http_request_proxy_websocket_request

数据面(execd · Go)

主题文件关键符号
进程入口(加载隔离配置、探测能力)components/execd/main.gomainisolation.LoadConfigisolation.Probe
HTTP 路由表components/execd/pkg/web/router.goNewRouter(/files /code /session /command /pty /metrics /v1/isolated)
命令执行(SSE 流式)components/execd/pkg/web/controller/command.goRunCommand
代码解释器components/execd/pkg/web/controller/codeinterpreting.gopkg/jupyter/RunCode、Jupyter 内核
PTY 终端(WebSocket)components/execd/pkg/web/controller/pty_controller.gopty_ws.goCreatePTYSessionPTYSessionWebSocket
bash 会话 / SQLcomponents/execd/pkg/runtime/bash_session.gosql.go

嵌套隔离(execd/isolation · Go)

主题文件关键符号
隔离类型与能力components/execd/pkg/isolation/isolator.goProfile(strict/balanced)、WorkspaceMode(rw/overlay/ro)
bwrap 命令行编排components/execd/pkg/isolation/bwrap.gobuildArgv(段序对应 OSEP §7)
seccomp 黑名单components/execd/pkg/isolation/seccomp_gen.godenylistSyscalls
用户态 overlay 视图components/execd/pkg/isolation/merged_view.goMergedView(upper/lower)
隔离会话路由components/execd/pkg/web/controller/isolated_session.goCreate/Run/Diff/Commit

网络平面(ingress / egress · Go)

主题文件关键符号
入站网关入口components/ingress/main.gomain(基于 knative injection)
路由模式与端点解析components/ingress/pkg/proxy/host.gogetSandboxHostDefinitionModeHeader/ModeURI
访问签名校验components/ingress/pkg/signature/CheckIngressSecureAccess
出站网关入口components/egress/main.gomain(引入 dnsproxy/iptables/mitmproxy/policy)
域名策略components/egress/pkg/policy/policy.goNetworkPolicyDefaultDenyPolicyEvaluate:82
nftables 透明重定向components/egress/nft.gosetupNft
凭证保险箱components/egress/pkg/credentialvault/vault.goStorereservedHeaderNames

文档同步提示: 本页结论锚定在上表的符号名上。上游若只挪动行号、符号未变,重锚定即可;只有当这些符号的语义变了,才需要重生成。sourceCommit 锁定在 d57e41a