Skip to content

apply_patch 协议

源码版本rust-v0.145.0

apply_patch 是 codex 用来改文件的自定义文本补丁格式,模型在 tool call 里生成一段 *** Begin Patch ... *** End Patch 文本,客户端解析成 hunks,落到 exec-server 的文件系统上。它比 unified diff 更宽松:支持 add/delete/update 三类 hunk,update 用 @@ context + +/-/ 行表示替换,还能用 *** Move to: 做 rename。整套逻辑在 apply-patch/ crate 里,core/src/apply_patch.rs 是 core 侧的安全门和协议转换层。

职责

  1. 解析 patch 文本:parse_patch 把字符串拆成 Hunk 枚举(AddFile / DeleteFile / UpdateFile),UpdateFile 内含 UpdateFileChunk 列表,每个 chunk 有 change_context + old_lines + new_lines (codex-rs/apply-patch/src/parser.rs:130-137)。
  2. 识别调用形态:maybe_parse_apply_patch 区分两种 argv——直接 apply_patch <body> 或 shell heredoc 形式 bash -c "apply_patch <<'EOF' ... EOF",后者要用 tree-sitter bash 解析提取 heredoc (codex-rs/apply-patch/src/invocation.rs:112-137)。
  3. 校验路径与 cwd:try_verify_apply_patch_args 把每个 hunk 的相对路径用 effective_cwd 解析成 PathUri,workdir 来自 heredoc 前缀的 cd <path> && (codex-rs/apply-patch/src/invocation.rs:180-200)。
  4. 应用到文件系统:apply_patch / apply_hunks 通过 ExecutorFileSystem trait 在沙箱里执行 add/delete/update,产出 AppliedPatchDelta (codex-rs/apply-patch/src/lib.rs:276-347)。
  5. core 侧做安全分类:core/src/apply_patch.rs::apply_patch 把 action 交给 assess_patch_safety,分 AutoApprove / AskUser / Reject 三类,再决定是直接执行还是走 runtime approval (codex-rs/core/src/apply_patch.rs:34-75)。

设计动机

为什么不直接用 unified diff?两个原因。第一,unified diff 的 hunk header(@@ -a,b +c,d @@)要求模型精确算行号,模型不擅长。apply_patch 的 update 用 @@ context 标记位置,允许 seek_sequence 在原文里搜上下文行定位,容忍行号偏移。第二,unified diff 不原生支持"新建文件"和"删除文件"作为同一 patch 的一部分,而 apply_patch*** Add File: / *** Delete File: 显式区分。

为什么还要 core/src/apply_patch.rs 这一层?因为 apply-patch crate 是纯文件操作,不知道 sandbox policy、approval、exec_approval_requirement 这些 core 概念。core 这一层负责把一个 ApplyPatchAction 翻译成 InternalApplyPatchInvocation:要么 Output(直接返回结果,用户已显式批准)、要么 DelegateToRuntime(交给 orchestrator 走沙箱或 approval 流程)。

ImplicitInvocation 错误是个有意思的设计:如果模型直接把 patch 文本当成 argv[0] 传进来(没包 apply_patch 命令),maybe_parse_apply_patch_verified 不会默默执行,而是返回 ImplicitInvocation 错误,提示"必须用 ["apply_patch", "<patch>"] 形式"。这避免了把 patch 内容误当成普通 shell 命令执行的安全风险。

关键文件

codex-rs/apply-patch/src/parser.rs:64-109Hunk 枚举,三类 hunk 的数据结构,resolve_path 用 cwd 解析。codex-rs/apply-patch/src/parser.rs:178-200parse_patch_text 主体,支持 Strict / Lenient 两种模式。codex-rs/apply-patch/src/streaming_parser.rs:22-46StreamingPatchParser,逐行状态机解析,适合大 patch。codex-rs/apply-patch/src/invocation.rs:36-52MaybeApplyPatch / ExtractHeredocError,识别 shell 包裹的 patch。codex-rs/apply-patch/src/invocation.rs:141-166maybe_parse_apply_patch_verified,先拒绝裸 patch,再走 verify。codex-rs/apply-patch/src/lib.rs:276-312apply_patch,把 patch 文本落到文件系统。codex-rs/apply-patch/src/lib.rs:361-389apply_hunks_to_files,逐 hunk 执行,try_write! 宏在写失败时把 delta 标为非 exact。codex-rs/apply-patch/src/lib.rs:675-711derive_new_contents_from_chunks,读原文件、算 replacement、生成新内容。codex-rs/core/src/apply_patch.rs:34-75 — core 侧 apply_patch,做安全分类后决定执行路径。

三类 hunk 的结构很直接,UpdateFile 额外带 move_path 做 rename:

rust
// parser.rs:64-82 — 三类 hunk,UpdateFile 支持多 chunk + rename
pub enum Hunk {
    AddFile {
        path: PathBuf,
        contents: String,
    },
    DeleteFile {
        path: PathBuf,
    },
    UpdateFile {
        path: PathBuf,
        move_path: Option<PathBuf>,
        chunks: Vec<UpdateFileChunk>,
    },
}

core 侧的安全判定是关键分支点。assess_patch_safety 根据 approval policy、permission_profile、sandbox policy 综合判断,把 patch 分成"自动放行"、"问用户"、"直接拒"三类。拒绝时直接把 reason 包进 FunctionCallError::RespondToModel,让模型知道为什么没改:

rust
// core/src/apply_patch.rs:39-74 — 三路分流,Output 表示不走 runtime
match assess_patch_safety(&action, ...) {
    SafetyCheck::AutoApprove { user_explicitly_approved, .. } => {
        InternalApplyPatchInvocation::DelegateToRuntime(ApplyPatchRuntimeInvocation {
            action,
            auto_approved: !user_explicitly_approved,
            exec_approval_requirement: ExecApprovalRequirement::Skip { .. },
        })
    }
    SafetyCheck::AskUser => InternalApplyPatchInvocation::DelegateToRuntime(..),
    SafetyCheck::Reject { reason } => InternalApplyPatchInvocation::Output(Err(
        FunctionCallError::RespondToModel(format!("patch rejected: {reason}")),
    )),
}

文件写入失败时,try_write! 宏会把 delta.exact 标为 false——意思是"我们记录了改动,但可能不完整"。比如 ENOSPC 时文件可能已经被 truncate 但没写完,这时 delta 还要保留,因为 rollback 需要知道原状:

rust
// lib.rs:378-388 — 写失败时 delta 立即失真
macro_rules! try_write {
    ($result:expr) => {
        match $result {
            Ok(value) => value,
            Err(error) => {
                delta.exact = false;
                return Err(anyhow::Error::from(error));
            }
        }
    };
}

数据流

边界与失败

小结

apply_patch 是 codex 改文件的标准协议,parser 负责把文本拆成 hunks,core 侧负责安全分类,exec-server 负责实际写入。整条链路里最容易出问题的是路径解析和 heredoc 识别——模型生成的 shell wrapper 形态各异,Lenient 模式就是为此存在。要看 patch 执行时怎么过沙箱,接 沙箱与命令隔离;要看 tool call 怎么被分发到 apply_patch,接 工具调用与 function_tool