feat(policy): Introduce config-based policy engine with TOML configuration (#11992)

This commit is contained in:
Allen Hutchison
2025-10-28 09:20:57 -07:00
committed by GitHub
parent 942b0520ad
commit 2a17b03d46
20 changed files with 3146 additions and 271 deletions
+5
View File
@@ -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[]) => {
+4
View File
@@ -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
View File
@@ -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);
}
+9 -1
View File
@@ -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',
+28 -12
View File
@@ -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',
+11 -4
View File
@@ -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,
);
}
}
+7 -1
View File
@@ -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,
);
}
}
+9
View File
@@ -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.
+15 -1
View File
@@ -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,
);
}
}