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:
2 files changed
+65
-3
No files matched your search
@@ -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
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in new issue
Block a user