Merge branch 'main' of github.com:NousResearch/hermes-agent into bb/gui
# Conflicts: # cli.py # hermes_cli/main.py # run_agent.py # tests/hermes_cli/test_cmd_update.py # tools/mcp_tool.py # web/src/lib/gatewayClient.ts
This commit is contained in:
+131
-35
@@ -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).
|
||||
@@ -350,6 +378,17 @@ TOOL_CATEGORIES = {
|
||||
"browser": {
|
||||
"name": "Browser Automation",
|
||||
"icon": "🌐",
|
||||
# Per-provider rows for Browserbase, Browser Use, and Firecrawl are
|
||||
# injected at runtime from plugins.browser.<vendor>.provider via
|
||||
# _plugin_browser_providers() in _visible_providers(). Only
|
||||
# non-provider UX setup-flow rows remain here:
|
||||
# - "Nous Subscription (Browser Use cloud)" — managed Browser Use
|
||||
# billed via Nous subscription (requires_nous_auth +
|
||||
# override_env_vars). Uses the browser-use plugin as the
|
||||
# underlying backend but has a distinct setup UX.
|
||||
# - "Local Browser" — non-cloud option, no CloudBrowserProvider.
|
||||
# - "Camofox" — anti-detection local Firefox; short-circuits the
|
||||
# cloud-provider dispatch path via _is_camofox_mode().
|
||||
"providers": [
|
||||
{
|
||||
"name": "Nous Subscription (Browser Use cloud)",
|
||||
@@ -370,37 +409,6 @@ TOOL_CATEGORIES = {
|
||||
"browser_provider": "local",
|
||||
"post_setup": "agent_browser",
|
||||
},
|
||||
{
|
||||
"name": "Browserbase",
|
||||
"badge": "paid",
|
||||
"tag": "Cloud browser with stealth and proxies",
|
||||
"env_vars": [
|
||||
{"key": "BROWSERBASE_API_KEY", "prompt": "Browserbase API key", "url": "https://browserbase.com"},
|
||||
{"key": "BROWSERBASE_PROJECT_ID", "prompt": "Browserbase project ID"},
|
||||
],
|
||||
"browser_provider": "browserbase",
|
||||
"post_setup": "agent_browser",
|
||||
},
|
||||
{
|
||||
"name": "Browser Use",
|
||||
"badge": "paid",
|
||||
"tag": "Cloud browser with remote execution",
|
||||
"env_vars": [
|
||||
{"key": "BROWSER_USE_API_KEY", "prompt": "Browser Use API key", "url": "https://browser-use.com"},
|
||||
],
|
||||
"browser_provider": "browser-use",
|
||||
"post_setup": "agent_browser",
|
||||
},
|
||||
{
|
||||
"name": "Firecrawl",
|
||||
"badge": "paid",
|
||||
"tag": "Cloud browser with remote execution",
|
||||
"env_vars": [
|
||||
{"key": "FIRECRAWL_API_KEY", "prompt": "Firecrawl API key", "url": "https://firecrawl.dev"},
|
||||
],
|
||||
"browser_provider": "firecrawl",
|
||||
"post_setup": "agent_browser",
|
||||
},
|
||||
{
|
||||
"name": "Camofox",
|
||||
"badge": "free · local",
|
||||
@@ -1170,6 +1178,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),
|
||||
@@ -1187,6 +1212,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,
|
||||
@@ -1653,6 +1683,61 @@ def _plugin_web_search_providers() -> list[dict]:
|
||||
return rows
|
||||
|
||||
|
||||
# Mirror of _plugin_web_search_providers for cloud browser backends. After
|
||||
# PR #25214, Browserbase / Browser Use / Firecrawl live as plugins under
|
||||
# plugins/browser/<vendor>/; this helper is the sole source of provider rows
|
||||
# for those three in the "Browser Automation" picker. The hardcoded
|
||||
# ``TOOL_CATEGORIES["browser"]`` entries that drove the category before
|
||||
# were deleted in the same PR; only non-provider UX setup-flow rows remain
|
||||
# ("Nous Subscription", "Local Browser", "Camofox") — see the comment block
|
||||
# in ``TOOL_CATEGORIES["browser"]`` for why each one stays hardcoded.
|
||||
def _plugin_browser_providers() -> list[dict]:
|
||||
"""Build picker-row dicts from plugin-registered cloud browser providers.
|
||||
|
||||
Each returned dict mirrors the legacy ``TOOL_CATEGORIES["browser"]``
|
||||
schema (``name`` / ``badge`` / ``tag`` / ``env_vars`` /
|
||||
``browser_provider`` / ``post_setup``) so the picker behaves identically
|
||||
whether a provider was hardcoded or plugin-registered.
|
||||
|
||||
Populates ``browser_provider`` (the legacy config key written to
|
||||
``browser.cloud_provider``) and a ``browser_plugin_name`` marker so
|
||||
setup / write paths can route through the registry when they want to.
|
||||
"""
|
||||
try:
|
||||
from agent.browser_registry import list_providers as _list_browser_providers
|
||||
from hermes_cli.plugins import _ensure_plugins_discovered
|
||||
|
||||
_ensure_plugins_discovered()
|
||||
providers = _list_browser_providers()
|
||||
except Exception:
|
||||
return []
|
||||
|
||||
rows: list[dict] = []
|
||||
for provider in providers:
|
||||
name = getattr(provider, "name", None)
|
||||
if not name:
|
||||
continue
|
||||
try:
|
||||
schema = provider.get_setup_schema()
|
||||
except Exception:
|
||||
continue
|
||||
if not isinstance(schema, dict):
|
||||
continue
|
||||
row = {
|
||||
"name": schema.get("name", provider.display_name),
|
||||
"badge": schema.get("badge", ""),
|
||||
"tag": schema.get("tag", ""),
|
||||
"env_vars": schema.get("env_vars", []),
|
||||
"browser_provider": name,
|
||||
"browser_plugin_name": name,
|
||||
}
|
||||
# Pass-through optional fields the schema can opt into.
|
||||
if schema.get("post_setup"):
|
||||
row["post_setup"] = schema["post_setup"]
|
||||
rows.append(row)
|
||||
return rows
|
||||
|
||||
|
||||
def _visible_providers(cat: dict, config: dict) -> list[dict]:
|
||||
"""Return provider entries visible for the current auth/config state."""
|
||||
features = get_nous_subscription_features(config)
|
||||
@@ -1682,6 +1767,14 @@ def _visible_providers(cat: dict, config: dict) -> list[dict]:
|
||||
if cat.get("name") == "Web Search & Extract":
|
||||
visible.extend(_plugin_web_search_providers())
|
||||
|
||||
# Inject plugin-registered cloud browser backends. After PR #25214,
|
||||
# Browserbase / Browser Use / Firecrawl are the plugin-supplied rows;
|
||||
# the hardcoded "Nous Subscription" / "Local Browser" / "Camofox" rows
|
||||
# stay because they're non-provider UX setup flows (subscription auth,
|
||||
# local fallback, and the REST-API anti-detection backend respectively).
|
||||
if cat.get("name") == "Browser Automation":
|
||||
visible.extend(_plugin_browser_providers())
|
||||
|
||||
return visible
|
||||
|
||||
|
||||
@@ -2590,6 +2683,9 @@ def _reconfigure_provider(provider: dict, config: dict):
|
||||
else:
|
||||
_print_info(" Kept current")
|
||||
|
||||
if provider.get("post_setup"):
|
||||
_run_post_setup(provider["post_setup"])
|
||||
|
||||
# Imagegen backends prompt for model selection on reconfig too.
|
||||
plugin_name = provider.get("image_gen_plugin_name")
|
||||
if plugin_name:
|
||||
|
||||
Reference in New Issue
Block a user