Override Gemini CLI trust with VScode workspace trust when in IDE (#7433)

This commit is contained in:
shrutip90
2025-09-03 11:44:26 -07:00
committed by GitHub
parent 5ccf46b5a0
commit 7c667e100e
16 changed files with 248 additions and 30 deletions

View File

@@ -255,3 +255,62 @@ describe('isWorkspaceTrusted', () => {
expect(isWorkspaceTrusted(mockSettings)).toBe(true);
});
});
import { getIdeTrust } from '@google/gemini-cli-core';
vi.mock('@google/gemini-cli-core', async (importOriginal) => {
const actual = await importOriginal<Record<string, unknown>>();
return {
...actual,
getIdeTrust: vi.fn(),
};
});
describe('isWorkspaceTrusted with IDE override', () => {
const mockSettings: Settings = {
security: {
folderTrust: {
enabled: true,
},
},
};
it('should return true when ideTrust is true, ignoring config', () => {
vi.mocked(getIdeTrust).mockReturnValue(true);
// Even if config says don't trust, ideTrust should win.
vi.spyOn(fs, 'readFileSync').mockReturnValue(
JSON.stringify({ [process.cwd()]: TrustLevel.DO_NOT_TRUST }),
);
expect(isWorkspaceTrusted(mockSettings)).toBe(true);
});
it('should return false when ideTrust is false, ignoring config', () => {
vi.mocked(getIdeTrust).mockReturnValue(false);
// Even if config says trust, ideTrust should win.
vi.spyOn(fs, 'readFileSync').mockReturnValue(
JSON.stringify({ [process.cwd()]: TrustLevel.TRUST_FOLDER }),
);
expect(isWorkspaceTrusted(mockSettings)).toBe(false);
});
it('should fall back to config when ideTrust is undefined', () => {
vi.mocked(getIdeTrust).mockReturnValue(undefined);
vi.spyOn(fs, 'existsSync').mockReturnValue(true);
vi.spyOn(fs, 'readFileSync').mockReturnValue(
JSON.stringify({ [process.cwd()]: TrustLevel.TRUST_FOLDER }),
);
expect(isWorkspaceTrusted(mockSettings)).toBe(true);
});
it('should always return true if folderTrust setting is disabled', () => {
const settings: Settings = {
security: {
folderTrust: {
enabled: false,
},
},
};
vi.mocked(getIdeTrust).mockReturnValue(false);
expect(isWorkspaceTrusted(settings)).toBe(true);
});
});

View File

@@ -7,7 +7,11 @@
import * as fs from 'node:fs';
import * as path from 'node:path';
import { homedir } from 'node:os';
import { getErrorMessage, isWithinRoot } from '@google/gemini-cli-core';
import {
getErrorMessage,
isWithinRoot,
getIdeTrust,
} from '@google/gemini-cli-core';
import type { Settings } from './settings.js';
import stripJsonComments from 'strip-json-comments';
@@ -159,11 +163,7 @@ export function isFolderTrustEnabled(settings: Settings): boolean {
return folderTrustSetting;
}
export function isWorkspaceTrusted(settings: Settings): boolean | undefined {
if (!isFolderTrustEnabled(settings)) {
return true;
}
function getWorkspaceTrustFromLocalConfig(): boolean | undefined {
const folders = loadTrustedFolders();
if (folders.errors.length > 0) {
@@ -176,3 +176,17 @@ export function isWorkspaceTrusted(settings: Settings): boolean | undefined {
return folders.isPathTrusted(process.cwd());
}
export function isWorkspaceTrusted(settings: Settings): boolean | undefined {
if (!isFolderTrustEnabled(settings)) {
return true;
}
const ideTrust = getIdeTrust();
if (ideTrust !== undefined) {
return ideTrust;
}
// Fall back to the local user configuration
return getWorkspaceTrustFromLocalConfig();
}

View File

@@ -242,6 +242,12 @@ vi.mock('./hooks/useFolderTrust', () => ({
})),
}));
vi.mock('./hooks/useIdeTrustListener', () => ({
useIdeTrustListener: vi.fn(() => ({
needsRestart: false,
})),
}));
vi.mock('./hooks/useLogger', () => ({
useLogger: vi.fn(() => ({
getPreviousUserMessages: vi.fn().mockResolvedValue([]),

View File

@@ -27,6 +27,7 @@ import { useLoadingIndicator } from './hooks/useLoadingIndicator.js';
import { useThemeCommand } from './hooks/useThemeCommand.js';
import { useAuthCommand } from './hooks/useAuthCommand.js';
import { useFolderTrust } from './hooks/useFolderTrust.js';
import { useIdeTrustListener } from './hooks/useIdeTrustListener.js';
import { useEditorSettings } from './hooks/useEditorSettings.js';
import { useSlashCommandProcessor } from './hooks/slashCommandProcessor.js';
import { useAutoAcceptIndicator } from './hooks/useAutoAcceptIndicator.js';
@@ -230,6 +231,7 @@ const App = ({ config, settings, startupWarnings = [], version }: AppProps) => {
IdeContext | undefined
>();
const [showEscapePrompt, setShowEscapePrompt] = useState(false);
const [showIdeRestartPrompt, setShowIdeRestartPrompt] = useState(false);
const [isProcessing, setIsProcessing] = useState<boolean>(false);
const {
@@ -304,6 +306,23 @@ const App = ({ config, settings, startupWarnings = [], version }: AppProps) => {
const { isFolderTrustDialogOpen, handleFolderTrustSelect, isRestarting } =
useFolderTrust(settings, setIsTrustedFolder);
const { needsRestart: ideNeedsRestart } = useIdeTrustListener(config);
useEffect(() => {
if (ideNeedsRestart) {
// IDE trust changed, force a restart.
setShowIdeRestartPrompt(true);
}
}, [ideNeedsRestart]);
useKeypress(
(key) => {
if (key.name === 'r' || key.name === 'R') {
process.exit(0);
}
},
{ isActive: showIdeRestartPrompt },
);
const {
isAuthDialogOpen,
openAuthDialog,
@@ -1102,6 +1121,17 @@ const App = ({ config, settings, startupWarnings = [], version }: AppProps) => {
}
}}
/>
) : showIdeRestartPrompt ? (
<Box
borderStyle="round"
borderColor={Colors.AccentYellow}
paddingX={1}
>
<Text color={Colors.AccentYellow}>
Workspace trust has changed. Press &apos;r&apos; to restart
Gemini to apply the changes.
</Text>
</Box>
) : isFolderTrustDialogOpen ? (
<FolderTrustDialog
onSelect={handleFolderTrustSelect}

View File

@@ -0,0 +1,47 @@
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { useCallback, useEffect, useState, useSyncExternalStore } from 'react';
import type { Config } from '@google/gemini-cli-core';
import { ideContext } from '@google/gemini-cli-core';
/**
* This hook listens for trust status updates from the IDE companion extension.
* It provides the current trust status from the IDE and a flag indicating
* if a restart is needed because the trust state has changed.
*/
export function useIdeTrustListener(config: Config) {
const subscribe = useCallback(
(onStoreChange: () => void) => {
const ideClient = config.getIdeClient();
ideClient.addTrustChangeListener(onStoreChange);
return () => {
ideClient.removeTrustChangeListener(onStoreChange);
};
},
[config],
);
const getSnapshot = () =>
ideContext.getIdeContext()?.workspaceState?.isTrusted;
const isIdeTrusted = useSyncExternalStore(subscribe, getSnapshot);
const [needsRestart, setNeedsRestart] = useState(false);
const [initialTrustValue] = useState(isIdeTrusted);
useEffect(() => {
if (
!needsRestart &&
initialTrustValue !== undefined &&
initialTrustValue !== isIdeTrusted
) {
setNeedsRestart(true);
}
}, [isIdeTrusted, initialTrustValue, needsRestart]);
return { isIdeTrusted, needsRestart };
}