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
@@ -321,3 +321,70 @@ def test_apply_nous_managed_defaults_preserves_existing_video_gen_section(monkey
assert config["video_gen"]["use_gateway"] is True
# Pre-existing keys should be preserved
assert config["video_gen"]["model"] == "pixverse-v6"
# ---------------------------------------------------------------------------
# ensure_nous_portal_access — inline login gate for `hermes tools`
# ---------------------------------------------------------------------------
def test_ensure_nous_portal_access_fast_path_when_already_paid(monkeypatch):
"""Already-entitled users return True without any login prompt."""
login_called = {"v": False}
monkeypatch.setattr(
ns, "get_nous_portal_account_info",
lambda **kw: _account(logged_in=True, paid=True),
)
def _login(**kw):
login_called["v"] = True
return True
monkeypatch.setattr(ns, "_run_nous_portal_login_only", _login)
assert ns.ensure_nous_portal_access() is True
assert login_called["v"] is False
def test_ensure_nous_portal_access_logs_in_then_grants(monkeypatch):
"""Logged-out user logs in, then entitlement re-check shows paid access."""
states = iter([
_account(logged_in=False, paid=None), # initial check
_account(logged_in=True, paid=True), # after login
])
monkeypatch.setattr(
ns, "get_nous_portal_account_info", lambda **kw: next(states),
)
monkeypatch.setattr(ns, "_run_nous_portal_login_only", lambda **kw: True)
assert ns.ensure_nous_portal_access() is True
def test_ensure_nous_portal_access_returns_false_when_login_declined(monkeypatch):
monkeypatch.setattr(
ns, "get_nous_portal_account_info",
lambda **kw: _account(logged_in=False, paid=None),
)
monkeypatch.setattr(ns, "_run_nous_portal_login_only", lambda **kw: False)
assert ns.ensure_nous_portal_access() is False
def test_ensure_nous_portal_access_false_when_logged_in_but_unpaid(monkeypatch):
"""Logged in already but no paid access — no login attempt, returns False."""
login_called = {"v": False}
monkeypatch.setattr(
ns, "get_nous_portal_account_info",
lambda **kw: _account(logged_in=True, paid=False),
)
def _login(**kw):
login_called["v"] = True
return True
monkeypatch.setattr(ns, "_run_nous_portal_login_only", _login)
assert ns.ensure_nous_portal_access() is False
# Already logged in, so no device-code login should be attempted.
assert login_called["v"] is False
+119 -20
View File
@@ -612,6 +612,52 @@ def test_visible_providers_include_nous_subscription_when_logged_in(monkeypatch)
assert providers[0]["name"].startswith("Nous Subscription")
def test_visible_providers_show_nous_subscription_when_logged_out(monkeypatch):
"""Nous-managed Tool Gateway rows are always listed, even logged out.
Selecting one triggers an inline Portal login (entitlement is checked at
selection time, not visibility time).
"""
config = {"model": {"provider": "openrouter"}}
monkeypatch.setattr(
"hermes_cli.nous_subscription.get_nous_portal_account_info",
lambda: NousPortalAccountInfo(
logged_in=False,
source="none",
fresh=False,
paid_service_access=None,
),
)
providers = _visible_providers(TOOL_CATEGORIES["browser"], config)
assert any(p["name"].startswith("Nous Subscription") for p in providers)
def test_visible_providers_show_nous_subscription_when_paid_access_is_false(monkeypatch):
"""Logged-in-but-unpaid users still see the managed rows.
The paid-access gate moved from visibility to selection time — the row is
shown; ``ensure_nous_portal_access`` blocks activation if still unpaid.
"""
config = {"model": {"provider": "nous"}}
monkeypatch.setattr(
"hermes_cli.nous_subscription.get_nous_portal_account_info",
lambda: NousPortalAccountInfo(
logged_in=True,
source="jwt",
fresh=False,
paid_service_access=False,
),
)
providers = _visible_providers(TOOL_CATEGORIES["browser"], config)
assert any(p["name"].startswith("Nous Subscription") for p in providers)
def test_visible_providers_force_fresh_shows_nous_subscription_after_upgrade(monkeypatch):
calls = []
@@ -643,24 +689,6 @@ def test_visible_providers_force_fresh_shows_nous_subscription_after_upgrade(mon
assert ("features", True) in calls
def test_visible_providers_hide_nous_subscription_when_paid_access_is_false(monkeypatch):
config = {"model": {"provider": "nous"}}
monkeypatch.setattr(
"hermes_cli.nous_subscription.get_nous_portal_account_info",
lambda: NousPortalAccountInfo(
logged_in=True,
source="jwt",
fresh=False,
paid_service_access=False,
),
)
providers = _visible_providers(TOOL_CATEGORIES["browser"], config)
assert all(not provider["name"].startswith("Nous Subscription") for provider in providers)
def test_local_browser_provider_is_saved_explicitly(monkeypatch):
config = {}
local_provider = next(
@@ -669,7 +697,6 @@ def test_local_browser_provider_is_saved_explicitly(monkeypatch):
if provider.get("browser_provider") == "local"
)
monkeypatch.setattr("hermes_cli.tools_config._run_post_setup", lambda key: None)
_configure_provider(local_provider, config)
assert config["browser"]["cloud_provider"] == "local"
@@ -1265,7 +1292,13 @@ def test_get_effective_configurable_toolsets_dedupes_bundled_plugins():
({"name": "B", "browser_provider": "browserbase", "env_vars": []}, "browser", False),
({"name": "W", "web_backend": "tavily", "env_vars": []}, "web", False),
])
def test_reconfigure_provider_syncs_use_gateway(provider, config_key, expected):
def test_reconfigure_provider_syncs_use_gateway(monkeypatch, provider, config_key, expected):
# Managed providers run the inline Portal entitlement gate; treat the user
# as already entitled so the test exercises the use_gateway sync.
monkeypatch.setattr(
"hermes_cli.nous_subscription.ensure_nous_portal_access",
lambda **kwargs: True,
)
config = {}
_reconfigure_provider(provider, config)
assert config[config_key]["use_gateway"] is expected
@@ -1301,3 +1334,69 @@ def test_reconfigure_provider_runs_post_setup_for_env_var_providers(
_reconfigure_provider(provider, {})
assert called == [post_setup_key]
# ---------------------------------------------------------------------------
# Inline Nous Portal login gate on managed-provider selection
# ---------------------------------------------------------------------------
def test_configure_managed_provider_blocks_when_not_entitled(monkeypatch):
"""Selecting a Nous-managed backend without paid access writes no config."""
monkeypatch.setattr(
"hermes_cli.nous_subscription.ensure_nous_portal_access",
lambda **kwargs: False,
)
provider = {
"name": "Nous Subscription (Firecrawl)",
"web_backend": "firecrawl",
"managed_nous_feature": "web",
"env_vars": [],
}
config = {}
_configure_provider(provider, config)
# No use_gateway / backend written — the gate returned before any mutation.
assert "web" not in config
def test_configure_managed_provider_enables_when_entitled(monkeypatch):
"""Once entitled, selecting the managed backend sets use_gateway=True."""
monkeypatch.setattr(
"hermes_cli.nous_subscription.ensure_nous_portal_access",
lambda **kwargs: True,
)
provider = {
"name": "Nous Subscription (Firecrawl)",
"web_backend": "firecrawl",
"managed_nous_feature": "web",
"env_vars": [],
}
config = {}
_configure_provider(provider, config)
assert config["web"]["backend"] == "firecrawl"
assert config["web"]["use_gateway"] is True
def test_configure_non_managed_provider_skips_portal_gate(monkeypatch):
"""A self-hosted provider must never trigger the Nous Portal login gate."""
called = {"gate": False}
def _boom(**kwargs):
called["gate"] = True
return False
monkeypatch.setattr(
"hermes_cli.nous_subscription.ensure_nous_portal_access", _boom
)
provider = {"name": "Tavily", "web_backend": "tavily", "env_vars": []}
config = {}
_configure_provider(provider, config)
assert called["gate"] is False
assert config["web"]["backend"] == "tavily"
assert config["web"]["use_gateway"] is False