agcodex_mcp_server/
patch_approval.rs1use 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(¶ms) {
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 {
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}