fix(auth): recompute Nous routing after shared recovery

This commit is contained in:
kshitijk4poor 2026-07-10 13:26:47 +05:30 committed by kshitij
parent ca6513542d
commit 03b8a00e26
2 changed files with 169 additions and 51 deletions

View file

@ -5561,64 +5561,63 @@ def resolve_nous_runtime_credentials(
persisted_state = dict(state)
state_persisted = False
portal_base_url = (
_optional_base_url(state.get("portal_base_url"))
or os.getenv("HERMES_PORTAL_BASE_URL")
or os.getenv("NOUS_PORTAL_BASE_URL")
or DEFAULT_NOUS_PORTAL_URL
).rstrip("/")
def _resolve_effective_routing_metadata() -> tuple[str, str, str, str]:
"""Resolve every routing value that shared OAuth state can replace."""
portal_url = (
_optional_base_url(state.get("portal_base_url"))
or os.getenv("HERMES_PORTAL_BASE_URL")
or os.getenv("NOUS_PORTAL_BASE_URL")
or DEFAULT_NOUS_PORTAL_URL
).rstrip("/")
# A persisted/stale portal_base_url is where the refresh token gets
# POSTed on refresh — reject any host outside the allowlist so a
# poisoned value can't exfiltrate the bearer, healing to the default.
# The trusted operator/deployment env override (HERMES_PORTAL_BASE_URL /
# NOUS_PORTAL_BASE_URL) bypasses this gate entirely — mirrors
# NOUS_INFERENCE_BASE_URL's treatment below; the allowlist exists to
# reject an untrusted NETWORK-provided value, not one the operator
# explicitly configured.
env_portal_override = _nous_portal_env_override()
if env_portal_override:
portal_base_url = env_portal_override.rstrip("/")
else:
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
# A persisted/stale portal_base_url is where the refresh token gets
# POSTed on refresh — reject any host outside the allowlist so a
# poisoned value can't exfiltrate the bearer, healing to the default.
# Trusted operator env overrides bypass this network-value gate.
env_portal_override = _nous_portal_env_override()
if env_portal_override:
portal_url = env_portal_override.rstrip("/")
else:
parsed_portal_url = urlparse(portal_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_url,
parsed_portal_url.hostname,
)
portal_url = DEFAULT_NOUS_PORTAL_URL
# Persisted value: validated network-provenance only. The stored
# inference_base_url is re-validated on read so a poisoned/stale
# staging host (persisted before the allowlist existed) heals to the
# production default on the no-refresh read path — this is what gets
# written back to auth.json. The env override is deliberately NOT
# folded in here: it must never be persisted (it's a runtime overlay).
stored_inference_base_url = (
_validate_nous_inference_url_from_network(
_optional_base_url(state.get("inference_base_url"))
)
or DEFAULT_NOUS_INFERENCE_URL
)
# Effective value used to build the client / returned to callers:
# the NOUS_INFERENCE_BASE_URL env override wins (documented dev/staging
# escape hatch), else the validated stored value.
inference_base_url = (
_nous_inference_env_override() or stored_inference_base_url
)
client_id = str(state.get("client_id") or DEFAULT_NOUS_CLIENT_ID)
def _refresh_effective_inference_base_url() -> None:
nonlocal stored_inference_base_url, inference_base_url
stored_inference_base_url = (
# Re-validate persisted network-provenance on every shared merge.
# The env override is runtime-only and must never be persisted.
stored_inference_url = (
_validate_nous_inference_url_from_network(
_optional_base_url(state.get("inference_base_url"))
)
or DEFAULT_NOUS_INFERENCE_URL
)
inference_base_url = (
_nous_inference_env_override() or stored_inference_base_url
effective_inference_url = (
_nous_inference_env_override() or stored_inference_url
)
effective_client_id = str(
state.get("client_id") or DEFAULT_NOUS_CLIENT_ID
)
return (
portal_url,
stored_inference_url,
effective_inference_url,
effective_client_id,
)
(
portal_base_url,
stored_inference_base_url,
inference_base_url,
client_id,
) = _resolve_effective_routing_metadata()
def _persist_state(reason: str) -> None:
nonlocal persisted_state, state_persisted
@ -5678,7 +5677,12 @@ def resolve_nous_runtime_credentials(
if _merge_shared_nous_oauth_state(state):
access_token = state.get("access_token")
refresh_token = state.get("refresh_token")
_refresh_effective_inference_base_url()
(
portal_base_url,
stored_inference_base_url,
inference_base_url,
client_id,
) = _resolve_effective_routing_metadata()
_persist_state("runtime_shared_merge_missing_access_token")
if not isinstance(access_token, str) or not access_token:
@ -5695,6 +5699,12 @@ def resolve_nous_runtime_credentials(
if _merge_shared_nous_oauth_state(state):
access_token = state.get("access_token")
refresh_token = state.get("refresh_token")
(
portal_base_url,
stored_inference_base_url,
inference_base_url,
client_id,
) = _resolve_effective_routing_metadata()
invoke_jwt_status = _nous_invoke_jwt_status(
access_token,
scope=state.get("scope"),

View file

@ -2002,6 +2002,114 @@ def test_runtime_credentials_merges_shared_token_before_empty_local_access_token
assert profile_state is not None
assert profile_state["access_token"] == shared_token
assert profile_state["refresh_token"] == "shared-fresh-refresh"
assert profile_state["agent_key"] == shared_token
def test_runtime_shared_recovery_recomputes_routing_before_force_refresh(
tmp_path, monkeypatch, shared_store_env,
):
"""A shared refresh token must use its own routing metadata."""
from hermes_cli import auth as auth_mod
profile_b = tmp_path / "profile_b"
_setup_nous_auth(
profile_b,
access_token="local-placeholder",
refresh_token="local-stale-refresh",
expires_at="2000-01-01T00:00:00+00:00",
expires_in=0,
)
auth_path = profile_b / "auth.json"
auth_payload = json.loads(auth_path.read_text())
local_state = auth_payload["providers"]["nous"]
local_state["access_token"] = None
local_state["portal_base_url"] = "http://127.0.0.1:8001"
local_state["client_id"] = "local-client"
auth_path.write_text(json.dumps(auth_payload, indent=2))
monkeypatch.setenv("HERMES_HOME", str(profile_b))
shared_state = _full_state_fixture()
shared_state["access_token"] = _invoke_jwt(seconds=3600)
shared_state["refresh_token"] = "shared-refresh"
shared_state["expires_at"] = _future_iso(3600)
shared_state["scope"] = auth_mod.DEFAULT_NOUS_SCOPE
shared_state["portal_base_url"] = "http://localhost:8002"
shared_state["client_id"] = "shared-client"
shared_state["inference_base_url"] = auth_mod.DEFAULT_NOUS_INFERENCE_URL
auth_mod._write_shared_nous_state(shared_state)
captured = {}
refreshed_token = _invoke_jwt(seconds=7200)
def _refresh(**kwargs):
captured.update(kwargs)
return {
"access_token": refreshed_token,
"refresh_token": "rotated-shared-refresh",
"expires_in": 7200,
"scope": auth_mod.DEFAULT_NOUS_SCOPE,
"inference_base_url": auth_mod.DEFAULT_NOUS_INFERENCE_URL,
}
monkeypatch.setattr(auth_mod, "_refresh_access_token", _refresh)
creds = auth_mod.resolve_nous_runtime_credentials(force_refresh=True)
assert captured["portal_base_url"] == "http://localhost:8002"
assert captured["client_id"] == "shared-client"
assert captured["refresh_token"] == "shared-refresh"
assert creds["api_key"] == refreshed_token
profile_state = auth_mod.get_provider_auth_state("nous")
assert profile_state is not None
assert profile_state["portal_base_url"] == "http://localhost:8002"
assert profile_state["client_id"] == "shared-client"
assert profile_state["refresh_token"] == "rotated-shared-refresh"
def test_runtime_shared_recovery_honors_inference_env_override(
tmp_path, monkeypatch, shared_store_env,
):
"""Shared state is persisted, but the operator inference override wins."""
from hermes_cli import auth as auth_mod
profile_b = tmp_path / "profile_b"
_setup_nous_auth(
profile_b,
access_token="local-placeholder",
refresh_token="local-stale-refresh",
expires_at="2000-01-01T00:00:00+00:00",
expires_in=0,
)
auth_path = profile_b / "auth.json"
auth_payload = json.loads(auth_path.read_text())
auth_payload["providers"]["nous"]["access_token"] = None
auth_path.write_text(json.dumps(auth_payload, indent=2))
monkeypatch.setenv("HERMES_HOME", str(profile_b))
override_url = "https://operator.example/v1"
monkeypatch.setenv("NOUS_INFERENCE_BASE_URL", override_url)
shared_state = _full_state_fixture()
shared_token = _invoke_jwt(seconds=3600)
shared_state["access_token"] = shared_token
shared_state["refresh_token"] = "shared-refresh"
shared_state["expires_at"] = _future_iso(3600)
shared_state["scope"] = auth_mod.DEFAULT_NOUS_SCOPE
shared_state["inference_base_url"] = auth_mod.DEFAULT_NOUS_INFERENCE_URL
auth_mod._write_shared_nous_state(shared_state)
monkeypatch.setattr(
auth_mod,
"_refresh_access_token",
lambda **_kwargs: (_ for _ in ()).throw(AssertionError("unexpected refresh")),
)
creds = auth_mod.resolve_nous_runtime_credentials()
assert creds["base_url"] == override_url
profile_state = auth_mod.get_provider_auth_state("nous")
assert profile_state is not None
assert profile_state["inference_base_url"] == auth_mod.DEFAULT_NOUS_INFERENCE_URL
def test_managed_gateway_access_token_uses_newer_shared_token(