diff --git a/docs/cli/authentication.md b/docs/cli/authentication.md index 3b452a8f..03fa73e1 100644 --- a/docs/cli/authentication.md +++ b/docs/cli/authentication.md @@ -107,3 +107,24 @@ GOOGLE_CLOUD_PROJECT="your-project-id" GEMINI_API_KEY="your-gemini-api-key" EOF ``` + +## Non-Interactive Mode / Headless Environments + +When running the Gemini CLI in a non-interactive environment, you cannot use the interactive login flow. +Instead, you must configure authentication using environment variables. + +The CLI will automatically detect if it is running in a non-interactive terminal and will use one of the +following authentication methods if available: + +1. **Gemini API Key:** + - Set the `GEMINI_API_KEY` environment variable. + - The CLI will use this key to authenticate with the Gemini API. + +2. **Vertex AI:** + - Set the `GOOGLE_GENAI_USE_VERTEXAI=true` environment variable. + - **Using an API Key:** Set the `GOOGLE_API_KEY` environment variable. + - **Using Application Default Credentials (ADC):** + - Run `gcloud auth application-default login` in your environment to configure ADC. + - Ensure the `GOOGLE_CLOUD_PROJECT` and `GOOGLE_CLOUD_LOCATION` environment variables are set. + +If none of these environment variables are set in a non-interactive session, the CLI will exit with an error. diff --git a/packages/cli/src/gemini.tsx b/packages/cli/src/gemini.tsx index 46d9dfce..2023431f 100644 --- a/packages/cli/src/gemini.tsx +++ b/packages/cli/src/gemini.tsx @@ -17,7 +17,6 @@ import { start_sandbox } from './utils/sandbox.js'; import { LoadedSettings, loadSettings, - USER_SETTINGS_PATH, SettingScope, } from './config/settings.js'; import { themeManager } from './ui/themes/theme-manager.js'; @@ -40,6 +39,7 @@ import { } from '@google/gemini-cli-core'; import { validateAuthMethod } from './config/auth.js'; import { setMaxSizedBoxDebugging } from './ui/components/shared/MaxSizedBox.js'; +import { validateNonInteractiveAuth } from './validateNonInterActiveAuth.js'; function getNodeMemoryArgs(config: Config): string[] { const totalMemoryMB = os.totalmem() / (1024 * 1024); @@ -320,33 +320,8 @@ async function loadNonInteractiveConfig( await finalConfig.initialize(); } - return await validateNonInterActiveAuth( + return await validateNonInteractiveAuth( settings.merged.selectedAuthType, finalConfig, ); } - -async function validateNonInterActiveAuth( - selectedAuthType: AuthType | undefined, - nonInteractiveConfig: Config, -) { - // making a special case for the cli. many headless environments might not have a settings.json set - // so if GEMINI_API_KEY is set, we'll use that. However since the oauth things are interactive anyway, we'll - // still expect that exists - if (!selectedAuthType && !process.env.GEMINI_API_KEY) { - console.error( - `Please set an Auth method in your ${USER_SETTINGS_PATH} OR specify GEMINI_API_KEY env variable file before running`, - ); - process.exit(1); - } - - selectedAuthType = selectedAuthType || AuthType.USE_GEMINI; - const err = validateAuthMethod(selectedAuthType); - if (err != null) { - console.error(err); - process.exit(1); - } - - await nonInteractiveConfig.refreshAuth(selectedAuthType); - return nonInteractiveConfig; -} diff --git a/packages/cli/src/validateNonInterActiveAuth.test.ts b/packages/cli/src/validateNonInterActiveAuth.test.ts new file mode 100644 index 00000000..9238bbe4 --- /dev/null +++ b/packages/cli/src/validateNonInterActiveAuth.test.ts @@ -0,0 +1,148 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; +import { + validateNonInteractiveAuth, + NonInteractiveConfig, +} from './validateNonInterActiveAuth.js'; +import { AuthType } from '@google/gemini-cli-core'; + +describe('validateNonInterActiveAuth', () => { + let originalEnvGeminiApiKey: string | undefined; + let originalEnvVertexAi: string | undefined; + let consoleErrorSpy: ReturnType; + let processExitSpy: ReturnType; + let refreshAuthMock: jest.MockedFunction< + (authType: AuthType) => Promise + >; + + beforeEach(() => { + originalEnvGeminiApiKey = process.env.GEMINI_API_KEY; + originalEnvVertexAi = process.env.GOOGLE_GENAI_USE_VERTEXAI; + delete process.env.GEMINI_API_KEY; + delete process.env.GOOGLE_GENAI_USE_VERTEXAI; + consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); + processExitSpy = vi.spyOn(process, 'exit').mockImplementation((code) => { + throw new Error(`process.exit(${code}) called`); + }); + refreshAuthMock = vi.fn().mockResolvedValue('refreshed'); + }); + + afterEach(() => { + if (originalEnvGeminiApiKey !== undefined) { + process.env.GEMINI_API_KEY = originalEnvGeminiApiKey; + } else { + delete process.env.GEMINI_API_KEY; + } + if (originalEnvVertexAi !== undefined) { + process.env.GOOGLE_GENAI_USE_VERTEXAI = originalEnvVertexAi; + } else { + delete process.env.GOOGLE_GENAI_USE_VERTEXAI; + } + vi.restoreAllMocks(); + }); + + it('exits if no auth type is configured or env vars set', async () => { + const nonInteractiveConfig: NonInteractiveConfig = { + refreshAuth: refreshAuthMock, + }; + try { + await validateNonInteractiveAuth(undefined, nonInteractiveConfig); + expect.fail('Should have exited'); + } catch (e) { + expect((e as Error).message).toContain('process.exit(1) called'); + } + expect(consoleErrorSpy).toHaveBeenCalledWith( + expect.stringContaining('Please set an Auth method'), + ); + expect(processExitSpy).toHaveBeenCalledWith(1); + }); + + it('uses USE_GEMINI if GEMINI_API_KEY is set', async () => { + process.env.GEMINI_API_KEY = 'fake-key'; + const nonInteractiveConfig: NonInteractiveConfig = { + refreshAuth: refreshAuthMock, + }; + await validateNonInteractiveAuth(undefined, nonInteractiveConfig); + expect(refreshAuthMock).toHaveBeenCalledWith(AuthType.USE_GEMINI); + }); + + it('uses USE_VERTEX_AI if GOOGLE_GENAI_USE_VERTEXAI is true (with GOOGLE_CLOUD_PROJECT and GOOGLE_CLOUD_LOCATION)', async () => { + process.env.GOOGLE_GENAI_USE_VERTEXAI = 'true'; + process.env.GOOGLE_CLOUD_PROJECT = 'test-project'; + process.env.GOOGLE_CLOUD_LOCATION = 'us-central1'; + const nonInteractiveConfig: NonInteractiveConfig = { + refreshAuth: refreshAuthMock, + }; + await validateNonInteractiveAuth(undefined, nonInteractiveConfig); + expect(refreshAuthMock).toHaveBeenCalledWith(AuthType.USE_VERTEX_AI); + }); + + it('uses USE_VERTEX_AI if GOOGLE_GENAI_USE_VERTEXAI is true and GOOGLE_API_KEY is set', async () => { + process.env.GOOGLE_GENAI_USE_VERTEXAI = 'true'; + process.env.GOOGLE_API_KEY = 'vertex-api-key'; + const nonInteractiveConfig: NonInteractiveConfig = { + refreshAuth: refreshAuthMock, + }; + await validateNonInteractiveAuth(undefined, nonInteractiveConfig); + expect(refreshAuthMock).toHaveBeenCalledWith(AuthType.USE_VERTEX_AI); + }); + + it('uses USE_VERTEX_AI if both GEMINI_API_KEY and GOOGLE_GENAI_USE_VERTEXAI are set', async () => { + process.env.GEMINI_API_KEY = 'fake-key'; + process.env.GOOGLE_GENAI_USE_VERTEXAI = 'true'; + process.env.GOOGLE_CLOUD_PROJECT = 'test-project'; + process.env.GOOGLE_CLOUD_LOCATION = 'us-central1'; + const nonInteractiveConfig: NonInteractiveConfig = { + refreshAuth: refreshAuthMock, + }; + await validateNonInteractiveAuth(undefined, nonInteractiveConfig); + expect(refreshAuthMock).toHaveBeenCalledWith(AuthType.USE_VERTEX_AI); + }); + + it('uses USE_GEMINI if GOOGLE_GENAI_USE_VERTEXAI is false, GEMINI_API_KEY is set, and project/location are available', async () => { + process.env.GOOGLE_GENAI_USE_VERTEXAI = 'false'; + process.env.GEMINI_API_KEY = 'fake-key'; + process.env.GOOGLE_CLOUD_PROJECT = 'test-project'; + process.env.GOOGLE_CLOUD_LOCATION = 'us-central1'; + const nonInteractiveConfig: NonInteractiveConfig = { + refreshAuth: refreshAuthMock, + }; + await validateNonInteractiveAuth(undefined, nonInteractiveConfig); + expect(refreshAuthMock).toHaveBeenCalledWith(AuthType.USE_GEMINI); + }); + + it('uses configuredAuthType if provided', async () => { + // Set required env var for USE_GEMINI + process.env.GEMINI_API_KEY = 'fake-key'; + const nonInteractiveConfig: NonInteractiveConfig = { + refreshAuth: refreshAuthMock, + }; + await validateNonInteractiveAuth(AuthType.USE_GEMINI, nonInteractiveConfig); + expect(refreshAuthMock).toHaveBeenCalledWith(AuthType.USE_GEMINI); + }); + + it('exits if validateAuthMethod returns error', async () => { + // Mock validateAuthMethod to return error + const mod = await import('./config/auth.js'); + vi.spyOn(mod, 'validateAuthMethod').mockReturnValue('Auth error!'); + const nonInteractiveConfig: NonInteractiveConfig = { + refreshAuth: refreshAuthMock, + }; + try { + await validateNonInteractiveAuth( + AuthType.USE_GEMINI, + nonInteractiveConfig, + ); + expect.fail('Should have exited'); + } catch (e) { + expect((e as Error).message).toContain('process.exit(1) called'); + } + expect(consoleErrorSpy).toHaveBeenCalledWith('Auth error!'); + expect(processExitSpy).toHaveBeenCalledWith(1); + }); +}); diff --git a/packages/cli/src/validateNonInterActiveAuth.ts b/packages/cli/src/validateNonInterActiveAuth.ts new file mode 100644 index 00000000..87a7f4ff --- /dev/null +++ b/packages/cli/src/validateNonInterActiveAuth.ts @@ -0,0 +1,38 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import { AuthType, Config } from '@google/gemini-cli-core'; +import { USER_SETTINGS_PATH } from './config/settings.js'; +import { validateAuthMethod } from './config/auth.js'; + +export async function validateNonInteractiveAuth( + configuredAuthType: AuthType | undefined, + nonInteractiveConfig: Config, +) { + const effectiveAuthType = + configuredAuthType || + (process.env.GOOGLE_GENAI_USE_VERTEXAI === 'true' + ? AuthType.USE_VERTEX_AI + : process.env.GEMINI_API_KEY + ? AuthType.USE_GEMINI + : undefined); + + if (!effectiveAuthType) { + console.error( + `Please set an Auth method in your ${USER_SETTINGS_PATH} or specify either the GEMINI_API_KEY or GOOGLE_GENAI_USE_VERTEXAI environment variables before running`, + ); + process.exit(1); + } + + const err = validateAuthMethod(effectiveAuthType); + if (err != null) { + console.error(err); + process.exit(1); + } + + await nonInteractiveConfig.refreshAuth(effectiveAuthType); + return nonInteractiveConfig; +}