fix(auth): align Codex OAuth persistence paths (#37517)

* fix(desktop): codex OAuth onboarding now resolves on fresh install

The desktop codex device-code worker persisted tokens with a hand-rolled
pool.add_entry(), writing only credential_pool.openai-codex. It never set
active_provider, so on a fresh install the onboarding setup.runtime_check
resolved provider "auto", couldn't detect the Codex OAuth session, and raised
"No inference provider configured" — while setup.status (which sniffs the pool)
reported configured. The disagreement surfaced as the onboarding banner
"Connected, but Hermes still cannot resolve a usable provider."

Use the canonical _save_codex_tokens() instead, matching the CLI's
`hermes auth add openai-codex` path and the Nous/MiniMax dashboard workers.
It writes the providers.openai-codex singleton (setting active_provider) and
syncs the pool.

* fix(auth): align Codex OAuth persistence paths

Ensure desktop and CLI Codex OAuth logins both write the canonical provider state so fresh installs resolve a usable runtime provider.

---------

Co-authored-by: teknium1 <127238744+teknium1@users.noreply.github.com>
This commit is contained in:
brooklyn!
2026-06-02 12:19:44 -05:00
committed by GitHub
co-authored by teknium1
parent 3e6b68252f
commit bb0619dbce
6 changed files with 85 additions and 49 deletions
+6 -7
View File
@@ -303,9 +303,11 @@ def test_auth_add_codex_oauth_persists_pool_entry(tmp_path, monkeypatch):
payload = json.loads((tmp_path / "hermes" / "auth.json").read_text())
entries = payload["credential_pool"]["openai-codex"]
entry = next(item for item in entries if item["source"] == "manual:device_code")
entry = next(item for item in entries if item["source"] == "device_code")
assert payload["active_provider"] == "openai-codex"
assert payload["providers"]["openai-codex"]["tokens"]["access_token"] == token
assert entry["label"] == "codex@example.com"
assert entry["source"] == "manual:device_code"
assert entry["source"] == "device_code"
assert entry["refresh_token"] == "refresh-token"
assert entry["base_url"] == "https://chatgpt.com/backend-api/codex"
@@ -1129,10 +1131,6 @@ def test_auth_remove_codex_manual_source_suppresses_reseed(tmp_path, monkeypatch
def test_auth_add_codex_clears_suppression_marker(tmp_path, monkeypatch):
"""Re-linking codex via `hermes auth add openai-codex` must clear any suppression marker."""
monkeypatch.setenv("HERMES_HOME", str(tmp_path / "hermes"))
monkeypatch.setattr(
"agent.credential_pool._seed_from_singletons",
lambda provider, entries: (False, set()),
)
hermes_home = tmp_path / "hermes"
hermes_home.mkdir(parents=True, exist_ok=True)
@@ -1171,7 +1169,8 @@ def test_auth_add_codex_clears_suppression_marker(tmp_path, monkeypatch):
assert "openai-codex" not in payload.get("suppressed_sources", {})
# New pool entry must be present
entries = payload["credential_pool"]["openai-codex"]
assert any(e["source"] == "manual:device_code" for e in entries)
assert any(e["source"] == "device_code" for e in entries)
assert payload["active_provider"] == "openai-codex"
def test_seed_from_singletons_respects_codex_suppression(tmp_path, monkeypatch):
@@ -146,6 +146,67 @@ def test_nous_dashboard_device_flow_does_not_retry_legacy_scope_on_invoke_refusa
assert requested_scopes == [auth_mod.DEFAULT_NOUS_SCOPE]
def test_codex_dashboard_worker_persists_runtime_provider(tmp_path, monkeypatch):
from hermes_cli import web_server as ws
from hermes_cli.auth import get_active_provider
from hermes_cli.runtime_provider import resolve_runtime_provider
access_token = "h.eyJleHAiOjk5OTk5OTk5OTl9.s"
class _Resp:
def __init__(self, status_code, payload):
self.status_code = status_code
self._payload = payload
def json(self):
return self._payload
class _Client:
def __init__(self, *args, **kwargs):
pass
def __enter__(self):
return self
def __exit__(self, *args):
return False
def post(self, url, **kwargs):
if url.endswith("/deviceauth/usercode"):
return _Resp(200, {
"device_auth_id": "device-auth-id",
"interval": 3,
"user_code": "CODEX-1234",
})
if url.endswith("/deviceauth/token"):
return _Resp(200, {
"authorization_code": "authorization-code",
"code_verifier": "code-verifier",
})
return _Resp(200, {
"access_token": access_token,
"refresh_token": "codex-refresh",
})
monkeypatch.setenv("HERMES_HOME", str(tmp_path))
monkeypatch.setattr(httpx, "Client", _Client)
monkeypatch.setattr(ws.time, "sleep", lambda _: None)
sid, _ = ws._new_oauth_session("openai-codex", "device_code")
try:
ws._codex_full_login_worker(sid)
assert ws._oauth_sessions[sid]["status"] == "approved"
assert get_active_provider() == "openai-codex"
runtime = resolve_runtime_provider(requested=None)
assert runtime["provider"] == "openai-codex"
assert runtime["api_key"] == access_token
assert runtime["api_mode"] == "codex_responses"
finally:
ws._oauth_sessions.pop(sid, None)
def test_nous_dashboard_poller_preserves_effective_scope_when_token_omits_scope(monkeypatch):
from hermes_cli import auth as auth_mod
from hermes_cli import web_server as ws