Add excludedTools to extensions. (#2853)

This commit is contained in:
Tommaso Sciortino 2025-07-01 16:13:46 -07:00 committed by GitHub
parent e94decea39
commit 3492c429b9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 148 additions and 2 deletions

View File

@ -28,7 +28,8 @@ The `gemini-extension.json` file contains the configuration for the extension. T
"command": "node my-server.js"
}
},
"contextFileName": "GEMINI.md"
"contextFileName": "GEMINI.md",
"excludeTools": ["run_shell_command"]
}
```
@ -36,5 +37,6 @@ The `gemini-extension.json` file contains the configuration for the extension. T
- `version`: The version of the extension.
- `mcpServers`: A map of MCP servers to configure. The key is the name of the server, and the value is the server configuration. These servers will be loaded on startup just like MCP servers configured in a [`settings.json` file](./cli/configuration.md). If both an extension and a `settings.json` file configure an MCP server with the same name, the server defined in the `settings.json` file takes precedence.
- `contextFileName`: The name of the file that contains the context for the extension. This will be used to load the context from the workspace. If this property is not used but a `GEMINI.md` file is present in your extension directory, then that file will be loaded.
- `excludeTools`: An array of tool names to exclude from the model. You can also specify command-specific restrictions for tools that support it, like the `run_shell_command` tool. For example, `"excludeTools": ["run_shell_command(rm -rf)"]` will block the `rm -rf` command.
When Gemini CLI starts, it loads all the extensions and merges their configurations. If there are any conflicts, the workspace configuration takes precedence.

View File

@ -350,3 +350,131 @@ describe('mergeMcpServers', () => {
expect(settings).toEqual(originalSettings);
});
});
describe('mergeExcludeTools', () => {
it('should merge excludeTools from settings and extensions', async () => {
const settings: Settings = { excludeTools: ['tool1', 'tool2'] };
const extensions: Extension[] = [
{
config: {
name: 'ext1',
version: '1.0.0',
excludeTools: ['tool3', 'tool4'],
},
contextFiles: [],
},
{
config: {
name: 'ext2',
version: '1.0.0',
excludeTools: ['tool5'],
},
contextFiles: [],
},
];
const config = await loadCliConfig(settings, extensions, 'test-session');
expect(config.getExcludeTools()).toEqual(
expect.arrayContaining(['tool1', 'tool2', 'tool3', 'tool4', 'tool5']),
);
expect(config.getExcludeTools()).toHaveLength(5);
});
it('should handle overlapping excludeTools between settings and extensions', async () => {
const settings: Settings = { excludeTools: ['tool1', 'tool2'] };
const extensions: Extension[] = [
{
config: {
name: 'ext1',
version: '1.0.0',
excludeTools: ['tool2', 'tool3'],
},
contextFiles: [],
},
];
const config = await loadCliConfig(settings, extensions, 'test-session');
expect(config.getExcludeTools()).toEqual(
expect.arrayContaining(['tool1', 'tool2', 'tool3']),
);
expect(config.getExcludeTools()).toHaveLength(3);
});
it('should handle overlapping excludeTools between extensions', async () => {
const settings: Settings = { excludeTools: ['tool1'] };
const extensions: Extension[] = [
{
config: {
name: 'ext1',
version: '1.0.0',
excludeTools: ['tool2', 'tool3'],
},
contextFiles: [],
},
{
config: {
name: 'ext2',
version: '1.0.0',
excludeTools: ['tool3', 'tool4'],
},
contextFiles: [],
},
];
const config = await loadCliConfig(settings, extensions, 'test-session');
expect(config.getExcludeTools()).toEqual(
expect.arrayContaining(['tool1', 'tool2', 'tool3', 'tool4']),
);
expect(config.getExcludeTools()).toHaveLength(4);
});
it('should return an empty array when no excludeTools are specified', async () => {
const settings: Settings = {};
const extensions: Extension[] = [];
const config = await loadCliConfig(settings, extensions, 'test-session');
expect(config.getExcludeTools()).toEqual([]);
});
it('should handle settings with excludeTools but no extensions', async () => {
const settings: Settings = { excludeTools: ['tool1', 'tool2'] };
const extensions: Extension[] = [];
const config = await loadCliConfig(settings, extensions, 'test-session');
expect(config.getExcludeTools()).toEqual(
expect.arrayContaining(['tool1', 'tool2']),
);
expect(config.getExcludeTools()).toHaveLength(2);
});
it('should handle extensions with excludeTools but no settings', async () => {
const settings: Settings = {};
const extensions: Extension[] = [
{
config: {
name: 'ext1',
version: '1.0.0',
excludeTools: ['tool1', 'tool2'],
},
contextFiles: [],
},
];
const config = await loadCliConfig(settings, extensions, 'test-session');
expect(config.getExcludeTools()).toEqual(
expect.arrayContaining(['tool1', 'tool2']),
);
expect(config.getExcludeTools()).toHaveLength(2);
});
it('should not modify the original settings object', async () => {
const settings: Settings = { excludeTools: ['tool1'] };
const extensions: Extension[] = [
{
config: {
name: 'ext1',
version: '1.0.0',
excludeTools: ['tool2'],
},
contextFiles: [],
},
];
const originalSettings = JSON.parse(JSON.stringify(settings));
await loadCliConfig(settings, extensions, 'test-session');
expect(settings).toEqual(originalSettings);
});
});

View File

@ -194,6 +194,7 @@ export async function loadCliConfig(
);
const mcpServers = mergeMcpServers(settings, extensions);
const excludeTools = mergeExcludeTools(settings, extensions);
const sandboxConfig = await loadSandboxConfig(settings, argv);
@ -206,7 +207,7 @@ export async function loadCliConfig(
question: argv.prompt || '',
fullContext: argv.all_files || false,
coreTools: settings.coreTools || undefined,
excludeTools: settings.excludeTools || undefined,
excludeTools,
toolDiscoveryCommand: settings.toolDiscoveryCommand,
toolCallCommand: settings.toolCallCommand,
mcpServerCommand: settings.mcpServerCommand,
@ -265,6 +266,20 @@ function mergeMcpServers(settings: Settings, extensions: Extension[]) {
}
return mcpServers;
}
function mergeExcludeTools(
settings: Settings,
extensions: Extension[],
): string[] {
const allExcludeTools = new Set(settings.excludeTools || []);
for (const extension of extensions) {
for (const tool of extension.config.excludeTools || []) {
allExcludeTools.add(tool);
}
}
return [...allExcludeTools];
}
function findEnvFile(startDir: string): string | null {
let currentDir = path.resolve(startDir);
while (true) {

View File

@ -22,6 +22,7 @@ export interface ExtensionConfig {
version: string;
mcpServers?: Record<string, MCPServerConfig>;
contextFileName?: string | string[];
excludeTools?: string[];
}
export function loadExtensions(workspaceDir: string): Extension[] {