@google/gemini-cli 0.12.0-nightly.20251027.cb0947c5 → 0.12.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/README.md +7 -5
- package/dist/package.json +2 -2
- package/dist/src/commands/extensions/disable.d.ts +1 -1
- package/dist/src/commands/extensions/disable.js +5 -4
- package/dist/src/commands/extensions/disable.js.map +1 -1
- package/dist/src/commands/extensions/enable.d.ts +1 -1
- package/dist/src/commands/extensions/enable.js +3 -2
- package/dist/src/commands/extensions/enable.js.map +1 -1
- package/dist/src/commands/extensions/install.js +2 -1
- package/dist/src/commands/extensions/install.js.map +1 -1
- package/dist/src/commands/extensions/install.test.js +1 -0
- package/dist/src/commands/extensions/install.test.js.map +1 -1
- package/dist/src/commands/extensions/link.js +2 -1
- package/dist/src/commands/extensions/link.js.map +1 -1
- package/dist/src/commands/extensions/list.js +2 -2
- package/dist/src/commands/extensions/list.js.map +1 -1
- package/dist/src/commands/extensions/uninstall.js +2 -1
- package/dist/src/commands/extensions/uninstall.js.map +1 -1
- package/dist/src/commands/extensions/update.js +2 -2
- package/dist/src/commands/extensions/update.js.map +1 -1
- package/dist/src/commands/mcp/list.js +2 -2
- package/dist/src/commands/mcp/list.js.map +1 -1
- package/dist/src/config/config.d.ts +5 -3
- package/dist/src/config/config.js +42 -9
- package/dist/src/config/config.js.map +1 -1
- package/dist/src/config/config.test.js +186 -161
- package/dist/src/config/config.test.js.map +1 -1
- package/dist/src/config/extension-manager.d.ts +23 -10
- package/dist/src/config/extension-manager.js +89 -62
- package/dist/src/config/extension-manager.js.map +1 -1
- package/dist/src/config/extension.test.js +158 -74
- package/dist/src/config/extension.test.js.map +1 -1
- package/dist/src/config/extensions/extensionSettings.d.ts +3 -3
- package/dist/src/config/extensions/extensionSettings.js +74 -24
- package/dist/src/config/extensions/extensionSettings.js.map +1 -1
- package/dist/src/config/extensions/extensionSettings.test.js +145 -24
- package/dist/src/config/extensions/extensionSettings.test.js.map +1 -1
- package/dist/src/config/extensions/github.js +3 -3
- package/dist/src/config/extensions/github.js.map +1 -1
- package/dist/src/config/extensions/github.test.js +1 -1
- package/dist/src/config/extensions/github.test.js.map +1 -1
- package/dist/src/config/extensions/update.js +7 -6
- package/dist/src/config/extensions/update.js.map +1 -1
- package/dist/src/config/extensions/update.test.js +54 -31
- package/dist/src/config/extensions/update.test.js.map +1 -1
- package/dist/src/config/keyBindings.js +1 -1
- package/dist/src/config/keyBindings.js.map +1 -1
- package/dist/src/config/policies/read-only.toml +56 -0
- package/dist/src/config/policies/write.toml +63 -0
- package/dist/src/config/policies/yolo.toml +31 -0
- package/dist/src/config/policy-engine.integration.test.js +41 -38
- package/dist/src/config/policy-engine.integration.test.js.map +1 -1
- package/dist/src/config/policy-toml-loader.d.ts +46 -0
- package/dist/src/config/policy-toml-loader.js +314 -0
- package/dist/src/config/policy-toml-loader.js.map +1 -0
- package/dist/src/config/policy-toml-loader.test.d.ts +6 -0
- package/dist/src/config/policy-toml-loader.test.js +626 -0
- package/dist/src/config/policy-toml-loader.test.js.map +1 -0
- package/dist/src/config/policy.d.ts +9 -2
- package/dist/src/config/policy.js +139 -110
- package/dist/src/config/policy.js.map +1 -1
- package/dist/src/config/policy.test.js +780 -82
- package/dist/src/config/policy.test.js.map +1 -1
- package/dist/src/config/settings.test.js +4 -4
- package/dist/src/config/settings.test.js.map +1 -1
- package/dist/src/gemini.js +6 -17
- package/dist/src/gemini.js.map +1 -1
- package/dist/src/gemini.test.js +1 -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/test-utils/render.d.ts +12 -0
- package/dist/src/test-utils/render.js +28 -1
- package/dist/src/test-utils/render.js.map +1 -1
- package/dist/src/test-utils/render.test.d.ts +6 -0
- package/dist/src/test-utils/render.test.js +54 -0
- package/dist/src/test-utils/render.test.js.map +1 -0
- package/dist/src/ui/AppContainer.js +28 -22
- package/dist/src/ui/AppContainer.js.map +1 -1
- package/dist/src/ui/AppContainer.test.js +8 -0
- package/dist/src/ui/AppContainer.test.js.map +1 -1
- package/dist/src/ui/commands/directoryCommand.js +1 -1
- package/dist/src/ui/commands/directoryCommand.js.map +1 -1
- package/dist/src/ui/commands/extensionsCommand.js +45 -1
- package/dist/src/ui/commands/extensionsCommand.js.map +1 -1
- package/dist/src/ui/commands/extensionsCommand.test.js +64 -1
- package/dist/src/ui/commands/extensionsCommand.test.js.map +1 -1
- package/dist/src/ui/commands/memoryCommand.js +1 -1
- package/dist/src/ui/commands/memoryCommand.js.map +1 -1
- package/dist/src/ui/commands/memoryCommand.test.js +3 -1
- package/dist/src/ui/commands/memoryCommand.test.js.map +1 -1
- package/dist/src/ui/components/ConsoleSummaryDisplay.js +1 -1
- package/dist/src/ui/components/ConsoleSummaryDisplay.js.map +1 -1
- package/dist/src/ui/components/DetailedMessagesDisplay.js +1 -1
- package/dist/src/ui/components/DetailedMessagesDisplay.js.map +1 -1
- package/dist/src/ui/components/FolderTrustDialog.test.js +4 -5
- package/dist/src/ui/components/FolderTrustDialog.test.js.map +1 -1
- package/dist/src/ui/components/Footer.js +1 -1
- package/dist/src/ui/components/Footer.js.map +1 -1
- package/dist/src/ui/components/Footer.test.js +24 -0
- package/dist/src/ui/components/Footer.test.js.map +1 -1
- package/dist/src/ui/components/Help.test.js +0 -1
- package/dist/src/ui/components/Help.test.js.map +1 -1
- package/dist/src/ui/components/ModelDialog.test.js +5 -6
- package/dist/src/ui/components/ModelDialog.test.js.map +1 -1
- package/dist/src/ui/components/PermissionsModifyTrustDialog.test.js +11 -13
- package/dist/src/ui/components/PermissionsModifyTrustDialog.test.js.map +1 -1
- package/dist/src/ui/components/SettingsDialog.test.js +12 -14
- package/dist/src/ui/components/SettingsDialog.test.js.map +1 -1
- package/dist/src/ui/components/shared/BaseSelectionList.test.js +11 -13
- package/dist/src/ui/components/shared/BaseSelectionList.test.js.map +1 -1
- package/dist/src/ui/components/shared/text-buffer.test.js +2 -2
- package/dist/src/ui/components/shared/text-buffer.test.js.map +1 -1
- package/dist/src/ui/contexts/KeypressContext.test.js +6 -5
- package/dist/src/ui/contexts/KeypressContext.test.js.map +1 -1
- package/dist/src/ui/contexts/SessionContext.test.js +27 -14
- package/dist/src/ui/contexts/SessionContext.test.js.map +1 -1
- package/dist/src/ui/hooks/atCommandProcessor.js +2 -2
- package/dist/src/ui/hooks/atCommandProcessor.js.map +1 -1
- package/dist/src/ui/hooks/useAtCompletion.test.js +32 -23
- package/dist/src/ui/hooks/useAtCompletion.test.js.map +1 -1
- package/dist/src/ui/hooks/useAutoAcceptIndicator.test.js +2 -2
- package/dist/src/ui/hooks/useAutoAcceptIndicator.test.js.map +1 -1
- package/dist/src/ui/hooks/useExtensionUpdates.d.ts +1 -2
- package/dist/src/ui/hooks/useExtensionUpdates.js +2 -1
- package/dist/src/ui/hooks/useExtensionUpdates.js.map +1 -1
- package/dist/src/ui/hooks/useExtensionUpdates.test.js +14 -20
- package/dist/src/ui/hooks/useExtensionUpdates.test.js.map +1 -1
- package/dist/src/ui/hooks/useFlickerDetector.test.js +9 -6
- package/dist/src/ui/hooks/useFlickerDetector.test.js.map +1 -1
- package/dist/src/ui/hooks/useFolderTrust.test.js +45 -23
- package/dist/src/ui/hooks/useFolderTrust.test.js.map +1 -1
- package/dist/src/ui/hooks/useGeminiStream.js +7 -5
- package/dist/src/ui/hooks/useGeminiStream.js.map +1 -1
- package/dist/src/ui/hooks/useGeminiStream.test.js +42 -41
- package/dist/src/ui/hooks/useGeminiStream.test.js.map +1 -1
- package/dist/src/ui/hooks/useHistoryManager.test.js +2 -2
- package/dist/src/ui/hooks/useHistoryManager.test.js.map +1 -1
- package/dist/src/ui/hooks/useInputHistory.test.js +2 -2
- package/dist/src/ui/hooks/useInputHistory.test.js.map +1 -1
- package/dist/src/ui/hooks/useInputHistoryStore.test.js +2 -2
- package/dist/src/ui/hooks/useInputHistoryStore.test.js.map +1 -1
- package/dist/src/ui/hooks/usePermissionsModifyTrust.test.js +2 -3
- package/dist/src/ui/hooks/usePermissionsModifyTrust.test.js.map +1 -1
- package/dist/src/ui/hooks/usePhraseCycler.js +1 -1
- package/dist/src/ui/hooks/usePhraseCycler.js.map +1 -1
- package/dist/src/ui/hooks/usePhraseCycler.test.js +83 -111
- package/dist/src/ui/hooks/usePhraseCycler.test.js.map +1 -1
- package/dist/src/ui/hooks/useQuotaAndFallback.test.js +2 -2
- package/dist/src/ui/hooks/useQuotaAndFallback.test.js.map +1 -1
- package/dist/src/ui/hooks/useReactToolScheduler.test.js +1 -2
- package/dist/src/ui/hooks/useReactToolScheduler.test.js.map +1 -1
- package/dist/src/ui/hooks/useReverseSearchCompletion.test.js +2 -2
- package/dist/src/ui/hooks/useReverseSearchCompletion.test.js.map +1 -1
- package/dist/src/ui/hooks/useShellHistory.test.js +40 -17
- package/dist/src/ui/hooks/useShellHistory.test.js.map +1 -1
- package/dist/src/ui/hooks/useSlashCompletion.test.js +54 -49
- package/dist/src/ui/hooks/useSlashCompletion.test.js.map +1 -1
- package/dist/src/ui/hooks/useToolScheduler.test.js +48 -42
- package/dist/src/ui/hooks/useToolScheduler.test.js.map +1 -1
- package/dist/src/ui/keyMatchers.test.js +3 -3
- package/dist/src/ui/keyMatchers.test.js.map +1 -1
- package/dist/src/zed-integration/zedIntegration.d.ts +2 -2
- package/dist/src/zed-integration/zedIntegration.js +4 -6
- package/dist/src/zed-integration/zedIntegration.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +3 -3
|
@@ -3,113 +3,141 @@
|
|
|
3
3
|
* Copyright 2025 Google LLC
|
|
4
4
|
* SPDX-License-Identifier: Apache-2.0
|
|
5
5
|
*/
|
|
6
|
-
import { describe, it, expect } from 'vitest';
|
|
7
|
-
import
|
|
6
|
+
import { describe, it, expect, vi, afterEach } from 'vitest';
|
|
7
|
+
import nodePath from 'node:path';
|
|
8
8
|
import { ApprovalMode, PolicyDecision, WEB_FETCH_TOOL_NAME, } from '@google/gemini-cli-core';
|
|
9
|
+
afterEach(() => {
|
|
10
|
+
vi.clearAllMocks();
|
|
11
|
+
});
|
|
9
12
|
describe('createPolicyEngineConfig', () => {
|
|
10
|
-
it('should return ASK_USER for write tools and ALLOW for read-only tools by default', () => {
|
|
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');
|
|
11
32
|
const settings = {};
|
|
12
|
-
const config = createPolicyEngineConfig(settings, ApprovalMode.DEFAULT);
|
|
33
|
+
const config = await createPolicyEngineConfig(settings, ApprovalMode.DEFAULT);
|
|
13
34
|
expect(config.defaultDecision).toBe(PolicyDecision.ASK_USER);
|
|
14
35
|
// The order of the rules is not guaranteed, so we sort them by tool name.
|
|
15
36
|
config.rules?.sort((a, b) => (a.toolName ?? '').localeCompare(b.toolName ?? ''));
|
|
37
|
+
// Default policies are transformed to tier 1: 1 + priority/1000
|
|
16
38
|
expect(config.rules).toEqual([
|
|
17
39
|
{
|
|
18
40
|
toolName: 'glob',
|
|
19
41
|
decision: PolicyDecision.ALLOW,
|
|
20
|
-
priority: 50
|
|
42
|
+
priority: 1.05, // 1 + 50/1000
|
|
21
43
|
},
|
|
22
44
|
{
|
|
23
45
|
toolName: 'google_web_search',
|
|
24
46
|
decision: PolicyDecision.ALLOW,
|
|
25
|
-
priority:
|
|
47
|
+
priority: 1.05,
|
|
26
48
|
},
|
|
27
49
|
{
|
|
28
50
|
toolName: 'list_directory',
|
|
29
51
|
decision: PolicyDecision.ALLOW,
|
|
30
|
-
priority:
|
|
52
|
+
priority: 1.05,
|
|
31
53
|
},
|
|
32
54
|
{
|
|
33
55
|
toolName: 'read_file',
|
|
34
56
|
decision: PolicyDecision.ALLOW,
|
|
35
|
-
priority:
|
|
57
|
+
priority: 1.05,
|
|
36
58
|
},
|
|
37
59
|
{
|
|
38
60
|
toolName: 'read_many_files',
|
|
39
61
|
decision: PolicyDecision.ALLOW,
|
|
40
|
-
priority:
|
|
62
|
+
priority: 1.05,
|
|
41
63
|
},
|
|
42
64
|
{
|
|
43
65
|
toolName: 'replace',
|
|
44
66
|
decision: PolicyDecision.ASK_USER,
|
|
45
|
-
priority: 10
|
|
67
|
+
priority: 1.01, // 1 + 10/1000
|
|
46
68
|
},
|
|
47
69
|
{
|
|
48
70
|
toolName: 'run_shell_command',
|
|
49
71
|
decision: PolicyDecision.ASK_USER,
|
|
50
|
-
priority:
|
|
72
|
+
priority: 1.01,
|
|
51
73
|
},
|
|
52
74
|
{
|
|
53
75
|
toolName: 'save_memory',
|
|
54
76
|
decision: PolicyDecision.ASK_USER,
|
|
55
|
-
priority:
|
|
77
|
+
priority: 1.01,
|
|
56
78
|
},
|
|
57
79
|
{
|
|
58
80
|
toolName: 'search_file_content',
|
|
59
81
|
decision: PolicyDecision.ALLOW,
|
|
60
|
-
priority:
|
|
82
|
+
priority: 1.05,
|
|
61
83
|
},
|
|
62
84
|
{
|
|
63
85
|
toolName: 'web_fetch',
|
|
64
86
|
decision: PolicyDecision.ASK_USER,
|
|
65
|
-
priority:
|
|
87
|
+
priority: 1.01,
|
|
66
88
|
},
|
|
67
89
|
{
|
|
68
90
|
toolName: 'write_file',
|
|
69
91
|
decision: PolicyDecision.ASK_USER,
|
|
70
|
-
priority:
|
|
92
|
+
priority: 1.01,
|
|
71
93
|
},
|
|
72
94
|
]);
|
|
95
|
+
vi.doUnmock('node:fs/promises');
|
|
73
96
|
});
|
|
74
|
-
it('should allow tools in tools.allowed', () => {
|
|
97
|
+
it('should allow tools in tools.allowed', async () => {
|
|
98
|
+
const { createPolicyEngineConfig } = await import('./policy.js');
|
|
75
99
|
const settings = {
|
|
76
100
|
tools: { allowed: ['run_shell_command'] },
|
|
77
101
|
};
|
|
78
|
-
const config = createPolicyEngineConfig(settings, ApprovalMode.DEFAULT);
|
|
102
|
+
const config = await createPolicyEngineConfig(settings, ApprovalMode.DEFAULT);
|
|
79
103
|
const rule = config.rules?.find((r) => r.toolName === 'run_shell_command' &&
|
|
80
104
|
r.decision === PolicyDecision.ALLOW);
|
|
81
105
|
expect(rule).toBeDefined();
|
|
82
|
-
expect(rule?.priority).
|
|
106
|
+
expect(rule?.priority).toBeCloseTo(2.3, 5); // Command line allow
|
|
83
107
|
});
|
|
84
|
-
it('should deny tools in tools.exclude', () => {
|
|
108
|
+
it('should deny tools in tools.exclude', async () => {
|
|
109
|
+
const { createPolicyEngineConfig } = await import('./policy.js');
|
|
85
110
|
const settings = {
|
|
86
111
|
tools: { exclude: ['run_shell_command'] },
|
|
87
112
|
};
|
|
88
|
-
const config = createPolicyEngineConfig(settings, ApprovalMode.DEFAULT);
|
|
113
|
+
const config = await createPolicyEngineConfig(settings, ApprovalMode.DEFAULT);
|
|
89
114
|
const rule = config.rules?.find((r) => r.toolName === 'run_shell_command' &&
|
|
90
115
|
r.decision === PolicyDecision.DENY);
|
|
91
116
|
expect(rule).toBeDefined();
|
|
92
|
-
expect(rule?.priority).
|
|
117
|
+
expect(rule?.priority).toBeCloseTo(2.4, 5); // Command line exclude
|
|
93
118
|
});
|
|
94
|
-
it('should allow tools from allowed MCP servers', () => {
|
|
119
|
+
it('should allow tools from allowed MCP servers', async () => {
|
|
120
|
+
const { createPolicyEngineConfig } = await import('./policy.js');
|
|
95
121
|
const settings = {
|
|
96
122
|
mcp: { allowed: ['my-server'] },
|
|
97
123
|
};
|
|
98
|
-
const config = createPolicyEngineConfig(settings, ApprovalMode.DEFAULT);
|
|
124
|
+
const config = await createPolicyEngineConfig(settings, ApprovalMode.DEFAULT);
|
|
99
125
|
const rule = config.rules?.find((r) => r.toolName === 'my-server__*' && r.decision === PolicyDecision.ALLOW);
|
|
100
126
|
expect(rule).toBeDefined();
|
|
101
|
-
expect(rule?.priority).toBe(
|
|
127
|
+
expect(rule?.priority).toBe(2.1); // MCP allowed server
|
|
102
128
|
});
|
|
103
|
-
it('should deny tools from excluded MCP servers', () => {
|
|
129
|
+
it('should deny tools from excluded MCP servers', async () => {
|
|
130
|
+
const { createPolicyEngineConfig } = await import('./policy.js');
|
|
104
131
|
const settings = {
|
|
105
132
|
mcp: { excluded: ['my-server'] },
|
|
106
133
|
};
|
|
107
|
-
const config = createPolicyEngineConfig(settings, ApprovalMode.DEFAULT);
|
|
134
|
+
const config = await createPolicyEngineConfig(settings, ApprovalMode.DEFAULT);
|
|
108
135
|
const rule = config.rules?.find((r) => r.toolName === 'my-server__*' && r.decision === PolicyDecision.DENY);
|
|
109
136
|
expect(rule).toBeDefined();
|
|
110
|
-
expect(rule?.priority).toBe(
|
|
137
|
+
expect(rule?.priority).toBe(2.9); // MCP excluded server
|
|
111
138
|
});
|
|
112
|
-
it('should allow tools from trusted MCP servers', () => {
|
|
139
|
+
it('should allow tools from trusted MCP servers', async () => {
|
|
140
|
+
const { createPolicyEngineConfig } = await import('./policy.js');
|
|
113
141
|
const settings = {
|
|
114
142
|
mcpServers: {
|
|
115
143
|
'trusted-server': {
|
|
@@ -124,17 +152,18 @@ describe('createPolicyEngineConfig', () => {
|
|
|
124
152
|
},
|
|
125
153
|
},
|
|
126
154
|
};
|
|
127
|
-
const config = createPolicyEngineConfig(settings, ApprovalMode.DEFAULT);
|
|
155
|
+
const config = await createPolicyEngineConfig(settings, ApprovalMode.DEFAULT);
|
|
128
156
|
const trustedRule = config.rules?.find((r) => r.toolName === 'trusted-server__*' &&
|
|
129
157
|
r.decision === PolicyDecision.ALLOW);
|
|
130
158
|
expect(trustedRule).toBeDefined();
|
|
131
|
-
expect(trustedRule?.priority).toBe(
|
|
159
|
+
expect(trustedRule?.priority).toBe(2.2); // MCP trusted server
|
|
132
160
|
// Untrusted server should not have an allow rule
|
|
133
161
|
const untrustedRule = config.rules?.find((r) => r.toolName === 'untrusted-server__*' &&
|
|
134
162
|
r.decision === PolicyDecision.ALLOW);
|
|
135
163
|
expect(untrustedRule).toBeUndefined();
|
|
136
164
|
});
|
|
137
|
-
it('should handle multiple MCP server configurations together', () => {
|
|
165
|
+
it('should handle multiple MCP server configurations together', async () => {
|
|
166
|
+
const { createPolicyEngineConfig } = await import('./policy.js');
|
|
138
167
|
const settings = {
|
|
139
168
|
mcp: {
|
|
140
169
|
allowed: ['allowed-server'],
|
|
@@ -148,41 +177,47 @@ describe('createPolicyEngineConfig', () => {
|
|
|
148
177
|
},
|
|
149
178
|
},
|
|
150
179
|
};
|
|
151
|
-
const config = createPolicyEngineConfig(settings, ApprovalMode.DEFAULT);
|
|
180
|
+
const config = await createPolicyEngineConfig(settings, ApprovalMode.DEFAULT);
|
|
152
181
|
// Check allowed server
|
|
153
182
|
const allowedRule = config.rules?.find((r) => r.toolName === 'allowed-server__*' &&
|
|
154
183
|
r.decision === PolicyDecision.ALLOW);
|
|
155
184
|
expect(allowedRule).toBeDefined();
|
|
156
|
-
expect(allowedRule?.priority).toBe(
|
|
185
|
+
expect(allowedRule?.priority).toBe(2.1); // MCP allowed server
|
|
157
186
|
// Check trusted server
|
|
158
187
|
const trustedRule = config.rules?.find((r) => r.toolName === 'trusted-server__*' &&
|
|
159
188
|
r.decision === PolicyDecision.ALLOW);
|
|
160
189
|
expect(trustedRule).toBeDefined();
|
|
161
|
-
expect(trustedRule?.priority).toBe(
|
|
190
|
+
expect(trustedRule?.priority).toBe(2.2); // MCP trusted server
|
|
162
191
|
// Check excluded server
|
|
163
192
|
const excludedRule = config.rules?.find((r) => r.toolName === 'excluded-server__*' &&
|
|
164
193
|
r.decision === PolicyDecision.DENY);
|
|
165
194
|
expect(excludedRule).toBeDefined();
|
|
166
|
-
expect(excludedRule?.priority).toBe(
|
|
195
|
+
expect(excludedRule?.priority).toBe(2.9); // MCP excluded server
|
|
167
196
|
});
|
|
168
|
-
it('should allow all tools in YOLO mode', () => {
|
|
197
|
+
it('should allow all tools in YOLO mode', async () => {
|
|
198
|
+
const { createPolicyEngineConfig } = await import('./policy.js');
|
|
169
199
|
const settings = {};
|
|
170
|
-
const config = createPolicyEngineConfig(settings, ApprovalMode.YOLO);
|
|
171
|
-
const rule = config.rules?.find((r) => r.decision === PolicyDecision.ALLOW && r.
|
|
200
|
+
const config = await createPolicyEngineConfig(settings, ApprovalMode.YOLO);
|
|
201
|
+
const rule = config.rules?.find((r) => r.decision === PolicyDecision.ALLOW && !r.toolName);
|
|
172
202
|
expect(rule).toBeDefined();
|
|
203
|
+
// Priority 999 in default tier → 1.999
|
|
204
|
+
expect(rule?.priority).toBeCloseTo(1.999, 5);
|
|
173
205
|
});
|
|
174
|
-
it('should allow edit tool in AUTO_EDIT mode', () => {
|
|
206
|
+
it('should allow edit tool in AUTO_EDIT mode', async () => {
|
|
207
|
+
const { createPolicyEngineConfig } = await import('./policy.js');
|
|
175
208
|
const settings = {};
|
|
176
|
-
const config = createPolicyEngineConfig(settings, ApprovalMode.AUTO_EDIT);
|
|
209
|
+
const config = await createPolicyEngineConfig(settings, ApprovalMode.AUTO_EDIT);
|
|
177
210
|
const rule = config.rules?.find((r) => r.toolName === 'replace' && r.decision === PolicyDecision.ALLOW);
|
|
178
211
|
expect(rule).toBeDefined();
|
|
179
|
-
|
|
212
|
+
// Priority 15 in default tier → 1.015
|
|
213
|
+
expect(rule?.priority).toBeCloseTo(1.015, 5);
|
|
180
214
|
});
|
|
181
|
-
it('should prioritize exclude over allow', () => {
|
|
215
|
+
it('should prioritize exclude over allow', async () => {
|
|
216
|
+
const { createPolicyEngineConfig } = await import('./policy.js');
|
|
182
217
|
const settings = {
|
|
183
218
|
tools: { allowed: ['run_shell_command'], exclude: ['run_shell_command'] },
|
|
184
219
|
};
|
|
185
|
-
const config = createPolicyEngineConfig(settings, ApprovalMode.DEFAULT);
|
|
220
|
+
const config = await createPolicyEngineConfig(settings, ApprovalMode.DEFAULT);
|
|
186
221
|
const denyRule = config.rules?.find((r) => r.toolName === 'run_shell_command' &&
|
|
187
222
|
r.decision === PolicyDecision.DENY);
|
|
188
223
|
const allowRule = config.rules?.find((r) => r.toolName === 'run_shell_command' &&
|
|
@@ -191,23 +226,25 @@ describe('createPolicyEngineConfig', () => {
|
|
|
191
226
|
expect(allowRule).toBeDefined();
|
|
192
227
|
expect(denyRule.priority).toBeGreaterThan(allowRule.priority);
|
|
193
228
|
});
|
|
194
|
-
it('should prioritize specific tool allows over MCP server excludes', () => {
|
|
229
|
+
it('should prioritize specific tool allows over MCP server excludes', async () => {
|
|
230
|
+
const { createPolicyEngineConfig } = await import('./policy.js');
|
|
195
231
|
const settings = {
|
|
196
232
|
mcp: { excluded: ['my-server'] },
|
|
197
233
|
tools: { allowed: ['my-server__specific-tool'] },
|
|
198
234
|
};
|
|
199
|
-
const config = createPolicyEngineConfig(settings, ApprovalMode.DEFAULT);
|
|
235
|
+
const config = await createPolicyEngineConfig(settings, ApprovalMode.DEFAULT);
|
|
200
236
|
const serverDenyRule = config.rules?.find((r) => r.toolName === 'my-server__*' && r.decision === PolicyDecision.DENY);
|
|
201
237
|
const toolAllowRule = config.rules?.find((r) => r.toolName === 'my-server__specific-tool' &&
|
|
202
238
|
r.decision === PolicyDecision.ALLOW);
|
|
203
239
|
expect(serverDenyRule).toBeDefined();
|
|
204
|
-
expect(serverDenyRule?.priority).toBe(
|
|
240
|
+
expect(serverDenyRule?.priority).toBe(2.9); // MCP excluded server
|
|
205
241
|
expect(toolAllowRule).toBeDefined();
|
|
206
|
-
expect(toolAllowRule?.priority).
|
|
207
|
-
//
|
|
208
|
-
// so server deny wins -
|
|
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)
|
|
209
245
|
});
|
|
210
|
-
it('should
|
|
246
|
+
it('should handle MCP server allows and tool excludes', async () => {
|
|
247
|
+
const { createPolicyEngineConfig } = await import('./policy.js');
|
|
211
248
|
const settings = {
|
|
212
249
|
mcp: { allowed: ['my-server'] },
|
|
213
250
|
mcpServers: {
|
|
@@ -219,24 +256,27 @@ describe('createPolicyEngineConfig', () => {
|
|
|
219
256
|
},
|
|
220
257
|
tools: { exclude: ['my-server__dangerous-tool'] },
|
|
221
258
|
};
|
|
222
|
-
const config = createPolicyEngineConfig(settings, ApprovalMode.DEFAULT);
|
|
259
|
+
const config = await createPolicyEngineConfig(settings, ApprovalMode.DEFAULT);
|
|
223
260
|
const serverAllowRule = config.rules?.find((r) => r.toolName === 'my-server__*' && r.decision === PolicyDecision.ALLOW);
|
|
224
261
|
const toolDenyRule = config.rules?.find((r) => r.toolName === 'my-server__dangerous-tool' &&
|
|
225
262
|
r.decision === PolicyDecision.DENY);
|
|
226
263
|
expect(serverAllowRule).toBeDefined();
|
|
227
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
|
|
228
267
|
expect(toolDenyRule.priority).toBeGreaterThan(serverAllowRule.priority);
|
|
229
268
|
});
|
|
230
|
-
it('should handle complex priority scenarios correctly', () => {
|
|
269
|
+
it('should handle complex priority scenarios correctly', async () => {
|
|
270
|
+
const { createPolicyEngineConfig } = await import('./policy.js');
|
|
231
271
|
const settings = {
|
|
232
272
|
tools: {
|
|
233
|
-
autoAccept: true, //
|
|
234
|
-
allowed: ['my-server__tool1', 'other-tool'], // Priority
|
|
235
|
-
exclude: ['my-server__tool2', 'glob'], // Priority
|
|
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
|
|
236
276
|
},
|
|
237
277
|
mcp: {
|
|
238
|
-
allowed: ['allowed-server'], // Priority
|
|
239
|
-
excluded: ['excluded-server'], // Priority
|
|
278
|
+
allowed: ['allowed-server'], // Priority 2.1
|
|
279
|
+
excluded: ['excluded-server'], // Priority 2.9
|
|
240
280
|
},
|
|
241
281
|
mcpServers: {
|
|
242
282
|
'trusted-server': {
|
|
@@ -246,14 +286,16 @@ describe('createPolicyEngineConfig', () => {
|
|
|
246
286
|
},
|
|
247
287
|
},
|
|
248
288
|
};
|
|
249
|
-
const config = createPolicyEngineConfig(settings, ApprovalMode.DEFAULT);
|
|
289
|
+
const config = await createPolicyEngineConfig(settings, ApprovalMode.DEFAULT);
|
|
250
290
|
// Verify glob is denied even though autoAccept would allow it
|
|
251
291
|
const globDenyRule = config.rules?.find((r) => r.toolName === 'glob' && r.decision === PolicyDecision.DENY);
|
|
252
292
|
const globAllowRule = config.rules?.find((r) => r.toolName === 'glob' && r.decision === PolicyDecision.ALLOW);
|
|
253
293
|
expect(globDenyRule).toBeDefined();
|
|
254
294
|
expect(globAllowRule).toBeDefined();
|
|
255
|
-
|
|
256
|
-
expect(
|
|
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);
|
|
257
299
|
// Verify all priority levels are correct
|
|
258
300
|
const priorities = config.rules
|
|
259
301
|
?.map((r) => ({
|
|
@@ -262,11 +304,12 @@ describe('createPolicyEngineConfig', () => {
|
|
|
262
304
|
priority: r.priority,
|
|
263
305
|
}))
|
|
264
306
|
.sort((a, b) => (b.priority ?? 0) - (a.priority ?? 0));
|
|
265
|
-
// Check that the highest priority items are the excludes
|
|
266
|
-
const highestPriorityExcludes = priorities?.filter((p) => p.priority
|
|
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);
|
|
267
309
|
expect(highestPriorityExcludes?.every((p) => p.decision === PolicyDecision.DENY)).toBe(true);
|
|
268
310
|
});
|
|
269
|
-
it('should handle MCP servers with undefined trust property', () => {
|
|
311
|
+
it('should handle MCP servers with undefined trust property', async () => {
|
|
312
|
+
const { createPolicyEngineConfig } = await import('./policy.js');
|
|
270
313
|
const settings = {
|
|
271
314
|
mcpServers: {
|
|
272
315
|
'no-trust-property': {
|
|
@@ -281,7 +324,7 @@ describe('createPolicyEngineConfig', () => {
|
|
|
281
324
|
},
|
|
282
325
|
},
|
|
283
326
|
};
|
|
284
|
-
const config = createPolicyEngineConfig(settings, ApprovalMode.DEFAULT);
|
|
327
|
+
const config = await createPolicyEngineConfig(settings, ApprovalMode.DEFAULT);
|
|
285
328
|
// Neither server should have an allow rule
|
|
286
329
|
const noTrustRule = config.rules?.find((r) => r.toolName === 'no-trust-property__*' &&
|
|
287
330
|
r.decision === PolicyDecision.ALLOW);
|
|
@@ -290,15 +333,18 @@ describe('createPolicyEngineConfig', () => {
|
|
|
290
333
|
expect(noTrustRule).toBeUndefined();
|
|
291
334
|
expect(explicitFalseRule).toBeUndefined();
|
|
292
335
|
});
|
|
293
|
-
it('should
|
|
336
|
+
it('should have YOLO allow-all rule beat write tool rules in YOLO mode', async () => {
|
|
337
|
+
const { createPolicyEngineConfig } = await import('./policy.js');
|
|
294
338
|
const settings = {
|
|
295
339
|
tools: { exclude: ['dangerous-tool'] },
|
|
296
340
|
};
|
|
297
|
-
const config = createPolicyEngineConfig(settings, ApprovalMode.YOLO);
|
|
298
|
-
// Should have the wildcard allow rule
|
|
299
|
-
const wildcardRule = config.rules?.find((r) => !r.toolName && r.decision === PolicyDecision.ALLOW
|
|
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);
|
|
300
344
|
expect(wildcardRule).toBeDefined();
|
|
301
|
-
//
|
|
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)
|
|
302
348
|
const writeToolRules = config.rules?.filter((r) => [
|
|
303
349
|
'replace',
|
|
304
350
|
'save_memory',
|
|
@@ -306,13 +352,18 @@ describe('createPolicyEngineConfig', () => {
|
|
|
306
352
|
'write_file',
|
|
307
353
|
WEB_FETCH_TOOL_NAME,
|
|
308
354
|
].includes(r.toolName || '') && r.decision === PolicyDecision.ASK_USER);
|
|
309
|
-
expect(writeToolRules).
|
|
310
|
-
//
|
|
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)
|
|
311
361
|
const excludeRule = config.rules?.find((r) => r.toolName === 'dangerous-tool' && r.decision === PolicyDecision.DENY);
|
|
312
362
|
expect(excludeRule).toBeDefined();
|
|
313
|
-
expect(excludeRule?.priority).
|
|
363
|
+
expect(excludeRule?.priority).toBeCloseTo(2.4, 5); // Command line exclude
|
|
314
364
|
});
|
|
315
|
-
it('should handle combination of trusted server and excluded server for same name', () => {
|
|
365
|
+
it('should handle combination of trusted server and excluded server for same name', async () => {
|
|
366
|
+
const { createPolicyEngineConfig } = await import('./policy.js');
|
|
316
367
|
const settings = {
|
|
317
368
|
mcpServers: {
|
|
318
369
|
'conflicted-server': {
|
|
@@ -325,36 +376,683 @@ describe('createPolicyEngineConfig', () => {
|
|
|
325
376
|
excluded: ['conflicted-server'], // Priority 195
|
|
326
377
|
},
|
|
327
378
|
};
|
|
328
|
-
const config = createPolicyEngineConfig(settings, ApprovalMode.DEFAULT);
|
|
379
|
+
const config = await createPolicyEngineConfig(settings, ApprovalMode.DEFAULT);
|
|
329
380
|
// Both rules should exist
|
|
330
381
|
const trustRule = config.rules?.find((r) => r.toolName === 'conflicted-server__*' &&
|
|
331
382
|
r.decision === PolicyDecision.ALLOW);
|
|
332
383
|
const excludeRule = config.rules?.find((r) => r.toolName === 'conflicted-server__*' &&
|
|
333
384
|
r.decision === PolicyDecision.DENY);
|
|
334
385
|
expect(trustRule).toBeDefined();
|
|
335
|
-
expect(trustRule?.priority).toBe(
|
|
386
|
+
expect(trustRule?.priority).toBe(2.2); // MCP trusted server
|
|
336
387
|
expect(excludeRule).toBeDefined();
|
|
337
|
-
expect(excludeRule?.priority).toBe(
|
|
388
|
+
expect(excludeRule?.priority).toBe(2.9); // MCP excluded server
|
|
338
389
|
// Exclude (195) should win over trust (90) when evaluated
|
|
339
390
|
});
|
|
340
|
-
it('should handle all approval modes correctly', () => {
|
|
391
|
+
it('should handle all approval modes correctly', async () => {
|
|
392
|
+
const { createPolicyEngineConfig } = await import('./policy.js');
|
|
341
393
|
const settings = {};
|
|
342
394
|
// Test DEFAULT mode
|
|
343
|
-
const defaultConfig = createPolicyEngineConfig(settings, ApprovalMode.DEFAULT);
|
|
395
|
+
const defaultConfig = await createPolicyEngineConfig(settings, ApprovalMode.DEFAULT);
|
|
344
396
|
expect(defaultConfig.defaultDecision).toBe(PolicyDecision.ASK_USER);
|
|
345
397
|
expect(defaultConfig.rules?.find((r) => !r.toolName && r.decision === PolicyDecision.ALLOW)).toBeUndefined();
|
|
346
398
|
// Test YOLO mode
|
|
347
|
-
const yoloConfig = createPolicyEngineConfig(settings, ApprovalMode.YOLO);
|
|
399
|
+
const yoloConfig = await createPolicyEngineConfig(settings, ApprovalMode.YOLO);
|
|
348
400
|
expect(yoloConfig.defaultDecision).toBe(PolicyDecision.ASK_USER);
|
|
349
401
|
const yoloWildcard = yoloConfig.rules?.find((r) => !r.toolName && r.decision === PolicyDecision.ALLOW);
|
|
350
402
|
expect(yoloWildcard).toBeDefined();
|
|
351
|
-
|
|
403
|
+
// Priority 999 in default tier → 1.999
|
|
404
|
+
expect(yoloWildcard?.priority).toBeCloseTo(1.999, 5);
|
|
352
405
|
// Test AUTO_EDIT mode
|
|
353
|
-
const autoEditConfig = createPolicyEngineConfig(settings, ApprovalMode.AUTO_EDIT);
|
|
406
|
+
const autoEditConfig = await createPolicyEngineConfig(settings, ApprovalMode.AUTO_EDIT);
|
|
354
407
|
expect(autoEditConfig.defaultDecision).toBe(PolicyDecision.ASK_USER);
|
|
355
408
|
const editRule = autoEditConfig.rules?.find((r) => r.toolName === 'replace' && r.decision === PolicyDecision.ALLOW);
|
|
356
409
|
expect(editRule).toBeDefined();
|
|
357
|
-
|
|
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');
|
|
358
1056
|
});
|
|
359
1057
|
});
|
|
360
1058
|
//# sourceMappingURL=policy.test.js.map
|