feat(slack): add groupRequireMention for allowlist channels

Slack's groupPolicy could either restrict to specific channels
("allowlist") or require an @mention ("mention"), but not both: in
allowlist mode the bot replied to every message in approved channels.

Add a groupRequireMention flag so that, when groupPolicy is "allowlist",
the bot only responds in channels listed in groupAllowFrom AND only when
@mentioned. Mirrors Signal's group.requireMention. No effect for the
"mention"/"open" policies, so existing configs are unchanged.

Extract the mention check into _is_mention and reuse it from both the
mention and allowlist branches.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
brendanlevy 2026-06-10 13:38:37 -07:00 committed by Xubin Ren
parent ffae1dca6d
commit 2d9260cb9f
3 changed files with 77 additions and 5 deletions

View File

@ -572,7 +572,9 @@ nanobot gateway
DM the bot directly or @mention it in a channel — it should respond!
> [!TIP]
> - `groupPolicy`: `"mention"` (default — respond only when @mentioned), `"open"` (respond to all channel messages), or `"allowlist"` (restrict to specific channels).
> - `groupPolicy`: `"mention"` (default — respond only when @mentioned), `"open"` (respond to all channel messages), or `"allowlist"` (restrict to specific channels via `groupAllowFrom`).
> - `groupAllowFrom`: channel IDs the bot may respond in when `groupPolicy` is `"allowlist"`.
> - `groupRequireMention`: when `true` and `groupPolicy` is `"allowlist"`, the bot only replies to channels in `groupAllowFrom` **and** only when @mentioned (instead of every message). No effect for `"mention"`/`"open"`. Use this to scope the bot to approved channels while keeping mention-only behavior.
> - DM policy defaults to open. Set `"dm": {"enabled": false}` to disable DMs.
</details>

View File

@ -47,6 +47,10 @@ class SlackConfig(Base):
allow_from: list[str] = Field(default_factory=list)
group_policy: str = "mention"
group_allow_from: list[str] = Field(default_factory=list)
# When group_policy is "allowlist", also require the bot to be @mentioned
# before responding (so it only replies to mentions in approved channels,
# instead of every message). No effect for "mention"/"open" policies.
group_require_mention: bool = False
dm: SlackDMConfig = Field(default_factory=SlackDMConfig)
@ -648,15 +652,22 @@ class SlackChannel(BaseChannel):
return chat_id in self.config.group_allow_from
return True
def _is_mention(self, event_type: str, text: str) -> bool:
if event_type == "app_mention":
return True
return self._bot_user_id is not None and f"<@{self._bot_user_id}>" in text
def _should_respond_in_channel(self, event_type: str, text: str, chat_id: str) -> bool:
if self.config.group_policy == "open":
return True
if self.config.group_policy == "mention":
if event_type == "app_mention":
return True
return self._bot_user_id is not None and f"<@{self._bot_user_id}>" in text
return self._is_mention(event_type, text)
if self.config.group_policy == "allowlist":
return chat_id in self.config.group_allow_from
if chat_id not in self.config.group_allow_from:
return False
if self.config.group_require_mention:
return self._is_mention(event_type, text)
return True
return False
def is_allowed(self, sender_id: str) -> bool:

View File

@ -655,3 +655,62 @@ def test_slack_channel_uses_channel_aware_allow_policy() -> None:
channel = SlackChannel(SlackConfig(enabled=True, allow_from=[]), MessageBus())
assert channel.is_allowed("U1") is True
assert channel._is_allowed("U1", "C123", "channel") is True
def test_mention_policy_responds_to_mentions_in_any_channel() -> None:
channel = SlackChannel(SlackConfig(enabled=True, group_policy="mention"), MessageBus())
channel._bot_user_id = "UBOT"
assert channel._should_respond_in_channel("app_mention", "<@UBOT> hi", "C123") is True
assert channel._should_respond_in_channel("message", "<@UBOT> hi", "C999") is True
assert channel._should_respond_in_channel("message", "no mention here", "C123") is False
def test_allowlist_policy_restricts_to_approved_channels() -> None:
channel = SlackChannel(
SlackConfig(enabled=True, group_policy="allowlist", group_allow_from=["C_OK"]),
MessageBus(),
)
channel._bot_user_id = "UBOT"
# In an approved channel without require_mention, respond to anything.
assert channel._should_respond_in_channel("message", "anything", "C_OK") is True
# An unapproved channel is always rejected.
assert channel._should_respond_in_channel("app_mention", "<@UBOT> hi", "C_NOPE") is False
# _is_allowed also gates on the channel allowlist.
assert channel._is_allowed("U1", "C_OK", "channel") is True
assert channel._is_allowed("U1", "C_NOPE", "channel") is False
def test_allowlist_with_require_mention_needs_both_channel_and_mention() -> None:
channel = SlackChannel(
SlackConfig(
enabled=True,
group_policy="allowlist",
group_allow_from=["C_OK"],
group_require_mention=True,
),
MessageBus(),
)
channel._bot_user_id = "UBOT"
# Approved channel + mention -> respond.
assert channel._should_respond_in_channel("app_mention", "<@UBOT> hi", "C_OK") is True
assert channel._should_respond_in_channel("message", "<@UBOT> hi", "C_OK") is True
# Approved channel but no mention -> stay quiet.
assert channel._should_respond_in_channel("message", "just chatting", "C_OK") is False
# Mention in an unapproved channel -> stay quiet.
assert channel._should_respond_in_channel("app_mention", "<@UBOT> hi", "C_NOPE") is False
def test_group_require_mention_accepts_camel_case_alias() -> None:
config = SlackConfig.model_validate(
{
"enabled": True,
"groupPolicy": "allowlist",
"groupAllowFrom": ["C_OK"],
"groupRequireMention": True,
}
)
assert config.group_require_mention is True
assert config.group_allow_from == ["C_OK"]