数据截至 (上游 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 | 稳定标识与显示名 |
host | Agent 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-36 的 makeLockedCloudBackend)。种完之后它就是一条普通记录,可改名、可删。
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-114 的 readInitialSelection)。
没有显式选择时选谁? 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-69 的 getAgentServerClientOptions)——绝不偷偷借用别的后端。
3. 健康追踪:失败计数,超阈值熔断
每个后端有一条健康记录,存在 localStorage:连续失败次数、最近错误、是否 disabled。recordBackendFailure 每次失败 +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、自动化。