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