mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-07-15 12:30:32 -07:00
feat(policy): Introduce config-based policy engine with TOML configuration (#11992)
This commit is contained in:
@@ -1101,6 +1101,11 @@ export class Config {
|
||||
async createToolRegistry(): Promise<ToolRegistry> {
|
||||
const registry = new ToolRegistry(this, this.eventEmitter);
|
||||
|
||||
// Set message bus on tool registry before discovery so MCP tools can access it
|
||||
if (this.getEnableMessageBusIntegration()) {
|
||||
registry.setMessageBus(this.messageBus);
|
||||
}
|
||||
|
||||
// helper to create & register core tools that are enabled
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const registerCoreTool = (ToolClass: any, ...args: unknown[]) => {
|
||||
|
||||
@@ -54,6 +54,10 @@ export class Storage {
|
||||
return path.join(Storage.getGlobalGeminiDir(), 'memory.md');
|
||||
}
|
||||
|
||||
static getUserPoliciesDir(): string {
|
||||
return path.join(Storage.getGlobalGeminiDir(), 'policies');
|
||||
}
|
||||
|
||||
static getGlobalTempDir(): string {
|
||||
return path.join(Storage.getGlobalGeminiDir(), TMP_DIR_NAME);
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
type PolicyRule,
|
||||
} from './types.js';
|
||||
import { stableStringify } from './stable-stringify.js';
|
||||
import { debugLogger } from '../utils/debugLogger.js';
|
||||
|
||||
function ruleMatches(
|
||||
rule: PolicyRule,
|
||||
@@ -71,14 +72,24 @@ export class PolicyEngine {
|
||||
stringifiedArgs = stableStringify(toolCall.args);
|
||||
}
|
||||
|
||||
debugLogger.debug(
|
||||
`[PolicyEngine.check] toolCall.name: ${toolCall.name}, stringifiedArgs: ${stringifiedArgs}`,
|
||||
);
|
||||
|
||||
// Find the first matching rule (already sorted by priority)
|
||||
for (const rule of this.rules) {
|
||||
if (ruleMatches(rule, toolCall, stringifiedArgs)) {
|
||||
debugLogger.debug(
|
||||
`[PolicyEngine.check] MATCHED rule: toolName=${rule.toolName}, decision=${rule.decision}, priority=${rule.priority}, argsPattern=${rule.argsPattern?.source || 'none'}`,
|
||||
);
|
||||
return this.applyNonInteractiveMode(rule.decision);
|
||||
}
|
||||
}
|
||||
|
||||
// No matching rule found, use default decision
|
||||
debugLogger.debug(
|
||||
`[PolicyEngine.check] NO MATCH - using default decision: ${this.defaultDecision}`,
|
||||
);
|
||||
return this.applyNonInteractiveMode(this.defaultDecision);
|
||||
}
|
||||
|
||||
|
||||
@@ -89,6 +89,7 @@ describe('mcp-client', () => {
|
||||
} as unknown as GenAiLib.CallableTool);
|
||||
const mockedToolRegistry = {
|
||||
registerTool: vi.fn(),
|
||||
getMessageBus: vi.fn().mockReturnValue(undefined),
|
||||
} as unknown as ToolRegistry;
|
||||
const client = new McpClient(
|
||||
'test-server',
|
||||
@@ -152,6 +153,7 @@ describe('mcp-client', () => {
|
||||
} as unknown as GenAiLib.CallableTool);
|
||||
const mockedToolRegistry = {
|
||||
registerTool: vi.fn(),
|
||||
getMessageBus: vi.fn().mockReturnValue(undefined),
|
||||
} as unknown as ToolRegistry;
|
||||
const client = new McpClient(
|
||||
'test-server',
|
||||
@@ -190,12 +192,16 @@ describe('mcp-client', () => {
|
||||
vi.mocked(GenAiLib.mcpToTool).mockReturnValue({
|
||||
tool: () => Promise.resolve({ functionDeclarations: [] }),
|
||||
} as unknown as GenAiLib.CallableTool);
|
||||
const mockedToolRegistry = {
|
||||
registerTool: vi.fn(),
|
||||
getMessageBus: vi.fn().mockReturnValue(undefined),
|
||||
} as unknown as ToolRegistry;
|
||||
const client = new McpClient(
|
||||
'test-server',
|
||||
{
|
||||
command: 'test-command',
|
||||
},
|
||||
{} as ToolRegistry,
|
||||
mockedToolRegistry,
|
||||
{} as PromptRegistry,
|
||||
workspaceContext,
|
||||
false,
|
||||
@@ -231,6 +237,7 @@ describe('mcp-client', () => {
|
||||
const mockedMcpToTool = vi.mocked(GenAiLib.mcpToTool);
|
||||
const mockedToolRegistry = {
|
||||
registerTool: vi.fn(),
|
||||
getMessageBus: vi.fn().mockReturnValue(undefined),
|
||||
} as unknown as ToolRegistry;
|
||||
const client = new McpClient(
|
||||
'test-server',
|
||||
@@ -279,6 +286,7 @@ describe('mcp-client', () => {
|
||||
} as unknown as GenAiLib.CallableTool);
|
||||
const mockedToolRegistry = {
|
||||
registerTool: vi.fn(),
|
||||
getMessageBus: vi.fn().mockReturnValue(undefined),
|
||||
} as unknown as ToolRegistry;
|
||||
const client = new McpClient(
|
||||
'test-server',
|
||||
|
||||
@@ -42,6 +42,7 @@ import type {
|
||||
} from '../utils/workspaceContext.js';
|
||||
import type { ToolRegistry } from './tool-registry.js';
|
||||
import { debugLogger } from '../utils/debugLogger.js';
|
||||
import type { MessageBus } from '../confirmation-bus/message-bus.js';
|
||||
import { coreEvents } from '../utils/events.js';
|
||||
|
||||
export const MCP_DEFAULT_TIMEOUT_MSEC = 10 * 60 * 1000; // default to 10 minutes
|
||||
@@ -198,6 +199,7 @@ export class McpClient {
|
||||
this.serverConfig,
|
||||
this.client!,
|
||||
cliConfig,
|
||||
this.toolRegistry.getMessageBus(),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -545,6 +547,7 @@ export async function connectAndDiscover(
|
||||
mcpServerConfig,
|
||||
mcpClient,
|
||||
cliConfig,
|
||||
toolRegistry.getMessageBus(),
|
||||
);
|
||||
|
||||
// If we have neither prompts nor tools, it's a failed discovery
|
||||
@@ -582,6 +585,8 @@ export async function connectAndDiscover(
|
||||
* @param mcpServerName The name of the MCP server.
|
||||
* @param mcpServerConfig The configuration for the MCP server.
|
||||
* @param mcpClient The active MCP client instance.
|
||||
* @param cliConfig The CLI configuration object.
|
||||
* @param messageBus Optional message bus for policy engine integration.
|
||||
* @returns A promise that resolves to an array of discovered and enabled tools.
|
||||
* @throws An error if no enabled tools are found or if the server provides invalid function declarations.
|
||||
*/
|
||||
@@ -590,6 +595,7 @@ export async function discoverTools(
|
||||
mcpServerConfig: MCPServerConfig,
|
||||
mcpClient: Client,
|
||||
cliConfig: Config,
|
||||
messageBus?: MessageBus,
|
||||
): Promise<DiscoveredMCPTool[]> {
|
||||
try {
|
||||
// Only request tools if the server supports them.
|
||||
@@ -612,19 +618,29 @@ export async function discoverTools(
|
||||
continue;
|
||||
}
|
||||
|
||||
discoveredTools.push(
|
||||
new DiscoveredMCPTool(
|
||||
mcpCallableTool,
|
||||
mcpServerName,
|
||||
funcDecl.name!,
|
||||
funcDecl.description ?? '',
|
||||
funcDecl.parametersJsonSchema ?? { type: 'object', properties: {} },
|
||||
mcpServerConfig.trust,
|
||||
undefined,
|
||||
cliConfig,
|
||||
mcpServerConfig.extension?.id,
|
||||
),
|
||||
const tool = new DiscoveredMCPTool(
|
||||
mcpCallableTool,
|
||||
mcpServerName,
|
||||
funcDecl.name!,
|
||||
funcDecl.description ?? '',
|
||||
funcDecl.parametersJsonSchema ?? { type: 'object', properties: {} },
|
||||
mcpServerConfig.trust,
|
||||
undefined,
|
||||
cliConfig,
|
||||
mcpServerConfig.extension?.id,
|
||||
messageBus,
|
||||
);
|
||||
|
||||
if (
|
||||
cliConfig.getDebugMode?.() &&
|
||||
cliConfig.getEnableMessageBusIntegration?.()
|
||||
) {
|
||||
debugLogger.log(
|
||||
`[DEBUG] Discovered MCP tool '${funcDecl.name}' from server '${mcpServerName}' with messageBus: ${messageBus ? 'YES' : 'NO'}`,
|
||||
);
|
||||
}
|
||||
|
||||
discoveredTools.push(tool);
|
||||
} catch (error) {
|
||||
coreEvents.emitFeedback(
|
||||
'error',
|
||||
|
||||
@@ -72,11 +72,15 @@ class DiscoveredMCPToolInvocation extends BaseToolInvocation<
|
||||
readonly trust?: boolean,
|
||||
params: ToolParams = {},
|
||||
private readonly cliConfig?: Config,
|
||||
messageBus?: MessageBus,
|
||||
) {
|
||||
super(params);
|
||||
// Use composite format for policy checks: serverName__toolName
|
||||
// This enables server wildcards (e.g., "google-workspace__*")
|
||||
// while still allowing specific tool rules
|
||||
super(params, messageBus, `${serverName}__${serverToolName}`, displayName);
|
||||
}
|
||||
|
||||
override async shouldConfirmExecute(
|
||||
protected override async getConfirmationDetails(
|
||||
_abortSignal: AbortSignal,
|
||||
): Promise<ToolCallConfirmationDetails | false> {
|
||||
const serverAllowListKey = this.serverName;
|
||||
@@ -215,6 +219,7 @@ export class DiscoveredMCPTool extends BaseDeclarativeTool<
|
||||
nameOverride?: string,
|
||||
private readonly cliConfig?: Config,
|
||||
override readonly extensionId?: string,
|
||||
messageBus?: MessageBus,
|
||||
) {
|
||||
super(
|
||||
nameOverride ?? generateValidName(serverToolName),
|
||||
@@ -223,8 +228,8 @@ export class DiscoveredMCPTool extends BaseDeclarativeTool<
|
||||
Kind.Other,
|
||||
parameterSchema,
|
||||
true, // isOutputMarkdown
|
||||
false, // canUpdateOutput
|
||||
undefined, // messageBus
|
||||
false, // canUpdateOutput,
|
||||
messageBus,
|
||||
extensionId,
|
||||
);
|
||||
}
|
||||
@@ -240,6 +245,7 @@ export class DiscoveredMCPTool extends BaseDeclarativeTool<
|
||||
`${this.serverName}__${this.serverToolName}`,
|
||||
this.cliConfig,
|
||||
this.extensionId,
|
||||
this.messageBus,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -257,6 +263,7 @@ export class DiscoveredMCPTool extends BaseDeclarativeTool<
|
||||
this.trust,
|
||||
params,
|
||||
this.cliConfig,
|
||||
_messageBus,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -60,8 +60,10 @@ export class ShellToolInvocation extends BaseToolInvocation<
|
||||
params: ShellToolParams,
|
||||
private readonly allowlist: Set<string>,
|
||||
messageBus?: MessageBus,
|
||||
_toolName?: string,
|
||||
_toolDisplayName?: string,
|
||||
) {
|
||||
super(params, messageBus);
|
||||
super(params, messageBus, _toolName, _toolDisplayName);
|
||||
}
|
||||
|
||||
getDescription(): string {
|
||||
@@ -451,12 +453,16 @@ export class ShellTool extends BaseDeclarativeTool<
|
||||
protected createInvocation(
|
||||
params: ShellToolParams,
|
||||
messageBus?: MessageBus,
|
||||
_toolName?: string,
|
||||
_toolDisplayName?: string,
|
||||
): ToolInvocation<ShellToolParams, ToolResult> {
|
||||
return new ShellToolInvocation(
|
||||
this.config,
|
||||
params,
|
||||
this.allowlist,
|
||||
messageBus,
|
||||
_toolName,
|
||||
_toolDisplayName,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -177,12 +177,21 @@ export class ToolRegistry {
|
||||
private tools: Map<string, AnyDeclarativeTool> = new Map();
|
||||
private config: Config;
|
||||
private mcpClientManager: McpClientManager;
|
||||
private messageBus?: MessageBus;
|
||||
|
||||
constructor(config: Config, eventEmitter?: EventEmitter) {
|
||||
this.config = config;
|
||||
this.mcpClientManager = new McpClientManager(this, eventEmitter);
|
||||
}
|
||||
|
||||
setMessageBus(messageBus: MessageBus): void {
|
||||
this.messageBus = messageBus;
|
||||
}
|
||||
|
||||
getMessageBus(): MessageBus | undefined {
|
||||
return this.messageBus;
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a tool definition.
|
||||
* @param tool - The tool object containing schema and execution logic.
|
||||
|
||||
@@ -99,6 +99,15 @@ class WriteTodosToolInvocation extends BaseToolInvocation<
|
||||
WriteTodosToolParams,
|
||||
ToolResult
|
||||
> {
|
||||
constructor(
|
||||
params: WriteTodosToolParams,
|
||||
messageBus?: MessageBus,
|
||||
_toolName?: string,
|
||||
_toolDisplayName?: string,
|
||||
) {
|
||||
super(params, messageBus, _toolName, _toolDisplayName);
|
||||
}
|
||||
|
||||
getDescription(): string {
|
||||
const count = this.params.todos?.length ?? 0;
|
||||
if (count === 0) {
|
||||
@@ -209,6 +218,11 @@ export class WriteTodosTool extends BaseDeclarativeTool<
|
||||
_toolName?: string,
|
||||
_displayName?: string,
|
||||
): ToolInvocation<WriteTodosToolParams, ToolResult> {
|
||||
return new WriteTodosToolInvocation(params);
|
||||
return new WriteTodosToolInvocation(
|
||||
params,
|
||||
_messageBus,
|
||||
_toolName,
|
||||
_displayName,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user