Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 34 additions & 1 deletion packages/cli/src/config/settings.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1674,6 +1674,7 @@
isWorkspaceTrustedValue = true,
}) {
delete process.env['TESTTEST']; // reset
delete process.env['GEMINI_API_KEY']; // reset
const geminiEnvPath = path.resolve(path.join(GEMINI_DIR, '.env'));

vi.mocked(isWorkspaceTrusted).mockReturnValue({
Expand All @@ -1700,7 +1701,8 @@
(p: fs.PathOrFileDescriptor) => {
if (p === USER_SETTINGS_PATH)
return JSON.stringify(userSettingsContent);
if (p === geminiEnvPath) return 'TESTTEST=1234';
if (p === geminiEnvPath)
return 'TESTTEST=1234\nGEMINI_API_KEY=test-key';
return '{}';
},
);
Expand All @@ -1711,13 +1713,44 @@
loadEnvironment(loadSettings(MOCK_WORKSPACE_DIR).merged);

expect(process.env['TESTTEST']).toEqual('1234');
expect(process.env['GEMINI_API_KEY']).toEqual('test-key');
});

it('does not load env files from untrusted spaces', () => {
setup({ isFolderTrustEnabled: true, isWorkspaceTrustedValue: false });
loadEnvironment(loadSettings(MOCK_WORKSPACE_DIR).merged);

expect(process.env['TESTTEST']).not.toEqual('1234');
expect(process.env['GEMINI_API_KEY']).not.toEqual('test-key');
});

it('loads whitelisted env files from untrusted spaces if sandboxing is enabled', () => {
setup({ isFolderTrustEnabled: true, isWorkspaceTrustedValue: false });
const settings = loadSettings(MOCK_WORKSPACE_DIR);
settings.merged.tools!.sandbox = true;

Check failure on line 1730 in packages/cli/src/config/settings.test.ts

View workflow job for this annotation

GitHub Actions / Lint

This assertion is unnecessary since it does not change the type of the expression
loadEnvironment(settings.merged);

// GEMINI_API_KEY is in the whitelist, so it should be loaded.
expect(process.env['GEMINI_API_KEY']).toEqual('test-key');
// TESTTEST is NOT in the whitelist, so it should be blocked.
expect(process.env['TESTTEST']).not.toEqual('1234');
});

it('loads whitelisted env files from untrusted spaces if sandboxing is enabled via CLI flag', () => {
const originalArgv = [...process.argv];
process.argv.push('-s');
try {
setup({ isFolderTrustEnabled: true, isWorkspaceTrustedValue: false });
const settings = loadSettings(MOCK_WORKSPACE_DIR);
// Ensure sandbox is NOT in settings to test argv sniffing
settings.merged.tools!.sandbox = undefined;

Check failure on line 1746 in packages/cli/src/config/settings.test.ts

View workflow job for this annotation

GitHub Actions / Lint

This assertion is unnecessary since it does not change the type of the expression
loadEnvironment(settings.merged);

expect(process.env['GEMINI_API_KEY']).toEqual('test-key');
expect(process.env['TESTTEST']).not.toEqual('1234');
} finally {
process.argv = originalArgv;
}
});
});

Expand Down
26 changes: 25 additions & 1 deletion packages/cli/src/config/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,13 @@ export const USER_SETTINGS_PATH = Storage.getGlobalSettingsPath();
export const USER_SETTINGS_DIR = path.dirname(USER_SETTINGS_PATH);
export const DEFAULT_EXCLUDED_ENV_VARS = ['DEBUG', 'DEBUG_MODE'];

const AUTH_ENV_VAR_WHITELIST = [
'GEMINI_API_KEY',
'GOOGLE_API_KEY',
'GOOGLE_CLOUD_PROJECT',
'GOOGLE_CLOUD_LOCATION',
];

export function getSystemSettingsPath(): string {
if (process.env['GEMINI_CLI_SYSTEM_SETTINGS_PATH']) {
return process.env['GEMINI_CLI_SYSTEM_SETTINGS_PATH'];
Expand Down Expand Up @@ -424,7 +431,15 @@ export function setUpCloudShellEnvironment(envFilePath: string | null): void {
export function loadEnvironment(settings: Settings): void {
const envFilePath = findEnvFile(process.cwd());

if (!isWorkspaceTrusted(settings).isTrusted) {
const isTrusted = isWorkspaceTrusted(settings).isTrusted;
// Check settings OR check process.argv directly since this might be called
// before arguments are fully parsed.
const isSandboxed =
!!settings.tools?.sandbox ||
process.argv.includes('-s') ||
process.argv.includes('--sandbox');

if (!isTrusted && !isSandboxed) {
return;
}

Expand All @@ -446,6 +461,15 @@ export function loadEnvironment(settings: Settings): void {

for (const key in parsedEnv) {
if (Object.hasOwn(parsedEnv, key)) {
// If the workspace is untrusted but we are sandboxed, only allow whitelisted variables.
if (
!isTrusted &&
isSandboxed &&
!AUTH_ENV_VAR_WHITELIST.includes(key)
) {
continue;
}

// If it's a project .env file, skip loading excluded variables.
if (isProjectEnvFile && excludedVars.includes(key)) {
continue;
Expand Down
64 changes: 64 additions & 0 deletions packages/cli/src/config/verification.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';

Check failure on line 1 in packages/cli/src/config/verification.test.ts

View workflow job for this annotation

GitHub Actions / Lint

No header found
import * as fs from 'node:fs';
import * as path from 'node:path';
import { loadEnvironment, loadSettings, getSettingsSchema } from './settings.js';
import { validateAuthMethod } from './auth.js';
import { isWorkspaceTrusted } from './trustedFolders.js';
import { AuthType } from '@google/gemini-cli-core';

vi.mock('./trustedFolders.js', () => ({
isWorkspaceTrusted: vi.fn(),
isFolderTrustEnabled: vi.fn(),
}));

vi.mock('node:fs', async (importOriginal) => {
const actual = await importOriginal<typeof fs>();
return {
...actual,
existsSync: vi.fn(),
readFileSync: vi.fn(),
realpathSync: vi.fn().mockImplementation((p) => p),
};
});

describe('Verification: Auth and Trust Interaction', () => {
beforeEach(() => {
vi.stubEnv('GEMINI_API_KEY', '');
vi.resetAllMocks();
vi.mocked(isWorkspaceTrusted).mockReturnValue({ isTrusted: true, source: 'file' });
});

afterEach(() => {
vi.unstubAllEnvs();
});

it('should verify loadEnvironment returns early and validateAuthMethod fails when untrusted', () => {
// 1. Mock untrusted workspace
vi.mocked(isWorkspaceTrusted).mockReturnValue({ isTrusted: false, source: 'file' });

// 2. Mock .env file existence
const envPath = path.resolve(process.cwd(), '.env');
vi.mocked(fs.existsSync).mockImplementation((p) => p === envPath);
vi.mocked(fs.readFileSync).mockImplementation((p) => {
if (p === envPath) return 'GEMINI_API_KEY=shhh-secret';
return '';
});

// 3. Load environment (should return early)
const settings = loadSettings(process.cwd());
loadEnvironment(settings.merged);

// 4. Verify env var NOT loaded
expect(process.env['GEMINI_API_KEY']).toBe('');

// 5. Verify validateAuthMethod fails
const result = validateAuthMethod(AuthType.USE_GEMINI);
expect(result).toContain('you must specify the GEMINI_API_KEY environment variable');
});

it('should identify if sandbox flag is available in Settings', () => {
const schema = getSettingsSchema();
expect(schema.tools.properties).toBeDefined();
expect('sandbox' in schema.tools.properties!).toBe(true);

Check failure on line 62 in packages/cli/src/config/verification.test.ts

View workflow job for this annotation

GitHub Actions / Lint

This assertion is unnecessary since it does not change the type of the expression
});
});
Loading