Skip to main content

mur_common/bridge/
slack_config.rs

1//! Slack bridge configuration (stored in agent profile.yaml `bridge:` block).
2
3use serde::{Deserialize, Serialize};
4
5/// Config block for a Slack bridge agent.
6/// Tokens are stored in the system keychain; the account names below
7/// are pointers, not the secrets themselves.
8#[derive(Debug, Clone, Serialize, Deserialize)]
9pub struct SlackConfig {
10    /// Human-readable workspace URL shown in error messages and logs.
11    pub workspace_url: String,
12    /// Keychain account name for the xoxb-… Bot Token.
13    pub bot_token_keychain_account: String,
14    /// Keychain account name for the xapp-… App Token (Socket Mode).
15    pub app_token_keychain_account: String,
16    /// Privacy gate: which Slack event types reach the user agent.
17    #[serde(default)]
18    pub privacy_mode: SlackPrivacyMode,
19    /// Allowed Slack channel IDs (C…). Empty = all channels allowed.
20    #[serde(default)]
21    pub allowed_channels: Vec<String>,
22    /// Allowed Slack user IDs (U…) permitted to drive the agent. Empty = no
23    /// user filter (any workspace member who can DM/@mention the bot). Set this
24    /// to the owner's user ID(s) to bind the bridge to specific senders.
25    #[serde(default)]
26    pub allowed_user_ids: Vec<String>,
27}
28
29#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
30#[serde(rename_all = "snake_case")]
31pub enum SlackPrivacyMode {
32    /// Only DMs to the bot reach the agent. Channel mentions are dropped.
33    DmOnly,
34    /// DMs and @mentions in channels both reach the agent (default).
35    #[default]
36    DmAndMentions,
37}
38
39#[cfg(test)]
40mod tests {
41    use super::*;
42
43    #[test]
44    fn round_trips_yaml() {
45        let yaml = r#"
46workspace_url: "https://myteam.slack.com"
47bot_token_keychain_account: "mur_slack_bot_myagent"
48app_token_keychain_account: "mur_slack_app_myagent"
49"#;
50        let cfg: SlackConfig = serde_yaml::from_str(yaml).unwrap();
51        assert_eq!(cfg.privacy_mode, SlackPrivacyMode::DmAndMentions);
52        assert!(cfg.allowed_channels.is_empty());
53    }
54
55    #[test]
56    fn dm_only_mode_round_trips() {
57        let yaml = r#"
58workspace_url: "https://myteam.slack.com"
59bot_token_keychain_account: "mur_slack_bot_x"
60app_token_keychain_account: "mur_slack_app_x"
61privacy_mode: dm_only
62"#;
63        let cfg: SlackConfig = serde_yaml::from_str(yaml).unwrap();
64        assert_eq!(cfg.privacy_mode, SlackPrivacyMode::DmOnly);
65    }
66
67    #[test]
68    fn allowed_channels_round_trips() {
69        let yaml = r#"
70workspace_url: "https://myteam.slack.com"
71bot_token_keychain_account: "mur_slack_bot_x"
72app_token_keychain_account: "mur_slack_app_x"
73allowed_channels: ["C111", "C222"]
74"#;
75        let cfg: SlackConfig = serde_yaml::from_str(yaml).unwrap();
76        assert_eq!(cfg.allowed_channels, vec!["C111", "C222"]);
77    }
78}