Protocole apply_patch
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
- Parser le texte du patch :
parse_patchdécoupe la chaîne en énumHunk(AddFile / DeleteFile / UpdateFile) ; UpdateFile contient une liste deUpdateFileChunk, chaque chunk a change_context + old_lines + new_lines (codex-rs/apply-patch/src/parser.rs:130-137). - Identifier la forme d'appel :
maybe_parse_apply_patchdistingue deux argv —apply_patch <body>direct ou la forme shell heredocbash -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). - Valider chemin et cwd :
try_verify_apply_patch_argsrésout chaque chemin relatif de hunk enPathUriviaeffective_cwd, le workdir venant du préfixecd <path> &&avant le heredoc (codex-rs/apply-patch/src/invocation.rs:180-200). - Appliquer au système de fichiers :
apply_patch/apply_hunksexécute add/delete/update via le traitExecutorFileSystemdans le bac à sable (sandbox), produisant unAppliedPatchDelta(codex-rs/apply-patch/src/lib.rs:276-347). - Classification de sécurité côté core :
core/src/apply_patch.rs::apply_patchconfie l'action àassess_patch_safety, qui la classeAutoApprove/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-200 — parse_patch_text, le corps, supporte Strict / Lenient.codex-rs/apply-patch/src/streaming_parser.rs:22-46 — StreamingPatchParser, automate par ligne, adapté aux gros patch.codex-rs/apply-patch/src/invocation.rs:36-52 — MaybeApplyPatch / ExtractHeredocError, identifie un patch enrobé par shell.codex-rs/apply-patch/src/invocation.rs:141-166 — maybe_parse_apply_patch_verified, refuse d'abord le patch nu, puis passe à verify.codex-rs/apply-patch/src/lib.rs:276-312 — apply_patch, écrit le patch sur le système de fichiers.codex-rs/apply-patch/src/lib.rs:361-389 — apply_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-711 — derive_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-75 — apply_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 :
// 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é :
// 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 :
// 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' ... EOFet 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
ImplicitInvocationen 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_emptyvérifie qu'un update hunk a du contenu, un hunk vide renvoieInvalidHunkError(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 = falsesignale 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_directoryvé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.