@google/gemini-cli 0.13.0-nightly.20251102.d7243fb8 → 0.13.0-preview.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/google-gemini-cli-0.13.0-nightly.20251031.c89bc30d.tgz +0 -0
- package/dist/package.json +3 -3
- package/dist/src/commands/mcp/list.test.js +25 -21
- package/dist/src/commands/mcp/list.test.js.map +1 -1
- package/dist/src/config/config.js +11 -84
- package/dist/src/config/config.js.map +1 -1
- package/dist/src/config/config.test.js +13 -30
- package/dist/src/config/config.test.js.map +1 -1
- package/dist/src/config/extension-manager.d.ts +18 -6
- package/dist/src/config/extension-manager.js +25 -19
- package/dist/src/config/extension-manager.js.map +1 -1
- package/dist/src/config/extension.test.js +9 -9
- package/dist/src/config/extension.test.js.map +1 -1
- package/dist/src/config/extensions/extensionSettings.test.js +39 -43
- package/dist/src/config/extensions/extensionSettings.test.js.map +1 -1
- package/dist/src/config/extensions/github.test.js +133 -165
- package/dist/src/config/extensions/github.test.js.map +1 -1
- package/dist/src/config/keyBindings.d.ts +3 -0
- package/dist/src/config/keyBindings.js +29 -7
- package/dist/src/config/keyBindings.js.map +1 -1
- package/dist/src/config/keyBindings.test.js +17 -0
- package/dist/src/config/keyBindings.test.js.map +1 -1
- package/dist/src/config/policy.d.ts +0 -7
- package/dist/src/config/policy.js +10 -177
- package/dist/src/config/policy.js.map +1 -1
- package/dist/src/config/settings.js +1 -0
- package/dist/src/config/settings.js.map +1 -1
- package/dist/src/config/settingsSchema.d.ts +123 -22
- package/dist/src/config/settingsSchema.js +371 -21
- package/dist/src/config/settingsSchema.js.map +1 -1
- package/dist/src/config/settingsSchema.test.js +40 -1
- package/dist/src/config/settingsSchema.test.js.map +1 -1
- package/dist/src/gemini.js +15 -5
- package/dist/src/gemini.js.map +1 -1
- package/dist/src/gemini.test.js +2 -0
- package/dist/src/gemini.test.js.map +1 -1
- package/dist/src/generated/git-commit.d.ts +2 -2
- package/dist/src/generated/git-commit.js +2 -2
- package/dist/src/generated/git-commit.js.map +1 -1
- package/dist/src/nonInteractiveCli.js +68 -1
- package/dist/src/nonInteractiveCli.js.map +1 -1
- package/dist/src/services/BuiltinCommandLoader.js +4 -0
- package/dist/src/services/BuiltinCommandLoader.js.map +1 -1
- package/dist/src/services/BuiltinCommandLoader.test.js +22 -0
- package/dist/src/services/BuiltinCommandLoader.test.js.map +1 -1
- package/dist/src/services/McpPromptLoader.js +2 -2
- package/dist/src/services/McpPromptLoader.js.map +1 -1
- package/dist/src/services/McpPromptLoader.test.js +4 -2
- package/dist/src/services/McpPromptLoader.test.js.map +1 -1
- package/dist/src/test-utils/render.d.ts +2 -1
- package/dist/src/test-utils/render.js +3 -2
- package/dist/src/test-utils/render.js.map +1 -1
- package/dist/src/ui/AppContainer.js +32 -15
- package/dist/src/ui/AppContainer.js.map +1 -1
- package/dist/src/ui/AppContainer.test.js +160 -0
- package/dist/src/ui/AppContainer.test.js.map +1 -1
- package/dist/src/ui/commands/mcpCommand.js +14 -14
- package/dist/src/ui/commands/mcpCommand.js.map +1 -1
- package/dist/src/ui/commands/mcpCommand.test.js +4 -0
- package/dist/src/ui/commands/mcpCommand.test.js.map +1 -1
- package/dist/src/ui/commands/policiesCommand.d.ts +7 -0
- package/dist/src/ui/commands/policiesCommand.js +59 -0
- package/dist/src/ui/commands/policiesCommand.js.map +1 -0
- package/dist/src/ui/commands/policiesCommand.test.js +83 -0
- package/dist/src/ui/commands/policiesCommand.test.js.map +1 -0
- package/dist/src/ui/components/Composer.js +1 -1
- package/dist/src/ui/components/Composer.js.map +1 -1
- package/dist/src/ui/components/Composer.test.js +4 -1
- package/dist/src/ui/components/Composer.test.js.map +1 -1
- package/dist/src/ui/components/ConfigInitDisplay.js +4 -6
- package/dist/src/ui/components/ConfigInitDisplay.js.map +1 -1
- package/dist/src/ui/components/InputPrompt.js +22 -2
- package/dist/src/ui/components/InputPrompt.js.map +1 -1
- package/dist/src/ui/components/InputPrompt.test.js +70 -5
- package/dist/src/ui/components/InputPrompt.test.js.map +1 -1
- package/dist/src/ui/components/MainContent.js +15 -4
- package/dist/src/ui/components/MainContent.js.map +1 -1
- package/dist/src/ui/components/Notifications.js +38 -5
- package/dist/src/ui/components/Notifications.js.map +1 -1
- package/dist/src/ui/components/SettingsDialog.js +32 -25
- package/dist/src/ui/components/SettingsDialog.js.map +1 -1
- package/dist/src/ui/components/ShellConfirmationDialog.js +1 -1
- package/dist/src/ui/components/ShellConfirmationDialog.js.map +1 -1
- package/dist/src/ui/components/messages/InfoMessage.js +1 -1
- package/dist/src/ui/components/messages/InfoMessage.js.map +1 -1
- package/dist/src/ui/components/messages/ToolConfirmationMessage.js +1 -1
- package/dist/src/ui/components/messages/ToolConfirmationMessage.js.map +1 -1
- package/dist/src/ui/components/messages/WarningMessage.js +2 -2
- package/dist/src/ui/components/messages/WarningMessage.js.map +1 -1
- package/dist/src/ui/components/shared/text-buffer.d.ts +1 -0
- package/dist/src/ui/components/shared/text-buffer.js +23 -0
- package/dist/src/ui/components/shared/text-buffer.js.map +1 -1
- package/dist/src/ui/components/shared/text-buffer.test.js +246 -201
- package/dist/src/ui/components/shared/text-buffer.test.js.map +1 -1
- package/dist/src/ui/contexts/KeypressContext.js +182 -132
- package/dist/src/ui/contexts/KeypressContext.js.map +1 -1
- package/dist/src/ui/contexts/KeypressContext.test.js +144 -8
- package/dist/src/ui/contexts/KeypressContext.test.js.map +1 -1
- package/dist/src/ui/contexts/MouseContext.d.ts +21 -0
- package/dist/src/ui/contexts/MouseContext.js +89 -0
- package/dist/src/ui/contexts/MouseContext.js.map +1 -0
- package/dist/src/ui/contexts/MouseContext.test.js +164 -0
- package/dist/src/ui/contexts/MouseContext.test.js.map +1 -0
- package/dist/src/ui/hooks/slashCommandProcessor.test.js +70 -73
- package/dist/src/ui/hooks/slashCommandProcessor.test.js.map +1 -1
- package/dist/src/ui/hooks/useGeminiStream.test.js +135 -368
- package/dist/src/ui/hooks/useGeminiStream.test.js.map +1 -1
- package/dist/src/ui/hooks/useKeypress.test.js +17 -9
- package/dist/src/ui/hooks/useKeypress.test.js.map +1 -1
- package/dist/src/ui/hooks/useMouse.d.ts +17 -0
- package/dist/src/ui/hooks/useMouse.js +27 -0
- package/dist/src/ui/hooks/useMouse.js.map +1 -0
- package/dist/src/ui/hooks/useMouse.test.d.ts +6 -0
- package/dist/src/ui/hooks/useMouse.test.js +57 -0
- package/dist/src/ui/hooks/useMouse.test.js.map +1 -0
- package/dist/src/ui/hooks/useSelectionList.js +5 -4
- package/dist/src/ui/hooks/useSelectionList.js.map +1 -1
- package/dist/src/ui/hooks/useSelectionList.test.js +24 -3
- package/dist/src/ui/hooks/useSelectionList.test.js.map +1 -1
- package/dist/src/ui/hooks/useToolScheduler.test.js +109 -200
- package/dist/src/ui/hooks/useToolScheduler.test.js.map +1 -1
- package/dist/src/ui/keyMatchers.test.js +27 -0
- package/dist/src/ui/keyMatchers.test.js.map +1 -1
- package/dist/src/ui/themes/no-color.js +1 -0
- package/dist/src/ui/themes/no-color.js.map +1 -1
- package/dist/src/ui/themes/semantic-tokens.d.ts +1 -0
- package/dist/src/ui/themes/semantic-tokens.js +3 -0
- package/dist/src/ui/themes/semantic-tokens.js.map +1 -1
- package/dist/src/ui/themes/theme.d.ts +1 -0
- package/dist/src/ui/themes/theme.js +4 -0
- package/dist/src/ui/themes/theme.js.map +1 -1
- package/dist/src/ui/utils/InlineMarkdownRenderer.d.ts +1 -0
- package/dist/src/ui/utils/InlineMarkdownRenderer.js +11 -10
- package/dist/src/ui/utils/InlineMarkdownRenderer.js.map +1 -1
- package/dist/src/ui/utils/MarkdownDisplay.js +11 -9
- package/dist/src/ui/utils/MarkdownDisplay.js.map +1 -1
- package/dist/src/ui/utils/input.d.ts +17 -0
- package/dist/src/ui/utils/input.js +51 -0
- package/dist/src/ui/utils/input.js.map +1 -0
- package/dist/src/ui/utils/input.test.d.ts +6 -0
- package/dist/src/ui/utils/input.test.js +44 -0
- package/dist/src/ui/utils/input.test.js.map +1 -0
- package/dist/src/ui/utils/kittyProtocolDetector.js +13 -4
- package/dist/src/ui/utils/kittyProtocolDetector.js.map +1 -1
- package/dist/src/ui/utils/mouse.d.ts +31 -0
- package/dist/src/ui/utils/mouse.js +164 -0
- package/dist/src/ui/utils/mouse.js.map +1 -0
- package/dist/src/ui/utils/mouse.test.d.ts +6 -0
- package/dist/src/ui/utils/mouse.test.js +131 -0
- package/dist/src/ui/utils/mouse.test.js.map +1 -0
- package/dist/src/utils/events.d.ts +11 -2
- package/dist/src/utils/events.js +1 -0
- package/dist/src/utils/events.js.map +1 -1
- package/dist/src/utils/sandbox.js +16 -18
- package/dist/src/utils/sandbox.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +4 -4
- package/dist/src/config/policy-toml-loader.d.ts +0 -46
- package/dist/src/config/policy-toml-loader.js +0 -314
- package/dist/src/config/policy-toml-loader.js.map +0 -1
- package/dist/src/config/policy-toml-loader.test.js +0 -626
- package/dist/src/config/policy-toml-loader.test.js.map +0 -1
- package/dist/src/config/policy.test.js +0 -1058
- package/dist/src/config/policy.test.js.map +0 -1
- /package/dist/src/{config/policy-toml-loader.test.d.ts → ui/commands/policiesCommand.test.d.ts} +0 -0
- /package/dist/src/{config/policy.test.d.ts → ui/contexts/MouseContext.test.d.ts} +0 -0
|
@@ -1,1058 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @license
|
|
3
|
-
* Copyright 2025 Google LLC
|
|
4
|
-
* SPDX-License-Identifier: Apache-2.0
|
|
5
|
-
*/
|
|
6
|
-
import { describe, it, expect, vi, afterEach } from 'vitest';
|
|
7
|
-
import nodePath from 'node:path';
|
|
8
|
-
import { ApprovalMode, PolicyDecision, WEB_FETCH_TOOL_NAME, } from '@google/gemini-cli-core';
|
|
9
|
-
afterEach(() => {
|
|
10
|
-
vi.clearAllMocks();
|
|
11
|
-
});
|
|
12
|
-
describe('createPolicyEngineConfig', () => {
|
|
13
|
-
it('should return ASK_USER for write tools and ALLOW for read-only tools by default', async () => {
|
|
14
|
-
const actualFs = await vi.importActual('node:fs/promises');
|
|
15
|
-
const mockReaddir = vi.fn(async (path, options) => {
|
|
16
|
-
if (typeof path === 'string' &&
|
|
17
|
-
nodePath
|
|
18
|
-
.normalize(path)
|
|
19
|
-
.includes(nodePath.normalize('.gemini/policies'))) {
|
|
20
|
-
// Return empty array for user policies
|
|
21
|
-
return [];
|
|
22
|
-
}
|
|
23
|
-
return actualFs.readdir(path, options);
|
|
24
|
-
});
|
|
25
|
-
vi.doMock('node:fs/promises', () => ({
|
|
26
|
-
...actualFs,
|
|
27
|
-
default: { ...actualFs, readdir: mockReaddir },
|
|
28
|
-
readdir: mockReaddir,
|
|
29
|
-
}));
|
|
30
|
-
vi.resetModules();
|
|
31
|
-
const { createPolicyEngineConfig } = await import('./policy.js');
|
|
32
|
-
const settings = {};
|
|
33
|
-
const config = await createPolicyEngineConfig(settings, ApprovalMode.DEFAULT);
|
|
34
|
-
expect(config.defaultDecision).toBe(PolicyDecision.ASK_USER);
|
|
35
|
-
// The order of the rules is not guaranteed, so we sort them by tool name.
|
|
36
|
-
config.rules?.sort((a, b) => (a.toolName ?? '').localeCompare(b.toolName ?? ''));
|
|
37
|
-
// Default policies are transformed to tier 1: 1 + priority/1000
|
|
38
|
-
expect(config.rules).toEqual([
|
|
39
|
-
{
|
|
40
|
-
toolName: 'glob',
|
|
41
|
-
decision: PolicyDecision.ALLOW,
|
|
42
|
-
priority: 1.05, // 1 + 50/1000
|
|
43
|
-
},
|
|
44
|
-
{
|
|
45
|
-
toolName: 'google_web_search',
|
|
46
|
-
decision: PolicyDecision.ALLOW,
|
|
47
|
-
priority: 1.05,
|
|
48
|
-
},
|
|
49
|
-
{
|
|
50
|
-
toolName: 'list_directory',
|
|
51
|
-
decision: PolicyDecision.ALLOW,
|
|
52
|
-
priority: 1.05,
|
|
53
|
-
},
|
|
54
|
-
{
|
|
55
|
-
toolName: 'read_file',
|
|
56
|
-
decision: PolicyDecision.ALLOW,
|
|
57
|
-
priority: 1.05,
|
|
58
|
-
},
|
|
59
|
-
{
|
|
60
|
-
toolName: 'read_many_files',
|
|
61
|
-
decision: PolicyDecision.ALLOW,
|
|
62
|
-
priority: 1.05,
|
|
63
|
-
},
|
|
64
|
-
{
|
|
65
|
-
toolName: 'replace',
|
|
66
|
-
decision: PolicyDecision.ASK_USER,
|
|
67
|
-
priority: 1.01, // 1 + 10/1000
|
|
68
|
-
},
|
|
69
|
-
{
|
|
70
|
-
toolName: 'run_shell_command',
|
|
71
|
-
decision: PolicyDecision.ASK_USER,
|
|
72
|
-
priority: 1.01,
|
|
73
|
-
},
|
|
74
|
-
{
|
|
75
|
-
toolName: 'save_memory',
|
|
76
|
-
decision: PolicyDecision.ASK_USER,
|
|
77
|
-
priority: 1.01,
|
|
78
|
-
},
|
|
79
|
-
{
|
|
80
|
-
toolName: 'search_file_content',
|
|
81
|
-
decision: PolicyDecision.ALLOW,
|
|
82
|
-
priority: 1.05,
|
|
83
|
-
},
|
|
84
|
-
{
|
|
85
|
-
toolName: 'web_fetch',
|
|
86
|
-
decision: PolicyDecision.ASK_USER,
|
|
87
|
-
priority: 1.01,
|
|
88
|
-
},
|
|
89
|
-
{
|
|
90
|
-
toolName: 'write_file',
|
|
91
|
-
decision: PolicyDecision.ASK_USER,
|
|
92
|
-
priority: 1.01,
|
|
93
|
-
},
|
|
94
|
-
]);
|
|
95
|
-
vi.doUnmock('node:fs/promises');
|
|
96
|
-
});
|
|
97
|
-
it('should allow tools in tools.allowed', async () => {
|
|
98
|
-
const { createPolicyEngineConfig } = await import('./policy.js');
|
|
99
|
-
const settings = {
|
|
100
|
-
tools: { allowed: ['run_shell_command'] },
|
|
101
|
-
};
|
|
102
|
-
const config = await createPolicyEngineConfig(settings, ApprovalMode.DEFAULT);
|
|
103
|
-
const rule = config.rules?.find((r) => r.toolName === 'run_shell_command' &&
|
|
104
|
-
r.decision === PolicyDecision.ALLOW);
|
|
105
|
-
expect(rule).toBeDefined();
|
|
106
|
-
expect(rule?.priority).toBeCloseTo(2.3, 5); // Command line allow
|
|
107
|
-
});
|
|
108
|
-
it('should deny tools in tools.exclude', async () => {
|
|
109
|
-
const { createPolicyEngineConfig } = await import('./policy.js');
|
|
110
|
-
const settings = {
|
|
111
|
-
tools: { exclude: ['run_shell_command'] },
|
|
112
|
-
};
|
|
113
|
-
const config = await createPolicyEngineConfig(settings, ApprovalMode.DEFAULT);
|
|
114
|
-
const rule = config.rules?.find((r) => r.toolName === 'run_shell_command' &&
|
|
115
|
-
r.decision === PolicyDecision.DENY);
|
|
116
|
-
expect(rule).toBeDefined();
|
|
117
|
-
expect(rule?.priority).toBeCloseTo(2.4, 5); // Command line exclude
|
|
118
|
-
});
|
|
119
|
-
it('should allow tools from allowed MCP servers', async () => {
|
|
120
|
-
const { createPolicyEngineConfig } = await import('./policy.js');
|
|
121
|
-
const settings = {
|
|
122
|
-
mcp: { allowed: ['my-server'] },
|
|
123
|
-
};
|
|
124
|
-
const config = await createPolicyEngineConfig(settings, ApprovalMode.DEFAULT);
|
|
125
|
-
const rule = config.rules?.find((r) => r.toolName === 'my-server__*' && r.decision === PolicyDecision.ALLOW);
|
|
126
|
-
expect(rule).toBeDefined();
|
|
127
|
-
expect(rule?.priority).toBe(2.1); // MCP allowed server
|
|
128
|
-
});
|
|
129
|
-
it('should deny tools from excluded MCP servers', async () => {
|
|
130
|
-
const { createPolicyEngineConfig } = await import('./policy.js');
|
|
131
|
-
const settings = {
|
|
132
|
-
mcp: { excluded: ['my-server'] },
|
|
133
|
-
};
|
|
134
|
-
const config = await createPolicyEngineConfig(settings, ApprovalMode.DEFAULT);
|
|
135
|
-
const rule = config.rules?.find((r) => r.toolName === 'my-server__*' && r.decision === PolicyDecision.DENY);
|
|
136
|
-
expect(rule).toBeDefined();
|
|
137
|
-
expect(rule?.priority).toBe(2.9); // MCP excluded server
|
|
138
|
-
});
|
|
139
|
-
it('should allow tools from trusted MCP servers', async () => {
|
|
140
|
-
const { createPolicyEngineConfig } = await import('./policy.js');
|
|
141
|
-
const settings = {
|
|
142
|
-
mcpServers: {
|
|
143
|
-
'trusted-server': {
|
|
144
|
-
command: 'node',
|
|
145
|
-
args: ['server.js'],
|
|
146
|
-
trust: true,
|
|
147
|
-
},
|
|
148
|
-
'untrusted-server': {
|
|
149
|
-
command: 'node',
|
|
150
|
-
args: ['server.js'],
|
|
151
|
-
trust: false,
|
|
152
|
-
},
|
|
153
|
-
},
|
|
154
|
-
};
|
|
155
|
-
const config = await createPolicyEngineConfig(settings, ApprovalMode.DEFAULT);
|
|
156
|
-
const trustedRule = config.rules?.find((r) => r.toolName === 'trusted-server__*' &&
|
|
157
|
-
r.decision === PolicyDecision.ALLOW);
|
|
158
|
-
expect(trustedRule).toBeDefined();
|
|
159
|
-
expect(trustedRule?.priority).toBe(2.2); // MCP trusted server
|
|
160
|
-
// Untrusted server should not have an allow rule
|
|
161
|
-
const untrustedRule = config.rules?.find((r) => r.toolName === 'untrusted-server__*' &&
|
|
162
|
-
r.decision === PolicyDecision.ALLOW);
|
|
163
|
-
expect(untrustedRule).toBeUndefined();
|
|
164
|
-
});
|
|
165
|
-
it('should handle multiple MCP server configurations together', async () => {
|
|
166
|
-
const { createPolicyEngineConfig } = await import('./policy.js');
|
|
167
|
-
const settings = {
|
|
168
|
-
mcp: {
|
|
169
|
-
allowed: ['allowed-server'],
|
|
170
|
-
excluded: ['excluded-server'],
|
|
171
|
-
},
|
|
172
|
-
mcpServers: {
|
|
173
|
-
'trusted-server': {
|
|
174
|
-
command: 'node',
|
|
175
|
-
args: ['server.js'],
|
|
176
|
-
trust: true,
|
|
177
|
-
},
|
|
178
|
-
},
|
|
179
|
-
};
|
|
180
|
-
const config = await createPolicyEngineConfig(settings, ApprovalMode.DEFAULT);
|
|
181
|
-
// Check allowed server
|
|
182
|
-
const allowedRule = config.rules?.find((r) => r.toolName === 'allowed-server__*' &&
|
|
183
|
-
r.decision === PolicyDecision.ALLOW);
|
|
184
|
-
expect(allowedRule).toBeDefined();
|
|
185
|
-
expect(allowedRule?.priority).toBe(2.1); // MCP allowed server
|
|
186
|
-
// Check trusted server
|
|
187
|
-
const trustedRule = config.rules?.find((r) => r.toolName === 'trusted-server__*' &&
|
|
188
|
-
r.decision === PolicyDecision.ALLOW);
|
|
189
|
-
expect(trustedRule).toBeDefined();
|
|
190
|
-
expect(trustedRule?.priority).toBe(2.2); // MCP trusted server
|
|
191
|
-
// Check excluded server
|
|
192
|
-
const excludedRule = config.rules?.find((r) => r.toolName === 'excluded-server__*' &&
|
|
193
|
-
r.decision === PolicyDecision.DENY);
|
|
194
|
-
expect(excludedRule).toBeDefined();
|
|
195
|
-
expect(excludedRule?.priority).toBe(2.9); // MCP excluded server
|
|
196
|
-
});
|
|
197
|
-
it('should allow all tools in YOLO mode', async () => {
|
|
198
|
-
const { createPolicyEngineConfig } = await import('./policy.js');
|
|
199
|
-
const settings = {};
|
|
200
|
-
const config = await createPolicyEngineConfig(settings, ApprovalMode.YOLO);
|
|
201
|
-
const rule = config.rules?.find((r) => r.decision === PolicyDecision.ALLOW && !r.toolName);
|
|
202
|
-
expect(rule).toBeDefined();
|
|
203
|
-
// Priority 999 in default tier → 1.999
|
|
204
|
-
expect(rule?.priority).toBeCloseTo(1.999, 5);
|
|
205
|
-
});
|
|
206
|
-
it('should allow edit tool in AUTO_EDIT mode', async () => {
|
|
207
|
-
const { createPolicyEngineConfig } = await import('./policy.js');
|
|
208
|
-
const settings = {};
|
|
209
|
-
const config = await createPolicyEngineConfig(settings, ApprovalMode.AUTO_EDIT);
|
|
210
|
-
const rule = config.rules?.find((r) => r.toolName === 'replace' && r.decision === PolicyDecision.ALLOW);
|
|
211
|
-
expect(rule).toBeDefined();
|
|
212
|
-
// Priority 15 in default tier → 1.015
|
|
213
|
-
expect(rule?.priority).toBeCloseTo(1.015, 5);
|
|
214
|
-
});
|
|
215
|
-
it('should prioritize exclude over allow', async () => {
|
|
216
|
-
const { createPolicyEngineConfig } = await import('./policy.js');
|
|
217
|
-
const settings = {
|
|
218
|
-
tools: { allowed: ['run_shell_command'], exclude: ['run_shell_command'] },
|
|
219
|
-
};
|
|
220
|
-
const config = await createPolicyEngineConfig(settings, ApprovalMode.DEFAULT);
|
|
221
|
-
const denyRule = config.rules?.find((r) => r.toolName === 'run_shell_command' &&
|
|
222
|
-
r.decision === PolicyDecision.DENY);
|
|
223
|
-
const allowRule = config.rules?.find((r) => r.toolName === 'run_shell_command' &&
|
|
224
|
-
r.decision === PolicyDecision.ALLOW);
|
|
225
|
-
expect(denyRule).toBeDefined();
|
|
226
|
-
expect(allowRule).toBeDefined();
|
|
227
|
-
expect(denyRule.priority).toBeGreaterThan(allowRule.priority);
|
|
228
|
-
});
|
|
229
|
-
it('should prioritize specific tool allows over MCP server excludes', async () => {
|
|
230
|
-
const { createPolicyEngineConfig } = await import('./policy.js');
|
|
231
|
-
const settings = {
|
|
232
|
-
mcp: { excluded: ['my-server'] },
|
|
233
|
-
tools: { allowed: ['my-server__specific-tool'] },
|
|
234
|
-
};
|
|
235
|
-
const config = await createPolicyEngineConfig(settings, ApprovalMode.DEFAULT);
|
|
236
|
-
const serverDenyRule = config.rules?.find((r) => r.toolName === 'my-server__*' && r.decision === PolicyDecision.DENY);
|
|
237
|
-
const toolAllowRule = config.rules?.find((r) => r.toolName === 'my-server__specific-tool' &&
|
|
238
|
-
r.decision === PolicyDecision.ALLOW);
|
|
239
|
-
expect(serverDenyRule).toBeDefined();
|
|
240
|
-
expect(serverDenyRule?.priority).toBe(2.9); // MCP excluded server
|
|
241
|
-
expect(toolAllowRule).toBeDefined();
|
|
242
|
-
expect(toolAllowRule?.priority).toBeCloseTo(2.3, 5); // Command line allow
|
|
243
|
-
// Server deny (2.9) has higher priority than tool allow (2.3),
|
|
244
|
-
// so server deny wins (this is expected behavior - server-level blocks are security critical)
|
|
245
|
-
});
|
|
246
|
-
it('should handle MCP server allows and tool excludes', async () => {
|
|
247
|
-
const { createPolicyEngineConfig } = await import('./policy.js');
|
|
248
|
-
const settings = {
|
|
249
|
-
mcp: { allowed: ['my-server'] },
|
|
250
|
-
mcpServers: {
|
|
251
|
-
'my-server': {
|
|
252
|
-
command: 'node',
|
|
253
|
-
args: ['server.js'],
|
|
254
|
-
trust: true,
|
|
255
|
-
},
|
|
256
|
-
},
|
|
257
|
-
tools: { exclude: ['my-server__dangerous-tool'] },
|
|
258
|
-
};
|
|
259
|
-
const config = await createPolicyEngineConfig(settings, ApprovalMode.DEFAULT);
|
|
260
|
-
const serverAllowRule = config.rules?.find((r) => r.toolName === 'my-server__*' && r.decision === PolicyDecision.ALLOW);
|
|
261
|
-
const toolDenyRule = config.rules?.find((r) => r.toolName === 'my-server__dangerous-tool' &&
|
|
262
|
-
r.decision === PolicyDecision.DENY);
|
|
263
|
-
expect(serverAllowRule).toBeDefined();
|
|
264
|
-
expect(toolDenyRule).toBeDefined();
|
|
265
|
-
// Command line exclude (2.4) has higher priority than MCP server trust (2.2)
|
|
266
|
-
// This is the correct behavior - specific exclusions should beat general server trust
|
|
267
|
-
expect(toolDenyRule.priority).toBeGreaterThan(serverAllowRule.priority);
|
|
268
|
-
});
|
|
269
|
-
it('should handle complex priority scenarios correctly', async () => {
|
|
270
|
-
const { createPolicyEngineConfig } = await import('./policy.js');
|
|
271
|
-
const settings = {
|
|
272
|
-
tools: {
|
|
273
|
-
autoAccept: true, // Not used in policy system (modes handle this)
|
|
274
|
-
allowed: ['my-server__tool1', 'other-tool'], // Priority 2.3
|
|
275
|
-
exclude: ['my-server__tool2', 'glob'], // Priority 2.4
|
|
276
|
-
},
|
|
277
|
-
mcp: {
|
|
278
|
-
allowed: ['allowed-server'], // Priority 2.1
|
|
279
|
-
excluded: ['excluded-server'], // Priority 2.9
|
|
280
|
-
},
|
|
281
|
-
mcpServers: {
|
|
282
|
-
'trusted-server': {
|
|
283
|
-
command: 'node',
|
|
284
|
-
args: ['server.js'],
|
|
285
|
-
trust: true, // Priority 90
|
|
286
|
-
},
|
|
287
|
-
},
|
|
288
|
-
};
|
|
289
|
-
const config = await createPolicyEngineConfig(settings, ApprovalMode.DEFAULT);
|
|
290
|
-
// Verify glob is denied even though autoAccept would allow it
|
|
291
|
-
const globDenyRule = config.rules?.find((r) => r.toolName === 'glob' && r.decision === PolicyDecision.DENY);
|
|
292
|
-
const globAllowRule = config.rules?.find((r) => r.toolName === 'glob' && r.decision === PolicyDecision.ALLOW);
|
|
293
|
-
expect(globDenyRule).toBeDefined();
|
|
294
|
-
expect(globAllowRule).toBeDefined();
|
|
295
|
-
// Deny from settings (user tier)
|
|
296
|
-
expect(globDenyRule.priority).toBeCloseTo(2.4, 5); // Command line exclude
|
|
297
|
-
// Allow from default TOML: 1 + 50/1000 = 1.05
|
|
298
|
-
expect(globAllowRule.priority).toBeCloseTo(1.05, 5);
|
|
299
|
-
// Verify all priority levels are correct
|
|
300
|
-
const priorities = config.rules
|
|
301
|
-
?.map((r) => ({
|
|
302
|
-
tool: r.toolName,
|
|
303
|
-
decision: r.decision,
|
|
304
|
-
priority: r.priority,
|
|
305
|
-
}))
|
|
306
|
-
.sort((a, b) => (b.priority ?? 0) - (a.priority ?? 0));
|
|
307
|
-
// Check that the highest priority items are the excludes (user tier: 2.4)
|
|
308
|
-
const highestPriorityExcludes = priorities?.filter((p) => Math.abs(p.priority - 2.4) < 0.01);
|
|
309
|
-
expect(highestPriorityExcludes?.every((p) => p.decision === PolicyDecision.DENY)).toBe(true);
|
|
310
|
-
});
|
|
311
|
-
it('should handle MCP servers with undefined trust property', async () => {
|
|
312
|
-
const { createPolicyEngineConfig } = await import('./policy.js');
|
|
313
|
-
const settings = {
|
|
314
|
-
mcpServers: {
|
|
315
|
-
'no-trust-property': {
|
|
316
|
-
command: 'node',
|
|
317
|
-
args: ['server.js'],
|
|
318
|
-
// trust property is undefined/missing
|
|
319
|
-
},
|
|
320
|
-
'explicit-false': {
|
|
321
|
-
command: 'node',
|
|
322
|
-
args: ['server.js'],
|
|
323
|
-
trust: false,
|
|
324
|
-
},
|
|
325
|
-
},
|
|
326
|
-
};
|
|
327
|
-
const config = await createPolicyEngineConfig(settings, ApprovalMode.DEFAULT);
|
|
328
|
-
// Neither server should have an allow rule
|
|
329
|
-
const noTrustRule = config.rules?.find((r) => r.toolName === 'no-trust-property__*' &&
|
|
330
|
-
r.decision === PolicyDecision.ALLOW);
|
|
331
|
-
const explicitFalseRule = config.rules?.find((r) => r.toolName === 'explicit-false__*' &&
|
|
332
|
-
r.decision === PolicyDecision.ALLOW);
|
|
333
|
-
expect(noTrustRule).toBeUndefined();
|
|
334
|
-
expect(explicitFalseRule).toBeUndefined();
|
|
335
|
-
});
|
|
336
|
-
it('should have YOLO allow-all rule beat write tool rules in YOLO mode', async () => {
|
|
337
|
-
const { createPolicyEngineConfig } = await import('./policy.js');
|
|
338
|
-
const settings = {
|
|
339
|
-
tools: { exclude: ['dangerous-tool'] },
|
|
340
|
-
};
|
|
341
|
-
const config = await createPolicyEngineConfig(settings, ApprovalMode.YOLO);
|
|
342
|
-
// Should have the wildcard allow rule
|
|
343
|
-
const wildcardRule = config.rules?.find((r) => !r.toolName && r.decision === PolicyDecision.ALLOW);
|
|
344
|
-
expect(wildcardRule).toBeDefined();
|
|
345
|
-
// Priority 999 in default tier → 1.999
|
|
346
|
-
expect(wildcardRule?.priority).toBeCloseTo(1.999, 5);
|
|
347
|
-
// Write tool ASK_USER rules are present (no modes restriction now)
|
|
348
|
-
const writeToolRules = config.rules?.filter((r) => [
|
|
349
|
-
'replace',
|
|
350
|
-
'save_memory',
|
|
351
|
-
'run_shell_command',
|
|
352
|
-
'write_file',
|
|
353
|
-
WEB_FETCH_TOOL_NAME,
|
|
354
|
-
].includes(r.toolName || '') && r.decision === PolicyDecision.ASK_USER);
|
|
355
|
-
expect(writeToolRules).toBeDefined();
|
|
356
|
-
// But YOLO allow-all rule has higher priority than all write tool rules
|
|
357
|
-
writeToolRules?.forEach((writeRule) => {
|
|
358
|
-
expect(wildcardRule.priority).toBeGreaterThan(writeRule.priority);
|
|
359
|
-
});
|
|
360
|
-
// Should still have the exclude rule (from settings, user tier)
|
|
361
|
-
const excludeRule = config.rules?.find((r) => r.toolName === 'dangerous-tool' && r.decision === PolicyDecision.DENY);
|
|
362
|
-
expect(excludeRule).toBeDefined();
|
|
363
|
-
expect(excludeRule?.priority).toBeCloseTo(2.4, 5); // Command line exclude
|
|
364
|
-
});
|
|
365
|
-
it('should handle combination of trusted server and excluded server for same name', async () => {
|
|
366
|
-
const { createPolicyEngineConfig } = await import('./policy.js');
|
|
367
|
-
const settings = {
|
|
368
|
-
mcpServers: {
|
|
369
|
-
'conflicted-server': {
|
|
370
|
-
command: 'node',
|
|
371
|
-
args: ['server.js'],
|
|
372
|
-
trust: true, // Priority 90
|
|
373
|
-
},
|
|
374
|
-
},
|
|
375
|
-
mcp: {
|
|
376
|
-
excluded: ['conflicted-server'], // Priority 195
|
|
377
|
-
},
|
|
378
|
-
};
|
|
379
|
-
const config = await createPolicyEngineConfig(settings, ApprovalMode.DEFAULT);
|
|
380
|
-
// Both rules should exist
|
|
381
|
-
const trustRule = config.rules?.find((r) => r.toolName === 'conflicted-server__*' &&
|
|
382
|
-
r.decision === PolicyDecision.ALLOW);
|
|
383
|
-
const excludeRule = config.rules?.find((r) => r.toolName === 'conflicted-server__*' &&
|
|
384
|
-
r.decision === PolicyDecision.DENY);
|
|
385
|
-
expect(trustRule).toBeDefined();
|
|
386
|
-
expect(trustRule?.priority).toBe(2.2); // MCP trusted server
|
|
387
|
-
expect(excludeRule).toBeDefined();
|
|
388
|
-
expect(excludeRule?.priority).toBe(2.9); // MCP excluded server
|
|
389
|
-
// Exclude (195) should win over trust (90) when evaluated
|
|
390
|
-
});
|
|
391
|
-
it('should handle all approval modes correctly', async () => {
|
|
392
|
-
const { createPolicyEngineConfig } = await import('./policy.js');
|
|
393
|
-
const settings = {};
|
|
394
|
-
// Test DEFAULT mode
|
|
395
|
-
const defaultConfig = await createPolicyEngineConfig(settings, ApprovalMode.DEFAULT);
|
|
396
|
-
expect(defaultConfig.defaultDecision).toBe(PolicyDecision.ASK_USER);
|
|
397
|
-
expect(defaultConfig.rules?.find((r) => !r.toolName && r.decision === PolicyDecision.ALLOW)).toBeUndefined();
|
|
398
|
-
// Test YOLO mode
|
|
399
|
-
const yoloConfig = await createPolicyEngineConfig(settings, ApprovalMode.YOLO);
|
|
400
|
-
expect(yoloConfig.defaultDecision).toBe(PolicyDecision.ASK_USER);
|
|
401
|
-
const yoloWildcard = yoloConfig.rules?.find((r) => !r.toolName && r.decision === PolicyDecision.ALLOW);
|
|
402
|
-
expect(yoloWildcard).toBeDefined();
|
|
403
|
-
// Priority 999 in default tier → 1.999
|
|
404
|
-
expect(yoloWildcard?.priority).toBeCloseTo(1.999, 5);
|
|
405
|
-
// Test AUTO_EDIT mode
|
|
406
|
-
const autoEditConfig = await createPolicyEngineConfig(settings, ApprovalMode.AUTO_EDIT);
|
|
407
|
-
expect(autoEditConfig.defaultDecision).toBe(PolicyDecision.ASK_USER);
|
|
408
|
-
const editRule = autoEditConfig.rules?.find((r) => r.toolName === 'replace' && r.decision === PolicyDecision.ALLOW);
|
|
409
|
-
expect(editRule).toBeDefined();
|
|
410
|
-
// Priority 15 in default tier → 1.015
|
|
411
|
-
expect(editRule?.priority).toBeCloseTo(1.015, 5);
|
|
412
|
-
});
|
|
413
|
-
it('should support argsPattern in policy rules', async () => {
|
|
414
|
-
const actualFs = await vi.importActual('node:fs/promises');
|
|
415
|
-
const mockReaddir = vi.fn(async (path, options) => {
|
|
416
|
-
if (typeof path === 'string' &&
|
|
417
|
-
nodePath
|
|
418
|
-
.normalize(path)
|
|
419
|
-
.includes(nodePath.normalize('.gemini/policies'))) {
|
|
420
|
-
return [
|
|
421
|
-
{
|
|
422
|
-
name: 'write.toml',
|
|
423
|
-
isFile: () => true,
|
|
424
|
-
isDirectory: () => false,
|
|
425
|
-
},
|
|
426
|
-
];
|
|
427
|
-
}
|
|
428
|
-
return actualFs.readdir(path, options);
|
|
429
|
-
});
|
|
430
|
-
const mockReadFile = vi.fn(async (path, options) => {
|
|
431
|
-
if (typeof path === 'string' &&
|
|
432
|
-
nodePath
|
|
433
|
-
.normalize(path)
|
|
434
|
-
.includes(nodePath.normalize('.gemini/policies/write.toml'))) {
|
|
435
|
-
return `
|
|
436
|
-
[[rule]]
|
|
437
|
-
toolName = "run_shell_command"
|
|
438
|
-
argsPattern = "\\"command\\":\\"git (status|diff|log)\\""
|
|
439
|
-
decision = "allow"
|
|
440
|
-
priority = 150
|
|
441
|
-
`;
|
|
442
|
-
}
|
|
443
|
-
return actualFs.readFile(path, options);
|
|
444
|
-
});
|
|
445
|
-
vi.doMock('node:fs/promises', () => ({
|
|
446
|
-
...actualFs,
|
|
447
|
-
default: { ...actualFs, readFile: mockReadFile, readdir: mockReaddir },
|
|
448
|
-
readFile: mockReadFile,
|
|
449
|
-
readdir: mockReaddir,
|
|
450
|
-
}));
|
|
451
|
-
vi.resetModules();
|
|
452
|
-
const { createPolicyEngineConfig } = await import('./policy.js');
|
|
453
|
-
const settings = {};
|
|
454
|
-
const config = await createPolicyEngineConfig(settings, ApprovalMode.DEFAULT);
|
|
455
|
-
const rule = config.rules?.find((r) => r.toolName === 'run_shell_command' &&
|
|
456
|
-
r.decision === PolicyDecision.ALLOW);
|
|
457
|
-
expect(rule).toBeDefined();
|
|
458
|
-
// Priority 150 in user tier → 2.150
|
|
459
|
-
expect(rule?.priority).toBeCloseTo(2.15, 5);
|
|
460
|
-
expect(rule?.argsPattern).toBeInstanceOf(RegExp);
|
|
461
|
-
expect(rule?.argsPattern?.test('{"command":"git status"}')).toBe(true);
|
|
462
|
-
expect(rule?.argsPattern?.test('{"command":"git diff"}')).toBe(true);
|
|
463
|
-
expect(rule?.argsPattern?.test('{"command":"git log"}')).toBe(true);
|
|
464
|
-
expect(rule?.argsPattern?.test('{"command":"git commit"}')).toBe(false);
|
|
465
|
-
expect(rule?.argsPattern?.test('{"command":"git push"}')).toBe(false);
|
|
466
|
-
vi.doUnmock('node:fs/promises');
|
|
467
|
-
});
|
|
468
|
-
it('should load and apply user-defined policies', async () => {
|
|
469
|
-
const actualFs = await vi.importActual('node:fs/promises');
|
|
470
|
-
const mockReaddir = vi.fn(async (path, options) => {
|
|
471
|
-
if (typeof path === 'string' &&
|
|
472
|
-
nodePath
|
|
473
|
-
.normalize(path)
|
|
474
|
-
.includes(nodePath.normalize('.gemini/policies'))) {
|
|
475
|
-
return [
|
|
476
|
-
{
|
|
477
|
-
name: 'write.toml',
|
|
478
|
-
isFile: () => true,
|
|
479
|
-
isDirectory: () => false,
|
|
480
|
-
},
|
|
481
|
-
];
|
|
482
|
-
}
|
|
483
|
-
return actualFs.readdir(path, options);
|
|
484
|
-
});
|
|
485
|
-
const mockReadFile = vi.fn(async (path, options) => {
|
|
486
|
-
if (typeof path === 'string' &&
|
|
487
|
-
nodePath
|
|
488
|
-
.normalize(path)
|
|
489
|
-
.includes(nodePath.normalize('.gemini/policies/write.toml'))) {
|
|
490
|
-
return `
|
|
491
|
-
[[rule]]
|
|
492
|
-
toolName = "run_shell_command"
|
|
493
|
-
decision = "allow"
|
|
494
|
-
priority = 150
|
|
495
|
-
`;
|
|
496
|
-
}
|
|
497
|
-
return actualFs.readFile(path, options);
|
|
498
|
-
});
|
|
499
|
-
vi.doMock('node:fs/promises', () => ({
|
|
500
|
-
...actualFs,
|
|
501
|
-
default: { ...actualFs, readFile: mockReadFile, readdir: mockReaddir },
|
|
502
|
-
readFile: mockReadFile,
|
|
503
|
-
readdir: mockReaddir,
|
|
504
|
-
}));
|
|
505
|
-
vi.resetModules();
|
|
506
|
-
const { createPolicyEngineConfig } = await import('./policy.js');
|
|
507
|
-
const settings = {};
|
|
508
|
-
const config = await createPolicyEngineConfig(settings, ApprovalMode.DEFAULT);
|
|
509
|
-
const rule = config.rules?.find((r) => r.toolName === 'run_shell_command' &&
|
|
510
|
-
r.decision === PolicyDecision.ALLOW);
|
|
511
|
-
expect(rule).toBeDefined();
|
|
512
|
-
// Priority 150 in user tier → 2.150
|
|
513
|
-
expect(rule?.priority).toBeCloseTo(2.15, 5);
|
|
514
|
-
vi.doUnmock('node:fs/promises');
|
|
515
|
-
});
|
|
516
|
-
it('should load and apply admin policies over user and default policies', async () => {
|
|
517
|
-
process.env['GEMINI_CLI_SYSTEM_SETTINGS_PATH'] = '/tmp/admin/settings.json';
|
|
518
|
-
const actualFs = await vi.importActual('node:fs/promises');
|
|
519
|
-
const mockReaddir = vi.fn(async (path, options) => {
|
|
520
|
-
if (typeof path === 'string') {
|
|
521
|
-
if (nodePath
|
|
522
|
-
.normalize(path)
|
|
523
|
-
.includes(nodePath.normalize('/tmp/admin/policies'))) {
|
|
524
|
-
return [
|
|
525
|
-
{
|
|
526
|
-
name: 'write.toml',
|
|
527
|
-
isFile: () => true,
|
|
528
|
-
isDirectory: () => false,
|
|
529
|
-
},
|
|
530
|
-
];
|
|
531
|
-
}
|
|
532
|
-
if (nodePath
|
|
533
|
-
.normalize(path)
|
|
534
|
-
.includes(nodePath.normalize('.gemini/policies'))) {
|
|
535
|
-
return [
|
|
536
|
-
{
|
|
537
|
-
name: 'write.toml',
|
|
538
|
-
isFile: () => true,
|
|
539
|
-
isDirectory: () => false,
|
|
540
|
-
},
|
|
541
|
-
];
|
|
542
|
-
}
|
|
543
|
-
}
|
|
544
|
-
return actualFs.readdir(path, options);
|
|
545
|
-
});
|
|
546
|
-
const mockReadFile = vi.fn(async (path, options) => {
|
|
547
|
-
if (typeof path === 'string' &&
|
|
548
|
-
(nodePath
|
|
549
|
-
.normalize(path)
|
|
550
|
-
.includes(nodePath.normalize('/tmp/admin/policies/write.toml')) ||
|
|
551
|
-
path.endsWith('tmp/admin/policies/write.toml'))) {
|
|
552
|
-
return `
|
|
553
|
-
[[rule]]
|
|
554
|
-
toolName = "run_shell_command"
|
|
555
|
-
decision = "deny"
|
|
556
|
-
priority = 200
|
|
557
|
-
`;
|
|
558
|
-
}
|
|
559
|
-
if (typeof path === 'string' &&
|
|
560
|
-
nodePath
|
|
561
|
-
.normalize(path)
|
|
562
|
-
.includes(nodePath.normalize('.gemini/policies/write.toml'))) {
|
|
563
|
-
return `
|
|
564
|
-
[[rule]]
|
|
565
|
-
toolName = "run_shell_command"
|
|
566
|
-
decision = "allow"
|
|
567
|
-
priority = 150
|
|
568
|
-
`;
|
|
569
|
-
}
|
|
570
|
-
return actualFs.readFile(path, options);
|
|
571
|
-
});
|
|
572
|
-
vi.doMock('node:fs/promises', () => ({
|
|
573
|
-
...actualFs,
|
|
574
|
-
default: { ...actualFs, readFile: mockReadFile, readdir: mockReaddir },
|
|
575
|
-
readFile: mockReadFile,
|
|
576
|
-
readdir: mockReaddir,
|
|
577
|
-
}));
|
|
578
|
-
vi.resetModules();
|
|
579
|
-
const { createPolicyEngineConfig } = await import('./policy.js');
|
|
580
|
-
const settings = {};
|
|
581
|
-
const config = await createPolicyEngineConfig(settings, ApprovalMode.DEFAULT);
|
|
582
|
-
const denyRule = config.rules?.find((r) => r.toolName === 'run_shell_command' &&
|
|
583
|
-
r.decision === PolicyDecision.DENY);
|
|
584
|
-
const allowRule = config.rules?.find((r) => r.toolName === 'run_shell_command' &&
|
|
585
|
-
r.decision === PolicyDecision.ALLOW);
|
|
586
|
-
expect(denyRule).toBeDefined();
|
|
587
|
-
// Priority 200 in admin tier → 3.200
|
|
588
|
-
expect(denyRule?.priority).toBeCloseTo(3.2, 5);
|
|
589
|
-
expect(allowRule).toBeDefined();
|
|
590
|
-
// Priority 150 in user tier → 2.150
|
|
591
|
-
expect(allowRule?.priority).toBeCloseTo(2.15, 5);
|
|
592
|
-
expect(denyRule.priority).toBeGreaterThan(allowRule.priority);
|
|
593
|
-
delete process.env['GEMINI_CLI_SYSTEM_SETTINGS_PATH'];
|
|
594
|
-
vi.doUnmock('node:fs/promises');
|
|
595
|
-
});
|
|
596
|
-
it('should apply priority bands to ensure Admin > User > Default hierarchy', async () => {
|
|
597
|
-
process.env['GEMINI_CLI_SYSTEM_SETTINGS_PATH'] = '/tmp/admin/settings.json';
|
|
598
|
-
const actualFs = await vi.importActual('node:fs/promises');
|
|
599
|
-
const mockReaddir = vi.fn(async (path, options) => {
|
|
600
|
-
if (typeof path === 'string') {
|
|
601
|
-
if (nodePath
|
|
602
|
-
.normalize(path)
|
|
603
|
-
.includes(nodePath.normalize('/tmp/admin/policies'))) {
|
|
604
|
-
return [
|
|
605
|
-
{
|
|
606
|
-
name: 'admin-policy.toml',
|
|
607
|
-
isFile: () => true,
|
|
608
|
-
isDirectory: () => false,
|
|
609
|
-
},
|
|
610
|
-
];
|
|
611
|
-
}
|
|
612
|
-
if (nodePath
|
|
613
|
-
.normalize(path)
|
|
614
|
-
.includes(nodePath.normalize('.gemini/policies'))) {
|
|
615
|
-
return [
|
|
616
|
-
{
|
|
617
|
-
name: 'user-policy.toml',
|
|
618
|
-
isFile: () => true,
|
|
619
|
-
isDirectory: () => false,
|
|
620
|
-
},
|
|
621
|
-
];
|
|
622
|
-
}
|
|
623
|
-
}
|
|
624
|
-
return actualFs.readdir(path, options);
|
|
625
|
-
});
|
|
626
|
-
const mockReadFile = vi.fn(async (path, options) => {
|
|
627
|
-
if (typeof path === 'string') {
|
|
628
|
-
// Admin policy with low priority (100)
|
|
629
|
-
if (nodePath
|
|
630
|
-
.normalize(path)
|
|
631
|
-
.includes(nodePath.normalize('/tmp/admin/policies/admin-policy.toml'))) {
|
|
632
|
-
return `
|
|
633
|
-
[[rule]]
|
|
634
|
-
toolName = "run_shell_command"
|
|
635
|
-
decision = "deny"
|
|
636
|
-
priority = 100
|
|
637
|
-
`;
|
|
638
|
-
}
|
|
639
|
-
// User policy with high priority (900)
|
|
640
|
-
if (nodePath
|
|
641
|
-
.normalize(path)
|
|
642
|
-
.includes(nodePath.normalize('.gemini/policies/user-policy.toml'))) {
|
|
643
|
-
return `
|
|
644
|
-
[[rule]]
|
|
645
|
-
toolName = "run_shell_command"
|
|
646
|
-
decision = "allow"
|
|
647
|
-
priority = 900
|
|
648
|
-
`;
|
|
649
|
-
}
|
|
650
|
-
}
|
|
651
|
-
return actualFs.readFile(path, options);
|
|
652
|
-
});
|
|
653
|
-
vi.doMock('node:fs/promises', () => ({
|
|
654
|
-
...actualFs,
|
|
655
|
-
default: { ...actualFs, readFile: mockReadFile, readdir: mockReaddir },
|
|
656
|
-
readFile: mockReadFile,
|
|
657
|
-
readdir: mockReaddir,
|
|
658
|
-
}));
|
|
659
|
-
vi.resetModules();
|
|
660
|
-
const { createPolicyEngineConfig } = await import('./policy.js');
|
|
661
|
-
const settings = {};
|
|
662
|
-
const config = await createPolicyEngineConfig(settings, ApprovalMode.DEFAULT);
|
|
663
|
-
const adminRule = config.rules?.find((r) => r.toolName === 'run_shell_command' &&
|
|
664
|
-
r.decision === PolicyDecision.DENY);
|
|
665
|
-
const userRule = config.rules?.find((r) => r.toolName === 'run_shell_command' &&
|
|
666
|
-
r.decision === PolicyDecision.ALLOW);
|
|
667
|
-
expect(adminRule).toBeDefined();
|
|
668
|
-
expect(userRule).toBeDefined();
|
|
669
|
-
// Admin priority should be 3.100 (tier 3 + 100/1000)
|
|
670
|
-
expect(adminRule?.priority).toBeCloseTo(3.1, 5);
|
|
671
|
-
// User priority should be 2.900 (tier 2 + 900/1000)
|
|
672
|
-
expect(userRule?.priority).toBeCloseTo(2.9, 5);
|
|
673
|
-
// Admin rule with low priority should still beat user rule with high priority
|
|
674
|
-
expect(adminRule.priority).toBeGreaterThan(userRule.priority);
|
|
675
|
-
delete process.env['GEMINI_CLI_SYSTEM_SETTINGS_PATH'];
|
|
676
|
-
vi.doUnmock('node:fs/promises');
|
|
677
|
-
});
|
|
678
|
-
it('should apply correct priority transformations for each tier', async () => {
|
|
679
|
-
process.env['GEMINI_CLI_SYSTEM_SETTINGS_PATH'] = '/tmp/admin/settings.json';
|
|
680
|
-
const actualFs = await vi.importActual('node:fs/promises');
|
|
681
|
-
const mockReaddir = vi.fn(async (path, options) => {
|
|
682
|
-
if (typeof path === 'string') {
|
|
683
|
-
if (nodePath
|
|
684
|
-
.normalize(path)
|
|
685
|
-
.includes(nodePath.normalize('/tmp/admin/policies'))) {
|
|
686
|
-
return [
|
|
687
|
-
{
|
|
688
|
-
name: 'admin.toml',
|
|
689
|
-
isFile: () => true,
|
|
690
|
-
isDirectory: () => false,
|
|
691
|
-
},
|
|
692
|
-
];
|
|
693
|
-
}
|
|
694
|
-
if (nodePath
|
|
695
|
-
.normalize(path)
|
|
696
|
-
.includes(nodePath.normalize('.gemini/policies'))) {
|
|
697
|
-
return [
|
|
698
|
-
{
|
|
699
|
-
name: 'user.toml',
|
|
700
|
-
isFile: () => true,
|
|
701
|
-
isDirectory: () => false,
|
|
702
|
-
},
|
|
703
|
-
];
|
|
704
|
-
}
|
|
705
|
-
}
|
|
706
|
-
return actualFs.readdir(path, options);
|
|
707
|
-
});
|
|
708
|
-
const mockReadFile = vi.fn(async (path, options) => {
|
|
709
|
-
if (typeof path === 'string') {
|
|
710
|
-
if (nodePath
|
|
711
|
-
.normalize(path)
|
|
712
|
-
.includes(nodePath.normalize('/tmp/admin/policies/admin.toml'))) {
|
|
713
|
-
return `
|
|
714
|
-
[[rule]]
|
|
715
|
-
toolName = "admin-tool"
|
|
716
|
-
decision = "allow"
|
|
717
|
-
priority = 500
|
|
718
|
-
`;
|
|
719
|
-
}
|
|
720
|
-
if (nodePath
|
|
721
|
-
.normalize(path)
|
|
722
|
-
.includes(nodePath.normalize('.gemini/policies/user.toml'))) {
|
|
723
|
-
return `
|
|
724
|
-
[[rule]]
|
|
725
|
-
toolName = "user-tool"
|
|
726
|
-
decision = "allow"
|
|
727
|
-
priority = 500
|
|
728
|
-
`;
|
|
729
|
-
}
|
|
730
|
-
}
|
|
731
|
-
return actualFs.readFile(path, options);
|
|
732
|
-
});
|
|
733
|
-
vi.doMock('node:fs/promises', () => ({
|
|
734
|
-
...actualFs,
|
|
735
|
-
default: { ...actualFs, readFile: mockReadFile, readdir: mockReaddir },
|
|
736
|
-
readFile: mockReadFile,
|
|
737
|
-
readdir: mockReaddir,
|
|
738
|
-
}));
|
|
739
|
-
vi.resetModules();
|
|
740
|
-
const { createPolicyEngineConfig } = await import('./policy.js');
|
|
741
|
-
const settings = {};
|
|
742
|
-
const config = await createPolicyEngineConfig(settings, ApprovalMode.DEFAULT);
|
|
743
|
-
const adminRule = config.rules?.find((r) => r.toolName === 'admin-tool');
|
|
744
|
-
const userRule = config.rules?.find((r) => r.toolName === 'user-tool');
|
|
745
|
-
expect(adminRule).toBeDefined();
|
|
746
|
-
expect(userRule).toBeDefined();
|
|
747
|
-
// Priority 500 in admin tier → 3.500
|
|
748
|
-
expect(adminRule?.priority).toBeCloseTo(3.5, 5);
|
|
749
|
-
// Priority 500 in user tier → 2.500
|
|
750
|
-
expect(userRule?.priority).toBeCloseTo(2.5, 5);
|
|
751
|
-
delete process.env['GEMINI_CLI_SYSTEM_SETTINGS_PATH'];
|
|
752
|
-
vi.doUnmock('node:fs/promises');
|
|
753
|
-
});
|
|
754
|
-
it('should support array syntax for toolName in TOML policies', async () => {
|
|
755
|
-
const actualFs = await vi.importActual('node:fs/promises');
|
|
756
|
-
const mockReaddir = vi.fn(async (path, options) => {
|
|
757
|
-
if (typeof path === 'string' &&
|
|
758
|
-
nodePath
|
|
759
|
-
.normalize(path)
|
|
760
|
-
.includes(nodePath.normalize('.gemini/policies'))) {
|
|
761
|
-
return [
|
|
762
|
-
{
|
|
763
|
-
name: 'array-test.toml',
|
|
764
|
-
isFile: () => true,
|
|
765
|
-
isDirectory: () => false,
|
|
766
|
-
},
|
|
767
|
-
];
|
|
768
|
-
}
|
|
769
|
-
return actualFs.readdir(path, options);
|
|
770
|
-
});
|
|
771
|
-
const mockReadFile = vi.fn(async (path, options) => {
|
|
772
|
-
if (typeof path === 'string' &&
|
|
773
|
-
nodePath
|
|
774
|
-
.normalize(path)
|
|
775
|
-
.includes(nodePath.normalize('.gemini/policies/array-test.toml'))) {
|
|
776
|
-
return `
|
|
777
|
-
# Test array syntax for toolName
|
|
778
|
-
[[rule]]
|
|
779
|
-
toolName = ["tool1", "tool2", "tool3"]
|
|
780
|
-
decision = "allow"
|
|
781
|
-
priority = 100
|
|
782
|
-
|
|
783
|
-
# Test array syntax with mcpName
|
|
784
|
-
[[rule]]
|
|
785
|
-
mcpName = "google-workspace"
|
|
786
|
-
toolName = ["calendar.findFreeTime", "calendar.getEvent", "calendar.list"]
|
|
787
|
-
decision = "allow"
|
|
788
|
-
priority = 150
|
|
789
|
-
`;
|
|
790
|
-
}
|
|
791
|
-
return actualFs.readFile(path, options);
|
|
792
|
-
});
|
|
793
|
-
vi.doMock('node:fs/promises', () => ({
|
|
794
|
-
...actualFs,
|
|
795
|
-
default: { ...actualFs, readFile: mockReadFile, readdir: mockReaddir },
|
|
796
|
-
readFile: mockReadFile,
|
|
797
|
-
readdir: mockReaddir,
|
|
798
|
-
}));
|
|
799
|
-
vi.resetModules();
|
|
800
|
-
const { createPolicyEngineConfig } = await import('./policy.js');
|
|
801
|
-
const settings = {};
|
|
802
|
-
const config = await createPolicyEngineConfig(settings, ApprovalMode.DEFAULT);
|
|
803
|
-
// Should create separate rules for each tool in the array
|
|
804
|
-
const tool1Rule = config.rules?.find((r) => r.toolName === 'tool1');
|
|
805
|
-
const tool2Rule = config.rules?.find((r) => r.toolName === 'tool2');
|
|
806
|
-
const tool3Rule = config.rules?.find((r) => r.toolName === 'tool3');
|
|
807
|
-
expect(tool1Rule).toBeDefined();
|
|
808
|
-
expect(tool2Rule).toBeDefined();
|
|
809
|
-
expect(tool3Rule).toBeDefined();
|
|
810
|
-
// All should have the same decision and priority
|
|
811
|
-
expect(tool1Rule?.decision).toBe(PolicyDecision.ALLOW);
|
|
812
|
-
expect(tool2Rule?.decision).toBe(PolicyDecision.ALLOW);
|
|
813
|
-
expect(tool3Rule?.decision).toBe(PolicyDecision.ALLOW);
|
|
814
|
-
// Priority 100 in user tier → 2.100
|
|
815
|
-
expect(tool1Rule?.priority).toBeCloseTo(2.1, 5);
|
|
816
|
-
expect(tool2Rule?.priority).toBeCloseTo(2.1, 5);
|
|
817
|
-
expect(tool3Rule?.priority).toBeCloseTo(2.1, 5);
|
|
818
|
-
// MCP tools should have composite names
|
|
819
|
-
const calendarFreeTime = config.rules?.find((r) => r.toolName === 'google-workspace__calendar.findFreeTime');
|
|
820
|
-
const calendarGetEvent = config.rules?.find((r) => r.toolName === 'google-workspace__calendar.getEvent');
|
|
821
|
-
const calendarList = config.rules?.find((r) => r.toolName === 'google-workspace__calendar.list');
|
|
822
|
-
expect(calendarFreeTime).toBeDefined();
|
|
823
|
-
expect(calendarGetEvent).toBeDefined();
|
|
824
|
-
expect(calendarList).toBeDefined();
|
|
825
|
-
// All should have the same decision and priority
|
|
826
|
-
expect(calendarFreeTime?.decision).toBe(PolicyDecision.ALLOW);
|
|
827
|
-
expect(calendarGetEvent?.decision).toBe(PolicyDecision.ALLOW);
|
|
828
|
-
expect(calendarList?.decision).toBe(PolicyDecision.ALLOW);
|
|
829
|
-
// Priority 150 in user tier → 2.150
|
|
830
|
-
expect(calendarFreeTime?.priority).toBeCloseTo(2.15, 5);
|
|
831
|
-
expect(calendarGetEvent?.priority).toBeCloseTo(2.15, 5);
|
|
832
|
-
expect(calendarList?.priority).toBeCloseTo(2.15, 5);
|
|
833
|
-
vi.doUnmock('node:fs/promises');
|
|
834
|
-
});
|
|
835
|
-
it('should support commandPrefix syntax for shell commands', async () => {
|
|
836
|
-
const actualFs = await vi.importActual('node:fs/promises');
|
|
837
|
-
const mockReaddir = vi.fn(async (path, options) => {
|
|
838
|
-
if (typeof path === 'string' &&
|
|
839
|
-
nodePath
|
|
840
|
-
.normalize(path)
|
|
841
|
-
.includes(nodePath.normalize('.gemini/policies'))) {
|
|
842
|
-
return [
|
|
843
|
-
{
|
|
844
|
-
name: 'shell.toml',
|
|
845
|
-
isFile: () => true,
|
|
846
|
-
isDirectory: () => false,
|
|
847
|
-
},
|
|
848
|
-
];
|
|
849
|
-
}
|
|
850
|
-
return actualFs.readdir(path, options);
|
|
851
|
-
});
|
|
852
|
-
const mockReadFile = vi.fn(async (path, options) => {
|
|
853
|
-
if (typeof path === 'string' &&
|
|
854
|
-
nodePath
|
|
855
|
-
.normalize(path)
|
|
856
|
-
.includes(nodePath.normalize('.gemini/policies/shell.toml'))) {
|
|
857
|
-
return `
|
|
858
|
-
[[rule]]
|
|
859
|
-
toolName = "run_shell_command"
|
|
860
|
-
commandPrefix = "git status"
|
|
861
|
-
decision = "allow"
|
|
862
|
-
priority = 100
|
|
863
|
-
`;
|
|
864
|
-
}
|
|
865
|
-
return actualFs.readFile(path, options);
|
|
866
|
-
});
|
|
867
|
-
vi.doMock('node:fs/promises', () => ({
|
|
868
|
-
...actualFs,
|
|
869
|
-
default: { ...actualFs, readFile: mockReadFile, readdir: mockReaddir },
|
|
870
|
-
readFile: mockReadFile,
|
|
871
|
-
readdir: mockReaddir,
|
|
872
|
-
}));
|
|
873
|
-
vi.resetModules();
|
|
874
|
-
const { createPolicyEngineConfig } = await import('./policy.js');
|
|
875
|
-
const settings = {};
|
|
876
|
-
const config = await createPolicyEngineConfig(settings, ApprovalMode.DEFAULT);
|
|
877
|
-
const rule = config.rules?.find((r) => r.toolName === 'run_shell_command' &&
|
|
878
|
-
r.decision === PolicyDecision.ALLOW);
|
|
879
|
-
expect(rule).toBeDefined();
|
|
880
|
-
expect(rule?.priority).toBeCloseTo(2.1, 5);
|
|
881
|
-
expect(rule?.argsPattern).toBeInstanceOf(RegExp);
|
|
882
|
-
// Should match commands starting with "git status"
|
|
883
|
-
expect(rule?.argsPattern?.test('{"command":"git status"}')).toBe(true);
|
|
884
|
-
expect(rule?.argsPattern?.test('{"command":"git status --short"}')).toBe(true);
|
|
885
|
-
// Should not match other commands
|
|
886
|
-
expect(rule?.argsPattern?.test('{"command":"git branch"}')).toBe(false);
|
|
887
|
-
vi.doUnmock('node:fs/promises');
|
|
888
|
-
});
|
|
889
|
-
it('should support array syntax for commandPrefix', async () => {
|
|
890
|
-
const actualFs = await vi.importActual('node:fs/promises');
|
|
891
|
-
const mockReaddir = vi.fn(async (path, options) => {
|
|
892
|
-
if (typeof path === 'string' &&
|
|
893
|
-
nodePath
|
|
894
|
-
.normalize(path)
|
|
895
|
-
.includes(nodePath.normalize('.gemini/policies'))) {
|
|
896
|
-
return [
|
|
897
|
-
{
|
|
898
|
-
name: 'shell.toml',
|
|
899
|
-
isFile: () => true,
|
|
900
|
-
isDirectory: () => false,
|
|
901
|
-
},
|
|
902
|
-
];
|
|
903
|
-
}
|
|
904
|
-
return actualFs.readdir(path, options);
|
|
905
|
-
});
|
|
906
|
-
const mockReadFile = vi.fn(async (path, options) => {
|
|
907
|
-
if (typeof path === 'string' &&
|
|
908
|
-
nodePath
|
|
909
|
-
.normalize(path)
|
|
910
|
-
.includes(nodePath.normalize('.gemini/policies/shell.toml'))) {
|
|
911
|
-
return `
|
|
912
|
-
[[rule]]
|
|
913
|
-
toolName = "run_shell_command"
|
|
914
|
-
commandPrefix = ["git status", "git branch", "git log"]
|
|
915
|
-
decision = "allow"
|
|
916
|
-
priority = 100
|
|
917
|
-
`;
|
|
918
|
-
}
|
|
919
|
-
return actualFs.readFile(path, options);
|
|
920
|
-
});
|
|
921
|
-
vi.doMock('node:fs/promises', () => ({
|
|
922
|
-
...actualFs,
|
|
923
|
-
default: { ...actualFs, readFile: mockReadFile, readdir: mockReaddir },
|
|
924
|
-
readFile: mockReadFile,
|
|
925
|
-
readdir: mockReaddir,
|
|
926
|
-
}));
|
|
927
|
-
vi.resetModules();
|
|
928
|
-
const { createPolicyEngineConfig } = await import('./policy.js');
|
|
929
|
-
const settings = {};
|
|
930
|
-
const config = await createPolicyEngineConfig(settings, ApprovalMode.DEFAULT);
|
|
931
|
-
const rules = config.rules?.filter((r) => r.toolName === 'run_shell_command' &&
|
|
932
|
-
r.decision === PolicyDecision.ALLOW);
|
|
933
|
-
// Should create 3 rules (one for each prefix)
|
|
934
|
-
expect(rules?.length).toBe(3);
|
|
935
|
-
// All rules should have the same priority and decision
|
|
936
|
-
rules?.forEach((rule) => {
|
|
937
|
-
expect(rule.priority).toBeCloseTo(2.1, 5);
|
|
938
|
-
expect(rule.decision).toBe(PolicyDecision.ALLOW);
|
|
939
|
-
});
|
|
940
|
-
// Test that each prefix pattern works
|
|
941
|
-
const patterns = rules?.map((r) => r.argsPattern);
|
|
942
|
-
expect(patterns?.some((p) => p?.test('{"command":"git status"}'))).toBe(true);
|
|
943
|
-
expect(patterns?.some((p) => p?.test('{"command":"git branch"}'))).toBe(true);
|
|
944
|
-
expect(patterns?.some((p) => p?.test('{"command":"git log"}'))).toBe(true);
|
|
945
|
-
// Should not match other commands
|
|
946
|
-
expect(patterns?.some((p) => p?.test('{"command":"git commit"}'))).toBe(false);
|
|
947
|
-
vi.doUnmock('node:fs/promises');
|
|
948
|
-
});
|
|
949
|
-
it('should support commandRegex syntax for shell commands', async () => {
|
|
950
|
-
const actualFs = await vi.importActual('node:fs/promises');
|
|
951
|
-
const mockReaddir = vi.fn(async (path, options) => {
|
|
952
|
-
if (typeof path === 'string' &&
|
|
953
|
-
nodePath
|
|
954
|
-
.normalize(path)
|
|
955
|
-
.includes(nodePath.normalize('.gemini/policies'))) {
|
|
956
|
-
return [
|
|
957
|
-
{
|
|
958
|
-
name: 'shell.toml',
|
|
959
|
-
isFile: () => true,
|
|
960
|
-
isDirectory: () => false,
|
|
961
|
-
},
|
|
962
|
-
];
|
|
963
|
-
}
|
|
964
|
-
return actualFs.readdir(path, options);
|
|
965
|
-
});
|
|
966
|
-
const mockReadFile = vi.fn(async (path, options) => {
|
|
967
|
-
if (typeof path === 'string' &&
|
|
968
|
-
nodePath
|
|
969
|
-
.normalize(path)
|
|
970
|
-
.includes(nodePath.normalize('.gemini/policies/shell.toml'))) {
|
|
971
|
-
return `
|
|
972
|
-
[[rule]]
|
|
973
|
-
toolName = "run_shell_command"
|
|
974
|
-
commandRegex = "git (status|branch|log).*"
|
|
975
|
-
decision = "allow"
|
|
976
|
-
priority = 100
|
|
977
|
-
`;
|
|
978
|
-
}
|
|
979
|
-
return actualFs.readFile(path, options);
|
|
980
|
-
});
|
|
981
|
-
vi.doMock('node:fs/promises', () => ({
|
|
982
|
-
...actualFs,
|
|
983
|
-
default: { ...actualFs, readFile: mockReadFile, readdir: mockReaddir },
|
|
984
|
-
readFile: mockReadFile,
|
|
985
|
-
readdir: mockReaddir,
|
|
986
|
-
}));
|
|
987
|
-
vi.resetModules();
|
|
988
|
-
const { createPolicyEngineConfig } = await import('./policy.js');
|
|
989
|
-
const settings = {};
|
|
990
|
-
const config = await createPolicyEngineConfig(settings, ApprovalMode.DEFAULT);
|
|
991
|
-
const rule = config.rules?.find((r) => r.toolName === 'run_shell_command' &&
|
|
992
|
-
r.decision === PolicyDecision.ALLOW);
|
|
993
|
-
expect(rule).toBeDefined();
|
|
994
|
-
expect(rule?.priority).toBeCloseTo(2.1, 5);
|
|
995
|
-
expect(rule?.argsPattern).toBeInstanceOf(RegExp);
|
|
996
|
-
// Should match commands matching the regex
|
|
997
|
-
expect(rule?.argsPattern?.test('{"command":"git status"}')).toBe(true);
|
|
998
|
-
expect(rule?.argsPattern?.test('{"command":"git status --short"}')).toBe(true);
|
|
999
|
-
expect(rule?.argsPattern?.test('{"command":"git branch"}')).toBe(true);
|
|
1000
|
-
expect(rule?.argsPattern?.test('{"command":"git log --all"}')).toBe(true);
|
|
1001
|
-
// Should not match commands not in the regex
|
|
1002
|
-
expect(rule?.argsPattern?.test('{"command":"git commit"}')).toBe(false);
|
|
1003
|
-
expect(rule?.argsPattern?.test('{"command":"git push"}')).toBe(false);
|
|
1004
|
-
vi.doUnmock('node:fs/promises');
|
|
1005
|
-
});
|
|
1006
|
-
it('should escape regex special characters in commandPrefix', async () => {
|
|
1007
|
-
const actualFs = await vi.importActual('node:fs/promises');
|
|
1008
|
-
const mockReaddir = vi.fn(async (path, options) => {
|
|
1009
|
-
if (typeof path === 'string' &&
|
|
1010
|
-
nodePath
|
|
1011
|
-
.normalize(path)
|
|
1012
|
-
.includes(nodePath.normalize('.gemini/policies'))) {
|
|
1013
|
-
return [
|
|
1014
|
-
{
|
|
1015
|
-
name: 'shell.toml',
|
|
1016
|
-
isFile: () => true,
|
|
1017
|
-
isDirectory: () => false,
|
|
1018
|
-
},
|
|
1019
|
-
];
|
|
1020
|
-
}
|
|
1021
|
-
return actualFs.readdir(path, options);
|
|
1022
|
-
});
|
|
1023
|
-
const mockReadFile = vi.fn(async (path, options) => {
|
|
1024
|
-
if (typeof path === 'string' &&
|
|
1025
|
-
nodePath
|
|
1026
|
-
.normalize(path)
|
|
1027
|
-
.includes(nodePath.normalize('.gemini/policies/shell.toml'))) {
|
|
1028
|
-
return `
|
|
1029
|
-
[[rule]]
|
|
1030
|
-
toolName = "run_shell_command"
|
|
1031
|
-
commandPrefix = "git log *.txt"
|
|
1032
|
-
decision = "allow"
|
|
1033
|
-
priority = 100
|
|
1034
|
-
`;
|
|
1035
|
-
}
|
|
1036
|
-
return actualFs.readFile(path, options);
|
|
1037
|
-
});
|
|
1038
|
-
vi.doMock('node:fs/promises', () => ({
|
|
1039
|
-
...actualFs,
|
|
1040
|
-
default: { ...actualFs, readFile: mockReadFile, readdir: mockReaddir },
|
|
1041
|
-
readFile: mockReadFile,
|
|
1042
|
-
readdir: mockReaddir,
|
|
1043
|
-
}));
|
|
1044
|
-
vi.resetModules();
|
|
1045
|
-
const { createPolicyEngineConfig } = await import('./policy.js');
|
|
1046
|
-
const settings = {};
|
|
1047
|
-
const config = await createPolicyEngineConfig(settings, ApprovalMode.DEFAULT);
|
|
1048
|
-
const rule = config.rules?.find((r) => r.toolName === 'run_shell_command' &&
|
|
1049
|
-
r.decision === PolicyDecision.ALLOW);
|
|
1050
|
-
expect(rule).toBeDefined();
|
|
1051
|
-
// Should match the literal string "git log *.txt" (asterisk is escaped)
|
|
1052
|
-
expect(rule?.argsPattern?.test('{"command":"git log *.txt"}')).toBe(true);
|
|
1053
|
-
// Should not match "git log a.txt" because * is escaped to literal asterisk
|
|
1054
|
-
expect(rule?.argsPattern?.test('{"command":"git log a.txt"}')).toBe(false);
|
|
1055
|
-
vi.doUnmock('node:fs/promises');
|
|
1056
|
-
});
|
|
1057
|
-
});
|
|
1058
|
-
//# sourceMappingURL=policy.test.js.map
|