Codex Handbook
core/src/tools/handlers/apply_patch.rs 647 lines
use std::collections::BTreeSet;use std::collections::HashMap;use std::path::Path;use std::path::PathBuf;use std::sync::Arc;use std::time::Duration;use std::time::Instant;use crate::apply_patch;use crate::apply_patch::InternalApplyPatchInvocation;use crate::apply_patch::convert_apply_patch_to_protocol;use crate::function_tool::FunctionCallError;use crate::session::session::Session;use crate::session::turn_context::TurnContext;use crate::session::turn_context::TurnEnvironment;use crate::tools::context::ApplyPatchToolOutput;use crate::tools::context::FunctionToolOutput;use crate::tools::context::SharedTurnDiffTracker;use crate::tools::context::ToolInvocation;use crate::tools::context::ToolPayload;use crate::tools::context::boxed_tool_output;use crate::tools::events::ToolEmitter;use crate::tools::events::ToolEventCtx;use crate::tools::handlers::apply_granted_turn_permissions;use crate::tools::handlers::apply_patch_spec::create_apply_patch_freeform_tool;use crate::tools::handlers::resolve_tool_environment;use crate::tools::handlers::updated_hook_command;use crate::tools::hook_names::HookToolName;use crate::tools::orchestrator::ToolOrchestrator;use crate::tools::registry::CoreToolRuntime;use crate::tools::registry::PostToolUsePayload;use crate::tools::registry::PreToolUsePayload;use crate::tools::registry::ToolArgumentDiffConsumer;use crate::tools::registry::ToolExecutor;use crate::tools::runtimes::apply_patch::ApplyPatchRequest;use crate::tools::runtimes::apply_patch::ApplyPatchRuntime;use crate::tools::sandboxing::ToolCtx;use codex_apply_patch::ApplyPatchAction;use codex_apply_patch::ApplyPatchFileChange;use codex_apply_patch::Hunk;use codex_apply_patch::StreamingPatchParser;use codex_exec_server::ExecutorFileSystem;use codex_features::Feature;use codex_protocol::models::AdditionalPermissionProfile;use codex_protocol::models::FileSystemPermissions;use codex_protocol::protocol::EventMsg;use codex_protocol::protocol::FileChange;use codex_protocol::protocol::PatchApplyUpdatedEvent;use codex_sandboxing::policy_transforms::effective_file_system_sandbox_policy;use codex_sandboxing::policy_transforms::merge_permission_profiles;use codex_sandboxing::policy_transforms::normalize_additional_permissions;use codex_tools::ToolName;use codex_tools::ToolSpec;use codex_utils_absolute_path::AbsolutePathBuf;use codex_utils_path_uri::PathUri;const APPLY_PATCH_ARGUMENT_DIFF_BUFFER_INTERVAL: Duration = Duration::from_millis(500);/// Handles freeform `apply_patch` requests and routes verified patches to the/// selected environment filesystem.#[derive(Default)]pub struct ApplyPatchHandler {    multi_environment: bool,}impl ApplyPatchHandler {    pub(crate) fn new(multi_environment: bool) -> Self {        Self { multi_environment }    }}#[derive(Default)]struct ApplyPatchArgumentDiffConsumer {    parser: StreamingPatchParser,    last_sent_at: Option<Instant>,    pending: Option<PatchApplyUpdatedEvent>,}impl ToolArgumentDiffConsumer for ApplyPatchArgumentDiffConsumer {    fn consume_diff(        &mut self,        turn: &TurnContext,        call_id: String,        diff: &str,    ) -> Option<EventMsg> {        if !turn.features.enabled(Feature::ApplyPatchStreamingEvents) {            return None;        }        self.push_delta(call_id, diff)            .map(EventMsg::PatchApplyUpdated)    }    fn finish(&mut self) -> Result<Option<EventMsg>, FunctionCallError> {        self.finish_update_on_complete()            .map(|event| event.map(EventMsg::PatchApplyUpdated))    }}impl ApplyPatchArgumentDiffConsumer {    fn push_delta(&mut self, call_id: String, delta: &str) -> Option<PatchApplyUpdatedEvent> {        let hunks = self.parser.push_delta(delta).ok()?;        if hunks.is_empty() {            return None;        }        let changes = convert_apply_patch_hunks_to_protocol(&hunks);        let event = PatchApplyUpdatedEvent { call_id, changes };        let now = Instant::now();        match self.last_sent_at {            Some(last_sent_at)                if now.duration_since(last_sent_at) < APPLY_PATCH_ARGUMENT_DIFF_BUFFER_INTERVAL =>            {                self.pending = Some(event);                None            }            Some(_) | None => {                self.pending = None;                self.last_sent_at = Some(now);                Some(event)            }        }    }    fn finish_update_on_complete(        &mut self,    ) -> Result<Option<PatchApplyUpdatedEvent>, FunctionCallError> {        self.parser.finish().map_err(|err| {            FunctionCallError::RespondToModel(format!("failed to parse apply_patch: {err}"))        })?;        let event = self.pending.take();        if event.is_some() {            self.last_sent_at = Some(Instant::now());        }        Ok(event)    }}fn convert_apply_patch_hunks_to_protocol(hunks: &[Hunk]) -> HashMap<PathBuf, FileChange> {    hunks        .iter()        .map(|hunk| {            let path = hunk_source_path(hunk).to_path_buf();            let change = match hunk {                Hunk::AddFile { contents, .. } => FileChange::Add {                    content: contents.clone(),                },                Hunk::DeleteFile { .. } => FileChange::Delete {                    content: String::new(),                },                Hunk::UpdateFile {                    chunks, move_path, ..                } => FileChange::Update {                    unified_diff: format_update_chunks_for_progress(chunks),                    move_path: move_path.clone(),                },            };            (path, change)        })        .collect()}fn hunk_source_path(hunk: &Hunk) -> &Path {    match hunk {        Hunk::AddFile { path, .. } | Hunk::DeleteFile { path } | Hunk::UpdateFile { path, .. } => {            path        }    }}fn format_update_chunks_for_progress(chunks: &[codex_apply_patch::UpdateFileChunk]) -> String {    let mut unified_diff = String::new();    for chunk in chunks {        match &chunk.change_context {            Some(context) => {                unified_diff.push_str("@@ ");                unified_diff.push_str(context);                unified_diff.push('\n');            }            None => {                unified_diff.push_str("@@");                unified_diff.push('\n');            }        }        for line in &chunk.old_lines {            unified_diff.push('-');            unified_diff.push_str(line);            unified_diff.push('\n');        }        for line in &chunk.new_lines {            unified_diff.push('+');            unified_diff.push_str(line);            unified_diff.push('\n');        }        if chunk.is_end_of_file {            unified_diff.push_str("*** End of File");            unified_diff.push('\n');        }    }    unified_diff}fn file_paths_for_action(action: &ApplyPatchAction) -> Vec<AbsolutePathBuf> {    let mut keys = Vec::new();    let cwd = &action.cwd;    for (path, change) in action.changes() {        if let Some(key) = to_abs_path(cwd, path) {            keys.push(key);        }        if let ApplyPatchFileChange::Update { move_path, .. } = change            && let Some(dest) = move_path            && let Some(key) = to_abs_path(cwd, dest)        {            keys.push(key);        }    }    keys}fn to_abs_path(cwd: &AbsolutePathBuf, path: &Path) -> Option<AbsolutePathBuf> {    Some(AbsolutePathBuf::resolve_path_against_base(path, cwd))}fn write_permissions_for_paths(    file_paths: &[AbsolutePathBuf],    file_system_sandbox_policy: &codex_protocol::permissions::FileSystemSandboxPolicy,    cwd: &AbsolutePathBuf,) -> Option<AdditionalPermissionProfile> {    let write_paths = file_paths        .iter()        .map(|path| {            path.parent()                .unwrap_or_else(|| path.clone())                .into_path_buf()        })        .filter(|path| {            !file_system_sandbox_policy.can_write_path_with_cwd(path.as_path(), cwd.as_path())        })        .collect::<BTreeSet<_>>()        .into_iter()        .map(AbsolutePathBuf::from_absolute_path)        .collect::<Result<Vec<_>, _>>()        .ok()?;    let permissions = (!write_paths.is_empty()).then_some(AdditionalPermissionProfile {        file_system: Some(FileSystemPermissions::from_read_write_roots(            Some(vec![]),            Some(write_paths),        )),        ..Default::default()    })?;    normalize_additional_permissions(permissions).ok()}/// Extracts the raw patch text used as the command-shaped hook input for apply_patch.fn apply_patch_payload_command(payload: &ToolPayload) -> Option<String> {    match payload {        ToolPayload::Custom { input } => Some(input.clone()),        _ => None,    }}async fn effective_patch_permissions(    session: &Session,    turn: &TurnContext,    environment_id: &str,    action: &ApplyPatchAction,    cwd: &AbsolutePathBuf,) -> (    Vec<AbsolutePathBuf>,    crate::tools::handlers::EffectiveAdditionalPermissions,    codex_protocol::permissions::FileSystemSandboxPolicy,) {    let file_paths = file_paths_for_action(action);    let granted_permissions = merge_permission_profiles(        session            .granted_session_permissions(environment_id)            .await            .as_ref(),        session            .granted_turn_permissions(environment_id)            .await            .as_ref(),    );    let base_file_system_sandbox_policy = turn.file_system_sandbox_policy();    let file_system_sandbox_policy = effective_file_system_sandbox_policy(        &base_file_system_sandbox_policy,        granted_permissions.as_ref(),    );    let effective_additional_permissions = apply_granted_turn_permissions(        session,        environment_id,        cwd.as_path(),        crate::sandboxing::SandboxPermissions::UseDefault,        write_permissions_for_paths(&file_paths, &file_system_sandbox_policy, cwd),    )    .await;    (        file_paths,        effective_additional_permissions,        file_system_sandbox_policy,    )}impl ToolExecutor<ToolInvocation> for ApplyPatchHandler {    fn tool_name(&self) -> ToolName {        ToolName::plain("apply_patch")    }    fn spec(&self) -> ToolSpec {        create_apply_patch_freeform_tool(self.multi_environment)    }    fn handle(&self, invocation: ToolInvocation) -> codex_tools::ToolExecutorFuture<'_> {        Box::pin(self.handle_call(invocation))    }}impl ApplyPatchHandler {    async fn handle_call(        &self,        invocation: ToolInvocation,    ) -> Result<Box<dyn crate::tools::context::ToolOutput>, FunctionCallError> {        let ToolInvocation {            session,            turn,            tracker,            call_id,            tool_name,            payload,            ..        } = invocation;        let ToolPayload::Custom { input: patch_input } = payload else {            return Err(FunctionCallError::RespondToModel(                "apply_patch handler received unsupported payload".to_string(),            ));        };        let args = match codex_apply_patch::parse_patch(&patch_input) {            Ok(args) => args,            Err(parse_error) => {                return Err(FunctionCallError::RespondToModel(format!(                    "apply_patch verification failed: {parse_error}"                )));            }        };        let selected_environment_id =            require_environment_id(args.environment_id.as_deref(), self.multi_environment)?;        // Verify the parsed patch against the selected environment filesystem.        let Some(turn_environment) =            resolve_tool_environment(turn.as_ref(), selected_environment_id.as_deref())?        else {            return Err(FunctionCallError::RespondToModel(                "apply_patch is unavailable in this session".to_string(),            ));        };        // TODO(anp): Migrate apply-patch verification and permission accounting to PathUri so        // patches can target environment-native foreign paths without host projection.        let cwd = turn_environment.cwd().to_abs_path().map_err(|err| {            FunctionCallError::RespondToModel(format!(                "apply_patch cwd `{}` is not native to the Codex host: {err}",                turn_environment.cwd()            ))        })?;        let fs = turn_environment.environment.get_filesystem();        let sandbox = turn.file_system_sandbox_context(            /*additional_permissions*/ None,            turn_environment.cwd(),        );        match codex_apply_patch::verify_apply_patch_args(args, &cwd, fs.as_ref(), Some(&sandbox))            .await        {            codex_apply_patch::MaybeApplyPatchVerified::Body(changes) => {                let (file_paths, effective_additional_permissions, file_system_sandbox_policy) =                    effective_patch_permissions(                        session.as_ref(),                        turn.as_ref(),                        &turn_environment.environment_id,                        &changes,                        &cwd,                    )                    .await;                match apply_patch::apply_patch(turn.as_ref(), &file_system_sandbox_policy, changes)                    .await                {                    InternalApplyPatchInvocation::Output(item) => {                        let content = item?;                        Ok(boxed_tool_output(ApplyPatchToolOutput::from_text(content)))                    }                    InternalApplyPatchInvocation::DelegateToRuntime(apply) => {                        let changes = convert_apply_patch_to_protocol(&apply.action);                        let emitter = ToolEmitter::apply_patch_for_environment(                            changes.clone(),                            apply.auto_approved,                            turn_environment.environment_id.clone(),                        );                        let event_ctx = ToolEventCtx::new(                            session.as_ref(),                            turn.as_ref(),                            &call_id,                            Some(&tracker),                        );                        emitter.begin(event_ctx).await;                        let req = ApplyPatchRequest {                            turn_environment: turn_environment.clone(),                            action: apply.action,                            file_paths,                            changes,                            exec_approval_requirement: apply.exec_approval_requirement,                            additional_permissions: effective_additional_permissions                                .additional_permissions,                            permissions_preapproved: effective_additional_permissions                                .permissions_preapproved,                        };                        let mut orchestrator = ToolOrchestrator::new();                        let mut runtime = ApplyPatchRuntime::new();                        let tool_ctx = ToolCtx {                            session: session.clone(),                            turn: turn.clone(),                            call_id: call_id.clone(),                            tool_name: tool_name.clone(),                        };                        let out = orchestrator                            .run(                                &mut runtime,                                &req,                                &tool_ctx,                                turn.as_ref(),                                turn.approval_policy.value(),                            )                            .await                            .map(|result| result.output);                        let (out, delta) = match out {                            Ok(output) => (Ok(output.exec_output), Some(output.delta)),                            Err(error) => (Err(error), Some(runtime.committed_delta().clone())),                        };                        let event_ctx = ToolEventCtx::new(                            session.as_ref(),                            turn.as_ref(),                            &call_id,                            Some(&tracker),                        );                        let content = emitter.finish(event_ctx, out, delta.as_ref()).await?;                        Ok(boxed_tool_output(ApplyPatchToolOutput::from_text(content)))                    }                }            }            codex_apply_patch::MaybeApplyPatchVerified::CorrectnessError(parse_error) => {                Err(FunctionCallError::RespondToModel(format!(                    "apply_patch verification failed: {parse_error}"                )))            }            codex_apply_patch::MaybeApplyPatchVerified::ShellParseError(error) => {                tracing::trace!("Failed to parse apply_patch input, {error:?}");                Err(FunctionCallError::RespondToModel(                    "apply_patch handler received invalid patch input".to_string(),                ))            }            codex_apply_patch::MaybeApplyPatchVerified::NotApplyPatch => {                Err(FunctionCallError::RespondToModel(                    "apply_patch handler received non-apply_patch input".to_string(),                ))            }        }    }}impl CoreToolRuntime for ApplyPatchHandler {    fn matches_kind(&self, payload: &ToolPayload) -> bool {        matches!(payload, ToolPayload::Custom { .. })    }    fn create_diff_consumer(&self) -> Option<Box<dyn ToolArgumentDiffConsumer>> {        Some(Box::<ApplyPatchArgumentDiffConsumer>::default())    }    fn pre_tool_use_payload(&self, invocation: &ToolInvocation) -> Option<PreToolUsePayload> {        apply_patch_payload_command(&invocation.payload).map(|command| PreToolUsePayload {            tool_name: HookToolName::apply_patch(),            tool_input: serde_json::json!({ "command": command }),        })    }    fn with_updated_hook_input(        &self,        mut invocation: ToolInvocation,        updated_input: serde_json::Value,    ) -> Result<ToolInvocation, FunctionCallError> {        let patch = updated_hook_command(&updated_input)?;        invocation.payload = match invocation.payload {            ToolPayload::Custom { .. } => ToolPayload::Custom {                input: patch.to_string(),            },            payload => payload,        };        Ok(invocation)    }    fn post_tool_use_payload(        &self,        invocation: &ToolInvocation,        result: &dyn crate::tools::context::ToolOutput,    ) -> Option<PostToolUsePayload> {        let tool_response =            result.post_tool_use_response(&invocation.call_id, &invocation.payload)?;        Some(PostToolUsePayload {            tool_name: HookToolName::apply_patch(),            tool_use_id: invocation.call_id.clone(),            tool_input: serde_json::json!({                "command": apply_patch_payload_command(&invocation.payload)?,            }),            tool_response,        })    }}#[allow(clippy::too_many_arguments)]pub(crate) async fn intercept_apply_patch(    command: &[String],    cwd: &AbsolutePathBuf,    fs: &dyn ExecutorFileSystem,    turn_environment: TurnEnvironment,    session: Arc<Session>,    turn: Arc<TurnContext>,    tracker: Option<&SharedTurnDiffTracker>,    call_id: &str,    tool_name: &str,) -> Result<Option<FunctionToolOutput>, FunctionCallError> {    let sandbox_cwd = PathUri::from_abs_path(cwd);    let sandbox =        turn.file_system_sandbox_context(/*additional_permissions*/ None, &sandbox_cwd);    match codex_apply_patch::maybe_parse_apply_patch_verified(command, cwd, fs, Some(&sandbox))        .await    {        codex_apply_patch::MaybeApplyPatchVerified::Body(changes) => {            let (approval_keys, effective_additional_permissions, file_system_sandbox_policy) =                effective_patch_permissions(                    session.as_ref(),                    turn.as_ref(),                    &turn_environment.environment_id,                    &changes,                    cwd,                )                .await;            match apply_patch::apply_patch(turn.as_ref(), &file_system_sandbox_policy, changes)                .await            {                InternalApplyPatchInvocation::Output(item) => {                    let content = item?;                    Ok(Some(FunctionToolOutput::from_text(content, Some(true))))                }                InternalApplyPatchInvocation::DelegateToRuntime(apply) => {                    let changes = convert_apply_patch_to_protocol(&apply.action);                    let emitter = ToolEmitter::apply_patch_for_environment(                        changes.clone(),                        apply.auto_approved,                        turn_environment.environment_id.clone(),                    );                    let event_ctx = ToolEventCtx::new(                        session.as_ref(),                        turn.as_ref(),                        call_id,                        tracker.as_ref().copied(),                    );                    emitter.begin(event_ctx).await;                    let req = ApplyPatchRequest {                        turn_environment,                        action: apply.action,                        file_paths: approval_keys,                        changes,                        exec_approval_requirement: apply.exec_approval_requirement,                        additional_permissions: effective_additional_permissions                            .additional_permissions,                        permissions_preapproved: effective_additional_permissions                            .permissions_preapproved,                    };                    let mut orchestrator = ToolOrchestrator::new();                    let mut runtime = ApplyPatchRuntime::new();                    let tool_ctx = ToolCtx {                        session: session.clone(),                        turn: turn.clone(),                        call_id: call_id.to_string(),                        tool_name: ToolName::plain(tool_name),                    };                    let out = orchestrator                        .run(                            &mut runtime,                            &req,                            &tool_ctx,                            turn.as_ref(),                            turn.approval_policy.value(),                        )                        .await                        .map(|result| result.output);                    let (out, delta) = match out {                        Ok(output) => (Ok(output.exec_output), Some(output.delta)),                        Err(error) => (Err(error), Some(runtime.committed_delta().clone())),                    };                    let event_ctx = ToolEventCtx::new(                        session.as_ref(),                        turn.as_ref(),                        call_id,                        tracker.as_ref().copied(),                    );                    let content = emitter.finish(event_ctx, out, delta.as_ref()).await?;                    Ok(Some(FunctionToolOutput::from_text(content, Some(true))))                }            }        }        codex_apply_patch::MaybeApplyPatchVerified::CorrectnessError(parse_error) => {            Err(FunctionCallError::RespondToModel(format!(                "apply_patch verification failed: {parse_error}"            )))        }        codex_apply_patch::MaybeApplyPatchVerified::ShellParseError(error) => {            tracing::trace!("Failed to parse apply_patch input, {error:?}");            Ok(None)        }        codex_apply_patch::MaybeApplyPatchVerified::NotApplyPatch => Ok(None),    }}fn require_environment_id(    parsed_environment_id: Option<&str>,    allow_environment_id: bool,) -> Result<Option<String>, FunctionCallError> {    match parsed_environment_id {        Some(_) if !allow_environment_id => Err(FunctionCallError::RespondToModel(            "apply_patch environment selection is unavailable for this turn".to_string(),        )),        Some(environment_id) => Ok(Some(environment_id.to_string())),        None => Ok(None),    }}#[cfg(test)]#[path = "apply_patch_tests.rs"]mod tests;