agent_server_store.rs

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