feat: Implement message bus and policy engine (#11523)

Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
This commit is contained in:
Allen Hutchison
2025-10-21 11:45:33 -07:00
committed by GitHub
parent 0658b4aa31
commit bf80263bd6
19 changed files with 339 additions and 94 deletions

View File

@@ -14,16 +14,70 @@ import {
} from '@google/gemini-cli-core';
describe('createPolicyEngineConfig', () => {
it('should return ASK_USER for all tools by default', () => {
it('should return ASK_USER for write tools and ALLOW for read-only tools by default', () => {
const settings: Settings = {};
const config = createPolicyEngineConfig(settings, ApprovalMode.DEFAULT);
expect(config.defaultDecision).toBe(PolicyDecision.ASK_USER);
// The order of the rules is not guaranteed, so we sort them by tool name.
config.rules?.sort((a, b) =>
(a.toolName ?? '').localeCompare(b.toolName ?? ''),
);
expect(config.rules).toEqual([
{ toolName: 'replace', decision: 'ask_user', priority: 10 },
{ toolName: 'save_memory', decision: 'ask_user', priority: 10 },
{ toolName: 'run_shell_command', decision: 'ask_user', priority: 10 },
{ toolName: 'write_file', decision: 'ask_user', priority: 10 },
{ toolName: WEB_FETCH_TOOL_NAME, decision: 'ask_user', priority: 10 },
{
toolName: 'glob',
decision: PolicyDecision.ALLOW,
priority: 50,
},
{
toolName: 'google_web_search',
decision: PolicyDecision.ALLOW,
priority: 50,
},
{
toolName: 'list_directory',
decision: PolicyDecision.ALLOW,
priority: 50,
},
{
toolName: 'read_file',
decision: PolicyDecision.ALLOW,
priority: 50,
},
{
toolName: 'read_many_files',
decision: PolicyDecision.ALLOW,
priority: 50,
},
{
toolName: 'replace',
decision: PolicyDecision.ASK_USER,
priority: 10,
},
{
toolName: 'run_shell_command',
decision: PolicyDecision.ASK_USER,
priority: 10,
},
{
toolName: 'save_memory',
decision: PolicyDecision.ASK_USER,
priority: 10,
},
{
toolName: 'search_file_content',
decision: PolicyDecision.ALLOW,
priority: 50,
},
{
toolName: 'web_fetch',
decision: PolicyDecision.ASK_USER,
priority: 10,
},
{
toolName: 'write_file',
decision: PolicyDecision.ASK_USER,
priority: 10,
},
]);
});
@@ -159,18 +213,6 @@ describe('createPolicyEngineConfig', () => {
expect(excludedRule?.priority).toBe(195);
});
it('should allow read-only tools if autoAccept is true', () => {
const settings: Settings = {
tools: { autoAccept: true },
};
const config = createPolicyEngineConfig(settings, ApprovalMode.DEFAULT);
const rule = config.rules?.find(
(r) => r.toolName === 'glob' && r.decision === PolicyDecision.ALLOW,
);
expect(rule).toBeDefined();
expect(rule?.priority).toBe(50);
});
it('should allow all tools in YOLO mode', () => {
const settings: Settings = {};
const config = createPolicyEngineConfig(settings, ApprovalMode.YOLO);
@@ -419,29 +461,6 @@ describe('createPolicyEngineConfig', () => {
// Exclude (195) should win over trust (90) when evaluated
});
it('should create all read-only tool rules when autoAccept is enabled', () => {
const settings: Settings = {
tools: { autoAccept: true },
};
const config = createPolicyEngineConfig(settings, ApprovalMode.DEFAULT);
// All read-only tools should have allow rules
const readOnlyTools = [
'glob',
'search_file_content',
'list_directory',
'read_file',
'read_many_files',
];
for (const tool of readOnlyTools) {
const rule = config.rules?.find(
(r) => r.toolName === tool && r.decision === PolicyDecision.ALLOW,
);
expect(rule).toBeDefined();
expect(rule?.priority).toBe(50);
}
});
it('should handle all approval modes correctly', () => {
const settings: Settings = {};

View File

@@ -22,8 +22,12 @@ import {
EDIT_TOOL_NAME,
MEMORY_TOOL_NAME,
WEB_SEARCH_TOOL_NAME,
type PolicyEngine,
type MessageBus,
MessageBusType,
type UpdatePolicy,
} from '@google/gemini-cli-core';
import type { Settings } from './settings.js';
import { type Settings } from './settings.js';
// READ_ONLY_TOOLS is a list of built-in tools that do not modify the user's
// files or system state.
@@ -69,6 +73,7 @@ export function createPolicyEngineConfig(
// 90: MCP servers with trust=true
// 100: Explicitly allowed individual tools
// 195: Explicitly excluded MCP servers
// 199: Tools that the user has selected as "Always Allow" in the interactive UI.
// 200: Explicitly excluded individual tools (highest priority)
// MCP servers that are explicitly allowed in settings.mcp.allowed
@@ -137,16 +142,14 @@ export function createPolicyEngineConfig(
}
}
// If auto-accept is enabled, allow all read-only tools.
// Allow all read-only tools.
// Priority: 50
if (settings.tools?.autoAccept) {
for (const tool of READ_ONLY_TOOLS) {
rules.push({
toolName: tool,
decision: PolicyDecision.ALLOW,
priority: 50,
});
}
for (const tool of READ_ONLY_TOOLS) {
rules.push({
toolName: tool,
decision: PolicyDecision.ALLOW,
priority: 50,
});
}
// Only add write tool rules if not in YOLO mode
@@ -179,3 +182,21 @@ export function createPolicyEngineConfig(
defaultDecision: PolicyDecision.ASK_USER,
};
}
export function createPolicyUpdater(
policyEngine: PolicyEngine,
messageBus: MessageBus,
) {
messageBus.subscribe(
MessageBusType.UPDATE_POLICY,
(message: UpdatePolicy) => {
const toolName = message.toolName;
policyEngine.addRule({
toolName,
decision: PolicyDecision.ALLOW,
priority: 199, // High priority, but lower than explicit DENY (200)
});
},
);
}

View File

@@ -170,6 +170,10 @@ describe('gemini.tsx main function', () => {
getScreenReader: () => false,
getGeminiMdFileCount: () => 0,
getProjectRoot: () => '/',
getPolicyEngine: vi.fn(),
getMessageBus: () => ({
subscribe: vi.fn(),
}),
} as unknown as Config;
});
vi.mocked(loadSettings).mockReturnValue({
@@ -301,6 +305,10 @@ describe('gemini.tsx main function kitty protocol', () => {
getExperimentalZedIntegration: () => false,
getScreenReader: () => false,
getGeminiMdFileCount: () => 0,
getPolicyEngine: vi.fn(),
getMessageBus: () => ({
subscribe: vi.fn(),
}),
} as unknown as Config);
vi.mocked(loadSettings).mockReturnValue({
errors: [],

View File

@@ -67,6 +67,7 @@ import {
relaunchOnExitCode,
} from './utils/relaunch.js';
import { loadSandboxConfig } from './config/sandboxConfig.js';
import { createPolicyUpdater } from './config/policy.js';
import { ExtensionEnablementManager } from './config/extensions/extensionEnablement.js';
export function validateDnsResolutionOrder(
@@ -370,6 +371,10 @@ export async function main() {
argv,
);
const policyEngine = config.getPolicyEngine();
const messageBus = config.getMessageBus();
createPolicyUpdater(policyEngine, messageBus);
// Cleanup sessions after config initialization
await cleanupExpiredSessions(config, settings.merged);