agent_server_store.rs

   1use remote::Interactive;
   2use std::{
   3    any::Any,
   4    borrow::Borrow,
   5    path::{Path, PathBuf},
   6    sync::Arc,
   7    time::Duration,
   8};
   9
  10use anyhow::{Context as _, Result, bail};
  11use collections::HashMap;
  12use fs::Fs;
  13use gpui::{AsyncApp, Context, Entity, EventEmitter, SharedString, Subscription, Task};
  14use http_client::{HttpClient, github::AssetKind};
  15use node_runtime::NodeRuntime;
  16use remote::RemoteClient;
  17use rpc::{
  18    AnyProtoClient, TypedEnvelope,
  19    proto::{self, ExternalExtensionAgent},
  20};
  21use schemars::JsonSchema;
  22use serde::{Deserialize, Serialize};
  23use settings::{RegisterSetting, SettingsStore};
  24use sha2::{Digest, Sha256};
  25use task::Shell;
  26use util::{ResultExt as _, debug_panic};
  27
  28use crate::ProjectEnvironment;
  29use crate::agent_registry_store::{AgentRegistryStore, RegistryAgent, RegistryTargetConfig};
  30
  31#[derive(Deserialize, Serialize, Clone, PartialEq, Eq, JsonSchema)]
  32pub struct AgentServerCommand {
  33    #[serde(rename = "command")]
  34    pub path: PathBuf,
  35    #[serde(default)]
  36    pub args: Vec<String>,
  37    pub env: Option<HashMap<String, String>>,
  38}
  39
  40impl std::fmt::Debug for AgentServerCommand {
  41    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
  42        let filtered_env = self.env.as_ref().map(|env| {
  43            env.iter()
  44                .map(|(k, v)| {
  45                    (
  46                        k,
  47                        if util::redact::should_redact(k) {
  48                            "[REDACTED]"
  49                        } else {
  50                            v
  51                        },
  52                    )
  53                })
  54                .collect::<Vec<_>>()
  55        });
  56
  57        f.debug_struct("AgentServerCommand")
  58            .field("path", &self.path)
  59            .field("args", &self.args)
  60            .field("env", &filtered_env)
  61            .finish()
  62    }
  63}
  64
  65#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
  66pub struct ExternalAgentServerName(pub SharedString);
  67
  68impl std::fmt::Display for ExternalAgentServerName {
  69    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
  70        write!(f, "{}", self.0)
  71    }
  72}
  73
  74impl From<&'static str> for ExternalAgentServerName {
  75    fn from(value: &'static str) -> Self {
  76        ExternalAgentServerName(value.into())
  77    }
  78}
  79
  80impl From<ExternalAgentServerName> for SharedString {
  81    fn from(value: ExternalAgentServerName) -> Self {
  82        value.0
  83    }
  84}
  85
  86impl Borrow<str> for ExternalAgentServerName {
  87    fn borrow(&self) -> &str {
  88        &self.0
  89    }
  90}
  91
  92#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
  93pub enum ExternalAgentSource {
  94    #[default]
  95    Custom,
  96    Extension,
  97    Registry,
  98}
  99
 100pub trait ExternalAgentServer {
 101    fn get_command(
 102        &mut self,
 103        extra_env: HashMap<String, String>,
 104        status_tx: Option<watch::Sender<SharedString>>,
 105        new_version_available_tx: Option<watch::Sender<Option<String>>>,
 106        cx: &mut AsyncApp,
 107    ) -> Task<Result<AgentServerCommand>>;
 108
 109    fn as_any_mut(&mut self) -> &mut dyn Any;
 110}
 111
 112impl dyn ExternalAgentServer {
 113    fn downcast_mut<T: ExternalAgentServer + 'static>(&mut self) -> Option<&mut T> {
 114        self.as_any_mut().downcast_mut()
 115    }
 116}
 117
 118enum AgentServerStoreState {
 119    Local {
 120        node_runtime: NodeRuntime,
 121        fs: Arc<dyn Fs>,
 122        project_environment: Entity<ProjectEnvironment>,
 123        downstream_client: Option<(u64, AnyProtoClient)>,
 124        settings: Option<AllAgentServersSettings>,
 125        http_client: Arc<dyn HttpClient>,
 126        extension_agents: Vec<(
 127            Arc<str>,
 128            String,
 129            HashMap<String, extension::TargetConfig>,
 130            HashMap<String, String>,
 131            Option<String>,
 132            Option<SharedString>,
 133        )>,
 134        _subscriptions: Vec<Subscription>,
 135    },
 136    Remote {
 137        project_id: u64,
 138        upstream_client: Entity<RemoteClient>,
 139    },
 140    Collab,
 141}
 142
 143pub struct ExternalAgentEntry {
 144    server: Box<dyn ExternalAgentServer>,
 145    icon: Option<SharedString>,
 146    display_name: Option<SharedString>,
 147    pub source: ExternalAgentSource,
 148}
 149
 150impl ExternalAgentEntry {
 151    pub fn new(
 152        server: Box<dyn ExternalAgentServer>,
 153        source: ExternalAgentSource,
 154        icon: Option<SharedString>,
 155        display_name: Option<SharedString>,
 156    ) -> Self {
 157        Self {
 158            server,
 159            icon,
 160            display_name,
 161            source,
 162        }
 163    }
 164}
 165
 166pub struct AgentServerStore {
 167    state: AgentServerStoreState,
 168    pub external_agents: HashMap<ExternalAgentServerName, ExternalAgentEntry>,
 169}
 170
 171pub struct AgentServersUpdated;
 172
 173impl EventEmitter<AgentServersUpdated> for AgentServerStore {}
 174
 175impl AgentServerStore {
 176    /// Synchronizes extension-provided agent servers with the store.
 177    pub fn sync_extension_agents<'a, I>(
 178        &mut self,
 179        manifests: I,
 180        extensions_dir: PathBuf,
 181        cx: &mut Context<Self>,
 182    ) where
 183        I: IntoIterator<Item = (&'a str, &'a extension::ExtensionManifest)>,
 184    {
 185        // Collect manifests first so we can iterate twice
 186        let manifests: Vec<_> = manifests.into_iter().collect();
 187
 188        // Remove all extension-provided agents
 189        // (They will be re-added below if they're in the currently installed extensions)
 190        self.external_agents
 191            .retain(|_, entry| entry.source != ExternalAgentSource::Extension);
 192
 193        // Insert agent servers from extension manifests
 194        match &mut self.state {
 195            AgentServerStoreState::Local {
 196                extension_agents, ..
 197            } => {
 198                extension_agents.clear();
 199                for (ext_id, manifest) in manifests {
 200                    for (agent_name, agent_entry) in &manifest.agent_servers {
 201                        let display_name = SharedString::from(agent_entry.name.clone());
 202                        let icon_path = agent_entry.icon.as_ref().and_then(|icon| {
 203                            resolve_extension_icon_path(&extensions_dir, ext_id, icon)
 204                        });
 205
 206                        extension_agents.push((
 207                            agent_name.clone(),
 208                            ext_id.to_owned(),
 209                            agent_entry.targets.clone(),
 210                            agent_entry.env.clone(),
 211                            icon_path,
 212                            Some(display_name),
 213                        ));
 214                    }
 215                }
 216                self.reregister_agents(cx);
 217            }
 218            AgentServerStoreState::Remote {
 219                project_id,
 220                upstream_client,
 221            } => {
 222                let mut agents = vec![];
 223                for (ext_id, manifest) in manifests {
 224                    for (agent_name, agent_entry) in &manifest.agent_servers {
 225                        let display_name = SharedString::from(agent_entry.name.clone());
 226                        let icon_path = agent_entry.icon.as_ref().and_then(|icon| {
 227                            resolve_extension_icon_path(&extensions_dir, ext_id, icon)
 228                        });
 229                        let icon_shared = icon_path
 230                            .as_ref()
 231                            .map(|path| SharedString::from(path.clone()));
 232                        let icon = icon_path;
 233                        let agent_server_name = ExternalAgentServerName(agent_name.clone().into());
 234                        self.external_agents
 235                            .entry(agent_server_name.clone())
 236                            .and_modify(|entry| {
 237                                entry.icon = icon_shared.clone();
 238                                entry.display_name = Some(display_name.clone());
 239                                entry.source = ExternalAgentSource::Extension;
 240                            })
 241                            .or_insert_with(|| {
 242                                ExternalAgentEntry::new(
 243                                    Box::new(RemoteExternalAgentServer {
 244                                        project_id: *project_id,
 245                                        upstream_client: upstream_client.clone(),
 246                                        name: agent_server_name.clone(),
 247                                        status_tx: None,
 248                                        new_version_available_tx: None,
 249                                    })
 250                                        as Box<dyn ExternalAgentServer>,
 251                                    ExternalAgentSource::Extension,
 252                                    icon_shared.clone(),
 253                                    Some(display_name.clone()),
 254                                )
 255                            });
 256
 257                        agents.push(ExternalExtensionAgent {
 258                            name: agent_name.to_string(),
 259                            icon_path: icon,
 260                            extension_id: ext_id.to_string(),
 261                            targets: agent_entry
 262                                .targets
 263                                .iter()
 264                                .map(|(k, v)| (k.clone(), v.to_proto()))
 265                                .collect(),
 266                            env: agent_entry
 267                                .env
 268                                .iter()
 269                                .map(|(k, v)| (k.clone(), v.clone()))
 270                                .collect(),
 271                        });
 272                    }
 273                }
 274                upstream_client
 275                    .read(cx)
 276                    .proto_client()
 277                    .send(proto::ExternalExtensionAgentsUpdated {
 278                        project_id: *project_id,
 279                        agents,
 280                    })
 281                    .log_err();
 282            }
 283            AgentServerStoreState::Collab => {
 284                // Do nothing
 285            }
 286        }
 287
 288        cx.emit(AgentServersUpdated);
 289    }
 290
 291    pub fn agent_icon(&self, name: &ExternalAgentServerName) -> Option<SharedString> {
 292        self.external_agents
 293            .get(name)
 294            .and_then(|entry| entry.icon.clone())
 295    }
 296
 297    pub fn agent_source(&self, name: &ExternalAgentServerName) -> Option<ExternalAgentSource> {
 298        self.external_agents.get(name).map(|entry| entry.source)
 299    }
 300}
 301
 302/// Safely resolves an extension icon path, ensuring it stays within the extension directory.
 303/// Returns `None` if the path would escape the extension directory (path traversal attack).
 304pub fn resolve_extension_icon_path(
 305    extensions_dir: &Path,
 306    extension_id: &str,
 307    icon_relative_path: &str,
 308) -> Option<String> {
 309    let extension_root = extensions_dir.join(extension_id);
 310    let icon_path = extension_root.join(icon_relative_path);
 311
 312    // Canonicalize both paths to resolve symlinks and normalize the paths.
 313    // For the extension root, we need to handle the case where it might be a symlink
 314    // (common for dev extensions).
 315    let canonical_extension_root = extension_root.canonicalize().unwrap_or(extension_root);
 316    let canonical_icon_path = match icon_path.canonicalize() {
 317        Ok(path) => path,
 318        Err(err) => {
 319            log::warn!(
 320                "Failed to canonicalize icon path for extension '{}': {} (path: {})",
 321                extension_id,
 322                err,
 323                icon_relative_path
 324            );
 325            return None;
 326        }
 327    };
 328
 329    // Verify the resolved icon path is within the extension directory
 330    if canonical_icon_path.starts_with(&canonical_extension_root) {
 331        Some(canonical_icon_path.to_string_lossy().to_string())
 332    } else {
 333        log::warn!(
 334            "Icon path '{}' for extension '{}' escapes extension directory, ignoring for security",
 335            icon_relative_path,
 336            extension_id
 337        );
 338        None
 339    }
 340}
 341
 342impl AgentServerStore {
 343    pub fn agent_display_name(&self, name: &ExternalAgentServerName) -> Option<SharedString> {
 344        self.external_agents
 345            .get(name)
 346            .and_then(|entry| entry.display_name.clone())
 347    }
 348
 349    pub fn init_remote(session: &AnyProtoClient) {
 350        session.add_entity_message_handler(Self::handle_external_agents_updated);
 351        session.add_entity_message_handler(Self::handle_loading_status_updated);
 352        session.add_entity_message_handler(Self::handle_new_version_available);
 353    }
 354
 355    pub fn init_headless(session: &AnyProtoClient) {
 356        session.add_entity_message_handler(Self::handle_external_extension_agents_updated);
 357        session.add_entity_request_handler(Self::handle_get_agent_server_command);
 358    }
 359
 360    fn agent_servers_settings_changed(&mut self, cx: &mut Context<Self>) {
 361        let AgentServerStoreState::Local {
 362            settings: old_settings,
 363            ..
 364        } = &mut self.state
 365        else {
 366            debug_panic!(
 367                "should not be subscribed to agent server settings changes in non-local project"
 368            );
 369            return;
 370        };
 371
 372        let new_settings = cx
 373            .global::<SettingsStore>()
 374            .get::<AllAgentServersSettings>(None)
 375            .clone();
 376        if Some(&new_settings) == old_settings.as_ref() {
 377            return;
 378        }
 379
 380        self.reregister_agents(cx);
 381    }
 382
 383    fn reregister_agents(&mut self, cx: &mut Context<Self>) {
 384        let AgentServerStoreState::Local {
 385            node_runtime,
 386            fs,
 387            project_environment,
 388            downstream_client,
 389            settings: old_settings,
 390            http_client,
 391            extension_agents,
 392            ..
 393        } = &mut self.state
 394        else {
 395            debug_panic!("Non-local projects should never attempt to reregister. This is a bug!");
 396
 397            return;
 398        };
 399
 400        let new_settings = cx
 401            .global::<SettingsStore>()
 402            .get::<AllAgentServersSettings>(None)
 403            .clone();
 404
 405        // If we don't have agents from the registry loaded yet, trigger a
 406        // refresh, which will cause this function to be called again
 407        let registry_store = AgentRegistryStore::try_global(cx);
 408        if new_settings.has_registry_agents()
 409            && let Some(registry) = registry_store.as_ref()
 410        {
 411            registry.update(cx, |registry, cx| registry.refresh_if_stale(cx));
 412        }
 413
 414        let registry_agents_by_id = registry_store
 415            .as_ref()
 416            .map(|store| {
 417                store
 418                    .read(cx)
 419                    .agents()
 420                    .iter()
 421                    .cloned()
 422                    .map(|agent| (agent.id().to_string(), agent))
 423                    .collect::<HashMap<_, _>>()
 424            })
 425            .unwrap_or_default();
 426
 427        self.external_agents.clear();
 428
 429        // Insert extension agents before custom/registry so registry entries override extensions.
 430        for (agent_name, ext_id, targets, env, icon_path, display_name) in extension_agents.iter() {
 431            let name = ExternalAgentServerName(agent_name.clone().into());
 432            let mut env = env.clone();
 433            if let Some(settings_env) =
 434                new_settings
 435                    .get(agent_name.as_ref())
 436                    .and_then(|settings| match settings {
 437                        CustomAgentServerSettings::Extension { env, .. } => Some(env.clone()),
 438                        _ => None,
 439                    })
 440            {
 441                env.extend(settings_env);
 442            }
 443            let icon = icon_path
 444                .as_ref()
 445                .map(|path| SharedString::from(path.clone()));
 446
 447            self.external_agents.insert(
 448                name.clone(),
 449                ExternalAgentEntry::new(
 450                    Box::new(LocalExtensionArchiveAgent {
 451                        fs: fs.clone(),
 452                        http_client: http_client.clone(),
 453                        node_runtime: node_runtime.clone(),
 454                        project_environment: project_environment.clone(),
 455                        extension_id: Arc::from(&**ext_id),
 456                        targets: targets.clone(),
 457                        env,
 458                        agent_id: agent_name.clone(),
 459                    }) as Box<dyn ExternalAgentServer>,
 460                    ExternalAgentSource::Extension,
 461                    icon,
 462                    display_name.clone(),
 463                ),
 464            );
 465        }
 466
 467        for (name, settings) in new_settings.iter() {
 468            match settings {
 469                CustomAgentServerSettings::Custom { command, .. } => {
 470                    let agent_name = ExternalAgentServerName(name.clone().into());
 471                    self.external_agents.insert(
 472                        agent_name.clone(),
 473                        ExternalAgentEntry::new(
 474                            Box::new(LocalCustomAgent {
 475                                command: command.clone(),
 476                                project_environment: project_environment.clone(),
 477                            }) as Box<dyn ExternalAgentServer>,
 478                            ExternalAgentSource::Custom,
 479                            None,
 480                            None,
 481                        ),
 482                    );
 483                }
 484                CustomAgentServerSettings::Registry { env, .. } => {
 485                    let Some(agent) = registry_agents_by_id.get(name) else {
 486                        if registry_store.is_some() {
 487                            log::debug!("Registry agent '{}' not found in ACP registry", name);
 488                        }
 489                        continue;
 490                    };
 491
 492                    let agent_name = ExternalAgentServerName(name.clone().into());
 493                    match agent {
 494                        RegistryAgent::Binary(agent) => {
 495                            if !agent.supports_current_platform {
 496                                log::warn!(
 497                                    "Registry agent '{}' has no compatible binary for this platform",
 498                                    name
 499                                );
 500                                continue;
 501                            }
 502
 503                            self.external_agents.insert(
 504                                agent_name.clone(),
 505                                ExternalAgentEntry::new(
 506                                    Box::new(LocalRegistryArchiveAgent {
 507                                        fs: fs.clone(),
 508                                        http_client: http_client.clone(),
 509                                        node_runtime: node_runtime.clone(),
 510                                        project_environment: project_environment.clone(),
 511                                        registry_id: Arc::from(name.as_str()),
 512                                        targets: agent.targets.clone(),
 513                                        env: env.clone(),
 514                                    })
 515                                        as Box<dyn ExternalAgentServer>,
 516                                    ExternalAgentSource::Registry,
 517                                    agent.metadata.icon_path.clone(),
 518                                    Some(agent.metadata.name.clone()),
 519                                ),
 520                            );
 521                        }
 522                        RegistryAgent::Npx(agent) => {
 523                            self.external_agents.insert(
 524                                agent_name.clone(),
 525                                ExternalAgentEntry::new(
 526                                    Box::new(LocalRegistryNpxAgent {
 527                                        node_runtime: node_runtime.clone(),
 528                                        project_environment: project_environment.clone(),
 529                                        package: agent.package.clone(),
 530                                        args: agent.args.clone(),
 531                                        distribution_env: agent.env.clone(),
 532                                        settings_env: env.clone(),
 533                                    })
 534                                        as Box<dyn ExternalAgentServer>,
 535                                    ExternalAgentSource::Registry,
 536                                    agent.metadata.icon_path.clone(),
 537                                    Some(agent.metadata.name.clone()),
 538                                ),
 539                            );
 540                        }
 541                    }
 542                }
 543                CustomAgentServerSettings::Extension { .. } => {}
 544            }
 545        }
 546
 547        *old_settings = Some(new_settings);
 548
 549        if let Some((project_id, downstream_client)) = downstream_client {
 550            downstream_client
 551                .send(proto::ExternalAgentsUpdated {
 552                    project_id: *project_id,
 553                    names: self
 554                        .external_agents
 555                        .keys()
 556                        .map(|name| name.to_string())
 557                        .collect(),
 558                })
 559                .log_err();
 560        }
 561        cx.emit(AgentServersUpdated);
 562    }
 563
 564    pub fn node_runtime(&self) -> Option<NodeRuntime> {
 565        match &self.state {
 566            AgentServerStoreState::Local { node_runtime, .. } => Some(node_runtime.clone()),
 567            _ => None,
 568        }
 569    }
 570
 571    pub fn local(
 572        node_runtime: NodeRuntime,
 573        fs: Arc<dyn Fs>,
 574        project_environment: Entity<ProjectEnvironment>,
 575        http_client: Arc<dyn HttpClient>,
 576        cx: &mut Context<Self>,
 577    ) -> Self {
 578        let mut subscriptions = vec![cx.observe_global::<SettingsStore>(|this, cx| {
 579            this.agent_servers_settings_changed(cx);
 580        })];
 581        if let Some(registry_store) = AgentRegistryStore::try_global(cx) {
 582            subscriptions.push(cx.observe(&registry_store, |this, _, cx| {
 583                this.reregister_agents(cx);
 584            }));
 585        }
 586        let mut this = Self {
 587            state: AgentServerStoreState::Local {
 588                node_runtime,
 589                fs,
 590                project_environment,
 591                http_client,
 592                downstream_client: None,
 593                settings: None,
 594                extension_agents: vec![],
 595                _subscriptions: subscriptions,
 596            },
 597            external_agents: HashMap::default(),
 598        };
 599        if let Some(_events) = extension::ExtensionEvents::try_global(cx) {}
 600        this.agent_servers_settings_changed(cx);
 601        this
 602    }
 603
 604    pub(crate) fn remote(project_id: u64, upstream_client: Entity<RemoteClient>) -> Self {
 605        Self {
 606            state: AgentServerStoreState::Remote {
 607                project_id,
 608                upstream_client,
 609            },
 610            external_agents: HashMap::default(),
 611        }
 612    }
 613
 614    pub fn collab() -> Self {
 615        Self {
 616            state: AgentServerStoreState::Collab,
 617            external_agents: HashMap::default(),
 618        }
 619    }
 620
 621    pub fn shared(&mut self, project_id: u64, client: AnyProtoClient, cx: &mut Context<Self>) {
 622        match &mut self.state {
 623            AgentServerStoreState::Local {
 624                downstream_client, ..
 625            } => {
 626                *downstream_client = Some((project_id, client.clone()));
 627                // Send the current list of external agents downstream, but only after a delay,
 628                // to avoid having the message arrive before the downstream project's agent server store
 629                // sets up its handlers.
 630                cx.spawn(async move |this, cx| {
 631                    cx.background_executor().timer(Duration::from_secs(1)).await;
 632                    let names = this.update(cx, |this, _| {
 633                        this.external_agents()
 634                            .map(|name| name.to_string())
 635                            .collect()
 636                    })?;
 637                    client
 638                        .send(proto::ExternalAgentsUpdated { project_id, names })
 639                        .log_err();
 640                    anyhow::Ok(())
 641                })
 642                .detach();
 643            }
 644            AgentServerStoreState::Remote { .. } => {
 645                debug_panic!(
 646                    "external agents over collab not implemented, remote project should not be shared"
 647                );
 648            }
 649            AgentServerStoreState::Collab => {
 650                debug_panic!("external agents over collab not implemented, should not be shared");
 651            }
 652        }
 653    }
 654
 655    pub fn get_external_agent(
 656        &mut self,
 657        name: &ExternalAgentServerName,
 658    ) -> Option<&mut (dyn ExternalAgentServer + 'static)> {
 659        self.external_agents
 660            .get_mut(name)
 661            .map(|entry| entry.server.as_mut())
 662    }
 663
 664    pub fn no_browser(&self) -> bool {
 665        match &self.state {
 666            AgentServerStoreState::Local {
 667                downstream_client, ..
 668            } => downstream_client
 669                .as_ref()
 670                .is_some_and(|(_, client)| !client.has_wsl_interop()),
 671            _ => false,
 672        }
 673    }
 674
 675    pub fn external_agents(&self) -> impl Iterator<Item = &ExternalAgentServerName> {
 676        self.external_agents.keys()
 677    }
 678
 679    async fn handle_get_agent_server_command(
 680        this: Entity<Self>,
 681        envelope: TypedEnvelope<proto::GetAgentServerCommand>,
 682        mut cx: AsyncApp,
 683    ) -> Result<proto::AgentServerCommand> {
 684        let command = this
 685            .update(&mut cx, |this, cx| {
 686                let AgentServerStoreState::Local {
 687                    downstream_client, ..
 688                } = &this.state
 689                else {
 690                    debug_panic!("should not receive GetAgentServerCommand in a non-local project");
 691                    bail!("unexpected GetAgentServerCommand request in a non-local project");
 692                };
 693                let no_browser = this.no_browser();
 694                let agent = this
 695                    .external_agents
 696                    .get_mut(&*envelope.payload.name)
 697                    .map(|entry| entry.server.as_mut())
 698                    .with_context(|| format!("agent `{}` not found", envelope.payload.name))?;
 699                let (status_tx, new_version_available_tx) = downstream_client
 700                    .clone()
 701                    .map(|(project_id, downstream_client)| {
 702                        let (status_tx, mut status_rx) = watch::channel(SharedString::from(""));
 703                        let (new_version_available_tx, mut new_version_available_rx) =
 704                            watch::channel(None);
 705                        cx.spawn({
 706                            let downstream_client = downstream_client.clone();
 707                            let name = envelope.payload.name.clone();
 708                            async move |_, _| {
 709                                while let Some(status) = status_rx.recv().await.ok() {
 710                                    downstream_client.send(
 711                                        proto::ExternalAgentLoadingStatusUpdated {
 712                                            project_id,
 713                                            name: name.clone(),
 714                                            status: status.to_string(),
 715                                        },
 716                                    )?;
 717                                }
 718                                anyhow::Ok(())
 719                            }
 720                        })
 721                        .detach_and_log_err(cx);
 722                        cx.spawn({
 723                            let name = envelope.payload.name.clone();
 724                            async move |_, _| {
 725                                if let Some(version) =
 726                                    new_version_available_rx.recv().await.ok().flatten()
 727                                {
 728                                    downstream_client.send(
 729                                        proto::NewExternalAgentVersionAvailable {
 730                                            project_id,
 731                                            name: name.clone(),
 732                                            version,
 733                                        },
 734                                    )?;
 735                                }
 736                                anyhow::Ok(())
 737                            }
 738                        })
 739                        .detach_and_log_err(cx);
 740                        (status_tx, new_version_available_tx)
 741                    })
 742                    .unzip();
 743                let mut extra_env = HashMap::default();
 744                if no_browser {
 745                    extra_env.insert("NO_BROWSER".to_owned(), "1".to_owned());
 746                }
 747                anyhow::Ok(agent.get_command(
 748                    extra_env,
 749                    status_tx,
 750                    new_version_available_tx,
 751                    &mut cx.to_async(),
 752                ))
 753            })?
 754            .await?;
 755        Ok(proto::AgentServerCommand {
 756            path: command.path.to_string_lossy().into_owned(),
 757            args: command.args,
 758            env: command
 759                .env
 760                .map(|env| env.into_iter().collect())
 761                .unwrap_or_default(),
 762            // root_dir and login are no longer used, but returned for backwards compatibility
 763            root_dir: paths::home_dir().to_string_lossy().to_string(),
 764            login: None,
 765        })
 766    }
 767
 768    async fn handle_external_agents_updated(
 769        this: Entity<Self>,
 770        envelope: TypedEnvelope<proto::ExternalAgentsUpdated>,
 771        mut cx: AsyncApp,
 772    ) -> Result<()> {
 773        this.update(&mut cx, |this, cx| {
 774            let AgentServerStoreState::Remote {
 775                project_id,
 776                upstream_client,
 777            } = &this.state
 778            else {
 779                debug_panic!(
 780                    "handle_external_agents_updated should not be called for a non-remote project"
 781                );
 782                bail!("unexpected ExternalAgentsUpdated message")
 783            };
 784
 785            let mut previous_entries = std::mem::take(&mut this.external_agents);
 786            let mut status_txs = HashMap::default();
 787            let mut new_version_available_txs = HashMap::default();
 788            let mut metadata = HashMap::default();
 789
 790            for (name, mut entry) in previous_entries.drain() {
 791                if let Some(agent) = entry.server.downcast_mut::<RemoteExternalAgentServer>() {
 792                    status_txs.insert(name.clone(), agent.status_tx.take());
 793                    new_version_available_txs
 794                        .insert(name.clone(), agent.new_version_available_tx.take());
 795                }
 796
 797                metadata.insert(name, (entry.icon, entry.display_name, entry.source));
 798            }
 799
 800            this.external_agents = envelope
 801                .payload
 802                .names
 803                .into_iter()
 804                .map(|name| {
 805                    let agent_name = ExternalAgentServerName(name.into());
 806                    let (icon, display_name, source) = metadata
 807                        .remove(&agent_name)
 808                        .or_else(|| {
 809                            AgentRegistryStore::try_global(cx)
 810                                .and_then(|store| store.read(cx).agent(&agent_name.0))
 811                                .map(|s| {
 812                                    (
 813                                        s.icon_path().cloned(),
 814                                        Some(s.name().clone()),
 815                                        ExternalAgentSource::Registry,
 816                                    )
 817                                })
 818                        })
 819                        .unwrap_or((None, None, ExternalAgentSource::default()));
 820                    let agent = RemoteExternalAgentServer {
 821                        project_id: *project_id,
 822                        upstream_client: upstream_client.clone(),
 823                        name: agent_name.clone(),
 824                        status_tx: status_txs.remove(&agent_name).flatten(),
 825                        new_version_available_tx: new_version_available_txs
 826                            .remove(&agent_name)
 827                            .flatten(),
 828                    };
 829                    (
 830                        agent_name,
 831                        ExternalAgentEntry::new(
 832                            Box::new(agent) as Box<dyn ExternalAgentServer>,
 833                            source,
 834                            icon,
 835                            display_name,
 836                        ),
 837                    )
 838                })
 839                .collect();
 840            cx.emit(AgentServersUpdated);
 841            Ok(())
 842        })
 843    }
 844
 845    async fn handle_external_extension_agents_updated(
 846        this: Entity<Self>,
 847        envelope: TypedEnvelope<proto::ExternalExtensionAgentsUpdated>,
 848        mut cx: AsyncApp,
 849    ) -> Result<()> {
 850        this.update(&mut cx, |this, cx| {
 851            let AgentServerStoreState::Local {
 852                extension_agents, ..
 853            } = &mut this.state
 854            else {
 855                panic!(
 856                    "handle_external_extension_agents_updated \
 857                    should not be called for a non-remote project"
 858                );
 859            };
 860
 861            for ExternalExtensionAgent {
 862                name,
 863                icon_path,
 864                extension_id,
 865                targets,
 866                env,
 867            } in envelope.payload.agents
 868            {
 869                extension_agents.push((
 870                    Arc::from(&*name),
 871                    extension_id,
 872                    targets
 873                        .into_iter()
 874                        .map(|(k, v)| (k, extension::TargetConfig::from_proto(v)))
 875                        .collect(),
 876                    env.into_iter().collect(),
 877                    icon_path,
 878                    None,
 879                ));
 880            }
 881
 882            this.reregister_agents(cx);
 883            cx.emit(AgentServersUpdated);
 884            Ok(())
 885        })
 886    }
 887
 888    async fn handle_loading_status_updated(
 889        this: Entity<Self>,
 890        envelope: TypedEnvelope<proto::ExternalAgentLoadingStatusUpdated>,
 891        mut cx: AsyncApp,
 892    ) -> Result<()> {
 893        this.update(&mut cx, |this, _| {
 894            if let Some(agent) = this.external_agents.get_mut(&*envelope.payload.name)
 895                && let Some(agent) = agent.server.downcast_mut::<RemoteExternalAgentServer>()
 896                && let Some(status_tx) = &mut agent.status_tx
 897            {
 898                status_tx.send(envelope.payload.status.into()).ok();
 899            }
 900        });
 901        Ok(())
 902    }
 903
 904    async fn handle_new_version_available(
 905        this: Entity<Self>,
 906        envelope: TypedEnvelope<proto::NewExternalAgentVersionAvailable>,
 907        mut cx: AsyncApp,
 908    ) -> Result<()> {
 909        this.update(&mut cx, |this, _| {
 910            if let Some(agent) = this.external_agents.get_mut(&*envelope.payload.name)
 911                && let Some(agent) = agent.server.downcast_mut::<RemoteExternalAgentServer>()
 912                && let Some(new_version_available_tx) = &mut agent.new_version_available_tx
 913            {
 914                new_version_available_tx
 915                    .send(Some(envelope.payload.version))
 916                    .ok();
 917            }
 918        });
 919        Ok(())
 920    }
 921
 922    pub fn get_extension_id_for_agent(
 923        &mut self,
 924        name: &ExternalAgentServerName,
 925    ) -> Option<Arc<str>> {
 926        self.external_agents.get_mut(name).and_then(|entry| {
 927            entry
 928                .server
 929                .as_any_mut()
 930                .downcast_ref::<LocalExtensionArchiveAgent>()
 931                .map(|ext_agent| ext_agent.extension_id.clone())
 932        })
 933    }
 934}
 935
 936struct RemoteExternalAgentServer {
 937    project_id: u64,
 938    upstream_client: Entity<RemoteClient>,
 939    name: ExternalAgentServerName,
 940    status_tx: Option<watch::Sender<SharedString>>,
 941    new_version_available_tx: Option<watch::Sender<Option<String>>>,
 942}
 943
 944impl ExternalAgentServer for RemoteExternalAgentServer {
 945    fn get_command(
 946        &mut self,
 947        extra_env: HashMap<String, String>,
 948        status_tx: Option<watch::Sender<SharedString>>,
 949        new_version_available_tx: Option<watch::Sender<Option<String>>>,
 950        cx: &mut AsyncApp,
 951    ) -> Task<Result<AgentServerCommand>> {
 952        let project_id = self.project_id;
 953        let name = self.name.to_string();
 954        let upstream_client = self.upstream_client.downgrade();
 955        self.status_tx = status_tx;
 956        self.new_version_available_tx = new_version_available_tx;
 957        cx.spawn(async move |cx| {
 958            let mut response = upstream_client
 959                .update(cx, |upstream_client, _| {
 960                    upstream_client
 961                        .proto_client()
 962                        .request(proto::GetAgentServerCommand {
 963                            project_id,
 964                            name,
 965                            root_dir: None,
 966                        })
 967                })?
 968                .await?;
 969            let root_dir = response.root_dir;
 970            response.env.extend(extra_env);
 971            let command = upstream_client.update(cx, |client, _| {
 972                client.build_command_with_options(
 973                    Some(response.path),
 974                    &response.args,
 975                    &response.env.into_iter().collect(),
 976                    Some(root_dir.clone()),
 977                    None,
 978                    Interactive::No,
 979                )
 980            })??;
 981            Ok(AgentServerCommand {
 982                path: command.program.into(),
 983                args: command.args,
 984                env: Some(command.env),
 985            })
 986        })
 987    }
 988
 989    fn as_any_mut(&mut self) -> &mut dyn Any {
 990        self
 991    }
 992}
 993
 994pub struct LocalExtensionArchiveAgent {
 995    pub fs: Arc<dyn Fs>,
 996    pub http_client: Arc<dyn HttpClient>,
 997    pub node_runtime: NodeRuntime,
 998    pub project_environment: Entity<ProjectEnvironment>,
 999    pub extension_id: Arc<str>,
1000    pub agent_id: Arc<str>,
1001    pub targets: HashMap<String, extension::TargetConfig>,
1002    pub env: HashMap<String, String>,
1003}
1004
1005impl ExternalAgentServer for LocalExtensionArchiveAgent {
1006    fn get_command(
1007        &mut self,
1008        extra_env: HashMap<String, String>,
1009        _status_tx: Option<watch::Sender<SharedString>>,
1010        _new_version_available_tx: Option<watch::Sender<Option<String>>>,
1011        cx: &mut AsyncApp,
1012    ) -> Task<Result<AgentServerCommand>> {
1013        let fs = self.fs.clone();
1014        let http_client = self.http_client.clone();
1015        let node_runtime = self.node_runtime.clone();
1016        let project_environment = self.project_environment.downgrade();
1017        let extension_id = self.extension_id.clone();
1018        let agent_id = self.agent_id.clone();
1019        let targets = self.targets.clone();
1020        let base_env = self.env.clone();
1021
1022        cx.spawn(async move |cx| {
1023            // Get project environment
1024            let mut env = project_environment
1025                .update(cx, |project_environment, cx| {
1026                    project_environment.local_directory_environment(
1027                        &Shell::System,
1028                        paths::home_dir().as_path().into(),
1029                        cx,
1030                    )
1031                })?
1032                .await
1033                .unwrap_or_default();
1034
1035            // Merge manifest env and extra env
1036            env.extend(base_env);
1037            env.extend(extra_env);
1038
1039            let cache_key = format!("{}/{}", extension_id, agent_id);
1040            let dir = paths::external_agents_dir().join(&cache_key);
1041            fs.create_dir(&dir).await?;
1042
1043            // Determine platform key
1044            let os = if cfg!(target_os = "macos") {
1045                "darwin"
1046            } else if cfg!(target_os = "linux") {
1047                "linux"
1048            } else if cfg!(target_os = "windows") {
1049                "windows"
1050            } else {
1051                anyhow::bail!("unsupported OS");
1052            };
1053
1054            let arch = if cfg!(target_arch = "aarch64") {
1055                "aarch64"
1056            } else if cfg!(target_arch = "x86_64") {
1057                "x86_64"
1058            } else {
1059                anyhow::bail!("unsupported architecture");
1060            };
1061
1062            let platform_key = format!("{}-{}", os, arch);
1063            let target_config = targets.get(&platform_key).with_context(|| {
1064                format!(
1065                    "no target specified for platform '{}'. Available platforms: {}",
1066                    platform_key,
1067                    targets
1068                        .keys()
1069                        .map(|k| k.as_str())
1070                        .collect::<Vec<_>>()
1071                        .join(", ")
1072                )
1073            })?;
1074
1075            let archive_url = &target_config.archive;
1076
1077            // Use URL as version identifier for caching
1078            // Hash the URL to get a stable directory name
1079            let mut hasher = Sha256::new();
1080            hasher.update(archive_url.as_bytes());
1081            let url_hash = format!("{:x}", hasher.finalize());
1082            let version_dir = dir.join(format!("v_{}", url_hash));
1083
1084            if !fs.is_dir(&version_dir).await {
1085                // Determine SHA256 for verification
1086                let sha256 = if let Some(provided_sha) = &target_config.sha256 {
1087                    // Use provided SHA256
1088                    Some(provided_sha.clone())
1089                } else if archive_url.starts_with("https://github.com/") {
1090                    // Try to fetch SHA256 from GitHub API
1091                    // Parse URL to extract repo and tag/file info
1092                    // Format: https://github.com/owner/repo/releases/download/tag/file.zip
1093                    if let Some(caps) = archive_url.strip_prefix("https://github.com/") {
1094                        let parts: Vec<&str> = caps.split('/').collect();
1095                        if parts.len() >= 6 && parts[2] == "releases" && parts[3] == "download" {
1096                            let repo = format!("{}/{}", parts[0], parts[1]);
1097                            let tag = parts[4];
1098                            let filename = parts[5..].join("/");
1099
1100                            // Try to get release info from GitHub
1101                            if let Ok(release) = ::http_client::github::get_release_by_tag_name(
1102                                &repo,
1103                                tag,
1104                                http_client.clone(),
1105                            )
1106                            .await
1107                            {
1108                                // Find matching asset
1109                                if let Some(asset) =
1110                                    release.assets.iter().find(|a| a.name == filename)
1111                                {
1112                                    // Strip "sha256:" prefix if present
1113                                    asset.digest.as_ref().map(|d| {
1114                                        d.strip_prefix("sha256:")
1115                                            .map(|s| s.to_string())
1116                                            .unwrap_or_else(|| d.clone())
1117                                    })
1118                                } else {
1119                                    None
1120                                }
1121                            } else {
1122                                None
1123                            }
1124                        } else {
1125                            None
1126                        }
1127                    } else {
1128                        None
1129                    }
1130                } else {
1131                    None
1132                };
1133
1134                // Determine archive type from URL
1135                let asset_kind = if archive_url.ends_with(".zip") {
1136                    AssetKind::Zip
1137                } else if archive_url.ends_with(".tar.gz") || archive_url.ends_with(".tgz") {
1138                    AssetKind::TarGz
1139                } else {
1140                    anyhow::bail!("unsupported archive type in URL: {}", archive_url);
1141                };
1142
1143                // Download and extract
1144                ::http_client::github_download::download_server_binary(
1145                    &*http_client,
1146                    archive_url,
1147                    sha256.as_deref(),
1148                    &version_dir,
1149                    asset_kind,
1150                )
1151                .await?;
1152            }
1153
1154            // Validate and resolve cmd path
1155            let cmd = &target_config.cmd;
1156
1157            let cmd_path = if cmd == "node" {
1158                // Use Zed's managed Node.js runtime
1159                node_runtime.binary_path().await?
1160            } else {
1161                if cmd.contains("..") {
1162                    anyhow::bail!("command path cannot contain '..': {}", cmd);
1163                }
1164
1165                if cmd.starts_with("./") || cmd.starts_with(".\\") {
1166                    // Relative to extraction directory
1167                    let cmd_path = version_dir.join(&cmd[2..]);
1168                    anyhow::ensure!(
1169                        fs.is_file(&cmd_path).await,
1170                        "Missing command {} after extraction",
1171                        cmd_path.to_string_lossy()
1172                    );
1173                    cmd_path
1174                } else {
1175                    // On PATH
1176                    anyhow::bail!("command must be relative (start with './'): {}", cmd);
1177                }
1178            };
1179
1180            let command = AgentServerCommand {
1181                path: cmd_path,
1182                args: target_config.args.clone(),
1183                env: Some(env),
1184            };
1185
1186            Ok(command)
1187        })
1188    }
1189
1190    fn as_any_mut(&mut self) -> &mut dyn Any {
1191        self
1192    }
1193}
1194
1195struct LocalRegistryArchiveAgent {
1196    fs: Arc<dyn Fs>,
1197    http_client: Arc<dyn HttpClient>,
1198    node_runtime: NodeRuntime,
1199    project_environment: Entity<ProjectEnvironment>,
1200    registry_id: Arc<str>,
1201    targets: HashMap<String, RegistryTargetConfig>,
1202    env: HashMap<String, String>,
1203}
1204
1205impl ExternalAgentServer for LocalRegistryArchiveAgent {
1206    fn get_command(
1207        &mut self,
1208        extra_env: HashMap<String, String>,
1209        _status_tx: Option<watch::Sender<SharedString>>,
1210        _new_version_available_tx: Option<watch::Sender<Option<String>>>,
1211        cx: &mut AsyncApp,
1212    ) -> Task<Result<AgentServerCommand>> {
1213        let fs = self.fs.clone();
1214        let http_client = self.http_client.clone();
1215        let node_runtime = self.node_runtime.clone();
1216        let project_environment = self.project_environment.downgrade();
1217        let registry_id = self.registry_id.clone();
1218        let targets = self.targets.clone();
1219        let settings_env = self.env.clone();
1220
1221        cx.spawn(async move |cx| {
1222            let mut env = project_environment
1223                .update(cx, |project_environment, cx| {
1224                    project_environment.local_directory_environment(
1225                        &Shell::System,
1226                        paths::home_dir().as_path().into(),
1227                        cx,
1228                    )
1229                })?
1230                .await
1231                .unwrap_or_default();
1232
1233            let dir = paths::external_agents_dir()
1234                .join("registry")
1235                .join(registry_id.as_ref());
1236            fs.create_dir(&dir).await?;
1237
1238            let os = if cfg!(target_os = "macos") {
1239                "darwin"
1240            } else if cfg!(target_os = "linux") {
1241                "linux"
1242            } else if cfg!(target_os = "windows") {
1243                "windows"
1244            } else {
1245                anyhow::bail!("unsupported OS");
1246            };
1247
1248            let arch = if cfg!(target_arch = "aarch64") {
1249                "aarch64"
1250            } else if cfg!(target_arch = "x86_64") {
1251                "x86_64"
1252            } else {
1253                anyhow::bail!("unsupported architecture");
1254            };
1255
1256            let platform_key = format!("{}-{}", os, arch);
1257            let target_config = targets.get(&platform_key).with_context(|| {
1258                format!(
1259                    "no target specified for platform '{}'. Available platforms: {}",
1260                    platform_key,
1261                    targets
1262                        .keys()
1263                        .map(|k| k.as_str())
1264                        .collect::<Vec<_>>()
1265                        .join(", ")
1266                )
1267            })?;
1268
1269            env.extend(target_config.env.clone());
1270            env.extend(extra_env);
1271            env.extend(settings_env);
1272
1273            let archive_url = &target_config.archive;
1274
1275            let mut hasher = Sha256::new();
1276            hasher.update(archive_url.as_bytes());
1277            let url_hash = format!("{:x}", hasher.finalize());
1278            let version_dir = dir.join(format!("v_{}", url_hash));
1279
1280            if !fs.is_dir(&version_dir).await {
1281                let sha256 = if let Some(provided_sha) = &target_config.sha256 {
1282                    Some(provided_sha.clone())
1283                } else if archive_url.starts_with("https://github.com/") {
1284                    if let Some(caps) = archive_url.strip_prefix("https://github.com/") {
1285                        let parts: Vec<&str> = caps.split('/').collect();
1286                        if parts.len() >= 6 && parts[2] == "releases" && parts[3] == "download" {
1287                            let repo = format!("{}/{}", parts[0], parts[1]);
1288                            let tag = parts[4];
1289                            let filename = parts[5..].join("/");
1290
1291                            if let Ok(release) = ::http_client::github::get_release_by_tag_name(
1292                                &repo,
1293                                tag,
1294                                http_client.clone(),
1295                            )
1296                            .await
1297                            {
1298                                if let Some(asset) =
1299                                    release.assets.iter().find(|a| a.name == filename)
1300                                {
1301                                    asset.digest.as_ref().and_then(|d| {
1302                                        d.strip_prefix("sha256:")
1303                                            .map(|s| s.to_string())
1304                                            .or_else(|| Some(d.clone()))
1305                                    })
1306                                } else {
1307                                    None
1308                                }
1309                            } else {
1310                                None
1311                            }
1312                        } else {
1313                            None
1314                        }
1315                    } else {
1316                        None
1317                    }
1318                } else {
1319                    None
1320                };
1321
1322                let asset_kind = if archive_url.ends_with(".zip") {
1323                    AssetKind::Zip
1324                } else if archive_url.ends_with(".tar.gz") || archive_url.ends_with(".tgz") {
1325                    AssetKind::TarGz
1326                } else {
1327                    anyhow::bail!("unsupported archive type in URL: {}", archive_url);
1328                };
1329
1330                ::http_client::github_download::download_server_binary(
1331                    &*http_client,
1332                    archive_url,
1333                    sha256.as_deref(),
1334                    &version_dir,
1335                    asset_kind,
1336                )
1337                .await?;
1338            }
1339
1340            let cmd = &target_config.cmd;
1341
1342            let cmd_path = if cmd == "node" {
1343                node_runtime.binary_path().await?
1344            } else {
1345                if cmd.contains("..") {
1346                    anyhow::bail!("command path cannot contain '..': {}", cmd);
1347                }
1348
1349                if cmd.starts_with("./") || cmd.starts_with(".\\") {
1350                    let cmd_path = version_dir.join(&cmd[2..]);
1351                    anyhow::ensure!(
1352                        fs.is_file(&cmd_path).await,
1353                        "Missing command {} after extraction",
1354                        cmd_path.to_string_lossy()
1355                    );
1356                    cmd_path
1357                } else {
1358                    anyhow::bail!("command must be relative (start with './'): {}", cmd);
1359                }
1360            };
1361
1362            let command = AgentServerCommand {
1363                path: cmd_path,
1364                args: target_config.args.clone(),
1365                env: Some(env),
1366            };
1367
1368            Ok(command)
1369        })
1370    }
1371
1372    fn as_any_mut(&mut self) -> &mut dyn Any {
1373        self
1374    }
1375}
1376
1377struct LocalRegistryNpxAgent {
1378    node_runtime: NodeRuntime,
1379    project_environment: Entity<ProjectEnvironment>,
1380    package: SharedString,
1381    args: Vec<String>,
1382    distribution_env: HashMap<String, String>,
1383    settings_env: HashMap<String, String>,
1384}
1385
1386impl ExternalAgentServer for LocalRegistryNpxAgent {
1387    fn get_command(
1388        &mut self,
1389        extra_env: HashMap<String, String>,
1390        _status_tx: Option<watch::Sender<SharedString>>,
1391        _new_version_available_tx: Option<watch::Sender<Option<String>>>,
1392        cx: &mut AsyncApp,
1393    ) -> Task<Result<AgentServerCommand>> {
1394        let node_runtime = self.node_runtime.clone();
1395        let project_environment = self.project_environment.downgrade();
1396        let package = self.package.clone();
1397        let args = self.args.clone();
1398        let distribution_env = self.distribution_env.clone();
1399        let settings_env = self.settings_env.clone();
1400
1401        cx.spawn(async move |cx| {
1402            let mut env = project_environment
1403                .update(cx, |project_environment, cx| {
1404                    project_environment.local_directory_environment(
1405                        &Shell::System,
1406                        paths::home_dir().as_path().into(),
1407                        cx,
1408                    )
1409                })?
1410                .await
1411                .unwrap_or_default();
1412
1413            let mut exec_args = Vec::new();
1414            exec_args.push("--yes".to_string());
1415            exec_args.push(package.to_string());
1416            if !args.is_empty() {
1417                exec_args.push("--".to_string());
1418                exec_args.extend(args);
1419            }
1420
1421            let npm_command = node_runtime
1422                .npm_command(
1423                    "exec",
1424                    &exec_args.iter().map(|a| a.as_str()).collect::<Vec<_>>(),
1425                )
1426                .await?;
1427
1428            env.extend(npm_command.env);
1429            env.extend(distribution_env);
1430            env.extend(extra_env);
1431            env.extend(settings_env);
1432
1433            let command = AgentServerCommand {
1434                path: npm_command.path,
1435                args: npm_command.args,
1436                env: Some(env),
1437            };
1438
1439            Ok(command)
1440        })
1441    }
1442
1443    fn as_any_mut(&mut self) -> &mut dyn Any {
1444        self
1445    }
1446}
1447
1448struct LocalCustomAgent {
1449    project_environment: Entity<ProjectEnvironment>,
1450    command: AgentServerCommand,
1451}
1452
1453impl ExternalAgentServer for LocalCustomAgent {
1454    fn get_command(
1455        &mut self,
1456        extra_env: HashMap<String, String>,
1457        _status_tx: Option<watch::Sender<SharedString>>,
1458        _new_version_available_tx: Option<watch::Sender<Option<String>>>,
1459        cx: &mut AsyncApp,
1460    ) -> Task<Result<AgentServerCommand>> {
1461        let mut command = self.command.clone();
1462        let project_environment = self.project_environment.downgrade();
1463        cx.spawn(async move |cx| {
1464            let mut env = project_environment
1465                .update(cx, |project_environment, cx| {
1466                    project_environment.local_directory_environment(
1467                        &Shell::System,
1468                        paths::home_dir().as_path().into(),
1469                        cx,
1470                    )
1471                })?
1472                .await
1473                .unwrap_or_default();
1474            env.extend(command.env.unwrap_or_default());
1475            env.extend(extra_env);
1476            command.env = Some(env);
1477            Ok(command)
1478        })
1479    }
1480
1481    fn as_any_mut(&mut self) -> &mut dyn Any {
1482        self
1483    }
1484}
1485
1486pub const GEMINI_NAME: &str = "gemini";
1487pub const CLAUDE_AGENT_NAME: &str = "claude-acp";
1488pub const CODEX_NAME: &str = "codex-acp";
1489
1490#[derive(Default, Clone, JsonSchema, Debug, PartialEq, RegisterSetting)]
1491pub struct AllAgentServersSettings(pub HashMap<String, CustomAgentServerSettings>);
1492
1493impl std::ops::Deref for AllAgentServersSettings {
1494    type Target = HashMap<String, CustomAgentServerSettings>;
1495
1496    fn deref(&self) -> &Self::Target {
1497        &self.0
1498    }
1499}
1500
1501impl std::ops::DerefMut for AllAgentServersSettings {
1502    fn deref_mut(&mut self) -> &mut Self::Target {
1503        &mut self.0
1504    }
1505}
1506
1507impl AllAgentServersSettings {
1508    pub fn has_registry_agents(&self) -> bool {
1509        self.values()
1510            .any(|s| matches!(s, CustomAgentServerSettings::Registry { .. }))
1511    }
1512}
1513
1514#[derive(Clone, JsonSchema, Debug, PartialEq)]
1515pub enum CustomAgentServerSettings {
1516    Custom {
1517        command: AgentServerCommand,
1518        /// The default mode to use for this agent.
1519        ///
1520        /// Note: Not only all agents support modes.
1521        ///
1522        /// Default: None
1523        default_mode: Option<String>,
1524        /// The default model to use for this agent.
1525        ///
1526        /// This should be the model ID as reported by the agent.
1527        ///
1528        /// Default: None
1529        default_model: Option<String>,
1530        /// The favorite models for this agent.
1531        ///
1532        /// Default: []
1533        favorite_models: Vec<String>,
1534        /// Default values for session config options.
1535        ///
1536        /// This is a map from config option ID to value ID.
1537        ///
1538        /// Default: {}
1539        default_config_options: HashMap<String, String>,
1540        /// Favorited values for session config options.
1541        ///
1542        /// This is a map from config option ID to a list of favorited value IDs.
1543        ///
1544        /// Default: {}
1545        favorite_config_option_values: HashMap<String, Vec<String>>,
1546    },
1547    Extension {
1548        /// Additional environment variables to pass to the agent.
1549        ///
1550        /// Default: {}
1551        env: HashMap<String, String>,
1552        /// The default mode to use for this agent.
1553        ///
1554        /// Note: Not only all agents support modes.
1555        ///
1556        /// Default: None
1557        default_mode: Option<String>,
1558        /// The default model to use for this agent.
1559        ///
1560        /// This should be the model ID as reported by the agent.
1561        ///
1562        /// Default: None
1563        default_model: Option<String>,
1564        /// The favorite models for this agent.
1565        ///
1566        /// Default: []
1567        favorite_models: Vec<String>,
1568        /// Default values for session config options.
1569        ///
1570        /// This is a map from config option ID to value ID.
1571        ///
1572        /// Default: {}
1573        default_config_options: HashMap<String, String>,
1574        /// Favorited values for session config options.
1575        ///
1576        /// This is a map from config option ID to a list of favorited value IDs.
1577        ///
1578        /// Default: {}
1579        favorite_config_option_values: HashMap<String, Vec<String>>,
1580    },
1581    Registry {
1582        /// Additional environment variables to pass to the agent.
1583        ///
1584        /// Default: {}
1585        env: HashMap<String, String>,
1586        /// The default mode to use for this agent.
1587        ///
1588        /// Note: Not only all agents support modes.
1589        ///
1590        /// Default: None
1591        default_mode: Option<String>,
1592        /// The default model to use for this agent.
1593        ///
1594        /// This should be the model ID as reported by the agent.
1595        ///
1596        /// Default: None
1597        default_model: Option<String>,
1598        /// The favorite models for this agent.
1599        ///
1600        /// Default: []
1601        favorite_models: Vec<String>,
1602        /// Default values for session config options.
1603        ///
1604        /// This is a map from config option ID to value ID.
1605        ///
1606        /// Default: {}
1607        default_config_options: HashMap<String, String>,
1608        /// Favorited values for session config options.
1609        ///
1610        /// This is a map from config option ID to a list of favorited value IDs.
1611        ///
1612        /// Default: {}
1613        favorite_config_option_values: HashMap<String, Vec<String>>,
1614    },
1615}
1616
1617impl CustomAgentServerSettings {
1618    pub fn command(&self) -> Option<&AgentServerCommand> {
1619        match self {
1620            CustomAgentServerSettings::Custom { command, .. } => Some(command),
1621            CustomAgentServerSettings::Extension { .. }
1622            | CustomAgentServerSettings::Registry { .. } => None,
1623        }
1624    }
1625
1626    pub fn default_mode(&self) -> Option<&str> {
1627        match self {
1628            CustomAgentServerSettings::Custom { default_mode, .. }
1629            | CustomAgentServerSettings::Extension { default_mode, .. }
1630            | CustomAgentServerSettings::Registry { default_mode, .. } => default_mode.as_deref(),
1631        }
1632    }
1633
1634    pub fn default_model(&self) -> Option<&str> {
1635        match self {
1636            CustomAgentServerSettings::Custom { default_model, .. }
1637            | CustomAgentServerSettings::Extension { default_model, .. }
1638            | CustomAgentServerSettings::Registry { default_model, .. } => default_model.as_deref(),
1639        }
1640    }
1641
1642    pub fn favorite_models(&self) -> &[String] {
1643        match self {
1644            CustomAgentServerSettings::Custom {
1645                favorite_models, ..
1646            }
1647            | CustomAgentServerSettings::Extension {
1648                favorite_models, ..
1649            }
1650            | CustomAgentServerSettings::Registry {
1651                favorite_models, ..
1652            } => favorite_models,
1653        }
1654    }
1655
1656    pub fn default_config_option(&self, config_id: &str) -> Option<&str> {
1657        match self {
1658            CustomAgentServerSettings::Custom {
1659                default_config_options,
1660                ..
1661            }
1662            | CustomAgentServerSettings::Extension {
1663                default_config_options,
1664                ..
1665            }
1666            | CustomAgentServerSettings::Registry {
1667                default_config_options,
1668                ..
1669            } => default_config_options.get(config_id).map(|s| s.as_str()),
1670        }
1671    }
1672
1673    pub fn favorite_config_option_values(&self, config_id: &str) -> Option<&[String]> {
1674        match self {
1675            CustomAgentServerSettings::Custom {
1676                favorite_config_option_values,
1677                ..
1678            }
1679            | CustomAgentServerSettings::Extension {
1680                favorite_config_option_values,
1681                ..
1682            }
1683            | CustomAgentServerSettings::Registry {
1684                favorite_config_option_values,
1685                ..
1686            } => favorite_config_option_values
1687                .get(config_id)
1688                .map(|v| v.as_slice()),
1689        }
1690    }
1691}
1692
1693impl From<settings::CustomAgentServerSettings> for CustomAgentServerSettings {
1694    fn from(value: settings::CustomAgentServerSettings) -> Self {
1695        match value {
1696            settings::CustomAgentServerSettings::Custom {
1697                path,
1698                args,
1699                env,
1700                default_mode,
1701                default_model,
1702                favorite_models,
1703                default_config_options,
1704                favorite_config_option_values,
1705            } => CustomAgentServerSettings::Custom {
1706                command: AgentServerCommand {
1707                    path: PathBuf::from(shellexpand::tilde(&path.to_string_lossy()).as_ref()),
1708                    args,
1709                    env: Some(env),
1710                },
1711                default_mode,
1712                default_model,
1713                favorite_models,
1714                default_config_options,
1715                favorite_config_option_values,
1716            },
1717            settings::CustomAgentServerSettings::Extension {
1718                env,
1719                default_mode,
1720                default_model,
1721                default_config_options,
1722                favorite_models,
1723                favorite_config_option_values,
1724            } => CustomAgentServerSettings::Extension {
1725                env,
1726                default_mode,
1727                default_model,
1728                default_config_options,
1729                favorite_models,
1730                favorite_config_option_values,
1731            },
1732            settings::CustomAgentServerSettings::Registry {
1733                env,
1734                default_mode,
1735                default_model,
1736                default_config_options,
1737                favorite_models,
1738                favorite_config_option_values,
1739            } => CustomAgentServerSettings::Registry {
1740                env,
1741                default_mode,
1742                default_model,
1743                default_config_options,
1744                favorite_models,
1745                favorite_config_option_values,
1746            },
1747        }
1748    }
1749}
1750
1751impl settings::Settings for AllAgentServersSettings {
1752    fn from_settings(content: &settings::SettingsContent) -> Self {
1753        let agent_settings = content.agent_servers.clone().unwrap();
1754        Self(
1755            agent_settings
1756                .0
1757                .into_iter()
1758                .map(|(k, v)| (k, v.into()))
1759                .collect(),
1760        )
1761    }
1762}