Protocolo apply_patch
apply_patch es el formato de parche de texto personalizado que codex usa para modificar archivos. El modelo genera en el tool call un fragmento de texto *** Begin Patch ... *** End Patch, y el cliente lo parsea en hunks que se aplican al sistema de archivos del exec-server. Es más permisivo que el unified diff: soporta tres tipos de hunk — add/delete/update —; update usa @@ context + líneas +/-/ para expresar el reemplazo, y además permite *** Move to: para renombrar. Toda esta lógica está en el crate apply-patch/; core/src/apply_patch.rs es el guardián de seguridad y la capa de conversión de protocolo del lado core.
Responsabilidades
- Parsear el texto del patch:
parse_patchparte la cadena en el enumHunk(AddFile / DeleteFile / UpdateFile); UpdateFile contiene una lista deUpdateFileChunk, cada chunk con change_context + old_lines + new_lines (codex-rs/apply-patch/src/parser.rs:130-137). - Identificar la forma de invocación:
maybe_parse_apply_patchdistingue dos argv —apply_patch <body>directo, o shell heredocbash -c "apply_patch <<'EOF' ... EOF". Este último necesita tree-sitter bash para extraer el heredoc (codex-rs/apply-patch/src/invocation.rs:112-137). - Validar rutas y cwd:
try_verify_apply_patch_argsresuelve la ruta relativa de cada hunk contraeffective_cwdpara obtener unPathUri; el workdir se saca del prefijocd <path> &&del heredoc (codex-rs/apply-patch/src/invocation.rs:180-200). - Aplicar al sistema de archivos:
apply_patch/apply_hunksejecutan add/delete/update dentro del sandbox vía el traitExecutorFileSystem, produciendo unAppliedPatchDelta(codex-rs/apply-patch/src/lib.rs:276-347). - Clasificación de seguridad en core:
core/src/apply_patch.rs::apply_patchpasa el action aassess_patch_safety, que lo reparte entreAutoApprove/AskUser/Reject, y en función de eso decide ejecutar directo o ir por runtime approval (codex-rs/core/src/apply_patch.rs:34-75).
Motivación de diseño
¿Por qué no usar unified diff directamente? Dos razones. Primera, el hunk header del unified diff (@@ -a,b +c,d @@) exige que el modelo calcule números de línea exactos, y los modelos no son buenos en eso. La update de apply_patch marca la posición con @@ context y permite que seek_sequence localice buscando líneas de contexto en el original, tolerando desplazamientos de línea. Segunda, el unified diff no soporta de forma nativa «crear archivo» y «borrar archivo» dentro de un mismo patch; apply_patch los distingue con *** Add File: / *** Delete File:.
¿Por qué hace falta otra capa en core/src/apply_patch.rs? Porque el crate apply-patch es pura operación de archivos y no conoce conceptos de core como sandbox policy, approval o exec_approval_requirement. La capa de core traduce un ApplyPatchAction en un InternalApplyPatchInvocation: o bien Output (devuelve el resultado directamente, cuando el usuario aprobó explícitamente), o bien DelegateToRuntime (cede al orchestrator para que vaya al sandbox o al flujo de approval).
El error ImplicitInvocation es un diseño interesante: si el modelo pasa el texto del patch directamente como argv[0] (sin envolverlo en el comando apply_patch), maybe_parse_apply_patch_verified no lo ejecuta silenciosamente, sino que devuelve un error ImplicitInvocation sugiriendo «debe usarse la forma ["apply_patch", "<patch>"]». Así se evita el riesgo de seguridad de ejecutar el contenido del patch por error como un comando shell cualquiera.
Archivos clave
codex-rs/apply-patch/src/parser.rs:64-109 — enum Hunk, estructura de datos de los tres tipos de hunk; resolve_path resuelve contra cwd.codex-rs/apply-patch/src/parser.rs:178-200 — cuerpo de parse_patch_text, soporta Strict / Lenient.codex-rs/apply-patch/src/streaming_parser.rs:22-46 — StreamingPatchParser, máquina de estados línea por línea, adecuada para patches grandes.codex-rs/apply-patch/src/invocation.rs:36-52 — MaybeApplyPatch / ExtractHeredocError, identifica el patch envuelto por shell.codex-rs/apply-patch/src/invocation.rs:141-166 — maybe_parse_apply_patch_verified: primero rechaza el patch desnudo, luego hace verify.codex-rs/apply-patch/src/lib.rs:276-312 — apply_patch, vuelca el texto del patch al sistema de archivos.codex-rs/apply-patch/src/lib.rs:361-389 — apply_hunks_to_files, ejecuta hunk a hunk; el macro try_write! marca el delta como no exact si la escritura falla.codex-rs/apply-patch/src/lib.rs:675-711 — derive_new_contents_from_chunks, lee el archivo original, calcula el replacement, genera el contenido nuevo.codex-rs/core/src/apply_patch.rs:34-75 — apply_patch del lado core, tras la clasificación de seguridad decide la ruta de ejecución.La estructura de los tres hunks es directa; UpdateFile además lleva move_path para renombrar:
// parser.rs:64-82 — tres tipos de hunk, UpdateFile soporta multi-chunk + rename
pub enum Hunk {
AddFile {
path: PathBuf,
contents: String,
},
DeleteFile {
path: PathBuf,
},
UpdateFile {
path: PathBuf,
move_path: Option<PathBuf>,
chunks: Vec<UpdateFileChunk>,
},
}La decisión de seguridad del lado core es el punto de bifurcación clave. assess_patch_safety combina approval policy, permission_profile y sandbox policy, y reparte el patch entre «aprobar automáticamente», «preguntar al usuario» y «rechazar directamente». Al rechazar, envuelve la razón en FunctionCallError::RespondToModel para que el modelo sepa por qué no se modificó:
// core/src/apply_patch.rs:39-74 — tres rutas; Output significa no pasar por 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}")),
)),
}Si la escritura falla, el macro try_write! marca delta.exact = false, que significa «registramos el cambio, pero podría estar incompleto». Por ejemplo, con ENOSPC el archivo podría haberse truncado sin terminar de escribir; el delta se conserva porque el rollback necesita saber el estado original:
// lib.rs:378-388 — al fallar la escritura, el delta se marca como no fiable de inmediato
macro_rules! try_write {
($result:expr) => {
match $result {
Ok(value) => value,
Err(error) => {
delta.exact = false;
return Err(anyhow::Error::from(error));
}
}
};
}Flujo de datos
Bordes y fallos
- Modo Lenient activado por defecto:
PARSE_IN_STRICT_MODE = false, porque gpt-4.1 envuelve el patch en un heredoc<<'EOF' ... EOFy lo pasa directamente como argv; el modo strict lo rechazaría todo (codex-rs/apply-patch/src/parser.rs:47-53). - Patch desnudo rechazado: si argv tiene un solo elemento y parsea como patch, se devuelve error
ImplicitInvocationsugiriendo rerun como["apply_patch", "<patch>"], para evitar ejecutar el patch por error como comando shell (codex-rs/apply-patch/src/invocation.rs:147-158). - UpdateFile con chunks vacíos da error:
StreamingPatchParser::ensure_update_hunk_is_not_emptycomprueba que un update hunk tenga contenido; un hunk vacío sueltaInvalidHunkErrordirecto (codex-rs/apply-patch/src/streaming_parser.rs:53-82). - Escritura fallida pero delta conservado: ante errores como ENOSPC, el archivo puede haberse truncado pero la escritura no haber terminado;
delta.exact = falsemarca el cambio como dudoso, como referencia para rollback o auditoría (codex-rs/apply-patch/src/lib.rs:375-388). - DeleteFile distingue directorio: antes de borrar, llama a
ensure_not_directorypara confirmar que el destino es archivo y no directorio, evitando borrar un directorio por error (codex-rs/apply-patch/src/lib.rs:423-430).
Resumen
apply_patch es el protocolo estándar de codex para modificar archivos: el parser descompone el texto en hunks, el lado core hace la clasificación de seguridad, y el exec-server hace la escritura real. En toda la cadena, lo más propenso a fallos es la resolución de rutas y la identificación del heredoc — el modelo genera wrappers shell de formas muy variadas, y el modo Lenient existe precisamente para eso. Para ver cómo pasa el patch por el sandbox al ejecutarse, sigue por Sandbox y aislamiento de comandos; para ver cómo se dispatcha el tool call a apply_patch, sigue por Llamada a herramientas y function_tool.