首页
/ OpenHands Canvas Live ACP e2e:用真实 Agent-Server 容器验证 ACP 凭证的 LookupSecret 全链路

OpenHands Canvas Live ACP e2e:用真实 Agent-Server 容器验证 ACP 凭证的 LookupSecret 全链路

2026-09-04 22:20:50作者:钟日瑜

本篇基于 Live ACP-in-Docker e2e 文档,讲解 OpenHands 前端仓库中一套"真容器 + 真凭证 + 真 API 调用"的端到端验证方案:它通过 Canvas 自身的代码路径,把 Codex / Claude Code / Gemini CLI 的凭证写入 agent-server 的 secret store,再以 LookupSecret 形式引用,最终断言拿到真实的 agent 回复。读完本文,你能掌握这套 e2e 的完整运行步骤(Docker 启动、脚本执行、环境参数)、三个 provider 的凭证采集实现,以及 buildStartConversationRequest 如何把每个保存的凭证发射为 LookupSecret 的源码级原理。

这套 e2e 验证的是什么

单元测试 __tests__/api/agent-server-adapter.test.ts 只断言"请求的形状"——即 buildStartConversationRequest 输出的 payload 结构是否符合预期;而 tests/e2e/live-acp/ 下的脚本断言的是"真的能跑通":针对一个真实运行的 agent-server 容器,发起真实的 provider API 调用,并检查 agent 的最终回复中包含约定的校验 token(如 ACPOK-CODEX)。

它验证的完整凭证链路是:

  1. onboarding 等价步骤:通过 SecretsService.createSecret(即应用 onboarding 中 "Set up credentials" 的同一个调用)把宿主机上采集到的凭证 upsert 进 agent-server 的 secret store(本地后端走 PUT /api/settings/secrets);
  2. 构建启动请求:调用 Canvas 自己的 buildStartConversationRequest,把每个已保存的凭证按名字引用为一个 LookupSecret,secret 的值不出现在请求里
  3. 服务端解析:agent-server 在 ACP 进程冷启动(spawn)时,通过 GET /api/settings/secrets/<name> 把值从 store 解析回来(日志中可见 200 响应),完成如 codex 的 auth.json 物化(Materialised ACP file-secret 'CODEX_AUTH_JSON' -> …/acp/codex/auth.json)或 Gemini 的 ADC 物化(GOOGLE_APPLICATION_CREDENTIALS_JSON -> …/acp/gemini-cli/gcloud-credentials.json)。

版本要求:需要 agent-server:1.25.0-python 或更新(software-agent-sdk#3510)。ACP 凭证以 loopback LookupSecret 的形式传递,只有 #3510 之后才在事件循环之外解析它们;旧镜像会在第一轮对话时死锁。运行脚本推荐镜像为 1.28.0-python(v1.28.0 新增了 Canvas 使用的 client_tools API,acp-docker-e2e.mts 头部注释明确要求该版本)。

该目录位于 tests/ 下,被 Vitest 排除,不属于 npm test——因为它需要一个正在运行的容器和宿主机上的真实凭证,只能手动执行。

运行步骤

1. 启动 agent-server 容器

# 1. Agent-server 容器。v1.28.0 新增 Canvas 使用的 client_tools API。
#    挂载 Python 目录保证迁移前的会话状态仍可加载。
docker run -d --name oh-acp -p 8010:8000 \
  -v oh-acp-data:/workspace \
  -v "$(pwd)/tools:/canvas-tools:ro" -e OH_EXTRA_PYTHON_PATH=/canvas-tools \
  ghcr.io/openhands/agent-server:1.28.0-python

几个挂载参数的含义:

  • -p 8010:8000:容器内 agent-server 监听 8000,映射到宿主机 8010,与 e2e 脚本默认 ACP_E2E_BASE_URL=http://localhost:8010 对应;
  • -v oh-acp-data:/workspace:命名卷挂载到 /workspace,作为所有会话 working_dir 的根(每个会话使用 <base>/<id_hex> 独立目录,以便 agent-server 为每个会话初始化独立的 git repo + worktree);
  • -v "$(pwd)/tools:/canvas-tools:ro" + OH_EXTRA_PYTHON_PATH=/canvas-tools:把仓库的 tools/ 目录(含 canvas_ui_tool.py)只读挂入容器并注入 Python 路径,供 Canvas 的 canvas_ui_control client tool 使用。

2. 运行 e2e 脚本

# 运行全部 provider,或指定子集
npx vite-node -c tests/e2e/live-acp/vite-node.config.mts \
  tests/e2e/live-acp/acp-docker-e2e.mts -- codex claude gemini

acp-docker-e2e.mts 是"请求构建器路径"脚本,验证通过后还会配套运行应用编排器路径脚本 acp-docker-app-e2e.mts(一次一个 provider,因为 settings 在 agent-server 上是全局的,新进程可避免 SettingsService 缓存在 provider 之间串扰):

npx vite-node -c tests/e2e/live-acp/vite-node.config.mts \
  tests/e2e/live-acp/acp-docker-app-e2e.mts -- codex

脚本通过 vite-node.config.mts 在完整 Vite/React-Router 管线之外运行:该最小配置只做了两件事——把 #/* 别名指向 src/*(应用平时靠 tsconfig-paths 解析,vite-node 不加载它),以及把 @openhands/typescript-client 设为 SSR inline,使其 ESM 解析方式与 Vitest 一致。

3. 清理(容器持有真实凭证)

docker rm -f oh-acp

宿主机凭证采集:三个 provider 的 collector

凭证从宿主机读取,全程不会打印。采集逻辑集中在共享的 harness.mts 中(harness.mts#L107-L156),任何 provider 的凭证缺失时该 provider 被跳过(跳过不算失败):

Provider acp_server 默认模型(可覆盖) 凭证来源 容器 secret 名
Codex codex gpt-5.5/mediumACP_E2E_CODEX_MODEL ~/.codex/auth.json CODEX_AUTH_JSON
Claude Code claude-code claude-opus-4-7ACP_E2E_CLAUDE_MODEL macOS 钥匙串(security find-generic-password -s "Claude Code-credentials" -w,取 claudeAiOauth.accessToken CLAUDE_CODE_OAUTH_TOKEN
Gemini CLI gemini-cli gemini-2.5-proACP_E2E_GEMINI_MODEL ~/.config/gcloud/application_default_credentials.json + gcloud 默认项目(需先 gcloud auth application-default login GOOGLE_APPLICATION_CREDENTIALS_JSONGOOGLE_CLOUD_PROJECTGOOGLE_CLOUD_LOCATIONGOOGLE_GENAI_USE_VERTEXAI=true

两个值得注意的实现细节(均可在 harness.mts 中验证):

  • Claude 故意不设置 ANTHROPIC_BASE_URL:继承的环境 base URL 会破坏 OAuth token 的 bearer 认证(harness.mts#L124-L128);
  • 非 macOS 上 Claude collector 直接返回 null:没有 security 命令时 execFileSync 抛错被 catch 掉,runner 打印 SKIP — credentials not present on host

另外 harness.mtsregisterDockerBackend() 把应用的后端注册表指向该容器(setRegisteredBackends + setActiveSelection),效果等同于用户在 backend selector 中手动添加——下游的 SecretsServiceSettingsService 以及 buildStartConversationRequest 发出的 LookupSecret 鉴权头全部经由这一选择解析宿主地址。

源码纵深:LookupSecret 是如何发射的

SecretsService:与 onboarding 相同的写入路径

SecretsService.createSecret 对本地后端调用 SettingsClient.upsertSecret({ name, value, description }),即 agent-server 的 PUT /api/settings/secrets(按名字 upsert);云后端则走 saveCloudSecret。secret 名字有约束:字母开头,仅字母/数字/下划线,1–64 字符。列表接口 getSecrets() 只返回名字和描述,不返回值——值只存在于 agent-server 的 store 中。

buildStartConversationRequest:把名字变成 LookupSecret

agent-server-adapter.ts 中,LookupSecret 的定义是:

interface LookupSecret {
  kind: "LookupSecret";
  url: string;          // 指向 secret store 的 loopback 地址
  headers?: Record<string, string>;
  description?: string;
}

buildStartConversationRequestL1085)接受 customSecrets: Array<{ name; description? }>L1076),并为每个名字生成一条 { kind: "LookupSecret", url, ... } 放入 payload 的 secrets 字段(L1239-L1251)。e2e 脚本正是利用这一点做不泄漏值的健全性检查:把 payload 中所有 secrets 打印出 kind,并断言全部为 LookupSecret——若不是,直接判 FAIL(acp-docker-e2e.mts#L104-L121)。

ACP 相关的启动参数通过 settings.agent_settings 传入:agent_kind: "acp"acp_server(即上表中的注册表键)、acp_model,可选 acp_session_mode;会话侧 max_iterations: 8acp-docker-e2e.mts#L54-L82)。

轮询与断言

harness.mts 提供两个共享 helper:

  • pollUntilTerminal(conversationId):每 2.5s 轮询 GET /api/conversations/<id> 直到 execution_status 进入终态集合 {finished, error, stuck, stopped} 或超时(默认 ACP_E2E_TIMEOUT_MS=180000)。注意 idle 刻意不在终态集合中——新建会话在 agent 启动前会报 idle,若把它当终态会在回复产生前就退出;
  • fetchFinalReply(conversationId):拉取 GET /api/conversations/<id>/agent_final_response,脚本断言其中包含对应 provider 的期望 token(ACPOK-CODEX / ACPOK-CLAUDE / ACPOK-GEMINI),任务指令就是 Reply with exactly: <token>

两条脚本路径覆盖的差异

acp-docker-e2e.mts(请求构建器路径) acp-docker-app-e2e.mts(应用编排器路径)
会话启动 直接调 buildStartConversationRequest,内联 agent_settingsacp_server/acp_model 显式传入) buildStartConversationRequestWithEncryptedSettingsL1290),基础 settings 从后端重新拉取
Agent 选择 请求内联指定 先经 buildAcpAgentSettingsDiff(acpServer, { model }) 构造 diff,PATCH /api/settingsagent_settings_diff),模拟应用的 choose-agent 步骤
额外证明 LookupSecret 从 store 解析、SDK 的 acp_file_secrets 物化端到端生效 保存的凭证往返经过后端 store,且编排器为每个已保存的 secret 名字发射正确的 LookupSecretacp-docker-app-e2e.mts#L78-L102
Provider 数量 可一次跑多个(-- codex claude gemini 每个进程一个 provider

harness.mts 的注释点明了共享策略:provider 计划(模型、凭证采集器)与 HTTP/轮询 helper 由两个脚本共用——改模型默认值或凭证参数时只改 harness,不要分别改脚本,避免两侧漂移。

环境参数(Knobs)一览

环境变量 默认值 说明
ACP_E2E_BASE_URL http://localhost:8010 agent-server 容器地址
ACP_E2E_CODEX_MODEL / ACP_E2E_CLAUDE_MODEL / ACP_E2E_GEMINI_MODEL gpt-5.5/medium / claude-opus-4-7 / gemini-2.5-pro 各 provider 的 ACP 模型,需是账户/Vertex 项目支持的模型
ACP_E2E_GEMINI_SESSION_MODE 不设置(SDK 用 provider 注册表默认) 设为 default 可绕过 gemini-cli ≥0.43 在 headless init 时 set_session_mode("yolo") 报错的 SDK 阻塞点
GOOGLE_CLOUD_PROJECT / GOOGLE_CLOUD_LOCATION 从 gcloud 读取 / us-central1 Gemini Vertex 的项目与区域
ACP_E2E_TIMEOUT_MS 180000 单轮会话轮询超时(harness 中定义)
ACP_E2E_WORKING_DIR_BASE /workspace/acp-e2e 请求构建器脚本的会话工作目录根

已验证结果与 Gemini 的三个前置条件

文档记录了 2026-06-07 针对 ghcr.io/openhands/agent-server:1.25.0-python(首个包含 software-agent-sdk#3510 的发布)在全新卷上的复验:每个凭证都从 secret store 种子注入、无残留状态;日志确认 agent-server 在 ACP 冷启动期间解析了 loopback LookupSecretGET /api/settings/secrets/<name> 返回 200),无死锁、无 "Failed to start ACP server: timed out"——这正是 #3510 修复的问题。三个 provider 的真实回复:

Provider 结果 日志证据
Codex 真实回复 ACPOK-CODEX(两个脚本) Materialised ACP file-secret 'CODEX_AUTH_JSON' -> …/acp/codex/auth.json;codex-acp 0.15.0;Authenticating with ACP method: chatgpt
Claude Code 真实回复 ACPOK-CLAUDE(两个脚本) claude-agent-acp 0.30.0;CLAUDE_CODE_OAUTH_TOKEN env 路径(未设 ANTHROPIC_BASE_URL
Gemini CLI 真实回复 ACPOK-GEMINI¹ Materialised ACP file-secret 'GOOGLE_APPLICATION_CREDENTIALS_JSON' -> …/acp/gemini-cli/gcloud-credentials.json;gemini-cli 0.45.1;Authenticating with ACP method: vertex-ai,在 gemini-2.5-pro 上发生真实 Vertex 推理

¹ Gemini 前置条件(三者缺一不可):

  1. 新鲜的宿主机 ADC——需重新执行 gcloud auth application-default login;过期的 ADC 会以 invalid_rapt 失败,这是凭证问题而非 Canvas 问题;
  2. 非 flash 的 gemini-2.5-pro 模型——gemini-cli 0.45.x 会在生成时把任何 *-flash id 重新解析为当前默认 flash,在不提供该模型的 project 上会 404(software-agent-sdk#3532),这也是 Canvas 预置 gemini-5.5-pro 之外的 gemini-2.5-pro 的原因;
  3. ACP_E2E_GEMINI_SESSION_MODE=default——绕过 gemini-cli ≥0.43 的 set_session_mode("yolo") headless-init 阻塞点。

acp-docker-e2e.mts 内置了对第三种情形的提示:若 gemini 在无 session mode 覆盖时报 error,脚本会提示"这大概率是 SDK/gemini-cli 的 set_session_mode('yolo') 阻塞,而非凭证问题,请用 ACP_E2E_GEMINI_SESSION_MODE=default 重跑以确认凭证链路端到端"(acp-docker-e2e.mts#L137-L147)。

小结:这套 e2e 的设计要点

  • 走 Canvas 自己的代码:凭证写入用 onboarding 相同的 SecretsService.createSecret;请求构建用应用相同的 buildStartConversationRequest / buildStartConversationRequestWithEncryptedSettings——单元测试无法覆盖的"它真的能用"检查由它补齐;
  • 凭证永不过客户端:值只存进 agent-server 的 secret store,请求里只有名字和 loopback LookupSecret 地址,脚本侧只打印 kind 做健全性检查;
  • 单点维护:模型默认值与凭证采集器统一收敛在 harness.mts,两条脚本路径共用;
  • 可复现的版本前提:agent-server ≥1.25.0(#3510 的 off-loop 解析)且推荐 1.28.0(client_tools API);镜像、挂载与清理命令见上文运行步骤。

相关文件索引:READMEharness.mtsacp-docker-e2e.mtsacp-docker-app-e2e.mtsvite-node.config.mtsagent-server-adapter.tssecrets-service.tsacp-providers.ts单元测试对照

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

项目优选

收起
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
981
502
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
540
384