From 7cfdf967e1dd11acfd3292e3fb4376cb06f4b619 Mon Sep 17 00:00:00 2001 From: Debug Agent Date: Wed, 27 May 2026 09:30:07 +0200 Subject: [PATCH] fix(acp): select gemini-cli 'oauth-personal' auth from cached creds _select_auth_method special-cased codex's 'chatgpt' subscription login (checks ~/.codex/auth.json) but had no handling for gemini-cli's 'oauth-personal' method. So a gemini-cli logged in via Google OAuth (no GEMINI_API_KEY) never received an authenticate() call and session creation failed with a JSON-RPC -32603, even though valid cached credentials existed. Mirror the chatgpt path: when the server offers 'oauth-personal' and ~/.gemini/oauth_creds.json is present, select it (preferred over GEMINI_API_KEY, consistent with chatgpt-over-OPENAI_API_KEY). In a server image the creds file is absent, so the GEMINI_API_KEY fallback is unaffected. Verified end-to-end against the pinned @google/gemini-cli@0.38.0: the SDK now authenticates via oauth-personal and creates a session with no API key. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../openhands/sdk/agent/acp_agent.py | 26 +++++++--- tests/sdk/agent/test_acp_agent.py | 48 +++++++++++++++++++ 2 files changed, 67 insertions(+), 7 deletions(-) diff --git a/openhands-sdk/openhands/sdk/agent/acp_agent.py b/openhands-sdk/openhands/sdk/agent/acp_agent.py index 5f9883954c..895d3675c6 100644 --- a/openhands-sdk/openhands/sdk/agent/acp_agent.py +++ b/openhands-sdk/openhands/sdk/agent/acp_agent.py @@ -175,6 +175,10 @@ def _make_dummy_llm() -> LLM: "gemini-api-key": "GEMINI_API_KEY", } _CHATGPT_AUTH_PATH = Path(".codex") / "auth.json" +# Gemini CLI personal (Google OAuth) login, cached by ``gemini login`` / +# ``gemini --acp``. Its presence lets us select the server's ``oauth-personal`` +# auth method without an API key (mirrors the ChatGPT subscription path). +_GEMINI_OAUTH_PATH = Path(".gemini") / "oauth_creds.json" def _select_auth_method( @@ -186,15 +190,23 @@ def _select_auth_method( Returns the ``id`` of the first matching method, or ``None`` if no supported credential source is available (the server may not require auth). - ChatGPT subscription login (device-code flow stored in - ``~/.codex/auth.json``) is checked first so it takes precedence over - explicit API keys, which serve as the fallback. + Subscription / OAuth logins (whose cached credential file is present) are + checked first so they take precedence over explicit API keys, which serve + as the fallback: + + - ``chatgpt`` (codex-acp) — ``~/.codex/auth.json`` + - ``oauth-personal`` (gemini-cli) — ``~/.gemini/oauth_creds.json`` + + In a server image these files are absent (no interactive login), so the + API-key fallback (e.g. ``GEMINI_API_KEY``) is used instead. """ method_ids = {m.id for m in auth_methods} - # Prefer ChatGPT subscription login when the auth file is present. - if "chatgpt" in method_ids: - if (Path.home() / _CHATGPT_AUTH_PATH).is_file(): - return "chatgpt" + # Prefer subscription / OAuth logins when their cached credential file is + # present. + if "chatgpt" in method_ids and (Path.home() / _CHATGPT_AUTH_PATH).is_file(): + return "chatgpt" + if "oauth-personal" in method_ids and (Path.home() / _GEMINI_OAUTH_PATH).is_file(): + return "oauth-personal" # Fall back to explicit API key env vars. for method_id, env_var in _AUTH_METHOD_ENV_MAP.items(): if method_id in method_ids and env_var in env: diff --git a/tests/sdk/agent/test_acp_agent.py b/tests/sdk/agent/test_acp_agent.py index 7f0bd2f689..b711cb7ba8 100644 --- a/tests/sdk/agent/test_acp_agent.py +++ b/tests/sdk/agent/test_acp_agent.py @@ -3161,6 +3161,54 @@ def test_chatgpt_auth_file(self, tmp_path): with patch("openhands.sdk.agent.acp_agent.Path.home", return_value=tmp_path): assert _select_auth_method(methods, {}) == "chatgpt" + def test_gemini_oauth_personal_when_creds_file_present(self, tmp_path): + """gemini-cli's OAuth login is selected when ~/.gemini/oauth_creds.json + exists, with no GEMINI_API_KEY needed.""" + methods = [ + self._make_auth_method("oauth-personal"), + self._make_auth_method("gemini-api-key"), + ] + gem_dir = tmp_path / ".gemini" + gem_dir.mkdir() + (gem_dir / "oauth_creds.json").write_text("{}", encoding="utf-8") + + with patch("openhands.sdk.agent.acp_agent.Path.home", return_value=tmp_path): + assert _select_auth_method(methods, {}) == "oauth-personal" + + def test_gemini_oauth_preferred_over_api_key(self, tmp_path): + """OAuth login takes precedence over GEMINI_API_KEY (mirrors chatgpt).""" + methods = [ + self._make_auth_method("oauth-personal"), + self._make_auth_method("gemini-api-key"), + ] + gem_dir = tmp_path / ".gemini" + gem_dir.mkdir() + (gem_dir / "oauth_creds.json").write_text("{}", encoding="utf-8") + + env = {"GEMINI_API_KEY": "g-test"} + with patch("openhands.sdk.agent.acp_agent.Path.home", return_value=tmp_path): + assert _select_auth_method(methods, env) == "oauth-personal" + + def test_gemini_api_key_fallback_when_no_oauth_file(self, tmp_path): + """Falls back to GEMINI_API_KEY when oauth-personal is offered but the + creds file is absent (e.g. in a server image).""" + methods = [ + self._make_auth_method("oauth-personal"), + self._make_auth_method("gemini-api-key"), + ] + env = {"GEMINI_API_KEY": "g-test"} + with patch("openhands.sdk.agent.acp_agent.Path.home", return_value=tmp_path): + assert _select_auth_method(methods, env) == "gemini-api-key" + + def test_gemini_oauth_offered_but_no_creds_no_key(self, tmp_path): + """oauth-personal offered, no creds file and no API key -> None.""" + methods = [ + self._make_auth_method("oauth-personal"), + self._make_auth_method("gemini-api-key"), + ] + with patch("openhands.sdk.agent.acp_agent.Path.home", return_value=tmp_path): + assert _select_auth_method(methods, {}) is None + def test_empty_auth_methods(self): assert _select_auth_method([], {}) is None