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