feat(tools): always show Nous Tool Gateway backends, login on select (#35792)

* feat(tools): always show Nous Tool Gateway backends, login on select

The Nous-managed Tool Gateway rows in `hermes tools` (Firecrawl, OpenAI
TTS, Browser Use, FAL image/video) were hidden unless the user was already
logged into Nous Portal with paid access. Now they are always listed.
Selecting one runs an inline Nous Portal device-code OAuth + entitlement
check — auth only, no inference-provider switch and no bulk 'enable all
tools' prompt (that stays in `hermes model`). The row only activates the
gateway once paid access is confirmed.

- _visible_providers: stop hiding managed_nous_feature rows (incl. those
  also flagged requires_nous_auth); pure pre-auth UX rows still gate on login
- nous_subscription.ensure_nous_portal_access(): auth + entitlement gate
  that preserves the user's active inference provider
- _configure_provider / _reconfigure_provider: run the inline gate for
  managed backends; write config only when entitled
- picker marker: 'via Nous Portal (login on select)' for logged-out users
- _hidden_nous_gateway_message: now a no-op (rows are never hidden)

* docs: hermes tools is a first-class Tool Gateway entry point

The Tool Gateway docs framed `hermes setup --portal` / `hermes model` as
the activation path and only mentioned `hermes tools` for mixing in your
own keys. With the inline-login change, picking a Nous-managed backend in
`hermes tools` is a complete path on its own — it logs you into Nous
Portal on select if needed, without switching your inference provider or
prompting to enable every other tool.

- tool-gateway.md: Get started now lists three peer entry points; new
  paragraph explaining login-on-select and the no-prompt fast path when
  OAuth is already active
- nous-portal.md + run-hermes-with-nous-portal.md: note that managed rows
  appear logged-out and trigger inline login on select
This commit is contained in:
Teknium
2026-05-31 03:39:17 -07:00
committed by GitHub
parent 8f4c8e7c82
commit 1fc7bdc5e6
7 changed files with 411 additions and 63 deletions
+73 -34
View File
@@ -1876,18 +1876,26 @@ def _visible_providers(
*,
force_fresh: bool = False,
) -> list[dict]:
"""Return provider entries visible for the current auth/config state."""
"""Return provider entries visible for the current auth/config state.
Nous-managed Tool Gateway rows (``managed_nous_feature``) are always
shown — even to logged-out / unentitled users — so the picker advertises
that the capability exists. Selecting one drives an inline Nous Portal
login + entitlement check (see ``_configure_provider``); the row only
*activates* the gateway once paid access is confirmed.
"""
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_available:
continue
if provider.get("requires_nous_auth") and not features.nous_auth_present:
# Nous-managed Tool Gateway rows stay visible regardless of auth —
# selecting one drives an inline Portal login. A `requires_nous_auth`
# row that is NOT a managed gateway feature (pure pre-auth UX) is
# still hidden until the user is logged in.
if (
provider.get("requires_nous_auth")
and not provider.get("managed_nous_feature")
and not features.nous_auth_present
):
continue
visible.append(provider)
@@ -1933,22 +1941,16 @@ def _hidden_nous_gateway_message(
*,
force_fresh: bool = False,
) -> str:
"""Return a reason when a category's Nous provider is hidden."""
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 ""
message = format_nous_portal_entitlement_message(
features.account_info,
capability=capability,
)
return message or ""
"""Deprecated: Nous Tool Gateway rows are no longer hidden.
Previously this returned a "log in / upgrade" banner shown above a
category when its Nous-managed rows were filtered out for unentitled
users. Those rows are now always listed (see ``_visible_providers``), and
the login + entitlement guidance happens inline when the user selects one
(``ensure_nous_portal_access``). Kept as a no-op so call sites stay simple;
always returns an empty string.
"""
return ""
_POST_SETUP_INSTALLED: dict = {
@@ -2132,14 +2134,17 @@ def _configure_tool_category(
configured = ""
else:
configured = " [configured]"
# Highlight Nous-managed entries when the user has Portal auth.
# curses_radiolist can't render ANSI inside item strings, so we
# use a plain unicode star + parenthetical phrase. Suppressed
# when no Portal auth is present so non-subscribers see the
# picker unchanged.
# Mark Nous-managed entries. Logged-in paid subscribers get the
# "included" star; everyone else gets a "via Nous Portal" hint so
# it's clear selecting the row triggers a Portal login. The rows
# are always shown now (see _visible_providers) — selecting one
# drives an inline login + entitlement check.
sub_marker = ""
if _nous_logged_in and p.get("managed_nous_feature"):
sub_marker = " ★ Included with your Nous subscription"
if p.get("managed_nous_feature"):
if _nous_logged_in:
sub_marker = " ★ Included with your Nous subscription"
else:
sub_marker = " ★ via Nous Portal (login on select)"
provider_choices.append(f"{p['name']}{badge}{tag}{configured}{sub_marker}")
# Add skip option
@@ -2558,7 +2563,26 @@ def _configure_provider(
env_vars = provider.get("env_vars", [])
managed_feature = provider.get("managed_nous_feature")
if provider.get("requires_nous_auth"):
# Nous-managed Tool Gateway backends are always listed (see
# _visible_providers), but only *activate* once the user has paid Nous
# Portal access. Selecting one runs an inline Portal login when needed —
# auth + entitlement only, no inference-provider switch and no bulk
# "enable all tools" prompt (that lives in `hermes model`).
if managed_feature:
from hermes_cli.nous_subscription import ensure_nous_portal_access
if not ensure_nous_portal_access(
capability=f"{provider.get('name', 'the Nous Tool Gateway')}"
):
_print_warning(
" Not enabled — Nous Portal paid access is required for this backend."
)
return
# Pure pre-auth UX rows (requires_nous_auth without a managed gateway
# feature) keep the old gate. Managed rows are handled by the inline
# login above, so don't double-check them here.
if provider.get("requires_nous_auth") and not managed_feature:
features = get_nous_subscription_features(config, force_fresh=force_fresh)
entitled = bool(
features.account_info and features.account_info.paid_service_access is True
@@ -2922,7 +2946,22 @@ def _reconfigure_provider(
env_vars = provider.get("env_vars", [])
managed_feature = provider.get("managed_nous_feature")
if provider.get("requires_nous_auth"):
# Same inline Nous Portal login + entitlement gate as _configure_provider:
# managed Tool Gateway backends only activate with paid Portal access.
if managed_feature:
from hermes_cli.nous_subscription import ensure_nous_portal_access
if not ensure_nous_portal_access(
capability=f"{provider.get('name', 'the Nous Tool Gateway')}"
):
_print_warning(
" Not enabled — Nous Portal paid access is required for this backend."
)
return
# Pure pre-auth UX rows keep the old gate; managed rows already handled
# by the inline login above.
if provider.get("requires_nous_auth") and not managed_feature:
features = get_nous_subscription_features(config, force_fresh=force_fresh)
entitled = bool(
features.account_info and features.account_info.paid_service_access is True