@google/gemini-cli-core 0.34.0-preview.2 → 0.35.0-nightly.20260313.bb060d7a9

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 (329) hide show
  1. package/dist/docs/changelogs/index.md +19 -0
  2. package/dist/docs/changelogs/latest.md +214 -191
  3. package/dist/docs/changelogs/preview.md +447 -184
  4. package/dist/docs/cli/model-routing.md +18 -1
  5. package/dist/docs/cli/model-steering.md +79 -0
  6. package/dist/docs/cli/plan-mode.md +99 -14
  7. package/dist/docs/cli/settings.md +14 -12
  8. package/dist/docs/cli/telemetry.md +45 -0
  9. package/dist/docs/cli/tutorials/plan-mode-steering.md +89 -0
  10. package/dist/docs/core/index.md +2 -0
  11. package/dist/docs/core/local-model-routing.md +193 -0
  12. package/dist/docs/reference/configuration.md +27 -3
  13. package/dist/docs/reference/keyboard-shortcuts.md +168 -90
  14. package/dist/docs/reference/policy-engine.md +30 -6
  15. package/dist/docs/resources/troubleshooting.md +15 -0
  16. package/dist/docs/sidebar.json +10 -0
  17. package/dist/docs/tools/shell.md +8 -0
  18. package/dist/google-gemini-cli-core-0.35.0-nightly.20260311.657f19c1f.tgz +0 -0
  19. package/dist/src/agents/a2a-client-manager.d.ts +11 -4
  20. package/dist/src/agents/a2a-client-manager.js +61 -34
  21. package/dist/src/agents/a2a-client-manager.js.map +1 -1
  22. package/dist/src/agents/a2a-client-manager.test.js +178 -115
  23. package/dist/src/agents/a2a-client-manager.test.js.map +1 -1
  24. package/dist/src/agents/a2aUtils.d.ts +4 -28
  25. package/dist/src/agents/a2aUtils.js +20 -181
  26. package/dist/src/agents/a2aUtils.js.map +1 -1
  27. package/dist/src/agents/a2aUtils.test.js +19 -106
  28. package/dist/src/agents/a2aUtils.test.js.map +1 -1
  29. package/dist/src/agents/agent-scheduler.d.ts +2 -0
  30. package/dist/src/agents/agent-scheduler.js +3 -2
  31. package/dist/src/agents/agent-scheduler.js.map +1 -1
  32. package/dist/src/agents/agent-scheduler.test.js +12 -3
  33. package/dist/src/agents/agent-scheduler.test.js.map +1 -1
  34. package/dist/src/agents/agentLoader.d.ts +2 -2
  35. package/dist/src/agents/agentLoader.js +15 -0
  36. package/dist/src/agents/agentLoader.js.map +1 -1
  37. package/dist/src/agents/auth-provider/factory.d.ts +2 -0
  38. package/dist/src/agents/auth-provider/factory.js +6 -3
  39. package/dist/src/agents/auth-provider/factory.js.map +1 -1
  40. package/dist/src/agents/auth-provider/google-credentials-provider.d.ts +26 -0
  41. package/dist/src/agents/auth-provider/google-credentials-provider.js +117 -0
  42. package/dist/src/agents/auth-provider/google-credentials-provider.js.map +1 -0
  43. package/dist/src/agents/auth-provider/google-credentials-provider.test.d.ts +6 -0
  44. package/dist/src/agents/auth-provider/google-credentials-provider.test.js +126 -0
  45. package/dist/src/agents/auth-provider/google-credentials-provider.test.js.map +1 -0
  46. package/dist/src/agents/browser/browserAgentFactory.js +11 -2
  47. package/dist/src/agents/browser/browserAgentFactory.js.map +1 -1
  48. package/dist/src/agents/browser/browserAgentInvocation.d.ts +4 -3
  49. package/dist/src/agents/browser/browserAgentInvocation.js +10 -4
  50. package/dist/src/agents/browser/browserAgentInvocation.js.map +1 -1
  51. package/dist/src/agents/browser/browserAgentInvocation.test.js +1 -0
  52. package/dist/src/agents/browser/browserAgentInvocation.test.js.map +1 -1
  53. package/dist/src/agents/browser/browserManager.d.ts +10 -0
  54. package/dist/src/agents/browser/browserManager.js +53 -8
  55. package/dist/src/agents/browser/browserManager.js.map +1 -1
  56. package/dist/src/agents/browser/inputBlocker.d.ts +51 -0
  57. package/dist/src/agents/browser/inputBlocker.js +234 -0
  58. package/dist/src/agents/browser/inputBlocker.js.map +1 -0
  59. package/dist/src/agents/browser/inputBlocker.test.d.ts +6 -0
  60. package/dist/src/agents/browser/inputBlocker.test.js +82 -0
  61. package/dist/src/agents/browser/inputBlocker.test.js.map +1 -0
  62. package/dist/src/agents/browser/mcpToolWrapper.d.ts +3 -2
  63. package/dist/src/agents/browser/mcpToolWrapper.js +49 -8
  64. package/dist/src/agents/browser/mcpToolWrapper.js.map +1 -1
  65. package/dist/src/agents/browser/mcpToolWrapper.test.js +51 -0
  66. package/dist/src/agents/browser/mcpToolWrapper.test.js.map +1 -1
  67. package/dist/src/agents/local-executor.d.ts +5 -4
  68. package/dist/src/agents/local-executor.js +30 -25
  69. package/dist/src/agents/local-executor.js.map +1 -1
  70. package/dist/src/agents/local-executor.test.js +269 -3
  71. package/dist/src/agents/local-executor.test.js.map +1 -1
  72. package/dist/src/agents/local-invocation.d.ts +4 -4
  73. package/dist/src/agents/local-invocation.js +12 -9
  74. package/dist/src/agents/local-invocation.js.map +1 -1
  75. package/dist/src/agents/local-invocation.test.js +19 -3
  76. package/dist/src/agents/local-invocation.test.js.map +1 -1
  77. package/dist/src/agents/registry.js +3 -2
  78. package/dist/src/agents/registry.js.map +1 -1
  79. package/dist/src/agents/registry.test.js +1 -0
  80. package/dist/src/agents/registry.test.js.map +1 -1
  81. package/dist/src/agents/remote-invocation.d.ts +0 -9
  82. package/dist/src/agents/remote-invocation.js +1 -28
  83. package/dist/src/agents/remote-invocation.js.map +1 -1
  84. package/dist/src/agents/remote-invocation.test.js +1 -0
  85. package/dist/src/agents/remote-invocation.test.js.map +1 -1
  86. package/dist/src/agents/subagent-tool-wrapper.d.ts +5 -4
  87. package/dist/src/agents/subagent-tool-wrapper.js +9 -5
  88. package/dist/src/agents/subagent-tool-wrapper.js.map +1 -1
  89. package/dist/src/agents/subagent-tool-wrapper.test.js +15 -1
  90. package/dist/src/agents/subagent-tool-wrapper.test.js.map +1 -1
  91. package/dist/src/agents/subagent-tool.d.ts +3 -3
  92. package/dist/src/agents/subagent-tool.js +16 -12
  93. package/dist/src/agents/subagent-tool.js.map +1 -1
  94. package/dist/src/agents/subagent-tool.test.js +13 -3
  95. package/dist/src/agents/subagent-tool.test.js.map +1 -1
  96. package/dist/src/code_assist/oauth2.js +0 -1
  97. package/dist/src/code_assist/oauth2.js.map +1 -1
  98. package/dist/src/code_assist/server.js +2 -2
  99. package/dist/src/code_assist/server.js.map +1 -1
  100. package/dist/src/code_assist/server.test.js +5 -1
  101. package/dist/src/code_assist/server.test.js.map +1 -1
  102. package/dist/src/code_assist/telemetry.d.ts +2 -2
  103. package/dist/src/code_assist/telemetry.js +5 -3
  104. package/dist/src/code_assist/telemetry.js.map +1 -1
  105. package/dist/src/code_assist/telemetry.test.js +14 -12
  106. package/dist/src/code_assist/telemetry.test.js.map +1 -1
  107. package/dist/src/code_assist/types.d.ts +14 -12
  108. package/dist/src/code_assist/types.js.map +1 -1
  109. package/dist/src/config/agent-loop-context.d.ts +3 -0
  110. package/dist/src/config/config.d.ts +75 -2
  111. package/dist/src/config/config.js +78 -11
  112. package/dist/src/config/config.js.map +1 -1
  113. package/dist/src/config/config.test.js +170 -10
  114. package/dist/src/config/config.test.js.map +1 -1
  115. package/dist/src/config/models.d.ts +14 -0
  116. package/dist/src/config/models.js +37 -0
  117. package/dist/src/config/models.js.map +1 -1
  118. package/dist/src/config/models.test.js +51 -1
  119. package/dist/src/config/models.test.js.map +1 -1
  120. package/dist/src/config/storage.d.ts +1 -0
  121. package/dist/src/config/storage.js +3 -0
  122. package/dist/src/config/storage.js.map +1 -1
  123. package/dist/src/core/baseLlmClient.js +19 -3
  124. package/dist/src/core/baseLlmClient.js.map +1 -1
  125. package/dist/src/core/baseLlmClient.test.js +2 -0
  126. package/dist/src/core/baseLlmClient.test.js.map +1 -1
  127. package/dist/src/core/client.d.ts +4 -3
  128. package/dist/src/core/client.js +42 -17
  129. package/dist/src/core/client.js.map +1 -1
  130. package/dist/src/core/client.test.js +8 -0
  131. package/dist/src/core/client.test.js.map +1 -1
  132. package/dist/src/core/contentGenerator.d.ts +1 -0
  133. package/dist/src/core/contentGenerator.js +35 -3
  134. package/dist/src/core/contentGenerator.js.map +1 -1
  135. package/dist/src/core/contentGenerator.test.js +137 -2
  136. package/dist/src/core/contentGenerator.test.js.map +1 -1
  137. package/dist/src/core/coreToolHookTriggers.d.ts +2 -3
  138. package/dist/src/core/coreToolHookTriggers.js +5 -11
  139. package/dist/src/core/coreToolHookTriggers.js.map +1 -1
  140. package/dist/src/core/coreToolHookTriggers.test.js +24 -0
  141. package/dist/src/core/coreToolHookTriggers.test.js.map +1 -1
  142. package/dist/src/core/coreToolScheduler.js +1 -1
  143. package/dist/src/core/coreToolScheduler.js.map +1 -1
  144. package/dist/src/core/geminiChat.js +31 -21
  145. package/dist/src/core/geminiChat.js.map +1 -1
  146. package/dist/src/core/geminiChat.test.js +6 -4
  147. package/dist/src/core/geminiChat.test.js.map +1 -1
  148. package/dist/src/core/geminiChat_network_retry.test.js +21 -11
  149. package/dist/src/core/geminiChat_network_retry.test.js.map +1 -1
  150. package/dist/src/fallback/handler.js +0 -4
  151. package/dist/src/fallback/handler.js.map +1 -1
  152. package/dist/src/fallback/handler.test.js +0 -6
  153. package/dist/src/fallback/handler.test.js.map +1 -1
  154. package/dist/src/generated/git-commit.d.ts +2 -2
  155. package/dist/src/generated/git-commit.js +2 -2
  156. package/dist/src/generated/git-commit.js.map +1 -1
  157. package/dist/src/ide/detect-ide.d.ts +8 -0
  158. package/dist/src/ide/detect-ide.js +12 -1
  159. package/dist/src/ide/detect-ide.js.map +1 -1
  160. package/dist/src/ide/detect-ide.test.js +12 -0
  161. package/dist/src/ide/detect-ide.test.js.map +1 -1
  162. package/dist/src/ide/ide-installer.js +20 -8
  163. package/dist/src/ide/ide-installer.js.map +1 -1
  164. package/dist/src/ide/ide-installer.test.js +60 -4
  165. package/dist/src/ide/ide-installer.test.js.map +1 -1
  166. package/dist/src/mcp/oauth-provider.js +0 -2
  167. package/dist/src/mcp/oauth-provider.js.map +1 -1
  168. package/dist/src/mcp/oauth-utils.js +0 -2
  169. package/dist/src/mcp/oauth-utils.js.map +1 -1
  170. package/dist/src/policy/config.d.ts +14 -2
  171. package/dist/src/policy/config.js +94 -49
  172. package/dist/src/policy/config.js.map +1 -1
  173. package/dist/src/policy/config.test.js +208 -488
  174. package/dist/src/policy/config.test.js.map +1 -1
  175. package/dist/src/policy/policies/plan.toml +1 -1
  176. package/dist/src/policy/stable-stringify.js +15 -5
  177. package/dist/src/policy/stable-stringify.js.map +1 -1
  178. package/dist/src/policy/types.d.ts +1 -0
  179. package/dist/src/policy/types.js.map +1 -1
  180. package/dist/src/policy/utils.d.ts +17 -0
  181. package/dist/src/policy/utils.js +27 -7
  182. package/dist/src/policy/utils.js.map +1 -1
  183. package/dist/src/prompts/snippets.js +2 -1
  184. package/dist/src/prompts/snippets.js.map +1 -1
  185. package/dist/src/prompts/utils.test.d.ts +6 -0
  186. package/dist/src/prompts/utils.test.js +225 -0
  187. package/dist/src/prompts/utils.test.js.map +1 -0
  188. package/dist/src/routing/modelRouterService.js +2 -2
  189. package/dist/src/routing/modelRouterService.js.map +1 -1
  190. package/dist/src/routing/modelRouterService.test.js +4 -3
  191. package/dist/src/routing/modelRouterService.test.js.map +1 -1
  192. package/dist/src/routing/strategies/numericalClassifierStrategy.js +6 -40
  193. package/dist/src/routing/strategies/numericalClassifierStrategy.js.map +1 -1
  194. package/dist/src/routing/strategies/numericalClassifierStrategy.test.js +29 -64
  195. package/dist/src/routing/strategies/numericalClassifierStrategy.test.js.map +1 -1
  196. package/dist/src/scheduler/policy.d.ts +3 -7
  197. package/dist/src/scheduler/policy.js +4 -3
  198. package/dist/src/scheduler/policy.js.map +1 -1
  199. package/dist/src/scheduler/policy.test.js +51 -31
  200. package/dist/src/scheduler/policy.test.js.map +1 -1
  201. package/dist/src/scheduler/scheduler.d.ts +4 -2
  202. package/dist/src/scheduler/scheduler.js +19 -16
  203. package/dist/src/scheduler/scheduler.js.map +1 -1
  204. package/dist/src/scheduler/scheduler.test.js +33 -10
  205. package/dist/src/scheduler/scheduler.test.js.map +1 -1
  206. package/dist/src/scheduler/scheduler_parallel.test.js +40 -2
  207. package/dist/src/scheduler/scheduler_parallel.test.js.map +1 -1
  208. package/dist/src/scheduler/scheduler_waiting_callback.test.js +1 -1
  209. package/dist/src/scheduler/scheduler_waiting_callback.test.js.map +1 -1
  210. package/dist/src/scheduler/tool-executor.d.ts +3 -3
  211. package/dist/src/scheduler/tool-executor.js +15 -21
  212. package/dist/src/scheduler/tool-executor.js.map +1 -1
  213. package/dist/src/scheduler/tool-executor.test.js +43 -8
  214. package/dist/src/scheduler/tool-executor.test.js.map +1 -1
  215. package/dist/src/services/chatRecordingService.js +2 -1
  216. package/dist/src/services/chatRecordingService.js.map +1 -1
  217. package/dist/src/services/chatRecordingService.test.js +21 -0
  218. package/dist/src/services/chatRecordingService.test.js.map +1 -1
  219. package/dist/src/services/executionLifecycleService.d.ts +83 -0
  220. package/dist/src/services/executionLifecycleService.js +271 -0
  221. package/dist/src/services/executionLifecycleService.js.map +1 -0
  222. package/dist/src/services/executionLifecycleService.test.d.ts +6 -0
  223. package/dist/src/services/executionLifecycleService.test.js +227 -0
  224. package/dist/src/services/executionLifecycleService.test.js.map +1 -0
  225. package/dist/src/services/sandboxManager.d.ts +54 -0
  226. package/dist/src/services/sandboxManager.js +30 -0
  227. package/dist/src/services/sandboxManager.js.map +1 -0
  228. package/dist/src/services/sandboxManager.test.d.ts +6 -0
  229. package/dist/src/services/sandboxManager.test.js +95 -0
  230. package/dist/src/services/sandboxManager.test.js.map +1 -0
  231. package/dist/src/services/shellExecutionService.d.ts +4 -52
  232. package/dist/src/services/shellExecutionService.js +437 -498
  233. package/dist/src/services/shellExecutionService.js.map +1 -1
  234. package/dist/src/services/shellExecutionService.test.js +10 -21
  235. package/dist/src/services/shellExecutionService.test.js.map +1 -1
  236. package/dist/src/telemetry/clearcut-logger/clearcut-logger.d.ts +13 -2
  237. package/dist/src/telemetry/clearcut-logger/clearcut-logger.js +91 -1
  238. package/dist/src/telemetry/clearcut-logger/clearcut-logger.js.map +1 -1
  239. package/dist/src/telemetry/clearcut-logger/clearcut-logger.test.js +77 -0
  240. package/dist/src/telemetry/clearcut-logger/clearcut-logger.test.js.map +1 -1
  241. package/dist/src/telemetry/clearcut-logger/event-metadata-key.d.ts +10 -1
  242. package/dist/src/telemetry/clearcut-logger/event-metadata-key.js +25 -1
  243. package/dist/src/telemetry/clearcut-logger/event-metadata-key.js.map +1 -1
  244. package/dist/src/telemetry/index.d.ts +3 -3
  245. package/dist/src/telemetry/index.js +3 -3
  246. package/dist/src/telemetry/index.js.map +1 -1
  247. package/dist/src/telemetry/loggers.d.ts +2 -1
  248. package/dist/src/telemetry/loggers.js +32 -1
  249. package/dist/src/telemetry/loggers.js.map +1 -1
  250. package/dist/src/telemetry/loggers.test.js +37 -2
  251. package/dist/src/telemetry/loggers.test.js.map +1 -1
  252. package/dist/src/telemetry/metrics.d.ts +13 -0
  253. package/dist/src/telemetry/metrics.js +17 -0
  254. package/dist/src/telemetry/metrics.js.map +1 -1
  255. package/dist/src/telemetry/startupProfiler.js +4 -4
  256. package/dist/src/telemetry/startupProfiler.js.map +1 -1
  257. package/dist/src/telemetry/startupProfiler.test.js +19 -0
  258. package/dist/src/telemetry/startupProfiler.test.js.map +1 -1
  259. package/dist/src/telemetry/telemetry-utils.test.js +3 -3
  260. package/dist/src/telemetry/types.d.ts +16 -2
  261. package/dist/src/telemetry/types.js +34 -0
  262. package/dist/src/telemetry/types.js.map +1 -1
  263. package/dist/src/telemetry/uiTelemetry.d.ts +7 -0
  264. package/dist/src/telemetry/uiTelemetry.js +78 -0
  265. package/dist/src/telemetry/uiTelemetry.js.map +1 -1
  266. package/dist/src/telemetry/uiTelemetry.test.js +101 -0
  267. package/dist/src/telemetry/uiTelemetry.test.js.map +1 -1
  268. package/dist/src/tools/exit-plan-mode.js +10 -2
  269. package/dist/src/tools/exit-plan-mode.js.map +1 -1
  270. package/dist/src/tools/exit-plan-mode.test.js +15 -0
  271. package/dist/src/tools/exit-plan-mode.test.js.map +1 -1
  272. package/dist/src/tools/ls.js +2 -2
  273. package/dist/src/tools/ls.js.map +1 -1
  274. package/dist/src/tools/mcp-client.js +0 -1
  275. package/dist/src/tools/mcp-client.js.map +1 -1
  276. package/dist/src/tools/mcp-client.test.js +4 -0
  277. package/dist/src/tools/mcp-client.test.js.map +1 -1
  278. package/dist/src/tools/mcp-tool.test.js +10 -1
  279. package/dist/src/tools/mcp-tool.test.js.map +1 -1
  280. package/dist/src/tools/read-many-files.js +2 -4
  281. package/dist/src/tools/read-many-files.js.map +1 -1
  282. package/dist/src/tools/shell.d.ts +1 -1
  283. package/dist/src/tools/shell.js +3 -3
  284. package/dist/src/tools/shell.js.map +1 -1
  285. package/dist/src/tools/tool-registry.test.js +4 -0
  286. package/dist/src/tools/tool-registry.test.js.map +1 -1
  287. package/dist/src/tools/tools.d.ts +34 -1
  288. package/dist/src/tools/tools.js +47 -1
  289. package/dist/src/tools/tools.js.map +1 -1
  290. package/dist/src/tools/web-fetch.d.ts +5 -0
  291. package/dist/src/tools/web-fetch.js +177 -100
  292. package/dist/src/tools/web-fetch.js.map +1 -1
  293. package/dist/src/tools/web-fetch.test.js +130 -26
  294. package/dist/src/tools/web-fetch.test.js.map +1 -1
  295. package/dist/src/tools/web-search.test.js +2 -0
  296. package/dist/src/tools/web-search.test.js.map +1 -1
  297. package/dist/src/utils/fetch.d.ts +0 -29
  298. package/dist/src/utils/fetch.js +10 -123
  299. package/dist/src/utils/fetch.js.map +1 -1
  300. package/dist/src/utils/fetch.test.js +1 -103
  301. package/dist/src/utils/fetch.test.js.map +1 -1
  302. package/dist/src/utils/language-detection.js +96 -89
  303. package/dist/src/utils/language-detection.js.map +1 -1
  304. package/dist/src/utils/language-detection.test.d.ts +6 -0
  305. package/dist/src/utils/language-detection.test.js +39 -0
  306. package/dist/src/utils/language-detection.test.js.map +1 -0
  307. package/dist/src/utils/oauth-flow.js +0 -2
  308. package/dist/src/utils/oauth-flow.js.map +1 -1
  309. package/dist/src/utils/paths.js +5 -2
  310. package/dist/src/utils/paths.js.map +1 -1
  311. package/dist/src/utils/paths.test.js +11 -0
  312. package/dist/src/utils/paths.test.js.map +1 -1
  313. package/dist/src/utils/process-utils.js +18 -4
  314. package/dist/src/utils/process-utils.js.map +1 -1
  315. package/dist/src/utils/process-utils.test.js +32 -10
  316. package/dist/src/utils/process-utils.test.js.map +1 -1
  317. package/dist/src/utils/retry.d.ts +7 -0
  318. package/dist/src/utils/retry.js +40 -3
  319. package/dist/src/utils/retry.js.map +1 -1
  320. package/dist/src/utils/retry.test.js +13 -0
  321. package/dist/src/utils/retry.test.js.map +1 -1
  322. package/dist/src/utils/summarizer.test.js +5 -0
  323. package/dist/src/utils/summarizer.test.js.map +1 -1
  324. package/dist/src/utils/surface.d.ts +18 -0
  325. package/dist/src/utils/surface.js +46 -0
  326. package/dist/src/utils/surface.js.map +1 -0
  327. package/dist/tsconfig.tsbuildinfo +1 -1
  328. package/package.json +1 -1
  329. package/dist/google-gemini-cli-core-0.34.0-preview.1.tgz +0 -0
@@ -5,29 +5,85 @@
5
5
  */
6
6
  import { describe, it, expect, vi, afterEach, beforeEach } from 'vitest';
7
7
  import nodePath from 'node:path';
8
+ import * as fs from 'node:fs/promises';
9
+ import {} from 'node:fs';
8
10
  import { ApprovalMode, PolicyDecision, InProcessCheckerType, } from './types.js';
9
11
  import { isDirectorySecure } from '../utils/security.js';
12
+ import { createPolicyEngineConfig, clearEmittedPolicyWarnings, getPolicyDirectories, } from './config.js';
13
+ import { Storage } from '../config/storage.js';
14
+ import * as tomlLoader from './toml-loader.js';
15
+ import { coreEvents } from '../utils/events.js';
10
16
  vi.unmock('../config/storage.js');
11
17
  vi.mock('../utils/security.js', () => ({
12
18
  isDirectorySecure: vi.fn().mockResolvedValue({ secure: true }),
13
19
  }));
20
+ vi.mock('node:fs/promises', async (importOriginal) => {
21
+ const actual = await importOriginal();
22
+ const mockFs = {
23
+ ...actual,
24
+ readdir: vi.fn(actual.readdir),
25
+ readFile: vi.fn(actual.readFile),
26
+ stat: vi.fn(actual.stat),
27
+ mkdir: vi.fn(actual.mkdir),
28
+ open: vi.fn(actual.open),
29
+ rename: vi.fn(actual.rename),
30
+ };
31
+ return {
32
+ ...mockFs,
33
+ default: mockFs,
34
+ };
35
+ });
14
36
  afterEach(() => {
15
- vi.clearAllMocks();
16
- vi.restoreAllMocks();
17
- vi.doUnmock('node:fs/promises');
37
+ vi.resetAllMocks();
18
38
  });
19
39
  describe('createPolicyEngineConfig', () => {
40
+ const MOCK_DEFAULT_DIR = '/tmp/mock/default/policies';
20
41
  beforeEach(async () => {
21
- vi.resetModules();
22
- const { Storage } = await import('../config/storage.js');
23
- // Mock Storage to avoid picking up real user/system policies from the host environment
42
+ clearEmittedPolicyWarnings();
43
+ // Mock Storage to avoid host environment contamination
24
44
  vi.spyOn(Storage, 'getUserPoliciesDir').mockReturnValue('/non/existent/user/policies');
25
45
  vi.spyOn(Storage, 'getSystemPoliciesDir').mockReturnValue('/non/existent/system/policies');
26
- // Reset security check to default secure
27
46
  vi.mocked(isDirectorySecure).mockResolvedValue({ secure: true });
28
47
  });
48
+ /**
49
+ * Helper to mock a policy file in the filesystem.
50
+ */
51
+ function mockPolicyFile(path, content) {
52
+ vi.mocked(fs.readdir).mockImplementation(async (p) => {
53
+ if (nodePath.resolve(p.toString()) === nodePath.dirname(path)) {
54
+ return [
55
+ {
56
+ name: nodePath.basename(path),
57
+ isFile: () => true,
58
+ isDirectory: () => false,
59
+ },
60
+ ];
61
+ }
62
+ return (await vi.importActual('node:fs/promises')).readdir(p);
63
+ });
64
+ vi.mocked(fs.stat).mockImplementation(async (p) => {
65
+ if (nodePath.resolve(p.toString()) === nodePath.dirname(path)) {
66
+ return {
67
+ isDirectory: () => true,
68
+ isFile: () => false,
69
+ };
70
+ }
71
+ if (nodePath.resolve(p.toString()) === path) {
72
+ return {
73
+ isDirectory: () => false,
74
+ isFile: () => true,
75
+ };
76
+ }
77
+ return (await vi.importActual('node:fs/promises')).stat(p);
78
+ });
79
+ vi.mocked(fs.readFile).mockImplementation(async (p) => {
80
+ if (nodePath.resolve(p.toString()) === path) {
81
+ return content;
82
+ }
83
+ return (await vi.importActual('node:fs/promises')).readFile(p);
84
+ });
85
+ }
29
86
  it('should filter out insecure system policy directories', async () => {
30
- const { Storage } = await import('../config/storage.js');
31
87
  const systemPolicyDir = '/insecure/system/policies';
32
88
  vi.spyOn(Storage, 'getSystemPoliciesDir').mockReturnValue(systemPolicyDir);
33
89
  vi.mocked(isDirectorySecure).mockImplementation(async (path) => {
@@ -36,125 +92,74 @@ describe('createPolicyEngineConfig', () => {
36
92
  }
37
93
  return { secure: true };
38
94
  });
39
- // We need to spy on loadPoliciesFromToml to verify which directories were passed
40
- // But it is not exported from config.js, it is imported.
41
- // We can spy on the module it comes from.
42
- const tomlLoader = await import('./toml-loader.js');
43
- const loadPoliciesSpy = vi.spyOn(tomlLoader, 'loadPoliciesFromToml');
44
- loadPoliciesSpy.mockResolvedValue({
45
- rules: [],
46
- checkers: [],
47
- errors: [],
48
- });
49
- const { createPolicyEngineConfig } = await import('./config.js');
50
- const settings = {};
51
- await createPolicyEngineConfig(settings, ApprovalMode.DEFAULT, '/tmp/mock/default/policies');
52
- // Verify loadPoliciesFromToml was called
95
+ const loadPoliciesSpy = vi
96
+ .spyOn(tomlLoader, 'loadPoliciesFromToml')
97
+ .mockResolvedValue({ rules: [], checkers: [], errors: [] });
98
+ await createPolicyEngineConfig({}, ApprovalMode.DEFAULT, '/tmp/mock/default/policies');
53
99
  expect(loadPoliciesSpy).toHaveBeenCalled();
54
100
  const calledDirs = loadPoliciesSpy.mock.calls[0][0];
55
- // The system directory should NOT be in the list
56
101
  expect(calledDirs).not.toContain(systemPolicyDir);
57
- // But other directories (user, default) should be there
58
102
  expect(calledDirs).toContain('/non/existent/user/policies');
59
103
  expect(calledDirs).toContain('/tmp/mock/default/policies');
60
104
  });
61
- it('should return ASK_USER for write tools and ALLOW for read-only tools by default', async () => {
62
- const actualFs = await vi.importActual('node:fs/promises');
63
- const mockReaddir = vi.fn(async (path, options) => {
64
- if (typeof path === 'string' &&
65
- nodePath
66
- .normalize(path)
67
- .includes(nodePath.normalize('.gemini/policies'))) {
68
- // Return empty array for user policies
69
- return [];
105
+ it('should NOT filter out insecure supplemental admin policy directories', async () => {
106
+ const adminPolicyDir = '/insecure/admin/policies';
107
+ vi.mocked(isDirectorySecure).mockImplementation(async (path) => {
108
+ if (nodePath.resolve(path) === nodePath.resolve(adminPolicyDir)) {
109
+ return { secure: false, reason: 'Insecure directory' };
70
110
  }
71
- return actualFs.readdir(path, options);
111
+ return { secure: true };
72
112
  });
73
- vi.doMock('node:fs/promises', () => ({
74
- ...actualFs,
75
- default: { ...actualFs, readdir: mockReaddir },
76
- readdir: mockReaddir,
77
- }));
78
- // Mock Storage to avoid actual filesystem access for policy dirs during tests if needed,
79
- // but for now relying on the fs mock above might be enough if it catches the right paths.
80
- // Let's see if we need to mock Storage.getUserPoliciesDir etc.
81
- vi.resetModules();
82
- const { createPolicyEngineConfig } = await import('./config.js');
83
- const settings = {};
84
- // Pass a dummy default policies dir to avoid it trying to resolve __dirname relative to the test file in a weird way
85
- const config = await createPolicyEngineConfig(settings, ApprovalMode.DEFAULT, '/tmp/mock/default/policies');
113
+ const loadPoliciesSpy = vi
114
+ .spyOn(tomlLoader, 'loadPoliciesFromToml')
115
+ .mockResolvedValue({ rules: [], checkers: [], errors: [] });
116
+ await createPolicyEngineConfig({ adminPolicyPaths: [adminPolicyDir] }, ApprovalMode.DEFAULT, '/tmp/mock/default/policies');
117
+ const calledDirs = loadPoliciesSpy.mock.calls[0][0];
118
+ expect(calledDirs).toContain(adminPolicyDir);
119
+ expect(calledDirs).toContain('/non/existent/system/policies');
120
+ expect(calledDirs).toContain('/non/existent/user/policies');
121
+ expect(calledDirs).toContain('/tmp/mock/default/policies');
122
+ });
123
+ it('should return ASK_USER for write tools and ALLOW for read-only tools by default', async () => {
124
+ vi.mocked(fs.readdir).mockResolvedValue([]);
125
+ const config = await createPolicyEngineConfig({}, ApprovalMode.DEFAULT, MOCK_DEFAULT_DIR);
86
126
  expect(config.defaultDecision).toBe(PolicyDecision.ASK_USER);
87
- // The order of the rules is not guaranteed, so we sort them by tool name.
88
- config.rules?.sort((a, b) => (a.toolName ?? '').localeCompare(b.toolName ?? ''));
89
- // Since we are mocking an empty policy directory, we expect NO rules from TOML.
90
- // Wait, the CLI test expected a bunch of default rules. Those must have come from
91
- // the actual default policies directory in the CLI package.
92
- // In the core package, we don't necessarily have those default policy files yet
93
- // or we need to point to them.
94
- // For this unit test, if we mock the default dir as empty, we should get NO rules
95
- // if no settings are provided.
96
- // Actually, let's look at how CLI test gets them. It uses `__dirname` in `policy.ts`.
97
- // If we want to test default rules, we need to provide them.
98
- // For now, let's assert it's empty if we provide no TOML files, to ensure the *mechanism* works.
99
- // Or better, mock one default rule to ensure it's loaded.
100
127
  expect(config.rules).toEqual([]);
101
- vi.doUnmock('node:fs/promises');
102
- }, 30000);
128
+ });
103
129
  it('should allow tools in tools.allowed', async () => {
104
- const { createPolicyEngineConfig } = await import('./config.js');
105
- const settings = {
106
- tools: { allowed: ['run_shell_command'] },
107
- };
108
- const config = await createPolicyEngineConfig(settings, ApprovalMode.DEFAULT, '/tmp/mock/default/policies');
130
+ vi.mocked(fs.readdir).mockResolvedValue([]);
131
+ const config = await createPolicyEngineConfig({ tools: { allowed: ['run_shell_command'] } }, ApprovalMode.DEFAULT, MOCK_DEFAULT_DIR);
109
132
  const rule = config.rules?.find((r) => r.toolName === 'run_shell_command' &&
110
133
  r.decision === PolicyDecision.ALLOW);
111
134
  expect(rule).toBeDefined();
112
135
  expect(rule?.priority).toBeCloseTo(4.3, 5); // Command line allow
113
136
  });
114
137
  it('should deny tools in tools.exclude', async () => {
115
- const { createPolicyEngineConfig } = await import('./config.js');
116
- const settings = {
117
- tools: { exclude: ['run_shell_command'] },
118
- };
119
- const config = await createPolicyEngineConfig(settings, ApprovalMode.DEFAULT, '/tmp/mock/default/policies');
138
+ const config = await createPolicyEngineConfig({ tools: { exclude: ['run_shell_command'] } }, ApprovalMode.DEFAULT, MOCK_DEFAULT_DIR);
120
139
  const rule = config.rules?.find((r) => r.toolName === 'run_shell_command' &&
121
140
  r.decision === PolicyDecision.DENY);
122
141
  expect(rule).toBeDefined();
123
142
  expect(rule?.priority).toBeCloseTo(4.4, 5); // Command line exclude
124
143
  });
125
144
  it('should allow tools from allowed MCP servers', async () => {
126
- const { createPolicyEngineConfig } = await import('./config.js');
127
- const settings = {
128
- mcp: { allowed: ['my-server'] },
129
- };
130
- const config = await createPolicyEngineConfig(settings, ApprovalMode.DEFAULT, '/tmp/mock/default/policies');
145
+ const config = await createPolicyEngineConfig({ mcp: { allowed: ['my-server'] } }, ApprovalMode.DEFAULT, MOCK_DEFAULT_DIR);
131
146
  const rule = config.rules?.find((r) => r.mcpName === 'my-server' && r.decision === PolicyDecision.ALLOW);
132
147
  expect(rule).toBeDefined();
133
148
  expect(rule?.priority).toBe(4.1); // MCP allowed server
134
149
  });
135
150
  it('should deny tools from excluded MCP servers', async () => {
136
- const { createPolicyEngineConfig } = await import('./config.js');
137
- const settings = {
138
- mcp: { excluded: ['my-server'] },
139
- };
140
- const config = await createPolicyEngineConfig(settings, ApprovalMode.DEFAULT, '/tmp/mock/default/policies');
151
+ const config = await createPolicyEngineConfig({ mcp: { excluded: ['my-server'] } }, ApprovalMode.DEFAULT, MOCK_DEFAULT_DIR);
141
152
  const rule = config.rules?.find((r) => r.mcpName === 'my-server' && r.decision === PolicyDecision.DENY);
142
153
  expect(rule).toBeDefined();
143
154
  expect(rule?.priority).toBe(4.9); // MCP excluded server
144
155
  });
145
156
  it('should allow tools from trusted MCP servers', async () => {
146
- const { createPolicyEngineConfig } = await import('./config.js');
147
- const settings = {
157
+ const config = await createPolicyEngineConfig({
148
158
  mcpServers: {
149
- 'trusted-server': {
150
- trust: true,
151
- },
152
- 'untrusted-server': {
153
- trust: false,
154
- },
159
+ 'trusted-server': { trust: true },
160
+ 'untrusted-server': { trust: false },
155
161
  },
156
- };
157
- const config = await createPolicyEngineConfig(settings, ApprovalMode.DEFAULT, '/tmp/mock/default/policies');
162
+ }, ApprovalMode.DEFAULT, MOCK_DEFAULT_DIR);
158
163
  const trustedRule = config.rules?.find((r) => r.mcpName === 'trusted-server' && r.decision === PolicyDecision.ALLOW);
159
164
  expect(trustedRule).toBeDefined();
160
165
  expect(trustedRule?.priority).toBe(4.2); // MCP trusted server
@@ -163,19 +168,10 @@ describe('createPolicyEngineConfig', () => {
163
168
  expect(untrustedRule).toBeUndefined();
164
169
  });
165
170
  it('should handle multiple MCP server configurations together', async () => {
166
- const { createPolicyEngineConfig } = await import('./config.js');
167
- const settings = {
168
- mcp: {
169
- allowed: ['allowed-server'],
170
- excluded: ['excluded-server'],
171
- },
172
- mcpServers: {
173
- 'trusted-server': {
174
- trust: true,
175
- },
176
- },
177
- };
178
- const config = await createPolicyEngineConfig(settings, ApprovalMode.DEFAULT, '/tmp/mock/default/policies');
171
+ const config = await createPolicyEngineConfig({
172
+ mcp: { allowed: ['allowed-server'], excluded: ['excluded-server'] },
173
+ mcpServers: { 'trusted-server': { trust: true } },
174
+ }, ApprovalMode.DEFAULT, MOCK_DEFAULT_DIR);
179
175
  // Check allowed server
180
176
  const allowedRule = config.rules?.find((r) => r.mcpName === 'allowed-server' && r.decision === PolicyDecision.ALLOW);
181
177
  expect(allowedRule).toBeDefined();
@@ -190,46 +186,38 @@ describe('createPolicyEngineConfig', () => {
190
186
  expect(excludedRule?.priority).toBe(4.9); // MCP excluded server
191
187
  });
192
188
  it('should allow all tools in YOLO mode', async () => {
193
- const { createPolicyEngineConfig } = await import('./config.js');
194
- const settings = {};
195
- const config = await createPolicyEngineConfig(settings, ApprovalMode.YOLO);
189
+ const config = await createPolicyEngineConfig({}, ApprovalMode.YOLO);
196
190
  const rule = config.rules?.find((r) => r.decision === PolicyDecision.ALLOW && !r.toolName);
197
191
  expect(rule).toBeDefined();
198
- // Priority 998 in default tier → 1.998 (999 reserved for ask_user exception)
199
192
  expect(rule?.priority).toBeCloseTo(1.998, 5);
200
193
  });
201
194
  it('should allow edit tool in AUTO_EDIT mode', async () => {
202
- const { createPolicyEngineConfig } = await import('./config.js');
203
- const settings = {};
204
- const config = await createPolicyEngineConfig(settings, ApprovalMode.AUTO_EDIT);
195
+ const config = await createPolicyEngineConfig({}, ApprovalMode.AUTO_EDIT);
205
196
  const rule = config.rules?.find((r) => r.toolName === 'replace' &&
206
197
  r.decision === PolicyDecision.ALLOW &&
207
198
  r.modes?.includes(ApprovalMode.AUTO_EDIT));
208
199
  expect(rule).toBeDefined();
209
- // Priority 15 in default tier → 1.015
210
200
  expect(rule?.priority).toBeCloseTo(1.015, 5);
211
201
  });
212
202
  it('should prioritize exclude over allow', async () => {
213
- const { createPolicyEngineConfig } = await import('./config.js');
214
- const settings = {
215
- tools: { allowed: ['run_shell_command'], exclude: ['run_shell_command'] },
216
- };
217
- const config = await createPolicyEngineConfig(settings, ApprovalMode.DEFAULT, '/tmp/mock/default/policies');
203
+ const config = await createPolicyEngineConfig({
204
+ tools: {
205
+ allowed: ['run_shell_command'],
206
+ exclude: ['run_shell_command'],
207
+ },
208
+ }, ApprovalMode.DEFAULT, MOCK_DEFAULT_DIR);
218
209
  const denyRule = config.rules?.find((r) => r.toolName === 'run_shell_command' &&
219
210
  r.decision === PolicyDecision.DENY);
220
211
  const allowRule = config.rules?.find((r) => r.toolName === 'run_shell_command' &&
221
212
  r.decision === PolicyDecision.ALLOW);
222
- expect(denyRule).toBeDefined();
223
- expect(allowRule).toBeDefined();
224
213
  expect(denyRule.priority).toBeGreaterThan(allowRule.priority);
225
214
  });
226
215
  it('should prioritize specific tool allows over MCP server excludes', async () => {
227
- const { createPolicyEngineConfig } = await import('./config.js');
228
216
  const settings = {
229
217
  mcp: { excluded: ['my-server'] },
230
218
  tools: { allowed: ['mcp_my-server_specific-tool'] },
231
219
  };
232
- const config = await createPolicyEngineConfig(settings, ApprovalMode.DEFAULT, '/tmp/mock/default/policies');
220
+ const config = await createPolicyEngineConfig(settings, ApprovalMode.DEFAULT, MOCK_DEFAULT_DIR);
233
221
  const serverDenyRule = config.rules?.find((r) => r.mcpName === 'my-server' && r.decision === PolicyDecision.DENY);
234
222
  const toolAllowRule = config.rules?.find((r) => r.toolName === 'mcp_my-server_specific-tool' &&
235
223
  r.decision === PolicyDecision.ALLOW);
@@ -262,6 +250,7 @@ describe('createPolicyEngineConfig', () => {
262
250
  expect(toolDenyRule.priority).toBeGreaterThan(serverAllowRule.priority);
263
251
  });
264
252
  it('should handle complex priority scenarios correctly', async () => {
253
+ mockPolicyFile(nodePath.join(MOCK_DEFAULT_DIR, 'default.toml'), '[[rule]]\ntoolName = "glob"\ndecision = "allow"\npriority = 50\n');
265
254
  const settings = {
266
255
  tools: {
267
256
  allowed: ['mcp_trusted-server_tool1', 'other-tool'], // Priority 4.3
@@ -277,57 +266,7 @@ describe('createPolicyEngineConfig', () => {
277
266
  },
278
267
  },
279
268
  };
280
- // Mock a default policy for 'glob' to test priority override
281
- const actualFs = await vi.importActual('node:fs/promises');
282
- const mockReaddir = vi.fn(async (p, _o) => {
283
- if (typeof p === 'string' && p.includes('/tmp/mock/default/policies')) {
284
- return [
285
- {
286
- name: 'default.toml',
287
- isFile: () => true,
288
- isDirectory: () => false,
289
- },
290
- ];
291
- }
292
- return [];
293
- });
294
- const mockStat = vi.fn(async (p) => {
295
- if (typeof p === 'string' && p.includes('/tmp/mock/default/policies')) {
296
- return {
297
- isDirectory: () => true,
298
- isFile: () => false,
299
- };
300
- }
301
- if (typeof p === 'string' && p.includes('default.toml')) {
302
- return {
303
- isDirectory: () => false,
304
- isFile: () => true,
305
- };
306
- }
307
- return actualFs.stat(p);
308
- });
309
- const mockReadFile = vi.fn(async (p, _o) => {
310
- if (typeof p === 'string' && p.includes('default.toml')) {
311
- return '[[rule]]\ntoolName = "glob"\ndecision = "allow"\npriority = 50\n';
312
- }
313
- return '';
314
- });
315
- vi.doMock('node:fs/promises', () => ({
316
- ...actualFs,
317
- default: {
318
- ...actualFs,
319
- readdir: mockReaddir,
320
- readFile: mockReadFile,
321
- stat: mockStat,
322
- },
323
- readdir: mockReaddir,
324
- readFile: mockReadFile,
325
- stat: mockStat,
326
- }));
327
- vi.resetModules();
328
- const { createPolicyEngineConfig: createConfig } = await import('./config.js');
329
- const config = await createConfig(settings, ApprovalMode.DEFAULT, '/tmp/mock/default/policies');
330
- // Verify glob is denied even though default would allow it
269
+ const config = await createPolicyEngineConfig(settings, ApprovalMode.DEFAULT, MOCK_DEFAULT_DIR);
331
270
  const globDenyRule = config.rules?.find((r) => r.toolName === 'glob' && r.decision === PolicyDecision.DENY);
332
271
  const globAllowRule = config.rules?.find((r) => r.toolName === 'glob' && r.decision === PolicyDecision.ALLOW);
333
272
  expect(globDenyRule).toBeDefined();
@@ -348,21 +287,14 @@ describe('createPolicyEngineConfig', () => {
348
287
  const highestPriorityExcludes = priorities?.filter((p) => Math.abs(p.priority - 4.4) < 0.01 ||
349
288
  Math.abs(p.priority - 4.9) < 0.01);
350
289
  expect(highestPriorityExcludes?.every((p) => p.decision === PolicyDecision.DENY)).toBe(true);
351
- vi.doUnmock('node:fs/promises');
352
290
  });
353
291
  it('should handle MCP servers with undefined trust property', async () => {
354
- const { createPolicyEngineConfig } = await import('./config.js');
355
- const settings = {
292
+ const config = await createPolicyEngineConfig({
356
293
  mcpServers: {
357
- 'no-trust-property': {
358
- // trust property is undefined/missing
359
- },
360
- 'explicit-false': {
361
- trust: false,
362
- },
294
+ 'no-trust-property': {},
295
+ 'explicit-false': { trust: false },
363
296
  },
364
- };
365
- const config = await createPolicyEngineConfig(settings, ApprovalMode.DEFAULT, '/tmp/mock/default/policies');
297
+ }, ApprovalMode.DEFAULT, MOCK_DEFAULT_DIR);
366
298
  // Neither server should have an allow rule
367
299
  const noTrustRule = config.rules?.find((r) => r.mcpName === 'no-trust-property' &&
368
300
  r.decision === PolicyDecision.ALLOW);
@@ -371,29 +303,11 @@ describe('createPolicyEngineConfig', () => {
371
303
  expect(explicitFalseRule).toBeUndefined();
372
304
  });
373
305
  it('should have YOLO allow-all rule beat write tool rules in YOLO mode', async () => {
374
- vi.resetModules();
375
- vi.doUnmock('node:fs/promises');
376
- const { createPolicyEngineConfig: createConfig } = await import('./config.js');
377
- // Re-mock Storage after resetModules because it was reloaded
378
- const { Storage: FreshStorage } = await import('../config/storage.js');
379
- vi.spyOn(FreshStorage, 'getUserPoliciesDir').mockReturnValue('/non/existent/user/policies');
380
- vi.spyOn(FreshStorage, 'getSystemPoliciesDir').mockReturnValue('/non/existent/system/policies');
381
- const settings = {
382
- tools: { exclude: ['dangerous-tool'] },
383
- };
384
- // Use default policy dir (no third arg) to load real yolo.toml and write.toml
385
- const config = await createConfig(settings, ApprovalMode.YOLO);
386
- // Should have the wildcard allow rule
306
+ const config = await createPolicyEngineConfig({ tools: { exclude: ['dangerous-tool'] } }, ApprovalMode.YOLO);
387
307
  const wildcardRule = config.rules?.find((r) => !r.toolName && r.decision === PolicyDecision.ALLOW);
388
- expect(wildcardRule).toBeDefined();
389
- // Priority 998 in default tier → 1.998 (999 reserved for ask_user exception)
390
- expect(wildcardRule?.priority).toBeCloseTo(1.998, 5);
391
- // Write tool ASK_USER rules are present (from write.toml)
392
308
  const writeToolRules = config.rules?.filter((r) => ['run_shell_command'].includes(r.toolName || '') &&
393
309
  r.decision === PolicyDecision.ASK_USER);
394
- expect(writeToolRules).toBeDefined();
395
- expect(writeToolRules?.length).toBeGreaterThan(0);
396
- // But YOLO allow-all rule has higher priority than all write tool rules
310
+ expect(wildcardRule).toBeDefined();
397
311
  writeToolRules?.forEach((writeRule) => {
398
312
  expect(wildcardRule.priority).toBeGreaterThan(writeRule.priority);
399
313
  });
@@ -403,106 +317,25 @@ describe('createPolicyEngineConfig', () => {
403
317
  expect(excludeRule?.priority).toBeCloseTo(4.4, 5); // Command line exclude
404
318
  });
405
319
  it('should support argsPattern in policy rules', async () => {
406
- const actualFs = await vi.importActual('node:fs/promises');
407
- const mockReaddir = vi.fn(async (path, options) => {
408
- if (typeof path === 'string' &&
409
- nodePath
410
- .normalize(path)
411
- .includes(nodePath.normalize('.gemini/policies'))) {
412
- return [
413
- {
414
- name: 'write.toml',
415
- isFile: () => true,
416
- isDirectory: () => false,
417
- },
418
- ];
419
- }
420
- return actualFs.readdir(path, options);
421
- });
422
- const mockReadFile = vi.fn(async (path, options) => {
423
- if (typeof path === 'string' &&
424
- nodePath
425
- .normalize(path)
426
- .includes(nodePath.normalize('.gemini/policies/write.toml'))) {
427
- return `
428
- [[rule]]
429
- toolName = "run_shell_command"
430
- argsPattern = "\\"command\\":\\"git (status|diff|log)\\""
431
- decision = "allow"
432
- priority = 150
433
- `;
434
- }
435
- return actualFs.readFile(path, options);
436
- });
437
- const mockStat = vi.fn(async (path, options) => {
438
- if (typeof path === 'string' &&
439
- nodePath
440
- .normalize(path)
441
- .includes(nodePath.normalize('.gemini/policies'))) {
442
- return {
443
- isDirectory: () => true,
444
- isFile: () => false,
445
- };
446
- }
447
- return actualFs.stat(path, options);
448
- });
449
- vi.doMock('node:fs/promises', () => ({
450
- ...actualFs,
451
- default: {
452
- ...actualFs,
453
- readFile: mockReadFile,
454
- readdir: mockReaddir,
455
- stat: mockStat,
456
- },
457
- readFile: mockReadFile,
458
- readdir: mockReaddir,
459
- stat: mockStat,
460
- }));
461
- vi.resetModules();
462
- const { createPolicyEngineConfig } = await import('./config.js');
463
- const settings = {};
464
- const config = await createPolicyEngineConfig(settings, ApprovalMode.DEFAULT, '/tmp/mock/default/policies');
320
+ mockPolicyFile(nodePath.join(MOCK_DEFAULT_DIR, 'write.toml'), `
321
+ [[rule]]
322
+ toolName = "run_shell_command"
323
+ argsPattern = "\\"command\\":\\"git (status|diff|log)\\""
324
+ decision = "allow"
325
+ priority = 150
326
+ `);
327
+ const config = await createPolicyEngineConfig({}, ApprovalMode.DEFAULT, MOCK_DEFAULT_DIR);
465
328
  const rule = config.rules?.find((r) => r.toolName === 'run_shell_command' &&
466
329
  r.decision === PolicyDecision.ALLOW);
467
330
  expect(rule).toBeDefined();
468
- // Priority 150 in user tier → 4.150
469
- expect(rule?.priority).toBeCloseTo(4.15, 5);
331
+ // Priority 150 in default tier → 1.150
332
+ expect(rule?.priority).toBeCloseTo(1.15, 5);
470
333
  expect(rule?.argsPattern).toBeInstanceOf(RegExp);
471
334
  expect(rule?.argsPattern?.test('{"command":"git status"}')).toBe(true);
472
- expect(rule?.argsPattern?.test('{"command":"git diff"}')).toBe(true);
473
- expect(rule?.argsPattern?.test('{"command":"git log"}')).toBe(true);
474
335
  expect(rule?.argsPattern?.test('{"command":"git commit"}')).toBe(false);
475
- expect(rule?.argsPattern?.test('{"command":"git push"}')).toBe(false);
476
- vi.doUnmock('node:fs/promises');
477
336
  });
478
337
  it('should load safety_checker configuration from TOML', async () => {
479
- const actualFs = await vi.importActual('node:fs/promises');
480
- const mockReaddir = vi.fn(async (path, options) => {
481
- if (typeof path === 'string' &&
482
- nodePath
483
- .normalize(path)
484
- .includes(nodePath.normalize('.gemini/policies'))) {
485
- return [
486
- {
487
- name: 'safety.toml',
488
- isFile: () => true,
489
- isDirectory: () => false,
490
- },
491
- ];
492
- }
493
- return actualFs.readdir(path, options);
494
- });
495
- const mockReadFile = vi.fn(async (path, options) => {
496
- if (typeof path === 'string' &&
497
- nodePath
498
- .normalize(path)
499
- .includes(nodePath.normalize('.gemini/policies/safety.toml'))) {
500
- return `
501
- [[rule]]
502
- toolName = "write_file"
503
- decision = "allow"
504
- priority = 10
505
-
338
+ mockPolicyFile(nodePath.join(MOCK_DEFAULT_DIR, 'safety.toml'), `
506
339
  [[rule]]
507
340
  toolName = "write_file"
508
341
  decision = "allow"
@@ -515,71 +348,14 @@ priority = 10
515
348
  type = "in-process"
516
349
  name = "allowed-path"
517
350
  required_context = ["environment"]
518
- [safety_checker.checker.config]
519
- `;
520
- }
521
- return actualFs.readFile(path, options);
522
- });
523
- const mockStat = vi.fn(async (path, options) => {
524
- if (typeof path === 'string' &&
525
- nodePath
526
- .normalize(path)
527
- .includes(nodePath.normalize('.gemini/policies'))) {
528
- return {
529
- isDirectory: () => true,
530
- isFile: () => false,
531
- };
532
- }
533
- return actualFs.stat(path, options);
534
- });
535
- vi.doMock('node:fs/promises', () => ({
536
- ...actualFs,
537
- default: {
538
- ...actualFs,
539
- readFile: mockReadFile,
540
- readdir: mockReaddir,
541
- stat: mockStat,
542
- },
543
- readFile: mockReadFile,
544
- readdir: mockReaddir,
545
- stat: mockStat,
546
- }));
547
- vi.resetModules();
548
- const { createPolicyEngineConfig } = await import('./config.js');
549
- const settings = {};
550
- const config = await createPolicyEngineConfig(settings, ApprovalMode.DEFAULT, '/tmp/mock/default/policies');
551
- const rule = config.rules?.find((r) => r.toolName === 'write_file' && r.decision === PolicyDecision.ALLOW);
552
- expect(rule).toBeDefined();
351
+ `);
352
+ const config = await createPolicyEngineConfig({}, ApprovalMode.DEFAULT, MOCK_DEFAULT_DIR);
353
+ expect(config.rules?.some((r) => r.toolName === 'write_file' && r.decision === PolicyDecision.ALLOW)).toBe(true);
553
354
  const checker = config.checkers?.find((c) => c.toolName === 'write_file' && c.checker.type === 'in-process');
554
- expect(checker).toBeDefined();
555
- expect(checker?.checker.type).toBe('in-process');
556
355
  expect(checker?.checker.name).toBe(InProcessCheckerType.ALLOWED_PATH);
557
- expect(checker?.checker.required_context).toEqual(['environment']);
558
- vi.doUnmock('node:fs/promises');
559
356
  });
560
357
  it('should reject invalid in-process checker names', async () => {
561
- const actualFs = await vi.importActual('node:fs/promises');
562
- const mockReaddir = vi.fn(async (path, options) => {
563
- if (typeof path === 'string' &&
564
- nodePath
565
- .normalize(path)
566
- .includes(nodePath.normalize('.gemini/policies'))) {
567
- return [
568
- {
569
- name: 'invalid_safety.toml',
570
- isFile: () => true,
571
- isDirectory: () => false,
572
- },
573
- ];
574
- }
575
- return actualFs.readdir(path, options);
576
- });
577
- const mockReadFile = vi.fn(async (path, options) => {
578
- if (typeof path === 'string' &&
579
- nodePath
580
- .normalize(path)
581
- .includes(nodePath.normalize('.gemini/policies/invalid_safety.toml'))) {
582
- return `
358
+ mockPolicyFile(nodePath.join(MOCK_DEFAULT_DIR, 'invalid_safety.toml'), `
583
359
  [[rule]]
584
360
  toolName = "write_file"
585
361
  decision = "allow"
@@ -591,75 +367,20 @@ priority = 10
591
367
  [safety_checker.checker]
592
368
  type = "in-process"
593
369
  name = "invalid-name"
594
- `;
595
- }
596
- return actualFs.readFile(path, options);
597
- });
598
- const mockStat = vi.fn(async (path, options) => {
599
- if (typeof path === 'string' &&
600
- nodePath
601
- .normalize(path)
602
- .includes(nodePath.normalize('.gemini/policies'))) {
603
- return {
604
- isDirectory: () => true,
605
- isFile: () => false,
606
- };
607
- }
608
- return actualFs.stat(path, options);
609
- });
610
- vi.doMock('node:fs/promises', () => ({
611
- ...actualFs,
612
- default: {
613
- ...actualFs,
614
- readFile: mockReadFile,
615
- readdir: mockReaddir,
616
- stat: mockStat,
617
- },
618
- readFile: mockReadFile,
619
- readdir: mockReaddir,
620
- stat: mockStat,
621
- }));
622
- vi.resetModules();
623
- const { createPolicyEngineConfig } = await import('./config.js');
624
- const settings = {};
625
- const config = await createPolicyEngineConfig(settings, ApprovalMode.DEFAULT, '/tmp/mock/default/policies');
626
- // The rule should be rejected because 'invalid-name' is not in the enum
627
- const rule = config.rules?.find((r) => r.toolName === 'write_file');
628
- expect(rule).toBeUndefined();
629
- vi.doUnmock('node:fs/promises');
370
+ `);
371
+ const config = await createPolicyEngineConfig({}, ApprovalMode.DEFAULT, MOCK_DEFAULT_DIR);
372
+ expect(config.rules?.find((r) => r.toolName === 'write_file')).toBeUndefined();
630
373
  });
631
374
  it('should have default ASK_USER rule for discovered tools', async () => {
632
- vi.resetModules();
633
- vi.doUnmock('node:fs/promises');
634
- const { createPolicyEngineConfig: createConfig } = await import('./config.js');
635
- // Re-mock Storage after resetModules because it was reloaded
636
- const { Storage: FreshStorage } = await import('../config/storage.js');
637
- vi.spyOn(FreshStorage, 'getUserPoliciesDir').mockReturnValue('/non/existent/user/policies');
638
- vi.spyOn(FreshStorage, 'getSystemPoliciesDir').mockReturnValue('/non/existent/system/policies');
639
- const settings = {};
640
- // Use default policy dir to load real discovered.toml
641
- const config = await createConfig(settings, ApprovalMode.DEFAULT);
375
+ const config = await createPolicyEngineConfig({}, ApprovalMode.DEFAULT);
642
376
  const discoveredRule = config.rules?.find((r) => r.toolName === 'discovered_tool_*' &&
643
377
  r.decision === PolicyDecision.ASK_USER);
644
378
  expect(discoveredRule).toBeDefined();
645
- // Priority 10 in default tier → 1.010
646
379
  expect(discoveredRule?.priority).toBeCloseTo(1.01, 5);
647
380
  });
648
381
  it('should normalize legacy "ShellTool" alias to "run_shell_command"', async () => {
649
- vi.resetModules();
650
- // Mock fs to return empty for policies
651
- const actualFs = await vi.importActual('node:fs/promises');
652
- const mockReaddir = vi.fn(async () => []);
653
- vi.doMock('node:fs/promises', () => ({
654
- ...actualFs,
655
- default: { ...actualFs, readdir: mockReaddir },
656
- readdir: mockReaddir,
657
- }));
658
- const { createPolicyEngineConfig } = await import('./config.js');
659
- const settings = {
660
- tools: { allowed: ['ShellTool'] },
661
- };
662
- const config = await createPolicyEngineConfig(settings, ApprovalMode.DEFAULT, '/tmp/mock/default/policies');
382
+ vi.mocked(fs.readdir).mockResolvedValue([]);
383
+ const config = await createPolicyEngineConfig({ tools: { allowed: ['ShellTool'] } }, ApprovalMode.DEFAULT, MOCK_DEFAULT_DIR);
663
384
  const rule = config.rules?.find((r) => r.toolName === 'run_shell_command' &&
664
385
  r.decision === PolicyDecision.ALLOW);
665
386
  expect(rule).toBeDefined();
@@ -667,34 +388,9 @@ name = "invalid-name"
667
388
  vi.doUnmock('node:fs/promises');
668
389
  });
669
390
  it('should allow overriding Plan Mode deny with user policy', async () => {
670
- const actualFs = await vi.importActual('node:fs/promises');
671
- const mockReaddir = vi.fn(async (path, options) => {
672
- const normalizedPath = nodePath.normalize(path.toString());
673
- if (normalizedPath.includes('gemini-cli-test/user/policies')) {
674
- return [
675
- {
676
- name: 'user-plan.toml',
677
- isFile: () => true,
678
- isDirectory: () => false,
679
- },
680
- ];
681
- }
682
- return actualFs.readdir(path, options);
683
- });
684
- const mockStat = vi.fn(async (path, options) => {
685
- const normalizedPath = nodePath.normalize(path.toString());
686
- if (normalizedPath.includes('gemini-cli-test/user/policies')) {
687
- return {
688
- isDirectory: () => true,
689
- isFile: () => false,
690
- };
691
- }
692
- return actualFs.stat(path, options);
693
- });
694
- const mockReadFile = vi.fn(async (path, options) => {
695
- const normalizedPath = nodePath.normalize(path.toString());
696
- if (normalizedPath.includes('user-plan.toml')) {
697
- return `
391
+ const userPolicyDir = '/tmp/gemini-cli-test/user/policies';
392
+ vi.spyOn(Storage, 'getUserPoliciesDir').mockReturnValue(userPolicyDir);
393
+ mockPolicyFile(nodePath.join(userPolicyDir, 'user-plan.toml'), `
698
394
  [[rule]]
699
395
  toolName = "run_shell_command"
700
396
  commandPrefix = ["git status", "git diff"]
@@ -707,53 +403,77 @@ toolName = "codebase_investigator"
707
403
  decision = "allow"
708
404
  priority = 100
709
405
  modes = ["plan"]
710
- `;
711
- }
712
- return actualFs.readFile(path, options);
713
- });
714
- vi.doMock('node:fs/promises', () => ({
715
- ...actualFs,
716
- default: {
717
- ...actualFs,
718
- readFile: mockReadFile,
719
- readdir: mockReaddir,
720
- stat: mockStat,
721
- },
722
- readFile: mockReadFile,
723
- readdir: mockReaddir,
724
- stat: mockStat,
725
- }));
726
- vi.resetModules();
727
- // Robustly mock Storage using doMock to ensure it persists through imports in config.js
728
- vi.doMock('../config/storage.js', async () => {
729
- const actual = await vi.importActual('../config/storage.js');
730
- class MockStorage extends actual.Storage {
731
- static getUserPoliciesDir() {
732
- return '/tmp/gemini-cli-test/user/policies';
733
- }
734
- static getSystemPoliciesDir() {
735
- return '/tmp/gemini-cli-test/system/policies';
736
- }
737
- }
738
- return { ...actual, Storage: MockStorage };
739
- });
740
- const { createPolicyEngineConfig } = await import('./config.js');
741
- const settings = {};
742
- const config = await createPolicyEngineConfig(settings, ApprovalMode.PLAN, nodePath.join(__dirname, 'policies'));
406
+ `);
407
+ const config = await createPolicyEngineConfig({}, ApprovalMode.PLAN, nodePath.join(__dirname, 'policies'));
743
408
  const shellRules = config.rules?.filter((r) => r.toolName === 'run_shell_command' &&
744
- r.decision === PolicyDecision.ALLOW &&
745
- r.modes?.includes(ApprovalMode.PLAN) &&
746
- r.argsPattern);
747
- expect(shellRules).toHaveLength(2);
748
- expect(shellRules?.some((r) => r.argsPattern?.test('{"command":"git status"}'))).toBe(true);
749
- expect(shellRules?.some((r) => r.argsPattern?.test('{"command":"git diff"}'))).toBe(true);
750
- expect(shellRules?.every((r) => !r.argsPattern?.test('{"command":"git commit"}'))).toBe(true);
751
- const subagentRule = config.rules?.find((r) => r.toolName === 'codebase_investigator' &&
752
409
  r.decision === PolicyDecision.ALLOW &&
753
410
  r.modes?.includes(ApprovalMode.PLAN));
411
+ expect(shellRules?.length).toBeGreaterThan(0);
412
+ shellRules?.forEach((r) => expect(r.priority).toBeCloseTo(4.1, 5));
413
+ const subagentRule = config.rules?.find((r) => r.toolName === 'codebase_investigator' &&
414
+ r.decision === PolicyDecision.ALLOW);
754
415
  expect(subagentRule).toBeDefined();
755
416
  expect(subagentRule?.priority).toBeCloseTo(4.1, 5);
756
- vi.doUnmock('node:fs/promises');
417
+ });
418
+ it('should deduplicate security warnings when called multiple times', async () => {
419
+ const systemPoliciesDir = '/tmp/gemini-cli-test/system/policies';
420
+ vi.spyOn(Storage, 'getSystemPoliciesDir').mockReturnValue(systemPoliciesDir);
421
+ vi.mocked(fs.readdir).mockImplementation(async (path) => {
422
+ if (nodePath.resolve(path.toString()) === systemPoliciesDir) {
423
+ return ['policy.toml'];
424
+ }
425
+ return [];
426
+ });
427
+ const feedbackSpy = vi
428
+ .spyOn(coreEvents, 'emitFeedback')
429
+ .mockImplementation(() => { });
430
+ // First call
431
+ await createPolicyEngineConfig({ adminPolicyPaths: ['/tmp/other/admin/policies'] }, ApprovalMode.DEFAULT);
432
+ expect(feedbackSpy).toHaveBeenCalledWith('warning', expect.stringContaining('Ignoring --admin-policy'));
433
+ const count = feedbackSpy.mock.calls.length;
434
+ // Second call
435
+ await createPolicyEngineConfig({ adminPolicyPaths: ['/tmp/other/admin/policies'] }, ApprovalMode.DEFAULT);
436
+ expect(feedbackSpy.mock.calls.length).toBe(count);
437
+ feedbackSpy.mockRestore();
438
+ });
439
+ });
440
+ describe('getPolicyDirectories', () => {
441
+ const USER_POLICIES_DIR = '/mock/user/policies';
442
+ const SYSTEM_POLICIES_DIR = '/mock/system/policies';
443
+ beforeEach(() => {
444
+ vi.spyOn(Storage, 'getUserPoliciesDir').mockReturnValue(USER_POLICIES_DIR);
445
+ vi.spyOn(Storage, 'getSystemPoliciesDir').mockReturnValue(SYSTEM_POLICIES_DIR);
446
+ });
447
+ it('should include default user policies directory when policyPaths is undefined', () => {
448
+ const dirs = getPolicyDirectories();
449
+ expect(dirs).toContain(USER_POLICIES_DIR);
450
+ });
451
+ it('should include default user policies directory when policyPaths is an empty array', () => {
452
+ // This is the specific case that regressed
453
+ const dirs = getPolicyDirectories(undefined, []);
454
+ expect(dirs).toContain(USER_POLICIES_DIR);
455
+ });
456
+ it('should replace default user policies directory when policyPaths has entries', () => {
457
+ const customPath = '/custom/policies';
458
+ const dirs = getPolicyDirectories(undefined, [customPath]);
459
+ expect(dirs).toContain(customPath);
460
+ expect(dirs).not.toContain(USER_POLICIES_DIR);
461
+ });
462
+ it('should include all tiers in correct order', () => {
463
+ const defaultDir = '/default/policies';
464
+ const workspaceDir = '/workspace/policies';
465
+ const adminPath = '/admin/extra/policies';
466
+ const userPath = '/user/custom/policies';
467
+ const dirs = getPolicyDirectories(defaultDir, [userPath], workspaceDir, [
468
+ adminPath,
469
+ ]);
470
+ // Order should be Admin -> User -> Workspace -> Default
471
+ // getPolicyDirectories returns them in that order (which is then reversed by the loader)
472
+ expect(dirs[0]).toBe(SYSTEM_POLICIES_DIR);
473
+ expect(dirs[1]).toBe(adminPath);
474
+ expect(dirs[2]).toBe(userPath);
475
+ expect(dirs[3]).toBe(workspaceDir);
476
+ expect(dirs[4]).toBe(defaultDir);
757
477
  });
758
478
  });
759
479
  //# sourceMappingURL=config.test.js.map