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. 解析補丁文字: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 不原生支援「新建檔案」和「刪除檔案」作為同一補丁的一部分,而 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 錯誤是個有意思的設計:如果模型直接把補丁文字當成 argv[0] 傳進來(沒包 apply_patch 命令),maybe_parse_apply_patch_verified 不會默默執行,而是回傳 ImplicitInvocation 錯誤,提示「必須用 ["apply_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,逐行狀態機解析,適合大補丁。codex-rs/apply-patch/src/invocation.rs:36-52MaybeApplyPatch / ExtractHeredocError,識別 shell 包裹的補丁。codex-rs/apply-patch/src/invocation.rs:141-166maybe_parse_apply_patch_verified,先拒絕裸補丁,再走 verify。codex-rs/apply-patch/src/lib.rs:276-312apply_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 綜合判斷,把補丁分成「自動放行」、「問使用者」、「直接拒」三類。拒絕時直接把 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 模式就是為此存在。要看補丁執行時怎麼過沙箱,接 沙箱與命令隔離;要看 tool call 怎麼被分發到 apply_patch,接 工具呼叫與 function_tool