diff --git a/nanobot/channels/manager.py b/nanobot/channels/manager.py index b63510fae..c310943cd 100644 --- a/nanobot/channels/manager.py +++ b/nanobot/channels/manager.py @@ -144,9 +144,11 @@ class ChannelManager: else: allow = getattr(cfg, "allow_from", None) if allow is None: - raise SystemExit( - f'Error: "{name}" is missing or null allowFrom. ' - f'Set ["*"] to allow everyone, or add specific user IDs.' + # allowFrom omitted → pairing-only mode. Unapproved senders + # receive a pairing code instead of being silently ignored. + logger.info( + '"{}" has no allowFrom; unapproved users will receive a pairing code', + name, ) def _should_send_progress(self, channel_name: str, *, tool_hint: bool = False) -> bool: diff --git a/tests/channels/test_channel_plugins.py b/tests/channels/test_channel_plugins.py index 9b6e79783..2309df2c2 100644 --- a/tests/channels/test_channel_plugins.py +++ b/tests/channels/test_channel_plugins.py @@ -1010,6 +1010,36 @@ async def test_validate_allow_from_allows_empty_dict_allow_from(): mgr._validate_allow_from() +@pytest.mark.asyncio +async def test_validate_allow_from_allows_missing_allow_from(): + """Omitted allowFrom is valid — channel operates in pairing-only mode.""" + fake_config = SimpleNamespace( + channels=ChannelsConfig(), + providers=SimpleNamespace(groq=SimpleNamespace(api_key="")), + ) + + class _NoAllowFromChannel(BaseChannel): + name = "noallow" + display_name = "No Allow" + + async def start(self) -> None: + pass + + async def stop(self) -> None: + pass + + async def send(self, msg: OutboundMessage) -> None: + pass + + mgr = ChannelManager.__new__(ChannelManager) + mgr.config = fake_config + mgr.channels = {"test": _NoAllowFromChannel({"enabled": True}, None)} + mgr._dispatch_task = None + + # Should not raise — pairing-only mode + mgr._validate_allow_from() + + @pytest.mark.asyncio async def test_get_channel_returns_channel_if_exists(): """get_channel should return the channel if it exists."""