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:
Brooklyn Nicholson
2026-05-18 01:26:56 -05:00
260 changed files with 24547 additions and 13573 deletions
+131 -35
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).
@@ -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: