Agent Pool + Cube 沙箱:架构方案
2026-10-30。dsh(大脑)从沙箱里挪出来,常驻在一个 Agent Pool 里;只有需要执行工具时,才给会话绑定一台 CubeSandbox(手)。本文先 review 现方案,再给目标架构、要验证的点,以及需要你在腾讯云上准备的 Cube 配置。
标 估算 的是推算;标 待验证 的是方案依赖、但还没实测的假设。
:8081。选了 AGS 托管(§10.1),工具 cs-tools-022,广州。dsh 是共享容器 cs-pool 里的 worker 进程(2 个预热),工具经 E2B 协议打到每会话一台的 AGS 沙箱;工作区快照在用过工具的一轮后和每次暂停前保存,沙箱丢失或归档后从快照重建;bash 换成跨调用、跨暂停都保留状态的持久 shell。make smoke-hands PAUSE_CHECK=1 TC_PY=… 全部通过。与本文方案的出入:会话目录和快照放在本机磁盘(单机够用,多机时换 CFS / COS);预热 worker 不复用,一个 worker 只服务一个会话。现行架构、一次完整对话的流程、耗时与开销以 docs/architecture.html 为准;本文保留为方案与决策记录,下文的估算已被那里的实测取代。结论
ctx.fs(文件)、ctx.subprocess(进程和终端)、ctx.shell(bash)。官方 README 写明“根据命令运行位置选择本地或远程执行”。我们写一个 Cube 版的实现插件,dsh 就能留在 Pool 里,把工具打到沙箱上执行。dsh-e2b 系列,停在 0.1.5-rc.3,比 dsh 0.2.1 落后一个大版本。它在插件启动时就建沙箱、关闭时删沙箱,不支持重连、暂停、模板。我们要自己写一个懒绑定、可暂停、可重连的版本,可以参考它的代码(MIT 许可)。1. 现方案 review
现状(方案 A):每个会话一个 Docker 容器,dsh 跑在容器里,网关通过 envd 持有 dsh 的 stdio。休眠时先 session/close,再杀 dsh,最后 docker stop;唤醒时 docker start,重启 dsh,再 session/resume。
| 问题 | 原因 | 依据 | Pool 方案下 |
|---|---|---|---|
| 模型 key 在沙箱里 | dsh 在容器里调模型,key 在 dsh 进程的环境变量中。模型执行的命令能否读到 /proc/<dsh>/environ 还没测 | handoff §10 #3 | 解决:key 只在 Pool |
| 等审批时永不休眠,一直占名额 | dsh 挂着审批请求,容器停了就丢 | handoff §10 #2 | 解决:dsh 在 Pool 里等,沙箱可以暂停 |
| 休眠丢进程、丢 shell 状态 | docker stop 杀光所有进程 | cube-restore-report §4 | 解决:Cube 暂停保留进程 |
| 每次唤醒冷启动 dsh 约 2 秒(占启动的 85%) | dsh 跟着容器一起停 | architecture §4.2 | 解决:Pool 里有预热好的 dsh |
| 只聊天也占一台 2C4G 的容器 | 沙箱和会话一一绑死 | — | 解决:第一次调工具才建沙箱 |
| 隔离弱:容器和宿主共享内核 | Docker | — | 解决:MicroVM 有独立内核 |
| 单机,换机器会话就丢 | 状态都在一台机器的磁盘上 | — | 部分:会话日志要放到共享存储(§4.3) |
| dsh 的 bash 每次调用都是新 shell,变量留不住 | 默认加载 dsh-tool-bash | 2026-10-29 实测 | 可选:换持久 bash,依赖远程终端(§9 S4) |
现方案做对了、要保留的:
- 事件流(seq + SSE +
Last-Event-ID)是唯一的渲染源,网关自己是 ACP 客户端; - 会话按设备隔离,他人会话一律 404;
- 文件访问严格限制在 workspace 内;
- App 侧 API。
这些在新方案里都不变。
2. 目标架构
实线是每轮都走的路径,虚线是按需或后台的路径。蓝线是新增的关键路径:每次工具调用都从 Pool 打到沙箱里的 envd。
| 组件 | 职责 | 状态在哪 |
|---|---|---|
| Gateway | 不变:鉴权、会话、事件流、审批、App 的文件 API。新增:把会话分配给一个 dsh worker | 事件日志、会话元数据 → 持久存储 |
| Agent Pool | 常驻一批 dsh 进程。一个活跃会话占一个 dsh 进程(原因见 §3.3)。保持若干个已启动、未分配的预热进程 | dsh 会话目录 → 共享存储,worker 本身无状态 |
| cube 执行插件(我们写,运行在 dsh 进程里) | 实现 ctx.fs 和 ctx.subprocess,把操作转成 envd 调用。第一次被调用时向 Broker 申请沙箱 | 无 |
| Sandbox Broker(网关里的一个模块) | 会话和沙箱一一对应;负责懒创建、恢复、暂停、归档,以及恢复失败时的兜底;校验沙箱身份;对接 Cube 或 AGS 的 API | 绑定表 → 网关元数据 |
| 沙箱 | 只做“手”:文件、命令、终端。镜像里是工具链加 envd,不装 dsh | workspace 在沙箱盘上,轮末归档到 COS |
3. 关键:dsh 的执行世界
3.1 dsh 0.2.1 的接口(README 原文,从运行镜像里读取)
| 接口 | 本地实现(现在加载的) | 谁在用它 |
|---|---|---|
ctx.fs:读、写、列目录、监听、原子编辑 | dsh-fs-local | dsh-tool-fs(read / write / edit)、dsh-workspace-changes |
ctx.subprocess:起进程、真实终端(PTY)、收集输出、终止 | dsh-subprocess-local | dsh-tool-fs-search(grep / glob)、dsh-terminal-bash(持久 shell)、dsh-workspace-changes(git) |
ctx.shell:bash 工具的执行方式 | dsh-bash-sandbox(landlock)/ dsh-bash-local | dsh-tool-bash |
dsh-subprocess 的 README 原文:“每个组合配置一个 subprocess 实现,并根据命令运行位置选择本地或远程执行。”dsh-terminal-bash 也写明“同一个后端既可以与本地执行世界组合,也可以与远程执行世界组合”。所以只要替换这几个实现,工具层不用改。
另外,dsh-e2b 的 README 列出了留在宿主(也就是 Pool)里的东西:Cordis 服务、agent 和会话状态、会话日志、模型请求、skill。这正是我们要的切分。
3.2 为什么不能直接用 dsh-e2b
Cube 兼容 E2B 协议,按理把 dsh-e2b + dsh-fs-e2b + dsh-subprocess-e2b 指向 Cube 就行。但是:
- 版本落后:最新只有 0.1.5-rc.3(2026-09-22),依赖 cordis 4.0.2;dsh 已经是 0.2.1-rc.5,依赖 cordis ~4.0.5。能不能装进 0.2.1 未知 待验证。
- 生命周期不对:插件加载时就建沙箱,dispose 时删沙箱,超时也是删除(
onTimeout: 'kill')。我们需要懒创建、按会话持久、暂停而不是删除、Pool 重启后能重连到原沙箱。 - README 自己写的“不在范围内”:重新连接、暂停保留、模板、卷、快照、网络策略、工作区同步。
所以我们写一个自己的执行插件 cloudsandbox-exec-cube(名字暂定):
- 对上实现 dsh 0.2.1 的
ctx.fs、ctx.subprocess、ctx.shell; - 对下用 E2B JS SDK 或直接调 envd 的 Connect-RPC;
- 沙箱句柄从 Broker 懒获取,允许过期后重新获取;
- 适配层逻辑尽量照搬
dsh-fs-e2b/dsh-subprocess-e2b,比如bash -l -c的引号转义、随机 HOME。
如果 DeepSeek 后续发布了支持外部生命周期的 0.2.x e2b 系列,就换回官方实现。这件事值得去问 dsh 团队,handoff 里已经列为待确认项。
3.3 一个会话占一个 dsh 进程
dsh 的执行世界是进程级的:一个组合里只有一个 ctx.fs 和一个 ctx.subprocess,dsh-e2b 也写明“一个沙箱,一个句柄”。如果一个 dsh 进程同时服务多个会话,它们会共用同一个沙箱,这就违反了按设备隔离。所以:
- 一个活跃会话对应一个 dsh 进程。现在单个容器里的 dsh 占用约 170–415 MiB(含工具进程),只算 dsh 本身会更少 估算。
- 预热池:提前启动 N 个 dsh 进程,完成
initialize,但还没有会话、没有绑定沙箱。会话来了就分配一个,发session/new或session/resume。dsh 启动的 2 秒就从关键路径上去掉了,只剩 resume 的约 60 ms(实测)。 - 插件怎么知道自己属于哪个会话:worker 启动时带一个 worker id 和一个一次性 token。插件第一次被调用时,拿它们去问 Broker “我该绑哪台沙箱”。Broker 根据“worker 分给了哪个会话”回答。会话 id 由网关决定,不由插件声明。
- 回收:会话空闲 T1(比如 10 分钟)后,
session/close再退出这个进程,把名额还给池子。这一步不影响沙箱,沙箱有自己的暂停策略。
4. 生命周期与状态
4.1 两条独立的生命周期
| 对象 | 状态 | 何时转换 |
|---|---|---|
| dsh worker(大脑) | 预热 → 已分配 → 已释放 | 会话第一次收到消息时分配;空闲 T1 后释放;之后来消息就从池里再分配一个,再 session/resume |
| 沙箱(手) | 无 → 运行 → 已暂停 → 已归档 |
第一次调工具时创建; 没有工具在跑,并空闲 T2(比如 2 分钟)后暂停,等审批期间同样可以暂停; 下次调工具时恢复; 暂停超过 T3(比如 7 天)后把 workspace 归档到 COS,再删除沙箱; 归档后再调工具:新建沙箱,从 COS 还原 workspace |
好处是两边各省各的:只聊天的会话只占大脑;用户离开了,大脑先释放,手稍后暂停,都不需要对方配合。
4.2 恢复失败的兜底
AGS 上实测恢复 100/100(cube-restore-report.html §9),本机实验环境则只有 8/18(§6)。生产形态没问题,但这只是 100 次样本,Broker 仍然要有退路:
- 重试:恢复失败后,等沙箱状态回到
paused再重试,最多 N 次(实测有的在第 4 次才成功)。 - 冷重建:从最近一次轮末归档新建沙箱,还原 workspace,并在工具结果里告诉模型“远程电脑已重建,之前的进程和终端状态已丢失”。
- 身份校验:每次绑定或恢复后,先读出沙箱里的一个随机 nonce,与 Broker 记录的比对,不一致就拒绝使用(对应报告 §6.2 观察到的错误路由)。
dsh 在 Pool 里,所以沙箱出问题只会表现为某次工具调用失败,对话本身不会断。这正是 WorkBuddy 从 SANDBOX 迁到 LAMP 的理由(见 docs/research/mobile-cloud-computer.md §1.2)。
4.3 状态放在哪
| 状态 | 放在哪 | 备注 |
|---|---|---|
| 事件日志、会话元数据 | 网关,现在是本地磁盘 | 多节点时迁到 MySQL / COS,这是独立的一步 |
| dsh 会话目录(对话上下文) | Pool 节点的共享存储(CFS / NFS),或者轮末归档到 COS、分配时取回 | 任何 worker 都能 session/resume。现在固定 DSH_HOME 的约束继续保留 |
| workspace | 沙箱盘上;轮末增量归档到 COS(tar 或 git bundle) | 不建议直接用 Cube 的 S3 Volume(s3fs)当 /workspace:git 和 npm install 这类小文件密集操作在 s3fs 上会很慢 待验证 |
| 进程、shell、内存 | 沙箱暂停快照 | 只在 L1(暂停)期间有效,归档后丢失 |
App 的文件 API 改成通过 envd 访问沙箱,沙箱暂停时会触发恢复。沙箱已归档时直接读 COS 上的归档,不用为看个文件把沙箱拉起来。
5. 一轮对话怎么走
App POST /prompt ─► Gateway
Gateway: 会话没有 worker → 从预热池取一个 → session/resume(约 60 ms)
Gateway ─ session/prompt ─► dsh(Pool)
dsh ─► 模型:思考、回复文字 (不碰沙箱)
dsh: 要执行 bash / read / grep
cube 插件 ─► Broker:申请沙箱
Broker:无 → create 已暂停 → connect(恢复) 运行中 → 直接用
Broker:nonce 校验 ─► 返回 envd 地址和访问令牌
cube 插件 ─► envd:执行;结果返回给 dsh
dsh ─► session/update 事件 ─► Gateway ─► SSE ─► App (与现在相同)
dsh: 需要审批 → Gateway 挂起请求;Broker 可以让沙箱先暂停
turn_end → Broker:增量归档 workspace;空闲 T2 后暂停沙箱
6. 安全
- 模型 key 不进沙箱。沙箱里只有模型生成的命令,没有我们的任何凭据。
- 沙箱出网:AGS 没有按域名的出站白名单,只有
PUBLIC(可出公网)/VPC(安全组按 IP 控制,出公网需 NAT)/SANDBOX(断网)三种(docs/research/ags-custom-image.md§7)。第一期用PUBLIC:沙箱里已经没有我们的凭据,主要风险是被当作出网跳板。之后需要的话,改成VPC+ NAT + 我们自己的出站代理。 - envd 入口:开启每沙箱访问令牌(
allow_public_traffic=false),只有 Broker 知道令牌,再下发给对应的 worker。 - Pool 与 Broker 之间:worker 的一次性 token 只能换到它被分配会话的沙箱。插件运行在 dsh 进程里,但模型生成的代码不在 Pool 执行,所以拿不到这个 token。前提是 Pool 里不加载任何本地执行的 provider,这一点要在组合配置里锁死,并写测试。
- 身份校验:见 §4.2,防错误路由。
- 保持不变:他人会话 404、App 文件 API 限制在 workspace 内(换成 envd 后,路径校验在网关侧做,同样要防符号链接逃逸 待验证)。
7. 时延与成本
7.1 每次工具调用的往返
AGS 实测(2026-10-30,从服务器 9.135.181.73 到广州 AGS,HTTPS 长连接,各 50 次):
| 操作 | p50 | p95 |
|---|---|---|
| 读文件 4 KB | 23.1 ms | 35.3 ms |
| 写文件 4 KB | 21.1 ms | 26.0 ms |
| 列目录(300 个文件) | 27.6 ms | 33.6 ms |
跑一条命令(true) | 25.0 ms | 28.8 ms |
rg 搜 300 个文件 | 31.9 ms | 39.4 ms |
达到验收线(p50 ≤ 30 ms、p95 ≤ 100 ms,命令 p50 25 ms)。一轮 30 次工具调用多出约 1 秒。Pool 直接放在现在的服务器上即可。
下面是更早的本机实验环境数据,仅作对照:
| 操作(经 CubeProxy,本机实验) | p50 | p95 |
|---|---|---|
| 读文件 4 KB | 90 ms | 404 ms |
| 写文件 4 KB | 44 ms | 83 ms |
| 列目录 | 52 ms | 118 ms |
跑一条命令(true) | 238 ms | 696 ms |
跑 grep -r | 158 ms | 408 ms |
环境:Mac → Lima 虚拟机 → KVM 嵌套,CubeProxy 是 amd64 镜像,在 arm64 上用 qemu 模拟,每次调用都是 TLS。所以这些是悲观上限。绕过代理直连 envd 的对照组没测成,实验集群开始拒绝新建沙箱(408)。
估算 一轮 30 次工具调用,每次平均 150 ms,多出约 4.5 秒;模型首字本身要 8–21 秒(handoff §7),这个量级可以接受。如果同可用区 x86 上每次在 10–30 ms,就只多 1 秒左右。验收线建议:同可用区 envd 命令 p50 ≤ 30 ms、p95 ≤ 100 ms。
可以降低往返的手段:
- Pool 与 Cube 放在同一可用区;
- 保持 HTTP/2 长连接,不要每次都握手 TLS;
- dsh 的工具单次调用本来就是一个 envd 请求,不需要额外合并。
7.2 资源与成本 估算
| 项 | 现在 | Pool 方案 |
|---|---|---|
| 只聊天的会话 | 一台 2C4G 容器 | 一个 dsh 进程(约 100–200 MB) |
| 活跃的编码会话 | 一台容器 | 一个 dsh 进程 + 一台沙箱 |
| 等审批 | 容器一直运行、占名额 | dsh 进程等待,沙箱暂停(只付存储) |
| 离开后 | 容器停止,冷恢复 2.3 秒 | worker 释放;沙箱暂停,恢复后进程都在 |
| 新增固定成本 | — | Pool 节点(几台常驻机器);Cube 控制面(AGS 托管免运维,或自建约 2.7 GB 内存加运维) |
8. 对现有代码的影响 估算
| 模块 | 改动 |
|---|---|
internal/sandbox/ | 新增 cube.go:Cube / AGS 控制面客户端(create、pause、connect、kill、snapshot)。Docker 运行时保留,用于本地开发 |
internal/gateway/session.go | 拆成两个状态机:worker(大脑)和沙箱(手)。ensureReady 只负责分配 worker;沙箱交给 Broker |
新增 internal/pool/ | 预热 worker 的启停、分配、回收。第一版 dsh 进程可以直接跑在网关所在机器上(本地子进程),之后再拆成独立 Pool 服务 |
新增 internal/broker/ | 绑定表、懒创建和恢复、重试与冷重建、nonce 校验、轮末归档。给插件提供一个只对内的 HTTP 接口 |
| 新增 dsh 插件(Node / TS) | ctx.fs、ctx.subprocess、ctx.shell 的 Cube 实现,加组合配置(替换 fs-local、subprocess-local、bash-sandbox) |
files.go | App 的文件 API 改为调 envd;已归档时读 COS |
image/ | 拆成两个镜像:Pool 镜像(dsh + 插件)和沙箱模板镜像(工具链 + envd,不装 dsh) |
| App 侧 API | 基本不变。sandbox 字段的含义改为“手”的状态(无 / 运行 / 暂停 / 归档),另加 worker 的状态 |
9. 开工前要验证的点(spike)
按风险排序。S1 到 S3 任何一条不成立,方案就要调整。
| # | 验证什么 | 怎么验 | 不成立时 |
|---|---|---|---|
| S1 通过 | dsh 0.2.1 能否加载我们的 ctx.fs / ctx.subprocess 实现,替换本地实现后工具都能工作(见 §9.1) | 先试把 dsh-*-e2b 0.1.5 装进 0.2.1,指向 Cube;不兼容就写最小实现,跑 read / write / edit / bash / grep / glob | 向 dsh 团队要 0.2.x 的远程实现,或者退回现方案 |
| S2 通过 | 同可用区每次 envd 调用的时延 | x86 Cube 上跑本文 §7.1 的脚本,加一组直连 envd 的对照 | 超过验收线:评估把 grep / glob 合并成一次远程命令 |
| S3 通过 | x86 上暂停 / 恢复的成功率(AGS 100/100,见 cube-restore-report §9) | 报告 §8 的 100 次测试 | 不用 L1 暂停,只用冷重建(仍比现在好:大脑不停) |
| S4 通过 | 持久 shell 能否跑在远程 PTY 上(spawnTerminal → envd PTY)。能;但沙箱暂停后 PTY 输出流会静默失效(不报错也不再有数据),fork 里按 lease 的 epoch 判断并按 pid 重连 | 启用 dsh-tool-bash-persistent | 继续用一次性 bash |
| S5 已解决 | dsh-tool-fs-search 在远程世界里起的是什么进程;沙箱镜像需要装什么 | 读插件代码,抓它的 spawn 参数 | 在模板镜像里装对应的二进制 |
| S6 | 子 Agent 和 stdio MCP 也走 ctx.subprocess,会被起在沙箱里,这是否符合预期 | 各跑一次 | 子 Agent 先禁用(与旧方案 B 的结论相同) |
| S7 | ctx.fs 的监听(watch)在远程怎么实现 | 看哪些插件依赖 watch | 远程实现返回“不支持”,或者用轮询 |
9.1 S1 结果(2026-10-30,AGS,经 E2B 协议)
做法:dsh 0.2.1 + 官方 dsh-fs-e2b / dsh-subprocess-e2b 0.1.5-rc.3 + 我们写的 cube-runtime(替换 dsh-e2b 管生命周期)。用一个按脚本发工具调用的假模型驱动(当天模型 relay 故障,而且脚本更可重复),在服务器上跑。细节和复现:poc/dsh-e2b-s1/README.md。
| 项 | 结果 |
|---|---|
| 功能 | 12 / 12:bash(在沙箱里,x86_64,宿主的 key 类变量一个都没带进去)、write / read / edit(含版本防护)、grep / glob、后台任务、相对路径、stderr 与退出码 |
| 每次工具调用耗时 | 0.4–1.5 秒;同样脚本用本地工具约 35 ms。单次 envd 往返只要 ~25 ms(§7.1),所以问题在调用次数 |
| 调用次数 | 12 步共 844 次 SDK 调用、600+ 次 envd 请求,每步 30–78 次 |
为什么慢(用 SDK 层追踪定位):
dsh-fs-e2b每解析一次路径就在沙箱里起一个进程跑realpath … | base64,再getInfo一次,没有缓存。光/workspace就解析了 55 次。- 每次请求模型前,系统提示词相关插件都要探测
/workspace/.git、/.git、AGENTS.md等,每步约 17 次。其中$DSH_HOME/AGENTS.md、~/.agents/AGENTS.md本该读宿主上的配置,现在也被送去了沙箱:ctx.fs是全局的,换成远程后所有读文件的插件都跟着变远程。 - 命令结束靠读沙箱里的 exit-code 文件轮询(
pollMs默认 20 ms)。
结论:dsh 的执行世界接口确实能把工具整体搬进 AGS,方向成立;但官方 e2b 适配器只是 POC 质量,不能直接上线。下一步写我们自己的 ctx.fs / ctx.subprocess:
- 路径解析用 envd 的 Stat,不起进程,并按轮缓存;
- 宿主配置路径(
DSH_HOME、~/.agents)走本地,只有工作区走远程; - 进程结束靠 envd 的进程事件流,不轮询文件。
目标是每次工具调用 1–3 次往返,也就是 50–100 ms。
9.2 优化后(2026-10-30)
按上面三条原因,把两个官方适配器 fork 成我们自己的 cloudsandbox-dsh-fs-cube、cloudsandbox-dsh-subprocess-cube(MIT,改动集中、可 diff),cube-runtime 负责记录沙箱里命令的启停。同一组 12 步脚本,在服务器上:
| 工具 | 本地工具 | 官方 e2b 适配器 | 优化后 | 优化后,模型每步思考 3 s |
|---|---|---|---|---|
| read | 35 ms | 0.8–1.1 s | 67 ms | 65 ms |
| write / edit | 35 ms | 1.0–1.4 s | 117–196 ms | 123–197 ms |
| bash | 35 ms | 0.7–1.4 s | 199–230 ms | 234–347 ms |
| grep / glob | 35 ms | 0.7–1.4 s | ~183 ms | 196–230 ms |
| 12 步总 SDK 调用 | — | 844 | 231 | 274 |
“模型每步思考 3 s”用假模型的响应延迟模拟。它验证缓存不依赖时间窗口:真实模型的两次工具调用之间隔着好几秒,靠 TTL 的缓存会全部过期。
做了什么:
- 路径解析不再起进程:用 envd 的 Stat,路径不是符号链接时,规范路径 = 父目录的规范路径 + 名字;符号链接才退回
realpath进程。 - stat 和规范路径按“代”缓存:沙箱里有 agent 自己的命令(以 background 方式启动的那条)开始或结束时换代;只有命令还在跑时才加 2 s TTL。换代时把上一代查过的路径并行重查一遍,模型请求前那一串顺序探测(
AGENTS.md、.git、skills 目录)就都能命中。 - 宿主配置路径不进沙箱:
hostPaths($DSH_HOME、~/.agents)直接按“不存在”处理。 - 写文件:从 9 次往返降到 4 次(写临时文件 → 一条命令 chmod 加发布 → stat)。只清掉被写的路径的缓存。
- 跑命令:沙箱登录环境每个沙箱只读一次;状态目录不再单独建、两次 chmod 并进包装脚本。进程组跟踪、kill、输出编码不变。
正确性:
- 符号链接用例全部正确:经由链接读、编辑、写,都落到真实文件。
- 版本防护正确:读完文件后,被 bash 改掉、或者被后台任务改掉,再编辑都会被拒绝(
file changed since it was read);重新读能拿到新内容。
还能再快的地方:bash 一步还有约 6 次往返(写状态文件、启动、读状态、清理、ps 查进程组),可以把清理和进程组检查放到后台。离 50–100 ms 的目标还差一点,但已经不影响体验:一轮 30 次工具调用多出约 5 秒,模型本身要几十秒。
9.3 真模型端到端(2026-10-30)
模型换成 DeepSeek 官方 API(deepseek-flash,OpenAI 兼容),任务是:在 /workspace/calc 下写一个 Python 小包(add / mul / safe_div),用 pytest 写测试并跑通,最后用 grep 确认 safe_div 里有 ValueError。工具全部在 AGS 沙箱里执行。
| 指标 | 结果 |
|---|---|
| 结果 | 6 个 pytest 用例全部通过(10 passed / 6 passed 取决于生成的用例数),grep 命中 raise ValueError |
| 整轮墙钟 | 25 秒(含建沙箱、装插件、6 次模型请求) |
| 模型请求 | 6 次,合计 9.3 秒 |
| 工具执行 | 5 步合计 1.5 秒,p50 373 ms,最长 1.3 秒(一次 ls 加一次 pytest) |
| SDK 调用 | 155 次,其中读文件 10 次(修掉 20 ms 轮询前是 6905 次,读 6006 次) |
| 工具调用 | 3 次 bash、2 次 write、1 次 grep |
途中发现的问题:第一版跑同一个任务要 157–394 秒,瓶颈是沙箱从公网 PyPI 装 pytest 只有 15–20 KB/s(1.3 MB 要两分多钟),而且 Debian 的 Python 是 externally-managed,直接 pip install 会被拒。镜像里加上腾讯云 pip / npm 镜像、预装 pytest、允许系统级 pip 安装后,同一任务降到 25 秒。这说明沙箱镜像该预装常用工具,而不是让模型现场装。
途中发现并解决的问题(都已写进代码或镜像):
| 问题 | 处理 |
|---|---|
dsh-e2b 建沙箱不带模板(E2B 默认 base 在 AGS 上不存在),启动即建、退出即删 | cube-runtime 替换它 |
AGS 对所有沙箱都报告 envdVersion = 0.2.10,与镜像里实际的 envd 无关。E2B SDK 按这个数字禁用 stdin、closeStdin、文件元数据 | cube-runtime 按配置的真实版本重建 SDK 句柄。这是 AGS 的 bug,应反馈 |
AGS 官方 envd 只有 v0.5.14;dsh-fs-e2b 的版本防护要文件元数据(envd ≥ 0.6.2) | 镜像换成上游 E2B envd 0.10.2(Apache-2.0,自己编译)。实测能在 AGS 上启动、鉴权、读写元数据 |
E2B SDK 默认以 user 用户执行 | 镜像里建 user(uid 1000) |
dsh-tool-fs-search 按宿主上 rg 的绝对路径起进程,不可配置 | 镜像里在同一路径放一个指向系统 rg 的软链接(要求 Pool 的 dsh 安装路径固定) |
permission 预设插件拒绝不做隔离的 bash 执行器 | 远程世界禁用它(隔离边界是 MicroVM) |
cordis 用 Proxy 包装服务,JS 私有字段(#x)不可用 | 插件里用普通字段 |
10. 腾讯云 Cube 配置清单(请你准备)
10.1 先定形态
| AGS(托管 Cube) | 自建 Cube | |
|---|---|---|
| 机器 | 不用管 | x86 云主机,OpenCloudOS 9 + PVM 内核,或者物理机。建议至少 16 核 / 32 GB,数据盘 XFS |
| 暂停、恢复 | 云 API:PauseSandboxInstance(Memory=true) / ResumeSandboxInstance | Cube API:/sandboxes/{id}/pause、/connect |
| 已知限制 | 暂停实例每账号默认约 10 个(需要提额);暂停按声明内存规格收存储费 | 运维 16 个服务;要跨机恢复需开 S3 后端 |
已定:AGS 配置全部经云 API 完成,资源清单见 poc/ags-lab/README.md。Broker 的接口两种都能接,选哪个你定。我倾向 AGS:免运维,能更快拿到 x86 上的真实数据。
10.2 需要配置的项
| 项 | 建议值 | 说明 |
|---|---|---|
| 地域 / 可用区 / VPC | 与 Pool 机器同一可用区 | 决定 §7.1 的时延。Pool 要能访问沙箱的 envd 入口(CubeProxy 或 AGS 数据面域名) |
| 沙箱模板(镜像) | 我来写 Dockerfile:Ubuntu + git、node、python、ripgrep、build-essential + envd | 不装 dsh、不放 key。AGS 要求镜像用 @sha256 固定、系统盘 20Gi(见 docs/research/ags-custom-image.md) |
| 规格 | 2C4G 起 | 和现在一致 |
| 存储后端 | 自建:先用 xfs,要跨机再开 s3 + COS 桶 | s3 后端必须在建模板时指定,之后不能改 |
| 生命周期 | 空闲超时由我们控制:创建时显式传 timeout;on_timeout=pause;auto_resume=false | 恢复由 Broker 显式调用,这样可以做重试和 nonce 校验。自建时把 paused_resource_release_ratio 调高 |
| 入口鉴权 | 开启每沙箱访问令牌 | 见 §6 |
| 网络模式 | PUBLIC | AGS 没有按域名的白名单;要收紧只能用 VPC + 安全组 + 出站代理,第二期再做 |
| 配额 | 并发运行 ≥ 20、暂停 ≥ 100(测试期) | AGS 暂停配额要提前申请 |
| 归档桶 | 一个 COS 桶,用于 workspace 归档和 dsh 会话目录 | Pool 和网关可写;沙箱不可访问 |
10.3 配好后请给我
- 控制面 API 地址,以及凭据的获取方式(SecretId/Key 放在哪、什么权限)。不要发在聊天里,像现在一样写进本地的 gitignored 文件。
- 数据面(envd)的访问方式:域名格式,是否需要私有 CA。
- 模板 ID,或者告诉我镜像仓库地址,我来推镜像、建模板。
- 配额数字,以及 Pool 机器(是否新申请;能否复用现在的
9.135.181.73,它和 Cube 不在同一可用区的话时延会变差)。
11. 分期 估算
| 阶段 | 内容 | 验收 |
|---|---|---|
| P0 spike(不依赖腾讯云) 完成 | §9 的 S1、S5–S7;dsh 插件最小实现跑通 read / write / bash / grep | 测试台上一轮编码任务的工具都打到 Cube 沙箱里执行 |
| P1 接入云上 Cube 完成 11-02 | S2、S3 实测;Broker(懒创建、暂停、恢复、重试、nonce);沙箱模板 | 时延和恢复成功率达到验收线;make smoke 全过。实际:internal/hands + image/dsh-plugins,make smoke-hands PAUSE_CHECK=1 全过,只聊天的会话不建沙箱 |
| P2 Pool 完成 11-02 | 预热 worker、分配与回收、会话目录放到共享存储、轮末归档、冷重建 | 杀掉任意 worker 或沙箱,对话可以继续;只聊天不建沙箱。实际:internal/gateway/pool.go、snapshot.go;smoke 里杀掉全部 worker 后模型仍记得暗号,云 API 停掉沙箱后文件从快照回来 |
| P3 收尾 完成 11-02 | 出网策略、App 文件 API 切到 envd / COS、持久 bash(S4)、文档与 Android 端字段适配 | handoff §10 的 #2、#3 关闭。实际:持久 bash(S4,PTY 断流后按 lease epoch 重连);出网实测并定为 PUBLIC(元数据服务、我们的内网都不可达);已归档会话的文件 API 直接读快照;测试台与 Android 展示 sandbox.hands 与 hands 事件;构建期锁死“Pool 里没有本地执行的 provider” |
P1 没有改动 :8080:工具沙箱模式是第二个网关实例 :8081(同一个二进制和镜像,GATEWAY_HANDS_TOOL 打开)。