From ae1f67df040e2304edbe31a1aaac6afe1335ad7e Mon Sep 17 00:00:00 2001 From: shrutip90 Date: Mon, 25 Aug 2025 17:30:04 -0700 Subject: [PATCH] feat: Disable YOLO and AUTO_EDIT modes for untrusted folders (#7041) --- packages/cli/src/config/config.test.ts | 40 +++- packages/cli/src/config/config.ts | 8 + packages/cli/src/ui/App.tsx | 2 +- .../messages/ToolConfirmationMessage.test.tsx | 102 +++++++++- .../messages/ToolConfirmationMessage.tsx | 92 ++++----- .../ui/hooks/useAutoAcceptIndicator.test.ts | 179 +++++++++++++++++- .../src/ui/hooks/useAutoAcceptIndicator.ts | 20 +- packages/core/src/config/config.test.ts | 58 +++++- packages/core/src/config/config.ts | 5 + 9 files changed, 451 insertions(+), 55 deletions(-) diff --git a/packages/cli/src/config/config.test.ts b/packages/cli/src/config/config.test.ts index f7a1bfa2..907af9e0 100644 --- a/packages/cli/src/config/config.test.ts +++ b/packages/cli/src/config/config.test.ts @@ -23,7 +23,7 @@ import * as ServerConfig from '@google/gemini-cli-core'; import { isWorkspaceTrusted } from './trustedFolders.js'; vi.mock('./trustedFolders.js', () => ({ - isWorkspaceTrusted: vi.fn(), + isWorkspaceTrusted: vi.fn().mockReturnValue(true), // Default to trusted })); vi.mock('fs', async (importOriginal) => { @@ -1002,6 +1002,7 @@ describe('Approval mode tool exclusion logic', () => { beforeEach(() => { process.stdin.isTTY = false; // Ensure non-interactive mode + vi.mocked(isWorkspaceTrusted).mockReturnValue(true); }); afterEach(() => { @@ -1680,6 +1681,7 @@ describe('loadCliConfig tool exclusions', () => { vi.mocked(os.homedir).mockReturnValue('/mock/home/user'); vi.stubEnv('GEMINI_API_KEY', 'test-api-key'); process.stdin.isTTY = true; + vi.mocked(isWorkspaceTrusted).mockReturnValue(true); }); afterEach(() => { @@ -1789,6 +1791,7 @@ describe('loadCliConfig approval mode', () => { vi.mocked(os.homedir).mockReturnValue('/mock/home/user'); vi.stubEnv('GEMINI_API_KEY', 'test-api-key'); process.argv = ['node', 'script.js']; // Reset argv for each test + vi.mocked(isWorkspaceTrusted).mockReturnValue(true); }); afterEach(() => { @@ -1856,6 +1859,41 @@ describe('loadCliConfig approval mode', () => { const config = await loadCliConfig({}, [], 'test-session', argv); expect(config.getApprovalMode()).toBe(ServerConfig.ApprovalMode.YOLO); }); + + // --- Untrusted Folder Scenarios --- + describe('when folder is NOT trusted', () => { + beforeEach(() => { + vi.mocked(isWorkspaceTrusted).mockReturnValue(false); + }); + + it('should override --approval-mode=yolo to DEFAULT', async () => { + process.argv = ['node', 'script.js', '--approval-mode', 'yolo']; + const argv = await parseArguments({} as Settings); + const config = await loadCliConfig({}, [], 'test-session', argv); + expect(config.getApprovalMode()).toBe(ServerConfig.ApprovalMode.DEFAULT); + }); + + it('should override --approval-mode=auto_edit to DEFAULT', async () => { + process.argv = ['node', 'script.js', '--approval-mode', 'auto_edit']; + const argv = await parseArguments({} as Settings); + const config = await loadCliConfig({}, [], 'test-session', argv); + expect(config.getApprovalMode()).toBe(ServerConfig.ApprovalMode.DEFAULT); + }); + + it('should override --yolo flag to DEFAULT', async () => { + process.argv = ['node', 'script.js', '--yolo']; + const argv = await parseArguments({} as Settings); + const config = await loadCliConfig({}, [], 'test-session', argv); + expect(config.getApprovalMode()).toBe(ServerConfig.ApprovalMode.DEFAULT); + }); + + it('should remain DEFAULT when --approval-mode=default', async () => { + process.argv = ['node', 'script.js', '--approval-mode', 'default']; + const argv = await parseArguments({} as Settings); + const config = await loadCliConfig({}, [], 'test-session', argv); + expect(config.getApprovalMode()).toBe(ServerConfig.ApprovalMode.DEFAULT); + }); + }); }); describe('loadCliConfig trustedFolder', () => { diff --git a/packages/cli/src/config/config.ts b/packages/cli/src/config/config.ts index 03ec676e..7a5de02e 100755 --- a/packages/cli/src/config/config.ts +++ b/packages/cli/src/config/config.ts @@ -406,6 +406,14 @@ export async function loadCliConfig( argv.yolo || false ? ApprovalMode.YOLO : ApprovalMode.DEFAULT; } + // Force approval mode to default if the folder is not trusted. + if (!trustedFolder && approvalMode !== ApprovalMode.DEFAULT) { + logger.warn( + `Approval mode overridden to "default" because the current folder is not trusted.`, + ); + approvalMode = ApprovalMode.DEFAULT; + } + const interactive = !!argv.promptInteractive || (process.stdin.isTTY && question.length === 0); // In non-interactive mode, exclude tools that require a prompt. diff --git a/packages/cli/src/ui/App.tsx b/packages/cli/src/ui/App.tsx index e65114f9..4561853b 100644 --- a/packages/cli/src/ui/App.tsx +++ b/packages/cli/src/ui/App.tsx @@ -627,7 +627,7 @@ const App = ({ config, settings, startupWarnings = [], version }: AppProps) => { const { elapsedTime, currentLoadingPhrase } = useLoadingIndicator(streamingState); - const showAutoAcceptIndicator = useAutoAcceptIndicator({ config }); + const showAutoAcceptIndicator = useAutoAcceptIndicator({ config, addItem }); const handleExit = useCallback( ( diff --git a/packages/cli/src/ui/components/messages/ToolConfirmationMessage.test.tsx b/packages/cli/src/ui/components/messages/ToolConfirmationMessage.test.tsx index 6e54e7a4..94cffff3 100644 --- a/packages/cli/src/ui/components/messages/ToolConfirmationMessage.test.tsx +++ b/packages/cli/src/ui/components/messages/ToolConfirmationMessage.test.tsx @@ -6,7 +6,10 @@ import { describe, it, expect, vi } from 'vitest'; import { ToolConfirmationMessage } from './ToolConfirmationMessage.js'; -import type { ToolCallConfirmationDetails } from '@google/gemini-cli-core'; +import type { + ToolCallConfirmationDetails, + Config, +} from '@google/gemini-cli-core'; import { renderWithProviders } from '../../../test-utils/render.js'; describe('ToolConfirmationMessage', () => { @@ -55,4 +58,101 @@ describe('ToolConfirmationMessage', () => { '- https://raw.githubusercontent.com/google/gemini-react/main/README.md', ); }); + + describe('with folder trust', () => { + const editConfirmationDetails: ToolCallConfirmationDetails = { + type: 'edit', + title: 'Confirm Edit', + fileName: 'test.txt', + filePath: '/test.txt', + fileDiff: '...diff...', + originalContent: 'a', + newContent: 'b', + onConfirm: vi.fn(), + }; + + const execConfirmationDetails: ToolCallConfirmationDetails = { + type: 'exec', + title: 'Confirm Execution', + command: 'echo "hello"', + rootCommand: 'echo', + onConfirm: vi.fn(), + }; + + const infoConfirmationDetails: ToolCallConfirmationDetails = { + type: 'info', + title: 'Confirm Web Fetch', + prompt: 'https://example.com', + urls: ['https://example.com'], + onConfirm: vi.fn(), + }; + + const mcpConfirmationDetails: ToolCallConfirmationDetails = { + type: 'mcp', + title: 'Confirm MCP Tool', + serverName: 'test-server', + toolName: 'test-tool', + toolDisplayName: 'Test Tool', + onConfirm: vi.fn(), + }; + + describe.each([ + { + description: 'for edit confirmations', + details: editConfirmationDetails, + alwaysAllowText: 'Yes, allow always', + }, + { + description: 'for exec confirmations', + details: execConfirmationDetails, + alwaysAllowText: 'Yes, allow always', + }, + { + description: 'for info confirmations', + details: infoConfirmationDetails, + alwaysAllowText: 'Yes, allow always', + }, + { + description: 'for mcp confirmations', + details: mcpConfirmationDetails, + alwaysAllowText: 'always allow', + }, + ])('$description', ({ details, alwaysAllowText }) => { + it('should show "allow always" when folder is trusted', () => { + const mockConfig = { + isTrustedFolder: () => true, + getIdeMode: () => false, + } as unknown as Config; + + const { lastFrame } = renderWithProviders( + , + ); + + expect(lastFrame()).toContain(alwaysAllowText); + }); + + it('should NOT show "allow always" when folder is untrusted', () => { + const mockConfig = { + isTrustedFolder: () => false, + getIdeMode: () => false, + } as unknown as Config; + + const { lastFrame } = renderWithProviders( + , + ); + + expect(lastFrame()).not.toContain(alwaysAllowText); + }); + }); + }); }); diff --git a/packages/cli/src/ui/components/messages/ToolConfirmationMessage.tsx b/packages/cli/src/ui/components/messages/ToolConfirmationMessage.tsx index 6dee3d74..8cd7756e 100644 --- a/packages/cli/src/ui/components/messages/ToolConfirmationMessage.tsx +++ b/packages/cli/src/ui/components/messages/ToolConfirmationMessage.tsx @@ -125,16 +125,16 @@ export const ToolConfirmationMessage: React.FC< } question = `Apply this change?`; - options.push( - { - label: 'Yes, allow once', - value: ToolConfirmationOutcome.ProceedOnce, - }, - { + options.push({ + label: 'Yes, allow once', + value: ToolConfirmationOutcome.ProceedOnce, + }); + if (config?.isTrustedFolder()) { + options.push({ label: 'Yes, allow always', value: ToolConfirmationOutcome.ProceedAlways, - }, - ); + }); + } if (config?.getIdeMode()) { options.push({ label: 'No (esc)', @@ -164,20 +164,20 @@ export const ToolConfirmationMessage: React.FC< confirmationDetails as ToolExecuteConfirmationDetails; question = `Allow execution of: '${executionProps.rootCommand}'?`; - options.push( - { - label: `Yes, allow once`, - value: ToolConfirmationOutcome.ProceedOnce, - }, - { + options.push({ + label: 'Yes, allow once', + value: ToolConfirmationOutcome.ProceedOnce, + }); + if (config?.isTrustedFolder()) { + options.push({ label: `Yes, allow always ...`, value: ToolConfirmationOutcome.ProceedAlways, - }, - { - label: 'No, suggest changes (esc)', - value: ToolConfirmationOutcome.Cancel, - }, - ); + }); + } + options.push({ + label: 'No, suggest changes (esc)', + value: ToolConfirmationOutcome.Cancel, + }); let bodyContentHeight = availableBodyContentHeight(); if (bodyContentHeight !== undefined) { @@ -204,20 +204,20 @@ export const ToolConfirmationMessage: React.FC< !(infoProps.urls.length === 1 && infoProps.urls[0] === infoProps.prompt); question = `Do you want to proceed?`; - options.push( - { - label: 'Yes, allow once', - value: ToolConfirmationOutcome.ProceedOnce, - }, - { + options.push({ + label: 'Yes, allow once', + value: ToolConfirmationOutcome.ProceedOnce, + }); + if (config?.isTrustedFolder()) { + options.push({ label: 'Yes, allow always', value: ToolConfirmationOutcome.ProceedAlways, - }, - { - label: 'No, suggest changes (esc)', - value: ToolConfirmationOutcome.Cancel, - }, - ); + }); + } + options.push({ + label: 'No, suggest changes (esc)', + value: ToolConfirmationOutcome.Cancel, + }); bodyContent = ( @@ -249,24 +249,24 @@ export const ToolConfirmationMessage: React.FC< ); question = `Allow execution of MCP tool "${mcpProps.toolName}" from server "${mcpProps.serverName}"?`; - options.push( - { - label: 'Yes, allow once', - value: ToolConfirmationOutcome.ProceedOnce, - }, - { + options.push({ + label: 'Yes, allow once', + value: ToolConfirmationOutcome.ProceedOnce, + }); + if (config?.isTrustedFolder()) { + options.push({ label: `Yes, always allow tool "${mcpProps.toolName}" from server "${mcpProps.serverName}"`, value: ToolConfirmationOutcome.ProceedAlwaysTool, // Cast until types are updated - }, - { + }); + options.push({ label: `Yes, always allow all tools from server "${mcpProps.serverName}"`, value: ToolConfirmationOutcome.ProceedAlwaysServer, - }, - { - label: 'No, suggest changes (esc)', - value: ToolConfirmationOutcome.Cancel, - }, - ); + }); + } + options.push({ + label: 'No, suggest changes (esc)', + value: ToolConfirmationOutcome.Cancel, + }); } return ( diff --git a/packages/cli/src/ui/hooks/useAutoAcceptIndicator.test.ts b/packages/cli/src/ui/hooks/useAutoAcceptIndicator.test.ts index ab85dd83..9db68f1b 100644 --- a/packages/cli/src/ui/hooks/useAutoAcceptIndicator.test.ts +++ b/packages/cli/src/ui/hooks/useAutoAcceptIndicator.test.ts @@ -20,6 +20,7 @@ import type { Config as ActualConfigType } from '@google/gemini-cli-core'; import { Config, ApprovalMode } from '@google/gemini-cli-core'; import type { Key } from './useKeypress.js'; import { useKeypress } from './useKeypress.js'; +import { MessageType } from '../types.js'; vi.mock('./useKeypress.js'); @@ -36,6 +37,7 @@ vi.mock('@google/gemini-cli-core', async () => { interface MockConfigInstanceShape { getApprovalMode: Mock<() => ApprovalMode>; setApprovalMode: Mock<(value: ApprovalMode) => void>; + isTrustedFolder: Mock<() => boolean>; getCoreTools: Mock<() => string[]>; getToolDiscoveryCommand: Mock<() => string | undefined>; getTargetDir: Mock<() => string>; @@ -74,6 +76,7 @@ describe('useAutoAcceptIndicator', () => { setApprovalMode: instanceSetApprovalModeMock as Mock< (value: ApprovalMode) => void >, + isTrustedFolder: vi.fn().mockReturnValue(true) as Mock<() => boolean>, getCoreTools: vi.fn().mockReturnValue([]) as Mock<() => string[]>, getToolDiscoveryCommand: vi.fn().mockReturnValue(undefined) as Mock< () => string | undefined @@ -124,6 +127,7 @@ describe('useAutoAcceptIndicator', () => { const { result } = renderHook(() => useAutoAcceptIndicator({ config: mockConfigInstance as unknown as ActualConfigType, + addItem: vi.fn(), }), ); expect(result.current).toBe(ApprovalMode.AUTO_EDIT); @@ -135,6 +139,7 @@ describe('useAutoAcceptIndicator', () => { const { result } = renderHook(() => useAutoAcceptIndicator({ config: mockConfigInstance as unknown as ActualConfigType, + addItem: vi.fn(), }), ); expect(result.current).toBe(ApprovalMode.DEFAULT); @@ -146,6 +151,7 @@ describe('useAutoAcceptIndicator', () => { const { result } = renderHook(() => useAutoAcceptIndicator({ config: mockConfigInstance as unknown as ActualConfigType, + addItem: vi.fn(), }), ); expect(result.current).toBe(ApprovalMode.YOLO); @@ -157,6 +163,7 @@ describe('useAutoAcceptIndicator', () => { const { result } = renderHook(() => useAutoAcceptIndicator({ config: mockConfigInstance as unknown as ActualConfigType, + addItem: vi.fn(), }), ); expect(result.current).toBe(ApprovalMode.DEFAULT); @@ -224,6 +231,7 @@ describe('useAutoAcceptIndicator', () => { renderHook(() => useAutoAcceptIndicator({ config: mockConfigInstance as unknown as ActualConfigType, + addItem: vi.fn(), }), ); @@ -280,10 +288,12 @@ describe('useAutoAcceptIndicator', () => { it('should update indicator when config value changes externally (useEffect dependency)', () => { mockConfigInstance.getApprovalMode.mockReturnValue(ApprovalMode.DEFAULT); const { result, rerender } = renderHook( - (props: { config: ActualConfigType }) => useAutoAcceptIndicator(props), + (props: { config: ActualConfigType; addItem: () => void }) => + useAutoAcceptIndicator(props), { initialProps: { config: mockConfigInstance as unknown as ActualConfigType, + addItem: vi.fn(), }, }, ); @@ -291,8 +301,173 @@ describe('useAutoAcceptIndicator', () => { mockConfigInstance.getApprovalMode.mockReturnValue(ApprovalMode.AUTO_EDIT); - rerender({ config: mockConfigInstance as unknown as ActualConfigType }); + rerender({ + config: mockConfigInstance as unknown as ActualConfigType, + addItem: vi.fn(), + }); expect(result.current).toBe(ApprovalMode.AUTO_EDIT); expect(mockConfigInstance.getApprovalMode).toHaveBeenCalledTimes(3); }); + + describe('in untrusted folders', () => { + beforeEach(() => { + mockConfigInstance.isTrustedFolder.mockReturnValue(false); + }); + + it('should not enable YOLO mode when Ctrl+Y is pressed', () => { + mockConfigInstance.getApprovalMode.mockReturnValue(ApprovalMode.DEFAULT); + mockConfigInstance.setApprovalMode.mockImplementation(() => { + throw new Error( + 'Cannot enable privileged approval modes in an untrusted folder.', + ); + }); + const mockAddItem = vi.fn(); + const { result } = renderHook(() => + useAutoAcceptIndicator({ + config: mockConfigInstance as unknown as ActualConfigType, + addItem: mockAddItem, + }), + ); + + expect(result.current).toBe(ApprovalMode.DEFAULT); + + act(() => { + capturedUseKeypressHandler({ name: 'y', ctrl: true } as Key); + }); + + // We expect setApprovalMode to be called, and the error to be caught. + expect(mockConfigInstance.setApprovalMode).toHaveBeenCalledWith( + ApprovalMode.YOLO, + ); + expect(mockAddItem).toHaveBeenCalled(); + // Verify the underlying config value was not changed + expect(mockConfigInstance.getApprovalMode()).toBe(ApprovalMode.DEFAULT); + }); + + it('should not enable AUTO_EDIT mode when Shift+Tab is pressed', () => { + mockConfigInstance.getApprovalMode.mockReturnValue(ApprovalMode.DEFAULT); + mockConfigInstance.setApprovalMode.mockImplementation(() => { + throw new Error( + 'Cannot enable privileged approval modes in an untrusted folder.', + ); + }); + const mockAddItem = vi.fn(); + const { result } = renderHook(() => + useAutoAcceptIndicator({ + config: mockConfigInstance as unknown as ActualConfigType, + addItem: mockAddItem, + }), + ); + + expect(result.current).toBe(ApprovalMode.DEFAULT); + + act(() => { + capturedUseKeypressHandler({ + name: 'tab', + shift: true, + } as Key); + }); + + // We expect setApprovalMode to be called, and the error to be caught. + expect(mockConfigInstance.setApprovalMode).toHaveBeenCalledWith( + ApprovalMode.AUTO_EDIT, + ); + expect(mockAddItem).toHaveBeenCalled(); + // Verify the underlying config value was not changed + expect(mockConfigInstance.getApprovalMode()).toBe(ApprovalMode.DEFAULT); + }); + + it('should disable YOLO mode when Ctrl+Y is pressed', () => { + mockConfigInstance.getApprovalMode.mockReturnValue(ApprovalMode.YOLO); + const mockAddItem = vi.fn(); + renderHook(() => + useAutoAcceptIndicator({ + config: mockConfigInstance as unknown as ActualConfigType, + addItem: mockAddItem, + }), + ); + + act(() => { + capturedUseKeypressHandler({ name: 'y', ctrl: true } as Key); + }); + + expect(mockConfigInstance.setApprovalMode).toHaveBeenCalledWith( + ApprovalMode.DEFAULT, + ); + expect(mockConfigInstance.getApprovalMode()).toBe(ApprovalMode.DEFAULT); + }); + + it('should disable AUTO_EDIT mode when Shift+Tab is pressed', () => { + mockConfigInstance.getApprovalMode.mockReturnValue( + ApprovalMode.AUTO_EDIT, + ); + const mockAddItem = vi.fn(); + renderHook(() => + useAutoAcceptIndicator({ + config: mockConfigInstance as unknown as ActualConfigType, + addItem: mockAddItem, + }), + ); + + act(() => { + capturedUseKeypressHandler({ + name: 'tab', + shift: true, + } as Key); + }); + + expect(mockConfigInstance.setApprovalMode).toHaveBeenCalledWith( + ApprovalMode.DEFAULT, + ); + expect(mockConfigInstance.getApprovalMode()).toBe(ApprovalMode.DEFAULT); + }); + + it('should show a warning when trying to enable privileged modes', () => { + // Mock the error thrown by setApprovalMode + const errorMessage = + 'Cannot enable privileged approval modes in an untrusted folder.'; + mockConfigInstance.setApprovalMode.mockImplementation(() => { + throw new Error(errorMessage); + }); + + const mockAddItem = vi.fn(); + renderHook(() => + useAutoAcceptIndicator({ + config: mockConfigInstance as unknown as ActualConfigType, + addItem: mockAddItem, + }), + ); + + // Try to enable YOLO mode + act(() => { + capturedUseKeypressHandler({ name: 'y', ctrl: true } as Key); + }); + + expect(mockAddItem).toHaveBeenCalledWith( + { + type: MessageType.INFO, + text: errorMessage, + }, + expect.any(Number), + ); + + // Try to enable AUTO_EDIT mode + act(() => { + capturedUseKeypressHandler({ + name: 'tab', + shift: true, + } as Key); + }); + + expect(mockAddItem).toHaveBeenCalledWith( + { + type: MessageType.INFO, + text: errorMessage, + }, + expect.any(Number), + ); + + expect(mockAddItem).toHaveBeenCalledTimes(2); + }); + }); }); diff --git a/packages/cli/src/ui/hooks/useAutoAcceptIndicator.ts b/packages/cli/src/ui/hooks/useAutoAcceptIndicator.ts index 2cc16077..8766a2db 100644 --- a/packages/cli/src/ui/hooks/useAutoAcceptIndicator.ts +++ b/packages/cli/src/ui/hooks/useAutoAcceptIndicator.ts @@ -7,13 +7,17 @@ import { useState, useEffect } from 'react'; import { ApprovalMode, type Config } from '@google/gemini-cli-core'; import { useKeypress } from './useKeypress.js'; +import type { HistoryItemWithoutId } from '../types.js'; +import { MessageType } from '../types.js'; export interface UseAutoAcceptIndicatorArgs { config: Config; + addItem: (item: HistoryItemWithoutId, timestamp: number) => void; } export function useAutoAcceptIndicator({ config, + addItem, }: UseAutoAcceptIndicatorArgs): ApprovalMode { const currentConfigValue = config.getApprovalMode(); const [showAutoAcceptIndicator, setShowAutoAcceptIndicator] = @@ -40,9 +44,19 @@ export function useAutoAcceptIndicator({ } if (nextApprovalMode) { - config.setApprovalMode(nextApprovalMode); - // Update local state immediately for responsiveness - setShowAutoAcceptIndicator(nextApprovalMode); + try { + config.setApprovalMode(nextApprovalMode); + // Update local state immediately for responsiveness + setShowAutoAcceptIndicator(nextApprovalMode); + } catch (e) { + addItem( + { + type: MessageType.INFO, + text: (e as Error).message, + }, + Date.now(), + ); + } } }, { isActive: true }, diff --git a/packages/core/src/config/config.test.ts b/packages/core/src/config/config.test.ts index e5d15d76..a37a9fe9 100644 --- a/packages/core/src/config/config.test.ts +++ b/packages/core/src/config/config.test.ts @@ -7,7 +7,7 @@ import { describe, it, expect, vi, beforeEach } from 'vitest'; import type { Mock } from 'vitest'; import type { ConfigParameters, SandboxConfig } from './config.js'; -import { Config } from './config.js'; +import { Config, ApprovalMode } from './config.js'; import * as path from 'node:path'; import { setGeminiMdFilename as mockSetGeminiMdFilename } from '../tools/memoryTool.js'; import { @@ -630,3 +630,59 @@ describe('Server Config (config.ts)', () => { }); }); }); + +describe('setApprovalMode with folder trust', () => { + it('should throw an error when setting YOLO mode in an untrusted folder', () => { + const config = new Config({ + sessionId: 'test', + targetDir: '.', + debugMode: false, + model: 'test-model', + cwd: '.', + trustedFolder: false, // Untrusted + }); + expect(() => config.setApprovalMode(ApprovalMode.YOLO)).toThrow( + 'Cannot enable privileged approval modes in an untrusted folder.', + ); + }); + + it('should throw an error when setting AUTO_EDIT mode in an untrusted folder', () => { + const config = new Config({ + sessionId: 'test', + targetDir: '.', + debugMode: false, + model: 'test-model', + cwd: '.', + trustedFolder: false, // Untrusted + }); + expect(() => config.setApprovalMode(ApprovalMode.AUTO_EDIT)).toThrow( + 'Cannot enable privileged approval modes in an untrusted folder.', + ); + }); + + it('should NOT throw an error when setting DEFAULT mode in an untrusted folder', () => { + const config = new Config({ + sessionId: 'test', + targetDir: '.', + debugMode: false, + model: 'test-model', + cwd: '.', + trustedFolder: false, // Untrusted + }); + expect(() => config.setApprovalMode(ApprovalMode.DEFAULT)).not.toThrow(); + }); + + it('should NOT throw an error when setting any mode in a trusted folder', () => { + const config = new Config({ + sessionId: 'test', + targetDir: '.', + debugMode: false, + model: 'test-model', + cwd: '.', + trustedFolder: true, // Trusted + }); + expect(() => config.setApprovalMode(ApprovalMode.YOLO)).not.toThrow(); + expect(() => config.setApprovalMode(ApprovalMode.AUTO_EDIT)).not.toThrow(); + expect(() => config.setApprovalMode(ApprovalMode.DEFAULT)).not.toThrow(); + }); +}); diff --git a/packages/core/src/config/config.ts b/packages/core/src/config/config.ts index 41759afc..4b2f508d 100644 --- a/packages/core/src/config/config.ts +++ b/packages/core/src/config/config.ts @@ -564,6 +564,11 @@ export class Config { } setApprovalMode(mode: ApprovalMode): void { + if (!this.isTrustedFolder() && mode !== ApprovalMode.DEFAULT) { + throw new Error( + 'Cannot enable privileged approval modes in an untrusted folder.', + ); + } this.approvalMode = mode; }