diff --git a/hermes_cli/dashboard_register.py b/hermes_cli/dashboard_register.py index fbec62705..4ece93d3f 100644 --- a/hermes_cli/dashboard_register.py +++ b/hermes_cli/dashboard_register.py @@ -25,6 +25,7 @@ so this client never needs to know the namespace convention. from __future__ import annotations import json +import os import random import sys import urllib.error @@ -61,8 +62,22 @@ def _generate_dashboard_name() -> str: return f"{random.choice(_NAME_ADJECTIVES)}_{random.choice(_NAME_NOUNS)}" -def _resolve_portal_base_url() -> str: - """Best-effort portal base URL from the stored Nous login, with default.""" +def _resolve_portal_base_url(override: Optional[str] = None) -> str: + """Resolve the portal base URL for the registration request. + + Precedence: + 1. ``override`` — explicit ``--portal-url`` flag or + ``HERMES_DASHBOARD_PORTAL_URL`` env (used for testing against a + preview/staging portal). NOTE: the access token must be valid at + this portal — it's minted by whatever portal you logged into, so an + override only works if the token's issuer matches (e.g. you logged + into the same staging/preview portal). + 2. The ``portal_base_url`` stored on the Nous login — this is the + portal that issued the token, so it's the correct default target. + 3. The production default. + """ + if isinstance(override, str) and override.strip(): + return override.rstrip("/") try: from hermes_cli.auth import DEFAULT_NOUS_PORTAL_URL, get_provider_auth_state @@ -223,7 +238,12 @@ def cmd_dashboard_register(args) -> None: print(f"✗ Could not resolve a Nous Portal access token: {exc}") sys.exit(1) - portal_base_url = _resolve_portal_base_url() + # Portal override: explicit --portal-url flag wins, else the + # HERMES_DASHBOARD_PORTAL_URL env var, else the stored login's portal. + portal_override = getattr(args, "portal_url", None) or os.environ.get( + "HERMES_DASHBOARD_PORTAL_URL" + ) + portal_base_url = _resolve_portal_base_url(portal_override) name = getattr(args, "name", None) or _generate_dashboard_name() custom_redirect_uri = getattr(args, "redirect_uri", None) diff --git a/hermes_cli/main.py b/hermes_cli/main.py index 7624b2ffa..4a3f7183c 100644 --- a/hermes_cli/main.py +++ b/hermes_cli/main.py @@ -15326,6 +15326,17 @@ Examples: "https://hermes.example.com/auth/callback. Omit for localhost-only use." ), ) + dashboard_register_parser.add_argument( + "--portal-url", + dest="portal_url", + default=None, + help=( + "Override the Nous Portal base URL for registration (default: the " + "portal you logged into). The access token must be valid at this " + "portal. Also settable via HERMES_DASHBOARD_PORTAL_URL. Mainly for " + "testing against a staging/preview portal." + ), + ) dashboard_register_parser.set_defaults(func=cmd_dashboard_register) # ========================================================================= diff --git a/tests/hermes_cli/test_dashboard_register.py b/tests/hermes_cli/test_dashboard_register.py index d24b61f60..b3bf90297 100644 --- a/tests/hermes_cli/test_dashboard_register.py +++ b/tests/hermes_cli/test_dashboard_register.py @@ -157,6 +157,34 @@ class TestHappyPath: ) +class TestPortalResolution: + def test_override_arg_wins(self): + assert ( + dr._resolve_portal_base_url("https://preview.example.com/") + == "https://preview.example.com" + ) + + def test_falls_back_to_stored_login_portal(self): + with patch( + "hermes_cli.auth.get_provider_auth_state", + return_value={"portal_base_url": "https://portal.staging-nousresearch.com"}, + ): + assert ( + dr._resolve_portal_base_url(None) + == "https://portal.staging-nousresearch.com" + ) + + def test_blank_override_ignored(self): + with patch( + "hermes_cli.auth.get_provider_auth_state", + return_value={"portal_base_url": "https://portal.staging-nousresearch.com"}, + ): + assert ( + dr._resolve_portal_base_url(" ") + == "https://portal.staging-nousresearch.com" + ) + + class TestPortalErrors: def _run_http_error(self, code, body): err = urllib.error.HTTPError(