mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-08-03 05:31:02 -07:00
Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 413098e03a | |||
| 66abea7a0e | |||
| dc82a395ac |
@@ -1452,6 +1452,11 @@ their corresponding top-level category object in your `settings.json` file.
|
|||||||
performance.
|
performance.
|
||||||
- **Default:** `true`
|
- **Default:** `true`
|
||||||
|
|
||||||
|
- **`tools.shell.rcFile`** (string):
|
||||||
|
- **Description:** The path to a bash file (e.g., .bashrc) to source before
|
||||||
|
executing shell commands.
|
||||||
|
- **Default:** `undefined`
|
||||||
|
|
||||||
- **`tools.core`** (array):
|
- **`tools.core`** (array):
|
||||||
- **Description:** Restrict the set of built-in tools with an allowlist. Match
|
- **Description:** Restrict the set of built-in tools with an allowlist. Match
|
||||||
semantics mirror tools.allowed; see the built-in tools documentation for
|
semantics mirror tools.allowed; see the built-in tools documentation for
|
||||||
|
|||||||
@@ -925,6 +925,7 @@ export async function loadCliConfig(
|
|||||||
toolDiscoveryCommand: settings.tools?.discoveryCommand,
|
toolDiscoveryCommand: settings.tools?.discoveryCommand,
|
||||||
toolCallCommand: settings.tools?.callCommand,
|
toolCallCommand: settings.tools?.callCommand,
|
||||||
mcpServerCommand,
|
mcpServerCommand,
|
||||||
|
shellToolRcFile: settings.tools?.shell?.rcFile,
|
||||||
mcpServers,
|
mcpServers,
|
||||||
mcpEnablementCallbacks,
|
mcpEnablementCallbacks,
|
||||||
mcpEnabled,
|
mcpEnabled,
|
||||||
|
|||||||
@@ -1637,6 +1637,16 @@ const SETTINGS_SCHEMA = {
|
|||||||
'Enable shell output efficiency optimizations for better performance.',
|
'Enable shell output efficiency optimizations for better performance.',
|
||||||
showInDialog: false,
|
showInDialog: false,
|
||||||
},
|
},
|
||||||
|
rcFile: {
|
||||||
|
type: 'string',
|
||||||
|
label: 'Shell Tool RC File',
|
||||||
|
category: 'Tools',
|
||||||
|
requiresRestart: false,
|
||||||
|
default: undefined as string | undefined,
|
||||||
|
description:
|
||||||
|
'The path to a bash file (e.g., .bashrc) to source before executing shell commands.',
|
||||||
|
showInDialog: false,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -602,6 +602,7 @@ export interface ConfigParameters {
|
|||||||
toolDiscoveryCommand?: string;
|
toolDiscoveryCommand?: string;
|
||||||
toolCallCommand?: string;
|
toolCallCommand?: string;
|
||||||
mcpServerCommand?: string;
|
mcpServerCommand?: string;
|
||||||
|
shellToolRcFile?: string;
|
||||||
mcpServers?: Record<string, MCPServerConfig>;
|
mcpServers?: Record<string, MCPServerConfig>;
|
||||||
mcpEnablementCallbacks?: McpEnablementCallbacks;
|
mcpEnablementCallbacks?: McpEnablementCallbacks;
|
||||||
userMemory?: string | HierarchicalMemory;
|
userMemory?: string | HierarchicalMemory;
|
||||||
@@ -779,6 +780,7 @@ export class Config implements McpContext, AgentLoopContext {
|
|||||||
private readonly toolDiscoveryCommand: string | undefined;
|
private readonly toolDiscoveryCommand: string | undefined;
|
||||||
private readonly toolCallCommand: string | undefined;
|
private readonly toolCallCommand: string | undefined;
|
||||||
private readonly mcpServerCommand: string | undefined;
|
private readonly mcpServerCommand: string | undefined;
|
||||||
|
private readonly shellToolRcFile: string | undefined;
|
||||||
private readonly mcpEnabled: boolean;
|
private readonly mcpEnabled: boolean;
|
||||||
private readonly extensionsEnabled: boolean;
|
private readonly extensionsEnabled: boolean;
|
||||||
private mcpServers: Record<string, MCPServerConfig> | undefined;
|
private mcpServers: Record<string, MCPServerConfig> | undefined;
|
||||||
@@ -1047,6 +1049,7 @@ export class Config implements McpContext, AgentLoopContext {
|
|||||||
this.toolDiscoveryCommand = params.toolDiscoveryCommand;
|
this.toolDiscoveryCommand = params.toolDiscoveryCommand;
|
||||||
this.toolCallCommand = params.toolCallCommand;
|
this.toolCallCommand = params.toolCallCommand;
|
||||||
this.mcpServerCommand = params.mcpServerCommand;
|
this.mcpServerCommand = params.mcpServerCommand;
|
||||||
|
this.shellToolRcFile = params.shellToolRcFile;
|
||||||
this.mcpServers = params.mcpServers;
|
this.mcpServers = params.mcpServers;
|
||||||
this.mcpEnablementCallbacks = params.mcpEnablementCallbacks;
|
this.mcpEnablementCallbacks = params.mcpEnablementCallbacks;
|
||||||
this.mcpEnabled = params.mcpEnabled ?? true;
|
this.mcpEnabled = params.mcpEnabled ?? true;
|
||||||
@@ -2318,6 +2321,10 @@ export class Config implements McpContext, AgentLoopContext {
|
|||||||
return this.mcpServerCommand;
|
return this.mcpServerCommand;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getShellToolRcFile(): string | undefined {
|
||||||
|
return this.shellToolRcFile;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The user configured MCP servers (via gemini settings files).
|
* The user configured MCP servers (via gemini settings files).
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -164,6 +164,8 @@ describe('ShellTool', () => {
|
|||||||
addPersistentApproval: vi.fn(),
|
addPersistentApproval: vi.fn(),
|
||||||
addSessionApproval: vi.fn(),
|
addSessionApproval: vi.fn(),
|
||||||
},
|
},
|
||||||
|
getShellToolRcFile: vi.fn().mockReturnValue(undefined),
|
||||||
|
isTrustedFolder: vi.fn().mockReturnValue(true),
|
||||||
} as unknown as Config;
|
} as unknown as Config;
|
||||||
|
|
||||||
const bus = createMockMessageBus();
|
const bus = createMockMessageBus();
|
||||||
@@ -486,6 +488,70 @@ EOF`;
|
|||||||
expect(mockShellExecutionService.mock.calls[0][0]).toMatch(/\nEOF\n\)\n/);
|
expect(mockShellExecutionService.mock.calls[0][0]).toMatch(/\nEOF\n\)\n/);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should source rcfile when shellToolRcFile setting is present and folder is trusted', async () => {
|
||||||
|
const rcFilePath = '~/.geminirc';
|
||||||
|
(mockConfig.getShellToolRcFile as Mock).mockReturnValue(rcFilePath);
|
||||||
|
(mockConfig.isTrustedFolder as Mock).mockReturnValue(true);
|
||||||
|
|
||||||
|
const invocation = shellTool.build({ command: 'my-command' });
|
||||||
|
const promise = invocation.execute({ abortSignal: mockAbortSignal });
|
||||||
|
resolveShellExecution();
|
||||||
|
await promise;
|
||||||
|
|
||||||
|
expect(mockShellExecutionService).toHaveBeenCalledWith(
|
||||||
|
expect.stringContaining(
|
||||||
|
`source '${rcFilePath.replace('~', '/home/user')}'; export PAGER=cat`,
|
||||||
|
),
|
||||||
|
expect.any(String),
|
||||||
|
expect.any(Function),
|
||||||
|
expect.any(AbortSignal),
|
||||||
|
false,
|
||||||
|
expect.any(Object),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should NOT source rcfile when shellToolRcFile setting is present but folder is untrusted', async () => {
|
||||||
|
const rcFilePath = '~/.geminirc';
|
||||||
|
(mockConfig.getShellToolRcFile as Mock).mockReturnValue(rcFilePath);
|
||||||
|
(mockConfig.isTrustedFolder as Mock).mockReturnValue(false);
|
||||||
|
|
||||||
|
const invocation = shellTool.build({ command: 'my-command' });
|
||||||
|
const promise = invocation.execute({ abortSignal: mockAbortSignal });
|
||||||
|
resolveShellExecution();
|
||||||
|
await promise;
|
||||||
|
|
||||||
|
expect(mockShellExecutionService).not.toHaveBeenCalledWith(
|
||||||
|
expect.stringContaining(`source '${rcFilePath}'`),
|
||||||
|
expect.any(String),
|
||||||
|
expect.any(Function),
|
||||||
|
expect.any(AbortSignal),
|
||||||
|
false,
|
||||||
|
expect.any(Object),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should properly escape quotes in rcFilePath', async () => {
|
||||||
|
const rcFilePath = "/path/with/'quotes'/rc";
|
||||||
|
(mockConfig.getShellToolRcFile as Mock).mockReturnValue(rcFilePath);
|
||||||
|
(mockConfig.isTrustedFolder as Mock).mockReturnValue(true);
|
||||||
|
|
||||||
|
const invocation = shellTool.build({ command: 'my-command' });
|
||||||
|
const promise = invocation.execute({ abortSignal: mockAbortSignal });
|
||||||
|
resolveShellExecution();
|
||||||
|
await promise;
|
||||||
|
|
||||||
|
expect(mockShellExecutionService).toHaveBeenCalledWith(
|
||||||
|
expect.stringContaining(
|
||||||
|
`source '/path/with/'\\''quotes'\\''/rc'; export PAGER=cat`,
|
||||||
|
),
|
||||||
|
expect.any(String),
|
||||||
|
expect.any(Function),
|
||||||
|
expect.any(AbortSignal),
|
||||||
|
false,
|
||||||
|
expect.any(Object),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
it('should format error messages correctly', async () => {
|
it('should format error messages correctly', async () => {
|
||||||
const error = new Error('wrapped command failed');
|
const error = new Error('wrapped command failed');
|
||||||
const invocation = shellTool.build({ command: 'user-command' });
|
const invocation = shellTool.build({ command: 'user-command' });
|
||||||
|
|||||||
@@ -50,7 +50,12 @@ import type { MessageBus } from '../confirmation-bus/message-bus.js';
|
|||||||
import { getShellDefinition } from './definitions/coreTools.js';
|
import { getShellDefinition } from './definitions/coreTools.js';
|
||||||
import { resolveToolDeclaration } from './definitions/resolver.js';
|
import { resolveToolDeclaration } from './definitions/resolver.js';
|
||||||
import type { AgentLoopContext } from '../config/agent-loop-context.js';
|
import type { AgentLoopContext } from '../config/agent-loop-context.js';
|
||||||
import { toPathKey, isSubpath, resolveToRealPath } from '../utils/paths.js';
|
import {
|
||||||
|
homedir,
|
||||||
|
toPathKey,
|
||||||
|
isSubpath,
|
||||||
|
resolveToRealPath,
|
||||||
|
} from '../utils/paths.js';
|
||||||
import {
|
import {
|
||||||
getProactiveToolSuggestions,
|
getProactiveToolSuggestions,
|
||||||
isNetworkReliantCommand,
|
isNetworkReliantCommand,
|
||||||
@@ -462,13 +467,30 @@ export class ShellToolInvocation extends BaseToolInvocation<
|
|||||||
const combinedController = new AbortController();
|
const combinedController = new AbortController();
|
||||||
|
|
||||||
const onAbort = () => combinedController.abort();
|
const onAbort = () => combinedController.abort();
|
||||||
|
let strippedCommandWithRc = strippedCommand;
|
||||||
|
if (!isWindows) {
|
||||||
|
strippedCommandWithRc = `export PAGER=cat GIT_PAGER=cat; ${strippedCommand}`;
|
||||||
|
const rcFilePath = this.context.config.getShellToolRcFile();
|
||||||
|
if (rcFilePath && this.context.config.isTrustedFolder()) {
|
||||||
|
let resolvedRcFilePath = rcFilePath;
|
||||||
|
if (rcFilePath === '~' || rcFilePath.startsWith('~/')) {
|
||||||
|
resolvedRcFilePath = homedir() + rcFilePath.substring(1);
|
||||||
|
}
|
||||||
|
const escapedRcFilePath = resolvedRcFilePath.replace(
|
||||||
|
/'/g,
|
||||||
|
() => "'\\''",
|
||||||
|
);
|
||||||
|
strippedCommandWithRc = `source '${escapedRcFilePath}'; ${strippedCommandWithRc}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gemini-shell-'));
|
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gemini-shell-'));
|
||||||
tempFilePath = path.join(tempDir, 'pgrep.tmp');
|
tempFilePath = path.join(tempDir, 'pgrep.tmp');
|
||||||
|
|
||||||
// pgrep is not available on Windows, so we can't get background PIDs
|
// pgrep is not available on Windows, so we can't get background PIDs
|
||||||
const commandToExecute = this.wrapCommandForPgrep(
|
const commandToExecute = this.wrapCommandForPgrep(
|
||||||
strippedCommand,
|
strippedCommandWithRc,
|
||||||
tempFilePath,
|
tempFilePath,
|
||||||
isWindows,
|
isWindows,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -2509,6 +2509,12 @@
|
|||||||
"markdownDescription": "Enable shell output efficiency optimizations for better performance.\n\n- Category: `Tools`\n- Requires restart: `no`\n- Default: `true`",
|
"markdownDescription": "Enable shell output efficiency optimizations for better performance.\n\n- Category: `Tools`\n- Requires restart: `no`\n- Default: `true`",
|
||||||
"default": true,
|
"default": true,
|
||||||
"type": "boolean"
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"rcFile": {
|
||||||
|
"title": "Shell Tool RC File",
|
||||||
|
"description": "The path to a bash file (e.g., .bashrc) to source before executing shell commands.",
|
||||||
|
"markdownDescription": "The path to a bash file (e.g., .bashrc) to source before executing shell commands.\n\n- Category: `Tools`\n- Requires restart: `no`",
|
||||||
|
"type": "string"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"additionalProperties": false
|
"additionalProperties": false
|
||||||
|
|||||||
Reference in New Issue
Block a user