1725 lines · 61.5 KB · Rust
RawDownload
use remote::Interactive;use std::{    any::Any,    path::{Path, PathBuf},    sync::Arc,    time::Duration,}; use anyhow::{Context as _, Result, bail};use collections::HashMap;use fs::Fs;use gpui::{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 serde::{Deserialize, Serialize};use settings::{RegisterSetting, SettingsStore};use sha2::{Digest, Sha256};use task::Shell;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, PartialOrd, Ord, Serialize, Deserialize, JsonSchema,)]#[serde(transparent)]pub struct AgentId(pub SharedString); impl AgentId {    pub fn new(id: impl Into<SharedString>) -> Self {        AgentId(id.into())    }} impl std::fmt::Display for AgentId {    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {        write!(f, "{}", self.0)    }} impl From<&'static str> for AgentId {    fn from(value: &'static str) -> Self {        AgentId(value.into())    }} impl From<AgentId> for SharedString {    fn from(value: AgentId) -> Self {        value.0    }} impl AsRef<str> for AgentId {    fn as_ref(&self) -> &str {        &self.0    }} impl std::borrow::Borrow<str> for AgentId {    fn borrow(&self) -> &str {        &self.0    }} #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]pub enum ExternalAgentSource {    #[default]    Custom,    Extension,    Registry,} pub trait ExternalAgentServer {    fn get_command(        &mut self,        extra_env: HashMap<String, String>,        new_version_available_tx: Option<watch::Sender<Option<String>>>,        cx: &mut AsyncApp,    ) -> Task<Result<AgentServerCommand>>;     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<AgentId, 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 = 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(),                                        name: agent_server_name.clone(),                                        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: &AgentId) -> Option<SharedString> {        self.external_agents            .get(name)            .and_then(|entry| entry.icon.clone())    }     pub fn agent_source(&self, name: &AgentId) -> 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: &AgentId) -> 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_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        let registry_store = AgentRegistryStore::try_global(cx);        if new_settings.has_registry_agents()            && let Some(registry) = registry_store.as_ref()        {            registry.update(cx, |registry, cx| registry.refresh_if_stale(cx));        }         let registry_agents_by_id = registry_store            .as_ref()            .map(|store| {                store                    .read(cx)                    .agents()                    .iter()                    .cloned()                    .map(|agent| (agent.id().to_string(), agent))                    .collect::<HashMap<_, _>>()            })            .unwrap_or_default();         self.external_agents.clear();         // Insert extension agents before custom/registry so registry entries override extensions.        for (agent_name, ext_id, targets, env, icon_path, display_name) in extension_agents.iter() {            let name = AgentId(agent_name.clone().into());            let mut env = env.clone();            if let Some(settings_env) =                new_settings                    .get(agent_name.as_ref())                    .and_then(|settings| match settings {                        CustomAgentServerSettings::Extension { env, .. } => Some(env.clone()),                        _ => None,                    })            {                env.extend(settings_env);            }            let icon = 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(&**ext_id),                        targets: targets.clone(),                        env,                        agent_id: agent_name.clone(),                    }) as Box<dyn ExternalAgentServer>,                    ExternalAgentSource::Extension,                    icon,                    display_name.clone(),                ),            );        }         for (name, settings) in new_settings.iter() {            match settings {                CustomAgentServerSettings::Custom { command, .. } => {                    let agent_name = AgentId(name.clone().into());                    self.external_agents.insert(                        agent_name.clone(),                        ExternalAgentEntry::new(                            Box::new(LocalCustomAgent {                                command: command.clone(),                                project_environment: project_environment.clone(),                            }) as Box<dyn ExternalAgentServer>,                            ExternalAgentSource::Custom,                            None,                            None,                        ),                    );                }                CustomAgentServerSettings::Registry { env, .. } => {                    let Some(agent) = registry_agents_by_id.get(name) else {                        if registry_store.is_some() {                            log::debug!("Registry agent '{}' not found in ACP registry", name);                        }                        continue;

Lines 1–500 of 1725.