mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-03-16 17:11:04 -07:00
feat(policy): support auto-add to policy by default and scoped persistence
This commit is contained in:
@@ -9,15 +9,12 @@ import { policiesCommand } from './policiesCommand.js';
|
||||
import { CommandKind } from './types.js';
|
||||
import { MessageType } from '../types.js';
|
||||
import { createMockCommandContext } from '../../test-utils/mockCommandContext.js';
|
||||
import * as fs from 'node:fs/promises';
|
||||
import {
|
||||
type Config,
|
||||
PolicyDecision,
|
||||
ApprovalMode,
|
||||
} from '@google/gemini-cli-core';
|
||||
|
||||
vi.mock('node:fs/promises');
|
||||
|
||||
describe('policiesCommand', () => {
|
||||
let mockContext: ReturnType<typeof createMockCommandContext>;
|
||||
|
||||
@@ -29,9 +26,8 @@ describe('policiesCommand', () => {
|
||||
expect(policiesCommand.name).toBe('policies');
|
||||
expect(policiesCommand.description).toBe('Manage policies');
|
||||
expect(policiesCommand.kind).toBe(CommandKind.BUILT_IN);
|
||||
expect(policiesCommand.subCommands).toHaveLength(2);
|
||||
expect(policiesCommand.subCommands).toHaveLength(1);
|
||||
expect(policiesCommand.subCommands![0].name).toBe('list');
|
||||
expect(policiesCommand.subCommands![1].name).toBe('undo');
|
||||
});
|
||||
|
||||
describe('list subcommand', () => {
|
||||
@@ -164,63 +160,4 @@ describe('policiesCommand', () => {
|
||||
expect(content).toContain('**ALLOW** tool: `shell` [Priority: 50]');
|
||||
});
|
||||
});
|
||||
|
||||
describe('undo subcommand', () => {
|
||||
it('should show error if config is missing', async () => {
|
||||
mockContext.services.config = null;
|
||||
const undoCommand = policiesCommand.subCommands![1];
|
||||
await undoCommand.action!(mockContext, '');
|
||||
expect(mockContext.ui.addItem).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
type: MessageType.ERROR,
|
||||
text: 'Error: Config not available.',
|
||||
}),
|
||||
expect.any(Number),
|
||||
);
|
||||
});
|
||||
|
||||
it('should show message if no backups found', async () => {
|
||||
const mockStorage = {
|
||||
getAutoSavedPolicyPath: vi.fn().mockReturnValue('user.toml'),
|
||||
getWorkspaceAutoSavedPolicyPath: vi.fn().mockReturnValue('ws.toml'),
|
||||
};
|
||||
mockContext.services.config = {
|
||||
storage: mockStorage,
|
||||
} as unknown as Config;
|
||||
|
||||
vi.mocked(fs.access).mockRejectedValue(new Error('no backup'));
|
||||
const undoCommand = policiesCommand.subCommands![1];
|
||||
await undoCommand.action!(mockContext, '');
|
||||
expect(mockContext.ui.addItem).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
type: MessageType.WARNING,
|
||||
text: 'No policy backups found to restore.',
|
||||
}),
|
||||
expect.any(Number),
|
||||
);
|
||||
});
|
||||
|
||||
it('should restore backups if found', async () => {
|
||||
const mockStorage = {
|
||||
getAutoSavedPolicyPath: vi.fn().mockReturnValue('user.toml'),
|
||||
getWorkspaceAutoSavedPolicyPath: vi.fn().mockReturnValue('ws.toml'),
|
||||
};
|
||||
mockContext.services.config = {
|
||||
storage: mockStorage,
|
||||
} as unknown as Config;
|
||||
|
||||
vi.mocked(fs.access).mockResolvedValue(undefined);
|
||||
vi.mocked(fs.copyFile).mockResolvedValue(undefined);
|
||||
const undoCommand = policiesCommand.subCommands![1];
|
||||
await undoCommand.action!(mockContext, '');
|
||||
expect(fs.copyFile).toHaveBeenCalled();
|
||||
expect(mockContext.ui.addItem).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
type: MessageType.INFO,
|
||||
text: expect.stringContaining('Successfully restored'),
|
||||
}),
|
||||
expect.any(Number),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import * as fs from 'node:fs/promises';
|
||||
import { ApprovalMode, type PolicyRule } from '@google/gemini-cli-core';
|
||||
import { CommandKind, type SlashCommand } from './types.js';
|
||||
import { MessageType } from '../types.js';
|
||||
@@ -112,66 +111,10 @@ const listPoliciesCommand: SlashCommand = {
|
||||
},
|
||||
};
|
||||
|
||||
const undoPoliciesCommand: SlashCommand = {
|
||||
name: 'undo',
|
||||
description: 'Undo the last auto-saved policy update',
|
||||
kind: CommandKind.BUILT_IN,
|
||||
autoExecute: true,
|
||||
action: async (context) => {
|
||||
const { config } = context.services;
|
||||
if (!config) {
|
||||
context.ui.addItem(
|
||||
{
|
||||
type: MessageType.ERROR,
|
||||
text: 'Error: Config not available.',
|
||||
},
|
||||
Date.now(),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const storage = config.storage;
|
||||
const paths = [
|
||||
storage.getAutoSavedPolicyPath(),
|
||||
storage.getWorkspaceAutoSavedPolicyPath(),
|
||||
];
|
||||
|
||||
let restoredCount = 0;
|
||||
for (const p of paths) {
|
||||
const bak = `${p}.bak`;
|
||||
try {
|
||||
await fs.access(bak);
|
||||
await fs.copyFile(bak, p);
|
||||
restoredCount++;
|
||||
} catch {
|
||||
// No backup or failed to restore
|
||||
}
|
||||
}
|
||||
|
||||
if (restoredCount > 0) {
|
||||
context.ui.addItem(
|
||||
{
|
||||
type: MessageType.INFO,
|
||||
text: `Successfully restored ${restoredCount} policy file(s) from backup. Please restart the CLI to apply changes.`,
|
||||
},
|
||||
Date.now(),
|
||||
);
|
||||
} else {
|
||||
context.ui.addItem(
|
||||
{
|
||||
type: MessageType.WARNING,
|
||||
text: 'No policy backups found to restore.',
|
||||
},
|
||||
Date.now(),
|
||||
);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
export const policiesCommand: SlashCommand = {
|
||||
name: 'policies',
|
||||
description: 'Manage policies',
|
||||
kind: CommandKind.BUILT_IN,
|
||||
autoExecute: false,
|
||||
subCommands: [listPoliciesCommand, undoPoliciesCommand],
|
||||
subCommands: [listPoliciesCommand],
|
||||
};
|
||||
|
||||
@@ -536,15 +536,6 @@ export function createPolicyUpdater(
|
||||
: storage.getAutoSavedPolicyPath();
|
||||
await fs.mkdir(path.dirname(policyFile), { recursive: true });
|
||||
|
||||
// Backup existing file if it exists
|
||||
try {
|
||||
await fs.copyFile(policyFile, `${policyFile}.bak`);
|
||||
} catch (error) {
|
||||
if (!isNodeError(error) || error.code !== 'ENOENT') {
|
||||
debugLogger.warn(`Failed to backup ${policyFile}`, error);
|
||||
}
|
||||
}
|
||||
|
||||
// Read existing file
|
||||
let existingData: { rule?: TomlRule[] } = {};
|
||||
try {
|
||||
|
||||
@@ -284,33 +284,4 @@ describe('createPolicyUpdater', () => {
|
||||
policyFile,
|
||||
);
|
||||
});
|
||||
|
||||
it('should backup existing policy file before writing', async () => {
|
||||
createPolicyUpdater(policyEngine, messageBus, mockStorage);
|
||||
|
||||
const policyFile = '/mock/user/.gemini/policies/auto-saved.toml';
|
||||
vi.spyOn(mockStorage, 'getAutoSavedPolicyPath').mockReturnValue(policyFile);
|
||||
(fs.mkdir as unknown as Mock).mockResolvedValue(undefined);
|
||||
(fs.readFile as unknown as Mock).mockResolvedValue(
|
||||
'[[rule]]\ntoolName = "existing"',
|
||||
);
|
||||
(fs.copyFile as unknown as Mock).mockResolvedValue(undefined);
|
||||
|
||||
const mockFileHandle = {
|
||||
writeFile: vi.fn().mockResolvedValue(undefined),
|
||||
close: vi.fn().mockResolvedValue(undefined),
|
||||
};
|
||||
(fs.open as unknown as Mock).mockResolvedValue(mockFileHandle);
|
||||
(fs.rename as unknown as Mock).mockResolvedValue(undefined);
|
||||
|
||||
await messageBus.publish({
|
||||
type: MessageBusType.UPDATE_POLICY,
|
||||
toolName: 'new_tool',
|
||||
persist: true,
|
||||
});
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
|
||||
expect(fs.copyFile).toHaveBeenCalledWith(policyFile, `${policyFile}.bak`);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user