mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-05-13 05:12:55 -07:00
fix(cli): Exit CLI when trust save unsuccessful during launch (#11968)
This commit is contained in:
@@ -76,11 +76,17 @@ export class LoadedTrustedFolders {
|
|||||||
* @param location path
|
* @param location path
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
isPathTrusted(location: string): boolean | undefined {
|
isPathTrusted(
|
||||||
|
location: string,
|
||||||
|
config?: Record<string, TrustLevel>,
|
||||||
|
): boolean | undefined {
|
||||||
|
const configToUse = config ?? this.user.config;
|
||||||
const trustedPaths: string[] = [];
|
const trustedPaths: string[] = [];
|
||||||
const untrustedPaths: string[] = [];
|
const untrustedPaths: string[] = [];
|
||||||
|
|
||||||
for (const rule of this.rules) {
|
for (const rule of Object.entries(configToUse).map(
|
||||||
|
([path, trustLevel]) => ({ path, trustLevel }),
|
||||||
|
)) {
|
||||||
switch (rule.trustLevel) {
|
switch (rule.trustLevel) {
|
||||||
case TrustLevel.TRUST_FOLDER:
|
case TrustLevel.TRUST_FOLDER:
|
||||||
trustedPaths.push(rule.path);
|
trustedPaths.push(rule.path);
|
||||||
@@ -113,8 +119,19 @@ export class LoadedTrustedFolders {
|
|||||||
}
|
}
|
||||||
|
|
||||||
setValue(path: string, trustLevel: TrustLevel): void {
|
setValue(path: string, trustLevel: TrustLevel): void {
|
||||||
|
const originalTrustLevel = this.user.config[path];
|
||||||
this.user.config[path] = trustLevel;
|
this.user.config[path] = trustLevel;
|
||||||
|
try {
|
||||||
saveTrustedFolders(this.user);
|
saveTrustedFolders(this.user);
|
||||||
|
} catch (e) {
|
||||||
|
// Revert the in-memory change if the save failed.
|
||||||
|
if (originalTrustLevel === undefined) {
|
||||||
|
delete this.user.config[path];
|
||||||
|
} else {
|
||||||
|
this.user.config[path] = originalTrustLevel;
|
||||||
|
}
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -174,7 +191,6 @@ export function loadTrustedFolders(): LoadedTrustedFolders {
|
|||||||
export function saveTrustedFolders(
|
export function saveTrustedFolders(
|
||||||
trustedFoldersFile: TrustedFoldersFile,
|
trustedFoldersFile: TrustedFoldersFile,
|
||||||
): void {
|
): void {
|
||||||
try {
|
|
||||||
// Ensure the directory exists
|
// Ensure the directory exists
|
||||||
const dirPath = path.dirname(trustedFoldersFile.path);
|
const dirPath = path.dirname(trustedFoldersFile.path);
|
||||||
if (!fs.existsSync(dirPath)) {
|
if (!fs.existsSync(dirPath)) {
|
||||||
@@ -186,9 +202,6 @@ export function saveTrustedFolders(
|
|||||||
JSON.stringify(trustedFoldersFile.config, null, 2),
|
JSON.stringify(trustedFoldersFile.config, null, 2),
|
||||||
{ encoding: 'utf-8', mode: 0o600 },
|
{ encoding: 'utf-8', mode: 0o600 },
|
||||||
);
|
);
|
||||||
} catch (error) {
|
|
||||||
console.error('Error saving trusted folders file:', error);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Is folder trust feature enabled per the current applied settings */
|
/** Is folder trust feature enabled per the current applied settings */
|
||||||
@@ -201,10 +214,7 @@ function getWorkspaceTrustFromLocalConfig(
|
|||||||
trustConfig?: Record<string, TrustLevel>,
|
trustConfig?: Record<string, TrustLevel>,
|
||||||
): TrustResult {
|
): TrustResult {
|
||||||
const folders = loadTrustedFolders();
|
const folders = loadTrustedFolders();
|
||||||
|
const configToUse = trustConfig ?? folders.user.config;
|
||||||
if (trustConfig) {
|
|
||||||
folders.user.config = trustConfig;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (folders.errors.length > 0) {
|
if (folders.errors.length > 0) {
|
||||||
const errorMessages = folders.errors.map(
|
const errorMessages = folders.errors.map(
|
||||||
@@ -215,7 +225,7 @@ function getWorkspaceTrustFromLocalConfig(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const isTrusted = folders.isPathTrusted(process.cwd());
|
const isTrusted = folders.isPathTrusted(process.cwd(), configToUse);
|
||||||
return {
|
return {
|
||||||
isTrusted,
|
isTrusted,
|
||||||
source: isTrusted !== undefined ? 'file' : undefined,
|
source: isTrusted !== undefined ? 'file' : undefined,
|
||||||
|
|||||||
@@ -148,10 +148,11 @@ describe('PermissionsModifyTrustDialog', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should commit, restart, and exit on `r` keypress', async () => {
|
it('should commit and restart `r` keypress', async () => {
|
||||||
const mockRelaunchApp = vi
|
const mockRelaunchApp = vi
|
||||||
.spyOn(processUtils, 'relaunchApp')
|
.spyOn(processUtils, 'relaunchApp')
|
||||||
.mockResolvedValue(undefined);
|
.mockResolvedValue(undefined);
|
||||||
|
mockCommitTrustLevelChange.mockReturnValue(true);
|
||||||
vi.mocked(usePermissionsModifyTrust).mockReturnValue({
|
vi.mocked(usePermissionsModifyTrust).mockReturnValue({
|
||||||
cwd: '/test/dir',
|
cwd: '/test/dir',
|
||||||
currentTrustLevel: TrustLevel.DO_NOT_TRUST,
|
currentTrustLevel: TrustLevel.DO_NOT_TRUST,
|
||||||
@@ -175,7 +176,6 @@ describe('PermissionsModifyTrustDialog', () => {
|
|||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(mockCommitTrustLevelChange).toHaveBeenCalled();
|
expect(mockCommitTrustLevelChange).toHaveBeenCalled();
|
||||||
expect(mockRelaunchApp).toHaveBeenCalled();
|
expect(mockRelaunchApp).toHaveBeenCalled();
|
||||||
expect(onExit).toHaveBeenCalled();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
mockRelaunchApp.mockRestore();
|
mockRelaunchApp.mockRestore();
|
||||||
|
|||||||
@@ -62,10 +62,13 @@ export function PermissionsModifyTrustDialog({
|
|||||||
onExit();
|
onExit();
|
||||||
}
|
}
|
||||||
if (needsRestart && key.name === 'r') {
|
if (needsRestart && key.name === 'r') {
|
||||||
commitTrustLevelChange();
|
const success = commitTrustLevelChange();
|
||||||
|
if (success) {
|
||||||
relaunchApp();
|
relaunchApp();
|
||||||
|
} else {
|
||||||
onExit();
|
onExit();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{ isActive: true },
|
{ isActive: true },
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -14,8 +14,10 @@ import { FolderTrustChoice } from '../components/FolderTrustDialog.js';
|
|||||||
import type { LoadedTrustedFolders } from '../../config/trustedFolders.js';
|
import type { LoadedTrustedFolders } from '../../config/trustedFolders.js';
|
||||||
import { TrustLevel } from '../../config/trustedFolders.js';
|
import { TrustLevel } from '../../config/trustedFolders.js';
|
||||||
import * as trustedFolders from '../../config/trustedFolders.js';
|
import * as trustedFolders from '../../config/trustedFolders.js';
|
||||||
|
import { coreEvents } from '@google/gemini-cli-core';
|
||||||
|
|
||||||
const mockedCwd = vi.hoisted(() => vi.fn());
|
const mockedCwd = vi.hoisted(() => vi.fn());
|
||||||
|
const mockedExit = vi.hoisted(() => vi.fn());
|
||||||
|
|
||||||
vi.mock('node:process', async () => {
|
vi.mock('node:process', async () => {
|
||||||
const actual =
|
const actual =
|
||||||
@@ -23,6 +25,7 @@ vi.mock('node:process', async () => {
|
|||||||
return {
|
return {
|
||||||
...actual,
|
...actual,
|
||||||
cwd: mockedCwd,
|
cwd: mockedCwd,
|
||||||
|
exit: mockedExit,
|
||||||
platform: 'linux',
|
platform: 'linux',
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
@@ -35,6 +38,7 @@ describe('useFolderTrust', () => {
|
|||||||
let addItem: Mock;
|
let addItem: Mock;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
vi.useFakeTimers();
|
||||||
mockSettings = {
|
mockSettings = {
|
||||||
merged: {
|
merged: {
|
||||||
security: {
|
security: {
|
||||||
@@ -60,6 +64,7 @@ describe('useFolderTrust', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
|
vi.useRealTimers();
|
||||||
vi.clearAllMocks();
|
vi.clearAllMocks();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -260,4 +265,30 @@ describe('useFolderTrust', () => {
|
|||||||
expect(result.current.isRestarting).toBe(false);
|
expect(result.current.isRestarting).toBe(false);
|
||||||
expect(result.current.isFolderTrustDialogOpen).toBe(false); // Dialog should close
|
expect(result.current.isFolderTrustDialogOpen).toBe(false); // Dialog should close
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should emit feedback on failure to set value', () => {
|
||||||
|
isWorkspaceTrustedSpy.mockReturnValue({
|
||||||
|
isTrusted: undefined,
|
||||||
|
source: undefined,
|
||||||
|
});
|
||||||
|
(mockTrustedFolders.setValue as Mock).mockImplementation(() => {
|
||||||
|
throw new Error('test error');
|
||||||
|
});
|
||||||
|
const emitFeedbackSpy = vi.spyOn(coreEvents, 'emitFeedback');
|
||||||
|
const { result } = renderHook(() =>
|
||||||
|
useFolderTrust(mockSettings, onTrustChange, addItem),
|
||||||
|
);
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
result.current.handleFolderTrustSelect(FolderTrustChoice.TRUST_FOLDER);
|
||||||
|
});
|
||||||
|
|
||||||
|
vi.runAllTimers();
|
||||||
|
|
||||||
|
expect(emitFeedbackSpy).toHaveBeenCalledWith(
|
||||||
|
'error',
|
||||||
|
'Failed to save trust settings. Exiting Gemini CLI.',
|
||||||
|
);
|
||||||
|
expect(mockedExit).toHaveBeenCalledWith(1);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import {
|
|||||||
} from '../../config/trustedFolders.js';
|
} from '../../config/trustedFolders.js';
|
||||||
import * as process from 'node:process';
|
import * as process from 'node:process';
|
||||||
import { type HistoryItemWithoutId, MessageType } from '../types.js';
|
import { type HistoryItemWithoutId, MessageType } from '../types.js';
|
||||||
|
import { coreEvents } from '@google/gemini-cli-core';
|
||||||
|
|
||||||
export const useFolderTrust = (
|
export const useFolderTrust = (
|
||||||
settings: LoadedSettings,
|
settings: LoadedSettings,
|
||||||
@@ -67,7 +68,19 @@ export const useFolderTrust = (
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
trustedFolders.setValue(cwd, trustLevel);
|
trustedFolders.setValue(cwd, trustLevel);
|
||||||
|
} catch (_e) {
|
||||||
|
coreEvents.emitFeedback(
|
||||||
|
'error',
|
||||||
|
'Failed to save trust settings. Exiting Gemini CLI.',
|
||||||
|
);
|
||||||
|
setTimeout(() => {
|
||||||
|
process.exit(1);
|
||||||
|
}, 100);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const currentIsTrusted =
|
const currentIsTrusted =
|
||||||
trustLevel === TrustLevel.TRUST_FOLDER ||
|
trustLevel === TrustLevel.TRUST_FOLDER ||
|
||||||
trustLevel === TrustLevel.TRUST_PARENT;
|
trustLevel === TrustLevel.TRUST_PARENT;
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import { usePermissionsModifyTrust } from './usePermissionsModifyTrust.js';
|
|||||||
import { TrustLevel } from '../../config/trustedFolders.js';
|
import { TrustLevel } from '../../config/trustedFolders.js';
|
||||||
import type { LoadedSettings } from '../../config/settings.js';
|
import type { LoadedSettings } from '../../config/settings.js';
|
||||||
import type { LoadedTrustedFolders } from '../../config/trustedFolders.js';
|
import type { LoadedTrustedFolders } from '../../config/trustedFolders.js';
|
||||||
|
import { coreEvents } from '@google/gemini-cli-core';
|
||||||
|
|
||||||
// Hoist mocks
|
// Hoist mocks
|
||||||
const mockedCwd = vi.hoisted(() => vi.fn());
|
const mockedCwd = vi.hoisted(() => vi.fn());
|
||||||
@@ -259,4 +260,70 @@ describe('usePermissionsModifyTrust', () => {
|
|||||||
expect.any(Number),
|
expect.any(Number),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should emit feedback when setValue throws in updateTrustLevel', () => {
|
||||||
|
const mockSetValue = vi.fn().mockImplementation(() => {
|
||||||
|
throw new Error('test error');
|
||||||
|
});
|
||||||
|
mockedLoadTrustedFolders.mockReturnValue({
|
||||||
|
user: { config: {} },
|
||||||
|
setValue: mockSetValue,
|
||||||
|
} as unknown as LoadedTrustedFolders);
|
||||||
|
|
||||||
|
mockedIsWorkspaceTrusted.mockReturnValue({
|
||||||
|
isTrusted: true,
|
||||||
|
source: 'file',
|
||||||
|
});
|
||||||
|
|
||||||
|
const emitFeedbackSpy = vi.spyOn(coreEvents, 'emitFeedback');
|
||||||
|
|
||||||
|
const { result } = renderHook(() =>
|
||||||
|
usePermissionsModifyTrust(mockOnExit, mockAddItem),
|
||||||
|
);
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
result.current.updateTrustLevel(TrustLevel.TRUST_PARENT);
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(emitFeedbackSpy).toHaveBeenCalledWith(
|
||||||
|
'error',
|
||||||
|
'Failed to save trust settings. Your changes may not persist.',
|
||||||
|
);
|
||||||
|
expect(mockOnExit).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should emit feedback when setValue throws in commitTrustLevelChange', () => {
|
||||||
|
const mockSetValue = vi.fn().mockImplementation(() => {
|
||||||
|
throw new Error('test error');
|
||||||
|
});
|
||||||
|
mockedLoadTrustedFolders.mockReturnValue({
|
||||||
|
user: { config: {} },
|
||||||
|
setValue: mockSetValue,
|
||||||
|
} as unknown as LoadedTrustedFolders);
|
||||||
|
|
||||||
|
mockedIsWorkspaceTrusted
|
||||||
|
.mockReturnValueOnce({ isTrusted: false, source: 'file' })
|
||||||
|
.mockReturnValueOnce({ isTrusted: true, source: 'file' });
|
||||||
|
|
||||||
|
const emitFeedbackSpy = vi.spyOn(coreEvents, 'emitFeedback');
|
||||||
|
|
||||||
|
const { result } = renderHook(() =>
|
||||||
|
usePermissionsModifyTrust(mockOnExit, mockAddItem),
|
||||||
|
);
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
result.current.updateTrustLevel(TrustLevel.TRUST_FOLDER);
|
||||||
|
});
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
const success = result.current.commitTrustLevelChange();
|
||||||
|
expect(success).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(emitFeedbackSpy).toHaveBeenCalledWith(
|
||||||
|
'error',
|
||||||
|
'Failed to save trust settings. Your changes may not persist.',
|
||||||
|
);
|
||||||
|
expect(result.current.needsRestart).toBe(false);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import { useSettings } from '../contexts/SettingsContext.js';
|
|||||||
import { MessageType } from '../types.js';
|
import { MessageType } from '../types.js';
|
||||||
import { type UseHistoryManagerReturn } from './useHistoryManager.js';
|
import { type UseHistoryManagerReturn } from './useHistoryManager.js';
|
||||||
import type { LoadedSettings } from '../../config/settings.js';
|
import type { LoadedSettings } from '../../config/settings.js';
|
||||||
|
import { coreEvents } from '@google/gemini-cli-core';
|
||||||
|
|
||||||
interface TrustState {
|
interface TrustState {
|
||||||
currentTrustLevel: TrustLevel | undefined;
|
currentTrustLevel: TrustLevel | undefined;
|
||||||
@@ -101,7 +102,14 @@ export const usePermissionsModifyTrust = (
|
|||||||
setNeedsRestart(true);
|
setNeedsRestart(true);
|
||||||
} else {
|
} else {
|
||||||
const folders = loadTrustedFolders();
|
const folders = loadTrustedFolders();
|
||||||
|
try {
|
||||||
folders.setValue(cwd, trustLevel);
|
folders.setValue(cwd, trustLevel);
|
||||||
|
} catch (_e) {
|
||||||
|
coreEvents.emitFeedback(
|
||||||
|
'error',
|
||||||
|
'Failed to save trust settings. Your changes may not persist.',
|
||||||
|
);
|
||||||
|
}
|
||||||
onExit();
|
onExit();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -111,8 +119,20 @@ export const usePermissionsModifyTrust = (
|
|||||||
const commitTrustLevelChange = useCallback(() => {
|
const commitTrustLevelChange = useCallback(() => {
|
||||||
if (pendingTrustLevel) {
|
if (pendingTrustLevel) {
|
||||||
const folders = loadTrustedFolders();
|
const folders = loadTrustedFolders();
|
||||||
|
try {
|
||||||
folders.setValue(cwd, pendingTrustLevel);
|
folders.setValue(cwd, pendingTrustLevel);
|
||||||
|
return true;
|
||||||
|
} catch (_e) {
|
||||||
|
coreEvents.emitFeedback(
|
||||||
|
'error',
|
||||||
|
'Failed to save trust settings. Your changes may not persist.',
|
||||||
|
);
|
||||||
|
setNeedsRestart(false);
|
||||||
|
setPendingTrustLevel(undefined);
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
}, [cwd, pendingTrustLevel]);
|
}, [cwd, pendingTrustLevel]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
Reference in New Issue
Block a user