{"repo":"NousResearch/hermes-agent","public_id":"37515","display_label":"Codex desktop OAuth login and runtime issues","summary":"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.","subsystem":"agent","counts":{"members":4,"open_issues":0,"open_issues_distinct":0,"open_prs":0,"closed_issues":2,"merged_prs":1,"attempt_prs":1},"landed_fix":{"status":"merged_to_main","basis":"structural","pr":37517},"resolution":{"state":"unresolved","by":null},"freshness":{"state":"verified","last_verified_at":"2026-06-02T17:19:45+00:00","is_stale":false},"grouping":{"backbone":"STRUCTURAL_PARTIAL","link_density":0.3933333333333333},"fix_edges":[{"issue_number":37515,"pr_number":37517,"tier":"verified","basis":"superseded_ref","confidence":0.9,"recoverable_from_timeline":false}],"members":[{"kind":"issue","number":37515,"title":"Bug: Desktop (Windows) runtime fails to resolve provider despite OAuth login succeeding and setup.status reporting credentials configured","url":null,"state":"closed"},{"kind":"issue","number":49159,"title":"[Bug]:  Open AI Auth problem","url":null,"state":"closed"},{"kind":"pr","number":37517,"title":"fix(auth): align Codex OAuth persistence paths","url":null,"state":"merged","close_reason":{"category":"merged","implication":"superseded"},"merged":true,"is_fix_pr":true,"fix_tier":"verified","files_changed":["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"],"fix_hunks":[{"file":"agent/credential_pool.py","hunks":["@@ -1891,6 +1891,7 @@ def _env_val(key: str) -> str:\n         # via `hermes auth openai-codex`.\n         if isinstance(tokens, dict) and tokens.get(\"access_token\"):\n             active_sources.add(\"device_code\")\n+            custom_label = str(state.get(\"label\") or \"\").strip()\n             changed |= _upsert_entry(\n                 entries,\n                 provider,","@@ -1902,7 +1903,7 @@ def _env_val(key: str) -> str:\n                     \"refresh_token\": tokens.get(\"refresh_token\"),\n                     \"base_url\": \"https://chatgpt.com/backend-api/codex\",\n                     \"last_refresh\": state.get(\"last_refresh\"),\n-                    \"label\": label_from_token(tokens.get(\"access_token\", \"\"), \"device_code\"),\n+                    \"label\": custom_label or label_from_token(tokens.get(\"access_token\", \"\"), \"device_code\"),\n                 },\n             )\n "]},{"file":"hermes_cli/auth.py","hunks":["@@ -3370,7 +3370,7 @@ def _sync_codex_pool_entries(\n         entry[\"last_error_reset_at\"] = None\n \n \n-def _save_codex_tokens(tokens: [redacted:secret-assignment] str], last_refresh: str = None) -> None:\n+def _save_codex_tokens(tokens: [redacted:secret-assignment] str], last_refresh: str = None, label: str = None) -> None:\n     \"\"\"Save Codex OAuth tokens to Hermes auth store (~/.hermes/auth.json).\"\"\"\n     if last_refresh is None:\n         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\n         state[\"tokens\"] = tokens\n         state[\"last_refresh\"] = last_refresh\n         state[\"auth_mode\"] = \"chatgpt\"\n+        if label and str(label).strip():\n+            state[\"label\"] = str(label).strip()\n         _save_provider_state(auth_store, \"openai-codex\", state)\n         _sync_codex_pool_entries(auth_store, tokens, last_refresh)\n         _save_auth_store(auth_store)"]},{"file":"hermes_cli/auth_commands.py","hunks":["@@ -307,28 +307,20 @@ def auth_add_command(args) -> None:\n         return\n \n     if provider == \"openai-codex\":\n-        # Clear any existing suppression marker so a re-link after `hermes auth\n-        # remove openai-codex` works without the new tokens being skipped.\n-        auth_mod.unsuppress_credential_source(provider, \"device_code\")\n         creds = auth_mod._codex_device_code_login()\n         label = (getattr(args, \"label\", None) or \"\").strip() or label_from_token(\n             creds[\"tokens\"][\"access_token\"],\n             _oauth_default_label(provider, len(pool.entries()) + 1),\n         )\n-        entry = PooledCredential(\n-            provider=provider,\n-            id=uuid.uuid4().hex[:6],\n-            label=label,\n-            auth_type=AUTH_TYPE_OAUTH,\n-            priority=0,\n-            source=f\"{SOURCE_MANUAL}:device_code\",\n-            access_token=[redacted:secret-assignment]\"tokens\"][\"access_token\"],\n-            refresh_token=[redacted:secret-assignment]\"tokens\"].get(\"refresh_token\"),\n-            base_url=creds.get(\"base_url\"),\n+        auth_mod._save_codex_tokens(\n+            creds[\"tokens\"],\n             last_refresh=creds.get(\"last_refresh\"),\n+            label=label,\n         )\n-        pool.add_entry(entry)\n-        print(f'Added {provider} OAuth credential #{len(pool.entries())}: \"{entry.label}\"')\n+        pool = load_pool(provider)\n+        entry = next((item for item in pool.entries() if item.source == \"device_code\"), None)\n+        shown_label = entry.label if entry is not None else label\n+        print(f'Saved {provider} OAuth device-code credentials: \"[redacted:secret-assignment]\"')\n         return\n \n     if provider == \"xai-oauth\":"]},{"file":"hermes_cli/web_server.py","hunks":["@@ -3717,31 +3717,12 @@ def _codex_full_login_worker(session_id: str) -> None:\n         if not access_token:\n             raise RuntimeError(\"token exchange did not return access_token\")\n \n-        # Persist via credential pool — same shape as auth_commands.add_command\n-        from agent.credential_pool import (\n-            PooledCredential,\n-            load_pool,\n-            AUTH_TYPE_OAUTH,\n-            SOURCE_MANUAL,\n-        )\n-        import uuid as _uuid\n-        pool = load_pool(\"openai-codex\")\n-        base_url = (\n-            os.getenv(\"HERMES_CODEX_BASE_URL\", \"\").strip().rstrip(\"/\")\n-            or DEFAULT_CODEX_BASE_URL\n-        )\n-        entry = PooledCredential(\n-            provider=\"openai-codex\",\n-            id=_uuid.uuid4().hex[:6],\n-            label=\"dashboard device_code\",\n-            auth_type=AUTH_TYPE_OAUTH,\n-            priority=0,\n-            source=f\"{SOURCE_MANUAL}:dashboard_device_code\",\n-            access_token=[redacted:secret-assignment]\n-            refresh_token=[redacted:secret-assignment]\n-            base_url=base_url,\n-        )\n-        pool.add_entry(entry)\n+        from hermes_cli.auth import _save_codex_tokens\n+\n+        _save_codex_tokens({\n+            \"access_token\": access_token,\n+            \"refresh_token\": refresh_token,\n+        })\n         with _oauth_sessions_lock:\n             sess[\"status\"] = \"approved\"\n         _log.info(\"oauth/device: openai-codex login completed (session=%s)\", session_id)"]},{"file":"tests/hermes_cli/test_auth_commands.py","hunks":["@@ -303,9 +303,11 @@ class _Args:\n \n     payload = json.loads((tmp_path / \"hermes\" / \"auth.json\").read_text())\n     entries = payload[\"credential_pool\"][\"openai-codex\"]\n-    entry = next(item for item in entries if item[\"source\"] == \"manual:device_code\")\n+    entry = next(item for item in entries if item[\"source\"] == \"device_code\")\n+    assert payload[\"active_provider\"] == \"openai-codex\"\n+    assert payload[\"providers\"][\"openai-codex\"][\"tokens\"][\"access_token\"] == token\n     assert entry[\"label\"] == \"[redacted:email]\"\n-    assert entry[\"source\"] == \"manual:device_code\"\n+    assert entry[\"source\"] == \"device_code\"\n     assert entry[\"refresh_token\"] == \"refresh-token\"\n     assert entry[\"base_url\"] == \"https://chatgpt.com/backend-api/codex\"\n ","@@ -1129,10 +1131,6 @@ def test_auth_remove_codex_manual_source_suppresses_reseed(tmp_path, monkeypatch\n def test_auth_add_codex_clears_suppression_marker(tmp_path, monkeypatch):\n     \"\"\"Re-linking codex via `hermes auth add openai-codex` must clear any suppression marker.\"\"\"\n     monkeypatch.setenv(\"HERMES_HOME\", str(tmp_path / \"hermes\"))\n-    monkeypatch.setattr(\n-        \"agent.credential_pool._seed_from_singletons\",\n-        lambda provider, entries: (False, set()),\n-    )\n     hermes_home = tmp_path / \"hermes\"\n     hermes_home.mkdir(parents=True, exist_ok=True)\n ","@@ -1171,7 +1169,8 @@ class _Args:\n     assert \"openai-codex\" not in payload.get(\"suppressed_sources\", {})\n     # New pool entry must be present\n     entries = payload[\"credential_pool\"][\"openai-codex\"]\n-    assert any(e[\"source\"] == \"manual:device_code\" for e in entries)\n+    assert any(e[\"source\"] == \"device_code\" for e in entries)\n+    assert payload[\"active_provider\"] == \"openai-codex\"\n \n \n def test_seed_from_singletons_respects_codex_suppression(tmp_path, monkeypatch):"]},{"file":"tests/hermes_cli/test_web_oauth_dispatch.py","hunks":["@@ -146,6 +146,67 @@ def fake_request_device_code(**kwargs):\n     assert requested_scopes == [auth_mod.DEFAULT_NOUS_SCOPE]\n \n \n+def test_codex_dashboard_worker_persists_runtime_provider(tmp_path, monkeypatch):\n+    from hermes_cli import web_server as ws\n+    from hermes_cli.auth import get_active_provider\n+    from hermes_cli.runtime_provider import resolve_runtime_provider\n+\n+    access_token = \"[redacted:secret-assignment]\"\n+\n+    class _Resp:\n+        def __init__(self, status_code, payload):\n+            self.status_code = status_code\n+            self._payload = payload\n+\n+        def json(self):\n+            return self._payload\n+\n+    class _Client:\n+        def __init__(self, *args, **kwargs):\n+            pass\n+\n+        def __enter__(self):\n+            return self\n+\n+        def __exit__(self, *args):\n+            return False\n+\n+        def post(self, url, **kwargs):\n+            if url.endswith(\"/deviceauth/usercode\"):\n+                return _Resp(200, {\n+                    \"device_auth_id\": \"device-auth-id\",\n+                    \"interval\": 3,\n+                    \"user_code\": \"CODEX-1234\",\n+                })\n+            if url.endswith(\"/deviceauth/token\"):\n+                return _Resp(200, {\n+                    \"authorization_code\": \"authorization-code\",\n+                    \"code_verifier\": \"code-verifier\",\n+                })\n+            return _Resp(200, {\n+                \"access_token\": access_token,\n+                \"refresh_token\": \"codex-refresh\",\n+            })\n+\n+    monkeypatch.setenv(\"HERMES_HOME\", str(tmp_path))\n+    monkeypatch.setattr(httpx, \"Client\", _Client)\n+    monkeypatch.setattr(ws.time, \"sleep\", lambda _: None)\n+\n+    sid, _ = ws._new_oauth_session(\"openai-codex\", \"device_code\")\n+    try:\n+        ws._codex_full_login_worker(sid)\n+\n+        assert ws._oauth_sessions[sid][\"status\"] == \"approved\"\n+        assert get_active_provider() == \"openai-codex\"\n+\n+        runtime = resolve_runtime_provider(requested=None)\n+        assert runtime[\"provider\"] == \"openai-codex\"\n+        assert runtime[\"api_key\"] == access_token\n+        assert runtime[\"api_mode\"] == \"codex_responses\"\n+    finally:\n+        ws._oauth_sessions.pop(sid, None)\n+\n+\n def test_nous_dashboard_poller_preserves_effective_scope_when_token_omits_scope(monkeypatch):\n     from hermes_cli import auth as auth_mod\n     from hermes_cli import web_server as ws\n"]}]},{"kind":"pr","number":37525,"title":"fix: select Codex runtime after desktop OAuth","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#37517","number":37517}],"category":"superseded_by_pr","implication":"superseded"},"merged":false}],"trust":{"default_tier":"verified","includes_suggested":false}}