mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-08 13:12:08 +00:00
fix(tools): enable platform-native toolsets when their composite is explicitly configured (#35527)
When a user explicitly configures a platform with its native composite (e.g. platform_toolsets.discord: [hermes-discord]), the discord and discord_admin toolsets were silently stripped by _DEFAULT_OFF_TOOLSETS even though the composite contains those tools. The strip could not tell an explicit composite opt-in apart from the unconfigured default. Track whether the platform was explicitly configured and, when it was, exempt toolsets that are both default-off and platform-restricted to the current platform from the strip. Only discord/discord_admin are affected (the sole entries in both _DEFAULT_OFF_TOOLSETS and _TOOLSET_PLATFORM_RESTRICTIONS). Unconfigured and empty-list platforms keep the security default-off behaviour.
This commit is contained in:
parent
b062083d0a
commit
8a726e91ba
2 changed files with 107 additions and 0 deletions
|
|
@ -1612,6 +1612,28 @@ def enabled_mcp_server_names(config: dict) -> Set[str]:
|
|||
}
|
||||
|
||||
|
||||
def _exempt_explicit_platform_native(
|
||||
default_off: Set[str], platform: str, *, explicitly_configured: bool
|
||||
) -> None:
|
||||
"""Let platform-native default-off toolsets through on explicit config.
|
||||
|
||||
Toolsets that are both in ``_DEFAULT_OFF_TOOLSETS`` and restricted to
|
||||
``platform`` via ``_TOOLSET_PLATFORM_RESTRICTIONS`` (currently
|
||||
``discord``/``discord_admin`` on the discord platform) are the platform's
|
||||
own native tools. They are kept off for *unconfigured* platforms (security
|
||||
opt-in), but once a user explicitly saves a toolset list for the platform
|
||||
the composite they chose (e.g. ``hermes-discord``, which contains those
|
||||
tools) is an opt-in — stripping them silently defeats the explicit
|
||||
configuration (#35527). Mutates ``default_off`` in place.
|
||||
"""
|
||||
if not explicitly_configured:
|
||||
return
|
||||
for ts in list(default_off):
|
||||
allowed = _TOOLSET_PLATFORM_RESTRICTIONS.get(ts)
|
||||
if allowed is not None and platform in allowed:
|
||||
default_off.discard(ts)
|
||||
|
||||
|
||||
def _get_platform_tools(
|
||||
config: dict,
|
||||
platform: str,
|
||||
|
|
@ -1623,6 +1645,11 @@ def _get_platform_tools(
|
|||
|
||||
platform_toolsets = config.get("platform_toolsets") or {}
|
||||
toolset_names = platform_toolsets.get(platform)
|
||||
# Track whether the user explicitly saved a toolset list for this platform
|
||||
# (vs. falling back to the platform default). An explicit composite (e.g.
|
||||
# ``hermes-discord``) is an opt-in to the platform's native default-off
|
||||
# toolsets — see _exempt_explicit_platform_native (#35527).
|
||||
explicitly_configured = isinstance(toolset_names, list)
|
||||
|
||||
if toolset_names is None or not isinstance(toolset_names, list):
|
||||
plat_info = PLATFORMS.get(platform)
|
||||
|
|
@ -1686,6 +1713,9 @@ def _get_platform_tools(
|
|||
default_off.remove(platform)
|
||||
if "homeassistant" in default_off and os.getenv("HASS_TOKEN"):
|
||||
default_off.remove("homeassistant")
|
||||
_exempt_explicit_platform_native(
|
||||
default_off, platform, explicitly_configured=explicitly_configured
|
||||
)
|
||||
expanded -= default_off
|
||||
|
||||
enabled_toolsets |= expanded
|
||||
|
|
@ -1748,6 +1778,9 @@ def _get_platform_tools(
|
|||
# strip the entry we just added.
|
||||
if x_search_auto_enabled and "x_search" in default_off:
|
||||
default_off.remove("x_search")
|
||||
_exempt_explicit_platform_native(
|
||||
default_off, platform, explicitly_configured=explicitly_configured
|
||||
)
|
||||
enabled_toolsets -= default_off
|
||||
|
||||
# Recover non-configurable platform toolsets (e.g. discord, feishu_doc,
|
||||
|
|
|
|||
|
|
@ -243,6 +243,80 @@ def test_get_platform_tools_x_search_auto_enabled_when_xai_oauth_present(monkeyp
|
|||
assert "x_search" in enabled, f"x_search missing for {plat}"
|
||||
|
||||
|
||||
# ─── #35527: platform-restricted default-off toolsets (discord/discord_admin)
|
||||
# are stripped by _DEFAULT_OFF_TOOLSETS even when the user explicitly opts in
|
||||
# via the platform's native composite. The composite ``hermes-discord``
|
||||
# contains both ``discord`` and ``discord_admin`` tools, so configuring it is
|
||||
# an explicit opt-in that should survive the default-off strip. ───────────────
|
||||
|
||||
|
||||
def test_discord_composite_only_enables_discord_toolsets():
|
||||
"""Layer 1: ``platform_toolsets.discord: [hermes-discord]`` is an explicit
|
||||
opt-in to the full Discord bundle (which includes the ``discord`` and
|
||||
``discord_admin`` tools). They must not be silently stripped."""
|
||||
config = {"platform_toolsets": {"discord": ["hermes-discord"]}}
|
||||
enabled = _get_platform_tools(config, "discord")
|
||||
assert "discord" in enabled, "discord toolset missing from hermes-discord composite"
|
||||
assert "discord_admin" in enabled, "discord_admin toolset missing from composite"
|
||||
|
||||
|
||||
def test_discord_composite_plus_configurable_enables_discord_toolsets():
|
||||
"""Layer 2: mixing the composite with a configurable key (e.g. spotify)
|
||||
still opts into the Discord toolsets carried by the composite."""
|
||||
config = {"platform_toolsets": {"discord": ["hermes-discord", "spotify"]}}
|
||||
enabled = _get_platform_tools(config, "discord")
|
||||
assert "discord" in enabled
|
||||
assert "discord_admin" in enabled
|
||||
|
||||
|
||||
def test_discord_composite_plus_partial_explicit_enables_sibling():
|
||||
"""Layer 3: ``[hermes-discord, discord]`` lists discord explicitly but
|
||||
discord_admin arrives only via the composite. Both must survive."""
|
||||
config = {"platform_toolsets": {"discord": ["hermes-discord", "discord"]}}
|
||||
enabled = _get_platform_tools(config, "discord")
|
||||
assert "discord" in enabled
|
||||
assert "discord_admin" in enabled
|
||||
|
||||
|
||||
def test_discord_unconfigured_keeps_discord_toolsets_off():
|
||||
"""Layer 4 (guard): an unconfigured discord platform keeps the platform
|
||||
toolsets OFF by default — explicit configuration is required to opt in."""
|
||||
enabled = _get_platform_tools({}, "discord")
|
||||
assert "discord" not in enabled
|
||||
assert "discord_admin" not in enabled
|
||||
|
||||
|
||||
def test_discord_empty_list_keeps_discord_toolsets_off():
|
||||
"""Layer 4 (guard): an explicit empty list means 'nothing' — the Discord
|
||||
toolsets must not be auto-added even though the fix keys off explicit
|
||||
configuration."""
|
||||
config = {"platform_toolsets": {"discord": []}}
|
||||
enabled = _get_platform_tools(config, "discord")
|
||||
assert "discord" not in enabled
|
||||
assert "discord_admin" not in enabled
|
||||
|
||||
|
||||
def test_discord_toolsets_do_not_leak_to_other_platforms():
|
||||
"""Layer 4 (guard): discord/discord_admin are platform-restricted — they
|
||||
must never appear on a non-discord platform even when that platform is
|
||||
explicitly configured."""
|
||||
config = {"platform_toolsets": {"telegram": ["hermes-telegram", "discord"]}}
|
||||
enabled = _get_platform_tools(config, "telegram")
|
||||
assert "discord" not in enabled
|
||||
assert "discord_admin" not in enabled
|
||||
|
||||
|
||||
def test_discord_explicit_workaround_still_works():
|
||||
"""Regression guard: the documented workaround of listing toolsets
|
||||
explicitly must keep working after the fix."""
|
||||
config = {
|
||||
"platform_toolsets": {"discord": ["hermes-discord", "discord", "discord_admin"]}
|
||||
}
|
||||
enabled = _get_platform_tools(config, "discord")
|
||||
assert "discord" in enabled
|
||||
assert "discord_admin" in enabled
|
||||
|
||||
|
||||
def test_get_platform_tools_x_search_auto_enabled_when_xai_api_key_present(monkeypatch):
|
||||
"""x_search toolset auto-enables when XAI_API_KEY is set, even without
|
||||
OAuth tokens — the API-key path is a supported credential source."""
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue