//! Lifecycle management for extensions: discovery, installation, authentication, //! and activation of channels, tools, and MCP servers. //! //! Extensions are the user-facing abstraction that unifies three runtime kinds: //! - **Channels** (Telegram, Slack, Discord) — messaging integrations (WASM) //! - **Tools** — sandboxed capabilities (WASM) //! - **MCP servers** — external API integrations via Model Context Protocol //! //! The agent can search a built-in registry (or discover online), install, //! authenticate, and activate extensions at runtime without CLI commands. //! //! ```text //! User: "add telegram" //! -> tool_search("telegram") -> finds channel in registry //! -> tool_install("telegram") -> copies bundled WASM to channels dir //! -> tool_activate("telegram") -> configures credentials, starts channel //! ``` pub mod discovery; pub mod manager; pub mod registry; pub use discovery::OnlineDiscovery; pub use manager::ExtensionManager; pub use registry::ExtensionRegistry; use serde::{Deserialize, Serialize}; /// The kind of extension, determining how it's installed, authenticated, and activated. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] pub enum ExtensionKind { /// Hosted MCP server, HTTP transport, OAuth 2.1 auth. McpServer, /// Sandboxed WASM module, file-based, capabilities auth. WasmTool, /// WASM channel module with hot-activation support. WasmChannel, } impl std::fmt::Display for ExtensionKind { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { ExtensionKind::McpServer => write!(f, "mcp_server"), ExtensionKind::WasmTool => write!(f, "wasm_tool"), ExtensionKind::WasmChannel => write!(f, "wasm_channel"), } } } /// A registry entry describing a known or discovered extension. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct RegistryEntry { /// Unique identifier (e.g., "notion", "weather", "telegram"). pub name: String, /// Human-readable name (e.g., "Notion", "Weather Tool"). pub display_name: String, /// What kind of extension this is. pub kind: ExtensionKind, /// Short description of what this extension does. pub description: String, /// Search keywords beyond the name. #[serde(default)] pub keywords: Vec, /// Where to get this extension. pub source: ExtensionSource, /// Fallback source when the primary source fails (e.g., download 404 → build from source). #[serde(default, skip_serializing_if = "Option::is_none")] pub fallback_source: Option>, /// How authentication works. pub auth_hint: AuthHint, } /// Where the extension binary or server lives. #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(tag = "type", rename_all = "snake_case")] pub enum ExtensionSource { /// URL to a hosted MCP server. McpUrl { url: String }, /// Downloadable WASM binary. WasmDownload { wasm_url: String, #[serde(default)] capabilities_url: Option, }, /// Build from local source directory. WasmBuildable { #[serde(alias = "repo_url")] source_dir: String, #[serde(default)] build_dir: Option, /// Crate name used to locate the build artifact binary. #[serde(default)] crate_name: Option, }, /// Discovered online (not yet validated for a specific source type). Discovered { url: String }, } /// Hint about what authentication method is needed. #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(tag = "type", rename_all = "snake_case")] pub enum AuthHint { /// MCP server supports Dynamic Client Registration (zero-config OAuth). Dcr, /// MCP server needs a pre-configured OAuth client_id. OAuthPreConfigured { /// URL where the user can create an OAuth app. setup_url: String, }, /// WASM tool has auth defined in its capabilities.json file. CapabilitiesAuth, /// No authentication needed. None, } /// Where a search result came from. #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] pub enum ResultSource { /// From the built-in curated registry. Registry, /// From online discovery (validated). Discovered, } /// Result of searching for extensions. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct SearchResult { /// The registry entry. #[serde(flatten)] pub entry: RegistryEntry, /// Where this result came from. pub source: ResultSource, /// Whether the endpoint was validated (for discovered entries). #[serde(default)] pub validated: bool, } /// Result of installing an extension. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct InstallResult { pub name: String, pub kind: ExtensionKind, pub message: String, } /// Result of authenticating an extension. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct AuthResult { pub name: String, pub kind: ExtensionKind, /// OAuth URL to open (for OAuth flows). #[serde(skip_serializing_if = "Option::is_none")] pub auth_url: Option, /// Whether using local or remote callback. #[serde(skip_serializing_if = "Option::is_none")] pub callback_type: Option, /// Instructions for manual token entry (for WASM tools). #[serde(skip_serializing_if = "Option::is_none")] pub instructions: Option, /// URL for manual token setup. #[serde(skip_serializing_if = "Option::is_none")] pub setup_url: Option, /// Whether the tool is waiting for a token from the user. #[serde(default)] pub awaiting_token: bool, /// Current auth status. pub status: String, } /// Result of activating an extension. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ActivateResult { pub name: String, pub kind: ExtensionKind, /// Names of tools that were loaded/registered. pub tools_loaded: Vec, pub message: String, } fn default_true() -> bool { true } /// An installed extension with its current status. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct InstalledExtension { pub name: String, pub kind: ExtensionKind, /// Human-readable display name (e.g. "Telegram Channel" vs "Telegram Tool"). #[serde(skip_serializing_if = "Option::is_none")] pub display_name: Option, #[serde(skip_serializing_if = "Option::is_none")] pub description: Option, /// Server or source URL (e.g. MCP server endpoint). #[serde(skip_serializing_if = "Option::is_none")] pub url: Option, pub authenticated: bool, pub active: bool, /// Tool names if active. #[serde(default)] pub tools: Vec, /// Whether this extension has a setup schema (required_secrets) that can be configured. #[serde(default)] pub needs_setup: bool, /// Whether this extension has an auth configuration (OAuth or manual token). #[serde(default)] pub has_auth: bool, /// Whether this extension is installed locally (false = available in registry but not installed). #[serde(default = "default_true")] pub installed: bool, /// Last activation error for WASM channels. #[serde(skip_serializing_if = "Option::is_none")] pub activation_error: Option, } /// Error type for extension operations. #[derive(Debug, thiserror::Error)] pub enum ExtensionError { #[error("Extension not found: {0}")] NotFound(String), #[error("Extension already installed: {0}")] AlreadyInstalled(String), #[error("Extension not installed: {0}")] NotInstalled(String), #[error("Authentication failed: {0}")] AuthFailed(String), #[error("Activation failed: {0}")] ActivationFailed(String), #[error("Installation failed: {0}")] InstallFailed(String), #[error("Discovery failed: {0}")] DiscoveryFailed(String), #[error("Invalid URL: {0}")] InvalidUrl(String), #[error("Download failed: {0}")] DownloadFailed(String), #[error("Config error: {0}")] Config(String), #[error("Primary install failed: {primary}; fallback install also failed: {fallback}")] FallbackFailed { primary: Box, fallback: Box, }, #[error("{0}")] Other(String), }