Skip to content

Protocole apply_patch

源码版本rust-v0.145.0

apply_patch est le format de patch textuel ad hoc que codex utilise pour modifier les fichiers. Le modèle génère, dans un tool call, un bloc de texte *** Begin Patch ... *** End Patch ; le client le parse en hunks et l'applique sur le système de fichiers de l'exec-server. Il est plus permissif que le unified diff : il supporte trois types de hunk — add/delete/update — l'update utilise @@ context + lignes +/-/ pour exprimer le remplacement, et *** Move to: fait le rename. Toute la logique est dans le crate apply-patch/ ; core/src/apply_patch.rs est la couche de sécurité et de conversion de protocole côté core.

Responsabilités

  1. Parser le texte du patch : parse_patch découpe la chaîne en énum Hunk (AddFile / DeleteFile / UpdateFile) ; UpdateFile contient une liste de UpdateFileChunk, chaque chunk a change_context + old_lines + new_lines (codex-rs/apply-patch/src/parser.rs:130-137).
  2. Identifier la forme d'appel : maybe_parse_apply_patch distingue deux argv — apply_patch <body> direct ou la forme shell heredoc bash -c "apply_patch <<'EOF' ... EOF", cette dernière exigeant tree-sitter bash pour extraire le heredoc (codex-rs/apply-patch/src/invocation.rs:112-137).
  3. Valider chemin et cwd : try_verify_apply_patch_args résout chaque chemin relatif de hunk en PathUri via effective_cwd, le workdir venant du préfixe cd <path> && avant le heredoc (codex-rs/apply-patch/src/invocation.rs:180-200).
  4. Appliquer au système de fichiers : apply_patch / apply_hunks exécute add/delete/update via le trait ExecutorFileSystem dans le bac à sable (sandbox), produisant un AppliedPatchDelta (codex-rs/apply-patch/src/lib.rs:276-347).
  5. Classification de sécurité côté core : core/src/apply_patch.rs::apply_patch confie l'action à assess_patch_safety, qui la classe AutoApprove / AskUser / Reject, puis décide d'exécuter directement ou de passer par le runtime approval (codex-rs/core/src/apply_patch.rs:34-75).

Motivations de conception

Pourquoi ne pas utiliser directement unified diff ? Deux raisons. Premièrement, le hunk header du unified diff (@@ -a,b +c,d @@) exige que le modèle calcule exactement les numéros de ligne, ce qu'il fait mal. L'update de apply_patch utilise @@ context pour marquer l'emplacement et permet à seek_sequence de chercher les lignes de contexte dans l'original, tolérant les décalages de numéros. Deuxièmement, le unified diff ne supporte pas nativement « créer un fichier » et « supprimer un fichier » dans un même patch, alors que apply_patch les distingue explicitement avec *** Add File: / *** Delete File:.

Pourquoi une couche core/src/apply_patch.rs en plus ? Parce que le crate apply-patch ne fait que des opérations fichier, il ne connaît pas les concepts core sandbox policy, approval, exec_approval_requirement. Cette couche core traduit un ApplyPatchAction en InternalApplyPatchInvocation : soit Output (renvoyer directement le résultat, l'utilisateur ayant approuvé explicitement), soit DelegateToRuntime (confier à l'orchestrator pour passer par le bac à sable ou le flux approval).

L'erreur ImplicitInvocation est un design intéressant : si le modèle passe directement le texte du patch comme argv[0] (sans l'enrober dans la commande apply_patch), maybe_parse_apply_patch_verified ne l'exécute pas silencieusement ; il retourne ImplicitInvocation pour suggérer « il faut utiliser la forme ["apply_patch", "<patch>"] ». Cela évite le risque de sécurité d'exécuter par erreur le contenu du patch comme une commande shell.

Fichiers clés

codex-rs/apply-patch/src/parser.rs:64-109 — l'énum Hunk, structures des trois types de hunk ; resolve_path résout via cwd.codex-rs/apply-patch/src/parser.rs:178-200parse_patch_text, le corps, supporte Strict / Lenient.codex-rs/apply-patch/src/streaming_parser.rs:22-46StreamingPatchParser, automate par ligne, adapté aux gros patch.codex-rs/apply-patch/src/invocation.rs:36-52MaybeApplyPatch / ExtractHeredocError, identifie un patch enrobé par shell.codex-rs/apply-patch/src/invocation.rs:141-166maybe_parse_apply_patch_verified, refuse d'abord le patch nu, puis passe à verify.codex-rs/apply-patch/src/lib.rs:276-312apply_patch, écrit le patch sur le système de fichiers.codex-rs/apply-patch/src/lib.rs:361-389apply_hunks_to_files, exécute hunk par hunk ; la macro try_write! marque le delta comme non exact en cas d'échec d'écriture.codex-rs/apply-patch/src/lib.rs:675-711derive_new_contents_from_chunks, lit le fichier original, calcule le replacement, génère le nouveau contenu.codex-rs/core/src/apply_patch.rs:34-75apply_patch côté core, classification de sécurité puis choix du chemin d'exécution.

La structure des trois types de hunk est directe ; UpdateFile porte en plus move_path pour le 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>,
    },
}

La décision de sécurité côté core est un branchement clé. assess_patch_safety combine approval policy, permission_profile et sandbox policy, et classe le patch en trois catégories — « auto approuvé », « demander l'utilisateur », « refuser ». En cas de refus, elle emballe la reason dans FunctionCallError::RespondToModel, pour que le modèle sache pourquoi rien n'a été modifié :

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

En cas d'échec d'écriture, la macro try_write! marque delta.exact = false — « on a enregistré la modification, mais elle est peut-être incomplète ». Par exemple en ENOSPC, le fichier a pu être truncate sans écriture complète ; le delta est quand même conservé, car le rollback a besoin de l'état original :

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

Flux de données

Limites et échecs

  • Mode Lenient activé par défaut : PARSE_IN_STRICT_MODE = false, parce que gpt-4.1 enrobe le patch dans un heredoc <<'EOF' ... EOF et le passe directement comme argv ; le mode strict refuserait tout (codex-rs/apply-patch/src/parser.rs:47-53).
  • Patch nu refusé : si argv n'a qu'un seul élément et qu'il se parse en patch, on retourne ImplicitInvocation en suggérant de rerun sous la forme ["apply_patch", "<patch>"], pour éviter d'exécuter le patch comme une commande shell par erreur (codex-rs/apply-patch/src/invocation.rs:147-158).
  • UpdateFile vide en erreur : StreamingPatchParser::ensure_update_hunk_is_not_empty vérifie qu'un update hunk a du contenu, un hunk vide renvoie InvalidHunkError (codex-rs/apply-patch/src/streaming_parser.rs:53-82).
  • Échec d'écriture mais delta conservé : sur ENOSPC etc., le fichier a pu être truncate sans écriture complète ; delta.exact = false signale la modification incertaine, pour rollback ou audit (codex-rs/apply-patch/src/lib.rs:375-388).
  • DeleteFile distingue fichier et répertoire : avant suppression, ensure_not_directory vérifie que la cible est un fichier et non un répertoire, pour éviter la suppression accidentelle d'un répertoire (codex-rs/apply-patch/src/lib.rs:423-430).

Récapitulatif

apply_patch est le protocole standard de codex pour modifier des fichiers : le parser découpe le texte en hunks, le côté core fait la classification de sécurité, et l'exec-server fait l'écriture effective. Dans toute la chaîne, les points de friction sont la résolution de chemin et l'identification du heredoc — les wrappers shell générés par le modèle sont très variés, et le mode Lenient est là pour ça. Pour voir comment le patch traverse le bac à sable à l'exécution, enchaînez sur Bac à sable et isolation de commande ; pour voir comment le tool call est dispatché vers apply_patch, voir Appel d'outils et function_tool.