From ad1aa1a037a0603b09593dfdce1efd8111936c8f Mon Sep 17 00:00:00 2001 From: Teknium <127238744+teknium1@users.noreply.github.com> Date: Sun, 17 May 2026 02:19:38 -0700 Subject: [PATCH] feat(x_search): auto-enable toolset when xAI OAuth or XAI_API_KEY is configured (#27376) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The x_search toolset is gated on xAI credentials (SuperGrok OAuth or XAI_API_KEY), but it was staying off-by-default even for users who had already configured those credentials — they had to also click through `hermes tools` → X (Twitter) Search to flip it on. The HASS_TOKEN → homeassistant rule already handles the parallel case cleanly; x_search needs the same treatment. Why a separate code path from HASS_TOKEN: `ha_*` tools live inside the `hermes-cli` composite, so the subset-inference loop picks them up and the HASS branch just unmasks default_off. `x_search` is its own one-tool toolset NOT in the composite, so the subset loop never adds it — it has to be injected directly. * Add `_xai_credentials_present()` — side-effect-free check for stored xAI OAuth tokens or XAI_API_KEY (dotenv or env). No network. * In `_get_platform_tools()` else branch (no explicit user config), inject `x_search` and carve a parallel hole in default_off. * Auto-enable does NOT fire when the user has saved an explicit toolset list via `hermes tools` — that list stays authoritative. * `agent.disabled_toolsets: [x_search]` still wins (global override). Tests: 4 new in test_tools_config.py covering OAuth path, API-key path, no-creds path, and explicit-config-respect. All pass alongside existing 70/70 in that file. --- hermes_cli/tools_config.py | 58 +++++++++++++++++++++++++-- tests/hermes_cli/test_tools_config.py | 56 ++++++++++++++++++++++++++ 2 files changed, 110 insertions(+), 4 deletions(-) diff --git a/hermes_cli/tools_config.py b/hermes_cli/tools_config.py index 06ba32bea9e..3114ed12a4c 100644 --- a/hermes_cli/tools_config.py +++ b/hermes_cli/tools_config.py @@ -88,12 +88,40 @@ CONFIGURABLE_TOOLSETS = [ # 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). @@ -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), @@ -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, diff --git a/tests/hermes_cli/test_tools_config.py b/tests/hermes_cli/test_tools_config.py index 8a94ce4302f..d6b18f1608a 100644 --- a/tests/hermes_cli/test_tools_config.py +++ b/tests/hermes_cli/test_tools_config.py @@ -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