Add codex acp (#39327)

Richard Feldman created

Behind a feature flag for now.

<img width="576" height="234" alt="Screenshot 2025-10-01 at 9 34 16 PM"
src="https://github.com/user-attachments/assets/f4e717cf-3fba-4256-af69-e3ffb5174717"
/>

Release Notes:

- N/A

Change summary

Cargo.lock                                    |   3 
crates/agent_servers/src/acp.rs               |   4 
crates/agent_servers/src/agent_servers.rs     |   2 
crates/agent_servers/src/codex.rs             |  80 +++++++
crates/agent_servers/src/e2e_tests.rs         |   7 
crates/agent_ui/src/acp/thread_view.rs        |  37 ++
crates/agent_ui/src/agent_configuration.rs    |   6 
crates/agent_ui/src/agent_panel.rs            |  42 +++
crates/agent_ui/src/agent_ui.rs               |   3 
crates/feature_flags/src/flags.rs             |   6 
crates/project/Cargo.toml                     |   3 
crates/project/src/agent_server_store.rs      | 235 ++++++++++++++++++++
crates/settings/src/settings_content/agent.rs |   1 
13 files changed, 410 insertions(+), 19 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -12082,6 +12082,8 @@ dependencies = [
  "aho-corasick",
  "anyhow",
  "askpass",
+ "async-compression",
+ "async-tar",
  "async-trait",
  "base64 0.22.1",
  "buffer_diff",
@@ -12094,6 +12096,7 @@ dependencies = [
  "dap_adapters",
  "extension",
  "fancy-regex 0.14.0",
+ "feature_flags",
  "fs",
  "futures 0.3.31",
  "fuzzy",

crates/agent_servers/src/acp.rs 🔗

@@ -380,6 +380,10 @@ impl AgentConnection for AcpConnection {
             match result {
                 Ok(response) => Ok(response),
                 Err(err) => {
+                    if err.code == acp::ErrorCode::AUTH_REQUIRED.code {
+                        return Err(anyhow!(acp::Error::auth_required()));
+                    }
+
                     if err.code != ErrorCode::INTERNAL_ERROR.code {
                         anyhow::bail!(err)
                     }

crates/agent_servers/src/agent_servers.rs 🔗

@@ -1,5 +1,6 @@
 mod acp;
 mod claude;
+mod codex;
 mod custom;
 mod gemini;
 
@@ -8,6 +9,7 @@ pub mod e2e_tests;
 
 pub use claude::*;
 use client::ProxySettings;
+pub use codex::*;
 use collections::HashMap;
 pub use custom::*;
 use fs::Fs;

crates/agent_servers/src/codex.rs 🔗

@@ -0,0 +1,80 @@
+use std::rc::Rc;
+use std::{any::Any, path::Path};
+
+use crate::{AgentServer, AgentServerDelegate, load_proxy_env};
+use acp_thread::AgentConnection;
+use anyhow::{Context as _, Result};
+use gpui::{App, SharedString, Task};
+use project::agent_server_store::CODEX_NAME;
+
+#[derive(Clone)]
+pub struct Codex;
+
+#[cfg(test)]
+pub(crate) mod tests {
+    use super::*;
+
+    crate::common_e2e_tests!(async |_, _, _| Codex, allow_option_id = "proceed_once");
+}
+
+impl AgentServer for Codex {
+    fn telemetry_id(&self) -> &'static str {
+        "codex"
+    }
+
+    fn name(&self) -> SharedString {
+        "Codex".into()
+    }
+
+    fn logo(&self) -> ui::IconName {
+        ui::IconName::AiOpenAi
+    }
+
+    fn connect(
+        &self,
+        root_dir: Option<&Path>,
+        delegate: AgentServerDelegate,
+        cx: &mut App,
+    ) -> Task<Result<(Rc<dyn AgentConnection>, Option<task::SpawnInTerminal>)>> {
+        let name = self.name();
+        let root_dir = root_dir.map(|root_dir| root_dir.to_string_lossy().into_owned());
+        let is_remote = delegate.project.read(cx).is_via_remote_server();
+        let store = delegate.store.downgrade();
+        let extra_env = load_proxy_env(cx);
+        let default_mode = self.default_mode(cx);
+
+        cx.spawn(async move |cx| {
+            let (command, root_dir, login) = store
+                .update(cx, |store, cx| {
+                    let agent = store
+                        .get_external_agent(&CODEX_NAME.into())
+                        .context("Codex is not registered")?;
+                    anyhow::Ok(agent.get_command(
+                        root_dir.as_deref(),
+                        extra_env,
+                        delegate.status_tx,
+                        // For now, report that there are no updates.
+                        // (A future PR will use the GitHub Releases API to fetch them.)
+                        delegate.new_version_available,
+                        &mut cx.to_async(),
+                    ))
+                })??
+                .await?;
+
+            let connection = crate::acp::connect(
+                name,
+                command,
+                root_dir.as_ref(),
+                default_mode,
+                is_remote,
+                cx,
+            )
+            .await?;
+            Ok((connection, login))
+        })
+    }
+
+    fn into_any(self: Rc<Self>) -> Rc<dyn Any> {
+        self
+    }
+}

crates/agent_servers/src/e2e_tests.rs 🔗

@@ -483,6 +483,13 @@ pub async fn init_test(cx: &mut TestAppContext) -> Arc<FakeFs> {
                     default_mode: None,
                 }),
                 gemini: Some(crate::gemini::tests::local_command().into()),
+                codex: Some(BuiltinAgentServerSettings {
+                    path: Some("codex-acp".into()),
+                    args: None,
+                    env: None,
+                    ignore_system_version: None,
+                    default_mode: None,
+                }),
                 custom: collections::HashMap::default(),
             },
             cx,

crates/agent_ui/src/acp/thread_view.rs 🔗

@@ -577,6 +577,31 @@ impl AcpThreadView {
 
                         AgentDiff::set_active_thread(&workspace, thread.clone(), window, cx);
 
+                        // Proactively surface Authentication Required if the agent advertises auth methods.
+                        if let Some(acp_conn) = thread
+                            .read(cx)
+                            .connection()
+                            .clone()
+                            .downcast::<agent_servers::AcpConnection>()
+                        {
+                            let methods = acp_conn.auth_methods();
+                            if !methods.is_empty() {
+                                // Immediately transition to auth-required UI, but defer to avoid re-entrant update.
+                                let err = AuthRequired {
+                                    description: None,
+                                    provider_id: None,
+                                };
+                                let this_weak = cx.weak_entity();
+                                let agent = agent.clone();
+                                let connection = thread.read(cx).connection().clone();
+                                window.defer(cx, move |window, cx| {
+                                    Self::handle_auth_required(
+                                        this_weak, err, agent, connection, window, cx,
+                                    );
+                                });
+                            }
+                        }
+
                         this.model_selector = thread
                             .read(cx)
                             .connection()
@@ -1012,11 +1037,13 @@ impl AcpThreadView {
             };
 
             let connection = thread.read(cx).connection().clone();
-            if !connection
-                .auth_methods()
-                .iter()
-                .any(|method| method.id.0.as_ref() == "claude-login")
-            {
+            let auth_methods = connection.auth_methods();
+            let has_supported_auth = auth_methods.iter().any(|method| {
+                let id = method.id.0.as_ref();
+                id == "claude-login" || id == "spawn-gemini-cli"
+            });
+            let can_login = has_supported_auth || auth_methods.is_empty() || self.login.is_some();
+            if !can_login {
                 return;
             };
             let this = cx.weak_entity();

crates/agent_ui/src/agent_configuration.rs 🔗

@@ -26,7 +26,7 @@ use language_model::{
 };
 use notifications::status_toast::{StatusToast, ToastIcon};
 use project::{
-    agent_server_store::{AgentServerStore, CLAUDE_CODE_NAME, GEMINI_NAME},
+    agent_server_store::{AgentServerStore, CLAUDE_CODE_NAME, CODEX_NAME, GEMINI_NAME},
     context_server_store::{ContextServerConfiguration, ContextServerStatus, ContextServerStore},
 };
 use settings::{Settings, SettingsStore, update_settings_file};
@@ -1014,7 +1014,9 @@ impl AgentConfiguration {
             .agent_server_store
             .read(cx)
             .external_agents()
-            .filter(|name| name.0 != GEMINI_NAME && name.0 != CLAUDE_CODE_NAME)
+            .filter(|name| {
+                name.0 != GEMINI_NAME && name.0 != CLAUDE_CODE_NAME && name.0 != CODEX_NAME
+            })
             .cloned()
             .collect::<Vec<_>>();
 

crates/agent_ui/src/agent_panel.rs 🔗

@@ -7,7 +7,7 @@ use acp_thread::AcpThread;
 use agent2::{DbThreadMetadata, HistoryEntry};
 use db::kvp::{Dismissable, KEY_VALUE_STORE};
 use project::agent_server_store::{
-    AgentServerCommand, AllAgentServersSettings, CLAUDE_CODE_NAME, GEMINI_NAME,
+    AgentServerCommand, AllAgentServersSettings, CLAUDE_CODE_NAME, CODEX_NAME, GEMINI_NAME,
 };
 use serde::{Deserialize, Serialize};
 use settings::{
@@ -75,6 +75,7 @@ use zed_actions::{
     assistant::{OpenRulesLibrary, ToggleFocus},
 };
 
+use feature_flags::{CodexAcpFeatureFlag, FeatureFlagAppExt as _};
 const AGENT_PANEL_KEY: &str = "agent_panel";
 
 #[derive(Serialize, Deserialize, Debug)]
@@ -216,6 +217,7 @@ pub enum AgentType {
     TextThread,
     Gemini,
     ClaudeCode,
+    Codex,
     NativeAgent,
     Custom {
         name: SharedString,
@@ -230,6 +232,7 @@ impl AgentType {
             Self::NativeAgent => "Agent 2".into(),
             Self::Gemini => "Gemini CLI".into(),
             Self::ClaudeCode => "Claude Code".into(),
+            Self::Codex => "Codex".into(),
             Self::Custom { name, .. } => name.into(),
         }
     }
@@ -239,6 +242,7 @@ impl AgentType {
             Self::Zed | Self::NativeAgent | Self::TextThread => None,
             Self::Gemini => Some(IconName::AiGemini),
             Self::ClaudeCode => Some(IconName::AiClaude),
+            Self::Codex => Some(IconName::AiOpenAi),
             Self::Custom { .. } => Some(IconName::Terminal),
         }
     }
@@ -249,6 +253,7 @@ impl From<ExternalAgent> for AgentType {
         match value {
             ExternalAgent::Gemini => Self::Gemini,
             ExternalAgent::ClaudeCode => Self::ClaudeCode,
+            ExternalAgent::Codex => Self::Codex,
             ExternalAgent::Custom { name, command } => Self::Custom { name, command },
             ExternalAgent::NativeAgent => Self::NativeAgent,
         }
@@ -1427,6 +1432,11 @@ impl AgentPanel {
                     cx,
                 )
             }
+            AgentType::Codex => {
+                self.selected_agent = AgentType::Codex;
+                self.serialize(cx);
+                self.external_thread(Some(crate::ExternalAgent::Codex), None, None, window, cx)
+            }
             AgentType::Custom { name, command } => self.external_thread(
                 Some(crate::ExternalAgent::Custom { name, command }),
                 None,
@@ -1991,12 +2001,40 @@ impl AgentPanel {
                                         }
                                     }),
                             )
+                            .when(cx.has_flag::<CodexAcpFeatureFlag>(), |this| {
+                                this.item(
+                                    ContextMenuEntry::new("New Codex Thread")
+                                        .icon(IconName::AiOpenAi)
+                                        .disabled(is_via_collab)
+                                        .icon_color(Color::Muted)
+                                        .handler({
+                                            let workspace = workspace.clone();
+                                            move |window, cx| {
+                                                if let Some(workspace) = workspace.upgrade() {
+                                                    workspace.update(cx, |workspace, cx| {
+                                                        if let Some(panel) =
+                                                            workspace.panel::<AgentPanel>(cx)
+                                                        {
+                                                            panel.update(cx, |panel, cx| {
+                                                                panel.new_agent_thread(
+                                                                    AgentType::Codex,
+                                                                    window,
+                                                                    cx,
+                                                                );
+                                                            });
+                                                        }
+                                                    });
+                                                }
+                                            }
+                                        }),
+                                )
+                            })
                             .map(|mut menu| {
                                 let agent_names = agent_server_store
                                     .read(cx)
                                     .external_agents()
                                     .filter(|name| {
-                                        name.0 != GEMINI_NAME && name.0 != CLAUDE_CODE_NAME
+                                        name.0 != GEMINI_NAME && name.0 != CLAUDE_CODE_NAME && name.0 != CODEX_NAME
                                     })
                                     .cloned()
                                     .collect::<Vec<_>>();

crates/agent_ui/src/agent_ui.rs 🔗

@@ -167,6 +167,7 @@ enum ExternalAgent {
     #[default]
     Gemini,
     ClaudeCode,
+    Codex,
     NativeAgent,
     Custom {
         name: SharedString,
@@ -188,6 +189,7 @@ impl ExternalAgent {
             Self::NativeAgent => "zed",
             Self::Gemini => "gemini-cli",
             Self::ClaudeCode => "claude-code",
+            Self::Codex => "codex",
             Self::Custom { .. } => "custom",
         }
     }
@@ -200,6 +202,7 @@ impl ExternalAgent {
         match self {
             Self::Gemini => Rc::new(agent_servers::Gemini),
             Self::ClaudeCode => Rc::new(agent_servers::ClaudeCode),
+            Self::Codex => Rc::new(agent_servers::Codex),
             Self::NativeAgent => Rc::new(agent2::NativeAgentServer::new(fs, history)),
             Self::Custom { name, command: _ } => {
                 Rc::new(agent_servers::CustomAgentServer::new(name.clone()))

crates/feature_flags/src/flags.rs 🔗

@@ -17,3 +17,9 @@ pub struct PanicFeatureFlag;
 impl FeatureFlag for PanicFeatureFlag {
     const NAME: &'static str = "panic";
 }
+
+pub struct CodexAcpFeatureFlag;
+
+impl FeatureFlag for CodexAcpFeatureFlag {
+    const NAME: &'static str = "codex-acp";
+}

crates/project/Cargo.toml 🔗

@@ -30,6 +30,8 @@ test-support = [
 aho-corasick.workspace = true
 anyhow.workspace = true
 askpass.workspace = true
+async-compression.workspace = true
+async-tar.workspace = true
 async-trait.workspace = true
 base64.workspace = true
 buffer_diff.workspace = true
@@ -90,6 +92,7 @@ which.workspace = true
 worktree.workspace = true
 zeroize.workspace = true
 zlog.workspace = true
+feature_flags.workspace = true
 workspace-hack.workspace = true
 
 [dev-dependencies]

crates/project/src/agent_server_store.rs 🔗

@@ -8,6 +8,7 @@ use std::{
 };
 
 use anyhow::{Context as _, Result, bail};
+use client::Client;
 use collections::HashMap;
 use fs::{Fs, RemoveOptions, RenameOptions};
 use futures::StreamExt as _;
@@ -182,6 +183,32 @@ impl AgentServerStore {
                     .unwrap_or(true),
             }),
         );
+        self.external_agents
+            .extend(new_settings.custom.iter().map(|(name, settings)| {
+                (
+                    ExternalAgentServerName(name.clone()),
+                    Box::new(LocalCustomAgent {
+                        command: settings.command.clone(),
+                        project_environment: project_environment.clone(),
+                    }) as Box<dyn ExternalAgentServer>,
+                )
+            }));
+
+        use feature_flags::FeatureFlagAppExt as _;
+        if cx.has_flag::<feature_flags::CodexAcpFeatureFlag>() || new_settings.codex.is_some() {
+            self.external_agents.insert(
+                CODEX_NAME.into(),
+                Box::new(LocalCodex {
+                    fs: fs.clone(),
+                    project_environment: project_environment.clone(),
+                    custom_command: new_settings
+                        .codex
+                        .clone()
+                        .and_then(|settings| settings.custom_command()),
+                }),
+            );
+        }
+
         self.external_agents.insert(
             CLAUDE_CODE_NAME.into(),
             Box::new(LocalClaudeCode {
@@ -194,16 +221,6 @@ impl AgentServerStore {
                     .and_then(|settings| settings.custom_command()),
             }),
         );
-        self.external_agents
-            .extend(new_settings.custom.iter().map(|(name, settings)| {
-                (
-                    ExternalAgentServerName(name.clone()),
-                    Box::new(LocalCustomAgent {
-                        command: settings.command.clone(),
-                        project_environment: project_environment.clone(),
-                    }) as Box<dyn ExternalAgentServer>,
-                )
-            }));
 
         *old_settings = Some(new_settings.clone());
 
@@ -214,6 +231,7 @@ impl AgentServerStore {
                     names: self
                         .external_agents
                         .keys()
+                        .filter(|name| name.0 != CODEX_NAME)
                         .map(|name| name.to_string())
                         .collect(),
                 })
@@ -950,6 +968,164 @@ impl ExternalAgentServer for LocalClaudeCode {
     }
 }
 
+struct LocalCodex {
+    fs: Arc<dyn Fs>,
+    project_environment: Entity<ProjectEnvironment>,
+    custom_command: Option<AgentServerCommand>,
+}
+
+impl ExternalAgentServer for LocalCodex {
+    fn get_command(
+        &mut self,
+        root_dir: Option<&str>,
+        extra_env: HashMap<String, String>,
+        _status_tx: Option<watch::Sender<SharedString>>,
+        _new_version_available_tx: Option<watch::Sender<Option<String>>>,
+        cx: &mut AsyncApp,
+    ) -> Task<Result<(AgentServerCommand, String, Option<task::SpawnInTerminal>)>> {
+        let fs = self.fs.clone();
+        let project_environment = self.project_environment.downgrade();
+        let custom_command = self.custom_command.clone();
+        let root_dir: Arc<Path> = root_dir
+            .map(|root_dir| Path::new(root_dir))
+            .unwrap_or(paths::home_dir())
+            .into();
+
+        cx.spawn(async move |cx| {
+            let mut env = project_environment
+                .update(cx, |project_environment, cx| {
+                    project_environment.get_directory_environment(root_dir.clone(), cx)
+                })?
+                .await
+                .unwrap_or_default();
+
+            let mut command = if let Some(mut custom_command) = custom_command {
+                env.extend(custom_command.env.unwrap_or_default());
+                custom_command.env = Some(env);
+                custom_command
+            } else {
+                let dir = paths::data_dir().join("external_agents").join(CODEX_NAME);
+                fs.create_dir(&dir).await?;
+
+                // Find or install the latest Codex release (no update checks for now).
+                let http = cx.update(|cx| Client::global(cx).http_client())?;
+                let release = ::http_client::github::latest_github_release(
+                    "zed-industries/codex-acp",
+                    true,
+                    false,
+                    http.clone(),
+                )
+                .await
+                .context("fetching Codex latest release")?;
+
+                let version_dir = dir.join(&release.tag_name);
+                if !fs.is_dir(&version_dir).await {
+                    // Assemble release download URL from prefix, tag, and filename based on target triple.
+                    // If unsupported, silently skip download.
+                    let tag = release.tag_name.clone(); // e.g. "v0.1.0"
+                    let version_number = tag.trim_start_matches('v');
+                    if let Some(asset_url) = codex_release_url(version_number) {
+                        let http = http.clone();
+                        let mut response = http
+                            .get(&asset_url, Default::default(), true)
+                            .await
+                            .with_context(|| {
+                                format!("downloading Codex binary from {}", asset_url)
+                            })?;
+                        anyhow::ensure!(
+                            response.status().is_success(),
+                            "failed to download Codex release: {}",
+                            response.status()
+                        );
+
+                        // Extract archive into the version directory.
+                        if asset_url.ends_with(".zip") {
+                            let reader = futures::io::BufReader::new(response.body_mut());
+                            util::archive::extract_zip(&version_dir, reader)
+                                .await
+                                .context("extracting Codex binary from zip")?;
+                        } else {
+                            // Decompress and extract the tar.gz into the version directory.
+                            let reader = futures::io::BufReader::new(response.body_mut());
+                            let decoder =
+                                async_compression::futures::bufread::GzipDecoder::new(reader);
+                            let archive = async_tar::Archive::new(decoder);
+                            archive
+                                .unpack(&version_dir)
+                                .await
+                                .context("extracting Codex binary from tar.gz")?;
+                        }
+                    }
+                }
+
+                let bin_name = if cfg!(windows) {
+                    "codex-acp.exe"
+                } else {
+                    "codex-acp"
+                };
+                let bin_path = version_dir.join(bin_name);
+                anyhow::ensure!(
+                    fs.is_file(&bin_path).await,
+                    "Missing Codex binary at {} after installation",
+                    bin_path.to_string_lossy()
+                );
+
+                let mut cmd = AgentServerCommand {
+                    path: bin_path,
+                    args: Vec::new(),
+                    env: None,
+                };
+                cmd.env = Some(env);
+                cmd
+            };
+
+            command.env.get_or_insert_default().extend(extra_env);
+            Ok((command, root_dir.to_string_lossy().into_owned(), None))
+        })
+    }
+
+    fn as_any_mut(&mut self) -> &mut dyn Any {
+        self
+    }
+}
+
+/// Assemble Codex release URL for the current OS/arch and the given version number.
+/// Returns None if the current target is unsupported.
+/// Example output:
+/// https://github.com/zed-industries/codex-acp/releases/download/v{version}/codex-acp-{version}-{arch}-{platform}.{ext}
+fn codex_release_url(version: &str) -> Option<String> {
+    let arch = if cfg!(target_arch = "x86_64") {
+        "x86_64"
+    } else if cfg!(target_arch = "aarch64") {
+        "aarch64"
+    } else {
+        return None;
+    };
+
+    let platform = if cfg!(target_os = "macos") {
+        "apple-darwin"
+    } else if cfg!(target_os = "windows") {
+        "pc-windows-msvc"
+    } else if cfg!(target_os = "linux") {
+        "unknown-linux-gnu"
+    } else {
+        return None;
+    };
+
+    // Only Windows x86_64 uses .zip in release assets
+    let ext = if cfg!(target_os = "windows") && cfg!(target_arch = "x86_64") {
+        "zip"
+    } else {
+        "tar.gz"
+    };
+
+    let prefix = "https://github.com/zed-industries/codex-acp/releases/download";
+
+    Some(format!(
+        "{prefix}/v{version}/codex-acp-{version}-{arch}-{platform}.{ext}"
+    ))
+}
+
 struct LocalCustomAgent {
     project_environment: Entity<ProjectEnvironment>,
     command: AgentServerCommand,
@@ -989,13 +1165,51 @@ impl ExternalAgentServer for LocalCustomAgent {
     }
 }
 
+#[cfg(test)]
+mod tests {
+    #[test]
+    fn assembles_codex_release_url_for_current_target() {
+        let version_number = "0.1.0";
+
+        // This test fails the build if we are building a version of Zed
+        // which does not have a known build of codex-acp, to prevent us
+        // from accidentally doing a release on a new target without
+        // realizing that codex-acp support will not work on that target!
+        //
+        // Additionally, it verifies that our logic for assembling URLs
+        // correctly resolves to a known-good URL on each of our targets.
+        let allowed = [
+            "https://github.com/zed-industries/codex-acp/releases/download/v0.1.0/codex-acp-0.1.0-aarch64-apple-darwin.tar.gz",
+            "https://github.com/zed-industries/codex-acp/releases/download/v0.1.0/codex-acp-0.1.0-aarch64-pc-windows-msvc.tar.gz",
+            "https://github.com/zed-industries/codex-acp/releases/download/v0.1.0/codex-acp-0.1.0-aarch64-unknown-linux-gnu.tar.gz",
+            "https://github.com/zed-industries/codex-acp/releases/download/v0.1.0/codex-acp-0.1.0-x86_64-apple-darwin.tar.gz",
+            "https://github.com/zed-industries/codex-acp/releases/download/v0.1.0/codex-acp-0.1.0-x86_64-pc-windows-msvc.zip",
+            "https://github.com/zed-industries/codex-acp/releases/download/v0.1.0/codex-acp-0.1.0-x86_64-unknown-linux-gnu.tar.gz",
+        ];
+
+        if let Some(url) = super::codex_release_url(version_number) {
+            assert!(
+                allowed.contains(&url.as_str()),
+                "Assembled URL {} not in allowed list",
+                url
+            );
+        } else {
+            panic!(
+                "This target does not have a known codex-acp release! We should fix this by building a release of codex-acp for this target, as otherwise codex-acp will not be usable with this Zed build."
+            );
+        }
+    }
+}
+
 pub const GEMINI_NAME: &'static str = "gemini";
 pub const CLAUDE_CODE_NAME: &'static str = "claude";
+pub const CODEX_NAME: &'static str = "codex";
 
 #[derive(Default, Clone, JsonSchema, Debug, PartialEq)]
 pub struct AllAgentServersSettings {
     pub gemini: Option<BuiltinAgentServerSettings>,
     pub claude: Option<BuiltinAgentServerSettings>,
+    pub codex: Option<BuiltinAgentServerSettings>,
     pub custom: HashMap<SharedString, CustomAgentServerSettings>,
 }
 #[derive(Default, Clone, JsonSchema, Debug, PartialEq)]
@@ -1070,6 +1284,7 @@ impl settings::Settings for AllAgentServersSettings {
         Self {
             gemini: agent_settings.gemini.map(Into::into),
             claude: agent_settings.claude.map(Into::into),
+            codex: agent_settings.codex.map(Into::into),
             custom: agent_settings
                 .custom
                 .into_iter()

crates/settings/src/settings_content/agent.rs 🔗

@@ -282,6 +282,7 @@ impl From<&str> for LanguageModelProviderSetting {
 pub struct AllAgentServersSettings {
     pub gemini: Option<BuiltinAgentServerSettings>,
     pub claude: Option<BuiltinAgentServerSettings>,
+    pub codex: Option<BuiltinAgentServerSettings>,
 
     /// Custom agent servers configured by the user
     #[serde(flatten)]