Codex desktop OAuth login and runtime issues

NousResearch/hermes-agent · cluster 37515 · agent

Data as of

Nothing open on GitHub. Fix merged in #37517. Check it covers your case.

The short answer

0open issues
0open PRs
4items grouped
Known?
Yes. 4 related items grouped here, 2 issues already closed.
Fix event
A fix was merged to main in PR #37517. Check it covers your case. No release data is tracked.
Tracker
The maintainers' tracker marks this unresolved. That is separate from the fix event.
Still open
Nothing open on GitHub.

These members concern bugs and fixes related to OAuth authentication flow on the desktop client, specifically around credential persistence paths and runtime provider resolution after a successful login.

Open issues0

No open issues.

Open PRs0

No open pull requests.

Fixes on record

Merged to main. A stamp counts the issues a fix is linked to.

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

    merged to main

    files: agent/credential_pool.py, hermes_cli/auth.py, hermes_cli/auth_commands.py, hermes_cli/web_server.py, tests/hermes_cli/test_auth_commands.py, tests/hermes_cli/test_web_oauth_dispatch.py

    Show the diff
    • agent/credential_pool.py
      @@ -1891,6 +1891,7 @@ def _env_val(key: str) -> str:
               # via `hermes auth openai-codex`.
               if isinstance(tokens, dict) and tokens.get("access_token"):
                   active_sources.add("device_code")
      +            custom_label = str(state.get("label") or "").strip()
                   changed |= _upsert_entry(
                       entries,
                       provider,
      @@ -1902,7 +1903,7 @@ def _env_val(key: str) -> str:
                           "refresh_token": tokens.get("refresh_token"),
                           "base_url": "https://chatgpt.com/backend-api/codex",
                           "last_refresh": state.get("last_refresh"),
      -                    "label": label_from_token(tokens.get("access_token", ""), "device_code"),
      +                    "label": custom_label or label_from_token(tokens.get("access_token", ""), "device_code"),
                       },
                   )
       
    • hermes_cli/auth.py
      @@ -3370,7 +3370,7 @@ def _sync_codex_pool_entries(
               entry["last_error_reset_at"] = None
       
       
      -def _save_codex_tokens(tokens: [redacted:secret-assignment] str], last_refresh: str = None) -> None:
      +def _save_codex_tokens(tokens: [redacted:secret-assignment] str], last_refresh: str = None, label: str = None) -> None:
           """Save Codex OAuth tokens to Hermes auth store (~/.hermes/auth.json)."""
           if last_refresh is None:
               last_refresh = datetime.now(timezone.utc).isoformat().replace("+00:00", "Z")
      @@ -3380,6 +3380,8 @@ def _save_codex_tokens(tokens: [redacted:secret-assignment] str], last_refresh: str = None) -> None
               state["tokens"] = tokens
               state["last_refresh"] = last_refresh
               state["auth_mode"] = "chatgpt"
      +        if label and str(label).strip():
      +            state["label"] = str(label).strip()
               _save_provider_state(auth_store, "openai-codex", state)
               _sync_codex_pool_entries(auth_store, tokens, last_refresh)
               _save_auth_store(auth_store)
    • hermes_cli/auth_commands.py
      @@ -307,28 +307,20 @@ def auth_add_command(args) -> None:
               return
       
           if provider == "openai-codex":
      -        # Clear any existing suppression marker so a re-link after `hermes auth
      -        # remove openai-codex` works without the new tokens being skipped.
      -        auth_mod.unsuppress_credential_source(provider, "device_code")
               creds = auth_mod._codex_device_code_login()
               label = (getattr(args, "label", None) or "").strip() or label_from_token(
                   creds["tokens"]["access_token"],
                   _oauth_default_label(provider, len(pool.entries()) + 1),
               )
      -        entry = PooledCredential(
      -            provider=provider,
      -            id=uuid.uuid4().hex[:6],
      -            label=label,
      -            auth_type=AUTH_TYPE_OAUTH,
      -            priority=0,
      -            source=f"{SOURCE_MANUAL}:device_code",
      -            access_token=[redacted:secret-assignment]"tokens"]["access_token"],
      -            refresh_token=[redacted:secret-assignment]"tokens"].get("refresh_token"),
      -            base_url=creds.get("base_url"),
      +        auth_mod._save_codex_tokens(
      +            creds["tokens"],
                   last_refresh=creds.get("last_refresh"),
      +            label=label,
               )
      -        pool.add_entry(entry)
      -        print(f'Added {provider} OAuth credential #{len(pool.entries())}: "{entry.label}"')
      +        pool = load_pool(provider)
      +        entry = next((item for item in pool.entries() if item.source == "device_code"), None)
      +        shown_label = entry.label if entry is not None else label
      +        print(f'Saved {provider} OAuth device-code credentials: "[redacted:secret-assignment]"')
               return
       
           if provider == "xai-oauth":
    • hermes_cli/web_server.py
      @@ -3717,31 +3717,12 @@ def _codex_full_login_worker(session_id: str) -> None:
               if not access_token:
                   raise RuntimeError("token exchange did not return access_token")
       
      -        # Persist via credential pool — same shape as auth_commands.add_command
      -        from agent.credential_pool import (
      -            PooledCredential,
      -            load_pool,
      -            AUTH_TYPE_OAUTH,
      -            SOURCE_MANUAL,
      -        )
      -        import uuid as _uuid
      -        pool = load_pool("openai-codex")
      -        base_url = (
      -            os.getenv("HERMES_CODEX_BASE_URL", "").strip().rstrip("/")
      -            or DEFAULT_CODEX_BASE_URL
      -        )
      -        entry = PooledCredential(
      -            provider="openai-codex",
      -            id=_uuid.uuid4().hex[:6],
      -            label="dashboard device_code",
      -            auth_type=AUTH_TYPE_OAUTH,
      -            priority=0,
      -            source=f"{SOURCE_MANUAL}:dashboard_device_code",
      -            access_token=[redacted:secret-assignment]
      -            refresh_token=[redacted:secret-assignment]
      -            base_url=base_url,
      -        )
      -        pool.add_entry(entry)
      +        from hermes_cli.auth import _save_codex_tokens
      +
      +        _save_codex_tokens({
      +            "access_token": access_token,
      +            "refresh_token": refresh_token,
      +        })
               with _oauth_sessions_lock:
                   sess["status"] = "approved"
               _log.info("oauth/device: openai-codex login completed (session=%s)", session_id)
    • tests/hermes_cli/test_auth_commands.py
      @@ -303,9 +303,11 @@ class _Args:
       
           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"] == "[redacted:email]"
      -    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 @@ class _Args:
           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):
    • tests/hermes_cli/test_web_oauth_dispatch.py
      @@ -146,6 +146,67 @@ def fake_request_device_code(**kwargs):
           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 = "[redacted:secret-assignment]"
      +
      +    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

1 PR closed without merging

From the close comments: superseded by another PR 1

  • #37525 fix: select Codex runtime after desktop OAuth · superseded by another PR → #37517
Closed issues · 2
  • Closed#37515Bug: Desktop (Windows) runtime fails to resolve provider despite OAuth login succeeding and setup.status reporting credentials configured
  • Closed#49159[Bug]: Open AI Auth problem
How this cluster was grouped
  • Link evidence (partly structural links): some members are connected by structural evidence (shared changed files, or a closing/superseded reference); the rest are joined by an automated relevance judgement over their text.
  • Members outside that structural core may belong to a different underlying problem.
  • Pairwise link density 0.39: the share of item pairs that carry a link, counting a structural link in full and a relevance-only link at 0.36. 1.00 means every pair is linked.

Pick up a Lab task

Check whether a fix covers the open issues here, with your own agent. Bring back evidence for maintainers.

Open the Lab

At a glance

Subsystem
agent
Merged PRs
#37517
Closed issues
2
Attempts
1 PR closed without merging
Fix link checked
2 Jun 2026

For your agent

The same facts, typed.