OpenSandbox Isolation Session 实战:用 bubblewrap 隔离会话在单个沙箱内并行跑多个互不干扰的任务
【免费下载链接】OpenSandboxSecure, Fast, and Extensible Sandbox runtime for AI agents.项目地址: https://gitcode.com/GitHub_Trending/ope/OpenSandbox
在跑批量任务时,常见需求是:不想为每个任务都新起一个容器,但又不能让任务之间互相污染进程、环境变量和文件系统。OpenSandbox 的 Isolation Session 解决这个问题——它让沙箱内的execd为每个会话 fork 一个 bubblewrap(bwrap)子进程,bwrap建好 Linux namespace 后在其中exec一个长驻bash,每个会话拥有独立的 PID、mount、tmpfs 和 env namespace。一个沙箱 pod 因此可以承载多个互不隔离的短任务,会话创建约 100 ms,之后同一会话内的每次run开销接近于零。
适用环境(来自文档的组件版本要求):
execd>= 1.0.20,推荐 >= 1.0.21(binds、List sessions、uid_mode: "userns"与默认可写白名单需要 1.0.21)opensandbox-server>= 0.2.1(镜像声明bootstrap.execd.isolation时,server 会注入CAP_SYS_ADMIN、apparmor=unconfined和bwrap所需的 tmpfs mount)- Python SDK >= 0.1.14(
isolation.run_once/isolation.session);JavaScript/TypeScript SDK >= 0.1.10;Kotlin >= 1.0.16;C# >= 0.1.4;Go >= 1.0.4 - 主机要求:execd 镜像内自带
bwrap二进制与受信任的 native workload gate、CAP_SYS_ADMIN、内核支持overlayfs(overlay工作区模式需要)。Linux 源码构建 execd 时,需先make build-session-gate再sudo make install-session-gate,把辅助程序装到/opt/opensandbox/opensandbox-session-gate,且该路径必须 root-owned、不允许组/全局可写;缺少它时隔离能力探测 fail-closed
适合:RL rollouts、批量代码判分、多工具 agent 运行——一个 worker 一个沙箱,沙箱内跑大量互相隔离的任务。不适合:跨语言内核(用/code)、交互式 REPL(用/session)、对抗内核漏洞的硬信任边界(用 gVisor/Kata,见 Secure Container Runtime)。
先探测:确认当前沙箱支持隔离会话
所有接口挂在 execd 的/v1/isolated/下(execd 默认监听 44772 端口)。如果 execd 配置了 access token,请求要带X-EXECD-ACCESS-TOKEN头。
curl -s http://localhost:44772/v1/isolated/capabilities文档示例输出:
{ "available": true, "isolator": "bwrap", "version": "0.9.0", "setpriv_available": true, "userns_available": false, "commit_supported": false, "diff_supported": false }判断方法:
available: true说明可以创建隔离会话。available: false对应三种原因:native workload gate 缺失或不受信任、bwrap缺失、主机无法创建所需 namespace(缺CAP_SYS_ADMIN、user-ns sysctl 受限等)。setpriv_available/userns_available分别表示uid_mode: "setpriv"(默认,真实 setuid/setgid 降权)和uid_mode: "userns"(user namespace 重映射)能否建会话。这两个字段是 execd v1.0.21 之后才有的,旧版 execd 不返回,客户端要容忍缺失。commit_supported/diff_supported目前是 Phase 2 占位,当前返回503。- 注意:缺少
overlayfs不会翻转available,但workspace.mode: "overlay"的会话创建仍可能在运行时失败,依赖 overlay 模式的主机需自行验证overlayfs支持。
创建会话并执行第一个任务
最短主路径(curl,SSE 流返回stdout/error/complete事件):
# 创建会话:strict profile + overlay 工作区 SESSION=$(curl -s -X POST http://localhost:44772/v1/isolated/session \ -H "Content-Type: application/json" \ -d '{ "profile": "strict", "workspace": {"path": "/workspace", "mode": "overlay"}, "idle_timeout_seconds": 300 }' | jq -r .session_id) # 第一次 run curl -N -X POST "http://localhost:44772/v1/isolated/session/$SESSION/run" \ -H "Content-Type: application/json" \ -d '{"code": "export X=1; echo $X", "timeout_seconds": 30}' # 第二次 run 复用 shell 状态,会打印 1 curl -N -X POST "http://localhost:44772/v1/isolated/session/$SESSION/run" \ -H "Content-Type: application/json" \ -d '{"code": "echo $X"}' # 用完后销毁 curl -X DELETE "http://localhost:44772/v1/isolated/session/$SESSION"bash是长驻的,所以同一会话内一次run里export X=1对下一次run可见——但只在同一会话内可见,另一个会话看不到。这就是"互不干扰"的核心:环境变量、进程和/tmp(strictprofile 下是私有 tmpfs)都按会话隔离。
用 Python SDK 的话,同一任务可以写成:
from opensandbox import Sandbox from opensandbox.models.isolated import ( CreateIsolatedSessionRequest, IsolatedWorkspaceSpec, IsolatedRunOpts, ) async with (await Sandbox.create("python:3.11")) as sandbox: # 一次性执行 await sandbox.isolation.run_once( "python -c 'print(42)'", workspace="/workspace", profile="strict", ) # 长驻会话 async with sandbox.isolation.session( CreateIsolatedSessionRequest( workspace=IsolatedWorkspaceSpec(path="/workspace", mode="overlay"), profile="strict", idle_timeout_seconds=300, ) ) as session: await session.run("export STAGE=train") await session.run("python train.py", opts=IsolatedRunOpts(timeout_seconds=600))客户端重启后可以用sandbox.isolation.attach(known_session_id)重新挂回已知会话。
并行跑多个互不干扰的任务
文档明确给出并行模型的边界:同一会话内的run是串行的,要做并行就创建多个会话。每个会话各自持有独立的 namespace 和 bash 进程,A 会话里 export 的变量、写/tmp的文件,B 会话都不可见。
# 两个相互隔离的会话 S1=$(curl -s -X POST http://localhost:44772/v1/isolated/session \ -H "Content-Type: application/json" \ -d '{"profile":"strict","workspace":{"path":"/workspace","mode":"overlay"},"idle_timeout_seconds":300}' \ | jq -r .session_id) S2=$(curl -s -X POST http://localhost:44772/v1/isolated/session \ -H "Content-Type: application/json" \ -d '{"profile":"strict","workspace":{"path":"/workspace","mode":"overlay"},"idle_timeout_seconds":300}' \ | jq -r .session_id) # 两个 run 同时在两个会话里执行 curl -N -X POST "http://localhost:44772/v1/isolated/session/$S1/run" \ -H "Content-Type: application/json" \ -d '{"code":"export TASK=a; sleep 3; echo task-a in $TASK"}' & curl -N -X POST "http://localhost:44772/v1/isolated/session/$S2/run" \ -H "Content-Type: application/json" \ -d '{"code":"export TASK=b; sleep 3; echo task-b in $TASK"}' & wait # 验证互不干扰:S1 看不到 S2 的 TASK 值 curl -N -X POST "http://localhost:44772/v1/isolated/session/$S1/run" \ -H "Content-Type: application/json" \ -d '{"code":"echo TASK=$TASK"}' # 输出 a;若输出 b 说明隔离失败创建多个会话时的成本结构要心里有数:创建会话约 100 ms(execd 在bwrap启动后短暂等待以检测立即退出的子进程),之后同一会话内每次POST /run复用同一个 bash,开销可忽略。所以批量任务的设计原则是把创建成本摊薄到一个会话内的多次run上,而不是每跑一条命令就建删会话。
选择工作区模式:任务之间文件系统怎么隔离
workspace.mode决定会话看到的/workspace语义,与 profile 相互独立(省略时 execd 会把mode归一化为overlay):
| 模式 | 语义 |
|---|---|
rw | 读写 bind-mount,写入持久化到宿主机 |
overlay(默认) | overlayfs copy-on-write;写入落在每会话独立的 upper 目录,DELETE会话后消失 |
ro | 只读 bind-mount,写入报EROFS |
并行任务的默认选择就是overlay:各会话写同一path互不影响,会话销毁后写入自动消失,也避免了一个池化沙箱的占用者把数据泄漏给下一个占用者。只有任务间需要交换文件时才用rw(写入落宿主机)或额外 bind。
额外暴露路径用两个字段,source 都会先做 symlink 解析再对照allowed_writable白名单(默认/workspace、/mnt、/media、/data,含子路径;空白名单拒绝一切额外 bind):
{ "workspace": { "path": "/workspace", "mode": "rw" }, "extra_writable": ["/data/scratch"], "binds": [ { "source": "/data/in", "dest": "/mnt/in", "readonly": true }, { "source": "/data/out", "dest": "/mnt/out" } ] }binds的dest必须已存在于沙箱镜像内(bwrap无法在只读根下新建挂载点),需要时打进镜像。
长任务:同一会话内的后台 run
如果希望某个任务跑着的同时还能在同一会话上继续其他工作,用 background run(文档中作为同一会话内的补充手段):
# 启动后台 run;202 返回 {"session_id", "run_id", "started_at"} RUN=$(curl -s -X POST "http://localhost:44772/v1/isolated/session/$SESSION/run" \ -H "Content-Type: application/json" \ -d '{"code": "sleep 5 && echo done", "background": true}' | jq -r .run_id) # 轮询状态直到 running=false curl -s "http://localhost:44772/v1/isolated/session/$SESSION/runs/$RUN" # 读输出(纯文本),EXECD-ISOLATED-TAIL-CURSOR 响应头给出增量读取的下一字节偏移 curl -s "http://localhost:44772/v1/isolated/session/$SESSION/runs/$RUN/logs?cursor=123"后台 run 的边界:timeout_seconds只对前台 run 生效,后台 run 不限时;后台 run 活跃期间该会话的 idle GC 挂起;ro工作区会因无可写日志位置而拒绝后台 run(400),rw/overlay可以;每次logs最多返回 16 MiB,单 run 日志保留上限也是 16 MiB,超出部分在 run 结束时丢弃,需要更多时要增量拉取。
验证隔离是否按预期工作
结合上面的步骤,核对点是:
- 能力探测:
/v1/isolated/capabilities返回available: true、isolator: "bwrap"。 - 会话状态隔离:在会话 A 里
export的变量,会话 B 里查不到(SSE 输出里应为空或旧值),同一会话内的第二次run能看到第一次的export结果。 - 工作区隔离:两个
overlay会话对/workspace各写一个文件,互相ls不应看到对方的写入;DELETE任一会话后,其 overlay 写入消失。 - 会话清单:
GET /v1/isolated/sessions列出当前活跃会话;GET /v1/isolated/session/{id}返回完整状态并回显创建参数,无状态客户端可据此重建句柄。 - 销毁:
DELETE /v1/isolated/session/{id}后,对已销毁会话的run会返回IsolatedError(session process has exited或ErrContextNotFound)。
失败处理与限制
非正常路径的行为文档有明确定义,处理时可以照表判断:
| 现象 | 行为 | 处理 |
|---|---|---|
前台run的timeout_seconds到期 | execd 取消 run context,向 bwrap 进程组发SIGINT,发IsolatedErrorSSE 事件;会话本身存活 | 换更短的命令分段或调大超时后重跑 |
| 客户端在 SSE 中途断开 | 请求 context 取消,execd 对运行中命令发SIGINT;run不会转入后台继续 | 需要跑完就改用background: true |
bwrap进程退出 | 下一次run返回IsolatedError(session process has exited) | DELETE后重建会话 |
| idle 超时到达 | GC 执行与DELETE相同的拆除 | 把idle_timeout_seconds设为0可禁用 idle GC,改为显式DELETE |
限制项(直接决定这套方案能用到哪里):
diff/commit是 Phase 2 占位,当前返回503(见 OSEP-0013)。- 没有硬件级保证:只有 namespace + seccomp,需要对抗内核漏洞时配合 gVisor/Kata 安全运行时。
- 仅支持 Linux,非 Linux 构建返回
available: false。 - 同一会话内 run 串行,并行必须多会话。
share_net省略时默认共享沙箱网络 namespace;沙箱级的 egress 与 Credential Vault 策略仍然生效。私有网络会话(share_net: false)无法跨越 execd 重启恢复,启用了它的部署要把 execd 视为沙箱关键进程,execd 退出时整个沙箱需要重建。- 会话状态只存在于 execd 内存中,execd 重启后所有会话状态不保留;
upper_root下残留的 overlay upper 目录会在启动时被回收。
服务端配置(可选)
隔离行为由 execd 读取的可选 TOML 控制,通过--isolation-config或环境变量EXECD_ISOLATION_CONFIG指定,完整示例见 isolation.example.toml:
# 每会话 overlay upper 目录的父目录 upper_root = "/var/lib/execd/isolation" # 所有会话 upper 目录的总大小硬上限(字节),默认 8 GiB;0 表示禁用配额 upper_max_bytes = 8589934592 # extra_writable / binds 允许的 source 前缀(symlink 解析后检查) # 默认 ["/workspace", "/mnt", "/media", "/data"];置空则拒绝所有额外 bind allowed_writable = ["/workspace", "/mnt", "/media", "/data"]多会话并行写放大时,upper_max_bytes是每个沙箱内全部会话共享的配额,会话可能因超配额而创建失败,需要按并发任务数和工作集大小调整。
进一步阅读:execd 组件文档、OSEP-0013 Isolated Execution API、Secure Container Runtime。
【免费下载链接】OpenSandboxSecure, Fast, and Extensible Sandbox runtime for AI agents.项目地址: https://gitcode.com/GitHub_Trending/ope/OpenSandbox
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考