mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-06-28 20:27:08 -07:00
feat(core): restrict auto-approve checkbox to safe commands
Hide the "Allow for all future sessions" checkbox during exec tool confirmation unless every command in the input passes a strict allowlist. - Introduce safeCommandAllowlist for read-only utilities (ls, cat, grep, etc.) - Introduce editCommandAllowlist for file-mutating commands, gated behind ApprovalMode.AUTO_EDIT - Use getCommandRoots (Wasm parser) to extract all base executables from piped, chained, and wrapped commands - Fail closed: hide checkbox if parser fails or any command root is unknown - Exclude find/awk/sed from safe list (can execute arbitrary commands) Ref: google-gemini/maintainers-gemini-cli#1578
This commit is contained in:
@@ -4,18 +4,28 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
||||
import {
|
||||
describe,
|
||||
it,
|
||||
expect,
|
||||
vi,
|
||||
beforeEach,
|
||||
afterEach,
|
||||
beforeAll,
|
||||
} from 'vitest';
|
||||
import { ToolConfirmationMessage } from './ToolConfirmationMessage.js';
|
||||
import {
|
||||
type SerializableConfirmationDetails,
|
||||
type ToolCallConfirmationDetails,
|
||||
type Config,
|
||||
ToolConfirmationOutcome,
|
||||
initializeShellParsers,
|
||||
} from '@google/gemini-cli-core';
|
||||
import { renderWithProviders } from '../../../test-utils/render.js';
|
||||
import { createMockSettings } from '../../../test-utils/settings.js';
|
||||
import { useToolActions } from '../../contexts/ToolActionsContext.js';
|
||||
import { act } from 'react';
|
||||
import '../../../test-utils/customMatchers.js';
|
||||
|
||||
vi.mock('../../contexts/ToolActionsContext.js', async (importOriginal) => {
|
||||
const actual =
|
||||
@@ -29,6 +39,141 @@ vi.mock('../../contexts/ToolActionsContext.js', async (importOriginal) => {
|
||||
});
|
||||
|
||||
describe('ToolConfirmationMessage', () => {
|
||||
beforeAll(async () => {
|
||||
await initializeShellParsers();
|
||||
});
|
||||
|
||||
describe('Auto-approve checkbox for exec tools', () => {
|
||||
const mockSettingsWithPermanent = createMockSettings({
|
||||
merged: { security: { enablePermanentToolApproval: true } },
|
||||
});
|
||||
|
||||
const mockConfigWithPermanent = {
|
||||
isTrustedFolder: () => true,
|
||||
getIdeMode: () => false,
|
||||
getDisableAlwaysAllow: () => false,
|
||||
getApprovalMode: () => 'default',
|
||||
getApprovalMode: () => 'default',
|
||||
} as unknown as Config;
|
||||
|
||||
it('shows permanent approval option for safe commands', async () => {
|
||||
const execSafe: SerializableConfirmationDetails = {
|
||||
type: 'exec',
|
||||
title: 'Confirm Execution',
|
||||
command: 'ls -la',
|
||||
rootCommand: 'ls',
|
||||
rootCommands: ['ls'],
|
||||
};
|
||||
|
||||
const { lastFrame, waitUntilReady, unmount } = renderWithProviders(
|
||||
<ToolConfirmationMessage
|
||||
callId="test-call-id"
|
||||
confirmationDetails={execSafe}
|
||||
config={mockConfigWithPermanent}
|
||||
getPreferredEditor={vi.fn()}
|
||||
availableTerminalHeight={30}
|
||||
terminalWidth={80}
|
||||
/>,
|
||||
{ settings: mockSettingsWithPermanent },
|
||||
);
|
||||
|
||||
await waitUntilReady();
|
||||
const output = lastFrame();
|
||||
expect(output).toContain('Allow this command for all future sessions');
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('hides permanent approval option for unsafe commands', async () => {
|
||||
const execUnsafe: SerializableConfirmationDetails = {
|
||||
type: 'exec',
|
||||
title: 'Confirm Execution',
|
||||
command: 'rm -rf /',
|
||||
rootCommand: 'rm',
|
||||
rootCommands: ['rm'],
|
||||
};
|
||||
|
||||
const { lastFrame, waitUntilReady, unmount } = renderWithProviders(
|
||||
<ToolConfirmationMessage
|
||||
callId="test-call-id"
|
||||
confirmationDetails={execUnsafe}
|
||||
config={mockConfigWithPermanent}
|
||||
getPreferredEditor={vi.fn()}
|
||||
availableTerminalHeight={30}
|
||||
terminalWidth={80}
|
||||
/>,
|
||||
{ settings: mockSettingsWithPermanent },
|
||||
);
|
||||
|
||||
await waitUntilReady();
|
||||
const output = lastFrame();
|
||||
expect(output).not.toContain(
|
||||
'Allow this command for all future sessions',
|
||||
);
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('shows permanent approval option for edit commands in AUTO_EDIT mode', async () => {
|
||||
const mockConfigAutoEdit = {
|
||||
...mockConfigWithPermanent,
|
||||
getApprovalMode: () => 'autoEdit',
|
||||
} as unknown as Config;
|
||||
|
||||
const execEdit: SerializableConfirmationDetails = {
|
||||
type: 'exec',
|
||||
title: 'Confirm Execution',
|
||||
command: 'mkdir test',
|
||||
rootCommand: 'mkdir',
|
||||
rootCommands: ['mkdir'],
|
||||
};
|
||||
|
||||
const { lastFrame, waitUntilReady, unmount } = renderWithProviders(
|
||||
<ToolConfirmationMessage
|
||||
callId="test-call-id"
|
||||
confirmationDetails={execEdit}
|
||||
config={mockConfigAutoEdit}
|
||||
getPreferredEditor={vi.fn()}
|
||||
availableTerminalHeight={30}
|
||||
terminalWidth={80}
|
||||
/>,
|
||||
{ settings: mockSettingsWithPermanent },
|
||||
);
|
||||
|
||||
await waitUntilReady();
|
||||
const output = lastFrame();
|
||||
expect(output).toContain('Allow this command for all future sessions');
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('hides permanent approval option for edit commands in DEFAULT mode', async () => {
|
||||
const execEdit: SerializableConfirmationDetails = {
|
||||
type: 'exec',
|
||||
title: 'Confirm Execution',
|
||||
command: 'mkdir test',
|
||||
rootCommand: 'mkdir',
|
||||
rootCommands: ['mkdir'],
|
||||
};
|
||||
|
||||
const { lastFrame, waitUntilReady, unmount } = renderWithProviders(
|
||||
<ToolConfirmationMessage
|
||||
callId="test-call-id"
|
||||
confirmationDetails={execEdit}
|
||||
config={mockConfigWithPermanent}
|
||||
getPreferredEditor={vi.fn()}
|
||||
availableTerminalHeight={30}
|
||||
terminalWidth={80}
|
||||
/>,
|
||||
{ settings: mockSettingsWithPermanent },
|
||||
);
|
||||
|
||||
await waitUntilReady();
|
||||
const output = lastFrame();
|
||||
expect(output).not.toContain(
|
||||
'Allow this command for all future sessions',
|
||||
);
|
||||
unmount();
|
||||
});
|
||||
});
|
||||
|
||||
const mockConfirm = vi.fn();
|
||||
vi.mocked(useToolActions).mockReturnValue({
|
||||
confirm: mockConfirm,
|
||||
@@ -40,6 +185,7 @@ describe('ToolConfirmationMessage', () => {
|
||||
isTrustedFolder: () => true,
|
||||
getIdeMode: () => false,
|
||||
getDisableAlwaysAllow: () => false,
|
||||
getApprovalMode: () => 'default',
|
||||
} as unknown as Config;
|
||||
|
||||
it('should not display urls if prompt and url are the same', async () => {
|
||||
@@ -335,6 +481,7 @@ describe('ToolConfirmationMessage', () => {
|
||||
isTrustedFolder: () => true,
|
||||
getIdeMode: () => false,
|
||||
getDisableAlwaysAllow: () => false,
|
||||
getApprovalMode: () => 'default',
|
||||
} as unknown as Config;
|
||||
const { lastFrame, waitUntilReady, unmount } = renderWithProviders(
|
||||
<ToolConfirmationMessage
|
||||
@@ -357,6 +504,7 @@ describe('ToolConfirmationMessage', () => {
|
||||
isTrustedFolder: () => false,
|
||||
getIdeMode: () => false,
|
||||
getDisableAlwaysAllow: () => false,
|
||||
getApprovalMode: () => 'default',
|
||||
} as unknown as Config;
|
||||
|
||||
const { lastFrame, waitUntilReady, unmount } = renderWithProviders(
|
||||
@@ -393,6 +541,7 @@ describe('ToolConfirmationMessage', () => {
|
||||
isTrustedFolder: () => true,
|
||||
getIdeMode: () => false,
|
||||
getDisableAlwaysAllow: () => false,
|
||||
getApprovalMode: () => 'default',
|
||||
} as unknown as Config;
|
||||
const { lastFrame, waitUntilReady, unmount } = renderWithProviders(
|
||||
<ToolConfirmationMessage
|
||||
@@ -420,6 +569,7 @@ describe('ToolConfirmationMessage', () => {
|
||||
isTrustedFolder: () => true,
|
||||
getIdeMode: () => false,
|
||||
getDisableAlwaysAllow: () => false,
|
||||
getApprovalMode: () => 'default',
|
||||
} as unknown as Config;
|
||||
const { lastFrame, waitUntilReady, unmount } = renderWithProviders(
|
||||
<ToolConfirmationMessage
|
||||
@@ -462,6 +612,7 @@ describe('ToolConfirmationMessage', () => {
|
||||
isTrustedFolder: () => true,
|
||||
getIdeMode: () => false,
|
||||
getDisableAlwaysAllow: () => false,
|
||||
getApprovalMode: () => 'default',
|
||||
} as unknown as Config;
|
||||
vi.mocked(useToolActions).mockReturnValue({
|
||||
confirm: vi.fn(),
|
||||
@@ -490,6 +641,7 @@ describe('ToolConfirmationMessage', () => {
|
||||
isTrustedFolder: () => true,
|
||||
getIdeMode: () => true,
|
||||
getDisableAlwaysAllow: () => false,
|
||||
getApprovalMode: () => 'default',
|
||||
} as unknown as Config;
|
||||
vi.mocked(useToolActions).mockReturnValue({
|
||||
confirm: vi.fn(),
|
||||
@@ -518,6 +670,7 @@ describe('ToolConfirmationMessage', () => {
|
||||
isTrustedFolder: () => true,
|
||||
getIdeMode: () => true,
|
||||
getDisableAlwaysAllow: () => false,
|
||||
getApprovalMode: () => 'default',
|
||||
} as unknown as Config;
|
||||
vi.mocked(useToolActions).mockReturnValue({
|
||||
confirm: vi.fn(),
|
||||
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
ToolConfirmationOutcome,
|
||||
type EditorType,
|
||||
hasRedirection,
|
||||
canShowAutoApproveCheckbox,
|
||||
debugLogger,
|
||||
} from '@google/gemini-cli-core';
|
||||
import { useToolActions } from '../../contexts/ToolActionsContext.js';
|
||||
@@ -299,7 +300,11 @@ export const ToolConfirmationMessage: React.FC<
|
||||
value: ToolConfirmationOutcome.ProceedAlways,
|
||||
key: `Allow for this session`,
|
||||
});
|
||||
if (allowPermanentApproval) {
|
||||
const isAutoApprovable = canShowAutoApproveCheckbox(
|
||||
confirmationDetails.command,
|
||||
config.getApprovalMode(),
|
||||
);
|
||||
if (allowPermanentApproval && isAutoApprovable) {
|
||||
options.push({
|
||||
label: `Allow this command for all future sessions`,
|
||||
value: ToolConfirmationOutcome.ProceedAlwaysAndSave,
|
||||
|
||||
+8
-24
@@ -4,29 +4,13 @@
|
||||
</style>
|
||||
<rect width="920" height="173" fill="#000000" />
|
||||
<g transform="translate(10, 10)">
|
||||
<text x="0" y="2" fill="#00cdcd" textLength="36" lengthAdjust="spacingAndGlyphs">echo</text>
|
||||
<text x="45" y="2" fill="#cdcd00" textLength="63" lengthAdjust="spacingAndGlyphs">"hello"</text>
|
||||
<text x="0" y="19" fill="#0000ee" textLength="27" lengthAdjust="spacingAndGlyphs">for</text>
|
||||
<text x="27" y="19" fill="#e5e5e5" textLength="27" lengthAdjust="spacingAndGlyphs"> i </text>
|
||||
<text x="54" y="19" fill="#0000ee" textLength="18" lengthAdjust="spacingAndGlyphs">in</text>
|
||||
<text x="72" y="19" fill="#e5e5e5" textLength="72" lengthAdjust="spacingAndGlyphs"> 1 2 3; </text>
|
||||
<text x="144" y="19" fill="#0000ee" textLength="18" lengthAdjust="spacingAndGlyphs">do</text>
|
||||
<text x="18" y="36" fill="#00cdcd" textLength="36" lengthAdjust="spacingAndGlyphs">echo</text>
|
||||
<text x="63" y="36" fill="#cd00cd" textLength="18" lengthAdjust="spacingAndGlyphs">$i</text>
|
||||
<text x="0" y="53" fill="#0000ee" textLength="36" lengthAdjust="spacingAndGlyphs">done</text>
|
||||
<text x="0" y="70" fill="#ffffff" textLength="243" lengthAdjust="spacingAndGlyphs">Allow execution of: 'echo'?</text>
|
||||
<rect x="0" y="102" width="9" height="17" fill="#001a00" />
|
||||
<text x="0" y="104" fill="#00cd00" textLength="9" lengthAdjust="spacingAndGlyphs">●</text>
|
||||
<rect x="9" y="102" width="9" height="17" fill="#001a00" />
|
||||
<rect x="18" y="102" width="18" height="17" fill="#001a00" />
|
||||
<text x="18" y="104" fill="#00cd00" textLength="18" lengthAdjust="spacingAndGlyphs">1.</text>
|
||||
<rect x="36" y="102" width="9" height="17" fill="#001a00" />
|
||||
<rect x="45" y="102" width="90" height="17" fill="#001a00" />
|
||||
<text x="45" y="104" fill="#00cd00" textLength="90" lengthAdjust="spacingAndGlyphs">Allow once</text>
|
||||
<rect x="135" y="102" width="135" height="17" fill="#001a00" />
|
||||
<text x="18" y="121" fill="#ffffff" textLength="18" lengthAdjust="spacingAndGlyphs">2.</text>
|
||||
<text x="45" y="121" fill="#ffffff" textLength="198" lengthAdjust="spacingAndGlyphs">Allow for this session</text>
|
||||
<text x="18" y="138" fill="#ffffff" textLength="18" lengthAdjust="spacingAndGlyphs">3.</text>
|
||||
<text x="45" y="138" fill="#ffffff" textLength="225" lengthAdjust="spacingAndGlyphs">No, suggest changes (esc)</text>
|
||||
<text x="0" y="2" fill="#ffffff" textLength="900" lengthAdjust="spacingAndGlyphs">echo "hello" </text>
|
||||
<text x="0" y="19" fill="#ffffff" textLength="900" lengthAdjust="spacingAndGlyphs">for i in 1 2 3; do </text>
|
||||
<text x="0" y="36" fill="#ffffff" textLength="900" lengthAdjust="spacingAndGlyphs"> echo $i </text>
|
||||
<text x="0" y="53" fill="#ffffff" textLength="900" lengthAdjust="spacingAndGlyphs">done </text>
|
||||
<text x="0" y="70" fill="#ffffff" textLength="900" lengthAdjust="spacingAndGlyphs">Allow execution of: 'echo'? </text>
|
||||
<text x="0" y="104" fill="#ffffff" textLength="900" lengthAdjust="spacingAndGlyphs">● 1. Allow once </text>
|
||||
<text x="0" y="121" fill="#ffffff" textLength="900" lengthAdjust="spacingAndGlyphs"> 2. Allow for this session </text>
|
||||
<text x="0" y="138" fill="#ffffff" textLength="900" lengthAdjust="spacingAndGlyphs"> 3. No, suggest changes (esc) </text>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 2.5 KiB After Width: | Height: | Size: 1.9 KiB |
+14
-14
@@ -8,7 +8,7 @@ exports[`ToolConfirmationMessage > enablePermanentToolApproval setting > should
|
||||
╰──────────────────────────────────────────────────────────────────────────────╯
|
||||
Apply this change?
|
||||
|
||||
● 1. Allow once
|
||||
● 1. Allow once
|
||||
2. Allow for this session
|
||||
3. Allow for this file in all future sessions
|
||||
4. Modify with external editor
|
||||
@@ -24,7 +24,7 @@ ls -la
|
||||
whoami
|
||||
Allow execution of 3 commands?
|
||||
|
||||
● 1. Allow once
|
||||
● 1. Allow once
|
||||
2. Allow for this session
|
||||
3. No, suggest changes (esc)
|
||||
"
|
||||
@@ -37,7 +37,7 @@ URLs to fetch:
|
||||
- https://raw.githubusercontent.com/google/gemini-react/main/README.md
|
||||
Do you want to proceed?
|
||||
|
||||
● 1. Allow once
|
||||
● 1. Allow once
|
||||
2. Allow for this session
|
||||
3. No, suggest changes (esc)
|
||||
"
|
||||
@@ -47,7 +47,7 @@ exports[`ToolConfirmationMessage > should not display urls if prompt and url are
|
||||
"https://example.com
|
||||
Do you want to proceed?
|
||||
|
||||
● 1. Allow once
|
||||
● 1. Allow once
|
||||
2. Allow for this session
|
||||
3. No, suggest changes (esc)
|
||||
"
|
||||
@@ -60,7 +60,7 @@ for i in 1 2 3; do
|
||||
done
|
||||
Allow execution of: 'echo'?
|
||||
|
||||
● 1. Allow once
|
||||
● 1. Allow once
|
||||
2. Allow for this session
|
||||
3. No, suggest changes (esc)
|
||||
"
|
||||
@@ -71,7 +71,7 @@ exports[`ToolConfirmationMessage > should strip BiDi characters from MCP tool an
|
||||
Tool: testtool
|
||||
Allow execution of MCP tool "testtool" from server "testserver"?
|
||||
|
||||
● 1. Allow once
|
||||
● 1. Allow once
|
||||
2. Allow tool for this session
|
||||
3. Allow all server tools for this session
|
||||
4. No, suggest changes (esc)
|
||||
@@ -86,7 +86,7 @@ exports[`ToolConfirmationMessage > with folder trust > 'for edit confirmations'
|
||||
╰──────────────────────────────────────────────────────────────────────────────╯
|
||||
Apply this change?
|
||||
|
||||
● 1. Allow once
|
||||
● 1. Allow once
|
||||
2. Modify with external editor
|
||||
3. No, suggest changes (esc)
|
||||
"
|
||||
@@ -100,7 +100,7 @@ exports[`ToolConfirmationMessage > with folder trust > 'for edit confirmations'
|
||||
╰──────────────────────────────────────────────────────────────────────────────╯
|
||||
Apply this change?
|
||||
|
||||
● 1. Allow once
|
||||
● 1. Allow once
|
||||
2. Allow for this session
|
||||
3. Modify with external editor
|
||||
4. No, suggest changes (esc)
|
||||
@@ -111,7 +111,7 @@ exports[`ToolConfirmationMessage > with folder trust > 'for exec confirmations'
|
||||
"echo "hello"
|
||||
Allow execution of: 'echo'?
|
||||
|
||||
● 1. Allow once
|
||||
● 1. Allow once
|
||||
2. No, suggest changes (esc)
|
||||
"
|
||||
`;
|
||||
@@ -120,7 +120,7 @@ exports[`ToolConfirmationMessage > with folder trust > 'for exec confirmations'
|
||||
"echo "hello"
|
||||
Allow execution of: 'echo'?
|
||||
|
||||
● 1. Allow once
|
||||
● 1. Allow once
|
||||
2. Allow for this session
|
||||
3. No, suggest changes (esc)
|
||||
"
|
||||
@@ -130,7 +130,7 @@ exports[`ToolConfirmationMessage > with folder trust > 'for info confirmations'
|
||||
"https://example.com
|
||||
Do you want to proceed?
|
||||
|
||||
● 1. Allow once
|
||||
● 1. Allow once
|
||||
2. No, suggest changes (esc)
|
||||
"
|
||||
`;
|
||||
@@ -139,7 +139,7 @@ exports[`ToolConfirmationMessage > with folder trust > 'for info confirmations'
|
||||
"https://example.com
|
||||
Do you want to proceed?
|
||||
|
||||
● 1. Allow once
|
||||
● 1. Allow once
|
||||
2. Allow for this session
|
||||
3. No, suggest changes (esc)
|
||||
"
|
||||
@@ -150,7 +150,7 @@ exports[`ToolConfirmationMessage > with folder trust > 'for mcp confirmations' >
|
||||
Tool: test-tool
|
||||
Allow execution of MCP tool "test-tool" from server "test-server"?
|
||||
|
||||
● 1. Allow once
|
||||
● 1. Allow once
|
||||
2. No, suggest changes (esc)
|
||||
"
|
||||
`;
|
||||
@@ -160,7 +160,7 @@ exports[`ToolConfirmationMessage > with folder trust > 'for mcp confirmations' >
|
||||
Tool: test-tool
|
||||
Allow execution of MCP tool "test-tool" from server "test-server"?
|
||||
|
||||
● 1. Allow once
|
||||
● 1. Allow once
|
||||
2. Allow tool for this session
|
||||
3. Allow all server tools for this session
|
||||
4. No, suggest changes (esc)
|
||||
|
||||
Reference in New Issue
Block a user