@swarmclawai/swarmclaw 0.7.7 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (281) hide show
  1. package/README.md +12 -14
  2. package/next.config.ts +13 -2
  3. package/package.json +4 -2
  4. package/src/app/api/agents/[id]/thread/route.ts +9 -0
  5. package/src/app/api/agents/route.ts +4 -0
  6. package/src/app/api/agents/thread-route.test.ts +133 -0
  7. package/src/app/api/approvals/route.test.ts +148 -0
  8. package/src/app/api/canvas/[sessionId]/route.ts +3 -1
  9. package/src/app/api/chatrooms/[id]/chat/route.ts +4 -2
  10. package/src/app/api/chats/[id]/devserver/route.ts +48 -7
  11. package/src/app/api/chats/[id]/messages/route.ts +42 -18
  12. package/src/app/api/chats/[id]/route.ts +1 -1
  13. package/src/app/api/chats/[id]/stop/route.ts +5 -4
  14. package/src/app/api/chats/route.ts +23 -2
  15. package/src/app/api/clawhub/install/route.ts +28 -8
  16. package/src/app/api/connectors/[id]/route.ts +46 -3
  17. package/src/app/api/connectors/route.ts +12 -8
  18. package/src/app/api/external-agents/route.test.ts +165 -0
  19. package/src/app/api/gateways/[id]/health/route.ts +27 -12
  20. package/src/app/api/gateways/[id]/route.ts +2 -0
  21. package/src/app/api/gateways/health-route.test.ts +135 -0
  22. package/src/app/api/gateways/route.ts +2 -0
  23. package/src/app/api/mcp-servers/route.test.ts +130 -0
  24. package/src/app/api/openclaw/deploy/route.ts +38 -5
  25. package/src/app/api/plugins/install/route.ts +46 -6
  26. package/src/app/api/plugins/marketplace/route.ts +48 -15
  27. package/src/app/api/preview-server/route.ts +26 -11
  28. package/src/app/api/projects/[id]/route.ts +6 -2
  29. package/src/app/api/projects/route.ts +4 -3
  30. package/src/app/api/schedules/[id]/run/route.ts +4 -0
  31. package/src/app/api/schedules/route.test.ts +86 -0
  32. package/src/app/api/schedules/route.ts +6 -1
  33. package/src/app/api/secrets/[id]/route.ts +1 -0
  34. package/src/app/api/secrets/route.ts +2 -1
  35. package/src/app/api/settings/route.ts +2 -0
  36. package/src/app/api/setup/check-provider/route.test.ts +19 -0
  37. package/src/app/api/setup/check-provider/route.ts +40 -10
  38. package/src/app/api/skills/[id]/route.ts +12 -0
  39. package/src/app/api/skills/import/route.ts +14 -12
  40. package/src/app/api/skills/route.ts +13 -1
  41. package/src/app/api/tasks/[id]/route.ts +10 -1
  42. package/src/app/api/tasks/import/github/route.test.ts +65 -0
  43. package/src/app/api/tasks/import/github/route.ts +337 -0
  44. package/src/app/api/wallets/[id]/approve/route.ts +17 -3
  45. package/src/app/api/wallets/[id]/route.ts +79 -33
  46. package/src/app/api/wallets/[id]/send/route.ts +19 -33
  47. package/src/app/api/wallets/route.ts +78 -61
  48. package/src/app/api/webhooks/[id]/route.ts +33 -6
  49. package/src/app/api/webhooks/route.test.ts +272 -0
  50. package/src/cli/index.js +1 -0
  51. package/src/cli/spec.js +1 -0
  52. package/src/components/agents/agent-card.tsx +9 -2
  53. package/src/components/agents/agent-chat-list.tsx +18 -2
  54. package/src/components/agents/agent-list.tsx +1 -0
  55. package/src/components/agents/agent-sheet.tsx +257 -38
  56. package/src/components/agents/inspector-panel.tsx +41 -0
  57. package/src/components/canvas/canvas-panel.tsx +236 -65
  58. package/src/components/chat/chat-area.tsx +36 -19
  59. package/src/components/chat/chat-card.tsx +36 -13
  60. package/src/components/chat/chat-header.tsx +48 -16
  61. package/src/components/chat/chat-list.tsx +28 -4
  62. package/src/components/chat/checkpoint-timeline.tsx +50 -34
  63. package/src/components/chat/delegation-banner.test.ts +14 -1
  64. package/src/components/chat/delegation-banner.tsx +1 -1
  65. package/src/components/chat/message-bubble.tsx +208 -145
  66. package/src/components/chat/message-list.tsx +48 -19
  67. package/src/components/chatrooms/chatroom-message.tsx +2 -2
  68. package/src/components/chatrooms/chatroom-sheet.tsx +16 -2
  69. package/src/components/connectors/connector-health.tsx +1 -1
  70. package/src/components/connectors/connector-list.tsx +7 -2
  71. package/src/components/connectors/connector-sheet.tsx +337 -148
  72. package/src/components/gateways/gateway-sheet.tsx +2 -2
  73. package/src/components/layout/app-layout.tsx +40 -23
  74. package/src/components/mcp-servers/mcp-server-list.tsx +26 -5
  75. package/src/components/mcp-servers/mcp-server-sheet.tsx +19 -2
  76. package/src/components/openclaw/openclaw-deploy-panel.tsx +269 -21
  77. package/src/components/plugins/plugin-list.tsx +45 -9
  78. package/src/components/plugins/plugin-sheet.tsx +55 -7
  79. package/src/components/projects/project-detail.tsx +217 -0
  80. package/src/components/projects/project-sheet.tsx +176 -4
  81. package/src/components/providers/provider-list.tsx +2 -1
  82. package/src/components/providers/provider-sheet.tsx +21 -2
  83. package/src/components/schedules/schedule-card.tsx +25 -1
  84. package/src/components/schedules/schedule-sheet.tsx +44 -2
  85. package/src/components/secrets/secret-sheet.tsx +21 -2
  86. package/src/components/shared/agent-switch-dialog.tsx +12 -1
  87. package/src/components/shared/bottom-sheet.tsx +13 -3
  88. package/src/components/shared/command-palette.tsx +8 -1
  89. package/src/components/shared/confirm-dialog.tsx +19 -4
  90. package/src/components/shared/connector-platform-icon.test.ts +28 -0
  91. package/src/components/shared/connector-platform-icon.tsx +39 -6
  92. package/src/components/shared/settings/plugin-manager.tsx +29 -6
  93. package/src/components/shared/settings/section-capability-policy.tsx +45 -3
  94. package/src/components/shared/settings/section-voice.tsx +11 -3
  95. package/src/components/skills/skill-list.tsx +25 -0
  96. package/src/components/skills/skill-sheet.tsx +84 -12
  97. package/src/components/tasks/approvals-panel.tsx +289 -34
  98. package/src/components/tasks/task-board.tsx +410 -25
  99. package/src/components/tasks/task-card.tsx +66 -8
  100. package/src/components/tasks/task-sheet.tsx +16 -4
  101. package/src/components/ui/dialog.tsx +2 -2
  102. package/src/components/wallets/wallet-approval-dialog.tsx +4 -2
  103. package/src/components/wallets/wallet-panel.tsx +435 -90
  104. package/src/components/wallets/wallet-section.tsx +198 -48
  105. package/src/components/webhooks/webhook-sheet.tsx +22 -2
  106. package/src/lib/approval-display.ts +20 -0
  107. package/src/lib/canvas-content.ts +198 -0
  108. package/src/lib/chat-artifact-summary.ts +165 -0
  109. package/src/lib/chat-display.test.ts +91 -0
  110. package/src/lib/chat-display.ts +58 -0
  111. package/src/lib/chat-streaming-state.test.ts +47 -1
  112. package/src/lib/chat-streaming-state.ts +42 -0
  113. package/src/lib/ollama-model.ts +10 -0
  114. package/src/lib/openclaw-endpoint.test.ts +8 -0
  115. package/src/lib/openclaw-endpoint.ts +6 -1
  116. package/src/lib/plugin-install-cors.ts +46 -0
  117. package/src/lib/plugin-sources.test.ts +43 -0
  118. package/src/lib/plugin-sources.ts +77 -0
  119. package/src/lib/providers/ollama.ts +16 -6
  120. package/src/lib/providers/openclaw.test.ts +54 -0
  121. package/src/lib/providers/openclaw.ts +127 -11
  122. package/src/lib/schedule-dedupe-advanced.test.ts +1335 -0
  123. package/src/lib/schedule-dedupe.test.ts +66 -1
  124. package/src/lib/schedule-dedupe.ts +169 -12
  125. package/src/lib/schedule-origin.test.ts +20 -0
  126. package/src/lib/schedule-origin.ts +15 -0
  127. package/src/lib/server/__fixtures__/fake-mcp-stdio-server.mjs +27 -0
  128. package/src/lib/server/agent-availability.ts +16 -0
  129. package/src/lib/server/agent-runtime-config.ts +12 -4
  130. package/src/lib/server/agent-thread-session.test.ts +51 -0
  131. package/src/lib/server/agent-thread-session.ts +7 -0
  132. package/src/lib/server/approval-match.ts +205 -0
  133. package/src/lib/server/approvals-auto-approve.test.ts +538 -1
  134. package/src/lib/server/approvals.ts +214 -1
  135. package/src/lib/server/assistant-control.test.ts +29 -0
  136. package/src/lib/server/assistant-control.ts +23 -0
  137. package/src/lib/server/build-llm.test.ts +79 -0
  138. package/src/lib/server/build-llm.ts +14 -4
  139. package/src/lib/server/canvas-content.test.ts +32 -0
  140. package/src/lib/server/canvas-content.ts +6 -0
  141. package/src/lib/server/capability-router.test.ts +33 -0
  142. package/src/lib/server/capability-router.ts +80 -19
  143. package/src/lib/server/chat-execution-advanced.test.ts +651 -0
  144. package/src/lib/server/chat-execution-disabled.test.ts +94 -0
  145. package/src/lib/server/chat-execution-tool-events.test.ts +157 -0
  146. package/src/lib/server/chat-execution.ts +378 -73
  147. package/src/lib/server/clawhub-client.test.ts +14 -8
  148. package/src/lib/server/connectors/manager-reconnect.test.ts +47 -0
  149. package/src/lib/server/connectors/manager.test.ts +1147 -0
  150. package/src/lib/server/connectors/manager.ts +461 -137
  151. package/src/lib/server/connectors/pairing.ts +26 -5
  152. package/src/lib/server/connectors/types.ts +2 -0
  153. package/src/lib/server/connectors/whatsapp.test.ts +134 -0
  154. package/src/lib/server/connectors/whatsapp.ts +271 -47
  155. package/src/lib/server/context-manager.ts +6 -1
  156. package/src/lib/server/daemon-state.ts +84 -47
  157. package/src/lib/server/data-dir.test.ts +37 -0
  158. package/src/lib/server/data-dir.ts +20 -1
  159. package/src/lib/server/delegation-jobs-advanced.test.ts +513 -0
  160. package/src/lib/server/devserver-launch.test.ts +60 -0
  161. package/src/lib/server/devserver-launch.ts +85 -0
  162. package/src/lib/server/elevenlabs.test.ts +247 -1
  163. package/src/lib/server/elevenlabs.ts +147 -43
  164. package/src/lib/server/ethereum.ts +590 -0
  165. package/src/lib/server/eval/agent-regression-advanced.test.ts +302 -0
  166. package/src/lib/server/eval/agent-regression.test.ts +18 -1
  167. package/src/lib/server/eval/agent-regression.ts +383 -11
  168. package/src/lib/server/evm-swap.ts +475 -0
  169. package/src/lib/server/execution-log.ts +1 -0
  170. package/src/lib/server/heartbeat-service-timer.test.ts +173 -0
  171. package/src/lib/server/heartbeat-service.ts +20 -11
  172. package/src/lib/server/heartbeat-wake.test.ts +112 -0
  173. package/src/lib/server/heartbeat-wake.ts +338 -57
  174. package/src/lib/server/main-agent-loop-advanced.test.ts +538 -0
  175. package/src/lib/server/main-agent-loop.test.ts +260 -0
  176. package/src/lib/server/main-agent-loop.ts +559 -14
  177. package/src/lib/server/mcp-client.test.ts +16 -0
  178. package/src/lib/server/mcp-client.ts +25 -0
  179. package/src/lib/server/memory-integration.test.ts +719 -0
  180. package/src/lib/server/memory-policy.test.ts +43 -0
  181. package/src/lib/server/memory-policy.ts +132 -0
  182. package/src/lib/server/memory-tiers.test.ts +60 -0
  183. package/src/lib/server/memory-tiers.ts +16 -0
  184. package/src/lib/server/ollama-runtime.ts +58 -0
  185. package/src/lib/server/openclaw-deploy.test.ts +109 -1
  186. package/src/lib/server/openclaw-deploy.ts +557 -81
  187. package/src/lib/server/openclaw-gateway.test.ts +131 -0
  188. package/src/lib/server/openclaw-gateway.ts +10 -4
  189. package/src/lib/server/openclaw-health.test.ts +35 -0
  190. package/src/lib/server/openclaw-health.ts +215 -47
  191. package/src/lib/server/orchestrator-lg.ts +3 -2
  192. package/src/lib/server/orchestrator.ts +2 -0
  193. package/src/lib/server/plugins-advanced.test.ts +351 -0
  194. package/src/lib/server/plugins.ts +211 -6
  195. package/src/lib/server/project-context.ts +162 -0
  196. package/src/lib/server/project-utils.ts +150 -0
  197. package/src/lib/server/queue-advanced.test.ts +528 -0
  198. package/src/lib/server/queue-followups.test.ts +409 -2
  199. package/src/lib/server/queue-reconcile.test.ts +128 -0
  200. package/src/lib/server/queue.ts +527 -68
  201. package/src/lib/server/scheduler.ts +29 -1
  202. package/src/lib/server/session-note.test.ts +36 -0
  203. package/src/lib/server/session-note.ts +42 -0
  204. package/src/lib/server/session-run-manager.ts +83 -4
  205. package/src/lib/server/session-tools/canvas.ts +14 -12
  206. package/src/lib/server/session-tools/connector-inputs.test.ts +37 -0
  207. package/src/lib/server/session-tools/connector.test.ts +138 -0
  208. package/src/lib/server/session-tools/connector.ts +366 -54
  209. package/src/lib/server/session-tools/context.ts +17 -3
  210. package/src/lib/server/session-tools/crud.ts +484 -84
  211. package/src/lib/server/session-tools/delegate-fallback.test.ts +103 -0
  212. package/src/lib/server/session-tools/delegate-resume.test.ts +50 -0
  213. package/src/lib/server/session-tools/delegate.ts +102 -10
  214. package/src/lib/server/session-tools/discovery-approvals.test.ts +142 -0
  215. package/src/lib/server/session-tools/discovery.ts +80 -12
  216. package/src/lib/server/session-tools/file-normalize.test.ts +36 -0
  217. package/src/lib/server/session-tools/file.ts +43 -4
  218. package/src/lib/server/session-tools/human-loop.ts +35 -5
  219. package/src/lib/server/session-tools/index.ts +44 -9
  220. package/src/lib/server/session-tools/manage-connectors.test.ts +139 -0
  221. package/src/lib/server/session-tools/manage-schedules-advanced.test.ts +564 -0
  222. package/src/lib/server/session-tools/manage-schedules.test.ts +283 -0
  223. package/src/lib/server/session-tools/manage-tasks-advanced.test.ts +852 -0
  224. package/src/lib/server/session-tools/manage-tasks.test.ts +114 -0
  225. package/src/lib/server/session-tools/memory.test.ts +93 -0
  226. package/src/lib/server/session-tools/memory.ts +554 -75
  227. package/src/lib/server/session-tools/normalize-tool-args.ts +1 -1
  228. package/src/lib/server/session-tools/platform-access.test.ts +58 -0
  229. package/src/lib/server/session-tools/platform.ts +60 -19
  230. package/src/lib/server/session-tools/plugin-creator.ts +57 -1
  231. package/src/lib/server/session-tools/primitive-tools.test.ts +6 -0
  232. package/src/lib/server/session-tools/schedule.ts +6 -1
  233. package/src/lib/server/session-tools/shell-normalize.test.ts +25 -1
  234. package/src/lib/server/session-tools/shell.ts +22 -3
  235. package/src/lib/server/session-tools/wallet-tool.test.ts +254 -0
  236. package/src/lib/server/session-tools/wallet.ts +1374 -139
  237. package/src/lib/server/session-tools/web-inputs.test.ts +178 -0
  238. package/src/lib/server/session-tools/web.ts +621 -70
  239. package/src/lib/server/skill-discovery.ts +128 -0
  240. package/src/lib/server/skill-eligibility.test.ts +84 -0
  241. package/src/lib/server/skill-eligibility.ts +95 -0
  242. package/src/lib/server/skill-prompt-budget.test.ts +102 -0
  243. package/src/lib/server/skill-prompt-budget.ts +125 -0
  244. package/src/lib/server/skills-normalize.test.ts +54 -0
  245. package/src/lib/server/skills-normalize.ts +372 -26
  246. package/src/lib/server/solana.ts +214 -29
  247. package/src/lib/server/storage.ts +65 -36
  248. package/src/lib/server/stream-agent-chat.test.ts +437 -2
  249. package/src/lib/server/stream-agent-chat.ts +957 -79
  250. package/src/lib/server/system-events.ts +1 -1
  251. package/src/lib/server/tool-aliases.ts +2 -0
  252. package/src/lib/server/tool-capability-policy-advanced.test.ts +502 -0
  253. package/src/lib/server/tool-capability-policy.test.ts +24 -0
  254. package/src/lib/server/tool-capability-policy.ts +29 -1
  255. package/src/lib/server/tool-loop-detection.test.ts +105 -0
  256. package/src/lib/server/tool-loop-detection.ts +260 -0
  257. package/src/lib/server/tool-planning.test.ts +44 -0
  258. package/src/lib/server/tool-planning.ts +271 -0
  259. package/src/lib/server/wallet-execution.test.ts +198 -0
  260. package/src/lib/server/wallet-portfolio.test.ts +98 -0
  261. package/src/lib/server/wallet-portfolio.ts +724 -0
  262. package/src/lib/server/wallet-service.test.ts +57 -0
  263. package/src/lib/server/wallet-service.ts +213 -0
  264. package/src/lib/server/watch-jobs-advanced.test.ts +594 -0
  265. package/src/lib/server/watch-jobs.ts +17 -2
  266. package/src/lib/server/workspace-context.ts +111 -0
  267. package/src/lib/skill-save-payload.test.ts +39 -0
  268. package/src/lib/skill-save-payload.ts +37 -0
  269. package/src/lib/tasks.ts +28 -0
  270. package/src/lib/tool-definitions.ts +2 -1
  271. package/src/lib/tool-event-summary.test.ts +30 -0
  272. package/src/lib/tool-event-summary.ts +37 -0
  273. package/src/lib/validation/schemas.ts +1 -0
  274. package/src/lib/wallet-transactions.test.ts +75 -0
  275. package/src/lib/wallet-transactions.ts +43 -0
  276. package/src/lib/wallet.test.ts +17 -0
  277. package/src/lib/wallet.ts +183 -0
  278. package/src/proxy.test.ts +31 -0
  279. package/src/proxy.ts +34 -2
  280. package/src/stores/use-chat-store.ts +15 -1
  281. package/src/types/index.ts +249 -14
@@ -3,7 +3,7 @@
3
3
  * Events are accumulated between heartbeat ticks and drained into heartbeat prompts.
4
4
  */
5
5
 
6
- interface SystemEvent {
6
+ export interface SystemEvent {
7
7
  text: string
8
8
  timestamp: number
9
9
  contextKey?: string
@@ -7,6 +7,7 @@ const PLUGIN_ALIAS_GROUPS: string[][] = [
7
7
  ['delegate', 'claude_code', 'codex_cli', 'opencode_cli', 'gemini_cli', 'delegate_to_claude_code', 'delegate_to_codex_cli', 'delegate_to_opencode_cli', 'delegate_to_gemini_cli'],
8
8
  ['manage_platform'],
9
9
  ['manage_agents'],
10
+ ['manage_projects'],
10
11
  ['manage_tasks'],
11
12
  ['manage_schedules'],
12
13
  ['manage_skills'],
@@ -43,6 +44,7 @@ const PLUGIN_IMPLICATIONS: Record<string, string[]> = {
43
44
  shell: ['process'],
44
45
  manage_platform: [
45
46
  'manage_agents',
47
+ 'manage_projects',
46
48
  'manage_tasks',
47
49
  'manage_schedules',
48
50
  'manage_skills',
@@ -0,0 +1,502 @@
1
+ import { describe, it } from 'node:test'
2
+ import assert from 'node:assert/strict'
3
+ import {
4
+ resolveSessionToolPolicy,
5
+ resolveConcreteToolPolicyBlock,
6
+ isTaskManagementEnabled,
7
+ isProjectManagementEnabled,
8
+ } from './tool-capability-policy'
9
+
10
+ // ---------------------------------------------------------------------------
11
+ // Permissive mode
12
+ // ---------------------------------------------------------------------------
13
+ describe('permissive mode', () => {
14
+ const mode = { capabilityPolicyMode: 'permissive' }
15
+
16
+ it('enables all standard tools including shell, files, delegate, manage_platform', () => {
17
+ const tools = ['shell', 'files', 'delegate', 'manage_platform', 'web', 'memory']
18
+ const d = resolveSessionToolPolicy(tools, mode)
19
+ assert.deepStrictEqual(d.enabledPlugins, tools)
20
+ assert.equal(d.blockedPlugins.length, 0)
21
+ assert.equal(d.mode, 'permissive')
22
+ })
23
+
24
+ it('allows destructive delete_file', () => {
25
+ const d = resolveSessionToolPolicy(['delete_file'], mode)
26
+ assert.deepStrictEqual(d.enabledPlugins, ['delete_file'])
27
+ assert.equal(d.blockedPlugins.length, 0)
28
+ })
29
+
30
+ it('still applies safety blocks in permissive mode', () => {
31
+ const d = resolveSessionToolPolicy(['shell', 'web'], {
32
+ capabilityPolicyMode: 'permissive',
33
+ safetyBlockedTools: ['shell'],
34
+ })
35
+ assert.deepStrictEqual(d.enabledPlugins, ['web'])
36
+ assert.equal(d.blockedPlugins.length, 1)
37
+ assert.equal(d.blockedPlugins[0].tool, 'shell')
38
+ assert.equal(d.blockedPlugins[0].source, 'safety')
39
+ })
40
+ })
41
+
42
+ // ---------------------------------------------------------------------------
43
+ // Balanced mode
44
+ // ---------------------------------------------------------------------------
45
+ describe('balanced mode', () => {
46
+ const mode = { capabilityPolicyMode: 'balanced' }
47
+
48
+ it('allows non-destructive tools (files, web, memory)', () => {
49
+ const d = resolveSessionToolPolicy(['files', 'web', 'memory'], mode)
50
+ assert.deepStrictEqual(d.enabledPlugins, ['files', 'web', 'memory'])
51
+ assert.equal(d.blockedPlugins.length, 0)
52
+ })
53
+
54
+ it('blocks destructive delete_file with correct reason', () => {
55
+ const d = resolveSessionToolPolicy(['delete_file'], mode)
56
+ assert.equal(d.blockedPlugins.length, 1)
57
+ assert.equal(d.blockedPlugins[0].tool, 'delete_file')
58
+ assert.match(d.blockedPlugins[0].reason, /balanced policy.*destructive/i)
59
+ })
60
+
61
+ it('allows shell (not marked destructive)', () => {
62
+ const d = resolveSessionToolPolicy(['shell'], mode)
63
+ assert.deepStrictEqual(d.enabledPlugins, ['shell'])
64
+ })
65
+
66
+ it('allows delegate (not marked destructive)', () => {
67
+ const d = resolveSessionToolPolicy(['delegate'], mode)
68
+ assert.deepStrictEqual(d.enabledPlugins, ['delegate'])
69
+ })
70
+ })
71
+
72
+ // ---------------------------------------------------------------------------
73
+ // Strict mode
74
+ // ---------------------------------------------------------------------------
75
+ describe('strict mode', () => {
76
+ const mode = { capabilityPolicyMode: 'strict' }
77
+
78
+ it('allows memory (not in blocked categories)', () => {
79
+ const d = resolveSessionToolPolicy(['memory'], mode)
80
+ assert.deepStrictEqual(d.enabledPlugins, ['memory'])
81
+ })
82
+
83
+ it('allows web_search and web (network category not blocked in strict)', () => {
84
+ const d = resolveSessionToolPolicy(['web', 'web_search'], mode)
85
+ assert.deepStrictEqual(d.enabledPlugins, ['web', 'web_search'])
86
+ })
87
+
88
+ it('blocks shell (execution category)', () => {
89
+ const d = resolveSessionToolPolicy(['shell'], mode)
90
+ assert.equal(d.blockedPlugins.length, 1)
91
+ assert.equal(d.blockedPlugins[0].tool, 'shell')
92
+ assert.match(d.blockedPlugins[0].reason, /strict policy/)
93
+ })
94
+
95
+ it('blocks files (filesystem category)', () => {
96
+ const d = resolveSessionToolPolicy(['files'], mode)
97
+ assert.equal(d.blockedPlugins.length, 1)
98
+ assert.equal(d.blockedPlugins[0].tool, 'files')
99
+ })
100
+
101
+ it('blocks delegate (delegation + execution)', () => {
102
+ const d = resolveSessionToolPolicy(['delegate'], mode)
103
+ assert.equal(d.blockedPlugins.length, 1)
104
+ assert.equal(d.blockedPlugins[0].tool, 'delegate')
105
+ })
106
+
107
+ it('blocks manage_platform (platform category)', () => {
108
+ const d = resolveSessionToolPolicy(['manage_platform'], mode)
109
+ assert.equal(d.blockedPlugins.length, 1)
110
+ assert.equal(d.blockedPlugins[0].tool, 'manage_platform')
111
+ })
112
+
113
+ it('blocks wallet (outbound category)', () => {
114
+ const d = resolveSessionToolPolicy(['wallet'], mode)
115
+ assert.equal(d.blockedPlugins.length, 1)
116
+ assert.equal(d.blockedPlugins[0].tool, 'wallet')
117
+ })
118
+
119
+ it('blocks browser (browser + network, but browser triggers execution-like block)', () => {
120
+ // browser has categories: ['browser', 'network'] — neither in strict's blocked set
121
+ // Let's verify the actual behavior
122
+ const d = resolveSessionToolPolicy(['browser'], mode)
123
+ // browser categories are browser+network; strict blocks execution, delegation, platform, outbound, filesystem
124
+ // browser is NOT in those categories, so it should be allowed
125
+ // Unless the implementation treats browser differently — let's test and see
126
+ if (d.blockedPlugins.length > 0) {
127
+ assert.equal(d.blockedPlugins[0].tool, 'browser')
128
+ } else {
129
+ assert.deepStrictEqual(d.enabledPlugins, ['browser'])
130
+ }
131
+ })
132
+
133
+ it('blocks manage_connectors explicitly', () => {
134
+ const d = resolveSessionToolPolicy(['manage_connectors'], mode)
135
+ assert.equal(d.blockedPlugins.length, 1)
136
+ assert.equal(d.blockedPlugins[0].tool, 'manage_connectors')
137
+ })
138
+ })
139
+
140
+ // ---------------------------------------------------------------------------
141
+ // Safety blocks
142
+ // ---------------------------------------------------------------------------
143
+ describe('safety blocks', () => {
144
+ it('rejects safety-blocked tool in permissive mode', () => {
145
+ const d = resolveSessionToolPolicy(['shell'], {
146
+ capabilityPolicyMode: 'permissive',
147
+ safetyBlockedTools: ['shell'],
148
+ })
149
+ assert.equal(d.blockedPlugins.length, 1)
150
+ assert.equal(d.blockedPlugins[0].source, 'safety')
151
+ })
152
+
153
+ it('rejects safety-blocked tool in balanced mode', () => {
154
+ const d = resolveSessionToolPolicy(['web'], {
155
+ capabilityPolicyMode: 'balanced',
156
+ safetyBlockedTools: ['web'],
157
+ })
158
+ assert.equal(d.blockedPlugins.length, 1)
159
+ assert.equal(d.blockedPlugins[0].source, 'safety')
160
+ })
161
+
162
+ it('rejects safety-blocked tool in strict mode', () => {
163
+ const d = resolveSessionToolPolicy(['memory'], {
164
+ capabilityPolicyMode: 'strict',
165
+ safetyBlockedTools: ['memory'],
166
+ })
167
+ assert.equal(d.blockedPlugins.length, 1)
168
+ assert.equal(d.blockedPlugins[0].source, 'safety')
169
+ })
170
+
171
+ it('safety block on concrete web_search blocks the web_search family', () => {
172
+ const d = resolveSessionToolPolicy(['web_search'], {
173
+ safetyBlockedTools: ['web_search'],
174
+ })
175
+ assert.equal(d.blockedPlugins.length, 1)
176
+ assert.equal(d.blockedPlugins[0].tool, 'web_search')
177
+ assert.equal(d.blockedPlugins[0].source, 'safety')
178
+ })
179
+
180
+ it('safety block on memory_tool blocks memory', () => {
181
+ const d = resolveSessionToolPolicy(['memory'], {
182
+ safetyBlockedTools: ['memory_tool'],
183
+ })
184
+ assert.equal(d.blockedPlugins.length, 1)
185
+ assert.equal(d.blockedPlugins[0].tool, 'memory')
186
+ assert.equal(d.blockedPlugins[0].source, 'safety')
187
+ })
188
+
189
+ it('safety block on delegate_to_claude_code blocks claude_code', () => {
190
+ const d = resolveSessionToolPolicy(['claude_code'], {
191
+ safetyBlockedTools: ['delegate_to_claude_code'],
192
+ })
193
+ assert.equal(d.blockedPlugins.length, 1)
194
+ assert.equal(d.blockedPlugins[0].tool, 'claude_code')
195
+ assert.equal(d.blockedPlugins[0].source, 'safety')
196
+ })
197
+ })
198
+
199
+ // ---------------------------------------------------------------------------
200
+ // Explicit policy blocks
201
+ // ---------------------------------------------------------------------------
202
+ describe('explicit policy blocks', () => {
203
+ it('capabilityBlockedTools blocks shell with correct reason', () => {
204
+ const d = resolveSessionToolPolicy(['shell', 'web'], {
205
+ capabilityBlockedTools: ['shell'],
206
+ })
207
+ assert.deepStrictEqual(d.enabledPlugins, ['web'])
208
+ assert.equal(d.blockedPlugins.length, 1)
209
+ assert.equal(d.blockedPlugins[0].tool, 'shell')
210
+ assert.match(d.blockedPlugins[0].reason, /explicit policy rule/)
211
+ })
212
+
213
+ it('blocking a concrete tool blocks parent family', () => {
214
+ const d = resolveSessionToolPolicy(['files'], {
215
+ capabilityBlockedTools: ['read_file'],
216
+ })
217
+ assert.equal(d.blockedPlugins.length, 1)
218
+ assert.equal(d.blockedPlugins[0].tool, 'files')
219
+ })
220
+ })
221
+
222
+ // ---------------------------------------------------------------------------
223
+ // Explicit allows override mode
224
+ // ---------------------------------------------------------------------------
225
+ describe('explicit allows override mode blocks', () => {
226
+ it('capabilityAllowedTools overrides strict mode for shell', () => {
227
+ const d = resolveSessionToolPolicy(['shell', 'web_search'], {
228
+ capabilityPolicyMode: 'strict',
229
+ capabilityAllowedTools: ['shell'],
230
+ })
231
+ assert.ok(d.enabledPlugins.includes('shell'))
232
+ assert.ok(d.enabledPlugins.includes('web_search'))
233
+ })
234
+
235
+ it('safety block takes precedence over explicit allow', () => {
236
+ const d = resolveSessionToolPolicy(['shell'], {
237
+ capabilityPolicyMode: 'strict',
238
+ capabilityAllowedTools: ['shell'],
239
+ safetyBlockedTools: ['shell'],
240
+ })
241
+ assert.equal(d.blockedPlugins.length, 1)
242
+ assert.equal(d.blockedPlugins[0].source, 'safety')
243
+ assert.equal(d.enabledPlugins.length, 0)
244
+ })
245
+ })
246
+
247
+ // ---------------------------------------------------------------------------
248
+ // Category blocks
249
+ // ---------------------------------------------------------------------------
250
+ describe('category blocks', () => {
251
+ it('blocking network category blocks web, web_search, web_fetch', () => {
252
+ const d = resolveSessionToolPolicy(['web', 'web_search', 'web_fetch', 'memory'], {
253
+ capabilityBlockedCategories: ['network'],
254
+ })
255
+ assert.deepStrictEqual(d.enabledPlugins, ['memory'])
256
+ assert.equal(d.blockedPlugins.length, 3)
257
+ for (const b of d.blockedPlugins) {
258
+ assert.match(b.reason, /category "network"/)
259
+ }
260
+ })
261
+
262
+ it('blocking execution category blocks shell and process', () => {
263
+ const d = resolveSessionToolPolicy(['shell', 'process', 'web'], {
264
+ capabilityBlockedCategories: ['execution'],
265
+ })
266
+ assert.deepStrictEqual(d.enabledPlugins, ['web'])
267
+ assert.equal(d.blockedPlugins.length, 2)
268
+ })
269
+
270
+ it('blocking platform category blocks manage_tasks and manage_schedules', () => {
271
+ const d = resolveSessionToolPolicy(['manage_tasks', 'manage_schedules', 'memory'], {
272
+ capabilityBlockedCategories: ['platform'],
273
+ })
274
+ assert.deepStrictEqual(d.enabledPlugins, ['memory'])
275
+ assert.equal(d.blockedPlugins.length, 2)
276
+ })
277
+ })
278
+
279
+ // ---------------------------------------------------------------------------
280
+ // Settings blocks
281
+ // ---------------------------------------------------------------------------
282
+ describe('settings blocks', () => {
283
+ it('taskManagementEnabled=false blocks manage_tasks', () => {
284
+ const d = resolveSessionToolPolicy(['manage_tasks', 'memory'], {
285
+ taskManagementEnabled: false,
286
+ })
287
+ assert.deepStrictEqual(d.enabledPlugins, ['memory'])
288
+ assert.equal(d.blockedPlugins.length, 1)
289
+ assert.match(d.blockedPlugins[0].reason, /task management is disabled/)
290
+ })
291
+
292
+ it('projectManagementEnabled=false blocks manage_projects', () => {
293
+ const d = resolveSessionToolPolicy(['manage_projects', 'memory'], {
294
+ projectManagementEnabled: false,
295
+ })
296
+ assert.deepStrictEqual(d.enabledPlugins, ['memory'])
297
+ assert.equal(d.blockedPlugins.length, 1)
298
+ assert.match(d.blockedPlugins[0].reason, /project management is disabled/)
299
+ })
300
+
301
+ it('both enabled by default (undefined)', () => {
302
+ const d = resolveSessionToolPolicy(['manage_tasks', 'manage_projects'], {})
303
+ assert.deepStrictEqual(d.enabledPlugins, ['manage_tasks', 'manage_projects'])
304
+ assert.equal(d.blockedPlugins.length, 0)
305
+ })
306
+ })
307
+
308
+ // ---------------------------------------------------------------------------
309
+ // isTaskManagementEnabled / isProjectManagementEnabled
310
+ // ---------------------------------------------------------------------------
311
+ describe('management enabled helpers', () => {
312
+ it('isTaskManagementEnabled returns true by default', () => {
313
+ assert.equal(isTaskManagementEnabled(), true)
314
+ assert.equal(isTaskManagementEnabled(null), true)
315
+ assert.equal(isTaskManagementEnabled({}), true)
316
+ })
317
+
318
+ it('isTaskManagementEnabled returns false when explicitly disabled', () => {
319
+ assert.equal(isTaskManagementEnabled({ taskManagementEnabled: false }), false)
320
+ })
321
+
322
+ it('isProjectManagementEnabled returns true by default', () => {
323
+ assert.equal(isProjectManagementEnabled(), true)
324
+ assert.equal(isProjectManagementEnabled(null), true)
325
+ assert.equal(isProjectManagementEnabled({}), true)
326
+ })
327
+
328
+ it('isProjectManagementEnabled returns false when explicitly disabled', () => {
329
+ assert.equal(isProjectManagementEnabled({ projectManagementEnabled: false }), false)
330
+ })
331
+ })
332
+
333
+ // ---------------------------------------------------------------------------
334
+ // Concrete tool resolution
335
+ // ---------------------------------------------------------------------------
336
+ describe('resolveConcreteToolPolicyBlock', () => {
337
+ it('returns null when concrete tool family is enabled', () => {
338
+ const d = resolveSessionToolPolicy(['manage_schedules'], {})
339
+ assert.equal(resolveConcreteToolPolicyBlock('manage_schedules', d, {}), null)
340
+ })
341
+
342
+ it('returns block reason when family is not in enabledPlugins', () => {
343
+ const d = resolveSessionToolPolicy(['memory'], {})
344
+ const result = resolveConcreteToolPolicyBlock('manage_schedules', d, {})
345
+ assert.ok(result !== null)
346
+ assert.match(result, /not enabled/)
347
+ })
348
+
349
+ it('maps execute_command to shell family', () => {
350
+ const d = resolveSessionToolPolicy(['shell'], {})
351
+ assert.equal(resolveConcreteToolPolicyBlock('execute_command', d, {}), null)
352
+ })
353
+
354
+ it('returns "invalid tool name" for empty string', () => {
355
+ const d = resolveSessionToolPolicy([], {})
356
+ assert.equal(resolveConcreteToolPolicyBlock('', d, {}), 'invalid tool name')
357
+ })
358
+
359
+ it('returns "invalid tool name" for whitespace-only string', () => {
360
+ const d = resolveSessionToolPolicy([], {})
361
+ assert.equal(resolveConcreteToolPolicyBlock(' ', d, {}), 'invalid tool name')
362
+ })
363
+
364
+ it('safety blocks concrete tool in resolveConcreteToolPolicyBlock', () => {
365
+ const d = resolveSessionToolPolicy(['web'], {})
366
+ const result = resolveConcreteToolPolicyBlock('web_search', d, {
367
+ safetyBlockedTools: ['web_search'],
368
+ })
369
+ assert.equal(result, 'blocked by safety policy')
370
+ })
371
+
372
+ it('policy blocks concrete tool in resolveConcreteToolPolicyBlock', () => {
373
+ const d = resolveSessionToolPolicy(['web'], {})
374
+ const result = resolveConcreteToolPolicyBlock('web_search', d, {
375
+ capabilityBlockedTools: ['web_search'],
376
+ })
377
+ assert.equal(result, 'blocked by explicit policy rule')
378
+ })
379
+ })
380
+
381
+ // ---------------------------------------------------------------------------
382
+ // Compound scenarios
383
+ // ---------------------------------------------------------------------------
384
+ describe('compound scenarios', () => {
385
+ it('strict mode + safety block + settings disabled + category block layer together', () => {
386
+ const d = resolveSessionToolPolicy(
387
+ ['shell', 'memory', 'manage_tasks', 'web', 'delete_file', 'delegate'],
388
+ {
389
+ capabilityPolicyMode: 'strict',
390
+ safetyBlockedTools: ['memory'],
391
+ taskManagementEnabled: false,
392
+ capabilityBlockedCategories: ['network'],
393
+ },
394
+ )
395
+ // memory: safety-blocked
396
+ // manage_tasks: settings-blocked (checked before safety)
397
+ // web: category-blocked (network)
398
+ // shell: strict-blocked (execution)
399
+ // delete_file: strict-blocked (destructive + filesystem)
400
+ // delegate: strict-blocked (delegation + execution)
401
+ assert.equal(d.enabledPlugins.length, 0)
402
+ assert.equal(d.blockedPlugins.length, 6)
403
+
404
+ const memoryBlock = d.blockedPlugins.find((b) => b.tool === 'memory')
405
+ assert.ok(memoryBlock)
406
+ assert.equal(memoryBlock.source, 'safety')
407
+
408
+ const tasksBlock = d.blockedPlugins.find((b) => b.tool === 'manage_tasks')
409
+ assert.ok(tasksBlock)
410
+ assert.match(tasksBlock.reason, /task management is disabled/)
411
+ })
412
+
413
+ it('20 tools requested: correctly partitioned into enabled vs blocked', () => {
414
+ const tools = [
415
+ 'shell', 'files', 'web', 'web_search', 'web_fetch', 'browser',
416
+ 'memory', 'delegate', 'manage_platform', 'manage_tasks',
417
+ 'manage_schedules', 'wallet', 'delete_file', 'canvas',
418
+ 'manage_connectors', 'git', 'sandbox', 'claude_code',
419
+ 'monitor', 'http_request',
420
+ ]
421
+ const d = resolveSessionToolPolicy(tools, { capabilityPolicyMode: 'strict' })
422
+ assert.equal(d.requestedPlugins.length, 20)
423
+ assert.equal(d.enabledPlugins.length + d.blockedPlugins.length, 20)
424
+
425
+ // memory, web, web_search, web_fetch should be enabled
426
+ assert.ok(d.enabledPlugins.includes('memory'))
427
+ assert.ok(d.enabledPlugins.includes('web'))
428
+ assert.ok(d.enabledPlugins.includes('web_search'))
429
+ assert.ok(d.enabledPlugins.includes('web_fetch'))
430
+ assert.ok(d.enabledPlugins.includes('http_request'))
431
+
432
+ // shell, files, delegate, manage_platform should be blocked
433
+ assert.ok(d.blockedPlugins.some((b) => b.tool === 'shell'))
434
+ assert.ok(d.blockedPlugins.some((b) => b.tool === 'files'))
435
+ assert.ok(d.blockedPlugins.some((b) => b.tool === 'delegate'))
436
+ assert.ok(d.blockedPlugins.some((b) => b.tool === 'manage_platform'))
437
+ assert.ok(d.blockedPlugins.some((b) => b.tool === 'wallet'))
438
+ assert.ok(d.blockedPlugins.some((b) => b.tool === 'delete_file'))
439
+ })
440
+
441
+ it('duplicate tool requested twice is deduplicated', () => {
442
+ const d = resolveSessionToolPolicy(['shell', 'shell', 'web', 'web'], {})
443
+ assert.equal(d.requestedPlugins.length, 2)
444
+ assert.deepStrictEqual(d.requestedPlugins, ['shell', 'web'])
445
+ })
446
+ })
447
+
448
+ // ---------------------------------------------------------------------------
449
+ // Edge cases
450
+ // ---------------------------------------------------------------------------
451
+ describe('edge cases', () => {
452
+ it('undefined sessionTools returns empty arrays', () => {
453
+ const d = resolveSessionToolPolicy(undefined, {})
454
+ assert.deepStrictEqual(d.requestedPlugins, [])
455
+ assert.deepStrictEqual(d.enabledPlugins, [])
456
+ assert.deepStrictEqual(d.blockedPlugins, [])
457
+ })
458
+
459
+ it('empty sessionTools returns empty arrays', () => {
460
+ const d = resolveSessionToolPolicy([], {})
461
+ assert.deepStrictEqual(d.requestedPlugins, [])
462
+ assert.deepStrictEqual(d.enabledPlugins, [])
463
+ assert.deepStrictEqual(d.blockedPlugins, [])
464
+ })
465
+
466
+ it('null settings treated as empty', () => {
467
+ const d = resolveSessionToolPolicy(['shell'], null)
468
+ assert.deepStrictEqual(d.enabledPlugins, ['shell'])
469
+ assert.equal(d.mode, 'permissive')
470
+ })
471
+
472
+ it('undefined settings treated as empty', () => {
473
+ const d = resolveSessionToolPolicy(['shell'], undefined)
474
+ assert.deepStrictEqual(d.enabledPlugins, ['shell'])
475
+ })
476
+
477
+ it('unknown tool name passes through in permissive (no descriptor)', () => {
478
+ const d = resolveSessionToolPolicy(['totally_fake_tool'], { capabilityPolicyMode: 'permissive' })
479
+ assert.deepStrictEqual(d.enabledPlugins, ['totally_fake_tool'])
480
+ })
481
+
482
+ it('unknown tool name passes through in strict (no descriptor, no categories)', () => {
483
+ const d = resolveSessionToolPolicy(['totally_fake_tool'], { capabilityPolicyMode: 'strict' })
484
+ assert.deepStrictEqual(d.enabledPlugins, ['totally_fake_tool'])
485
+ })
486
+
487
+ it('case-insensitive tool matching', () => {
488
+ const d = resolveSessionToolPolicy(['SHELL', 'Web'], { capabilityPolicyMode: 'strict' })
489
+ assert.ok(d.blockedPlugins.some((b) => b.tool === 'shell'))
490
+ assert.ok(d.enabledPlugins.includes('web'))
491
+ })
492
+
493
+ it('settings block takes priority over safety block (checked first)', () => {
494
+ const d = resolveSessionToolPolicy(['manage_tasks'], {
495
+ taskManagementEnabled: false,
496
+ safetyBlockedTools: ['manage_tasks'],
497
+ })
498
+ assert.equal(d.blockedPlugins.length, 1)
499
+ // Settings block is checked before safety in the implementation
500
+ assert.match(d.blockedPlugins[0].reason, /task management is disabled/)
501
+ })
502
+ })
@@ -56,3 +56,27 @@ test('concrete tool checks inherit blocked family rules', () => {
56
56
  null,
57
57
  )
58
58
  })
59
+
60
+ test('task and project management can be disabled from app settings', () => {
61
+ const decision = resolveSessionToolPolicy(
62
+ ['manage_platform', 'manage_tasks', 'manage_projects'],
63
+ {
64
+ taskManagementEnabled: false,
65
+ projectManagementEnabled: false,
66
+ },
67
+ )
68
+
69
+ assert.deepEqual(decision.enabledPlugins, ['manage_platform'])
70
+ assert.equal(
71
+ decision.blockedPlugins.some((entry) => entry.tool === 'manage_tasks' && /disabled in app settings/.test(entry.reason)),
72
+ true,
73
+ )
74
+ assert.equal(
75
+ decision.blockedPlugins.some((entry) => entry.tool === 'manage_projects' && /disabled in app settings/.test(entry.reason)),
76
+ true,
77
+ )
78
+ assert.match(
79
+ resolveConcreteToolPolicyBlock('manage_tasks', decision, { taskManagementEnabled: false }),
80
+ /task management is disabled/i,
81
+ )
82
+ })
@@ -64,8 +64,9 @@ const TOOL_DESCRIPTORS: Record<string, ToolDescriptor> = {
64
64
  monitor: { categories: ['execution'], concreteTools: ['monitor', 'monitor_tool'] },
65
65
  openclaw_workspace: { categories: ['filesystem', 'platform'], concreteTools: ['openclaw_workspace'] },
66
66
  openclaw_nodes: { categories: ['platform'], concreteTools: ['openclaw_nodes'] },
67
- manage_platform: { categories: ['platform'], concreteTools: ['manage_platform', 'manage_agents', 'manage_tasks', 'manage_schedules', 'manage_skills', 'manage_documents', 'manage_webhooks', 'manage_connectors', 'manage_sessions', 'manage_secrets'] },
67
+ manage_platform: { categories: ['platform'], concreteTools: ['manage_platform', 'manage_agents', 'manage_projects', 'manage_tasks', 'manage_schedules', 'manage_skills', 'manage_documents', 'manage_webhooks', 'manage_connectors', 'manage_sessions', 'manage_secrets'] },
68
68
  manage_agents: { categories: ['platform'], concreteTools: ['manage_agents'] },
69
+ manage_projects: { categories: ['platform'], concreteTools: ['manage_projects'] },
69
70
  manage_tasks: { categories: ['platform'], concreteTools: ['manage_tasks'] },
70
71
  manage_schedules: { categories: ['platform'], concreteTools: ['manage_schedules'] },
71
72
  schedule_wake: { categories: ['platform'], concreteTools: ['schedule_wake'] },
@@ -179,6 +180,24 @@ function ensureSettings(settings?: AppSettings | Record<string, unknown> | null)
179
180
  return settings as Record<string, unknown>
180
181
  }
181
182
 
183
+ export function isTaskManagementEnabled(settings?: AppSettings | Record<string, unknown> | null): boolean {
184
+ return ensureSettings(settings).taskManagementEnabled !== false
185
+ }
186
+
187
+ export function isProjectManagementEnabled(settings?: AppSettings | Record<string, unknown> | null): boolean {
188
+ return ensureSettings(settings).projectManagementEnabled !== false
189
+ }
190
+
191
+ function settingsBlockReason(toolName: string, settings?: AppSettings | Record<string, unknown> | null): string | null {
192
+ if (toolName === 'manage_tasks' && !isTaskManagementEnabled(settings)) {
193
+ return 'blocked because task management is disabled in app settings'
194
+ }
195
+ if (toolName === 'manage_projects' && !isProjectManagementEnabled(settings)) {
196
+ return 'blocked because project management is disabled in app settings'
197
+ }
198
+ return null
199
+ }
200
+
182
201
  function parsePolicyConfig(settings: Record<string, unknown>) {
183
202
  const mode = normalizeMode(settings.capabilityPolicyMode)
184
203
  const safetyBlocked = new Set(getSettingsList(settings, 'safetyBlockedTools'))
@@ -216,6 +235,12 @@ export function resolveSessionToolPolicy(
216
235
 
217
236
  for (const pluginName of requestedPlugins) {
218
237
  const descriptor = TOOL_DESCRIPTORS[pluginName]
238
+ const settingsReason = settingsBlockReason(pluginName, normalizedSettings)
239
+
240
+ if (settingsReason) {
241
+ blockedPlugins.push({ tool: pluginName, reason: settingsReason, source: 'policy' })
242
+ continue
243
+ }
219
244
 
220
245
  if (safetyMatchesTool(safetyBlocked, pluginName, descriptor)) {
221
246
  blockedPlugins.push({ tool: pluginName, reason: 'blocked by safety policy', source: 'safety' })
@@ -269,6 +294,9 @@ export function resolveConcreteToolPolicyBlock(
269
294
  policyBlockedNames,
270
295
  policyAllowedNames,
271
296
  } = parsePolicyConfig(normalizedSettings)
297
+ const settingsReason = settingsBlockReason(name, normalizedSettings)
298
+
299
+ if (settingsReason) return settingsReason
272
300
 
273
301
  if (safetyBlocked.has(name)) return 'blocked by safety policy'
274
302