provider: ~/.gemini/oauth_creds.json is Gemini CLI's only when Gemini CLI's OAuth client minted it (#143) — a sign-in whose ID token names another client (Antigravity's) can't be refreshed as Gemini CLI's and is no longer listed as its account; agy keeps its sign-in in the system keyring, not there, so a personal account Google turns away from Gemini CLI is now told to add itself under Antigravity in magpie

This commit is contained in:
yetone committed 2026-09-28 18:01:49 +08:00
1 parent 502246c563
commit 10d4e98e19
2 files changed
+65 -3

No files matched your search

+24 -2
View File
@@ -131,10 +131,21 @@ func geminiDir() string {
return filepath.Join(home, ".gemini")
}
// geminiOwnLogin reads Gemini CLI's own Google sign-in.
// geminiOwnLogin reads Gemini CLI's own Google sign-in. One another app's
// OAuth client minted there isn't Gemini CLI's: Google refreshes a token
// only for the client that minted it, so it is left alone. (Antigravity's
// CLI, agy, keeps its sign-in in the system keyring, not here.)
func geminiOwnLogin() (googleAccount, bool) {
path := filepath.Join(geminiDir(), "oauth_creds.json")
var a googleAuth
if !readJSON(filepath.Join(geminiDir(), "oauth_creds.json"), &a) || a.RefreshToken == "" {
if !readJSON(path, &a) || a.RefreshToken == "" {
return googleAccount{}, false
}
var id struct {
IDToken string `json:"id_token"`
}
readJSON(path, &id)
if c := googleClientOf(id.IDToken); c != "" && c != geminiApp.clientID {
return googleAccount{}, false
}
var accts struct {
@@ -154,6 +165,16 @@ func geminiOwnLogin() (googleAccount, bool) {
return googleAccount{app: app, user: user, auth: a, own: true}, true
}
// googleClientOf is the OAuth client a Google ID token was minted for, ""
// when there is none to tell.
func googleClientOf(idToken string) string {
c := jwtClaims(idToken)
if azp := claimString(c, "azp"); azp != "" {
return azp
}
return claimString(c, "aud")
}
// envFileValue is one KEY=value of a dotenv file.
func envFileValue(path, key string) string {
f, err := os.Open(path)
@@ -579,6 +600,7 @@ func (g googleAccount) needsProject(why string) error {
if g.own {
msg += " or GOOGLE_CLOUD_PROJECT in ~/.gemini/.env"
}
msg += ". A personal Google account is served to Antigravity instead: add it under Antigravity in magpie"
if why != "" {
msg = why + " — " + msg
}
+41 -1
View File
@@ -3,6 +3,7 @@ package provider
import (
"bytes"
"context"
"encoding/base64"
"encoding/json"
"io"
"net/http"
@@ -126,6 +127,44 @@ func TestGeminiOwnLoginRefreshesInMemory(t *testing.T) {
}
}
// Only a sign-in Gemini CLI's own OAuth client minted is Gemini CLI's: one
// minted for another client (Antigravity's) can't be refreshed as Gemini
// CLI's and isn't listed; one without an ID token is taken as Gemini CLI's.
func TestGeminiOwnLoginOnlyGeminiCLIsClient(t *testing.T) {
idToken := func(claims map[string]any) string {
b, _ := json.Marshal(claims)
return "e30." + base64.RawURLEncoding.EncodeToString(b) + ".sig"
}
for _, c := range []struct {
name, idToken string
want bool
}{
{"no id token", "", true},
{"gemini cli", idToken(map[string]any{"aud": geminiApp.clientID, "azp": geminiApp.clientID}), true},
{"antigravity", idToken(map[string]any{"aud": antigravityApp.clientID, "azp": antigravityApp.clientID}), false},
{"antigravity aud only", idToken(map[string]any{"aud": antigravityApp.clientID}), false},
} {
t.Run(c.name, func(t *testing.T) {
googleSandbox(t, &fakeGoogle{})
dir := geminiDir()
os.MkdirAll(dir, 0o700)
creds, _ := json.Marshal(map[string]any{"access_token": "old", "refresh_token": "rt-own", "expiry_date": 1, "id_token": c.idToken})
os.WriteFile(filepath.Join(dir, "oauth_creds.json"), creds, 0o600)
os.WriteFile(filepath.Join(dir, "google_accounts.json"), []byte(`{"active":"me@example.com","old":[]}`), 0o600)
_, ok := geminiOwnLogin()
if ok != c.want {
t.Fatalf("own login found = %v, want %v", ok, c.want)
}
if n := len(googleLoginList("gemini")); n != map[bool]int{true: 1, false: 0}[c.want] {
t.Errorf("gemini lists %d accounts", n)
}
if n := len(googleLoginList("antigravity")); n != 0 {
t.Errorf("antigravity lists %d accounts", n)
}
})
}
}
// Google turns individuals away from Gemini CLI's sign-in; the account
// then needs a project, and magpie says how to name one.
func TestGeminiNeedsAProject(t *testing.T) {
@@ -136,7 +175,8 @@ func TestGeminiNeedsAProject(t *testing.T) {
g, _ := geminiOwnLogin()
_, err := g.project(context.Background())
if err == nil || !strings.Contains(err.Error(), "magpie accounts project gemini me@example.com") ||
!strings.Contains(err.Error(), "no longer supported") || !strings.Contains(err.Error(), "~/.gemini/.env") {
!strings.Contains(err.Error(), "no longer supported") || !strings.Contains(err.Error(), "~/.gemini/.env") ||
!strings.Contains(err.Error(), "add it under Antigravity in magpie") {
t.Fatalf("err = %v", err)
}
if len(f.onboards) != 0 {