fix(auth): heal poisoned Nous inference URL on refresh instead of retaining it
A nous inference_base_url that fails the host allowlist (e.g. a stale stg-inference-api.nousresearch.com persisted before the allowlist existed) was only replaced 'if refreshed_url:' — so when the validator rejected the URL it left the poisoned value in place. The 'falling back to default' warning fired but never took effect: every subsequent call, including the auxiliary compression call, kept hitting the dead staging endpoint and 401'd. Reset to DEFAULT_NOUS_INFERENCE_URL when validation returns None at both refresh sites in resolve_nous_runtime_credentials, so a poisoned auth.json self-heals on the next refresh. The proxy adapter already did this correctly; this brings the two auth.py sites in line.
This commit is contained in:
parent
92d40c2553
commit
37a4dd4982
2 changed files with 94 additions and 4 deletions
|
|
@ -5430,9 +5430,15 @@ def refresh_nous_oauth_pure(
|
|||
state["refresh_token"] = refreshed.get("refresh_token") or refresh_token_value
|
||||
state["token_type"] = refreshed.get("token_type") or state.get("token_type") or "Bearer"
|
||||
state["scope"] = refreshed.get("scope") or state.get("scope")
|
||||
# Heal a poisoned stored value: when the Portal-returned URL is
|
||||
# rejected by the allowlist (returns None), reset to the production
|
||||
# default instead of leaving a previously-persisted bad host (e.g. a
|
||||
# stale staging URL) in place. Without this reset, an auth.json that
|
||||
# was poisoned before the allowlist existed keeps re-validating to
|
||||
# None on every refresh and silently re-uses the dead endpoint —
|
||||
# the "falling back to default" warning never actually takes effect.
|
||||
refreshed_url = _validate_nous_inference_url_from_network(refreshed.get("inference_base_url"))
|
||||
if refreshed_url:
|
||||
state["inference_base_url"] = refreshed_url
|
||||
state["inference_base_url"] = refreshed_url or DEFAULT_NOUS_INFERENCE_URL
|
||||
state["obtained_at"] = now.isoformat()
|
||||
state["expires_in"] = access_ttl
|
||||
state["expires_at"] = datetime.fromtimestamp(
|
||||
|
|
@ -5705,9 +5711,13 @@ def resolve_nous_runtime_credentials(
|
|||
state["refresh_token"] = refreshed.get("refresh_token") or refresh_token
|
||||
state["token_type"] = refreshed.get("token_type") or state.get("token_type") or "Bearer"
|
||||
state["scope"] = refreshed.get("scope") or state.get("scope")
|
||||
# Heal a poisoned stored value (see refresh_nous_oauth_pure):
|
||||
# reject → reset to production default, don't keep a stale
|
||||
# staging host that re-validates to None every refresh.
|
||||
# The local inference_base_url is persisted to state below
|
||||
# (and used for the client), so healing it here suffices.
|
||||
refreshed_url = _validate_nous_inference_url_from_network(refreshed.get("inference_base_url"))
|
||||
if refreshed_url:
|
||||
inference_base_url = refreshed_url
|
||||
inference_base_url = refreshed_url or DEFAULT_NOUS_INFERENCE_URL
|
||||
state["obtained_at"] = now.isoformat()
|
||||
state["expires_in"] = access_ttl
|
||||
state["expires_at"] = datetime.fromtimestamp(
|
||||
|
|
|
|||
|
|
@ -211,3 +211,83 @@ class TestEnvOverrideNotGated:
|
|||
"env override path must not gate through the network "
|
||||
"validator — it would break documented dev/staging use."
|
||||
)
|
||||
|
||||
|
||||
class TestHealsPoisonedStoredValue:
|
||||
"""A stored inference_base_url that is NOT in the allowlist (e.g. a
|
||||
stale ``stg-inference-api.nousresearch.com`` persisted before the
|
||||
allowlist existed) must be HEALED back to the production default on
|
||||
the next refresh — not silently retained.
|
||||
|
||||
Before the fix, the refresh sites only assigned the validated URL
|
||||
``if refreshed_url:`` and otherwise left the poisoned value in place,
|
||||
so the "falling back to default" warning was logged but never
|
||||
actually took effect — every subsequent call kept hitting the dead
|
||||
staging endpoint (real incident: opus-4.8 routed to nous, nous pinned
|
||||
to staging, every request + the aux compression call 401'd).
|
||||
"""
|
||||
|
||||
def test_refresh_resets_rejected_url_to_default(self, monkeypatch):
|
||||
import hermes_cli.auth as auth
|
||||
|
||||
poisoned = "https://stg-inference-api.nousresearch.com/v1"
|
||||
state = {
|
||||
"access_token": "tok",
|
||||
"refresh_token": "rtok",
|
||||
"client_id": "hermes-cli",
|
||||
"portal_base_url": auth.DEFAULT_NOUS_PORTAL_URL,
|
||||
"inference_base_url": poisoned,
|
||||
}
|
||||
|
||||
# Force the refresh branch and return another rejected (staging) URL,
|
||||
# exercising the validator-returns-None heal path.
|
||||
monkeypatch.setattr(auth, "_nous_invoke_jwt_status", lambda *a, **k: "needs_refresh")
|
||||
monkeypatch.setattr(
|
||||
auth,
|
||||
"_refresh_access_token",
|
||||
lambda **k: {
|
||||
"access_token": "newtok",
|
||||
"refresh_token": "newrtok",
|
||||
"expires_in": 3600,
|
||||
"inference_base_url": poisoned, # Portal still hands back staging
|
||||
},
|
||||
)
|
||||
# Skip the JWT usability assertions (orthogonal to URL healing).
|
||||
monkeypatch.setattr(auth, "_assert_nous_inference_jwt_usable", lambda *a, **k: None)
|
||||
monkeypatch.setattr(auth, "_select_nous_invoke_jwt", lambda *a, **k: None)
|
||||
|
||||
result = auth.refresh_nous_oauth_from_state(state, force_refresh=True)
|
||||
|
||||
assert result["inference_base_url"] == auth.DEFAULT_NOUS_INFERENCE_URL, (
|
||||
"rejected Portal URL must heal to the production default, "
|
||||
f"got {result['inference_base_url']!r}"
|
||||
)
|
||||
|
||||
def test_refresh_keeps_valid_url(self, monkeypatch):
|
||||
"""A legitimate allowlisted URL from the Portal is preserved."""
|
||||
import hermes_cli.auth as auth
|
||||
|
||||
good = "https://inference-api.nousresearch.com/v1"
|
||||
state = {
|
||||
"access_token": "tok",
|
||||
"refresh_token": "rtok",
|
||||
"client_id": "hermes-cli",
|
||||
"portal_base_url": auth.DEFAULT_NOUS_PORTAL_URL,
|
||||
"inference_base_url": good,
|
||||
}
|
||||
monkeypatch.setattr(auth, "_nous_invoke_jwt_status", lambda *a, **k: "needs_refresh")
|
||||
monkeypatch.setattr(
|
||||
auth,
|
||||
"_refresh_access_token",
|
||||
lambda **k: {
|
||||
"access_token": "newtok",
|
||||
"refresh_token": "newrtok",
|
||||
"expires_in": 3600,
|
||||
"inference_base_url": good,
|
||||
},
|
||||
)
|
||||
monkeypatch.setattr(auth, "_assert_nous_inference_jwt_usable", lambda *a, **k: None)
|
||||
monkeypatch.setattr(auth, "_select_nous_invoke_jwt", lambda *a, **k: None)
|
||||
|
||||
result = auth.refresh_nous_oauth_from_state(state, force_refresh=True)
|
||||
assert result["inference_base_url"] == good
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue