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 {
1106                    anyhow::bail!("unsupported archive type in URL: {}", archive_url);
1107                };
1108
1109                // Download and extract
1110                ::http_client::github_download::download_server_binary(
1111                    &*http_client,
1112                    archive_url,
1113                    sha256.as_deref(),
1114                    &version_dir,
1115                    asset_kind,
1116                )
1117                .await?;
1118            }
1119
1120            // Validate and resolve cmd path
1121            let cmd = &target_config.cmd;
1122
1123            let cmd_path = if cmd == "node" {
1124                // Use Zed's managed Node.js runtime
1125                node_runtime.binary_path().await?
1126            } else {
1127                if cmd.contains("..") {
1128                    anyhow::bail!("command path cannot contain '..': {}", cmd);
1129                }
1130
1131                if cmd.starts_with("./") || cmd.starts_with(".\\") {
1132                    // Relative to extraction directory
1133                    let cmd_path = version_dir.join(&cmd[2..]);
1134                    anyhow::ensure!(
1135                        fs.is_file(&cmd_path).await,
1136                        "Missing command {} after extraction",
1137                        cmd_path.to_string_lossy()
1138                    );
1139                    cmd_path
1140                } else {
1141                    // On PATH
1142                    anyhow::bail!("command must be relative (start with './'): {}", cmd);
1143                }
1144            };
1145
1146            let command = AgentServerCommand {
1147                path: cmd_path,
1148                args: target_config.args.clone(),
1149                env: Some(env),
1150            };
1151
1152            Ok(command)
1153        })
1154    }
1155
1156    fn as_any_mut(&mut self) -> &mut dyn Any {
1157        self
1158    }
1159}
1160
1161struct LocalRegistryArchiveAgent {
1162    fs: Arc<dyn Fs>,
1163    http_client: Arc<dyn HttpClient>,
1164    node_runtime: NodeRuntime,
1165    project_environment: Entity<ProjectEnvironment>,
1166    registry_id: Arc<str>,
1167    targets: HashMap<String, RegistryTargetConfig>,
1168    env: HashMap<String, String>,
1169}
1170
1171impl ExternalAgentServer for LocalRegistryArchiveAgent {
1172    fn get_command(
1173        &mut self,
1174        extra_env: HashMap<String, String>,
1175        _new_version_available_tx: Option<watch::Sender<Option<String>>>,
1176        cx: &mut AsyncApp,
1177    ) -> Task<Result<AgentServerCommand>> {
1178        let fs = self.fs.clone();
1179        let http_client = self.http_client.clone();
1180        let node_runtime = self.node_runtime.clone();
1181        let project_environment = self.project_environment.downgrade();
1182        let registry_id = self.registry_id.clone();
1183        let targets = self.targets.clone();
1184        let settings_env = self.env.clone();
1185
1186        cx.spawn(async move |cx| {
1187            let mut env = project_environment
1188                .update(cx, |project_environment, cx| {
1189                    project_environment.local_directory_environment(
1190                        &Shell::System,
1191                        paths::home_dir().as_path().into(),
1192                        cx,
1193                    )
1194                })?
1195                .await
1196                .unwrap_or_default();
1197
1198            let dir = paths::external_agents_dir()
1199                .join("registry")
1200                .join(registry_id.as_ref());
1201            fs.create_dir(&dir).await?;
1202
1203            let os = if cfg!(target_os = "macos") {
1204                "darwin"
1205            } else if cfg!(target_os = "linux") {
1206                "linux"
1207            } else if cfg!(target_os = "windows") {
1208                "windows"
1209            } else {
1210                anyhow::bail!("unsupported OS");
1211            };
1212
1213            let arch = if cfg!(target_arch = "aarch64") {
1214                "aarch64"
1215            } else if cfg!(target_arch = "x86_64") {
1216                "x86_64"
1217            } else {
1218                anyhow::bail!("unsupported architecture");
1219            };
1220
1221            let platform_key = format!("{}-{}", os, arch);
1222            let target_config = targets.get(&platform_key).with_context(|| {
1223                format!(
1224                    "no target specified for platform '{}'. Available platforms: {}",
1225                    platform_key,
1226                    targets
1227                        .keys()
1228                        .map(|k| k.as_str())
1229                        .collect::<Vec<_>>()
1230                        .join(", ")
1231                )
1232            })?;
1233
1234            env.extend(target_config.env.clone());
1235            env.extend(extra_env);
1236            env.extend(settings_env);
1237
1238            let archive_url = &target_config.archive;
1239
1240            let mut hasher = Sha256::new();
1241            hasher.update(archive_url.as_bytes());
1242            let url_hash = format!("{:x}", hasher.finalize());
1243            let version_dir = dir.join(format!("v_{}", url_hash));
1244
1245            if !fs.is_dir(&version_dir).await {
1246                let sha256 = if let Some(provided_sha) = &target_config.sha256 {
1247                    Some(provided_sha.clone())
1248                } else if archive_url.starts_with("https://github.com/") {
1249                    if let Some(caps) = archive_url.strip_prefix("https://github.com/") {
1250                        let parts: Vec<&str> = caps.split('/').collect();
1251                        if parts.len() >= 6 && parts[2] == "releases" && parts[3] == "download" {
1252                            let repo = format!("{}/{}", parts[0], parts[1]);
1253                            let tag = parts[4];
1254                            let filename = parts[5..].join("/");
1255
1256                            if let Ok(release) = ::http_client::github::get_release_by_tag_name(
1257                                &repo,
1258                                tag,
1259                                http_client.clone(),
1260                            )
1261                            .await
1262                            {
1263                                if let Some(asset) =
1264                                    release.assets.iter().find(|a| a.name == filename)
1265                                {
1266                                    asset.digest.as_ref().and_then(|d| {
1267                                        d.strip_prefix("sha256:")
1268                                            .map(|s| s.to_string())
1269                                            .or_else(|| Some(d.clone()))
1270                                    })
1271                                } else {
1272                                    None
1273                                }
1274                            } else {
1275                                None
1276                            }
1277                        } else {
1278                            None
1279                        }
1280                    } else {
1281                        None
1282                    }
1283                } else {
1284                    None
1285                };
1286
1287                let asset_kind = if archive_url.ends_with(".zip") {
1288                    AssetKind::Zip
1289                } else if archive_url.ends_with(".tar.gz") || archive_url.ends_with(".tgz") {
1290                    AssetKind::TarGz
1291                } else {
1292                    anyhow::bail!("unsupported archive type in URL: {}", archive_url);
1293                };
1294
1295                ::http_client::github_download::download_server_binary(
1296                    &*http_client,
1297                    archive_url,
1298                    sha256.as_deref(),
1299                    &version_dir,
1300                    asset_kind,
1301                )
1302                .await?;
1303            }
1304
1305            let cmd = &target_config.cmd;
1306
1307            let cmd_path = if cmd == "node" {
1308                node_runtime.binary_path().await?
1309            } else {
1310                if cmd.contains("..") {
1311                    anyhow::bail!("command path cannot contain '..': {}", cmd);
1312                }
1313
1314                if cmd.starts_with("./") || cmd.starts_with(".\\") {
1315                    let cmd_path = version_dir.join(&cmd[2..]);
1316                    anyhow::ensure!(
1317                        fs.is_file(&cmd_path).await,
1318                        "Missing command {} after extraction",
1319                        cmd_path.to_string_lossy()
1320                    );
1321                    cmd_path
1322                } else {
1323                    anyhow::bail!("command must be relative (start with './'): {}", cmd);
1324                }
1325            };
1326
1327            let command = AgentServerCommand {
1328                path: cmd_path,
1329                args: target_config.args.clone(),
1330                env: Some(env),
1331            };
1332
1333            Ok(command)
1334        })
1335    }
1336
1337    fn as_any_mut(&mut self) -> &mut dyn Any {
1338        self
1339    }
1340}
1341
1342struct LocalRegistryNpxAgent {
1343    node_runtime: NodeRuntime,
1344    project_environment: Entity<ProjectEnvironment>,
1345    package: SharedString,
1346    args: Vec<String>,
1347    distribution_env: HashMap<String, String>,
1348    settings_env: HashMap<String, String>,
1349}
1350
1351impl ExternalAgentServer for LocalRegistryNpxAgent {
1352    fn get_command(
1353        &mut self,
1354        extra_env: HashMap<String, String>,
1355        _new_version_available_tx: Option<watch::Sender<Option<String>>>,
1356        cx: &mut AsyncApp,
1357    ) -> Task<Result<AgentServerCommand>> {
1358        let node_runtime = self.node_runtime.clone();
1359        let project_environment = self.project_environment.downgrade();
1360        let package = self.package.clone();
1361        let args = self.args.clone();
1362        let distribution_env = self.distribution_env.clone();
1363        let settings_env = self.settings_env.clone();
1364
1365        cx.spawn(async move |cx| {
1366            let mut env = project_environment
1367                .update(cx, |project_environment, cx| {
1368                    project_environment.local_directory_environment(
1369                        &Shell::System,
1370                        paths::home_dir().as_path().into(),
1371                        cx,
1372                    )
1373                })?
1374                .await
1375                .unwrap_or_default();
1376
1377            let mut exec_args = Vec::new();
1378            exec_args.push("--yes".to_string());
1379            exec_args.push(package.to_string());
1380            if !args.is_empty() {
1381                exec_args.push("--".to_string());
1382                exec_args.extend(args);
1383            }
1384
1385            let npm_command = node_runtime
1386                .npm_command(
1387                    "exec",
1388                    &exec_args.iter().map(|a| a.as_str()).collect::<Vec<_>>(),
1389                )
1390                .await?;
1391
1392            env.extend(npm_command.env);
1393            env.extend(distribution_env);
1394            env.extend(extra_env);
1395            env.extend(settings_env);
1396
1397            let command = AgentServerCommand {
1398                path: npm_command.path,
1399                args: npm_command.args,
1400                env: Some(env),
1401            };
1402
1403            Ok(command)
1404        })
1405    }
1406
1407    fn as_any_mut(&mut self) -> &mut dyn Any {
1408        self
1409    }
1410}
1411
1412struct LocalCustomAgent {
1413    project_environment: Entity<ProjectEnvironment>,
1414    command: AgentServerCommand,
1415}
1416
1417impl ExternalAgentServer for LocalCustomAgent {
1418    fn get_command(
1419        &mut self,
1420        extra_env: HashMap<String, String>,
1421        _new_version_available_tx: Option<watch::Sender<Option<String>>>,
1422        cx: &mut AsyncApp,
1423    ) -> Task<Result<AgentServerCommand>> {
1424        let mut command = self.command.clone();
1425        let project_environment = self.project_environment.downgrade();
1426        cx.spawn(async move |cx| {
1427            let mut env = project_environment
1428                .update(cx, |project_environment, cx| {
1429                    project_environment.local_directory_environment(
1430                        &Shell::System,
1431                        paths::home_dir().as_path().into(),
1432                        cx,
1433                    )
1434                })?
1435                .await
1436                .unwrap_or_default();
1437            env.extend(command.env.unwrap_or_default());
1438            env.extend(extra_env);
1439            command.env = Some(env);
1440            Ok(command)
1441        })
1442    }
1443
1444    fn as_any_mut(&mut self) -> &mut dyn Any {
1445        self
1446    }
1447}
1448
1449#[derive(Default, Clone, JsonSchema, Debug, PartialEq, RegisterSetting)]
1450pub struct AllAgentServersSettings(pub HashMap<String, CustomAgentServerSettings>);
1451
1452impl std::ops::Deref for AllAgentServersSettings {
1453    type Target = HashMap<String, CustomAgentServerSettings>;
1454
1455    fn deref(&self) -> &Self::Target {
1456        &self.0
1457    }
1458}
1459
1460impl std::ops::DerefMut for AllAgentServersSettings {
1461    fn deref_mut(&mut self) -> &mut Self::Target {
1462        &mut self.0
1463    }
1464}
1465
1466impl AllAgentServersSettings {
1467    pub fn has_registry_agents(&self) -> bool {
1468        self.values()
1469            .any(|s| matches!(s, CustomAgentServerSettings::Registry { .. }))
1470    }
1471}
1472
1473#[derive(Clone, JsonSchema, Debug, PartialEq)]
1474pub enum CustomAgentServerSettings {
1475    Custom {
1476        command: AgentServerCommand,
1477        /// The default mode to use for this agent.
1478        ///
1479        /// Note: Not only all agents support modes.
1480        ///
1481        /// Default: None
1482        default_mode: Option<String>,
1483        /// The default model to use for this agent.
1484        ///
1485        /// This should be the model ID as reported by the agent.
1486        ///
1487        /// Default: None
1488        default_model: Option<String>,
1489        /// The favorite models for this agent.
1490        ///
1491        /// Default: []
1492        favorite_models: Vec<String>,
1493        /// Default values for session config options.
1494        ///
1495        /// This is a map from config option ID to value ID.
1496        ///
1497        /// Default: {}
1498        default_config_options: HashMap<String, String>,
1499        /// Favorited values for session config options.
1500        ///
1501        /// This is a map from config option ID to a list of favorited value IDs.
1502        ///
1503        /// Default: {}
1504        favorite_config_option_values: HashMap<String, Vec<String>>,
1505    },
1506    Extension {
1507        /// Additional environment variables to pass to the agent.
1508        ///
1509        /// Default: {}
1510        env: HashMap<String, String>,
1511        /// The default mode to use for this agent.
1512        ///
1513        /// Note: Not only all agents support modes.
1514        ///
1515        /// Default: None
1516        default_mode: Option<String>,
1517        /// The default model to use for this agent.
1518        ///
1519        /// This should be the model ID as reported by the agent.
1520        ///
1521        /// Default: None
1522        default_model: Option<String>,
1523        /// The favorite models for this agent.
1524        ///
1525        /// Default: []
1526        favorite_models: Vec<String>,
1527        /// Default values for session config options.
1528        ///
1529        /// This is a map from config option ID to value ID.
1530        ///
1531        /// Default: {}
1532        default_config_options: HashMap<String, String>,
1533        /// Favorited values for session config options.
1534        ///
1535        /// This is a map from config option ID to a list of favorited value IDs.
1536        ///
1537        /// Default: {}
1538        favorite_config_option_values: HashMap<String, Vec<String>>,
1539    },
1540    Registry {
1541        /// Additional environment variables to pass to the agent.
1542        ///
1543        /// Default: {}
1544        env: HashMap<String, String>,
1545        /// The default mode to use for this agent.
1546        ///
1547        /// Note: Not only all agents support modes.
1548        ///
1549        /// Default: None
1550        default_mode: Option<String>,
1551        /// The default model to use for this agent.
1552        ///
1553        /// This should be the model ID as reported by the agent.
1554        ///
1555        /// Default: None
1556        default_model: Option<String>,
1557        /// The favorite models for this agent.
1558        ///
1559        /// Default: []
1560        favorite_models: Vec<String>,
1561        /// Default values for session config options.
1562        ///
1563        /// This is a map from config option ID to value ID.
1564        ///
1565        /// Default: {}
1566        default_config_options: HashMap<String, String>,
1567        /// Favorited values for session config options.
1568        ///
1569        /// This is a map from config option ID to a list of favorited value IDs.
1570        ///
1571        /// Default: {}
1572        favorite_config_option_values: HashMap<String, Vec<String>>,
1573    },
1574}
1575
1576impl CustomAgentServerSettings {
1577    pub fn command(&self) -> Option<&AgentServerCommand> {
1578        match self {
1579            CustomAgentServerSettings::Custom { command, .. } => Some(command),
1580            CustomAgentServerSettings::Extension { .. }
1581            | CustomAgentServerSettings::Registry { .. } => None,
1582        }
1583    }
1584
1585    pub fn default_mode(&self) -> Option<&str> {
1586        match self {
1587            CustomAgentServerSettings::Custom { default_mode, .. }
1588            | CustomAgentServerSettings::Extension { default_mode, .. }
1589            | CustomAgentServerSettings::Registry { default_mode, .. } => default_mode.as_deref(),
1590        }
1591    }
1592
1593    pub fn default_model(&self) -> Option<&str> {
1594        match self {
1595            CustomAgentServerSettings::Custom { default_model, .. }
1596            | CustomAgentServerSettings::Extension { default_model, .. }
1597            | CustomAgentServerSettings::Registry { default_model, .. } => default_model.as_deref(),
1598        }
1599    }
1600
1601    pub fn favorite_models(&self) -> &[String] {
1602        match self {
1603            CustomAgentServerSettings::Custom {
1604                favorite_models, ..
1605            }
1606            | CustomAgentServerSettings::Extension {
1607                favorite_models, ..
1608            }
1609            | CustomAgentServerSettings::Registry {
1610                favorite_models, ..
1611            } => favorite_models,
1612        }
1613    }
1614
1615    pub fn default_config_option(&self, config_id: &str) -> Option<&str> {
1616        match self {
1617            CustomAgentServerSettings::Custom {
1618                default_config_options,
1619                ..
1620            }
1621            | CustomAgentServerSettings::Extension {
1622                default_config_options,
1623                ..
1624            }
1625            | CustomAgentServerSettings::Registry {
1626                default_config_options,
1627                ..
1628            } => default_config_options.get(config_id).map(|s| s.as_str()),
1629        }
1630    }
1631
1632    pub fn favorite_config_option_values(&self, config_id: &str) -> Option<&[String]> {
1633        match self {
1634            CustomAgentServerSettings::Custom {
1635                favorite_config_option_values,
1636                ..
1637            }
1638            | CustomAgentServerSettings::Extension {
1639                favorite_config_option_values,
1640                ..
1641            }
1642            | CustomAgentServerSettings::Registry {
1643                favorite_config_option_values,
1644                ..
1645            } => favorite_config_option_values
1646                .get(config_id)
1647                .map(|v| v.as_slice()),
1648        }
1649    }
1650}
1651
1652impl From<settings::CustomAgentServerSettings> for CustomAgentServerSettings {
1653    fn from(value: settings::CustomAgentServerSettings) -> Self {
1654        match value {
1655            settings::CustomAgentServerSettings::Custom {
1656                path,
1657                args,
1658                env,
1659                default_mode,
1660                default_model,
1661                favorite_models,
1662                default_config_options,
1663                favorite_config_option_values,
1664            } => CustomAgentServerSettings::Custom {
1665                command: AgentServerCommand {
1666                    path: PathBuf::from(shellexpand::tilde(&path.to_string_lossy()).as_ref()),
1667                    args,
1668                    env: Some(env),
1669                },
1670                default_mode,
1671                default_model,
1672                favorite_models,
1673                default_config_options,
1674                favorite_config_option_values,
1675            },
1676            settings::CustomAgentServerSettings::Extension {
1677                env,
1678                default_mode,
1679                default_model,
1680                default_config_options,
1681                favorite_models,
1682                favorite_config_option_values,
1683            } => CustomAgentServerSettings::Extension {
1684                env,
1685                default_mode,
1686                default_model,
1687                default_config_options,
1688                favorite_models,
1689                favorite_config_option_values,
1690            },
1691            settings::CustomAgentServerSettings::Registry {
1692                env,
1693                default_mode,
1694                default_model,
1695                default_config_options,
1696                favorite_models,
1697                favorite_config_option_values,
1698            } => CustomAgentServerSettings::Registry {
1699                env,
1700                default_mode,
1701                default_model,
1702                default_config_options,
1703                favorite_models,
1704                favorite_config_option_values,
1705            },
1706        }
1707    }
1708}
1709
1710impl settings::Settings for AllAgentServersSettings {
1711    fn from_settings(content: &settings::SettingsContent) -> Self {
1712        let agent_settings = content.agent_servers.clone().unwrap();
1713        Self(
1714            agent_settings
1715                .0
1716                .into_iter()
1717                .map(|(k, v)| (k, v.into()))
1718                .collect(),
1719        )
1720    }
1721}