@google/gemini-cli-core 0.15.0-preview.3 → 0.16.0-nightly.20251113.ad1f0d99
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/google-gemini-cli-core-0.16.0-nightly.20251112.c961f274.tgz +0 -0
- package/dist/src/agents/codebase-investigator.test.d.ts +6 -0
- package/dist/src/agents/codebase-investigator.test.js +35 -0
- package/dist/src/agents/codebase-investigator.test.js.map +1 -0
- package/dist/src/agents/executor.test.js +181 -1
- package/dist/src/agents/executor.test.js.map +1 -1
- package/dist/src/code_assist/codeAssist.test.d.ts +6 -0
- package/dist/src/code_assist/codeAssist.test.js +99 -0
- package/dist/src/code_assist/codeAssist.test.js.map +1 -0
- package/dist/src/code_assist/experiments/client_metadata.js +2 -1
- package/dist/src/code_assist/experiments/client_metadata.js.map +1 -1
- package/dist/src/code_assist/experiments/client_metadata.test.d.ts +6 -0
- package/dist/src/code_assist/experiments/client_metadata.test.js +99 -0
- package/dist/src/code_assist/experiments/client_metadata.test.js.map +1 -0
- package/dist/src/code_assist/experiments/experiments.test.d.ts +6 -0
- package/dist/src/code_assist/experiments/experiments.test.js +92 -0
- package/dist/src/code_assist/experiments/experiments.test.js.map +1 -0
- package/dist/src/code_assist/oauth-credential-storage.test.js +49 -0
- package/dist/src/code_assist/oauth-credential-storage.test.js.map +1 -1
- package/dist/src/code_assist/server.js +5 -8
- package/dist/src/code_assist/server.js.map +1 -1
- package/dist/src/code_assist/server.test.js +109 -28
- package/dist/src/code_assist/server.test.js.map +1 -1
- package/dist/src/config/defaultModelConfigs.js +6 -0
- package/dist/src/config/defaultModelConfigs.js.map +1 -1
- package/dist/src/confirmation-bus/message-bus.d.ts +1 -1
- package/dist/src/confirmation-bus/message-bus.js +2 -2
- package/dist/src/confirmation-bus/message-bus.js.map +1 -1
- package/dist/src/confirmation-bus/message-bus.test.js +30 -24
- package/dist/src/confirmation-bus/message-bus.test.js.map +1 -1
- package/dist/src/core/loggingContentGenerator.test.d.ts +6 -0
- package/dist/src/core/loggingContentGenerator.test.js +180 -0
- package/dist/src/core/loggingContentGenerator.test.js.map +1 -0
- package/dist/src/core/tokenLimits.test.d.ts +6 -0
- package/dist/src/core/tokenLimits.test.js +26 -0
- package/dist/src/core/tokenLimits.test.js.map +1 -0
- 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/hooks/hookAggregator.d.ts +68 -0
- package/dist/src/hooks/hookAggregator.js +262 -0
- package/dist/src/hooks/hookAggregator.js.map +1 -0
- package/dist/src/hooks/hookAggregator.test.d.ts +6 -0
- package/dist/src/hooks/hookAggregator.test.js +387 -0
- package/dist/src/hooks/hookAggregator.test.js.map +1 -0
- package/dist/src/hooks/types.js +1 -1
- package/dist/src/hooks/types.js.map +1 -1
- package/dist/src/hooks/types.test.js +280 -2
- package/dist/src/hooks/types.test.js.map +1 -1
- package/dist/src/ide/ide-client.test.js +159 -0
- package/dist/src/ide/ide-client.test.js.map +1 -1
- package/dist/src/mcp/oauth-provider.test.js +177 -0
- package/dist/src/mcp/oauth-provider.test.js.map +1 -1
- package/dist/src/policy/config.js +3 -1
- package/dist/src/policy/config.js.map +1 -1
- package/dist/src/policy/config.test.js +118 -1
- package/dist/src/policy/config.test.js.map +1 -1
- package/dist/src/policy/policies/write.toml +10 -0
- package/dist/src/policy/policy-engine.d.ts +12 -3
- package/dist/src/policy/policy-engine.js +61 -7
- package/dist/src/policy/policy-engine.js.map +1 -1
- package/dist/src/policy/policy-engine.test.js +422 -86
- package/dist/src/policy/policy-engine.test.js.map +1 -1
- package/dist/src/policy/toml-loader.d.ts +2 -1
- package/dist/src/policy/toml-loader.js +103 -6
- package/dist/src/policy/toml-loader.js.map +1 -1
- package/dist/src/policy/toml-loader.test.js +32 -88
- package/dist/src/policy/toml-loader.test.js.map +1 -1
- package/dist/src/policy/types.d.ts +65 -0
- package/dist/src/policy/types.js +4 -0
- package/dist/src/policy/types.js.map +1 -1
- package/dist/src/prompts/mcp-prompts.test.d.ts +6 -0
- package/dist/src/prompts/mcp-prompts.test.js +40 -0
- package/dist/src/prompts/mcp-prompts.test.js.map +1 -0
- package/dist/src/prompts/prompt-registry.test.d.ts +6 -0
- package/dist/src/prompts/prompt-registry.test.js +111 -0
- package/dist/src/prompts/prompt-registry.test.js.map +1 -0
- package/dist/src/safety/built-in.d.ts +21 -0
- package/dist/src/safety/built-in.js +106 -0
- package/dist/src/safety/built-in.js.map +1 -0
- package/dist/src/safety/built-in.test.d.ts +6 -0
- package/dist/src/safety/built-in.test.js +199 -0
- package/dist/src/safety/built-in.test.js.map +1 -0
- package/dist/src/safety/checker-runner.d.ts +48 -0
- package/dist/src/safety/checker-runner.js +208 -0
- package/dist/src/safety/checker-runner.js.map +1 -0
- package/dist/src/safety/checker-runner.test.d.ts +6 -0
- package/dist/src/safety/checker-runner.test.js +238 -0
- package/dist/src/safety/checker-runner.test.js.map +1 -0
- package/dist/src/safety/context-builder.d.ts +23 -0
- package/dist/src/safety/context-builder.js +47 -0
- package/dist/src/safety/context-builder.js.map +1 -0
- package/dist/src/safety/context-builder.test.d.ts +6 -0
- package/dist/src/safety/context-builder.test.js +49 -0
- package/dist/src/safety/context-builder.test.js.map +1 -0
- package/dist/src/safety/protocol.d.ts +88 -0
- package/dist/src/safety/protocol.js +15 -0
- package/dist/src/safety/protocol.js.map +1 -0
- package/dist/src/safety/registry.d.ts +26 -0
- package/dist/src/safety/registry.js +65 -0
- package/dist/src/safety/registry.js.map +1 -0
- package/dist/src/safety/registry.test.d.ts +6 -0
- package/dist/src/safety/registry.test.js +31 -0
- package/dist/src/safety/registry.test.js.map +1 -0
- package/dist/src/services/loopDetectionService.d.ts +3 -0
- package/dist/src/services/loopDetectionService.js +81 -41
- package/dist/src/services/loopDetectionService.js.map +1 -1
- package/dist/src/services/loopDetectionService.test.js +96 -4
- package/dist/src/services/loopDetectionService.test.js.map +1 -1
- package/dist/src/services/test-data/resolved-aliases.golden.json +7 -0
- package/dist/src/telemetry/clearcut-logger/clearcut-logger.d.ts +4 -2
- package/dist/src/telemetry/clearcut-logger/clearcut-logger.js +29 -0
- package/dist/src/telemetry/clearcut-logger/clearcut-logger.js.map +1 -1
- package/dist/src/telemetry/clearcut-logger/clearcut-logger.test.js +31 -0
- package/dist/src/telemetry/clearcut-logger/clearcut-logger.test.js.map +1 -1
- package/dist/src/telemetry/clearcut-logger/event-metadata-key.d.ts +5 -1
- package/dist/src/telemetry/clearcut-logger/event-metadata-key.js +12 -1
- package/dist/src/telemetry/clearcut-logger/event-metadata-key.js.map +1 -1
- package/dist/src/telemetry/loggers.d.ts +2 -1
- package/dist/src/telemetry/loggers.js +11 -0
- package/dist/src/telemetry/loggers.js.map +1 -1
- package/dist/src/telemetry/types.d.ts +15 -2
- package/dist/src/telemetry/types.js +42 -3
- package/dist/src/telemetry/types.js.map +1 -1
- package/dist/src/tools/base-tool-invocation.test.js +2 -2
- package/dist/src/tools/base-tool-invocation.test.js.map +1 -1
- package/dist/src/tools/memoryTool.js +1 -1
- package/dist/src/tools/memoryTool.js.map +1 -1
- package/dist/src/tools/memoryTool.test.js +1 -1
- package/dist/src/tools/memoryTool.test.js.map +1 -1
- package/dist/src/tools/ripGrep.d.ts +24 -7
- package/dist/src/tools/ripGrep.js +125 -145
- package/dist/src/tools/ripGrep.js.map +1 -1
- package/dist/src/tools/ripGrep.test.js +144 -20
- package/dist/src/tools/ripGrep.test.js.map +1 -1
- package/dist/src/tools/tools.js +1 -1
- package/dist/src/tools/tools.js.map +1 -1
- package/dist/src/tools/write-todos.js +1 -1
- package/dist/src/tools/write-todos.js.map +1 -1
- package/dist/src/utils/llm-edit-fixer.test.js +8 -1
- package/dist/src/utils/llm-edit-fixer.test.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +1 -1
- package/dist/google-gemini-cli-core-0.15.0-preview.2.tgz +0 -0
|
@@ -3,22 +3,27 @@
|
|
|
3
3
|
* Copyright 2025 Google LLC
|
|
4
4
|
* SPDX-License-Identifier: Apache-2.0
|
|
5
5
|
*/
|
|
6
|
-
import { describe, it, expect, beforeEach } from 'vitest';
|
|
6
|
+
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
|
7
7
|
import { PolicyEngine } from './policy-engine.js';
|
|
8
|
-
import { PolicyDecision, } from './types.js';
|
|
8
|
+
import { PolicyDecision, InProcessCheckerType, } from './types.js';
|
|
9
|
+
import { SafetyCheckDecision } from '../safety/protocol.js';
|
|
9
10
|
describe('PolicyEngine', () => {
|
|
10
11
|
let engine;
|
|
12
|
+
let mockCheckerRunner;
|
|
11
13
|
beforeEach(() => {
|
|
12
|
-
|
|
14
|
+
mockCheckerRunner = {
|
|
15
|
+
runChecker: vi.fn(),
|
|
16
|
+
};
|
|
17
|
+
engine = new PolicyEngine({}, mockCheckerRunner);
|
|
13
18
|
});
|
|
14
19
|
describe('constructor', () => {
|
|
15
|
-
it('should use default config when none provided', () => {
|
|
16
|
-
const decision = engine.check({ name: 'test' }, undefined);
|
|
20
|
+
it('should use default config when none provided', async () => {
|
|
21
|
+
const { decision } = await engine.check({ name: 'test' }, undefined);
|
|
17
22
|
expect(decision).toBe(PolicyDecision.ASK_USER);
|
|
18
23
|
});
|
|
19
|
-
it('should respect custom default decision', () => {
|
|
24
|
+
it('should respect custom default decision', async () => {
|
|
20
25
|
engine = new PolicyEngine({ defaultDecision: PolicyDecision.DENY });
|
|
21
|
-
const decision = engine.check({ name: 'test' }, undefined);
|
|
26
|
+
const { decision } = await engine.check({ name: 'test' }, undefined);
|
|
22
27
|
expect(decision).toBe(PolicyDecision.DENY);
|
|
23
28
|
});
|
|
24
29
|
it('should sort rules by priority', () => {
|
|
@@ -35,17 +40,17 @@ describe('PolicyEngine', () => {
|
|
|
35
40
|
});
|
|
36
41
|
});
|
|
37
42
|
describe('check', () => {
|
|
38
|
-
it('should match tool by name', () => {
|
|
43
|
+
it('should match tool by name', async () => {
|
|
39
44
|
const rules = [
|
|
40
45
|
{ toolName: 'shell', decision: PolicyDecision.ALLOW },
|
|
41
46
|
{ toolName: 'edit', decision: PolicyDecision.DENY },
|
|
42
47
|
];
|
|
43
48
|
engine = new PolicyEngine({ rules });
|
|
44
|
-
expect(engine.check({ name: 'shell' }, undefined)).toBe(PolicyDecision.ALLOW);
|
|
45
|
-
expect(engine.check({ name: 'edit' }, undefined)).toBe(PolicyDecision.DENY);
|
|
46
|
-
expect(engine.check({ name: 'other' }, undefined)).toBe(PolicyDecision.ASK_USER);
|
|
49
|
+
expect((await engine.check({ name: 'shell' }, undefined)).decision).toBe(PolicyDecision.ALLOW);
|
|
50
|
+
expect((await engine.check({ name: 'edit' }, undefined)).decision).toBe(PolicyDecision.DENY);
|
|
51
|
+
expect((await engine.check({ name: 'other' }, undefined)).decision).toBe(PolicyDecision.ASK_USER);
|
|
47
52
|
});
|
|
48
|
-
it('should match by args pattern', () => {
|
|
53
|
+
it('should match by args pattern', async () => {
|
|
49
54
|
const rules = [
|
|
50
55
|
{
|
|
51
56
|
toolName: 'shell',
|
|
@@ -66,28 +71,28 @@ describe('PolicyEngine', () => {
|
|
|
66
71
|
name: 'shell',
|
|
67
72
|
args: { command: 'ls -la' },
|
|
68
73
|
};
|
|
69
|
-
expect(engine.check(dangerousCall, undefined)).toBe(PolicyDecision.DENY);
|
|
70
|
-
expect(engine.check(safeCall, undefined)).toBe(PolicyDecision.ALLOW);
|
|
74
|
+
expect((await engine.check(dangerousCall, undefined)).decision).toBe(PolicyDecision.DENY);
|
|
75
|
+
expect((await engine.check(safeCall, undefined)).decision).toBe(PolicyDecision.ALLOW);
|
|
71
76
|
});
|
|
72
|
-
it('should apply rules by priority', () => {
|
|
77
|
+
it('should apply rules by priority', async () => {
|
|
73
78
|
const rules = [
|
|
74
79
|
{ toolName: 'shell', decision: PolicyDecision.DENY, priority: 1 },
|
|
75
80
|
{ toolName: 'shell', decision: PolicyDecision.ALLOW, priority: 10 },
|
|
76
81
|
];
|
|
77
82
|
engine = new PolicyEngine({ rules });
|
|
78
83
|
// Higher priority rule (ALLOW) should win
|
|
79
|
-
expect(engine.check({ name: 'shell' }, undefined)).toBe(PolicyDecision.ALLOW);
|
|
84
|
+
expect((await engine.check({ name: 'shell' }, undefined)).decision).toBe(PolicyDecision.ALLOW);
|
|
80
85
|
});
|
|
81
|
-
it('should apply wildcard rules (no toolName)', () => {
|
|
86
|
+
it('should apply wildcard rules (no toolName)', async () => {
|
|
82
87
|
const rules = [
|
|
83
88
|
{ decision: PolicyDecision.DENY }, // Applies to all tools
|
|
84
89
|
{ toolName: 'safe-tool', decision: PolicyDecision.ALLOW, priority: 10 },
|
|
85
90
|
];
|
|
86
91
|
engine = new PolicyEngine({ rules });
|
|
87
|
-
expect(engine.check({ name: 'safe-tool' }, undefined)).toBe(PolicyDecision.ALLOW);
|
|
88
|
-
expect(engine.check({ name: 'any-other-tool' }, undefined)).toBe(PolicyDecision.DENY);
|
|
92
|
+
expect((await engine.check({ name: 'safe-tool' }, undefined)).decision).toBe(PolicyDecision.ALLOW);
|
|
93
|
+
expect((await engine.check({ name: 'any-other-tool' }, undefined)).decision).toBe(PolicyDecision.DENY);
|
|
89
94
|
});
|
|
90
|
-
it('should handle non-interactive mode', () => {
|
|
95
|
+
it('should handle non-interactive mode', async () => {
|
|
91
96
|
const config = {
|
|
92
97
|
nonInteractive: true,
|
|
93
98
|
rules: [
|
|
@@ -97,11 +102,11 @@ describe('PolicyEngine', () => {
|
|
|
97
102
|
};
|
|
98
103
|
engine = new PolicyEngine(config);
|
|
99
104
|
// ASK_USER should become DENY in non-interactive mode
|
|
100
|
-
expect(engine.check({ name: 'interactive-tool' }, undefined)).toBe(PolicyDecision.DENY);
|
|
105
|
+
expect((await engine.check({ name: 'interactive-tool' }, undefined)).decision).toBe(PolicyDecision.DENY);
|
|
101
106
|
// ALLOW should remain ALLOW
|
|
102
|
-
expect(engine.check({ name: 'allowed-tool' }, undefined)).toBe(PolicyDecision.ALLOW);
|
|
107
|
+
expect((await engine.check({ name: 'allowed-tool' }, undefined)).decision).toBe(PolicyDecision.ALLOW);
|
|
103
108
|
// Default ASK_USER should also become DENY
|
|
104
|
-
expect(engine.check({ name: 'unknown-tool' }, undefined)).toBe(PolicyDecision.DENY);
|
|
109
|
+
expect((await engine.check({ name: 'unknown-tool' }, undefined)).decision).toBe(PolicyDecision.DENY);
|
|
105
110
|
});
|
|
106
111
|
});
|
|
107
112
|
describe('addRule', () => {
|
|
@@ -127,10 +132,10 @@ describe('PolicyEngine', () => {
|
|
|
127
132
|
expect(rules[1].priority).toBe(5);
|
|
128
133
|
expect(rules[2].priority).toBe(1);
|
|
129
134
|
});
|
|
130
|
-
it('should apply newly added rules', () => {
|
|
131
|
-
expect(engine.check({ name: 'new-tool' }, undefined)).toBe(PolicyDecision.ASK_USER);
|
|
135
|
+
it('should apply newly added rules', async () => {
|
|
136
|
+
expect((await engine.check({ name: 'new-tool' }, undefined)).decision).toBe(PolicyDecision.ASK_USER);
|
|
132
137
|
engine.addRule({ toolName: 'new-tool', decision: PolicyDecision.ALLOW });
|
|
133
|
-
expect(engine.check({ name: 'new-tool' }, undefined)).toBe(PolicyDecision.ALLOW);
|
|
138
|
+
expect((await engine.check({ name: 'new-tool' }, undefined)).decision).toBe(PolicyDecision.ALLOW);
|
|
134
139
|
});
|
|
135
140
|
});
|
|
136
141
|
describe('removeRulesForTool', () => {
|
|
@@ -169,7 +174,7 @@ describe('PolicyEngine', () => {
|
|
|
169
174
|
});
|
|
170
175
|
});
|
|
171
176
|
describe('MCP server wildcard patterns', () => {
|
|
172
|
-
it('should match MCP server wildcard patterns', () => {
|
|
177
|
+
it('should match MCP server wildcard patterns', async () => {
|
|
173
178
|
const rules = [
|
|
174
179
|
{
|
|
175
180
|
toolName: 'my-server__*',
|
|
@@ -184,17 +189,21 @@ describe('PolicyEngine', () => {
|
|
|
184
189
|
];
|
|
185
190
|
engine = new PolicyEngine({ rules });
|
|
186
191
|
// Should match my-server tools
|
|
187
|
-
expect(engine.check({ name: 'my-server__tool1' }, undefined)).toBe(PolicyDecision.ALLOW);
|
|
188
|
-
expect(engine.check({ name: 'my-server__another_tool' }, undefined))
|
|
192
|
+
expect((await engine.check({ name: 'my-server__tool1' }, undefined)).decision).toBe(PolicyDecision.ALLOW);
|
|
193
|
+
expect((await engine.check({ name: 'my-server__another_tool' }, undefined))
|
|
194
|
+
.decision).toBe(PolicyDecision.ALLOW);
|
|
189
195
|
// Should match blocked-server tools
|
|
190
|
-
expect(engine.check({ name: 'blocked-server__tool1' }, undefined))
|
|
191
|
-
|
|
196
|
+
expect((await engine.check({ name: 'blocked-server__tool1' }, undefined))
|
|
197
|
+
.decision).toBe(PolicyDecision.DENY);
|
|
198
|
+
expect((await engine.check({ name: 'blocked-server__dangerous' }, undefined))
|
|
199
|
+
.decision).toBe(PolicyDecision.DENY);
|
|
192
200
|
// Should not match other patterns
|
|
193
|
-
expect(engine.check({ name: 'other-server__tool' }, undefined))
|
|
194
|
-
|
|
195
|
-
expect(engine.check({ name: 'my-server' }, undefined)).toBe(PolicyDecision.ASK_USER); // No
|
|
201
|
+
expect((await engine.check({ name: 'other-server__tool' }, undefined))
|
|
202
|
+
.decision).toBe(PolicyDecision.ASK_USER);
|
|
203
|
+
expect((await engine.check({ name: 'my-server-tool' }, undefined)).decision).toBe(PolicyDecision.ASK_USER); // No __ separator
|
|
204
|
+
expect((await engine.check({ name: 'my-server' }, undefined)).decision).toBe(PolicyDecision.ASK_USER); // No tool name
|
|
196
205
|
});
|
|
197
|
-
it('should prioritize specific tool rules over server wildcards', () => {
|
|
206
|
+
it('should prioritize specific tool rules over server wildcards', async () => {
|
|
198
207
|
const rules = [
|
|
199
208
|
{
|
|
200
209
|
toolName: 'my-server__*',
|
|
@@ -209,10 +218,12 @@ describe('PolicyEngine', () => {
|
|
|
209
218
|
];
|
|
210
219
|
engine = new PolicyEngine({ rules });
|
|
211
220
|
// Specific tool deny should override server allow
|
|
212
|
-
expect(engine.check({ name: 'my-server__dangerous-tool' }, undefined))
|
|
213
|
-
|
|
221
|
+
expect((await engine.check({ name: 'my-server__dangerous-tool' }, undefined))
|
|
222
|
+
.decision).toBe(PolicyDecision.DENY);
|
|
223
|
+
expect((await engine.check({ name: 'my-server__safe-tool' }, undefined))
|
|
224
|
+
.decision).toBe(PolicyDecision.ALLOW);
|
|
214
225
|
});
|
|
215
|
-
it('should NOT match spoofed server names when using wildcards', () => {
|
|
226
|
+
it('should NOT match spoofed server names when using wildcards', async () => {
|
|
216
227
|
// Vulnerability: A rule for 'prefix__*' matches 'prefix__suffix__tool'
|
|
217
228
|
// effectively allowing a server named 'prefix__suffix' to spoof 'prefix'.
|
|
218
229
|
const rules = [
|
|
@@ -226,9 +237,10 @@ describe('PolicyEngine', () => {
|
|
|
226
237
|
const spoofedToolCall = { name: 'safe_server__malicious__tool' };
|
|
227
238
|
// CURRENT BEHAVIOR (FIXED): Matches because it starts with 'safe_server__' BUT serverName doesn't match 'safe_server'
|
|
228
239
|
// We expect this to FAIL matching the ALLOW rule, thus falling back to default (ASK_USER)
|
|
229
|
-
expect(engine.check(spoofedToolCall, 'safe_server__malicious'))
|
|
240
|
+
expect((await engine.check(spoofedToolCall, 'safe_server__malicious'))
|
|
241
|
+
.decision).toBe(PolicyDecision.ASK_USER);
|
|
230
242
|
});
|
|
231
|
-
it('should verify tool name prefix even if serverName matches', () => {
|
|
243
|
+
it('should verify tool name prefix even if serverName matches', async () => {
|
|
232
244
|
const rules = [
|
|
233
245
|
{
|
|
234
246
|
toolName: 'safe_server__*',
|
|
@@ -238,9 +250,9 @@ describe('PolicyEngine', () => {
|
|
|
238
250
|
engine = new PolicyEngine({ rules });
|
|
239
251
|
// serverName matches, but tool name does not start with prefix
|
|
240
252
|
const invalidToolCall = { name: 'other_server__tool' };
|
|
241
|
-
expect(engine.check(invalidToolCall, 'safe_server')).toBe(PolicyDecision.ASK_USER);
|
|
253
|
+
expect((await engine.check(invalidToolCall, 'safe_server')).decision).toBe(PolicyDecision.ASK_USER);
|
|
242
254
|
});
|
|
243
|
-
it('should allow when both serverName and tool name prefix match', () => {
|
|
255
|
+
it('should allow when both serverName and tool name prefix match', async () => {
|
|
244
256
|
const rules = [
|
|
245
257
|
{
|
|
246
258
|
toolName: 'safe_server__*',
|
|
@@ -249,11 +261,11 @@ describe('PolicyEngine', () => {
|
|
|
249
261
|
];
|
|
250
262
|
engine = new PolicyEngine({ rules });
|
|
251
263
|
const validToolCall = { name: 'safe_server__tool' };
|
|
252
|
-
expect(engine.check(validToolCall, 'safe_server')).toBe(PolicyDecision.ALLOW);
|
|
264
|
+
expect((await engine.check(validToolCall, 'safe_server')).decision).toBe(PolicyDecision.ALLOW);
|
|
253
265
|
});
|
|
254
266
|
});
|
|
255
267
|
describe('complex scenarios', () => {
|
|
256
|
-
it('should handle multiple matching rules with different priorities', () => {
|
|
268
|
+
it('should handle multiple matching rules with different priorities', async () => {
|
|
257
269
|
const rules = [
|
|
258
270
|
{ decision: PolicyDecision.DENY, priority: 0 }, // Default deny all
|
|
259
271
|
{ toolName: 'shell', decision: PolicyDecision.ASK_USER, priority: 5 },
|
|
@@ -266,13 +278,13 @@ describe('PolicyEngine', () => {
|
|
|
266
278
|
];
|
|
267
279
|
engine = new PolicyEngine({ rules });
|
|
268
280
|
// Matches highest priority rule (ls command)
|
|
269
|
-
expect(engine.check({ name: 'shell', args: { command: 'ls -la' } }, undefined)).toBe(PolicyDecision.ALLOW);
|
|
281
|
+
expect((await engine.check({ name: 'shell', args: { command: 'ls -la' } }, undefined)).decision).toBe(PolicyDecision.ALLOW);
|
|
270
282
|
// Matches middle priority rule (shell without ls)
|
|
271
|
-
expect(engine.check({ name: 'shell', args: { command: 'pwd' } }, undefined)).toBe(PolicyDecision.ASK_USER);
|
|
283
|
+
expect((await engine.check({ name: 'shell', args: { command: 'pwd' } }, undefined)).decision).toBe(PolicyDecision.ASK_USER);
|
|
272
284
|
// Matches lowest priority rule (not shell)
|
|
273
|
-
expect(engine.check({ name: 'edit' }, undefined)).toBe(PolicyDecision.DENY);
|
|
285
|
+
expect((await engine.check({ name: 'edit' }, undefined)).decision).toBe(PolicyDecision.DENY);
|
|
274
286
|
});
|
|
275
|
-
it('should handle tools with no args', () => {
|
|
287
|
+
it('should handle tools with no args', async () => {
|
|
276
288
|
const rules = [
|
|
277
289
|
{
|
|
278
290
|
toolName: 'read',
|
|
@@ -282,13 +294,13 @@ describe('PolicyEngine', () => {
|
|
|
282
294
|
];
|
|
283
295
|
engine = new PolicyEngine({ rules });
|
|
284
296
|
// Tool call without args should not match pattern
|
|
285
|
-
expect(engine.check({ name: 'read' }, undefined)).toBe(PolicyDecision.ASK_USER);
|
|
297
|
+
expect((await engine.check({ name: 'read' }, undefined)).decision).toBe(PolicyDecision.ASK_USER);
|
|
286
298
|
// Tool call with args not matching pattern
|
|
287
|
-
expect(engine.check({ name: 'read', args: { file: 'public.txt' } }, undefined)).toBe(PolicyDecision.ASK_USER);
|
|
299
|
+
expect((await engine.check({ name: 'read', args: { file: 'public.txt' } }, undefined)).decision).toBe(PolicyDecision.ASK_USER);
|
|
288
300
|
// Tool call with args matching pattern
|
|
289
|
-
expect(engine.check({ name: 'read', args: { file: 'secret.txt' } }, undefined)).toBe(PolicyDecision.DENY);
|
|
301
|
+
expect((await engine.check({ name: 'read', args: { file: 'secret.txt' } }, undefined)).decision).toBe(PolicyDecision.DENY);
|
|
290
302
|
});
|
|
291
|
-
it('should match args pattern regardless of property order', () => {
|
|
303
|
+
it('should match args pattern regardless of property order', async () => {
|
|
292
304
|
const rules = [
|
|
293
305
|
{
|
|
294
306
|
toolName: 'shell',
|
|
@@ -301,13 +313,16 @@ describe('PolicyEngine', () => {
|
|
|
301
313
|
// Same args with different property order should both match
|
|
302
314
|
const args1 = { command: 'rm -rf /', path: '/home' };
|
|
303
315
|
const args2 = { path: '/home', command: 'rm -rf /' };
|
|
304
|
-
expect(engine.check({ name: 'shell', args: args1 }, undefined))
|
|
305
|
-
|
|
316
|
+
expect((await engine.check({ name: 'shell', args: args1 }, undefined))
|
|
317
|
+
.decision).toBe(PolicyDecision.DENY);
|
|
318
|
+
expect((await engine.check({ name: 'shell', args: args2 }, undefined))
|
|
319
|
+
.decision).toBe(PolicyDecision.DENY);
|
|
306
320
|
// Verify safe command doesn't match
|
|
307
321
|
const safeArgs = { command: 'ls -la', path: '/home' };
|
|
308
|
-
expect(engine.check({ name: 'shell', args: safeArgs }, undefined))
|
|
322
|
+
expect((await engine.check({ name: 'shell', args: safeArgs }, undefined))
|
|
323
|
+
.decision).toBe(PolicyDecision.ASK_USER);
|
|
309
324
|
});
|
|
310
|
-
it('should handle nested objects in args with stable stringification', () => {
|
|
325
|
+
it('should handle nested objects in args with stable stringification', async () => {
|
|
311
326
|
const rules = [
|
|
312
327
|
{
|
|
313
328
|
toolName: 'api',
|
|
@@ -325,10 +340,10 @@ describe('PolicyEngine', () => {
|
|
|
325
340
|
method: 'POST',
|
|
326
341
|
data: { value: 'secret', sensitive: true },
|
|
327
342
|
};
|
|
328
|
-
expect(engine.check({ name: 'api', args: args1 }, undefined)).toBe(PolicyDecision.DENY);
|
|
329
|
-
expect(engine.check({ name: 'api', args: args2 }, undefined)).toBe(PolicyDecision.DENY);
|
|
343
|
+
expect((await engine.check({ name: 'api', args: args1 }, undefined)).decision).toBe(PolicyDecision.DENY);
|
|
344
|
+
expect((await engine.check({ name: 'api', args: args2 }, undefined)).decision).toBe(PolicyDecision.DENY);
|
|
330
345
|
});
|
|
331
|
-
it('should handle circular references without stack overflow', () => {
|
|
346
|
+
it('should handle circular references without stack overflow', async () => {
|
|
332
347
|
const rules = [
|
|
333
348
|
{
|
|
334
349
|
toolName: 'test',
|
|
@@ -345,14 +360,16 @@ describe('PolicyEngine', () => {
|
|
|
345
360
|
circularArgs.data['self'] =
|
|
346
361
|
circularArgs.data;
|
|
347
362
|
// Should not throw stack overflow error
|
|
348
|
-
expect(
|
|
363
|
+
await expect(engine.check({ name: 'test', args: circularArgs }, undefined)).resolves.not.toThrow();
|
|
349
364
|
// Should detect the circular reference pattern
|
|
350
|
-
expect(engine.check({ name: 'test', args: circularArgs }, undefined))
|
|
365
|
+
expect((await engine.check({ name: 'test', args: circularArgs }, undefined))
|
|
366
|
+
.decision).toBe(PolicyDecision.DENY);
|
|
351
367
|
// Non-circular object should not match
|
|
352
368
|
const normalArgs = { name: 'test', data: { value: 'normal' } };
|
|
353
|
-
expect(engine.check({ name: 'test', args: normalArgs }, undefined))
|
|
369
|
+
expect((await engine.check({ name: 'test', args: normalArgs }, undefined))
|
|
370
|
+
.decision).toBe(PolicyDecision.ASK_USER);
|
|
354
371
|
});
|
|
355
|
-
it('should handle deep circular references', () => {
|
|
372
|
+
it('should handle deep circular references', async () => {
|
|
356
373
|
const rules = [
|
|
357
374
|
{
|
|
358
375
|
toolName: 'deep',
|
|
@@ -372,11 +389,12 @@ describe('PolicyEngine', () => {
|
|
|
372
389
|
const level3 = deepCircular.level1.level2.level3;
|
|
373
390
|
level3['back'] = deepCircular.level1;
|
|
374
391
|
// Should handle without stack overflow
|
|
375
|
-
expect(
|
|
392
|
+
await expect(engine.check({ name: 'deep', args: deepCircular }, undefined)).resolves.not.toThrow();
|
|
376
393
|
// Should detect the circular reference
|
|
377
|
-
expect(engine.check({ name: 'deep', args: deepCircular }, undefined))
|
|
394
|
+
expect((await engine.check({ name: 'deep', args: deepCircular }, undefined))
|
|
395
|
+
.decision).toBe(PolicyDecision.DENY);
|
|
378
396
|
});
|
|
379
|
-
it('should handle repeated non-circular objects correctly', () => {
|
|
397
|
+
it('should handle repeated non-circular objects correctly', async () => {
|
|
380
398
|
const rules = [
|
|
381
399
|
{
|
|
382
400
|
toolName: 'test',
|
|
@@ -399,9 +417,9 @@ describe('PolicyEngine', () => {
|
|
|
399
417
|
third: { nested: sharedObj },
|
|
400
418
|
};
|
|
401
419
|
// Should NOT mark repeated objects as circular, and should match the shared value pattern
|
|
402
|
-
expect(engine.check({ name: 'test', args }, undefined)).toBe(PolicyDecision.ALLOW);
|
|
420
|
+
expect((await engine.check({ name: 'test', args }, undefined)).decision).toBe(PolicyDecision.ALLOW);
|
|
403
421
|
});
|
|
404
|
-
it('should omit undefined and function values from objects', () => {
|
|
422
|
+
it('should omit undefined and function values from objects', async () => {
|
|
405
423
|
const rules = [
|
|
406
424
|
{
|
|
407
425
|
toolName: 'test',
|
|
@@ -417,7 +435,7 @@ describe('PolicyEngine', () => {
|
|
|
417
435
|
nullValue: null,
|
|
418
436
|
};
|
|
419
437
|
// Should match pattern with defined value, undefined and functions omitted
|
|
420
|
-
expect(engine.check({ name: 'test', args }, undefined)).toBe(PolicyDecision.ALLOW);
|
|
438
|
+
expect((await engine.check({ name: 'test', args }, undefined)).decision).toBe(PolicyDecision.ALLOW);
|
|
421
439
|
// Check that the pattern would NOT match if undefined was included
|
|
422
440
|
const rulesWithUndefined = [
|
|
423
441
|
{
|
|
@@ -427,7 +445,7 @@ describe('PolicyEngine', () => {
|
|
|
427
445
|
},
|
|
428
446
|
];
|
|
429
447
|
engine = new PolicyEngine({ rules: rulesWithUndefined });
|
|
430
|
-
expect(engine.check({ name: 'test', args }, undefined)).toBe(PolicyDecision.ASK_USER);
|
|
448
|
+
expect((await engine.check({ name: 'test', args }, undefined)).decision).toBe(PolicyDecision.ASK_USER);
|
|
431
449
|
// Check that the pattern would NOT match if function was included
|
|
432
450
|
const rulesWithFunction = [
|
|
433
451
|
{
|
|
@@ -437,9 +455,9 @@ describe('PolicyEngine', () => {
|
|
|
437
455
|
},
|
|
438
456
|
];
|
|
439
457
|
engine = new PolicyEngine({ rules: rulesWithFunction });
|
|
440
|
-
expect(engine.check({ name: 'test', args }, undefined)).toBe(PolicyDecision.ASK_USER);
|
|
458
|
+
expect((await engine.check({ name: 'test', args }, undefined)).decision).toBe(PolicyDecision.ASK_USER);
|
|
441
459
|
});
|
|
442
|
-
it('should convert undefined and functions to null in arrays', () => {
|
|
460
|
+
it('should convert undefined and functions to null in arrays', async () => {
|
|
443
461
|
const rules = [
|
|
444
462
|
{
|
|
445
463
|
toolName: 'test',
|
|
@@ -452,9 +470,9 @@ describe('PolicyEngine', () => {
|
|
|
452
470
|
array: ['value', undefined, () => 'hello', null],
|
|
453
471
|
};
|
|
454
472
|
// Should match pattern with undefined and functions converted to null
|
|
455
|
-
expect(engine.check({ name: 'test', args }, undefined)).toBe(PolicyDecision.ALLOW);
|
|
473
|
+
expect((await engine.check({ name: 'test', args }, undefined)).decision).toBe(PolicyDecision.ALLOW);
|
|
456
474
|
});
|
|
457
|
-
it('should produce valid JSON for all inputs', () => {
|
|
475
|
+
it('should produce valid JSON for all inputs', async () => {
|
|
458
476
|
const testCases = [
|
|
459
477
|
{ input: { simple: 'string' }, desc: 'simple object' },
|
|
460
478
|
{
|
|
@@ -482,12 +500,13 @@ describe('PolicyEngine', () => {
|
|
|
482
500
|
];
|
|
483
501
|
engine = new PolicyEngine({ rules });
|
|
484
502
|
// Should not throw when checking (which internally uses stableStringify)
|
|
485
|
-
expect(
|
|
503
|
+
await expect(engine.check({ name: 'test', args: input }, undefined)).resolves.not.toThrow();
|
|
486
504
|
// The check should succeed
|
|
487
|
-
expect(engine.check({ name: 'test', args: input }, undefined))
|
|
505
|
+
expect((await engine.check({ name: 'test', args: input }, undefined))
|
|
506
|
+
.decision).toBe(PolicyDecision.ALLOW);
|
|
488
507
|
}
|
|
489
508
|
});
|
|
490
|
-
it('should respect toJSON methods on objects', () => {
|
|
509
|
+
it('should respect toJSON methods on objects', async () => {
|
|
491
510
|
const rules = [
|
|
492
511
|
{
|
|
493
512
|
toolName: 'test',
|
|
@@ -509,9 +528,9 @@ describe('PolicyEngine', () => {
|
|
|
509
528
|
},
|
|
510
529
|
};
|
|
511
530
|
// Should match the sanitized pattern, not the dangerous one
|
|
512
|
-
expect(engine.check({ name: 'test', args }, undefined)).toBe(PolicyDecision.ALLOW);
|
|
531
|
+
expect((await engine.check({ name: 'test', args }, undefined)).decision).toBe(PolicyDecision.ALLOW);
|
|
513
532
|
});
|
|
514
|
-
it('should handle toJSON that returns primitives', () => {
|
|
533
|
+
it('should handle toJSON that returns primitives', async () => {
|
|
515
534
|
const rules = [
|
|
516
535
|
{
|
|
517
536
|
toolName: 'test',
|
|
@@ -527,9 +546,9 @@ describe('PolicyEngine', () => {
|
|
|
527
546
|
},
|
|
528
547
|
};
|
|
529
548
|
// toJSON returns a string, which should be properly stringified
|
|
530
|
-
expect(engine.check({ name: 'test', args }, undefined)).toBe(PolicyDecision.ALLOW);
|
|
549
|
+
expect((await engine.check({ name: 'test', args }, undefined)).decision).toBe(PolicyDecision.ALLOW);
|
|
531
550
|
});
|
|
532
|
-
it('should handle toJSON that throws an error', () => {
|
|
551
|
+
it('should handle toJSON that throws an error', async () => {
|
|
533
552
|
const rules = [
|
|
534
553
|
{
|
|
535
554
|
toolName: 'test',
|
|
@@ -547,16 +566,333 @@ describe('PolicyEngine', () => {
|
|
|
547
566
|
},
|
|
548
567
|
};
|
|
549
568
|
// Should fall back to regular object serialization when toJSON throws
|
|
550
|
-
expect(engine.check({ name: 'test', args }, undefined)).toBe(PolicyDecision.ALLOW);
|
|
569
|
+
expect((await engine.check({ name: 'test', args }, undefined)).decision).toBe(PolicyDecision.ALLOW);
|
|
570
|
+
});
|
|
571
|
+
});
|
|
572
|
+
describe('safety checker integration', () => {
|
|
573
|
+
it('should call checker when rule allows and has safety_checker', async () => {
|
|
574
|
+
const rules = [
|
|
575
|
+
{
|
|
576
|
+
toolName: 'test-tool',
|
|
577
|
+
decision: PolicyDecision.ALLOW,
|
|
578
|
+
},
|
|
579
|
+
];
|
|
580
|
+
const checkers = [
|
|
581
|
+
{
|
|
582
|
+
toolName: 'test-tool',
|
|
583
|
+
checker: {
|
|
584
|
+
type: 'external',
|
|
585
|
+
name: 'test-checker',
|
|
586
|
+
config: { content: 'test-content' },
|
|
587
|
+
},
|
|
588
|
+
},
|
|
589
|
+
];
|
|
590
|
+
engine = new PolicyEngine({ rules, checkers }, mockCheckerRunner);
|
|
591
|
+
vi.mocked(mockCheckerRunner.runChecker).mockResolvedValue({
|
|
592
|
+
decision: SafetyCheckDecision.ALLOW,
|
|
593
|
+
});
|
|
594
|
+
const result = await engine.check({ name: 'test-tool', args: { foo: 'bar' } }, undefined);
|
|
595
|
+
expect(result.decision).toBe(PolicyDecision.ALLOW);
|
|
596
|
+
expect(mockCheckerRunner.runChecker).toHaveBeenCalledWith({ name: 'test-tool', args: { foo: 'bar' } }, {
|
|
597
|
+
type: 'external',
|
|
598
|
+
name: 'test-checker',
|
|
599
|
+
config: { content: 'test-content' },
|
|
600
|
+
});
|
|
601
|
+
});
|
|
602
|
+
it('should handle checker errors as DENY', async () => {
|
|
603
|
+
const rules = [
|
|
604
|
+
{
|
|
605
|
+
toolName: 'test',
|
|
606
|
+
decision: PolicyDecision.ALLOW,
|
|
607
|
+
},
|
|
608
|
+
];
|
|
609
|
+
const checkers = [
|
|
610
|
+
{
|
|
611
|
+
toolName: 'test',
|
|
612
|
+
checker: {
|
|
613
|
+
type: 'in-process',
|
|
614
|
+
name: InProcessCheckerType.ALLOWED_PATH,
|
|
615
|
+
},
|
|
616
|
+
},
|
|
617
|
+
];
|
|
618
|
+
mockCheckerRunner.runChecker = vi
|
|
619
|
+
.fn()
|
|
620
|
+
.mockRejectedValue(new Error('Checker failed'));
|
|
621
|
+
engine = new PolicyEngine({ rules, checkers }, mockCheckerRunner);
|
|
622
|
+
const { decision } = await engine.check({ name: 'test' }, undefined);
|
|
623
|
+
expect(decision).toBe(PolicyDecision.DENY);
|
|
624
|
+
});
|
|
625
|
+
it('should return DENY when checker denies', async () => {
|
|
626
|
+
const rules = [
|
|
627
|
+
{
|
|
628
|
+
toolName: 'test-tool',
|
|
629
|
+
decision: PolicyDecision.ALLOW,
|
|
630
|
+
},
|
|
631
|
+
];
|
|
632
|
+
const checkers = [
|
|
633
|
+
{
|
|
634
|
+
toolName: 'test-tool',
|
|
635
|
+
checker: {
|
|
636
|
+
type: 'external',
|
|
637
|
+
name: 'test-checker',
|
|
638
|
+
config: { content: 'test-content' },
|
|
639
|
+
},
|
|
640
|
+
},
|
|
641
|
+
];
|
|
642
|
+
engine = new PolicyEngine({ rules, checkers }, mockCheckerRunner);
|
|
643
|
+
vi.mocked(mockCheckerRunner.runChecker).mockResolvedValue({
|
|
644
|
+
decision: SafetyCheckDecision.DENY,
|
|
645
|
+
reason: 'test reason',
|
|
646
|
+
});
|
|
647
|
+
const result = await engine.check({ name: 'test-tool', args: { foo: 'bar' } }, undefined);
|
|
648
|
+
expect(result.decision).toBe(PolicyDecision.DENY);
|
|
649
|
+
expect(mockCheckerRunner.runChecker).toHaveBeenCalled();
|
|
650
|
+
});
|
|
651
|
+
it('should not call checker if decision is not ALLOW', async () => {
|
|
652
|
+
const rules = [
|
|
653
|
+
{
|
|
654
|
+
toolName: 'test-tool',
|
|
655
|
+
decision: PolicyDecision.ASK_USER,
|
|
656
|
+
},
|
|
657
|
+
];
|
|
658
|
+
const checkers = [
|
|
659
|
+
{
|
|
660
|
+
toolName: 'test-tool',
|
|
661
|
+
checker: {
|
|
662
|
+
type: 'external',
|
|
663
|
+
name: 'test-checker',
|
|
664
|
+
config: { content: 'test-content' },
|
|
665
|
+
},
|
|
666
|
+
},
|
|
667
|
+
];
|
|
668
|
+
engine = new PolicyEngine({ rules, checkers }, mockCheckerRunner);
|
|
669
|
+
vi.mocked(mockCheckerRunner.runChecker).mockResolvedValue({
|
|
670
|
+
decision: SafetyCheckDecision.ALLOW,
|
|
671
|
+
});
|
|
672
|
+
const result = await engine.check({ name: 'test-tool', args: { foo: 'bar' } }, undefined);
|
|
673
|
+
expect(result.decision).toBe(PolicyDecision.ASK_USER);
|
|
674
|
+
expect(mockCheckerRunner.runChecker).toHaveBeenCalled();
|
|
675
|
+
});
|
|
676
|
+
it('should run checkers when rule allows', async () => {
|
|
677
|
+
const rules = [
|
|
678
|
+
{
|
|
679
|
+
toolName: 'test',
|
|
680
|
+
decision: PolicyDecision.ALLOW,
|
|
681
|
+
},
|
|
682
|
+
];
|
|
683
|
+
const checkers = [
|
|
684
|
+
{
|
|
685
|
+
toolName: 'test',
|
|
686
|
+
checker: {
|
|
687
|
+
type: 'in-process',
|
|
688
|
+
name: InProcessCheckerType.ALLOWED_PATH,
|
|
689
|
+
},
|
|
690
|
+
},
|
|
691
|
+
];
|
|
692
|
+
mockCheckerRunner.runChecker = vi.fn().mockResolvedValue({
|
|
693
|
+
decision: SafetyCheckDecision.ALLOW,
|
|
694
|
+
});
|
|
695
|
+
engine = new PolicyEngine({ rules, checkers }, mockCheckerRunner);
|
|
696
|
+
const { decision } = await engine.check({ name: 'test' }, undefined);
|
|
697
|
+
expect(decision).toBe(PolicyDecision.ALLOW);
|
|
698
|
+
expect(mockCheckerRunner.runChecker).toHaveBeenCalledTimes(1);
|
|
699
|
+
});
|
|
700
|
+
it('should not call checker if rule has no safety_checker', async () => {
|
|
701
|
+
const rules = [
|
|
702
|
+
{
|
|
703
|
+
toolName: 'test-tool',
|
|
704
|
+
decision: PolicyDecision.ALLOW,
|
|
705
|
+
},
|
|
706
|
+
];
|
|
707
|
+
engine = new PolicyEngine({ rules }, mockCheckerRunner);
|
|
708
|
+
const result = await engine.check({ name: 'test-tool', args: { foo: 'bar' } }, undefined);
|
|
709
|
+
expect(result.decision).toBe(PolicyDecision.ALLOW);
|
|
710
|
+
expect(mockCheckerRunner.runChecker).not.toHaveBeenCalled();
|
|
551
711
|
});
|
|
552
712
|
});
|
|
553
713
|
describe('serverName requirement', () => {
|
|
554
|
-
it('should require serverName for checks', () => {
|
|
714
|
+
it('should require serverName for checks', async () => {
|
|
555
715
|
// @ts-expect-error - intentionally testing missing serverName
|
|
556
|
-
expect(engine.check({ name: 'test' })).toBe(PolicyDecision.ASK_USER);
|
|
716
|
+
expect((await engine.check({ name: 'test' })).decision).toBe(PolicyDecision.ASK_USER);
|
|
557
717
|
// When serverName is provided (even undefined), it should work
|
|
558
|
-
expect(engine.check({ name: 'test' }, undefined)).toBe(PolicyDecision.ASK_USER);
|
|
559
|
-
expect(engine.check({ name: 'test' }, 'some-server')).toBe(PolicyDecision.ASK_USER);
|
|
718
|
+
expect((await engine.check({ name: 'test' }, undefined)).decision).toBe(PolicyDecision.ASK_USER);
|
|
719
|
+
expect((await engine.check({ name: 'test' }, 'some-server')).decision).toBe(PolicyDecision.ASK_USER);
|
|
720
|
+
});
|
|
721
|
+
it('should run multiple checkers in priority order and stop at first denial', async () => {
|
|
722
|
+
const rules = [
|
|
723
|
+
{
|
|
724
|
+
toolName: 'test',
|
|
725
|
+
decision: PolicyDecision.ALLOW,
|
|
726
|
+
},
|
|
727
|
+
];
|
|
728
|
+
const checkers = [
|
|
729
|
+
{
|
|
730
|
+
toolName: 'test',
|
|
731
|
+
priority: 10,
|
|
732
|
+
checker: { type: 'external', name: 'checker1' },
|
|
733
|
+
},
|
|
734
|
+
{
|
|
735
|
+
toolName: 'test',
|
|
736
|
+
priority: 20, // Should run first
|
|
737
|
+
checker: { type: 'external', name: 'checker2' },
|
|
738
|
+
},
|
|
739
|
+
];
|
|
740
|
+
mockCheckerRunner.runChecker = vi
|
|
741
|
+
.fn()
|
|
742
|
+
.mockImplementation(async (_toolCall, config) => {
|
|
743
|
+
if (config.name === 'checker2') {
|
|
744
|
+
return {
|
|
745
|
+
decision: SafetyCheckDecision.DENY,
|
|
746
|
+
reason: 'checker2 denied',
|
|
747
|
+
};
|
|
748
|
+
}
|
|
749
|
+
return { decision: SafetyCheckDecision.ALLOW };
|
|
750
|
+
});
|
|
751
|
+
engine = new PolicyEngine({ rules, checkers }, mockCheckerRunner);
|
|
752
|
+
const { decision, rule } = await engine.check({ name: 'test' }, undefined);
|
|
753
|
+
expect(decision).toBe(PolicyDecision.DENY);
|
|
754
|
+
expect(rule).toBeDefined();
|
|
755
|
+
expect(mockCheckerRunner.runChecker).toHaveBeenCalledTimes(1);
|
|
756
|
+
expect(mockCheckerRunner.runChecker).toHaveBeenCalledWith(expect.anything(), expect.objectContaining({ name: 'checker2' }));
|
|
757
|
+
});
|
|
758
|
+
});
|
|
759
|
+
describe('addChecker', () => {
|
|
760
|
+
it('should add a new checker and maintain priority order', () => {
|
|
761
|
+
const checker1 = {
|
|
762
|
+
checker: { type: 'external', name: 'checker1' },
|
|
763
|
+
priority: 5,
|
|
764
|
+
};
|
|
765
|
+
const checker2 = {
|
|
766
|
+
checker: { type: 'external', name: 'checker2' },
|
|
767
|
+
priority: 10,
|
|
768
|
+
};
|
|
769
|
+
engine.addChecker(checker1);
|
|
770
|
+
engine.addChecker(checker2);
|
|
771
|
+
const checkers = engine.getCheckers();
|
|
772
|
+
expect(checkers).toHaveLength(2);
|
|
773
|
+
expect(checkers[0].priority).toBe(10);
|
|
774
|
+
expect(checkers[0].checker.name).toBe('checker2');
|
|
775
|
+
expect(checkers[1].priority).toBe(5);
|
|
776
|
+
expect(checkers[1].checker.name).toBe('checker1');
|
|
777
|
+
});
|
|
778
|
+
});
|
|
779
|
+
describe('checker matching logic', () => {
|
|
780
|
+
it('should match checkers using toolName and argsPattern', async () => {
|
|
781
|
+
const rules = [
|
|
782
|
+
{ toolName: 'tool', decision: PolicyDecision.ALLOW },
|
|
783
|
+
];
|
|
784
|
+
const matchingChecker = {
|
|
785
|
+
checker: { type: 'external', name: 'matching' },
|
|
786
|
+
toolName: 'tool',
|
|
787
|
+
argsPattern: /"safe":true/,
|
|
788
|
+
};
|
|
789
|
+
const nonMatchingChecker = {
|
|
790
|
+
checker: { type: 'external', name: 'non-matching' },
|
|
791
|
+
toolName: 'other',
|
|
792
|
+
};
|
|
793
|
+
engine = new PolicyEngine({ rules, checkers: [matchingChecker, nonMatchingChecker] }, mockCheckerRunner);
|
|
794
|
+
vi.mocked(mockCheckerRunner.runChecker).mockResolvedValue({
|
|
795
|
+
decision: SafetyCheckDecision.ALLOW,
|
|
796
|
+
});
|
|
797
|
+
await engine.check({ name: 'tool', args: { safe: true } }, undefined);
|
|
798
|
+
expect(mockCheckerRunner.runChecker).toHaveBeenCalledWith(expect.anything(), expect.objectContaining({ name: 'matching' }));
|
|
799
|
+
expect(mockCheckerRunner.runChecker).not.toHaveBeenCalledWith(expect.anything(), expect.objectContaining({ name: 'non-matching' }));
|
|
800
|
+
});
|
|
801
|
+
it('should support wildcard patterns for checkers', async () => {
|
|
802
|
+
const rules = [
|
|
803
|
+
{ toolName: 'server__tool', decision: PolicyDecision.ALLOW },
|
|
804
|
+
];
|
|
805
|
+
const wildcardChecker = {
|
|
806
|
+
checker: { type: 'external', name: 'wildcard' },
|
|
807
|
+
toolName: 'server__*',
|
|
808
|
+
};
|
|
809
|
+
engine = new PolicyEngine({ rules, checkers: [wildcardChecker] }, mockCheckerRunner);
|
|
810
|
+
vi.mocked(mockCheckerRunner.runChecker).mockResolvedValue({
|
|
811
|
+
decision: SafetyCheckDecision.ALLOW,
|
|
812
|
+
});
|
|
813
|
+
await engine.check({ name: 'server__tool' }, 'server');
|
|
814
|
+
expect(mockCheckerRunner.runChecker).toHaveBeenCalledWith(expect.anything(), expect.objectContaining({ name: 'wildcard' }));
|
|
815
|
+
});
|
|
816
|
+
it('should run safety checkers when decision is ASK_USER and downgrade to DENY on failure', async () => {
|
|
817
|
+
const rules = [
|
|
818
|
+
{ toolName: 'tool', decision: PolicyDecision.ASK_USER },
|
|
819
|
+
];
|
|
820
|
+
const checkers = [
|
|
821
|
+
{
|
|
822
|
+
checker: {
|
|
823
|
+
type: 'in-process',
|
|
824
|
+
name: InProcessCheckerType.ALLOWED_PATH,
|
|
825
|
+
},
|
|
826
|
+
},
|
|
827
|
+
];
|
|
828
|
+
engine = new PolicyEngine({ rules, checkers }, mockCheckerRunner);
|
|
829
|
+
vi.mocked(mockCheckerRunner.runChecker).mockResolvedValue({
|
|
830
|
+
decision: SafetyCheckDecision.DENY,
|
|
831
|
+
reason: 'Safety check failed',
|
|
832
|
+
});
|
|
833
|
+
const result = await engine.check({ name: 'tool' }, undefined);
|
|
834
|
+
expect(result.decision).toBe(PolicyDecision.DENY);
|
|
835
|
+
expect(mockCheckerRunner.runChecker).toHaveBeenCalled();
|
|
836
|
+
});
|
|
837
|
+
it('should run safety checkers when decision is ASK_USER and keep ASK_USER on success', async () => {
|
|
838
|
+
const rules = [
|
|
839
|
+
{ toolName: 'tool', decision: PolicyDecision.ASK_USER },
|
|
840
|
+
];
|
|
841
|
+
const checkers = [
|
|
842
|
+
{
|
|
843
|
+
checker: {
|
|
844
|
+
type: 'in-process',
|
|
845
|
+
name: InProcessCheckerType.ALLOWED_PATH,
|
|
846
|
+
},
|
|
847
|
+
},
|
|
848
|
+
];
|
|
849
|
+
engine = new PolicyEngine({ rules, checkers }, mockCheckerRunner);
|
|
850
|
+
vi.mocked(mockCheckerRunner.runChecker).mockResolvedValue({
|
|
851
|
+
decision: SafetyCheckDecision.ALLOW,
|
|
852
|
+
});
|
|
853
|
+
const result = await engine.check({ name: 'tool' }, undefined);
|
|
854
|
+
expect(result.decision).toBe(PolicyDecision.ASK_USER);
|
|
855
|
+
expect(mockCheckerRunner.runChecker).toHaveBeenCalled();
|
|
856
|
+
});
|
|
857
|
+
it('should downgrade ALLOW to ASK_USER if checker returns ASK_USER', async () => {
|
|
858
|
+
const rules = [
|
|
859
|
+
{ toolName: 'tool', decision: PolicyDecision.ALLOW },
|
|
860
|
+
];
|
|
861
|
+
const checkers = [
|
|
862
|
+
{
|
|
863
|
+
checker: {
|
|
864
|
+
type: 'in-process',
|
|
865
|
+
name: InProcessCheckerType.ALLOWED_PATH,
|
|
866
|
+
},
|
|
867
|
+
},
|
|
868
|
+
];
|
|
869
|
+
engine = new PolicyEngine({ rules, checkers }, mockCheckerRunner);
|
|
870
|
+
vi.mocked(mockCheckerRunner.runChecker).mockResolvedValue({
|
|
871
|
+
decision: SafetyCheckDecision.ASK_USER,
|
|
872
|
+
reason: 'Suspicious path',
|
|
873
|
+
});
|
|
874
|
+
const result = await engine.check({ name: 'tool' }, undefined);
|
|
875
|
+
expect(result.decision).toBe(PolicyDecision.ASK_USER);
|
|
876
|
+
});
|
|
877
|
+
it('should DENY if checker returns ASK_USER in non-interactive mode', async () => {
|
|
878
|
+
const rules = [
|
|
879
|
+
{ toolName: 'tool', decision: PolicyDecision.ALLOW },
|
|
880
|
+
];
|
|
881
|
+
const checkers = [
|
|
882
|
+
{
|
|
883
|
+
checker: {
|
|
884
|
+
type: 'in-process',
|
|
885
|
+
name: InProcessCheckerType.ALLOWED_PATH,
|
|
886
|
+
},
|
|
887
|
+
},
|
|
888
|
+
];
|
|
889
|
+
engine = new PolicyEngine({ rules, checkers, nonInteractive: true }, mockCheckerRunner);
|
|
890
|
+
vi.mocked(mockCheckerRunner.runChecker).mockResolvedValue({
|
|
891
|
+
decision: SafetyCheckDecision.ASK_USER,
|
|
892
|
+
reason: 'Suspicious path',
|
|
893
|
+
});
|
|
894
|
+
const result = await engine.check({ name: 'tool' }, undefined);
|
|
895
|
+
expect(result.decision).toBe(PolicyDecision.DENY);
|
|
560
896
|
});
|
|
561
897
|
});
|
|
562
898
|
});
|