use remote::Interactive;use std::{ any::Any, borrow::Borrow, path::{Path, PathBuf}, str::FromStr as _, sync::Arc, time::Duration,}; use anyhow::{Context as _, Result, bail};use collections::HashMap;use fs::{Fs, RemoveOptions, RenameOptions};use futures::StreamExt as _;use gpui::{ AppContext as _, AsyncApp, Context, Entity, EventEmitter, SharedString, Subscription, Task,};use http_client::{HttpClient, github::AssetKind};use node_runtime::NodeRuntime;use remote::RemoteClient;use rpc::{ AnyProtoClient, TypedEnvelope, proto::{self, ExternalExtensionAgent},};use schemars::JsonSchema;use semver::Version;use serde::{Deserialize, Serialize};use settings::{RegisterSetting, SettingsStore};use task::{Shell, SpawnInTerminal};use util::{ResultExt as _, debug_panic}; use crate::ProjectEnvironment;use crate::agent_registry_store::{AgentRegistryStore, RegistryAgent, RegistryTargetConfig}; #[derive(Deserialize, Serialize, Clone, PartialEq, Eq, JsonSchema)]pub struct AgentServerCommand { #[serde(rename = "command")] pub path: PathBuf, #[serde(default)] pub args: Vec<String>, pub env: Option<HashMap<String, String>>,} impl std::fmt::Debug for AgentServerCommand { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { let filtered_env = self.env.as_ref().map(|env| { env.iter() .map(|(k, v)| { ( k, if util::redact::should_redact(k) { "[REDACTED]" } else { v }, ) }) .collect::<Vec<_>>() }); f.debug_struct("AgentServerCommand") .field("path", &self.path) .field("args", &self.args) .field("env", &filtered_env) .finish() }} #[derive(Clone, Debug, PartialEq, Eq, Hash)]pub struct ExternalAgentServerName(pub SharedString); impl std::fmt::Display for ExternalAgentServerName { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "{}", self.0) }} impl From<&'static str> for ExternalAgentServerName { fn from(value: &'static str) -> Self { ExternalAgentServerName(value.into()) }} impl From<ExternalAgentServerName> for SharedString { fn from(value: ExternalAgentServerName) -> Self { value.0 }} impl Borrow<str> for ExternalAgentServerName { fn borrow(&self) -> &str { &self.0 }} #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]pub enum ExternalAgentSource { Builtin, #[default] Custom, Extension, Registry,} pub trait ExternalAgentServer { fn get_command( &mut self, root_dir: Option<&str>, extra_env: HashMap<String, String>, status_tx: Option<watch::Sender<SharedString>>, new_version_available_tx: Option<watch::Sender<Option<String>>>, cx: &mut AsyncApp, ) -> Task<Result<(AgentServerCommand, String, Option<task::SpawnInTerminal>)>>; fn as_any_mut(&mut self) -> &mut dyn Any;} impl dyn ExternalAgentServer { fn downcast_mut<T: ExternalAgentServer + 'static>(&mut self) -> Option<&mut T> { self.as_any_mut().downcast_mut() }} enum AgentServerStoreState { Local { node_runtime: NodeRuntime, fs: Arc<dyn Fs>, project_environment: Entity<ProjectEnvironment>, downstream_client: Option<(u64, AnyProtoClient)>, settings: Option<AllAgentServersSettings>, http_client: Arc<dyn HttpClient>, extension_agents: Vec<( Arc<str>, String, HashMap<String, extension::TargetConfig>, HashMap<String, String>, Option<String>, Option<SharedString>, )>, _subscriptions: Vec<Subscription>, }, Remote { project_id: u64, upstream_client: Entity<RemoteClient>, }, Collab,} pub struct ExternalAgentEntry { server: Box<dyn ExternalAgentServer>, icon: Option<SharedString>, display_name: Option<SharedString>, pub source: ExternalAgentSource,} impl ExternalAgentEntry { pub fn new( server: Box<dyn ExternalAgentServer>, source: ExternalAgentSource, icon: Option<SharedString>, display_name: Option<SharedString>, ) -> Self { Self { server, icon, display_name, source, } }} pub struct AgentServerStore { state: AgentServerStoreState, pub external_agents: HashMap<ExternalAgentServerName, ExternalAgentEntry>,} pub struct AgentServersUpdated; impl EventEmitter<AgentServersUpdated> for AgentServerStore {} impl AgentServerStore { /// Synchronizes extension-provided agent servers with the store. pub fn sync_extension_agents<'a, I>( &mut self, manifests: I, extensions_dir: PathBuf, cx: &mut Context<Self>, ) where I: IntoIterator<Item = (&'a str, &'a extension::ExtensionManifest)>, { // Collect manifests first so we can iterate twice let manifests: Vec<_> = manifests.into_iter().collect(); // 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(( agent_name.clone(), ext_id.to_owned(), agent_entry.targets.clone(), agent_entry.env.clone(), icon_path, Some(display_name), )); } } self.reregister_agents(cx); } AgentServerStoreState::Remote { project_id, upstream_client, } => { 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 = ExternalAgentServerName(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(), name: agent_server_name.clone(), status_tx: None, new_version_available_tx: None, }) as Box<dyn ExternalAgentServer>, 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(), }); } } upstream_client .read(cx) .proto_client() .send(proto::ExternalExtensionAgentsUpdated { project_id: *project_id, agents, }) .log_err(); } AgentServerStoreState::Collab => { // Do nothing } } cx.emit(AgentServersUpdated); } pub fn agent_icon(&self, name: &ExternalAgentServerName) -> Option<SharedString> { self.external_agents .get(name) .and_then(|entry| entry.icon.clone()) } pub fn agent_source(&self, name: &ExternalAgentServerName) -> Option<ExternalAgentSource> { self.external_agents.get(name).map(|entry| entry.source) }} /// 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<String> { 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: &ExternalAgentServerName) -> Option<SharedString> { self.external_agents .get(name) .and_then(|entry| entry.display_name.clone()) } pub fn init_remote(session: &AnyProtoClient) { session.add_entity_message_handler(Self::handle_external_agents_updated); session.add_entity_message_handler(Self::handle_loading_status_updated); session.add_entity_message_handler(Self::handle_new_version_available); } 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); } fn agent_servers_settings_changed(&mut self, cx: &mut Context<Self>) { let AgentServerStoreState::Local { settings: old_settings, .. } = &mut self.state else { debug_panic!( "should not be subscribed to agent server settings changes in non-local project" ); return; }; let new_settings = cx .global::<SettingsStore>() .get::<AllAgentServersSettings>(None) .clone(); if Some(&new_settings) == old_settings.as_ref() { return; } self.reregister_agents(cx); } fn reregister_agents(&mut self, cx: &mut Context<Self>) { let AgentServerStoreState::Local { node_runtime, fs, project_environment, downstream_client, settings: old_settings, http_client, extension_agents, .. } = &mut self.state else { debug_panic!("Non-local projects should never attempt to reregister. This is a bug!"); return; }; let new_settings = cx .global::<SettingsStore>() .get::<AllAgentServersSettings>(None) .clone(); // If we don't have agents from the registry loaded yet, trigger a // refresh, which will cause this function to be called again if new_settings.has_registry_agents() && let Some(registry) = AgentRegistryStore::try_global(cx) { registry.update(cx, |registry, cx| registry.refresh_if_stale(cx)); } self.external_agents.clear(); self.external_agents.insert( GEMINI_NAME.into(), ExternalAgentEntry::new( Box::new(LocalGemini { fs: fs.clone(), node_runtime: node_runtime.clone(), project_environment: project_environment.clone(), custom_command: new_settings .gemini .clone() .and_then(|settings| settings.custom_command()), settings_env: new_settings .gemini .as_ref() .and_then(|settings| settings.env.clone()), ignore_system_version: new_settings .gemini .as_ref() .and_then(|settings| settings.ignore_system_version) .unwrap_or(true), }), ExternalAgentSource::Builtin, None, None, ), ); self.external_agents.insert( CODEX_NAME.into(), ExternalAgentEntry::new( Box::new(LocalCodex { fs: fs.clone(), project_environment: project_environment.clone(), custom_command: new_settings .codex .clone() .and_then(|settings| settings.custom_command()), settings_env: new_settings .codex .as_ref() .and_then(|settings| settings.env.clone()), http_client: http_client.clone(), no_browser: downstream_client .as_ref() .is_some_and(|(_, client)| !client.has_wsl_interop()), }), ExternalAgentSource::Builtin, None, None, ), ); self.external_agents.insert( CLAUDE_AGENT_NAME.into(), ExternalAgentEntry::new( Box::new(LocalClaudeCode { fs: fs.clone(), node_runtime: node_runtime.clone(), project_environment: project_environment.clone(), custom_command: new_settings .claude .clone() .and_then(|settings| settings.custom_command()), settings_env: new_settings .claude .as_ref() .and_then(|settings| settings.env.clone()), }), ExternalAgentSource::Builtin, None, None, ), ); let registry_store = AgentRegistryStore::try_global(cx); let registry_agents_by_id = registry_store .as_ref() .map(|store| { store .read(cx) .agents() .iter() .cloned()Lines 1–500 of 2555. Load more
use remote::Interactive; use std::{ any::Any, borrow::Borrow, path::{Path, PathBuf}, str::FromStr as _, sync::Arc, time::Duration, }; use anyhow::{Context as _, Result, bail}; use collections::HashMap; use fs::{Fs, RemoveOptions, RenameOptions}; use futures::StreamExt as _; use gpui::{ AppContext as _, AsyncApp, Context, Entity, EventEmitter, SharedString, Subscription, Task, }; use http_client::{HttpClient, github::AssetKind}; use node_runtime::NodeRuntime; use remote::RemoteClient; use rpc::{ AnyProtoClient, TypedEnvelope, proto::{self, ExternalExtensionAgent}, }; use schemars::JsonSchema; use semver::Version; use serde::{Deserialize, Serialize}; use settings::{RegisterSetting, SettingsStore}; use task::{Shell, SpawnInTerminal}; use util::{ResultExt as _, debug_panic}; use crate::ProjectEnvironment; use crate::agent_registry_store::{AgentRegistryStore, RegistryAgent, RegistryTargetConfig}; #[derive(Deserialize, Serialize, Clone, PartialEq, Eq, JsonSchema)] pub struct AgentServerCommand { #[serde(rename = "command")] pub path: PathBuf, #[serde(default)] pub args: Vec<String>, pub env: Option<HashMap<String, String>>, } impl std::fmt::Debug for AgentServerCommand { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { let filtered_env = self.env.as_ref().map(|env| { env.iter() .map(|(k, v)| { ( k, if util::redact::should_redact(k) { "[REDACTED]" } else { v }, ) }) .collect::<Vec<_>>() }); f.debug_struct("AgentServerCommand") .field("path", &self.path) .field("args", &self.args) .field("env", &filtered_env) .finish() } } #[derive(Clone, Debug, PartialEq, Eq, Hash)] pub struct ExternalAgentServerName(pub SharedString); impl std::fmt::Display for ExternalAgentServerName { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "{}", self.0) } } impl From<&'static str> for ExternalAgentServerName { fn from(value: &'static str) -> Self { ExternalAgentServerName(value.into()) } } impl From<ExternalAgentServerName> for SharedString { fn from(value: ExternalAgentServerName) -> Self { value.0 } } impl Borrow<str> for ExternalAgentServerName { fn borrow(&self) -> &str { &self.0 } } #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] pub enum ExternalAgentSource { Builtin, #[default] Custom, Extension, Registry, } pub trait ExternalAgentServer { fn get_command( &mut self, root_dir: Option<&str>, extra_env: HashMap<String, String>, status_tx: Option<watch::Sender<SharedString>>, new_version_available_tx: Option<watch::Sender<Option<String>>>, cx: &mut AsyncApp, ) -> Task<Result<(AgentServerCommand, String, Option<task::SpawnInTerminal>)>>; fn as_any_mut(&mut self) -> &mut dyn Any; } impl dyn ExternalAgentServer { fn downcast_mut<T: ExternalAgentServer + 'static>(&mut self) -> Option<&mut T> { self.as_any_mut().downcast_mut() } } enum AgentServerStoreState { Local { node_runtime: NodeRuntime, fs: Arc<dyn Fs>, project_environment: Entity<ProjectEnvironment>, downstream_client: Option<(u64, AnyProtoClient)>, settings: Option<AllAgentServersSettings>, http_client: Arc<dyn HttpClient>, extension_agents: Vec<( Arc<str>, String, HashMap<String, extension::TargetConfig>, HashMap<String, String>, Option<String>, Option<SharedString>, )>, _subscriptions: Vec<Subscription>, }, Remote { project_id: u64, upstream_client: Entity<RemoteClient>, }, Collab, } pub struct ExternalAgentEntry { server: Box<dyn ExternalAgentServer>, icon: Option<SharedString>, display_name: Option<SharedString>, pub source: ExternalAgentSource, } impl ExternalAgentEntry { pub fn new( server: Box<dyn ExternalAgentServer>, source: ExternalAgentSource, icon: Option<SharedString>, display_name: Option<SharedString>, ) -> Self { Self { server, icon, display_name, source, } } } pub struct AgentServerStore { state: AgentServerStoreState, pub external_agents: HashMap<ExternalAgentServerName, ExternalAgentEntry>, } pub struct AgentServersUpdated; impl EventEmitter<AgentServersUpdated> for AgentServerStore {} impl AgentServerStore { /// Synchronizes extension-provided agent servers with the store. pub fn sync_extension_agents<'a, I>( &mut self, manifests: I, extensions_dir: PathBuf, cx: &mut Context<Self>, ) where I: IntoIterator<Item = (&'a str, &'a extension::ExtensionManifest)>, { // Collect manifests first so we can iterate twice let manifests: Vec<_> = manifests.into_iter().collect(); // 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(( agent_name.clone(), ext_id.to_owned(), agent_entry.targets.clone(), agent_entry.env.clone(), icon_path, Some(display_name), )); } } self.reregister_agents(cx); } AgentServerStoreState::Remote { project_id, upstream_client, } => { 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 = ExternalAgentServerName(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(), name: agent_server_name.clone(), status_tx: None, new_version_available_tx: None, }) as Box<dyn ExternalAgentServer>, 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(), }); } } upstream_client .read(cx) .proto_client() .send(proto::ExternalExtensionAgentsUpdated { project_id: *project_id, agents, }) .log_err(); } AgentServerStoreState::Collab => { // Do nothing } } cx.emit(AgentServersUpdated); } pub fn agent_icon(&self, name: &ExternalAgentServerName) -> Option<SharedString> { self.external_agents .get(name) .and_then(|entry| entry.icon.clone()) } pub fn agent_source(&self, name: &ExternalAgentServerName) -> Option<ExternalAgentSource> { self.external_agents.get(name).map(|entry| entry.source) } } /// 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<String> { 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: &ExternalAgentServerName) -> Option<SharedString> { self.external_agents .get(name) .and_then(|entry| entry.display_name.clone()) } pub fn init_remote(session: &AnyProtoClient) { session.add_entity_message_handler(Self::handle_external_agents_updated); session.add_entity_message_handler(Self::handle_loading_status_updated); session.add_entity_message_handler(Self::handle_new_version_available); } 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); } fn agent_servers_settings_changed(&mut self, cx: &mut Context<Self>) { let AgentServerStoreState::Local { settings: old_settings, .. } = &mut self.state else { debug_panic!( "should not be subscribed to agent server settings changes in non-local project" ); return; }; let new_settings = cx .global::<SettingsStore>() .get::<AllAgentServersSettings>(None) .clone(); if Some(&new_settings) == old_settings.as_ref() { return; } self.reregister_agents(cx); } fn reregister_agents(&mut self, cx: &mut Context<Self>) { let AgentServerStoreState::Local { node_runtime, fs, project_environment, downstream_client, settings: old_settings, http_client, extension_agents, .. } = &mut self.state else { debug_panic!("Non-local projects should never attempt to reregister. This is a bug!"); return; }; let new_settings = cx .global::<SettingsStore>() .get::<AllAgentServersSettings>(None) .clone(); // If we don't have agents from the registry loaded yet, trigger a // refresh, which will cause this function to be called again if new_settings.has_registry_agents() && let Some(registry) = AgentRegistryStore::try_global(cx) { registry.update(cx, |registry, cx| registry.refresh_if_stale(cx)); } self.external_agents.clear(); self.external_agents.insert( GEMINI_NAME.into(), ExternalAgentEntry::new( Box::new(LocalGemini { fs: fs.clone(), node_runtime: node_runtime.clone(), project_environment: project_environment.clone(), custom_command: new_settings .gemini .clone() .and_then(|settings| settings.custom_command()), settings_env: new_settings .gemini .as_ref() .and_then(|settings| settings.env.clone()), ignore_system_version: new_settings .gemini .as_ref() .and_then(|settings| settings.ignore_system_version) .unwrap_or(true), }), ExternalAgentSource::Builtin, None, None, ), ); self.external_agents.insert( CODEX_NAME.into(), ExternalAgentEntry::new( Box::new(LocalCodex { fs: fs.clone(), project_environment: project_environment.clone(), custom_command: new_settings .codex .clone() .and_then(|settings| settings.custom_command()), settings_env: new_settings .codex .as_ref() .and_then(|settings| settings.env.clone()), http_client: http_client.clone(), no_browser: downstream_client .as_ref() .is_some_and(|(_, client)| !client.has_wsl_interop()), }), ExternalAgentSource::Builtin, None, None, ), ); self.external_agents.insert( CLAUDE_AGENT_NAME.into(), ExternalAgentEntry::new( Box::new(LocalClaudeCode { fs: fs.clone(), node_runtime: node_runtime.clone(), project_environment: project_environment.clone(), custom_command: new_settings .claude .clone() .and_then(|settings| settings.custom_command()), settings_env: new_settings .claude .as_ref() .and_then(|settings| settings.env.clone()), }), ExternalAgentSource::Builtin, None, None, ), ); let registry_store = AgentRegistryStore::try_global(cx); let registry_agents_by_id = registry_store .as_ref() .map(|store| { store .read(cx) .agents() .iter() .cloned()