diff --git a/channels-src/telegram/telegram.capabilities.json b/channels-src/telegram/telegram.capabilities.json index 8317307b..e50b79ae 100644 --- a/channels-src/telegram/telegram.capabilities.json +++ b/channels-src/telegram/telegram.capabilities.json @@ -1,9 +1,17 @@ { - "version": "0.2.0", + "version": "0.2.2", "wit_version": "0.3.0", "type": "channel", "name": "telegram", "description": "Telegram Bot API channel for receiving and responding to Telegram messages", + "auth": { + "secret_name": "telegram_bot_token", + "display_name": "Telegram", + "instructions": "Get your bot token from @BotFather on Telegram (https://t.me/BotFather). Send /newbot or /token to get it.", + "setup_url": "https://t.me/BotFather", + "token_hint": "Looks like 123456789:AABBccDDeeFFgg...", + "env_var": "TELEGRAM_BOT_TOKEN" + }, "setup": { "required_secrets": [ { diff --git a/registry/channels/telegram.json b/registry/channels/telegram.json index 45bf5426..42fd7fb3 100644 --- a/registry/channels/telegram.json +++ b/registry/channels/telegram.json @@ -2,7 +2,7 @@ "name": "telegram", "display_name": "Telegram Channel", "kind": "channel", - "version": "0.2.1", + "version": "0.2.2", "wit_version": "0.3.0", "description": "Talk to your agent through a Telegram bot", "keywords": [ diff --git a/src/cli/mcp.rs b/src/cli/mcp.rs index f9d3acf0..b13bf598 100644 --- a/src/cli/mcp.rs +++ b/src/cli/mcp.rs @@ -10,8 +10,6 @@ use clap::{Args, Subcommand}; use crate::config::Config; use crate::db::Database; -#[cfg(feature = "postgres")] -use crate::secrets::PostgresSecretsStore; use crate::secrets::{SecretsCrypto, SecretsStore}; use crate::tools::mcp::{ McpClient, McpServerConfig, McpSessionManager, OAuthConfig, @@ -638,61 +636,9 @@ async fn get_secrets_store() -> anyhow::Result anyhow::Result = { - #[cfg(feature = "postgres")] - { - let store = crate::history::Store::new(&config.database).await?; - store.run_migrations().await?; - Arc::new(PostgresSecretsStore::new(store.pool(), Arc::new(crypto))) - } - #[cfg(all(feature = "libsql", not(feature = "postgres")))] - { - use crate::db::Database as _; - use crate::db::libsql::LibSqlBackend; - use secrecy::ExposeSecret as _; - - let default_path = crate::config::default_libsql_path(); - let db_path = config - .database - .libsql_path - .as_deref() - .unwrap_or(&default_path); - - let backend = if let Some(ref url) = config.database.libsql_url { - let token = config.database.libsql_auth_token.as_ref().ok_or_else(|| { - anyhow::anyhow!("LIBSQL_AUTH_TOKEN is required when LIBSQL_URL is set") - })?; - LibSqlBackend::new_remote_replica(db_path, url, token.expose_secret()) - .await - .map_err(|e| anyhow::anyhow!("{}", e))? - } else { - LibSqlBackend::new_local(db_path) - .await - .map_err(|e| anyhow::anyhow!("{}", e))? - }; - backend - .run_migrations() - .await - .map_err(|e| anyhow::anyhow!("{}", e))?; - - Arc::new(crate::secrets::LibSqlSecretsStore::new( - backend.shared_db(), - Arc::new(crypto), - )) - } - #[cfg(not(any(feature = "postgres", feature = "libsql")))] - { - let _ = crypto; - anyhow::bail!( - "No database backend available for secrets. Enable 'postgres' or 'libsql' feature." - ); - } - }; - Ok(store) + Ok(crate::db::create_secrets_store(&config.database, crypto).await?) } /// Configure authentication for a tool. diff --git a/src/db/mod.rs b/src/db/mod.rs index 560d682a..d7e11c12 100644 --- a/src/db/mod.rs +++ b/src/db/mod.rs @@ -91,6 +91,64 @@ pub async fn connect_from_config( } } +/// Create a secrets store from database and secrets configuration. +/// +/// This is the shared factory for CLI commands and other call sites that need +/// a `SecretsStore` without going through the full `AppBuilder`. Mirrors the +/// pattern of [`connect_from_config`] but returns a secrets-specific store. +pub async fn create_secrets_store( + config: &crate::config::DatabaseConfig, + crypto: Arc, +) -> Result, DatabaseError> { + match config.backend { + #[cfg(feature = "libsql")] + crate::config::DatabaseBackend::LibSql => { + use secrecy::ExposeSecret as _; + + let default_path = crate::config::default_libsql_path(); + let db_path = config.libsql_path.as_deref().unwrap_or(&default_path); + + let backend = if let Some(ref url) = config.libsql_url { + let token = config.libsql_auth_token.as_ref().ok_or_else(|| { + DatabaseError::Pool( + "LIBSQL_AUTH_TOKEN required when LIBSQL_URL is set".to_string(), + ) + })?; + libsql::LibSqlBackend::new_remote_replica(db_path, url, token.expose_secret()) + .await + .map_err(|e| DatabaseError::Pool(e.to_string()))? + } else { + libsql::LibSqlBackend::new_local(db_path) + .await + .map_err(|e| DatabaseError::Pool(e.to_string()))? + }; + backend.run_migrations().await?; + + Ok(Arc::new(crate::secrets::LibSqlSecretsStore::new( + backend.shared_db(), + crypto, + ))) + } + #[cfg(feature = "postgres")] + _ => { + let pg = postgres::PgBackend::new(config) + .await + .map_err(|e| DatabaseError::Pool(e.to_string()))?; + pg.run_migrations().await?; + + Ok(Arc::new(crate::secrets::PostgresSecretsStore::new( + pg.pool(), + crypto, + ))) + } + #[cfg(not(feature = "postgres"))] + _ => Err(DatabaseError::Pool( + "No database backend available for secrets. Enable 'postgres' or 'libsql' feature." + .to_string(), + )), + } +} + // ==================== Sub-traits ==================== // // Each sub-trait groups related persistence methods. The `Database` supertrait @@ -435,3 +493,46 @@ pub trait Database: /// Run schema migrations for this backend. async fn run_migrations(&self) -> Result<(), DatabaseError>; } + +#[cfg(test)] +mod tests { + use super::*; + + /// Regression test: `create_secrets_store` selects the correct backend at + /// runtime based on `DatabaseConfig`, not at compile time. Previously the + /// CLI duplicated this logic with compile-time `#[cfg]` gates that always + /// chose postgres when both features were enabled (PR #209). + #[cfg(feature = "libsql")] + #[tokio::test] + async fn test_create_secrets_store_libsql_backend() { + use secrecy::SecretString; + + let tmp = tempfile::tempdir().unwrap(); + let db_path = tmp.path().join("test.db"); + + let config = crate::config::DatabaseConfig { + backend: crate::config::DatabaseBackend::LibSql, + libsql_path: Some(db_path), + libsql_url: None, + libsql_auth_token: None, + url: SecretString::from("unused://libsql".to_string()), + pool_size: 1, + ssl_mode: crate::config::SslMode::default(), + }; + + let master_key = SecretString::from("a]".repeat(16)); + let crypto = Arc::new(crate::secrets::SecretsCrypto::new(master_key).unwrap()); + + let store = create_secrets_store(&config, crypto).await; + assert!( + store.is_ok(), + "create_secrets_store should succeed for libsql backend" + ); + + // Verify basic operation works + let store = store.unwrap(); + let exists = store.exists("test_user", "nonexistent_secret").await; + assert!(exists.is_ok()); + assert!(!exists.unwrap()); + } +} diff --git a/src/setup/wizard.rs b/src/setup/wizard.rs index 67dec9dc..2064a2ec 100644 --- a/src/setup/wizard.rs +++ b/src/setup/wizard.rs @@ -1612,48 +1612,50 @@ impl SetupWizard { }; // Create backend-appropriate secrets store. - // Respect the user's selected backend when both features are compiled, - // so we don't accidentally use a postgres pool from DATABASE_URL when - // libsql was chosen (or vice versa). + // Use runtime dispatch based on the user's selected backend. + // Default to whichever backend is compiled in. When only libsql is + // available, we must not default to "postgres" or we'd skip store creation. + let default_backend = { + #[cfg(feature = "postgres")] + { + "postgres" + } + #[cfg(not(feature = "postgres"))] + { + "libsql" + } + }; let selected_backend = self .settings .database_backend .as_deref() - .unwrap_or("postgres"); + .unwrap_or(default_backend); - #[cfg(all(feature = "libsql", feature = "postgres"))] - { - if selected_backend == "libsql" { + match selected_backend { + #[cfg(feature = "libsql")] + "libsql" | "turso" | "sqlite" => { if let Some(store) = self.create_libsql_secrets_store(&crypto)? { return Ok(SecretsContext::from_store(store, "default")); } + // Fallback to postgres if libsql store creation returned None + #[cfg(feature = "postgres")] if let Some(store) = self.create_postgres_secrets_store(&crypto).await? { return Ok(SecretsContext::from_store(store, "default")); } - } else { + } + #[cfg(feature = "postgres")] + _ => { if let Some(store) = self.create_postgres_secrets_store(&crypto).await? { return Ok(SecretsContext::from_store(store, "default")); } + // Fallback to libsql if postgres store creation returned None + #[cfg(feature = "libsql")] if let Some(store) = self.create_libsql_secrets_store(&crypto)? { return Ok(SecretsContext::from_store(store, "default")); } } - } - - #[cfg(all(feature = "postgres", not(feature = "libsql")))] - { - let _ = selected_backend; - if let Some(store) = self.create_postgres_secrets_store(&crypto).await? { - return Ok(SecretsContext::from_store(store, "default")); - } - } - - #[cfg(all(feature = "libsql", not(feature = "postgres")))] - { - let _ = selected_backend; - if let Some(store) = self.create_libsql_secrets_store(&crypto)? { - return Ok(SecretsContext::from_store(store, "default")); - } + #[cfg(not(feature = "postgres"))] + _ => {} } Err(SetupError::Config(