feat: ungate Tool Gateway — subscription-based access with per-tool opt-in

Replace the HERMES_ENABLE_NOUS_MANAGED_TOOLS env-var feature flag with
subscription-based detection. The Tool Gateway is now available to any
paid Nous subscriber without needing a hidden env var.

Core changes:
- managed_nous_tools_enabled() checks get_nous_auth_status() +
  check_nous_free_tier() instead of an env var
- New use_gateway config flag per tool section (web, tts, browser,
  image_gen) records explicit user opt-in and overrides direct API
  keys at runtime
- New prefers_gateway(section) shared helper in tool_backend_helpers.py
  used by all 4 tool runtimes (web, tts, image gen, browser)

UX flow:
- hermes model: after Nous login/model selection, shows a curses
  prompt listing all gateway-eligible tools with current status.
  User chooses to enable all, enable only unconfigured tools, or skip.
  Defaults to Enable for new users, Skip when direct keys exist.
- hermes tools: provider selection now manages use_gateway flag —
  selecting Nous Subscription sets it, selecting any other provider
  clears it
- hermes status: renamed section to Nous Tool Gateway, added
  free-tier upgrade nudge for logged-in free users
- curses_radiolist: new description parameter for multi-line context
  that survives the screen clear

Runtime behavior:
- Each tool runtime (web_tools, tts_tool, image_generation_tool,
  browser_use) checks prefers_gateway() before falling back to
  direct env-var credentials
- get_nous_subscription_features() respects use_gateway flags,
  suppressing direct credential detection when the user opted in

Removed:
- HERMES_ENABLE_NOUS_MANAGED_TOOLS env var and all references
- apply_nous_provider_defaults() silent TTS auto-set
- get_nous_subscription_explainer_lines() static text
- Override env var warnings (use_gateway handles this properly now)
This commit is contained in:
emozilla 2026-04-16 01:59:51 -04:00 committed by Teknium
parent 25c7b1baa7
commit f188ac74f0
26 changed files with 544 additions and 187 deletions

View file

@ -954,34 +954,49 @@ def _configure_provider(provider: dict, config: dict):
# Set TTS provider in config if applicable
if provider.get("tts_provider"):
config.setdefault("tts", {})["provider"] = provider["tts_provider"]
tts_cfg = config.setdefault("tts", {})
tts_cfg["provider"] = provider["tts_provider"]
tts_cfg["use_gateway"] = bool(managed_feature)
# Set browser cloud provider in config if applicable
if "browser_provider" in provider:
bp = provider["browser_provider"]
browser_cfg = config.setdefault("browser", {})
if bp == "local":
config.setdefault("browser", {})["cloud_provider"] = "local"
browser_cfg["cloud_provider"] = "local"
_print_success(" Browser set to local mode")
elif bp:
config.setdefault("browser", {})["cloud_provider"] = bp
browser_cfg["cloud_provider"] = bp
_print_success(f" Browser cloud provider set to: {bp}")
browser_cfg["use_gateway"] = bool(managed_feature)
# Set web search backend in config if applicable
if provider.get("web_backend"):
config.setdefault("web", {})["backend"] = provider["web_backend"]
web_cfg = config.setdefault("web", {})
web_cfg["backend"] = provider["web_backend"]
web_cfg["use_gateway"] = bool(managed_feature)
_print_success(f" Web backend set to: {provider['web_backend']}")
# For tools without a specific config key (e.g. image_gen), still
# track use_gateway so the runtime knows the user's intent.
if managed_feature and managed_feature not in ("web", "tts", "browser"):
config.setdefault(managed_feature, {})["use_gateway"] = True
elif not managed_feature:
# User picked a non-gateway provider — find which category this
# belongs to and clear use_gateway if it was previously set.
for cat_key, cat in TOOL_CATEGORIES.items():
if provider in cat.get("providers", []):
section = config.get(cat_key)
if isinstance(section, dict) and section.get("use_gateway"):
section["use_gateway"] = False
break
if not env_vars:
if provider.get("post_setup"):
_run_post_setup(provider["post_setup"])
_print_success(f" {provider['name']} - no configuration needed!")
if managed_feature:
_print_info(" Requests for this tool will be billed to your Nous subscription.")
override_envs = provider.get("override_env_vars", [])
if any(get_env_value(env_var) for env_var in override_envs):
_print_warning(
" Direct credentials are still configured and may take precedence until you remove them from ~/.hermes/.env."
)
return
# Prompt for each required env var
@ -1187,11 +1202,6 @@ def _reconfigure_provider(provider: dict, config: dict):
_print_success(f" {provider['name']} - no configuration needed!")
if managed_feature:
_print_info(" Requests for this tool will be billed to your Nous subscription.")
override_envs = provider.get("override_env_vars", [])
if any(get_env_value(env_var) for env_var in override_envs):
_print_warning(
" Direct credentials are still configured and may take precedence until you remove them from ~/.hermes/.env."
)
return
for var in env_vars: