沙箱核心功能系列 05:沙箱内 Agent 与进程执行 API

调研对象:E2B、CubeSandbox、OpenSandbox、agent-sandbox(源码级)

前四篇是平台侧:创建、生命周期、存储、网络。本篇是沙箱内侧------Agent 发命令、读文件、拿输出,实际打到哪套接口。


一、问题

这一层拆开看,主要是五件事:

  1. 「Agent 位置与通道」:daemon(守护进程)在 guest(虚拟机客户机)/ 容器里还是宿主上;请求走公网域名,还是控制面带外直连
  2. 「进程执行」:Agent / SDK 调 API,让沙箱内 daemon fork 出进程跑命令,再回传 stdout / stderr 与退出码------不是 Jupyter 那种有状态代码执行。细节包括同步或流式、stdin / PTY(伪终端)、信号、进程组、超时、孤儿清理;kill 打单个 PID 还是整组
  3. 「文件 API」:base64 塞进 JSON(体积膨胀、大文件吃亏),还是裸字节流;路径圈禁是词法检查(只看字符串),还是解析 symlink(符号链接真实落点)
  4. 「有状态代码执行」:Jupyter kernel 焊进 daemon,还是留给模板;MCP(Model Context Protocol,给 Agent 接工具的协议)这类端点放哪
  5. 「安全模型」:daemon 常以 root 跑,API 本身就是边界;token 存原值还是只存 digest(哈希摘要);默认开不开鉴权

envd 已经是事实标准。Cube 直接装进 guest(49983、Connect-RPC、Basic user 头沿用);agent-sandbox 的 runtime KEP 把它列为两大范式之一,并带「envd 当 runtime」示例。但当前 E2B 源码里的 envd 已重写为 chi + Connect,「Jupyter 网关已删除」------标准指的是进程 / 文件协议;Cube 的 49999 Jupyter 是旧血统遗留。

第二节按这五个维度逐家拆开;第三节再横向对照。


二、四家怎么做

每家统一按:位置与通道 → 进程 → 文件 → 有状态代码 → 安全;产品特有能力(MCP、热升级、版本耦合)收在末尾。

1. CubeSandbox

「Agent 位置与通道」

guest 里两个 Agent,分工严格:

Agent 通道 职责
「envd」(用户面) 49983 Connect-RPC(gRPC 风格 RPC,跑在 HTTP 上),走 CubeProxy 通配域名 进程 / 文件 / PTY / Jupyter / 拉起 MCP
「cube-agent」(基础设施面) VM 内 PID 1(init);vsock 接 CubeShim(ttrpc) 把 OCI 镜像在 guest 里跑成容器:Create/Start/Exec/Kill/Delete、cgroup、指标

「「VM 套容器」是什么」(容易和「沙箱在 Pod 里」搞混):

less 复制代码
ounter(lineounter(lineounter(lineounter(lineounter(line宿主机 KVM └── MicroVM(沙箱的硬件边界;是宿主的直接客户机,不是跑在 Pod 里面)      └── cube-agent(PID 1)           └── OCI 容器(用户镜像)                └── 其中再跑 envd 等

沙箱创建在宿主机之上。K8s 模式下 cube-node Pod 里跑的是 CubeShim 等管理面;VM 本身始终是宿主 KVM 客户机------不是嵌套虚拟化,也不是「Pod 里再套一台虚拟机」。

为什么 guest 里还要按容器跑,而不是像 E2B 那样摊成盘直接跑进程?隔离要 VM,生态要容器:对外接 containerd / Shim v2,镜像、Exec、sidecar、按容器 cgroup 采指标都得成立。内层容器只是同内核里的 namespace / cgroup / 挂载再 exec,不是再套一层虚拟机;相对 MicroVM 本身,这点开销通常不是瓶颈。换成「直接跑」模型更简单,但会丢掉运行时 OCI 语义------那是产品选型,不是因为容器更快。

「cube-agent」 就是改过的 Kata agent + rustjail:宿主 CubeShim 经 vsock 下发 OCI 生命周期,一 VM ≈ 一 Pod。它负责「把镜像变成环境」;Agent 日常 run command 仍走 envd。SDK 自写 Connect-JSON,不依赖 e2b 包。

「进程执行」

用户面与 E2B 同范式:调 guest 里的 envd(经 CubeProxy 到 49983),不是 cube-agent。

  1. 「接命令」 --- process.Process/Start;命令固定 /bin/bash -l -c(login shell,拿完整 PATH);Connect-Timeout-Ms 硬墙钟截止(按真实时间到点就停)
  2. 「起进程」 --- 仍由 envd fork / exec(旧血统实现)
  3. 「定身份」 --- Basic user 头表达「以哪个用户跑」,默认偏 root
  4. 「回传输出」 --- 事件流里 stdout / stderr 仍是 「base64 塞进 JSON」(兼容旧 envd,大文件更吃亏;E2B 主线已改裸流)
  5. 「信号 / PTY」 --- 走同一 process.Process 服务;信号用字符串枚举(如 SIGNAL_SIGKILL)

「文件 API」

双通道:

  • /files HTTP:读写大文件
  • Filesystem Connect-RPC:ListDir / Stat / Remove / Move / MakeDir / WatchDir(目录监听)

「有状态代码执行」

  • Jupyter 仍留在 49999(旧 envd 血统;E2B 主线已删)
  • MCP 网关不进平台、进模板:SDK 经 envd 以 root 起 mcp-gateway;平台只做校验、启动、路由------沙箱里跑什么由模板决定

「安全模型」

  • X-Access-Token + Basic user 头(无密码,只用来表达「以哪个用户跑」)
  • 默认用户偏 root(兼容旧 envd「未指定用户」直接拒绝)

「版本耦合」

模板内存快照固化了 guest 里的 cube-agent------节点升级不等于沙箱升级;彻底升级只能删了重建。兼容的是已停更的旧 envd(base64、Jupyter);E2B 主线这两样已经淘汰。

2. E2B

「Agent 位置与通道」

  • envd 单端口 0.0.0.0:49983;orchestrator(节点侧编排)以 slot IP 直连,不经公网入站
  • 控制面与用户面混布同一端口:靠 x-internal 标记 + 节点代理对控制路径一律 404------隔离靠路由,不靠端口分拆
  • 已重写为 chi + Connect;旧 JSON-over-HTTP / Jupyter 网关已不存在

和 Cube 的对照:E2B 构建期也吃 Docker / OCI(拉镜像 → 抽成 ext4 rootfs → 注入 envd → 打快照),但那只是**「文件从哪来」;运行时 guest 里是普通 Linux + envd,「没有」** Docker / containerd / 内层 OCI 运行时。租户隔离交给整台 MicroVM;envd 只做「在这台小机器上起进程」(fork/exec、切用户、进 cgroup),不是简化版容器------namespace、多容器生命周期那些,因为有了 VM 就不必在 guest 里再演一遍。

「进程执行」

SDK 调 commands.run("python clean.py")(底层是 process.Process/Start),guest 里的 envd 大致走这几步:

  1. 「接命令」 --- Connect-RPC 收到请求;冻结期间直接拒绝(fork 进已冻结 cgroup 会卡在 exec 前,拖死 Go 的 GC STW,连 thaw 都救不回来)
  2. 「起进程」 --- wrapper 先写 oom_score_adj=100(OOM 时优先杀用户进程、保住 daemon),再 exec 换成真正命令------shell 被顶替,kill 该 pid 即 kill 用户命令;minimal 镜像缺 ionice / nice 时优雅降级
  3. 「定身份 / cgroup」 --- syscall.Credential 直设 uid/gid,不走 sudo;CLONE_INTO_CGROUP 让子进程生而进对应 cgroup,没有「先 fork 再迁移」的窗口
  4. 「回传输出」 --- stdout / stderr 流式推;结束后退出码保留约 30s(断连重连还能查结局);无人订阅时输出直接丢弃,不能回放
  5. 「结束 / kill」 --- 目前只杀单个 PID,不做进程组。孙进程若还握着管道写端,读端会一直等不到 EOF------源码注释里的失败模式

「文件 API」

  • 目录走 RPC;大文件走裸 HTTP,全程无 base64
  • ServeContent 支持 Range(断点续传);copy_file_range(内核零拷贝拼接)做 compose
  • /files 另有签名 URL------凭证编进 URL 哈希,token 本身不上 wire

「有状态代码执行」

Jupyter 已删;所谓 code interpreter 也只是普通 command + Python------和上面的进程执行是同一条路,不是独立的有状态 kernel。runtime 收窄到进程与文件本体。

「安全模型」

鉴权三通道:

  1. 常规:X-Access-Token(memguard:内存里加密持有,降低明文落盘风险)
  2. /files:签名 URL
  3. /init:MMDS(Firecracker 元数据口,不经普通出站网络)做 hash 校验

升级后 fail-closed(不确定就不放行):新 envd 在 token 恢复前只放行 /init 和 /health。

硬伤:token 未设置时签名校验直接放行------/init 之前 guest 内任何进程可用 root 文件 API。daemon 以 root 跑;root fallback 经 X-Envd-Defaults 打指标------按异常监控,不是设计目标。

「热升级」

冻结 → 序列化进程表 → fd(文件描述符)挪到固定槽位 → syscall.Exec 同 PID 换二进制 → pidfd re-adopt(用 pidfd 把旧进程重新接管回来)。成功标志是连接被重置------旧进程不会正常回包。

3. OpenSandbox

「Agent 位置与通道」

daemon 叫 「execd」,始终跑在沙箱里面(和 E2B 的 envd 同级)。三种底层,投放方式不同,目的一样------让沙箱里出现一个可打的 HTTP 服务:

底层 execd 怎么进沙箱
「Docker」 先起一个临时容器,从专用镜像里把 execd 二进制拷进目标目录(如 /opt/opensandbox/),再改入口脚本在后台拉起它------不是运行时反复 docker exec
「K8s」 用 initContainer(业务容器启动前跑的一次性容器)把 execd 等文件拷进共享卷,业务容器起来后就能用
「fsb」(Firecracker 快路径) 构建模板时就把 execd 写进镜像,恢复出来自带

共同点:单端口 「44772」 ;命令一律 HTTP → 沙箱内 execd → fork/exec。控制面只解析「这个沙箱的 IP/端口是啥」,SDK 拿到后**「直连 execd」**,控制面不在数据路径上。

「进程执行」

含义与 E2B 相同:SDK 发命令,execd 在沙箱内起进程,回传输出与退出码。例如跑 python clean.py → POST /command → execd fork → SSE 推日志。相对 envd,实现几乎处处反向:

  1. 「接命令」 --- 协议是 SSE(Server-Sent Events,HTTP 单向事件流),不是 Connect-RPC;头惰性提交------首个事件写出前还没定成 SSE,执行前的同步错误仍能返回普通 JSON。命令可以是一段 shell,也可以是原生 argv;没有 bash 则回退 sh
  2. 「起进程」 --- Setpgid 把自己和孩子放进同一进程组,方便整组杀掉
  3. 「定身份」 --- 请求可带 uid/gid,syscall.Credential 直设(细节见安全模型)
  4. 「回传输出」 --- 先写入私有目录 /tmp/opensandbox-execd/(权限 0700,防 workload 预置恶意 symlink),再经 SSE 增量推给客户端;保留约 24h(E2B 只留退出码 30s)。完成后多 sleep 200ms,给客户端与 sidecar 一点收敛时间
  5. 「结束 / kill」 --- Kill(-pid)(负号 = 打整组);kill 前检查 done channel,防 pid 被内核复用后误杀新进程。PTY 另有单 holder(可写)+ 多 viewer(只读),takeover=1 可抢占

「文件 API」

  • /files + /directories 全套
  • 另有身份前缀版 /v1/filesystem/{uid}/{gid}/...:每请求 fork 独立 worker,带所选凭证执行,不继承 execd 的补充组

「有状态代码执行」

和「进程执行」分开:

  • 「Jupyter」 (/code):四家里唯一还活着的内置实现;每语言一个持久 kernel(变量 / 导入跨调用保留);sql 直连沙箱内 MySQL
  • 「bash session」:无 TTY,但 env / cwd 跨调用保持------介于一次性 command 与 Jupyter 之间

「安全模型」

  • 每请求可带 uid/gid;same-identity(请求身份与当前进程相同)必须走无凭证路径------即便 uid 不变,设 Credential 也会触发 setgroups,能力已 drop 的沙箱里必然 EPERM(权限拒绝)
  • 鉴权机制在、默认关:本仓库 server 不调用 /internal/init 注入 token;token 空即全放行
  • Docker publish_host 默认 0.0.0.0(绑所有网卡)------机制存在不等于被调用

4. agent-sandbox

定位和其他三家不同:「不卖自己的 runtime 产品」 ,而是在 K8s 上游做标准化(KEP-539.2)------规定「进程 / 文件 API 该长什么样」,并给一个参考实现 「sandboxd」。灵感来自 OpenSandbox 的 REST 与 E2B 的 envd。协议按操作拆:进程走 gRPC(长连接流式推输出),文件走 REST(大文件别再包 base64)。

「Agent 位置与通道」

sandboxd 跑在沙箱 「Pod 里面」(命令实际执行在「托管 sandboxd 的那个容器」里)。对外两个端口:

端口 协议 干什么
:9090 gRPC ProcessService------起进程、推输出、发信号
:8080 HTTP FilesystemService------读写文件、健康检查、metadata

怎么让客户端连上它:复用 K8s 已有能力------kubectl port-forward、Pod IP、或 headless Service,「没有」自研公网代理。注意:官方 sandbox-router 上游禁了 HTTP/2,「带不了 gRPC」,所以 runtime 流量不能指望走 router。

sandboxd 怎么进 Pod(两种常见拓扑):

拓扑 做法 效果
「A. 专用 runtime 容器」 Pod 里一个极简容器专门跑 sandboxd 隔离强;要加工具得重建这个镜像
「B. 二进制注入」 initContainer 把 sandboxd 静态二进制拷进业务镜像的共享目录 免重建;命令跑在**「你的」**业务镜像里,能直接用里面的 python 等

踩坑:若把 sandboxd 挂成 「sidecar」 却只共享 volume------共享的是文件,不是「能看见对方镜像里的工具」。看起来像 B,实际命令仍跑在 sidecar 自己的根文件系统里,业务镜像里的软件找不到。

另外:官方示例支持直接把 「E2B 的 envd 当 runtime」 挂上(标准 Linux 容器即可)------标准接口与参考实现可以拆开用。

「进程执行」

含义仍是:客户端发命令 → sandboxd 在 Pod 内起进程 → 流式回传输出与退出码。例如 gRPC Start 跑 python clean.py。相对 envd / execd,sandboxd 刻意做「最小集」:

  1. 「接命令」 --- Start 是 server-stream:先 InitEvent(带上 daemon 自己发的 process_id)→ 若干段 stdout/stderr(「裸 bytes,不 base64」 )→ 最后恰好一个 ExitEvent。另有一元 Execute:跑完再一次性返回。可选 PTY
  2. 「起进程」 --- 身份 = sandboxd 所在容器的用户,「不做」请求级 uid 切换;也「没有」 API 级 timeout------超时交给调用方自己管
  3. 「会话 ID」 --- 虚拟 PID registry:daemon 内存里发单调递增 ID,不绑 Linux 真实 PID(只要求「当前活着的进程之间不撞号」)
  4. 「回传输出」 --- 流式即焚,不落盘、不保留(对比 execd 24h、envd 退出码 30s)
  5. 「结束 / kill」 --- SendSignal 用 Kill(-pid) 打进程组。daemon 自己要退出时:SIGTERM 广播给孩子 → 宽限 10s → SIGKILL 清道夫

「文件 API」

  • GET/PUT/DELETE /v1/files/{path}:文件回原始字节流,目录回 JSON------同一 URL,按类型变响应
  • PUT 原子写:先写临时文件再 rename,读端要么全旧要么全新
  • 路径圈禁较严:已存在路径先 EvalSymlinks(解析符号链接到真实路径)再检查仍在根内;新文件则解析「最近已存在的祖先目录」------防止用 symlink 把写入拐出沙箱根

「有状态代码执行」

KEP 把 Jupyter 列在 Full 能力级,但 「sandboxd 参考实现未做」------需要自己当普通进程在 workload 里起。

「安全模型」

  • 默认不鉴权:相信 K8s 信任链(NetworkPolicy 限制谁能连到 Pod,port-forward 要过 apiserver)。「脱离集群后这个默认不成立」
  • 可选 --auth-token-sha256:Pod 里只放 token 的 digest(哈希),不放原值------命令行、环境变量、子进程 env 就算被抄走,也推不出原 token
  • token 是 NetworkPolicy 旁边的纵深,不是替代;验的是**「集群外驱动客户端」**,不是沙箱里的代码(沙箱内代码本来就够得着本机 API)
  • /v1/metadata:只下发允许前缀的环境变量;名字里带 TOKEN / SECRET / PASSWORD 等标记的一律不下发

三、横向对比

1. Agent 位置与通道

  • E2B:guest 内只有 envd;orchestrator 带外直连;控制面 / 用户面混布单端口。OCI 止于构建期,运行时不跑容器
  • Cube:双 Agent------envd 走 CubeProxy(用户命令);cube-agent 走 vsock(OCI 生命周期)。沙箱是宿主上的 MicroVM,VM 内再套容器接生态
  • OpenSandbox:execd 在沙箱内;控制面解析端点,SDK 直连
  • agent-sandbox:sandboxd 在 Pod 内;通道靠 K8s 原语

只做执行与文件时,端点解析 + 直连够用;要注入 env、挂卷、冻结、原地升级,协议会变重。E2B / Cube 都用 VM 做强隔离;分叉在 guest 内还要不要保留容器运行时------要则换 OCI / containerd 兼容(Cube),不要则路径更短(E2B)。

2. 进程与文件

  • kill:OpenSandbox / sandboxd 做进程组 + pid 复用防御;E2B 单 PID 是已知短板
  • 命令包装:exec 替换、无 shell 残留
  • 输出生命周期:即焚(sandboxd)/ 30s 只留 exit(envd)/ 24h 全量(execd)------产品选择
  • 退出码:全员 128+signal(信号致死的惯例写法),死后可查
  • 超时:envd 当 API 契约;sandboxd 外包
  • 文件编码:E2B 新版 / sandboxd / OpenSandbox 裸流;Cube 仍 base64-in-JSON
  • 路径圈禁:sandboxd 的 symlink 解析最完整

3. Jupyter / MCP

E2B 删了 → Cube 因兼容还留 → OpenSandbox 内置 → KEP 未落地。代码执行在往 runtime 之上的可选服务挪;kernel 本质是长命进程。MCP 一致收窄:不进平台、进模板。

4. 鉴权

  • 事实开放:OpenSandbox(server 不注入)+ E2B /init 前的 /files
  • 静态 token / 签名 URL:E2B
  • digest-only:sandboxd
  • Cube:token + Basic,默认用户偏 root

形态上都有解,默认启用普遍偏弱。验收要看默认路径有没有真正调用鉴权,不能只看代码在不在。


四、自研建议

  1. 协议分治,且兼容 envd:进程 Connect / gRPC 流,文件 REST 裸流;提供 49983 + Basic user 兼容层。
  2. 进程组 kill + pid 复用防御是底线。
  3. 命令包装抄 E2B:exec 替换 + OOM 偏置 + CLONE_INTO_CGROUP。
  4. 身份分层,root fallback 可观测;same-identity 走无凭证路径;sudo 是模板能力,不是 API 能力。
  5. 鉴权抄 sandboxd digest;OpenSandbox、E2B 两个事实开放案例进评审清单;文件下载加签名 URL。
  6. 控制面与用户面分端口或显式分权;混布单端口至少公网不可达 init。
  7. 输出保留做成产品决策;退出码保留窗口必备。
  8. 热升级 v1 可不做,接口留白:进程表可序列化、升级窗口 fail-closed。
  9. Jupyter 不进 runtime;MCP 抄 Cube------平台只做校验 / 启动 / 路由。
  10. 可观测性搭 init 便车;若做 pause,冻结期间拒绝 spawn。

五、小结

Agent 循环消费的是:命令边界清楚(进程组、OOM 偏置)、结局可查(退出码窗口)、产物可取回(流式文件)。断连、超时、暂停、daemon 升级都不该逼它重跑上一步。

envd 协议是生态入口;进程组 kill 与 digest 鉴权是底线;Jupyter / MCP 进模板不进平台。四家默认鉴权都不理想------「机制被默认调用」要写进验收。

下一篇:编排调度与资源管理。

相关推荐
UNIHHIOT2 小时前
开源一套工业物联网平台:驱动崩了不影响别的协议,64MB 堆跑边缘网关
前端·后端
废柴程序员_熊熊2 小时前
DeepSeek Harness 实测:两条命令装好,11 步写完 632 行代码,3 个测试全绿
后端
用户5947614425952 小时前
Windows 部署 Playwright MCP 供 linux Codex 远程操作 Chrome:403 Forbidden 与 0 Tools 问题排查
后端
用户394995084632 小时前
Python 的哈希契约:为什么 list 不能当字典的键
后端
leo在掘金2 小时前
744B模型,32GB内存跑起来
后端·架构
dpharness2 小时前
我亲手跑了一遍存量统计,兜底类居然排到第二
后端
智迁2 小时前
MySQL 到 Kingbase 实时同步:从 Docker 部署到增删改验证
前端·后端
Ramble_Naylor2 小时前
Rust深度解读:异步流(Async Streams)与生成器的处理艺术
开发语言·后端·rust
用户360055579002 小时前
Kestrel_VLLM - 30 · MTP 头适配:十五个张量与一个不存在的 oracle
后端