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 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 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            use std::collections::hash_map::DefaultHasher;
1079            use std::hash::{Hash, Hasher};
1080            let mut hasher = DefaultHasher::new();
1081            archive_url.hash(&mut hasher);
1082            let url_hash = hasher.finish();
1083            let version_dir = dir.join(format!("v_{:x}", url_hash));
1084
1085            if !fs.is_dir(&version_dir).await {
1086                // Determine SHA256 for verification
1087                let sha256 = if let Some(provided_sha) = &target_config.sha256 {
1088                    // Use provided SHA256
1089                    Some(provided_sha.clone())
1090                } else if archive_url.starts_with("https://github.com/") {
1091                    // Try to fetch SHA256 from GitHub API
1092                    // Parse URL to extract repo and tag/file info
1093                    // Format: https://github.com/owner/repo/releases/download/tag/file.zip
1094                    if let Some(caps) = archive_url.strip_prefix("https://github.com/") {
1095                        let parts: Vec<&str> = caps.split('/').collect();
1096                        if parts.len() >= 6 && parts[2] == "releases" && parts[3] == "download" {
1097                            let repo = format!("{}/{}", parts[0], parts[1]);
1098                            let tag = parts[4];
1099                            let filename = parts[5..].join("/");
1100
1101                            // Try to get release info from GitHub
1102                            if let Ok(release) = ::http_client::github::get_release_by_tag_name(
1103                                &repo,
1104                                tag,
1105                                http_client.clone(),
1106                            )
1107                            .await
1108                            {
1109                                // Find matching asset
1110                                if let Some(asset) =
1111                                    release.assets.iter().find(|a| a.name == filename)
1112                                {
1113                                    // Strip "sha256:" prefix if present
1114                                    asset.digest.as_ref().map(|d| {
1115                                        d.strip_prefix("sha256:")
1116                                            .map(|s| s.to_string())
1117                                            .unwrap_or_else(|| d.clone())
1118                                    })
1119                                } else {
1120                                    None
1121                                }
1122                            } else {
1123                                None
1124                            }
1125                        } else {
1126                            None
1127                        }
1128                    } else {
1129                        None
1130                    }
1131                } else {
1132                    None
1133                };
1134
1135                // Determine archive type from URL
1136                let asset_kind = if archive_url.ends_with(".zip") {
1137                    AssetKind::Zip
1138                } else if archive_url.ends_with(".tar.gz") || archive_url.ends_with(".tgz") {
1139                    AssetKind::TarGz
1140                } else {
1141                    anyhow::bail!("unsupported archive type in URL: {}", archive_url);
1142                };
1143
1144                // Download and extract
1145                ::http_client::github_download::download_server_binary(
1146                    &*http_client,
1147                    archive_url,
1148                    sha256.as_deref(),
1149                    &version_dir,
1150                    asset_kind,
1151                )
1152                .await?;
1153            }
1154
1155            // Validate and resolve cmd path
1156            let cmd = &target_config.cmd;
1157
1158            let cmd_path = if cmd == "node" {
1159                // Use Zed's managed Node.js runtime
1160                node_runtime.binary_path().await?
1161            } else {
1162                if cmd.contains("..") {
1163                    anyhow::bail!("command path cannot contain '..': {}", cmd);
1164                }
1165
1166                if cmd.starts_with("./") || cmd.starts_with(".\\") {
1167                    // Relative to extraction directory
1168                    let cmd_path = version_dir.join(&cmd[2..]);
1169                    anyhow::ensure!(
1170                        fs.is_file(&cmd_path).await,
1171                        "Missing command {} after extraction",
1172                        cmd_path.to_string_lossy()
1173                    );
1174                    cmd_path
1175                } else {
1176                    // On PATH
1177                    anyhow::bail!("command must be relative (start with './'): {}", cmd);
1178                }
1179            };
1180
1181            let command = AgentServerCommand {
1182                path: cmd_path,
1183                args: target_config.args.clone(),
1184                env: Some(env),
1185            };
1186
1187            Ok(command)
1188        })
1189    }
1190
1191    fn as_any_mut(&mut self) -> &mut dyn Any {
1192        self
1193    }
1194}
1195
1196struct LocalRegistryArchiveAgent {
1197    fs: Arc<dyn Fs>,
1198    http_client: Arc<dyn HttpClient>,
1199    node_runtime: NodeRuntime,
1200    project_environment: Entity<ProjectEnvironment>,
1201    registry_id: Arc<str>,
1202    targets: HashMap<String, RegistryTargetConfig>,
1203    env: HashMap<String, String>,
1204}
1205
1206impl ExternalAgentServer for LocalRegistryArchiveAgent {
1207    fn get_command(
1208        &mut self,
1209        extra_env: HashMap<String, String>,
1210        _status_tx: Option<watch::Sender<SharedString>>,
1211        _new_version_available_tx: Option<watch::Sender<Option<String>>>,
1212        cx: &mut AsyncApp,
1213    ) -> Task<Result<AgentServerCommand>> {
1214        let fs = self.fs.clone();
1215        let http_client = self.http_client.clone();
1216        let node_runtime = self.node_runtime.clone();
1217        let project_environment = self.project_environment.downgrade();
1218        let registry_id = self.registry_id.clone();
1219        let targets = self.targets.clone();
1220        let settings_env = self.env.clone();
1221
1222        cx.spawn(async move |cx| {
1223            let mut env = project_environment
1224                .update(cx, |project_environment, cx| {
1225                    project_environment.local_directory_environment(
1226                        &Shell::System,
1227                        paths::home_dir().as_path().into(),
1228                        cx,
1229                    )
1230                })?
1231                .await
1232                .unwrap_or_default();
1233
1234            let dir = paths::external_agents_dir()
1235                .join("registry")
1236                .join(registry_id.as_ref());
1237            fs.create_dir(&dir).await?;
1238
1239            let os = if cfg!(target_os = "macos") {
1240                "darwin"
1241            } else if cfg!(target_os = "linux") {
1242                "linux"
1243            } else if cfg!(target_os = "windows") {
1244                "windows"
1245            } else {
1246                anyhow::bail!("unsupported OS");
1247            };
1248
1249            let arch = if cfg!(target_arch = "aarch64") {
1250                "aarch64"
1251            } else if cfg!(target_arch = "x86_64") {
1252                "x86_64"
1253            } else {
1254                anyhow::bail!("unsupported architecture");
1255            };
1256
1257            let platform_key = format!("{}-{}", os, arch);
1258            let target_config = targets.get(&platform_key).with_context(|| {
1259                format!(
1260                    "no target specified for platform '{}'. Available platforms: {}",
1261                    platform_key,
1262                    targets
1263                        .keys()
1264                        .map(|k| k.as_str())
1265                        .collect::<Vec<_>>()
1266                        .join(", ")
1267                )
1268            })?;
1269
1270            env.extend(target_config.env.clone());
1271            env.extend(extra_env);
1272            env.extend(settings_env);
1273
1274            let archive_url = &target_config.archive;
1275
1276            use std::collections::hash_map::DefaultHasher;
1277            use std::hash::{Hash, Hasher};
1278            let mut hasher = DefaultHasher::new();
1279            archive_url.hash(&mut hasher);
1280            let url_hash = hasher.finish();
1281            let version_dir = dir.join(format!("v_{:x}", url_hash));
1282
1283            if !fs.is_dir(&version_dir).await {
1284                let sha256 = if let Some(provided_sha) = &target_config.sha256 {
1285                    Some(provided_sha.clone())
1286                } else if archive_url.starts_with("https://github.com/") {
1287                    if let Some(caps) = archive_url.strip_prefix("https://github.com/") {
1288                        let parts: Vec<&str> = caps.split('/').collect();
1289                        if parts.len() >= 6 && parts[2] == "releases" && parts[3] == "download" {
1290                            let repo = format!("{}/{}", parts[0], parts[1]);
1291                            let tag = parts[4];
1292                            let filename = parts[5..].join("/");
1293
1294                            if let Ok(release) = ::http_client::github::get_release_by_tag_name(
1295                                &repo,
1296                                tag,
1297                                http_client.clone(),
1298                            )
1299                            .await
1300                            {
1301                                if let Some(asset) =
1302                                    release.assets.iter().find(|a| a.name == filename)
1303                                {
1304                                    asset.digest.as_ref().and_then(|d| {
1305                                        d.strip_prefix("sha256:")
1306                                            .map(|s| s.to_string())
1307                                            .or_else(|| Some(d.clone()))
1308                                    })
1309                                } else {
1310                                    None
1311                                }
1312                            } else {
1313                                None
1314                            }
1315                        } else {
1316                            None
1317                        }
1318                    } else {
1319                        None
1320                    }
1321                } else {
1322                    None
1323                };
1324
1325                let asset_kind = if archive_url.ends_with(".zip") {
1326                    AssetKind::Zip
1327                } else if archive_url.ends_with(".tar.gz") || archive_url.ends_with(".tgz") {
1328                    AssetKind::TarGz
1329                } else {
1330                    anyhow::bail!("unsupported archive type in URL: {}", archive_url);
1331                };
1332
1333                ::http_client::github_download::download_server_binary(
1334                    &*http_client,
1335                    archive_url,
1336                    sha256.as_deref(),
1337                    &version_dir,
1338                    asset_kind,
1339                )
1340                .await?;
1341            }
1342
1343            let cmd = &target_config.cmd;
1344
1345            let cmd_path = if cmd == "node" {
1346                node_runtime.binary_path().await?
1347            } else {
1348                if cmd.contains("..") {
1349                    anyhow::bail!("command path cannot contain '..': {}", cmd);
1350                }
1351
1352                if cmd.starts_with("./") || cmd.starts_with(".\\") {
1353                    let cmd_path = version_dir.join(&cmd[2..]);
1354                    anyhow::ensure!(
1355                        fs.is_file(&cmd_path).await,
1356                        "Missing command {} after extraction",
1357                        cmd_path.to_string_lossy()
1358                    );
1359                    cmd_path
1360                } else {
1361                    anyhow::bail!("command must be relative (start with './'): {}", cmd);
1362                }
1363            };
1364
1365            let command = AgentServerCommand {
1366                path: cmd_path,
1367                args: target_config.args.clone(),
1368                env: Some(env),
1369            };
1370
1371            Ok(command)
1372        })
1373    }
1374
1375    fn as_any_mut(&mut self) -> &mut dyn Any {
1376        self
1377    }
1378}
1379
1380struct LocalRegistryNpxAgent {
1381    node_runtime: NodeRuntime,
1382    project_environment: Entity<ProjectEnvironment>,
1383    package: SharedString,
1384    args: Vec<String>,
1385    distribution_env: HashMap<String, String>,
1386    settings_env: HashMap<String, String>,
1387}
1388
1389impl ExternalAgentServer for LocalRegistryNpxAgent {
1390    fn get_command(
1391        &mut self,
1392        extra_env: HashMap<String, String>,
1393        _status_tx: Option<watch::Sender<SharedString>>,
1394        _new_version_available_tx: Option<watch::Sender<Option<String>>>,
1395        cx: &mut AsyncApp,
1396    ) -> Task<Result<AgentServerCommand>> {
1397        let node_runtime = self.node_runtime.clone();
1398        let project_environment = self.project_environment.downgrade();
1399        let package = self.package.clone();
1400        let args = self.args.clone();
1401        let distribution_env = self.distribution_env.clone();
1402        let settings_env = self.settings_env.clone();
1403
1404        cx.spawn(async move |cx| {
1405            let mut env = project_environment
1406                .update(cx, |project_environment, cx| {
1407                    project_environment.local_directory_environment(
1408                        &Shell::System,
1409                        paths::home_dir().as_path().into(),
1410                        cx,
1411                    )
1412                })?
1413                .await
1414                .unwrap_or_default();
1415
1416            let mut exec_args = Vec::new();
1417            exec_args.push("--yes".to_string());
1418            exec_args.push(package.to_string());
1419            if !args.is_empty() {
1420                exec_args.push("--".to_string());
1421                exec_args.extend(args);
1422            }
1423
1424            let npm_command = node_runtime
1425                .npm_command(
1426                    "exec",
1427                    &exec_args.iter().map(|a| a.as_str()).collect::<Vec<_>>(),
1428                )
1429                .await?;
1430
1431            env.extend(npm_command.env);
1432            env.extend(distribution_env);
1433            env.extend(extra_env);
1434            env.extend(settings_env);
1435
1436            let command = AgentServerCommand {
1437                path: npm_command.path,
1438                args: npm_command.args,
1439                env: Some(env),
1440            };
1441
1442            Ok(command)
1443        })
1444    }
1445
1446    fn as_any_mut(&mut self) -> &mut dyn Any {
1447        self
1448    }
1449}
1450
1451struct LocalCustomAgent {
1452    project_environment: Entity<ProjectEnvironment>,
1453    command: AgentServerCommand,
1454}
1455
1456impl ExternalAgentServer for LocalCustomAgent {
1457    fn get_command(
1458        &mut self,
1459        extra_env: HashMap<String, String>,
1460        _status_tx: Option<watch::Sender<SharedString>>,
1461        _new_version_available_tx: Option<watch::Sender<Option<String>>>,
1462        cx: &mut AsyncApp,
1463    ) -> Task<Result<AgentServerCommand>> {
1464        let mut command = self.command.clone();
1465        let project_environment = self.project_environment.downgrade();
1466        cx.spawn(async move |cx| {
1467            let mut env = project_environment
1468                .update(cx, |project_environment, cx| {
1469                    project_environment.local_directory_environment(
1470                        &Shell::System,
1471                        paths::home_dir().as_path().into(),
1472                        cx,
1473                    )
1474                })?
1475                .await
1476                .unwrap_or_default();
1477            env.extend(command.env.unwrap_or_default());
1478            env.extend(extra_env);
1479            command.env = Some(env);
1480            Ok(command)
1481        })
1482    }
1483
1484    fn as_any_mut(&mut self) -> &mut dyn Any {
1485        self
1486    }
1487}
1488
1489pub const GEMINI_NAME: &str = "gemini";
1490pub const CLAUDE_AGENT_NAME: &str = "claude-acp";
1491pub const CODEX_NAME: &str = "codex-acp";
1492
1493#[derive(Default, Clone, JsonSchema, Debug, PartialEq, RegisterSetting)]
1494pub struct AllAgentServersSettings(pub HashMap<String, CustomAgentServerSettings>);
1495
1496impl std::ops::Deref for AllAgentServersSettings {
1497    type Target = HashMap<String, CustomAgentServerSettings>;
1498
1499    fn deref(&self) -> &Self::Target {
1500        &self.0
1501    }
1502}
1503
1504impl std::ops::DerefMut for AllAgentServersSettings {
1505    fn deref_mut(&mut self) -> &mut Self::Target {
1506        &mut self.0
1507    }
1508}
1509
1510impl AllAgentServersSettings {
1511    pub fn has_registry_agents(&self) -> bool {
1512        self.values()
1513            .any(|s| matches!(s, CustomAgentServerSettings::Registry { .. }))
1514    }
1515}
1516
1517#[derive(Clone, JsonSchema, Debug, PartialEq)]
1518pub enum CustomAgentServerSettings {
1519    Custom {
1520        command: AgentServerCommand,
1521        /// The default mode to use for this agent.
1522        ///
1523        /// Note: Not only all agents support modes.
1524        ///
1525        /// Default: None
1526        default_mode: Option<String>,
1527        /// The default model to use for this agent.
1528        ///
1529        /// This should be the model ID as reported by the agent.
1530        ///
1531        /// Default: None
1532        default_model: Option<String>,
1533        /// The favorite models for this agent.
1534        ///
1535        /// Default: []
1536        favorite_models: Vec<String>,
1537        /// Default values for session config options.
1538        ///
1539        /// This is a map from config option ID to value ID.
1540        ///
1541        /// Default: {}
1542        default_config_options: HashMap<String, String>,
1543        /// Favorited values for session config options.
1544        ///
1545        /// This is a map from config option ID to a list of favorited value IDs.
1546        ///
1547        /// Default: {}
1548        favorite_config_option_values: HashMap<String, Vec<String>>,
1549    },
1550    Extension {
1551        /// Additional environment variables to pass to the agent.
1552        ///
1553        /// Default: {}
1554        env: HashMap<String, String>,
1555        /// The default mode to use for this agent.
1556        ///
1557        /// Note: Not only all agents support modes.
1558        ///
1559        /// Default: None
1560        default_mode: Option<String>,
1561        /// The default model to use for this agent.
1562        ///
1563        /// This should be the model ID as reported by the agent.
1564        ///
1565        /// Default: None
1566        default_model: Option<String>,
1567        /// The favorite models for this agent.
1568        ///
1569        /// Default: []
1570        favorite_models: Vec<String>,
1571        /// Default values for session config options.
1572        ///
1573        /// This is a map from config option ID to value ID.
1574        ///
1575        /// Default: {}
1576        default_config_options: HashMap<String, String>,
1577        /// Favorited values for session config options.
1578        ///
1579        /// This is a map from config option ID to a list of favorited value IDs.
1580        ///
1581        /// Default: {}
1582        favorite_config_option_values: HashMap<String, Vec<String>>,
1583    },
1584    Registry {
1585        /// Additional environment variables to pass to the agent.
1586        ///
1587        /// Default: {}
1588        env: HashMap<String, String>,
1589        /// The default mode to use for this agent.
1590        ///
1591        /// Note: Not only all agents support modes.
1592        ///
1593        /// Default: None
1594        default_mode: Option<String>,
1595        /// The default model to use for this agent.
1596        ///
1597        /// This should be the model ID as reported by the agent.
1598        ///
1599        /// Default: None
1600        default_model: Option<String>,
1601        /// The favorite models for this agent.
1602        ///
1603        /// Default: []
1604        favorite_models: Vec<String>,
1605        /// Default values for session config options.
1606        ///
1607        /// This is a map from config option ID to value ID.
1608        ///
1609        /// Default: {}
1610        default_config_options: HashMap<String, String>,
1611        /// Favorited values for session config options.
1612        ///
1613        /// This is a map from config option ID to a list of favorited value IDs.
1614        ///
1615        /// Default: {}
1616        favorite_config_option_values: HashMap<String, Vec<String>>,
1617    },
1618}
1619
1620impl CustomAgentServerSettings {
1621    pub fn command(&self) -> Option<&AgentServerCommand> {
1622        match self {
1623            CustomAgentServerSettings::Custom { command, .. } => Some(command),
1624            CustomAgentServerSettings::Extension { .. }
1625            | CustomAgentServerSettings::Registry { .. } => None,
1626        }
1627    }
1628
1629    pub fn default_mode(&self) -> Option<&str> {
1630        match self {
1631            CustomAgentServerSettings::Custom { default_mode, .. }
1632            | CustomAgentServerSettings::Extension { default_mode, .. }
1633            | CustomAgentServerSettings::Registry { default_mode, .. } => default_mode.as_deref(),
1634        }
1635    }
1636
1637    pub fn default_model(&self) -> Option<&str> {
1638        match self {
1639            CustomAgentServerSettings::Custom { default_model, .. }
1640            | CustomAgentServerSettings::Extension { default_model, .. }
1641            | CustomAgentServerSettings::Registry { default_model, .. } => default_model.as_deref(),
1642        }
1643    }
1644
1645    pub fn favorite_models(&self) -> &[String] {
1646        match self {
1647            CustomAgentServerSettings::Custom {
1648                favorite_models, ..
1649            }
1650            | CustomAgentServerSettings::Extension {
1651                favorite_models, ..
1652            }
1653            | CustomAgentServerSettings::Registry {
1654                favorite_models, ..
1655            } => favorite_models,
1656        }
1657    }
1658
1659    pub fn default_config_option(&self, config_id: &str) -> Option<&str> {
1660        match self {
1661            CustomAgentServerSettings::Custom {
1662                default_config_options,
1663                ..
1664            }
1665            | CustomAgentServerSettings::Extension {
1666                default_config_options,
1667                ..
1668            }
1669            | CustomAgentServerSettings::Registry {
1670                default_config_options,
1671                ..
1672            } => default_config_options.get(config_id).map(|s| s.as_str()),
1673        }
1674    }
1675
1676    pub fn favorite_config_option_values(&self, config_id: &str) -> Option<&[String]> {
1677        match self {
1678            CustomAgentServerSettings::Custom {
1679                favorite_config_option_values,
1680                ..
1681            }
1682            | CustomAgentServerSettings::Extension {
1683                favorite_config_option_values,
1684                ..
1685            }
1686            | CustomAgentServerSettings::Registry {
1687                favorite_config_option_values,
1688                ..
1689            } => favorite_config_option_values
1690                .get(config_id)
1691                .map(|v| v.as_slice()),
1692        }
1693    }
1694}
1695
1696impl From<settings::CustomAgentServerSettings> for CustomAgentServerSettings {
1697    fn from(value: settings::CustomAgentServerSettings) -> Self {
1698        match value {
1699            settings::CustomAgentServerSettings::Custom {
1700                path,
1701                args,
1702                env,
1703                default_mode,
1704                default_model,
1705                favorite_models,
1706                default_config_options,
1707                favorite_config_option_values,
1708            } => CustomAgentServerSettings::Custom {
1709                command: AgentServerCommand {
1710                    path: PathBuf::from(shellexpand::tilde(&path.to_string_lossy()).as_ref()),
1711                    args,
1712                    env: Some(env),
1713                },
1714                default_mode,
1715                default_model,
1716                favorite_models,
1717                default_config_options,
1718                favorite_config_option_values,
1719            },
1720            settings::CustomAgentServerSettings::Extension {
1721                env,
1722                default_mode,
1723                default_model,
1724                default_config_options,
1725                favorite_models,
1726                favorite_config_option_values,
1727            } => CustomAgentServerSettings::Extension {
1728                env,
1729                default_mode,
1730                default_model,
1731                default_config_options,
1732                favorite_models,
1733                favorite_config_option_values,
1734            },
1735            settings::CustomAgentServerSettings::Registry {
1736                env,
1737                default_mode,
1738                default_model,
1739                default_config_options,
1740                favorite_models,
1741                favorite_config_option_values,
1742            } => CustomAgentServerSettings::Registry {
1743                env,
1744                default_mode,
1745                default_model,
1746                default_config_options,
1747                favorite_models,
1748                favorite_config_option_values,
1749            },
1750        }
1751    }
1752}
1753
1754impl settings::Settings for AllAgentServersSettings {
1755    fn from_settings(content: &settings::SettingsContent) -> Self {
1756        let agent_settings = content.agent_servers.clone().unwrap();
1757        Self(
1758            agent_settings
1759                .0
1760                .into_iter()
1761                .map(|(k, v)| (k, v.into()))
1762                .collect(),
1763        )
1764    }
1765}