summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--docs/cli/authentication.md21
-rw-r--r--packages/cli/src/gemini.tsx29
-rw-r--r--packages/cli/src/validateNonInterActiveAuth.test.ts148
-rw-r--r--packages/cli/src/validateNonInterActiveAuth.ts38
4 files changed, 209 insertions, 27 deletions
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<typeof vi.spyOn>;
+ let processExitSpy: ReturnType<typeof vi.spyOn>;
+ let refreshAuthMock: jest.MockedFunction<
+ (authType: AuthType) => Promise<unknown>
+ >;
+
+ 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;
+}