OpenHands Canvas Live ACP e2e:用真实 Agent-Server 容器验证 ACP 凭证的 LookupSecret 全链路
2026/9/5 20:12:04 网站建设 项目流程

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

【免费下载链接】OpenHands🙌 OpenHands: AI-Driven Development项目地址: https://gitcode.com/GitHub_Trending/ope/OpenHands

本篇基于 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 凭证以 loopbackLookupSecret的形式传递,只有 #3510 之后才在事件循环之外解析它们;旧镜像会在第一轮对话时死锁。运行脚本推荐镜像为1.28.0-python(v1.28.0 新增了 Canvas 使用的client_toolsAPI,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_controlclient 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 被跳过(跳过不算失败):

Provideracp_server默认模型(可覆盖)凭证来源容器 secret 名
Codexcodexgpt-5.5/mediumACP_E2E_CODEX_MODEL~/.codex/auth.jsonCODEX_AUTH_JSON
Claude Codeclaude-codeclaude-opus-4-7ACP_E2E_CLAUDE_MODELmacOS 钥匙串(security find-generic-password -s "Claude Code-credentials" -w,取claudeAiOauth.accessTokenCLAUDE_CODE_OAUTH_TOKEN
Gemini CLIgemini-cligemini-2.5-proACP_E2E_GEMINI_MODEL~/.config/gcloud/application_default_credentials.json+ gcloud 默认项目(需先gcloud auth application-default loginGOOGLE_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.mts 的registerDockerBackend()把应用的后端注册表指向该容器(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; }

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

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

环境参数(Knobs)一览

环境变量默认值说明
ACP_E2E_BASE_URLhttp://localhost:8010agent-server 容器地址
ACP_E2E_CODEX_MODEL/ACP_E2E_CLAUDE_MODEL/ACP_E2E_GEMINI_MODELgpt-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-central1Gemini Vertex 的项目与区域
ACP_E2E_TIMEOUT_MS180000单轮会话轮询超时(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 冷启动期间解析了 loopbackLookupSecretGET /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_TOKENenv 路径(未设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 会在生成时把任何*-flashid 重新解析为当前默认 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,请求里只有名字和 loopbackLookupSecret地址,脚本侧只打印kind做健全性检查;
  • 单点维护:模型默认值与凭证采集器统一收敛在 harness.mts,两条脚本路径共用;
  • 可复现的版本前提:agent-server ≥1.25.0(#3510 的 off-loop 解析)且推荐 1.28.0(client_toolsAPI);镜像、挂载与清理命令见上文运行步骤。

相关文件索引:README、harness.mts、acp-docker-e2e.mts、acp-docker-app-e2e.mts、vite-node.config.mts、agent-server-adapter.ts、secrets-service.ts、acp-providers.ts、单元测试对照。

【免费下载链接】OpenHands🙌 OpenHands: AI-Driven Development项目地址: https://gitcode.com/GitHub_Trending/ope/OpenHands

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询