主题
codex-cli 架构深度解析
包名:
@openai/codex(v0.1.2504251709) 语言: TypeScript + TSX (React/Ink) 运行时: Node.js >= 22 (ESM) 构建: esbuild → 单一 ESM bundle 测试: Vitest (72 个测试文件)
一、目录结构
codex-cli/
├── bin/codex.js # CLI 可执行入口(ESM 引导脚本)
├── build.mjs # esbuild 构建脚本(prod/dev 双模式)
├── package.json # NPM 包配置 (dependencies + scripts)
├── tsconfig.json # TypeScript 配置(ESNext + strict + bundler)
├── vite.config.ts # Vitest 配置
├── require-shim.js # CJS require 兼容 shim
├── ignore-react-devtools-plugin.js # esbuild 插件:过滤 react-devtools-core
├── Dockerfile # Docker 容器运行配置
├── .eslintrc.cjs # ESLint 配置
│
├── src/ # ========== 源代码 ==========
│ ├── cli.tsx ★ [19KB] # 主入口:meow 参数解析 + 多模式分发
│ ├── cli-singlepass.tsx [583B] # 全上下文模式入口
│ ├── app.tsx [2.9KB] # React 根组件:Git 检查 + 路由
│ ├── approvals.ts ★ [15KB] # 审批策略引擎(三级安全评估)
│ ├── text-buffer.ts ★ [28KB] # 多行文本编辑器核心(Unicode-aware)
│ ├── format-command.ts [1.9KB] # 命令格式化展示
│ ├── parse-apply-patch.ts [2.7KB] # Patch 前缀常量定义
│ ├── typings.d.ts [2.4KB] # 全局类型声明
│ ├── shims-external.d.ts [736B] # 外部模块声明
│ │
│ ├── components/ # ── UI 组件层 ──
│ │ ├── chat/ # 核心聊天界面(12个组件)
│ │ │ ├── terminal-chat.tsx ★ [25KB] # 主聊天容器:AgentLoop 生命周期 + Overlay 系统
│ │ │ ├── terminal-chat-input.tsx ★ [29KB] # 输入处理:斜杠命令 + 历史 + 文件补全
│ │ │ ├── terminal-chat-response-item.tsx [8KB] # 响应渲染:message/function_call/reasoning
│ │ │ ├── terminal-chat-command-review.tsx [9KB] # 命令审批交互界面
│ │ │ ├── terminal-chat-input-thinking.tsx [3KB] # 加载动画(滚动球 + 计时)
│ │ │ ├── terminal-chat-tool-call-command.tsx [4KB] # 工具调用命令展示
│ │ │ ├── multiline-editor.tsx [14KB] # 多行编辑器组件(封装 TextBuffer)
│ │ │ ├── terminal-chat-completions.tsx [2KB] # Tab 补全列表
│ │ │ ├── terminal-chat-past-rollout.tsx [2KB] # 历史会话回放
│ │ │ ├── terminal-header.tsx [3KB] # 顶部状态栏
│ │ │ ├── terminal-message-history.tsx [3KB] # 消息历史列表
│ │ │ ├── message-history.tsx [3KB] # 消息历史数据
│ │ │ └── use-message-grouping.ts [255B] # 消息分组 hook
│ │ │
│ │ ├── approval-mode-overlay.tsx [1KB] # 审批模式切换覆盖层
│ │ ├── diff-overlay.tsx [3KB] # Git diff 查看覆盖层
│ │ ├── help-overlay.tsx [3KB] # 帮助信息覆盖层
│ │ ├── history-overlay.tsx [7KB] # 对话历史覆盖层
│ │ ├── model-overlay.tsx [5KB] # 模型/Provider 切换覆盖层
│ │ ├── typeahead-overlay.tsx [5KB] # 自动补全覆盖层
│ │ ├── singlepass-cli-app.tsx [18KB] # 单次执行模式 UI
│ │ │
│ │ ├── onboarding/ # 新手引导
│ │ │ └── onboarding-approval-mode.tsx
│ │ ├── select-input/ # 自定义选择组件
│ │ │ ├── select-input.tsx / indicator.tsx / item.tsx
│ │ └── vendor/ # 第三方 vendor 组件
│ │ ├── ink-spinner.tsx / ink-text-input.tsx
│ │ ├── cli-spinners/index.js
│ │ └── ink-select/ (7 个文件)
│ │
│ ├── hooks/ # ── React Hooks ──
│ │ ├── use-confirmation.ts [2KB] # 基于队列的确认对话框机制
│ │ └── use-terminal-size.ts [634B] # 终端尺寸监听
│ │
│ └── utils/ # ── 工具函数层 ──
│ ├── agent/ # Agent 核心引擎
│ │ ├── agent-loop.ts ★★ [62KB] # 核心!Agent 循环 + 流式处理 + 重试 + 系统提示
│ │ ├── handle-exec-command.ts [12KB] # 命令执行编排(审批→沙盒→执行)
│ │ ├── apply-patch.ts [22KB] # Patch 解析与应用引擎
│ │ ├── exec.ts [4KB] # 执行器:apply_patch vs shell 分发
│ │ ├── parse-apply-patch.ts [3KB] # Patch 操作类型解析
│ │ ├── platform-commands.ts [2KB] # 跨平台命令适配
│ │ ├── review.ts [382B] # ReviewDecision 枚举定义
│ │ └── sandbox/ # 沙盒执行层
│ │ ├── interface.ts [663B] # SandboxType 枚举 + ExecInput/ExecResult 类型
│ │ ├── raw-exec.ts [7KB] # 底层进程 spawn(进程组管理 + 信号处理)
│ │ ├── macos-seatbelt.ts [5KB] # macOS Seatbelt 沙盒策略
│ │ └── create-truncating-collector.ts [2KB] # 输出截断收集器
│ │
│ ├── config.ts ★ [19KB] # 配置管理(JSON/YAML + 多级优先级)
│ ├── responses.ts ★ [21KB] # 双 API 协议适配(Responses ↔ Chat Completions)
│ ├── providers.ts [1KB] # 8 种 LLM Provider 注册表
│ ├── model-utils.ts [7KB] # 模型列表获取 + 上下文窗口计算
│ ├── model-info.ts [5KB] # 模型元数据注册表
│ ├── parsers.ts [3KB] # 工具调用参数解析
│ ├── session.ts [1.5KB] # Session ID + 版本管理
│ ├── compact-summary.ts [3KB] # 对话压缩摘要生成
│ ├── approximate-tokens-used.ts [2KB] # Token 用量估算(4字符≈1token)
│ ├── auto-approval-mode.ts [217B] # 审批模式枚举
│ ├── slash-commands.ts [1KB] # 斜杠命令定义
│ ├── input-utils.ts [1KB] # 用户输入构建
│ ├── check-in-git.ts [1KB] # Git 仓库检测
│ ├── check-updates.ts [4KB] # CLI 更新检测
│ ├── get-diff.ts [4KB] # Git diff 获取
│ ├── bug-report.ts [2KB] # Bug report URL 生成
│ ├── file-system-suggestions.ts [1KB] # 文件路径补全
│ ├── extract-applied-patches.ts [1KB] # 已应用 patch 提取
│ ├── package-manager-detector.ts [2KB] # 包管理器检测
│ ├── short-path.ts [777B] # 路径缩短
│ ├── terminal.ts [3KB] # 终端控制(清屏 + 退出清理)
│ │
│ ├── logger/ # 日志系统
│ │ └── log.ts [4KB] # AsyncLogger(DEBUG=1 启用)
│ ├── storage/ # 持久化存储
│ │ ├── command-history.ts [3KB] # 命令历史(~/.codex/history.json)
│ │ └── save-rollout.ts [1KB] # 会话回放保存(~/.codex/sessions/)
│ └── singlepass/ # 全上下文单次执行模式
│ ├── context.ts [2KB] # TaskContext 接口 + XML 渲染
│ ├── context_files.ts [9KB] # LRU 文件缓存 + 忽略规则
│ ├── context_limit.ts [6KB] # 目录大小分析
│ ├── code_diff.ts [6KB] # Diff 生成与着色
│ └── file_ops.ts [1KB] # Zod Schema(文件操作定义)
│
├── tests/ # ========== 测试 ==========
│ ├── 72 个测试文件 (.test.ts / .test.tsx)
│ ├── ui-test-helpers.tsx # UI 测试辅助
│ ├── __fixtures__/ # 测试数据
│ └── __snapshots__/ # 快照
│
├── scripts/ # Docker 容器脚本
│ ├── build_container.sh
│ ├── init_firewall.sh
│ └── run_in_container.sh
│
└── examples/ # 使用示例
├── README.md / prompting_guide.md
├── build-codex-demo/
├── camerascii/
├── impossible-pong/
└── prompt-analyzer/文件统计:
| 指标 | 数量 |
|---|---|
| 源文件 (src/) | 87 个 |
| 测试文件 (tests/) | 72 个 |
| 最大文件 | agent-loop.ts (62KB, ~1522行) |
| 总估算代码量 | ~15,000-18,000 行 |
二、模块功能与关系
2.1 七大核心模块
| # | 模块 | 位置 | 核心职责 |
|---|---|---|---|
| M1 | CLI 入口 | cli.tsx + app.tsx | 参数解析、模式分发、React 渲染启动 |
| M2 | UI 组件层 | components/ + hooks/ | 终端交互界面(React/Ink 组件树) |
| M3 | Agent 引擎 | utils/agent/agent-loop.ts | 核心循环:请求→流消费→工具调用→响应 |
| M4 | 命令执行层 | utils/agent/handle-exec-command.ts + sandbox/ | 审批→沙盒选择→进程执行 |
| M5 | 审批策略 | approvals.ts | 三级安全评估(suggest/auto-edit/full-auto) |
| M6 | API 适配层 | utils/responses.ts + utils/providers.ts | 双协议适配 + 8 种 Provider 支持 |
| M7 | 配置与存储 | utils/config.ts + utils/storage/ | 多格式配置加载 + 会话持久化 |
2.2 模块关系 Mermaid 图
2.3 数据流:一次完整交互
2.4 各模块详细说明
M1 - CLI 入口 (cli.tsx)
核心职责:解析 CLI 参数,根据不同模式分发到对应的执行路径。
用户输入 → meow 解析参数
↓
┌─────────┼──────────┬──────────┬─────────┐
│ │ │ │ │
--help --config --view --quiet 默认交互
(帮助) (编辑器) (回放) (静默模式) (React/Ink)
│ │ │ │ │
exit exit exit runQuiet render(<App>)
Mode()支持的命令行参数:
| 参数 | 简写 | 类型 | 说明 |
|---|---|---|---|
--model | -m | string | 指定模型 (默认 o4-mini) |
--provider | -p | string | 指定 Provider (默认 openai) |
--image | -i | string[] | 附加图片路径 |
--quiet | -q | boolean | 静默非交互模式 |
--approval-mode | -a | string | 审批策略 (suggest/auto-edit/full-auto) |
--full-auto | — | boolean | 全自动模式 |
--auto-edit | — | boolean | 自动编辑模式 |
--full-context | -f | boolean | 全上下文单次执行模式 |
--flex-mode | — | boolean | Flex 处理模式 (仅 o3/o4-mini) |
--disable-response-storage | — | boolean | 禁用服务端响应存储 |
--writable-root | -w | string[] | 沙盒可写目录 |
--view | -v | string | 查看历史回放 |
--notify | — | boolean | 启用桌面通知 |
--full-stdout | — | boolean | 不截断命令输出 |
M2 - UI 组件层
采用 React + Ink 架构,在终端中渲染 React 组件树:
<App>
├── <TerminalChatPastRollout> # --view 模式
├── <ConfirmInput> # 非 Git 仓库警告
└── <TerminalChat> # 正常交互模式
├── <TerminalMessageHistory> # 消息历史展示
│ └── <TerminalChatResponseItem> × N
│ ├── <Markdown> # marked + marked-terminal 渲染
│ ├── <TerminalChatResponseToolCall>
│ └── <TerminalChatResponseToolCallOutput>
│
├── <TerminalChatInput> # 输入区域
│ ├── <TerminalChatInputThinking> # 加载状态(滚动球动画)
│ ├── <MultilineTextEditor> # 多行编辑器
│ │ └── TextBuffer # 底层缓冲区
│ ├── <TextCompletions> # Tab 补全列表
│ └── <TerminalChatCommandReview> # 命令审批交互
│
└── Overlay 系统 (互斥)
├── <HistoryOverlay> /history
├── <ModelOverlay> /model
├── <ApprovalModeOverlay> /approval
├── <HelpOverlay> /help
└── <DiffOverlay> /diff斜杠命令系统:
| 命令 | 功能 |
|---|---|
/help | 显示帮助信息 |
/model | 切换模型/Provider |
/approval | 切换审批模式 |
/history | 查看对话历史 |
/diff | 查看 Git diff |
/compact | 压缩上下文(生成摘要) |
/clear | 清除当前会话 |
/clearhistory | 清除命令历史 |
/bug | 生成 Bug report URL |
M3 - Agent 引擎 (agent-loop.ts)
这是整个项目的核心,1522 行代码实现了完整的 AI Agent 循环。
核心类 AgentLoop 的关键字段:
| 字段 | 类型 | 作用 |
|---|---|---|
oai | OpenAI | OpenAI SDK 客户端实例 |
generation | number | 防止旧 run 的过时事件泄漏到新 run |
canceled | boolean | 用户取消标志 |
transcript | ResponseInputItem[] | 本地会话记录(ZDR 模式使用) |
pendingAborts | Set<string> | 被取消但未应答的 function call ID |
currentStream | unknown | 当前活跃的 SSE 流引用 |
execAbortController | AbortController | 用于中止进行中的工具调用 |
hardAbort | AbortController | 终止信号(terminate() 触发) |
Agent Loop 状态机:
┌─────────────────────┐
│ IDLE │
│ (等待用户输入) │
└─────────┬───────────┘
│ run(input)
▼
┌─────────────────────┐
┌──────│ REQUESTING │──────┐
│ │ (发送 API 请求) │ │
│ └─────────┬───────────┘ │
│ │ stream 开始 │ 超时/错误
│ ▼ │ → 重试(max 8次)
│ ┌─────────────────────┐ │
│ │ STREAMING │◄─────┘
│ │ (消费流式事件) │
│ └─────────┬───────────┘
│ │
cancel() ┌─────┴─────┐
│ │ │
│ 文本输出 function_call
│ │ │
│ ▼ ▼
│ stageItem handleFunctionCall()
│ │ │
│ │ handleExecCommand()
│ │ │
│ │ ┌─────┴─────┐
│ │ │ │
│ │ canAutoApprove askUser
│ │ │ │
│ │ ▼ ▼
│ │ exec() 用户决策
│ │ │ │
│ │ ▼ │
│ │ function_call_output
│ │ │
│ │ ▼
│ │ turnInput = [output]
│ │ │
│ └──┬──┘
│ │
│ ▼
│ turnInput.length > 0?
│ ├── Yes → 回到 REQUESTING
│ └── No ↓
│ ▼
└────►┌─────────────────────┐
│ FLUSH / DONE │
│ (清理 + onLoading) │
└─────────────────────┘M4 - 命令执行层
handleExecCommand()
│
├── 1. 检查 alwaysApprovedCommands 缓存 → 直接执行
│
├── 2. canAutoApprove() 安全评估
│ ├── "auto-approve" → 执行 (可能在沙盒中)
│ ├── "ask-user" → askUserPermission()
│ └── "reject" → 返回 "aborted"
│
├── 3. execCommand()
│ ├── apply_patch → execApplyPatch()
│ │ └── Parser.parse() → apply_changes()
│ └── shell 命令 → exec()
│ └── getSandbox()
│ ├── macOS → MACOS_SEATBELT
│ │ └── execWithSeatbelt()
│ │ └── sandbox-exec -p <policy> -- <cmd>
│ └── 其他 → NONE
│ └── raw-exec.exec()
│ └── spawn(prog, args, {detached:true})
│
└── 4. 沙盒执行失败 + fullAutoErrorMode=ASK_USER
→ 再次询问用户 → 非沙盒重试M5 - 审批策略 (approvals.ts)
三级审批策略的安全评估逻辑:
| 策略 | 安全命令 | apply_patch | 其他命令 |
|---|---|---|---|
| suggest | ✅ 自动 (无沙盒) | ❌ 询问用户 | ❌ 询问用户 |
| auto-edit | ✅ 自动 (无沙盒) | ✅ 自动 (可写路径内) | ❌ 询问用户 |
| full-auto | ✅ 自动 (无沙盒) | ✅ 自动 (沙盒) | ✅ 自动 (沙盒) |
安全命令白名单 (isSafeCommand()):
导航: cd, pwd
搜索: ls, find(排除-exec/-delete), grep, rg, which
读取: cat, head, tail, wc, sed -n
版本: git status/branch/log/diff/show
其他: echo, true, cargo checkM6 - API 适配层
┌────────────────────┐
│ AgentLoop │
└────────┬───────────┘
│
┌─────────────┼─────────────┐
│ │
provider=openai provider=其他
│ │
▼ ▼
oai.responses.create() responsesCreateViaChatCompletions()
│ │
│ ┌────────┴────────┐
│ │ Chat Completions │
│ │ → 转换为 │
│ │ Responses 事件流 │
│ └─────────────────┘
│ │
└─────────┬─────────────────┘
│
AsyncIterable<ResponseEvent>
│
统一的事件消费逻辑支持的 8 种 Provider:
| Provider | Base URL | API Key 环境变量 |
|---|---|---|
| OpenAI | api.openai.com/v1 | OPENAI_API_KEY |
| OpenRouter | openrouter.ai/api/v1 | OPENROUTER_API_KEY |
| Gemini | generativelanguage.googleapis.com/v1beta/openai | GEMINI_API_KEY |
| Ollama | localhost:11434/v1 | OLLAMA_API_KEY |
| Mistral | api.mistral.ai/v1 | MISTRAL_API_KEY |
| DeepSeek | api.deepseek.com | DEEPSEEK_API_KEY |
| xAI | api.x.ai/v1 | XAI_API_KEY |
| Groq | api.groq.com/openai/v1 | GROQ_API_KEY |
M7 - 配置与存储
配置加载优先级(高→低):
CLI flags > config.json/yaml > 环境变量 > .env 文件 > ~/.codex.env > 默认值
配置文件路径:
~/.codex/config.json (或 .yaml / .yml)
~/.codex/instructions.md
<project>/codex.md (或 .codex.md / CODEX.md)
存储位置:
~/.codex/history.json # 命令历史
~/.codex/sessions/rollout-*.json # 会话回放三、如何使用
3.1 安装与运行
bash
# 安装
npm install -g @openai/codex
# 设置 API Key
export OPENAI_API_KEY="sk-..."
# 交互模式(默认)
codex
# 带提示词启动
codex "Write a Python script that sorts a list"
# 静默模式(CI/脚本)
codex -q "fix all TypeScript errors"
# 全自动模式
codex --full-auto "add unit tests for utils.ts"
# 指定模型和 Provider
codex -m gpt-4.1 -p openai "explain this codebase"
# 使用 Ollama 本地模型
codex -p ollama -m llama3 "refactor this function"
# 全上下文模式(一次性加载整个仓库)
codex -f "restructure the project layout"
# 查看历史会话
codex --view ~/.codex/sessions/rollout-2025-04-25-abc123.json
# 生成 shell 补全脚本
codex completion bash >> ~/.bashrc3.2 配置文件示例
~/.codex/config.yaml:
yaml
model: o4-mini
provider: openai
approvalMode: suggest
disableResponseStorage: false
reasoningEffort: high
notify: false
history:
maxSize: 1000
saveHistory: true
sensitivePatterns: []
providers:
openai:
name: OpenAI
baseURL: https://api.openai.com/v1
envKey: OPENAI_API_KEY~/.codex/instructions.md(自定义指令):
markdown
You are working on a TypeScript project that uses pnpm.
Always use `pnpm` instead of `npm` for package management.
Follow the existing code style with 2-space indentation.<project>/codex.md(项目级文档,自动发现):
markdown
# Project Context
This is a React Native app using Expo.
The main entry is `app/index.tsx`.
Run tests with `pnpm test`.3.3 交互模式中的操作
| 操作 | 按键/命令 |
|---|---|
| 发送消息 | Enter |
| 换行 | Shift+Enter |
| 历史导航 | ↑ / ↓ |
| 文件补全 | Tab |
| 中断生成 | Esc × 2 (快速双击) |
| 退出 | Ctrl+C 或输入 exit / q |
| 帮助 | /help |
| 切换模型 | /model |
| 切换审批 | /approval |
| 查看差异 | /diff |
| 压缩上下文 | /compact |
| 清除会话 | /clear |
3.4 审批模式详解
当模型请求执行命令时,不同审批模式下的行为:
suggest 模式(默认):
codex fix the build error
command
$ npm run build
(y)es (n)o continue (n)o abort (e)xplain (a)lways approve
> █auto-edit 模式:文件编辑自动执行,shell 命令仍需确认。
full-auto 模式:所有操作在沙盒中自动执行,仅在失败时询问。
3.5 从源码构建
bash
# 克隆仓库
git clone https://github.com/openai/codex
cd codex/codex-cli
# 安装依赖
pnpm install
# 生产构建
pnpm run build # → dist/cli.js (minified)
# 开发构建 + 运行
pnpm run build:dev # → dist/cli-dev.js (sourcemap)
# 运行测试
pnpm test
# 类型检查
pnpm run typecheck
# 代码规范
pnpm run lint
pnpm run format四、关键函数代码解析
4.1 AgentLoop.run() — Agent 核心循环
文件:
src/utils/agent/agent-loop.ts(L436-1431, ~1000行)
这是整个 Codex CLI 最核心的函数,实现了完整的 Agent 循环。
typescript
// src/utils/agent/agent-loop.ts
public async run(
input: Array<ResponseInputItem>,
previousResponseId: string = "",
): Promise<void> {
try {
// ① 终止检查 + generation 递增(防止旧事件泄漏)
if (this.terminated) throw new Error("AgentLoop has been terminated");
const thisGeneration = ++this.generation;
this.canceled = false;
// ② 处理上次取消遗留的 pendingAborts
// 为每个未应答的 function_call 生成虚拟的 "aborted" 输出
const abortOutputs: Array<ResponseInputItem> = [];
if (this.pendingAborts.size > 0) {
for (const id of this.pendingAborts) {
abortOutputs.push({
type: "function_call_output",
call_id: id,
output: JSON.stringify({ output: "aborted", metadata: { exit_code: 1 } }),
});
}
this.pendingAborts.clear();
}
// ③ 构建本次请求的 input
// - disableResponseStorage=true: 发送完整 transcript(本地维护的会话历史)
// - disableResponseStorage=false: 仅发送增量,依赖 previous_response_id
let turnInput = this.disableResponseStorage
? [...this.transcript, ...abortOutputs].map(stripInternalFields)
: [...abortOutputs, ...input].map(stripInternalFields);
// ④ 主循环:turnInput 不为空就一直请求
while (turnInput.length > 0) {
if (this.canceled) return;
// ⑤ 发送请求到 OpenAI(支持最多 8 次重试)
const responseCall = provider === "openai"
? (params) => this.oai.responses.create(params) // Responses API
: (params) => responsesCreateViaChatCompletions(...); // Chat Completions 兼容
stream = await responseCall({
model: this.model,
instructions: mergedInstructions, // 系统提示 prefix + 用户 instructions
input: turnInput,
stream: true,
tools: [shellTool], // 唯一的工具:shell
tool_choice: "auto",
reasoning, // o3/o4-mini 支持 summary: "auto"
...(this.disableResponseStorage ? { store: false } : {
store: true, previous_response_id: lastResponseId
}),
});
// ⑥ 消费流式事件
for await (const event of stream) {
if (event.type === "response.output_item.done") {
if (item.type === "function_call") {
this.pendingAborts.add(callId); // 追踪待处理的工具调用
} else {
stageItem(item); // 3ms 延迟交付 UI
}
}
if (event.type === "response.completed") {
// ⑦ 处理工具调用 → 生成新的 turnInput
newTurnInput = await this.processEventsWithoutStreaming(
event.response.output, stageItem
);
}
}
turnInput = newTurnInput; // 有新的工具输出 → 继续循环
}
// ⑧ 循环结束,flush 暂存项
} catch (err) {
// ⑨ 错误处理:网络错误、速率限制、model_not_found 等
}
}关键设计要点:
generation 计数器:每次
run()递增,cancel()也递增。stageItem()在交付前检查 generation 是否匹配,过时事件直接丢弃。pendingAborts 机制:如果用户在 function_call 返回后、执行完成前取消了操作,call_id 会被记录。下次
run()时会发送虚拟的"aborted"输出,满足 OpenAI API 的"每个 function_call 必须有对应 output"的约定。双模式存储:
store: true+previous_response_id:服务端保留上下文,每次只发增量store: false+ 完整transcript:本地维护完整历史,每次发全量
4.2 canAutoApprove() — 安全审批评估
文件:
src/approvals.ts(L72-167)
typescript
// src/approvals.ts
export function canAutoApprove(
command: ReadonlyArray<string>,
workdir: string | undefined,
policy: ApprovalPolicy,
writableRoots: ReadonlyArray<string>,
): SafetyAssessment {
// ① apply_patch 命令特殊处理
if (command[0] === "apply_patch") {
return canAutoApproveApplyPatch(command[1], workdir, writableRoots, policy);
// suggest → ask-user (总是询问)
// auto-edit → 检查路径是否在可写范围内 → auto-approve 或 ask-user
// full-auto → auto-approve (沙盒执行)
}
// ② 检查已知安全命令白名单
const isSafe = isSafeCommand(command);
if (isSafe != null) {
return { type: "auto-approve", runInSandbox: false, ...isSafe };
// ls, cat, grep, git status 等 → 直接批准,不需要沙盒
}
// ③ bash -lc "..." 包装的命令:需要解析内部脚本
if (command[0] === "bash" && command[1] === "-lc") {
// 尝试解析为 apply_patch
const applyPatchArg = tryParseApplyPatch(command[2]);
if (applyPatchArg != null) {
return canAutoApproveApplyPatch(applyPatchArg, workdir, writableRoots, policy);
}
// 使用 shell-quote 解析复合表达式
const bashCmd = parse(command[2]);
// "ls && cat file.txt" → ['ls', {op:'&&'}, 'cat', 'file.txt']
// 检查整个 shell 表达式是否全部由安全命令组成
const shellSafe = isEntireShellExpressionSafe(bashCmd);
if (shellSafe != null) {
return { type: "auto-approve", runInSandbox: false, ...shellSafe };
}
}
// ④ 兜底:full-auto 模式在沙盒中执行,其他模式询问用户
return policy === "full-auto"
? { type: "auto-approve", runInSandbox: true, reason: "Full auto mode" }
: { type: "ask-user" };
}评估流程图:
command 输入
│
├── apply_patch? ──► canAutoApproveApplyPatch()
│ ├── suggest → ask-user
│ ├── auto-edit → 检查可写路径
│ └── full-auto → auto-approve(sandbox)
│
├── isSafeCommand()? ──► auto-approve(无沙盒)
│
├── bash -lc "..."?
│ ├── 内含 apply_patch? ──► canAutoApproveApplyPatch()
│ ├── shell-quote 解析成功?
│ │ └── isEntireShellExpressionSafe()? ──► auto-approve(无沙盒)
│ └── 解析失败?
│ ├── full-auto → auto-approve(沙盒)
│ └── 其他 → ask-user
│
└── 兜底
├── full-auto → auto-approve(沙盒)
└── 其他 → ask-user4.3 handleExecCommand() — 命令执行编排
文件:
src/utils/agent/handle-exec-command.ts(L74-189)
typescript
// src/utils/agent/handle-exec-command.ts
export async function handleExecCommand(
args: ExecInput,
config: AppConfig,
policy: ApprovalPolicy,
additionalWritableRoots: ReadonlyArray<string>,
getCommandConfirmation: (...) => Promise<CommandConfirmation>,
abortSignal?: AbortSignal,
): Promise<HandleExecCommandResult> {
const { cmd: command, workdir } = args;
const key = deriveCommandKey(command);
// deriveCommandKey(): "apply_patch" 命令 → key="apply_patch"
// "bash -lc 'npm test'" → key="npm"
// 其他 → key=程序名
// ① 会话级"总是批准"缓存 → 跳过一切检查
if (alwaysApprovedCommands.has(key)) {
return execCommand(args, undefined, false, ...).then(convertSummaryToResult);
}
// ② 安全评估
const safety = canAutoApprove(command, workdir, policy, [process.cwd()]);
switch (safety.type) {
case "ask-user": {
// 调用 UI 层的确认回调
const review = await askUserPermission(args, safety.applyPatch, getCommandConfirmation);
// review 为 null → 用户批准; 非 null → 用户拒绝
if (review != null) return review;
runInSandbox = false;
break;
}
case "auto-approve":
runInSandbox = safety.runInSandbox;
break;
case "reject":
return { outputText: "aborted", metadata: { error: "command rejected" } };
}
// ③ 执行命令
const summary = await execCommand(args, applyPatch, runInSandbox, ...);
// ④ 沙盒执行失败 + fullAutoErrorMode=ASK_USER → 非沙盒重试
if (summary.exitCode !== 0 && runInSandbox &&
config.fullAutoErrorMode === FullAutoErrorMode.ASK_USER) {
const review = await askUserPermission(...);
if (review != null) return review;
// 用户批准 → 非沙盒重试
return execCommand(args, applyPatch, false, ...).then(convertSummaryToResult);
}
return convertSummaryToResult(summary);
}4.4 raw-exec.exec() — 底层进程执行
文件:
src/utils/agent/sandbox/raw-exec.ts(L20-201)
typescript
// src/utils/agent/sandbox/raw-exec.ts
export function exec(
command: Array<string>,
options: SpawnOptions,
_writableRoots: ReadonlyArray<string>,
abortSignal?: AbortSignal,
): Promise<ExecResult> {
// ① 跨平台命令适配(如 Windows 上 ls → dir)
const adaptedCommand = adaptCommandForPlatform(command);
// ② 使用 spawn 而非 exec/execFile
// 原因:exec/execFile 对 stdin 的处理会导致 ripgrep 等工具挂起
const child = spawn(prog, adaptedCommand.slice(1), {
...options,
stdio: ["ignore", "pipe", "pipe"], // stdin=ignore 防止挂起
detached: true, // ★ 独立进程组,便于终止整棵进程树
});
// ③ abort 信号处理:先 SIGTERM,2秒后 SIGKILL
if (abortSignal) {
const abortHandler = () => {
// 发送到 -child.pid(整个进程组)
process.kill(-child.pid, "SIGTERM");
setTimeout(() => {
if (!child.killed) process.kill(-child.pid, "SIGKILL");
}, 2000).unref();
};
abortSignal.addEventListener("abort", abortHandler, { once: true });
}
// ④ 收集输出(支持截断)
return new Promise((resolve) => {
const stdoutCollector = createTruncatingCollector(child.stdout);
const stderrCollector = createTruncatingCollector(child.stderr);
child.on("exit", (code, signal) => {
// 映射退出码:code 优先,否则 128 + signal 编号
let exitCode = code ?? (signal ? 128 + os.constants.signals[signal] : 1);
resolve(addTruncationWarningsIfNecessary(
{ stdout, stderr, exitCode }, stdoutCollector.hit, stderrCollector.hit
));
});
child.on("error", (err) => {
resolve({ stdout: "", stderr: String(err), exitCode: 1 });
});
});
}关键设计:
detached: true创建独立进程组,process.kill(-pid, signal)可一次性终止包括所有子进程的进程树stdio: ["ignore", "pipe", "pipe"]解决了 ripgrep 等工具检测 stdin 是否为 TTY 时的挂起问题- 先 SIGTERM 优雅终止,2 秒超时后 SIGKILL 强制终止
4.5 responsesCreateViaChatCompletions() — 双协议适配
文件:
src/utils/responses.ts(L150-718, 核心约 400 行)
这个函数是整个多 Provider 支持的关键:将 Chat Completions API 的响应实时转换为 Responses API 的事件流格式。
typescript
// src/utils/responses.ts (简化说明)
export async function* responsesCreateViaChatCompletions(
oai: OpenAI,
params: ResponseCreateParams & { stream: true },
): AsyncIterable<ResponseEvent> {
// ① 将 Responses API 参数转换为 Chat Completions 格式
const messages = convertInputToMessages(params.input, params.instructions);
const tools = convertToolsToFunctions(params.tools);
// ② 发起 Chat Completions 流式请求
const stream = await oai.chat.completions.create({
model: params.model,
messages,
tools,
stream: true,
...
});
// ③ 实时转换事件格式
yield { type: "response.created", response: { id: responseId, ... } };
yield { type: "response.in_progress", response: { ... } };
for await (const chunk of stream) {
const delta = chunk.choices[0]?.delta;
if (delta?.content) {
// 文本内容 → response.output_text.delta
yield { type: "response.output_text.delta", delta: delta.content, ... };
}
if (delta?.tool_calls) {
// 工具调用 → response.function_call_arguments.delta
yield { type: "response.function_call_arguments.delta", delta: args, ... };
}
}
// ④ 流结束,组装完整响应
yield { type: "response.completed", response: fullResponse };
}事件映射关系:
| Chat Completions 事件 | → | Responses API 事件 |
|---|---|---|
| 流开始 | → | response.created + response.in_progress |
delta.content | → | response.output_text.delta |
delta.tool_calls | → | response.function_call_arguments.delta |
| 工具调用完成 | → | response.output_item.done (function_call) |
| 消息完成 | → | response.output_item.done (message) |
| 流结束 | → | response.completed |
4.6 TextBuffer — 多行文本编辑器核心
文件:
src/text-buffer.ts(896 行)
终端内的完整多行编辑器实现,支持 Unicode、撤销/重做、Emacs 快捷键。
typescript
// src/text-buffer.ts (关键方法)
export default class TextBuffer {
private lines: Array<string>; // 按行存储
private cursorRow = 0; // 光标行
private cursorCol = 0; // 光标列
private preferredCol: number | null; // 垂直移动时记忆的列位置
private undoStack / redoStack; // 100 步历史
private clipboard: string | null; // 剪贴板
// ★ 高级入口:接收 Ink 的 input/key 对象,映射到所有编辑操作
handleInput(input: string | undefined, key: Record<string, boolean>, vp: Viewport): boolean {
// 方向键
if (key["leftArrow"]) this.move("left");
if (key["rightArrow"]) this.move("right");
if (key["upArrow"]) this.move("up");
if (key["downArrow"]) this.move("down");
// Option+Arrow → 按词移动(兼容 macOS Terminal ESC-b/ESC-f)
if (key["meta"] && input === "b") this.move("wordLeft");
if (key["meta"] && input === "f") this.move("wordRight");
// 删除
if ((key["ctrl"] || key["meta"]) && key["backspace"]) this.deleteWordLeft();
if (key["backspace"] || input === "\x7f") this.backspace();
// Emacs/readline 快捷键
if (key["ctrl"] && input === "a") this.moveToStartOfDocument(); // Ctrl+A
if (key["ctrl"] && input === "e") this.moveToEndOfDocument(); // Ctrl+E
if (key["ctrl"] && input === "k") this.deleteToLineEnd(); // Ctrl+K (kill)
if (key["ctrl"] && input === "u") this.deleteToLineStart(); // Ctrl+U
if (key["ctrl"] && input === "w") this.deleteWordLeft(); // Ctrl+W
// 普通输入
if (input && !key["ctrl"] && !key["meta"]) this.insert(input);
this.ensureCursorInRange();
this.ensureCursorVisible(vp);
return /* true if changed */;
}
}Unicode 处理:使用 Intl.Segmenter 按字素簇(grapheme cluster)而非 UTF-16 code unit 操作,正确处理 emoji 等多码点字符:
typescript
function toCodePoints(str: string): Array<string> {
if (typeof Intl !== "undefined" && "Segmenter" in Intl) {
const seg = new Intl.Segmenter();
return [...seg.segment(str)].map(seg => seg.segment);
}
return Array.from(str); // fallback
}4.7 系统提示 prefix — Agent 角色定义
文件:
src/utils/agent/agent-loop.ts(L1462-1507)
typescript
const prefix = `You are operating as and within the Codex CLI, a terminal-based
agentic coding assistant built by OpenAI...
You can:
- Receive user prompts, project context, and files.
- Stream responses and emit function calls (e.g., shell commands, code edits).
- Apply patches, run commands, and manage user approvals based on policy.
- Work inside a sandboxed, git-backed workspace with rollback support.
You are an agent - please keep going until the user's query is completely
resolved, before ending your turn...
CODING GUIDELINES:
- Fix the problem at the root cause rather than applying surface-level patches
- Avoid unneeded complexity in your solution
- Keep changes consistent with the style of the existing codebase
- Use apply_patch to edit files: {"cmd":["apply_patch","*** Begin Patch\\n..."]}
- Once you finish coding:
- Check git status to sanity check your changes
- Remove all inline comments you added
- Try to run pre-commit if available`;这段约 45 行的系统提示定义了 Codex CLI Agent 的:
- 身份认知:终端编码助手,不是旧版 Codex 模型
- 核心能力:流式响应、工具调用、patch 应用、沙盒执行
- 行为准则:持续解决问题直到完成、不猜测答案
- 编码规范:根因修复、最小化变更、保持代码风格、检查 git status
五、技术亮点总结
| 亮点 | 实现方式 |
|---|---|
| 终端内 React UI | React + Ink 框架,在终端中渲染组件树 |
| 双协议适配 | responsesCreateViaChatCompletions() 将 Chat Completions 实时转换为 Responses 事件流 |
| generation 防泄漏 | 每次 run/cancel 递增计数器,过时事件被自动丢弃 |
| pendingAborts 机制 | 记录被取消的 function_call,下次请求补发虚拟输出 |
| 进程组管理 | detached: true + kill(-pid) 终止整棵进程树 |
| macOS 沙盒 | Seatbelt 策略文件动态生成,参数化可写目录 |
| Unicode 编辑器 | Intl.Segmenter 按字素簇操作,正确处理 emoji |
| 多级安全评估 | shell-quote 解析 + 白名单 + 路径约束 + 复合表达式分析 |
| Token 估算 | 4 字符 ≈ 1 token 启发式,无需完整 tokenizer 依赖 |
| LRU 文件缓存 | singlepass 模式下避免重复读取未变化的文件 |