OpenSandbox 项目学习

项目地址:https://github.com/opensandbox-group/OpenSandbox

官方文档:https://open-sandbox.ai

一、它是什么

一句话:OpenSandbox 是面向 AI 应用的通用沙箱平台(general-purpose sandbox platform),提供多语言 SDK、统一的沙箱协议,以及 Docker / Kubernetes 两套运行时,覆盖 Coding Agent、GUI Agent、Agent 评测、AI 代码执行、RL 训练等场景。

为什么需要它?如今 Agent(尤其 Coding Agent、代码解释器、浏览器自动化、RL 训练)需要在隔离环境里跑不可信代码、装第三方依赖、访问网络、长出进程,甚至要批量起几万实例。直接在宿主机上跑既不安全也不可规模化。OpenSandbox 把「隔离 + 可编程 + 可调度」这三件事标准化了:

  • 隔离:容器级隔离,并可选 gVisor / Kata / Firecracker microVM 强化;网络出入站可策略化。
  • 可编程:统一的生命周期 API + 沙箱内执行 API(命令、文件、代码解释器、PTY),多语言 SDK + CLI + MCP。
  • 可调度:本地 Docker 单机跑,也能上 K8s 用 BatchSandbox / Pool 做池化与高吞吐交付。

它已进入 CNCF Landscape、有 OpenSSF Best Practices 徽章,是阿里开源、社区化运营的项目。

二、整体架构:六个表面

OpenSandbox 的设计核心是分层 + 协议优先。官方把系统拆成六个「表面(surface)」,理解这六个面就能拿住全貌:

  1. Client surface — SDK(Python/JS/TS/Java/Kotlin/C#/.NET/Go)、osb CLI、MCP server。
  2. Protocol surfacespecs/ 下的 OpenAPI 契约(生命周期、诊断、execd 执行、egress 策略),是整个系统的「真理之源」。
  3. Lifecycle control planeserver/ 下的 FastAPI 服务器,负责鉴权、校验、编排、端点解析、诊断、持久化。
  4. Runtime backends — Docker(本地/单机)与 Kubernetes(通过 BatchSandbox 或 kubernetes-sigs/agent-sandbox 工作负载提供方)。
  5. Sandbox data plane — 用户工作负载容器 + 注入的 execd 守护进程 + 可选 Jupyter/code-interpreter + 卷 + 可选 egress sidecar。
  6. Network & security plane — 端点解析、server 代理、K8s ingress 网关路由、安全端点访问、egress 策略、资源限额、安全容器运行时。

这种切分是有意的:

SDK 和工具依赖公共契约;server 只负责生命周期编排;runtime provider 负责平台相关的资源创建execd/egress 负责沙箱网络与文件命名空间内的操作。

也就是经典的 control plane vs data plane + runtime-neutral API, runtime-specific execution

三、协议面(specs/)

OpenSandbox 把 specs/ 当作公开契约的唯一来源(single source of truth),改 spec 等于改公开 API,要同步动 server / SDK / docs / CLI。主要四份:

  • sandbox-lifecycle.yml — 生命周期 API,base path /v1,由 server 提供。包含:

    • Sandboxes:从 image 或 snapshot 创建、list、get、delete、pause、resume、续期、解析端口端点。
    • Snapshots:从 sandbox 创建持久快照、list、get、delete。
    • 请求体特性丰富:image/snapshotId 启动源、entrypoint/环境变量/metadata/不透明 extensionsresourceLimits(CPU/内存/GPU)、platform 约束、volumes(host / pvc / ossfs)、networkPolicy(egress sidecar 配置)、secureAccess(K8s ingress 网关端点凭证)。
  • execd-api.yaml — 沙箱内执行 API,由 execd 暴露:/ping/code/contexts/code/session/command、后台命令日志、/files/*/directories/metrics/metrics/watch。命令与代码执行用 SSE 流式输出,另有 PTY WebSocket(/pty)做长连接 shell。

  • egress-api.yaml — egress sidecar 直接暴露的策略 API:GET /policyPATCH /policy,支持运行时热更新。

  • diagnostic-api.yml — 沙箱日志/事件的最佳努力诊断描述,另有面向运维和 AI 排障工作流的纯文本诊断路由。

协议优先的好处:多语言 SDK 可以从同一份 OpenAPI 生成普通请求/响应客户端,再在之上加手写适配层做流式、传输生命周期、错误映射、高层模型。spec、实现、SDK、文档、配置、CLI 行为需要保持对齐。

四、控制面(server/)

server/ 是 FastAPI 生命周期服务器。关键包:

  • main.py — app 启动、中间件、路由注册、运行时校验、renew-intent 启动。
  • api/ — 生命周期路由、代理路由、池路由、诊断路由、请求/响应 schema。
  • services/ — 生命周期服务接口 + Docker / K8s 实现。
  • services/k8s/ — K8s 工作负载提供方、端点解析、卷/egress 辅助、informer。
  • repositories/ — 持久化适配器(目前用于 server 管理的快照记录)。
  • integrations/renew_intent/ — 可选的「访问即续期」集成。
  • middleware/ — API-key 鉴权、request ID 中间件。

几个值得记的设计点:

  1. 运行时服务二选一[runtime].type 决定 dockerDockerSandboxServicekubernetesKubernetesSandboxService,两者实现同一个 SandboxService 接口,于是 API 路由很薄,只委托给 service,平台细节藏在 service 边界后。

  2. 持久化[store] 选 server 管理的元数据存储,默认 SQLite(~/.opensandbox/opensandbox.db)。快照元数据是第一个被持久化的 server 资源;未来的持久化记录复用同一个 repository 边界。

  3. 端点解析 + server 代理:生命周期端点 API 返回沙箱内某端口的可达地址,按运行时与配置可能是:

    • Docker host/bridge 映射端点;
    • K8s ingress 网关端点;
    • use_server_proxy=true 时的 server 代理 URL(/sandboxes/{sandboxId}/proxy/{port}),支持 HTTP 和 WebSocket,并可挂钩 renew-on-access。

五、运行时后端

5.1 Docker 运行时

本地/单机后端,直接和 Docker daemon 对话,管容器、timer、label、volume、port、可选 sidecar、snapshot。核心职责:

  • 拉取公共/私有镜像(支持 per-request registry 鉴权);
  • 用 CPU/内存/GPU/platform/capability/AppArmor/seccomp/PID/安全运行时设置建容器;
  • execd 二进制从 [runtime].execd_image 暂存进沙箱,装一个 bootstrap launcher,再启动用户 entrypoint
  • 网络 host/bridge/自定义网络;非 host 模式下为 execd 与用户服务端点分配宿主端口;
  • 服务重启后恢复已有受管容器的过期 timer;
  • 支持 host bind mount、Docker named volume(pvc 模型)、OSSFS 挂载;
  • 请求 networkPolicy 时挂 egress sidecar;
  • 把沙箱 commit 成本地镜像做持久快照、并支持从快照镜像恢复。

Docker 的 pause/resume 直接用容器级 pause/resume。

5.2 Kubernetes 运行时

K8s 运行时把真正的负载创建委托给 kubernetes.workload_provider 选定的工作负载提供方:

  • batchsandbox(默认)— 背靠 OpenSandbox 自己的 controller 和 BatchSandbox CRD;
  • agent-sandbox — 背靠 kubernetes-sigs/agent-sandbox

K8s 服务端路径负责:K8s client 初始化 + 可选 informer;从 image 请求建负载(snapshotId 启动时解析到存储的可恢复镜像);BatchSandbox 与 agent-sandbox manifest 的模板合并;per-request image pull secrets;资源限额 + GPU → K8s extended resources 的翻译;platform 约束 + RuntimeClass 接入安全运行时;卷、egress sidecar、安全端点访问注解;端点解析;pause/resume 委托给 provider;K8s 资源的纯文本诊断。

5.3 BatchSandbox Controller

kubernetes/ 下的 controller 实现了 OpenSandbox 专属 CRD,做高吞吐 + 池化的沙箱交付:

  • BatchSandbox — 从一个 pod template 建一个或多个沙箱副本;
  • Pool — 维护预热资源,实现快速分配;
  • SandboxSnapshot — K8s pause/resume 用的内部 rootfs 快照记录。

BatchSandbox 同时支持模板建法池化建法extensions.poolRef),并可选任务编排(batch / RL 类负载)。

K8s pause/resume 的实现很有意思:对 BatchSandbox.spec.replicas=1,pause 把沙箱 rootfs commit 成 OCI 镜像并释放运行时资源;resume 把负载模板改写为使用该快照镜像、重建运行时,同时保留 sandbox ID。注意公共快照 API 目前只有 Docker 运行时实现;K8s pause/resume 走的是 controller 内部 SandboxSnapshot 流程,是一套独立的运行时关注点。

六、沙箱数据面

6.1 execd

components/execd/ 是用 Gin 写的 Go 守护进程,跑在沙箱内部,暴露执行 API。职责:

  • shell 命令执行(SSE 流式);
  • 后台命令状态 + 增量日志拉取;
  • 持久 bash session;
  • PTY 交互式 session(WebSocket);
  • 文件/目录操作;
  • 基于 Jupyter 的代码上下文与代码执行;
  • 本地 CPU/内存 metrics,可选 OpenTelemetry 导出;
  • 可选 X-EXECD-ACCESS-TOKEN 共享访问令牌强制。

execd 的注入方式:Docker 里由 server 把 execd 暂存进容器并装 bootstrap 脚本;K8s BatchSandbox 模板模式下,用一个 init container 从配置好的 execd_imageexecdbootstrap.sh 拷进一个 emptyDir volume,再挂给主沙箱容器。这是一种典型的「sidecar/initcontainer 注入控制进程」模式。

6.2 Code Interpreter 运行时

官方 code-interpreter 镜像在沙箱内启动 Jupyter,execd 通过 HTTP/WebSocket 和 Jupyter 通信,把 kernel message 翻译成 OpenSandbox 流式事件。镜像支持 Python/Java/Node.js/Go 运行时,以及 Python/Java/TS/JS/Go/Bash 的 Jupyter kernel,语言版本由 PYTHON_VERSION 等环境变量控制。Code Interpreter SDK 是可选高层客户端,底层执行 API 始终可用。

6.3 卷(Volumes)

生命周期 API 暴露运行时无关的卷模型:

  • host:绑定允许的宿主路径;
  • pvc:平台管理的命名存储——Docker 映射成 Docker named volume,K8s 映射成 PVC;
  • ossfs:通过 server/运行时集成挂载阿里云 OSS。

不同 provider 校验和物化方式不同,但 API 形状共享。

6.4 Egress Sidecar

components/egress/ 从沙箱网络命名空间强制出站网络策略:

  • FQDN + 通配域名 allow/deny;
  • dns 模式做 DNS 过滤;
  • dns+nft 模式在 DNS 之外用 nftables 强制已解析 IP 与 CIDR/IP 规则(环境支持时);
  • 运行时 /policy 检查与 patch;
  • 可选 sidecar 鉴权、平台强制的 always-allow / always-deny 叠加、实验性透明 HTTPS MITM。

部署形态:Docker 把 egress 当独立容器起,主沙箱容器跑在 sidecar 网络命名空间里;K8s 把 egress sidecar 追加进 pod spec,并从主沙箱容器 drop 掉 NET_ADMIN,保证只有 sidecar 能改网络规则——这是个很干净的最小权限实践。

七、网络与访问

7.1 Ingress 网关

components/ingress/ 是 K8s 导向的 HTTP/WebSocket 反向代理,watch 沙箱资源并把流量路由到沙箱端口。路由模式:

  • Header 模式OpenSandbox-Ingress-To: <sandbox-id>-<port> 或 host 解析;
  • URI 模式/<sandbox-id>/<port>/<path>
  • 通配 host 模式:经 server 端点格式化。

BatchSandbox 下 ingress 读 sandbox.opensandbox.io/endpoints 注解;agent-sandbox 下读 status.serviceFQDN

7.2 Secure Access / Auto-Renew

  • secureAccess:当前仅支持经 ingress 网关暴露的 K8s 沙箱。开启后 server 给端点配凭证,端点响应里带回必需 header;网关 secure-access 签名密钥配好时还支持签名路由 token。
  • Auto-renew on access:可选 renew-intent 集成,观察到访问就延长沙箱 TTL,触发源可以是 server 代理请求或经 Redis 投递的 ingress 网关事件。单沙箱 opt-in 由 extensions["access.renew.extend.seconds"] 控制。

八、核心流程串讲

把上面几节串成五条主线:

1. 沙箱创建(异步):

1
2
3
4
5
6
Client/SDK/CLI/MCP
-> POST /v1/sandboxes
-> FastAPI 校验请求与配置
-> 选定运行时建 Docker 容器 / K8s 负载
-> 运行时暂存 execd + 可选 egress/volume/network 配置
-> 沙箱进入 Running 或报 Failed(带 reason/message)

客户端应轮询 GET /v1/sandboxes/{sandboxId} 或用 SDK 就绪辅助方法。

2. 命令/文件/代码执行

1
2
3
4
5
Client
-> 从沙箱元数据或 server 代理解析 execd 端点
-> 必要时带 X-EXECD-ACCESS-TOKEN
-> execd 跑命令/文件操作/session/PTY/Jupyter 代码
-> execd 用 SSE/WebSocket 流式输出或返回结构化响应

3. 服务暴露

1
2
3
4
5
Client
-> GET /v1/sandboxes/{sandboxId}/endpoints/{port}
-> server 返回 Docker 映射 / ingress 网关 / server 代理端点
-> secure access 或 sidecar auth 要求时带返回的 header
-> HTTP/WebSocket 流量打到目标沙箱端口

4. Egress 策略

1
2
3
4
5
创建请求带 networkPolicy
-> server 校验 [egress] 配置
-> 运行时挂 egress sidecar(带初始策略)
-> 沙箱出站 DNS/网络流量被 sidecar 过滤
-> 客户端可解析 egress 端点并运行时 PATCH /policy

5. Pause/Resume 与 Snapshot

1
2
3
4
5
6
7
8
9
Pause/Resume
-> 生命周期 server 委托给运行时 provider
-> Docker 容器级 pause/resume
-> BatchSandbox 用 rootfs 快照 commit/recreate(单副本时)

公共 Snapshot API
-> server 持久化快照元数据
-> Docker 运行时把沙箱 commit 成可恢复镜像
-> 从快照创建时解析该镜像并起新沙箱

九、设计原则小结

读完源码与架构文档,OpenSandbox 反复强调几条原则,很值得做基础设施时借鉴:

  1. Protocol First — 公开行为从 specs/ 的 OpenAPI 契约出发,生成产物要重新生成而非手工补丁。
  2. Control plane vs Data plane — server 只编排与校验,平台相关 provisioning 进 runtime service,沙箱内操作进 execd/egress。
  3. Runtime-neutral API, Runtime-specific execution — 共享概念(resource limits、volumes、endpoints、network policy、metadata),Docker/K8s 各自物化但保持契约。
  4. Secure defaults with explicit escape hatches — API-key 鉴权、无鉴权模式启动 guardrail、资源限额、capability drop、可选安全运行时、egress 控制、端点 header、平台网络隔离;更宽松的模式留给本地开发或显式运营选择。
  5. Observable failures — 沙箱状态有 state/reason/message/transition time;execd 暴露 metrics,server 暴露诊断,ingress/egress/execd 有日志与 OTel,request ID 全链路传播便于排障。

十、典型用例

  • Coding agents:在隔离沙箱里跑 Claude Code / Gemini CLI / Codex CLI / Qwen Code / Kimi CLI 等。
  • AI code execution:执行模型生成代码,用命令/文件/code-interpreter API 拿流式反馈。
  • 浏览器自动化:受控文件系统与网络行为下跑 Chrome / Playwright。
  • 远程开发:经沙箱端点暴露 VS Code Web、桌面、VNC、开发服务器。
  • RL 与评测:用 K8s BatchSandbox、Pool、任务编排做高吞吐沙箱交付(一个 trial 一个沙箱)。
  • 企业级隔离:组合安全运行时 + ingress + egress + 端点访问 header + K8s 部署控制。

十一、本地快速上手

1
2
3
4
5
6
7
8
9
10
11
12
# 安装并配置沙箱服务器
uvx opensandbox-server init-config ~/.sandbox.toml --example docker
uvx opensandbox-server

# CLI 速用
pip install opensandbox-cli
osb config init
osb config set connection.domain localhost:8080
osb config set connection.protocol http
osb config set connection.api_key <your-api-key>
osb sandbox create --image python:3.12 --timeout 30m -o json
osb command run <sandbox-id> -o raw -- python -c "print(1 + 1)"

Python SDK 起一个 code interpreter 的最小例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
import asyncio
from datetime import timedelta
from code_interpreter import CodeInterpreter, SupportedLanguage
from opensandbox import Sandbox
from opensandbox.models import WriteEntry

async def main() -> None:
sandbox = await Sandbox.create(
"opensandbox/code-interpreter:v1.1.0",
entrypoint=["/opt/code-interpreter/code-interpreter.sh"],
env={"PYTHON_VERSION": "3.11"},
timeout=timedelta(minutes=10),
)
async with sandbox:
execution = await sandbox.commands.run("echo 'Hello OpenSandbox!'")
print(execution.logs.stdout[0].text)
await sandbox.files.write_files([
WriteEntry(path="/tmp/hello.txt", data="Hello World", mode=644)
])
content = await sandbox.files.read_file("/tmp/hello.txt")
print(f"Content: {content}")
interpreter = await CodeInterpreter.create(sandbox)
result = await interpreter.codes.run(
"import sys\nprint(sys.version)\nresult = 2 + 2\nresult",
language=SupportedLanguage.PYTHON,
)
print(result.result[0].text) # 4
print(result.logs.stdout[0].text) # 3.11.x
await sandbox.kill()

if __name__ == "__main__":
asyncio.run(main())

十二、实战:云上 Agent 接入沙箱

上面是本地把 server 起起来玩的路径。更常见的真实需求是:我在云上已经部署好了 agent,想给它接一个沙箱/起一个沙箱。这一节专门讲这条路。

12.1 先理清调用关系

1
2
3
4
5
6
7
8
你的 agent (云上)
│ HTTP/OpenAPI + API key

OpenSandbox server (控制面) ← 你要先有一台跑起来的 server
│ 生命周期编排

真实沙箱容器 (Docker / K8s pod)
└─ 注入 execd 守护进程 ← agent 后续在这里跑命令/代码/读写文件

关键认知:agent 不直连沙箱,而是连 OpenSandbox server(控制面);server 负责把沙箱建起来并告诉你 execd 的端点;之后 agent 再直接打 execd 做命令/文件/代码执行。所以「接一个沙箱」=「让 agent 学会调 OpenSandbox server 的 API」。

12.2 前置:先有一台 server

如果云上还没有 OpenSandbox server,最快的起法(单机 Docker):

1
2
3
4
5
6
# 在你云上某台机器,装好 Docker 后
uvx opensandbox-server init-config ~/.sandbox.toml --example docker
# 编辑 ~/.sandbox.toml:
# - 改 api_key(给一个强随机串,这就是 agent 要带的鉴权凭据)
# - 暴露端口 / 是否启用 TLS
uvx opensandbox-server

规模化、要池化/高吞吐就上 K8s 部署(kubernetes/ 下有 Helm chart,用 BatchSandbox + Pool 预热)。生产建议直接走 K8s。

云上必须确认三件事:

  1. server 的域名/端口对 agent 可达(同 VPC 或公网 + 鉴权);
  2. server 所在机器能访问 Docker daemon 或 K8s 集群;
  3. 配好 api_key,关掉无鉴权模式(server 有启动 guardrail)。

12.3 三种接入方式,按你的 agent 形态选

你的 agent 形态 推荐方式 说明
自研 Python/Java/JS/Go 后端服务 SDK 直连 程序里 import 调用,最灵活,生产首选
Claude Code / Cursor / 其它 MCP 客户端 MCP server 装一个 opensandbox-mcp,工具自动暴露给 agent
运维脚本 / 临时排查 osb CLI 终端一行起沙箱

方式 A:SDK 直连(最常见)

装 SDK:pip install opensandbox(也有 Java/Kotlin、JS/TS、C#、Go 版本)。最小可用例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
import asyncio
from datetime import timedelta
from opensandbox.sandbox import Sandbox
from opensandbox.config import ConnectionConfig
from opensandbox.models import WriteEntry

async def main():
# 1. 配置连接 —— 指向你云上的 OpenSandbox server
config = ConnectionConfig(
domain="sandbox.your-cloud.example", # server 地址
api_key="your-api-key", # ~/.sandbox.toml 里设的那个
)

# 2. 起一个沙箱
sandbox = await Sandbox.create(
"python:3.12", # 镜像;或用 opensandbox/code-interpreter:v1.1.0
connection_config=config,
timeout=timedelta(minutes=15), # 过期自动回收,别忘设
)
try:
# 3. 在沙箱里跑命令
exec_res = await sandbox.commands.run(
"pip install numpy && python -c 'import numpy; print(numpy.__version__)'"
)
print(exec_res.logs.stdout[0].text)

# 4. 读写文件
await sandbox.files.write_files([
WriteEntry(path="/tmp/in.txt", data="hello", mode=644)
])

# 5. (可选)暴露沙箱内某端口给 agent / 外部访问
endpoint = await sandbox.get_endpoint(8080) # 返回可达地址 + 必要 header
print(endpoint)
finally:
# 6. 用完销毁(destroy = kill + close 本地资源)
await sandbox.destroy()

asyncio.run(main())

把它包进你 agent 的工具函数里,agent 就「接上沙箱」了。如果 agent 是同步栈,用 SandboxSync / ConnectionConfigSync,语义一致。

方式 B:MCP(给 Claude Code / Cursor 用)

1
2
pip install opensandbox-mcp
opensandbox-mcp --domain sandbox.your-cloud.example --protocol http

客户端配置(stdio):

1
2
3
4
5
6
7
8
{
"mcpServers": {
"opensandbox": {
"command": "opensandbox-mcp",
"args": ["--domain", "sandbox.your-cloud.example", "--protocol", "http"]
}
}
}

之后 agent 自动多出「创建沙箱 / 跑命令 / 读写文件」几个工具,无需写代码。

方式 C:CLI(临时/运维)

1
2
3
4
osb config set connection.domain sandbox.your-cloud.example
osb config set connection.api_key your-api-key
osb sandbox create --image python:3.12 --timeout 15m -o json # 拿 sandbox-id
osb command run <sandbox-id> -o raw -- python -c "print(1+1)"

12.4 云上生产的几个要点

  1. 一定要设 timeout:沙箱有 TTL,到期自动回收。长时间任务用 renew(续期)或开启 auto-renew on access(extensions["access.renew.extend.seconds"]),有人访问就自动续命。
  2. 池化预热降延迟:如果 agent 高频起沙箱,用 Pool 预热一批就绪沙箱,SDK 端用 SandboxPoolSync 直接 acquire,免掉冷启动。
  3. egress 策略:沙箱要访问外部 API/数据源时,用 networkPolicy 挂 egress sidecar,只放行白名单 FQDN——不可信代码跑在沙箱里时这条尤其重要。
  4. 端点暴露:get_endpoint(port) 拿到地址;K8s ingress 网关模式下若开了 secureAccess,响应会带回必需 header,agent 后续请求要带上。
  5. 用完 destroy():create-use-discard 流程务必销毁,否则沙箱堆积吃资源;池化场景由池管理生命周期。
  6. 不可信代码隔离:跑模型生成代码/第三方依赖时,叠加 gVisor / Kata / Firecracker 安全运行时(RuntimeClass),别只用裸容器。
  7. 诊断:出问题用 GET /v1/sandboxes/{id}state/reason/message,server 有纯文本诊断路由,execd 暴露 /metrics,且全链路带 request id 便于排障。

12.5 一句话总结流程

部署/确认 server → agent 用 SDK(或 MCP/CLI)带 api_key 调 Sandbox.create → 拿 execd 端点跑命令/代码 → 用完 destroy()

十三、读后感

OpenSandbox 的工程化水平相当成熟:把「沙箱」这种历史上散落在各种脚本里的东西,按协议 → 控制面 → 运行时 → 数据面 → 网络安全面严格分层,每层都用 OpenAPI 契约钉死边界,再让 Docker/K8s 各自实现。几个印象深刻之处:

  • execd 注入模式:把控制进程暂存进沙箱、用 init container 拷进 emptyDir,是「在不可信环境里长出可控 API」的标准解法,比硬塞 agent 进容器干净得多。
  • K8s pause/resume 用 rootfs 快照:commit 成 OCI 镜像 + 释放运行时 + 保留 sandbox ID,兼顾「省资源」和「ID 连续」,对 RL/评测的高频 pause/resume 很实用。
  • egress 的最小权限:K8s 里从主容器 drop NET_ADMIN、只让 sidecar 改规则,把「谁能改网络」收敛到一处。
  • 协议优先 + 多语言 SDK 生成:避免「五份 SDK 五份行为」,是做开放平台该有的姿势。

后续可以顺着几条线深挖:specs/ 的 OpenAPI 全文 + SDK 生成流水线、BatchSandbox controller 的 reconcile 与池化调度算法、egress 的 dns+nft 双重过滤实现、以及 secure container runtime(gVisor/Kata/Firecracker)的具体接入点。

十四、底层技术栈与关键代码架构

读完架构文档还不够,真正想理解"沙箱底层用的是什么",得去翻代码。这一节给出按调用链梳理的实打实说明,均带文件位置。

14.1 底层技术栈

OpenSandbox 自己不发明容器/虚拟化技术,而是分层组合现成的内核与容器原语:

用什么 在哪
隔离(粗粒度) Docker 容器 / Kubernetes Pod(+ 可选 RuntimeClass:gVisor、Kata、Firecracker microVM) server/services/docker/server/services/k8s/
隔离(细粒度,进程级) bubblewrap (bwrap) + overlay + seccomp-bpf components/execd/pkg/isolation/
控制面 Python + FastAPI server/opensandbox_server/
数据面(execd/ingress/egress) Go 1.25 + Gin / gorilla-websocket / creack/pty / gopsutil / OTel components/execdcomponents/ingresscomponents/egress
调度面 Go + kubebuilder 风格 controller + 自研 scheduler kubernetes/
网络出入站 nftables / iptables / 自建 DNS proxy / mitmproxy(透明 HTTPS MITM) components/egress/

go.mod 里几个关键依赖很说明问题:elastic/go-seccomp-bpf(seccomp)、creack/pty(终端)、knative.dev/pkg(informer/controller 框架)、redis/go-redis(ingress 续期事件)、pelletier/go-toml(配置)。

14.2 关键认知:隔离是"两层"的

很多人以为"沙箱 = 一个 Docker 容器",OpenSandbox 不是。它有两层隔离:

第一层(容器/Pod 级):server 建一个 Docker 容器或 K8s Pod,可选叠加安全运行时。这一层隔离的是"这个工作负载 vs 宿主机/其他工作负载"。

第二层(进程级,execd 内部):这是最有料的部分。execd 跑在沙箱里,但它执行每条用户命令时,会再用 bwrap + seccomp 把这条命令包进一层隔离——独立的 mount namespace、tmpfs、overlay upper 层、seccomp-bpf 系统调用过滤。也就是说,沙箱里跑的命令本身还套着一层"OS 级沙箱"。

代码证据(components/execd/pkg/isolation/):

  • bwrap.go — 构造 bubblewrap 命令行 argv:bwrapNamespaceSegment(userns/mount)、bwrapTmpSegmentbwrapWorkspaceSegmentbwrapEnvSegment,并把 seccomp fd 通过 --seccomp 注入;
  • seccomp_gen.go — 生成 seccomp-bpf filter;
  • upper.go — overlay 的 upper 层(写时复制的工作区);
  • probe.go — 启动时探测内核能力(isolation.Probe),决定能不能用 bwrap,不能用就降级(bwrap_stub.goAvailable=false);
  • main.go 启动流程里:isolation.LoadConfigisolation.Probe → 拿到 isolator,后续 runtime 跑命令时 Wrap(exec.Cmd) 套上。

14.3 关键代码架构(按调用链)

控制面(server,FastAPI):

1
2
3
4
client → FastAPI 路由 (api/) → services/factory.py + runtime_resolver.py
按 [runtime].type 二选一 SandboxService 接口实现:
├─ DockerSandboxService (services/docker/docker_service.py:create_sandbox @626)
└─ KubernetesSandboxService (services/k8s/kubernetes_service.py)
  • main.py — app 启动、中间件、startup_guard(api_key 强制)、renew_intent consumer;
  • services/sandbox_service.py — 公共 SandboxService 接口,API 路由很薄,只委托;
  • services/docker/docker_service.py(创建)、container_ops.pyport_allocator.pynetworking.pyvolumes.pyossfs_mixin.pydocker_diagnostics.pysnapshot_runtime.py;
  • services/k8s/kubernetes_service.pybatchsandbox_template.py(生成 BatchSandbox manifest)、workload_mapper.pysecurity_context.py(RuntimeClass/cap drop)、volume_helper.py;
  • repositories/ — SQLite 持久化快照元数据(默认 ~/.opensandbox/opensandbox.db);
  • middleware/ — api_key 鉴权 + request id。

Docker 运行时核心:docker_service.py:create_sandbox 做的事:拉镜像 → 建容器(挂 CPU/mem/GPU/capability/AppArmor/seccomp/PID/secure-runtime)→ 把 execd 二进制从 execd_image 暂存(staging)进容器 → 装 bootstrap.sh 启动器 → 配端口/host bind/named volume/OSSFS → 可选挂 egress sidecar → 启动用户 entrypoint。快照走 snapshot_runtime.py(把容器 commit 成镜像)。

Kubernetes 运行时 + 控制器:

1
2
3
4
5
6
7
server kubernetes_service → 生成 BatchSandbox manifest → 提交到集群

kubernetes/ 控制器 (kubebuilder) reconcile BatchSandbox CRD:
apis/sandbox/v1alpha1/{batchsandbox,pool,sandboxsnapshot}_types.go
internal/controller/{algorithm,strategy,poolassign,recycle,eviction}/
internal/scheduler/ ← 池化调度
internal/task-executor/ ← batch/RL 任务编排 (runtime/server/storage/manager)

CRD 状态机很清晰:Pending → Succeed,以及 Pausing ⇄ Paused ⇄ Resuming,失败转 Failed(条件类型有 Ready/Progressing/Paused/PauseFailed/ResumeFailed/PodFailed)。pause/resume 的 rootfs 快照逻辑挂在 controller 里。execd 注入走 init containerexecd_image 拷进 emptyDir

数据面 execd(Go):

1
2
3
main.go → flag.InitFlags → isolation.LoadConfig/Probe → web(Gin) → controller/
→ runtime/Controller: command.go / bash_session.go / pty_session.go
/ jupyter.go / isolated_session.go / context.go / replay_buffer.go
  • runtime/command.go — 命令执行 + SSE 流式;command_status.go — 后台命令状态/增量日志;
  • runtime/bash_session.go — 持久 bash 会话(exec.CommandContext + export 解析保 cwd/env);
  • runtime/pty_session.gocreack/pty + WebSocket 长连接 shell;
  • runtime/jupyter.go + pkg/jupyter/ — code interpreter,把 kernel message 翻成流式事件;
  • runtime/isolated_session.go — 命令级隔离会话(配合 bwrap);
  • runtime/replay_buffer.go — 重放缓冲,断线重连能拿回历史输出。

网络面:

  • ingress(components/ingress/):Go 反代,pkg/proxy(HTTP/WS 转发)、pkg/sandbox(watch 沙箱资源拿端点)、pkg/signature(安全路由签名 token)、pkg/renewintent(消费 Redis 续期事件)。用 knative.dev/pkg 的 informer 框架做 watch。
  • egress(components/egress/):pkg/nftables(nft 规则)+ pkg/dnsproxy(DNS 过滤)+ pkg/iptables(降级方案)+ pkg/mitmproxy(mitmproxy_transparent.go 透明 HTTPS MITM)+ pkg/credentialvault(凭证注入,真实 secret 不进工作负载)+ pkg/policy(/policy GET/PATCH 热更新)。nft.go 里的 setupNft 把静态策略写到 nft,再把 DNS 解析出的 IP 动态加进 allow set——这是 dns+nft 双重过滤的实现。

14.4 三个最值得看的实现点

  1. 进程级隔离层 components/execd/pkg/isolation/:bwrap 构造 mount namespace + overlay upper + seccomp-bpf 注入。沙箱里再套沙箱,这是它"通用 + 安全"的底座。
  2. K8s 调度器 kubernetes/internal/controller/{algorithm,strategy,poolassign,recycle,eviction}/ + internal/scheduler/:池化预热、分配、回收、驱逐策略,这是高吞吐 RL/评测场景的核心,比单纯"起个 Pod"复杂得多。
  3. egress 的 dns+nft 动态联动 components/egress/nft.go + pkg/dnsproxy:DNS 过滤拿到解析 IP 后动态写入 nft allow set,既过滤域名又能处理 CDN 多 IP,加 mitmproxy 做透明 HTTPS 拦截 + credential vault 注入凭证——这是"可控出站"的完整工程化。

14.5 控制面 vs 数据面:执行边界

一个容易混淆的点:控制面 server 会根据请求创建 Pod,但它不在里面执行命令。执行走的是数据面(execd),agent 直连 execd,不再经过 server。两阶段、两条不同链路:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
┌─────────────────────────────────────────────────────────────────┐
│ 阶段① 生命周期(过控制面 server) │
│ │
│ agent → POST /v1/sandboxes (FastAPI server) │
│ → KubernetesSandboxService 生成 BatchSandbox manifest │
│ → 提交到集群 → controller reconcile → 起 Pod │
│ (init container 把 execd 拷进 emptyDir → 主容器起 execd)│
│ agent → GET /v1/sandboxes/{id}/endpoints/{port} │
│ → server 返回 execd 的可达地址(+ secureAccess 时的 header) │
└─────────────────────────────────────────────────────────────────┘
↓ Pod 起来,execd 在监听
┌─────────────────────────────────────────────────────────────────┐
│ 阶段② 执行(直连数据面 execd,绕开 server) │
│ │
│ agent → execd 端点 /command、/files、/code、/pty │
│ → execd 在 Pod 内部用 bwrap+seccomp 跑命令/读写文件 │
│ → SSE/WebSocket 流式回输出 │
└─────────────────────────────────────────────────────────────────┘

逐字澄清:

  • 控制面根据请求创建 Pod —— 是。K8s 模式下 KubernetesSandboxService 把 create 请求翻译成 BatchSandbox manifest 提交,controller 负责 reconcile 出真正的 Pod(services/k8s/kubernetes_service.py + batchsandbox_template.py)。Docker 模式下则是 DockerSandboxService 调 Docker daemon 建容器。
  • 然后在里面执行对应的命令 —— 不是。Pod 起来后,server 的活就基本干完了(只剩端点解析/续期/诊断/快照这些生命周期管理)。执行命令是 agent 直接连 Pod 里的 execd,server 不参与、不做中转、不解析输出。

为什么这样设计:就是架构文档反复强调的 Control plane vs Data plane。server(控制面)只做编排与校验——鉴权、配额、资源限额翻译、工作负载创建、端点解析、pause/resume、快照元数据持久化,是"管理面",不碰业务数据流;execd(数据面)做沙箱内的一切实际操作——跑命令、读写文件、PTY、Jupyter 代码执行,它跑在 Pod 里,和用户工作负载共享网络/文件命名空间,所以能直接操作沙箱内部。好处是执行流量(可能很大,有流式输出、长连接 PTY)不经过控制面,server 不会被业务流量打爆;execd 失败只影响单个沙箱,不波及全局调度。

一个例外:server proxy。如果网络拓扑让 agent 摸不到 execd 直连地址,可以开 use_server_proxy=true,server 会用 /sandboxes/{sandboxId}/proxy/{port} 转发 HTTP/WebSocket 流量到 execd。但注意——这仍是转发,不是 server 自己执行命令;真正的执行还是在 Pod 里的 execd。这是逃生通道,不是默认路径。

落到代码位置:

行为 谁干 代码位置
create → 建 Pod 控制面 server server/services/k8s/kubernetes_service.pybatchsandbox_template.py
BatchSandbox CRD → 真 Pod K8s controller kubernetes/internal/controller/ + apis/sandbox/v1alpha1/batchsandbox_types.go
端点解析(告诉 agent execd 在哪) 控制面 server lifecycle endpoint API(/v1/sandboxes/{id}/endpoints/{port})
跑命令/文件/代码 数据面 execd components/execd/pkg/web/controller/ + pkg/runtime/(command.go/bash_session.go/pty_session.go/jupyter.go)

所以本质是:create 阶段过控制面建 Pod,execute 阶段 agent 直连 Pod 里的 execd——server 只管"把沙箱建起来并告诉你地址",不管"在沙箱里跑什么"。

一句话总结:底层 = Docker/K8s 容器(可叠安全运行时)+ execd 内部 bwrap/seccomp 进程级隔离;控制面 FastAPI,数据面/网络面/调度面全是 Go;关键代码集中在 server/services/docker|k8scomponents/execd/pkg/{isolation,runtime,web}kubernetes/internal/{controller,scheduler,task-executor}components/{ingress,egress}

十五、鉴权到底怎么做的(大白话版)

很多人第一反应是:这玩意跑在 K8s 上,鉴权肯定用 ServiceAccount token 吧?不是。 先把结论摆这儿:agent 访问沙箱全程不用 K8s 的 SA token,用的全是 OpenSandbox 自己生成的 api_key 和随机字符串;SA token 只在一个不沾边的地方出现——server 自己去调 K8s 接口时。

15.1 打个比方:进一个沙箱像进一套房子

把你要访问的沙箱想象成一套房子,要过四道门禁:

门禁 你到哪了 刷什么卡 谁发的卡
小区大门 agent → server(控制面) OPEN-SANDBOX-API-KEY 运维配的 api_key
楼栋门 agent → ingress 网关 OpenSandbox-Secure-Access + 签名 server 随机生成,塞进 K8s 注解
入户门 agent → execd(数据面) X-EXECD-ACCESS-TOKEN execd 启动时带的 token
阳台门 agent → egress sidecar OpenSandbox-Egress-Auth server 随机生成

没有一道门用的是 K8s SA token。 全是"小区物业"自己印的卡。

15.2 SA token 到底在哪——只有一个地方

server/services/k8s/client.py 里:

1
2
3
4
if self.config.kubeconfig_path:
config.load_kube_config(...) # 用 kubeconfig 文件
else:
config.load_incluster_config() # 用集群里挂进来的 ServiceAccount token

意思是:server 这个程序自己要去跟 K8s “说话”(建 Pod、查状态、建 PVC),它说话时得亮明自己是谁——这时才用 SA token(或 kubeconfig)。这就像物业经理进物业系统办业务要刷自己的工牌,跟业主(agent)能不能进房子毫无关系。agent 永远拿不到这张工牌,也不需要。

15.3 四道门禁逐个说

① 小区大门:api_key(OPEN-SANDBOX-API-KEY)

最外层。你在 ~/.sandbox.toml 里配的那个 api_key,agent 每次调 server 都得在 header 里带上。server 的 middleware/auth.py 负责验:单租户模式直接比对你配的那串;多租户模式拿 api_key 去 tenant_provider.lookup() 查你是哪个租户。没带或不匹配直接拒。还有个 startup_guard 守门:你要是不配 api_key 想裸跑,它不让你在公网上启动,防止"大门敞开"。

② 楼栋门:secureAccess(OpenSandbox-Secure-Access)

这一道只在你走 K8s ingress 网关模式、且开了 secureAccess 时才有。server 建沙箱时随手生成一个随机串(secrets.token_urlsafe(24),24 字节随机数编码成 url 安全字符串),写进这个沙箱的 K8s 注解里存着;你问"端点在哪"时,server 把这串塞进响应的 header 还给你。你后续请求网关就带着这个 header,网关(components/ingress/pkg/signature)对一下注解里的值,对得上才放行。如果还配了签名密钥,网关再验一个 HMAC 签名,防止这串被改过。

说句实在话:这串是明文存在 K8s 注解里的,网关只要有权限读注解就能看到。所以它的定位是"路由层挡一挡",不是什么高强度机密保护——真要防黑客级别的攻击,得靠后面那道入户门 + 网络隔离。

③ 入户门:execd token(X-EXECD-ACCESS-TOKEN)

这是真正到了 Pod 里、要操作沙箱(execd)时的门禁。execd 启动时通过 --access-token 参数或 EXECD_ACCESS_TOKEN 环境变量拿到一个 token;你调 /command/files 这些接口必须在 header 里带 X-EXECD-ACCESS-TOKEN,execd 的 accessTokenMiddleware(pkg/web/router.go)拿你给的跟自己存的比对,对不上返回 401。

有个坑要注意:这串是 execd 启动参数传的,如果你不配(token 为空),execd 直接跳过校验——等于入户门没锁。本地开发可以这么干,生产环境务必配上。

④ 阳台门:egress auth(OpenSandbox-Egress-Auth)

挂了 egress sidecar、想运行时改出站策略(PATCH /policy)才用得上。同样是 server 随机生成一个 token,端点响应里带给你,你改策略时带上,sidecar 验。机制跟 secureAccess 一样,随机串 + header 比对。

15.4 为什么不用 SA token

不是不会用,是不该用:

  1. agent 多半不在 K8s 集群里,它压根拿不到 SA token;就算在,把 SA token 给 agent 等于把 K8s API 的钥匙交出去了——SA token 能干的事太多(看 secrets、建 Pod……),粒度太粗,给一个只想跑两条命令的 agent 太危险。
  2. 沙箱是短命的、一次性的,起起停停,每个都该有自己的临时钥匙。SA token 是长期的、namespace 级的"工牌",跟"用完就扔"的沙箱对不上号。
  3. server 当中间人发卡最合适:api_key 是你长期持有的"业主卡";secureAccess 和 egress 两把是 server 给每个沙箱临时随机发的"访客卡",沙箱一销毁就废;但 execd token 是运维在 execd 镜像/配置里静态配的,不随沙箱发放(详见 15.6)。

所以整套就是个自研的轻量凭据体系:一道长期 api_key 守大门,三类随机 token 守到 Pod 各个面,SA token 只服务 server 自己跟 K8s 打交道。

15.5 一句话总结

agent 进沙箱过四道门:api_key(进 server)、secureAccess token(进网关)、execd token(进数据面)、egress token(改出站策略)——其中 secureAccess/egress 是 server 现场随机发的,execd token 是运维静态配的(不配则不校验),没有一把是 K8s ServiceAccount token;SA token 只有 server 自己调 K8s API 时当"工牌"用,agent 看不见也用不着。代码都在 server/middleware/auth.pyserver/services/endpoint_auth.pyserver/services/k8s/{create_helpers,endpoint_resolver}.pycomponents/execd/pkg/web/router.gocomponents/ingress/pkg/signature

15.6 secureAccess token vs execd token:别搞混这两把

secureAccess token 和 execd token 都是"门禁卡",但管的是两道不同的门、由不同的人发卡、在不同位置校验,而且是串联关系,不是替代关系。

对比表:

维度 secureAccess token execd token
header 名 OpenSandbox-Secure-Access X-EXECD-ACCESS-TOKEN
谁生成 server 现场随机(secrets.token_urlsafe(24)) 运维/execd 镜像静态配置(--access-token / EXECD_ACCESS_TOKEN)
存哪 K8s 注解 / Docker label execd 进程内存
谁校验 ingress 网关(components/ingress/pkg/signature) execd 自己(pkg/web/router.go:accessTokenMiddleware)
校验位置 agent → 网关 这一跳 agent → execd 这一跳
适用场景 仅 K8s 网关模式 + 开了 secureAccess 所有模式,只要 execd 配了 token
粒度 per-sandbox,每把不同 一个 execd 镜像/配置一把(可能多沙箱同值)
防伪强度 随机串 + 可选 HMAC 签名(OSEP-0011) 纯字符串比对,不配就跳过(等于没锁)
作用对象 路由层:能不能走网关进这个沙箱 执行层:能不能调 execd 跑命令/读写文件

它俩是串联的,各管一段。K8s 网关模式 + secureAccess + execd 配了 token 的完整链路:

1
2
3
4
5
6
7
8
9
10
agent 请求
│ 带 OpenSandbox-Secure-Access

ingress 网关 ── 验 secureAccess token(查注解 + 验 HMAC 签名) ← 第一道门
│ 放行,转发到该 Pod:44772

execd ── 验 X-EXECD-ACCESS-TOKEN(字符串比对) ← 第二道门
│ 放行,执行 /command /files /code

真正跑命令

一个请求要两道都过才能执行:secureAccess 管"你能不能从网关进来",execd token 管"你能不能调执行 API"。两把钥匙互不通用——网关不认 execd token,execd 也不认 secureAccess token。

几个容易混的点:

  1. Docker 模式下没有 secureAccess 这道门。secureAccess 只在 K8s ingress 网关模式有效(server 会校验 ingress.mode == gateway 才允许开 secureAccess)。Docker 模式 agent 直连宿主端口到 execd,只过 execd token 这一道(若配了)。
  2. execd token 不配等于没锁。execd 中间件逻辑是 if token == "" → 跳过校验,空串直接放行。secureAccess 不一样——它要靠 server 显式生成并写进注解才存在,不生成就没有这道门的概念。
  3. 发卡方不同导致轮换难度不同。secureAccess 是 server per-sandbox 现场发的,沙箱一销毁就废,天然短期;execd token 是运维配的固定值,要轮换得改 execd 镜像/配置重启,多个沙箱可能共用同一把。
  4. 强度不同。secureAccess 可叠加 HMAC 签名(防 token 被改/重放);execd token 是裸字符串比对,没签名机制,主要防"随手乱调",不防"认真攻击"。

一句话:secureAccess token = 网关发的路由通行证(server 随机发、网关验、仅 K8s 网关模式);execd token = execd 自己的执行 API 钥匙(运维配、execd 验、所有模式)。一个管"进不进得来",一个管"能不能动手",串联在一起。

参考