Agent 与沙箱:架构与关系解析
采集环境:主机
2663239c2aaa· Ubuntu 24.04.3 LTS · 内核 6.6.117-45.11.6.tl4.x86_64采集时间:2026-10-15 10:18 CST
证据来源:
/proc/<pid>/environ、/proc/<pid>/ns、supervisord 配置、workspace-init启动脚本、ss -tnpsocket 连接、cgroup 文件、mount 表
0. 一句话结论
Agent 不是沙箱之外的东西,而是沙箱内部一个由
sandbox-proxy派生出来的普通进程。控制面不直接连接 Agent:它先把配置注入
envd,再由沙箱内的sandbox-proxy拉起 Agent 进程;Agent 通过沙箱共享的文件系统、网络和命令执行能力完成工作,出网统一经由宿主网关172.24.0.1:80。
换句话说:沙箱是"机器",Agent 是跑在这台机器里的"操作员"------两者不是并列关系,而是宿主与租户、容器与进程的关系。
1. 环境身份:这是一个什么沙箱
从 Agent 进程(PID 1416)和 sandbox-proxy(PID 125)的环境变量中可以看到完整身份:
| 类别 | 关键变量 | 值 |
|---|---|---|
| 平台 | X_IDE_IS_CLOUDSTUDIO / X_IDE_SPACE_HOST |
TRUE / sandbox.cloudstudio.club |
| 编排 | WORKSPACE_NAMESPACE |
cs-spacelet-v2-headless.coding-workspace.svc.cluster.local. |
| 区域 | X_IDE_SPACE_REGION |
sh4(上海) |
| 环境 | IDE_ENV |
prod |
| 运行时镜像 | IDE_USER_RUNTIME_IMAGE |
cloudstudio-sh.tencentcloudcr.com/saas.container/full-1.0.0:202505140048 |
| 宿主名 | HOSTNAME |
VM-32-6-tencentos |
| IDE | IDE_EDITOR_TYPE |
VSCODE |
| 工作区 | IDE_WORK_DIR |
/workspace |
| 基础目录 | IDE_EDITOR_SERVER_DIR |
/.PlnPyKFp4CRfFtgC1 |
结论:这是腾讯云 CloudStudio 的 spacelet 沙箱 ,运行在一个名为 cs-spacelet-v2-headless 的 K8s 工作负载中(命名空间 coding-workspace),采用 headless(无界面)形态,区域 sh4。
资源配额(envd 下发与 cgroup 实测一致)
| 项目 | envd 声明 | cgroup 实测 | 一致性 |
|---|---|---|---|
| 内存 | X_IDE_MEMORY_LIMIT=8G |
memory.max = 8589934592(8 GiB) |
✅ 一致 |
| CPU | X_IDE_CPU_LIMIT=4 |
cpu.max = 400000/100000(4 核) |
✅ 一致 |
| 磁盘 | X_IDE_DISK_QUOTA=250G |
根 overlay 256 G | ✅ 基本一致 |
| PID 上限 | --- | pids.max = 151533 |
--- |
注意:在容器内直接执行
free会看到宿主的 123 GiB 内存(未挂载 lxcfs),判断配额必须读 cgroup。这一点在排障时极易误判。
2. 三层结构总览
┌─ 外部(控制面与网关)────────────────────────────────────┐
│ CloudStudio 控制面(SaaS) 宿主网关 172.24.0.1 │
│ 沙箱调度 · 配置下发 · 会话路由 出网 :80 · 日志 :18100 │
└───────────────────────┬──────────────────────────────────┘
│ 配置下发(HTTP)
┌─ 沙箱边界(K8s Pod / 容器)───────────────────────────────┐
│ │
│ 【沙箱基础服务层】由 supervisord (PID 61) 守护 │
│ envd:49983 sync_server editor_sdk node api │
│ terminal:65310 log-collector dockerd/containerd │
│ │
│ 【Agent 子系统(cbc.d)】 │
│ sandbox-proxy:9090/65225 ← Agent 启动器 + 出网代理 │
│ └─ codebuddy ×3 (prewarm 池) │
│ └─ mcp-stdio / MCP 服务 / zsh 命令 shell │
│ │
│ 【共享资源】/workspace · cgroup 8GiB/4vCPU · cwd 共享 │
└──────────────────────────────────────────────────────────┘
关键点:服务层与 Agent 层位于同一个容器内,共享 PID / 网络 / 挂载命名空间,之间没有硬隔离。
3. 沙箱侧:基础服务与守护体系
3.1 守护骨架
PID 1 docker-init(容器入口,执行 exec supervisord)
└ PID 61 supervisord -c supervisord-conf/supervisord.conf
├ [program:proxy] → sandbox-proxy ← 经由 start-cbc.d 拉起
├ [program:e2b-envd] → envd
├ [program:api] → node api
├ [program:terminal] → terminal/app, local-proxy
├ [program:docker] → dockerd → containerd
└ 其他一次性/常驻:init-workspace、sandbox-log-collector、editor_sdk、sync_server
supervisord 配置中 autostart=true / autorestart=true,意味着这些服务被杀死后会自动重启------这是沙箱自愈能力的来源。
3.2 服务清单与端口
| 服务 | PID | 监听 | 职责 |
|---|---|---|---|
envd |
1284 | *:49983 |
配置权威源 + 控制面入口 (/envs 接口) |
sync_server |
42496 | 127.0.0.1:8089、*:18601 |
工作区文件双向同步(本地 ↔ 沙箱) |
editor_sdk |
42465 | 127.0.0.1:39099 |
编辑器 SDK(仅回环,--cors_origin=*) |
node api |
107 | *:65213 |
沙箱 API 服务 |
terminal / app |
95 | *:65310 |
终端服务 |
local-proxy |
93 | *:65216 |
本地代理 |
sandbox-proxy |
125 | *:9090、*:65225 |
出网代理 + Agent 启动器 |
sandbox-log-collector |
118 | 出向 :18100 |
日志汇聚转发 |
dockerd / containerd |
29424 / 29488 | unix socket | 容器引擎(供沙箱内嵌套构建) |
3.3 envd ------ 最容易被忽略但最关键的一环
envd 不只是"环境变量服务",它是沙箱与控制面之间的唯一配置通道:
- 对外做配置分发 :
GET http://localhost:49983/envs返回所有环境变量,启动脚本靠它决定功能开关。 - 对内接收控制面连接 :
ss显示envd的 49983 端口上有来自172.24.0.1(宿主侧) 的已建立连接(fd 6、fd 9),说明控制面是主动连进沙箱的。
bash
# start-cbc.d 和 agent-init 的标准模式
ENVD_URL="http://localhost:49983/envs"
ENVD_RESPONSE=$(curl -s "${ENVD_URL}")
ENABLE_CBC_D_VALUE=$(echo "$ENVD_RESPONSE" | grep -o '"ENABLE_CBC_D":"[^"]*"' | cut -d'"' -f4)
if [ "$ENABLE_CBC_D_VALUE" != "true" ]; then exit 0; fi
设计含义 :功能开关(
ENABLE_CBC_D、ENABLE_AGENT_INIT)不在镜像里写死,而是由控制面通过 envd 动态下发。这使得平台可以在不改镜像的前提下灰度开关 Agent 能力。
4. Agent 侧:形态、位置与派生关系
4.1 Agent 是谁
node /root/.nvm/versions/node/v22.13.1/lib/node_modules/@tencent-ai/codebuddy-code/bin/codebuddy --prewarm --prewarm-id sbx-125-N
- 安装包:
@tencent-ai/codebuddy-code - 运行时:Node.js v22.13.1(nvm 管理)
- 当前沙箱内共 3 个实例,构成预热池:
| PID | 启动时间 | RSS | 参数 | 角色 |
|---|---|---|---|---|
| 1416 | 10:04:43 | ~335 MB | --prewarm-id sbx-125-1 |
当前活跃会话 |
| 1417 | 10:04:43 | 162 MB | --prewarm-id sbx-125-2 |
预热待命 |
| 42241 | 10:18:03 | 162 MB | --prewarm-id sbx-125-3 |
预热待命 |
三者的父进程都是 sandbox-proxy(PID 125)------这是判定"Agent 属于沙箱"最直接的证据。
4.2 Agent 的两种形态
| 形态 | 启动方式 | 监听 | 用途 |
|---|---|---|---|
| prewarm 模式 | sandbox-proxy 派生 |
无对外端口 | 预热待命,收到会话请求即接管(当前形态) |
| serve 模式 | start-codebuddy-serve 脚本 |
0.0.0.0:65222 |
作为常驻 HTTP 服务运行 |
start-codebuddy-serve 脚本内容:
bash
cd "/workspace"
# 使用 setsid 完全脱离控制终端,避免 codebuddy 因 tty 问题被挂起
exec setsid codebuddy --serve --host 0.0.0.0 --port 65222 </dev/null
4.3 Agent 调用外部能力的方式
在 /proc/1416/fd 与进程树中可以看到 Agent 的两类子进程:
| 子进程 | PID | 作用 |
|---|---|---|
sandbox-proxy mcp-stdio |
42268 | MCP 协议的 stdio 传输通道 |
node .../workbuddy-sheetagent/mcp/start.mjs |
42512 | 表格处理 MCP 服务 |
zsh |
53271+ | 由 Agent 派发的命令执行 shell(临时生命周期) |
MCP 插件清单(/root/.codebuddy/.mcp.json)包含:agent-browser、docx、pdf、pptx、xlsx、playwright-cli、workbuddy-docx、workbuddy-pptx、workbuddy-sheetagent、hot-skills。
重要机制 :Agent 的"工具执行"在本机表现为 fork 子 shell / 起 MCP 子进程。每一次工具调用都是沙箱内的一次真实进程创建,而不是远程 RPC。
5. 两者的接口契约:四条通道
Agent 与沙箱/控制面之间只有四条交互通道,边界非常清晰:
| # | 通道 | 方向 | 载体 | 证据 |
|---|---|---|---|---|
| ① | 配置下发 | 控制面 → 沙箱 | envd:49983 的 /envs 接口 |
启动脚本 curl localhost:49983/envs;envd 上有来自 172.24.0.1 的入站连接 |
| ② | 进程派生 | 沙箱 → Agent | supervisord → start-cbc.d → sandbox-proxy → codebuddy |
ps --ppid 125 显示 3 个 codebuddy |
| ③ | 出网访问 | Agent/沙箱 → 外网 | 宿主网关 172.24.0.1:80 |
1416 的 fd 20/22、125 的多个 fd 均指向 172.24.0.1:80 |
| ④ | 日志上报 | 沙箱 → 宿主 | log-forward-tee → nc → 172.24.0.1:18100 |
SANDBOX_LOG_FORWARD=1;多个 nc 172.24.0.1 18100 进程 |
注意 ③ 的细节 :Agent 进程 自身也直连 172.24.0.1:80,并非所有流量都绕经 sandbox-proxy。说明 sandbox-proxy 的"代理"职责与"启动器"职责是两件独立的事。
6. 完整启动链路(从容器到 Agent)
1. 容器启动
PID 1 docker-init -- bash -c "...exec supervisord"
2. 进程管理
PID 61 supervisord(按 supervisord.conf 拉起各 program)
3. 读取平台配置
start-cbc.d → curl http://localhost:49983/envs(envd)
→ 解析 ENABLE_CBC_D,为 true 才继续
4. 获取 Agent 运行时(box)
优先:/root/proxy/box-current(sandbox-componentd 版本管理软链)
兜底:curl https://cs-res-1324618448.cos.ap-shanghai.myqcloud.com/sandbox/tools/box.tar.gz
→ 解压到 /root/proxy/box
5. 识别产物形态并运行
Go 形态: /root/proxy/box/sandbox-proxy → exec 前台运行
Node 形态: ecosystem.config.js → pm2 start → pm2 logs 保持前台
6. Agent 派生
sandbox-proxy(125) → codebuddy --prewarm --prewarm-id sbx-125-{1,2,3}
7. 会话接入
当前会话落在 codebuddy 1416;工具调用 → fork zsh / 启 MCP 子进程
sandbox-proxy二进制实测为 46 MB 的 Go 静态链接 ELF (statically linked、带 debug_info),说明它是外部分发、独立升级的组件------这正好解释了start-cbc.d中"从 COS 下载 + 支持box-current软链切换版本"的设计。
启动脚本的一个工程细节
agent-init 脚本中有一段明确注释,记录了一个已修复的线上问题,很能反映这套架构的可靠性设计:
历史上这里有 download-if-absent 兜底(文件不存在就从固定 COS URL 拉取)。该兜底有两个问题已移除:
- COS 那份长期未更新(停在 2026-02 旧版),一旦触发就把系统盘自带的新版覆盖成旧版(生产实测 sh3:base 盘 1.0.22 被覆盖成 1.0.10);
- 触发条件是竞态------warm 空壳的系统盘是 base 盘的 COW fork,脚本在 NBD 盘就绪前的窗口内
[ -f ]判为不存在 → 误触下载。
含义 :沙箱使用 warm 池 + NBD 远程克隆盘做秒级启动,因此启动早期存在"文件尚未可见"的时间窗,任何"文件缺失即下载"的逻辑都会误判。
7. 边界与共享:Agent 到底能看到什么
7.1 命名空间:无隔离
/proc/1416/ns 显示 Agent 与沙箱服务处于同一组命名空间:
net:[4026534002] pid:[4026534001] mnt:[4026533985] ipc:[4026534000]
因此:
- Agent 能直接访问
127.0.0.1:49983(envd)、127.0.0.1:8089(sync_server)、127.0.0.1:39099(editor_sdk) - Agent 与沙箱服务共享同一张网络栈、同一个 PID 空间、同一套挂载
7.2 文件系统:完全共享
| 观察项 | 值 |
|---|---|
/proc/1416/cwd |
/workspace |
/proc/1416/root |
/ |
| 根挂载 | overlay,upperdir=/mnt/user-data/46e3d0d99e364ee9b2f628933b04a755/diff |
| warm 池来源 | /var/run/cloudstudio/warm-b492449d-8153-4eb5-8096-dcd22815b96d... |
| 嵌套容器盘 | /dev/nbd503(btrfs)→ /var/lib/docker、/var/lib/buildkit |
Agent 对沙箱文件系统的读写就是直接的磁盘读写 ,/workspace 同时被 sync_server 同步到用户本地。
7.3 存储分层
NBD 远程盘(/dev/vdb、/dev/nbd503,btrfs)
└─ overlay upperdir(/mnt/user-data/<uuid>/diff)
└─ 容器根 /(rw) → /workspace(用户工作区)
启动方式为 NBD 克隆盘 + overlay,配合 warm 池实现"空壳预热、按需接管"。
8. 通信关系总表
| 源 | 目标 | 协议/端口 | 用途 |
|---|---|---|---|
| 控制面 | envd |
172.24.0.1 → 172.24.0.11:49983 |
配置/指令下发 |
codebuddy 1416 |
宿主网关 | 172.24.0.11 → 172.24.0.1:80 |
Agent 出网(云端模型调用等) |
sandbox-proxy 125 |
宿主网关 | → 172.24.0.1:80(多条) |
代理出网 |
sandbox-proxy 125 |
外网 | → 43.141.132.124:443 |
HTTPS 出网 |
sandbox-proxy 125 |
envd |
127.0.0.1:58324 → :49983 |
本地配置读取 |
sandbox-proxy 125 |
sync_server |
127.0.0.1:54252 → :8089 |
同步服务调用 |
codebuddy 1416 |
自身 | 127.0.0.1:35729 |
内部 RPC / 扩展通道 |
nc / log-collector |
宿主 | → 172.24.0.1:18100 |
日志转发 |
sync_server |
宿主 | → 172.24.0.1:80 |
同步数据回传 |
9. 关键洞察与排障提示
洞察 1:职责分离做得比想象中干净
envd管配置 ,sandbox-proxy管进程与出网 ,sync_server管文件 ,log-collector管日志- 控制面从不直连 Agent 进程,只通过 envd 注入配置,Agent 由沙箱内部自行派生
洞察 2:Agent 是"可替换的租户",沙箱是"稳定的宿主"
- 三个
codebuddy实例由sandbox-proxy统一派生,会话切换只是换一个 prewarm 实例 - Agent 崩溃不影响沙箱存活:
sandbox-proxy在,随时能再拉一个
洞察 3:prewarm 池 = 首字延迟优化
CODEBUDDY_STREAM_TIMEOUT_MS=300000、CODEBUDDY_FIRST_TOKEN_TIMEOUT_MS=300000(均 5 分钟)- 两份待命实例让新会话无需冷启动
洞察 4:热升级路径清晰
sandbox-proxy从 COS 分发,支持box-current软链切版本- Agent 包在
codebuddy-codenpm 目录(独立 overlay 挂载),可单独更新
排障提示
| 现象 | 正确排查方式 |
|---|---|
| 内存/CPU 显示与预期不符 | 读 cat /sys/fs/cgroup/memory.max、cpu.max,不要用 free |
| 判断某能力是否开启 | `curl -s localhost:49983/envs |
| 确认 Agent 归属 | ps --ppid <sandbox-proxy-pid> 看是否为其子进程 |
| 出网异常 | 检查到 172.24.0.1:80 的连接,而非公网直连 |
| 日志丢失 | 检查 nc 172.24.0.1 18100 链路与 SANDBOX_LOG_FORWARD=1 |
附录:一句话对照表
| 概念 | 具体实体 | 定位 |
|---|---|---|
| 沙箱 | CloudStudio spacelet(K8s Pod / 容器) | 宿主,提供机器、文件、网络、命令 |
| 控制面 | CloudStudio SaaS(codingcorp.cloudstudio.net/api) |
调度与配置来源 |
| 配置通道 | envd:49983 |
控制面进入沙箱的唯一入口 |
| Agent | codebuddy(@tencent-ai/codebuddy-code) |
沙箱内的租户进程,执行任务 |
| Agent 启动器 | sandbox-proxy:9090/65225 |
派生 Agent + 出网代理 |
| 边界出口 | 宿主网关 172.24.0.1(:80 出网 / :18100 日志) |
沙箱与外界的唯一出入口 |