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:
@@ -88,12 +88,40 @@ CONFIGURABLE_TOOLSETS = [
|
|||||||
# who want it opt in via `hermes tools` → Video Generation, which walks
|
# who want it opt in via `hermes tools` → Video Generation, which walks
|
||||||
# them through provider + model selection.
|
# them through provider + model selection.
|
||||||
#
|
#
|
||||||
# X search is off by default — gated on xAI credentials (SuperGrok OAuth
|
# X search is off by default for users without xAI credentials, but
|
||||||
# or XAI_API_KEY). Users opt in via `hermes tools` → X (Twitter) Search,
|
# auto-enables when SuperGrok OAuth tokens are stored OR XAI_API_KEY is
|
||||||
# which walks them through credential setup. The tool's check_fn means
|
# set — mirroring the HASS_TOKEN → homeassistant auto-enable below. The
|
||||||
# the schema won't appear to the model even if enabled without credentials.
|
# `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"}
|
_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
|
# Platform-scoped toolsets: only appear in the `hermes tools` checklist for
|
||||||
# these platforms, and only resolve/save for these platforms. A toolset
|
# these platforms, and only resolve/save for these platforms. A toolset
|
||||||
# absent from this map is available on every platform (current behaviour).
|
# 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):
|
if ts_tools and ts_tools.issubset(all_tool_names):
|
||||||
enabled_toolsets.add(ts_key)
|
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)
|
default_off = set(_DEFAULT_OFF_TOOLSETS)
|
||||||
# Legacy safety: if the platform's own name matches a default-off
|
# Legacy safety: if the platform's own name matches a default-off
|
||||||
# toolset (e.g. `homeassistant` platform + `homeassistant` toolset),
|
# 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.
|
# regressed after #14798 made cron honor per-platform tool config.
|
||||||
if "homeassistant" in default_off and os.getenv("HASS_TOKEN"):
|
if "homeassistant" in default_off and os.getenv("HASS_TOKEN"):
|
||||||
default_off.remove("homeassistant")
|
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
|
enabled_toolsets -= default_off
|
||||||
|
|
||||||
# Recover non-configurable platform toolsets (e.g. discord, feishu_doc,
|
# Recover non-configurable platform toolsets (e.g. discord, feishu_doc,
|
||||||
|
|||||||
@@ -125,6 +125,62 @@ def test_get_platform_tools_homeassistant_toolset_off_for_cron_when_hass_token_m
|
|||||||
assert "homeassistant" not in cron_enabled
|
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():
|
def test_get_platform_tools_expands_composite_when_mixed_with_configurable():
|
||||||
"""``[hermes-cli, spotify]`` (composite + configurable) must keep the full
|
"""``[hermes-cli, spotify]`` (composite + configurable) must keep the full
|
||||||
``hermes-cli`` toolset alongside the explicit Spotify opt-in. The
|
``hermes-cli`` toolset alongside the explicit Spotify opt-in. The
|
||||||
|
|||||||
Reference in New Issue
Block a user