mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-09 13:21:42 +00:00
fix(auth): validate portal_base_url and migrate stale api.nousresearch.com (#44710)
This commit is contained in:
parent
3b41df6d46
commit
f3c5327e67
2 changed files with 179 additions and 0 deletions
|
|
@ -1087,6 +1087,8 @@ def _load_auth_store(auth_file: Optional[Path] = None) -> Dict[str, Any]:
|
|||
or isinstance(raw.get("credential_pool"), dict)
|
||||
):
|
||||
raw.setdefault("providers", {})
|
||||
if isinstance(raw.get("providers"), dict):
|
||||
_migrate_stale_nous_portal_url(raw["providers"])
|
||||
return raw
|
||||
|
||||
# Migrate from PR's "systems" format if present
|
||||
|
|
@ -1782,6 +1784,35 @@ def _optional_base_url(value: Any) -> Optional[str]:
|
|||
return cleaned if cleaned else None
|
||||
|
||||
|
||||
_NOUS_STALE_PORTAL_HOSTS: FrozenSet[str] = frozenset({
|
||||
"api.nousresearch.com",
|
||||
})
|
||||
|
||||
# Allowlist of valid Nous Portal hosts. A portal_base_url outside this
|
||||
# set is treated as a misconfiguration and falls back to the default.
|
||||
# "localhost" / "127.0.0.1" are valid for local development and testing.
|
||||
_NOUS_PORTAL_ALLOWED_HOSTS: FrozenSet[str] = frozenset({
|
||||
"portal.nousresearch.com",
|
||||
"localhost",
|
||||
"127.0.0.1",
|
||||
})
|
||||
|
||||
|
||||
def _migrate_stale_nous_portal_url(providers: Dict[str, Any]) -> None:
|
||||
nous = providers.get("nous")
|
||||
if not isinstance(nous, dict):
|
||||
return
|
||||
stored = (nous.get("portal_base_url") or "").strip()
|
||||
if stored:
|
||||
parsed = urlparse(stored)
|
||||
if parsed.hostname in _NOUS_STALE_PORTAL_HOSTS:
|
||||
logger.warning(
|
||||
"auth: migrating stale nous portal_base_url %s -> %s",
|
||||
stored, DEFAULT_NOUS_PORTAL_URL,
|
||||
)
|
||||
nous["portal_base_url"] = DEFAULT_NOUS_PORTAL_URL
|
||||
|
||||
# Allowlist of hosts the Nous Portal proxy is willing to forward inference
|
||||
# Allowlist of hosts the Nous Portal proxy is willing to forward inference
|
||||
# JWTs to. Sending a bearer anywhere else would leak it.
|
||||
#
|
||||
|
|
@ -5321,6 +5352,15 @@ def resolve_nous_access_token(
|
|||
or os.getenv("NOUS_PORTAL_BASE_URL")
|
||||
or DEFAULT_NOUS_PORTAL_URL
|
||||
).rstrip("/")
|
||||
|
||||
parsed_portal_url = urlparse(portal_base_url)
|
||||
if parsed_portal_url.hostname and parsed_portal_url.hostname not in _NOUS_PORTAL_ALLOWED_HOSTS:
|
||||
logger.warning(
|
||||
"auth: ignoring invalid portal_base_url %r (host %r not in allowlist), using default",
|
||||
portal_base_url, parsed_portal_url.hostname,
|
||||
)
|
||||
portal_base_url = DEFAULT_NOUS_PORTAL_URL
|
||||
|
||||
client_id = str(state.get("client_id") or DEFAULT_NOUS_CLIENT_ID)
|
||||
verify = _resolve_verify(insecure=insecure, ca_bundle=ca_bundle, auth_state=state)
|
||||
|
||||
|
|
|
|||
|
|
@ -1984,3 +1984,142 @@ def test_managed_gateway_access_token_uses_newer_shared_token(
|
|||
profile_state = auth_mod.get_provider_auth_state("nous")
|
||||
assert profile_state is not None
|
||||
assert profile_state["refresh_token"] == "shared-fresh-refresh"
|
||||
|
||||
class TestStalePortalBaseUrlMigration:
|
||||
"""_migrate_stale_nous_portal_url auto-corrects stale portal_base_url on load."""
|
||||
|
||||
def test_migrates_stale_portal_url_on_load(self, tmp_path, monkeypatch):
|
||||
from hermes_cli.auth import _load_auth_store, DEFAULT_NOUS_PORTAL_URL
|
||||
|
||||
monkeypatch.setenv("HERMES_HOME", str(tmp_path))
|
||||
auth_file = tmp_path / "auth.json"
|
||||
auth_file.write_text(json.dumps({
|
||||
"version": 1,
|
||||
"active_provider": "nous",
|
||||
"providers": {
|
||||
"nous": {
|
||||
"portal_base_url": "https://api.nousresearch.com",
|
||||
"access_token": "test-token",
|
||||
"refresh_token": "test-refresh",
|
||||
}
|
||||
},
|
||||
}))
|
||||
|
||||
store = _load_auth_store(auth_file)
|
||||
nous = store["providers"]["nous"]
|
||||
assert nous["portal_base_url"] == DEFAULT_NOUS_PORTAL_URL
|
||||
|
||||
def test_preserves_correct_portal_url(self, tmp_path, monkeypatch):
|
||||
from hermes_cli.auth import _load_auth_store, DEFAULT_NOUS_PORTAL_URL
|
||||
|
||||
monkeypatch.setenv("HERMES_HOME", str(tmp_path))
|
||||
auth_file = tmp_path / "auth.json"
|
||||
auth_file.write_text(json.dumps({
|
||||
"version": 1,
|
||||
"active_provider": "nous",
|
||||
"providers": {
|
||||
"nous": {
|
||||
"portal_base_url": DEFAULT_NOUS_PORTAL_URL,
|
||||
"access_token": "test-token",
|
||||
"refresh_token": "test-refresh",
|
||||
}
|
||||
},
|
||||
}))
|
||||
|
||||
store = _load_auth_store(auth_file)
|
||||
nous = store["providers"]["nous"]
|
||||
assert nous["portal_base_url"] == DEFAULT_NOUS_PORTAL_URL
|
||||
|
||||
def test_ignores_other_providers(self, tmp_path, monkeypatch):
|
||||
from hermes_cli.auth import _load_auth_store, DEFAULT_NOUS_PORTAL_URL
|
||||
|
||||
monkeypatch.setenv("HERMES_HOME", str(tmp_path))
|
||||
auth_file = tmp_path / "auth.json"
|
||||
auth_file.write_text(json.dumps({
|
||||
"version": 1,
|
||||
"active_provider": "openai-codex",
|
||||
"providers": {},
|
||||
}))
|
||||
|
||||
store = _load_auth_store(auth_file)
|
||||
assert "nous" not in store.get("providers", {})
|
||||
|
||||
def test_noop_when_nous_state_not_dict(self, tmp_path, monkeypatch):
|
||||
from hermes_cli.auth import _load_auth_store
|
||||
|
||||
monkeypatch.setenv("HERMES_HOME", str(tmp_path))
|
||||
auth_file = tmp_path / "auth.json"
|
||||
auth_file.write_text(json.dumps({
|
||||
"version": 1,
|
||||
"active_provider": "nous",
|
||||
"providers": {"nous": None},
|
||||
}))
|
||||
|
||||
store = _load_auth_store(auth_file)
|
||||
assert store["providers"]["nous"] is None
|
||||
|
||||
def test_runtime_fallback_for_invalid_portal_url(self, tmp_path, monkeypatch):
|
||||
from hermes_cli import auth as auth_mod
|
||||
|
||||
monkeypatch.setenv("HERMES_HOME", str(tmp_path))
|
||||
_setup_nous_auth(
|
||||
tmp_path,
|
||||
access_token="expired-access",
|
||||
refresh_token="valid-refresh",
|
||||
expires_at="2025-01-01T00:00:00+00:00",
|
||||
)
|
||||
auth_file = tmp_path / "auth.json"
|
||||
store = json.loads(auth_file.read_text())
|
||||
store["providers"]["nous"]["portal_base_url"] = "https://api.nousresearch.com"
|
||||
auth_file.write_text(json.dumps(store, indent=2))
|
||||
|
||||
refresh_calls = []
|
||||
|
||||
def _fake_refresh_access_token(*, client, portal_base_url, client_id, refresh_token):
|
||||
del client, client_id, refresh_token
|
||||
refresh_calls.append(portal_base_url)
|
||||
return {
|
||||
"access_token": "refreshed-access",
|
||||
"refresh_token": "new-refresh",
|
||||
"expires_in": 3600,
|
||||
}
|
||||
|
||||
monkeypatch.setattr(auth_mod, "_refresh_access_token", _fake_refresh_access_token)
|
||||
|
||||
token = auth_mod.resolve_nous_access_token()
|
||||
assert token == "refreshed-access"
|
||||
assert len(refresh_calls) == 1
|
||||
assert refresh_calls[0] == auth_mod.DEFAULT_NOUS_PORTAL_URL
|
||||
|
||||
def test_runtime_accepts_localhost(self, tmp_path, monkeypatch):
|
||||
from hermes_cli import auth as auth_mod
|
||||
|
||||
monkeypatch.setenv("HERMES_HOME", str(tmp_path))
|
||||
_setup_nous_auth(
|
||||
tmp_path,
|
||||
access_token="expired-access",
|
||||
refresh_token="valid-refresh",
|
||||
expires_at="2025-01-01T00:00:00+00:00",
|
||||
)
|
||||
auth_file = tmp_path / "auth.json"
|
||||
store = json.loads(auth_file.read_text())
|
||||
store["providers"]["nous"]["portal_base_url"] = "http://localhost:8080/"
|
||||
auth_file.write_text(json.dumps(store, indent=2))
|
||||
|
||||
refresh_calls = []
|
||||
|
||||
def _fake_refresh_access_token(*, client, portal_base_url, client_id, refresh_token):
|
||||
del client, client_id, refresh_token
|
||||
refresh_calls.append(portal_base_url)
|
||||
return {
|
||||
"access_token": "refreshed-access",
|
||||
"refresh_token": "new-refresh",
|
||||
"expires_in": 3600,
|
||||
}
|
||||
|
||||
monkeypatch.setattr(auth_mod, "_refresh_access_token", _fake_refresh_access_token)
|
||||
|
||||
token = auth_mod.resolve_nous_access_token()
|
||||
assert token == "refreshed-access"
|
||||
assert len(refresh_calls) == 1
|
||||
assert "localhost" in refresh_calls[0]
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue