use std::collections::HashMap; use std::path::PathBuf; use std::sync::Arc; use codex_core::CodexConversation; use codex_core::protocol::FileChange; use codex_core::protocol::Op; use codex_core::protocol::ReviewDecision; use mcp_types::ElicitRequest; use mcp_types::ElicitRequestParamsRequestedSchema; use mcp_types::JSONRPCErrorError; use mcp_types::ModelContextProtocolRequest; use mcp_types::RequestId; use serde::Deserialize; use serde::Serialize; use serde_json::json; use tracing::error; use crate::codex_tool_runner::INVALID_PARAMS_ERROR_CODE; use crate::outgoing_message::OutgoingMessageSender; #[derive(Debug, Serialize)] pub struct PatchApprovalElicitRequestParams { pub message: String, #[serde(rename = "requestedSchema")] pub requested_schema: ElicitRequestParamsRequestedSchema, pub codex_elicitation: String, pub codex_mcp_tool_call_id: String, pub codex_event_id: String, pub codex_call_id: String, #[serde(skip_serializing_if = "Option::is_none")] pub codex_reason: Option, #[serde(skip_serializing_if = "Option::is_none")] pub codex_grant_root: Option, pub codex_changes: HashMap, } #[derive(Debug, Deserialize, Serialize)] pub struct PatchApprovalResponse { pub decision: ReviewDecision, } #[allow(clippy::too_many_arguments)] pub(crate) async fn handle_patch_approval_request( call_id: String, reason: Option, grant_root: Option, changes: HashMap, outgoing: Arc, codex: Arc, request_id: RequestId, tool_call_id: String, event_id: String, ) { let mut message_lines = Vec::new(); if let Some(r) = &reason { message_lines.push(r.clone()); } message_lines.push("Allow Codex to apply proposed code changes?".to_string()); let params = PatchApprovalElicitRequestParams { message: message_lines.join("\n"), requested_schema: ElicitRequestParamsRequestedSchema { r#type: "object".to_string(), properties: json!({}), required: None, }, codex_elicitation: "patch-approval".to_string(), codex_mcp_tool_call_id: tool_call_id.clone(), codex_event_id: event_id.clone(), codex_call_id: call_id, codex_reason: reason, codex_grant_root: grant_root, codex_changes: changes, }; let params_json = match serde_json::to_value(¶ms) { Ok(value) => value, Err(err) => { let message = format!("Failed to serialize PatchApprovalElicitRequestParams: {err}"); error!("{message}"); outgoing .send_error( request_id.clone(), JSONRPCErrorError { code: INVALID_PARAMS_ERROR_CODE, message, data: None, }, ) .await; return; } }; let on_response = outgoing .send_request(ElicitRequest::METHOD, Some(params_json)) .await; // Listen for the response on a separate task so we don't block the main agent loop. { let codex = codex.clone(); let event_id = event_id.clone(); tokio::spawn(async move { on_patch_approval_response(event_id, on_response, codex).await; }); } } pub(crate) async fn on_patch_approval_response( event_id: String, receiver: tokio::sync::oneshot::Receiver, codex: Arc, ) { let response = receiver.await; let value = match response { Ok(value) => value, Err(err) => { error!("request failed: {err:?}"); if let Err(submit_err) = codex .submit(Op::PatchApproval { id: event_id.clone(), decision: ReviewDecision::Denied, }) .await { error!("failed to submit denied PatchApproval after request failure: {submit_err}"); } return; } }; let response = serde_json::from_value::(value).unwrap_or_else(|err| { error!("failed to deserialize PatchApprovalResponse: {err}"); PatchApprovalResponse { decision: ReviewDecision::Denied, } }); if let Err(err) = codex .submit(Op::PatchApproval { id: event_id, decision: response.decision, }) .await { error!("failed to submit PatchApproval: {err}"); } }