diff --git a/charts/openab/templates/configmap.yaml b/charts/openab/templates/configmap.yaml index 2347f8c..2749495 100644 --- a/charts/openab/templates/configmap.yaml +++ b/charts/openab/templates/configmap.yaml @@ -11,11 +11,10 @@ metadata: data: config.toml: | {{- if ($cfg.discord).enabled }} - {{- if not $cfg.discord.allowedChannels }} - {{- fail (printf "agents.%s.discord.allowedChannels is required — empty means the bot will not respond to any messages. Add at least one channel ID." $name) }} - {{- end }} [discord] bot_token = "${DISCORD_BOT_TOKEN}" + allow_all_channels = {{ if and (hasKey $cfg.discord "allowAllChannels") (ne $cfg.discord.allowAllChannels nil) }}{{ $cfg.discord.allowAllChannels }}{{ else if $cfg.discord.allowedChannels }}false{{ else }}true{{ end }} + allow_all_users = {{ if and (hasKey $cfg.discord "allowAllUsers") (ne $cfg.discord.allowAllUsers nil) }}{{ $cfg.discord.allowAllUsers }}{{ else if $cfg.discord.allowedUsers }}false{{ else }}true{{ end }} {{- range $cfg.discord.allowedChannels }} {{- if regexMatch "e\\+|E\\+" (toString .) }} {{- fail (printf "discord.allowedChannels contains a mangled ID: %s — use --set-string instead of --set for channel IDs" (toString .)) }} @@ -55,12 +54,11 @@ data: {{- end }} {{- if and ($cfg.slack).enabled }} - {{- if not (($cfg.slack).allowedChannels) }} - {{- fail (printf "agents.%s.slack.allowedChannels is required — empty means the bot will not respond to any messages. Add at least one channel ID." $name) }} - {{- end }} [slack] bot_token = "${SLACK_BOT_TOKEN}" app_token = "${SLACK_APP_TOKEN}" + allow_all_channels = {{ if and (hasKey ($cfg.slack) "allowAllChannels") (ne ($cfg.slack).allowAllChannels nil) }}{{ ($cfg.slack).allowAllChannels }}{{ else if ($cfg.slack).allowedChannels }}false{{ else }}true{{ end }} + allow_all_users = {{ if and (hasKey ($cfg.slack) "allowAllUsers") (ne ($cfg.slack).allowAllUsers nil) }}{{ ($cfg.slack).allowAllUsers }}{{ else if ($cfg.slack).allowedUsers }}false{{ else }}true{{ end }} {{- range ($cfg.slack).allowedChannels }} {{- if regexMatch "e\\+|E\\+" (toString .) }} {{- fail (printf "slack.allowedChannels contains a mangled ID: %s — use --set-string instead of --set for channel IDs" (toString .)) }} diff --git a/charts/openab/tests/adapter-enablement_test.yaml b/charts/openab/tests/adapter-enablement_test.yaml index 6d7fcac..2e924cc 100644 --- a/charts/openab/tests/adapter-enablement_test.yaml +++ b/charts/openab/tests/adapter-enablement_test.yaml @@ -33,8 +33,6 @@ tests: - it: renders [slack] when enabled=true set: agents.kiro.slack.enabled: true - agents.kiro.slack.allowedChannels: - - "C0123456789" asserts: - matchRegex: path: data["config.toml"] @@ -51,8 +49,6 @@ tests: - it: renders [slack] with placeholder tokens when enabled=true set: agents.kiro.slack.enabled: true - agents.kiro.slack.allowedChannels: - - "C0123456789" asserts: - matchRegex: path: data["config.toml"] @@ -65,8 +61,6 @@ tests: set: agents.kiro.discord.enabled: true agents.kiro.slack.enabled: true - agents.kiro.slack.allowedChannels: - - "C0123456789" asserts: - matchRegex: path: data["config.toml"] diff --git a/charts/openab/tests/configmap_test.yaml b/charts/openab/tests/configmap_test.yaml index 0326e24..6af408c 100644 --- a/charts/openab/tests/configmap_test.yaml +++ b/charts/openab/tests/configmap_test.yaml @@ -92,27 +92,8 @@ tests: - it: renders slack allow_user_messages = "multibot-mentions" set: agents.kiro.slack.enabled: true - agents.kiro.slack.allowedChannels: - - "C0123456789" agents.kiro.slack.allowUserMessages: multibot-mentions asserts: - matchRegex: path: data["config.toml"] pattern: 'allow_user_messages = "multibot-mentions"' - - - it: rejects empty discord allowedChannels - set: - agents.kiro.discord.enabled: true - agents.kiro.discord.allowedChannels: [] - asserts: - - failedTemplate: - errorPattern: "discord.allowedChannels is required" - - - it: rejects empty slack allowedChannels - set: - agents.kiro.discord.enabled: false - agents.kiro.slack.enabled: true - agents.kiro.slack.allowedChannels: [] - asserts: - - failedTemplate: - errorPattern: "slack.allowedChannels is required" diff --git a/charts/openab/values.yaml b/charts/openab/values.yaml index d19cd8e..1de5118 100644 --- a/charts/openab/values.yaml +++ b/charts/openab/values.yaml @@ -121,11 +121,15 @@ agents: enabled: true # set to false to disable the discord adapter # botToken is no longer required at render time; the actual token # is injected at runtime via the DISCORD_BOT_TOKEN env var. + # allowAllChannels: true (default) = allow all channels | false = only allowedChannels + # If not set, auto-inferred: non-empty allowedChannels → false, empty → true + # allowAllUsers: true (default) = allow all users | false = only allowedUsers + # If not set, auto-inferred: non-empty allowedUsers → false, empty → true # ⚠️ Use --set-string for channel IDs to avoid float64 precision loss allowedChannels: - "YOUR_CHANNEL_ID" # ⚠️ Use --set-string for user IDs to avoid float64 precision loss - allowedUsers: [] # empty = allow all users (default) + allowedUsers: [] # only checked when allowAllUsers=false # allowBotMessages: "off" (default) | "mentions" | "all" # recommended for multi-agent collaboration allowBotMessages: "off" @@ -135,8 +139,9 @@ agents: enabled: false botToken: "" # Bot User OAuth Token (xoxb-...) appToken: "" # App-Level Token (xapp-...) for Socket Mode - allowedChannels: [] # required — empty = deny all channels (secure by default) - allowedUsers: [] # empty = allow all users + # allowAllChannels/allowAllUsers: same auto-infer logic as discord + allowedChannels: [] # empty + no allowAllChannels → allow all (auto-inferred) + allowedUsers: [] # empty + no allowAllUsers → allow all (auto-inferred) # allowBotMessages: "off" (default) | "mentions" | "all" allowBotMessages: "off" # trustedBotIds: Bot User IDs (U...) — find via Slack UI: click bot profile → Copy member ID diff --git a/config.toml.example b/config.toml.example index bd72a09..8a317b2 100644 --- a/config.toml.example +++ b/config.toml.example @@ -2,8 +2,12 @@ [discord] bot_token = "${DISCORD_BOT_TOKEN}" -allowed_channels = ["1234567890"] # required — empty or omitted = deny all channels (secure by default) -# allowed_users = [""] # empty or omitted = allow all users +# allow_all_channels = true # true = allow all channels; false = only allowed_channels +# # omitted = auto-detect from list (non-empty → false, empty → true) +allowed_channels = ["1234567890"] # ↑ omitted + non-empty list → auto-detected as false +# allow_all_users = true # true = any user; false = only allowed_users +# # omitted = auto-detect from list (non-empty → false, empty → true) +# allowed_users = [""] # ↑ omitted + empty/absent list → auto-detected as true # allow_bot_messages = "off" # "off" (default) | "mentions" | "all" # "mentions" is recommended for multi-agent collaboration # trusted_bot_ids = [] # empty = any bot (mode permitting); set to restrict @@ -14,8 +18,10 @@ allowed_channels = ["1234567890"] # required — empty or omitted = deny a # [slack] # bot_token = "${SLACK_BOT_TOKEN}" # Bot User OAuth Token (xoxb-...) # app_token = "${SLACK_APP_TOKEN}" # App-Level Token (xapp-...) for Socket Mode -# allowed_channels = ["C0123456789"] # required — empty or omitted = deny all channels (secure by default) -# allowed_users = ["U0123456789"] # empty or omitted = allow all users +# allow_all_channels = true # true (default) = allow all channels; false = only allowed_channels +# allowed_channels = ["C0123456789"] # only checked when allow_all_channels = false +# allow_all_users = true # true (default) = any user; false = only allowed_users +# allowed_users = ["U0123456789"] # only checked when allow_all_users = false # allow_bot_messages = "off" # "off" (default) | "mentions" | "all" # trusted_bot_ids = [] # empty = any bot (mode permitting); set to restrict # allow_user_messages = "involved" # "involved" (default) | "mentions" diff --git a/docs/discord.md b/docs/discord.md index 7f71f09..7b7896f 100644 --- a/docs/discord.md +++ b/docs/discord.md @@ -64,7 +64,7 @@ Complete guide to setting up, configuring, and running OpenAB with Discord. ```toml [discord] bot_token = "${DISCORD_BOT_TOKEN}" -allowed_channels = ["123456789"] # channel ID allowlist (empty = deny all) +allowed_channels = ["123456789"] # channel ID allowlist (empty = all) allowed_users = ["987654321"] # user ID allowlist (empty = all) allow_bot_messages = "off" # off | mentions | all allow_user_messages = "involved" # involved | mentions @@ -75,12 +75,11 @@ trusted_bot_ids = [] # bot user IDs allowed through (empty = an | `allowed_channels` | `allowed_users` | Result | |---|---|---| -| empty | empty | **No channels, no users — bot ignores all messages** | +| empty | empty | All users, all channels (default) | | set | empty | Only these channels, all users | -| empty | set | **Bot ignores all messages** (channels must be configured first) | +| empty | set | All channels, only these users | | set | set | **AND** — must be in allowed channel AND allowed user | -- Empty `allowed_channels` = bot will not respond anywhere (secure by default) - Empty `allowed_users` (default) = no user filtering - Denied users get a 🚫 reaction and no reply diff --git a/docs/slack-bot-howto.md b/docs/slack-bot-howto.md index 2a3a04d..8fa774e 100644 --- a/docs/slack-bot-howto.md +++ b/docs/slack-bot-howto.md @@ -63,22 +63,10 @@ Add the `[slack]` section to your `config.toml`: [slack] bot_token = "${SLACK_BOT_TOKEN}" app_token = "${SLACK_APP_TOKEN}" -allowed_channels = ["C0123456789"] # required — empty = deny all channels (secure by default) +allowed_channels = [] # empty = allow all channels # allowed_users = ["U0123456789"] # empty = allow all users ``` -### Access control behavior - -| `allowed_channels` | `allowed_users` | Result | -|---|---|---| -| empty | empty | **No channels, no users — bot ignores all messages** | -| set | empty | Only these channels, all users | -| empty | set | **Bot ignores all messages** (channels must be configured first) | -| set | set | **AND** — must be in allowed channel AND allowed user | - -- Empty `allowed_channels` = bot will not respond anywhere (secure by default) -- Empty `allowed_users` (default) = no user filtering - Set the environment variables: ```bash diff --git a/src/config.rs b/src/config.rs index 486bff8..5ca2506 100644 --- a/src/config.rs +++ b/src/config.rs @@ -73,6 +73,12 @@ fn default_stt_base_url() -> String { "https://api.groq.com/openai/v1".into() } #[derive(Debug, Deserialize)] pub struct DiscordConfig { pub bot_token: String, + /// Explicit flag: true = allow all channels, false = check allowed_channels list. + /// When not set, auto-detected: non-empty list → false, empty list → true. + pub allow_all_channels: Option, + /// Explicit flag: true = allow all users, false = check allowed_users list. + /// When not set, auto-detected: non-empty list → false, empty list → true. + pub allow_all_users: Option, #[serde(default)] pub allowed_channels: Vec, #[serde(default)] @@ -128,6 +134,12 @@ impl<'de> Deserialize<'de> for AllowUsers { pub struct SlackConfig { pub bot_token: String, pub app_token: String, + /// Explicit flag: true = allow all channels, false = check allowed_channels list. + /// When not set, auto-detected: non-empty list → false, empty list → true. + pub allow_all_channels: Option, + /// Explicit flag: true = allow all users, false = check allowed_users list. + /// When not set, auto-detected: non-empty list → false, empty list → true. + pub allow_all_users: Option, #[serde(default)] pub allowed_channels: Vec, #[serde(default)] @@ -265,6 +277,12 @@ impl Default for ReactionTiming { // --- loading --- +/// Resolve an allow_all flag: if explicitly set, use it; otherwise infer from the list. +/// Non-empty list → false (respect the list), empty list → true (allow all). +pub fn resolve_allow_all(flag: Option, list: &[String]) -> bool { + flag.unwrap_or(list.is_empty()) +} + fn expand_env_vars(raw: &str) -> String { let re = Regex::new(r"\$\{(\w+)\}").unwrap(); re.replace_all(raw, |caps: ®ex::Captures| { diff --git a/src/discord.rs b/src/discord.rs index 00d3ee7..027967f 100644 --- a/src/discord.rs +++ b/src/discord.rs @@ -110,6 +110,8 @@ impl ChatAdapter for DiscordAdapter { pub struct Handler { pub router: Arc, + pub allow_all_channels: bool, + pub allow_all_users: bool, pub allowed_channels: HashSet, pub allowed_users: HashSet, pub stt_config: SttConfig, @@ -234,11 +236,8 @@ impl EventHandler for Handler { }).clone(); let channel_id = msg.channel_id.get(); - if self.allowed_channels.is_empty() { - debug!("allowed_channels is empty — ignoring message in channel {}", channel_id); - return; - } - let in_allowed_channel = self.allowed_channels.contains(&channel_id); + let in_allowed_channel = + self.allow_all_channels || self.allowed_channels.contains(&channel_id); let is_mentioned = msg.mentions_user_id(bot_id) || msg.content.contains(&format!("<@{}>", bot_id)); @@ -298,7 +297,7 @@ impl EventHandler for Handler { let (in_thread, bot_owns_thread) = if !in_allowed_channel { match msg.channel_id.to_channel(&ctx.http).await { Ok(serenity::model::channel::Channel::Guild(gc)) => { - let parent_allowed = gc + let parent_allowed = self.allow_all_channels || gc .parent_id .is_some_and(|pid| self.allowed_channels.contains(&pid.get())); let owned = gc.owner_id.is_some_and(|oid| oid == bot_id); @@ -387,7 +386,7 @@ impl EventHandler for Handler { } } - if !self.allowed_users.is_empty() && !self.allowed_users.contains(&msg.author.id.get()) { + if !self.allow_all_users && !self.allowed_users.contains(&msg.author.id.get()) { tracing::info!(user_id = %msg.author.id, "denied user, ignoring"); let msg_ref = discord_msg_ref(&msg); let _ = adapter.add_reaction(&msg_ref, "🚫").await; diff --git a/src/main.rs b/src/main.rs index 015d5e5..53927f4 100644 --- a/src/main.rs +++ b/src/main.rs @@ -111,10 +111,14 @@ async fn main() -> anyhow::Result<()> { // Spawn Slack adapter (background task) let slack_handle = if let Some(slack_cfg) = cfg.slack { - if slack_cfg.allowed_channels.is_empty() { - warn!("no allowed_channels configured for Slack — bot will not respond to any messages. Add at least one channel ID to [slack] allowed_channels."); + let allow_all_channels = config::resolve_allow_all(slack_cfg.allow_all_channels, &slack_cfg.allowed_channels); + let allow_all_users = config::resolve_allow_all(slack_cfg.allow_all_users, &slack_cfg.allowed_users); + if !allow_all_channels && slack_cfg.allowed_channels.is_empty() { + warn!("allow_all_channels=false with empty allowed_channels for Slack — bot will deny all channels"); } info!( + allow_all_channels, + allow_all_users, channels = slack_cfg.allowed_channels.len(), users = slack_cfg.allowed_users.len(), allow_bot_messages = ?slack_cfg.allow_bot_messages, @@ -128,6 +132,8 @@ async fn main() -> anyhow::Result<()> { if let Err(e) = slack::run_slack_adapter( slack_cfg.bot_token, slack_cfg.app_token, + allow_all_channels, + allow_all_users, slack_cfg.allowed_channels.into_iter().collect(), slack_cfg.allowed_users.into_iter().collect(), slack_cfg.allow_bot_messages, @@ -149,14 +155,18 @@ async fn main() -> anyhow::Result<()> { // Run Discord adapter (foreground, blocking) or wait for ctrl_c if let Some(discord_cfg) = cfg.discord { + let allow_all_channels = config::resolve_allow_all(discord_cfg.allow_all_channels, &discord_cfg.allowed_channels); + let allow_all_users = config::resolve_allow_all(discord_cfg.allow_all_users, &discord_cfg.allowed_users); let allowed_channels = parse_id_set(&discord_cfg.allowed_channels, "discord.allowed_channels")?; - if allowed_channels.is_empty() { - warn!("no allowed_channels configured for Discord — bot will not respond to any messages. Add at least one channel ID to [discord] allowed_channels."); + if !allow_all_channels && allowed_channels.is_empty() { + warn!("allow_all_channels=false with empty allowed_channels for Discord — bot will deny all channels"); } let allowed_users = parse_id_set(&discord_cfg.allowed_users, "discord.allowed_users")?; let trusted_bot_ids = parse_id_set(&discord_cfg.trusted_bot_ids, "discord.trusted_bot_ids")?; info!( + allow_all_channels, + allow_all_users, channels = allowed_channels.len(), users = allowed_users.len(), trusted_bots = trusted_bot_ids.len(), @@ -167,6 +177,8 @@ async fn main() -> anyhow::Result<()> { let handler = discord::Handler { router, + allow_all_channels, + allow_all_users, allowed_channels, allowed_users, stt_config: cfg.stt.clone(), diff --git a/src/slack.rs b/src/slack.rs index 5ddd190..92155e2 100644 --- a/src/slack.rs +++ b/src/slack.rs @@ -447,6 +447,8 @@ const MAX_CONSECUTIVE_BOT_TURNS: usize = 10; pub async fn run_slack_adapter( bot_token: String, app_token: String, + allow_all_channels: bool, + allow_all_users: bool, allowed_channels: HashSet, allowed_users: HashSet, allow_bot_messages: AllowBots, @@ -552,6 +554,8 @@ pub async fn run_slack_adapter( true, &adapter, &bot_token, + allow_all_channels, + allow_all_users, &allowed_channels, &allowed_users, &stt_config, @@ -707,6 +711,8 @@ pub async fn run_slack_adapter( is_dm, &adapter, &bot_token, + allow_all_channels, + allow_all_users, &allowed_channels, &allowed_users, &stt_config, @@ -777,6 +783,8 @@ async fn handle_message( strip_mentions: bool, adapter: &Arc, bot_token: &str, + allow_all_channels: bool, + allow_all_users: bool, allowed_channels: &HashSet, allowed_users: &HashSet, stt_config: &SttConfig, @@ -803,17 +811,13 @@ async fn handle_message( }; let thread_ts = event["thread_ts"].as_str().map(|s| s.to_string()); - // Check allowed channels (empty = deny all) - if allowed_channels.is_empty() { - tracing::debug!("allowed_channels is empty — ignoring message in channel {}", channel_id); - return; - } - if !allowed_channels.contains(&channel_id) { + // Check allowed channels + if !allow_all_channels && !allowed_channels.contains(&channel_id) { return; } // Check allowed users — skip for bot messages (they go through trusted_bot_ids instead) - if !is_bot_msg && !allowed_users.is_empty() && !allowed_users.contains(&user_id) { + if !is_bot_msg && !allow_all_users && !allowed_users.contains(&user_id) { tracing::info!(user_id, "denied Slack user, ignoring"); let msg_ref = MessageRef { channel: ChannelRef {