mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-03-29 07:21:27 -07:00
feat(core): improve shell redirection transparency and security (#16486)
This commit is contained in:
@@ -0,0 +1,54 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { describe, it, expect, vi, beforeAll } from 'vitest';
|
||||
import { ToolConfirmationMessage } from './ToolConfirmationMessage.js';
|
||||
import type {
|
||||
ToolCallConfirmationDetails,
|
||||
Config,
|
||||
} from '@google/gemini-cli-core';
|
||||
import { initializeShellParsers } from '@google/gemini-cli-core';
|
||||
import { renderWithProviders } from '../../../test-utils/render.js';
|
||||
|
||||
describe('ToolConfirmationMessage Redirection', () => {
|
||||
beforeAll(async () => {
|
||||
await initializeShellParsers();
|
||||
});
|
||||
|
||||
const mockConfig = {
|
||||
isTrustedFolder: () => true,
|
||||
getIdeMode: () => false,
|
||||
} as unknown as Config;
|
||||
|
||||
it('should display redirection warning and tip for redirected commands', () => {
|
||||
const confirmationDetails: ToolCallConfirmationDetails = {
|
||||
type: 'exec',
|
||||
title: 'Confirm Shell Command',
|
||||
command: 'echo "hello" > test.txt',
|
||||
rootCommand: 'echo, redirection (>)',
|
||||
rootCommands: ['echo'],
|
||||
onConfirm: vi.fn(),
|
||||
};
|
||||
|
||||
const { lastFrame } = renderWithProviders(
|
||||
<ToolConfirmationMessage
|
||||
confirmationDetails={confirmationDetails}
|
||||
config={mockConfig}
|
||||
availableTerminalHeight={30}
|
||||
terminalWidth={100}
|
||||
/>,
|
||||
);
|
||||
|
||||
const output = lastFrame();
|
||||
expect(output).toContain('echo "hello" > test.txt');
|
||||
expect(output).toContain(
|
||||
'Note: Command contains redirection which can be undesirable.',
|
||||
);
|
||||
expect(output).toContain(
|
||||
'Tip: Toggle auto-edit (Shift+Tab) to allow redirection in the future.',
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -13,13 +13,23 @@ import type {
|
||||
ToolCallConfirmationDetails,
|
||||
Config,
|
||||
} from '@google/gemini-cli-core';
|
||||
import { IdeClient, ToolConfirmationOutcome } from '@google/gemini-cli-core';
|
||||
import {
|
||||
IdeClient,
|
||||
ToolConfirmationOutcome,
|
||||
hasRedirection,
|
||||
} from '@google/gemini-cli-core';
|
||||
import type { RadioSelectItem } from '../shared/RadioButtonSelect.js';
|
||||
import { RadioButtonSelect } from '../shared/RadioButtonSelect.js';
|
||||
import { MaxSizedBox } from '../shared/MaxSizedBox.js';
|
||||
import { MaxSizedBox, MINIMUM_MAX_HEIGHT } from '../shared/MaxSizedBox.js';
|
||||
import { useKeypress } from '../../hooks/useKeypress.js';
|
||||
import { theme } from '../../semantic-colors.js';
|
||||
import { useSettings } from '../../contexts/SettingsContext.js';
|
||||
import {
|
||||
REDIRECTION_WARNING_NOTE_LABEL,
|
||||
REDIRECTION_WARNING_NOTE_TEXT,
|
||||
REDIRECTION_WARNING_TIP_LABEL,
|
||||
REDIRECTION_WARNING_TIP_TEXT,
|
||||
} from '../../textConstants.js';
|
||||
|
||||
export interface ToolConfirmationMessageProps {
|
||||
confirmationDetails: ToolCallConfirmationDetails;
|
||||
@@ -270,30 +280,79 @@ export const ToolConfirmationMessage: React.FC<
|
||||
}
|
||||
} else if (confirmationDetails.type === 'exec') {
|
||||
const executionProps = confirmationDetails;
|
||||
|
||||
const commandsToDisplay =
|
||||
executionProps.commands && executionProps.commands.length > 1
|
||||
? executionProps.commands
|
||||
: [executionProps.command];
|
||||
const containsRedirection = commandsToDisplay.some((cmd) =>
|
||||
hasRedirection(cmd),
|
||||
);
|
||||
|
||||
let bodyContentHeight = availableBodyContentHeight();
|
||||
let warnings: React.ReactNode = null;
|
||||
|
||||
if (bodyContentHeight !== undefined) {
|
||||
bodyContentHeight -= 2; // Account for padding;
|
||||
}
|
||||
|
||||
if (containsRedirection) {
|
||||
// Calculate lines needed for Note and Tip
|
||||
const safeWidth = Math.max(terminalWidth, 1);
|
||||
const noteLength =
|
||||
REDIRECTION_WARNING_NOTE_LABEL.length +
|
||||
REDIRECTION_WARNING_NOTE_TEXT.length;
|
||||
const tipLength =
|
||||
REDIRECTION_WARNING_TIP_LABEL.length +
|
||||
REDIRECTION_WARNING_TIP_TEXT.length;
|
||||
|
||||
const noteLines = Math.ceil(noteLength / safeWidth);
|
||||
const tipLines = Math.ceil(tipLength / safeWidth);
|
||||
const spacerLines = 1;
|
||||
const warningHeight = noteLines + tipLines + spacerLines;
|
||||
|
||||
if (bodyContentHeight !== undefined) {
|
||||
bodyContentHeight = Math.max(
|
||||
bodyContentHeight - warningHeight,
|
||||
MINIMUM_MAX_HEIGHT,
|
||||
);
|
||||
}
|
||||
|
||||
warnings = (
|
||||
<>
|
||||
<Box height={1} />
|
||||
<Box>
|
||||
<Text color={theme.text.primary}>
|
||||
<Text bold>{REDIRECTION_WARNING_NOTE_LABEL}</Text>
|
||||
{REDIRECTION_WARNING_NOTE_TEXT}
|
||||
</Text>
|
||||
</Box>
|
||||
<Box>
|
||||
<Text color={theme.border.default}>
|
||||
<Text bold>{REDIRECTION_WARNING_TIP_LABEL}</Text>
|
||||
{REDIRECTION_WARNING_TIP_TEXT}
|
||||
</Text>
|
||||
</Box>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
bodyContent = (
|
||||
<MaxSizedBox
|
||||
maxHeight={bodyContentHeight}
|
||||
maxWidth={Math.max(terminalWidth, 1)}
|
||||
>
|
||||
<Box flexDirection="column">
|
||||
{executionProps.commands && executionProps.commands.length > 1 ? (
|
||||
executionProps.commands.map((cmd, idx) => (
|
||||
<Box flexDirection="column">
|
||||
<MaxSizedBox
|
||||
maxHeight={bodyContentHeight}
|
||||
maxWidth={Math.max(terminalWidth, 1)}
|
||||
>
|
||||
<Box flexDirection="column">
|
||||
{commandsToDisplay.map((cmd, idx) => (
|
||||
<Text key={idx} color={theme.text.link}>
|
||||
{cmd}
|
||||
</Text>
|
||||
))
|
||||
) : (
|
||||
<Box>
|
||||
<Text color={theme.text.link}>{executionProps.command}</Text>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
</MaxSizedBox>
|
||||
))}
|
||||
</Box>
|
||||
</MaxSizedBox>
|
||||
{warnings}
|
||||
</Box>
|
||||
);
|
||||
} else if (confirmationDetails.type === 'info') {
|
||||
const infoProps = confirmationDetails;
|
||||
|
||||
@@ -11,3 +11,10 @@ export const SCREEN_READER_MODEL_PREFIX = 'Model: ';
|
||||
export const SCREEN_READER_LOADING = 'loading';
|
||||
|
||||
export const SCREEN_READER_RESPONDING = 'responding';
|
||||
|
||||
export const REDIRECTION_WARNING_NOTE_LABEL = 'Note: ';
|
||||
export const REDIRECTION_WARNING_NOTE_TEXT =
|
||||
'Command contains redirection which can be undesirable.';
|
||||
export const REDIRECTION_WARNING_TIP_LABEL = 'Tip: '; // Padded to align with "Note: "
|
||||
export const REDIRECTION_WARNING_TIP_TEXT =
|
||||
'Toggle auto-edit (Shift+Tab) to allow redirection in the future.';
|
||||
|
||||
Reference in New Issue
Block a user