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}