use std::collections::HashMap; use mcp_client::client::Error as ClientError; use mcp_core::tool::Tool; use serde::{Deserialize, Serialize}; use thiserror::Error; use tracing::warn; use utoipa::ToSchema; use crate::config; use crate::config::extensions::name_to_key; use crate::config::permission::PermissionLevel; /// Errors from Extension operation #[derive(Error, Debug)] pub enum ExtensionError { #[error("Failed to start the MCP server from configuration `{0}` `{1}`")] Initialization(ExtensionConfig, ClientError), #[error("Failed a client call to an MCP server: {0}")] Client(#[from] ClientError), #[error("User Message exceeded context-limit. History could not be truncated to accomodate.")] ContextLimit, #[error("Transport error: {0}")] Transport(#[from] mcp_client::transport::Error), #[error("Environment variable `{0}` is not allowed to be overridden.")] InvalidEnvVar(String), #[error("Error during extension setup: {0}")] SetupError(String), #[error("Join error occurred during task execution: {0}")] TaskJoinError(#[from] tokio::task::JoinError), } pub type ExtensionResult = Result; #[derive(Debug, Clone, Deserialize, Serialize, Default, ToSchema)] pub struct Envs { /// A map of environment variables to set, e.g. API_KEY -> some_secret, HOST -> host #[serde(default)] #[serde(flatten)] map: HashMap, } impl Envs { /// List of sensitive env vars that should not be overridden const DISALLOWED_KEYS: [&'static str; 31] = [ // πŸ”§ Binary path manipulation "PATH", // Controls executable lookup paths β€” critical for command hijacking "PATHEXT", // Windows: Determines recognized executable extensions (e.g., .exe, .bat) "SystemRoot", // Windows: Can affect system DLL resolution (e.g., `kernel32.dll`) "windir", // Windows: Alternative to SystemRoot (used in legacy apps) // 🧬 Dynamic linker hijacking (Linux/macOS) "LD_LIBRARY_PATH", // Alters shared library resolution "LD_PRELOAD", // Forces preloading of shared libraries β€” common attack vector "LD_AUDIT", // Loads a monitoring library that can intercept execution "LD_DEBUG", // Enables verbose linker logging (information disclosure risk) "LD_BIND_NOW", // Forces immediate symbol resolution, affecting ASLR "LD_ASSUME_KERNEL", // Tricks linker into thinking it’s running on an older kernel // 🍎 macOS dynamic linker variables "DYLD_LIBRARY_PATH", // Same as LD_LIBRARY_PATH but for macOS "DYLD_INSERT_LIBRARIES", // macOS equivalent of LD_PRELOAD "DYLD_FRAMEWORK_PATH", // Overrides framework lookup paths // 🐍 Python / Node / Ruby / Java / Golang hijacking "PYTHONPATH", // Overrides Python module resolution "PYTHONHOME", // Overrides Python root directory "NODE_OPTIONS", // Injects options/scripts into every Node.js process "RUBYOPT", // Injects Ruby execution flags "GEM_PATH", // Alters where RubyGems looks for installed packages "GEM_HOME", // Changes RubyGems default install location "CLASSPATH", // Java: Controls where classes are loaded from β€” critical for RCE attacks "GO111MODULE", // Go: Forces use of module proxy or disables it "GOROOT", // Go: Changes root installation directory (could lead to execution hijacking) // πŸ–₯️ Windows-specific process & DLL hijacking "APPINIT_DLLS", // Forces Windows to load a DLL into every process "SESSIONNAME", // Affects Windows session configuration "ComSpec", // Determines default command interpreter (can replace `cmd.exe`) "TEMP", "TMP", // Redirects temporary file storage (useful for injection attacks) "LOCALAPPDATA", // Controls application data paths (can be abused for persistence) "USERPROFILE", // Windows user directory (can affect profile-based execution paths) "HOMEDRIVE", "HOMEPATH", // Changes where the user's home directory is located ]; /// Constructs a new Envs, skipping disallowed env vars with a warning pub fn new(map: HashMap) -> Self { let mut validated = HashMap::new(); for (key, value) in map { if Self::is_disallowed(&key) { warn!("Skipping disallowed env var: {}", key); continue; } validated.insert(key, value); } Self { map: validated } } /// Returns a copy of the validated env vars pub fn get_env(&self) -> HashMap { self.map.clone() } /// Returns an error if any disallowed env var is present pub fn validate(&self) -> Result<(), Box> { for key in self.map.keys() { if Self::is_disallowed(key) { return Err(Box::new(ExtensionError::InvalidEnvVar(key.clone()))); } } Ok(()) } fn is_disallowed(key: &str) -> bool { Self::DISALLOWED_KEYS .iter() .any(|disallowed| disallowed.eq_ignore_ascii_case(key)) } } /// Represents the different types of MCP extensions that can be added to the manager #[derive(Debug, Clone, Deserialize, Serialize, ToSchema)] #[serde(tag = "type")] pub enum ExtensionConfig { /// Server-sent events client with a URI endpoint #[serde(rename = "sse")] Sse { /// The name used to identify this extension name: String, uri: String, #[serde(default)] envs: Envs, #[serde(default)] env_keys: Vec, description: Option, // NOTE: set timeout to be optional for compatibility. // However, new configurations should include this field. timeout: Option, /// Whether this extension is bundled with Goose #[serde(default)] bundled: Option, }, /// Standard I/O client with command and arguments #[serde(rename = "stdio")] Stdio { /// The name used to identify this extension name: String, cmd: String, args: Vec, #[serde(default)] envs: Envs, #[serde(default)] env_keys: Vec, timeout: Option, description: Option, /// Whether this extension is bundled with Goose #[serde(default)] bundled: Option, }, /// Built-in extension that is part of the goose binary #[serde(rename = "builtin")] Builtin { /// The name used to identify this extension name: String, display_name: Option, // needed for the UI timeout: Option, /// Whether this extension is bundled with Goose #[serde(default)] bundled: Option, }, /// Frontend-provided tools that will be called through the frontend #[serde(rename = "frontend")] Frontend { /// The name used to identify this extension name: String, /// The tools provided by the frontend tools: Vec, /// Instructions for how to use these tools instructions: Option, /// Whether this extension is bundled with Goose #[serde(default)] bundled: Option, }, } impl Default for ExtensionConfig { fn default() -> Self { Self::Builtin { name: config::DEFAULT_EXTENSION.to_string(), display_name: Some(config::DEFAULT_DISPLAY_NAME.to_string()), timeout: Some(config::DEFAULT_EXTENSION_TIMEOUT), bundled: Some(true), } } } impl ExtensionConfig { pub fn sse, T: Into>(name: S, uri: S, description: S, timeout: T) -> Self { Self::Sse { name: name.into(), uri: uri.into(), envs: Envs::default(), env_keys: Vec::new(), description: Some(description.into()), timeout: Some(timeout.into()), bundled: None, } } pub fn stdio, T: Into>( name: S, cmd: S, description: S, timeout: T, ) -> Self { Self::Stdio { name: name.into(), cmd: cmd.into(), args: vec![], envs: Envs::default(), env_keys: Vec::new(), description: Some(description.into()), timeout: Some(timeout.into()), bundled: None, } } pub fn with_args(self, args: I) -> Self where I: IntoIterator, S: Into, { match self { Self::Stdio { name, cmd, envs, env_keys, timeout, description, bundled, .. } => Self::Stdio { name, cmd, envs, env_keys, args: args.into_iter().map(Into::into).collect(), description, timeout, bundled, }, other => other, } } pub fn key(&self) -> String { let name = self.name(); name_to_key(&name) } /// Get the extension name regardless of variant pub fn name(&self) -> String { match self { Self::Sse { name, .. } => name, Self::Stdio { name, .. } => name, Self::Builtin { name, .. } => name, Self::Frontend { name, .. } => name, } .to_string() } } impl std::fmt::Display for ExtensionConfig { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { ExtensionConfig::Sse { name, uri, .. } => write!(f, "SSE({}: {})", name, uri), ExtensionConfig::Stdio { name, cmd, args, .. } => { write!(f, "Stdio({}: {} {})", name, cmd, args.join(" ")) } ExtensionConfig::Builtin { name, .. } => write!(f, "Builtin({})", name), ExtensionConfig::Frontend { name, tools, .. } => { write!(f, "Frontend({}: {} tools)", name, tools.len()) } } } } /// Information about the extension used for building prompts #[derive(Clone, Debug, Serialize)] pub struct ExtensionInfo { pub name: String, pub instructions: String, pub has_resources: bool, } impl ExtensionInfo { pub fn new(name: &str, instructions: &str, has_resources: bool) -> Self { Self { name: name.to_string(), instructions: instructions.to_string(), has_resources, } } } /// Information about the tool used for building prompts #[derive(Clone, Debug, Serialize, ToSchema)] pub struct ToolInfo { pub name: String, pub description: String, pub parameters: Vec, pub permission: Option, } impl ToolInfo { pub fn new( name: &str, description: &str, parameters: Vec, permission: Option, ) -> Self { Self { name: name.to_string(), description: description.to_string(), parameters, permission, } } }