mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-03-18 10:01:29 -07:00
478 lines
15 KiB
TypeScript
478 lines
15 KiB
TypeScript
/**
|
|
* @license
|
|
* Copyright 2025 Google LLC
|
|
* SPDX-License-Identifier: Apache-2.0
|
|
*/
|
|
|
|
import { describe, it, expect } from 'vitest';
|
|
import { createPolicyEngineConfig } from './policy.js';
|
|
import type { Settings } from './settings.js';
|
|
import { ApprovalMode, PolicyDecision } from '@google/gemini-cli-core';
|
|
|
|
describe('createPolicyEngineConfig', () => {
|
|
it('should return ASK_USER for all tools by default', () => {
|
|
const settings: Settings = {};
|
|
const config = createPolicyEngineConfig(settings, ApprovalMode.DEFAULT);
|
|
expect(config.defaultDecision).toBe(PolicyDecision.ASK_USER);
|
|
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', decision: 'ask_user', priority: 10 },
|
|
]);
|
|
});
|
|
|
|
it('should allow tools in tools.allowed', () => {
|
|
const settings: Settings = {
|
|
tools: { allowed: ['run_shell_command'] },
|
|
};
|
|
const config = createPolicyEngineConfig(settings, ApprovalMode.DEFAULT);
|
|
const rule = config.rules?.find(
|
|
(r) =>
|
|
r.toolName === 'run_shell_command' &&
|
|
r.decision === PolicyDecision.ALLOW,
|
|
);
|
|
expect(rule).toBeDefined();
|
|
expect(rule?.priority).toBe(100);
|
|
});
|
|
|
|
it('should deny tools in tools.exclude', () => {
|
|
const settings: Settings = {
|
|
tools: { exclude: ['run_shell_command'] },
|
|
};
|
|
const config = createPolicyEngineConfig(settings, ApprovalMode.DEFAULT);
|
|
const rule = config.rules?.find(
|
|
(r) =>
|
|
r.toolName === 'run_shell_command' &&
|
|
r.decision === PolicyDecision.DENY,
|
|
);
|
|
expect(rule).toBeDefined();
|
|
expect(rule?.priority).toBe(200);
|
|
});
|
|
|
|
it('should allow tools from allowed MCP servers', () => {
|
|
const settings: Settings = {
|
|
mcp: { allowed: ['my-server'] },
|
|
};
|
|
const config = createPolicyEngineConfig(settings, ApprovalMode.DEFAULT);
|
|
const rule = config.rules?.find(
|
|
(r) =>
|
|
r.toolName === 'my-server__*' && r.decision === PolicyDecision.ALLOW,
|
|
);
|
|
expect(rule).toBeDefined();
|
|
expect(rule?.priority).toBe(85);
|
|
});
|
|
|
|
it('should deny tools from excluded MCP servers', () => {
|
|
const settings: Settings = {
|
|
mcp: { excluded: ['my-server'] },
|
|
};
|
|
const config = createPolicyEngineConfig(settings, ApprovalMode.DEFAULT);
|
|
const rule = config.rules?.find(
|
|
(r) =>
|
|
r.toolName === 'my-server__*' && r.decision === PolicyDecision.DENY,
|
|
);
|
|
expect(rule).toBeDefined();
|
|
expect(rule?.priority).toBe(195);
|
|
});
|
|
|
|
it('should allow tools from trusted MCP servers', () => {
|
|
const settings: Settings = {
|
|
mcpServers: {
|
|
'trusted-server': {
|
|
command: 'node',
|
|
args: ['server.js'],
|
|
trust: true,
|
|
},
|
|
'untrusted-server': {
|
|
command: 'node',
|
|
args: ['server.js'],
|
|
trust: false,
|
|
},
|
|
},
|
|
};
|
|
const config = createPolicyEngineConfig(settings, ApprovalMode.DEFAULT);
|
|
|
|
const trustedRule = config.rules?.find(
|
|
(r) =>
|
|
r.toolName === 'trusted-server__*' &&
|
|
r.decision === PolicyDecision.ALLOW,
|
|
);
|
|
expect(trustedRule).toBeDefined();
|
|
expect(trustedRule?.priority).toBe(90);
|
|
|
|
// Untrusted server should not have an allow rule
|
|
const untrustedRule = config.rules?.find(
|
|
(r) =>
|
|
r.toolName === 'untrusted-server__*' &&
|
|
r.decision === PolicyDecision.ALLOW,
|
|
);
|
|
expect(untrustedRule).toBeUndefined();
|
|
});
|
|
|
|
it('should handle multiple MCP server configurations together', () => {
|
|
const settings: Settings = {
|
|
mcp: {
|
|
allowed: ['allowed-server'],
|
|
excluded: ['excluded-server'],
|
|
},
|
|
mcpServers: {
|
|
'trusted-server': {
|
|
command: 'node',
|
|
args: ['server.js'],
|
|
trust: true,
|
|
},
|
|
},
|
|
};
|
|
const config = createPolicyEngineConfig(settings, ApprovalMode.DEFAULT);
|
|
|
|
// Check allowed server
|
|
const allowedRule = config.rules?.find(
|
|
(r) =>
|
|
r.toolName === 'allowed-server__*' &&
|
|
r.decision === PolicyDecision.ALLOW,
|
|
);
|
|
expect(allowedRule).toBeDefined();
|
|
expect(allowedRule?.priority).toBe(85);
|
|
|
|
// Check trusted server
|
|
const trustedRule = config.rules?.find(
|
|
(r) =>
|
|
r.toolName === 'trusted-server__*' &&
|
|
r.decision === PolicyDecision.ALLOW,
|
|
);
|
|
expect(trustedRule).toBeDefined();
|
|
expect(trustedRule?.priority).toBe(90);
|
|
|
|
// Check excluded server
|
|
const excludedRule = config.rules?.find(
|
|
(r) =>
|
|
r.toolName === 'excluded-server__*' &&
|
|
r.decision === PolicyDecision.DENY,
|
|
);
|
|
expect(excludedRule).toBeDefined();
|
|
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);
|
|
const rule = config.rules?.find(
|
|
(r) => r.decision === PolicyDecision.ALLOW && r.priority === 0,
|
|
);
|
|
expect(rule).toBeDefined();
|
|
});
|
|
|
|
it('should allow edit tool in AUTO_EDIT mode', () => {
|
|
const settings: Settings = {};
|
|
const config = createPolicyEngineConfig(settings, ApprovalMode.AUTO_EDIT);
|
|
const rule = config.rules?.find(
|
|
(r) => r.toolName === 'replace' && r.decision === PolicyDecision.ALLOW,
|
|
);
|
|
expect(rule).toBeDefined();
|
|
expect(rule?.priority).toBe(15);
|
|
});
|
|
|
|
it('should prioritize exclude over allow', () => {
|
|
const settings: Settings = {
|
|
tools: { allowed: ['run_shell_command'], exclude: ['run_shell_command'] },
|
|
};
|
|
const config = createPolicyEngineConfig(settings, ApprovalMode.DEFAULT);
|
|
const denyRule = config.rules?.find(
|
|
(r) =>
|
|
r.toolName === 'run_shell_command' &&
|
|
r.decision === PolicyDecision.DENY,
|
|
);
|
|
const allowRule = config.rules?.find(
|
|
(r) =>
|
|
r.toolName === 'run_shell_command' &&
|
|
r.decision === PolicyDecision.ALLOW,
|
|
);
|
|
expect(denyRule).toBeDefined();
|
|
expect(allowRule).toBeDefined();
|
|
expect(denyRule!.priority).toBeGreaterThan(allowRule!.priority!);
|
|
});
|
|
|
|
it('should prioritize specific tool allows over MCP server excludes', () => {
|
|
const settings: Settings = {
|
|
mcp: { excluded: ['my-server'] },
|
|
tools: { allowed: ['my-server__specific-tool'] },
|
|
};
|
|
const config = createPolicyEngineConfig(settings, ApprovalMode.DEFAULT);
|
|
|
|
const serverDenyRule = config.rules?.find(
|
|
(r) =>
|
|
r.toolName === 'my-server__*' && r.decision === PolicyDecision.DENY,
|
|
);
|
|
const toolAllowRule = config.rules?.find(
|
|
(r) =>
|
|
r.toolName === 'my-server__specific-tool' &&
|
|
r.decision === PolicyDecision.ALLOW,
|
|
);
|
|
|
|
expect(serverDenyRule).toBeDefined();
|
|
expect(serverDenyRule?.priority).toBe(195);
|
|
expect(toolAllowRule).toBeDefined();
|
|
expect(toolAllowRule?.priority).toBe(100);
|
|
|
|
// Tool allow (100) has lower priority than server deny (195),
|
|
// so server deny wins - this might be counterintuitive
|
|
});
|
|
|
|
it('should prioritize specific tool excludes over MCP server allows', () => {
|
|
const settings: Settings = {
|
|
mcp: { allowed: ['my-server'] },
|
|
mcpServers: {
|
|
'my-server': {
|
|
command: 'node',
|
|
args: ['server.js'],
|
|
trust: true,
|
|
},
|
|
},
|
|
tools: { exclude: ['my-server__dangerous-tool'] },
|
|
};
|
|
const config = createPolicyEngineConfig(settings, ApprovalMode.DEFAULT);
|
|
|
|
const serverAllowRule = config.rules?.find(
|
|
(r) =>
|
|
r.toolName === 'my-server__*' && r.decision === PolicyDecision.ALLOW,
|
|
);
|
|
const toolDenyRule = config.rules?.find(
|
|
(r) =>
|
|
r.toolName === 'my-server__dangerous-tool' &&
|
|
r.decision === PolicyDecision.DENY,
|
|
);
|
|
|
|
expect(serverAllowRule).toBeDefined();
|
|
expect(toolDenyRule).toBeDefined();
|
|
expect(toolDenyRule!.priority).toBeGreaterThan(serverAllowRule!.priority!);
|
|
});
|
|
|
|
it('should handle complex priority scenarios correctly', () => {
|
|
const settings: Settings = {
|
|
tools: {
|
|
autoAccept: true, // Priority 50 for read-only tools
|
|
allowed: ['my-server__tool1', 'other-tool'], // Priority 100
|
|
exclude: ['my-server__tool2', 'glob'], // Priority 200
|
|
},
|
|
mcp: {
|
|
allowed: ['allowed-server'], // Priority 85
|
|
excluded: ['excluded-server'], // Priority 195
|
|
},
|
|
mcpServers: {
|
|
'trusted-server': {
|
|
command: 'node',
|
|
args: ['server.js'],
|
|
trust: true, // Priority 90
|
|
},
|
|
},
|
|
};
|
|
const config = createPolicyEngineConfig(settings, ApprovalMode.DEFAULT);
|
|
|
|
// Verify glob is denied even though autoAccept would allow it
|
|
const globDenyRule = config.rules?.find(
|
|
(r) => r.toolName === 'glob' && r.decision === PolicyDecision.DENY,
|
|
);
|
|
const globAllowRule = config.rules?.find(
|
|
(r) => r.toolName === 'glob' && r.decision === PolicyDecision.ALLOW,
|
|
);
|
|
expect(globDenyRule).toBeDefined();
|
|
expect(globAllowRule).toBeDefined();
|
|
expect(globDenyRule!.priority).toBe(200);
|
|
expect(globAllowRule!.priority).toBe(50);
|
|
|
|
// Verify all priority levels are correct
|
|
const priorities = config.rules
|
|
?.map((r) => ({
|
|
tool: r.toolName,
|
|
decision: r.decision,
|
|
priority: r.priority,
|
|
}))
|
|
.sort((a, b) => (b.priority ?? 0) - (a.priority ?? 0));
|
|
|
|
// Check that the highest priority items are the excludes
|
|
const highestPriorityExcludes = priorities?.filter(
|
|
(p) => p.priority === 200,
|
|
);
|
|
expect(
|
|
highestPriorityExcludes?.every((p) => p.decision === PolicyDecision.DENY),
|
|
).toBe(true);
|
|
});
|
|
|
|
it('should handle MCP servers with undefined trust property', () => {
|
|
const settings: Settings = {
|
|
mcpServers: {
|
|
'no-trust-property': {
|
|
command: 'node',
|
|
args: ['server.js'],
|
|
// trust property is undefined/missing
|
|
},
|
|
'explicit-false': {
|
|
command: 'node',
|
|
args: ['server.js'],
|
|
trust: false,
|
|
},
|
|
},
|
|
};
|
|
const config = createPolicyEngineConfig(settings, ApprovalMode.DEFAULT);
|
|
|
|
// Neither server should have an allow rule
|
|
const noTrustRule = config.rules?.find(
|
|
(r) =>
|
|
r.toolName === 'no-trust-property__*' &&
|
|
r.decision === PolicyDecision.ALLOW,
|
|
);
|
|
const explicitFalseRule = config.rules?.find(
|
|
(r) =>
|
|
r.toolName === 'explicit-false__*' &&
|
|
r.decision === PolicyDecision.ALLOW,
|
|
);
|
|
|
|
expect(noTrustRule).toBeUndefined();
|
|
expect(explicitFalseRule).toBeUndefined();
|
|
});
|
|
|
|
it('should not add write tool rules in YOLO mode', () => {
|
|
const settings: Settings = {
|
|
tools: { exclude: ['dangerous-tool'] },
|
|
};
|
|
const config = createPolicyEngineConfig(settings, ApprovalMode.YOLO);
|
|
|
|
// Should have the wildcard allow rule with priority 0
|
|
const wildcardRule = config.rules?.find(
|
|
(r) =>
|
|
!r.toolName && r.decision === PolicyDecision.ALLOW && r.priority === 0,
|
|
);
|
|
expect(wildcardRule).toBeDefined();
|
|
|
|
// Should NOT have any write tool rules (which would have priority 10)
|
|
const writeToolRules = config.rules?.filter(
|
|
(r) =>
|
|
[
|
|
'replace',
|
|
'save_memory',
|
|
'run_shell_command',
|
|
'write_file',
|
|
'web_fetch',
|
|
].includes(r.toolName || '') && r.decision === PolicyDecision.ASK_USER,
|
|
);
|
|
expect(writeToolRules).toHaveLength(0);
|
|
|
|
// Should still have the exclude rule
|
|
const excludeRule = config.rules?.find(
|
|
(r) =>
|
|
r.toolName === 'dangerous-tool' && r.decision === PolicyDecision.DENY,
|
|
);
|
|
expect(excludeRule).toBeDefined();
|
|
expect(excludeRule?.priority).toBe(200);
|
|
});
|
|
|
|
it('should handle combination of trusted server and excluded server for same name', () => {
|
|
const settings: Settings = {
|
|
mcpServers: {
|
|
'conflicted-server': {
|
|
command: 'node',
|
|
args: ['server.js'],
|
|
trust: true, // Priority 90
|
|
},
|
|
},
|
|
mcp: {
|
|
excluded: ['conflicted-server'], // Priority 195
|
|
},
|
|
};
|
|
const config = createPolicyEngineConfig(settings, ApprovalMode.DEFAULT);
|
|
|
|
// Both rules should exist
|
|
const trustRule = config.rules?.find(
|
|
(r) =>
|
|
r.toolName === 'conflicted-server__*' &&
|
|
r.decision === PolicyDecision.ALLOW,
|
|
);
|
|
const excludeRule = config.rules?.find(
|
|
(r) =>
|
|
r.toolName === 'conflicted-server__*' &&
|
|
r.decision === PolicyDecision.DENY,
|
|
);
|
|
|
|
expect(trustRule).toBeDefined();
|
|
expect(trustRule?.priority).toBe(90);
|
|
expect(excludeRule).toBeDefined();
|
|
expect(excludeRule?.priority).toBe(195);
|
|
|
|
// 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 = {};
|
|
|
|
// Test DEFAULT mode
|
|
const defaultConfig = createPolicyEngineConfig(
|
|
settings,
|
|
ApprovalMode.DEFAULT,
|
|
);
|
|
expect(defaultConfig.defaultDecision).toBe(PolicyDecision.ASK_USER);
|
|
expect(
|
|
defaultConfig.rules?.find(
|
|
(r) => !r.toolName && r.decision === PolicyDecision.ALLOW,
|
|
),
|
|
).toBeUndefined();
|
|
|
|
// Test YOLO mode
|
|
const yoloConfig = createPolicyEngineConfig(settings, ApprovalMode.YOLO);
|
|
expect(yoloConfig.defaultDecision).toBe(PolicyDecision.ASK_USER);
|
|
const yoloWildcard = yoloConfig.rules?.find(
|
|
(r) => !r.toolName && r.decision === PolicyDecision.ALLOW,
|
|
);
|
|
expect(yoloWildcard).toBeDefined();
|
|
expect(yoloWildcard?.priority).toBe(0);
|
|
|
|
// Test AUTO_EDIT mode
|
|
const autoEditConfig = createPolicyEngineConfig(
|
|
settings,
|
|
ApprovalMode.AUTO_EDIT,
|
|
);
|
|
expect(autoEditConfig.defaultDecision).toBe(PolicyDecision.ASK_USER);
|
|
const editRule = autoEditConfig.rules?.find(
|
|
(r) => r.toolName === 'replace' && r.decision === PolicyDecision.ALLOW,
|
|
);
|
|
expect(editRule).toBeDefined();
|
|
expect(editRule?.priority).toBe(15);
|
|
});
|
|
});
|