agent_server_store.rs

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