fix(auth): refresh Nous entitlement in tool menus

This commit is contained in:
Robin Fernandes 2026-05-28 09:53:22 +10:00 committed by Teknium
parent 406901b27d
commit 1cf5e639b3
8 changed files with 304 additions and 108 deletions

View file

@ -228,6 +228,8 @@ def _resolve_browser_feature_state(
def get_nous_subscription_features(
config: Optional[Dict[str, object]] = None,
*,
force_fresh: bool = False,
) -> NousSubscriptionFeatures:
if config is None:
config = load_config() or {}
@ -236,7 +238,10 @@ def get_nous_subscription_features(
provider_is_nous = str(model_cfg.get("provider") or "").strip().lower() == "nous"
try:
account_info = get_nous_portal_account_info()
if force_fresh:
account_info = get_nous_portal_account_info(force_fresh=True)
else:
account_info = get_nous_portal_account_info()
except Exception:
account_info = None
@ -322,6 +327,7 @@ def get_nous_subscription_features(
modal_mode,
has_direct=direct_modal,
managed_ready=managed_modal_available,
managed_enabled=managed_tools_flag,
)
web_managed = web_backend == "firecrawl" and managed_web_available and not direct_firecrawl
@ -499,11 +505,15 @@ def apply_nous_managed_defaults(
config: Dict[str, object],
*,
enabled_toolsets: Optional[Iterable[str]] = None,
force_fresh: bool = False,
) -> set[str]:
if not managed_nous_tools_enabled():
features = get_nous_subscription_features(config, force_fresh=force_fresh)
if not (
features.account_info
and features.account_info.logged_in
and features.account_info.paid_service_access is True
):
return set()
features = get_nous_subscription_features(config)
if not features.provider_is_nous:
return set()
@ -600,6 +610,8 @@ _ALL_GATEWAY_KEYS = ("web", "image_gen", "tts", "browser")
def get_gateway_eligible_tools(
config: Optional[Dict[str, object]] = None,
*,
force_fresh: bool = False,
) -> tuple[list[str], list[str], list[str]]:
"""Return (unconfigured, has_direct, already_managed) tool key lists.
@ -610,7 +622,11 @@ def get_gateway_eligible_tools(
All lists are empty when the user is not a paid Nous subscriber or
is not using Nous as their provider.
"""
if not managed_nous_tools_enabled():
if force_fresh:
managed_enabled = managed_nous_tools_enabled(force_fresh=True)
else:
managed_enabled = managed_nous_tools_enabled()
if not managed_enabled:
return [], [], []
if config is None:
@ -701,7 +717,11 @@ def apply_gateway_defaults(
return changed
def prompt_enable_tool_gateway(config: Dict[str, object]) -> set[str]:
def prompt_enable_tool_gateway(
config: Dict[str, object],
*,
force_fresh: bool = True,
) -> set[str]:
"""If eligible tools exist, prompt the user to enable the Tool Gateway.
Uses prompt_choice() with a description parameter so the curses TUI
@ -710,7 +730,10 @@ def prompt_enable_tool_gateway(config: Dict[str, object]) -> set[str]:
Returns the set of tools that were enabled, or empty set if the user
declined or no tools were eligible.
"""
unconfigured, has_direct, already_managed = get_gateway_eligible_tools(config)
unconfigured, has_direct, already_managed = get_gateway_eligible_tools(
config,
force_fresh=force_fresh,
)
if not unconfigured and not has_direct:
return set()

View file

@ -29,7 +29,7 @@ from hermes_cli.nous_subscription import (
get_nous_subscription_features,
)
from hermes_cli.nous_account import format_nous_portal_entitlement_message
from tools.tool_backend_helpers import fal_key_is_configured, managed_nous_tools_enabled
from tools.tool_backend_helpers import fal_key_is_configured
from utils import base_url_hostname, is_truthy_value
logger = logging.getLogger(__name__)
@ -1400,7 +1400,12 @@ def _save_platform_tools(config: dict, platform: str, enabled_toolset_keys: Set[
save_config(config)
def _toolset_has_keys(ts_key: str, config: dict = None) -> bool:
def _toolset_has_keys(
ts_key: str,
config: dict = None,
*,
force_fresh: bool = False,
) -> bool:
"""Check if a toolset's required API keys are configured."""
if config is None:
config = load_config()
@ -1415,7 +1420,7 @@ def _toolset_has_keys(ts_key: str, config: dict = None) -> bool:
return False
if ts_key in {"web", "image_gen", "tts", "browser"}:
features = get_nous_subscription_features(config)
features = get_nous_subscription_features(config, force_fresh=force_fresh)
feature = features.features.get(ts_key)
if feature and (feature.available or feature.managed_by_nous):
return True
@ -1423,7 +1428,7 @@ def _toolset_has_keys(ts_key: str, config: dict = None) -> bool:
# Check TOOL_CATEGORIES first (provider-aware)
cat = TOOL_CATEGORIES.get(ts_key)
if cat:
for provider in _visible_providers(cat, config):
for provider in _visible_providers(cat, config, force_fresh=force_fresh):
env_vars = provider.get("env_vars", [])
if not env_vars:
return True # No-key provider (e.g. Local Browser, Edge TTS)
@ -1494,7 +1499,13 @@ def _estimate_tool_tokens() -> Dict[str, int]:
return _tool_token_cache
def _prompt_toolset_checklist(platform_label: str, enabled: Set[str], platform: str = "cli") -> Set[str]:
def _prompt_toolset_checklist(
platform_label: str,
enabled: Set[str],
platform: str = "cli",
*,
force_fresh: bool = True,
) -> Set[str]:
"""Multi-select checklist of toolsets. Returns set of selected toolset keys."""
from hermes_cli.curses_ui import curses_checklist
from toolsets import resolve_toolset
@ -1512,7 +1523,10 @@ def _prompt_toolset_checklist(platform_label: str, enabled: Set[str], platform:
labels = []
for ts_key, ts_label, ts_desc in effective:
suffix = ""
if not _toolset_has_keys(ts_key) and (TOOL_CATEGORIES.get(ts_key) or TOOLSET_ENV_REQUIREMENTS.get(ts_key)):
if (
not _toolset_has_keys(ts_key, force_fresh=force_fresh)
and (TOOL_CATEGORIES.get(ts_key) or TOOLSET_ENV_REQUIREMENTS.get(ts_key))
):
suffix = " [no API key]"
labels.append(f"{ts_label} ({ts_desc}){suffix}")
@ -1548,7 +1562,12 @@ def _prompt_toolset_checklist(platform_label: str, enabled: Set[str], platform:
# ─── Provider-Aware Configuration ────────────────────────────────────────────
def _configure_toolset(ts_key: str, config: dict):
def _configure_toolset(
ts_key: str,
config: dict,
*,
force_fresh: bool = True,
):
"""Configure a toolset - provider selection + API keys.
Uses TOOL_CATEGORIES for provider-aware config, falls back to simple
@ -1557,7 +1576,7 @@ def _configure_toolset(ts_key: str, config: dict):
cat = TOOL_CATEGORIES.get(ts_key)
if cat:
_configure_tool_category(ts_key, cat, config)
_configure_tool_category(ts_key, cat, config, force_fresh=force_fresh)
else:
# Simple fallback for vision, moa, etc.
_configure_simple_requirements(ts_key)
@ -1810,12 +1829,22 @@ def _plugin_tts_providers() -> list[dict]:
return rows
def _visible_providers(cat: dict, config: dict) -> list[dict]:
def _visible_providers(
cat: dict,
config: dict,
*,
force_fresh: bool = False,
) -> list[dict]:
"""Return provider entries visible for the current auth/config state."""
features = get_nous_subscription_features(config)
features = get_nous_subscription_features(config, force_fresh=force_fresh)
managed_available = bool(
features.account_info
and features.account_info.logged_in
and features.account_info.paid_service_access is True
)
visible = []
for provider in cat.get("providers", []):
if provider.get("managed_nous_feature") and not managed_nous_tools_enabled():
if provider.get("managed_nous_feature") and not managed_available:
continue
if provider.get("requires_nous_auth") and not features.nous_auth_present:
continue
@ -1856,13 +1885,24 @@ def _visible_providers(cat: dict, config: dict) -> list[dict]:
return visible
def _hidden_nous_gateway_message(cat: dict, config: dict, capability: str) -> str:
def _hidden_nous_gateway_message(
cat: dict,
config: dict,
capability: str,
*,
force_fresh: bool = False,
) -> str:
"""Return a reason when a category's Nous provider is hidden."""
if managed_nous_tools_enabled():
features = get_nous_subscription_features(config, force_fresh=force_fresh)
managed_available = bool(
features.account_info
and features.account_info.logged_in
and features.account_info.paid_service_access is True
)
if managed_available:
return ""
if not any(p.get("managed_nous_feature") for p in cat.get("providers", [])):
return ""
features = get_nous_subscription_features(config)
message = format_nous_portal_entitlement_message(
features.account_info,
capability=capability,
@ -1901,17 +1941,22 @@ def _post_setup_already_installed(post_setup_key: str) -> bool:
return True
def _toolset_needs_configuration_prompt(ts_key: str, config: dict) -> bool:
def _toolset_needs_configuration_prompt(
ts_key: str,
config: dict,
*,
force_fresh: bool = False,
) -> bool:
"""Return True when enabling this toolset should open provider setup."""
cat = TOOL_CATEGORIES.get(ts_key)
if not cat:
return not _toolset_has_keys(ts_key, config)
return not _toolset_has_keys(ts_key, config, force_fresh=force_fresh)
# If any visible provider has a registered post_setup install-state
# check that hasn't been satisfied (e.g. cua-driver binary not on
# PATH yet), force the configuration flow so `_configure_provider`
# invokes `_run_post_setup` and the install actually runs.
for provider in _visible_providers(cat, config):
for provider in _visible_providers(cat, config, force_fresh=force_fresh):
post_setup = provider.get("post_setup")
if post_setup and not _post_setup_already_installed(post_setup):
return True
@ -1962,18 +2007,25 @@ def _toolset_needs_configuration_prompt(ts_key: str, config: dict) -> bool:
pass
return True
return not _toolset_has_keys(ts_key, config)
return not _toolset_has_keys(ts_key, config, force_fresh=force_fresh)
def _configure_tool_category(ts_key: str, cat: dict, config: dict):
def _configure_tool_category(
ts_key: str,
cat: dict,
config: dict,
*,
force_fresh: bool = True,
):
"""Configure a tool category with provider selection."""
icon = cat.get("icon", "")
name = cat["name"]
providers = _visible_providers(cat, config)
providers = _visible_providers(cat, config, force_fresh=force_fresh)
hidden_nous_message = _hidden_nous_gateway_message(
cat,
config,
f"the Nous Subscription provider for {name}",
force_fresh=force_fresh,
)
# Check Python version requirement
@ -1998,7 +2050,7 @@ def _configure_tool_category(ts_key: str, cat: dict, config: dict):
if hidden_nous_message:
for line in hidden_nous_message.splitlines():
_print_warning(f" {line}")
_configure_provider(provider, config)
_configure_provider(provider, config, force_fresh=force_fresh)
else:
# Multiple providers - let user choose
print()
@ -2018,7 +2070,10 @@ def _configure_tool_category(ts_key: str, cat: dict, config: dict):
# obvious which options cost extra vs. cost nothing on top of Nous.
try:
_nous_logged_in = bool(
get_nous_subscription_features(config).nous_auth_present
get_nous_subscription_features(
config,
force_fresh=force_fresh,
).nous_auth_present
)
except Exception:
_nous_logged_in = False
@ -2030,7 +2085,7 @@ def _configure_tool_category(ts_key: str, cat: dict, config: dict):
configured = ""
env_vars = p.get("env_vars", [])
if not env_vars or all(get_env_value(v["key"]) for v in env_vars):
if _is_provider_active(p, config):
if _is_provider_active(p, config, force_fresh=force_fresh):
configured = " [active]"
elif not env_vars:
configured = ""
@ -2050,7 +2105,11 @@ def _configure_tool_category(ts_key: str, cat: dict, config: dict):
provider_choices.append("Skip — keep defaults / configure later")
# Detect current provider as default
default_idx = _detect_active_provider_index(providers, config)
default_idx = _detect_active_provider_index(
providers,
config,
force_fresh=force_fresh,
)
provider_idx = _prompt_choice(f" {title}:", provider_choices, default_idx)
@ -2059,10 +2118,15 @@ def _configure_tool_category(ts_key: str, cat: dict, config: dict):
_print_info(f" Skipped {name}")
return
_configure_provider(providers[provider_idx], config)
_configure_provider(providers[provider_idx], config, force_fresh=force_fresh)
def _is_provider_active(provider: dict, config: dict) -> bool:
def _is_provider_active(
provider: dict,
config: dict,
*,
force_fresh: bool = False,
) -> bool:
"""Check if a provider entry matches the currently active config."""
plugin_name = provider.get("image_gen_plugin_name")
if plugin_name:
@ -2076,7 +2140,7 @@ def _is_provider_active(provider: dict, config: dict) -> bool:
managed_feature = provider.get("managed_nous_feature")
if managed_feature:
features = get_nous_subscription_features(config)
features = get_nous_subscription_features(config, force_fresh=force_fresh)
feature = features.features.get(managed_feature)
if feature is None:
return False
@ -2123,10 +2187,15 @@ def _is_provider_active(provider: dict, config: dict) -> bool:
return False
def _detect_active_provider_index(providers: list, config: dict) -> int:
def _detect_active_provider_index(
providers: list,
config: dict,
*,
force_fresh: bool = False,
) -> int:
"""Return the index of the currently active provider, or 0."""
for i, p in enumerate(providers):
if _is_provider_active(p, config):
if _is_provider_active(p, config, force_fresh=force_fresh):
return i
# Fallback: env vars present → likely configured
env_vars = p.get("env_vars", [])
@ -2429,13 +2498,18 @@ def _select_plugin_video_gen_provider(plugin_name: str, config: dict) -> None:
_configure_videogen_model_for_plugin(plugin_name, config)
def _configure_provider(provider: dict, config: dict):
def _configure_provider(
provider: dict,
config: dict,
*,
force_fresh: bool = True,
):
"""Configure a single provider - prompt for API keys and set config."""
env_vars = provider.get("env_vars", [])
managed_feature = provider.get("managed_nous_feature")
if provider.get("requires_nous_auth"):
features = get_nous_subscription_features(config)
features = get_nous_subscription_features(config, force_fresh=force_fresh)
entitled = bool(
features.account_info and features.account_info.paid_service_access is True
)
@ -2536,7 +2610,10 @@ def _configure_provider(provider: dict, config: dict):
_has_managed_sibling = True
break
if _has_managed_sibling:
_features = get_nous_subscription_features(config)
_features = get_nous_subscription_features(
config,
force_fresh=force_fresh,
)
_show_portal_hint = not _features.nous_auth_present
except Exception:
_show_portal_hint = False
@ -2654,7 +2731,11 @@ def _configure_simple_requirements(ts_key: str):
_print_warning(" Skipped")
def _reconfigure_tool(config: dict):
def _reconfigure_tool(
config: dict,
*,
force_fresh: bool = True,
):
"""Let user reconfigure an existing tool's provider or API key."""
# Build list of configurable tools that are currently set up
configurable = []
@ -2662,7 +2743,10 @@ def _reconfigure_tool(config: dict):
cat = TOOL_CATEGORIES.get(ts_key)
reqs = TOOLSET_ENV_REQUIREMENTS.get(ts_key)
if cat or reqs:
if _toolset_has_keys(ts_key, config) or _toolset_enabled_for_reconfigure(ts_key, config):
if (
_toolset_has_keys(ts_key, config, force_fresh=force_fresh)
or _toolset_enabled_for_reconfigure(ts_key, config)
):
configurable.append((ts_key, ts_label))
if not configurable:
@ -2681,7 +2765,12 @@ def _reconfigure_tool(config: dict):
cat = TOOL_CATEGORIES.get(ts_key)
if cat:
_configure_tool_category_for_reconfig(ts_key, cat, config)
_configure_tool_category_for_reconfig(
ts_key,
cat,
config,
force_fresh=force_fresh,
)
else:
_reconfigure_simple_requirements(ts_key)
@ -2710,15 +2799,22 @@ def _toolset_enabled_for_reconfigure(ts_key: str, config: dict) -> bool:
return False
def _configure_tool_category_for_reconfig(ts_key: str, cat: dict, config: dict):
def _configure_tool_category_for_reconfig(
ts_key: str,
cat: dict,
config: dict,
*,
force_fresh: bool = True,
):
"""Reconfigure a tool category - provider selection + API key update."""
icon = cat.get("icon", "")
name = cat["name"]
providers = _visible_providers(cat, config)
providers = _visible_providers(cat, config, force_fresh=force_fresh)
hidden_nous_message = _hidden_nous_gateway_message(
cat,
config,
f"the Nous Subscription provider for {name}",
force_fresh=force_fresh,
)
if len(providers) == 1:
@ -2728,7 +2824,7 @@ def _configure_tool_category_for_reconfig(ts_key: str, cat: dict, config: dict):
if hidden_nous_message:
for line in hidden_nous_message.splitlines():
_print_warning(f" {line}")
_reconfigure_provider(provider, config)
_reconfigure_provider(provider, config, force_fresh=force_fresh)
else:
print()
print(color(f" --- {icon} {name} - Choose a provider ---", Colors.CYAN))
@ -2744,7 +2840,7 @@ def _configure_tool_category_for_reconfig(ts_key: str, cat: dict, config: dict):
configured = ""
env_vars = p.get("env_vars", [])
if not env_vars or all(get_env_value(v["key"]) for v in env_vars):
if _is_provider_active(p, config):
if _is_provider_active(p, config, force_fresh=force_fresh):
configured = " [active]"
elif not env_vars:
configured = ""
@ -2752,19 +2848,32 @@ def _configure_tool_category_for_reconfig(ts_key: str, cat: dict, config: dict):
configured = " [configured]"
provider_choices.append(f"{p['name']}{badge}{tag}{configured}")
default_idx = _detect_active_provider_index(providers, config)
default_idx = _detect_active_provider_index(
providers,
config,
force_fresh=force_fresh,
)
provider_idx = _prompt_choice(" Select provider:", provider_choices, default_idx)
_reconfigure_provider(providers[provider_idx], config)
_reconfigure_provider(
providers[provider_idx],
config,
force_fresh=force_fresh,
)
def _reconfigure_provider(provider: dict, config: dict):
def _reconfigure_provider(
provider: dict,
config: dict,
*,
force_fresh: bool = True,
):
"""Reconfigure a provider - update API keys."""
env_vars = provider.get("env_vars", [])
managed_feature = provider.get("managed_nous_feature")
if provider.get("requires_nous_auth"):
features = get_nous_subscription_features(config)
features = get_nous_subscription_features(config, force_fresh=force_fresh)
entitled = bool(
features.account_info and features.account_info.paid_service_access is True
)
@ -2976,11 +3085,11 @@ def tools_command(args=None, first_install: bool = False, config: dict = None):
auto_configured = apply_nous_managed_defaults(
config,
enabled_toolsets=new_enabled,
force_fresh=True,
)
if managed_nous_tools_enabled():
for ts_key in sorted(auto_configured):
label = next((l for k, l, _ in CONFIGURABLE_TOOLSETS if k == ts_key), ts_key)
print(color(f"{label}: using your Nous subscription defaults", Colors.GREEN))
for ts_key in sorted(auto_configured):
label = next((l for k, l, _ in CONFIGURABLE_TOOLSETS if k == ts_key), ts_key)
print(color(f"{label}: using your Nous subscription defaults", Colors.GREEN))
# Walk through ALL selected tools that have provider options or
# need API keys. This ensures browser (Local vs Browserbase),
@ -3048,7 +3157,7 @@ def tools_command(args=None, first_install: bool = False, config: dict = None):
# "Reconfigure" selected
if idx == _reconfig_idx:
_reconfigure_tool(config)
_reconfigure_tool(config, force_fresh=True)
print()
continue
@ -3064,7 +3173,11 @@ def tools_command(args=None, first_install: bool = False, config: dict = None):
all_current = set()
for pk in platform_keys:
all_current |= _get_platform_tools(config, pk, include_default_mcp_servers=False)
new_enabled = _prompt_toolset_checklist("All platforms", all_current)
new_enabled = _prompt_toolset_checklist(
"All platforms",
all_current,
force_fresh=True,
)
if new_enabled != all_current:
for pk in platform_keys:
prev = _get_platform_tools(config, pk, include_default_mcp_servers=False)
@ -3082,7 +3195,11 @@ def tools_command(args=None, first_install: bool = False, config: dict = None):
# Configure API keys for newly enabled tools
for ts_key in sorted(added):
if (TOOL_CATEGORIES.get(ts_key) or TOOLSET_ENV_REQUIREMENTS.get(ts_key)):
if _toolset_needs_configuration_prompt(ts_key, config):
if _toolset_needs_configuration_prompt(
ts_key,
config,
force_fresh=True,
):
_configure_toolset(ts_key, config)
_save_platform_tools(config, pk, new_enabled)
save_config(config)
@ -3104,7 +3221,11 @@ def tools_command(args=None, first_install: bool = False, config: dict = None):
current_enabled = _get_platform_tools(config, pkey, include_default_mcp_servers=False)
# Show checklist
new_enabled = _prompt_toolset_checklist(pinfo["label"], current_enabled)
new_enabled = _prompt_toolset_checklist(
pinfo["label"],
current_enabled,
force_fresh=True,
)
if new_enabled != current_enabled:
added = new_enabled - current_enabled
@ -3122,7 +3243,11 @@ def tools_command(args=None, first_install: bool = False, config: dict = None):
# Configure newly enabled toolsets that need API keys
for ts_key in sorted(added):
if (TOOL_CATEGORIES.get(ts_key) or TOOLSET_ENV_REQUIREMENTS.get(ts_key)):
if _toolset_needs_configuration_prompt(ts_key, config):
if _toolset_needs_configuration_prompt(
ts_key,
config,
force_fresh=True,
):
_configure_toolset(ts_key, config)
_save_platform_tools(config, pkey, new_enabled)