codex.rs

  1use collections::HashMap;
  2use context_server::types::requests::CallTool;
  3use context_server::types::{CallToolParams, ToolResponseContent};
  4use context_server::{ContextServer, ContextServerCommand, ContextServerId};
  5use futures::channel::{mpsc, oneshot};
  6use project::Project;
  7use settings::SettingsStore;
  8use smol::stream::StreamExt;
  9use std::cell::RefCell;
 10use std::path::{Path, PathBuf};
 11use std::rc::Rc;
 12use std::sync::Arc;
 13use uuid::Uuid;
 14
 15use agentic_coding_protocol::{
 16    self as acp, AnyAgentRequest, AnyAgentResult, Client as _, ProtocolVersion,
 17};
 18use anyhow::{Context, Result, anyhow};
 19use futures::future::LocalBoxFuture;
 20use futures::{FutureExt, SinkExt as _};
 21use gpui::{App, AppContext, Entity, Task};
 22use serde::{Deserialize, Serialize};
 23use util::ResultExt;
 24
 25use crate::mcp_server::{self, McpServerConfig, ZedMcpServer};
 26use crate::tools::{EditToolParams, ReadToolParams};
 27use crate::{AgentServer, AgentServerCommand, AllAgentServersSettings};
 28use acp_thread::{AcpClientDelegate, AcpThread, AgentConnection};
 29
 30#[derive(Clone)]
 31pub struct Codex;
 32
 33impl AgentServer for Codex {
 34    fn name(&self) -> &'static str {
 35        "Codex"
 36    }
 37
 38    fn empty_state_headline(&self) -> &'static str {
 39        self.name()
 40    }
 41
 42    fn empty_state_message(&self) -> &'static str {
 43        ""
 44    }
 45
 46    fn logo(&self) -> ui::IconName {
 47        ui::IconName::AiOpenAi
 48    }
 49
 50    fn supports_always_allow(&self) -> bool {
 51        false
 52    }
 53
 54    fn new_thread(
 55        &self,
 56        root_dir: &Path,
 57        project: &Entity<Project>,
 58        cx: &mut App,
 59    ) -> Task<Result<Entity<AcpThread>>> {
 60        let project = project.clone();
 61        let root_dir = root_dir.to_path_buf();
 62        let title = self.name().into();
 63        cx.spawn(async move |cx| {
 64            let (mut delegate_tx, delegate_rx) = watch::channel(None);
 65            let tool_id_map = Rc::new(RefCell::new(HashMap::default()));
 66
 67            let zed_mcp_server = ZedMcpServer::new(
 68                delegate_rx,
 69                tool_id_map.clone(),
 70                mcp_server::EnabledTools {
 71                    permission: false,
 72                    ..Default::default()
 73                },
 74                cx,
 75            )
 76            .await?;
 77
 78            let settings = cx.read_global(|settings: &SettingsStore, _| {
 79                settings.get::<AllAgentServersSettings>(None).codex.clone()
 80            })?;
 81
 82            let Some(command) =
 83                AgentServerCommand::resolve("codex", &["mcp"], settings, &project, cx).await
 84            else {
 85                anyhow::bail!("Failed to find codex binary");
 86            };
 87
 88            let codex_mcp_client: Arc<ContextServer> = ContextServer::stdio(
 89                ContextServerId("codex-mcp-server".into()),
 90                ContextServerCommand {
 91                    path: command.path,
 92                    args: command.args,
 93                    env: command.env,
 94                },
 95            )
 96            .into();
 97
 98            ContextServer::start(codex_mcp_client.clone(), cx).await?;
 99            // todo! stop
100
101            let (notification_tx, mut notification_rx) = mpsc::unbounded();
102            let (request_tx, mut request_rx) = mpsc::unbounded();
103
104            let client = codex_mcp_client
105                .client()
106                .context("Failed to subscribe to server")?;
107
108            client.on_notification("codex/event", {
109                move |event, cx| {
110                    let mut notification_tx = notification_tx.clone();
111                    cx.background_spawn(async move {
112                        log::trace!("Notification: {:?}", serde_json::to_string_pretty(&event));
113                        if let Some(event) = serde_json::from_value::<CodexEvent>(event).log_err() {
114                            notification_tx.send(event.msg).await.log_err();
115                        }
116                    })
117                    .detach();
118                }
119            });
120
121            client.on_request::<CodexApproval, _>({
122                move |elicitation, cx| {
123                    let (tx, rx) = oneshot::channel::<Result<CodexApprovalResponse>>();
124                    let mut request_tx = request_tx.clone();
125                    cx.background_spawn(async move {
126                        log::trace!("Elicitation: {:?}", elicitation);
127                        request_tx.send((elicitation, tx)).await?;
128                        rx.await?
129                    })
130                }
131            });
132
133            let requested_call_id = Rc::new(RefCell::new(None));
134            let session_id = Rc::new(RefCell::new(None));
135
136            cx.new(|cx| {
137                let delegate = AcpClientDelegate::new(cx.entity().downgrade(), cx.to_async());
138                delegate_tx.send(Some(delegate.clone())).log_err();
139
140                let handler_task = cx.spawn({
141                    let delegate = delegate.clone();
142                    let tool_id_map = tool_id_map.clone();
143                    let requested_call_id = requested_call_id.clone();
144                    let session_id = session_id.clone();
145                    async move |_, _cx| {
146                        while let Some(notification) = notification_rx.next().await {
147                            CodexAgentConnection::handle_acp_notification(
148                                &delegate,
149                                notification,
150                                &session_id,
151                                &tool_id_map,
152                                &requested_call_id,
153                            )
154                            .await
155                            .log_err();
156                        }
157                    }
158                });
159
160                let request_task = cx.spawn({
161                    let delegate = delegate.clone();
162                    async move |_, _cx| {
163                        while let Some((elicitation, respond)) = request_rx.next().await {
164                            if let Some((id, decision)) =
165                                CodexAgentConnection::handle_elicitation(&delegate, elicitation)
166                                    .await
167                                    .log_err()
168                            {
169                                requested_call_id.replace(Some(id));
170
171                                respond
172                                    .send(Ok(CodexApprovalResponse { decision }))
173                                    .log_err();
174                            }
175                        }
176                    }
177                });
178
179                let connection = CodexAgentConnection {
180                    root_dir,
181                    codex_mcp: codex_mcp_client,
182                    cancel_request_tx: Default::default(),
183                    session_id,
184                    zed_mcp_server,
185                    _handler_task: handler_task,
186                    _request_task: request_task,
187                };
188
189                acp_thread::AcpThread::new(connection, title, None, project.clone(), cx)
190            })
191        })
192    }
193}
194
195struct CodexAgentConnection {
196    codex_mcp: Arc<context_server::ContextServer>,
197    root_dir: PathBuf,
198    cancel_request_tx: Rc<RefCell<Option<oneshot::Sender<()>>>>,
199    session_id: Rc<RefCell<Option<Uuid>>>,
200    zed_mcp_server: ZedMcpServer,
201    _handler_task: Task<()>,
202    _request_task: Task<()>,
203}
204
205impl AgentConnection for CodexAgentConnection {
206    /// Send a request to the agent and wait for a response.
207    fn request_any(
208        &self,
209        params: AnyAgentRequest,
210    ) -> LocalBoxFuture<'static, Result<acp::AnyAgentResult>> {
211        let client = self.codex_mcp.client();
212        let root_dir = self.root_dir.clone();
213        let cancel_request_tx = self.cancel_request_tx.clone();
214        let mcp_config = self.zed_mcp_server.server_config();
215        let session_id = self.session_id.clone();
216        async move {
217            let client = client.context("Codex MCP server is not initialized")?;
218
219            match params {
220                // todo: consider sending an empty request so we get the init response?
221                AnyAgentRequest::InitializeParams(_) => Ok(AnyAgentResult::InitializeResponse(
222                    acp::InitializeResponse {
223                        is_authenticated: true,
224                        protocol_version: ProtocolVersion::latest(),
225                    },
226                )),
227                AnyAgentRequest::AuthenticateParams(_) => {
228                    Err(anyhow!("Authentication not supported"))
229                }
230                AnyAgentRequest::SendUserMessageParams(message) => {
231                    let (new_cancel_tx, cancel_rx) = oneshot::channel();
232                    cancel_request_tx.borrow_mut().replace(new_cancel_tx);
233
234                    let prompt = message
235                        .chunks
236                        .into_iter()
237                        .filter_map(|chunk| match chunk {
238                            acp::UserMessageChunk::Text { text } => Some(text),
239                            acp::UserMessageChunk::Path { .. } => {
240                                // todo!
241                                None
242                            }
243                        })
244                        .collect();
245
246                    let params = if let Some(session_id) = *session_id.borrow() {
247                        CallToolParams {
248                            name: "codex-reply".into(),
249                            arguments: Some(serde_json::to_value(CodexToolCallReplyParam {
250                                prompt,
251                                session_id,
252                            })?),
253                            meta: None,
254                        }
255                    } else {
256                        CallToolParams {
257                            name: "codex".into(),
258                            arguments: Some(serde_json::to_value(CodexToolCallParam {
259                                prompt,
260                                cwd: root_dir,
261                                config: Some(CodexConfig {
262                                    mcp_servers: Some(
263                                        mcp_config
264                                            .into_iter()
265                                            .map(|config| {
266                                                (mcp_server::SERVER_NAME.to_string(), config)
267                                            })
268                                            .collect(),
269                                    ),
270                                }),
271                            })?),
272                            meta: None,
273                        }
274                    };
275
276                    client
277                        .request_with::<CallTool>(params, Some(cancel_rx), None)
278                        .await?;
279
280                    Ok(AnyAgentResult::SendUserMessageResponse(
281                        acp::SendUserMessageResponse,
282                    ))
283                }
284                AnyAgentRequest::CancelSendMessageParams(_) => {
285                    if let Ok(mut borrow) = cancel_request_tx.try_borrow_mut() {
286                        if let Some(cancel_tx) = borrow.take() {
287                            cancel_tx.send(()).ok();
288                        }
289                    }
290
291                    Ok(AnyAgentResult::CancelSendMessageResponse(
292                        acp::CancelSendMessageResponse,
293                    ))
294                }
295            }
296        }
297        .boxed_local()
298    }
299}
300
301#[derive(Debug, Serialize, Deserialize)]
302pub struct CodexConfig {
303    mcp_servers: Option<HashMap<String, McpServerConfig>>,
304}
305
306impl CodexAgentConnection {
307    async fn handle_elicitation(
308        delegate: &AcpClientDelegate,
309        elicitation: CodexElicitation,
310    ) -> Result<(acp::ToolCallId, ReviewDecision)> {
311        let confirmation = match elicitation {
312            CodexElicitation::ExecApproval(exec) => {
313                let inner_command = strip_bash_lc_and_escape(&exec.codex_command);
314
315                acp::RequestToolCallConfirmationParams {
316                    tool_call: acp::PushToolCallParams {
317                        label: format!("`{inner_command}`"),
318                        icon: acp::Icon::Terminal,
319                        content: None,
320                        locations: vec![],
321                    },
322                    confirmation: acp::ToolCallConfirmation::Execute {
323                        root_command: inner_command
324                            .split(" ")
325                            .next()
326                            .unwrap_or_default()
327                            .to_string(),
328                        command: inner_command,
329                        description: Some(exec.message),
330                    },
331                }
332            }
333            CodexElicitation::PatchApproval(patch) => {
334                acp::RequestToolCallConfirmationParams {
335                    tool_call: acp::PushToolCallParams {
336                        label: "Edit".to_string(),
337                        icon: acp::Icon::Pencil,
338                        content: None, // todo!()
339                        locations: patch
340                            .codex_changes
341                            .keys()
342                            .map(|path| acp::ToolCallLocation {
343                                path: path.clone(),
344                                line: None,
345                            })
346                            .collect(),
347                    },
348                    confirmation: acp::ToolCallConfirmation::Edit {
349                        description: Some(patch.message),
350                    },
351                }
352            }
353        };
354
355        let response = delegate
356            .request_tool_call_confirmation(confirmation)
357            .await?;
358
359        let decision = match response.outcome {
360            acp::ToolCallConfirmationOutcome::Allow => ReviewDecision::Approved,
361            acp::ToolCallConfirmationOutcome::AlwaysAllow
362            | acp::ToolCallConfirmationOutcome::AlwaysAllowMcpServer
363            | acp::ToolCallConfirmationOutcome::AlwaysAllowTool => {
364                ReviewDecision::ApprovedForSession
365            }
366            acp::ToolCallConfirmationOutcome::Reject => ReviewDecision::Denied,
367            acp::ToolCallConfirmationOutcome::Cancel => ReviewDecision::Abort,
368        };
369
370        Ok((response.id, decision))
371    }
372
373    async fn handle_acp_notification(
374        delegate: &AcpClientDelegate,
375        event: AcpNotification,
376        session_id: &Rc<RefCell<Option<Uuid>>>,
377        tool_id_map: &Rc<RefCell<HashMap<String, acp::ToolCallId>>>,
378        requested_call_id: &Rc<RefCell<Option<acp::ToolCallId>>>,
379    ) -> Result<()> {
380        match event {
381            AcpNotification::SessionConfigured(sesh) => {
382                session_id.replace(Some(sesh.session_id));
383            }
384            AcpNotification::AgentMessage(message) => {
385                delegate
386                    .stream_assistant_message_chunk(acp::StreamAssistantMessageChunkParams {
387                        chunk: acp::AssistantMessageChunk::Text {
388                            text: message.message,
389                        },
390                    })
391                    .await?;
392            }
393            AcpNotification::AgentReasoning(message) => {
394                delegate
395                    .stream_assistant_message_chunk(acp::StreamAssistantMessageChunkParams {
396                        chunk: acp::AssistantMessageChunk::Thought {
397                            thought: message.text,
398                        },
399                    })
400                    .await?
401            }
402            AcpNotification::McpToolCallBegin(mut event) => {
403                if let Some(requested_tool_id) = requested_call_id.take() {
404                    tool_id_map
405                        .borrow_mut()
406                        .insert(event.call_id, requested_tool_id);
407                } else {
408                    let mut tool_call = acp::PushToolCallParams {
409                        label: format!("`{}: {}`", event.server, event.tool),
410                        icon: acp::Icon::Hammer,
411                        content: event.arguments.as_ref().and_then(|args| {
412                            Some(acp::ToolCallContent::Markdown {
413                                markdown: md_codeblock(
414                                    "json",
415                                    &serde_json::to_string_pretty(args).ok()?,
416                                ),
417                            })
418                        }),
419                        locations: vec![],
420                    };
421
422                    if event.server == mcp_server::SERVER_NAME
423                        && event.tool == mcp_server::EDIT_TOOL
424                        && let Some(params) = event.arguments.take().and_then(|args| {
425                            serde_json::from_value::<EditToolParams>(args).log_err()
426                        })
427                    {
428                        tool_call = acp::PushToolCallParams {
429                            label: "Edit".into(),
430                            icon: acp::Icon::Pencil,
431                            content: Some(acp::ToolCallContent::Diff {
432                                diff: acp::Diff {
433                                    path: params.abs_path.clone(),
434                                    old_text: Some(params.old_text),
435                                    new_text: params.new_text,
436                                },
437                            }),
438                            locations: vec![acp::ToolCallLocation {
439                                path: params.abs_path,
440                                line: None,
441                            }],
442                        };
443                    } else if event.server == mcp_server::SERVER_NAME
444                        && event.tool == mcp_server::READ_TOOL
445                        && let Some(params) = event.arguments.take().and_then(|args| {
446                            serde_json::from_value::<ReadToolParams>(args).log_err()
447                        })
448                    {
449                        tool_call = acp::PushToolCallParams {
450                            label: "Read".into(),
451                            icon: acp::Icon::FileSearch,
452                            content: None,
453                            locations: vec![acp::ToolCallLocation {
454                                path: params.abs_path,
455                                line: params.offset,
456                            }],
457                        }
458                    }
459
460                    let result = delegate.push_tool_call(tool_call).await?;
461
462                    tool_id_map.borrow_mut().insert(event.call_id, result.id);
463                }
464            }
465            AcpNotification::McpToolCallEnd(event) => {
466                let acp_call_id = tool_id_map
467                    .borrow_mut()
468                    .remove(&event.call_id)
469                    .context("Missing tool call")?;
470
471                let (status, content) = match event.result {
472                    Ok(value) => {
473                        if let Ok(response) =
474                            serde_json::from_value::<context_server::types::CallToolResponse>(value)
475                        {
476                            (
477                                acp::ToolCallStatus::Finished,
478                                mcp_tool_content_to_acp(response.content),
479                            )
480                        } else {
481                            (
482                                acp::ToolCallStatus::Error,
483                                Some(acp::ToolCallContent::Markdown {
484                                    markdown: "Failed to parse tool response".to_string(),
485                                }),
486                            )
487                        }
488                    }
489                    Err(error) => (
490                        acp::ToolCallStatus::Error,
491                        Some(acp::ToolCallContent::Markdown { markdown: error }),
492                    ),
493                };
494
495                delegate
496                    .update_tool_call(acp::UpdateToolCallParams {
497                        tool_call_id: acp_call_id,
498                        status,
499                        content,
500                    })
501                    .await?;
502            }
503            AcpNotification::ExecCommandBegin(event) => {
504                if let Some(requested_tool_id) = requested_call_id.take() {
505                    tool_id_map
506                        .borrow_mut()
507                        .insert(event.call_id, requested_tool_id);
508                } else {
509                    let inner_command = strip_bash_lc_and_escape(&event.command);
510
511                    let result = delegate
512                        .push_tool_call(acp::PushToolCallParams {
513                            label: format!("`{}`", inner_command),
514                            icon: acp::Icon::Terminal,
515                            content: None,
516                            locations: vec![],
517                        })
518                        .await?;
519
520                    tool_id_map.borrow_mut().insert(event.call_id, result.id);
521                }
522            }
523            AcpNotification::ExecCommandEnd(event) => {
524                let acp_call_id = tool_id_map
525                    .borrow_mut()
526                    .remove(&event.call_id)
527                    .context("Missing tool call")?;
528
529                let mut content = String::new();
530                if !event.stdout.is_empty() {
531                    use std::fmt::Write;
532                    writeln!(
533                        &mut content,
534                        "### Output\n\n{}",
535                        md_codeblock("", &event.stdout)
536                    )
537                    .unwrap();
538                }
539                if !event.stdout.is_empty() && !event.stderr.is_empty() {
540                    use std::fmt::Write;
541                    writeln!(&mut content).unwrap();
542                }
543                if !event.stderr.is_empty() {
544                    use std::fmt::Write;
545                    writeln!(
546                        &mut content,
547                        "### Error\n\n{}",
548                        md_codeblock("", &event.stderr)
549                    )
550                    .unwrap();
551                }
552                let success = event.exit_code == 0;
553                if !success {
554                    use std::fmt::Write;
555                    writeln!(&mut content, "\nExit code: `{}`", event.exit_code).unwrap();
556                }
557
558                delegate
559                    .update_tool_call(acp::UpdateToolCallParams {
560                        tool_call_id: acp_call_id,
561                        status: if success {
562                            acp::ToolCallStatus::Finished
563                        } else {
564                            acp::ToolCallStatus::Error
565                        },
566                        content: Some(acp::ToolCallContent::Markdown { markdown: content }),
567                    })
568                    .await?;
569            }
570            AcpNotification::Other => {}
571        }
572
573        Ok(())
574    }
575}
576
577/// todo! use types from h2a crate when we have one
578
579#[derive(Debug, Serialize, Deserialize)]
580#[serde(rename_all = "kebab-case")]
581pub(crate) struct CodexToolCallParam {
582    pub prompt: String,
583    pub cwd: PathBuf,
584    pub config: Option<CodexConfig>,
585}
586
587#[derive(Debug, Clone, Serialize, Deserialize)]
588#[serde(rename_all = "camelCase")]
589pub(crate) struct CodexToolCallReplyParam {
590    pub session_id: Uuid,
591    pub prompt: String,
592}
593
594#[derive(Debug, Clone, Serialize, Deserialize)]
595struct CodexEvent {
596    pub msg: AcpNotification,
597}
598
599#[derive(Debug, Clone, Serialize, Deserialize)]
600#[serde(tag = "type", rename_all = "snake_case")]
601pub enum AcpNotification {
602    SessionConfigured(SessionConfiguredEvent),
603    AgentMessage(AgentMessageEvent),
604    AgentReasoning(AgentReasoningEvent),
605    McpToolCallBegin(McpToolCallBeginEvent),
606    McpToolCallEnd(McpToolCallEndEvent),
607    ExecCommandBegin(ExecCommandBeginEvent),
608    ExecCommandEnd(ExecCommandEndEvent),
609    #[serde(other)]
610    Other,
611}
612
613#[derive(Debug, Clone, Serialize, Deserialize)]
614pub struct AgentMessageEvent {
615    pub message: String,
616}
617
618#[derive(Debug, Clone, Deserialize, Serialize)]
619pub struct AgentReasoningEvent {
620    pub text: String,
621}
622
623#[derive(Debug, Clone, Serialize, Deserialize)]
624pub struct McpToolCallBeginEvent {
625    pub call_id: String,
626    pub server: String,
627    pub tool: String,
628    pub arguments: Option<serde_json::Value>,
629}
630
631#[derive(Debug, Clone, Serialize, Deserialize)]
632pub struct McpToolCallEndEvent {
633    pub call_id: String,
634    pub result: Result<serde_json::Value, String>,
635}
636
637#[derive(Debug, Clone, Serialize, Deserialize)]
638pub struct ExecCommandBeginEvent {
639    pub call_id: String,
640    pub command: Vec<String>,
641    pub cwd: PathBuf,
642}
643
644#[derive(Debug, Clone, Serialize, Deserialize)]
645pub struct ExecCommandEndEvent {
646    pub call_id: String,
647    pub stdout: String,
648    pub stderr: String,
649    pub exit_code: i32,
650}
651
652#[derive(Debug, Default, Clone, Deserialize, Serialize)]
653pub struct SessionConfiguredEvent {
654    pub session_id: Uuid,
655}
656
657// Helper functions
658fn md_codeblock(lang: &str, content: &str) -> String {
659    if content.ends_with('\n') {
660        format!("```{}\n{}```", lang, content)
661    } else {
662        format!("```{}\n{}\n```", lang, content)
663    }
664}
665
666fn strip_bash_lc_and_escape(command: &[String]) -> String {
667    match command {
668        // exactly three items
669        [first, second, third]
670            // first two must be "bash", "-lc"
671            if first == "bash" && second == "-lc" =>
672        {
673            third.clone()
674        }
675        _ => escape_command(command),
676    }
677}
678
679fn escape_command(command: &[String]) -> String {
680    shlex::try_join(command.iter().map(|s| s.as_str())).unwrap_or_else(|_| command.join(" "))
681}
682
683fn mcp_tool_content_to_acp(chunks: Vec<ToolResponseContent>) -> Option<acp::ToolCallContent> {
684    let mut content = String::new();
685
686    for chunk in chunks {
687        match chunk {
688            ToolResponseContent::Text { text } => content.push_str(&text),
689            ToolResponseContent::Image { .. } => {
690                // todo!
691            }
692            ToolResponseContent::Audio { .. } => {
693                // todo!
694            }
695            ToolResponseContent::Resource { .. } => {
696                // todo!
697            }
698        }
699    }
700
701    if !content.is_empty() {
702        Some(acp::ToolCallContent::Markdown { markdown: content })
703    } else {
704        None
705    }
706}
707
708pub struct CodexApproval;
709impl context_server::types::Request for CodexApproval {
710    type Params = CodexElicitation;
711    type Response = CodexApprovalResponse;
712    const METHOD: &'static str = "elicitation/create";
713}
714
715#[derive(Debug, Serialize, Deserialize)]
716pub struct ExecApprovalRequest {
717    // These fields are required so that `params`
718    // conforms to ElicitRequestParams.
719    pub message: String,
720    // #[serde(rename = "requestedSchema")]
721    // pub requested_schema: ElicitRequestParamsRequestedSchema,
722
723    // // These are additional fields the client can use to
724    // // correlate the request with the codex tool call.
725    pub codex_mcp_tool_call_id: String,
726    // pub codex_event_id: String,
727    pub codex_command: Vec<String>,
728    pub codex_cwd: PathBuf,
729}
730
731#[derive(Debug, Serialize, Deserialize)]
732pub struct PatchApprovalRequest {
733    pub message: String,
734    // #[serde(rename = "requestedSchema")]
735    // pub requested_schema: ElicitRequestParamsRequestedSchema,
736    pub codex_mcp_tool_call_id: String,
737    pub codex_event_id: String,
738    #[serde(skip_serializing_if = "Option::is_none")]
739    pub codex_reason: Option<String>,
740    #[serde(skip_serializing_if = "Option::is_none")]
741    pub codex_grant_root: Option<PathBuf>,
742    pub codex_changes: HashMap<PathBuf, FileChange>,
743}
744
745#[derive(Debug, Serialize, Deserialize)]
746#[serde(tag = "codex_elicitation", rename_all = "kebab-case")]
747pub enum CodexElicitation {
748    ExecApproval(ExecApprovalRequest),
749    PatchApproval(PatchApprovalRequest),
750}
751
752#[derive(Debug, Clone, Deserialize, Serialize)]
753#[serde(rename_all = "snake_case")]
754pub enum FileChange {
755    Add {
756        content: String,
757    },
758    Delete,
759    Update {
760        unified_diff: String,
761        move_path: Option<PathBuf>,
762    },
763}
764
765#[derive(Debug, Serialize, Deserialize)]
766pub struct CodexApprovalResponse {
767    pub decision: ReviewDecision,
768}
769
770/// User's decision in response to an ExecApprovalRequest.
771#[derive(Debug, Default, Clone, Copy, Deserialize, Serialize, PartialEq, Eq)]
772#[serde(rename_all = "snake_case")]
773pub enum ReviewDecision {
774    /// User has approved this command and the agent should execute it.
775    Approved,
776
777    /// User has approved this command and wants to automatically approve any
778    /// future identical instances (`command` and `cwd` match exactly) for the
779    /// remainder of the session.
780    ApprovedForSession,
781
782    /// User has denied this command and the agent should not execute it, but
783    /// it should continue the session and try something else.
784    #[default]
785    Denied,
786
787    /// User has denied this command and the agent should not do anything until
788    /// the user's next command.
789    Abort,
790}
791
792#[cfg(test)]
793pub mod tests {
794    use super::*;
795
796    crate::common_e2e_tests!(Codex);
797
798    pub fn local_command() -> AgentServerCommand {
799        let cli_path =
800            Path::new(env!("CARGO_MANIFEST_DIR")).join("../../../codex/code-rs/target/debug/codex");
801
802        AgentServerCommand {
803            path: cli_path,
804            args: vec!["mcp".into()],
805            env: None,
806        }
807    }
808}