@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.
Files changed (166) hide show
  1. package/dist/google-gemini-cli-0.13.0-nightly.20251031.c89bc30d.tgz +0 -0
  2. package/dist/package.json +3 -3
  3. package/dist/src/commands/mcp/list.test.js +25 -21
  4. package/dist/src/commands/mcp/list.test.js.map +1 -1
  5. package/dist/src/config/config.js +11 -84
  6. package/dist/src/config/config.js.map +1 -1
  7. package/dist/src/config/config.test.js +13 -30
  8. package/dist/src/config/config.test.js.map +1 -1
  9. package/dist/src/config/extension-manager.d.ts +18 -6
  10. package/dist/src/config/extension-manager.js +25 -19
  11. package/dist/src/config/extension-manager.js.map +1 -1
  12. package/dist/src/config/extension.test.js +9 -9
  13. package/dist/src/config/extension.test.js.map +1 -1
  14. package/dist/src/config/extensions/extensionSettings.test.js +39 -43
  15. package/dist/src/config/extensions/extensionSettings.test.js.map +1 -1
  16. package/dist/src/config/extensions/github.test.js +133 -165
  17. package/dist/src/config/extensions/github.test.js.map +1 -1
  18. package/dist/src/config/keyBindings.d.ts +3 -0
  19. package/dist/src/config/keyBindings.js +29 -7
  20. package/dist/src/config/keyBindings.js.map +1 -1
  21. package/dist/src/config/keyBindings.test.js +17 -0
  22. package/dist/src/config/keyBindings.test.js.map +1 -1
  23. package/dist/src/config/policy.d.ts +0 -7
  24. package/dist/src/config/policy.js +10 -177
  25. package/dist/src/config/policy.js.map +1 -1
  26. package/dist/src/config/settings.js +1 -0
  27. package/dist/src/config/settings.js.map +1 -1
  28. package/dist/src/config/settingsSchema.d.ts +123 -22
  29. package/dist/src/config/settingsSchema.js +371 -21
  30. package/dist/src/config/settingsSchema.js.map +1 -1
  31. package/dist/src/config/settingsSchema.test.js +40 -1
  32. package/dist/src/config/settingsSchema.test.js.map +1 -1
  33. package/dist/src/gemini.js +15 -5
  34. package/dist/src/gemini.js.map +1 -1
  35. package/dist/src/gemini.test.js +2 -0
  36. package/dist/src/gemini.test.js.map +1 -1
  37. package/dist/src/generated/git-commit.d.ts +2 -2
  38. package/dist/src/generated/git-commit.js +2 -2
  39. package/dist/src/generated/git-commit.js.map +1 -1
  40. package/dist/src/nonInteractiveCli.js +68 -1
  41. package/dist/src/nonInteractiveCli.js.map +1 -1
  42. package/dist/src/services/BuiltinCommandLoader.js +4 -0
  43. package/dist/src/services/BuiltinCommandLoader.js.map +1 -1
  44. package/dist/src/services/BuiltinCommandLoader.test.js +22 -0
  45. package/dist/src/services/BuiltinCommandLoader.test.js.map +1 -1
  46. package/dist/src/services/McpPromptLoader.js +2 -2
  47. package/dist/src/services/McpPromptLoader.js.map +1 -1
  48. package/dist/src/services/McpPromptLoader.test.js +4 -2
  49. package/dist/src/services/McpPromptLoader.test.js.map +1 -1
  50. package/dist/src/test-utils/render.d.ts +2 -1
  51. package/dist/src/test-utils/render.js +3 -2
  52. package/dist/src/test-utils/render.js.map +1 -1
  53. package/dist/src/ui/AppContainer.js +32 -15
  54. package/dist/src/ui/AppContainer.js.map +1 -1
  55. package/dist/src/ui/AppContainer.test.js +160 -0
  56. package/dist/src/ui/AppContainer.test.js.map +1 -1
  57. package/dist/src/ui/commands/mcpCommand.js +14 -14
  58. package/dist/src/ui/commands/mcpCommand.js.map +1 -1
  59. package/dist/src/ui/commands/mcpCommand.test.js +4 -0
  60. package/dist/src/ui/commands/mcpCommand.test.js.map +1 -1
  61. package/dist/src/ui/commands/policiesCommand.d.ts +7 -0
  62. package/dist/src/ui/commands/policiesCommand.js +59 -0
  63. package/dist/src/ui/commands/policiesCommand.js.map +1 -0
  64. package/dist/src/ui/commands/policiesCommand.test.js +83 -0
  65. package/dist/src/ui/commands/policiesCommand.test.js.map +1 -0
  66. package/dist/src/ui/components/Composer.js +1 -1
  67. package/dist/src/ui/components/Composer.js.map +1 -1
  68. package/dist/src/ui/components/Composer.test.js +4 -1
  69. package/dist/src/ui/components/Composer.test.js.map +1 -1
  70. package/dist/src/ui/components/ConfigInitDisplay.js +4 -6
  71. package/dist/src/ui/components/ConfigInitDisplay.js.map +1 -1
  72. package/dist/src/ui/components/InputPrompt.js +22 -2
  73. package/dist/src/ui/components/InputPrompt.js.map +1 -1
  74. package/dist/src/ui/components/InputPrompt.test.js +70 -5
  75. package/dist/src/ui/components/InputPrompt.test.js.map +1 -1
  76. package/dist/src/ui/components/MainContent.js +15 -4
  77. package/dist/src/ui/components/MainContent.js.map +1 -1
  78. package/dist/src/ui/components/Notifications.js +38 -5
  79. package/dist/src/ui/components/Notifications.js.map +1 -1
  80. package/dist/src/ui/components/SettingsDialog.js +32 -25
  81. package/dist/src/ui/components/SettingsDialog.js.map +1 -1
  82. package/dist/src/ui/components/ShellConfirmationDialog.js +1 -1
  83. package/dist/src/ui/components/ShellConfirmationDialog.js.map +1 -1
  84. package/dist/src/ui/components/messages/InfoMessage.js +1 -1
  85. package/dist/src/ui/components/messages/InfoMessage.js.map +1 -1
  86. package/dist/src/ui/components/messages/ToolConfirmationMessage.js +1 -1
  87. package/dist/src/ui/components/messages/ToolConfirmationMessage.js.map +1 -1
  88. package/dist/src/ui/components/messages/WarningMessage.js +2 -2
  89. package/dist/src/ui/components/messages/WarningMessage.js.map +1 -1
  90. package/dist/src/ui/components/shared/text-buffer.d.ts +1 -0
  91. package/dist/src/ui/components/shared/text-buffer.js +23 -0
  92. package/dist/src/ui/components/shared/text-buffer.js.map +1 -1
  93. package/dist/src/ui/components/shared/text-buffer.test.js +246 -201
  94. package/dist/src/ui/components/shared/text-buffer.test.js.map +1 -1
  95. package/dist/src/ui/contexts/KeypressContext.js +182 -132
  96. package/dist/src/ui/contexts/KeypressContext.js.map +1 -1
  97. package/dist/src/ui/contexts/KeypressContext.test.js +144 -8
  98. package/dist/src/ui/contexts/KeypressContext.test.js.map +1 -1
  99. package/dist/src/ui/contexts/MouseContext.d.ts +21 -0
  100. package/dist/src/ui/contexts/MouseContext.js +89 -0
  101. package/dist/src/ui/contexts/MouseContext.js.map +1 -0
  102. package/dist/src/ui/contexts/MouseContext.test.js +164 -0
  103. package/dist/src/ui/contexts/MouseContext.test.js.map +1 -0
  104. package/dist/src/ui/hooks/slashCommandProcessor.test.js +70 -73
  105. package/dist/src/ui/hooks/slashCommandProcessor.test.js.map +1 -1
  106. package/dist/src/ui/hooks/useGeminiStream.test.js +135 -368
  107. package/dist/src/ui/hooks/useGeminiStream.test.js.map +1 -1
  108. package/dist/src/ui/hooks/useKeypress.test.js +17 -9
  109. package/dist/src/ui/hooks/useKeypress.test.js.map +1 -1
  110. package/dist/src/ui/hooks/useMouse.d.ts +17 -0
  111. package/dist/src/ui/hooks/useMouse.js +27 -0
  112. package/dist/src/ui/hooks/useMouse.js.map +1 -0
  113. package/dist/src/ui/hooks/useMouse.test.d.ts +6 -0
  114. package/dist/src/ui/hooks/useMouse.test.js +57 -0
  115. package/dist/src/ui/hooks/useMouse.test.js.map +1 -0
  116. package/dist/src/ui/hooks/useSelectionList.js +5 -4
  117. package/dist/src/ui/hooks/useSelectionList.js.map +1 -1
  118. package/dist/src/ui/hooks/useSelectionList.test.js +24 -3
  119. package/dist/src/ui/hooks/useSelectionList.test.js.map +1 -1
  120. package/dist/src/ui/hooks/useToolScheduler.test.js +109 -200
  121. package/dist/src/ui/hooks/useToolScheduler.test.js.map +1 -1
  122. package/dist/src/ui/keyMatchers.test.js +27 -0
  123. package/dist/src/ui/keyMatchers.test.js.map +1 -1
  124. package/dist/src/ui/themes/no-color.js +1 -0
  125. package/dist/src/ui/themes/no-color.js.map +1 -1
  126. package/dist/src/ui/themes/semantic-tokens.d.ts +1 -0
  127. package/dist/src/ui/themes/semantic-tokens.js +3 -0
  128. package/dist/src/ui/themes/semantic-tokens.js.map +1 -1
  129. package/dist/src/ui/themes/theme.d.ts +1 -0
  130. package/dist/src/ui/themes/theme.js +4 -0
  131. package/dist/src/ui/themes/theme.js.map +1 -1
  132. package/dist/src/ui/utils/InlineMarkdownRenderer.d.ts +1 -0
  133. package/dist/src/ui/utils/InlineMarkdownRenderer.js +11 -10
  134. package/dist/src/ui/utils/InlineMarkdownRenderer.js.map +1 -1
  135. package/dist/src/ui/utils/MarkdownDisplay.js +11 -9
  136. package/dist/src/ui/utils/MarkdownDisplay.js.map +1 -1
  137. package/dist/src/ui/utils/input.d.ts +17 -0
  138. package/dist/src/ui/utils/input.js +51 -0
  139. package/dist/src/ui/utils/input.js.map +1 -0
  140. package/dist/src/ui/utils/input.test.d.ts +6 -0
  141. package/dist/src/ui/utils/input.test.js +44 -0
  142. package/dist/src/ui/utils/input.test.js.map +1 -0
  143. package/dist/src/ui/utils/kittyProtocolDetector.js +13 -4
  144. package/dist/src/ui/utils/kittyProtocolDetector.js.map +1 -1
  145. package/dist/src/ui/utils/mouse.d.ts +31 -0
  146. package/dist/src/ui/utils/mouse.js +164 -0
  147. package/dist/src/ui/utils/mouse.js.map +1 -0
  148. package/dist/src/ui/utils/mouse.test.d.ts +6 -0
  149. package/dist/src/ui/utils/mouse.test.js +131 -0
  150. package/dist/src/ui/utils/mouse.test.js.map +1 -0
  151. package/dist/src/utils/events.d.ts +11 -2
  152. package/dist/src/utils/events.js +1 -0
  153. package/dist/src/utils/events.js.map +1 -1
  154. package/dist/src/utils/sandbox.js +16 -18
  155. package/dist/src/utils/sandbox.js.map +1 -1
  156. package/dist/tsconfig.tsbuildinfo +1 -1
  157. package/package.json +4 -4
  158. package/dist/src/config/policy-toml-loader.d.ts +0 -46
  159. package/dist/src/config/policy-toml-loader.js +0 -314
  160. package/dist/src/config/policy-toml-loader.js.map +0 -1
  161. package/dist/src/config/policy-toml-loader.test.js +0 -626
  162. package/dist/src/config/policy-toml-loader.test.js.map +0 -1
  163. package/dist/src/config/policy.test.js +0 -1058
  164. package/dist/src/config/policy.test.js.map +0 -1
  165. /package/dist/src/{config/policy-toml-loader.test.d.ts → ui/commands/policiesCommand.test.d.ts} +0 -0
  166. /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