Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 54 additions & 4 deletions hermes_cli/tools_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,12 +88,40 @@
# who want it opt in via `hermes tools` → Video Generation, which walks
# them through provider + model selection.
#
# X search is off by default — gated on xAI credentials (SuperGrok OAuth
# or XAI_API_KEY). Users opt in via `hermes tools` → X (Twitter) Search,
# which walks them through credential setup. The tool's check_fn means
# the schema won't appear to the model even if enabled without credentials.
# X search is off by default for users without xAI credentials, but
# auto-enables when SuperGrok OAuth tokens are stored OR XAI_API_KEY is
# set — mirroring the HASS_TOKEN → homeassistant auto-enable below. The
# `hermes tools` → X (Twitter) Search setup walks users through credential
# setup. The tool's check_fn means the schema still won't appear to the
# model if the credential later goes missing or expires.
_DEFAULT_OFF_TOOLSETS = {"moa", "homeassistant", "spotify", "discord", "discord_admin", "video", "video_gen", "x_search"}


def _xai_credentials_present() -> bool:
"""Cheap, side-effect-free check for usable xAI credentials.

Used to auto-enable the ``x_search`` toolset when the user has either
completed xAI Grok OAuth (SuperGrok subscription) or set
``XAI_API_KEY``. Does NOT hit the network — only inspects the local
auth store and environment. The tool's runtime ``check_fn`` still
gates schema registration if creds later expire or get revoked.
"""
try:
from hermes_cli.auth import _read_xai_oauth_tokens

_read_xai_oauth_tokens()
return True
except Exception:
pass
try:
from tools.xai_http import get_env_value as _xai_get_env_value

if str(_xai_get_env_value("XAI_API_KEY") or "").strip():
return True
except Exception:
pass
return bool(str(os.environ.get("XAI_API_KEY") or "").strip())

# Platform-scoped toolsets: only appear in the `hermes tools` checklist for
# these platforms, and only resolve/save for these platforms. A toolset
# absent from this map is available on every platform (current behaviour).
Expand Down Expand Up @@ -1129,6 +1157,23 @@ def _get_platform_tools(
if ts_tools and ts_tools.issubset(all_tool_names):
enabled_toolsets.add(ts_key)

# Auto-enable ``x_search`` when xAI credentials are configured.
# Unlike ``homeassistant`` (whose ``ha_*`` tools live inside the
# platform composite and thus pass the subset check above),
# ``x_search`` is its own one-tool toolset that the composite does
# NOT include, so the subset loop never picks it up. Inject it
# directly here, mirroring the HASS_TOKEN → ``homeassistant`` rule
# below: once you have working creds, you don't have to also click
# through ``hermes tools`` to flip the toolset on. Only fires when
# the user has not yet saved an explicit toolset list — once they
# do, the saved list is authoritative.
x_search_auto_enabled = (
_toolset_allowed_for_platform("x_search", platform)
and _xai_credentials_present()
)
if x_search_auto_enabled:
enabled_toolsets.add("x_search")

default_off = set(_DEFAULT_OFF_TOOLSETS)
# Legacy safety: if the platform's own name matches a default-off
# toolset (e.g. `homeassistant` platform + `homeassistant` toolset),
Expand All @@ -1146,6 +1191,11 @@ def _get_platform_tools(
# regressed after #14798 made cron honor per-platform tool config.
if "homeassistant" in default_off and os.getenv("HASS_TOKEN"):
default_off.remove("homeassistant")
# Symmetric carve-out for x_search auto-enable (see the inject
# block above). Without this, the default_off subtraction would
# strip the entry we just added.
if x_search_auto_enabled and "x_search" in default_off:
default_off.remove("x_search")
enabled_toolsets -= default_off

# Recover non-configurable platform toolsets (e.g. discord, feishu_doc,
Expand Down
56 changes: 56 additions & 0 deletions tests/hermes_cli/test_tools_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,62 @@ def test_get_platform_tools_homeassistant_toolset_off_for_cron_when_hass_token_m
assert "homeassistant" not in cron_enabled


def test_get_platform_tools_x_search_auto_enabled_when_xai_oauth_present(monkeypatch):
"""x_search toolset auto-enables across platforms when xAI Grok OAuth
tokens are present, mirroring the HASS_TOKEN → homeassistant rule.

The user already authenticated via SuperGrok OAuth; they shouldn't have
to also click through `hermes tools` → X (Twitter) Search to flip the
toolset on. Tool's check_fn still gates schema registration if creds
later go missing.
"""
monkeypatch.delenv("XAI_API_KEY", raising=False)
monkeypatch.setattr(
"hermes_cli.tools_config._xai_credentials_present", lambda: True
)

for plat in ("cli", "cron", "telegram"):
enabled = _get_platform_tools({}, plat)
assert "x_search" in enabled, f"x_search missing for {plat}"


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."""
monkeypatch.setenv("XAI_API_KEY", "fake-xai-key")

cli_enabled = _get_platform_tools({}, "cli")
assert "x_search" in cli_enabled


def test_get_platform_tools_x_search_off_when_no_xai_credentials(monkeypatch):
"""Without any xAI credentials, x_search stays off — preserves the
"don't ship the schema to users who can't use it" default."""
monkeypatch.delenv("XAI_API_KEY", raising=False)
monkeypatch.setattr(
"hermes_cli.tools_config._xai_credentials_present", lambda: False
)

cli_enabled = _get_platform_tools({}, "cli")
assert "x_search" not in cli_enabled


def test_get_platform_tools_x_search_respects_explicit_config(monkeypatch):
"""Once the user has saved an explicit toolset list via `hermes tools`,
that list is authoritative — x_search auto-enable does NOT fire even
when xAI creds exist. The saved list represents deliberate choices."""
monkeypatch.delenv("XAI_API_KEY", raising=False)
monkeypatch.setattr(
"hermes_cli.tools_config._xai_credentials_present", lambda: True
)

# User explicitly opted into spotify but not x_search via `hermes tools`.
config = {"platform_toolsets": {"cli": ["hermes-cli", "spotify"]}}
enabled = _get_platform_tools(config, "cli")
assert "x_search" not in enabled
assert "spotify" in enabled


def test_get_platform_tools_expands_composite_when_mixed_with_configurable():
"""``[hermes-cli, spotify]`` (composite + configurable) must keep the full
``hermes-cli`` toolset alongside the explicit Spotify opt-in. The
Expand Down
Loading