mirror of
https://github.com/QwenLM/qwen-code.git
synced 2025-12-19 09:33:53 +00:00
feat: Disable YOLO and AUTO_EDIT modes for untrusted folders (#7041)
This commit is contained in:
@@ -23,7 +23,7 @@ import * as ServerConfig from '@google/gemini-cli-core';
|
|||||||
import { isWorkspaceTrusted } from './trustedFolders.js';
|
import { isWorkspaceTrusted } from './trustedFolders.js';
|
||||||
|
|
||||||
vi.mock('./trustedFolders.js', () => ({
|
vi.mock('./trustedFolders.js', () => ({
|
||||||
isWorkspaceTrusted: vi.fn(),
|
isWorkspaceTrusted: vi.fn().mockReturnValue(true), // Default to trusted
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock('fs', async (importOriginal) => {
|
vi.mock('fs', async (importOriginal) => {
|
||||||
@@ -1002,6 +1002,7 @@ describe('Approval mode tool exclusion logic', () => {
|
|||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
process.stdin.isTTY = false; // Ensure non-interactive mode
|
process.stdin.isTTY = false; // Ensure non-interactive mode
|
||||||
|
vi.mocked(isWorkspaceTrusted).mockReturnValue(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
@@ -1680,6 +1681,7 @@ describe('loadCliConfig tool exclusions', () => {
|
|||||||
vi.mocked(os.homedir).mockReturnValue('/mock/home/user');
|
vi.mocked(os.homedir).mockReturnValue('/mock/home/user');
|
||||||
vi.stubEnv('GEMINI_API_KEY', 'test-api-key');
|
vi.stubEnv('GEMINI_API_KEY', 'test-api-key');
|
||||||
process.stdin.isTTY = true;
|
process.stdin.isTTY = true;
|
||||||
|
vi.mocked(isWorkspaceTrusted).mockReturnValue(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
@@ -1789,6 +1791,7 @@ describe('loadCliConfig approval mode', () => {
|
|||||||
vi.mocked(os.homedir).mockReturnValue('/mock/home/user');
|
vi.mocked(os.homedir).mockReturnValue('/mock/home/user');
|
||||||
vi.stubEnv('GEMINI_API_KEY', 'test-api-key');
|
vi.stubEnv('GEMINI_API_KEY', 'test-api-key');
|
||||||
process.argv = ['node', 'script.js']; // Reset argv for each test
|
process.argv = ['node', 'script.js']; // Reset argv for each test
|
||||||
|
vi.mocked(isWorkspaceTrusted).mockReturnValue(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
@@ -1856,6 +1859,41 @@ describe('loadCliConfig approval mode', () => {
|
|||||||
const config = await loadCliConfig({}, [], 'test-session', argv);
|
const config = await loadCliConfig({}, [], 'test-session', argv);
|
||||||
expect(config.getApprovalMode()).toBe(ServerConfig.ApprovalMode.YOLO);
|
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', () => {
|
describe('loadCliConfig trustedFolder', () => {
|
||||||
|
|||||||
@@ -406,6 +406,14 @@ export async function loadCliConfig(
|
|||||||
argv.yolo || false ? ApprovalMode.YOLO : ApprovalMode.DEFAULT;
|
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 =
|
const interactive =
|
||||||
!!argv.promptInteractive || (process.stdin.isTTY && question.length === 0);
|
!!argv.promptInteractive || (process.stdin.isTTY && question.length === 0);
|
||||||
// In non-interactive mode, exclude tools that require a prompt.
|
// In non-interactive mode, exclude tools that require a prompt.
|
||||||
|
|||||||
@@ -627,7 +627,7 @@ const App = ({ config, settings, startupWarnings = [], version }: AppProps) => {
|
|||||||
|
|
||||||
const { elapsedTime, currentLoadingPhrase } =
|
const { elapsedTime, currentLoadingPhrase } =
|
||||||
useLoadingIndicator(streamingState);
|
useLoadingIndicator(streamingState);
|
||||||
const showAutoAcceptIndicator = useAutoAcceptIndicator({ config });
|
const showAutoAcceptIndicator = useAutoAcceptIndicator({ config, addItem });
|
||||||
|
|
||||||
const handleExit = useCallback(
|
const handleExit = useCallback(
|
||||||
(
|
(
|
||||||
|
|||||||
@@ -6,7 +6,10 @@
|
|||||||
|
|
||||||
import { describe, it, expect, vi } from 'vitest';
|
import { describe, it, expect, vi } from 'vitest';
|
||||||
import { ToolConfirmationMessage } from './ToolConfirmationMessage.js';
|
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';
|
import { renderWithProviders } from '../../../test-utils/render.js';
|
||||||
|
|
||||||
describe('ToolConfirmationMessage', () => {
|
describe('ToolConfirmationMessage', () => {
|
||||||
@@ -55,4 +58,101 @@ describe('ToolConfirmationMessage', () => {
|
|||||||
'- https://raw.githubusercontent.com/google/gemini-react/main/README.md',
|
'- 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(
|
||||||
|
<ToolConfirmationMessage
|
||||||
|
confirmationDetails={details}
|
||||||
|
config={mockConfig}
|
||||||
|
availableTerminalHeight={30}
|
||||||
|
terminalWidth={80}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
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(
|
||||||
|
<ToolConfirmationMessage
|
||||||
|
confirmationDetails={details}
|
||||||
|
config={mockConfig}
|
||||||
|
availableTerminalHeight={30}
|
||||||
|
terminalWidth={80}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(lastFrame()).not.toContain(alwaysAllowText);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -125,16 +125,16 @@ export const ToolConfirmationMessage: React.FC<
|
|||||||
}
|
}
|
||||||
|
|
||||||
question = `Apply this change?`;
|
question = `Apply this change?`;
|
||||||
options.push(
|
options.push({
|
||||||
{
|
label: 'Yes, allow once',
|
||||||
label: 'Yes, allow once',
|
value: ToolConfirmationOutcome.ProceedOnce,
|
||||||
value: ToolConfirmationOutcome.ProceedOnce,
|
});
|
||||||
},
|
if (config?.isTrustedFolder()) {
|
||||||
{
|
options.push({
|
||||||
label: 'Yes, allow always',
|
label: 'Yes, allow always',
|
||||||
value: ToolConfirmationOutcome.ProceedAlways,
|
value: ToolConfirmationOutcome.ProceedAlways,
|
||||||
},
|
});
|
||||||
);
|
}
|
||||||
if (config?.getIdeMode()) {
|
if (config?.getIdeMode()) {
|
||||||
options.push({
|
options.push({
|
||||||
label: 'No (esc)',
|
label: 'No (esc)',
|
||||||
@@ -164,20 +164,20 @@ export const ToolConfirmationMessage: React.FC<
|
|||||||
confirmationDetails as ToolExecuteConfirmationDetails;
|
confirmationDetails as ToolExecuteConfirmationDetails;
|
||||||
|
|
||||||
question = `Allow execution of: '${executionProps.rootCommand}'?`;
|
question = `Allow execution of: '${executionProps.rootCommand}'?`;
|
||||||
options.push(
|
options.push({
|
||||||
{
|
label: 'Yes, allow once',
|
||||||
label: `Yes, allow once`,
|
value: ToolConfirmationOutcome.ProceedOnce,
|
||||||
value: ToolConfirmationOutcome.ProceedOnce,
|
});
|
||||||
},
|
if (config?.isTrustedFolder()) {
|
||||||
{
|
options.push({
|
||||||
label: `Yes, allow always ...`,
|
label: `Yes, allow always ...`,
|
||||||
value: ToolConfirmationOutcome.ProceedAlways,
|
value: ToolConfirmationOutcome.ProceedAlways,
|
||||||
},
|
});
|
||||||
{
|
}
|
||||||
label: 'No, suggest changes (esc)',
|
options.push({
|
||||||
value: ToolConfirmationOutcome.Cancel,
|
label: 'No, suggest changes (esc)',
|
||||||
},
|
value: ToolConfirmationOutcome.Cancel,
|
||||||
);
|
});
|
||||||
|
|
||||||
let bodyContentHeight = availableBodyContentHeight();
|
let bodyContentHeight = availableBodyContentHeight();
|
||||||
if (bodyContentHeight !== undefined) {
|
if (bodyContentHeight !== undefined) {
|
||||||
@@ -204,20 +204,20 @@ export const ToolConfirmationMessage: React.FC<
|
|||||||
!(infoProps.urls.length === 1 && infoProps.urls[0] === infoProps.prompt);
|
!(infoProps.urls.length === 1 && infoProps.urls[0] === infoProps.prompt);
|
||||||
|
|
||||||
question = `Do you want to proceed?`;
|
question = `Do you want to proceed?`;
|
||||||
options.push(
|
options.push({
|
||||||
{
|
label: 'Yes, allow once',
|
||||||
label: 'Yes, allow once',
|
value: ToolConfirmationOutcome.ProceedOnce,
|
||||||
value: ToolConfirmationOutcome.ProceedOnce,
|
});
|
||||||
},
|
if (config?.isTrustedFolder()) {
|
||||||
{
|
options.push({
|
||||||
label: 'Yes, allow always',
|
label: 'Yes, allow always',
|
||||||
value: ToolConfirmationOutcome.ProceedAlways,
|
value: ToolConfirmationOutcome.ProceedAlways,
|
||||||
},
|
});
|
||||||
{
|
}
|
||||||
label: 'No, suggest changes (esc)',
|
options.push({
|
||||||
value: ToolConfirmationOutcome.Cancel,
|
label: 'No, suggest changes (esc)',
|
||||||
},
|
value: ToolConfirmationOutcome.Cancel,
|
||||||
);
|
});
|
||||||
|
|
||||||
bodyContent = (
|
bodyContent = (
|
||||||
<Box flexDirection="column" paddingX={1} marginLeft={1}>
|
<Box flexDirection="column" paddingX={1} marginLeft={1}>
|
||||||
@@ -249,24 +249,24 @@ export const ToolConfirmationMessage: React.FC<
|
|||||||
);
|
);
|
||||||
|
|
||||||
question = `Allow execution of MCP tool "${mcpProps.toolName}" from server "${mcpProps.serverName}"?`;
|
question = `Allow execution of MCP tool "${mcpProps.toolName}" from server "${mcpProps.serverName}"?`;
|
||||||
options.push(
|
options.push({
|
||||||
{
|
label: 'Yes, allow once',
|
||||||
label: 'Yes, allow once',
|
value: ToolConfirmationOutcome.ProceedOnce,
|
||||||
value: ToolConfirmationOutcome.ProceedOnce,
|
});
|
||||||
},
|
if (config?.isTrustedFolder()) {
|
||||||
{
|
options.push({
|
||||||
label: `Yes, always allow tool "${mcpProps.toolName}" from server "${mcpProps.serverName}"`,
|
label: `Yes, always allow tool "${mcpProps.toolName}" from server "${mcpProps.serverName}"`,
|
||||||
value: ToolConfirmationOutcome.ProceedAlwaysTool, // Cast until types are updated
|
value: ToolConfirmationOutcome.ProceedAlwaysTool, // Cast until types are updated
|
||||||
},
|
});
|
||||||
{
|
options.push({
|
||||||
label: `Yes, always allow all tools from server "${mcpProps.serverName}"`,
|
label: `Yes, always allow all tools from server "${mcpProps.serverName}"`,
|
||||||
value: ToolConfirmationOutcome.ProceedAlwaysServer,
|
value: ToolConfirmationOutcome.ProceedAlwaysServer,
|
||||||
},
|
});
|
||||||
{
|
}
|
||||||
label: 'No, suggest changes (esc)',
|
options.push({
|
||||||
value: ToolConfirmationOutcome.Cancel,
|
label: 'No, suggest changes (esc)',
|
||||||
},
|
value: ToolConfirmationOutcome.Cancel,
|
||||||
);
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import type { Config as ActualConfigType } from '@google/gemini-cli-core';
|
|||||||
import { Config, ApprovalMode } from '@google/gemini-cli-core';
|
import { Config, ApprovalMode } from '@google/gemini-cli-core';
|
||||||
import type { Key } from './useKeypress.js';
|
import type { Key } from './useKeypress.js';
|
||||||
import { useKeypress } from './useKeypress.js';
|
import { useKeypress } from './useKeypress.js';
|
||||||
|
import { MessageType } from '../types.js';
|
||||||
|
|
||||||
vi.mock('./useKeypress.js');
|
vi.mock('./useKeypress.js');
|
||||||
|
|
||||||
@@ -36,6 +37,7 @@ vi.mock('@google/gemini-cli-core', async () => {
|
|||||||
interface MockConfigInstanceShape {
|
interface MockConfigInstanceShape {
|
||||||
getApprovalMode: Mock<() => ApprovalMode>;
|
getApprovalMode: Mock<() => ApprovalMode>;
|
||||||
setApprovalMode: Mock<(value: ApprovalMode) => void>;
|
setApprovalMode: Mock<(value: ApprovalMode) => void>;
|
||||||
|
isTrustedFolder: Mock<() => boolean>;
|
||||||
getCoreTools: Mock<() => string[]>;
|
getCoreTools: Mock<() => string[]>;
|
||||||
getToolDiscoveryCommand: Mock<() => string | undefined>;
|
getToolDiscoveryCommand: Mock<() => string | undefined>;
|
||||||
getTargetDir: Mock<() => string>;
|
getTargetDir: Mock<() => string>;
|
||||||
@@ -74,6 +76,7 @@ describe('useAutoAcceptIndicator', () => {
|
|||||||
setApprovalMode: instanceSetApprovalModeMock as Mock<
|
setApprovalMode: instanceSetApprovalModeMock as Mock<
|
||||||
(value: ApprovalMode) => void
|
(value: ApprovalMode) => void
|
||||||
>,
|
>,
|
||||||
|
isTrustedFolder: vi.fn().mockReturnValue(true) as Mock<() => boolean>,
|
||||||
getCoreTools: vi.fn().mockReturnValue([]) as Mock<() => string[]>,
|
getCoreTools: vi.fn().mockReturnValue([]) as Mock<() => string[]>,
|
||||||
getToolDiscoveryCommand: vi.fn().mockReturnValue(undefined) as Mock<
|
getToolDiscoveryCommand: vi.fn().mockReturnValue(undefined) as Mock<
|
||||||
() => string | undefined
|
() => string | undefined
|
||||||
@@ -124,6 +127,7 @@ describe('useAutoAcceptIndicator', () => {
|
|||||||
const { result } = renderHook(() =>
|
const { result } = renderHook(() =>
|
||||||
useAutoAcceptIndicator({
|
useAutoAcceptIndicator({
|
||||||
config: mockConfigInstance as unknown as ActualConfigType,
|
config: mockConfigInstance as unknown as ActualConfigType,
|
||||||
|
addItem: vi.fn(),
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
expect(result.current).toBe(ApprovalMode.AUTO_EDIT);
|
expect(result.current).toBe(ApprovalMode.AUTO_EDIT);
|
||||||
@@ -135,6 +139,7 @@ describe('useAutoAcceptIndicator', () => {
|
|||||||
const { result } = renderHook(() =>
|
const { result } = renderHook(() =>
|
||||||
useAutoAcceptIndicator({
|
useAutoAcceptIndicator({
|
||||||
config: mockConfigInstance as unknown as ActualConfigType,
|
config: mockConfigInstance as unknown as ActualConfigType,
|
||||||
|
addItem: vi.fn(),
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
expect(result.current).toBe(ApprovalMode.DEFAULT);
|
expect(result.current).toBe(ApprovalMode.DEFAULT);
|
||||||
@@ -146,6 +151,7 @@ describe('useAutoAcceptIndicator', () => {
|
|||||||
const { result } = renderHook(() =>
|
const { result } = renderHook(() =>
|
||||||
useAutoAcceptIndicator({
|
useAutoAcceptIndicator({
|
||||||
config: mockConfigInstance as unknown as ActualConfigType,
|
config: mockConfigInstance as unknown as ActualConfigType,
|
||||||
|
addItem: vi.fn(),
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
expect(result.current).toBe(ApprovalMode.YOLO);
|
expect(result.current).toBe(ApprovalMode.YOLO);
|
||||||
@@ -157,6 +163,7 @@ describe('useAutoAcceptIndicator', () => {
|
|||||||
const { result } = renderHook(() =>
|
const { result } = renderHook(() =>
|
||||||
useAutoAcceptIndicator({
|
useAutoAcceptIndicator({
|
||||||
config: mockConfigInstance as unknown as ActualConfigType,
|
config: mockConfigInstance as unknown as ActualConfigType,
|
||||||
|
addItem: vi.fn(),
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
expect(result.current).toBe(ApprovalMode.DEFAULT);
|
expect(result.current).toBe(ApprovalMode.DEFAULT);
|
||||||
@@ -224,6 +231,7 @@ describe('useAutoAcceptIndicator', () => {
|
|||||||
renderHook(() =>
|
renderHook(() =>
|
||||||
useAutoAcceptIndicator({
|
useAutoAcceptIndicator({
|
||||||
config: mockConfigInstance as unknown as ActualConfigType,
|
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)', () => {
|
it('should update indicator when config value changes externally (useEffect dependency)', () => {
|
||||||
mockConfigInstance.getApprovalMode.mockReturnValue(ApprovalMode.DEFAULT);
|
mockConfigInstance.getApprovalMode.mockReturnValue(ApprovalMode.DEFAULT);
|
||||||
const { result, rerender } = renderHook(
|
const { result, rerender } = renderHook(
|
||||||
(props: { config: ActualConfigType }) => useAutoAcceptIndicator(props),
|
(props: { config: ActualConfigType; addItem: () => void }) =>
|
||||||
|
useAutoAcceptIndicator(props),
|
||||||
{
|
{
|
||||||
initialProps: {
|
initialProps: {
|
||||||
config: mockConfigInstance as unknown as ActualConfigType,
|
config: mockConfigInstance as unknown as ActualConfigType,
|
||||||
|
addItem: vi.fn(),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@@ -291,8 +301,173 @@ describe('useAutoAcceptIndicator', () => {
|
|||||||
|
|
||||||
mockConfigInstance.getApprovalMode.mockReturnValue(ApprovalMode.AUTO_EDIT);
|
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(result.current).toBe(ApprovalMode.AUTO_EDIT);
|
||||||
expect(mockConfigInstance.getApprovalMode).toHaveBeenCalledTimes(3);
|
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);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -7,13 +7,17 @@
|
|||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import { ApprovalMode, type Config } from '@google/gemini-cli-core';
|
import { ApprovalMode, type Config } from '@google/gemini-cli-core';
|
||||||
import { useKeypress } from './useKeypress.js';
|
import { useKeypress } from './useKeypress.js';
|
||||||
|
import type { HistoryItemWithoutId } from '../types.js';
|
||||||
|
import { MessageType } from '../types.js';
|
||||||
|
|
||||||
export interface UseAutoAcceptIndicatorArgs {
|
export interface UseAutoAcceptIndicatorArgs {
|
||||||
config: Config;
|
config: Config;
|
||||||
|
addItem: (item: HistoryItemWithoutId, timestamp: number) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useAutoAcceptIndicator({
|
export function useAutoAcceptIndicator({
|
||||||
config,
|
config,
|
||||||
|
addItem,
|
||||||
}: UseAutoAcceptIndicatorArgs): ApprovalMode {
|
}: UseAutoAcceptIndicatorArgs): ApprovalMode {
|
||||||
const currentConfigValue = config.getApprovalMode();
|
const currentConfigValue = config.getApprovalMode();
|
||||||
const [showAutoAcceptIndicator, setShowAutoAcceptIndicator] =
|
const [showAutoAcceptIndicator, setShowAutoAcceptIndicator] =
|
||||||
@@ -40,9 +44,19 @@ export function useAutoAcceptIndicator({
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (nextApprovalMode) {
|
if (nextApprovalMode) {
|
||||||
config.setApprovalMode(nextApprovalMode);
|
try {
|
||||||
// Update local state immediately for responsiveness
|
config.setApprovalMode(nextApprovalMode);
|
||||||
setShowAutoAcceptIndicator(nextApprovalMode);
|
// Update local state immediately for responsiveness
|
||||||
|
setShowAutoAcceptIndicator(nextApprovalMode);
|
||||||
|
} catch (e) {
|
||||||
|
addItem(
|
||||||
|
{
|
||||||
|
type: MessageType.INFO,
|
||||||
|
text: (e as Error).message,
|
||||||
|
},
|
||||||
|
Date.now(),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{ isActive: true },
|
{ isActive: true },
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||||
import type { Mock } from 'vitest';
|
import type { Mock } from 'vitest';
|
||||||
import type { ConfigParameters, SandboxConfig } from './config.js';
|
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 * as path from 'node:path';
|
||||||
import { setGeminiMdFilename as mockSetGeminiMdFilename } from '../tools/memoryTool.js';
|
import { setGeminiMdFilename as mockSetGeminiMdFilename } from '../tools/memoryTool.js';
|
||||||
import {
|
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();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -564,6 +564,11 @@ export class Config {
|
|||||||
}
|
}
|
||||||
|
|
||||||
setApprovalMode(mode: ApprovalMode): void {
|
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;
|
this.approvalMode = mode;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user