agent_server_store.rs

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