Appearance
学习日志
进度总览
- 初始化学习站点
- 完成架构概览主题
- 完成代理循环主题
- 完成沙箱系统主题
- 完成终端界面主题
- 完成非交互模式主题
- 完成 MCP 协议主题
- 完成配置系统主题
- 完成应用服务器协议主题
- 完成 SDK 与 IDE 集成主题
- 完成 CLI 调度器主题
Day 1 — 架构概览
日期:2025-06-05 状态:✅ 已完成
今日摘要
- Codex 采用 Cargo workspace 组织 119 个 crate,分为 Interface(cli、tui、exec)、Protocol(app-server-protocol、app-server)、Core(core、protocol、config)、Sandbox(sandboxing、linux-sandbox、bwrap)、Tools(codex-mcp、apply-patch)五大层
- 核心结构
Codex本质是一个 队列对(queue pair):tx_sub: Sender<Submission>提交操作,rx_event: Receiver<Event>接收事件,实现完全异步的事件驱动架构 - 所有界面(TUI、Exec、SDK)均通过 App Server Protocol(JSON-RPC) 与 agent runtime 通信,确保行为一致
- 沙箱层通过
SandboxManager::transform()根据编译时#[cfg(target_os)]自动选择 Seatbelt(macOS)、Landlock+Bwrap(Linux)或 Restricted Token(Windows) - CLI 入口
main.rs约 3773 行,采用 arg0 dispatch 模式:同一二进制文件通过argv[0]和符号链接伪装为多个工具
困惑与突破
- 困惑:
Codex(队列对)、Session(Arc 内部状态机)、CodexThread(外部包装)三者的关系最初很混乱——为什么需要三层抽象? - 突破:理解到
Codex是底层的消息通道,Session持有实际状态(对话历史、上下文),CodexThread是面向外部的高层 API——这种分层让事件循环、状态管理、外部调用各司其职
Day 2 — 代理循环
日期:2025-06-06 状态:✅ 已完成
今日摘要
- Agent Loop 状态机流转:Idle → ContextBuilding → Streaming → ToolCall → Executing → ApprovalWait → Streaming(循环)→ Completed → Idle,由
core/src/session/mod.rs(3404 行)驱动 - 上下文管理:
build_initial_context()组装 system prompt、对话历史(TurnItem列表)、工具声明和工作区文件内容;超出上下文窗口时compact_conversation_history()用 LLM 生成摘要替代早期轮次 - Apply Patch:使用自定义
*** Begin Patch格式(非标准 unified diff),因为 LLM 生成更可靠;StreamingPatchParser支持流式解析不完整输出 - Rollout 执行策略:控制迭代上限(
max_turns)、执行策略(ExecPolicy的 Allow/Prompt/Forbidden 决策),并通过RolloutRecorder记录完整执行轨迹 CodexThread提供submit()、next_event()、steer_input()等外部 API,支持每轮覆盖设置(cwd、model、approval_policy 等)
困惑与突破
- 困惑:
steer_input()允许在轮次执行中途中断——这如何与状态机配合?不会导致状态不一致吗? - 突破:
steer_input()本质是向事件循环注入一个新的用户消息,状态机收到后会中断当前 streaming,将新输入与已有上下文合并后重新提交给 LLM——这赋予了代理"边做边调整"的能力
Day 3 — 沙箱系统
日期:2025-06-07 状态:✅ 已完成
今日摘要
SandboxManager是无状态抽象,提供select_initial(preference)和transform(request)两个核心方法,通过#[cfg(target_os)]实现平台分发- Linux 双层隔离:Layer 1 = Landlock LSM(文件访问规则:工作区 RW、系统 RO、
~/.ssh等敏感路径 DENY),Layer 2 = Bubblewrap(PID/mount/network namespace 隔离),codex-linux-sandbox是独立辅助二进制 - macOS Seatbelt:动态生成 plist 规则,允许工作区写入、屏蔽敏感路径,通过
sandbox-exec执行,子进程继承沙箱限制 - ExecPolicy 是独立于沙箱的策略层:
Policy::check()通过PrefixRule(命令前缀匹配)和NetworkRule(域名匹配)产生Decision::Allow/Prompt/Forbidden SandboxTransformError包含平台特定失败模式:MissingLinuxSandboxExecutable、Wsl1UnsupportedForBubblewrap、SeatbeltUnavailable
困惑与突破
- 困惑:Linux 为什么需要 Landlock + Bwrap 两层?单独一层不够吗?
- 突破:Landlock 只做文件访问控制(无 namespace 隔离),Bwrap 只做 namespace 隔离(无细粒度文件规则)——两者互补。理解这种"纵深防御"设计后,沙箱架构豁然开朗
Day 4 — 终端界面
日期:2025-06-08 状态:✅ 已完成
今日摘要
- 基于 Ratatui 的即时模式渲染(immediate-mode):每帧完全重绘,
App结构体是中央状态机,持有ChatWidget、BacktrackState、AppServerSession、ModelCatalog、RuntimeKeymap等组件 - 多源事件分发:终端事件(crossterm)、代理事件(
rx_event)、定时器事件统一通过event_dispatch路由到input、thread_events、approval_events、app_server_requests - 流式输出渲染:增量式——仅追加新 token,不做全量重绘;渐进式 Markdown 解析处理流式传输中的不完整代码块和链接
- Diff 渲染:
diff_render模块渲染代码变更,绿色+前缀表示添加、红色-表示删除,集成到ChatWidget中apply_patch工具执行时 - TUI 包含 70+ 子模块,组织为:chat 核心、渲染、交互、会话、审批、搜索、主题七大类
困惑与突破
- 困惑:即时模式意味着每次变化都触发全帧重建——性能不会是问题吗?
- 突破:Ratatui 通过差分终端更新(differential terminal updates)优化:虽然逻辑上是全量重绘,但实际只发送变化的 cell 到终端——理解了"逻辑全量、物理增量"的设计哲学
Day 5 — 非交互模式
日期:2025-06-09 状态:✅ 已完成
今日摘要
- Exec 模式是 TUI 的非交互对应物:共享核心层,但单轮执行后退出;所有交互请求(审批、用户输入、MCP elicitation)自动拒绝,确保自动化安全
- Ephemeral 执行:临时环境,任务完成后清理,无状态持久化(除非显式
--resume) - 双输出格式:JSON Lines 模式(
--experimental-json)输出结构化事件类型(TurnStartedEvent、AgentMessageItem、CommandExecutionItem、FileChangeItem等);或默认的人类可读彩色文本 - 自动化安全模型:通过 ExecPolicy 预定义允许的命令(
Decision::Allow),使用更严格的沙箱配置,支持output_schema_path约束输出结构 EventProcessor将内部Event变体映射为ThreadEvent输出类型,执行过滤和转换
困惑与突破
- 困惑:所有
ServerRequest都被自动拒绝——即使是看起来无害的请求?这不就限制了代理能力吗? - 突破:这正是设计意图——无人值守模式下,任何需要人类判断的请求都不应该被自动批准。通过 ExecPolicy 预授权和沙箱策略,在启动前就划定安全边界,而非运行时临时决定
Day 6 — MCP 协议
日期:2025-06-10 状态:✅ 已完成
今日摘要
- MCP(Model Context Protocol) 是 AI 模型与外部工具/数据源交互的标准化协议,
McpConnectionManager管理与多个 MCP 服务器的连接,支持三种传输:InProcess(Tokio DuplexStream)、Stdio(子进程)、StreamableHttp(HTTP SSE + POST) - 工具注册与发现:从
config.toml的[mcp_servers]加载配置,建立连接后获取工具列表,tool_is_model_visible()过滤,tool_plugin_provenance追踪来源 - MCP 能力超越工具:Resources(可读数据)、Prompts(预定义模板注入上下文)、Tools(可调用函数)
- Elicitation:MCP 服务器可通过
ElicitationRequestManager请求用户交互(表单),有 5 级优先链(auto_deny → auto-approve → policy deny → reviewer → user event) - 交互暂停机制:
active_time_timeout使用watch::Receiver<bool>在用户交互期间暂停超时计时,防止工具调用因等待用户输入而超时
困惑与突破
- 困惑:
futures::future::Shared在AsyncManagedClient中的用途是什么?为什么启动需要共享? - 突破:多个调用者可能同时请求同一个 MCP 服务器的连接——
.clone().await同一个Shared<Future>让所有人等待同一个启动过程,而非各自发起重复连接。这是一种高级的异步去重模式
Day 7 — 配置系统
日期:2025-06-11 状态:✅ 已完成
今日摘要
- config.toml 是主配置文件,通过
configcrate 管理,包含[model]、[sandbox]、[mcp_servers]、[hooks]、[skills]、[profiles]等段落 - 五层配置合并优先级:CLI 覆盖 > Cloud 配置(
CloudConfigBundle)> 项目 config.toml > 全局 config.toml(~/.config/codex/)> 硬编码默认值,每个值通过ConfigLayerSource追踪来源 - JSON Schema 验证:
Constrained<T>包装配置值,访问时自动检查约束;ConfigError、ConfigLoadError、ConstraintResult(Valid/Invalid/Warning)形成错误层级 - 云配置层叠:
CloudConfigBundleLoader从云端获取组织级和团队级配置,多层可合并 - 指纹变更检测:
MtimeConfigReloader监控文件修改时间,通过fingerprint检测运行时配置变更并触发重载
困惑与突破
- 困惑:五层优先级理解起来不难,但实际调试时如何知道某个值来自哪一层?
- 突破:每个配置值都附带
ConfigLayerSource标记——通过诊断工具或日志可以追踪到具体来源。Diagnostics提供丰富的错误消息,帮助理解合并过程中的冲突和覆盖
Day 8 — 应用服务器协议
日期:2025-06-12 状态:✅ 已完成
今日摘要
- App Server Protocol 基于 JSON-RPC 2.0:所有界面(TUI、Exec、SDK)与 agent runtime 的唯一通信协议,宏生成 60+
ClientRequest变体 - 三种传输模式:Stdio(嵌入式进程内)、Unix Socket(本地守护进程)、TCP/WebSocket(远程服务器),
Transport管理连接,MessageProcessor路由 JSON-RPC 消息 - Backend Client 双层设计:
ModelClient(会话级,整个对话)创建ModelClientSession(轮次级,单次调用),优先 WebSocket(增量 delta),SSE 降级,使用 Responses API/responses端点 - 序列化作用域控制并发和资源访问:
Global、GlobalSharedRead、Thread { thread_id }、ThreadPath、CommandExecProcess、FuzzyFileSearchSession、FsWatch、McpOauth - In-process 模式绕过所有序列化实现零开销——直接函数调用而非 JSON-RPC 帧封装
困惑与突破
- 困惑:
ClientRequestSerializationScope为什么既是序列化标记又是并发控制? - 突破:作用域实际上控制的是"哪些内部锁/mutex 可以同时持有"——不同作用域的请求可以并行执行,相同作用域的请求串行化。这种设计避免了死锁的同时保持了高并发,是精巧的分层锁策略
Day 9 — SDK 与 IDE 集成
日期:2025-06-13 状态:✅ 已完成
今日摘要
- 两种 SDK 架构截然不同:TypeScript SDK 使用
codex exec --experimental-json子进程(每轮一个进程,fire-and-forget,stdin 立即关闭);Python SDK 使用codex app-server --listen stdio://(持久进程,双向 JSON-RPC,stdin 保持打开) - TypeScript SDK 的
Thread包装异步生成器,每轮生成器 spawn 子进程,事件生命周期:thread.started → turn.started → [item.started → item.updated → item.completed]* → turn.completed - Python SDK 的
MessageRouter解决"单一 stdout 流、多消费者"问题:per-turn 通知队列 + pending deque 缓冲队列注册前到达的事件(防竞态) - IDE 集成:TypeScript SDK 用于 Node.js 环境(VS Code),Python SDK 用于 Python 环境(JetBrains),两者都通过 App Server Protocol 保持一致性
- 错误层级与重试:
CodexError→JsonRpcError(多种子类型)+TransportClosedError;retry_on_overload()使用指数退避 + 抖动,仅针对ServerBusyError
困惑与突破
- 困惑:为什么 TS 和 Python SDK 的架构差异这么大?不能统一吗?
- 突破:TypeScript SDK 的 fire-and-forget 设计适合 Node.js 的异步模型和 IDE 插件的沙箱需求——每轮独立进程确保隔离;Python SDK 的持久连接适合需要多轮交互和复杂状态管理的场景。统一架构反而会增加各自的复杂度
Day 10 — CLI 调度器
日期:2025-06-14 状态:✅ 已完成
今日摘要
- arg0 dispatch 机制:单一二进制文件通过
argv[0]检测路由:codex→cli_main()、codex-tui→tui::run_main()、codex-exec→exec::run_main()、codex-linux-sandbox→ 沙箱、apply_patch→ 补丁工具,符号链接创建不同入口点 - MultitoolCli 基于 clap,包含 25+ 子命令、
CliConfigOverrides(-c key=value)、FeatureToggles(--enable/--disable),无子命令时默认进入交互 TUI 模式 - Feature Flag 系统:60+ 特性,生命周期阶段
UnderDevelopment → Experimental → Stable → Deprecated → Removed,Features::from_sources()从默认值 → 基础配置 → profile → CLI 覆盖 → 依赖归一化构建有效集合 - 配置覆盖合并:
fold_config_overrides()组合--enable/--disable和-c key=value;TOML 解析技巧:将值包装为_x_ = {value}解析以处理裸字符串 - PATH 环境准备:
prepare_path_env_var_with_aliases()在~/.codex/tmp/arg0/创建指向当前可执行文件的符号链接,主线程运行在 16 MB 栈 + 自定义 tokio runtime 上
困惑与突破
- 困惑:
Removed特性为什么保留为 no-op 而不直接删除?这不增加维护负担吗? - 突破:这是刻意的前向兼容设计——用户配置文件可能引用已移除的特性,如果直接删除会导致解析失败。保留为 no-op 让旧配置静默降级,避免破坏性变更。理解了"永远不破坏用户配置"这一 API 设计原则