agcodex_mcp_server/
patch_approval.rs

1use std::collections::HashMap;
2use std::path::PathBuf;
3use std::sync::Arc;
4
5use agcodex_core::CodexConversation;
6use agcodex_core::protocol::FileChange;
7use agcodex_core::protocol::Op;
8use agcodex_core::protocol::ReviewDecision;
9use agcodex_mcp_types::ElicitRequest;
10use agcodex_mcp_types::ElicitRequestParamsRequestedSchema;
11use agcodex_mcp_types::JSONRPCErrorError;
12use agcodex_mcp_types::ModelContextProtocolRequest;
13use agcodex_mcp_types::RequestId;
14use serde::Deserialize;
15use serde::Serialize;
16use serde_json::json;
17use tracing::error;
18
19use crate::codex_tool_runner::INVALID_PARAMS_ERROR_CODE;
20use crate::outgoing_message::OutgoingMessageSender;
21
22#[derive(Debug, Serialize)]
23pub struct PatchApprovalElicitRequestParams {
24    pub message: String,
25    #[serde(rename = "requestedSchema")]
26    pub requested_schema: ElicitRequestParamsRequestedSchema,
27    pub codex_elicitation: String,
28    pub codex_mcp_tool_call_id: String,
29    pub codex_event_id: String,
30    pub codex_call_id: String,
31    #[serde(skip_serializing_if = "Option::is_none")]
32    pub codex_reason: Option<String>,
33    #[serde(skip_serializing_if = "Option::is_none")]
34    pub codex_grant_root: Option<PathBuf>,
35    pub codex_changes: HashMap<PathBuf, FileChange>,
36}
37
38#[derive(Debug, Deserialize, Serialize)]
39pub struct PatchApprovalResponse {
40    pub decision: ReviewDecision,
41}
42
43#[allow(clippy::too_many_arguments)]
44pub(crate) async fn handle_patch_approval_request(
45    call_id: String,
46    reason: Option<String>,
47    grant_root: Option<PathBuf>,
48    changes: HashMap<PathBuf, FileChange>,
49    outgoing: Arc<OutgoingMessageSender>,
50    codex: Arc<CodexConversation>,
51    request_id: RequestId,
52    tool_call_id: String,
53    event_id: String,
54) {
55    let mut message_lines = Vec::new();
56    if let Some(r) = &reason {
57        message_lines.push(r.clone());
58    }
59    message_lines.push("Allow Codex to apply proposed code changes?".to_string());
60
61    let params = PatchApprovalElicitRequestParams {
62        message: message_lines.join("\n"),
63        requested_schema: ElicitRequestParamsRequestedSchema {
64            r#type: "object".to_string(),
65            properties: json!({}),
66            required: None,
67        },
68        codex_elicitation: "patch-approval".to_string(),
69        codex_mcp_tool_call_id: tool_call_id.clone(),
70        codex_event_id: event_id.clone(),
71        codex_call_id: call_id,
72        codex_reason: reason,
73        codex_grant_root: grant_root,
74        codex_changes: changes,
75    };
76    let params_json = match serde_json::to_value(&params) {
77        Ok(value) => value,
78        Err(err) => {
79            let message = format!("Failed to serialize PatchApprovalElicitRequestParams: {err}");
80            error!("{message}");
81
82            outgoing
83                .send_error(
84                    request_id.clone(),
85                    JSONRPCErrorError {
86                        code: INVALID_PARAMS_ERROR_CODE,
87                        message,
88                        data: None,
89                    },
90                )
91                .await;
92
93            return;
94        }
95    };
96
97    let on_response = outgoing
98        .send_request(ElicitRequest::METHOD, Some(params_json))
99        .await;
100
101    // Listen for the response on a separate task so we don't block the main agent loop.
102    {
103        let codex = codex.clone();
104        let event_id = event_id.clone();
105        tokio::spawn(async move {
106            on_patch_approval_response(event_id, on_response, codex).await;
107        });
108    }
109}
110
111pub(crate) async fn on_patch_approval_response(
112    event_id: String,
113    receiver: tokio::sync::oneshot::Receiver<agcodex_mcp_types::Result>,
114    codex: Arc<CodexConversation>,
115) {
116    let response = receiver.await;
117    let value = match response {
118        Ok(value) => value,
119        Err(err) => {
120            error!("request failed: {err:?}");
121            if let Err(submit_err) = codex
122                .submit(Op::PatchApproval {
123                    id: event_id.clone(),
124                    decision: ReviewDecision::Denied,
125                })
126                .await
127            {
128                error!("failed to submit denied PatchApproval after request failure: {submit_err}");
129            }
130            return;
131        }
132    };
133
134    let response = serde_json::from_value::<PatchApprovalResponse>(value).unwrap_or_else(|err| {
135        error!("failed to deserialize PatchApprovalResponse: {err}");
136        PatchApprovalResponse {
137            decision: ReviewDecision::Denied,
138        }
139    });
140
141    if let Err(err) = codex
142        .submit(Op::PatchApproval {
143            id: event_id,
144            decision: response.decision,
145        })
146        .await
147    {
148        error!("failed to submit PatchApproval: {err}");
149    }
150}