Skip to content

Agent 的 Tool Call 如何在沙箱里执行

当你在 agent 框架里看到一次 tool call,表面上像是一句:

  • run_code()
  • commands.run()
  • files.read()
  • 浏览器 / HTTP / CLI 工具调用

但在 CubeSandbox 里,这些调用并不是“直接跑在控制面上”,而是被拆成了控制面创建 + 数据面路由 + 沙箱内执行器处理三段。

这篇文章专门回答两个问题:

  1. 一个 agent 的 tool call,为什么最后能进入某个具体沙箱?
  2. 请求到了沙箱后,又是谁真正执行它?

1. 先看总图:一次 tool call 穿过了哪些层

这张图里最容易被忽略的一点是:

  • 创建沙箱 走的是控制面链路:CubeAPI -> CubeMaster -> Cubelet
  • 执行 tool call 走的是数据面链路:SDK -> CubeProxy -> sandbox port

换句话说:

控制面负责“把沙箱准备出来”,数据面负责“把请求送进已经存在的沙箱”。


2. 第一步:agent 并不是直接找 Cubelet,而是先拿一个可访问的 Sandbox 对象

从 SDK 视角,一个 tool call 的前置条件不是“知道某台节点 IP”,而是先拿到 Sandbox 对象。

在 Go SDK 里,这个对象里最关键的字段包括:

  • SandboxID
  • Domain
  • EnvdAccessToken
  • TrafficAccessToken

而这些字段的来源,根本上仍然来自控制面创建结果。

控制面创建后,SDK/客户端会拿到足够的信息,能够构造出类似下面这样的数据面地址:

  • 49999-<sandboxID>.<domain>
  • 49983-<sandboxID>.<domain>

这类地址本身并不是“真实服务发现 DNS”,而是让 CubeProxy 识别该请求应该落到哪个沙箱。


3. 第二步:为什么 host 看起来像 49999-sandboxid.domain

在 Go SDK 里,sdk/go/sandbox.goGetHost(port) 会直接生成:

text
<port>-<sandboxID>.<domain>

例如:

text
49999-sb-123.cube.app

这不是美观问题,而是路由协议本身

CubeProxy 会根据 Host 里的:

  • 端口号
  • sandboxID
  • domain

去找到目标沙箱及其宿主端口映射,再把请求转发进去。

因此,从数据面视角,sandboxID 并不只是对象标识,它还是路由键的一部分。


4. 第三步:tool call 真正打到哪个端口,取决于工具类型

在 CubeSandbox 里,不同工具并不一定走同一个端口。

4.1 RunCode:通常走 49999

Go SDK 的 Sandbox.RunCode() 会向:

  • POST https://49999-<sandboxID>.<domain>/execute

发送请求。

这意味着 run_code 依赖的是代码执行网关 / Jupyter 风格执行器,而不是普通 shell。

4.2 Commands.Run:也是到数据面,但协议不同

sdk/go/envd.go 里,Commands.Run() 最终会调:

  • POST /process.Process/Start

它用的是 connect 风格流式协议,底层由 envd 的 process API 拉起进程,并返回:

  • stdout
  • stderr
  • exit code

所以从 agent 视角,“执行一个命令”和“执行一段代码”都叫 tool call;但从沙箱内部视角,它们是两个不同的执行通道

4.3 Files.Read:走 envd 的文件 API

sdk/go/envd.go 里,Files.Read() 调的是:

  • GET /files?path=...

这说明文件读取也不是通过 SSH 或 shell,而是走沙箱内置的文件服务。


5. 第四步:CubeProxy 负责把请求送到正确节点,但不负责执行代码

这一点非常重要。

CubeProxy 的角色是:

  • 解析 Host 或路径中的 sandboxID
  • 找到对应节点和宿主机端口
  • 把 HTTP / WebSocket / connect 流量转发过去

不负责

  • 解释 Python 代码
  • 创建 shell 进程
  • 读取文件
  • 管理 Jupyter kernel

这些都是沙箱内部服务在做。

所以可以把它理解成:

  • CubeProxy:入站路由器
  • envd / Jupyter / code-interpreter gateway:沙箱内执行器

6. 第五步:为什么创建时要把端口映射写进控制面元数据

CubeMaster/pkg/service/sandbox/sandbox_run.go 里,创建成功后会调用 setProxyToRedis()

这里会把 ContainerToHostPorts 等信息写入 SandboxProxyMap

这对 tool call 至关重要,因为 CubeProxy 需要知道:

  • 某个 sandboxID 现在在哪台节点上
  • 它的容器端口 49999 映射到了宿主哪个端口
  • 它的 49983 是否也可访问

如果这份映射没写进去,结果通常就是:

  • 控制面“已经创建成功”
  • 但数据面仍然无法访问
  • SDK 上表现为连接失败、超时、404/502 等问题

所以对 agent 场景来说,创建成功只是前半场,端口路由登记才决定 tool call 能否真正打进去。


7. 第六步:到了沙箱内部,是谁执行了 tool call

从当前开源代码和 SDK 约定看,至少有两类内部执行器:

7.1 Jupyter / code-interpreter 风格执行器

RunCode() 调用的是 /execute,并以流式方式返回:

  • stdout
  • stderr
  • result
  • error
  • execution_count 等事件

这说明端口 49999 上跑的不是通用反向代理,而是一个能理解代码执行语义的服务。

它更像:

  • 接收一段代码
  • 在对应 kernel / execution context 中运行
  • 以结构化流的方式把结果吐回 SDK

这也是为什么模板里如果只暴露了 49983、没暴露 49999run_code 会失败。

7.2 envd 进程服务

Commands.Run()process.Process/Start,本质是让沙箱内的 envd process API 启动一个进程,例如:

  • /bin/bash -l -c <command>

它返回的是进程事件流,而不是 notebook 风格结果流。

这条链路更适合:

  • shell 命令
  • 文件生成
  • 工具安装
  • 调试命令
  • agent 的外壳式操作

因此,同样是 tool call:

  • run_code 更偏“代码解释器”
  • commands.run 更偏“系统命令执行器”

8. 第七步:为什么要有 EnvdAccessToken

sdk/go/envd.go 里,newEnvdRequest() 会在有 token 时设置:

  • X-Access-Token: <EnvdAccessToken>

这说明数据面并不是“只要知道 sandbox 域名就能打进去”,而是可以在沙箱内执行器这一层再加一层访问控制。

从架构上看,这意味着数据面至少分成两层权限:

  1. 你能不能被路由到这个沙箱
  2. 即使路由到了,沙箱内服务是否接受你的调用

这对 agent 场景很重要,因为一个 tool call 通常不只是“能连上”,还要确保执行器不会被任意未授权客户端直接调用。


9. 第八步:为什么 connect 和 resume 会影响 tool call

如果一个沙箱已经被 pause,数据面虽然还保留着逻辑身份,但 runtime task 已经不在可执行状态。

这时:

  • connect_sandbox() 会先看状态
  • 如果是 paused,就先调用 resume
  • 再返回新的沙箱详情

所以从 agent 编排角度,正确理解应该是:

  • tool call 之前,不只是“有 sandboxID”
  • 还要“这个 sandbox 当前处于可接收执行请求的运行态”

否则你可能拿到的是一个存在于控制面的对象,但数据面执行器尚未恢复就绪。


10. 一次典型的 agent tool call,可以拆成 8 个动作

run_code() 为例,可以把一次完整调用拆成下面 8 步:

  1. agent 决定需要一个代码执行工具。
  2. SDK 若无现成沙箱,则先走控制面创建沙箱。
  3. CubeMaster 创建成功后把 sandboxID -> hostIP / port mapping 写入元数据。
  4. SDK 拿到 Sandbox 对象,生成 49999-<sandboxID>.<domain>
  5. SDK 向 /execute 发起 HTTP 请求。
  6. CubeProxy 根据 Host 把流量路由到目标节点的对应宿主端口。
  7. 沙箱内 49999 端口上的执行器接收代码并运行。
  8. 执行结果以结构化流返回给 SDK,再由 SDK 还原为 tool call 输出。

如果是 commands.run(),第 7 步会从“代码解释器”变成“envd 进程执行器”。


11. 最容易踩的几个误区

11.1 误区一:以为控制面直接执行了代码

不是。

控制面只负责:

  • 创建
  • 调度
  • 状态管理
  • 销毁

代码和命令的真正执行发生在数据面和沙箱内部。

11.2 误区二:以为只要有 sandboxID 就能访问

也不是。

还需要:

  • 正确的 domain / Host 规则
  • CubeProxy 路由元数据
  • 对应端口已暴露
  • 对应执行器已启动
  • 必要时带上 EnvdAccessToken

11.3 误区三:把 4998349999 当成一个东西

不是。

它们通常代表不同的数据面服务:

  • 一个偏 envd / 通用操作
  • 一个偏 code interpreter / Jupyter 执行

agent 用什么 tool,取决于它要的是哪类能力。


12. 用一句话概括这条链路

如果只记一句话,可以记这个:

agent 的 tool call 先依赖控制面把沙箱和路由准备好,再由 SDK 按 <port>-<sandboxID>.<domain> 规则经 CubeProxy 把请求送到沙箱内的执行器,最终由 envd 或 code-interpreter 服务真正完成执行。

这也是 CubeSandbox 能同时兼容:

  • serverless 风格创建语义
  • agent 风格工具调用语义

的根本原因。


13. 建议继续阅读