diff --git a/Cargo.lock b/Cargo.lock index 73206015b40..2ccdca4d6cc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6166,7 +6166,6 @@ name = "extensions_ui" version = "0.1.0" dependencies = [ "anyhow", - "client", "cloud_api_types", "collections", "db", diff --git a/crates/agent_servers/src/custom.rs b/crates/agent_servers/src/custom.rs index b3574f6e81a..77c9595f171 100644 --- a/crates/agent_servers/src/custom.rs +++ b/crates/agent_servers/src/custom.rs @@ -88,22 +88,18 @@ impl AgentServer for CustomAgentServer { let config_id = config_id.to_string(); let value_id = value_id.to_string(); - update_settings_file(fs, cx, move |settings, cx| { + update_settings_file(fs, cx, move |settings, _cx| { let settings = settings .agent_servers .get_or_insert_default() .entry(agent_id.0.to_string()) - .or_insert_with(|| default_settings_for_agent(agent_id, cx)); + .or_insert_with(default_settings_for_agent); match settings { settings::CustomAgentServerSettings::Custom { favorite_config_option_values, .. } - | settings::CustomAgentServerSettings::Extension { - favorite_config_option_values, - .. - } | settings::CustomAgentServerSettings::Registry { favorite_config_option_values, .. @@ -129,16 +125,15 @@ impl AgentServer for CustomAgentServer { fn set_default_mode(&self, mode_id: Option, fs: Arc, cx: &mut App) { let agent_id = self.agent_id(); - update_settings_file(fs, cx, move |settings, cx| { + update_settings_file(fs, cx, move |settings, _cx| { let settings = settings .agent_servers .get_or_insert_default() .entry(agent_id.0.to_string()) - .or_insert_with(|| default_settings_for_agent(agent_id, cx)); + .or_insert_with(default_settings_for_agent); match settings { settings::CustomAgentServerSettings::Custom { default_mode, .. } - | settings::CustomAgentServerSettings::Extension { default_mode, .. } | settings::CustomAgentServerSettings::Registry { default_mode, .. } => { *default_mode = mode_id.map(|m| m.to_string()); } @@ -161,16 +156,15 @@ impl AgentServer for CustomAgentServer { fn set_default_model(&self, model_id: Option, fs: Arc, cx: &mut App) { let agent_id = self.agent_id(); - update_settings_file(fs, cx, move |settings, cx| { + update_settings_file(fs, cx, move |settings, _cx| { let settings = settings .agent_servers .get_or_insert_default() .entry(agent_id.0.to_string()) - .or_insert_with(|| default_settings_for_agent(agent_id, cx)); + .or_insert_with(default_settings_for_agent); match settings { settings::CustomAgentServerSettings::Custom { default_model, .. } - | settings::CustomAgentServerSettings::Extension { default_model, .. } | settings::CustomAgentServerSettings::Registry { default_model, .. } => { *default_model = model_id.map(|m| m.to_string()); } @@ -205,20 +199,17 @@ impl AgentServer for CustomAgentServer { cx: &App, ) { let agent_id = self.agent_id(); - update_settings_file(fs, cx, move |settings, cx| { + update_settings_file(fs, cx, move |settings, _cx| { let settings = settings .agent_servers .get_or_insert_default() .entry(agent_id.0.to_string()) - .or_insert_with(|| default_settings_for_agent(agent_id, cx)); + .or_insert_with(default_settings_for_agent); let favorite_models = match settings { settings::CustomAgentServerSettings::Custom { favorite_models, .. } - | settings::CustomAgentServerSettings::Extension { - favorite_models, .. - } | settings::CustomAgentServerSettings::Registry { favorite_models, .. } => favorite_models, @@ -258,22 +249,18 @@ impl AgentServer for CustomAgentServer { let agent_id = self.agent_id(); let config_id = config_id.to_string(); let value_id = value_id.map(|s| s.to_string()); - update_settings_file(fs, cx, move |settings, cx| { + update_settings_file(fs, cx, move |settings, _cx| { let settings = settings .agent_servers .get_or_insert_default() .entry(agent_id.0.to_string()) - .or_insert_with(|| default_settings_for_agent(agent_id, cx)); + .or_insert_with(default_settings_for_agent); match settings { settings::CustomAgentServerSettings::Custom { default_config_options, .. } - | settings::CustomAgentServerSettings::Extension { - default_config_options, - .. - } | settings::CustomAgentServerSettings::Registry { default_config_options, .. @@ -307,10 +294,6 @@ impl AgentServer for CustomAgentServer { default_config_options, .. } - | project::agent_server_store::CustomAgentServerSettings::Extension { - default_config_options, - .. - } | project::agent_server_store::CustomAgentServerSettings::Registry { default_config_options, .. @@ -422,28 +405,14 @@ fn is_registry_agent(agent_id: impl Into, cx: &App) -> bool { is_in_registry || is_settings_registry } -fn default_settings_for_agent( - agent_id: impl Into, - cx: &App, -) -> settings::CustomAgentServerSettings { - if is_registry_agent(agent_id, cx) { - settings::CustomAgentServerSettings::Registry { - default_model: None, - default_mode: None, - env: Default::default(), - favorite_models: Vec::new(), - default_config_options: Default::default(), - favorite_config_option_values: Default::default(), - } - } else { - settings::CustomAgentServerSettings::Extension { - default_model: None, - default_mode: None, - env: Default::default(), - favorite_models: Vec::new(), - default_config_options: Default::default(), - favorite_config_option_values: Default::default(), - } +fn default_settings_for_agent() -> settings::CustomAgentServerSettings { + settings::CustomAgentServerSettings::Registry { + default_model: None, + default_mode: None, + env: Default::default(), + favorite_models: Vec::new(), + default_config_options: Default::default(), + favorite_config_option_values: Default::default(), } } @@ -547,53 +516,4 @@ mod tests { assert!(is_registry_agent("agent-from-settings", cx)); }); } - - #[gpui::test] - fn test_agent_with_extension_settings_type_is_not_registry(cx: &mut TestAppContext) { - init_test(cx); - set_agent_server_settings( - cx, - vec![( - "my-extension-agent", - settings::CustomAgentServerSettings::Extension { - env: HashMap::default(), - default_mode: None, - default_model: None, - favorite_models: Vec::new(), - default_config_options: HashMap::default(), - favorite_config_option_values: HashMap::default(), - }, - )], - ); - cx.update(|cx| { - assert!(!is_registry_agent("my-extension-agent", cx)); - }); - } - - #[gpui::test] - fn test_default_settings_for_extension_agent(cx: &mut TestAppContext) { - init_test(cx); - cx.update(|cx| { - assert!(matches!( - default_settings_for_agent("some-extension-agent", cx), - settings::CustomAgentServerSettings::Extension { .. } - )); - }); - } - - #[gpui::test] - fn test_default_settings_for_agent_in_registry(cx: &mut TestAppContext) { - init_test(cx); - init_registry_with_agents(cx, &["new-registry-agent"]); - cx.update(|cx| { - assert!(matches!( - default_settings_for_agent("new-registry-agent", cx), - settings::CustomAgentServerSettings::Registry { .. } - )); - assert!(matches!( - default_settings_for_agent("not-in-registry", cx), - settings::CustomAgentServerSettings::Extension { .. } - )); - }); - } } diff --git a/crates/agent_ui/src/agent_configuration.rs b/crates/agent_ui/src/agent_configuration.rs index eb6ea3e81fc..b90a02c30e5 100644 --- a/crates/agent_ui/src/agent_configuration.rs +++ b/crates/agent_ui/src/agent_configuration.rs @@ -1174,7 +1174,6 @@ impl AgentConfiguration { }; let source_kind = match source { - ExternalAgentSource::Extension => AiSettingItemSource::Extension, ExternalAgentSource::Registry => AiSettingItemSource::Registry, ExternalAgentSource::Custom => AiSettingItemSource::Custom, }; @@ -1218,26 +1217,6 @@ impl AgentConfiguration { }); let uninstall_button = match source { - ExternalAgentSource::Extension => Some( - IconButton::new( - SharedString::from(format!("uninstall-{}", id)), - IconName::Trash, - ) - .icon_color(Color::Muted) - .icon_size(IconSize::Small) - .tooltip(Tooltip::text("Uninstall Agent Extension")) - .on_click(cx.listener(move |this, _, _window, cx| { - let agent_name = agent_server_name.clone(); - - if let Some(ext_id) = this.agent_server_store.update(cx, |store, _cx| { - store.get_extension_id_for_agent(&agent_name) - }) { - ExtensionStore::global(cx) - .update(cx, |store, cx| store.uninstall_extension(ext_id, cx)) - .detach_and_log_err(cx); - } - })), - ), ExternalAgentSource::Registry => { let fs = self.fs.clone(); Some( diff --git a/crates/agent_ui/src/agent_panel.rs b/crates/agent_ui/src/agent_panel.rs index cd3f7ae2264..f868ff9360e 100644 --- a/crates/agent_ui/src/agent_panel.rs +++ b/crates/agent_ui/src/agent_panel.rs @@ -59,7 +59,6 @@ use client::UserStore; use cloud_api_types::Plan; use collections::HashMap; use editor::{Editor, MultiBuffer}; -use extension::ExtensionEvents; use extension_host::ExtensionStore; use fs::Fs; @@ -1234,21 +1233,14 @@ impl AgentPanel { }); // Subscribe to extension events to sync agent servers when extensions change - let extension_subscription = if let Some(extension_events) = ExtensionEvents::try_global(cx) - { - Some( - cx.subscribe(&extension_events, |this, _source, event, cx| match event { - extension::Event::ExtensionInstalled(_) - | extension::Event::ExtensionUninstalled(_) - | extension::Event::ExtensionsInstalledChanged => { - this.sync_agent_servers_from_extensions(cx); - } - _ => {} - }), - ) - } else { - None - }; + let extension_subscription = ExtensionStore::try_global(cx).map(|store| { + cx.subscribe(&store, |this, _source, event, cx| match event { + extension_host::Event::ExtensionUninstalled(id) => { + this.migrate_agent_server_from_extensions(id.clone(), cx); + } + _ => {} + }) + }); let connection_store = cx.new(|cx| AgentConnectionStore::new(project.clone(), cx)); let _project_subscription = @@ -1280,7 +1272,7 @@ impl AgentPanel { }) .detach(); - let mut panel = Self { + let panel = Self { workspace_id, base_view, last_created_entry_kind: AgentPanelEntryKind::Thread, @@ -1321,8 +1313,6 @@ impl AgentPanel { is_active: false, }; - // Initial sync of agent servers from extensions - panel.sync_agent_servers_from_extensions(cx); panel.ensure_native_agent_connection(cx); panel } @@ -3901,29 +3891,12 @@ impl AgentPanel { }) } - fn sync_agent_servers_from_extensions(&mut self, cx: &mut Context) { - if let Some(extension_store) = ExtensionStore::try_global(cx) { - let (manifests, extensions_dir) = { - let store = extension_store.read(cx); - let installed = store.installed_extensions(); - let manifests: Vec<_> = installed - .iter() - .map(|(id, entry)| (id.clone(), entry.manifest.clone())) - .collect(); - let extensions_dir = paths::extensions_dir().join("installed"); - (manifests, extensions_dir) - }; - - self.project.update(cx, |project, cx| { - project.agent_server_store().update(cx, |store, cx| { - let manifest_refs: Vec<_> = manifests - .iter() - .map(|(id, manifest)| (id.as_ref(), manifest.as_ref())) - .collect(); - store.sync_extension_agents(manifest_refs, extensions_dir, cx); - }); + fn migrate_agent_server_from_extensions(&mut self, id: Arc, cx: &mut Context) { + self.project.update(cx, |project, cx| { + project.agent_server_store().update(cx, |store, cx| { + store.migrate_agent_server_from_extensions(id, project.fs().clone(), cx); }); - } + }); } pub fn new_agent_thread_with_external_source_prompt( diff --git a/crates/agent_ui/src/agent_registry_ui.rs b/crates/agent_ui/src/agent_registry_ui.rs index be842e0338d..c6918d869b7 100644 --- a/crates/agent_ui/src/agent_registry_ui.rs +++ b/crates/agent_ui/src/agent_registry_ui.rs @@ -34,7 +34,6 @@ enum RegistryInstallStatus { NotInstalled, InstalledRegistry, InstalledCustom, - InstalledExtension, } #[derive(IntoElement)] @@ -155,9 +154,6 @@ impl AgentRegistryPage { RegistryInstallStatus::InstalledRegistry } CustomAgentServerSettings::Custom { .. } => RegistryInstallStatus::InstalledCustom, - CustomAgentServerSettings::Extension { .. } => { - RegistryInstallStatus::InstalledExtension - } }; self.installed_statuses.insert(id.clone(), status); } @@ -560,9 +556,6 @@ impl AgentRegistryPage { RegistryInstallStatus::InstalledCustom => Button::new(button_id, "Installed") .style(ButtonStyle::OutlinedGhost) .disabled(true), - RegistryInstallStatus::InstalledExtension => Button::new(button_id, "Installed") - .style(ButtonStyle::OutlinedGhost) - .disabled(true), } } } diff --git a/crates/cloud_api_types/src/extension.rs b/crates/cloud_api_types/src/extension.rs index 1b00312bad0..bb928ddab0d 100644 --- a/crates/cloud_api_types/src/extension.rs +++ b/crates/cloud_api_types/src/extension.rs @@ -42,8 +42,11 @@ pub enum ExtensionProvides { Grammars, LanguageServers, ContextServers, + /// Deprecated AgentServers, + /// Deprecated SlashCommands, + /// Deprecated IndexedDocsProviders, Snippets, DebugAdapters, diff --git a/crates/extension/src/extension_manifest.rs b/crates/extension/src/extension_manifest.rs index 919051867d5..c4f7ecd07e7 100644 --- a/crates/extension/src/extension_manifest.rs +++ b/crates/extension/src/extension_manifest.rs @@ -107,8 +107,6 @@ pub struct ExtensionManifest { #[serde(default)] pub context_servers: BTreeMap, ContextServerManifestEntry>, #[serde(default)] - pub agent_servers: BTreeMap, AgentServerManifestEntry>, - #[serde(default)] pub slash_commands: BTreeMap, SlashCommandManifestEntry>, #[serde(default)] pub snippets: Option, @@ -150,10 +148,6 @@ impl ExtensionManifest { provides.insert(ExtensionProvides::ContextServers); } - if !self.agent_servers.is_empty() { - provides.insert(ExtensionProvides::AgentServers); - } - if self.snippets.is_some() { provides.insert(ExtensionProvides::Snippets); } @@ -433,7 +427,6 @@ fn manifest_from_old_manifest( .collect(), language_servers: Default::default(), context_servers: BTreeMap::default(), - agent_servers: BTreeMap::default(), slash_commands: BTreeMap::default(), snippets: None, capabilities: Vec::new(), @@ -445,7 +438,6 @@ fn manifest_from_old_manifest( #[cfg(test)] mod tests { - use indoc::indoc; use pretty_assertions::assert_eq; use util::rel_path::rel_path_buf; @@ -469,7 +461,6 @@ mod tests { grammars: BTreeMap::default(), language_servers: BTreeMap::default(), context_servers: BTreeMap::default(), - agent_servers: BTreeMap::default(), slash_commands: BTreeMap::default(), snippets: None, capabilities: vec![], @@ -578,6 +569,8 @@ mod tests { #[test] #[cfg(target_os = "windows")] fn test_deserialize_manifest_with_windows_separators() { + use indoc::indoc; + let content = indoc! {r#" id = "test-manifest" name = "Test Manifest" @@ -588,32 +581,4 @@ mod tests { let manifest: ExtensionManifest = toml::from_str(&content).expect("manifest should parse"); assert_eq!(manifest.languages, vec![rel_path_buf("foo/bar")]); } - - #[test] - fn parse_manifest_with_agent_server_archive_launcher() { - let toml_src = indoc! {r#" - id = "example.agent-server-ext" - name = "Agent Server Example" - version = "1.0.0" - schema_version = 0 - - [agent_servers.foo] - name = "Foo Agent" - - [agent_servers.foo.targets.linux-x86_64] - archive = "https://example.com/agent-linux-x64.tar.gz" - cmd = "./agent" - args = ["--serve"] - "#}; - - let manifest: ExtensionManifest = toml::from_str(toml_src).expect("manifest should parse"); - assert_eq!(manifest.id.as_ref(), "example.agent-server-ext"); - assert!(manifest.agent_servers.contains_key("foo")); - let entry = manifest.agent_servers.get("foo").unwrap(); - assert!(entry.targets.contains_key("linux-x86_64")); - let target = entry.targets.get("linux-x86_64").unwrap(); - assert_eq!(target.archive, "https://example.com/agent-linux-x64.tar.gz"); - assert_eq!(target.cmd, "./agent"); - assert_eq!(target.args, vec!["--serve"]); - } } diff --git a/crates/extension_cli/src/main.rs b/crates/extension_cli/src/main.rs index 737b497fef0..0b7c491ec11 100644 --- a/crates/extension_cli/src/main.rs +++ b/crates/extension_cli/src/main.rs @@ -207,21 +207,6 @@ async fn copy_extension_resources( .context("failed to copy icons")?; } - for (_, agent_entry) in &manifest.agent_servers { - if let Some(icon_path) = &agent_entry.icon { - let source_icon = extension_path.join(icon_path); - let dest_icon = output_dir.join(icon_path); - - // Create parent directory if needed - if let Some(parent) = dest_icon.parent() { - fs::create_dir_all(parent)?; - } - - fs::copy(&source_icon, &dest_icon) - .with_context(|| format!("failed to copy agent server icon '{}'", icon_path))?; - } - } - if !manifest.languages.is_empty() { let output_languages_dir = output_dir.join("languages"); fs::create_dir_all(&output_languages_dir)?; diff --git a/crates/extension_host/benches/extension_compilation_benchmark.rs b/crates/extension_host/benches/extension_compilation_benchmark.rs index 2d3448afe75..b6dade97183 100644 --- a/crates/extension_host/benches/extension_compilation_benchmark.rs +++ b/crates/extension_host/benches/extension_compilation_benchmark.rs @@ -137,7 +137,6 @@ fn manifest() -> ExtensionManifest { .into_iter() .collect(), context_servers: BTreeMap::default(), - agent_servers: BTreeMap::default(), slash_commands: BTreeMap::default(), snippets: None, capabilities: vec![ExtensionCapability::ProcessExec( diff --git a/crates/extension_host/src/capability_granter.rs b/crates/extension_host/src/capability_granter.rs index 6278deef0a7..2cc08d1242c 100644 --- a/crates/extension_host/src/capability_granter.rs +++ b/crates/extension_host/src/capability_granter.rs @@ -107,7 +107,6 @@ mod tests { grammars: BTreeMap::default(), language_servers: BTreeMap::default(), context_servers: BTreeMap::default(), - agent_servers: BTreeMap::default(), slash_commands: BTreeMap::default(), snippets: None, capabilities: vec![], diff --git a/crates/extension_host/src/extension_host.rs b/crates/extension_host/src/extension_host.rs index 3de8e99c5e1..b22d175da56 100644 --- a/crates/extension_host/src/extension_host.rs +++ b/crates/extension_host/src/extension_host.rs @@ -11,7 +11,7 @@ use async_compression::futures::bufread::GzipDecoder; use async_tar::Archive; use client::{Client, proto, telemetry::Telemetry}; use cloud_api_types::{ExtensionMetadata, ExtensionProvides, GetExtensionsResponse}; -use collections::{BTreeMap, BTreeSet, HashSet, btree_map}; +use collections::{BTreeMap, BTreeSet, FxHashSet, HashSet, btree_map}; pub use extension::ExtensionManifest; use extension::extension_builder::{CompileExtensionOptions, ExtensionBuilder}; use extension::{ @@ -48,6 +48,7 @@ use serde::{Deserialize, Serialize}; use settings::{SemanticTokenRules, Settings, SettingsStore}; use std::ops::RangeInclusive; use std::str::FromStr; +use std::sync::LazyLock; use std::{ cmp::Ordering, path::{self, Path, PathBuf}, @@ -77,7 +78,25 @@ const CURRENT_SCHEMA_VERSION: SchemaVersion = SchemaVersion(1); /// /// These snippets should no longer be downloaded or loaded, because their /// functionality has been integrated into the core editor. -const SUPPRESSED_EXTENSIONS: &[&str] = &["snippets", "ruff", "ty", "basedpyright", "basher"]; +static SUPPRESSED_EXTENSIONS: LazyLock> = LazyLock::new(|| { + FxHashSet::from_iter([ + "snippets", + "ruff", + "ty", + "basedpyright", + "basher", + // ACP + "opencode", + "mistral-vibe", + "auggie", + "stakpak", + "codebuddy", + "autohand-acp", + "corust-agent", + "factory-droid", + "qqcode", + ]) +}); /// Returns the [`SchemaVersion`] range that is compatible with this version of Zed. pub fn schema_version_range() -> RangeInclusive { @@ -604,7 +623,7 @@ impl ExtensionStore { .extension_index .extensions .contains_key(extension_id.as_ref()); - !is_already_installed && !SUPPRESSED_EXTENSIONS.contains(&extension_id.as_ref()) + !is_already_installed && !SUPPRESSED_EXTENSIONS.contains(extension_id.as_ref()) }) .cloned() .collect::>(); @@ -687,7 +706,7 @@ impl ExtensionStore { response .data - .retain(|extension| !SUPPRESSED_EXTENSIONS.contains(&extension.id.as_ref())); + .retain(|extension| !SUPPRESSED_EXTENSIONS.contains(extension.id.as_ref())); Ok(response.data) }) @@ -1112,9 +1131,12 @@ impl ExtensionStore { ) -> Task<()> { let old_index = &self.extension_index; - new_index + let suppressed_extensions_to_remove = new_index .extensions - .retain(|extension_id, _| !SUPPRESSED_EXTENSIONS.contains(&extension_id.as_ref())); + .extract_if(.., |extension_id, _| { + SUPPRESSED_EXTENSIONS.contains(extension_id.as_ref()) + }) + .collect::>(); // Determine which extensions need to be loaded and unloaded, based // on the changes to the manifest and the extensions that we know have been @@ -1155,8 +1177,16 @@ impl ExtensionStore { self.modified_extensions.clear(); } + let trigger_suppressed_extension_removal = + move |this: &mut ExtensionStore, cx: &mut Context| { + for (id, _) in suppressed_extensions_to_remove { + this.uninstall_extension(id, cx).detach_and_log_err(cx); + } + }; + if extensions_to_load.is_empty() && extensions_to_unload.is_empty() { self.reload_complete_senders.clear(); + trigger_suppressed_extension_removal(self, cx); return Task::ready(()); } @@ -1496,6 +1526,7 @@ impl ExtensionStore { this.proxy.set_extensions_loaded(); this.proxy.reload_current_theme(cx); this.proxy.reload_current_icon_theme(cx); + trigger_suppressed_extension_removal(this, cx); if let Some(events) = ExtensionEvents::try_global(cx) { events.update(cx, |this, cx| { @@ -1566,10 +1597,6 @@ impl ExtensionStore { let mut extension_manifest = ExtensionManifest::load(fs.clone(), &extension_dir).await?; let extension_id = extension_manifest.id.clone(); - if SUPPRESSED_EXTENSIONS.contains(&extension_id.as_ref()) { - return Ok(()); - } - // TODO: distinguish dev extensions more explicitly, by the absence // of a checksum file that we'll create when downloading normal extensions. let is_dev = fs diff --git a/crates/extension_host/src/extension_store_test.rs b/crates/extension_host/src/extension_store_test.rs index 2e2408ea2d9..dc554d965ad 100644 --- a/crates/extension_host/src/extension_store_test.rs +++ b/crates/extension_host/src/extension_store_test.rs @@ -162,7 +162,6 @@ async fn test_extension_store(cx: &mut TestAppContext) { .collect(), language_servers: BTreeMap::default(), context_servers: BTreeMap::default(), - agent_servers: BTreeMap::default(), slash_commands: BTreeMap::default(), snippets: None, capabilities: Vec::new(), @@ -194,7 +193,6 @@ async fn test_extension_store(cx: &mut TestAppContext) { grammars: BTreeMap::default(), language_servers: BTreeMap::default(), context_servers: BTreeMap::default(), - agent_servers: BTreeMap::default(), slash_commands: BTreeMap::default(), snippets: None, capabilities: Vec::new(), @@ -377,7 +375,6 @@ async fn test_extension_store(cx: &mut TestAppContext) { grammars: BTreeMap::default(), language_servers: BTreeMap::default(), context_servers: BTreeMap::default(), - agent_servers: BTreeMap::default(), slash_commands: BTreeMap::default(), snippets: None, capabilities: Vec::new(), diff --git a/crates/extensions_ui/Cargo.toml b/crates/extensions_ui/Cargo.toml index 6b6b6838313..b1446f0128d 100644 --- a/crates/extensions_ui/Cargo.toml +++ b/crates/extensions_ui/Cargo.toml @@ -13,7 +13,6 @@ path = "src/extensions_ui.rs" [dependencies] anyhow.workspace = true -client.workspace = true cloud_api_types.workspace = true collections.workspace = true db.workspace = true diff --git a/crates/extensions_ui/src/extensions_ui.rs b/crates/extensions_ui/src/extensions_ui.rs index af3b9031e44..04c6876b360 100644 --- a/crates/extensions_ui/src/extensions_ui.rs +++ b/crates/extensions_ui/src/extensions_ui.rs @@ -7,7 +7,6 @@ use std::time::Duration; use std::{ops::Range, sync::Arc}; use anyhow::Context as _; -use client::zed_urls; use cloud_api_types::{ExtensionMetadata, ExtensionProvides}; use collections::{BTreeMap, BTreeSet}; use editor::{Editor, EditorElement, EditorStyle}; @@ -68,7 +67,6 @@ pub fn init(cx: &mut App) { ExtensionCategoryFilter::ContextServers => { ExtensionProvides::ContextServers } - ExtensionCategoryFilter::AgentServers => ExtensionProvides::AgentServers, ExtensionCategoryFilter::Snippets => ExtensionProvides::Snippets, ExtensionCategoryFilter::DebugAdapters => ExtensionProvides::DebugAdapters, }); @@ -287,19 +285,6 @@ fn keywords_by_feature() -> &'static BTreeMap> { }) } -fn acp_registry_upsell_keywords() -> &'static [&'static str] { - &[ - "opencode", - "mistral", - "auggie", - "stakpak", - "codebuddy", - "autohand", - "factory droid", - "corust", - ] -} - fn extension_button_id(extension_id: &Arc, operation: ExtensionOperation) -> ElementId { (SharedString::from(extension_id.clone()), operation as usize).into() } @@ -326,7 +311,6 @@ pub struct ExtensionsPage { _subscriptions: [gpui::Subscription; 2], extension_fetch_task: Option>, upsells: BTreeSet, - show_acp_registry_upsell: bool, } impl ExtensionsPage { @@ -389,7 +373,6 @@ impl ExtensionsPage { _subscriptions: subscriptions, query_editor, upsells: BTreeSet::default(), - show_acp_registry_upsell: false, }; this.fetch_extensions( this.search_query(cx), @@ -824,7 +807,8 @@ impl ExtensionsPage { .iter() .filter_map(|provides| { match provides { - ExtensionProvides::SlashCommands + ExtensionProvides::AgentServers + | ExtensionProvides::SlashCommands | ExtensionProvides::IndexedDocsProviders => { return None; } @@ -1416,13 +1400,11 @@ impl ExtensionsPage { fn refresh_feature_upsells(&mut self, cx: &mut Context) { let Some(search) = self.search_query(cx) else { self.upsells.clear(); - self.show_acp_registry_upsell = false; return; }; if let Some(id) = search.strip_prefix("id:") { self.upsells.clear(); - self.show_acp_registry_upsell = false; let upsell = match id.to_lowercase().as_str() { "ruff" => Some(Feature::ExtensionRuff), @@ -1454,61 +1436,6 @@ impl ExtensionsPage { self.upsells.remove(feature); } } - - self.show_acp_registry_upsell = acp_registry_upsell_keywords() - .iter() - .any(|keyword| search_terms.iter().any(|term| keyword.contains(term))); - } - - fn render_acp_registry_upsell(&self, cx: &mut Context) -> impl IntoElement { - let registry_url = zed_urls::acp_registry_blog(cx); - - let view_registry = Button::new("view_registry", "View Registry") - .style(ButtonStyle::Tinted(ui::TintColor::Warning)) - .on_click({ - let registry_url = registry_url.clone(); - move |_, window, cx| { - telemetry::event!( - "ACP Registry Opened from Extensions", - source = "ACP Registry Upsell", - url = registry_url, - ); - window.dispatch_action(Box::new(zed_actions::AcpRegistry), cx) - } - }); - let open_registry_button = Button::new("open_registry", "Learn More") - .end_icon( - Icon::new(IconName::ArrowUpRight) - .size(IconSize::Small) - .color(Color::Muted), - ) - .on_click({ - move |_event, _window, cx| { - telemetry::event!( - "ACP Registry Viewed", - source = "ACP Registry Upsell", - url = registry_url, - ); - cx.open_url(®istry_url) - } - }); - - div().pt_4().px_4().child( - Banner::new() - .severity(Severity::Warning) - .child( - Label::new( - "Agent Server extensions will be deprecated in favor of the ACP registry.", - ) - .mt_0p5(), - ) - .action_slot( - h_flex() - .gap_1() - .child(open_registry_button) - .child(view_registry), - ), - ) } fn render_feature_upsell_banner( @@ -1832,11 +1759,6 @@ impl Render for ExtensionsPage { ) })), ) - .when( - self.provides_filter == Some(ExtensionProvides::AgentServers) - || self.show_acp_registry_upsell, - |this| this.child(self.render_acp_registry_upsell(cx)), - ) .child(self.render_feature_upsells(cx)) .child(v_flex().px_4().size_full().overflow_y_hidden().map(|this| { let mut count = self.filtered_remote_extension_indices.len(); diff --git a/crates/project/src/agent_server_store.rs b/crates/project/src/agent_server_store.rs index f43f045c5e0..81779906ed6 100644 --- a/crates/project/src/agent_server_store.rs +++ b/crates/project/src/agent_server_store.rs @@ -1,7 +1,7 @@ use std::{ any::Any, path::{Path, PathBuf}, - sync::Arc, + sync::{Arc, LazyLock}, time::Duration, }; @@ -17,14 +17,11 @@ use http_client::{HttpClient, github::AssetKind}; use node_runtime::NodeRuntime; use percent_encoding::percent_decode_str; use remote::RemoteClient; -use rpc::{ - AnyProtoClient, TypedEnvelope, - proto::{self, ExternalExtensionAgent}, -}; +use rpc::{AnyProtoClient, TypedEnvelope, proto}; use schemars::JsonSchema; use semver::Version; use serde::{Deserialize, Serialize}; -use settings::{RegisterSetting, SettingsStore}; +use settings::{RegisterSetting, SettingsStore, update_settings_file}; use sha2::{Digest, Sha256}; use url::Url; use util::{ResultExt as _, debug_panic}; @@ -114,7 +111,6 @@ impl std::borrow::Borrow for AgentId { pub enum ExternalAgentSource { #[default] Custom, - Extension, Registry, } @@ -140,16 +136,6 @@ pub trait ExternalAgentServer { fn as_any_mut(&mut self) -> &mut dyn Any; } -struct ExtensionAgentEntry { - agent_name: Arc, - extension_id: String, - targets: HashMap, - env: HashMap, - icon_path: Option, - display_name: Option, - version: Option, -} - enum AgentServerStoreState { Local { node_runtime: NodeRuntime, @@ -158,7 +144,6 @@ enum AgentServerStoreState { downstream_client: Option<(u64, AnyProtoClient)>, settings: Option, http_client: Arc, - extension_agents: Vec, _subscriptions: Vec, }, Remote { @@ -201,123 +186,54 @@ pub struct AgentServersUpdated; impl EventEmitter for AgentServerStore {} +static EXTENSION_TO_REGISTRY_IDS: LazyLock> = + LazyLock::new(|| { + HashMap::from_iter([ + ("opencode", "opencode"), + ("mistral-vibe", "mistral-vibe"), + ("auggie", "auggie"), + ("stakpak", "stakpak"), + ("codebuddy", "codebuddy-code"), + ("autohand-acp", "autohand"), + ("corust-agent", "corust-agent"), + ("factory-droid", "factory-droid"), + // Unmaintained + // ("qqcode", ""), + ]) + }); + impl AgentServerStore { - /// Synchronizes extension-provided agent servers with the store. - pub fn sync_extension_agents<'a, I>( + pub fn migrate_agent_server_from_extensions( &mut self, - manifests: I, - extensions_dir: PathBuf, + id: Arc, + fs: Arc, cx: &mut Context, - ) where - I: IntoIterator, - { - // Collect manifests first so we can iterate twice - let manifests: Vec<_> = manifests.into_iter().collect(); + ) { + let Some(registry_id) = EXTENSION_TO_REGISTRY_IDS.get(id.as_ref()) else { + return; + }; - // Remove all extension-provided agents - // (They will be re-added below if they're in the currently installed extensions) - self.external_agents - .retain(|_, entry| entry.source != ExternalAgentSource::Extension); - - // Insert agent servers from extension manifests - match &mut self.state { - AgentServerStoreState::Local { - extension_agents, .. - } => { - extension_agents.clear(); - for (ext_id, manifest) in manifests { - for (agent_name, agent_entry) in &manifest.agent_servers { - let display_name = SharedString::from(agent_entry.name.clone()); - let icon_path = agent_entry.icon.as_ref().and_then(|icon| { - resolve_extension_icon_path(&extensions_dir, ext_id, icon) - }); - - extension_agents.push(ExtensionAgentEntry { - agent_name: agent_name.clone(), - extension_id: ext_id.to_owned(), - targets: agent_entry.targets.clone(), - env: agent_entry.env.clone(), - icon_path, - display_name: Some(display_name), - version: Some(SharedString::from(manifest.version.clone())), - }); - } - } - self.reregister_agents(cx); + update_settings_file(fs, cx, move |settings, _| { + let agent_servers = settings.agent_servers.get_or_insert_default(); + // Take the old settings + let settings = agent_servers.remove(id.as_ref()); + // If they had both installed, just remove the extension settings, leave theirregistry settings alone + if agent_servers.contains_key(*registry_id) { + return; } - AgentServerStoreState::Remote { - project_id, - upstream_client, - worktree_store, - } => { - let mut agents = vec![]; - for (ext_id, manifest) in manifests { - for (agent_name, agent_entry) in &manifest.agent_servers { - let display_name = SharedString::from(agent_entry.name.clone()); - let icon_path = agent_entry.icon.as_ref().and_then(|icon| { - resolve_extension_icon_path(&extensions_dir, ext_id, icon) - }); - let icon_shared = icon_path - .as_ref() - .map(|path| SharedString::from(path.clone())); - let icon = icon_path; - let agent_server_name = AgentId(agent_name.clone().into()); - self.external_agents - .entry(agent_server_name.clone()) - .and_modify(|entry| { - entry.icon = icon_shared.clone(); - entry.display_name = Some(display_name.clone()); - entry.source = ExternalAgentSource::Extension; - }) - .or_insert_with(|| { - ExternalAgentEntry::new( - Box::new(RemoteExternalAgentServer { - project_id: *project_id, - upstream_client: upstream_client.clone(), - worktree_store: worktree_store.clone(), - name: agent_server_name.clone(), - new_version_available_tx: None, - }) - as Box, - ExternalAgentSource::Extension, - icon_shared.clone(), - Some(display_name.clone()), - ) - }); - - agents.push(ExternalExtensionAgent { - name: agent_name.to_string(), - icon_path: icon, - extension_id: ext_id.to_string(), - targets: agent_entry - .targets - .iter() - .map(|(k, v)| (k.clone(), v.to_proto())) - .collect(), - env: agent_entry - .env - .iter() - .map(|(k, v)| (k.clone(), v.clone())) - .collect(), - version: Some(manifest.version.to_string()), - }); - } - } - upstream_client - .read(cx) - .proto_client() - .send(proto::ExternalExtensionAgentsUpdated { - project_id: *project_id, - agents, - }) - .log_err(); - } - AgentServerStoreState::Collab => { - // Do nothing - } - } - - cx.emit(AgentServersUpdated); + // Insert the old settings, or write new ones so it is "installed" via the registry + agent_servers.insert( + registry_id.to_string(), + settings.unwrap_or_else(|| settings::CustomAgentServerSettings::Registry { + default_mode: None, + default_model: None, + env: Default::default(), + favorite_models: Vec::new(), + default_config_options: HashMap::default(), + favorite_config_option_values: HashMap::default(), + }), + ); + }); } pub fn agent_icon(&self, id: &AgentId) -> Option { @@ -331,46 +247,6 @@ impl AgentServerStore { } } -/// Safely resolves an extension icon path, ensuring it stays within the extension directory. -/// Returns `None` if the path would escape the extension directory (path traversal attack). -pub fn resolve_extension_icon_path( - extensions_dir: &Path, - extension_id: &str, - icon_relative_path: &str, -) -> Option { - let extension_root = extensions_dir.join(extension_id); - let icon_path = extension_root.join(icon_relative_path); - - // Canonicalize both paths to resolve symlinks and normalize the paths. - // For the extension root, we need to handle the case where it might be a symlink - // (common for dev extensions). - let canonical_extension_root = extension_root.canonicalize().unwrap_or(extension_root); - let canonical_icon_path = match icon_path.canonicalize() { - Ok(path) => path, - Err(err) => { - log::warn!( - "Failed to canonicalize icon path for extension '{}': {} (path: {})", - extension_id, - err, - icon_relative_path - ); - return None; - } - }; - - // Verify the resolved icon path is within the extension directory - if canonical_icon_path.starts_with(&canonical_extension_root) { - Some(canonical_icon_path.to_string_lossy().to_string()) - } else { - log::warn!( - "Icon path '{}' for extension '{}' escapes extension directory, ignoring for security", - icon_relative_path, - extension_id - ); - None - } -} - impl AgentServerStore { pub fn agent_display_name(&self, name: &AgentId) -> Option { self.external_agents @@ -384,7 +260,6 @@ impl AgentServerStore { } pub fn init_headless(session: &AnyProtoClient) { - session.add_entity_message_handler(Self::handle_external_extension_agents_updated); session.add_entity_request_handler(Self::handle_get_agent_server_command); } @@ -419,7 +294,6 @@ impl AgentServerStore { downstream_client, settings: old_settings, http_client, - extension_agents, .. } = &mut self.state else { @@ -470,47 +344,6 @@ impl AgentServerStore { } } - // Insert extension agents before custom/registry so registry entries override extensions. - for entry in extension_agents.iter() { - let name = AgentId(entry.agent_name.clone().into()); - let mut env = entry.env.clone(); - if let Some(settings_env) = - new_settings - .get(entry.agent_name.as_ref()) - .and_then(|settings| match settings { - CustomAgentServerSettings::Extension { env, .. } => Some(env.clone()), - _ => None, - }) - { - env.extend(settings_env); - } - let icon = entry - .icon_path - .as_ref() - .map(|path| SharedString::from(path.clone())); - - self.external_agents.insert( - name.clone(), - ExternalAgentEntry::new( - Box::new(LocalExtensionArchiveAgent { - fs: fs.clone(), - http_client: http_client.clone(), - node_runtime: node_runtime.clone(), - project_environment: project_environment.clone(), - extension_id: Arc::from(&*entry.extension_id), - targets: entry.targets.clone(), - env, - agent_id: entry.agent_name.clone(), - version: entry.version.clone(), - new_version_available_tx: None, - }) as Box, - ExternalAgentSource::Extension, - icon, - entry.display_name.clone(), - ), - ); - } - for (name, settings) in new_settings.iter() { match settings { CustomAgentServerSettings::Custom { command, .. } => { @@ -593,7 +426,6 @@ impl AgentServerStore { } } } - CustomAgentServerSettings::Extension { .. } => {} } } @@ -662,12 +494,10 @@ impl AgentServerStore { http_client, downstream_client: None, settings: None, - extension_agents: vec![], _subscriptions: subscriptions, }, external_agents: HashMap::default(), }; - if let Some(_events) = extension::ExtensionEvents::try_global(cx) {} this.agent_servers_settings_changed(cx); this } @@ -900,52 +730,6 @@ impl AgentServerStore { }) } - async fn handle_external_extension_agents_updated( - this: Entity, - envelope: TypedEnvelope, - mut cx: AsyncApp, - ) -> Result<()> { - this.update(&mut cx, |this, cx| { - let AgentServerStoreState::Local { - extension_agents, .. - } = &mut this.state - else { - panic!( - "handle_external_extension_agents_updated \ - should not be called for a non-remote project" - ); - }; - - extension_agents.clear(); - for ExternalExtensionAgent { - name, - icon_path, - extension_id, - targets, - env, - version, - } in envelope.payload.agents - { - extension_agents.push(ExtensionAgentEntry { - agent_name: Arc::from(&*name), - extension_id, - targets: targets - .into_iter() - .map(|(k, v)| (k, extension::TargetConfig::from_proto(v))) - .collect(), - env: env.into_iter().collect(), - icon_path, - display_name: None, - version: version.map(SharedString::from), - }); - } - - this.reregister_agents(cx); - cx.emit(AgentServersUpdated); - Ok(()) - }) - } - async fn handle_new_version_available( this: Entity, envelope: TypedEnvelope, @@ -961,16 +745,6 @@ impl AgentServerStore { }); Ok(()) } - - pub fn get_extension_id_for_agent(&self, name: &AgentId) -> Option> { - self.external_agents.get(name).and_then(|entry| { - entry - .server - .as_any() - .downcast_ref::() - .map(|ext_agent| ext_agent.extension_id.clone()) - }) - } } struct RemoteExternalAgentServer { @@ -1196,213 +970,6 @@ async fn remove_stale_versioned_archive_cache_dirs( Ok(()) } -pub struct LocalExtensionArchiveAgent { - pub fs: Arc, - pub http_client: Arc, - pub node_runtime: NodeRuntime, - pub project_environment: Entity, - pub extension_id: Arc, - pub agent_id: Arc, - pub targets: HashMap, - pub env: HashMap, - pub version: Option, - pub new_version_available_tx: Option>>, -} - -impl ExternalAgentServer for LocalExtensionArchiveAgent { - fn version(&self) -> Option<&SharedString> { - self.version.as_ref() - } - - fn take_new_version_available_tx(&mut self) -> Option>> { - self.new_version_available_tx.take() - } - - fn set_new_version_available_tx(&mut self, tx: watch::Sender>) { - self.new_version_available_tx = Some(tx); - } - - fn get_command( - &self, - extra_args: Vec, - extra_env: HashMap, - cx: &mut AsyncApp, - ) -> Task> { - let fs = self.fs.clone(); - let http_client = self.http_client.clone(); - let node_runtime = self.node_runtime.clone(); - let project_environment = self.project_environment.downgrade(); - let extension_id = self.extension_id.clone(); - let agent_id = self.agent_id.clone(); - let targets = self.targets.clone(); - let base_env = self.env.clone(); - let version = self.version.clone(); - - cx.spawn(async move |cx| { - // Get project environment - let mut env = project_environment - .update(cx, |project_environment, cx| { - project_environment.default_environment(cx) - })? - .await - .unwrap_or_default(); - - // Merge manifest env and extra env - env.extend(base_env); - env.extend(extra_env); - - let cache_key = format!("{}/{}", extension_id, agent_id); - let dir = paths::external_agents_dir().join(&cache_key); - fs.create_dir(&dir).await?; - - // Determine platform key - let os = if cfg!(target_os = "macos") { - "darwin" - } else if cfg!(target_os = "linux") { - "linux" - } else if cfg!(target_os = "windows") { - "windows" - } else { - anyhow::bail!("unsupported OS"); - }; - - let arch = if cfg!(target_arch = "aarch64") { - "aarch64" - } else if cfg!(target_arch = "x86_64") { - "x86_64" - } else { - anyhow::bail!("unsupported architecture"); - }; - - let platform_key = format!("{}-{}", os, arch); - let target_config = targets.get(&platform_key).with_context(|| { - format!( - "no target specified for platform '{}'. Available platforms: {}", - platform_key, - targets - .keys() - .map(|k| k.as_str()) - .collect::>() - .join(", ") - ) - })?; - - let archive_url = &target_config.archive; - let version_dir = versioned_archive_cache_dir( - &dir, - version.as_ref().map(|version| version.as_ref()), - archive_url, - ); - - if !fs.is_dir(&version_dir).await { - // Determine SHA256 for verification - let sha256 = if let Some(provided_sha) = &target_config.sha256 { - // Use provided SHA256 - Some(provided_sha.clone()) - } else if let Some(github_archive) = github_release_archive_from_url(archive_url) { - // Try to fetch SHA256 from GitHub API - if let Ok(release) = ::http_client::github::get_release_by_tag_name( - &github_archive.repo_name_with_owner, - &github_archive.tag, - http_client.clone(), - ) - .await - { - // Find matching asset - if let Some(asset) = release - .assets - .iter() - .find(|a| a.name == github_archive.asset_name) - { - // Strip "sha256:" prefix if present - asset.digest.as_ref().map(|d| { - d.strip_prefix("sha256:") - .map(|s| s.to_string()) - .unwrap_or_else(|| d.clone()) - }) - } else { - None - } - } else { - None - } - } else { - None - }; - - let asset_kind = asset_kind_for_archive_url(archive_url)?; - - // Download and extract - ::http_client::github_download::download_server_binary( - &*http_client, - archive_url, - sha256.as_deref(), - &version_dir, - asset_kind, - ) - .await?; - } - - // Validate and resolve cmd path - let cmd = &target_config.cmd; - - let cmd_path = if cmd == "node" { - // Use Zed's managed Node.js runtime - node_runtime.binary_path().await? - } else { - if cmd.contains("..") { - anyhow::bail!("command path cannot contain '..': {}", cmd); - } - - if cmd.starts_with("./") || cmd.starts_with(".\\") { - // Relative to extraction directory - let cmd_path = version_dir.join(&cmd[2..]); - anyhow::ensure!( - fs.is_file(&cmd_path).await, - "Missing command {} after extraction", - cmd_path.to_string_lossy() - ); - cmd_path - } else { - // On PATH - anyhow::bail!("command must be relative (start with './'): {}", cmd); - } - }; - - cx.background_spawn({ - let fs = fs.clone(); - let dir = dir.clone(); - let version_dir = version_dir.clone(); - async move { - remove_stale_versioned_archive_cache_dirs(fs, &dir, &version_dir) - .await - .log_err(); - } - }) - .detach(); - - let mut args = target_config.args.clone(); - args.extend(extra_args); - - let command = AgentServerCommand { - path: cmd_path, - args, - env: Some(env), - }; - - Ok(command) - }) - } - - fn as_any(&self) -> &dyn Any { - self - } - - fn as_any_mut(&mut self) -> &mut dyn Any { - self - } -} - struct LocalRegistryArchiveAgent { fs: Arc, http_client: Arc, @@ -1813,40 +1380,6 @@ pub enum CustomAgentServerSettings { /// Default: {} favorite_config_option_values: HashMap>, }, - Extension { - /// Additional environment variables to pass to the agent. - /// - /// Default: {} - env: HashMap, - /// The default mode to use for this agent. - /// - /// Note: Not only all agents support modes. - /// - /// Default: None - default_mode: Option, - /// The default model to use for this agent. - /// - /// This should be the model ID as reported by the agent. - /// - /// Default: None - default_model: Option, - /// The favorite models for this agent. - /// - /// Default: [] - favorite_models: Vec, - /// Default values for session config options. - /// - /// This is a map from config option ID to value ID. - /// - /// Default: {} - default_config_options: HashMap, - /// Favorited values for session config options. - /// - /// This is a map from config option ID to a list of favorited value IDs. - /// - /// Default: {} - favorite_config_option_values: HashMap>, - }, Registry { /// Additional environment variables to pass to the agent. /// @@ -1887,15 +1420,13 @@ impl CustomAgentServerSettings { pub fn command(&self) -> Option<&AgentServerCommand> { match self { CustomAgentServerSettings::Custom { command, .. } => Some(command), - CustomAgentServerSettings::Extension { .. } - | CustomAgentServerSettings::Registry { .. } => None, + CustomAgentServerSettings::Registry { .. } => None, } } pub fn default_mode(&self) -> Option<&str> { match self { CustomAgentServerSettings::Custom { default_mode, .. } - | CustomAgentServerSettings::Extension { default_mode, .. } | CustomAgentServerSettings::Registry { default_mode, .. } => default_mode.as_deref(), } } @@ -1903,7 +1434,6 @@ impl CustomAgentServerSettings { pub fn default_model(&self) -> Option<&str> { match self { CustomAgentServerSettings::Custom { default_model, .. } - | CustomAgentServerSettings::Extension { default_model, .. } | CustomAgentServerSettings::Registry { default_model, .. } => default_model.as_deref(), } } @@ -1913,9 +1443,6 @@ impl CustomAgentServerSettings { CustomAgentServerSettings::Custom { favorite_models, .. } - | CustomAgentServerSettings::Extension { - favorite_models, .. - } | CustomAgentServerSettings::Registry { favorite_models, .. } => favorite_models, @@ -1928,10 +1455,6 @@ impl CustomAgentServerSettings { default_config_options, .. } - | CustomAgentServerSettings::Extension { - default_config_options, - .. - } | CustomAgentServerSettings::Registry { default_config_options, .. @@ -1945,10 +1468,6 @@ impl CustomAgentServerSettings { favorite_config_option_values, .. } - | CustomAgentServerSettings::Extension { - favorite_config_option_values, - .. - } | CustomAgentServerSettings::Registry { favorite_config_option_values, .. @@ -1983,21 +1502,6 @@ impl From for CustomAgentServerSettings { default_config_options, favorite_config_option_values, }, - settings::CustomAgentServerSettings::Extension { - env, - default_mode, - default_model, - default_config_options, - favorite_models, - favorite_config_option_values, - } => CustomAgentServerSettings::Extension { - env, - default_mode, - default_model, - default_config_options, - favorite_models, - favorite_config_option_values, - }, settings::CustomAgentServerSettings::Registry { env, default_mode, @@ -2024,7 +1528,15 @@ impl settings::Settings for AllAgentServersSettings { agent_settings .0 .into_iter() - .map(|(k, v)| (k, v.into())) + .map(|(k, v)| { + ( + EXTENSION_TO_REGISTRY_IDS + .get(&k.as_str()) + .map(|v| v.to_string()) + .unwrap_or(k), + v.into(), + ) + }) .collect(), ) } diff --git a/crates/project/tests/integration/ext_agent_tests.rs b/crates/project/tests/integration/ext_agent_tests.rs deleted file mode 100644 index 82135485d3f..00000000000 --- a/crates/project/tests/integration/ext_agent_tests.rs +++ /dev/null @@ -1,224 +0,0 @@ -use anyhow::Result; -use collections::HashMap; -use gpui::{AsyncApp, SharedString, Task}; -use project::agent_server_store::*; -use std::{any::Any, collections::HashSet, fmt::Write as _, path::PathBuf}; -// A simple fake that implements ExternalAgentServer without needing async plumbing. -struct NoopExternalAgent; - -impl ExternalAgentServer for NoopExternalAgent { - fn get_command( - &self, - _extra_args: Vec, - _extra_env: HashMap, - _cx: &mut AsyncApp, - ) -> Task> { - Task::ready(Ok(AgentServerCommand { - path: PathBuf::from("noop"), - args: Vec::new(), - env: None, - })) - } - - fn as_any(&self) -> &dyn Any { - self - } - - fn as_any_mut(&mut self) -> &mut dyn Any { - self - } -} - -#[test] -fn external_agent_server_name_display() { - let name = AgentId(SharedString::from("Ext: Tool")); - let mut s = String::new(); - write!(&mut s, "{name}").unwrap(); - assert_eq!(s, "Ext: Tool"); -} - -#[test] -fn sync_extension_agents_removes_previous_extension_entries() { - let mut store = AgentServerStore::collab(); - - // Seed with a couple of agents that will be replaced by extensions - store.external_agents.insert( - AgentId(SharedString::from("foo-agent")), - ExternalAgentEntry::new( - Box::new(NoopExternalAgent) as Box, - ExternalAgentSource::Custom, - None, - None, - ), - ); - store.external_agents.insert( - AgentId(SharedString::from("bar-agent")), - ExternalAgentEntry::new( - Box::new(NoopExternalAgent) as Box, - ExternalAgentSource::Custom, - None, - None, - ), - ); - store.external_agents.insert( - AgentId(SharedString::from("custom")), - ExternalAgentEntry::new( - Box::new(NoopExternalAgent) as Box, - ExternalAgentSource::Custom, - None, - None, - ), - ); - - // Simulate the removal phase: if we're syncing extensions that provide - // "foo-agent" and "bar-agent", those should be removed first - let extension_agent_names: HashSet = ["foo-agent".to_string(), "bar-agent".to_string()] - .into_iter() - .collect(); - - let keys_to_remove: Vec<_> = store - .external_agents - .keys() - .filter(|name| extension_agent_names.contains(name.0.as_ref())) - .cloned() - .collect(); - - for key in keys_to_remove { - store.external_agents.remove(&key); - } - - // Only the custom entry should remain. - let remaining: Vec<_> = store - .external_agents - .keys() - .map(|k| k.0.to_string()) - .collect(); - assert_eq!(remaining, vec!["custom".to_string()]); -} - -#[test] -fn resolve_extension_icon_path_allows_valid_paths() { - // Create a temporary directory structure for testing - let temp_dir = tempfile::tempdir().unwrap(); - let extensions_dir = temp_dir.path(); - let ext_dir = extensions_dir.join("my-extension"); - std::fs::create_dir_all(&ext_dir).unwrap(); - - // Create a valid icon file - let icon_path = ext_dir.join("icon.svg"); - std::fs::write(&icon_path, "").unwrap(); - - // Test that a valid relative path works - let result = project::agent_server_store::resolve_extension_icon_path( - extensions_dir, - "my-extension", - "icon.svg", - ); - assert!(result.is_some()); - assert!(result.unwrap().ends_with("icon.svg")); -} - -#[test] -fn resolve_extension_icon_path_allows_nested_paths() { - let temp_dir = tempfile::tempdir().unwrap(); - let extensions_dir = temp_dir.path(); - let ext_dir = extensions_dir.join("my-extension"); - let icons_dir = ext_dir.join("assets").join("icons"); - std::fs::create_dir_all(&icons_dir).unwrap(); - - let icon_path = icons_dir.join("logo.svg"); - std::fs::write(&icon_path, "").unwrap(); - - let result = project::agent_server_store::resolve_extension_icon_path( - extensions_dir, - "my-extension", - "assets/icons/logo.svg", - ); - assert!(result.is_some()); - assert!(result.unwrap().ends_with("logo.svg")); -} - -#[test] -fn resolve_extension_icon_path_blocks_path_traversal() { - let temp_dir = tempfile::tempdir().unwrap(); - let extensions_dir = temp_dir.path(); - - // Create two extension directories - let ext1_dir = extensions_dir.join("extension1"); - let ext2_dir = extensions_dir.join("extension2"); - std::fs::create_dir_all(&ext1_dir).unwrap(); - std::fs::create_dir_all(&ext2_dir).unwrap(); - - // Create a file in extension2 - let secret_file = ext2_dir.join("secret.svg"); - std::fs::write(&secret_file, "secret").unwrap(); - - // Try to access extension2's file from extension1 using path traversal - let result = project::agent_server_store::resolve_extension_icon_path( - extensions_dir, - "extension1", - "../extension2/secret.svg", - ); - assert!( - result.is_none(), - "Path traversal to sibling extension should be blocked" - ); -} - -#[test] -fn resolve_extension_icon_path_blocks_absolute_escape() { - let temp_dir = tempfile::tempdir().unwrap(); - let extensions_dir = temp_dir.path(); - let ext_dir = extensions_dir.join("my-extension"); - std::fs::create_dir_all(&ext_dir).unwrap(); - - // Create a file outside the extensions directory - let outside_file = temp_dir.path().join("outside.svg"); - std::fs::write(&outside_file, "outside").unwrap(); - - // Try to escape to parent directory - let result = project::agent_server_store::resolve_extension_icon_path( - extensions_dir, - "my-extension", - "../outside.svg", - ); - assert!( - result.is_none(), - "Path traversal to parent directory should be blocked" - ); -} - -#[test] -fn resolve_extension_icon_path_blocks_deep_traversal() { - let temp_dir = tempfile::tempdir().unwrap(); - let extensions_dir = temp_dir.path(); - let ext_dir = extensions_dir.join("my-extension"); - std::fs::create_dir_all(&ext_dir).unwrap(); - - // Try deep path traversal - let result = project::agent_server_store::resolve_extension_icon_path( - extensions_dir, - "my-extension", - "../../../../../../etc/passwd", - ); - assert!( - result.is_none(), - "Deep path traversal should be blocked (file doesn't exist)" - ); -} - -#[test] -fn resolve_extension_icon_path_returns_none_for_nonexistent() { - let temp_dir = tempfile::tempdir().unwrap(); - let extensions_dir = temp_dir.path(); - let ext_dir = extensions_dir.join("my-extension"); - std::fs::create_dir_all(&ext_dir).unwrap(); - - // Try to access a file that doesn't exist - let result = project::agent_server_store::resolve_extension_icon_path( - extensions_dir, - "my-extension", - "nonexistent.svg", - ); - assert!(result.is_none(), "Nonexistent file should return None"); -} diff --git a/crates/project/tests/integration/extension_agent_tests.rs b/crates/project/tests/integration/extension_agent_tests.rs deleted file mode 100644 index 5af2cd229c4..00000000000 --- a/crates/project/tests/integration/extension_agent_tests.rs +++ /dev/null @@ -1,332 +0,0 @@ -use anyhow::Result; -use collections::HashMap; -use gpui::{AppContext, AsyncApp, SharedString, Task, TestAppContext}; -use node_runtime::NodeRuntime; -use project::worktree_store::WorktreeStore; -use project::{agent_server_store::*, worktree_store::WorktreeIdCounter}; -use std::{any::Any, path::PathBuf, sync::Arc}; - -#[test] -fn extension_agent_constructs_proper_display_names() { - // Verify the display name format for extension-provided agents - let name1 = AgentId(SharedString::from("Extension: Agent")); - assert!(name1.0.contains(": ")); - - let name2 = AgentId(SharedString::from("MyExt: MyAgent")); - assert_eq!(name2.0, "MyExt: MyAgent"); - - // Non-extension agents shouldn't have the separator - let custom = AgentId(SharedString::from("custom")); - assert!(!custom.0.contains(": ")); -} - -struct NoopExternalAgent; - -impl ExternalAgentServer for NoopExternalAgent { - fn get_command( - &self, - _extra_args: Vec, - _extra_env: HashMap, - _cx: &mut AsyncApp, - ) -> Task> { - Task::ready(Ok(AgentServerCommand { - path: PathBuf::from("noop"), - args: Vec::new(), - env: None, - })) - } - - fn as_any(&self) -> &dyn Any { - self - } - - fn as_any_mut(&mut self) -> &mut dyn Any { - self - } -} - -#[test] -fn sync_removes_only_extension_provided_agents() { - let mut store = AgentServerStore::collab(); - - // Seed with extension agents (contain ": ") and custom agents (don't contain ": ") - store.external_agents.insert( - AgentId(SharedString::from("Ext1: Agent1")), - ExternalAgentEntry::new( - Box::new(NoopExternalAgent) as Box, - ExternalAgentSource::Extension, - None, - None, - ), - ); - store.external_agents.insert( - AgentId(SharedString::from("Ext2: Agent2")), - ExternalAgentEntry::new( - Box::new(NoopExternalAgent) as Box, - ExternalAgentSource::Extension, - None, - None, - ), - ); - store.external_agents.insert( - AgentId(SharedString::from("custom-agent")), - ExternalAgentEntry::new( - Box::new(NoopExternalAgent) as Box, - ExternalAgentSource::Custom, - None, - None, - ), - ); - - // Simulate removal phase - store - .external_agents - .retain(|_, entry| entry.source != ExternalAgentSource::Extension); - - // Only custom-agent should remain - assert_eq!(store.external_agents.len(), 1); - assert!( - store - .external_agents - .contains_key(&AgentId(SharedString::from("custom-agent"))) - ); -} - -#[test] -fn archive_launcher_constructs_with_all_fields() { - use extension::AgentServerManifestEntry; - - let mut env = HashMap::default(); - env.insert("GITHUB_TOKEN".into(), "secret".into()); - - let mut targets = HashMap::default(); - targets.insert( - "darwin-aarch64".to_string(), - extension::TargetConfig { - archive: - "https://github.com/owner/repo/releases/download/v1.0.0/agent-darwin-arm64.zip" - .into(), - cmd: "./agent".into(), - args: vec![], - sha256: None, - env: Default::default(), - }, - ); - - let _entry = AgentServerManifestEntry { - name: "GitHub Agent".into(), - targets, - env, - icon: None, - }; - - // Verify display name construction - let expected_name = AgentId(SharedString::from("GitHub Agent")); - assert_eq!(expected_name.0, "GitHub Agent"); -} - -#[gpui::test] -async fn archive_agent_uses_extension_and_agent_id_for_cache_key(cx: &mut TestAppContext) { - let fs = fs::FakeFs::new(cx.background_executor.clone()); - let http_client = http_client::FakeHttpClient::with_404_response(); - let worktree_store = - cx.new(|cx| WorktreeStore::local(false, fs.clone(), WorktreeIdCounter::get(cx))); - let project_environment = cx.new(|cx| { - crate::ProjectEnvironment::new(None, worktree_store.downgrade(), None, false, cx) - }); - - let agent = LocalExtensionArchiveAgent { - fs, - http_client, - node_runtime: node_runtime::NodeRuntime::unavailable(), - project_environment, - extension_id: Arc::from("my-extension"), - agent_id: Arc::from("my-agent"), - version: Some(SharedString::from("1.0.0")), - targets: { - let mut map = HashMap::default(); - map.insert( - "darwin-aarch64".to_string(), - extension::TargetConfig { - archive: "https://example.com/my-agent-darwin-arm64.zip".into(), - cmd: "./my-agent".into(), - args: vec!["--serve".into()], - sha256: None, - env: Default::default(), - }, - ); - map - }, - env: { - let mut map = HashMap::default(); - map.insert("PORT".into(), "8080".into()); - map - }, - new_version_available_tx: None, - }; - - // Verify agent is properly constructed - assert_eq!(agent.extension_id.as_ref(), "my-extension"); - assert_eq!(agent.agent_id.as_ref(), "my-agent"); - assert_eq!(agent.env.get("PORT"), Some(&"8080".to_string())); - assert!(agent.targets.contains_key("darwin-aarch64")); -} - -#[test] -fn sync_extension_agents_registers_archive_launcher() { - use extension::AgentServerManifestEntry; - - let expected_name = AgentId(SharedString::from("Release Agent")); - assert_eq!(expected_name.0, "Release Agent"); - - // Verify the manifest entry structure for archive-based installation - let mut env = HashMap::default(); - env.insert("API_KEY".into(), "secret".into()); - - let mut targets = HashMap::default(); - targets.insert( - "linux-x86_64".to_string(), - extension::TargetConfig { - archive: "https://github.com/org/project/releases/download/v2.1.0/release-agent-linux-x64.tar.gz".into(), - cmd: "./release-agent".into(), - args: vec!["serve".into()], - sha256: None, - env: Default::default(), - }, - ); - - let manifest_entry = AgentServerManifestEntry { - name: "Release Agent".into(), - targets: targets.clone(), - env, - icon: None, - }; - - // Verify target config is present - assert!(manifest_entry.targets.contains_key("linux-x86_64")); - let target = manifest_entry.targets.get("linux-x86_64").unwrap(); - assert_eq!(target.cmd, "./release-agent"); -} - -#[gpui::test] -async fn test_node_command_uses_managed_runtime(cx: &mut TestAppContext) { - let fs = fs::FakeFs::new(cx.background_executor.clone()); - let http_client = http_client::FakeHttpClient::with_404_response(); - let node_runtime = NodeRuntime::unavailable(); - let worktree_store = - cx.new(|cx| WorktreeStore::local(false, fs.clone(), WorktreeIdCounter::get(cx))); - let project_environment = cx.new(|cx| { - crate::ProjectEnvironment::new(None, worktree_store.downgrade(), None, false, cx) - }); - - let agent = LocalExtensionArchiveAgent { - fs: fs.clone(), - http_client, - node_runtime, - project_environment, - extension_id: Arc::from("node-extension"), - agent_id: Arc::from("node-agent"), - version: Some(SharedString::from("1.0.0")), - targets: { - let mut map = HashMap::default(); - map.insert( - "darwin-aarch64".to_string(), - extension::TargetConfig { - archive: "https://example.com/node-agent.zip".into(), - cmd: "node".into(), - args: vec!["index.js".into()], - sha256: None, - env: Default::default(), - }, - ); - map - }, - env: HashMap::default(), - new_version_available_tx: None, - }; - - // Verify that when cmd is "node", it attempts to use the node runtime - assert_eq!(agent.extension_id.as_ref(), "node-extension"); - assert_eq!(agent.agent_id.as_ref(), "node-agent"); - - let target = agent.targets.get("darwin-aarch64").unwrap(); - assert_eq!(target.cmd, "node"); - assert_eq!(target.args, vec!["index.js"]); -} - -#[gpui::test] -async fn test_commands_run_in_extraction_directory(cx: &mut TestAppContext) { - let fs = fs::FakeFs::new(cx.background_executor.clone()); - let http_client = http_client::FakeHttpClient::with_404_response(); - let node_runtime = NodeRuntime::unavailable(); - let worktree_store = - cx.new(|cx| WorktreeStore::local(false, fs.clone(), WorktreeIdCounter::get(cx))); - let project_environment = cx.new(|cx| { - crate::ProjectEnvironment::new(None, worktree_store.downgrade(), None, false, cx) - }); - - let agent = LocalExtensionArchiveAgent { - fs: fs.clone(), - http_client, - node_runtime, - project_environment, - extension_id: Arc::from("test-ext"), - agent_id: Arc::from("test-agent"), - version: Some(SharedString::from("1.0.0")), - targets: { - let mut map = HashMap::default(); - map.insert( - "darwin-aarch64".to_string(), - extension::TargetConfig { - archive: "https://example.com/test.zip".into(), - cmd: "node".into(), - args: vec![ - "server.js".into(), - "--config".into(), - "./config.json".into(), - ], - sha256: None, - env: Default::default(), - }, - ); - map - }, - env: Default::default(), - new_version_available_tx: None, - }; - - // Verify the agent is configured with relative paths in args - let target = agent.targets.get("darwin-aarch64").unwrap(); - assert_eq!(target.args[0], "server.js"); - assert_eq!(target.args[2], "./config.json"); - // These relative paths will resolve relative to the extraction directory - // when the command is executed -} - -#[test] -fn test_tilde_expansion_in_settings() { - let settings = settings::CustomAgentServerSettings::Custom { - path: PathBuf::from("~/custom/agent"), - args: vec!["serve".into()], - env: Default::default(), - default_mode: None, - default_model: None, - favorite_models: vec![], - default_config_options: Default::default(), - favorite_config_option_values: Default::default(), - }; - - let converted: CustomAgentServerSettings = settings.into(); - let CustomAgentServerSettings::Custom { - command: AgentServerCommand { path, .. }, - .. - } = converted - else { - panic!("Expected Custom variant"); - }; - - assert!( - !path.to_string_lossy().starts_with("~"), - "Tilde should be expanded for custom agent path" - ); -} diff --git a/crates/project/tests/integration/project_tests.rs b/crates/project/tests/integration/project_tests.rs index b93dd8a7274..be3a278a662 100644 --- a/crates/project/tests/integration/project_tests.rs +++ b/crates/project/tests/integration/project_tests.rs @@ -5,8 +5,6 @@ mod bookmark_store; mod color_extractor; mod context_server_store; mod debugger; -mod ext_agent_tests; -mod extension_agent_tests; mod git_store; mod image_store; mod lsp_command; diff --git a/crates/settings_content/src/agent.rs b/crates/settings_content/src/agent.rs index 1a1d4fc6423..d5169b25634 100644 --- a/crates/settings_content/src/agent.rs +++ b/crates/settings_content/src/agent.rs @@ -523,46 +523,8 @@ pub enum CustomAgentServerSettings { #[serde(default, skip_serializing_if = "HashMap::is_empty")] favorite_config_option_values: HashMap>, }, - Extension { - /// Additional environment variables to pass to the agent. - /// - /// Default: {} - #[serde(default, skip_serializing_if = "HashMap::is_empty")] - env: HashMap, - /// The default mode to use for this agent. - /// - /// Note: Not only all agents support modes. - /// - /// Default: None - default_mode: Option, - /// The default model to use for this agent. - /// - /// This should be the model ID as reported by the agent. - /// - /// Default: None - default_model: Option, - /// The favorite models for this agent. - /// - /// These are the model IDs as reported by the agent. - /// - /// Default: [] - #[serde(default, skip_serializing_if = "Vec::is_empty")] - favorite_models: Vec, - /// Default values for session config options. - /// - /// This is a map from config option ID to value ID. - /// - /// Default: {} - #[serde(default, skip_serializing_if = "HashMap::is_empty")] - default_config_options: HashMap, - /// Favorited values for session config options. - /// - /// This is a map from config option ID to a list of favorited value IDs. - /// - /// Default: {} - #[serde(default, skip_serializing_if = "HashMap::is_empty")] - favorite_config_option_values: HashMap>, - }, + // Used for the ACP extension migration + #[serde(alias = "extension")] Registry { /// Additional environment variables to pass to the agent. /// diff --git a/crates/zed_actions/src/lib.rs b/crates/zed_actions/src/lib.rs index 8390cda86c7..1c6bcec766e 100644 --- a/crates/zed_actions/src/lib.rs +++ b/crates/zed_actions/src/lib.rs @@ -87,7 +87,6 @@ pub enum ExtensionCategoryFilter { Grammars, LanguageServers, ContextServers, - AgentServers, Snippets, DebugAdapters, }