agent_server_store.rs

   1use remote::Interactive;
   2use std::{
   3    any::Any,
   4    borrow::Borrow,
   5    path::{Path, PathBuf},
   6    str::FromStr as _,
   7    sync::Arc,
   8    time::Duration,
   9};
  10
  11use anyhow::{Context as _, Result, bail};
  12use collections::HashMap;
  13use fs::{Fs, RemoveOptions, RenameOptions};
  14use futures::StreamExt as _;
  15use gpui::{
  16    AppContext as _, AsyncApp, Context, Entity, EventEmitter, SharedString, Subscription, Task,
  17};
  18use http_client::{HttpClient, github::AssetKind};
  19use node_runtime::NodeRuntime;
  20use remote::RemoteClient;
  21use rpc::{
  22    AnyProtoClient, TypedEnvelope,
  23    proto::{self, ExternalExtensionAgent},
  24};
  25use schemars::JsonSchema;
  26use semver::Version;
  27use serde::{Deserialize, Serialize};
  28use settings::{RegisterSetting, SettingsStore};
  29use task::{Shell, SpawnInTerminal};
  30use util::{ResultExt as _, debug_panic};
  31
  32use crate::ProjectEnvironment;
  33use crate::agent_registry_store::{AgentRegistryStore, RegistryAgent, RegistryTargetConfig};
  34
  35#[derive(Deserialize, Serialize, Clone, PartialEq, Eq, JsonSchema)]
  36pub struct AgentServerCommand {
  37    #[serde(rename = "command")]
  38    pub path: PathBuf,
  39    #[serde(default)]
  40    pub args: Vec<String>,
  41    pub env: Option<HashMap<String, String>>,
  42}
  43
  44impl std::fmt::Debug for AgentServerCommand {
  45    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
  46        let filtered_env = self.env.as_ref().map(|env| {
  47            env.iter()
  48                .map(|(k, v)| {
  49                    (
  50                        k,
  51                        if util::redact::should_redact(k) {
  52                            "[REDACTED]"
  53                        } else {
  54                            v
  55                        },
  56                    )
  57                })
  58                .collect::<Vec<_>>()
  59        });
  60
  61        f.debug_struct("AgentServerCommand")
  62            .field("path", &self.path)
  63            .field("args", &self.args)
  64            .field("env", &filtered_env)
  65            .finish()
  66    }
  67}
  68
  69#[derive(Clone, Debug, PartialEq, Eq, Hash)]
  70pub struct ExternalAgentServerName(pub SharedString);
  71
  72impl std::fmt::Display for ExternalAgentServerName {
  73    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
  74        write!(f, "{}", self.0)
  75    }
  76}
  77
  78impl From<&'static str> for ExternalAgentServerName {
  79    fn from(value: &'static str) -> Self {
  80        ExternalAgentServerName(value.into())
  81    }
  82}
  83
  84impl From<ExternalAgentServerName> for SharedString {
  85    fn from(value: ExternalAgentServerName) -> Self {
  86        value.0
  87    }
  88}
  89
  90impl Borrow<str> for ExternalAgentServerName {
  91    fn borrow(&self) -> &str {
  92        &self.0
  93    }
  94}
  95
  96#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
  97pub enum ExternalAgentSource {
  98    Builtin,
  99    #[default]
 100    Custom,
 101    Extension,
 102    Registry,
 103}
 104
 105pub trait ExternalAgentServer {
 106    fn get_command(
 107        &mut self,
 108        root_dir: Option<&str>,
 109        extra_env: HashMap<String, String>,
 110        status_tx: Option<watch::Sender<SharedString>>,
 111        new_version_available_tx: Option<watch::Sender<Option<String>>>,
 112        cx: &mut AsyncApp,
 113    ) -> Task<Result<(AgentServerCommand, String, Option<task::SpawnInTerminal>)>>;
 114
 115    fn as_any_mut(&mut self) -> &mut dyn Any;
 116}
 117
 118impl dyn ExternalAgentServer {
 119    fn downcast_mut<T: ExternalAgentServer + 'static>(&mut self) -> Option<&mut T> {
 120        self.as_any_mut().downcast_mut()
 121    }
 122}
 123
 124enum AgentServerStoreState {
 125    Local {
 126        node_runtime: NodeRuntime,
 127        fs: Arc<dyn Fs>,
 128        project_environment: Entity<ProjectEnvironment>,
 129        downstream_client: Option<(u64, AnyProtoClient)>,
 130        settings: Option<AllAgentServersSettings>,
 131        http_client: Arc<dyn HttpClient>,
 132        extension_agents: Vec<(
 133            Arc<str>,
 134            String,
 135            HashMap<String, extension::TargetConfig>,
 136            HashMap<String, String>,
 137            Option<String>,
 138            Option<SharedString>,
 139        )>,
 140        _subscriptions: Vec<Subscription>,
 141    },
 142    Remote {
 143        project_id: u64,
 144        upstream_client: Entity<RemoteClient>,
 145    },
 146    Collab,
 147}
 148
 149pub struct ExternalAgentEntry {
 150    server: Box<dyn ExternalAgentServer>,
 151    icon: Option<SharedString>,
 152    display_name: Option<SharedString>,
 153    pub source: ExternalAgentSource,
 154}
 155
 156impl ExternalAgentEntry {
 157    pub fn new(
 158        server: Box<dyn ExternalAgentServer>,
 159        source: ExternalAgentSource,
 160        icon: Option<SharedString>,
 161        display_name: Option<SharedString>,
 162    ) -> Self {
 163        Self {
 164            server,
 165            icon,
 166            display_name,
 167            source,
 168        }
 169    }
 170}
 171
 172pub struct AgentServerStore {
 173    state: AgentServerStoreState,
 174    pub external_agents: HashMap<ExternalAgentServerName, ExternalAgentEntry>,
 175}
 176
 177pub struct AgentServersUpdated;
 178
 179impl EventEmitter<AgentServersUpdated> for AgentServerStore {}
 180
 181impl AgentServerStore {
 182    /// Synchronizes extension-provided agent servers with the store.
 183    pub fn sync_extension_agents<'a, I>(
 184        &mut self,
 185        manifests: I,
 186        extensions_dir: PathBuf,
 187        cx: &mut Context<Self>,
 188    ) where
 189        I: IntoIterator<Item = (&'a str, &'a extension::ExtensionManifest)>,
 190    {
 191        // Collect manifests first so we can iterate twice
 192        let manifests: Vec<_> = manifests.into_iter().collect();
 193
 194        // Remove all extension-provided agents
 195        // (They will be re-added below if they're in the currently installed extensions)
 196        self.external_agents
 197            .retain(|_, entry| entry.source != ExternalAgentSource::Extension);
 198
 199        // Insert agent servers from extension manifests
 200        match &mut self.state {
 201            AgentServerStoreState::Local {
 202                extension_agents, ..
 203            } => {
 204                extension_agents.clear();
 205                for (ext_id, manifest) in manifests {
 206                    for (agent_name, agent_entry) in &manifest.agent_servers {
 207                        let display_name = SharedString::from(agent_entry.name.clone());
 208                        let icon_path = agent_entry.icon.as_ref().and_then(|icon| {
 209                            resolve_extension_icon_path(&extensions_dir, ext_id, icon)
 210                        });
 211
 212                        extension_agents.push((
 213                            agent_name.clone(),
 214                            ext_id.to_owned(),
 215                            agent_entry.targets.clone(),
 216                            agent_entry.env.clone(),
 217                            icon_path,
 218                            Some(display_name),
 219                        ));
 220                    }
 221                }
 222                self.reregister_agents(cx);
 223            }
 224            AgentServerStoreState::Remote {
 225                project_id,
 226                upstream_client,
 227            } => {
 228                let mut agents = vec![];
 229                for (ext_id, manifest) in manifests {
 230                    for (agent_name, agent_entry) in &manifest.agent_servers {
 231                        let display_name = SharedString::from(agent_entry.name.clone());
 232                        let icon_path = agent_entry.icon.as_ref().and_then(|icon| {
 233                            resolve_extension_icon_path(&extensions_dir, ext_id, icon)
 234                        });
 235                        let icon_shared = icon_path
 236                            .as_ref()
 237                            .map(|path| SharedString::from(path.clone()));
 238                        let icon = icon_path;
 239                        let agent_server_name = ExternalAgentServerName(agent_name.clone().into());
 240                        self.external_agents
 241                            .entry(agent_server_name.clone())
 242                            .and_modify(|entry| {
 243                                entry.icon = icon_shared.clone();
 244                                entry.display_name = Some(display_name.clone());
 245                                entry.source = ExternalAgentSource::Extension;
 246                            })
 247                            .or_insert_with(|| {
 248                                ExternalAgentEntry::new(
 249                                    Box::new(RemoteExternalAgentServer {
 250                                        project_id: *project_id,
 251                                        upstream_client: upstream_client.clone(),
 252                                        name: agent_server_name.clone(),
 253                                        status_tx: None,
 254                                        new_version_available_tx: None,
 255                                    })
 256                                        as Box<dyn ExternalAgentServer>,
 257                                    ExternalAgentSource::Extension,
 258                                    icon_shared.clone(),
 259                                    Some(display_name.clone()),
 260                                )
 261                            });
 262
 263                        agents.push(ExternalExtensionAgent {
 264                            name: agent_name.to_string(),
 265                            icon_path: icon,
 266                            extension_id: ext_id.to_string(),
 267                            targets: agent_entry
 268                                .targets
 269                                .iter()
 270                                .map(|(k, v)| (k.clone(), v.to_proto()))
 271                                .collect(),
 272                            env: agent_entry
 273                                .env
 274                                .iter()
 275                                .map(|(k, v)| (k.clone(), v.clone()))
 276                                .collect(),
 277                        });
 278                    }
 279                }
 280                upstream_client
 281                    .read(cx)
 282                    .proto_client()
 283                    .send(proto::ExternalExtensionAgentsUpdated {
 284                        project_id: *project_id,
 285                        agents,
 286                    })
 287                    .log_err();
 288            }
 289            AgentServerStoreState::Collab => {
 290                // Do nothing
 291            }
 292        }
 293
 294        cx.emit(AgentServersUpdated);
 295    }
 296
 297    pub fn agent_icon(&self, name: &ExternalAgentServerName) -> Option<SharedString> {
 298        self.external_agents
 299            .get(name)
 300            .and_then(|entry| entry.icon.clone())
 301    }
 302
 303    pub fn agent_source(&self, name: &ExternalAgentServerName) -> Option<ExternalAgentSource> {
 304        self.external_agents.get(name).map(|entry| entry.source)
 305    }
 306}
 307
 308/// Safely resolves an extension icon path, ensuring it stays within the extension directory.
 309/// Returns `None` if the path would escape the extension directory (path traversal attack).
 310pub fn resolve_extension_icon_path(
 311    extensions_dir: &Path,
 312    extension_id: &str,
 313    icon_relative_path: &str,
 314) -> Option<String> {
 315    let extension_root = extensions_dir.join(extension_id);
 316    let icon_path = extension_root.join(icon_relative_path);
 317
 318    // Canonicalize both paths to resolve symlinks and normalize the paths.
 319    // For the extension root, we need to handle the case where it might be a symlink
 320    // (common for dev extensions).
 321    let canonical_extension_root = extension_root.canonicalize().unwrap_or(extension_root);
 322    let canonical_icon_path = match icon_path.canonicalize() {
 323        Ok(path) => path,
 324        Err(err) => {
 325            log::warn!(
 326                "Failed to canonicalize icon path for extension '{}': {} (path: {})",
 327                extension_id,
 328                err,
 329                icon_relative_path
 330            );
 331            return None;
 332        }
 333    };
 334
 335    // Verify the resolved icon path is within the extension directory
 336    if canonical_icon_path.starts_with(&canonical_extension_root) {
 337        Some(canonical_icon_path.to_string_lossy().to_string())
 338    } else {
 339        log::warn!(
 340            "Icon path '{}' for extension '{}' escapes extension directory, ignoring for security",
 341            icon_relative_path,
 342            extension_id
 343        );
 344        None
 345    }
 346}
 347
 348impl AgentServerStore {
 349    pub fn agent_display_name(&self, name: &ExternalAgentServerName) -> Option<SharedString> {
 350        self.external_agents
 351            .get(name)
 352            .and_then(|entry| entry.display_name.clone())
 353    }
 354
 355    pub fn init_remote(session: &AnyProtoClient) {
 356        session.add_entity_message_handler(Self::handle_external_agents_updated);
 357        session.add_entity_message_handler(Self::handle_loading_status_updated);
 358        session.add_entity_message_handler(Self::handle_new_version_available);
 359    }
 360
 361    pub fn init_headless(session: &AnyProtoClient) {
 362        session.add_entity_message_handler(Self::handle_external_extension_agents_updated);
 363        session.add_entity_request_handler(Self::handle_get_agent_server_command);
 364    }
 365
 366    fn agent_servers_settings_changed(&mut self, cx: &mut Context<Self>) {
 367        let AgentServerStoreState::Local {
 368            settings: old_settings,
 369            ..
 370        } = &mut self.state
 371        else {
 372            debug_panic!(
 373                "should not be subscribed to agent server settings changes in non-local project"
 374            );
 375            return;
 376        };
 377
 378        let new_settings = cx
 379            .global::<SettingsStore>()
 380            .get::<AllAgentServersSettings>(None)
 381            .clone();
 382        if Some(&new_settings) == old_settings.as_ref() {
 383            return;
 384        }
 385
 386        self.reregister_agents(cx);
 387    }
 388
 389    fn reregister_agents(&mut self, cx: &mut Context<Self>) {
 390        let AgentServerStoreState::Local {
 391            node_runtime,
 392            fs,
 393            project_environment,
 394            downstream_client,
 395            settings: old_settings,
 396            http_client,
 397            extension_agents,
 398            ..
 399        } = &mut self.state
 400        else {
 401            debug_panic!("Non-local projects should never attempt to reregister. This is a bug!");
 402
 403            return;
 404        };
 405
 406        let new_settings = cx
 407            .global::<SettingsStore>()
 408            .get::<AllAgentServersSettings>(None)
 409            .clone();
 410
 411        self.external_agents.clear();
 412        self.external_agents.insert(
 413            GEMINI_NAME.into(),
 414            ExternalAgentEntry::new(
 415                Box::new(LocalGemini {
 416                    fs: fs.clone(),
 417                    node_runtime: node_runtime.clone(),
 418                    project_environment: project_environment.clone(),
 419                    custom_command: new_settings
 420                        .gemini
 421                        .clone()
 422                        .and_then(|settings| settings.custom_command()),
 423                    settings_env: new_settings
 424                        .gemini
 425                        .as_ref()
 426                        .and_then(|settings| settings.env.clone()),
 427                    ignore_system_version: new_settings
 428                        .gemini
 429                        .as_ref()
 430                        .and_then(|settings| settings.ignore_system_version)
 431                        .unwrap_or(true),
 432                }),
 433                ExternalAgentSource::Builtin,
 434                None,
 435                None,
 436            ),
 437        );
 438        self.external_agents.insert(
 439            CODEX_NAME.into(),
 440            ExternalAgentEntry::new(
 441                Box::new(LocalCodex {
 442                    fs: fs.clone(),
 443                    project_environment: project_environment.clone(),
 444                    custom_command: new_settings
 445                        .codex
 446                        .clone()
 447                        .and_then(|settings| settings.custom_command()),
 448                    settings_env: new_settings
 449                        .codex
 450                        .as_ref()
 451                        .and_then(|settings| settings.env.clone()),
 452                    http_client: http_client.clone(),
 453                    no_browser: downstream_client
 454                        .as_ref()
 455                        .is_some_and(|(_, client)| !client.has_wsl_interop()),
 456                }),
 457                ExternalAgentSource::Builtin,
 458                None,
 459                None,
 460            ),
 461        );
 462        self.external_agents.insert(
 463            CLAUDE_AGENT_NAME.into(),
 464            ExternalAgentEntry::new(
 465                Box::new(LocalClaudeCode {
 466                    fs: fs.clone(),
 467                    node_runtime: node_runtime.clone(),
 468                    project_environment: project_environment.clone(),
 469                    custom_command: new_settings
 470                        .claude
 471                        .clone()
 472                        .and_then(|settings| settings.custom_command()),
 473                    settings_env: new_settings
 474                        .claude
 475                        .as_ref()
 476                        .and_then(|settings| settings.env.clone()),
 477                }),
 478                ExternalAgentSource::Builtin,
 479                None,
 480                None,
 481            ),
 482        );
 483
 484        let registry_store = AgentRegistryStore::try_global(cx);
 485        let registry_agents_by_id = registry_store
 486            .as_ref()
 487            .map(|store| {
 488                store
 489                    .read(cx)
 490                    .agents()
 491                    .iter()
 492                    .cloned()
 493                    .map(|agent| (agent.id().to_string(), agent))
 494                    .collect::<HashMap<_, _>>()
 495            })
 496            .unwrap_or_default();
 497
 498        // Insert extension agents before custom/registry so registry entries override extensions.
 499        for (agent_name, ext_id, targets, env, icon_path, display_name) in extension_agents.iter() {
 500            let name = ExternalAgentServerName(agent_name.clone().into());
 501            let mut env = env.clone();
 502            if let Some(settings_env) =
 503                new_settings
 504                    .custom
 505                    .get(agent_name.as_ref())
 506                    .and_then(|settings| match settings {
 507                        CustomAgentServerSettings::Extension { env, .. } => Some(env.clone()),
 508                        _ => None,
 509                    })
 510            {
 511                env.extend(settings_env);
 512            }
 513            let icon = icon_path
 514                .as_ref()
 515                .map(|path| SharedString::from(path.clone()));
 516
 517            self.external_agents.insert(
 518                name.clone(),
 519                ExternalAgentEntry::new(
 520                    Box::new(LocalExtensionArchiveAgent {
 521                        fs: fs.clone(),
 522                        http_client: http_client.clone(),
 523                        node_runtime: node_runtime.clone(),
 524                        project_environment: project_environment.clone(),
 525                        extension_id: Arc::from(&**ext_id),
 526                        targets: targets.clone(),
 527                        env,
 528                        agent_id: agent_name.clone(),
 529                    }) as Box<dyn ExternalAgentServer>,
 530                    ExternalAgentSource::Extension,
 531                    icon,
 532                    display_name.clone(),
 533                ),
 534            );
 535        }
 536
 537        for (name, settings) in &new_settings.custom {
 538            match settings {
 539                CustomAgentServerSettings::Custom { command, .. } => {
 540                    let agent_name = ExternalAgentServerName(name.clone().into());
 541                    self.external_agents.insert(
 542                        agent_name.clone(),
 543                        ExternalAgentEntry::new(
 544                            Box::new(LocalCustomAgent {
 545                                command: command.clone(),
 546                                project_environment: project_environment.clone(),
 547                            }) as Box<dyn ExternalAgentServer>,
 548                            ExternalAgentSource::Custom,
 549                            None,
 550                            None,
 551                        ),
 552                    );
 553                }
 554                CustomAgentServerSettings::Registry { env, .. } => {
 555                    let Some(agent) = registry_agents_by_id.get(name) else {
 556                        if registry_store.is_some() {
 557                            log::warn!("Registry agent '{}' not found in ACP registry", name);
 558                        }
 559                        continue;
 560                    };
 561
 562                    let agent_name = ExternalAgentServerName(name.clone().into());
 563                    match agent {
 564                        RegistryAgent::Binary(agent) => {
 565                            if !agent.supports_current_platform {
 566                                log::warn!(
 567                                    "Registry agent '{}' has no compatible binary for this platform",
 568                                    name
 569                                );
 570                                continue;
 571                            }
 572
 573                            self.external_agents.insert(
 574                                agent_name.clone(),
 575                                ExternalAgentEntry::new(
 576                                    Box::new(LocalRegistryArchiveAgent {
 577                                        fs: fs.clone(),
 578                                        http_client: http_client.clone(),
 579                                        node_runtime: node_runtime.clone(),
 580                                        project_environment: project_environment.clone(),
 581                                        registry_id: Arc::from(name.as_str()),
 582                                        targets: agent.targets.clone(),
 583                                        env: env.clone(),
 584                                    })
 585                                        as Box<dyn ExternalAgentServer>,
 586                                    ExternalAgentSource::Registry,
 587                                    agent.metadata.icon_path.clone(),
 588                                    Some(agent.metadata.name.clone()),
 589                                ),
 590                            );
 591                        }
 592                        RegistryAgent::Npx(agent) => {
 593                            self.external_agents.insert(
 594                                agent_name.clone(),
 595                                ExternalAgentEntry::new(
 596                                    Box::new(LocalRegistryNpxAgent {
 597                                        node_runtime: node_runtime.clone(),
 598                                        project_environment: project_environment.clone(),
 599                                        package: agent.package.clone(),
 600                                        args: agent.args.clone(),
 601                                        distribution_env: agent.env.clone(),
 602                                        settings_env: env.clone(),
 603                                    })
 604                                        as Box<dyn ExternalAgentServer>,
 605                                    ExternalAgentSource::Registry,
 606                                    agent.metadata.icon_path.clone(),
 607                                    Some(agent.metadata.name.clone()),
 608                                ),
 609                            );
 610                        }
 611                    }
 612                }
 613                CustomAgentServerSettings::Extension { .. } => {}
 614            }
 615        }
 616
 617        *old_settings = Some(new_settings);
 618
 619        if let Some((project_id, downstream_client)) = downstream_client {
 620            downstream_client
 621                .send(proto::ExternalAgentsUpdated {
 622                    project_id: *project_id,
 623                    names: self
 624                        .external_agents
 625                        .keys()
 626                        .map(|name| name.to_string())
 627                        .collect(),
 628                })
 629                .log_err();
 630        }
 631        cx.emit(AgentServersUpdated);
 632    }
 633
 634    pub fn node_runtime(&self) -> Option<NodeRuntime> {
 635        match &self.state {
 636            AgentServerStoreState::Local { node_runtime, .. } => Some(node_runtime.clone()),
 637            _ => None,
 638        }
 639    }
 640
 641    pub fn local(
 642        node_runtime: NodeRuntime,
 643        fs: Arc<dyn Fs>,
 644        project_environment: Entity<ProjectEnvironment>,
 645        http_client: Arc<dyn HttpClient>,
 646        cx: &mut Context<Self>,
 647    ) -> Self {
 648        let mut subscriptions = vec![cx.observe_global::<SettingsStore>(|this, cx| {
 649            this.agent_servers_settings_changed(cx);
 650        })];
 651        if let Some(registry_store) = AgentRegistryStore::try_global(cx) {
 652            subscriptions.push(cx.observe(&registry_store, |this, _, cx| {
 653                this.reregister_agents(cx);
 654            }));
 655        }
 656        let mut this = Self {
 657            state: AgentServerStoreState::Local {
 658                node_runtime,
 659                fs,
 660                project_environment,
 661                http_client,
 662                downstream_client: None,
 663                settings: None,
 664                extension_agents: vec![],
 665                _subscriptions: subscriptions,
 666            },
 667            external_agents: Default::default(),
 668        };
 669        if let Some(_events) = extension::ExtensionEvents::try_global(cx) {}
 670        this.agent_servers_settings_changed(cx);
 671        this
 672    }
 673
 674    pub(crate) fn remote(project_id: u64, upstream_client: Entity<RemoteClient>) -> Self {
 675        // Set up the builtin agents here so they're immediately available in
 676        // remote projects--we know that the HeadlessProject on the other end
 677        // will have them.
 678        let external_agents: [(ExternalAgentServerName, ExternalAgentEntry); 3] = [
 679            (
 680                CLAUDE_AGENT_NAME.into(),
 681                ExternalAgentEntry::new(
 682                    Box::new(RemoteExternalAgentServer {
 683                        project_id,
 684                        upstream_client: upstream_client.clone(),
 685                        name: CLAUDE_AGENT_NAME.into(),
 686                        status_tx: None,
 687                        new_version_available_tx: None,
 688                    }) as Box<dyn ExternalAgentServer>,
 689                    ExternalAgentSource::Builtin,
 690                    None,
 691                    None,
 692                ),
 693            ),
 694            (
 695                CODEX_NAME.into(),
 696                ExternalAgentEntry::new(
 697                    Box::new(RemoteExternalAgentServer {
 698                        project_id,
 699                        upstream_client: upstream_client.clone(),
 700                        name: CODEX_NAME.into(),
 701                        status_tx: None,
 702                        new_version_available_tx: None,
 703                    }) as Box<dyn ExternalAgentServer>,
 704                    ExternalAgentSource::Builtin,
 705                    None,
 706                    None,
 707                ),
 708            ),
 709            (
 710                GEMINI_NAME.into(),
 711                ExternalAgentEntry::new(
 712                    Box::new(RemoteExternalAgentServer {
 713                        project_id,
 714                        upstream_client: upstream_client.clone(),
 715                        name: GEMINI_NAME.into(),
 716                        status_tx: None,
 717                        new_version_available_tx: None,
 718                    }) as Box<dyn ExternalAgentServer>,
 719                    ExternalAgentSource::Builtin,
 720                    None,
 721                    None,
 722                ),
 723            ),
 724        ];
 725
 726        Self {
 727            state: AgentServerStoreState::Remote {
 728                project_id,
 729                upstream_client,
 730            },
 731            external_agents: external_agents.into_iter().collect(),
 732        }
 733    }
 734
 735    pub fn collab() -> Self {
 736        Self {
 737            state: AgentServerStoreState::Collab,
 738            external_agents: Default::default(),
 739        }
 740    }
 741
 742    pub fn shared(&mut self, project_id: u64, client: AnyProtoClient, cx: &mut Context<Self>) {
 743        match &mut self.state {
 744            AgentServerStoreState::Local {
 745                downstream_client, ..
 746            } => {
 747                *downstream_client = Some((project_id, client.clone()));
 748                // Send the current list of external agents downstream, but only after a delay,
 749                // to avoid having the message arrive before the downstream project's agent server store
 750                // sets up its handlers.
 751                cx.spawn(async move |this, cx| {
 752                    cx.background_executor().timer(Duration::from_secs(1)).await;
 753                    let names = this.update(cx, |this, _| {
 754                        this.external_agents()
 755                            .map(|name| name.to_string())
 756                            .collect()
 757                    })?;
 758                    client
 759                        .send(proto::ExternalAgentsUpdated { project_id, names })
 760                        .log_err();
 761                    anyhow::Ok(())
 762                })
 763                .detach();
 764            }
 765            AgentServerStoreState::Remote { .. } => {
 766                debug_panic!(
 767                    "external agents over collab not implemented, remote project should not be shared"
 768                );
 769            }
 770            AgentServerStoreState::Collab => {
 771                debug_panic!("external agents over collab not implemented, should not be shared");
 772            }
 773        }
 774    }
 775
 776    pub fn get_external_agent(
 777        &mut self,
 778        name: &ExternalAgentServerName,
 779    ) -> Option<&mut (dyn ExternalAgentServer + 'static)> {
 780        self.external_agents
 781            .get_mut(name)
 782            .map(|entry| entry.server.as_mut())
 783    }
 784
 785    pub fn external_agents(&self) -> impl Iterator<Item = &ExternalAgentServerName> {
 786        self.external_agents.keys()
 787    }
 788
 789    async fn handle_get_agent_server_command(
 790        this: Entity<Self>,
 791        envelope: TypedEnvelope<proto::GetAgentServerCommand>,
 792        mut cx: AsyncApp,
 793    ) -> Result<proto::AgentServerCommand> {
 794        let (command, root_dir, login_command) = this
 795            .update(&mut cx, |this, cx| {
 796                let AgentServerStoreState::Local {
 797                    downstream_client, ..
 798                } = &this.state
 799                else {
 800                    debug_panic!("should not receive GetAgentServerCommand in a non-local project");
 801                    bail!("unexpected GetAgentServerCommand request in a non-local project");
 802                };
 803                let agent = this
 804                    .external_agents
 805                    .get_mut(&*envelope.payload.name)
 806                    .map(|entry| entry.server.as_mut())
 807                    .with_context(|| format!("agent `{}` not found", envelope.payload.name))?;
 808                let (status_tx, new_version_available_tx) = downstream_client
 809                    .clone()
 810                    .map(|(project_id, downstream_client)| {
 811                        let (status_tx, mut status_rx) = watch::channel(SharedString::from(""));
 812                        let (new_version_available_tx, mut new_version_available_rx) =
 813                            watch::channel(None);
 814                        cx.spawn({
 815                            let downstream_client = downstream_client.clone();
 816                            let name = envelope.payload.name.clone();
 817                            async move |_, _| {
 818                                while let Some(status) = status_rx.recv().await.ok() {
 819                                    downstream_client.send(
 820                                        proto::ExternalAgentLoadingStatusUpdated {
 821                                            project_id,
 822                                            name: name.clone(),
 823                                            status: status.to_string(),
 824                                        },
 825                                    )?;
 826                                }
 827                                anyhow::Ok(())
 828                            }
 829                        })
 830                        .detach_and_log_err(cx);
 831                        cx.spawn({
 832                            let name = envelope.payload.name.clone();
 833                            async move |_, _| {
 834                                if let Some(version) =
 835                                    new_version_available_rx.recv().await.ok().flatten()
 836                                {
 837                                    downstream_client.send(
 838                                        proto::NewExternalAgentVersionAvailable {
 839                                            project_id,
 840                                            name: name.clone(),
 841                                            version,
 842                                        },
 843                                    )?;
 844                                }
 845                                anyhow::Ok(())
 846                            }
 847                        })
 848                        .detach_and_log_err(cx);
 849                        (status_tx, new_version_available_tx)
 850                    })
 851                    .unzip();
 852                anyhow::Ok(agent.get_command(
 853                    envelope.payload.root_dir.as_deref(),
 854                    HashMap::default(),
 855                    status_tx,
 856                    new_version_available_tx,
 857                    &mut cx.to_async(),
 858                ))
 859            })?
 860            .await?;
 861        Ok(proto::AgentServerCommand {
 862            path: command.path.to_string_lossy().into_owned(),
 863            args: command.args,
 864            env: command
 865                .env
 866                .map(|env| env.into_iter().collect())
 867                .unwrap_or_default(),
 868            root_dir: root_dir,
 869            login: login_command.map(|cmd| cmd.to_proto()),
 870        })
 871    }
 872
 873    async fn handle_external_agents_updated(
 874        this: Entity<Self>,
 875        envelope: TypedEnvelope<proto::ExternalAgentsUpdated>,
 876        mut cx: AsyncApp,
 877    ) -> Result<()> {
 878        this.update(&mut cx, |this, cx| {
 879            let AgentServerStoreState::Remote {
 880                project_id,
 881                upstream_client,
 882            } = &this.state
 883            else {
 884                debug_panic!(
 885                    "handle_external_agents_updated should not be called for a non-remote project"
 886                );
 887                bail!("unexpected ExternalAgentsUpdated message")
 888            };
 889
 890            let mut previous_entries = std::mem::take(&mut this.external_agents);
 891            let mut status_txs = HashMap::default();
 892            let mut new_version_available_txs = HashMap::default();
 893            let mut metadata = HashMap::default();
 894
 895            for (name, mut entry) in previous_entries.drain() {
 896                if let Some(agent) = entry.server.downcast_mut::<RemoteExternalAgentServer>() {
 897                    status_txs.insert(name.clone(), agent.status_tx.take());
 898                    new_version_available_txs
 899                        .insert(name.clone(), agent.new_version_available_tx.take());
 900                }
 901
 902                metadata.insert(name, (entry.icon, entry.display_name, entry.source));
 903            }
 904
 905            this.external_agents = envelope
 906                .payload
 907                .names
 908                .into_iter()
 909                .map(|name| {
 910                    let agent_name = ExternalAgentServerName(name.clone().into());
 911                    let fallback_source =
 912                        if name == GEMINI_NAME || name == CLAUDE_AGENT_NAME || name == CODEX_NAME {
 913                            ExternalAgentSource::Builtin
 914                        } else {
 915                            ExternalAgentSource::Custom
 916                        };
 917                    let (icon, display_name, source) =
 918                        metadata
 919                            .remove(&agent_name)
 920                            .unwrap_or((None, None, fallback_source));
 921                    let source = if fallback_source == ExternalAgentSource::Builtin {
 922                        ExternalAgentSource::Builtin
 923                    } else {
 924                        source
 925                    };
 926                    let agent = RemoteExternalAgentServer {
 927                        project_id: *project_id,
 928                        upstream_client: upstream_client.clone(),
 929                        name: agent_name.clone(),
 930                        status_tx: status_txs.remove(&agent_name).flatten(),
 931                        new_version_available_tx: new_version_available_txs
 932                            .remove(&agent_name)
 933                            .flatten(),
 934                    };
 935                    (
 936                        agent_name,
 937                        ExternalAgentEntry::new(
 938                            Box::new(agent) as Box<dyn ExternalAgentServer>,
 939                            source,
 940                            icon,
 941                            display_name,
 942                        ),
 943                    )
 944                })
 945                .collect();
 946            cx.emit(AgentServersUpdated);
 947            Ok(())
 948        })
 949    }
 950
 951    async fn handle_external_extension_agents_updated(
 952        this: Entity<Self>,
 953        envelope: TypedEnvelope<proto::ExternalExtensionAgentsUpdated>,
 954        mut cx: AsyncApp,
 955    ) -> Result<()> {
 956        this.update(&mut cx, |this, cx| {
 957            let AgentServerStoreState::Local {
 958                extension_agents, ..
 959            } = &mut this.state
 960            else {
 961                panic!(
 962                    "handle_external_extension_agents_updated \
 963                    should not be called for a non-remote project"
 964                );
 965            };
 966
 967            for ExternalExtensionAgent {
 968                name,
 969                icon_path,
 970                extension_id,
 971                targets,
 972                env,
 973            } in envelope.payload.agents
 974            {
 975                extension_agents.push((
 976                    Arc::from(&*name),
 977                    extension_id,
 978                    targets
 979                        .into_iter()
 980                        .map(|(k, v)| (k, extension::TargetConfig::from_proto(v)))
 981                        .collect(),
 982                    env.into_iter().collect(),
 983                    icon_path,
 984                    None,
 985                ));
 986            }
 987
 988            this.reregister_agents(cx);
 989            cx.emit(AgentServersUpdated);
 990            Ok(())
 991        })
 992    }
 993
 994    async fn handle_loading_status_updated(
 995        this: Entity<Self>,
 996        envelope: TypedEnvelope<proto::ExternalAgentLoadingStatusUpdated>,
 997        mut cx: AsyncApp,
 998    ) -> Result<()> {
 999        this.update(&mut cx, |this, _| {
1000            if let Some(agent) = this.external_agents.get_mut(&*envelope.payload.name)
1001                && let Some(agent) = agent.server.downcast_mut::<RemoteExternalAgentServer>()
1002                && let Some(status_tx) = &mut agent.status_tx
1003            {
1004                status_tx.send(envelope.payload.status.into()).ok();
1005            }
1006        });
1007        Ok(())
1008    }
1009
1010    async fn handle_new_version_available(
1011        this: Entity<Self>,
1012        envelope: TypedEnvelope<proto::NewExternalAgentVersionAvailable>,
1013        mut cx: AsyncApp,
1014    ) -> Result<()> {
1015        this.update(&mut cx, |this, _| {
1016            if let Some(agent) = this.external_agents.get_mut(&*envelope.payload.name)
1017                && let Some(agent) = agent.server.downcast_mut::<RemoteExternalAgentServer>()
1018                && let Some(new_version_available_tx) = &mut agent.new_version_available_tx
1019            {
1020                new_version_available_tx
1021                    .send(Some(envelope.payload.version))
1022                    .ok();
1023            }
1024        });
1025        Ok(())
1026    }
1027
1028    pub fn get_extension_id_for_agent(
1029        &mut self,
1030        name: &ExternalAgentServerName,
1031    ) -> Option<Arc<str>> {
1032        self.external_agents.get_mut(name).and_then(|entry| {
1033            entry
1034                .server
1035                .as_any_mut()
1036                .downcast_ref::<LocalExtensionArchiveAgent>()
1037                .map(|ext_agent| ext_agent.extension_id.clone())
1038        })
1039    }
1040}
1041
1042fn get_or_npm_install_builtin_agent(
1043    binary_name: SharedString,
1044    package_name: SharedString,
1045    entrypoint_path: PathBuf,
1046    minimum_version: Option<semver::Version>,
1047    status_tx: Option<watch::Sender<SharedString>>,
1048    new_version_available: Option<watch::Sender<Option<String>>>,
1049    fs: Arc<dyn Fs>,
1050    node_runtime: NodeRuntime,
1051    cx: &mut AsyncApp,
1052) -> Task<std::result::Result<AgentServerCommand, anyhow::Error>> {
1053    cx.spawn(async move |cx| {
1054        let node_path = node_runtime.binary_path().await?;
1055        let dir = paths::external_agents_dir().join(binary_name.as_str());
1056        fs.create_dir(&dir).await?;
1057
1058        let mut stream = fs.read_dir(&dir).await?;
1059        let mut versions = Vec::new();
1060        let mut to_delete = Vec::new();
1061        while let Some(entry) = stream.next().await {
1062            let Ok(entry) = entry else { continue };
1063            let Some(file_name) = entry.file_name() else {
1064                continue;
1065            };
1066
1067            if let Some(name) = file_name.to_str()
1068                && let Some(version) = semver::Version::from_str(name).ok()
1069                && fs
1070                    .is_file(&dir.join(file_name).join(&entrypoint_path))
1071                    .await
1072            {
1073                versions.push((version, file_name.to_owned()));
1074            } else {
1075                to_delete.push(file_name.to_owned())
1076            }
1077        }
1078
1079        versions.sort();
1080        let newest_version = if let Some((version, _)) = versions.last().cloned()
1081            && minimum_version.is_none_or(|minimum_version| version >= minimum_version)
1082        {
1083            versions.pop()
1084        } else {
1085            None
1086        };
1087        log::debug!("existing version of {package_name}: {newest_version:?}");
1088        to_delete.extend(versions.into_iter().map(|(_, file_name)| file_name));
1089
1090        cx.background_spawn({
1091            let fs = fs.clone();
1092            let dir = dir.clone();
1093            async move {
1094                for file_name in to_delete {
1095                    fs.remove_dir(
1096                        &dir.join(file_name),
1097                        RemoveOptions {
1098                            recursive: true,
1099                            ignore_if_not_exists: false,
1100                        },
1101                    )
1102                    .await
1103                    .ok();
1104                }
1105            }
1106        })
1107        .detach();
1108
1109        let version = if let Some((version, file_name)) = newest_version {
1110            cx.background_spawn({
1111                let dir = dir.clone();
1112                let fs = fs.clone();
1113                async move {
1114                    let latest_version = node_runtime
1115                        .npm_package_latest_version(&package_name)
1116                        .await
1117                        .ok();
1118                    if let Some(latest_version) = latest_version
1119                        && latest_version != version
1120                    {
1121                        let download_result = download_latest_version(
1122                            fs,
1123                            dir.clone(),
1124                            node_runtime,
1125                            package_name.clone(),
1126                        )
1127                        .await
1128                        .log_err();
1129                        if let Some(mut new_version_available) = new_version_available
1130                            && download_result.is_some()
1131                        {
1132                            new_version_available
1133                                .send(Some(latest_version.to_string()))
1134                                .ok();
1135                        }
1136                    }
1137                }
1138            })
1139            .detach();
1140            file_name
1141        } else {
1142            if let Some(mut status_tx) = status_tx {
1143                status_tx.send("Installing…".into()).ok();
1144            }
1145            let dir = dir.clone();
1146            cx.background_spawn(download_latest_version(
1147                fs.clone(),
1148                dir.clone(),
1149                node_runtime,
1150                package_name.clone(),
1151            ))
1152            .await?
1153            .to_string()
1154            .into()
1155        };
1156
1157        let agent_server_path = dir.join(version).join(entrypoint_path);
1158        let agent_server_path_exists = fs.is_file(&agent_server_path).await;
1159        anyhow::ensure!(
1160            agent_server_path_exists,
1161            "Missing entrypoint path {} after installation",
1162            agent_server_path.to_string_lossy()
1163        );
1164
1165        anyhow::Ok(AgentServerCommand {
1166            path: node_path,
1167            args: vec![agent_server_path.to_string_lossy().into_owned()],
1168            env: None,
1169        })
1170    })
1171}
1172
1173fn find_bin_in_path(
1174    bin_name: SharedString,
1175    root_dir: PathBuf,
1176    env: HashMap<String, String>,
1177    cx: &mut AsyncApp,
1178) -> Task<Option<PathBuf>> {
1179    cx.background_executor().spawn(async move {
1180        let which_result = if cfg!(windows) {
1181            which::which(bin_name.as_str())
1182        } else {
1183            let shell_path = env.get("PATH").cloned();
1184            which::which_in(bin_name.as_str(), shell_path.as_ref(), &root_dir)
1185        };
1186
1187        if let Err(which::Error::CannotFindBinaryPath) = which_result {
1188            return None;
1189        }
1190
1191        which_result.log_err()
1192    })
1193}
1194
1195async fn download_latest_version(
1196    fs: Arc<dyn Fs>,
1197    dir: PathBuf,
1198    node_runtime: NodeRuntime,
1199    package_name: SharedString,
1200) -> Result<Version> {
1201    log::debug!("downloading latest version of {package_name}");
1202
1203    let tmp_dir = tempfile::tempdir_in(&dir)?;
1204
1205    node_runtime
1206        .npm_install_packages(tmp_dir.path(), &[(&package_name, "latest")])
1207        .await?;
1208
1209    let version = node_runtime
1210        .npm_package_installed_version(tmp_dir.path(), &package_name)
1211        .await?
1212        .context("expected package to be installed")?;
1213
1214    fs.rename(
1215        &tmp_dir.keep(),
1216        &dir.join(version.to_string()),
1217        RenameOptions {
1218            ignore_if_exists: true,
1219            overwrite: true,
1220            create_parents: false,
1221        },
1222    )
1223    .await?;
1224
1225    anyhow::Ok(version)
1226}
1227
1228struct RemoteExternalAgentServer {
1229    project_id: u64,
1230    upstream_client: Entity<RemoteClient>,
1231    name: ExternalAgentServerName,
1232    status_tx: Option<watch::Sender<SharedString>>,
1233    new_version_available_tx: Option<watch::Sender<Option<String>>>,
1234}
1235
1236impl ExternalAgentServer for RemoteExternalAgentServer {
1237    fn get_command(
1238        &mut self,
1239        root_dir: Option<&str>,
1240        extra_env: HashMap<String, String>,
1241        status_tx: Option<watch::Sender<SharedString>>,
1242        new_version_available_tx: Option<watch::Sender<Option<String>>>,
1243        cx: &mut AsyncApp,
1244    ) -> Task<Result<(AgentServerCommand, String, Option<task::SpawnInTerminal>)>> {
1245        let project_id = self.project_id;
1246        let name = self.name.to_string();
1247        let upstream_client = self.upstream_client.downgrade();
1248        let root_dir = root_dir.map(|root_dir| root_dir.to_owned());
1249        self.status_tx = status_tx;
1250        self.new_version_available_tx = new_version_available_tx;
1251        cx.spawn(async move |cx| {
1252            let mut response = upstream_client
1253                .update(cx, |upstream_client, _| {
1254                    upstream_client
1255                        .proto_client()
1256                        .request(proto::GetAgentServerCommand {
1257                            project_id,
1258                            name,
1259                            root_dir: root_dir.clone(),
1260                        })
1261                })?
1262                .await?;
1263            let root_dir = response.root_dir;
1264            response.env.extend(extra_env);
1265            let command = upstream_client.update(cx, |client, _| {
1266                client.build_command_with_options(
1267                    Some(response.path),
1268                    &response.args,
1269                    &response.env.into_iter().collect(),
1270                    Some(root_dir.clone()),
1271                    None,
1272                    Interactive::No,
1273                )
1274            })??;
1275            Ok((
1276                AgentServerCommand {
1277                    path: command.program.into(),
1278                    args: command.args,
1279                    env: Some(command.env),
1280                },
1281                root_dir,
1282                response.login.map(SpawnInTerminal::from_proto),
1283            ))
1284        })
1285    }
1286
1287    fn as_any_mut(&mut self) -> &mut dyn Any {
1288        self
1289    }
1290}
1291
1292struct LocalGemini {
1293    fs: Arc<dyn Fs>,
1294    node_runtime: NodeRuntime,
1295    project_environment: Entity<ProjectEnvironment>,
1296    custom_command: Option<AgentServerCommand>,
1297    settings_env: Option<HashMap<String, String>>,
1298    ignore_system_version: bool,
1299}
1300
1301impl ExternalAgentServer for LocalGemini {
1302    fn get_command(
1303        &mut self,
1304        root_dir: Option<&str>,
1305        extra_env: HashMap<String, String>,
1306        status_tx: Option<watch::Sender<SharedString>>,
1307        new_version_available_tx: Option<watch::Sender<Option<String>>>,
1308        cx: &mut AsyncApp,
1309    ) -> Task<Result<(AgentServerCommand, String, Option<task::SpawnInTerminal>)>> {
1310        let fs = self.fs.clone();
1311        let node_runtime = self.node_runtime.clone();
1312        let project_environment = self.project_environment.downgrade();
1313        let custom_command = self.custom_command.clone();
1314        let settings_env = self.settings_env.clone();
1315        let ignore_system_version = self.ignore_system_version;
1316        let root_dir: Arc<Path> = root_dir
1317            .map(|root_dir| Path::new(root_dir))
1318            .unwrap_or(paths::home_dir())
1319            .into();
1320
1321        cx.spawn(async move |cx| {
1322            let mut env = project_environment
1323                .update(cx, |project_environment, cx| {
1324                    project_environment.local_directory_environment(
1325                        &Shell::System,
1326                        root_dir.clone(),
1327                        cx,
1328                    )
1329                })?
1330                .await
1331                .unwrap_or_default();
1332
1333            env.extend(settings_env.unwrap_or_default());
1334
1335            let mut command = if let Some(mut custom_command) = custom_command {
1336                custom_command.env = Some(env);
1337                custom_command
1338            } else if !ignore_system_version
1339                && let Some(bin) =
1340                    find_bin_in_path("gemini".into(), root_dir.to_path_buf(), env.clone(), cx).await
1341            {
1342                AgentServerCommand {
1343                    path: bin,
1344                    args: Vec::new(),
1345                    env: Some(env),
1346                }
1347            } else {
1348                let mut command = get_or_npm_install_builtin_agent(
1349                    GEMINI_NAME.into(),
1350                    "@google/gemini-cli".into(),
1351                    "node_modules/@google/gemini-cli/dist/index.js".into(),
1352                    if cfg!(windows) {
1353                        // v0.8.x on Windows has a bug that causes the initialize request to hang forever
1354                        Some("0.9.0".parse().unwrap())
1355                    } else {
1356                        Some("0.2.1".parse().unwrap())
1357                    },
1358                    status_tx,
1359                    new_version_available_tx,
1360                    fs,
1361                    node_runtime,
1362                    cx,
1363                )
1364                .await?;
1365                command.env = Some(env);
1366                command
1367            };
1368
1369            // Gemini CLI doesn't seem to have a dedicated invocation for logging in--we just run it normally without any arguments.
1370            let login = task::SpawnInTerminal {
1371                command: Some(command.path.to_string_lossy().into_owned()),
1372                args: command.args.clone(),
1373                env: command.env.clone().unwrap_or_default(),
1374                label: "gemini /auth".into(),
1375                ..Default::default()
1376            };
1377
1378            command.env.get_or_insert_default().extend(extra_env);
1379            command.args.push("--experimental-acp".into());
1380            Ok((
1381                command,
1382                root_dir.to_string_lossy().into_owned(),
1383                Some(login),
1384            ))
1385        })
1386    }
1387
1388    fn as_any_mut(&mut self) -> &mut dyn Any {
1389        self
1390    }
1391}
1392
1393struct LocalClaudeCode {
1394    fs: Arc<dyn Fs>,
1395    node_runtime: NodeRuntime,
1396    project_environment: Entity<ProjectEnvironment>,
1397    custom_command: Option<AgentServerCommand>,
1398    settings_env: Option<HashMap<String, String>>,
1399}
1400
1401impl ExternalAgentServer for LocalClaudeCode {
1402    fn get_command(
1403        &mut self,
1404        root_dir: Option<&str>,
1405        extra_env: HashMap<String, String>,
1406        status_tx: Option<watch::Sender<SharedString>>,
1407        new_version_available_tx: Option<watch::Sender<Option<String>>>,
1408        cx: &mut AsyncApp,
1409    ) -> Task<Result<(AgentServerCommand, String, Option<task::SpawnInTerminal>)>> {
1410        let fs = self.fs.clone();
1411        let node_runtime = self.node_runtime.clone();
1412        let project_environment = self.project_environment.downgrade();
1413        let custom_command = self.custom_command.clone();
1414        let settings_env = self.settings_env.clone();
1415        let root_dir: Arc<Path> = root_dir
1416            .map(|root_dir| Path::new(root_dir))
1417            .unwrap_or(paths::home_dir())
1418            .into();
1419
1420        cx.spawn(async move |cx| {
1421            let mut env = project_environment
1422                .update(cx, |project_environment, cx| {
1423                    project_environment.local_directory_environment(
1424                        &Shell::System,
1425                        root_dir.clone(),
1426                        cx,
1427                    )
1428                })?
1429                .await
1430                .unwrap_or_default();
1431            env.insert("ANTHROPIC_API_KEY".into(), "".into());
1432
1433            env.extend(settings_env.unwrap_or_default());
1434
1435            let (mut command, login_command) = if let Some(mut custom_command) = custom_command {
1436                custom_command.env = Some(env);
1437                (custom_command, None)
1438            } else {
1439                let mut command = get_or_npm_install_builtin_agent(
1440                    "claude-agent-acp".into(),
1441                    "@zed-industries/claude-agent-acp".into(),
1442                    "node_modules/@zed-industries/claude-agent-acp/dist/index.js".into(),
1443                    Some("0.17.0".parse().unwrap()),
1444                    status_tx,
1445                    new_version_available_tx,
1446                    fs,
1447                    node_runtime,
1448                    cx,
1449                )
1450                .await?;
1451                command.env = Some(env);
1452
1453                (command, None)
1454            };
1455
1456            command.env.get_or_insert_default().extend(extra_env);
1457            Ok((
1458                command,
1459                root_dir.to_string_lossy().into_owned(),
1460                login_command,
1461            ))
1462        })
1463    }
1464
1465    fn as_any_mut(&mut self) -> &mut dyn Any {
1466        self
1467    }
1468}
1469
1470struct LocalCodex {
1471    fs: Arc<dyn Fs>,
1472    project_environment: Entity<ProjectEnvironment>,
1473    http_client: Arc<dyn HttpClient>,
1474    custom_command: Option<AgentServerCommand>,
1475    settings_env: Option<HashMap<String, String>>,
1476    no_browser: bool,
1477}
1478
1479impl ExternalAgentServer for LocalCodex {
1480    fn get_command(
1481        &mut self,
1482        root_dir: Option<&str>,
1483        extra_env: HashMap<String, String>,
1484        mut status_tx: Option<watch::Sender<SharedString>>,
1485        _new_version_available_tx: Option<watch::Sender<Option<String>>>,
1486        cx: &mut AsyncApp,
1487    ) -> Task<Result<(AgentServerCommand, String, Option<task::SpawnInTerminal>)>> {
1488        let fs = self.fs.clone();
1489        let project_environment = self.project_environment.downgrade();
1490        let http = self.http_client.clone();
1491        let custom_command = self.custom_command.clone();
1492        let settings_env = self.settings_env.clone();
1493        let root_dir: Arc<Path> = root_dir
1494            .map(|root_dir| Path::new(root_dir))
1495            .unwrap_or(paths::home_dir())
1496            .into();
1497        let no_browser = self.no_browser;
1498
1499        cx.spawn(async move |cx| {
1500            let mut env = project_environment
1501                .update(cx, |project_environment, cx| {
1502                    project_environment.local_directory_environment(
1503                        &Shell::System,
1504                        root_dir.clone(),
1505                        cx,
1506                    )
1507                })?
1508                .await
1509                .unwrap_or_default();
1510            if no_browser {
1511                env.insert("NO_BROWSER".to_owned(), "1".to_owned());
1512            }
1513
1514            env.extend(settings_env.unwrap_or_default());
1515
1516            let mut command = if let Some(mut custom_command) = custom_command {
1517                custom_command.env = Some(env);
1518                custom_command
1519            } else {
1520                let dir = paths::external_agents_dir().join(CODEX_NAME);
1521                fs.create_dir(&dir).await?;
1522
1523                let bin_name = if cfg!(windows) {
1524                    "codex-acp.exe"
1525                } else {
1526                    "codex-acp"
1527                };
1528
1529                let find_latest_local_version = async || -> Option<PathBuf> {
1530                    let mut local_versions: Vec<(semver::Version, String)> = Vec::new();
1531                    let mut stream = fs.read_dir(&dir).await.ok()?;
1532                    while let Some(entry) = stream.next().await {
1533                        let Ok(entry) = entry else { continue };
1534                        let Some(file_name) = entry.file_name() else {
1535                            continue;
1536                        };
1537                        let version_path = dir.join(&file_name);
1538                        if fs.is_file(&version_path.join(bin_name)).await {
1539                            let version_str = file_name.to_string_lossy();
1540                            if let Ok(version) =
1541                                semver::Version::from_str(version_str.trim_start_matches('v'))
1542                            {
1543                                local_versions.push((version, version_str.into_owned()));
1544                            }
1545                        }
1546                    }
1547                    local_versions.sort_by(|(a, _), (b, _)| a.cmp(b));
1548                    local_versions.last().map(|(_, v)| dir.join(v))
1549                };
1550
1551                let fallback_to_latest_local_version =
1552                    async |err: anyhow::Error| -> Result<PathBuf, anyhow::Error> {
1553                        if let Some(local) = find_latest_local_version().await {
1554                            log::info!(
1555                                "Falling back to locally installed Codex version: {}",
1556                                local.display()
1557                            );
1558                            Ok(local)
1559                        } else {
1560                            Err(err)
1561                        }
1562                    };
1563
1564                let version_dir = match ::http_client::github::latest_github_release(
1565                    CODEX_ACP_REPO,
1566                    true,
1567                    false,
1568                    http.clone(),
1569                )
1570                .await
1571                {
1572                    Ok(release) => {
1573                        let version_dir = dir.join(&release.tag_name);
1574                        if !fs.is_dir(&version_dir).await {
1575                            if let Some(ref mut status_tx) = status_tx {
1576                                status_tx.send("Installing…".into()).ok();
1577                            }
1578
1579                            let tag = release.tag_name.clone();
1580                            let version_number = tag.trim_start_matches('v');
1581                            let asset_name = asset_name(version_number)
1582                                .context("codex acp is not supported for this architecture")?;
1583                            let asset = release
1584                                .assets
1585                                .into_iter()
1586                                .find(|asset| asset.name == asset_name)
1587                                .with_context(|| {
1588                                    format!("no asset found matching `{asset_name:?}`")
1589                                })?;
1590                            // Strip "sha256:" prefix from digest if present (GitHub API format)
1591                            let digest = asset
1592                                .digest
1593                                .as_deref()
1594                                .and_then(|d| d.strip_prefix("sha256:").or(Some(d)));
1595                            match ::http_client::github_download::download_server_binary(
1596                                &*http,
1597                                &asset.browser_download_url,
1598                                digest,
1599                                &version_dir,
1600                                if cfg!(target_os = "windows") && cfg!(target_arch = "x86_64") {
1601                                    AssetKind::Zip
1602                                } else {
1603                                    AssetKind::TarGz
1604                                },
1605                            )
1606                            .await
1607                            {
1608                                Ok(()) => {
1609                                    // remove older versions
1610                                    util::fs::remove_matching(&dir, |entry| entry != version_dir)
1611                                        .await;
1612                                    version_dir
1613                                }
1614                                Err(err) => {
1615                                    log::error!(
1616                                        "Failed to download Codex release {}: {err:#}",
1617                                        release.tag_name
1618                                    );
1619                                    fallback_to_latest_local_version(err).await?
1620                                }
1621                            }
1622                        } else {
1623                            version_dir
1624                        }
1625                    }
1626                    Err(err) => {
1627                        log::error!("Failed to fetch Codex latest release: {err:#}");
1628                        fallback_to_latest_local_version(err).await?
1629                    }
1630                };
1631
1632                let bin_path = version_dir.join(bin_name);
1633                anyhow::ensure!(
1634                    fs.is_file(&bin_path).await,
1635                    "Missing Codex binary at {} after installation",
1636                    bin_path.to_string_lossy()
1637                );
1638
1639                let mut cmd = AgentServerCommand {
1640                    path: bin_path,
1641                    args: Vec::new(),
1642                    env: None,
1643                };
1644                cmd.env = Some(env);
1645                cmd
1646            };
1647
1648            command.env.get_or_insert_default().extend(extra_env);
1649            Ok((command, root_dir.to_string_lossy().into_owned(), None))
1650        })
1651    }
1652
1653    fn as_any_mut(&mut self) -> &mut dyn Any {
1654        self
1655    }
1656}
1657
1658pub const CODEX_ACP_REPO: &str = "zed-industries/codex-acp";
1659
1660fn get_platform_info() -> Option<(&'static str, &'static str, &'static str)> {
1661    let arch = if cfg!(target_arch = "x86_64") {
1662        "x86_64"
1663    } else if cfg!(target_arch = "aarch64") {
1664        "aarch64"
1665    } else {
1666        return None;
1667    };
1668
1669    let platform = if cfg!(target_os = "macos") {
1670        "apple-darwin"
1671    } else if cfg!(target_os = "windows") {
1672        "pc-windows-msvc"
1673    } else if cfg!(target_os = "linux") {
1674        "unknown-linux-gnu"
1675    } else {
1676        return None;
1677    };
1678
1679    // Windows uses .zip in release assets
1680    let ext = if cfg!(target_os = "windows") {
1681        "zip"
1682    } else {
1683        "tar.gz"
1684    };
1685
1686    Some((arch, platform, ext))
1687}
1688
1689fn asset_name(version: &str) -> Option<String> {
1690    let (arch, platform, ext) = get_platform_info()?;
1691    Some(format!("codex-acp-{version}-{arch}-{platform}.{ext}"))
1692}
1693
1694pub struct LocalExtensionArchiveAgent {
1695    pub fs: Arc<dyn Fs>,
1696    pub http_client: Arc<dyn HttpClient>,
1697    pub node_runtime: NodeRuntime,
1698    pub project_environment: Entity<ProjectEnvironment>,
1699    pub extension_id: Arc<str>,
1700    pub agent_id: Arc<str>,
1701    pub targets: HashMap<String, extension::TargetConfig>,
1702    pub env: HashMap<String, String>,
1703}
1704
1705impl ExternalAgentServer for LocalExtensionArchiveAgent {
1706    fn get_command(
1707        &mut self,
1708        root_dir: Option<&str>,
1709        extra_env: HashMap<String, String>,
1710        _status_tx: Option<watch::Sender<SharedString>>,
1711        _new_version_available_tx: Option<watch::Sender<Option<String>>>,
1712        cx: &mut AsyncApp,
1713    ) -> Task<Result<(AgentServerCommand, String, Option<task::SpawnInTerminal>)>> {
1714        let fs = self.fs.clone();
1715        let http_client = self.http_client.clone();
1716        let node_runtime = self.node_runtime.clone();
1717        let project_environment = self.project_environment.downgrade();
1718        let extension_id = self.extension_id.clone();
1719        let agent_id = self.agent_id.clone();
1720        let targets = self.targets.clone();
1721        let base_env = self.env.clone();
1722
1723        let root_dir: Arc<Path> = root_dir
1724            .map(|root_dir| Path::new(root_dir))
1725            .unwrap_or(paths::home_dir())
1726            .into();
1727
1728        cx.spawn(async move |cx| {
1729            // Get project environment
1730            let mut env = project_environment
1731                .update(cx, |project_environment, cx| {
1732                    project_environment.local_directory_environment(
1733                        &Shell::System,
1734                        root_dir.clone(),
1735                        cx,
1736                    )
1737                })?
1738                .await
1739                .unwrap_or_default();
1740
1741            // Merge manifest env and extra env
1742            env.extend(base_env);
1743            env.extend(extra_env);
1744
1745            let cache_key = format!("{}/{}", extension_id, agent_id);
1746            let dir = paths::external_agents_dir().join(&cache_key);
1747            fs.create_dir(&dir).await?;
1748
1749            // Determine platform key
1750            let os = if cfg!(target_os = "macos") {
1751                "darwin"
1752            } else if cfg!(target_os = "linux") {
1753                "linux"
1754            } else if cfg!(target_os = "windows") {
1755                "windows"
1756            } else {
1757                anyhow::bail!("unsupported OS");
1758            };
1759
1760            let arch = if cfg!(target_arch = "aarch64") {
1761                "aarch64"
1762            } else if cfg!(target_arch = "x86_64") {
1763                "x86_64"
1764            } else {
1765                anyhow::bail!("unsupported architecture");
1766            };
1767
1768            let platform_key = format!("{}-{}", os, arch);
1769            let target_config = targets.get(&platform_key).with_context(|| {
1770                format!(
1771                    "no target specified for platform '{}'. Available platforms: {}",
1772                    platform_key,
1773                    targets
1774                        .keys()
1775                        .map(|k| k.as_str())
1776                        .collect::<Vec<_>>()
1777                        .join(", ")
1778                )
1779            })?;
1780
1781            let archive_url = &target_config.archive;
1782
1783            // Use URL as version identifier for caching
1784            // Hash the URL to get a stable directory name
1785            use std::collections::hash_map::DefaultHasher;
1786            use std::hash::{Hash, Hasher};
1787            let mut hasher = DefaultHasher::new();
1788            archive_url.hash(&mut hasher);
1789            let url_hash = hasher.finish();
1790            let version_dir = dir.join(format!("v_{:x}", url_hash));
1791
1792            if !fs.is_dir(&version_dir).await {
1793                // Determine SHA256 for verification
1794                let sha256 = if let Some(provided_sha) = &target_config.sha256 {
1795                    // Use provided SHA256
1796                    Some(provided_sha.clone())
1797                } else if archive_url.starts_with("https://github.com/") {
1798                    // Try to fetch SHA256 from GitHub API
1799                    // Parse URL to extract repo and tag/file info
1800                    // Format: https://github.com/owner/repo/releases/download/tag/file.zip
1801                    if let Some(caps) = archive_url.strip_prefix("https://github.com/") {
1802                        let parts: Vec<&str> = caps.split('/').collect();
1803                        if parts.len() >= 6 && parts[2] == "releases" && parts[3] == "download" {
1804                            let repo = format!("{}/{}", parts[0], parts[1]);
1805                            let tag = parts[4];
1806                            let filename = parts[5..].join("/");
1807
1808                            // Try to get release info from GitHub
1809                            if let Ok(release) = ::http_client::github::get_release_by_tag_name(
1810                                &repo,
1811                                tag,
1812                                http_client.clone(),
1813                            )
1814                            .await
1815                            {
1816                                // Find matching asset
1817                                if let Some(asset) =
1818                                    release.assets.iter().find(|a| a.name == filename)
1819                                {
1820                                    // Strip "sha256:" prefix if present
1821                                    asset.digest.as_ref().map(|d| {
1822                                        d.strip_prefix("sha256:")
1823                                            .map(|s| s.to_string())
1824                                            .unwrap_or_else(|| d.clone())
1825                                    })
1826                                } else {
1827                                    None
1828                                }
1829                            } else {
1830                                None
1831                            }
1832                        } else {
1833                            None
1834                        }
1835                    } else {
1836                        None
1837                    }
1838                } else {
1839                    None
1840                };
1841
1842                // Determine archive type from URL
1843                let asset_kind = if archive_url.ends_with(".zip") {
1844                    AssetKind::Zip
1845                } else if archive_url.ends_with(".tar.gz") || archive_url.ends_with(".tgz") {
1846                    AssetKind::TarGz
1847                } else {
1848                    anyhow::bail!("unsupported archive type in URL: {}", archive_url);
1849                };
1850
1851                // Download and extract
1852                ::http_client::github_download::download_server_binary(
1853                    &*http_client,
1854                    archive_url,
1855                    sha256.as_deref(),
1856                    &version_dir,
1857                    asset_kind,
1858                )
1859                .await?;
1860            }
1861
1862            // Validate and resolve cmd path
1863            let cmd = &target_config.cmd;
1864
1865            let cmd_path = if cmd == "node" {
1866                // Use Zed's managed Node.js runtime
1867                node_runtime.binary_path().await?
1868            } else {
1869                if cmd.contains("..") {
1870                    anyhow::bail!("command path cannot contain '..': {}", cmd);
1871                }
1872
1873                if cmd.starts_with("./") || cmd.starts_with(".\\") {
1874                    // Relative to extraction directory
1875                    let cmd_path = version_dir.join(&cmd[2..]);
1876                    anyhow::ensure!(
1877                        fs.is_file(&cmd_path).await,
1878                        "Missing command {} after extraction",
1879                        cmd_path.to_string_lossy()
1880                    );
1881                    cmd_path
1882                } else {
1883                    // On PATH
1884                    anyhow::bail!("command must be relative (start with './'): {}", cmd);
1885                }
1886            };
1887
1888            let command = AgentServerCommand {
1889                path: cmd_path,
1890                args: target_config.args.clone(),
1891                env: Some(env),
1892            };
1893
1894            Ok((command, version_dir.to_string_lossy().into_owned(), None))
1895        })
1896    }
1897
1898    fn as_any_mut(&mut self) -> &mut dyn Any {
1899        self
1900    }
1901}
1902
1903struct LocalRegistryArchiveAgent {
1904    fs: Arc<dyn Fs>,
1905    http_client: Arc<dyn HttpClient>,
1906    node_runtime: NodeRuntime,
1907    project_environment: Entity<ProjectEnvironment>,
1908    registry_id: Arc<str>,
1909    targets: HashMap<String, RegistryTargetConfig>,
1910    env: HashMap<String, String>,
1911}
1912
1913impl ExternalAgentServer for LocalRegistryArchiveAgent {
1914    fn get_command(
1915        &mut self,
1916        root_dir: Option<&str>,
1917        extra_env: HashMap<String, String>,
1918        _status_tx: Option<watch::Sender<SharedString>>,
1919        _new_version_available_tx: Option<watch::Sender<Option<String>>>,
1920        cx: &mut AsyncApp,
1921    ) -> Task<Result<(AgentServerCommand, String, Option<task::SpawnInTerminal>)>> {
1922        let fs = self.fs.clone();
1923        let http_client = self.http_client.clone();
1924        let node_runtime = self.node_runtime.clone();
1925        let project_environment = self.project_environment.downgrade();
1926        let registry_id = self.registry_id.clone();
1927        let targets = self.targets.clone();
1928        let settings_env = self.env.clone();
1929
1930        let root_dir: Arc<Path> = root_dir
1931            .map(|root_dir| Path::new(root_dir))
1932            .unwrap_or(paths::home_dir())
1933            .into();
1934
1935        cx.spawn(async move |cx| {
1936            let mut env = project_environment
1937                .update(cx, |project_environment, cx| {
1938                    project_environment.local_directory_environment(
1939                        &Shell::System,
1940                        root_dir.clone(),
1941                        cx,
1942                    )
1943                })?
1944                .await
1945                .unwrap_or_default();
1946
1947            let dir = paths::external_agents_dir()
1948                .join("registry")
1949                .join(registry_id.as_ref());
1950            fs.create_dir(&dir).await?;
1951
1952            let os = if cfg!(target_os = "macos") {
1953                "darwin"
1954            } else if cfg!(target_os = "linux") {
1955                "linux"
1956            } else if cfg!(target_os = "windows") {
1957                "windows"
1958            } else {
1959                anyhow::bail!("unsupported OS");
1960            };
1961
1962            let arch = if cfg!(target_arch = "aarch64") {
1963                "aarch64"
1964            } else if cfg!(target_arch = "x86_64") {
1965                "x86_64"
1966            } else {
1967                anyhow::bail!("unsupported architecture");
1968            };
1969
1970            let platform_key = format!("{}-{}", os, arch);
1971            let target_config = targets.get(&platform_key).with_context(|| {
1972                format!(
1973                    "no target specified for platform '{}'. Available platforms: {}",
1974                    platform_key,
1975                    targets
1976                        .keys()
1977                        .map(|k| k.as_str())
1978                        .collect::<Vec<_>>()
1979                        .join(", ")
1980                )
1981            })?;
1982
1983            env.extend(target_config.env.clone());
1984            env.extend(extra_env);
1985            env.extend(settings_env);
1986
1987            let archive_url = &target_config.archive;
1988
1989            use std::collections::hash_map::DefaultHasher;
1990            use std::hash::{Hash, Hasher};
1991            let mut hasher = DefaultHasher::new();
1992            archive_url.hash(&mut hasher);
1993            let url_hash = hasher.finish();
1994            let version_dir = dir.join(format!("v_{:x}", url_hash));
1995
1996            if !fs.is_dir(&version_dir).await {
1997                let sha256 = if let Some(provided_sha) = &target_config.sha256 {
1998                    Some(provided_sha.clone())
1999                } else if archive_url.starts_with("https://github.com/") {
2000                    if let Some(caps) = archive_url.strip_prefix("https://github.com/") {
2001                        let parts: Vec<&str> = caps.split('/').collect();
2002                        if parts.len() >= 6 && parts[2] == "releases" && parts[3] == "download" {
2003                            let repo = format!("{}/{}", parts[0], parts[1]);
2004                            let tag = parts[4];
2005                            let filename = parts[5..].join("/");
2006
2007                            if let Ok(release) = ::http_client::github::get_release_by_tag_name(
2008                                &repo,
2009                                tag,
2010                                http_client.clone(),
2011                            )
2012                            .await
2013                            {
2014                                if let Some(asset) =
2015                                    release.assets.iter().find(|a| a.name == filename)
2016                                {
2017                                    asset.digest.as_ref().and_then(|d| {
2018                                        d.strip_prefix("sha256:")
2019                                            .map(|s| s.to_string())
2020                                            .or_else(|| Some(d.clone()))
2021                                    })
2022                                } else {
2023                                    None
2024                                }
2025                            } else {
2026                                None
2027                            }
2028                        } else {
2029                            None
2030                        }
2031                    } else {
2032                        None
2033                    }
2034                } else {
2035                    None
2036                };
2037
2038                let asset_kind = if archive_url.ends_with(".zip") {
2039                    AssetKind::Zip
2040                } else if archive_url.ends_with(".tar.gz") || archive_url.ends_with(".tgz") {
2041                    AssetKind::TarGz
2042                } else {
2043                    anyhow::bail!("unsupported archive type in URL: {}", archive_url);
2044                };
2045
2046                ::http_client::github_download::download_server_binary(
2047                    &*http_client,
2048                    archive_url,
2049                    sha256.as_deref(),
2050                    &version_dir,
2051                    asset_kind,
2052                )
2053                .await?;
2054            }
2055
2056            let cmd = &target_config.cmd;
2057
2058            let cmd_path = if cmd == "node" {
2059                node_runtime.binary_path().await?
2060            } else {
2061                if cmd.contains("..") {
2062                    anyhow::bail!("command path cannot contain '..': {}", cmd);
2063                }
2064
2065                if cmd.starts_with("./") || cmd.starts_with(".\\") {
2066                    let cmd_path = version_dir.join(&cmd[2..]);
2067                    anyhow::ensure!(
2068                        fs.is_file(&cmd_path).await,
2069                        "Missing command {} after extraction",
2070                        cmd_path.to_string_lossy()
2071                    );
2072                    cmd_path
2073                } else {
2074                    anyhow::bail!("command must be relative (start with './'): {}", cmd);
2075                }
2076            };
2077
2078            let command = AgentServerCommand {
2079                path: cmd_path,
2080                args: target_config.args.clone(),
2081                env: Some(env),
2082            };
2083
2084            Ok((command, version_dir.to_string_lossy().into_owned(), None))
2085        })
2086    }
2087
2088    fn as_any_mut(&mut self) -> &mut dyn Any {
2089        self
2090    }
2091}
2092
2093struct LocalRegistryNpxAgent {
2094    node_runtime: NodeRuntime,
2095    project_environment: Entity<ProjectEnvironment>,
2096    package: SharedString,
2097    args: Vec<String>,
2098    distribution_env: HashMap<String, String>,
2099    settings_env: HashMap<String, String>,
2100}
2101
2102impl ExternalAgentServer for LocalRegistryNpxAgent {
2103    fn get_command(
2104        &mut self,
2105        root_dir: Option<&str>,
2106        extra_env: HashMap<String, String>,
2107        _status_tx: Option<watch::Sender<SharedString>>,
2108        _new_version_available_tx: Option<watch::Sender<Option<String>>>,
2109        cx: &mut AsyncApp,
2110    ) -> Task<Result<(AgentServerCommand, String, Option<task::SpawnInTerminal>)>> {
2111        let node_runtime = self.node_runtime.clone();
2112        let project_environment = self.project_environment.downgrade();
2113        let package = self.package.clone();
2114        let args = self.args.clone();
2115        let distribution_env = self.distribution_env.clone();
2116        let settings_env = self.settings_env.clone();
2117
2118        let env_root_dir: Arc<Path> = root_dir
2119            .map(|root_dir| Path::new(root_dir))
2120            .unwrap_or(paths::home_dir())
2121            .into();
2122
2123        cx.spawn(async move |cx| {
2124            let mut env = project_environment
2125                .update(cx, |project_environment, cx| {
2126                    project_environment.local_directory_environment(
2127                        &Shell::System,
2128                        env_root_dir.clone(),
2129                        cx,
2130                    )
2131                })?
2132                .await
2133                .unwrap_or_default();
2134
2135            let mut exec_args = Vec::new();
2136            exec_args.push("--yes".to_string());
2137            exec_args.push(package.to_string());
2138            if !args.is_empty() {
2139                exec_args.push("--".to_string());
2140                exec_args.extend(args);
2141            }
2142
2143            let npm_command = node_runtime
2144                .npm_command(
2145                    "exec",
2146                    &exec_args.iter().map(|a| a.as_str()).collect::<Vec<_>>(),
2147                )
2148                .await?;
2149
2150            env.extend(npm_command.env);
2151            env.extend(distribution_env);
2152            env.extend(extra_env);
2153            env.extend(settings_env);
2154
2155            let command = AgentServerCommand {
2156                path: npm_command.path,
2157                args: npm_command.args,
2158                env: Some(env),
2159            };
2160
2161            Ok((command, env_root_dir.to_string_lossy().into_owned(), None))
2162        })
2163    }
2164
2165    fn as_any_mut(&mut self) -> &mut dyn Any {
2166        self
2167    }
2168}
2169
2170struct LocalCustomAgent {
2171    project_environment: Entity<ProjectEnvironment>,
2172    command: AgentServerCommand,
2173}
2174
2175impl ExternalAgentServer for LocalCustomAgent {
2176    fn get_command(
2177        &mut self,
2178        root_dir: Option<&str>,
2179        extra_env: HashMap<String, String>,
2180        _status_tx: Option<watch::Sender<SharedString>>,
2181        _new_version_available_tx: Option<watch::Sender<Option<String>>>,
2182        cx: &mut AsyncApp,
2183    ) -> Task<Result<(AgentServerCommand, String, Option<task::SpawnInTerminal>)>> {
2184        let mut command = self.command.clone();
2185        let root_dir: Arc<Path> = root_dir
2186            .map(|root_dir| Path::new(root_dir))
2187            .unwrap_or(paths::home_dir())
2188            .into();
2189        let project_environment = self.project_environment.downgrade();
2190        cx.spawn(async move |cx| {
2191            let mut env = project_environment
2192                .update(cx, |project_environment, cx| {
2193                    project_environment.local_directory_environment(
2194                        &Shell::System,
2195                        root_dir.clone(),
2196                        cx,
2197                    )
2198                })?
2199                .await
2200                .unwrap_or_default();
2201            env.extend(command.env.unwrap_or_default());
2202            env.extend(extra_env);
2203            command.env = Some(env);
2204            Ok((command, root_dir.to_string_lossy().into_owned(), None))
2205        })
2206    }
2207
2208    fn as_any_mut(&mut self) -> &mut dyn Any {
2209        self
2210    }
2211}
2212
2213pub const GEMINI_NAME: &'static str = "gemini";
2214pub const CLAUDE_AGENT_NAME: &'static str = "claude";
2215pub const CODEX_NAME: &'static str = "codex";
2216
2217#[derive(Default, Clone, JsonSchema, Debug, PartialEq, RegisterSetting)]
2218pub struct AllAgentServersSettings {
2219    pub gemini: Option<BuiltinAgentServerSettings>,
2220    pub claude: Option<BuiltinAgentServerSettings>,
2221    pub codex: Option<BuiltinAgentServerSettings>,
2222    pub custom: HashMap<String, CustomAgentServerSettings>,
2223}
2224#[derive(Default, Clone, JsonSchema, Debug, PartialEq)]
2225pub struct BuiltinAgentServerSettings {
2226    pub path: Option<PathBuf>,
2227    pub args: Option<Vec<String>>,
2228    pub env: Option<HashMap<String, String>>,
2229    pub ignore_system_version: Option<bool>,
2230    pub default_mode: Option<String>,
2231    pub default_model: Option<String>,
2232    pub favorite_models: Vec<String>,
2233    pub default_config_options: HashMap<String, String>,
2234    pub favorite_config_option_values: HashMap<String, Vec<String>>,
2235}
2236
2237impl BuiltinAgentServerSettings {
2238    fn custom_command(self) -> Option<AgentServerCommand> {
2239        self.path.map(|path| AgentServerCommand {
2240            path,
2241            args: self.args.unwrap_or_default(),
2242            // Settings env are always applied, so we don't need to supply them here as well
2243            env: None,
2244        })
2245    }
2246}
2247
2248impl From<settings::BuiltinAgentServerSettings> for BuiltinAgentServerSettings {
2249    fn from(value: settings::BuiltinAgentServerSettings) -> Self {
2250        BuiltinAgentServerSettings {
2251            path: value
2252                .path
2253                .map(|p| PathBuf::from(shellexpand::tilde(&p.to_string_lossy()).as_ref())),
2254            args: value.args,
2255            env: value.env,
2256            ignore_system_version: value.ignore_system_version,
2257            default_mode: value.default_mode,
2258            default_model: value.default_model,
2259            favorite_models: value.favorite_models,
2260            default_config_options: value.default_config_options,
2261            favorite_config_option_values: value.favorite_config_option_values,
2262        }
2263    }
2264}
2265
2266impl From<AgentServerCommand> for BuiltinAgentServerSettings {
2267    fn from(value: AgentServerCommand) -> Self {
2268        BuiltinAgentServerSettings {
2269            path: Some(value.path),
2270            args: Some(value.args),
2271            env: value.env,
2272            ..Default::default()
2273        }
2274    }
2275}
2276
2277#[derive(Clone, JsonSchema, Debug, PartialEq)]
2278pub enum CustomAgentServerSettings {
2279    Custom {
2280        command: AgentServerCommand,
2281        /// The default mode to use for this agent.
2282        ///
2283        /// Note: Not only all agents support modes.
2284        ///
2285        /// Default: None
2286        default_mode: Option<String>,
2287        /// The default model to use for this agent.
2288        ///
2289        /// This should be the model ID as reported by the agent.
2290        ///
2291        /// Default: None
2292        default_model: Option<String>,
2293        /// The favorite models for this agent.
2294        ///
2295        /// Default: []
2296        favorite_models: Vec<String>,
2297        /// Default values for session config options.
2298        ///
2299        /// This is a map from config option ID to value ID.
2300        ///
2301        /// Default: {}
2302        default_config_options: HashMap<String, String>,
2303        /// Favorited values for session config options.
2304        ///
2305        /// This is a map from config option ID to a list of favorited value IDs.
2306        ///
2307        /// Default: {}
2308        favorite_config_option_values: HashMap<String, Vec<String>>,
2309    },
2310    Extension {
2311        /// Additional environment variables to pass to the agent.
2312        ///
2313        /// Default: {}
2314        env: HashMap<String, String>,
2315        /// The default mode to use for this agent.
2316        ///
2317        /// Note: Not only all agents support modes.
2318        ///
2319        /// Default: None
2320        default_mode: Option<String>,
2321        /// The default model to use for this agent.
2322        ///
2323        /// This should be the model ID as reported by the agent.
2324        ///
2325        /// Default: None
2326        default_model: Option<String>,
2327        /// The favorite models for this agent.
2328        ///
2329        /// Default: []
2330        favorite_models: Vec<String>,
2331        /// Default values for session config options.
2332        ///
2333        /// This is a map from config option ID to value ID.
2334        ///
2335        /// Default: {}
2336        default_config_options: HashMap<String, String>,
2337        /// Favorited values for session config options.
2338        ///
2339        /// This is a map from config option ID to a list of favorited value IDs.
2340        ///
2341        /// Default: {}
2342        favorite_config_option_values: HashMap<String, Vec<String>>,
2343    },
2344    Registry {
2345        /// Additional environment variables to pass to the agent.
2346        ///
2347        /// Default: {}
2348        env: HashMap<String, String>,
2349        /// The default mode to use for this agent.
2350        ///
2351        /// Note: Not only all agents support modes.
2352        ///
2353        /// Default: None
2354        default_mode: Option<String>,
2355        /// The default model to use for this agent.
2356        ///
2357        /// This should be the model ID as reported by the agent.
2358        ///
2359        /// Default: None
2360        default_model: Option<String>,
2361        /// The favorite models for this agent.
2362        ///
2363        /// Default: []
2364        favorite_models: Vec<String>,
2365        /// Default values for session config options.
2366        ///
2367        /// This is a map from config option ID to value ID.
2368        ///
2369        /// Default: {}
2370        default_config_options: HashMap<String, String>,
2371        /// Favorited values for session config options.
2372        ///
2373        /// This is a map from config option ID to a list of favorited value IDs.
2374        ///
2375        /// Default: {}
2376        favorite_config_option_values: HashMap<String, Vec<String>>,
2377    },
2378}
2379
2380impl CustomAgentServerSettings {
2381    pub fn command(&self) -> Option<&AgentServerCommand> {
2382        match self {
2383            CustomAgentServerSettings::Custom { command, .. } => Some(command),
2384            CustomAgentServerSettings::Extension { .. }
2385            | CustomAgentServerSettings::Registry { .. } => None,
2386        }
2387    }
2388
2389    pub fn default_mode(&self) -> Option<&str> {
2390        match self {
2391            CustomAgentServerSettings::Custom { default_mode, .. }
2392            | CustomAgentServerSettings::Extension { default_mode, .. }
2393            | CustomAgentServerSettings::Registry { default_mode, .. } => default_mode.as_deref(),
2394        }
2395    }
2396
2397    pub fn default_model(&self) -> Option<&str> {
2398        match self {
2399            CustomAgentServerSettings::Custom { default_model, .. }
2400            | CustomAgentServerSettings::Extension { default_model, .. }
2401            | CustomAgentServerSettings::Registry { default_model, .. } => default_model.as_deref(),
2402        }
2403    }
2404
2405    pub fn favorite_models(&self) -> &[String] {
2406        match self {
2407            CustomAgentServerSettings::Custom {
2408                favorite_models, ..
2409            }
2410            | CustomAgentServerSettings::Extension {
2411                favorite_models, ..
2412            }
2413            | CustomAgentServerSettings::Registry {
2414                favorite_models, ..
2415            } => favorite_models,
2416        }
2417    }
2418
2419    pub fn default_config_option(&self, config_id: &str) -> Option<&str> {
2420        match self {
2421            CustomAgentServerSettings::Custom {
2422                default_config_options,
2423                ..
2424            }
2425            | CustomAgentServerSettings::Extension {
2426                default_config_options,
2427                ..
2428            }
2429            | CustomAgentServerSettings::Registry {
2430                default_config_options,
2431                ..
2432            } => default_config_options.get(config_id).map(|s| s.as_str()),
2433        }
2434    }
2435
2436    pub fn favorite_config_option_values(&self, config_id: &str) -> Option<&[String]> {
2437        match self {
2438            CustomAgentServerSettings::Custom {
2439                favorite_config_option_values,
2440                ..
2441            }
2442            | CustomAgentServerSettings::Extension {
2443                favorite_config_option_values,
2444                ..
2445            }
2446            | CustomAgentServerSettings::Registry {
2447                favorite_config_option_values,
2448                ..
2449            } => favorite_config_option_values
2450                .get(config_id)
2451                .map(|v| v.as_slice()),
2452        }
2453    }
2454}
2455
2456impl From<settings::CustomAgentServerSettings> for CustomAgentServerSettings {
2457    fn from(value: settings::CustomAgentServerSettings) -> Self {
2458        match value {
2459            settings::CustomAgentServerSettings::Custom {
2460                path,
2461                args,
2462                env,
2463                default_mode,
2464                default_model,
2465                favorite_models,
2466                default_config_options,
2467                favorite_config_option_values,
2468            } => CustomAgentServerSettings::Custom {
2469                command: AgentServerCommand {
2470                    path: PathBuf::from(shellexpand::tilde(&path.to_string_lossy()).as_ref()),
2471                    args,
2472                    env: Some(env),
2473                },
2474                default_mode,
2475                default_model,
2476                favorite_models,
2477                default_config_options,
2478                favorite_config_option_values,
2479            },
2480            settings::CustomAgentServerSettings::Extension {
2481                env,
2482                default_mode,
2483                default_model,
2484                default_config_options,
2485                favorite_models,
2486                favorite_config_option_values,
2487            } => CustomAgentServerSettings::Extension {
2488                env,
2489                default_mode,
2490                default_model,
2491                default_config_options,
2492                favorite_models,
2493                favorite_config_option_values,
2494            },
2495            settings::CustomAgentServerSettings::Registry {
2496                env,
2497                default_mode,
2498                default_model,
2499                default_config_options,
2500                favorite_models,
2501                favorite_config_option_values,
2502            } => CustomAgentServerSettings::Registry {
2503                env,
2504                default_mode,
2505                default_model,
2506                default_config_options,
2507                favorite_models,
2508                favorite_config_option_values,
2509            },
2510        }
2511    }
2512}
2513
2514impl settings::Settings for AllAgentServersSettings {
2515    fn from_settings(content: &settings::SettingsContent) -> Self {
2516        let agent_settings = content.agent_servers.clone().unwrap();
2517        Self {
2518            gemini: agent_settings.gemini.map(Into::into),
2519            claude: agent_settings.claude.map(Into::into),
2520            codex: agent_settings.codex.map(Into::into),
2521            custom: agent_settings
2522                .custom
2523                .into_iter()
2524                .map(|(k, v)| (k, v.into()))
2525                .collect(),
2526        }
2527    }
2528}