use crate::codex::Session; use crate::codex::TurnContext; use crate::function_tool::FunctionCallError; use crate::protocol::FileChange; use crate::protocol::ReviewDecision; use crate::safety::SafetyCheck; use crate::safety::assess_patch_safety; use codex_apply_patch::ApplyPatchAction; use codex_apply_patch::ApplyPatchFileChange; use std::collections::HashMap; use std::path::PathBuf; pub const CODEX_APPLY_PATCH_ARG1: &str = "--codex-run-as-apply-patch"; pub(crate) enum InternalApplyPatchInvocation { /// The `apply_patch` call was handled programmatically, without any sort /// of sandbox, because the user explicitly approved it. This is the /// result to use with the `shell` function call that contained `apply_patch`. Output(Result), /// The `apply_patch` call was approved, either automatically because it /// appears that it should be allowed based on the user's sandbox policy /// *or* because the user explicitly approved it. In either case, we use /// exec with [`CODEX_APPLY_PATCH_ARG1`] to realize the `apply_patch` call, /// but [`ApplyPatchExec::auto_approved`] is used to determine the sandbox /// used with the `exec()`. DelegateToExec(ApplyPatchExec), } #[derive(Debug)] pub(crate) struct ApplyPatchExec { pub(crate) action: ApplyPatchAction, pub(crate) user_explicitly_approved_this_action: bool, } pub(crate) async fn apply_patch( sess: &Session, turn_context: &TurnContext, sub_id: &str, call_id: &str, action: ApplyPatchAction, ) -> InternalApplyPatchInvocation { match assess_patch_safety( &action, turn_context.approval_policy, &turn_context.sandbox_policy, &turn_context.cwd, ) { SafetyCheck::AutoApprove { user_explicitly_approved, .. } => InternalApplyPatchInvocation::DelegateToExec(ApplyPatchExec { action, user_explicitly_approved_this_action: user_explicitly_approved, }), SafetyCheck::AskUser => { // Compute a readable summary of path changes to include in the // approval request so the user can make an informed decision. // // Note that it might be worth expanding this approval request to // give the user the option to expand the set of writable roots so // that similar patches can be auto-approved in the future during // this session. let rx_approve = sess .request_patch_approval(sub_id.to_owned(), call_id.to_owned(), &action, None, None) .await; match rx_approve.await.unwrap_or_default() { ReviewDecision::Approved | ReviewDecision::ApprovedForSession => { InternalApplyPatchInvocation::DelegateToExec(ApplyPatchExec { action, user_explicitly_approved_this_action: true, }) } ReviewDecision::Denied | ReviewDecision::Abort => { InternalApplyPatchInvocation::Output(Err(FunctionCallError::RespondToModel( "patch rejected by user".to_string(), ))) } } } SafetyCheck::Reject { reason } => InternalApplyPatchInvocation::Output(Err( FunctionCallError::RespondToModel(format!("patch rejected: {reason}")), )), } } pub(crate) fn convert_apply_patch_to_protocol( action: &ApplyPatchAction, ) -> HashMap { let changes = action.changes(); let mut result = HashMap::with_capacity(changes.len()); for (path, change) in changes { let protocol_change = match change { ApplyPatchFileChange::Add { content } => FileChange::Add { content: content.clone(), }, ApplyPatchFileChange::Delete { content } => FileChange::Delete { content: content.clone(), }, ApplyPatchFileChange::Update { unified_diff, move_path, new_content: _new_content, } => FileChange::Update { unified_diff: unified_diff.clone(), move_path: move_path.clone(), }, }; result.insert(path.clone(), protocol_change); } result } #[cfg(test)] mod tests { use super::*; use pretty_assertions::assert_eq; use tempfile::tempdir; #[test] fn convert_apply_patch_maps_add_variant() { let tmp = tempdir().expect("tmp"); let p = tmp.path().join("a.txt"); // Create an action with a single Add change let action = ApplyPatchAction::new_add_for_test(&p, "hello".to_string()); let got = convert_apply_patch_to_protocol(&action); assert_eq!( got.get(&p), Some(&FileChange::Add { content: "hello".to_string() }) ); } }