diff --git a/backend/internal/pkg/geminicli/constants.go b/backend/internal/pkg/geminicli/constants.go index 8dd8c645..63f48727 100644 --- a/backend/internal/pkg/geminicli/constants.go +++ b/backend/internal/pkg/geminicli/constants.go @@ -22,11 +22,19 @@ const ( // DefaultScopes for AI Studio (uses generativelanguage API with OAuth) // Reference: https://ai.google.dev/gemini-api/docs/oauth // For regular Google accounts, supports API calls to generativelanguage.googleapis.com - DefaultAIStudioScopes = "https://www.googleapis.com/auth/cloud-platform https://www.googleapis.com/auth/generative-language" + // Note: Google Auth platform currently documents the OAuth scope as + // https://www.googleapis.com/auth/generative-language.retriever (often with cloud-platform). + DefaultAIStudioScopes = "https://www.googleapis.com/auth/cloud-platform https://www.googleapis.com/auth/generative-language.retriever" // GeminiCLIRedirectURI is the redirect URI used by Gemini CLI for Code Assist OAuth. GeminiCLIRedirectURI = "https://codeassist.google.com/authcode" + // GeminiCLIOAuthClientID/Secret are the public OAuth client credentials used by Google Gemini CLI. + // They enable the "login without creating your own OAuth client" experience, but Google may + // restrict which scopes are allowed for this client. + GeminiCLIOAuthClientID = "681255809395-oo8ft2oprdrnp9e3aqf6av3hmdib135j.apps.googleusercontent.com" + GeminiCLIOAuthClientSecret = "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" + SessionTTL = 30 * time.Minute // GeminiCLIUserAgent mimics Gemini CLI to maximize compatibility with internal endpoints. diff --git a/backend/internal/pkg/geminicli/oauth.go b/backend/internal/pkg/geminicli/oauth.go index e502de65..f93d99b9 100644 --- a/backend/internal/pkg/geminicli/oauth.go +++ b/backend/internal/pkg/geminicli/oauth.go @@ -140,9 +140,13 @@ func base64URLEncode(data []byte) string { } // EffectiveOAuthConfig returns the effective OAuth configuration. -// oauthType: "code_assist" or "ai_studio" (defaults to "code_assist" if empty) -// Returns error if ClientID or ClientSecret is not configured. -// Configure via GEMINI_OAUTH_CLIENT_ID and GEMINI_OAUTH_CLIENT_SECRET environment variables. +// oauthType: "code_assist" or "ai_studio" (defaults to "code_assist" if empty). +// +// If ClientID/ClientSecret is not provided, this falls back to the built-in Gemini CLI OAuth client. +// +// Note: The built-in Gemini CLI OAuth client is restricted and may reject some scopes (e.g. +// https://www.googleapis.com/auth/generative-language), which will surface as +// "restricted_client" / "Unregistered scope(s)" errors during browser authorization. func EffectiveOAuthConfig(cfg OAuthConfig, oauthType string) (OAuthConfig, error) { effective := OAuthConfig{ ClientID: strings.TrimSpace(cfg.ClientID), @@ -150,19 +154,61 @@ func EffectiveOAuthConfig(cfg OAuthConfig, oauthType string) (OAuthConfig, error Scopes: strings.TrimSpace(cfg.Scopes), } - // Require OAuth credentials to be configured - if effective.ClientID == "" || effective.ClientSecret == "" { - return OAuthConfig{}, fmt.Errorf("gemini OAuth credentials not configured, set GEMINI_OAUTH_CLIENT_ID and GEMINI_OAUTH_CLIENT_SECRET environment variables") + // Normalize scopes: allow comma-separated input but send space-delimited scopes to Google. + if effective.Scopes != "" { + effective.Scopes = strings.Join(strings.Fields(strings.ReplaceAll(effective.Scopes, ",", " ")), " ") } + // Fall back to built-in Gemini CLI OAuth client when not configured. + if effective.ClientID == "" && effective.ClientSecret == "" { + effective.ClientID = GeminiCLIOAuthClientID + effective.ClientSecret = GeminiCLIOAuthClientSecret + } else if effective.ClientID == "" || effective.ClientSecret == "" { + return OAuthConfig{}, fmt.Errorf("OAuth client not configured: please set both client_id and client_secret (or leave both empty to use the built-in Gemini CLI client)") + } + + isBuiltinClient := effective.ClientID == GeminiCLIOAuthClientID && + effective.ClientSecret == GeminiCLIOAuthClientSecret + if effective.Scopes == "" { // Use different default scopes based on OAuth type if oauthType == "ai_studio" { - effective.Scopes = DefaultAIStudioScopes + // Built-in client can't request some AI Studio scopes (notably generative-language). + if isBuiltinClient { + effective.Scopes = DefaultCodeAssistScopes + } else { + effective.Scopes = DefaultAIStudioScopes + } } else { // Default to Code Assist scopes effective.Scopes = DefaultCodeAssistScopes } + } else if oauthType == "ai_studio" && isBuiltinClient { + // If user overrides scopes while still using the built-in client, strip restricted scopes. + parts := strings.Fields(effective.Scopes) + filtered := make([]string, 0, len(parts)) + for _, s := range parts { + if strings.Contains(s, "generative-language") { + continue + } + filtered = append(filtered, s) + } + if len(filtered) == 0 { + effective.Scopes = DefaultCodeAssistScopes + } else { + effective.Scopes = strings.Join(filtered, " ") + } + } + + // Backward compatibility: normalize older AI Studio scope to the currently documented one. + if oauthType == "ai_studio" && effective.Scopes != "" { + parts := strings.Fields(effective.Scopes) + for i := range parts { + if parts[i] == "https://www.googleapis.com/auth/generative-language" { + parts[i] = "https://www.googleapis.com/auth/generative-language.retriever" + } + } + effective.Scopes = strings.Join(parts, " ") } return effective, nil