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:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user