mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-05-29 06:31:32 +00:00
test(xai-oauth): pin tier-denied 403 behavior + docs warning for #26847
Tests:
* ``test_refresh_xai_oauth_pure_403_marked_tier_denied_not_relogin`` —
refresh-403 raises ``xai_oauth_tier_denied`` with
``relogin_required=False`` and the API-key fallback hint in body.
* ``test_format_auth_error_tier_denied_does_not_suggest_relogin`` —
the renderer does not append "Run ``hermes model``" for the new
code.
* ``test_recover_with_credential_pool_skips_refresh_on_bare_403_for_xai_oauth`` —
bare ``{"reason":"forbidden","message":"Forbidden"}`` body (which
does not match the existing keyword heuristic) still short-circuits
``try_refresh_current`` on xai-oauth.
Docs:
* Drop the "(any active tier)" claim from the xai-grok-oauth guide,
add a top-of-page warning callout, and a Troubleshooting section
for the 403-after-login case pointing at ``XAI_API_KEY`` +
``provider: xai`` as the documented fallback.
This commit is contained in:
parent
3b6f57fa66
commit
34f34ba322
3 changed files with 118 additions and 1 deletions
|
|
@ -24,6 +24,7 @@ from hermes_cli.auth import (
|
|||
_xai_oauth_build_authorize_url,
|
||||
_xai_start_callback_server,
|
||||
_xai_validate_loopback_redirect_uri,
|
||||
format_auth_error,
|
||||
get_xai_oauth_auth_status,
|
||||
refresh_xai_oauth_pure,
|
||||
resolve_provider,
|
||||
|
|
@ -732,6 +733,53 @@ def test_refresh_xai_oauth_pure_no_relogin_on_500(monkeypatch):
|
|||
assert exc.value.relogin_required is False
|
||||
|
||||
|
||||
def test_refresh_xai_oauth_pure_403_marked_tier_denied_not_relogin(monkeypatch):
|
||||
"""403 from xAI's token endpoint is tier/entitlement, not stale tokens.
|
||||
|
||||
Regression test for #26847 — xAI's backend has been seen to 403
|
||||
standard SuperGrok subscribers despite the in-app subscription
|
||||
being active. Re-running ``hermes model`` won't help in that
|
||||
case, so the AuthError must NOT set ``relogin_required=True``,
|
||||
and must carry the dedicated ``xai_oauth_tier_denied`` code so
|
||||
``format_auth_error`` doesn't append the misleading re-auth hint.
|
||||
"""
|
||||
response = _StubHTTPResponse(403, {"error": "permission_denied"})
|
||||
_patch_httpx_client(monkeypatch, response)
|
||||
with pytest.raises(AuthError) as exc:
|
||||
refresh_xai_oauth_pure(
|
||||
"at", "rt", token_endpoint="https://auth.x.ai/oauth2/token"
|
||||
)
|
||||
assert exc.value.code == "xai_oauth_tier_denied"
|
||||
assert exc.value.relogin_required is False
|
||||
message = str(exc.value).lower()
|
||||
assert "403" in message
|
||||
assert "xai_api_key" in message
|
||||
assert "tier" in message
|
||||
|
||||
|
||||
def test_format_auth_error_tier_denied_does_not_suggest_relogin():
|
||||
"""``xai_oauth_tier_denied`` must not append the re-authenticate hint.
|
||||
|
||||
Regression for #26847: telling a tier-gated user to ``hermes model``
|
||||
is actively wrong — re-logging in won't change xAI's allowlist
|
||||
decision. The full message (with ``XAI_API_KEY`` fallback) is built
|
||||
into the error itself.
|
||||
"""
|
||||
err = AuthError(
|
||||
"xAI token refresh failed with HTTP 403. Response: forbidden. "
|
||||
"This OAuth account is not authorized for xAI API access — "
|
||||
"xAI may be restricting API/OAuth use to specific SuperGrok tiers. "
|
||||
"Set ``XAI_API_KEY`` and switch to ``provider: xai``.",
|
||||
provider="xai-oauth",
|
||||
code="xai_oauth_tier_denied",
|
||||
relogin_required=False,
|
||||
)
|
||||
rendered = format_auth_error(err)
|
||||
assert "re-authenticate" not in rendered.lower()
|
||||
assert "hermes model" not in rendered.lower()
|
||||
assert "XAI_API_KEY" in rendered
|
||||
|
||||
|
||||
def test_refresh_xai_oauth_pure_returns_updated_tokens(monkeypatch):
|
||||
new_access = _jwt_with_exp(int(time.time()) + 3600)
|
||||
response = _StubHTTPResponse(
|
||||
|
|
|
|||
|
|
@ -512,6 +512,56 @@ def test_recover_with_credential_pool_skips_refresh_on_entitlement_403():
|
|||
assert refresh_calls["n"] == 0, "try_refresh_current must NOT be called on entitlement 403"
|
||||
|
||||
|
||||
def test_recover_with_credential_pool_skips_refresh_on_bare_403_for_xai_oauth():
|
||||
"""A bare HTTP 403 from ``xai-oauth`` (no keyword match) must NOT loop refresh.
|
||||
|
||||
Regression for #26847 — xAI's backend has been seen to 403 standard
|
||||
SuperGrok subscribers with a terser body that doesn't contain any of
|
||||
the existing entitlement keywords ("do not have an active Grok
|
||||
subscription", etc.). Before the defense-in-depth guard, the recovery
|
||||
path would happily mint a fresh token, get a fresh 403, and spin.
|
||||
"""
|
||||
from run_agent import AIAgent
|
||||
from agent.error_classifier import FailoverReason
|
||||
|
||||
agent = _make_codex_agent()
|
||||
assert agent.provider == "xai-oauth"
|
||||
|
||||
refresh_calls = {"n": 0}
|
||||
|
||||
class _FakePool:
|
||||
def try_refresh_current(self):
|
||||
refresh_calls["n"] += 1
|
||||
return MagicMock(id="should_not_be_called")
|
||||
|
||||
def mark_exhausted_and_rotate(self, **_kwargs):
|
||||
return None
|
||||
|
||||
def has_available(self):
|
||||
return False
|
||||
|
||||
agent._credential_pool = _FakePool()
|
||||
|
||||
error_context = {
|
||||
"reason": "forbidden",
|
||||
"message": "Forbidden",
|
||||
}
|
||||
assert not AIAgent._is_entitlement_failure(error_context, 403), (
|
||||
"Pre-condition: bare 'Forbidden' body must NOT match the keyword "
|
||||
"heuristic — otherwise this test isn't covering the defense-in-depth path."
|
||||
)
|
||||
|
||||
recovered, _retried_429 = agent._recover_with_credential_pool(
|
||||
status_code=403,
|
||||
has_retried_429=False,
|
||||
classified_reason=FailoverReason.auth,
|
||||
error_context=error_context,
|
||||
)
|
||||
|
||||
assert recovered is False, "Bare 403 on xai-oauth must surface, not refresh-loop"
|
||||
assert refresh_calls["n"] == 0, "try_refresh_current must NOT be called on xai-oauth 403"
|
||||
|
||||
|
||||
def test_recover_with_credential_pool_still_refreshes_genuine_auth_failure():
|
||||
"""Regression guard: legitimate auth errors must still trigger refresh."""
|
||||
from run_agent import AIAgent
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ The same OAuth bearer token is also reused by every direct-to-xAI surface in Her
|
|||
| Endpoint | `https://api.x.ai/v1` |
|
||||
| Auth server | `https://accounts.x.ai` |
|
||||
| Requires env var | No (`XAI_API_KEY` is **not** used for this provider) |
|
||||
| Subscription | [SuperGrok](https://x.ai/grok) (any active tier) |
|
||||
| Subscription | [SuperGrok](https://x.ai/grok) — see note below |
|
||||
|
||||
## Prerequisites
|
||||
|
||||
|
|
@ -33,6 +33,10 @@ The same OAuth bearer token is also reused by every direct-to-xAI surface in Her
|
|||
- An active SuperGrok subscription on your xAI account
|
||||
- A browser available on the local machine (or use `--no-browser` for remote sessions)
|
||||
|
||||
:::warning xAI may restrict OAuth API access by tier
|
||||
xAI's backend enforces its own allowlist on the OAuth API surface and has been seen to reject standard SuperGrok subscribers with `HTTP 403` (see issue [#26847](https://github.com/NousResearch/hermes-agent/issues/26847)) even though the in-app subscription is active. If OAuth login succeeds in the browser but inference returns 403, set `XAI_API_KEY` and switch to the API-key path (`provider: xai`) — that surface is not subject to the same gating today.
|
||||
:::
|
||||
|
||||
## Quick Start
|
||||
|
||||
```bash
|
||||
|
|
@ -208,6 +212,21 @@ hermes auth add xai-oauth --no-browser
|
|||
|
||||
Full walkthrough (jump boxes, mosh/tmux, port conflicts): [OAuth over SSH / Remote Hosts](./oauth-over-ssh.md).
|
||||
|
||||
### HTTP 403 after a successful login (tier / entitlement)
|
||||
|
||||
OAuth completed in the browser, tokens are saved, but inference or token refresh returns `HTTP 403` with a message similar to *"The caller does not have permission to execute the specified operation"*.
|
||||
|
||||
This is **not** a stale-token problem — re-running `hermes model` won't change it. xAI's backend has been seen to restrict OAuth API access to specific SuperGrok tiers despite the in-app subscription being active (issue [#26847](https://github.com/NousResearch/hermes-agent/issues/26847)).
|
||||
|
||||
**Fix:** set `XAI_API_KEY` and switch to the API-key path:
|
||||
|
||||
```bash
|
||||
export XAI_API_KEY=xai-...
|
||||
hermes config set model.provider xai
|
||||
```
|
||||
|
||||
Or upgrade your subscription at [x.ai/grok](https://x.ai/grok) if the OAuth route is required.
|
||||
|
||||
### "No xAI credentials found" error at runtime
|
||||
|
||||
The auth store has no `xai-oauth` entry and no `XAI_API_KEY` is set. You haven't logged in yet, or the credential file was deleted.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue