apply_patch プロトコル
apply_patch は codex がファイルを書き換えるために使うカスタムテキストパッチ形式だ。モデルが tool call の中で *** Begin Patch ... *** End Patch テキストを生成し、クライアントが hunk にパースして exec-server のファイルシステムに落とす。unified diff より緩い:add/delete/update の三つの hunk をサポートし、update は @@ context + +/-/ 行で置換を表し、*** Move to: で rename もできる。全体のロジックは apply-patch/ crate にあり、core/src/apply_patch.rs は core 側の安全ゲート兼プロトコル変換層だ。
責務
- 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)。 - 呼び出し形態の識別:
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 は「新規ファイル作成」「ファイル削除」を同一 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-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。行ごとの状態機械でパースし、大きな patch 向け。codex-rs/apply-patch/src/invocation.rs:36-52 — MaybeApplyPatch / ExtractHeredocError。shell で包まれた patch を識別。codex-rs/apply-patch/src/invocation.rs:141-166 — maybe_parse_apply_patch_verified。まず裸 patch を拒否し、その後 verify に進む。codex-rs/apply-patch/src/lib.rs:276-312 — apply_patch。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 を総合して判断し、patch を「自動許可」「ユーザに問う」「直接拒否」の三つに分ける。拒否時は理由を 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 が patch を<<'EOF' ... EOFheredoc で包んで直接 argv として渡すため、strict モードだと全て拒否される (codex-rs/apply-patch/src/parser.rs:47-53)。 - 裸 patch は実行拒否:argv が一要素だけでかつ patch としてパース可能な場合、
ImplicitInvocationエラーを返し、["apply_patch", "<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 がテキストを hunk に分割し、core 側が安全分類を行い、exec-server が実際の書き込みを担う。リンク全体で最も問題が起きやすいのはパス解析と heredoc 識別だ——モデルが生成する shell wrapper は形が様々で、Lenient モードはそのために存在する。patch 実行がどうサンドボックスを通るかは サンドボックスとコマンド隔離 へ、tool call がどう apply_patch にディスパッチされるかは ツール呼び出しと function_tool へ。