From e1790b5380d35cd9c7f8bb1df0b9f9a9ffb52c58 Mon Sep 17 00:00:00 2001 From: Brandon Freeman <24freemanb@gmail.com> Date: Sun, 29 Mar 2026 12:15:02 -0400 Subject: [PATCH 1/5] fix: add explicit crypto provider --- Cargo.lock | 1 + Cargo.toml | 1 + crates/openfang-kernel/Cargo.toml | 1 + crates/openfang-kernel/src/kernel.rs | 2 ++ 4 files changed, 5 insertions(+) diff --git a/Cargo.lock b/Cargo.lock index 96ef6f2606..c060aa6518 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4180,6 +4180,7 @@ dependencies = [ "openfang-wire", "rand 0.8.5", "reqwest 0.12.28", + "rustls 0.23.37", "serde", "serde_json", "subtle", diff --git a/Cargo.toml b/Cargo.toml index 39f648df99..4d955fbb15 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -63,6 +63,7 @@ clap_complete = "4" # HTTP client (for LLM drivers) reqwest = { version = "0.12", default-features = false, features = ["json", "stream", "multipart", "rustls-tls", "gzip", "deflate", "brotli"] } +rustls = { version = "0.23", default-features = false, features = ["ring"] } # Async trait async-trait = "0.1" diff --git a/crates/openfang-kernel/Cargo.toml b/crates/openfang-kernel/Cargo.toml index c9176ef248..24f1119f88 100644 --- a/crates/openfang-kernel/Cargo.toml +++ b/crates/openfang-kernel/Cargo.toml @@ -33,6 +33,7 @@ subtle = { workspace = true } rand = { workspace = true } hex = { workspace = true } reqwest = { workspace = true } +rustls = { workspace = true } cron = "0.15" zeroize = { workspace = true } diff --git a/crates/openfang-kernel/src/kernel.rs b/crates/openfang-kernel/src/kernel.rs index cc070a15c5..dbd2263fef 100644 --- a/crates/openfang-kernel/src/kernel.rs +++ b/crates/openfang-kernel/src/kernel.rs @@ -511,6 +511,8 @@ impl OpenFangKernel { /// Boot the kernel with an explicit configuration. pub fn boot_with_config(mut config: KernelConfig) -> KernelResult { + let _ = rustls::crypto::ring::default_provider().install_default(); + use openfang_types::config::KernelMode; // Env var overrides — useful for Docker where config.toml is baked in. From af51f6c9712ba738097d8c506d6cd4f924370da5 Mon Sep 17 00:00:00 2001 From: Brandon Freeman <24freemanb@gmail.com> Date: Mon, 30 Mar 2026 22:05:43 -0400 Subject: [PATCH 2/5] chore: don't fail silently, leave a debug message. --- crates/openfang-kernel/src/kernel.rs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/crates/openfang-kernel/src/kernel.rs b/crates/openfang-kernel/src/kernel.rs index dbd2263fef..9173fa2539 100644 --- a/crates/openfang-kernel/src/kernel.rs +++ b/crates/openfang-kernel/src/kernel.rs @@ -511,7 +511,12 @@ impl OpenFangKernel { /// Boot the kernel with an explicit configuration. pub fn boot_with_config(mut config: KernelConfig) -> KernelResult { - let _ = rustls::crypto::ring::default_provider().install_default(); + if rustls::crypto::ring::default_provider() + .install_default() + .is_err() + { + debug!("rustls crypto provider already installed, skipping"); + } use openfang_types::config::KernelMode; From d94508e72eecd22f7b9fa43207613fecb3aa6047 Mon Sep 17 00:00:00 2001 From: Matteo De Agazio Date: Tue, 31 Mar 2026 19:07:49 +0200 Subject: [PATCH 3/5] feat: add free_response_channels support for Discord Allow certain Discord channel IDs to respond without requiring @mention, similar to Hermes gateway's free_response_channels. - Add free_response_channels field to DiscordConfig - Add free_response_channels method to ChannelBridgeHandle trait - Implement free_response_channels in KernelBridgeAdapter - Modify dispatch_message to bypass mention_only policy for free channels - Add test for free_response_channels deserialization --- crates/openfang-api/src/channel_bridge.rs | 13 +++++++++ crates/openfang-channels/src/bridge.rs | 34 ++++++++++++++++------- crates/openfang-types/src/config.rs | 25 +++++++++++++++++ 3 files changed, 62 insertions(+), 10 deletions(-) diff --git a/crates/openfang-api/src/channel_bridge.rs b/crates/openfang-api/src/channel_bridge.rs index f8336ba59f..3ad3a8d3bc 100644 --- a/crates/openfang-api/src/channel_bridge.rs +++ b/crates/openfang-api/src/channel_bridge.rs @@ -816,6 +816,19 @@ impl ChannelBridgeHandle for KernelBridgeAdapter { } } + async fn free_response_channels(&self, channel_type: &str) -> Vec { + let channels = &self.kernel.config.channels; + match channel_type { + "discord" => channels + .discord + .as_ref() + .map(|c| c.free_response_channels.clone()) + .unwrap_or_default(), + // Add other channel types here as needed (e.g., "telegram" => ...) + _ => Vec::new(), + } + } + async fn authorize_channel_user( &self, channel_type: &str, diff --git a/crates/openfang-channels/src/bridge.rs b/crates/openfang-channels/src/bridge.rs index 7cf6870c64..f09031492b 100644 --- a/crates/openfang-channels/src/bridge.rs +++ b/crates/openfang-channels/src/bridge.rs @@ -220,6 +220,13 @@ pub trait ChannelBridgeHandle: Send + Sync { None } + /// Get channel IDs that respond without requiring @mention (free response mode). + /// + /// Returns an empty vector if the channel type is not configured or has no free response channels. + async fn free_response_channels(&self, _channel_type: &str) -> Vec { + Vec::new() + } + /// Record a delivery result for tracking (optional — default no-op). /// /// `thread_id` preserves Telegram forum-topic context so cron/workflow @@ -659,16 +666,23 @@ async fn dispatch_message( } } GroupPolicy::MentionOnly => { - // Only allow messages where the bot was @mentioned or commands. - let was_mentioned = message - .metadata - .get("was_mentioned") - .and_then(|v| v.as_bool()) - .unwrap_or(false); - let is_command = matches!(&message.content, ChannelContent::Command { .. }); - if !was_mentioned && !is_command { - debug!("Ignoring group message on {ct_str} (group_policy=mention_only, not mentioned)"); - return; + // Check if this channel is in the free_response list - if so, allow all messages + let free_channels = handle.free_response_channels(ct_str).await; + let channel_id = &message.sender.platform_id; + let is_free_channel = free_channels.iter().any(|id| id == channel_id); + + if !is_free_channel { + // Only allow messages where the bot was @mentioned or commands. + let was_mentioned = message + .metadata + .get("was_mentioned") + .and_then(|v| v.as_bool()) + .unwrap_or(false); + let is_command = matches!(&message.content, ChannelContent::Command { .. }); + if !was_mentioned && !is_command { + debug!("Ignoring group message on {ct_str} (group_policy=mention_only, not mentioned)"); + return; + } } } GroupPolicy::All => {} diff --git a/crates/openfang-types/src/config.rs b/crates/openfang-types/src/config.rs index f290bbf8cf..7b80e32516 100644 --- a/crates/openfang-types/src/config.rs +++ b/crates/openfang-types/src/config.rs @@ -1792,6 +1792,10 @@ pub struct DiscordConfig { /// Default channel ID for outgoing messages when no recipient is specified. #[serde(default)] pub default_channel_id: Option, + /// Channel IDs that respond without requiring @mention (free response mode). + /// In these channels, the bot responds to all group messages without needing to be mentioned. + #[serde(default, deserialize_with = "deserialize_string_or_int_vec")] + pub free_response_channels: Vec, /// Per-channel behavior overrides. #[serde(default)] pub overrides: ChannelOverrides, @@ -1807,6 +1811,7 @@ impl Default for DiscordConfig { intents: 37376, ignore_bots: true, default_channel_id: None, + free_response_channels: vec![], overrides: ChannelOverrides::default(), } } @@ -3706,6 +3711,26 @@ mod tests { assert!(dc2.ignore_bots); } + #[test] + fn test_discord_config_free_response_channels_deserialization() { + // Test with free_response_channels as list of strings + let toml_str = r#" + bot_token_env = "DISCORD_BOT_TOKEN" + free_response_channels = ["123456789", "987654321"] + "#; + let dc: DiscordConfig = toml::from_str(toml_str).unwrap(); + assert_eq!(dc.free_response_channels.len(), 2); + assert_eq!(dc.free_response_channels[0], "123456789"); + assert_eq!(dc.free_response_channels[1], "987654321"); + + // Test default (empty list) + let toml_str2 = r#" + bot_token_env = "DISCORD_BOT_TOKEN" + "#; + let dc2: DiscordConfig = toml::from_str(toml_str2).unwrap(); + assert!(dc2.free_response_channels.is_empty()); + } + #[test] fn test_slack_config_defaults() { let sl = SlackConfig::default(); From 2d9496362725d4c5969b539b23ccc1ee25f6b8ed Mon Sep 17 00:00:00 2001 From: Matteo De Agazio Date: Thu, 9 Apr 2026 17:16:39 +0200 Subject: [PATCH 4/5] chore: gitignore personal deploy script --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index 3a829c5561..317e7b71c5 100644 --- a/.gitignore +++ b/.gitignore @@ -45,3 +45,6 @@ Thumbs.db *.swo *~ .serena/ + +# Personal deploy scripts +scripts/deploy-remote.sh From e988572c82fd94b9914ae495e80876a29cec74d0 Mon Sep 17 00:00:00 2001 From: Matteo De Agazio Date: Thu, 9 Apr 2026 17:31:28 +0200 Subject: [PATCH 5/5] fix: clippy bool_comparison in web_fetch test --- crates/openfang-runtime/src/web_fetch.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/openfang-runtime/src/web_fetch.rs b/crates/openfang-runtime/src/web_fetch.rs index 81021aefca..9fa8d622a5 100644 --- a/crates/openfang-runtime/src/web_fetch.rs +++ b/crates/openfang-runtime/src/web_fetch.rs @@ -514,7 +514,7 @@ mod tests { let allow = vec!["*.example.com".to_string()]; assert!(check_ssrf("http://api.example.com", &allow).is_ok()); // Non-matching domain should still go through normal checks - assert!(is_host_allowed("other.net", &allow) == false); + assert!(!is_host_allowed("other.net", &allow)); } #[test]