Skip to content

fix(acp): select gemini-cli 'oauth-personal' auth from cached creds#3396

Merged
simonrosenberg merged 1 commit into
mainfrom
fix-acp-gemini-oauth-auth
May 27, 2026
Merged

fix(acp): select gemini-cli 'oauth-personal' auth from cached creds#3396
simonrosenberg merged 1 commit into
mainfrom
fix-acp-gemini-oauth-auth

Conversation

@simonrosenberg
Copy link
Copy Markdown
Collaborator

@simonrosenberg simonrosenberg commented May 27, 2026

Problem

_select_auth_method special-cases codex-acp'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 (i.e. ~/.gemini/oauth_creds.json present, no GEMINI_API_KEY) never received an authenticate() call — the SDK logged "ACP server offers auth methods ['oauth-personal', 'gemini-api-key', ...] but no matching env var is set" and session creation failed with a JSON-RPC -32603 even though valid cached credentials existed.

Fix

Mirror the chatgpt path: when the server offers oauth-personal and ~/.gemini/oauth_creds.json exists, select it. Precedence matches the established design (OAuth/subscription login is preferred over an explicit API key, exactly like chatgpt over OPENAI_API_KEY).

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"
# ... GEMINI_API_KEY fallback unchanged

Server images are unaffected: there's no interactive OAuth login there, so oauth_creds.json is absent and the GEMINI_API_KEY fallback is used exactly as before. This only fixes local/dev usage of an OAuth-logged-in gemini-cli.

Validation

  • Unit (TestSelectAuthMethod, 4 new cases): creds-file present → oauth-personal; preferred over GEMINI_API_KEY; falls back to the key when the creds file is absent; None when neither is available. Full tests/sdk/agent/test_acp_agent.py: 192 passed.
  • Real end-to-end against the pinned @google/gemini-cli@0.38.0 (the version in the agent-server Dockerfile): the SDK now logs Authenticating with ACP method: oauth-personal, with zero "no matching env var" warnings, and creates a working session with no API key. Before this change the same setup failed at session creation with -32603.

Context

Independent of #3390 (runtime switch_acp_model). Surfaced while validating ACP model switching for OpenHands/agent-canvas#769 — this auth gap was the actual reason gemini-cli was effectively "untested": the SDK couldn't even establish a gemini session under OAuth auth.

🤖 Generated with Claude Code


Agent Server images for this PR

GHCR package: https://github.com/OpenHands/agent-sdk/pkgs/container/agent-server

Variants & Base Images

Variant Architectures Base Image Docs / Tags
java amd64, arm64 eclipse-temurin:17-jdk Link
python amd64, arm64 nikolaik/python-nodejs:python3.13-nodejs22-slim Link
golang amd64, arm64 golang:1.21-bookworm Link

Pull (multi-arch manifest)

# Each variant is a multi-arch manifest supporting both amd64 and arm64
docker pull ghcr.io/openhands/agent-server:7cfdf96-python

Run

docker run -it --rm \
  -p 8000:8000 \
  --name agent-server-7cfdf96-python \
  ghcr.io/openhands/agent-server:7cfdf96-python

All tags pushed for this build

ghcr.io/openhands/agent-server:7cfdf96-golang-amd64
ghcr.io/openhands/agent-server:7cfdf967e1dd11acfd3292e3fb4376cb06f4b619-golang-amd64
ghcr.io/openhands/agent-server:fix-acp-gemini-oauth-auth-golang-amd64
ghcr.io/openhands/agent-server:7cfdf96-golang_tag_1.21-bookworm-amd64
ghcr.io/openhands/agent-server:7cfdf96-golang-arm64
ghcr.io/openhands/agent-server:7cfdf967e1dd11acfd3292e3fb4376cb06f4b619-golang-arm64
ghcr.io/openhands/agent-server:fix-acp-gemini-oauth-auth-golang-arm64
ghcr.io/openhands/agent-server:7cfdf96-golang_tag_1.21-bookworm-arm64
ghcr.io/openhands/agent-server:7cfdf96-java-amd64
ghcr.io/openhands/agent-server:7cfdf967e1dd11acfd3292e3fb4376cb06f4b619-java-amd64
ghcr.io/openhands/agent-server:fix-acp-gemini-oauth-auth-java-amd64
ghcr.io/openhands/agent-server:7cfdf96-eclipse-temurin_tag_17-jdk-amd64
ghcr.io/openhands/agent-server:7cfdf96-java-arm64
ghcr.io/openhands/agent-server:7cfdf967e1dd11acfd3292e3fb4376cb06f4b619-java-arm64
ghcr.io/openhands/agent-server:fix-acp-gemini-oauth-auth-java-arm64
ghcr.io/openhands/agent-server:7cfdf96-eclipse-temurin_tag_17-jdk-arm64
ghcr.io/openhands/agent-server:7cfdf96-python-amd64
ghcr.io/openhands/agent-server:7cfdf967e1dd11acfd3292e3fb4376cb06f4b619-python-amd64
ghcr.io/openhands/agent-server:fix-acp-gemini-oauth-auth-python-amd64
ghcr.io/openhands/agent-server:7cfdf96-nikolaik_s_python-nodejs_tag_python3.13-nodejs22-slim-amd64
ghcr.io/openhands/agent-server:7cfdf96-python-arm64
ghcr.io/openhands/agent-server:7cfdf967e1dd11acfd3292e3fb4376cb06f4b619-python-arm64
ghcr.io/openhands/agent-server:fix-acp-gemini-oauth-auth-python-arm64
ghcr.io/openhands/agent-server:7cfdf96-nikolaik_s_python-nodejs_tag_python3.13-nodejs22-slim-arm64
ghcr.io/openhands/agent-server:7cfdf96-golang
ghcr.io/openhands/agent-server:7cfdf967e1dd11acfd3292e3fb4376cb06f4b619-golang
ghcr.io/openhands/agent-server:fix-acp-gemini-oauth-auth-golang
ghcr.io/openhands/agent-server:7cfdf96-golang_tag_1.21-bookworm
ghcr.io/openhands/agent-server:7cfdf96-java
ghcr.io/openhands/agent-server:7cfdf967e1dd11acfd3292e3fb4376cb06f4b619-java
ghcr.io/openhands/agent-server:fix-acp-gemini-oauth-auth-java
ghcr.io/openhands/agent-server:7cfdf96-eclipse-temurin_tag_17-jdk
ghcr.io/openhands/agent-server:7cfdf96-python
ghcr.io/openhands/agent-server:7cfdf967e1dd11acfd3292e3fb4376cb06f4b619-python
ghcr.io/openhands/agent-server:fix-acp-gemini-oauth-auth-python
ghcr.io/openhands/agent-server:7cfdf96-nikolaik_s_python-nodejs_tag_python3.13-nodejs22-slim

About Multi-Architecture Support

  • Each variant tag (e.g., 7cfdf96-python) is a multi-arch manifest supporting both amd64 and arm64
  • Docker automatically pulls the correct architecture for your platform
  • Individual architecture tags (e.g., 7cfdf96-python-amd64) are also available if needed

_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) <noreply@anthropic.com>
@github-actions
Copy link
Copy Markdown
Contributor

Python API breakage checks — ✅ PASSED

Result:PASSED

Action log

@github-actions
Copy link
Copy Markdown
Contributor

REST API breakage checks (OpenAPI) — ✅ PASSED

Result:PASSED

Action log

Copy link
Copy Markdown
Collaborator

@all-hands-bot all-hands-bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 Taste Rating: Good taste — small, direct parity with the existing cached ChatGPT auth path.

[RISK ASSESSMENT]

  • [Overall PR] ⚠️ Risk Assessment: 🟢 LOW
    Auth-method selection only, with focused unit coverage and no dependency or public API changes.

VERDICT: ✅ Worth merging. Focused tests passed: uv run pytest tests/sdk/agent/test_acp_agent.py::TestSelectAuthMethod.

This review was created by an AI agent (OpenHands) on behalf of the user.


Was this automated review useful? React with 👍 or 👎 to this review to help us measure review quality.
Workflow run: https://github.com/OpenHands/software-agent-sdk/actions/runs/26497416747

@github-actions
Copy link
Copy Markdown
Contributor

Coverage

Coverage Report •
FileStmtsMissCoverMissing
openhands-sdk/openhands/sdk/agent
   acp_agent.py6986291%354–356, 486–487, 520, 522, 526, 530, 538, 601–602, 607, 674, 827, 830–831, 848–849, 878, 883, 901, 911, 940–943, 1140–1143, 1147–1149, 1152–1156, 1158, 1327, 1680–1682, 1719, 1723–1724, 1727, 1735–1737, 1739, 1741, 1745, 1748, 1757–1759, 1761, 1780, 1875–1876
TOTAL27847818270% 

@simonrosenberg simonrosenberg self-assigned this May 27, 2026
@simonrosenberg simonrosenberg merged commit 5c11fec into main May 27, 2026
32 checks passed
@simonrosenberg simonrosenberg deleted the fix-acp-gemini-oauth-auth branch May 27, 2026 07:36
Copy link
Copy Markdown
Collaborator

@all-hands-bot all-hands-bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ QA Report: PASS

The SDK auth-selection behavior now matches the PR goal: cached Gemini OAuth credentials select oauth-personal, while API-key fallback still works when the cache is absent.

Does this PR achieve its stated goal?

Yes. I exercised the changed SDK behavior with real acp.schema.AuthMethod objects and a temporary user home containing ~/.gemini/oauth_creds.json: on origin/main, the cached-OAuth/no-key case returned None, while the PR branch returned 'oauth-personal'. I also verified the intended precedence (oauth-personal beats GEMINI_API_KEY) and fallback behavior (gemini-api-key is still selected when no OAuth cache exists).

Phase Result
Environment Setup make build completed successfully with the uv workspace environment
CI Status ⏳ No failures observed; 23 checks successful, 4 skipped, 8 still in progress at review time
Functional Verification ✅ Before/after SDK auth-selection probe confirms the fix and fallback behavior
Functional Verification

Test 1: Gemini cached OAuth is selected before API-key fallback

Step 1 — Reproduce / establish baseline (without the fix):
Ran git checkout --detach origin/main && OPENHANDS_SUPPRESS_BANNER=1 uv run python /tmp/qa_acp_auth_probe.py:

commit: 58771eeb
cached_oauth_without_api_key: None
cached_oauth_with_api_key: 'gemini-api-key'
no_oauth_with_api_key: 'gemini-api-key'
no_oauth_no_api_key: None

This confirms the reported bug in the base branch: even when the offered ACP methods include oauth-personal and a Gemini OAuth cache file exists, the SDK does not select OAuth. With both cached OAuth and GEMINI_API_KEY, base falls through to gemini-api-key instead of preferring the cached personal OAuth login.

Step 2 — Apply the PR's changes:
Checked out fix-acp-gemini-oauth-auth at 7cfdf967e1dd11acfd3292e3fb4376cb06f4b619.

Step 3 — Re-run with the fix in place:
Ran git checkout fix-acp-gemini-oauth-auth && OPENHANDS_SUPPRESS_BANNER=1 uv run python /tmp/qa_acp_auth_probe.py:

commit: 7cfdf967
cached_oauth_without_api_key: 'oauth-personal'
cached_oauth_with_api_key: 'oauth-personal'
no_oauth_with_api_key: 'gemini-api-key'
no_oauth_no_api_key: None

This shows the fix works for the stated behavior: cached Gemini OAuth now selects oauth-personal without requiring GEMINI_API_KEY, OAuth takes precedence when both credentials are available, and the existing server-image/API-key fallback remains intact when the OAuth cache is absent.

Unable to Verify

I did not run a live gemini-cli --acp session backed by a real Google OAuth login because this QA environment does not contain valid ~/.gemini/oauth_creds.json credentials. I avoided presenting fake OAuth credentials as evidence of live Google authentication. Future QA would be stronger with AGENTS.md guidance for a credentials-independent ACP auth smoke test or a sanctioned local Gemini OAuth test fixture.

Issues Found

None.

This QA review was created by an AI agent (OpenHands) on behalf of the user.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants