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, name: &AgentId) -> Option<SharedString> {
 311        self.external_agents
 312            .get(name)
 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 external_agents(&self) -> impl Iterator<Item = &AgentId> {
 699        self.external_agents.keys()
 700    }
 701
 702    async fn handle_get_agent_server_command(
 703        this: Entity<Self>,
 704        envelope: TypedEnvelope<proto::GetAgentServerCommand>,
 705        mut cx: AsyncApp,
 706    ) -> Result<proto::AgentServerCommand> {
 707        let command = this
 708            .update(&mut cx, |this, cx| {
 709                let AgentServerStoreState::Local {
 710                    downstream_client, ..
 711                } = &this.state
 712                else {
 713                    debug_panic!("should not receive GetAgentServerCommand in a non-local project");
 714                    bail!("unexpected GetAgentServerCommand request in a non-local project");
 715                };
 716                let no_browser = this.no_browser();
 717                let agent = this
 718                    .external_agents
 719                    .get_mut(&*envelope.payload.name)
 720                    .map(|entry| entry.server.as_mut())
 721                    .with_context(|| format!("agent `{}` not found", envelope.payload.name))?;
 722                let new_version_available_tx =
 723                    downstream_client
 724                        .clone()
 725                        .map(|(project_id, downstream_client)| {
 726                            let (new_version_available_tx, mut new_version_available_rx) =
 727                                watch::channel(None);
 728                            cx.spawn({
 729                                let name = envelope.payload.name.clone();
 730                                async move |_, _| {
 731                                    if let Some(version) =
 732                                        new_version_available_rx.recv().await.ok().flatten()
 733                                    {
 734                                        downstream_client.send(
 735                                            proto::NewExternalAgentVersionAvailable {
 736                                                project_id,
 737                                                name: name.clone(),
 738                                                version,
 739                                            },
 740                                        )?;
 741                                    }
 742                                    anyhow::Ok(())
 743                                }
 744                            })
 745                            .detach_and_log_err(cx);
 746                            new_version_available_tx
 747                        });
 748                let mut extra_env = HashMap::default();
 749                if no_browser {
 750                    extra_env.insert("NO_BROWSER".to_owned(), "1".to_owned());
 751                }
 752                anyhow::Ok(agent.get_command(
 753                    extra_env,
 754                    new_version_available_tx,
 755                    &mut cx.to_async(),
 756                ))
 757            })?
 758            .await?;
 759        Ok(proto::AgentServerCommand {
 760            path: command.path.to_string_lossy().into_owned(),
 761            args: command.args,
 762            env: command
 763                .env
 764                .map(|env| env.into_iter().collect())
 765                .unwrap_or_default(),
 766            root_dir: envelope
 767                .payload
 768                .root_dir
 769                .unwrap_or_else(|| paths::home_dir().to_string_lossy().to_string()),
 770            login: None,
 771        })
 772    }
 773
 774    async fn handle_external_agents_updated(
 775        this: Entity<Self>,
 776        envelope: TypedEnvelope<proto::ExternalAgentsUpdated>,
 777        mut cx: AsyncApp,
 778    ) -> Result<()> {
 779        this.update(&mut cx, |this, cx| {
 780            let AgentServerStoreState::Remote {
 781                project_id,
 782                upstream_client,
 783                worktree_store,
 784            } = &this.state
 785            else {
 786                debug_panic!(
 787                    "handle_external_agents_updated should not be called for a non-remote project"
 788                );
 789                bail!("unexpected ExternalAgentsUpdated message")
 790            };
 791
 792            let mut previous_entries = std::mem::take(&mut this.external_agents);
 793            let mut new_version_available_txs = HashMap::default();
 794            let mut metadata = HashMap::default();
 795
 796            for (name, mut entry) in previous_entries.drain() {
 797                if let Some(agent) = entry.server.downcast_mut::<RemoteExternalAgentServer>() {
 798                    new_version_available_txs
 799                        .insert(name.clone(), agent.new_version_available_tx.take());
 800                }
 801
 802                metadata.insert(name, (entry.icon, entry.display_name, entry.source));
 803            }
 804
 805            this.external_agents = envelope
 806                .payload
 807                .names
 808                .into_iter()
 809                .map(|name| {
 810                    let agent_id = AgentId(name.into());
 811                    let (icon, display_name, source) = metadata
 812                        .remove(&agent_id)
 813                        .or_else(|| {
 814                            AgentRegistryStore::try_global(cx)
 815                                .and_then(|store| store.read(cx).agent(&agent_id))
 816                                .map(|s| {
 817                                    (
 818                                        s.icon_path().cloned(),
 819                                        Some(s.name().clone()),
 820                                        ExternalAgentSource::Registry,
 821                                    )
 822                                })
 823                        })
 824                        .unwrap_or((None, None, ExternalAgentSource::default()));
 825                    let agent = RemoteExternalAgentServer {
 826                        project_id: *project_id,
 827                        upstream_client: upstream_client.clone(),
 828                        worktree_store: worktree_store.clone(),
 829                        name: agent_id.clone(),
 830                        new_version_available_tx: new_version_available_txs
 831                            .remove(&agent_id)
 832                            .flatten(),
 833                    };
 834                    (
 835                        agent_id,
 836                        ExternalAgentEntry::new(
 837                            Box::new(agent) as Box<dyn ExternalAgentServer>,
 838                            source,
 839                            icon,
 840                            display_name,
 841                        ),
 842                    )
 843                })
 844                .collect();
 845            cx.emit(AgentServersUpdated);
 846            Ok(())
 847        })
 848    }
 849
 850    async fn handle_external_extension_agents_updated(
 851        this: Entity<Self>,
 852        envelope: TypedEnvelope<proto::ExternalExtensionAgentsUpdated>,
 853        mut cx: AsyncApp,
 854    ) -> Result<()> {
 855        this.update(&mut cx, |this, cx| {
 856            let AgentServerStoreState::Local {
 857                extension_agents, ..
 858            } = &mut this.state
 859            else {
 860                panic!(
 861                    "handle_external_extension_agents_updated \
 862                    should not be called for a non-remote project"
 863                );
 864            };
 865
 866            for ExternalExtensionAgent {
 867                name,
 868                icon_path,
 869                extension_id,
 870                targets,
 871                env,
 872            } in envelope.payload.agents
 873            {
 874                extension_agents.push((
 875                    Arc::from(&*name),
 876                    extension_id,
 877                    targets
 878                        .into_iter()
 879                        .map(|(k, v)| (k, extension::TargetConfig::from_proto(v)))
 880                        .collect(),
 881                    env.into_iter().collect(),
 882                    icon_path,
 883                    None,
 884                ));
 885            }
 886
 887            this.reregister_agents(cx);
 888            cx.emit(AgentServersUpdated);
 889            Ok(())
 890        })
 891    }
 892
 893    async fn handle_new_version_available(
 894        this: Entity<Self>,
 895        envelope: TypedEnvelope<proto::NewExternalAgentVersionAvailable>,
 896        mut cx: AsyncApp,
 897    ) -> Result<()> {
 898        this.update(&mut cx, |this, _| {
 899            if let Some(agent) = this.external_agents.get_mut(&*envelope.payload.name)
 900                && let Some(agent) = agent.server.downcast_mut::<RemoteExternalAgentServer>()
 901                && let Some(new_version_available_tx) = &mut agent.new_version_available_tx
 902            {
 903                new_version_available_tx
 904                    .send(Some(envelope.payload.version))
 905                    .ok();
 906            }
 907        });
 908        Ok(())
 909    }
 910
 911    pub fn get_extension_id_for_agent(&mut self, name: &AgentId) -> Option<Arc<str>> {
 912        self.external_agents.get_mut(name).and_then(|entry| {
 913            entry
 914                .server
 915                .as_any_mut()
 916                .downcast_ref::<LocalExtensionArchiveAgent>()
 917                .map(|ext_agent| ext_agent.extension_id.clone())
 918        })
 919    }
 920}
 921
 922struct RemoteExternalAgentServer {
 923    project_id: u64,
 924    upstream_client: Entity<RemoteClient>,
 925    worktree_store: Entity<WorktreeStore>,
 926    name: AgentId,
 927    new_version_available_tx: Option<watch::Sender<Option<String>>>,
 928}
 929
 930impl ExternalAgentServer for RemoteExternalAgentServer {
 931    fn get_command(
 932        &mut self,
 933        extra_env: HashMap<String, String>,
 934        new_version_available_tx: Option<watch::Sender<Option<String>>>,
 935        cx: &mut AsyncApp,
 936    ) -> Task<Result<AgentServerCommand>> {
 937        let project_id = self.project_id;
 938        let name = self.name.to_string();
 939        let upstream_client = self.upstream_client.downgrade();
 940        let worktree_store = self.worktree_store.clone();
 941        self.new_version_available_tx = new_version_available_tx;
 942        cx.spawn(async move |cx| {
 943            let root_dir = worktree_store.read_with(cx, |worktree_store, cx| {
 944                crate::Project::default_visible_worktree_paths(worktree_store, cx)
 945                    .into_iter()
 946                    .next()
 947                    .map(|path| path.display().to_string())
 948            });
 949
 950            let mut response = upstream_client
 951                .update(cx, |upstream_client, _| {
 952                    upstream_client
 953                        .proto_client()
 954                        .request(proto::GetAgentServerCommand {
 955                            project_id,
 956                            name,
 957                            root_dir,
 958                        })
 959                })?
 960                .await?;
 961            let root_dir = response.root_dir;
 962            response.env.extend(extra_env);
 963            let command = upstream_client.update(cx, |client, _| {
 964                client.build_command_with_options(
 965                    Some(response.path),
 966                    &response.args,
 967                    &response.env.into_iter().collect(),
 968                    Some(root_dir.clone()),
 969                    None,
 970                    Interactive::No,
 971                )
 972            })??;
 973            Ok(AgentServerCommand {
 974                path: command.program.into(),
 975                args: command.args,
 976                env: Some(command.env),
 977            })
 978        })
 979    }
 980
 981    fn as_any_mut(&mut self) -> &mut dyn Any {
 982        self
 983    }
 984}
 985
 986fn asset_kind_for_archive_url(archive_url: &str) -> Result<AssetKind> {
 987    let archive_path = Url::parse(archive_url)
 988        .ok()
 989        .map(|url| url.path().to_string())
 990        .unwrap_or_else(|| archive_url.to_string());
 991
 992    if archive_path.ends_with(".zip") {
 993        Ok(AssetKind::Zip)
 994    } else if archive_path.ends_with(".tar.gz") || archive_path.ends_with(".tgz") {
 995        Ok(AssetKind::TarGz)
 996    } else if archive_path.ends_with(".tar.bz2") || archive_path.ends_with(".tbz2") {
 997        Ok(AssetKind::TarBz2)
 998    } else {
 999        bail!("unsupported archive type in URL: {archive_url}");
1000    }
1001}
1002
1003struct GithubReleaseArchive {
1004    repo_name_with_owner: String,
1005    tag: String,
1006    asset_name: String,
1007}
1008
1009fn github_release_archive_from_url(archive_url: &str) -> Option<GithubReleaseArchive> {
1010    fn decode_path_segment(segment: &str) -> Option<String> {
1011        percent_decode_str(segment)
1012            .decode_utf8()
1013            .ok()
1014            .map(|segment| segment.into_owned())
1015    }
1016
1017    let url = Url::parse(archive_url).ok()?;
1018    if url.scheme() != "https" || url.host_str()? != "github.com" {
1019        return None;
1020    }
1021
1022    let segments = url.path_segments()?.collect::<Vec<_>>();
1023    if segments.len() < 6 || segments[2] != "releases" || segments[3] != "download" {
1024        return None;
1025    }
1026
1027    Some(GithubReleaseArchive {
1028        repo_name_with_owner: format!("{}/{}", segments[0], segments[1]),
1029        tag: decode_path_segment(segments[4])?,
1030        asset_name: segments[5..]
1031            .iter()
1032            .map(|segment| decode_path_segment(segment))
1033            .collect::<Option<Vec<_>>>()?
1034            .join("/"),
1035    })
1036}
1037
1038pub struct LocalExtensionArchiveAgent {
1039    pub fs: Arc<dyn Fs>,
1040    pub http_client: Arc<dyn HttpClient>,
1041    pub node_runtime: NodeRuntime,
1042    pub project_environment: Entity<ProjectEnvironment>,
1043    pub extension_id: Arc<str>,
1044    pub agent_id: Arc<str>,
1045    pub targets: HashMap<String, extension::TargetConfig>,
1046    pub env: HashMap<String, String>,
1047}
1048
1049impl ExternalAgentServer for LocalExtensionArchiveAgent {
1050    fn get_command(
1051        &mut self,
1052        extra_env: HashMap<String, String>,
1053        _new_version_available_tx: Option<watch::Sender<Option<String>>>,
1054        cx: &mut AsyncApp,
1055    ) -> Task<Result<AgentServerCommand>> {
1056        let fs = self.fs.clone();
1057        let http_client = self.http_client.clone();
1058        let node_runtime = self.node_runtime.clone();
1059        let project_environment = self.project_environment.downgrade();
1060        let extension_id = self.extension_id.clone();
1061        let agent_id = self.agent_id.clone();
1062        let targets = self.targets.clone();
1063        let base_env = self.env.clone();
1064
1065        cx.spawn(async move |cx| {
1066            // Get project environment
1067            let mut env = project_environment
1068                .update(cx, |project_environment, cx| {
1069                    project_environment.local_directory_environment(
1070                        &Shell::System,
1071                        paths::home_dir().as_path().into(),
1072                        cx,
1073                    )
1074                })?
1075                .await
1076                .unwrap_or_default();
1077
1078            // Merge manifest env and extra env
1079            env.extend(base_env);
1080            env.extend(extra_env);
1081
1082            let cache_key = format!("{}/{}", extension_id, agent_id);
1083            let dir = paths::external_agents_dir().join(&cache_key);
1084            fs.create_dir(&dir).await?;
1085
1086            // Determine platform key
1087            let os = if cfg!(target_os = "macos") {
1088                "darwin"
1089            } else if cfg!(target_os = "linux") {
1090                "linux"
1091            } else if cfg!(target_os = "windows") {
1092                "windows"
1093            } else {
1094                anyhow::bail!("unsupported OS");
1095            };
1096
1097            let arch = if cfg!(target_arch = "aarch64") {
1098                "aarch64"
1099            } else if cfg!(target_arch = "x86_64") {
1100                "x86_64"
1101            } else {
1102                anyhow::bail!("unsupported architecture");
1103            };
1104
1105            let platform_key = format!("{}-{}", os, arch);
1106            let target_config = targets.get(&platform_key).with_context(|| {
1107                format!(
1108                    "no target specified for platform '{}'. Available platforms: {}",
1109                    platform_key,
1110                    targets
1111                        .keys()
1112                        .map(|k| k.as_str())
1113                        .collect::<Vec<_>>()
1114                        .join(", ")
1115                )
1116            })?;
1117
1118            let archive_url = &target_config.archive;
1119
1120            // Use URL as version identifier for caching
1121            // Hash the URL to get a stable directory name
1122            let mut hasher = Sha256::new();
1123            hasher.update(archive_url.as_bytes());
1124            let url_hash = format!("{:x}", hasher.finalize());
1125            let version_dir = dir.join(format!("v_{}", url_hash));
1126
1127            if !fs.is_dir(&version_dir).await {
1128                // Determine SHA256 for verification
1129                let sha256 = if let Some(provided_sha) = &target_config.sha256 {
1130                    // Use provided SHA256
1131                    Some(provided_sha.clone())
1132                } else if let Some(github_archive) = github_release_archive_from_url(archive_url) {
1133                    // Try to fetch SHA256 from GitHub API
1134                    if let Ok(release) = ::http_client::github::get_release_by_tag_name(
1135                        &github_archive.repo_name_with_owner,
1136                        &github_archive.tag,
1137                        http_client.clone(),
1138                    )
1139                    .await
1140                    {
1141                        // Find matching asset
1142                        if let Some(asset) = release
1143                            .assets
1144                            .iter()
1145                            .find(|a| a.name == github_archive.asset_name)
1146                        {
1147                            // Strip "sha256:" prefix if present
1148                            asset.digest.as_ref().map(|d| {
1149                                d.strip_prefix("sha256:")
1150                                    .map(|s| s.to_string())
1151                                    .unwrap_or_else(|| d.clone())
1152                            })
1153                        } else {
1154                            None
1155                        }
1156                    } else {
1157                        None
1158                    }
1159                } else {
1160                    None
1161                };
1162
1163                let asset_kind = asset_kind_for_archive_url(archive_url)?;
1164
1165                // Download and extract
1166                ::http_client::github_download::download_server_binary(
1167                    &*http_client,
1168                    archive_url,
1169                    sha256.as_deref(),
1170                    &version_dir,
1171                    asset_kind,
1172                )
1173                .await?;
1174            }
1175
1176            // Validate and resolve cmd path
1177            let cmd = &target_config.cmd;
1178
1179            let cmd_path = if cmd == "node" {
1180                // Use Zed's managed Node.js runtime
1181                node_runtime.binary_path().await?
1182            } else {
1183                if cmd.contains("..") {
1184                    anyhow::bail!("command path cannot contain '..': {}", cmd);
1185                }
1186
1187                if cmd.starts_with("./") || cmd.starts_with(".\\") {
1188                    // Relative to extraction directory
1189                    let cmd_path = version_dir.join(&cmd[2..]);
1190                    anyhow::ensure!(
1191                        fs.is_file(&cmd_path).await,
1192                        "Missing command {} after extraction",
1193                        cmd_path.to_string_lossy()
1194                    );
1195                    cmd_path
1196                } else {
1197                    // On PATH
1198                    anyhow::bail!("command must be relative (start with './'): {}", cmd);
1199                }
1200            };
1201
1202            let command = AgentServerCommand {
1203                path: cmd_path,
1204                args: target_config.args.clone(),
1205                env: Some(env),
1206            };
1207
1208            Ok(command)
1209        })
1210    }
1211
1212    fn as_any_mut(&mut self) -> &mut dyn Any {
1213        self
1214    }
1215}
1216
1217struct LocalRegistryArchiveAgent {
1218    fs: Arc<dyn Fs>,
1219    http_client: Arc<dyn HttpClient>,
1220    node_runtime: NodeRuntime,
1221    project_environment: Entity<ProjectEnvironment>,
1222    registry_id: Arc<str>,
1223    targets: HashMap<String, RegistryTargetConfig>,
1224    env: HashMap<String, String>,
1225}
1226
1227impl ExternalAgentServer for LocalRegistryArchiveAgent {
1228    fn get_command(
1229        &mut self,
1230        extra_env: HashMap<String, String>,
1231        _new_version_available_tx: Option<watch::Sender<Option<String>>>,
1232        cx: &mut AsyncApp,
1233    ) -> Task<Result<AgentServerCommand>> {
1234        let fs = self.fs.clone();
1235        let http_client = self.http_client.clone();
1236        let node_runtime = self.node_runtime.clone();
1237        let project_environment = self.project_environment.downgrade();
1238        let registry_id = self.registry_id.clone();
1239        let targets = self.targets.clone();
1240        let settings_env = self.env.clone();
1241
1242        cx.spawn(async move |cx| {
1243            let mut env = project_environment
1244                .update(cx, |project_environment, cx| {
1245                    project_environment.local_directory_environment(
1246                        &Shell::System,
1247                        paths::home_dir().as_path().into(),
1248                        cx,
1249                    )
1250                })?
1251                .await
1252                .unwrap_or_default();
1253
1254            let dir = paths::external_agents_dir()
1255                .join("registry")
1256                .join(registry_id.as_ref());
1257            fs.create_dir(&dir).await?;
1258
1259            let os = if cfg!(target_os = "macos") {
1260                "darwin"
1261            } else if cfg!(target_os = "linux") {
1262                "linux"
1263            } else if cfg!(target_os = "windows") {
1264                "windows"
1265            } else {
1266                anyhow::bail!("unsupported OS");
1267            };
1268
1269            let arch = if cfg!(target_arch = "aarch64") {
1270                "aarch64"
1271            } else if cfg!(target_arch = "x86_64") {
1272                "x86_64"
1273            } else {
1274                anyhow::bail!("unsupported architecture");
1275            };
1276
1277            let platform_key = format!("{}-{}", os, arch);
1278            let target_config = targets.get(&platform_key).with_context(|| {
1279                format!(
1280                    "no target specified for platform '{}'. Available platforms: {}",
1281                    platform_key,
1282                    targets
1283                        .keys()
1284                        .map(|k| k.as_str())
1285                        .collect::<Vec<_>>()
1286                        .join(", ")
1287                )
1288            })?;
1289
1290            env.extend(target_config.env.clone());
1291            env.extend(extra_env);
1292            env.extend(settings_env);
1293
1294            let archive_url = &target_config.archive;
1295
1296            let mut hasher = Sha256::new();
1297            hasher.update(archive_url.as_bytes());
1298            let url_hash = format!("{:x}", hasher.finalize());
1299            let version_dir = dir.join(format!("v_{}", url_hash));
1300
1301            if !fs.is_dir(&version_dir).await {
1302                let sha256 = if let Some(provided_sha) = &target_config.sha256 {
1303                    Some(provided_sha.clone())
1304                } else if let Some(github_archive) = github_release_archive_from_url(archive_url) {
1305                    if let Ok(release) = ::http_client::github::get_release_by_tag_name(
1306                        &github_archive.repo_name_with_owner,
1307                        &github_archive.tag,
1308                        http_client.clone(),
1309                    )
1310                    .await
1311                    {
1312                        if let Some(asset) = release
1313                            .assets
1314                            .iter()
1315                            .find(|a| a.name == github_archive.asset_name)
1316                        {
1317                            asset.digest.as_ref().and_then(|d| {
1318                                d.strip_prefix("sha256:")
1319                                    .map(|s| s.to_string())
1320                                    .or_else(|| Some(d.clone()))
1321                            })
1322                        } else {
1323                            None
1324                        }
1325                    } else {
1326                        None
1327                    }
1328                } else {
1329                    None
1330                };
1331
1332                let asset_kind = asset_kind_for_archive_url(archive_url)?;
1333
1334                ::http_client::github_download::download_server_binary(
1335                    &*http_client,
1336                    archive_url,
1337                    sha256.as_deref(),
1338                    &version_dir,
1339                    asset_kind,
1340                )
1341                .await?;
1342            }
1343
1344            let cmd = &target_config.cmd;
1345
1346            let cmd_path = if cmd == "node" {
1347                node_runtime.binary_path().await?
1348            } else {
1349                if cmd.contains("..") {
1350                    anyhow::bail!("command path cannot contain '..': {}", cmd);
1351                }
1352
1353                if cmd.starts_with("./") || cmd.starts_with(".\\") {
1354                    let cmd_path = version_dir.join(&cmd[2..]);
1355                    anyhow::ensure!(
1356                        fs.is_file(&cmd_path).await,
1357                        "Missing command {} after extraction",
1358                        cmd_path.to_string_lossy()
1359                    );
1360                    cmd_path
1361                } else {
1362                    anyhow::bail!("command must be relative (start with './'): {}", cmd);
1363                }
1364            };
1365
1366            let command = AgentServerCommand {
1367                path: cmd_path,
1368                args: target_config.args.clone(),
1369                env: Some(env),
1370            };
1371
1372            Ok(command)
1373        })
1374    }
1375
1376    fn as_any_mut(&mut self) -> &mut dyn Any {
1377        self
1378    }
1379}
1380
1381struct LocalRegistryNpxAgent {
1382    node_runtime: NodeRuntime,
1383    project_environment: Entity<ProjectEnvironment>,
1384    package: SharedString,
1385    args: Vec<String>,
1386    distribution_env: HashMap<String, String>,
1387    settings_env: HashMap<String, String>,
1388}
1389
1390impl ExternalAgentServer for LocalRegistryNpxAgent {
1391    fn get_command(
1392        &mut self,
1393        extra_env: HashMap<String, String>,
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!["--yes".to_string(), "--".to_string(), package.to_string()];
1417            exec_args.extend(args);
1418
1419            let npm_command = node_runtime
1420                .npm_command(
1421                    "exec",
1422                    &exec_args.iter().map(|a| a.as_str()).collect::<Vec<_>>(),
1423                )
1424                .await?;
1425
1426            env.extend(npm_command.env);
1427            env.extend(distribution_env);
1428            env.extend(extra_env);
1429            env.extend(settings_env);
1430
1431            let command = AgentServerCommand {
1432                path: npm_command.path,
1433                args: npm_command.args,
1434                env: Some(env),
1435            };
1436
1437            Ok(command)
1438        })
1439    }
1440
1441    fn as_any_mut(&mut self) -> &mut dyn Any {
1442        self
1443    }
1444}
1445
1446struct LocalCustomAgent {
1447    project_environment: Entity<ProjectEnvironment>,
1448    command: AgentServerCommand,
1449}
1450
1451impl ExternalAgentServer for LocalCustomAgent {
1452    fn get_command(
1453        &mut self,
1454        extra_env: HashMap<String, String>,
1455        _new_version_available_tx: Option<watch::Sender<Option<String>>>,
1456        cx: &mut AsyncApp,
1457    ) -> Task<Result<AgentServerCommand>> {
1458        let mut command = self.command.clone();
1459        let project_environment = self.project_environment.downgrade();
1460        cx.spawn(async move |cx| {
1461            let mut env = project_environment
1462                .update(cx, |project_environment, cx| {
1463                    project_environment.local_directory_environment(
1464                        &Shell::System,
1465                        paths::home_dir().as_path().into(),
1466                        cx,
1467                    )
1468                })?
1469                .await
1470                .unwrap_or_default();
1471            env.extend(command.env.unwrap_or_default());
1472            env.extend(extra_env);
1473            command.env = Some(env);
1474            Ok(command)
1475        })
1476    }
1477
1478    fn as_any_mut(&mut self) -> &mut dyn Any {
1479        self
1480    }
1481}
1482
1483#[derive(Default, Clone, JsonSchema, Debug, PartialEq, RegisterSetting)]
1484pub struct AllAgentServersSettings(pub HashMap<String, CustomAgentServerSettings>);
1485
1486impl std::ops::Deref for AllAgentServersSettings {
1487    type Target = HashMap<String, CustomAgentServerSettings>;
1488
1489    fn deref(&self) -> &Self::Target {
1490        &self.0
1491    }
1492}
1493
1494impl std::ops::DerefMut for AllAgentServersSettings {
1495    fn deref_mut(&mut self) -> &mut Self::Target {
1496        &mut self.0
1497    }
1498}
1499
1500impl AllAgentServersSettings {
1501    pub fn has_registry_agents(&self) -> bool {
1502        self.values()
1503            .any(|s| matches!(s, CustomAgentServerSettings::Registry { .. }))
1504    }
1505}
1506
1507#[derive(Clone, JsonSchema, Debug, PartialEq)]
1508pub enum CustomAgentServerSettings {
1509    Custom {
1510        command: AgentServerCommand,
1511        /// The default mode to use for this agent.
1512        ///
1513        /// Note: Not only all agents support modes.
1514        ///
1515        /// Default: None
1516        default_mode: Option<String>,
1517        /// The default model to use for this agent.
1518        ///
1519        /// This should be the model ID as reported by the agent.
1520        ///
1521        /// Default: None
1522        default_model: Option<String>,
1523        /// The favorite models for this agent.
1524        ///
1525        /// Default: []
1526        favorite_models: Vec<String>,
1527        /// Default values for session config options.
1528        ///
1529        /// This is a map from config option ID to value ID.
1530        ///
1531        /// Default: {}
1532        default_config_options: HashMap<String, String>,
1533        /// Favorited values for session config options.
1534        ///
1535        /// This is a map from config option ID to a list of favorited value IDs.
1536        ///
1537        /// Default: {}
1538        favorite_config_option_values: HashMap<String, Vec<String>>,
1539    },
1540    Extension {
1541        /// Additional environment variables to pass to the agent.
1542        ///
1543        /// Default: {}
1544        env: HashMap<String, String>,
1545        /// The default mode to use for this agent.
1546        ///
1547        /// Note: Not only all agents support modes.
1548        ///
1549        /// Default: None
1550        default_mode: Option<String>,
1551        /// The default model to use for this agent.
1552        ///
1553        /// This should be the model ID as reported by the agent.
1554        ///
1555        /// Default: None
1556        default_model: Option<String>,
1557        /// The favorite models for this agent.
1558        ///
1559        /// Default: []
1560        favorite_models: Vec<String>,
1561        /// Default values for session config options.
1562        ///
1563        /// This is a map from config option ID to value ID.
1564        ///
1565        /// Default: {}
1566        default_config_options: HashMap<String, String>,
1567        /// Favorited values for session config options.
1568        ///
1569        /// This is a map from config option ID to a list of favorited value IDs.
1570        ///
1571        /// Default: {}
1572        favorite_config_option_values: HashMap<String, Vec<String>>,
1573    },
1574    Registry {
1575        /// Additional environment variables to pass to the agent.
1576        ///
1577        /// Default: {}
1578        env: HashMap<String, String>,
1579        /// The default mode to use for this agent.
1580        ///
1581        /// Note: Not only all agents support modes.
1582        ///
1583        /// Default: None
1584        default_mode: Option<String>,
1585        /// The default model to use for this agent.
1586        ///
1587        /// This should be the model ID as reported by the agent.
1588        ///
1589        /// Default: None
1590        default_model: Option<String>,
1591        /// The favorite models for this agent.
1592        ///
1593        /// Default: []
1594        favorite_models: Vec<String>,
1595        /// Default values for session config options.
1596        ///
1597        /// This is a map from config option ID to value ID.
1598        ///
1599        /// Default: {}
1600        default_config_options: HashMap<String, String>,
1601        /// Favorited values for session config options.
1602        ///
1603        /// This is a map from config option ID to a list of favorited value IDs.
1604        ///
1605        /// Default: {}
1606        favorite_config_option_values: HashMap<String, Vec<String>>,
1607    },
1608}
1609
1610impl CustomAgentServerSettings {
1611    pub fn command(&self) -> Option<&AgentServerCommand> {
1612        match self {
1613            CustomAgentServerSettings::Custom { command, .. } => Some(command),
1614            CustomAgentServerSettings::Extension { .. }
1615            | CustomAgentServerSettings::Registry { .. } => None,
1616        }
1617    }
1618
1619    pub fn default_mode(&self) -> Option<&str> {
1620        match self {
1621            CustomAgentServerSettings::Custom { default_mode, .. }
1622            | CustomAgentServerSettings::Extension { default_mode, .. }
1623            | CustomAgentServerSettings::Registry { default_mode, .. } => default_mode.as_deref(),
1624        }
1625    }
1626
1627    pub fn default_model(&self) -> Option<&str> {
1628        match self {
1629            CustomAgentServerSettings::Custom { default_model, .. }
1630            | CustomAgentServerSettings::Extension { default_model, .. }
1631            | CustomAgentServerSettings::Registry { default_model, .. } => default_model.as_deref(),
1632        }
1633    }
1634
1635    pub fn favorite_models(&self) -> &[String] {
1636        match self {
1637            CustomAgentServerSettings::Custom {
1638                favorite_models, ..
1639            }
1640            | CustomAgentServerSettings::Extension {
1641                favorite_models, ..
1642            }
1643            | CustomAgentServerSettings::Registry {
1644                favorite_models, ..
1645            } => favorite_models,
1646        }
1647    }
1648
1649    pub fn default_config_option(&self, config_id: &str) -> Option<&str> {
1650        match self {
1651            CustomAgentServerSettings::Custom {
1652                default_config_options,
1653                ..
1654            }
1655            | CustomAgentServerSettings::Extension {
1656                default_config_options,
1657                ..
1658            }
1659            | CustomAgentServerSettings::Registry {
1660                default_config_options,
1661                ..
1662            } => default_config_options.get(config_id).map(|s| s.as_str()),
1663        }
1664    }
1665
1666    pub fn favorite_config_option_values(&self, config_id: &str) -> Option<&[String]> {
1667        match self {
1668            CustomAgentServerSettings::Custom {
1669                favorite_config_option_values,
1670                ..
1671            }
1672            | CustomAgentServerSettings::Extension {
1673                favorite_config_option_values,
1674                ..
1675            }
1676            | CustomAgentServerSettings::Registry {
1677                favorite_config_option_values,
1678                ..
1679            } => favorite_config_option_values
1680                .get(config_id)
1681                .map(|v| v.as_slice()),
1682        }
1683    }
1684}
1685
1686#[cfg(test)]
1687mod tests {
1688    use super::*;
1689
1690    #[test]
1691    fn detects_supported_archive_suffixes() {
1692        assert!(matches!(
1693            asset_kind_for_archive_url("https://example.com/agent.zip"),
1694            Ok(AssetKind::Zip)
1695        ));
1696        assert!(matches!(
1697            asset_kind_for_archive_url("https://example.com/agent.zip?download=1"),
1698            Ok(AssetKind::Zip)
1699        ));
1700        assert!(matches!(
1701            asset_kind_for_archive_url("https://example.com/agent.tar.gz"),
1702            Ok(AssetKind::TarGz)
1703        ));
1704        assert!(matches!(
1705            asset_kind_for_archive_url("https://example.com/agent.tar.gz?download=1#latest"),
1706            Ok(AssetKind::TarGz)
1707        ));
1708        assert!(matches!(
1709            asset_kind_for_archive_url("https://example.com/agent.tgz"),
1710            Ok(AssetKind::TarGz)
1711        ));
1712        assert!(matches!(
1713            asset_kind_for_archive_url("https://example.com/agent.tgz#download"),
1714            Ok(AssetKind::TarGz)
1715        ));
1716        assert!(matches!(
1717            asset_kind_for_archive_url("https://example.com/agent.tar.bz2"),
1718            Ok(AssetKind::TarBz2)
1719        ));
1720        assert!(matches!(
1721            asset_kind_for_archive_url("https://example.com/agent.tar.bz2?download=1"),
1722            Ok(AssetKind::TarBz2)
1723        ));
1724        assert!(matches!(
1725            asset_kind_for_archive_url("https://example.com/agent.tbz2"),
1726            Ok(AssetKind::TarBz2)
1727        ));
1728        assert!(matches!(
1729            asset_kind_for_archive_url("https://example.com/agent.tbz2#download"),
1730            Ok(AssetKind::TarBz2)
1731        ));
1732    }
1733
1734    #[test]
1735    fn parses_github_release_archive_urls() {
1736        let github_archive = github_release_archive_from_url(
1737            "https://github.com/owner/repo/releases/download/release%2F2.3.5/agent.tar.bz2?download=1",
1738        )
1739        .unwrap();
1740
1741        assert_eq!(github_archive.repo_name_with_owner, "owner/repo");
1742        assert_eq!(github_archive.tag, "release/2.3.5");
1743        assert_eq!(github_archive.asset_name, "agent.tar.bz2");
1744    }
1745
1746    #[test]
1747    fn rejects_unsupported_archive_suffixes() {
1748        let error = asset_kind_for_archive_url("https://example.com/agent.tar.xz")
1749            .err()
1750            .map(|error| error.to_string());
1751
1752        assert_eq!(
1753            error,
1754            Some("unsupported archive type in URL: https://example.com/agent.tar.xz".to_string())
1755        );
1756    }
1757}
1758
1759impl From<settings::CustomAgentServerSettings> for CustomAgentServerSettings {
1760    fn from(value: settings::CustomAgentServerSettings) -> Self {
1761        match value {
1762            settings::CustomAgentServerSettings::Custom {
1763                path,
1764                args,
1765                env,
1766                default_mode,
1767                default_model,
1768                favorite_models,
1769                default_config_options,
1770                favorite_config_option_values,
1771            } => CustomAgentServerSettings::Custom {
1772                command: AgentServerCommand {
1773                    path: PathBuf::from(shellexpand::tilde(&path.to_string_lossy()).as_ref()),
1774                    args,
1775                    env: Some(env),
1776                },
1777                default_mode,
1778                default_model,
1779                favorite_models,
1780                default_config_options,
1781                favorite_config_option_values,
1782            },
1783            settings::CustomAgentServerSettings::Extension {
1784                env,
1785                default_mode,
1786                default_model,
1787                default_config_options,
1788                favorite_models,
1789                favorite_config_option_values,
1790            } => CustomAgentServerSettings::Extension {
1791                env,
1792                default_mode,
1793                default_model,
1794                default_config_options,
1795                favorite_models,
1796                favorite_config_option_values,
1797            },
1798            settings::CustomAgentServerSettings::Registry {
1799                env,
1800                default_mode,
1801                default_model,
1802                default_config_options,
1803                favorite_models,
1804                favorite_config_option_values,
1805            } => CustomAgentServerSettings::Registry {
1806                env,
1807                default_mode,
1808                default_model,
1809                default_config_options,
1810                favorite_models,
1811                favorite_config_option_values,
1812            },
1813        }
1814    }
1815}
1816
1817impl settings::Settings for AllAgentServersSettings {
1818    fn from_settings(content: &settings::SettingsContent) -> Self {
1819        let agent_settings = content.agent_servers.clone().unwrap();
1820        Self(
1821            agent_settings
1822                .0
1823                .into_iter()
1824                .map(|(k, v)| (k, v.into()))
1825                .collect(),
1826        )
1827    }
1828}