feat(x_search): auto-enable toolset when xAI OAuth or XAI_API_KEY is configured (#27376)

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.
This commit is contained in:
Teknium 2026-05-17 02:19:38 -07:00 committed by GitHub
parent 519657aa98
commit ad1aa1a037
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 110 additions and 4 deletions

View file

@ -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,

View file

@ -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