Skip to content

fix(gateway): respect explicit enabled:false in YAML for all platforms - #38745

Closed
porkmagus wants to merge 2 commits into
NousResearch:mainfrom
porkmagus:fix/env-respects-explicit-disable
Closed

fix(gateway): respect explicit enabled:false in YAML for all platforms#38745
porkmagus wants to merge 2 commits into
NousResearch:mainfrom
porkmagus:fix/env-respects-explicit-disable

Conversation

@porkmagus

Copy link
Copy Markdown

Fix: .env env vars silently override explicit platforms.*.enabled: false

Problem

When a user disables a platform in config.yaml (or via hermes setup gateway), the corresponding env vars remain in ~/.hermes/.env. On the next gateway restart, _apply_env_overrides() sees those env vars and re-enables the platform, ignoring the explicit disable.

This affects all platforms. The pattern is identical for tools: env vars in .env keep tools active even after hermes tools disable <name>.

Related Issues

Reproduction

  1. hermes setup gateway → enable Slack → saves SLACK_BOT_TOKEN to .env and platforms.slack.enabled: true to config.yaml
  2. Restart gateway → Slack works
  3. hermes config set platforms.slack.enabled false (or uncheck in setup wizard)
  4. config.yaml now says enabled: false
  5. Restart gateway → Slack still works because SLACK_BOT_TOKEN is in .env
  6. Same for WhatsApp (WHATSAPP_ENABLED=true), Signal (SIGNAL_HTTP_URL), etc.

Root Cause

gateway/config.py::_apply_env_overrides() treats the presence of an env var as implicit activation intent:

# Current (broken) — Slack example
slack_token = os.getenv("SLACK_BOT_TOKEN")
if slack_token:
    if Platform.SLACK not in config.platforms:
        config.platforms[Platform.SLACK] = PlatformConfig()
        config.platforms[Platform.SLACK].enabled = True
    else:
        slack_config = config.platforms[Platform.SLACK]
        enabled_was_explicit = bool(slack_config.extra.pop("_enabled_explicit", False))
        if not slack_config.enabled and not enabled_was_explicit:
            slack_config.enabled = True
    slack_config.token = slack_token

The _enabled_explicit flag in extra dict was supposed to prevent this, but:

  • It is only set during YAML merge
  • It is popped (consumed) on first load
  • It is never set by hermes config set
  • After the first restart, the guard is gone and the zombie resurrects

For WhatsApp, it is even worse: WHATSAPP_ENABLED env var unconditionally sets enabled = True without checking the existing config at all.

For HomeAssistant, Email, SMS, DingTalk, Feishu, WeCom, WeCom callback, Weixin, BlueBubbles, QQ, Yuanbao, API Server, Webhook, and MSGraph Webhook — the same unconditional enabled = True pattern exists with no guard at all.

User Impact

  • Users cannot disable platforms without manually editing a hidden .env file
  • No CLI command exists to inspect or edit .env (hermes config env-path only prints the path)
  • The setup wizard silently leaves .env poison behind on disable
  • Users must discover ~/.hermes/.env exists, then use a text editor to remove variables

Solution

1. config.yaml wins for enabled state (all platforms)

Change _apply_env_overrides() to only auto-enable a platform when no YAML config exists for that platform. If the user has ever configured the platform, the env var is treated as a credential only, not an activation signal.

# Fixed — Slack example
if slack_token := os.getenv("SLACK_BOT_TOKEN"):
    if Platform.SLACK not in config.platforms:
        # First-time env-only setup: auto-enable
        config.platforms[Platform.SLACK] = PlatformConfig(
            enabled=True, token=slack_token
        )
    else:
        # YAML config exists: only update token, respect enabled state
        config.platforms[Platform.SLACK].token = slack_token

Apply this to all platforms currently using the broken pattern:

  • Telegram, Discord, Slack, WhatsApp, Signal, Mattermost, Matrix
  • HomeAssistant, Email, SMS, DingTalk, Feishu, WeCom, WeCom callback, Weixin, BlueBubbles, QQ, Yuanbao
  • API Server, Webhook, MSGraph Webhook

For WhatsApp, the WHATSAPP_ENABLED env var must stop overriding the YAML enabled flag. It should only be read as a fallback when no YAML config exists.

2. Promote enabled_explicit to a real PlatformConfig field

The current _enabled_explicit is stored in extra dict and consumed on load. Promote it to a proper bool field on the dataclass:

@dataclass
class PlatformConfig:
    enabled: bool = False
    token: Optional[str] = None
    # ... other fields ...
    enabled_explicit: bool = False  # new
  • from_dict() sets enabled_explicit = True whenever enabled is present in YAML
  • hermes config set sets enabled_explicit = True when it modifies enabled
  • _apply_env_overrides() checks enabled_explicit before auto-enabling
  • The field is persisted in YAML, not consumed and discarded

3. Add hermes env CLI suite

Users currently have no way to discover or edit .env from the CLI.

hermes env path          # Show .env path
hermes env list          # Show all env vars (values masked)
hermes env get KEY       # Show value of one key
hermes env set KEY VALUE # Set a key
hermes env rm KEY        # Remove a key
hermes env edit          # Open in $EDITOR with a WARNING header

The edit command shows a warning before opening:

WARNING: This file contains sensitive credentials.
Removing a token here may break the associated platform or tool.
Use `hermes config set` to disable features safely; it will clean up
.env automatically. Only edit this file directly if you know what
you are doing.

Press Enter to continue...

4. Automatic .env cleanup on disable

When a user disables a platform or tool, remove the corresponding env vars from .env immediately. hermes_cli/config.py already has remove_env_value() — we need to call it.

Platform mapping (derived from actual _apply_env_overrides and platform code):

PLATFORM_ENV_VARS = {
    "slack": ["SLACK_BOT_TOKEN", "SLACK_APP_TOKEN"],
    "telegram": ["TELEGRAM_BOT_TOKEN", "TELEGRAM_REPLY_TO_MODE", "TELEGRAM_FALLBACK_IPS", "TELEGRAM_HOME_CHANNEL", "TELEGRAM_HOME_CHANNEL_NAME", "TELEGRAM_HOME_CHANNEL_THREAD_ID"],
    "discord": ["DISCORD_BOT_TOKEN", "DISCORD_HOME_CHANNEL", "DISCORD_HOME_CHANNEL_NAME", "DISCORD_HOME_CHANNEL_THREAD_ID", "DISCORD_REPLY_TO_MODE"],
    "whatsapp": ["WHATSAPP_ENABLED", "WHATSAPP_HOME_CHANNEL", "WHATSAPP_HOME_CHANNEL_NAME", "WHATSAPP_HOME_CHANNEL_THREAD_ID"],
    "signal": ["SIGNAL_HTTP_URL", "SIGNAL_ACCOUNT", "SIGNAL_IGNORE_STORIES", "SIGNAL_HOME_CHANNEL", "SIGNAL_HOME_CHANNEL_NAME", "SIGNAL_HOME_CHANNEL_THREAD_ID"],
    "mattermost": ["MATTERMOST_TOKEN", "MATTERMOST_URL", "MATTERMOST_HOME_CHANNEL", "MATTERMOST_HOME_CHANNEL_NAME", "MATTERMOST_HOME_CHANNEL_THREAD_ID"],
    "matrix": ["MATRIX_ACCESS_TOKEN", "MATRIX_HOMESERVER", "MATRIX_PASSWORD", "MATRIX_USER_ID", "MATRIX_ENCRYPTION", "MATRIX_DEVICE_ID", "MATRIX_HOME_ROOM", "MATRIX_HOME_ROOM_NAME", "MATRIX_HOME_ROOM_THREAD_ID"],
    "homeassistant": ["HASS_TOKEN", "HASS_URL"],
    "email": ["EMAIL_ADDRESS", "EMAIL_PASSWORD", "EMAIL_IMAP_HOST", "EMAIL_SMTP_HOST", "EMAIL_HOME_ADDRESS", "EMAIL_HOME_ADDRESS_NAME", "EMAIL_HOME_ADDRESS_THREAD_ID"],
    "sms": ["TWILIO_ACCOUNT_SID", "TWILIO_AUTH_TOKEN", "SMS_HOME_CHANNEL", "SMS_HOME_CHANNEL_NAME", "SMS_HOME_CHANNEL_THREAD_ID"],
    "dingtalk": ["DINGTALK_CLIENT_ID", "DINGTALK_CLIENT_SECRET", "DINGTALK_HOME_CHANNEL", "DINGTALK_HOME_CHANNEL_NAME", "DINGTALK_HOME_CHANNEL_THREAD_ID"],
    "api_server": ["API_SERVER_ENABLED", "API_SERVER_KEY", "API_SERVER_CORS_ORIGINS", "API_SERVER_PORT", "API_SERVER_HOST", "API_SERVER_MODEL_NAME"],
    "webhook": ["WEBHOOK_ENABLED", "WEBHOOK_PORT", "WEBHOOK_SECRET"],
    "msgraph_webhook": ["MSGRAPH_WEBHOOK_ENABLED", "MSGRAPH_WEBHOOK_PORT", "MSGRAPH_WEBHOOK_CLIENT_STATE", "MSGRAPH_WEBHOOK_ACCEPTED_RESOURCES", "MSGRAPH_WEBHOOK_ALLOWED_SOURCE_CIDRS"],
    "feishu": ["FEISHU_APP_ID", "FEISHU_APP_SECRET", "FEISHU_DOMAIN", "FEISHU_CONNECTION_MODE", "FEISHU_ENCRYPT_KEY", "FEISHU_VERIFICATION_TOKEN", "FEISHU_HOME_CHANNEL", "FEISHU_HOME_CHANNEL_NAME", "FEISHU_HOME_CHANNEL_THREAD_ID"],
    "wecom": ["WECOM_BOT_ID", "WECOM_SECRET", "WECOM_WEBSOCKET_URL", "WECOM_HOME_CHANNEL", "WECOM_HOME_CHANNEL_NAME", "WECOM_HOME_CHANNEL_THREAD_ID"],
    "wecom_callback": ["WECOM_CALLBACK_CORP_ID", "WECOM_CALLBACK_CORP_SECRET", "WECOM_CALLBACK_AGENT_ID", "WECOM_CALLBACK_TOKEN", "WECOM_CALLBACK_ENCODING_AES_KEY", "WECOM_CALLBACK_HOST", "WECOM_CALLBACK_PORT"],
    "weixin": ["WEIXIN_TOKEN", "WEIXIN_ACCOUNT_ID", "WEIXIN_BASE_URL", "WEIXIN_CDN_BASE_URL", "WEIXIN_DM_POLICY", "WEIXIN_GROUP_POLICY", "WEIXIN_ALLOWED_USERS", "WEIXIN_GROUP_ALLOWED_USERS", "WEIXIN_SPLIT_MULTILINE_MESSAGES", "WEIXIN_HOME_CHANNEL", "WEIXIN_HOME_CHANNEL_NAME", "WEIXIN_HOME_CHANNEL_THREAD_ID"],
    "bluebubbles": ["BLUEBUBBLES_SERVER_URL", "BLUEBUBBLES_PASSWORD", "BLUEBUBBLES_WEBHOOK_HOST", "BLUEBUBBLES_WEBHOOK_PORT", "BLUEBUBBLES_WEBHOOK_PATH", "BLUEBUBBLES_SEND_READ_RECEIPTS", "BLUEBUBBLES_REQUIRE_MENTION", "BLUEBUBBLES_MENTION_PATTERNS", "BLUEBUBBLES_HOME_CHANNEL", "BLUEBUBBLES_HOME_CHANNEL_NAME", "BLUEBUBBLES_HOME_CHANNEL_THREAD_ID"],
    "qqbot": ["QQ_APP_ID", "QQ_CLIENT_SECRET", "QQ_ALLOWED_USERS", "QQ_GROUP_ALLOWED_USERS", "QQBOT_HOME_CHANNEL", "QQBOT_HOME_CHANNEL_NAME", "QQBOT_HOME_CHANNEL_THREAD_ID", "QQ_HOME_CHANNEL", "QQ_HOME_CHANNEL_NAME", "QQ_HOME_CHANNEL_THREAD_ID"],
    "yuanbao": ["YUANBAO_APP_ID", "YUANBAO_APP_KEY", "YUANBAO_APP_SECRET"],
}

Tool mapping (derived from TOOL_CATEGORIES and TOOLSET_ENV_REQUIREMENTS):

TOOL_ENV_VARS = {
    "vision": ["OPENROUTER_API_KEY"],
    "moa": ["OPENROUTER_API_KEY"],
    "tts": ["VOICE_TOOLS_OPENAI_KEY", "ELEVENLABS_API_KEY", "MISTRAL_API_KEY", "GEMINI_API_KEY"],
    "web": ["FIRECRAWL_API_URL", "FIRECRAWL_API_KEY"],
    "image_gen": ["FAL_KEY"],
    "video_gen": ["FAL_KEY"],
    "x_search": ["XAI_API_KEY"],
    "browser": ["BROWSER_USE_API_KEY", "CAMOFOX_URL"],
    "homeassistant": ["HASS_TOKEN", "HASS_URL"],
    "langfuse": ["HERMES_LANGFUSE_PUBLIC_KEY", "HERMES_LANGFUSE_SECRET_KEY", "HERMES_LANGFUSE_BASE_URL"],
    # ... and any other tool env vars defined in TOOL_CATEGORIES or TOOLSET_ENV_REQUIREMENTS
}

Hook this into:

  • hermes config set platforms.<name>.enabled false → removes env vars
  • hermes setup gateway uncheck → removes env vars
  • hermes tools disable <name> → removes env vars

5. Single source of truth for env→feature mapping

Create a single module that maps features to their env vars:

# hermes_cli/env_cleanup.py
from typing import Dict, List

PLATFORM_ENV_VARS: Dict[str, List[str]] = { ... }
TOOL_ENV_VARS: Dict[str, List[str]] = { ... }

def remove_env_vars_for_feature(category: str, name: str) -> List[str]:
    """Remove env vars for a disabled feature. Returns list of removed keys."""
    from hermes_cli.config import remove_env_value
    removed = []
    mapping = PLATFORM_ENV_VARS if category == "platform" else TOOL_ENV_VARS
    for key in mapping.get(name, []):
        if remove_env_value(key):
            removed.append(key)
    return removed

Import this in hermes_cli/config.py (for config set), hermes_cli/gateway.py (for setup wizard), and hermes_cli/tools_config.py (for tools disable).

6. Backwards compatibility

  • Users with existing enabled: false in YAML but env vars still present: on next load, the platform stays disabled (fixed behavior). The env vars remain in .env but are ignored. A one-time warning is logged on first restart: "Platform X is disabled in config.yaml but env vars remain in .env. Run hermes env rm X_TOKEN to clean up, or re-enable the platform to use them."
  • Users with env-only setup (no YAML): continue to work exactly as before. Auto-enable still happens.
  • No breaking changes to CLI commands or config schema.

Files to Modify

File Change
gateway/config.py Fix _apply_env_overrides() for all platforms; add enabled_explicit: bool to PlatformConfig
hermes_cli/config.py Add env subcommand suite; call remove_env_vars_for_feature() on platforms.*.enabled false
hermes_cli/env_cleanup.py New — single source of truth for env var mappings
hermes_cli/gateway.py Call remove_env_vars_for_feature() on platform disable in setup wizard
hermes_cli/tools_config.py Call remove_env_vars_for_feature() on tools disable
hermes_cli/main.py Register env command group
tests/gateway/test_config.py Add regression tests for all platforms
tests/cli/test_env.py New — test hermes env commands
tests/cli/test_env_cleanup.py New — test cleanup mappings

Test Plan

Regression Tests

class TestEnvOverrideRespectsExplicitDisable:
    """Env vars must NOT override explicit enabled: false in YAML."""

    def test_slack_token_does_not_override_explicit_disable(self, monkeypatch):
        config = GatewayConfig.from_dict({
            "platforms": {"slack": {"enabled": False}}
        })
        monkeypatch.setenv("SLACK_BOT_TOKEN", "xoxb-test")
        _apply_env_overrides(config)
        assert config.platforms[Platform.SLACK].enabled is False
        assert config.platforms[Platform.SLACK].token == "xoxb-test"

    def test_whatsapp_enabled_env_does_not_override_explicit_disable(self, monkeypatch):
        config = GatewayConfig.from_dict({
            "platforms": {"whatsapp": {"enabled": False}}
        })
        monkeypatch.setenv("WHATSAPP_ENABLED", "true")
        _apply_env_overrides(config)
        assert config.platforms[Platform.WHATSAPP].enabled is False

    def test_signal_env_does_not_override_explicit_disable(self, monkeypatch):
        config = GatewayConfig.from_dict({
            "platforms": {"signal": {"enabled": False}}
        })
        monkeypatch.setenv("SIGNAL_HTTP_URL", "http://localhost:8080")
        monkeypatch.setenv("SIGNAL_ACCOUNT", "+15551234567")
        _apply_env_overrides(config)
        assert config.platforms[Platform.SIGNAL].enabled is False

    def test_telegram_env_does_not_override_explicit_disable(self, monkeypatch):
        config = GatewayConfig.from_dict({
            "platforms": {"telegram": {"enabled": False}}
        })
        monkeypatch.setenv("TELEGRAM_BOT_TOKEN", "123:test")
        _apply_env_overrides(config)
        assert config.platforms[Platform.TELEGRAM].enabled is False

    def test_discord_env_does_not_override_explicit_disable(self, monkeypatch):
        config = GatewayConfig.from_dict({
            "platforms": {"discord": {"enabled": False}}
        })
        monkeypatch.setenv("DISCORD_BOT_TOKEN", "test-token")
        _apply_env_overrides(config)
        assert config.platforms[Platform.DISCORD].enabled is False

    def test_homeassistant_env_does_not_override_explicit_disable(self, monkeypatch):
        config = GatewayConfig.from_dict({
            "platforms": {"homeassistant": {"enabled": False}}
        })
        monkeypatch.setenv("HASS_TOKEN", "test-token")
        _apply_env_overrides(config)
        assert config.platforms[Platform.HOMEASSISTANT].enabled is False

    def test_api_server_env_does_not_override_explicit_disable(self, monkeypatch):
        config = GatewayConfig.from_dict({
            "platforms": {"api_server": {"enabled": False}}
        })
        monkeypatch.setenv("API_SERVER_ENABLED", "true")
        _apply_env_overrides(config)
        assert config.platforms[Platform.API_SERVER].enabled is False

    def test_env_only_setup_still_auto_enables(self, monkeypatch):
        """First-time env-only setup must still work."""
        config = GatewayConfig.from_dict({})
        monkeypatch.setenv("SLACK_BOT_TOKEN", "xoxb-test")
        _apply_env_overrides(config)
        assert config.platforms[Platform.SLACK].enabled is True
        assert config.platforms[Platform.SLACK].token == "xoxb-test"

    def test_enabled_explicit_persisted_across_restarts(self):
        """enabled_explicit must survive save/load round-trip."""
        config = GatewayConfig.from_dict({
            "platforms": {"slack": {"enabled": False}}
        })
        assert config.platforms[Platform.SLACK].enabled_explicit is True

        saved = config.to_dict()
        loaded = GatewayConfig.from_dict(saved)
        assert loaded.platforms[Platform.SLACK].enabled_explicit is True

CLI Tests

class TestEnvCommand:
    def test_env_list_masks_values(self, runner):
        result = runner.invoke(cli, ["env", "list"])
        assert "SLACK_BOT_TOKEN" in result.output
        assert "xoxb" not in result.output  # masked

    def test_env_rm_removes_key(self, runner, tmp_env):
        runner.invoke(cli, ["env", "rm", "SLACK_BOT_TOKEN"])
        assert "SLACK_BOT_TOKEN" not in tmp_env.read_text()

    def test_env_edit_shows_warning(self, runner):
        result = runner.invoke(cli, ["env", "edit"])
        assert "WARNING" in result.output
        assert "sensitive credentials" in result.output

Cleanup Tests

class TestEnvCleanup:
    def test_disable_platform_removes_env_vars(self, runner, tmp_env):
        runner.invoke(cli, ["config", "set", "platforms.slack.enabled", "false"])
        env_text = tmp_env.read_text()
        assert "SLACK_BOT_TOKEN" not in env_text
        assert "SLACK_APP_TOKEN" not in env_text

    def test_disable_tool_removes_env_vars(self, runner, tmp_env):
        runner.invoke(cli, ["tools", "disable", "homeassistant"])
        env_text = tmp_env.read_text()
        assert "HASS_TOKEN" not in env_text
        assert "HASS_URL" not in env_text

Migration / Rollout

  1. No breaking changes. Existing users with env-only setups continue to work.
  2. One-time warning. Users with existing enabled: false + env vars will see a log warning on first restart after upgrade. The platform stays disabled (correct behavior), but they are informed about orphaned env vars.
  3. Optional cleanup script. A hermes env cleanup subcommand can be added later to remove all env vars for disabled features in one pass.

Design Notes

Why not remove .env entirely?

.env is still needed for Docker deployments, CI/CD pipelines, and users who prefer env-only configuration. The goal is not to eliminate .env, but to stop it from silently overriding config.yaml.

Why not merge PR #35562 instead?

PR #35562 is a good partial fix for Telegram/Discord. This PR:

  1. Extends the same pattern to all platforms
  2. Adds the missing .env cleanup mechanism
  3. Adds the hermes env CLI for discoverability
  4. Fixes the tool variant of the same bug

These are complementary. If #35562 merges first, this PR can be rebased to add the remaining platforms + CLI + cleanup.

Why enabled_explicit as a dataclass field?

The current extra dict approach is fragile: untyped, consumed on load, not visible in the schema, and not set by hermes config set. A real field solves all of these and makes the intent explicit in the code.


Checklist

When a user disables a platform in config.yaml (or via hermes setup
gateway), the corresponding env vars remain in ~/.hermes/.env. On the
next gateway restart, _apply_env_overrides() sees those env vars and
re-enables the platform, ignoring the explicit disable.

This fix:
1. Promotes _enabled_explicit from extra dict (consumed on load) to a
   real PlatformConfig dataclass field that persists across restarts.
2. Replaces _enable_from_env() with _maybe_enable_from_env() that only
   auto-enables when no YAML config exists for that platform.
3. Applies the fix to ALL platforms (not just Telegram/Discord), including
   HomeAssistant, Email, SMS, DingTalk, Feishu, WeCom, WeCom callback,
   Weixin, BlueBubbles, QQ, Yuanbao, API Server, Webhook, MSGraph Webhook.
4. Adds hermes env CLI suite (path, list, get, set, rm, edit) for
   discoverability of .env.
5. Adds automatic .env cleanup on disable via env_cleanup.py module.
6. Hooks cleanup into hermes config set, hermes tools disable, and
   gateway setup wizard.

Fixes #35555, #37609
Refs PR #35562 (partial fix for Telegram/Discord only)
Refs PR #18764 (merged May 2 - same pattern for agent.max_turns)
Copilot AI review requested due to automatic review settings June 4, 2026 05:23

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Note

Copilot was unable to run its full agentic suite in this review.

Adds CLI support and safety mechanisms to prevent disabled platforms/tools from being silently re-enabled by leftover .env variables, and introduces an env subcommand for managing ~/.hermes/.env.

Changes:

  • Add env-var cleanup mappings and cleanup hooks when disabling platforms/tools.
  • Introduce hermes env ... CLI subcommands (path/list/get/set/rm/edit) implemented in hermes_cli.config.
  • Make gateway env overrides respect an explicit enabled choice via a persistent enabled_explicit flag.

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 9 comments.

Show a summary per file
File Description
hermes_cli/tools_config.py Removes tool-related env vars when toolsets are disabled.
hermes_cli/main.py Wires a new env CLI command and subcommands into the main parser.
hermes_cli/env_cleanup.py New central mapping + helper to remove env vars for platforms/tools.
hermes_cli/config.py Cleans platform env vars on explicit disable; implements env_command handler.
gateway/config.py Persists enabled_explicit and prevents env vars from re-enabling explicitly-disabled platforms.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread hermes_cli/config.py
Comment on lines +5919 to +5925
# Clean up .env vars when a platform is explicitly disabled
if key.startswith("platforms.") and key.endswith(".enabled") and value is False:
platform_name = key.split(".")[1]
from hermes_cli.env_cleanup import remove_env_vars_for_feature
removed = remove_env_vars_for_feature("platform", platform_name)
if removed:
logger.info("Removed %s env vars for disabled platform %s", len(removed), platform_name)

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed: Changed type to Union[str, bool, int, float] and added isinstance(value, bool) guard.

Comment thread hermes_cli/config.py Outdated
Comment on lines +6274 to +6279
if value is not None:
# Mask secrets
if len(value) > 8:
print(f"{key}={value[:4]}{'*' * (len(value) - 8)}{value[-4:]}")
else:
print(f"{key}={value}")

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed: Always mask secrets. Short values (≤4 chars) show all asterisks; longer values show first/last 2 chars.

Comment thread hermes_cli/config.py
Comment on lines +6306 to +6307
editor = os.environ.get("EDITOR", "nano")
env_path = get_env_path()

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed: Uses shlex.split() with fallback to raw string for $EDITOR with arguments/spaces.

Comment thread hermes_cli/config.py Outdated
Comment on lines +6315 to +6317
if not env_path.exists():
env_path.write_text("# Hermes environment variables\n", encoding="utf-8")
subprocess.run([editor, str(env_path)])

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed: Uses shlex.split() with fallback to raw string for $EDITOR with arguments/spaces.

Comment thread hermes_cli/env_cleanup.py
}


def remove_env_vars_for_feature(category: str, name: str) -> List[str]:

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed: Added ValueError if category is not 'platform' or 'tool'.

Comment thread hermes_cli/env_cleanup.py
Comment on lines +231 to +238
from hermes_cli.config import remove_env_value

removed: List[str] = []
mapping = PLATFORM_ENV_VARS if category == "platform" else TOOL_ENV_VARS
for key in mapping.get(name, []):
if remove_env_value(key):
removed.append(key)
return removed

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed: Added ValueError if category is not 'platform' or 'tool'.

Comment thread hermes_cli/main.py Outdated
Comment on lines +13676 to +13677
# env list
env_subparsers.add_parser("list", help="List all env vars (values masked)")

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed: Updated help text to 'List env var names' to match actual behavior.

Comment on lines +3663 to +3666
# Clean up .env vars for disabled tools
for tool_name in toolset_names:
from hermes_cli.env_cleanup import remove_env_vars_for_feature
removed = remove_env_vars_for_feature("tool", tool_name)

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed: Moved import to top of function.

Comment thread gateway/config.py
Comment on lines 1344 to 1351
# WhatsApp (typically uses different auth mechanism)
whatsapp_enabled = os.getenv("WHATSAPP_ENABLED", "").lower() in {"true", "1", "yes"}
whatsapp_disabled_explicitly = os.getenv("WHATSAPP_ENABLED", "").lower() in {"false", "0", "no"}
if Platform.WHATSAPP in config.platforms:
# YAML config exists — respect explicit disable
# YAML config exists — respect explicit disable. Only override if the
# user never explicitly set enabled (legacy env-only migration).
wa_cfg = config.platforms[Platform.WHATSAPP]
if whatsapp_disabled_explicitly:
wa_cfg.enabled = False
elif whatsapp_enabled:
if whatsapp_enabled and not wa_cfg.enabled and not wa_cfg.enabled_explicit:
wa_cfg.enabled = True

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed: Added back whatsapp_disabled_explicitly with enabled_explicit gate. YAML wins when explicitly set; env wins only for legacy/unconfigured setups.

@liuhao1024

Copy link
Copy Markdown
Contributor

I found one issue that looks worth fixing before merge.

hermes_cli/config.py:328 — the env cleanup guard value is False is always False.

# Line 328:
if key.startswith("platforms.") and key.endswith(".enabled") and value is False:

set_config_value declares value: str in its signature (line 5862). When called from the CLI via argparse, value is always a string like "false" or "0", never the Python boolean False. So value is False is always False, and the env cleanup code below it is dead — disabling a platform via hermes config set platforms.telegram.enabled false will never remove the associated env vars from .env.

Suggested fix:

if key.startswith("platforms.") and key.endswith(".enabled") and value.lower() in ("false", "0", "no"):

Why it matters: The whole point of the enabled_explicit + env cleanup feature is to prevent "zombie resurrection" where leftover env vars re-enable a disabled platform. With the current guard, hermes config set platforms.telegram.enabled false writes the YAML flag but leaves TELEGRAM_BOT_TOKEN in .env, so the next gateway restart still auto-enables Telegram via _maybe_enable_from_env.

@porkmagus

Copy link
Copy Markdown
Author

Fixed in commit 810a6c9. Addressed all 9 Copilot review items and the bug reported by @liuhao1024:

  • Changed type to and added guard before the disable check
  • now always masks secrets (short ≤4 chars → all asterisks; longer → first/last 2 chars visible)
  • uses for with spaces/arguments
  • help text corrected to 'List env var names'
  • imported at top of function, not inside loop
  • raises on invalid category instead of silent fallback
  • WhatsApp semantics restored: env-based disable works when YAML doesn't have explicit enabled flag

All 4 modified files pass .

@porkmagus

Copy link
Copy Markdown
Author

Fixed in commit 810a6c9. Addressed all 9 Copilot review items and the value is False bug reported by @liuhao1024:

  • Changed set_config_value type to Union[str, bool, int, float] and added isinstance(value, bool) guard before the disable check
  • env get now always masks secrets (short <=4 chars -> all asterisks; longer -> first/last 2 chars visible)
  • env edit uses shlex.split() for $EDITOR with spaces/arguments
  • env list help text corrected to "List env var names"
  • remove_env_vars_for_feature imported at top of function, not inside loop
  • env_cleanup.py raises ValueError on invalid category instead of silent fallback
  • WhatsApp WHATSAPP_ENABLED=false semantics restored: env-based disable works when YAML doesn't have explicit enabled flag

All 4 modified files pass python3 -m py_compile.

@porkmagus

Copy link
Copy Markdown
Author

@liuhao1024 Fixed the bug you found in commit 810a6c9. Changed type to and added guard. The cleanup now triggers correctly when ✓ Set platforms.telegram.enabled = False in /Users/sean/.hermes/config.yaml is called. Thanks for the catch.

@porkmagus

Copy link
Copy Markdown
Author

@liuhao1024 Fixed the value is False bug you found in commit 810a6c9. Changed set_config_value type to Union[str, bool, int, float] and added isinstance(value, bool) guard. The cleanup now triggers correctly when hermes config set platforms.telegram.enabled false is called. Thanks for the catch.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/config Config system, migrations, profiles comp/gateway Gateway runner, session dispatch, delivery P2 Medium — degraded but workaround exists type/bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants