Refactor PolicyEngine to Core Package (#12325)

Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
This commit is contained in:
Allen Hutchison
2025-11-03 15:41:00 -08:00
committed by GitHub
parent 60d2c2cc90
commit ffc5e4d048
35 changed files with 1357 additions and 2156 deletions

View File

@@ -52,7 +52,6 @@ import {
} from '@google/gemini-cli-core';
import { validateAuthMethod } from '../config/auth.js';
import { loadHierarchicalGeminiMemory } from '../config/config.js';
import { getPolicyErrorsForUI } from '../config/policy.js';
import process from 'node:process';
import { useHistory } from './hooks/useHistoryManager.js';
import { useMemoryMonitor } from './hooks/useMemoryMonitor.js';
@@ -937,18 +936,6 @@ Logging in with Google... Please restart Gemini CLI to continue.
};
appEvents.on(AppEvent.LogError, logErrorHandler);
// Emit any policy errors that were stored during config loading
// Only show these when message bus integration is enabled, as policies
// are only active when the message bus is being used.
if (config.getEnableMessageBusIntegration()) {
const policyErrors = getPolicyErrorsForUI();
if (policyErrors.length > 0) {
for (const error of policyErrors) {
appEvents.emit(AppEvent.LogError, error);
}
}
}
return () => {
appEvents.off(AppEvent.OpenDebugConsole, openDebugConsole);
appEvents.off(AppEvent.LogError, logErrorHandler);

View File

@@ -0,0 +1,108 @@
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { policiesCommand } from './policiesCommand.js';
import { CommandKind } from './types.js';
import { MessageType } from '../types.js';
import { createMockCommandContext } from '../../test-utils/mockCommandContext.js';
import { type Config, PolicyDecision } from '@google/gemini-cli-core';
describe('policiesCommand', () => {
let mockContext: ReturnType<typeof createMockCommandContext>;
beforeEach(() => {
mockContext = createMockCommandContext();
});
it('should have correct command definition', () => {
expect(policiesCommand.name).toBe('policies');
expect(policiesCommand.description).toBe('Manage policies');
expect(policiesCommand.kind).toBe(CommandKind.BUILT_IN);
expect(policiesCommand.subCommands).toHaveLength(1);
expect(policiesCommand.subCommands![0].name).toBe('list');
});
describe('list subcommand', () => {
it('should show error if config is missing', async () => {
mockContext.services.config = null;
const listCommand = policiesCommand.subCommands![0];
await listCommand.action!(mockContext, '');
expect(mockContext.ui.addItem).toHaveBeenCalledWith(
expect.objectContaining({
type: MessageType.ERROR,
text: 'Error: Config not available.',
}),
expect.any(Number),
);
});
it('should show message when no policies are active', async () => {
const mockPolicyEngine = {
getRules: vi.fn().mockReturnValue([]),
};
mockContext.services.config = {
getPolicyEngine: vi.fn().mockReturnValue(mockPolicyEngine),
} as unknown as Config;
const listCommand = policiesCommand.subCommands![0];
await listCommand.action!(mockContext, '');
expect(mockContext.ui.addItem).toHaveBeenCalledWith(
expect.objectContaining({
type: MessageType.INFO,
text: 'No active policies.',
}),
expect.any(Number),
);
});
it('should list active policies in correct format', async () => {
const mockRules = [
{
decision: PolicyDecision.DENY,
toolName: 'dangerousTool',
priority: 10,
},
{
decision: PolicyDecision.ALLOW,
argsPattern: /safe/,
},
{
decision: PolicyDecision.ASK_USER,
},
];
const mockPolicyEngine = {
getRules: vi.fn().mockReturnValue(mockRules),
};
mockContext.services.config = {
getPolicyEngine: vi.fn().mockReturnValue(mockPolicyEngine),
} as unknown as Config;
const listCommand = policiesCommand.subCommands![0];
await listCommand.action!(mockContext, '');
expect(mockContext.ui.addItem).toHaveBeenCalledWith(
expect.objectContaining({
type: MessageType.INFO,
text: expect.stringContaining('**Active Policies**'),
}),
expect.any(Number),
);
const call = vi.mocked(mockContext.ui.addItem).mock.calls[0];
const content = (call[0] as { text: string }).text;
expect(content).toContain(
'1. **DENY** tool: `dangerousTool` [Priority: 10]',
);
expect(content).toContain('2. **ALLOW** all tools (args match: `safe`)');
expect(content).toContain('3. **ASK_USER** all tools');
});
});
});

View File

@@ -0,0 +1,73 @@
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { CommandKind, type SlashCommand } from './types.js';
import { MessageType } from '../types.js';
const listPoliciesCommand: SlashCommand = {
name: 'list',
description: 'List all active policies',
kind: CommandKind.BUILT_IN,
action: async (context) => {
const { config } = context.services;
if (!config) {
context.ui.addItem(
{
type: MessageType.ERROR,
text: 'Error: Config not available.',
},
Date.now(),
);
return;
}
const policyEngine = config.getPolicyEngine();
const rules = policyEngine.getRules();
if (rules.length === 0) {
context.ui.addItem(
{
type: MessageType.INFO,
text: 'No active policies.',
},
Date.now(),
);
return;
}
let content = '**Active Policies**\n\n';
rules.forEach((rule, index) => {
content += `${index + 1}. **${rule.decision.toUpperCase()}**`;
if (rule.toolName) {
content += ` tool: \`${rule.toolName}\``;
} else {
content += ` all tools`;
}
if (rule.argsPattern) {
content += ` (args match: \`${rule.argsPattern.source}\`)`;
}
if (rule.priority !== undefined) {
content += ` [Priority: ${rule.priority}]`;
}
content += '\n';
});
context.ui.addItem(
{
type: MessageType.INFO,
text: content,
},
Date.now(),
);
},
};
export const policiesCommand: SlashCommand = {
name: 'policies',
description: 'Manage policies',
kind: CommandKind.BUILT_IN,
subCommands: [listPoliciesCommand],
};