diff --git a/docs/get-started/configuration.md b/docs/get-started/configuration.md index e3415eccfa..567bde0894 100644 --- a/docs/get-started/configuration.md +++ b/docs/get-started/configuration.md @@ -731,6 +731,11 @@ their corresponding top-level category object in your `settings.json` file. - **Default:** `false` - **Requires restart:** Yes +- **`security.enablePermanentToolApproval`** (boolean): + - **Description:** Enable the "Allow for all future sessions" option in tool + confirmation dialogs. + - **Default:** `false` + - **`security.blockGitExtensions`** (boolean): - **Description:** Blocks installing and loading extensions from Git. - **Default:** `false` diff --git a/packages/cli/src/config/settingsSchema.ts b/packages/cli/src/config/settingsSchema.ts index 5b8f75af60..ee2a9d3824 100644 --- a/packages/cli/src/config/settingsSchema.ts +++ b/packages/cli/src/config/settingsSchema.ts @@ -1164,6 +1164,16 @@ const SETTINGS_SCHEMA = { description: 'Disable YOLO mode, even if enabled by a flag.', showInDialog: true, }, + enablePermanentToolApproval: { + type: 'boolean', + label: 'Allow Permanent Tool Approval', + category: 'Security', + requiresRestart: false, + default: false, + description: + 'Enable the "Allow for all future sessions" option in tool confirmation dialogs.', + showInDialog: true, + }, blockGitExtensions: { type: 'boolean', label: 'Blocks extensions from Git', diff --git a/packages/cli/src/ui/components/messages/ToolConfirmationMessage.test.tsx b/packages/cli/src/ui/components/messages/ToolConfirmationMessage.test.tsx index 4063af9116..0291396e63 100644 --- a/packages/cli/src/ui/components/messages/ToolConfirmationMessage.test.tsx +++ b/packages/cli/src/ui/components/messages/ToolConfirmationMessage.test.tsx @@ -10,7 +10,10 @@ import type { ToolCallConfirmationDetails, Config, } from '@google/gemini-cli-core'; -import { renderWithProviders } from '../../../test-utils/render.js'; +import { + renderWithProviders, + createMockSettings, +} from '../../../test-utils/render.js'; describe('ToolConfirmationMessage', () => { const mockConfig = { @@ -159,4 +162,63 @@ describe('ToolConfirmationMessage', () => { }); }); }); + + describe('enablePermanentToolApproval setting', () => { + const editConfirmationDetails: ToolCallConfirmationDetails = { + type: 'edit', + title: 'Confirm Edit', + fileName: 'test.txt', + filePath: '/test.txt', + fileDiff: '...diff...', + originalContent: 'a', + newContent: 'b', + onConfirm: vi.fn(), + }; + + it('should NOT show "Allow for all future sessions" when setting is false (default)', () => { + const mockConfig = { + isTrustedFolder: () => true, + getIdeMode: () => false, + } as unknown as Config; + + const { lastFrame } = renderWithProviders( + , + { + settings: createMockSettings({ + security: { enablePermanentToolApproval: false }, + }), + }, + ); + + expect(lastFrame()).not.toContain('Allow for all future sessions'); + }); + + it('should show "Allow for all future sessions" when setting is true', () => { + const mockConfig = { + isTrustedFolder: () => true, + getIdeMode: () => false, + } as unknown as Config; + + const { lastFrame } = renderWithProviders( + , + { + settings: createMockSettings({ + security: { enablePermanentToolApproval: true }, + }), + }, + ); + + expect(lastFrame()).toContain('Allow for all future sessions'); + }); + }); }); diff --git a/packages/cli/src/ui/components/messages/ToolConfirmationMessage.tsx b/packages/cli/src/ui/components/messages/ToolConfirmationMessage.tsx index 17b4477067..5be9169f02 100644 --- a/packages/cli/src/ui/components/messages/ToolConfirmationMessage.tsx +++ b/packages/cli/src/ui/components/messages/ToolConfirmationMessage.tsx @@ -20,6 +20,7 @@ import { MaxSizedBox } from '../shared/MaxSizedBox.js'; import { useKeypress } from '../../hooks/useKeypress.js'; import { theme } from '../../semantic-colors.js'; import { useAlternateBuffer } from '../../hooks/useAlternateBuffer.js'; +import { useSettings } from '../../contexts/SettingsContext.js'; export interface ToolConfirmationMessageProps { confirmationDetails: ToolCallConfirmationDetails; @@ -41,6 +42,9 @@ export const ToolConfirmationMessage: React.FC< const { onConfirm } = confirmationDetails; const isAlternateBuffer = useAlternateBuffer(); + const settings = useSettings(); + const allowPermanentApproval = + settings.merged.security?.enablePermanentToolApproval ?? false; const [ideClient, setIdeClient] = useState(null); const [isDiffingEnabled, setIsDiffingEnabled] = useState(false); @@ -112,11 +116,13 @@ export const ToolConfirmationMessage: React.FC< value: ToolConfirmationOutcome.ProceedAlways, key: 'Allow for this session', }); - options.push({ - label: 'Allow for all future sessions', - value: ToolConfirmationOutcome.ProceedAlwaysAndSave, - key: 'Allow for all future sessions', - }); + if (allowPermanentApproval) { + options.push({ + label: 'Allow for all future sessions', + value: ToolConfirmationOutcome.ProceedAlwaysAndSave, + key: 'Allow for all future sessions', + }); + } } if (!config.getIdeMode() || !isDiffingEnabled) { options.push({ @@ -147,11 +153,13 @@ export const ToolConfirmationMessage: React.FC< value: ToolConfirmationOutcome.ProceedAlways, key: `Allow for this session`, }); - options.push({ - label: `Allow for all future sessions`, - value: ToolConfirmationOutcome.ProceedAlwaysAndSave, - key: `Allow for all future sessions`, - }); + if (allowPermanentApproval) { + options.push({ + label: `Allow for all future sessions`, + value: ToolConfirmationOutcome.ProceedAlwaysAndSave, + key: `Allow for all future sessions`, + }); + } } options.push({ label: 'No, suggest changes (esc)', @@ -171,11 +179,13 @@ export const ToolConfirmationMessage: React.FC< value: ToolConfirmationOutcome.ProceedAlways, key: 'Allow for this session', }); - options.push({ - label: 'Allow for all future sessions', - value: ToolConfirmationOutcome.ProceedAlwaysAndSave, - key: 'Allow for all future sessions', - }); + if (allowPermanentApproval) { + options.push({ + label: 'Allow for all future sessions', + value: ToolConfirmationOutcome.ProceedAlwaysAndSave, + key: 'Allow for all future sessions', + }); + } } options.push({ label: 'No, suggest changes (esc)', @@ -202,11 +212,13 @@ export const ToolConfirmationMessage: React.FC< value: ToolConfirmationOutcome.ProceedAlwaysServer, key: 'Allow all server tools for this session', }); - options.push({ - label: 'Allow tool for all future sessions', - value: ToolConfirmationOutcome.ProceedAlwaysAndSave, - key: 'Allow tool for all future sessions', - }); + if (allowPermanentApproval) { + options.push({ + label: 'Allow tool for all future sessions', + value: ToolConfirmationOutcome.ProceedAlwaysAndSave, + key: 'Allow tool for all future sessions', + }); + } } options.push({ label: 'No, suggest changes (esc)', @@ -327,6 +339,7 @@ export const ToolConfirmationMessage: React.FC< availableTerminalHeight, terminalWidth, isAlternateBuffer, + allowPermanentApproval, ]); if (confirmationDetails.type === 'edit') { diff --git a/packages/cli/src/ui/components/messages/ToolGroupMessage.test.tsx b/packages/cli/src/ui/components/messages/ToolGroupMessage.test.tsx index ee1e15763e..b36523715a 100644 --- a/packages/cli/src/ui/components/messages/ToolGroupMessage.test.tsx +++ b/packages/cli/src/ui/components/messages/ToolGroupMessage.test.tsx @@ -4,7 +4,10 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { renderWithProviders } from '../../../test-utils/render.js'; +import { + renderWithProviders, + createMockSettings, +} from '../../../test-utils/render.js'; import { describe, it, expect, vi } from 'vitest'; import { ToolGroupMessage } from './ToolGroupMessage.js'; import type { IndividualToolCallDisplay } from '../../types.js'; @@ -376,5 +379,57 @@ describe('', () => { expect(lastFrame()).toMatchSnapshot(); unmount(); }); + + it('renders confirmation with permanent approval enabled', () => { + const toolCalls = [ + createToolCall({ + callId: 'tool-1', + name: 'confirm-tool', + status: ToolCallStatus.Confirming, + confirmationDetails: { + type: 'info', + title: 'Confirm Tool', + prompt: 'Do you want to proceed?', + onConfirm: vi.fn(), + }, + }), + ]; + const settings = createMockSettings({ + security: { enablePermanentToolApproval: true }, + }); + const { lastFrame, unmount } = renderWithProviders( + , + { settings }, + ); + expect(lastFrame()).toContain('Allow for all future sessions'); + expect(lastFrame()).toMatchSnapshot(); + unmount(); + }); + + it('renders confirmation with permanent approval disabled', () => { + const toolCalls = [ + createToolCall({ + callId: 'tool-1', + name: 'confirm-tool', + status: ToolCallStatus.Confirming, + confirmationDetails: { + type: 'info', + title: 'Confirm Tool', + prompt: 'Do you want to proceed?', + onConfirm: vi.fn(), + }, + }), + ]; + const settings = createMockSettings({ + security: { enablePermanentToolApproval: false }, + }); + const { lastFrame, unmount } = renderWithProviders( + , + { settings }, + ); + expect(lastFrame()).not.toContain('Allow for all future sessions'); + expect(lastFrame()).toMatchSnapshot(); + unmount(); + }); }); }); diff --git a/packages/cli/src/ui/components/messages/__snapshots__/ToolConfirmationMessage.test.tsx.snap b/packages/cli/src/ui/components/messages/__snapshots__/ToolConfirmationMessage.test.tsx.snap index 9187fad554..d89a4686eb 100644 --- a/packages/cli/src/ui/components/messages/__snapshots__/ToolConfirmationMessage.test.tsx.snap +++ b/packages/cli/src/ui/components/messages/__snapshots__/ToolConfirmationMessage.test.tsx.snap @@ -10,8 +10,7 @@ Do you want to proceed? ● 1. Allow once 2. Allow for this session - 3. Allow for all future sessions - 4. No, suggest changes (esc) + 3. No, suggest changes (esc) " `; @@ -22,8 +21,7 @@ Do you want to proceed? ● 1. Allow once 2. Allow for this session - 3. Allow for all future sessions - 4. No, suggest changes (esc) + 3. No, suggest changes (esc) " `; @@ -53,9 +51,8 @@ Apply this change? ● 1. Allow once 2. Allow for this session - 3. Allow for all future sessions - 4. Modify with external editor - 5. No, suggest changes (esc) + 3. Modify with external editor + 4. No, suggest changes (esc) " `; @@ -76,8 +73,7 @@ Allow execution of: 'echo'? ● 1. Allow once 2. Allow for this session - 3. Allow for all future sessions - 4. No, suggest changes (esc) + 3. No, suggest changes (esc) " `; @@ -98,8 +94,7 @@ Do you want to proceed? ● 1. Allow once 2. Allow for this session - 3. Allow for all future sessions - 4. No, suggest changes (esc) + 3. No, suggest changes (esc) " `; @@ -123,7 +118,6 @@ Allow execution of MCP tool "test-tool" from server "test-server"? ● 1. Allow once 2. Allow tool for this session 3. Allow all server tools for this session - 4. Allow tool for all future sessions - 5. No, suggest changes (esc) + 4. No, suggest changes (esc) " `; diff --git a/packages/cli/src/ui/components/messages/__snapshots__/ToolGroupMessage.test.tsx.snap b/packages/cli/src/ui/components/messages/__snapshots__/ToolGroupMessage.test.tsx.snap index 7133df2b58..6bea5eecd5 100644 --- a/packages/cli/src/ui/components/messages/__snapshots__/ToolGroupMessage.test.tsx.snap +++ b/packages/cli/src/ui/components/messages/__snapshots__/ToolGroupMessage.test.tsx.snap @@ -28,6 +28,39 @@ exports[` > Border Color Logic > uses yellow border when too ╰──────────────────────────────────────────────────────────────────────────────╯" `; +exports[` > Confirmation Handling > renders confirmation with permanent approval disabled 1`] = ` +"╭──────────────────────────────────────────────────────────────────────────────╮ +│ ? confirm-tool A tool for testing ← │ +│ │ +│ Test result │ +│ Do you want to proceed? │ +│ │ +│ Do you want to proceed? │ +│ │ +│ ● 1. Allow once │ +│ 2. Allow for this session │ +│ 3. No, suggest changes (esc) │ +│ │ +╰──────────────────────────────────────────────────────────────────────────────╯" +`; + +exports[` > Confirmation Handling > renders confirmation with permanent approval enabled 1`] = ` +"╭──────────────────────────────────────────────────────────────────────────────╮ +│ ? confirm-tool A tool for testing ← │ +│ │ +│ Test result │ +│ Do you want to proceed? │ +│ │ +│ Do you want to proceed? │ +│ │ +│ ● 1. Allow once │ +│ 2. Allow for this session │ +│ 3. Allow for all future sessions │ +│ 4. No, suggest changes (esc) │ +│ │ +╰──────────────────────────────────────────────────────────────────────────────╯" +`; + exports[` > Confirmation Handling > shows confirmation dialog for first confirming tool only 1`] = ` "╭──────────────────────────────────────────────────────────────────────────────╮ │ ? first-confirm A tool for testing ← │ @@ -39,8 +72,7 @@ exports[` > Confirmation Handling > shows confirmation dialo │ │ │ ● 1. Allow once │ │ 2. Allow for this session │ -│ 3. Allow for all future sessions │ -│ 4. No, suggest changes (esc) │ +│ 3. No, suggest changes (esc) │ │ │ │ │ │ ? second-confirm A tool for testing │ @@ -123,8 +155,7 @@ exports[` > Golden Snapshots > renders tool call awaiting co │ │ │ ● 1. Allow once │ │ 2. Allow for this session │ -│ 3. Allow for all future sessions │ -│ 4. No, suggest changes (esc) │ +│ 3. No, suggest changes (esc) │ │ │ ╰──────────────────────────────────────────────────────────────────────────────╯" `; diff --git a/schemas/settings.schema.json b/schemas/settings.schema.json index 7570abe6b8..b391496aa6 100644 --- a/schemas/settings.schema.json +++ b/schemas/settings.schema.json @@ -1200,6 +1200,13 @@ "default": false, "type": "boolean" }, + "enablePermanentToolApproval": { + "title": "Allow Permanent Tool Approval", + "description": "Enable the \"Allow for all future sessions\" option in tool confirmation dialogs.", + "markdownDescription": "Enable the \"Allow for all future sessions\" option in tool confirmation dialogs.\n\n- Category: `Security`\n- Requires restart: `no`\n- Default: `false`", + "default": false, + "type": "boolean" + }, "blockGitExtensions": { "title": "Blocks extensions from Git", "description": "Blocks installing and loading extensions from Git.",