首页
/ OpenHands Agent Canvas:容器化 Agent Server 驱动 ACP 代理(Codex / Claude Code / Gemini CLI)实战指南

OpenHands Agent Canvas:容器化 Agent Server 驱动 ACP 代理(Codex / Claude Code / Gemini CLI)实战指南

2026-09-04 11:39:18作者:邬祺芯Juliet

本文基于仓库中的 examples/acp-docker 快速上手文档 展开,讲解如何在本地用 Docker Compose 拉起一个容器化的 OpenHands Agent Server,让 Agent Canvas 在没有任何宿主机登录态的情况下驱动 Codex、Claude Code、Gemini CLI 这三个 ACP 代理:读完你将掌握镜像版本固定(pin)机制、Canvas 指针对接方式、通过 UI 注入凭据的完整流程,以及各凭据字段背后的 LookupSecret 解析原理与常见陷阱。

1. 背景:为什么 ACP 代理需要“容器化 + UI 凭据”两条腿

Agent Canvas 可以用内置的 OpenHands 代理驱动会话,也可以通过 Agent Client Protocol(ACP) 驱动外部代理:Agent Server 不直接调 LLM,而是把代理自带的 CLI(如 claude-agent-acpcodex-acpgemini-cli --acp)作为子进程 spawn 出来,在 stdio 上以 JSON-RPC 转发每一轮对话。完整原理见 docs/ACP_AGENTS.md

本地后端跑在开发者自己的机器上时,代理 CLI 可以直接复用宿主机上已登录的订阅态(macOS Keychain、~/.codex/auth.json 等)。但 容器是全新环境,没有任何宿主登录态,所以凭据必须从你这里来:要么在 Canvas 的 onboarding “Set up credentials” 步骤里填写(推荐),要么通过 .env 烘焙进容器(适合非交互 / CI 场景)。examples/acp-docker/ 就是这个本地 Docker 路径的现成脚手架,与云部署路径互为对应。

examples/acp-docker/ 目录包含三个文件:

2. 第一步:拉起容器化的 agent-server

2.1 零配置路径

cd examples/acp-docker
docker compose up

这会在 http://localhost:8010 启动 ghcr.io/openhands/agent-server:latest-python,并挂载一个持久化 acp-data 卷。该镜像预装了 ACP CLI wrapper(claude-agent-acp / codex-acp / gemini),SDK 会在容器内把 Canvas 默认下发的 npx -y <pkg> 命令改写到这些预装固定版本二进制上,因此 Canvas 侧无需改动任何启动命令。

2.2 compose 文件的逐项解读

docker-compose.yml 中的关键配置值得逐项理解:

配置 取值 说明
image ${AGENT_SERVER_IMAGE:-ghcr.io/openhands/agent-server:latest-python} 默认回落 latest-python(始终不低于 Canvas 兼容下限);可被 .env 中的 AGENT_SERVER_IMAGE 覆盖为固定版本
container_name oh-acp 固定容器名,便于 docker logs oh-acp 排查
ports 8010:8000 宿主机 8010 映射到容器内 agent-server 的 8000 端口,Canvas 把 VITE_BACKEND_BASE_URL 指向 8010
environment.OH_EXTRA_PYTHON_PATH=/canvas-tools 把仓库 tools/ 目录以只读卷挂到 /canvas-tools 并加入 Python 路径:新会话用 client_tools 定义 canvas_ui_control,但旧版本持久化的会话仍需要导入旧的 Python 模块才能恢复 CanvasUIAction / CanvasUIObservation 事件
environment.OH_SECRET_KEY 注释掉的可选项 不设置时 ACP 会话也能工作(Canvas 把 ACP 凭据作为回环 LookupSecret 从 agent-server 自身的 secret store 解析,且不标记 secrets_encrypted);设置它用于 (a) 跨容器重启持久化已保存的 secrets,(b) 启用非 ACP 的 OpenHands 会话所用的加密设置路径。可用 python -c "import secrets;print(secrets.token_urlsafe(32))" 生成
environment.ANTHROPIC_API_KEY 可选透传 ANTHROPIC_API_KEYCLAUDE_CODE_OAUTH_TOKENOPENAI_API_KEYGEMINI_API_KEYGOOGLE_CLOUD_PROJECTGOOGLE_CLOUD_LOCATIONGOOGLE_GENAI_USE_VERTEXAI 均从你的 shell / .env 透传,未设置的不会导出——仅供“烘焙凭据”场景
volumes.acp-data acp-data:/workspace 持久化两类状态:会话历史,以及 SDK 落盘的凭据文件(Codex 的 auth.json 位于 CODEX_HOME 下、Gemini 的 ADC/SA JSON)
volumes.tools ../../tools:/canvas-tools:ro 只读挂载,相对 compose 文件位置解析
restart unless-stopped 守护式重启策略

镜像的 CORS 已允许 localhost 来源,所以浏览器可以直连容器,不需要任何额外代理配置。

2.3 可复现的固定镜像路径(推荐)

latest-python 适合尝鲜,但团队协作用固定版本更稳妥。仓库把版本固定收敛到单一事实源 config/defaults.json:其中 versions.agentServer 当前为 1.44.0,兼容下限 compatibility.minimumAgentServer1.28.0,镜像仓库地址在 images.agentServer

在仓库根目录执行:

npm run example:acp-docker:env   # 写入 examples/acp-docker/.env
cd examples/acp-docker && docker compose up

该 npm script 对应 scripts/gen-acp-docker-env.mjs,其工作方式是:

  1. 读取 config/defaults.json,由 computeAgentServerImage() 拼出 ghcr.io/openhands/agent-server:<versions.agentServer>-python 这样的完整 tag;
  2. upsertEnvLine()AGENT_SERVER_IMAGE=... 一行幂等地写入/替换进 examples/acp-docker/.env——已有该行就原位替换,没有就追加,其余行(如你手填的凭据)原样保留。

这条链路有专门的漂移检测测试 tests/scripts/acp-docker-env-sync.test.ts,它强制三条不变量:生成的 pin 必须等于 versions.agentServer;无配置 compose 回落必须保持 latest-python;固定 tag 不得低于 compatibility.minimumAgentServer(否则会渲染 “Disconnected”)。

版本兼容性提示:如果你手里有一份早年手写的 .env 且包含旧的 AGENT_SERVER_IMAGE,请重跑 npm run example:acp-docker:env 或删掉该覆盖项,避免示例一直钉在 compatibility.minimumAgentServer 之下。

想手工钉一个更新的发布版或 main 分支构建,也可以直接:

AGENT_SERVER_IMAGE=ghcr.io/openhands/agent-server:$(gh api repos/OpenHands/software-agent-sdk/commits/main --jq '.sha[0:7]')-python docker compose up

2.4 可选:把凭据烘焙进 .env

如果不想在 Canvas 里填凭据(例如非交互 / CI 环境),先复制模板:

cd examples/acp-docker && cp .env.example .env

然后在 .env.example 中填入对应 provider 的环境变量;compose 文件只会透传“有值”的变量。注意:推荐路径仍然是 Canvas 内填写(凭据随 start 请求以 secrets 形式下发),.env 烘焙是替代方案,且可能过不了 onboarding 登录探测——这一点见下文第 5 节的警告。

3. 第二步:让 Canvas 指向容器

回到仓库根目录:

cd ../..                      # repo root
VITE_BACKEND_BASE_URL=http://localhost:8010 npm run dev:frontend

由于镜像 CORS 允许 localhost,浏览器会直接和容器通信。也可以在 Canvas 的后端选择器(backend selector)里把它添加为一个 backend,host 填 http://localhost:8010,效果等价。

4. 第三步:在 UI 中完成凭据注入

在 onboarding 里选择 ACP provider,进入 Set up credentials 步骤。在容器化后端上这一步是必填的(没有宿主登录可回退)。需要粘贴的内容按 provider 如下:

Provider 需要粘贴/填写的内容
Codex(订阅) CODEX_AUTH_JSON —— ~/.codex/auth.json 的完整内容
Claude Code(订阅) CLAUDE_CODE_OAUTH_TOKEN —— 你的 Pro/Max OAuth token
Gemini CLI(Vertex) GOOGLE_APPLICATION_CREDENTIALS_JSON(SA / ADC JSON)+ GOOGLE_CLOUD_PROJECT + GOOGLE_CLOUD_LOCATION + GOOGLE_GENAI_USE_VERTEXAI=true

每个 provider 也接受 API key 路径(OPENAI_API_KEY / ANTHROPIC_API_KEY / GEMINI_API_KEY)。

4.1 凭据字段的来源:getAcpProviderSecrets

上述字段清单不是文档里的口头约定,而是由 Canvas 源码 src/constants/acp-providers.ts 单一来源生成的:

  • ACP_RESERVED_CREDENTIALS 定义了每个 provider 的“容器凭据”:Codex 的 CODEX_AUTH_JSON(多行文本,提示粘贴 ~/.codex/auth.json)、Claude Code 的 CLAUDE_CODE_OAUTH_TOKEN、Gemini 的 GOOGLE_APPLICATION_CREDENTIALS_JSON(多行,提示粘贴 ~/.config/gcloud/application_default_credentials.json)加三个 GCP 标量字段;
  • getAcpProviderSecrets() 按“容器订阅/Vertex 凭据 → API key → 可选 base URL”的顺序拼装字段列表,其中 API key / base URL 的变量名直接取自 SDK 注册表(经 @openhands/typescript-client 镜像),避免前端与 agent-server 环境变量漂移;
  • 字段 name 同时就是全局 secret 名和 agent-server 导出到 ACP 子进程的环境变量名——保持同名正是已保存密钥能真正到达 provider CLI 的关键。onboarding 表单的状态逻辑由 src/hooks/use-acp-credential-form.ts 复用(含已存 secret 查询、冲突检测与保存流程)。

4.2 从 UI 到子进程:LookupSecret 解析链

每个凭据被存为 agent-server secret store 里的全局 secret(与 Settings → Secrets 里添加完全等价,可随时编辑/删除)。会话启动请求并不携带明文凭据,而是把每个字段引用为一个 LookupSecret(ACP 与非 ACP 会话统一如此);agent-server 在 spawn 子进程时从自己的 store 回查取值。从源码注释与文档交叉印证看,ACP 场景下该回查特意运行在事件循环之外(对应 software-agent-sdk 的 #3510 修复),避免回环 HTTP 请求自死锁。

拿到值之后,SDK 的 acp_file_secrets 默认行为负责“落地”:

  • CODEX_AUTH_JSON 被还原为 CODEX_HOME 下的 auth.json,并把 Codex 指向它;
  • GOOGLE_APPLICATION_CREDENTIALS_JSON 被写成一个文件,由 GOOGLE_APPLICATION_CREDENTIALS 指向,路由 Gemini 走 Vertex AI;
  • 其余值(CLAUDE_CODE_OAUTH_TOKEN、project/location、各 API key)直接作为环境变量导出给 CLI。

也就是说 Canvas 只负责发送 secrets,文件物化完全由 SDK 完成——这也是为什么 acp-data 卷要把 /workspace 持久化:物化出来的凭据文件重启后仍在。

4.3 凭据冲突警告

表单内置了冲突检测(ACP_CREDENTIAL_CONFLICTS / getAcpCredentialConflicts,同样位于 src/constants/acp-providers.ts):不要与 Claude OAuth token 一起设置 ANTHROPIC_BASE_URL。被继承的 LiteLLM base URL 会悄悄破坏 bearer 认证——Canvas 从不替你设置 base URL,但你自己保存过ANTHROPIC_BASE_URL secret 会随每个 start 请求搭便车,所以表单会对这一对组合发出警告。同理,CLAUDE_CODE_OAUTH_TOKENANTHROPIC_API_KEY 同时存在时 token 会静默压过 key,SDK 侧也会剥离冲突项(对应 SDK 的 _ENV_CONFLICT_MAP,#3588)。

5. 三个必须知道的警告

(1)Gemini Vertex 的 ADC 必须新鲜。 复制 ADC 前执行 gcloud auth application-default login——过期 token 会返回 invalid_rapt,这是凭据问题而非 Canvas bug。另外按 docs/ACP_AGENTS.md 的说明,Gemini 请选非 flash 模型:gemini-cli 0.45.x 会在生成时把任意 *-flash 模型 id 重新解析为“当前默认 flash”,导致固定 gemini-2.5-flash 实际跑了不存在的 flash 模型而 404,因此 Canvas 对 Gemini 预选 gemini-2.5-pro(源码中的 ACP_VERTEX_SAFE_MODEL 常量,src/constants/acp-providers.ts)。若某轮 Gemini 报错 Publisher Model … was not found,先检查所选模型是否碰巧是 flash id。

(2).env 烘焙的凭据可能过不了 onboarding 门禁。 登录探测检查的是 CLI 的登录状态(claude auth status / codex login status / Gemini 的 OAuth 凭据文件),而不是容器环境变量——只有 GEMINI_API_KEY 烘焙进 .env 的容器通常仍会被探测为“未登录”,凭据步骤会卡住 “Next”。在 UI 里(重新)填写一次凭据即可继续;烘焙的环境变量对 agent 本身依然生效。

(3)同容器并发会话共享 HOME。 同一 provider 的并发会话共用 HOME,可能在 CLI 的 auth/config/lock 文件上互相竞态。SDK 已支持按会话隔离数据目录(acp_isolate_data_dir,#3492),但当前发布的 @openhands/typescript-client 尚未在 ACPAgentSettings 上暴露该字段,Canvas 无法安全下发(跟踪于 agent-canvas#1019)。本地临时避免办法是错峰使用同一 provider 的多个会话。

6. 收尾与拆除

docker compose down           # 保留 volume(会话 + 物化凭据)
docker compose down -v        # 同时删除 credentials/conversations

down 不带 -vacp-data 卷保留,SDK 物化的凭据文件与会话历史在下次 up 后原样可用;带 -v 则是彻底清场,等价于一个全新的容器环境,onboarding 凭据步骤需要重新走一遍。

7. 小结:这个示例沉淀的工程实践

examples/acp-docker/ 的价值不只是“能跑”,它还示范了几条可复用的工程约定:

  1. 双轨版本策略:零配置走 latest-python(永远满足兼容下限),可复现走 config/defaults.jsonnpm run example:acp-docker:env 生成的精确 tag,两条轨都由 acp-docker-env-sync 测试 锁住不漂移;
  2. 凭据不落镜像、不落 Canvas 前端:一律经 agent-server 的 secret store + LookupSecret 在 spawn 时解析,文件物化交给 SDK 的 acp_file_secrets
  3. 持久化边界清晰acp-data 卷同时承载会话与物化凭据,/canvas-tools 只读挂载只负责旧会话的可恢复性,职责互不干扰。

examples/acp-docker/README.md 的三步(docker compose upVITE_BACKEND_BASE_URL=http://localhost:8010 npm run dev:frontend → UI 填凭据)即可在本地跑通完整的容器化 ACP 链路;更深入的 ACP 概念、认证优先级(订阅登录优先于 API key)与后续切换 agent/模型的说明,可继续阅读 docs/ACP_AGENTS.md

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.12 K
2.72 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
527
590
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
904
1.82 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
854
1.34 K
docsdocs
暂无描述
Markdown
889
5.78 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.52 K
1.01 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.33 K
1.45 K
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
982
502
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
540
384