diff --git a/README.md b/README.md index ab50c15f..4cb03ab2 100644 --- a/README.md +++ b/README.md @@ -50,6 +50,7 @@ Edit `config.toml`: [discord] bot_token = "${DISCORD_BOT_TOKEN}" allowed_channels = ["YOUR_CHANNEL_ID"] +# allowed_users = ["YOUR_USER_ID"] # optional: restrict who can use the bot [agent] command = "kiro-cli" @@ -164,6 +165,7 @@ env = { GEMINI_API_KEY = "${GEMINI_API_KEY}" } [discord] bot_token = "${DISCORD_BOT_TOKEN}" # supports env var expansion allowed_channels = ["123456789"] # channel ID allowlist +# allowed_users = ["987654321"] # user ID allowlist (empty = all users) [agent] command = "kiro-cli" # CLI command diff --git a/charts/openab/templates/configmap.yaml b/charts/openab/templates/configmap.yaml index 273e924c..9b97cfd8 100644 --- a/charts/openab/templates/configmap.yaml +++ b/charts/openab/templates/configmap.yaml @@ -17,7 +17,13 @@ data: {{- fail (printf "discord.allowedChannels contains a mangled ID: %s — use --set-string instead of --set for channel IDs" (toString .)) }} {{- end }} {{- end }} - allowed_channels = [{{ range $i, $ch := $cfg.discord.allowedChannels }}{{ if $i }}, {{ end }}"{{ $ch }}"{{ end }}] + allowed_channels = {{ $cfg.discord.allowedChannels | default list | toJson }} + {{- range $cfg.discord.allowedUsers }} + {{- if regexMatch "e\\+|E\\+" (toString .) }} + {{- fail (printf "discord.allowedUsers contains a mangled ID: %s — use --set-string instead of --set for user IDs" (toString .)) }} + {{- end }} + {{- end }} + allowed_users = {{ $cfg.discord.allowedUsers | default list | toJson }} [agent] command = "{{ $cfg.command }}" diff --git a/charts/openab/values.yaml b/charts/openab/values.yaml index 1f7c2134..d2c7a783 100644 --- a/charts/openab/values.yaml +++ b/charts/openab/values.yaml @@ -28,6 +28,7 @@ agents: # # ⚠️ Use --set-string for channel IDs to avoid float64 precision loss # allowedChannels: # - "YOUR_CHANNEL_ID" + # allowedUsers: [] # workingDir: /home/agent # env: {} # envFrom: [] @@ -57,6 +58,8 @@ agents: # ⚠️ 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) workingDir: /home/agent env: {} envFrom: [] diff --git a/config.toml.example b/config.toml.example index c4227dc6..598c3017 100644 --- a/config.toml.example +++ b/config.toml.example @@ -1,6 +1,7 @@ [discord] bot_token = "${DISCORD_BOT_TOKEN}" allowed_channels = ["1234567890"] +# allowed_users = [""] # empty or omitted = allow all users [agent] command = "kiro-cli" diff --git a/docs/discord-bot-howto.md b/docs/discord-bot-howto.md index 672ac8c9..b80cd9e2 100644 --- a/docs/discord-bot-howto.md +++ b/docs/discord-bot-howto.md @@ -47,7 +47,14 @@ Step-by-step guide to create and configure a Discord bot for openab. 3. Click **Copy Channel ID** 4. Use this ID in `allowed_channels` in your config -## 7. Configure openab +## 7. Get Your User ID (optional) + +1. Make sure **Developer Mode** is enabled (see step 6) +2. Right-click your own username (in a message or the member list) +3. Click **Copy User ID** +4. Use this ID in `allowed_users` to restrict who can interact with the bot + +## 8. Configure openab Set the bot token and channel ID: @@ -61,15 +68,28 @@ In `config.toml`: [discord] bot_token = "${DISCORD_BOT_TOKEN}" allowed_channels = ["your-channel-id-from-step-6"] +# allowed_users = ["your-user-id-from-step-7"] # optional: restrict who can use the bot ``` +### Access control behavior + +| `allowed_channels` | `allowed_users` | Result | +|---|---|---| +| empty | empty | All users, all channels (default) | +| set | empty | Only these channels, all users | +| empty | set | All channels, only these users | +| set | set | **AND** — must be in allowed channel AND allowed user | + +- Empty `allowed_users` (default) = no user filtering, fully backward compatible +- Denied users get a 🚫 reaction and no reply + For Kubernetes: ```bash kubectl create secret generic openab-secret \ --from-literal=discord-bot-token="your-token-from-step-3" ``` -## 8. Test +## 9. Test In the allowed channel, mention the bot: diff --git a/src/config.rs b/src/config.rs index 719feafa..6d341e27 100644 --- a/src/config.rs +++ b/src/config.rs @@ -18,6 +18,8 @@ pub struct DiscordConfig { pub bot_token: String, #[serde(default)] pub allowed_channels: Vec, + #[serde(default)] + pub allowed_users: Vec, } #[derive(Debug, Deserialize)] diff --git a/src/discord.rs b/src/discord.rs index da52c691..5b4bb8b0 100644 --- a/src/discord.rs +++ b/src/discord.rs @@ -3,7 +3,7 @@ use crate::config::ReactionsConfig; use crate::format; use crate::reactions::StatusReactionController; use serenity::async_trait; -use serenity::model::channel::Message; +use serenity::model::channel::{Message, ReactionType}; use serenity::model::gateway::Ready; use serenity::model::id::{ChannelId, MessageId}; use serenity::prelude::*; @@ -15,6 +15,7 @@ use tracing::{error, info}; pub struct Handler { pub pool: Arc, pub allowed_channels: HashSet, + pub allowed_users: HashSet, pub reactions_config: ReactionsConfig, } @@ -64,6 +65,14 @@ impl EventHandler for Handler { return; } + if !self.allowed_users.is_empty() && !self.allowed_users.contains(&msg.author.id.get()) { + tracing::info!(user_id = %msg.author.id, "denied user, ignoring"); + if let Err(e) = msg.react(&ctx.http, ReactionType::Unicode("🚫".into())).await { + tracing::warn!(error = %e, "failed to react with 🚫"); + } + return; + } + let prompt = if is_mentioned { strip_mention(&msg.content) } else { diff --git a/src/main.rs b/src/main.rs index a216b668..05bbfd84 100644 --- a/src/main.rs +++ b/src/main.rs @@ -29,6 +29,7 @@ async fn main() -> anyhow::Result<()> { agent_cmd = %cfg.agent.command, pool_max = cfg.pool.max_sessions, channels = ?cfg.discord.allowed_channels, + users = ?cfg.discord.allowed_users, reactions = cfg.reactions.enabled, "config loaded" ); @@ -36,16 +37,14 @@ async fn main() -> anyhow::Result<()> { let pool = Arc::new(acp::SessionPool::new(cfg.agent, cfg.pool.max_sessions)); let ttl_secs = cfg.pool.session_ttl_hours * 3600; - let allowed_channels: HashSet = cfg - .discord - .allowed_channels - .iter() - .filter_map(|s| s.parse().ok()) - .collect(); + let allowed_channels = parse_id_set(&cfg.discord.allowed_channels, "allowed_channels")?; + let allowed_users = parse_id_set(&cfg.discord.allowed_users, "allowed_users")?; + info!(channels = allowed_channels.len(), users = allowed_users.len(), "parsed allowlists"); let handler = discord::Handler { pool: pool.clone(), allowed_channels, + allowed_users, reactions_config: cfg.reactions, }; @@ -84,3 +83,20 @@ async fn main() -> anyhow::Result<()> { info!("openab shut down"); Ok(()) } + +fn parse_id_set(raw: &[String], label: &str) -> anyhow::Result> { + let set: HashSet = raw + .iter() + .filter_map(|s| match s.parse() { + Ok(id) => Some(id), + Err(_) => { + tracing::warn!(value = %s, label = label, "ignoring invalid entry"); + None + } + }) + .collect(); + if !raw.is_empty() && set.is_empty() { + anyhow::bail!("all {label} entries failed to parse — refusing to start with an empty allowlist"); + } + Ok(set) +}