2026-03-26 15:27:27 -07:00
|
|
|
"""Shared helpers for tool backend selection."""
|
|
|
|
|
|
|
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
|
|
|
import os
|
|
|
|
|
from pathlib import Path
|
2026-03-31 08:48:54 +09:00
|
|
|
from typing import Any, Dict
|
2026-03-26 15:27:27 -07:00
|
|
|
|
|
|
|
|
|
|
|
|
|
_DEFAULT_BROWSER_PROVIDER = "local"
|
|
|
|
|
_DEFAULT_MODAL_MODE = "auto"
|
|
|
|
|
_VALID_MODAL_MODES = {"auto", "direct", "managed"}
|
|
|
|
|
|
|
|
|
|
|
2026-03-30 13:28:10 +09:00
|
|
|
def managed_nous_tools_enabled() -> bool:
|
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)
2026-04-16 01:59:51 -04:00
|
|
|
"""Return True when the user has an active paid Nous subscription.
|
|
|
|
|
|
|
|
|
|
The Tool Gateway is available to any Nous subscriber who is NOT on
|
|
|
|
|
the free tier. We intentionally catch all exceptions and return
|
|
|
|
|
False — never block the agent startup path.
|
|
|
|
|
"""
|
|
|
|
|
try:
|
|
|
|
|
from hermes_cli.auth import get_nous_auth_status
|
|
|
|
|
|
|
|
|
|
status = get_nous_auth_status()
|
|
|
|
|
if not status.get("logged_in"):
|
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
from hermes_cli.models import check_nous_free_tier
|
|
|
|
|
|
|
|
|
|
if check_nous_free_tier():
|
|
|
|
|
return False # free-tier users don't get gateway access
|
|
|
|
|
return True
|
|
|
|
|
except Exception:
|
|
|
|
|
return False
|
2026-03-30 13:28:10 +09:00
|
|
|
|
|
|
|
|
|
2026-03-26 15:27:27 -07:00
|
|
|
def normalize_browser_cloud_provider(value: object | None) -> str:
|
|
|
|
|
"""Return a normalized browser provider key."""
|
|
|
|
|
provider = str(value or _DEFAULT_BROWSER_PROVIDER).strip().lower()
|
|
|
|
|
return provider or _DEFAULT_BROWSER_PROVIDER
|
|
|
|
|
|
|
|
|
|
|
2026-03-30 13:28:10 +09:00
|
|
|
def coerce_modal_mode(value: object | None) -> str:
|
|
|
|
|
"""Return the requested modal mode when valid, else the default."""
|
2026-03-26 15:27:27 -07:00
|
|
|
mode = str(value or _DEFAULT_MODAL_MODE).strip().lower()
|
|
|
|
|
if mode in _VALID_MODAL_MODES:
|
|
|
|
|
return mode
|
|
|
|
|
return _DEFAULT_MODAL_MODE
|
|
|
|
|
|
|
|
|
|
|
2026-03-30 13:28:10 +09:00
|
|
|
def normalize_modal_mode(value: object | None) -> str:
|
|
|
|
|
"""Return a normalized modal execution mode."""
|
2026-03-31 08:48:54 +09:00
|
|
|
return coerce_modal_mode(value)
|
2026-03-30 13:28:10 +09:00
|
|
|
|
|
|
|
|
|
2026-03-26 15:27:27 -07:00
|
|
|
def has_direct_modal_credentials() -> bool:
|
|
|
|
|
"""Return True when direct Modal credentials/config are available."""
|
|
|
|
|
return bool(
|
|
|
|
|
(os.getenv("MODAL_TOKEN_ID") and os.getenv("MODAL_TOKEN_SECRET"))
|
|
|
|
|
or (Path.home() / ".modal.toml").exists()
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
2026-03-31 08:48:54 +09:00
|
|
|
def resolve_modal_backend_state(
|
|
|
|
|
modal_mode: object | None,
|
|
|
|
|
*,
|
|
|
|
|
has_direct: bool,
|
|
|
|
|
managed_ready: bool,
|
|
|
|
|
) -> Dict[str, Any]:
|
|
|
|
|
"""Resolve direct vs managed Modal backend selection.
|
|
|
|
|
|
|
|
|
|
Semantics:
|
|
|
|
|
- ``direct`` means direct-only
|
|
|
|
|
- ``managed`` means managed-only
|
|
|
|
|
- ``auto`` prefers managed when available, then falls back to direct
|
|
|
|
|
"""
|
|
|
|
|
requested_mode = coerce_modal_mode(modal_mode)
|
|
|
|
|
normalized_mode = normalize_modal_mode(modal_mode)
|
|
|
|
|
managed_mode_blocked = (
|
|
|
|
|
requested_mode == "managed" and not managed_nous_tools_enabled()
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
if normalized_mode == "managed":
|
|
|
|
|
selected_backend = "managed" if managed_nous_tools_enabled() and managed_ready else None
|
|
|
|
|
elif normalized_mode == "direct":
|
|
|
|
|
selected_backend = "direct" if has_direct else None
|
|
|
|
|
else:
|
|
|
|
|
selected_backend = "managed" if managed_nous_tools_enabled() and managed_ready else "direct" if has_direct else None
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
"requested_mode": requested_mode,
|
|
|
|
|
"mode": normalized_mode,
|
|
|
|
|
"has_direct": has_direct,
|
|
|
|
|
"managed_ready": managed_ready,
|
|
|
|
|
"managed_mode_blocked": managed_mode_blocked,
|
|
|
|
|
"selected_backend": selected_backend,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
2026-03-26 15:27:27 -07:00
|
|
|
def resolve_openai_audio_api_key() -> str:
|
|
|
|
|
"""Prefer the voice-tools key, but fall back to the normal OpenAI key."""
|
|
|
|
|
return (
|
|
|
|
|
os.getenv("VOICE_TOOLS_OPENAI_KEY", "")
|
|
|
|
|
or os.getenv("OPENAI_API_KEY", "")
|
|
|
|
|
).strip()
|
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)
2026-04-16 01:59:51 -04:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def prefers_gateway(config_section: str) -> bool:
|
|
|
|
|
"""Return True when the user opted into the Tool Gateway for this tool.
|
|
|
|
|
|
|
|
|
|
Reads ``<section>.use_gateway`` from config.yaml. Never raises.
|
|
|
|
|
"""
|
|
|
|
|
try:
|
|
|
|
|
from hermes_cli.config import load_config
|
|
|
|
|
section = (load_config() or {}).get(config_section)
|
|
|
|
|
if isinstance(section, dict):
|
|
|
|
|
return bool(section.get("use_gateway"))
|
|
|
|
|
except Exception:
|
|
|
|
|
pass
|
|
|
|
|
return False
|
2026-04-21 01:59:15 -07:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def fal_key_is_configured() -> bool:
|
|
|
|
|
"""Return True when FAL_KEY is set to a non-whitespace value.
|
|
|
|
|
|
|
|
|
|
Consults both ``os.environ`` and ``~/.hermes/.env`` (via
|
|
|
|
|
``hermes_cli.config.get_env_value`` when available) so tool-side
|
|
|
|
|
checks and CLI setup-time checks agree. A whitespace-only value
|
|
|
|
|
is treated as unset everywhere.
|
|
|
|
|
"""
|
|
|
|
|
value = os.getenv("FAL_KEY")
|
|
|
|
|
if value is None:
|
|
|
|
|
# Fall back to the .env file for CLI paths that may run before
|
|
|
|
|
# dotenv is loaded into os.environ.
|
|
|
|
|
try:
|
|
|
|
|
from hermes_cli.config import get_env_value
|
|
|
|
|
|
|
|
|
|
value = get_env_value("FAL_KEY")
|
|
|
|
|
except Exception:
|
|
|
|
|
value = None
|
|
|
|
|
return bool(value and value.strip())
|