@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.
Files changed (144) hide show
  1. package/dist/google-gemini-cli-core-0.16.0-nightly.20251112.c961f274.tgz +0 -0
  2. package/dist/src/agents/codebase-investigator.test.d.ts +6 -0
  3. package/dist/src/agents/codebase-investigator.test.js +35 -0
  4. package/dist/src/agents/codebase-investigator.test.js.map +1 -0
  5. package/dist/src/agents/executor.test.js +181 -1
  6. package/dist/src/agents/executor.test.js.map +1 -1
  7. package/dist/src/code_assist/codeAssist.test.d.ts +6 -0
  8. package/dist/src/code_assist/codeAssist.test.js +99 -0
  9. package/dist/src/code_assist/codeAssist.test.js.map +1 -0
  10. package/dist/src/code_assist/experiments/client_metadata.js +2 -1
  11. package/dist/src/code_assist/experiments/client_metadata.js.map +1 -1
  12. package/dist/src/code_assist/experiments/client_metadata.test.d.ts +6 -0
  13. package/dist/src/code_assist/experiments/client_metadata.test.js +99 -0
  14. package/dist/src/code_assist/experiments/client_metadata.test.js.map +1 -0
  15. package/dist/src/code_assist/experiments/experiments.test.d.ts +6 -0
  16. package/dist/src/code_assist/experiments/experiments.test.js +92 -0
  17. package/dist/src/code_assist/experiments/experiments.test.js.map +1 -0
  18. package/dist/src/code_assist/oauth-credential-storage.test.js +49 -0
  19. package/dist/src/code_assist/oauth-credential-storage.test.js.map +1 -1
  20. package/dist/src/code_assist/server.js +5 -8
  21. package/dist/src/code_assist/server.js.map +1 -1
  22. package/dist/src/code_assist/server.test.js +109 -28
  23. package/dist/src/code_assist/server.test.js.map +1 -1
  24. package/dist/src/config/defaultModelConfigs.js +6 -0
  25. package/dist/src/config/defaultModelConfigs.js.map +1 -1
  26. package/dist/src/confirmation-bus/message-bus.d.ts +1 -1
  27. package/dist/src/confirmation-bus/message-bus.js +2 -2
  28. package/dist/src/confirmation-bus/message-bus.js.map +1 -1
  29. package/dist/src/confirmation-bus/message-bus.test.js +30 -24
  30. package/dist/src/confirmation-bus/message-bus.test.js.map +1 -1
  31. package/dist/src/core/loggingContentGenerator.test.d.ts +6 -0
  32. package/dist/src/core/loggingContentGenerator.test.js +180 -0
  33. package/dist/src/core/loggingContentGenerator.test.js.map +1 -0
  34. package/dist/src/core/tokenLimits.test.d.ts +6 -0
  35. package/dist/src/core/tokenLimits.test.js +26 -0
  36. package/dist/src/core/tokenLimits.test.js.map +1 -0
  37. package/dist/src/generated/git-commit.d.ts +2 -2
  38. package/dist/src/generated/git-commit.js +2 -2
  39. package/dist/src/generated/git-commit.js.map +1 -1
  40. package/dist/src/hooks/hookAggregator.d.ts +68 -0
  41. package/dist/src/hooks/hookAggregator.js +262 -0
  42. package/dist/src/hooks/hookAggregator.js.map +1 -0
  43. package/dist/src/hooks/hookAggregator.test.d.ts +6 -0
  44. package/dist/src/hooks/hookAggregator.test.js +387 -0
  45. package/dist/src/hooks/hookAggregator.test.js.map +1 -0
  46. package/dist/src/hooks/types.js +1 -1
  47. package/dist/src/hooks/types.js.map +1 -1
  48. package/dist/src/hooks/types.test.js +280 -2
  49. package/dist/src/hooks/types.test.js.map +1 -1
  50. package/dist/src/ide/ide-client.test.js +159 -0
  51. package/dist/src/ide/ide-client.test.js.map +1 -1
  52. package/dist/src/mcp/oauth-provider.test.js +177 -0
  53. package/dist/src/mcp/oauth-provider.test.js.map +1 -1
  54. package/dist/src/policy/config.js +3 -1
  55. package/dist/src/policy/config.js.map +1 -1
  56. package/dist/src/policy/config.test.js +118 -1
  57. package/dist/src/policy/config.test.js.map +1 -1
  58. package/dist/src/policy/policies/write.toml +10 -0
  59. package/dist/src/policy/policy-engine.d.ts +12 -3
  60. package/dist/src/policy/policy-engine.js +61 -7
  61. package/dist/src/policy/policy-engine.js.map +1 -1
  62. package/dist/src/policy/policy-engine.test.js +422 -86
  63. package/dist/src/policy/policy-engine.test.js.map +1 -1
  64. package/dist/src/policy/toml-loader.d.ts +2 -1
  65. package/dist/src/policy/toml-loader.js +103 -6
  66. package/dist/src/policy/toml-loader.js.map +1 -1
  67. package/dist/src/policy/toml-loader.test.js +32 -88
  68. package/dist/src/policy/toml-loader.test.js.map +1 -1
  69. package/dist/src/policy/types.d.ts +65 -0
  70. package/dist/src/policy/types.js +4 -0
  71. package/dist/src/policy/types.js.map +1 -1
  72. package/dist/src/prompts/mcp-prompts.test.d.ts +6 -0
  73. package/dist/src/prompts/mcp-prompts.test.js +40 -0
  74. package/dist/src/prompts/mcp-prompts.test.js.map +1 -0
  75. package/dist/src/prompts/prompt-registry.test.d.ts +6 -0
  76. package/dist/src/prompts/prompt-registry.test.js +111 -0
  77. package/dist/src/prompts/prompt-registry.test.js.map +1 -0
  78. package/dist/src/safety/built-in.d.ts +21 -0
  79. package/dist/src/safety/built-in.js +106 -0
  80. package/dist/src/safety/built-in.js.map +1 -0
  81. package/dist/src/safety/built-in.test.d.ts +6 -0
  82. package/dist/src/safety/built-in.test.js +199 -0
  83. package/dist/src/safety/built-in.test.js.map +1 -0
  84. package/dist/src/safety/checker-runner.d.ts +48 -0
  85. package/dist/src/safety/checker-runner.js +208 -0
  86. package/dist/src/safety/checker-runner.js.map +1 -0
  87. package/dist/src/safety/checker-runner.test.d.ts +6 -0
  88. package/dist/src/safety/checker-runner.test.js +238 -0
  89. package/dist/src/safety/checker-runner.test.js.map +1 -0
  90. package/dist/src/safety/context-builder.d.ts +23 -0
  91. package/dist/src/safety/context-builder.js +47 -0
  92. package/dist/src/safety/context-builder.js.map +1 -0
  93. package/dist/src/safety/context-builder.test.d.ts +6 -0
  94. package/dist/src/safety/context-builder.test.js +49 -0
  95. package/dist/src/safety/context-builder.test.js.map +1 -0
  96. package/dist/src/safety/protocol.d.ts +88 -0
  97. package/dist/src/safety/protocol.js +15 -0
  98. package/dist/src/safety/protocol.js.map +1 -0
  99. package/dist/src/safety/registry.d.ts +26 -0
  100. package/dist/src/safety/registry.js +65 -0
  101. package/dist/src/safety/registry.js.map +1 -0
  102. package/dist/src/safety/registry.test.d.ts +6 -0
  103. package/dist/src/safety/registry.test.js +31 -0
  104. package/dist/src/safety/registry.test.js.map +1 -0
  105. package/dist/src/services/loopDetectionService.d.ts +3 -0
  106. package/dist/src/services/loopDetectionService.js +81 -41
  107. package/dist/src/services/loopDetectionService.js.map +1 -1
  108. package/dist/src/services/loopDetectionService.test.js +96 -4
  109. package/dist/src/services/loopDetectionService.test.js.map +1 -1
  110. package/dist/src/services/test-data/resolved-aliases.golden.json +7 -0
  111. package/dist/src/telemetry/clearcut-logger/clearcut-logger.d.ts +4 -2
  112. package/dist/src/telemetry/clearcut-logger/clearcut-logger.js +29 -0
  113. package/dist/src/telemetry/clearcut-logger/clearcut-logger.js.map +1 -1
  114. package/dist/src/telemetry/clearcut-logger/clearcut-logger.test.js +31 -0
  115. package/dist/src/telemetry/clearcut-logger/clearcut-logger.test.js.map +1 -1
  116. package/dist/src/telemetry/clearcut-logger/event-metadata-key.d.ts +5 -1
  117. package/dist/src/telemetry/clearcut-logger/event-metadata-key.js +12 -1
  118. package/dist/src/telemetry/clearcut-logger/event-metadata-key.js.map +1 -1
  119. package/dist/src/telemetry/loggers.d.ts +2 -1
  120. package/dist/src/telemetry/loggers.js +11 -0
  121. package/dist/src/telemetry/loggers.js.map +1 -1
  122. package/dist/src/telemetry/types.d.ts +15 -2
  123. package/dist/src/telemetry/types.js +42 -3
  124. package/dist/src/telemetry/types.js.map +1 -1
  125. package/dist/src/tools/base-tool-invocation.test.js +2 -2
  126. package/dist/src/tools/base-tool-invocation.test.js.map +1 -1
  127. package/dist/src/tools/memoryTool.js +1 -1
  128. package/dist/src/tools/memoryTool.js.map +1 -1
  129. package/dist/src/tools/memoryTool.test.js +1 -1
  130. package/dist/src/tools/memoryTool.test.js.map +1 -1
  131. package/dist/src/tools/ripGrep.d.ts +24 -7
  132. package/dist/src/tools/ripGrep.js +125 -145
  133. package/dist/src/tools/ripGrep.js.map +1 -1
  134. package/dist/src/tools/ripGrep.test.js +144 -20
  135. package/dist/src/tools/ripGrep.test.js.map +1 -1
  136. package/dist/src/tools/tools.js +1 -1
  137. package/dist/src/tools/tools.js.map +1 -1
  138. package/dist/src/tools/write-todos.js +1 -1
  139. package/dist/src/tools/write-todos.js.map +1 -1
  140. package/dist/src/utils/llm-edit-fixer.test.js +8 -1
  141. package/dist/src/utils/llm-edit-fixer.test.js.map +1 -1
  142. package/dist/tsconfig.tsbuildinfo +1 -1
  143. package/package.json +1 -1
  144. 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
- engine = new PolicyEngine();
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)).toBe(PolicyDecision.ALLOW);
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)).toBe(PolicyDecision.DENY);
191
- expect(engine.check({ name: 'blocked-server__dangerous' }, undefined)).toBe(PolicyDecision.DENY);
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)).toBe(PolicyDecision.ASK_USER);
194
- expect(engine.check({ name: 'my-server-tool' }, undefined)).toBe(PolicyDecision.ASK_USER); // No __ separator
195
- expect(engine.check({ name: 'my-server' }, undefined)).toBe(PolicyDecision.ASK_USER); // No tool name
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)).toBe(PolicyDecision.DENY);
213
- expect(engine.check({ name: 'my-server__safe-tool' }, undefined)).toBe(PolicyDecision.ALLOW);
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')).toBe(PolicyDecision.ASK_USER);
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)).toBe(PolicyDecision.DENY);
305
- expect(engine.check({ name: 'shell', args: args2 }, undefined)).toBe(PolicyDecision.DENY);
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)).toBe(PolicyDecision.ASK_USER);
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(() => engine.check({ name: 'test', args: circularArgs }, undefined)).not.toThrow();
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)).toBe(PolicyDecision.DENY);
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)).toBe(PolicyDecision.ASK_USER);
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(() => engine.check({ name: 'deep', args: deepCircular }, undefined)).not.toThrow();
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)).toBe(PolicyDecision.DENY);
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(() => engine.check({ name: 'test', args: input }, undefined)).not.toThrow();
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)).toBe(PolicyDecision.ALLOW);
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
  });