Skip to content

学习日志

进度总览

  • 初始化学习站点
  • 完成架构概览主题
  • 完成代理循环主题
  • 完成沙箱系统主题
  • 完成终端界面主题
  • 完成非交互模式主题
  • 完成 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 包含平台特定失败模式:MissingLinuxSandboxExecutableWsl1UnsupportedForBubblewrapSeatbeltUnavailable

困惑与突破

  • 困惑:Linux 为什么需要 Landlock + Bwrap 两层?单独一层不够吗?
  • 突破:Landlock 只做文件访问控制(无 namespace 隔离),Bwrap 只做 namespace 隔离(无细粒度文件规则)——两者互补。理解这种"纵深防御"设计后,沙箱架构豁然开朗

Day 4 — 终端界面

日期:2025-06-08 状态:✅ 已完成

今日摘要

  • 基于 Ratatui 的即时模式渲染(immediate-mode):每帧完全重绘,App 结构体是中央状态机,持有 ChatWidgetBacktrackStateAppServerSessionModelCatalogRuntimeKeymap 等组件
  • 多源事件分发:终端事件(crossterm)、代理事件(rx_event)、定时器事件统一通过 event_dispatch 路由到 inputthread_eventsapproval_eventsapp_server_requests
  • 流式输出渲染:增量式——仅追加新 token,不做全量重绘;渐进式 Markdown 解析处理流式传输中的不完整代码块和链接
  • Diff 渲染diff_render 模块渲染代码变更,绿色 + 前缀表示添加、红色 - 表示删除,集成到 ChatWidgetapply_patch 工具执行时
  • TUI 包含 70+ 子模块,组织为:chat 核心、渲染、交互、会话、审批、搜索、主题七大类

困惑与突破

  • 困惑:即时模式意味着每次变化都触发全帧重建——性能不会是问题吗?
  • 突破:Ratatui 通过差分终端更新(differential terminal updates)优化:虽然逻辑上是全量重绘,但实际只发送变化的 cell 到终端——理解了"逻辑全量、物理增量"的设计哲学

Day 5 — 非交互模式

日期:2025-06-09 状态:✅ 已完成

今日摘要

  • Exec 模式是 TUI 的非交互对应物:共享核心层,但单轮执行后退出;所有交互请求(审批、用户输入、MCP elicitation)自动拒绝,确保自动化安全
  • Ephemeral 执行:临时环境,任务完成后清理,无状态持久化(除非显式 --resume
  • 双输出格式:JSON Lines 模式(--experimental-json)输出结构化事件类型(TurnStartedEventAgentMessageItemCommandExecutionItemFileChangeItem 等);或默认的人类可读彩色文本
  • 自动化安全模型:通过 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::SharedAsyncManagedClient 中的用途是什么?为什么启动需要共享?
  • 突破:多个调用者可能同时请求同一个 MCP 服务器的连接——.clone().await 同一个 Shared<Future> 让所有人等待同一个启动过程,而非各自发起重复连接。这是一种高级的异步去重模式

Day 7 — 配置系统

日期:2025-06-11 状态:✅ 已完成

今日摘要

  • config.toml 是主配置文件,通过 config crate 管理,包含 [model][sandbox][mcp_servers][hooks][skills][profiles] 等段落
  • 五层配置合并优先级:CLI 覆盖 > Cloud 配置(CloudConfigBundle)> 项目 config.toml > 全局 config.toml(~/.config/codex/)> 硬编码默认值,每个值通过 ConfigLayerSource 追踪来源
  • JSON Schema 验证Constrained<T> 包装配置值,访问时自动检查约束;ConfigErrorConfigLoadErrorConstraintResult(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 端点
  • 序列化作用域控制并发和资源访问:GlobalGlobalSharedReadThread { thread_id }ThreadPathCommandExecProcessFuzzyFileSearchSessionFsWatchMcpOauth
  • 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 保持一致性
  • 错误层级与重试:CodexErrorJsonRpcError(多种子类型)+ TransportClosedErrorretry_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] 检测路由:codexcli_main()codex-tuitui::run_main()codex-execexec::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 → RemovedFeatures::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 设计原则