apply_patch 協議
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 側的安全門和協議轉換層。
職責
- 解析補丁文字:
parse_patch把字串拆成Hunk列舉(AddFile / DeleteFile / UpdateFile),UpdateFile 內含UpdateFileChunk列表,每個 chunk 有 change_context + old_lines + new_lines (codex-rs/apply-patch/src/parser.rs:130-137)。 - 識別呼叫形態:
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)。 - 校驗路徑與 cwd:
try_verify_apply_patch_args把每個 hunk 的相對路徑用effective_cwd解析成PathUri,workdir 來自 heredoc 前綴的cd <path> &&(codex-rs/apply-patch/src/invocation.rs:180-200)。 - 應用到檔案系統:
apply_patch/apply_hunks透過ExecutorFileSystemtrait 在沙箱裡執行 add/delete/update,產出AppliedPatchDelta(codex-rs/apply-patch/src/lib.rs:276-347)。 - 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-109 — Hunk 列舉,三類 hunk 的資料結構,resolve_path 用 cwd 解析。codex-rs/apply-patch/src/parser.rs:178-200 — parse_patch_text 主體,支援 Strict / Lenient 兩種模式。codex-rs/apply-patch/src/streaming_parser.rs:22-46 — StreamingPatchParser,逐行狀態機解析,適合大補丁。codex-rs/apply-patch/src/invocation.rs:36-52 — MaybeApplyPatch / ExtractHeredocError,識別 shell 包裹的補丁。codex-rs/apply-patch/src/invocation.rs:141-166 — maybe_parse_apply_patch_verified,先拒絕裸補丁,再走 verify。codex-rs/apply-patch/src/lib.rs:276-312 — apply_patch,把補丁文字落到檔案系統。codex-rs/apply-patch/src/lib.rs:361-389 — apply_hunks_to_files,逐 hunk 執行,try_write! 巨集在寫失敗時把 delta 標為非 exact。codex-rs/apply-patch/src/lib.rs:675-711 — derive_new_contents_from_chunks,讀原檔案、算 replacement、生成新內容。codex-rs/core/src/apply_patch.rs:34-75 — core 側 apply_patch,做安全分類後決定執行路徑。三類 hunk 的結構很直接,UpdateFile 額外帶 move_path 做 rename:
// 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,讓模型知道為什麼沒改:
// 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 需要知道原狀:
// 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));
}
}
};
}資料流
邊界與失敗
- Lenient 模式預設開啟:
PARSE_IN_STRICT_MODE = false,因為 gpt-4.1 會把補丁包在<<'EOF' ... EOFheredoc 裡直接當 argv 傳,strict 模式會全部拒掉 (codex-rs/apply-patch/src/parser.rs:47-53)。 - 裸補丁拒絕執行:argv 只有一個元素且能解析成補丁時,回傳
ImplicitInvocation錯誤,提示 rerun 成["apply_patch", "<patch>"],防止把補丁當 shell 命令誤執行 (codex-rs/apply-patch/src/invocation.rs:147-158)。 - UpdateFile 空 chunks 報錯:
StreamingPatchParser::ensure_update_hunk_is_not_empty檢查 update hunk 必須有內容,空 hunk 直接報InvalidHunkError(codex-rs/apply-patch/src/streaming_parser.rs:53-82)。 - 寫失敗但 delta 保留:ENOSPC 等錯誤發生時,檔案可能已被 truncate 但寫入未完成,
delta.exact = false標記改動不確定,供後續 rollback 或審計參考 (codex-rs/apply-patch/src/lib.rs:375-388)。 - DeleteFile 區分目錄:刪除前調
ensure_not_directory確認目標是檔案不是目錄,避免誤刪目錄 (codex-rs/apply-patch/src/lib.rs:423-430)。
小結
apply_patch 是 codex 改檔案的標準協議,parser 負責把文字拆成 hunks,core 側負責安全分類,exec-server 負責實際寫入。整條鏈路裡最容易出問題的是路徑解析和 heredoc 識別——模型生成的 shell wrapper 形態各異,Lenient 模式就是為此存在。要看補丁執行時怎麼過沙箱,接 沙箱與命令隔離;要看 tool call 怎麼被分發到 apply_patch,接 工具呼叫與 function_tool。