diff --git a/gateway/config.py b/gateway/config.py index 71e4d1d3fced..245784beb01b 100644 --- a/gateway/config.py +++ b/gateway/config.py @@ -440,6 +440,22 @@ class ChannelOverride: ) +# Canonical map of platforms whose primary credential is ``PlatformConfig.token`` +# and the env var it loads from. Used for empty-token warnings at config +# validation and by the multiplex primary-startup credential gate in +# ``gateway.run`` (#64674). Platforms absent from this map authenticate some +# other way (session files, port-bound webhooks, api_key-only) and must never +# be skipped for a missing token. +PLATFORM_TOKEN_ENV_NAMES: dict["Platform", str] = { + Platform.TELEGRAM: "TELEGRAM_BOT_TOKEN", + Platform.DISCORD: "DISCORD_BOT_TOKEN", + Platform.SLACK: "SLACK_BOT_TOKEN", + Platform.MATTERMOST: "MATTERMOST_TOKEN", + Platform.MATRIX: "MATRIX_ACCESS_TOKEN", + Platform.WEIXIN: "WEIXIN_TOKEN", +} + + @dataclass class PlatformConfig: """Configuration for a single messaging platform.""" @@ -1424,14 +1440,7 @@ def _validate_gateway_config(config: "GatewayConfig") -> None: # Warn about empty bot tokens — platforms that loaded an empty string # won't connect and the cause can be confusing without a log line. - _token_env_names = { - Platform.TELEGRAM: "TELEGRAM_BOT_TOKEN", - Platform.DISCORD: "DISCORD_BOT_TOKEN", - Platform.SLACK: "SLACK_BOT_TOKEN", - Platform.MATTERMOST: "MATTERMOST_TOKEN", - Platform.MATRIX: "MATRIX_ACCESS_TOKEN", - Platform.WEIXIN: "WEIXIN_TOKEN", - } + _token_env_names = PLATFORM_TOKEN_ENV_NAMES for platform, pconfig in config.platforms.items(): if not pconfig.enabled: continue diff --git a/gateway/run.py b/gateway/run.py index fe4e021d96be..9b085594a680 100644 --- a/gateway/run.py +++ b/gateway/run.py @@ -1513,16 +1513,9 @@ def _platform_has_bot_credential(platform: "Platform", platform_config: "Platfor Platforms that do not use ``PlatformConfig.token`` always return True so we never skip them here (Signal session paths, port-binding HTTP adapters, etc.). """ - # Keep in sync with gateway.config token env map used for empty-token warnings. - token_platforms = { - Platform.TELEGRAM, - Platform.DISCORD, - Platform.SLACK, - Platform.MATTERMOST, - Platform.MATRIX, - Platform.WEIXIN, - } - if platform not in token_platforms: + from gateway.config import PLATFORM_TOKEN_ENV_NAMES + + if platform not in PLATFORM_TOKEN_ENV_NAMES: return True token = getattr(platform_config, "token", None) or "" if isinstance(token, str) and token.strip(): @@ -7232,6 +7225,7 @@ class GatewayRunner(GatewayAuthorizationMixin, GatewayKanbanWatchersMixin, Gatew # Initialize and connect each configured platform _multiplex_on = bool(getattr(self.config, "multiplex_profiles", False)) + _multiplex_skipped_platforms: list[Platform] = [] for platform, platform_config in self.config.platforms.items(): if await self._abort_startup_if_shutdown_requested(): return True @@ -7251,6 +7245,7 @@ class GatewayRunner(GatewayAuthorizationMixin, GatewayKanbanWatchersMixin, Gatew "provide the token will still connect.", platform.value, ) + _multiplex_skipped_platforms.append(platform) continue enabled_platform_count += 1 @@ -7396,6 +7391,25 @@ class GatewayRunner(GatewayAuthorizationMixin, GatewayKanbanWatchersMixin, Gatew except Exception as e: logger.error("Secondary-profile adapter startup failed: %s", e, exc_info=True) + # A platform we skipped on the primary for a missing credential was + # supposed to be picked up by a secondary profile that owns the token. + # If none did, the platform is enabled in config.yaml yet silently + # unserved — surface it loudly so the operator sees a config problem + # instead of a quiet dead channel (#64674 follow-up). + for _skipped in _multiplex_skipped_platforms: + _served_by_secondary = any( + _skipped in _profile_map + for _profile_map in self._profile_adapters.values() + ) + if not _served_by_secondary: + logger.warning( + "%s is enabled but no profile (default or secondary) " + "provided a bot credential for it — the platform is not " + "being served. Add its token to the profile that should " + "own it, or disable the platform.", + _skipped.value, + ) + if connected_count == 0: if startup_nonretryable_errors: reason = "; ".join(startup_nonretryable_errors) diff --git a/scripts/release.py b/scripts/release.py index 6a1d5629b64c..96b4b2fe6064 100755 --- a/scripts/release.py +++ b/scripts/release.py @@ -45,6 +45,7 @@ ACP_REGISTRY_MANIFEST = REPO_ROOT / "acp_registry" / "agent.json" # Auto-extracted from noreply emails + manual overrides AUTHOR_MAP = { + "doogie@spark.local": "SAMBAS123", # PR #64986 salvage (gateway: multiplex primary bot token scope) "41409874+2751738943@users.noreply.github.com": "2751738943", # PR #54785 salvage (tui: post-turn completion ownership routing) "Burgunthy@users.noreply.github.com": "Burgunthy", # PR #20096 salvage (gateway: profile-based routing for inbound messages) "75556242+webtecnica@users.noreply.github.com": "webtecnica", # PR #63360 salvage (nous: restore inference-api base_url)