跳到主要内容

数据截至 (上游 commit 1916c9046c4e)

后端抽象与运行时(原「沙箱」章)

本章经过 pivot 重写。 旧版这一章讲「沙箱服务」:控制中心给每个会话开一个 Docker 容器、暴露端口、用 session key 当门票。那套代码已不在本仓库。 新架构里,Agent Canvas 只跟「一个已经在跑的 Agent Server」对话,容器/隔离由部署形态承担(第 01 章 §5)。与之对应的核心机制是:怎么抽象「一个 agent 后端」、怎么在多个后端间切换、怎么安全地够到不同形态的运行时。这就是本章内容。

1. 「一个后端」的最小模型

后端注册表(src/api/backend-registry/)把「一台能跑 agent 的 Agent Server」压缩成七字段的 Backend(src/api/backend-registry/types.ts:4-13):

字段含义
id / name稳定标识与显示名
hostAgent Server 的 base URL
apiKey会话密钥(local 是 session key,cloud 是 bearer token)
kind"local""cloud"(src/api/backend-registry/types.ts:1)——双路由的总开关
authMode"api-key""cookie"(cloud 同站部署可用 cookie)
connectionRevision凭据变更计数,用于让缓存数据失效

持久化就在浏览器 localStorage 里,两个 key:openhands-backends(注册表)与 openhands-active-backend(当前选择),常量在 src/api/backend-registry/storage.ts:13-14

默认后端是「种」出来的。 首次启动时,若启动器注入了 host + session key,makeDefaultLocalBackend 会生成一条 id 为 default-local 的本地后端写入注册表(src/api/backend-registry/default-backend.ts:53-70);若是「锁定云」部署(VITE_LOCK_TO_CLOUD),则只种一条锁定的 cloud 后端、禁止 local 种子(src/api/backend-registry/default-backend.ts:22-36makeLockedCloudBackend)。种完之后它就是一条普通记录,可改名、可删。

2. 活动后端:tab 级隔离 + URL 自描述

「当前用哪个后端」不是全局唯一值,而是按浏览器 tab 隔离的:读取时先查 sessionStorage(tab 级),没有才回落 localStorage(跨 tab 的「上次使用」),见 readStoredActiveBackend(src/api/backend-registry/storage.ts:205-219)。这样你在 tab A 看云端会话、tab B 跑本地 agent,互不抢。

但 tab 隔离有个洞:cmd/ctrl-点击开新 tab 时新 tab 拿不到旧 tab 的 sessionStorage,会错误地落到「任意 tab 最后用的后端」。解法是把后端身份写进 URL:?backend=<id>&org=<orgId> 两个查询参数(src/api/backend-registry/url-selection.ts:18-19),链接自带归属,新 tab 打开时以 URL 为准并回写存储(src/api/backend-registry/active-store.ts:104-114readInitialSelection)。

没有显式选择时选谁? pickFallbackBackend 的顺序是:健康的 local 后端 → 任意 local 后端 → 列表第一项 → NO_BACKEND 哨兵(src/api/backend-registry/active-store.ts:52-62)。为什么偏好 local?因为大部分 GUI 服务说的是 local agent-server 协议,拿一个 cloud 后端去调会全灭。

这条偏好还有第二道闸:getEffectiveLocalBackend 只在活动后端确实是 local 时才返回它,否则返回 null(src/api/backend-registry/active-store.ts:140-144)。服务层凡走 local 协议的都经它取连接参数,取不到就抛 NoBackendAvailableError(src/api/agent-server-client-options.ts:52-69getAgentServerClientOptions)——绝不偷偷借用别的后端

3. 健康追踪:失败计数,超阈值熔断

每个后端有一条健康记录,存在 localStorage:连续失败次数、最近错误、是否 disabledrecordBackendFailure 每次失败 +1,达到上限(MAX_CONSECUTIVE_FAILURES)就置 disabled: true,探活 hook 停止打它(src/api/backend-registry/health-store.ts:44-58);成功一次就清零(recordBackendSuccess)。上一节的 fallback 选择会读这份记录,跳过被熔断的后端。

4. 双路由:local 直连,cloud 走代理

kind 字段驱动的「双路由」是服务层最普遍的模式。以运行时命令执行为例(AgentServerRuntimeService.executeCommand,src/api/runtime-service/agent-server-runtime-service.ts:25-68):

service 方法被调
│ getActiveBackend().backend.kind
├─ "local" ──► 直连:typescript-client 的 RemoteWorkspace
│ 浏览器 → agent-server(CORS 同栈无碍)
└─ "cloud" ──► 代理:callCloudProxy(hostOverride=会话运行时 URL)
浏览器 → 云后端 /api/cloud-proxy → 运行时沙箱

为什么 cloud 不能直连?会话的运行时沙箱在 *.prod-runtime.all-hands.dev 这类域上,不允许来自 localhost 的 CORS,所以请求必须经云后端做一次服务端跳转(src/api/runtime-service/agent-server-runtime-service.ts:14-23 的类注释)。callCloudProxy 就是这条通道(src/api/cloud/proxy.ts:17-36),hostOverride 指定运行时地址,鉴权用会话自己的 session_api_key

鉴权头也因 kind 而异:buildAuthHeaders 对 local 出 X-Session-API-Key,对 cloud 出 Authorization: Bearer,cookie 模式则不出头(src/api/backend-registry/auth.ts:9-19)。

5. 版本门禁:不兼容就明确报错

后端是可以独立升级的外部服务,前端必须容忍「对面太旧」。机制是版本门禁:启动时拉 /server_info,把版本与最低要求比对——最低兼容版本锁在 config/defaults.json 里,经 MINIMUM_COMPATIBLE_AGENT_SERVER_VERSION 暴露(src/api/agent-server-compatibility.ts:16-17,当前为 1.28.0)。低于它就抛 AgentServerUnsupportedVersionError,错误信息直接告诉你「需要 ≥ X,对面是 Y」(src/api/agent-server-compatibility.ts:71-79)。这比「调用了不存在的端点然后 404」友好得多。

6. 那「沙箱」去哪儿了?——隔离变成部署决策

新架构里,隔离的强度完全由你把 Agent Server 部署在哪决定:

部署形态隔离强度说明
npm / 源码直跑agent 拥有你整个文件系统;README 对此挂了 WARNING(README.md:63-66)
Docker 全一体镜像容器级三服务一容器,只挂载你显式给的 PROJECTS_PATH(README.md:82-99;镜像组装见 docker/Dockerfile:5-16)
远程主机 / VM机器级后端跑在别的机器上,前端只发 API 调用
OpenHands Cloud沙箱级每个会话一个云端沙箱,经云代理访问(§4)

注意一个呼应:cloud 路径的会话模型里仍保留着 sandbox_status 字段(src/api/agent-server-adapter.ts:54-55 注释)——那是云后端的概念,由云服务返回;本仓库只是把它透传给 UI 显示。「沙箱」作为被管理对象,已经完全外移到云后端与 SDK。

7. 本章小结

  • Backend 七字段模型 + localStorage 持久化,默认后端由启动器注入「种」出来。
  • 活动后端 tab 级隔离,URL 参数让链接自描述;fallback 偏好健康 local。
  • 双路由:local 直连 typed client,cloud 经 callCloudProxy 跳转运行时。
  • 版本门禁把「对面太旧」变成一条可读错误。
  • 沙箱没了,隔离是部署决策。
  • 下一章讲这套控制台向外伸出的所有「插头」:ACP agent、Agent Profile、客户端工具、技能/插件/MCP、自动化。