fix(gateway): honor explicit platforms.<x>.enabled: false over env credentials (#48820)
Twelve credential-presence branches in _apply_env_overrides (weixin, whatsapp_cloud, homeassistant, email, sms, dingtalk, feishu, wecom, wecom_callback, bluebubbles, qqbot, yuanbao) force-set enabled = True unconditionally, so a user's explicit `platforms.<x>.enabled: false` in config.yaml was silently overridden whenever the platform's token/secret lived in .env. Telegram/Discord/Slack/Signal/Matrix already routed through _enable_from_env, which honors the `_enabled_explicit` marker written by load_gateway_config. Route all twelve sites through the same helper. Credentials are still wired into the (disabled) PlatformConfig so send-only tooling keeps working — the same contract Slack and api_server already follow. Live repro (real load_gateway_config against a temp HERMES_HOME, yaml `enabled: false` + creds in env): 12/13 platforms flipped to enabled=True on main; 0/13 after the fix (telegram control unchanged). Bug 2 of #48820. Fix direction from @JoaoMarcos44 in #48852 (surgically reapplied on current main — the June branch no longer applies). Co-authored-by: JoaoMarcos44 <joaomarcosdias444@gmail.com>
This commit is contained in:
@@ -2083,9 +2083,8 @@ def _apply_env_overrides(config: GatewayConfig) -> None:
|
||||
whatsapp_cloud_phone_id = getenv("WHATSAPP_CLOUD_PHONE_NUMBER_ID")
|
||||
whatsapp_cloud_token = getenv("WHATSAPP_CLOUD_ACCESS_TOKEN")
|
||||
if whatsapp_cloud_phone_id and whatsapp_cloud_token:
|
||||
if Platform.WHATSAPP_CLOUD not in config.platforms:
|
||||
config.platforms[Platform.WHATSAPP_CLOUD] = PlatformConfig()
|
||||
config.platforms[Platform.WHATSAPP_CLOUD].enabled = True
|
||||
# Honors an explicit ``platforms.whatsapp_cloud.enabled: false`` (#48820).
|
||||
_enable_from_env(Platform.WHATSAPP_CLOUD)
|
||||
config.platforms[Platform.WHATSAPP_CLOUD].extra.update({
|
||||
"phone_number_id": whatsapp_cloud_phone_id,
|
||||
"access_token": whatsapp_cloud_token,
|
||||
@@ -2248,9 +2247,8 @@ def _apply_env_overrides(config: GatewayConfig) -> None:
|
||||
# Home Assistant
|
||||
hass_token = getenv("HASS_TOKEN")
|
||||
if hass_token:
|
||||
if Platform.HOMEASSISTANT not in config.platforms:
|
||||
config.platforms[Platform.HOMEASSISTANT] = PlatformConfig()
|
||||
config.platforms[Platform.HOMEASSISTANT].enabled = True
|
||||
# Honors an explicit ``platforms.homeassistant.enabled: false`` (#48820).
|
||||
_enable_from_env(Platform.HOMEASSISTANT)
|
||||
config.platforms[Platform.HOMEASSISTANT].token = hass_token
|
||||
hass_url = getenv("HASS_URL")
|
||||
if hass_url:
|
||||
@@ -2262,9 +2260,8 @@ def _apply_env_overrides(config: GatewayConfig) -> None:
|
||||
email_imap = getenv("EMAIL_IMAP_HOST")
|
||||
email_smtp = getenv("EMAIL_SMTP_HOST")
|
||||
if all([email_addr, email_pwd, email_imap, email_smtp]):
|
||||
if Platform.EMAIL not in config.platforms:
|
||||
config.platforms[Platform.EMAIL] = PlatformConfig()
|
||||
config.platforms[Platform.EMAIL].enabled = True
|
||||
# Honors an explicit ``platforms.email.enabled: false`` (#48820).
|
||||
_enable_from_env(Platform.EMAIL)
|
||||
config.platforms[Platform.EMAIL].extra.update({
|
||||
"address": email_addr,
|
||||
"imap_host": email_imap,
|
||||
@@ -2282,9 +2279,8 @@ def _apply_env_overrides(config: GatewayConfig) -> None:
|
||||
# SMS (Twilio)
|
||||
twilio_sid = getenv("TWILIO_ACCOUNT_SID")
|
||||
if twilio_sid:
|
||||
if Platform.SMS not in config.platforms:
|
||||
config.platforms[Platform.SMS] = PlatformConfig()
|
||||
config.platforms[Platform.SMS].enabled = True
|
||||
# Honors an explicit ``platforms.sms.enabled: false`` (#48820).
|
||||
_enable_from_env(Platform.SMS)
|
||||
config.platforms[Platform.SMS].api_key = getenv("TWILIO_AUTH_TOKEN", "")
|
||||
sms_home = getenv("SMS_HOME_CHANNEL")
|
||||
if sms_home and Platform.SMS in config.platforms:
|
||||
@@ -2410,9 +2406,8 @@ def _apply_env_overrides(config: GatewayConfig) -> None:
|
||||
dingtalk_client_id = getenv("DINGTALK_CLIENT_ID")
|
||||
dingtalk_client_secret = getenv("DINGTALK_CLIENT_SECRET")
|
||||
if dingtalk_client_id and dingtalk_client_secret:
|
||||
if Platform.DINGTALK not in config.platforms:
|
||||
config.platforms[Platform.DINGTALK] = PlatformConfig()
|
||||
config.platforms[Platform.DINGTALK].enabled = True
|
||||
# Honors an explicit ``platforms.dingtalk.enabled: false`` (#48820).
|
||||
_enable_from_env(Platform.DINGTALK)
|
||||
config.platforms[Platform.DINGTALK].extra.update({
|
||||
"client_id": dingtalk_client_id,
|
||||
"client_secret": dingtalk_client_secret,
|
||||
@@ -2430,9 +2425,8 @@ def _apply_env_overrides(config: GatewayConfig) -> None:
|
||||
feishu_app_id = getenv("FEISHU_APP_ID")
|
||||
feishu_app_secret = getenv("FEISHU_APP_SECRET")
|
||||
if feishu_app_id and feishu_app_secret:
|
||||
if Platform.FEISHU not in config.platforms:
|
||||
config.platforms[Platform.FEISHU] = PlatformConfig()
|
||||
config.platforms[Platform.FEISHU].enabled = True
|
||||
# Honors an explicit ``platforms.feishu.enabled: false`` (#48820).
|
||||
_enable_from_env(Platform.FEISHU)
|
||||
config.platforms[Platform.FEISHU].extra.update({
|
||||
"app_id": feishu_app_id,
|
||||
"app_secret": feishu_app_secret,
|
||||
@@ -2458,9 +2452,8 @@ def _apply_env_overrides(config: GatewayConfig) -> None:
|
||||
wecom_bot_id = getenv("WECOM_BOT_ID")
|
||||
wecom_secret = getenv("WECOM_SECRET")
|
||||
if wecom_bot_id and wecom_secret:
|
||||
if Platform.WECOM not in config.platforms:
|
||||
config.platforms[Platform.WECOM] = PlatformConfig()
|
||||
config.platforms[Platform.WECOM].enabled = True
|
||||
# Honors an explicit ``platforms.wecom.enabled: false`` (#48820).
|
||||
_enable_from_env(Platform.WECOM)
|
||||
config.platforms[Platform.WECOM].extra.update({
|
||||
"bot_id": wecom_bot_id,
|
||||
"secret": wecom_secret,
|
||||
@@ -2481,9 +2474,8 @@ def _apply_env_overrides(config: GatewayConfig) -> None:
|
||||
wecom_callback_corp_id = getenv("WECOM_CALLBACK_CORP_ID")
|
||||
wecom_callback_corp_secret = getenv("WECOM_CALLBACK_CORP_SECRET")
|
||||
if wecom_callback_corp_id and wecom_callback_corp_secret:
|
||||
if Platform.WECOM_CALLBACK not in config.platforms:
|
||||
config.platforms[Platform.WECOM_CALLBACK] = PlatformConfig()
|
||||
config.platforms[Platform.WECOM_CALLBACK].enabled = True
|
||||
# Honors an explicit ``platforms.wecom_callback.enabled: false`` (#48820).
|
||||
_enable_from_env(Platform.WECOM_CALLBACK)
|
||||
config.platforms[Platform.WECOM_CALLBACK].extra.update({
|
||||
"corp_id": wecom_callback_corp_id,
|
||||
"corp_secret": wecom_callback_corp_secret,
|
||||
@@ -2501,9 +2493,8 @@ def _apply_env_overrides(config: GatewayConfig) -> None:
|
||||
weixin_token = getenv("WEIXIN_TOKEN")
|
||||
weixin_account_id = getenv("WEIXIN_ACCOUNT_ID")
|
||||
if weixin_token or weixin_account_id:
|
||||
if Platform.WEIXIN not in config.platforms:
|
||||
config.platforms[Platform.WEIXIN] = PlatformConfig()
|
||||
config.platforms[Platform.WEIXIN].enabled = True
|
||||
# Honors an explicit ``platforms.weixin.enabled: false`` (#48820).
|
||||
_enable_from_env(Platform.WEIXIN)
|
||||
if weixin_token:
|
||||
config.platforms[Platform.WEIXIN].token = weixin_token
|
||||
extra = config.platforms[Platform.WEIXIN].extra
|
||||
@@ -2543,9 +2534,8 @@ def _apply_env_overrides(config: GatewayConfig) -> None:
|
||||
bluebubbles_server_url = getenv("BLUEBUBBLES_SERVER_URL")
|
||||
bluebubbles_password = getenv("BLUEBUBBLES_PASSWORD")
|
||||
if bluebubbles_server_url and bluebubbles_password:
|
||||
if Platform.BLUEBUBBLES not in config.platforms:
|
||||
config.platforms[Platform.BLUEBUBBLES] = PlatformConfig()
|
||||
config.platforms[Platform.BLUEBUBBLES].enabled = True
|
||||
# Honors an explicit ``platforms.bluebubbles.enabled: false`` (#48820).
|
||||
_enable_from_env(Platform.BLUEBUBBLES)
|
||||
config.platforms[Platform.BLUEBUBBLES].extra.update({
|
||||
"server_url": bluebubbles_server_url.rstrip("/"),
|
||||
"password": bluebubbles_password,
|
||||
@@ -2583,9 +2573,8 @@ def _apply_env_overrides(config: GatewayConfig) -> None:
|
||||
qq_app_id = getenv("QQ_APP_ID")
|
||||
qq_client_secret = getenv("QQ_CLIENT_SECRET")
|
||||
if qq_app_id or qq_client_secret:
|
||||
if Platform.QQBOT not in config.platforms:
|
||||
config.platforms[Platform.QQBOT] = PlatformConfig()
|
||||
config.platforms[Platform.QQBOT].enabled = True
|
||||
# Honors an explicit ``platforms.qqbot.enabled: false`` (#48820).
|
||||
_enable_from_env(Platform.QQBOT)
|
||||
extra = config.platforms[Platform.QQBOT].extra
|
||||
if qq_app_id:
|
||||
extra["app_id"] = qq_app_id
|
||||
@@ -2625,9 +2614,8 @@ def _apply_env_overrides(config: GatewayConfig) -> None:
|
||||
yuanbao_app_id = getenv("YUANBAO_APP_ID") or getenv("YUANBAO_APP_KEY")
|
||||
yuanbao_app_secret = getenv("YUANBAO_APP_SECRET")
|
||||
if yuanbao_app_id and yuanbao_app_secret:
|
||||
if Platform.YUANBAO not in config.platforms:
|
||||
config.platforms[Platform.YUANBAO] = PlatformConfig()
|
||||
config.platforms[Platform.YUANBAO].enabled = True
|
||||
# Honors an explicit ``platforms.yuanbao.enabled: false`` (#48820).
|
||||
_enable_from_env(Platform.YUANBAO)
|
||||
extra = config.platforms[Platform.YUANBAO].extra
|
||||
extra["app_id"] = yuanbao_app_id
|
||||
extra["app_secret"] = yuanbao_app_secret
|
||||
|
||||
121
tests/gateway/test_env_override_explicit_disable_48820.py
Normal file
121
tests/gateway/test_env_override_explicit_disable_48820.py
Normal file
@@ -0,0 +1,121 @@
|
||||
"""Regression tests for #48820 Bug 2: an explicit ``platforms.<x>.enabled: false``
|
||||
in config.yaml must survive ``_apply_env_overrides`` when that platform's
|
||||
credentials are present in the environment.
|
||||
|
||||
Before the fix, twelve credential-presence branches (weixin, whatsapp_cloud,
|
||||
homeassistant, email, sms, dingtalk, feishu, wecom, wecom_callback, bluebubbles,
|
||||
qqbot, yuanbao) force-set ``enabled = True`` unconditionally, while Telegram /
|
||||
Discord / Slack routed through ``_enable_from_env`` and honored the
|
||||
``_enabled_explicit`` marker. These tests drive the real ``load_gateway_config``
|
||||
against a temp HERMES_HOME — real YAML I/O, no mocks of the code under test.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
|
||||
from gateway.config import Platform, load_gateway_config
|
||||
|
||||
|
||||
# platform -> env credentials that trigger its env-enable branch
|
||||
CRED_ENV = {
|
||||
"weixin": {
|
||||
"WEIXIN_TOKEN": "wx_9f8e7d6c5b4a3f2e1d0c9b8a7f6e5d4c3b2a1f0e",
|
||||
"WEIXIN_ACCOUNT_ID": "acct_12345",
|
||||
},
|
||||
"whatsapp_cloud": {
|
||||
"WHATSAPP_CLOUD_PHONE_NUMBER_ID": "1234567890",
|
||||
"WHATSAPP_CLOUD_ACCESS_TOKEN": "EAAB-test-access-token",
|
||||
},
|
||||
"homeassistant": {"HASS_TOKEN": "hass-long-lived-token"},
|
||||
"email": {
|
||||
"EMAIL_ADDRESS": "bot@example.com",
|
||||
"EMAIL_PASSWORD": "app-password",
|
||||
"EMAIL_IMAP_HOST": "imap.example.com",
|
||||
"EMAIL_SMTP_HOST": "smtp.example.com",
|
||||
},
|
||||
"sms": {"TWILIO_ACCOUNT_SID": "ACxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"},
|
||||
"dingtalk": {"DINGTALK_CLIENT_ID": "ding-id", "DINGTALK_CLIENT_SECRET": "ding-secret"},
|
||||
"feishu": {"FEISHU_APP_ID": "cli_feishu", "FEISHU_APP_SECRET": "feishu-secret"},
|
||||
"wecom": {"WECOM_BOT_ID": "wecom-bot", "WECOM_SECRET": "wecom-secret"},
|
||||
"wecom_callback": {
|
||||
"WECOM_CALLBACK_CORP_ID": "corp-id",
|
||||
"WECOM_CALLBACK_CORP_SECRET": "corp-secret",
|
||||
},
|
||||
"bluebubbles": {
|
||||
"BLUEBUBBLES_SERVER_URL": "http://127.0.0.1:1234",
|
||||
"BLUEBUBBLES_PASSWORD": "bb-password",
|
||||
},
|
||||
"qqbot": {"QQ_APP_ID": "qq-app", "QQ_CLIENT_SECRET": "qq-secret"},
|
||||
"yuanbao": {"YUANBAO_APP_ID": "yb-app", "YUANBAO_APP_SECRET": "yb-secret"},
|
||||
# control: the pattern that always honored the explicit disable
|
||||
"telegram": {"TELEGRAM_BOT_TOKEN": "123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11"},
|
||||
}
|
||||
|
||||
_PLATFORM_ENV_PREFIXES = (
|
||||
"TELEGRAM_", "DISCORD_", "SLACK_", "WEIXIN_", "WHATSAPP_", "HASS_", "EMAIL_",
|
||||
"TWILIO_", "DINGTALK_", "FEISHU_", "WECOM_", "BLUEBUBBLES_", "QQ_", "QQBOT_",
|
||||
"YUANBAO_", "GATEWAY_RELAY", "SIGNAL_", "MATTERMOST_", "MATRIX_",
|
||||
)
|
||||
|
||||
|
||||
def _isolate(monkeypatch, tmp_path, env):
|
||||
import os
|
||||
|
||||
for key in list(os.environ):
|
||||
if key.startswith(_PLATFORM_ENV_PREFIXES):
|
||||
monkeypatch.delenv(key, raising=False)
|
||||
hermes_home = tmp_path / ".hermes"
|
||||
hermes_home.mkdir()
|
||||
monkeypatch.setenv("HERMES_HOME", str(hermes_home))
|
||||
for k, v in env.items():
|
||||
monkeypatch.setenv(k, v)
|
||||
return hermes_home
|
||||
|
||||
|
||||
@pytest.mark.parametrize("platform", sorted(CRED_ENV))
|
||||
def test_yaml_explicit_disable_survives_env_credentials(platform, tmp_path, monkeypatch):
|
||||
"""``platforms.<x>.enabled: false`` + credentials in env -> stays disabled."""
|
||||
hermes_home = _isolate(monkeypatch, tmp_path, CRED_ENV[platform])
|
||||
(hermes_home / "config.yaml").write_text(
|
||||
f"platforms:\n {platform}:\n enabled: false\n", encoding="utf-8"
|
||||
)
|
||||
|
||||
config = load_gateway_config()
|
||||
|
||||
cfg = config.platforms.get(Platform(platform))
|
||||
assert cfg is not None
|
||||
assert cfg.enabled is False, (
|
||||
f"{platform}: env credentials re-enabled a platform the user explicitly "
|
||||
"disabled in config.yaml (#48820 Bug 2)"
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("platform", sorted(CRED_ENV))
|
||||
def test_env_credentials_still_enable_without_yaml_opinion(platform, tmp_path, monkeypatch):
|
||||
"""No ``enabled`` key in YAML + credentials in env -> env-only setup still works."""
|
||||
hermes_home = _isolate(monkeypatch, tmp_path, CRED_ENV[platform])
|
||||
(hermes_home / "config.yaml").write_text("platforms: {}\n", encoding="utf-8")
|
||||
|
||||
config = load_gateway_config()
|
||||
|
||||
cfg = config.platforms.get(Platform(platform))
|
||||
assert cfg is not None and cfg.enabled is True, (
|
||||
f"{platform}: env-only configuration must still enable the platform"
|
||||
)
|
||||
|
||||
|
||||
def test_env_credentials_still_populate_extra_when_yaml_disables(tmp_path, monkeypatch):
|
||||
"""The disable only gates ``enabled``; credentials are still wired through
|
||||
(mirrors the Slack/API-server contract so send-only tooling keeps working)."""
|
||||
hermes_home = _isolate(monkeypatch, tmp_path, CRED_ENV["weixin"])
|
||||
(hermes_home / "config.yaml").write_text(
|
||||
"platforms:\n weixin:\n enabled: false\n", encoding="utf-8"
|
||||
)
|
||||
|
||||
config = load_gateway_config()
|
||||
|
||||
cfg = config.platforms[Platform.WEIXIN]
|
||||
assert cfg.enabled is False
|
||||
assert cfg.token == CRED_ENV["weixin"]["WEIXIN_TOKEN"]
|
||||
assert cfg.extra.get("account_id") == "acct_12345"
|
||||
# marker never leaks out of config load
|
||||
assert "_enabled_explicit" not in cfg.extra
|
||||
Reference in New Issue
Block a user