Skip to content

Protocolo apply_patch

源码版本rust-v0.145.0

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

  1. Parsear el texto del patch: parse_patch parte la cadena en el enum Hunk (AddFile / DeleteFile / UpdateFile); UpdateFile contiene una lista de UpdateFileChunk, cada chunk con change_context + old_lines + new_lines (codex-rs/apply-patch/src/parser.rs:130-137).
  2. Identificar la forma de invocación: maybe_parse_apply_patch distingue dos argv — apply_patch <body> directo, o shell heredoc bash -c "apply_patch <<'EOF' ... EOF". Este último necesita tree-sitter bash para extraer el heredoc (codex-rs/apply-patch/src/invocation.rs:112-137).
  3. Validar rutas y cwd: try_verify_apply_patch_args resuelve la ruta relativa de cada hunk contra effective_cwd para obtener un PathUri; el workdir se saca del prefijo cd <path> && del heredoc (codex-rs/apply-patch/src/invocation.rs:180-200).
  4. Aplicar al sistema de archivos: apply_patch / apply_hunks ejecutan add/delete/update dentro del sandbox vía el trait ExecutorFileSystem, produciendo un AppliedPatchDelta (codex-rs/apply-patch/src/lib.rs:276-347).
  5. Clasificación de seguridad en core: core/src/apply_patch.rs::apply_patch pasa el action a assess_patch_safety, que lo reparte entre AutoApprove / 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-46StreamingPatchParser, máquina de estados línea por línea, adecuada para patches grandes.codex-rs/apply-patch/src/invocation.rs:36-52MaybeApplyPatch / ExtractHeredocError, identifica el patch envuelto por shell.codex-rs/apply-patch/src/invocation.rs:141-166maybe_parse_apply_patch_verified: primero rechaza el patch desnudo, luego hace verify.codex-rs/apply-patch/src/lib.rs:276-312apply_patch, vuelca el texto del patch al sistema de archivos.codex-rs/apply-patch/src/lib.rs:361-389apply_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-711derive_new_contents_from_chunks, lee el archivo original, calcula el replacement, genera el contenido nuevo.codex-rs/core/src/apply_patch.rs:34-75apply_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:

rust
// 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ó:

rust
// 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:

rust
// 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' ... EOF y 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 ImplicitInvocation sugiriendo 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_empty comprueba que un update hunk tenga contenido; un hunk vacío suelta InvalidHunkError directo (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 = false marca 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_directory para 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.