Skip to content

apply_patch Protocol

源码版本rust-v0.145.0

apply_patch is the custom text patch format codex uses to edit files. The model generates a *** Begin Patch ... *** End Patch text in a tool call; the client parses it into hunks and lands them on the exec-server's file system. It is more lenient than unified diff: it supports three hunk types (add / delete / update), update uses @@ context + +/-/ lines for replacement, and *** Move to: enables rename. The whole logic lives in the apply-patch/ crate; core/src/apply_patch.rs is the core-side safety gate and protocol conversion layer.

Responsibilities

  1. Parse patch text: parse_patch splits the string into a Hunk enum (AddFile / DeleteFile / UpdateFile); UpdateFile contains a list of UpdateFileChunk, each with change_context + old_lines + new_lines (codex-rs/apply-patch/src/parser.rs:130-137).
  2. Recognize invocation forms: maybe_parse_apply_patch distinguishes two argv forms — direct apply_patch <body> or shell heredoc bash -c "apply_patch <<'EOF' ... EOF"; the latter requires tree-sitter bash to extract the heredoc (codex-rs/apply-patch/src/invocation.rs:112-137).
  3. Validate paths and cwd: try_verify_apply_patch_args resolves each hunk's relative path with effective_cwd into a PathUri; the workdir comes from the cd <path> && prefix of the heredoc (codex-rs/apply-patch/src/invocation.rs:180-200).
  4. Apply to the file system: apply_patch / apply_hunks runs add / delete / update through the ExecutorFileSystem trait in the sandbox, producing an AppliedPatchDelta (codex-rs/apply-patch/src/lib.rs:276-347).
  5. Core-side safety classification: core/src/apply_patch.rs::apply_patch passes the action to assess_patch_safety, which classifies into AutoApprove / AskUser / Reject, then decides whether to execute directly or go through runtime approval (codex-rs/core/src/apply_patch.rs:34-75).

Design motivation

Why not just use unified diff? Two reasons. First, unified diff's hunk header (@@ -a,b +c,d @@) requires the model to compute line numbers precisely, which models are bad at. apply_patch's update uses @@ context to mark position and lets seek_sequence search the original for the context line, tolerating line-number drift. Second, unified diff does not natively support "create file" and "delete file" as part of the same patch, while apply_patch uses *** Add File: / *** Delete File: to distinguish explicitly.

Why have a core/src/apply_patch.rs layer? Because the apply-patch crate is pure file operation and does not know about sandbox policy, approval, or exec_approval_requirement — core concepts. The core layer translates an ApplyPatchAction into an InternalApplyPatchInvocation: either Output (return the result directly, the user has explicitly approved) or DelegateToRuntime (hand to the orchestrator to go through the sandbox or approval flow).

The ImplicitInvocation error is an interesting design: if the model passes the patch text directly as argv[0] (without wrapping it in the apply_patch command), maybe_parse_apply_patch_verified does not silently execute; it returns an ImplicitInvocation error hinting "must use the ["apply_patch", "<patch>"] form". This avoids the security risk of treating patch content as a regular shell command.

Key files

codex-rs/apply-patch/src/parser.rs:64-109 — the Hunk enum, the data structures for the three hunk types; resolve_path resolves with cwd.codex-rs/apply-patch/src/parser.rs:178-200parse_patch_text main body, supporting Strict / Lenient modes.codex-rs/apply-patch/src/streaming_parser.rs:22-46StreamingPatchParser, a line-by-line state machine for large patches.codex-rs/apply-patch/src/invocation.rs:36-52MaybeApplyPatch / ExtractHeredocError, recognizing shell-wrapped patches.codex-rs/apply-patch/src/invocation.rs:141-166maybe_parse_apply_patch_verified, first rejecting bare patches, then verifying.codex-rs/apply-patch/src/lib.rs:276-312apply_patch, landing the patch text onto the file system.codex-rs/apply-patch/src/lib.rs:361-389apply_hunks_to_files, running per-hunk; the try_write! macro marks delta as non-exact on write failure.codex-rs/apply-patch/src/lib.rs:675-711derive_new_contents_from_chunks, reading the original file, computing replacement, generating new content.codex-rs/core/src/apply_patch.rs:34-75 — core-side apply_patch, doing safety classification then deciding the execution path.

The three hunk structures are straightforward; UpdateFile additionally carries move_path for 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>,
    },
}

The core-side safety check is the key branch point. assess_patch_safety combines approval policy, permission_profile, and sandbox policy to classify the patch into "auto-approve", "ask user", or "reject". On reject it wraps the reason into FunctionCallError::RespondToModel so the model knows why nothing was changed:

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}")),
    )),
}

On file-write failure the try_write! macro marks delta.exact as false — meaning "we recorded the change, but it may be incomplete". For example on ENOSPC the file may already have been truncated but not fully written; the delta is still kept because rollback needs to know the original state:

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));
            }
        }
    };
}

Data flow

Boundaries and failures

  • Lenient mode is default-on: PARSE_IN_STRICT_MODE = false, because gpt-4.1 wraps patches in <<'EOF' ... EOF heredocs and passes them directly as argv; strict mode would reject all of them (codex-rs/apply-patch/src/parser.rs:47-53).
  • Bare patches refused: when argv has exactly one element and it parses as a patch, an ImplicitInvocation error is returned, hinting to rerun as ["apply_patch", "<patch>"], preventing patches from being misrun as shell commands (codex-rs/apply-patch/src/invocation.rs:147-158).
  • Empty UpdateFile chunks error: StreamingPatchParser::ensure_update_hunk_is_not_empty checks that update hunks have content; an empty hunk raises InvalidHunkError (codex-rs/apply-patch/src/streaming_parser.rs:53-82).
  • Write failure still keeps delta: on ENOSPC and similar errors, the file may already be truncated but writing is incomplete; delta.exact = false marks the change as uncertain, for later rollback or audit reference (codex-rs/apply-patch/src/lib.rs:375-388).
  • DeleteFile distinguishes directories: before deleting, ensure_not_directory confirms the target is a file not a directory, avoiding accidental directory deletion (codex-rs/apply-patch/src/lib.rs:423-430).

Summary

apply_patch is codex's standard protocol for editing files: the parser splits text into hunks, the core side does safety classification, and exec-server does the actual writing. The parts most prone to problems along this chain are path resolution and heredoc recognition — models generate shell wrappers in various forms, and Lenient mode exists for this reason. To see how patches go through the sandbox at execution time, continue with Sandbox and command isolation; to see how tool calls get dispatched to apply_patch, continue with tool calls and function_tool.