feat(cli): Add --allowed-tools flag to bypass tool confirmation (#2417) (#6453)

This commit is contained in:
Andrew Garrett
2025-08-27 02:17:43 +10:00
committed by GitHub
parent c33a0da1df
commit 52dae2c583
14 changed files with 524 additions and 135 deletions

View File

@@ -23,6 +23,9 @@ import { GeminiClient } from '../core/client.js';
import { GitService } from '../services/gitService.js';
import { ClearcutLogger } from '../telemetry/clearcut-logger/clearcut-logger.js';
import { ShellTool } from '../tools/shell.js';
import { ReadFileTool } from '../tools/read-file.js';
vi.mock('fs', async (importOriginal) => {
const actual = await importOriginal<typeof import('fs')>();
return {
@@ -629,6 +632,36 @@ describe('Server Config (config.ts)', () => {
expect(config.getUseRipgrep()).toBe(false);
});
});
describe('createToolRegistry', () => {
it('should register a tool if coreTools contains an argument-specific pattern', async () => {
const params: ConfigParameters = {
...baseParams,
coreTools: ['ShellTool(git status)'],
};
const config = new Config(params);
await config.initialize();
// The ToolRegistry class is mocked, so we can inspect its prototype's methods.
const registerToolMock = (
(await vi.importMock('../tools/tool-registry')) as {
ToolRegistry: { prototype: { registerTool: Mock } };
}
).ToolRegistry.prototype.registerTool;
// Check that registerTool was called for ShellTool
const wasShellToolRegistered = (registerToolMock as Mock).mock.calls.some(
(call) => call[0] instanceof vi.mocked(ShellTool),
);
expect(wasShellToolRegistered).toBe(true);
// Check that registerTool was NOT called for ReadFileTool
const wasReadFileToolRegistered = (
registerToolMock as Mock
).mock.calls.some((call) => call[0] instanceof vi.mocked(ReadFileTool));
expect(wasReadFileToolRegistered).toBe(false);
});
});
});
describe('setApprovalMode with folder trust', () => {

View File

@@ -49,7 +49,8 @@ import { logCliConfiguration, logIdeConnection } from '../telemetry/loggers.js';
import { IdeConnectionEvent, IdeConnectionType } from '../telemetry/types.js';
// Re-export OAuth config type
export type { MCPOAuthConfig };
export type { MCPOAuthConfig, AnyToolInvocation };
import type { AnyToolInvocation } from '../tools/tools.js';
import { WorkspaceContext } from '../utils/workspaceContext.js';
import { Storage } from './storage.js';
import { FileExclusions } from '../utils/ignorePatterns.js';
@@ -159,6 +160,7 @@ export interface ConfigParameters {
question?: string;
fullContext?: boolean;
coreTools?: string[];
allowedTools?: string[];
excludeTools?: string[];
toolDiscoveryCommand?: string;
toolCallCommand?: string;
@@ -221,6 +223,7 @@ export class Config {
private readonly question: string | undefined;
private readonly fullContext: boolean;
private readonly coreTools: string[] | undefined;
private readonly allowedTools: string[] | undefined;
private readonly excludeTools: string[] | undefined;
private readonly toolDiscoveryCommand: string | undefined;
private readonly toolCallCommand: string | undefined;
@@ -295,6 +298,7 @@ export class Config {
this.question = params.question;
this.fullContext = params.fullContext ?? false;
this.coreTools = params.coreTools;
this.allowedTools = params.allowedTools;
this.excludeTools = params.excludeTools;
this.toolDiscoveryCommand = params.toolDiscoveryCommand;
this.toolCallCommand = params.toolCallCommand;
@@ -523,6 +527,10 @@ export class Config {
return this.coreTools;
}
getAllowedTools(): string[] | undefined {
return this.allowedTools;
}
getExcludeTools(): string[] | undefined {
return this.excludeTools;
}
@@ -807,12 +815,10 @@ export class Config {
const className = ToolClass.name;
const toolName = ToolClass.Name || className;
const coreTools = this.getCoreTools();
const excludeTools = this.getExcludeTools();
const excludeTools = this.getExcludeTools() || [];
let isEnabled = false;
if (coreTools === undefined) {
isEnabled = true;
} else {
let isEnabled = true; // Enabled by default if coreTools is not set.
if (coreTools) {
isEnabled = coreTools.some(
(tool) =>
tool === className ||
@@ -822,10 +828,11 @@ export class Config {
);
}
if (
excludeTools?.includes(className) ||
excludeTools?.includes(toolName)
) {
const isExcluded = excludeTools.some(
(tool) => tool === className || tool === toolName,
);
if (isExcluded) {
isEnabled = false;
}