03-providers.md 15 KB

03 引擎接入

已定:Claude 用 Agent SDK(ADR-0002),Codex 用 codex app-server(ADR-0003)。

1. 方案比较(选型依据)

1.1 Claude Code

维度 Agent SDK(采用) CLI claude -p --output-format stream-json
集成形态 TS 库,内部拉起 CLI,返回类型化消息流 自行 spawn,逐行解析 JSON
交互审批 canUseTool 异步回调,可挂起等待人工 只能借 --permission-prompt-tool 绕道 MCP
Agent 提问 AskUserQuestion 同样经回调,可转成前端表单 难以处理
自定义工具 进程内 MCP(createSdkMcpServer) 需独立 MCP Server
恢复/中断 resume / interrupt() resume 可,中断只能杀进程
配置隔离 默认不加载用户本地 settings,行为可预测 读取本机配置
图片输入 流式输入模式下可发 image content block 需 stream-json 输入,自行组装
缺点 与 SDK 版本耦合 一切自建

1.2 Codex

维度 Codex SDK(TS) app-server(采用) codex exec --json
形态 codex exec 轻封装 JSON-RPC over stdio(VS Code 插件同款协议) 一次性命令
交互审批 ✗ ✓ 服务端向客户端发起审批请求 ✗
流式粒度 item 级 item + delta 级 item 级
多轮/恢复/中断 ✓ / ✓ / 有限 ✓ / ✓ / ✓ 有限
账户/限额查询 ✗ ✓(账户与登录相关方法) ✗
类型 自带 codex app-server generate-ts 生成 无
缺点 无审批 协议仍标注 experimental,需锁版本 无审批

结论:交互式审批是硬需求,只有 SDK(Claude)和 app-server(Codex)能原生满足。

2. ClaudeAdapter 设计

2.1 调用方式

  • 使用 流式输入模式(prompt 传 AsyncIterable),以便发送图片与在 Run 内追加输入。
  • 关键 Options:
    • cwd: 工作区目录
    • resume: session.providerSessionId(首轮为空)
    • model、systemPrompt({ type: 'preset', preset: 'claude_code', append },保留内置工具说明并追加 Profile 提示词)
    • permissionMode: default(所有非只读操作经 canUseTool)
    • canUseTool: 桥接到 ctx.interact(见 2.3)
    • allowedTools / disallowedTools: 来自 Profile
    • additionalDirectories: 资料库只读目录
    • mcpServers: { gateway: { type: 'http', url, headers: { Authorization: 'Bearer <run-token>' } } }
    • settingSources: [](不加载个人的 settings、插件、hooks);沙箱与权限规则由文件访问策略生成,通过 settings 选项显式传入(见 06 §4.2)
    • pathToClaudeCodeExecutable: 后台设定的 CLI 路径(默认 ~/.local/bin/claude),与终端使用同一个 CLI 和登录态
    • env: 使用默认的 ~/.claude 登录态;不传 ANTHROPIC_API_KEY;设置 MATPLOTLIBRC 和 Python venv 的 PATH
    • includePartialMessages: true:获取文本增量
    • abortController: 绑定 ctx.signal
  • 首条 system/init 消息中取 session_id 回写 session.providerSessionId。

2.2 消息 → 事件映射

SDK 消息 AgentEvent
system (init) run.started(附模型、工具清单)
stream_event text delta message.delta
stream_event thinking delta reasoning.delta
assistant text block message.completed
assistant tool_use block tool.started
user tool_result block tool.completed(Write/Edit 产出文件额外发 file.changed)
result success usage + run.completed
result error_* run.failed

2.3 交互桥接

canUseTool: async (toolName, input, { signal, suggestions }) => {
  if (toolName === 'AskUserQuestion')
    return answerQuestion(await ctx.interact({ kind: 'question', questions: input.questions }))
  if (policy.autoAllow(toolName, input)) return { behavior: 'allow', updatedInput: input }
  const r = await ctx.interact({ kind: 'approval', tool: toolName, input, suggestions })
  return r.decision === 'allow'
    ? { behavior: 'allow', updatedInput: r.updatedInput ?? input }
    : { behavior: 'deny', message: r.reason ?? '用户拒绝' }
}
  • policy.autoAllow:只读工具(Read/Grep/Glob/WebSearch)、工作区内写入、沙箱内的 Bash 自动放行;"总是允许"的选择记入会话级规则。
  • 实现时需核对当前 SDK 版本中 AskUserQuestion 的回答格式。

3. CodexAdapter 设计

3.1 进程管理

  • 单个常驻进程:codex app-server(stdio),使用默认的 ~/.codex 登录态。
  • 必须覆盖个人配置:你的 ~/.codex/config.toml 中启用了插件(browser、computer-use 等)和 MCP(node_repl)。启动时通过 -c 覆盖禁用这些项(providers.codex.configOverrides,在后台可编辑),并在启动自检中确认最终生效的工具清单里不含这些项。
  • 启动后执行 initialize 握手;崩溃后指数退避重启。
  • 协议类型由 codex app-server generate-ts 生成,提交到 apps/server/src/providers/codex/protocol/,并与锁定的 CLI 版本对应。

3.2 会话映射

AgentServer app-server
Session(首轮) 新建 thread(指定 cwd、model、sandbox、approval policy)
Session(续聊) 恢复 thread(providerSessionId = threadId)
Run 一个 turn(输入:文本 + 本地图片路径)
interrupt 中断当前 turn

Thread 参数:approvalPolicy = on-request、cwd = workspaceDir,沙箱使用权限配置(permission profiles,由文件访问策略生成,见 06 §4.2),不再设置 sandbox_mode。MCP Gateway 通过 -c mcp_servers.gateway... 注入。

权限配置能否按 thread 指定(不同 Profile 使用不同策略)需要在实现时核对;如果只能在进程级配置,就在启动时预先注册所有 Profile 的权限配置(permissions.<profileId>.*),按 thread 选择使用哪一个。

具体方法名以生成的协议类型为准,实现阶段落定后回填本节。

3.3 通知 → 事件映射

app-server 通知 / 请求 AgentEvent
turn 开始 run.started
agent message delta / item 完成 message.delta / message.completed
reasoning delta reasoning.delta
命令执行 item 开始/完成 tool.started / tool.completed(name=shell)
文件变更 item tool.* + file.changed
MCP 工具调用 item tool.*(name=mcp__gateway__xxx)
web search item tool.*(name=web_search)
token 用量通知 usage
turn 完成 / 失败 run.completed / run.failed
服务端请求:命令执行审批 / 文件变更审批 ctx.interact({kind:'approval'}) → 回复 accept / decline(含"本会话总是允许")

3.4 实现记录(M2,2026-10-23,codex 0.164.0)

  • 版本与部署:@openai/codex@0.164.0 作为服务端依赖锁定,随 pnpm deploy 发布;协议类型由该版本 codex app-server generate-ts 生成,位于 apps/server/src/providers/codex/protocol/(升级 codex 时重新生成)
  • CODEX_HOME:服务专用(生产 /srv/agent-server/home/.codex,开发 .dev-data/home/.codex),不加载个人 ~/.codex 的插件与 MCP(Q3 关闭);auth.json 列入全局禁止项
  • 进程:单个常驻 app-server,JSON-RPC 每行一个 JSON;按 threadId 把通知和服务端请求分发给对应的 Run;进程退出时正在运行的 Run 失败,下次调用自动重启
  • 会话:首轮 thread/start,之后 thread/resume(失败则新建);每个 Run 一个 turn/start,中断用 turn/interrupt
  • 文件访问策略(Q1 关闭):每次 thread/start / resume 通过 config 传入动态权限配置 default_permissions = "agent_server" + permissions.agent_server.filesystem(受限模式 ":root" = "deny"),响应中 activePermissionProfile 确认生效。必须把 CODEX_HOME/tmp/arg0-linux 和 codex 二进制目录设为可读,否则沙箱辅助程序无法启动;/tmp 不可写,TMPDIR 指向工作区 .agent/tmp
  • 环境变量:shell_environment_policy.inherit = "core",只继承基础变量,再设置 Python/Node 运行环境变量,不把服务进程的密钥带入命令
  • 权限模式:需审批 → approvalPolicy = "on-request"(沙箱内命令自动执行,越界才请求审批,与 Claude 的"每条命令都询问"不同);自动 / 只读 → "never";只读模式工作区为 read
  • 审批(Q2 关闭):item/commandExecution/requestApproval / item/fileChange/requestApproval 回复 accept | acceptForSession | decline | cancel;命令联网请求一律拒绝并审计(Codex 命令联网后不受域名限制);文件变更与 item/permissions/requestApproval 先按访问策略检查,越界自动拒绝
  • 提问:item/tool/requestUserInput 映射为提问卡片,回复 { answers: { [questionId]: { answers: [...] } } }
  • 用量:thread/tokenUsage/updated 给出线程累计与最近一次增量,本次 Run 用量 = 最新累计 −(首次通知的累计 − 首次增量)
  • 登录:account/login/start { type: "chatgptDeviceCode" } 返回网址和短码,管理员在 OpenAI 页面输入,完成后收到 account/login/completed;AgentServer 不接触凭证。终端方式:agent-codex login --device-auth
  • 工具:LibreOffice 在 Codex 沙箱中同样无法运行(禁止 Unix socket),convert_document / recalc_workbook / web_fetch / kb_list_roots 由 MCP Gateway 提供,与 Claude 共用(08)
  • 功能基线:服务账号登录 ChatGPT 后,Codex 会自动安装 openai-curated-remote 中的远程插件,例如 Google Drive、Gmail、Outlook、Teams、Pages、Sites、Work Pets。这些插件会带来插件技能、应用连接器,以及 request_plugin_install 安装建议,调用方可能借此访问账号绑定的个人数据。因此每次对话的 config 都覆盖为:
    • features:plugins / remote_plugin / apps / tool_suggest / skill_mcp_dependency_install / multi_agent / browser_use / computer_use 全部为 false
    • apps._default.enabled = false
    • 已验证:覆盖后对话中只剩 AgentServer 提供的技能与 MCP Gateway
    • 例外:后台 Playground 创建的 Codex 会话开放插件与应用连接器(ADR-0018)
    • 遗留问题:collaboration.*(子代理)工具在关闭 multi_agent 后仍然出现,子代理继承同一沙箱,待确认对应开关
  • 网关令牌:Codex 的会话在 app-server 中保持加载,thread/resume 不会更新已建立的 MCP 连接的请求头,所以 Codex 每个会话使用固定的令牌字符串:
    • 每次 Run 开始时绑定到当前 Run,结束后失效
    • 会话删除时清除
    • 以前每个 Run 换新令牌,导致同一会话第二轮起网关调用返回 401
  • 配置变化时分叉会话(ADR-0020):已加载会话的配置不会因 thread/resume 更新(已验证:同一会话 resume 时传入新的访问策略,命令仍按旧策略执行)。
    • 适配器为每个已加载会话记录配置指纹,覆盖每轮传入的全部配置,包括访问策略、权限模式、技能、全局禁止项、功能开关和系统提示词
    • 指纹变化时改用 thread/fork:保留全部历史,采用新配置,并把会话映射更新为新的 thread id;放宽和收紧都已验证
    • 分叉失败时新建会话(丢失上下文,但配置正确)
    • app-server 重启后,已加载记录清空,下一轮正常 resume

4. 能力差异与对齐

能力 Claude Codex 对齐策略
主动提问 ✓ AskUserQuestion 无专门工具 Codex 侧以普通文本提问,前端照常渲染
图片输入 ✓ ✓ 统一为上传后引用
PDF 原生阅读 ✓(Read 工具多模态) ✗(需 pdftotext 等) Profile 中为 Codex 默认附带文档技能
网页抓取 WebFetch(本机发起) 无内置抓取,靠 web_search 见 07 §4
思考过程 ✓ ✓(reasoning summary) 统一 reasoning.delta,默认折叠
Skills ✓ ✓ 共享同一份技能目录(见 07 §3)

5. 版本管理

  • claude、codex CLI 版本与 SDK 版本在 apps/server/package.json 和部署脚本中锁定。
  • 升级流程:升级 → 重新生成 Codex 协议类型 → 运行 Playground 回归用例(见 09 §3.6)→ 发布。

6. 实现记录(M1,2026-10-22,SDK 0.3.308 / CLI 2.1.307)

实测中确认、并已体现在代码里的行为:

现象 处理
canUseTool 不会对只读工具(Read/Grep/Glob)和沙箱内自动放行的 Bash 触发 路径校验、私网拦截和是否需要审批统一放在 PreToolUse hook 中决定(providers/claude/policy.ts);hook 返回 ask 时 CLI 才调用 canUseTool,再转成交互
hook 不显式返回 allow 时,MCP 工具仍会进入权限询问 hook 对放行的工具显式返回 permissionDecision: 'allow'
AskUserQuestion 的回答格式 canUseTool 返回 allow,updatedInput.answers 以问题原文为键,多选用逗号拼接
settingSources: [] 不能阻止加载用户自己的技能(包括 claude.ai 同步的技能) 始终显式传入 skills 列表(来自共享技能目录,形如 agent-skills:<name>);目录为空时传 []
覆盖 TMPDIR 到工作区后,沙箱报 "Failed to create bridge sockets" 沙箱会在 TMPDIR 中创建 Unix socket,路径过长会失败;不覆盖 TMPDIR
单条消息的流式输入在输出 result 后正常结束 每个 Run 调用一次 query() 并用 resume 续接,无需常驻进程
登录态 system/init 中 apiKeySource: none 表示走 claude.ai OAuth;服务环境中删除 ANTHROPIC_API_KEY / ANTHROPIC_AUTH_TOKEN

| 按路径加载插件(plugins: [{ type: 'local', path }])时,没有 .claude-plugin/plugin.json 的插件以目录名作为技能前缀,并加载 skills/ 下的全部技能(官方仓库的 document-skills 实际是 dbd4588f9e10:xlsx 等,并会带上属于 example-skills 的 15 个技能) | 读取市场清单 marketplace.json 中为该插件声明的技能子集,只展示和启用这些;后台与 Profile 使用友好 ID(document-skills:xlsx),运行时翻译为 CLI 的 ID,时间线显示时再换回来(2026-10-23) | | skills: 'all' 会包含 CLI 自带的捆绑技能(deep-research、dataviz、code-review 等) | 始终传显式列表,只包含本地技能目录和已启用插件声明的技能 |

服务端提供的进程内 MCP 工具:mcp__office__convert_document(LibreOffice 文档转换,见 06 §8)。M2 起会迁移到 MCP Gateway,供 Codex 共用。