已定:Claude 用 Agent SDK(ADR-0002),Codex 用 codex app-server(ADR-0003)。
| 维度 | 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 版本耦合 | 一切自建 |
| 维度 | 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)能原生满足。
prompt 传 AsyncIterable),以便发送图片与在 Run 内追加输入。cwd: 工作区目录resume: session.providerSessionId(首轮为空)model、systemPrompt({ type: 'preset', preset: 'claude_code', append },保留内置工具说明并追加 Profile 提示词)permissionMode: default(所有非只读操作经 canUseTool)canUseTool: 桥接到 ctx.interact(见 2.3)allowedTools / disallowedTools: 来自 ProfileadditionalDirectories: 资料库只读目录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 的 PATHincludePartialMessages: true:获取文本增量abortController: 绑定 ctx.signalsystem/init 消息中取 session_id 回写 session.providerSessionId。| 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 |
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 自动放行;"总是允许"的选择记入会话级规则。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 版本对应。| 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 选择使用哪一个。具体方法名以生成的协议类型为准,实现阶段落定后回填本节。
| 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(含"本会话总是允许") |
@openai/codex@0.164.0 作为服务端依赖锁定,随 pnpm deploy 发布;协议类型由该版本 codex app-server generate-ts 生成,位于 apps/server/src/providers/codex/protocol/(升级 codex 时重新生成)/srv/agent-server/home/.codex,开发 .dev-data/home/.codex),不加载个人 ~/.codex 的插件与 MCP(Q3 关闭);auth.json 列入全局禁止项thread/start,之后 thread/resume(失败则新建);每个 Run 一个 turn/start,中断用 turn/interruptconfig 传入动态权限配置 default_permissions = "agent_server" + permissions.agent_server.filesystem(受限模式 ":root" = "deny"),响应中 activePermissionProfile 确认生效。必须把 CODEX_HOME/tmp/arg0-linux 和 codex 二进制目录设为可读,否则沙箱辅助程序无法启动;/tmp 不可写,TMPDIR 指向工作区 .agent/tmpshell_environment_policy.inherit = "core",只继承基础变量,再设置 Python/Node 运行环境变量,不把服务进程的密钥带入命令approvalPolicy = "on-request"(沙箱内命令自动执行,越界才请求审批,与 Claude 的"每条命令都询问"不同);自动 / 只读 → "never";只读模式工作区为 readitem/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-authconvert_document / recalc_workbook / web_fetch / kb_list_roots 由 MCP Gateway 提供,与 Claude 共用(08)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 全部为 falseapps._default.enabled = falsecollaboration.*(子代理)工具在关闭 multi_agent 后仍然出现,子代理继承同一沙箱,待确认对应开关thread/resume 不会更新已建立的 MCP 连接的请求头,所以 Codex 每个会话使用固定的令牌字符串:
thread/unsubscribe 不会卸载会话,turn/start 没有权限覆盖项)。会话进行中修改 Profile 的文件访问策略,要等新会话或服务重启后才对 Codex 命令生效;网关工具每次 Run 都使用最新策略| 能力 | Claude | Codex | 对齐策略 |
|---|---|---|---|
| 主动提问 | ✓ AskUserQuestion | 无专门工具 | Codex 侧以普通文本提问,前端照常渲染 |
| 图片输入 | ✓ | ✓ | 统一为上传后引用 |
| PDF 原生阅读 | ✓(Read 工具多模态) | ✗(需 pdftotext 等) | Profile 中为 Codex 默认附带文档技能 |
| 网页抓取 | WebFetch(本机发起) | 无内置抓取,靠 web_search | 见 07 §4 |
| 思考过程 | ✓ | ✓(reasoning summary) | 统一 reasoning.delta,默认折叠 |
| Skills | ✓ | ✓ | 共享同一份技能目录(见 07 §3) |
claude、codex CLI 版本与 SDK 版本在 apps/server/package.json 和部署脚本中锁定。实测中确认、并已体现在代码里的行为:
| 现象 | 处理 |
|---|---|
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 共用。