@swarmclawai/swarmclaw 0.7.1 → 0.7.3

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 (237) hide show
  1. package/README.md +155 -150
  2. package/package.json +1 -1
  3. package/src/app/api/agents/[id]/route.ts +26 -0
  4. package/src/app/api/agents/[id]/thread/route.ts +37 -9
  5. package/src/app/api/agents/route.ts +13 -2
  6. package/src/app/api/auth/route.ts +76 -7
  7. package/src/app/api/chatrooms/[id]/chat/route.ts +7 -2
  8. package/src/app/api/{sessions → chats}/[id]/browser/route.ts +5 -1
  9. package/src/app/api/{sessions → chats}/[id]/chat/route.ts +7 -3
  10. package/src/app/api/{sessions → chats}/[id]/checkpoints/route.ts +1 -1
  11. package/src/app/api/chats/[id]/main-loop/route.ts +13 -0
  12. package/src/app/api/{sessions → chats}/[id]/messages/route.ts +19 -13
  13. package/src/app/api/{sessions → chats}/[id]/restore/route.ts +1 -1
  14. package/src/app/api/{sessions → chats}/[id]/route.ts +22 -52
  15. package/src/app/api/{sessions → chats}/[id]/stop/route.ts +6 -1
  16. package/src/app/api/{sessions → chats}/route.ts +21 -7
  17. package/src/app/api/connectors/[id]/doctor/route.ts +26 -0
  18. package/src/app/api/connectors/doctor/route.ts +13 -0
  19. package/src/app/api/files/open/route.ts +16 -14
  20. package/src/app/api/memory/maintenance/route.ts +11 -1
  21. package/src/app/api/openclaw/agent-files/route.ts +27 -4
  22. package/src/app/api/openclaw/skills/route.ts +11 -3
  23. package/src/app/api/plugins/dependencies/route.ts +24 -0
  24. package/src/app/api/plugins/install/route.ts +15 -92
  25. package/src/app/api/plugins/route.ts +6 -26
  26. package/src/app/api/plugins/settings/route.ts +40 -0
  27. package/src/app/api/plugins/ui/route.ts +1 -0
  28. package/src/app/api/settings/route.ts +49 -7
  29. package/src/app/api/tasks/[id]/route.ts +15 -6
  30. package/src/app/api/tasks/bulk/route.ts +2 -2
  31. package/src/app/api/tasks/route.ts +9 -4
  32. package/src/app/api/usage/route.ts +30 -0
  33. package/src/app/api/webhooks/[id]/route.ts +8 -1
  34. package/src/app/page.tsx +9 -2
  35. package/src/cli/index.js +39 -33
  36. package/src/cli/index.ts +43 -49
  37. package/src/cli/spec.js +29 -27
  38. package/src/components/agents/agent-card.tsx +16 -13
  39. package/src/components/agents/agent-chat-list.tsx +104 -4
  40. package/src/components/agents/agent-list.tsx +54 -22
  41. package/src/components/agents/agent-sheet.tsx +209 -18
  42. package/src/components/agents/cron-job-form.tsx +3 -3
  43. package/src/components/agents/inspector-panel.tsx +110 -50
  44. package/src/components/auth/access-key-gate.tsx +36 -97
  45. package/src/components/auth/setup-wizard.tsx +5 -38
  46. package/src/components/chat/chat-area.tsx +39 -27
  47. package/src/components/{sessions/session-card.tsx → chat/chat-card.tsx} +7 -23
  48. package/src/components/chat/chat-header.tsx +299 -314
  49. package/src/components/{sessions/session-list.tsx → chat/chat-list.tsx} +11 -14
  50. package/src/components/chat/chat-tool-toggles.tsx +26 -17
  51. package/src/components/chat/checkpoint-timeline.tsx +4 -4
  52. package/src/components/chat/message-bubble.tsx +4 -1
  53. package/src/components/chat/message-list.tsx +5 -3
  54. package/src/components/chat/session-debug-panel.tsx +1 -1
  55. package/src/components/chat/tool-request-banner.tsx +3 -3
  56. package/src/components/chatrooms/agent-hover-card.tsx +3 -3
  57. package/src/components/chatrooms/chatroom-tool-request-banner.tsx +2 -2
  58. package/src/components/chatrooms/chatroom-view.tsx +347 -205
  59. package/src/components/connectors/connector-list.tsx +265 -127
  60. package/src/components/connectors/connector-sheet.tsx +218 -1
  61. package/src/components/home/home-view.tsx +129 -5
  62. package/src/components/layout/app-layout.tsx +392 -182
  63. package/src/components/layout/mobile-header.tsx +26 -8
  64. package/src/components/plugins/plugin-list.tsx +487 -254
  65. package/src/components/plugins/plugin-sheet.tsx +236 -13
  66. package/src/components/projects/project-detail.tsx +183 -0
  67. package/src/components/settings/gateway-connection-panel.tsx +1 -1
  68. package/src/components/shared/agent-picker-list.tsx +2 -2
  69. package/src/components/shared/command-palette.tsx +111 -25
  70. package/src/components/shared/settings/plugin-manager.tsx +20 -4
  71. package/src/components/shared/settings/section-capability-policy.tsx +105 -0
  72. package/src/components/shared/settings/section-heartbeat.tsx +78 -1
  73. package/src/components/shared/settings/section-orchestrator.tsx +3 -3
  74. package/src/components/shared/settings/section-providers.tsx +1 -1
  75. package/src/components/shared/settings/section-runtime-loop.tsx +5 -5
  76. package/src/components/shared/settings/section-secrets.tsx +6 -6
  77. package/src/components/shared/settings/section-user-preferences.tsx +1 -1
  78. package/src/components/shared/settings/section-voice.tsx +5 -1
  79. package/src/components/shared/settings/section-web-search.tsx +10 -2
  80. package/src/components/shared/settings/settings-page.tsx +244 -56
  81. package/src/components/tasks/approvals-panel.tsx +205 -18
  82. package/src/components/tasks/task-board.tsx +242 -46
  83. package/src/components/usage/metrics-dashboard.tsx +147 -1
  84. package/src/components/wallets/wallet-panel.tsx +17 -5
  85. package/src/components/webhooks/webhook-sheet.tsx +8 -8
  86. package/src/lib/auth.ts +17 -0
  87. package/src/lib/chat-streaming-state.test.ts +108 -0
  88. package/src/lib/chat-streaming-state.ts +108 -0
  89. package/src/lib/chat.ts +1 -1
  90. package/src/lib/{sessions.ts → chats.ts} +28 -18
  91. package/src/lib/openclaw-agent-id.test.ts +14 -0
  92. package/src/lib/openclaw-agent-id.ts +31 -0
  93. package/src/lib/providers/claude-cli.ts +1 -1
  94. package/src/lib/server/agent-assignment.test.ts +112 -0
  95. package/src/lib/server/agent-assignment.ts +169 -0
  96. package/src/lib/server/approval-connector-notify.test.ts +253 -0
  97. package/src/lib/server/approvals-auto-approve.test.ts +205 -0
  98. package/src/lib/server/approvals.ts +483 -75
  99. package/src/lib/server/autonomy-runtime.test.ts +341 -0
  100. package/src/lib/server/browser-state.test.ts +118 -0
  101. package/src/lib/server/browser-state.ts +123 -0
  102. package/src/lib/server/build-llm.test.ts +36 -0
  103. package/src/lib/server/build-llm.ts +11 -4
  104. package/src/lib/server/builtin-plugins.ts +34 -0
  105. package/src/lib/server/capability-router.ts +10 -8
  106. package/src/lib/server/chat-execution-heartbeat.test.ts +40 -0
  107. package/src/lib/server/chat-execution-tool-events.test.ts +134 -0
  108. package/src/lib/server/chat-execution.ts +285 -165
  109. package/src/lib/server/chatroom-health.test.ts +26 -0
  110. package/src/lib/server/chatroom-health.ts +2 -3
  111. package/src/lib/server/chatroom-helpers.test.ts +67 -2
  112. package/src/lib/server/chatroom-helpers.ts +48 -8
  113. package/src/lib/server/connectors/discord.ts +175 -11
  114. package/src/lib/server/connectors/doctor.test.ts +80 -0
  115. package/src/lib/server/connectors/doctor.ts +116 -0
  116. package/src/lib/server/connectors/manager.ts +948 -112
  117. package/src/lib/server/connectors/policy.test.ts +222 -0
  118. package/src/lib/server/connectors/policy.ts +452 -0
  119. package/src/lib/server/connectors/slack.ts +188 -9
  120. package/src/lib/server/connectors/telegram.ts +65 -15
  121. package/src/lib/server/connectors/thread-context.test.ts +44 -0
  122. package/src/lib/server/connectors/thread-context.ts +72 -0
  123. package/src/lib/server/connectors/types.ts +41 -11
  124. package/src/lib/server/cost.ts +34 -1
  125. package/src/lib/server/daemon-state.ts +61 -3
  126. package/src/lib/server/data-dir.ts +13 -0
  127. package/src/lib/server/delegation-jobs.test.ts +140 -0
  128. package/src/lib/server/delegation-jobs.ts +248 -0
  129. package/src/lib/server/document-utils.test.ts +47 -0
  130. package/src/lib/server/document-utils.ts +397 -0
  131. package/src/lib/server/heartbeat-service.ts +14 -40
  132. package/src/lib/server/heartbeat-source.test.ts +22 -0
  133. package/src/lib/server/heartbeat-source.ts +7 -0
  134. package/src/lib/server/identity-continuity.test.ts +77 -0
  135. package/src/lib/server/identity-continuity.ts +127 -0
  136. package/src/lib/server/mailbox-utils.ts +347 -0
  137. package/src/lib/server/main-agent-loop.ts +28 -1103
  138. package/src/lib/server/memory-db.ts +4 -6
  139. package/src/lib/server/memory-tiers.ts +40 -0
  140. package/src/lib/server/openclaw-agent-resolver.test.ts +70 -0
  141. package/src/lib/server/openclaw-agent-resolver.ts +128 -0
  142. package/src/lib/server/openclaw-exec-config.ts +5 -6
  143. package/src/lib/server/openclaw-skills-normalize.test.ts +56 -0
  144. package/src/lib/server/openclaw-skills-normalize.ts +136 -0
  145. package/src/lib/server/openclaw-sync.ts +3 -2
  146. package/src/lib/server/orchestrator-lg.ts +20 -9
  147. package/src/lib/server/orchestrator.ts +7 -7
  148. package/src/lib/server/playwright-proxy.mjs +27 -3
  149. package/src/lib/server/plugins.test.ts +207 -0
  150. package/src/lib/server/plugins.ts +927 -66
  151. package/src/lib/server/provider-health.ts +38 -6
  152. package/src/lib/server/queue.ts +13 -28
  153. package/src/lib/server/scheduler.ts +2 -0
  154. package/src/lib/server/session-archive-memory.test.ts +85 -0
  155. package/src/lib/server/session-archive-memory.ts +230 -0
  156. package/src/lib/server/session-mailbox.ts +8 -18
  157. package/src/lib/server/session-reset-policy.test.ts +99 -0
  158. package/src/lib/server/session-reset-policy.ts +311 -0
  159. package/src/lib/server/session-run-manager.ts +33 -82
  160. package/src/lib/server/session-tools/autonomy-tools.test.ts +105 -0
  161. package/src/lib/server/session-tools/calendar.ts +366 -0
  162. package/src/lib/server/session-tools/canvas.ts +1 -1
  163. package/src/lib/server/session-tools/chatroom.ts +4 -2
  164. package/src/lib/server/session-tools/connector.ts +114 -10
  165. package/src/lib/server/session-tools/context.ts +21 -5
  166. package/src/lib/server/session-tools/crawl.ts +447 -0
  167. package/src/lib/server/session-tools/crud.ts +74 -28
  168. package/src/lib/server/session-tools/delegate-fallback.test.ts +219 -0
  169. package/src/lib/server/session-tools/delegate.ts +497 -24
  170. package/src/lib/server/session-tools/discovery.ts +24 -6
  171. package/src/lib/server/session-tools/document.ts +283 -0
  172. package/src/lib/server/session-tools/edit_file.ts +4 -2
  173. package/src/lib/server/session-tools/email.ts +320 -0
  174. package/src/lib/server/session-tools/extract.ts +137 -0
  175. package/src/lib/server/session-tools/file-normalize.test.ts +93 -0
  176. package/src/lib/server/session-tools/file-send.test.ts +84 -1
  177. package/src/lib/server/session-tools/file.ts +241 -25
  178. package/src/lib/server/session-tools/git.ts +1 -1
  179. package/src/lib/server/session-tools/http.ts +1 -1
  180. package/src/lib/server/session-tools/human-loop.ts +227 -0
  181. package/src/lib/server/session-tools/image-gen.ts +380 -0
  182. package/src/lib/server/session-tools/index.ts +130 -50
  183. package/src/lib/server/session-tools/mailbox.ts +276 -0
  184. package/src/lib/server/session-tools/memory.ts +172 -3
  185. package/src/lib/server/session-tools/monitor.ts +151 -8
  186. package/src/lib/server/session-tools/normalize-tool-args.ts +17 -14
  187. package/src/lib/server/session-tools/openclaw-nodes.ts +1 -1
  188. package/src/lib/server/session-tools/openclaw-workspace.ts +1 -1
  189. package/src/lib/server/session-tools/platform-normalize.test.ts +142 -0
  190. package/src/lib/server/session-tools/platform.ts +148 -7
  191. package/src/lib/server/session-tools/plugin-creator.ts +89 -26
  192. package/src/lib/server/session-tools/primitive-tools.test.ts +257 -0
  193. package/src/lib/server/session-tools/replicate.ts +301 -0
  194. package/src/lib/server/session-tools/sample-ui.ts +1 -1
  195. package/src/lib/server/session-tools/sandbox.ts +4 -2
  196. package/src/lib/server/session-tools/schedule.ts +24 -12
  197. package/src/lib/server/session-tools/session-info.ts +43 -7
  198. package/src/lib/server/session-tools/session-tools-wiring.test.ts +31 -17
  199. package/src/lib/server/session-tools/shell.ts +5 -2
  200. package/src/lib/server/session-tools/subagent.ts +194 -28
  201. package/src/lib/server/session-tools/table.ts +587 -0
  202. package/src/lib/server/session-tools/wallet.ts +42 -12
  203. package/src/lib/server/session-tools/web-browser-config.test.ts +39 -0
  204. package/src/lib/server/session-tools/web.ts +926 -91
  205. package/src/lib/server/storage.ts +255 -16
  206. package/src/lib/server/stream-agent-chat.ts +116 -268
  207. package/src/lib/server/structured-extract.test.ts +72 -0
  208. package/src/lib/server/structured-extract.ts +373 -0
  209. package/src/lib/server/task-mention.test.ts +16 -2
  210. package/src/lib/server/task-mention.ts +61 -10
  211. package/src/lib/server/tool-aliases.ts +66 -18
  212. package/src/lib/server/tool-capability-policy.test.ts +9 -9
  213. package/src/lib/server/tool-capability-policy.ts +38 -27
  214. package/src/lib/server/tool-retry.ts +2 -0
  215. package/src/lib/server/watch-jobs.test.ts +173 -0
  216. package/src/lib/server/watch-jobs.ts +532 -0
  217. package/src/lib/server/ws-hub.ts +5 -3
  218. package/src/lib/tool-definitions.ts +4 -0
  219. package/src/lib/validation/schemas.test.ts +26 -0
  220. package/src/lib/validation/schemas.ts +10 -1
  221. package/src/lib/ws-client.ts +14 -12
  222. package/src/proxy.ts +5 -5
  223. package/src/stores/use-app-store.ts +5 -11
  224. package/src/stores/use-chat-store.ts +38 -9
  225. package/src/types/index.ts +352 -47
  226. package/src/app/api/sessions/[id]/main-loop/route.ts +0 -94
  227. package/src/components/sessions/new-session-sheet.tsx +0 -253
  228. package/src/lib/server/main-session.ts +0 -24
  229. package/src/lib/server/session-run-manager.test.ts +0 -23
  230. /package/src/app/api/{sessions → chats}/[id]/clear/route.ts +0 -0
  231. /package/src/app/api/{sessions → chats}/[id]/deploy/route.ts +0 -0
  232. /package/src/app/api/{sessions → chats}/[id]/devserver/route.ts +0 -0
  233. /package/src/app/api/{sessions → chats}/[id]/edit-resend/route.ts +0 -0
  234. /package/src/app/api/{sessions → chats}/[id]/fork/route.ts +0 -0
  235. /package/src/app/api/{sessions → chats}/[id]/mailbox/route.ts +0 -0
  236. /package/src/app/api/{sessions → chats}/[id]/retry/route.ts +0 -0
  237. /package/src/app/api/{sessions → chats}/heartbeat/route.ts +0 -0
@@ -1,10 +1,10 @@
1
- const TOOL_ALIAS_GROUPS: string[][] = [
1
+ const PLUGIN_ALIAS_GROUPS: string[][] = [
2
2
  ['shell', 'execute_command', 'process_tool', 'process'],
3
3
  ['files', 'read_file', 'write_file', 'list_files', 'copy_file', 'move_file', 'delete_file', 'send_file'],
4
4
  ['edit_file'],
5
5
  ['web', 'web_search', 'web_fetch'],
6
6
  ['browser', 'openclaw_browser'],
7
- ['delegate', 'claude_code', 'codex_cli', 'opencode_cli', 'delegate_to_claude_code', 'delegate_to_codex_cli', 'delegate_to_opencode_cli'],
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', 'manage_agents', 'manage_tasks', 'manage_schedules', 'manage_skills', 'manage_documents', 'manage_webhooks', 'manage_secrets', 'manage_sessions'],
9
9
  ['manage_connectors', 'connectors', 'connector_message_tool'],
10
10
  ['manage_chatrooms', 'chatroom'],
@@ -20,37 +20,75 @@ const TOOL_ALIAS_GROUPS: string[][] = [
20
20
  ['context_mgmt', 'context_status', 'context_summarize'],
21
21
  ['openclaw_workspace'],
22
22
  ['openclaw_nodes'],
23
+ ['image_gen', 'generate_image'],
24
+ ['email', 'send_email'],
25
+ ['calendar', 'calendar_events'],
26
+ ['replicate', 'replicate_run', 'replicate_models'],
27
+ ['mailbox', 'inbox'],
28
+ ['ask_human', 'human_loop'],
29
+ ['document', 'ocr_document', 'parse_document'],
30
+ ['extract', 'extract_structured'],
31
+ ['table', 'dataframe'],
32
+ ['crawl', 'site_crawler'],
23
33
  ]
24
34
 
25
- const TOOL_ALIAS_MAP = (() => {
35
+ const PLUGIN_CANONICAL_MAP = (() => {
36
+ const map = new Map<string, string>()
37
+ for (const group of PLUGIN_ALIAS_GROUPS) {
38
+ const normalized = group.map((id) => id.trim().toLowerCase()).filter(Boolean)
39
+ const canonical = normalized[0]
40
+ if (!canonical) continue
41
+ for (const id of normalized) map.set(id, canonical)
42
+ }
43
+ return map
44
+ })()
45
+
46
+ const PLUGIN_ALIAS_MAP = (() => {
26
47
  const map = new Map<string, Set<string>>()
27
- for (const group of TOOL_ALIAS_GROUPS) {
28
- const normalized = group.map((tool) => tool.trim().toLowerCase()).filter(Boolean)
29
- for (const tool of normalized) {
30
- const current = map.get(tool) || new Set<string>()
48
+ for (const group of PLUGIN_ALIAS_GROUPS) {
49
+ const normalized = group.map((id) => id.trim().toLowerCase()).filter(Boolean)
50
+ for (const id of normalized) {
51
+ const current = map.get(id) || new Set<string>()
31
52
  for (const alias of normalized) current.add(alias)
32
- map.set(tool, current)
53
+ map.set(id, current)
33
54
  }
34
55
  }
35
56
  return map
36
57
  })()
37
58
 
38
- export function normalizeToolId(value: unknown): string {
59
+ export function normalizePluginId(value: unknown): string {
39
60
  return typeof value === 'string' ? value.trim().toLowerCase() : ''
40
61
  }
41
62
 
42
- export function expandToolIds(values: string[] | null | undefined): string[] {
63
+ export function canonicalizePluginId(value: unknown): string {
64
+ const raw = typeof value === 'string' ? value.trim() : ''
65
+ const normalized = normalizePluginId(value)
66
+ if (!normalized) return raw
67
+ return PLUGIN_CANONICAL_MAP.get(normalized) || raw
68
+ }
69
+
70
+ export function getPluginAliases(value: unknown): string[] {
71
+ const normalized = normalizePluginId(value)
72
+ if (!normalized) return []
73
+ const aliases = PLUGIN_ALIAS_MAP.get(normalized)
74
+ if (!aliases) return [normalized]
75
+ return Array.from(aliases)
76
+ }
77
+
78
+ export function expandPluginIds(values: string[] | null | undefined): string[] {
43
79
  if (!Array.isArray(values) || values.length === 0) return []
44
80
  const expanded = new Set<string>()
45
81
  const queue: string[] = values
46
- .map((tool) => normalizeToolId(tool))
82
+ .map((id) => typeof id === 'string' ? id.trim() : '')
47
83
  .filter(Boolean)
48
84
 
49
85
  while (queue.length > 0) {
50
86
  const next = queue.shift()!
51
- if (expanded.has(next)) continue
52
- expanded.add(next)
53
- const aliases = TOOL_ALIAS_MAP.get(next)
87
+ const normalized = normalizePluginId(next)
88
+ const aliases = PLUGIN_ALIAS_MAP.get(normalized)
89
+ const key = aliases ? normalized : next
90
+ if (expanded.has(key)) continue
91
+ expanded.add(key)
54
92
  if (!aliases) continue
55
93
  for (const alias of aliases) {
56
94
  if (!expanded.has(alias)) queue.push(alias)
@@ -60,9 +98,19 @@ export function expandToolIds(values: string[] | null | undefined): string[] {
60
98
  return Array.from(expanded)
61
99
  }
62
100
 
63
- export function toolIdMatches(enabledTools: string[] | null | undefined, toolId: string): boolean {
64
- const normalized = normalizeToolId(toolId)
65
- if (!normalized) return false
66
- return expandToolIds(enabledTools).includes(normalized)
101
+ export function pluginIdMatches(enabledPlugins: string[] | null | undefined, pluginId: string): boolean {
102
+ const raw = typeof pluginId === 'string' ? pluginId.trim() : ''
103
+ const normalized = normalizePluginId(pluginId)
104
+ if (!normalized && !raw) return false
105
+ const expanded = expandPluginIds(enabledPlugins)
106
+ return expanded.includes(raw) || expanded.includes(normalized) || expanded.includes(canonicalizePluginId(pluginId))
67
107
  }
68
108
 
109
+ /** @deprecated Use normalizePluginId */
110
+ export const normalizeToolId = normalizePluginId
111
+ /** @deprecated Use canonicalizePluginId */
112
+ export const canonicalizeToolId = canonicalizePluginId
113
+ /** @deprecated Use expandPluginIds */
114
+ export const expandToolIds = expandPluginIds
115
+ /** @deprecated Use pluginIdMatches */
116
+ export const toolIdMatches = pluginIdMatches
@@ -7,15 +7,15 @@ import {
7
7
 
8
8
  test('capability policy permissive mode allows non-blocked tools', () => {
9
9
  const decision = resolveSessionToolPolicy(['shell', 'web_search'], { capabilityPolicyMode: 'permissive' })
10
- assert.deepEqual(decision.enabledTools, ['shell', 'web_search'])
11
- assert.equal(decision.blockedTools.length, 0)
10
+ assert.deepEqual(decision.enabledPlugins, ['shell', 'web_search'])
11
+ assert.equal(decision.blockedPlugins.length, 0)
12
12
  })
13
13
 
14
14
  test('capability policy balanced mode blocks destructive delete_file', () => {
15
15
  const decision = resolveSessionToolPolicy(['files', 'delete_file'], { capabilityPolicyMode: 'balanced' })
16
- assert.deepEqual(decision.enabledTools, ['files'])
17
- assert.equal(decision.blockedTools.length, 1)
18
- assert.equal(decision.blockedTools[0].tool, 'delete_file')
16
+ assert.deepEqual(decision.enabledPlugins, ['files'])
17
+ assert.equal(decision.blockedPlugins.length, 1)
18
+ assert.equal(decision.blockedPlugins[0].tool, 'delete_file')
19
19
  })
20
20
 
21
21
  test('capability policy strict mode blocks execution/platform families', () => {
@@ -23,9 +23,9 @@ test('capability policy strict mode blocks execution/platform families', () => {
23
23
  ['shell', 'manage_tasks', 'web_search', 'memory'],
24
24
  { capabilityPolicyMode: 'strict' },
25
25
  )
26
- assert.deepEqual(decision.enabledTools, ['web_search', 'memory'])
27
- assert.equal(decision.blockedTools.some((entry) => entry.tool === 'shell'), true)
28
- assert.equal(decision.blockedTools.some((entry) => entry.tool === 'manage_tasks'), true)
26
+ assert.deepEqual(decision.enabledPlugins, ['web_search', 'memory'])
27
+ assert.equal(decision.blockedPlugins.some((entry) => entry.tool === 'shell'), true)
28
+ assert.equal(decision.blockedPlugins.some((entry) => entry.tool === 'manage_tasks'), true)
29
29
  })
30
30
 
31
31
  test('capability policy respects explicit allow overrides', () => {
@@ -36,7 +36,7 @@ test('capability policy respects explicit allow overrides', () => {
36
36
  capabilityAllowedTools: ['shell'],
37
37
  },
38
38
  )
39
- assert.deepEqual(decision.enabledTools, ['shell', 'web_search'])
39
+ assert.deepEqual(decision.enabledPlugins, ['shell', 'web_search'])
40
40
  })
41
41
 
42
42
  test('concrete tool checks inherit blocked family rules', () => {
@@ -8,13 +8,16 @@ export interface CapabilityPolicyBlock {
8
8
  source: 'safety' | 'policy'
9
9
  }
10
10
 
11
- export interface SessionToolPolicyDecision {
11
+ export interface PluginPolicyDecision {
12
12
  mode: CapabilityPolicyMode
13
- requestedTools: string[]
14
- enabledTools: string[]
15
- blockedTools: CapabilityPolicyBlock[]
13
+ requestedPlugins: string[]
14
+ enabledPlugins: string[]
15
+ blockedPlugins: CapabilityPolicyBlock[]
16
16
  }
17
17
 
18
+ /** @deprecated Use PluginPolicyDecision */
19
+ export type SessionToolPolicyDecision = PluginPolicyDecision
20
+
18
21
  type CapabilityCategory =
19
22
  | 'filesystem'
20
23
  | 'execution'
@@ -47,10 +50,11 @@ const TOOL_DESCRIPTORS: Record<string, ToolDescriptor> = {
47
50
  web_search: { categories: ['network'], concreteTools: ['web_search'] },
48
51
  web_fetch: { categories: ['network'], concreteTools: ['web_fetch'] },
49
52
  browser: { categories: ['browser', 'network'], concreteTools: ['browser', 'openclaw_browser'] },
50
- delegate: { categories: ['delegation', 'execution'], concreteTools: ['delegate', 'delegate_to_claude_code', 'delegate_to_codex_cli', 'delegate_to_opencode_cli'] },
53
+ delegate: { categories: ['delegation', 'execution'], concreteTools: ['delegate', 'delegate_to_claude_code', 'delegate_to_codex_cli', 'delegate_to_opencode_cli', 'delegate_to_gemini_cli'] },
51
54
  claude_code: { categories: ['delegation', 'execution'], concreteTools: ['delegate_to_claude_code'] },
52
55
  codex_cli: { categories: ['delegation', 'execution'], concreteTools: ['delegate_to_codex_cli'] },
53
56
  opencode_cli: { categories: ['delegation', 'execution'], concreteTools: ['delegate_to_opencode_cli'] },
57
+ gemini_cli: { categories: ['delegation', 'execution'], concreteTools: ['delegate_to_gemini_cli'] },
54
58
  memory: { categories: ['memory'], concreteTools: ['memory', 'memory_tool', 'context_status', 'context_summarize'] },
55
59
  sandbox: { categories: ['execution', 'filesystem'], concreteTools: ['sandbox', 'sandbox_exec', 'sandbox_list_runtimes', 'openclaw_sandbox'] },
56
60
  git: { categories: ['execution', 'filesystem'], concreteTools: ['git'] },
@@ -78,6 +82,12 @@ const TOOL_DESCRIPTORS: Record<string, ToolDescriptor> = {
78
82
  context_mgmt: { categories: ['memory'], concreteTools: ['context_mgmt', 'context_status', 'context_summarize'] },
79
83
  plugin_creator: { categories: ['filesystem', 'execution'], concreteTools: ['plugin_creator', 'plugin_creator_tool'] },
80
84
  sample_ui: { categories: ['platform'], concreteTools: ['sample_ui', 'show_plugin_card'] },
85
+ mailbox: { categories: ['network', 'platform', 'outbound'], concreteTools: ['mailbox', 'inbox'] },
86
+ ask_human: { categories: ['platform'], concreteTools: ['ask_human', 'human_loop'] },
87
+ document: { categories: ['filesystem', 'platform'], concreteTools: ['document', 'ocr_document', 'parse_document'] },
88
+ extract: { categories: ['filesystem', 'network'], concreteTools: ['extract', 'extract_structured'] },
89
+ table: { categories: ['filesystem'], concreteTools: ['table', 'dataframe'] },
90
+ crawl: { categories: ['network'], concreteTools: ['crawl', 'site_crawler'] },
81
91
  }
82
92
 
83
93
  const CONCRETE_TOOL_TO_SESSION_TOOL = new Map<string, string>()
@@ -144,6 +154,7 @@ function safetyMatchesTool(safetyBlocked: Set<string>, toolName: string, descrip
144
154
  if (toolName === 'claude_code' && safetyBlocked.has('delegate_to_claude_code')) return true
145
155
  if (toolName === 'codex_cli' && safetyBlocked.has('delegate_to_codex_cli')) return true
146
156
  if (toolName === 'opencode_cli' && safetyBlocked.has('delegate_to_opencode_cli')) return true
157
+ if (toolName === 'gemini_cli' && safetyBlocked.has('delegate_to_gemini_cli')) return true
147
158
  return false
148
159
  }
149
160
 
@@ -196,51 +207,51 @@ export function resolveSessionToolPolicy(
196
207
  blockedCategories,
197
208
  } = parsePolicyConfig(normalizedSettings)
198
209
 
199
- const requestedTools = Array.isArray(sessionTools)
200
- ? Array.from(new Set(sessionTools.map((tool) => normalizeName(tool)).filter(Boolean)))
210
+ const requestedPlugins = Array.isArray(sessionTools)
211
+ ? Array.from(new Set(sessionTools.map((id) => normalizeName(id)).filter(Boolean)))
201
212
  : []
202
213
 
203
- const enabledTools: string[] = []
204
- const blockedTools: CapabilityPolicyBlock[] = []
214
+ const enabledPlugins: string[] = []
215
+ const blockedPlugins: CapabilityPolicyBlock[] = []
205
216
 
206
- for (const toolName of requestedTools) {
207
- const descriptor = TOOL_DESCRIPTORS[toolName]
217
+ for (const pluginName of requestedPlugins) {
218
+ const descriptor = TOOL_DESCRIPTORS[pluginName]
208
219
 
209
- if (safetyMatchesTool(safetyBlocked, toolName, descriptor)) {
210
- blockedTools.push({ tool: toolName, reason: 'blocked by safety policy', source: 'safety' })
220
+ if (safetyMatchesTool(safetyBlocked, pluginName, descriptor)) {
221
+ blockedPlugins.push({ tool: pluginName, reason: 'blocked by safety policy', source: 'safety' })
211
222
  continue
212
223
  }
213
224
 
214
- if (policyAllowedNames.has(toolName)) {
215
- enabledTools.push(toolName)
225
+ if (policyAllowedNames.has(pluginName)) {
226
+ enabledPlugins.push(pluginName)
216
227
  continue
217
228
  }
218
229
 
219
- if (policyMatchesTool(policyBlockedNames, toolName, descriptor)) {
220
- blockedTools.push({ tool: toolName, reason: 'blocked by explicit policy rule', source: 'policy' })
230
+ if (policyMatchesTool(policyBlockedNames, pluginName, descriptor)) {
231
+ blockedPlugins.push({ tool: pluginName, reason: 'blocked by explicit policy rule', source: 'policy' })
221
232
  continue
222
233
  }
223
234
 
224
235
  const categoryReason = categoryBlockReason(blockedCategories, descriptor)
225
236
  if (categoryReason) {
226
- blockedTools.push({ tool: toolName, reason: categoryReason, source: 'policy' })
237
+ blockedPlugins.push({ tool: pluginName, reason: categoryReason, source: 'policy' })
227
238
  continue
228
239
  }
229
240
 
230
- const modeReason = modeBlocksTool(mode, toolName, descriptor)
241
+ const modeReason = modeBlocksTool(mode, pluginName, descriptor)
231
242
  if (modeReason) {
232
- blockedTools.push({ tool: toolName, reason: modeReason, source: 'policy' })
243
+ blockedPlugins.push({ tool: pluginName, reason: modeReason, source: 'policy' })
233
244
  continue
234
245
  }
235
246
 
236
- enabledTools.push(toolName)
247
+ enabledPlugins.push(pluginName)
237
248
  }
238
249
 
239
250
  return {
240
251
  mode,
241
- requestedTools,
242
- enabledTools,
243
- blockedTools,
252
+ requestedPlugins,
253
+ enabledPlugins,
254
+ blockedPlugins,
244
255
  }
245
256
  }
246
257
 
@@ -270,11 +281,11 @@ export function resolveConcreteToolPolicyBlock(
270
281
  }
271
282
 
272
283
  if (mappedTool) {
273
- const blockedRoot = decision.blockedTools.find((entry) => entry.tool === mappedTool)
284
+ const blockedRoot = decision.blockedPlugins.find((entry) => entry.tool === mappedTool)
274
285
  if (blockedRoot) return blockedRoot.reason
275
286
 
276
- const enabledRoot = decision.enabledTools.includes(mappedTool)
277
- if (!enabledRoot) return `tool family "${mappedTool}" is not enabled for this session`
287
+ const enabledRoot = decision.enabledPlugins.includes(mappedTool)
288
+ if (!enabledRoot) return `plugin family "${mappedTool}" is not enabled for this chat`
278
289
  }
279
290
 
280
291
  return null
@@ -6,6 +6,7 @@ export interface RetryOptions {
6
6
  maxAttempts?: number
7
7
  backoffMs?: number
8
8
  retryable?: RegExp[]
9
+ onRetry?: (attempt: number, lastResult: string) => Promise<void> | void
9
10
  }
10
11
 
11
12
  const DEFAULT_RETRYABLE: RegExp[] = [
@@ -49,6 +50,7 @@ export async function withRetry<TArgs>(
49
50
 
50
51
  // Only retry if the result looks like a retryable error
51
52
  if (attempt < maxAttempts && isRetryableError(lastResult, retryable)) {
53
+ await opts?.onRetry?.(attempt, lastResult)
52
54
  const delay = backoffMs * Math.pow(2, attempt - 1)
53
55
  console.warn(
54
56
  `[tool-retry] Attempt ${attempt}/${maxAttempts} matched retryable pattern, retrying in ${delay}ms`,
@@ -0,0 +1,173 @@
1
+ import assert from 'node:assert/strict'
2
+ import fs from 'node:fs'
3
+ import os from 'node:os'
4
+ import path from 'node:path'
5
+ import { after, before, describe, it } from 'node:test'
6
+
7
+ const originalEnv = {
8
+ DATA_DIR: process.env.DATA_DIR,
9
+ WORKSPACE_DIR: process.env.WORKSPACE_DIR,
10
+ SWARMCLAW_BUILD_MODE: process.env.SWARMCLAW_BUILD_MODE,
11
+ }
12
+
13
+ let tempDir = ''
14
+ let watchJobs: typeof import('./watch-jobs')
15
+ let storage: typeof import('./storage')
16
+
17
+ before(async () => {
18
+ tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'swarmclaw-watch-jobs-'))
19
+ process.env.DATA_DIR = path.join(tempDir, 'data')
20
+ process.env.WORKSPACE_DIR = path.join(tempDir, 'workspace')
21
+ process.env.SWARMCLAW_BUILD_MODE = '1'
22
+ watchJobs = await import('./watch-jobs')
23
+ storage = await import('./storage')
24
+ })
25
+
26
+ after(() => {
27
+ if (originalEnv.DATA_DIR === undefined) delete process.env.DATA_DIR
28
+ else process.env.DATA_DIR = originalEnv.DATA_DIR
29
+ if (originalEnv.WORKSPACE_DIR === undefined) delete process.env.WORKSPACE_DIR
30
+ else process.env.WORKSPACE_DIR = originalEnv.WORKSPACE_DIR
31
+ if (originalEnv.SWARMCLAW_BUILD_MODE === undefined) delete process.env.SWARMCLAW_BUILD_MODE
32
+ else process.env.SWARMCLAW_BUILD_MODE = originalEnv.SWARMCLAW_BUILD_MODE
33
+ fs.rmSync(tempDir, { recursive: true, force: true })
34
+ })
35
+
36
+ describe('watch-jobs', () => {
37
+ it('validates required targets for durable watches', async () => {
38
+ await assert.rejects(
39
+ watchJobs.createWatchJob({
40
+ type: 'http',
41
+ resumeMessage: 'resume',
42
+ target: {},
43
+ condition: {},
44
+ }),
45
+ /url target/,
46
+ )
47
+
48
+ await assert.rejects(
49
+ watchJobs.createWatchJob({
50
+ type: 'time',
51
+ resumeMessage: 'resume',
52
+ target: { source: 'test' },
53
+ condition: {},
54
+ }),
55
+ /runAt or delayMinutes/,
56
+ )
57
+ })
58
+
59
+ it('triggers time and task watches durably', async () => {
60
+ const tasks = storage.loadTasks()
61
+ tasks.task_done = {
62
+ id: 'task_done',
63
+ title: 'done',
64
+ status: 'completed',
65
+ result: 'ok',
66
+ createdAt: Date.now(),
67
+ updatedAt: Date.now(),
68
+ }
69
+ storage.saveTasks(tasks)
70
+
71
+ const timeJob = await watchJobs.createWatchJob({
72
+ type: 'time',
73
+ resumeMessage: 'wake up',
74
+ target: { source: 'schedule_wake' },
75
+ condition: {},
76
+ runAt: Date.now() - 1000,
77
+ })
78
+ const taskJob = await watchJobs.createWatchJob({
79
+ type: 'task',
80
+ resumeMessage: 'task finished',
81
+ target: { taskId: 'task_done' },
82
+ condition: { statusIn: ['completed'] },
83
+ })
84
+
85
+ const outcome = await watchJobs.processDueWatchJobs(Date.now())
86
+
87
+ assert.equal(outcome.triggered >= 2, true)
88
+ assert.equal(watchJobs.getWatchJob(timeJob.id)?.status, 'triggered')
89
+ assert.equal(watchJobs.getWatchJob(taskJob.id)?.status, 'triggered')
90
+ assert.equal(watchJobs.getWatchJob(taskJob.id)?.result?.status, 'completed')
91
+ })
92
+
93
+ it('captures file changes and webhook triggers', async () => {
94
+ const watchedFile = path.join(tempDir, 'watch.txt')
95
+ fs.writeFileSync(watchedFile, 'alpha')
96
+
97
+ const fileJob = await watchJobs.createWatchJob({
98
+ type: 'file',
99
+ resumeMessage: 'file changed',
100
+ target: { path: watchedFile },
101
+ condition: { changed: true },
102
+ })
103
+ const webhookJob = await watchJobs.createWatchJob({
104
+ type: 'webhook',
105
+ resumeMessage: 'webhook arrived',
106
+ target: { webhookId: 'wh_1' },
107
+ condition: { event: 'build.finished' },
108
+ })
109
+
110
+ fs.writeFileSync(watchedFile, 'beta')
111
+ await watchJobs.processDueWatchJobs(Date.now())
112
+ const webhookMatches = watchJobs.triggerWebhookWatchJobs({
113
+ webhookId: 'wh_1',
114
+ event: 'build.finished',
115
+ payloadPreview: '{"ok":true}',
116
+ })
117
+
118
+ assert.equal(watchJobs.getWatchJob(fileJob.id)?.status, 'triggered')
119
+ assert.match(String(watchJobs.getWatchJob(fileJob.id)?.result?.preview || ''), /beta/)
120
+ assert.equal(webhookMatches.length, 1)
121
+ assert.equal(watchJobs.getWatchJob(webhookJob.id)?.status, 'triggered')
122
+ })
123
+
124
+ it('wakes mailbox and approval watches from event triggers', async () => {
125
+ storage.upsertApproval('approval_1', {
126
+ id: 'approval_1',
127
+ category: 'human_loop',
128
+ title: 'Need approval',
129
+ description: 'Approve the action',
130
+ data: {},
131
+ createdAt: Date.now(),
132
+ updatedAt: Date.now(),
133
+ status: 'pending',
134
+ })
135
+
136
+ const mailboxJob = await watchJobs.createWatchJob({
137
+ type: 'mailbox',
138
+ resumeMessage: 'human replied',
139
+ target: { sessionId: 'session_1' },
140
+ condition: { type: 'human_reply', correlationId: 'corr_1' },
141
+ })
142
+ const approvalJob = await watchJobs.createWatchJob({
143
+ type: 'approval',
144
+ resumeMessage: 'approval updated',
145
+ target: { approvalId: 'approval_1' },
146
+ condition: { statusIn: ['approved'] },
147
+ })
148
+
149
+ const mailboxMatches = watchJobs.triggerMailboxWatchJobs({
150
+ sessionId: 'session_1',
151
+ envelope: {
152
+ id: 'env_1',
153
+ type: 'human_reply',
154
+ payload: 'approved',
155
+ toSessionId: 'session_1',
156
+ correlationId: 'corr_1',
157
+ status: 'new',
158
+ createdAt: Date.now(),
159
+ },
160
+ })
161
+ const approvalMatches = watchJobs.triggerApprovalWatchJobs({
162
+ approvalId: 'approval_1',
163
+ status: 'approved',
164
+ })
165
+
166
+ assert.equal(mailboxMatches.length, 1)
167
+ assert.equal(approvalMatches.length, 1)
168
+ assert.equal(watchJobs.getWatchJob(mailboxJob.id)?.status, 'triggered')
169
+ assert.equal(watchJobs.getWatchJob(approvalJob.id)?.status, 'triggered')
170
+ assert.equal(watchJobs.getWatchJob(mailboxJob.id)?.result?.correlationId, 'corr_1')
171
+ assert.equal(watchJobs.getWatchJob(approvalJob.id)?.result?.status, 'approved')
172
+ })
173
+ })