@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
@@ -47,6 +47,25 @@ function getFileEntryContent(entry: Record<string, unknown> | undefined): string
47
47
  return typeof raw === 'string' ? raw : JSON.stringify(raw)
48
48
  }
49
49
 
50
+ function parseFileEntries(value: unknown): Array<Record<string, unknown>> | undefined {
51
+ const candidates = [value]
52
+ if (typeof value === 'string') {
53
+ const trimmed = value.trim()
54
+ if (trimmed.startsWith('[')) {
55
+ try {
56
+ candidates.unshift(JSON.parse(trimmed))
57
+ } catch {
58
+ // ignore malformed JSON payloads and fall back to the raw string
59
+ }
60
+ }
61
+ }
62
+ for (const candidate of candidates) {
63
+ if (!Array.isArray(candidate)) continue
64
+ return candidate.filter((entry): entry is Record<string, unknown> => !!entry && typeof entry === 'object' && !Array.isArray(entry))
65
+ }
66
+ return undefined
67
+ }
68
+
50
69
  function inferFileAction(
51
70
  normalized: Record<string, unknown>,
52
71
  files: Array<Record<string, unknown>> | undefined,
@@ -75,9 +94,7 @@ export function normalizeFileArgs(rawArgs: Record<string, unknown>): Record<stri
75
94
  ...normalized,
76
95
  ...(actionPayload?.value || {}),
77
96
  }
78
- const files = Array.isArray(merged.files)
79
- ? merged.files.filter((entry): entry is Record<string, unknown> => !!entry && typeof entry === 'object' && !Array.isArray(entry))
80
- : undefined
97
+ const files = parseFileEntries(merged.files)
81
98
 
82
99
  let action = pickNonEmptyString(normalized.action, actionPayload?.action)
83
100
  if (!action && Array.isArray(files) && files.length > 0) {
@@ -131,6 +148,24 @@ function resolveFileToolPath(cwd: string, target: string): string {
131
148
  }
132
149
  }
133
150
 
151
+ const BINARY_FILE_EXTENSIONS = new Set([
152
+ '.png', '.jpg', '.jpeg', '.gif', '.webp', '.bmp', '.ico', '.svg', '.pdf',
153
+ '.zip', '.gz', '.tar', '.tgz', '.7z', '.rar',
154
+ '.mp3', '.wav', '.ogg', '.m4a', '.mp4', '.mov', '.avi', '.webm',
155
+ '.woff', '.woff2', '.ttf', '.otf',
156
+ '.exe', '.dll', '.so', '.dylib', '.bin',
157
+ ])
158
+
159
+ function isLikelyBinaryFile(resolvedPath: string, data: Buffer): boolean {
160
+ const ext = path.extname(resolvedPath).toLowerCase()
161
+ if (BINARY_FILE_EXTENSIONS.has(ext)) return true
162
+ const sample = data.subarray(0, Math.min(data.length, 512))
163
+ for (const byte of sample) {
164
+ if (byte === 0) return true
165
+ }
166
+ return false
167
+ }
168
+
134
169
  /**
135
170
  * Unified File Execution Logic
136
171
  */
@@ -154,7 +189,11 @@ export async function executeFileAction(args: Record<string, unknown>, bctx: { c
154
189
  const target = filePath || getFileEntryPath(files?.[0])
155
190
  if (!target) return 'Error: no filePath or path provided.'
156
191
  const resolved = resolveFileToolPath(bctx.cwd, target)
157
- return truncate(fs.readFileSync(resolved, 'utf-8'), MAX_FILE)
192
+ const data = fs.readFileSync(resolved)
193
+ if (isLikelyBinaryFile(resolved, data)) {
194
+ return `Binary file: ${target} (${data.byteLength} bytes). I did not inline its contents. Use send_file with this path to share it.`
195
+ }
196
+ return truncate(data.toString('utf-8'), MAX_FILE)
158
197
  }
159
198
 
160
199
  case 'write': {
@@ -58,7 +58,12 @@ async function executeHumanLoopAction(args: Record<string, unknown>, bctx: { ses
58
58
  if (action === 'ack_mailbox') {
59
59
  const sessionId = typeof normalized.sessionId === 'string' ? normalized.sessionId : bctx.sessionId
60
60
  if (!sessionId) return 'Error: sessionId or current session is required.'
61
- const envelopeId = typeof normalized.envelopeId === 'string' ? normalized.envelopeId.trim() : ''
61
+ let envelopeId = typeof normalized.envelopeId === 'string' ? normalized.envelopeId.trim() : ''
62
+ if (!envelopeId) {
63
+ const newestReply = listMailbox(sessionId, { limit: 50 })
64
+ .find((envelope) => envelope.type === 'human_reply' && envelope.status !== 'ack')
65
+ if (newestReply) envelopeId = newestReply.id
66
+ }
62
67
  if (!envelopeId) return 'Error: envelopeId is required.'
63
68
  const envelope = ackMailboxEnvelope(sessionId, envelopeId)
64
69
  return envelope ? JSON.stringify(envelope) : `Error: mailbox envelope "${envelopeId}" not found.`
@@ -108,7 +113,10 @@ async function executeHumanLoopAction(args: Record<string, unknown>, bctx: { ses
108
113
  containsText: typeof normalized.containsText === 'string' ? normalized.containsText : undefined,
109
114
  },
110
115
  })
111
- return JSON.stringify(job)
116
+ return JSON.stringify({
117
+ ...job,
118
+ message: 'Durable wait registered. Stop active tool use now and continue on the next agent turn when the human reply arrives.',
119
+ })
112
120
  }
113
121
 
114
122
  if (action === 'wait_for_approval') {
@@ -135,7 +143,10 @@ async function executeHumanLoopAction(args: Record<string, unknown>, bctx: { ses
135
143
  : ['approved', 'rejected'],
136
144
  },
137
145
  })
138
- return JSON.stringify(job)
146
+ return JSON.stringify({
147
+ ...job,
148
+ message: 'Durable approval wait registered. Stop active tool use now and continue on the next agent turn when the approval decision arrives.',
149
+ })
139
150
  }
140
151
 
141
152
  if (action === 'status') {
@@ -150,7 +161,7 @@ async function executeHumanLoopAction(args: Record<string, unknown>, bctx: { ses
150
161
  const watch = getWatchJob(watchJobId)
151
162
  return watch ? JSON.stringify(watch) : `Error: watch job "${watchJobId}" not found.`
152
163
  }
153
- return 'Error: approvalId or watchJobId is required for status.'
164
+ return 'Error: approvalId or watchJobId is required for status. Use list_mailbox to inspect replies, or wait_for_reply / wait_for_approval to create a durable watch first.'
154
165
  }
155
166
 
156
167
  return `Error: Unknown action "${action}".`
@@ -166,11 +177,30 @@ const HumanLoopPlugin: Plugin = {
166
177
  hooks: {
167
178
  getCapabilityDescription: () =>
168
179
  'I can request structured human input or explicit approvals with `ask_human`, then pause on durable wait handles until the response arrives.',
180
+ getApprovalGuidance: ({ approval, phase, approved }) => {
181
+ if (approval.category !== 'human_loop') return null
182
+ if (phase === 'request') {
183
+ return [
184
+ 'When this approval is decided, continue the blocked task instead of asking the same human approval question again.',
185
+ 'Use `ask_human` only for fresh questions, durable waits, or status checks. Do not duplicate the same approval request while it is pending.',
186
+ ]
187
+ }
188
+ if (phase === 'connector_reminder') {
189
+ return 'Approving this lets the agent resume the blocked task without repeating the same human-loop request.'
190
+ }
191
+ if (approved !== true) {
192
+ return 'Do not repeat the rejected human-loop approval request unless the question or requested action materially changes.'
193
+ }
194
+ return [
195
+ 'Resume the blocked task immediately after approval.',
196
+ 'Do not call `ask_human` action `request_approval` again for the same exact question.',
197
+ ]
198
+ },
169
199
  } as PluginHooks,
170
200
  tools: [
171
201
  {
172
202
  name: 'ask_human',
173
- description: 'Human-loop tool. Actions: request_input, request_approval, wait_for_reply, wait_for_approval, list_mailbox, ack_mailbox, status.',
203
+ description: 'Human-loop tool. Use request_input(question, ...) to ask a human, wait_for_reply(correlationId) for durable waiting, list_mailbox to read replies, ack_mailbox(envelopeId) to acknowledge them, and status(approvalId or watchJobId) only when you have an id.',
174
204
  parameters: {
175
205
  type: 'object',
176
206
  properties: {
@@ -1,7 +1,7 @@
1
1
  import { z } from 'zod'
2
2
  import { tool, type StructuredToolInterface } from '@langchain/core/tools'
3
3
  import type { Session } from '@/types'
4
- import { loadSettings, loadSessions, saveSessions, loadMcpServers } from '../storage'
4
+ import { loadApprovals, loadSettings, loadSessions, saveSessions, loadMcpServers } from '../storage'
5
5
  import { loadRuntimeSettings } from '../runtime-settings'
6
6
  import { log } from '../logger'
7
7
  import { resolveSessionToolPolicy } from '../tool-capability-policy'
@@ -29,7 +29,6 @@ import { buildCrudTools } from './crud'
29
29
  import { buildSessionInfoTools } from './session-info'
30
30
  import { buildOpenClawNodeTools } from './openclaw-nodes'
31
31
  import { buildContextTools } from './context-mgmt'
32
- import { buildConnectorTools } from './connector'
33
32
  import { buildDiscoveryTools } from './discovery'
34
33
  import { buildMonitorTools } from './monitor'
35
34
  import { buildSampleUITools } from './sample-ui'
@@ -44,6 +43,7 @@ import { buildDocumentTools } from './document'
44
43
  import { buildExtractTools } from './extract'
45
44
  import { buildTableTools } from './table'
46
45
  import { buildCrawlTools } from './crawl'
46
+ import './connector'
47
47
  import { normalizeToolInputArgs } from './normalize-tool-args'
48
48
 
49
49
  import { getPluginManager } from '../plugins'
@@ -52,6 +52,23 @@ import { jsonSchemaToZod } from '../mcp-client'
52
52
  export type { ToolContext, SessionToolsResult }
53
53
  export { sweepOrphanedBrowsers, cleanupSessionBrowser, getActiveBrowserCount, hasActiveBrowser }
54
54
 
55
+ function approvedToolAccessIds(ctx?: ToolContext): string[] {
56
+ if (!ctx?.sessionId && !ctx?.agentId) return []
57
+ const approvals = loadApprovals()
58
+ const granted = new Set<string>()
59
+ for (const request of Object.values(approvals) as Array<Record<string, unknown>>) {
60
+ if (request?.status !== 'approved' || request?.category !== 'tool_access') continue
61
+ const sessionMatch = ctx.sessionId && request.sessionId === ctx.sessionId
62
+ const agentMatch = ctx.agentId && request.agentId === ctx.agentId
63
+ if (!sessionMatch && !agentMatch) continue
64
+ const toolId = typeof request.data === 'object' && request.data && !Array.isArray(request.data)
65
+ ? String((request.data as Record<string, unknown>).toolId || (request.data as Record<string, unknown>).pluginId || '').trim()
66
+ : ''
67
+ if (toolId) granted.add(toolId)
68
+ }
69
+ return [...granted]
70
+ }
71
+
55
72
  export async function buildSessionTools(cwd: string, enabledPlugins: string[], ctx?: ToolContext): Promise<SessionToolsResult> {
56
73
  const tools: StructuredToolInterface[] = []
57
74
  const cleanupFns: (() => Promise<void>)[] = []
@@ -62,7 +79,26 @@ export async function buildSessionTools(cwd: string, enabledPlugins: string[], c
62
79
  const claudeTimeoutMs = runtime.claudeCodeTimeoutMs
63
80
  const cliProcessTimeoutMs = runtime.cliProcessTimeoutMs
64
81
  const appSettings = loadSettings()
65
- const toolPolicy = resolveSessionToolPolicy(enabledPlugins, appSettings)
82
+ const grantedToolIds = approvedToolAccessIds(ctx)
83
+ const effectiveEnabledPlugins = Array.from(new Set([
84
+ ...(Array.isArray(enabledPlugins) ? enabledPlugins : []),
85
+ ...grantedToolIds,
86
+ ]))
87
+ if (ctx?.sessionId && grantedToolIds.length > 0) {
88
+ const sessions = loadSessions()
89
+ const currentSession = sessions[ctx.sessionId]
90
+ if (currentSession) {
91
+ const currentPlugins = Array.isArray(currentSession.plugins) ? currentSession.plugins : []
92
+ const mergedPlugins = Array.from(new Set([...currentPlugins, ...grantedToolIds]))
93
+ if (mergedPlugins.length !== currentPlugins.length) {
94
+ currentSession.plugins = mergedPlugins
95
+ currentSession.updatedAt = Date.now()
96
+ sessions[ctx.sessionId] = currentSession
97
+ saveSessions(sessions)
98
+ }
99
+ }
100
+ }
101
+ const toolPolicy = resolveSessionToolPolicy(effectiveEnabledPlugins, appSettings)
66
102
  const expandedEnabled = expandPluginIds(toolPolicy.enabledPlugins)
67
103
  const expandedBlocked = expandPluginIds(toolPolicy.blockedPlugins.map((entry) => entry.tool))
68
104
  const blockedSet = new Set(expandedBlocked)
@@ -72,7 +108,7 @@ export async function buildSessionTools(cwd: string, enabledPlugins: string[], c
72
108
  && !filteredEnabled.includes('process')
73
109
  && !blockedSet.has('process')
74
110
  ? [...filteredEnabled, 'process']
75
- : filteredEnabled).filter(t => pluginManager.isEnabled(t))
111
+ : filteredEnabled).filter((pluginId) => !pluginManager.isExplicitlyDisabled(pluginId))
76
112
  const activePluginSet = new Set(activePlugins)
77
113
  const hasPlugin = (pluginName: string) => activePluginSet.has(pluginName)
78
114
  /** @deprecated Use hasPlugin */
@@ -155,7 +191,6 @@ export async function buildSessionTools(cwd: string, enabledPlugins: string[], c
155
191
  ['manage_sessions', buildSessionInfoTools],
156
192
  ['openclaw_nodes', buildOpenClawNodeTools],
157
193
  ['context_mgmt', buildContextTools],
158
- ['manage_connectors', buildConnectorTools],
159
194
  ['discovery', buildDiscoveryTools],
160
195
  ['monitor', buildMonitorTools],
161
196
  ['sample_ui', buildSampleUITools],
@@ -206,13 +241,13 @@ export async function buildSessionTools(cwd: string, enabledPlugins: string[], c
206
241
  tools.push(
207
242
  tool(
208
243
  async (args) => {
209
- if (!pluginManager.isEnabled(entry.pluginId)) {
244
+ if (pluginManager.isExplicitlyDisabled(entry.pluginId)) {
210
245
  throw new Error(`Plugin "${entry.pluginId}" is disabled`)
211
246
  }
212
247
  try {
213
248
  const normalizedArgs = normalizeToolInputArgs((args ?? {}) as Record<string, unknown>)
214
249
  const res = await pt.execute(normalizedArgs, {
215
- session: { ...ctx, cwd } as any,
250
+ session: { ...(ctx || {}), ...bctx } as any,
216
251
  message: '',
217
252
  })
218
253
  pluginManager.recordExternalToolSuccess(entry.pluginId)
@@ -293,14 +328,14 @@ export async function buildSessionTools(cwd: string, enabledPlugins: string[], c
293
328
  type: 'tool_request',
294
329
  toolId,
295
330
  autoApproved: true,
296
- message: `Tool access for "${toolId}" was granted. Proceed to use it directly.`,
331
+ message: `Tool access for "${toolId}" was granted. It will be available on the next agent turn.`,
297
332
  })
298
333
  }
299
334
  return JSON.stringify({
300
335
  type: 'tool_request',
301
336
  toolId,
302
337
  reason,
303
- message: `Tool access request sent to user for "${toolId}". Once granted, continue immediately with the original task using the newly available tool.`,
338
+ message: `Tool access request sent to user for "${toolId}". Once granted, use it on the next agent turn.`,
304
339
  })
305
340
  },
306
341
  {
@@ -0,0 +1,139 @@
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 { spawnSync } from 'node:child_process'
6
+ import { describe, it } from 'node:test'
7
+
8
+ const repoRoot = path.resolve(path.dirname(new URL(import.meta.url).pathname), '../../../..')
9
+
10
+ function runWithTempDataDir(script: string) {
11
+ const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'swarmclaw-connectors-tool-'))
12
+ try {
13
+ const result = spawnSync(process.execPath, ['--import', 'tsx', '--input-type=module', '--eval', script], {
14
+ cwd: repoRoot,
15
+ env: {
16
+ ...process.env,
17
+ DATA_DIR: path.join(tempDir, 'data'),
18
+ WORKSPACE_DIR: path.join(tempDir, 'workspace'),
19
+ },
20
+ encoding: 'utf-8',
21
+ })
22
+ assert.equal(result.status, 0, result.stderr || result.stdout || 'subprocess failed')
23
+ const lines = (result.stdout || '')
24
+ .trim()
25
+ .split('\n')
26
+ .map((line) => line.trim())
27
+ .filter(Boolean)
28
+ const jsonLine = [...lines].reverse().find((line) => line.startsWith('{'))
29
+ return JSON.parse(jsonLine || '{}')
30
+ } finally {
31
+ fs.rmSync(tempDir, { recursive: true, force: true })
32
+ }
33
+ }
34
+
35
+ describe('manage_connectors tool', () => {
36
+ it('drops transient outbound-send args on create', () => {
37
+ const output = runWithTempDataDir(`
38
+ const storageMod = await import('./src/lib/server/storage.ts')
39
+ const crudMod = await import('./src/lib/server/session-tools/crud.ts')
40
+ const storage = storageMod.default || storageMod
41
+ const crud = crudMod.default || crudMod
42
+
43
+ const tools = crud.buildCrudTools({
44
+ cwd: process.env.WORKSPACE_DIR,
45
+ ctx: { sessionId: 'session-1', agentId: 'agent-1', platformAssignScope: 'all' },
46
+ hasPlugin: (name) => name === 'manage_connectors',
47
+ })
48
+ const tool = tools.find((entry) => entry.name === 'manage_connectors')
49
+ await tool.invoke({
50
+ action: 'create',
51
+ data: JSON.stringify({
52
+ name: 'Main WhatsApp',
53
+ platform: 'whatsapp',
54
+ agentId: 'agent-1',
55
+ enabled: true,
56
+ action: 'send_voice_note',
57
+ message: 'hello',
58
+ mediaPath: 'voice_note_gran.mp3',
59
+ connectorId: 'd81cd63b',
60
+ config: {
61
+ taskFollowups: true,
62
+ action: 'send',
63
+ },
64
+ }),
65
+ })
66
+
67
+ const connector = Object.values(storage.loadConnectors())[0]
68
+ console.log(JSON.stringify({ connector }))
69
+ `)
70
+
71
+ assert.equal(output.connector.name, 'Main WhatsApp')
72
+ assert.equal(output.connector.platform, 'whatsapp')
73
+ assert.equal(output.connector.agentId, 'agent-1')
74
+ assert.equal(output.connector.isEnabled, true)
75
+ assert.equal(output.connector.action, undefined)
76
+ assert.equal(output.connector.message, undefined)
77
+ assert.equal(output.connector.mediaPath, undefined)
78
+ assert.equal(output.connector.connectorId, undefined)
79
+ assert.deepEqual(output.connector.config, {
80
+ taskFollowups: 'true',
81
+ action: 'send',
82
+ })
83
+ })
84
+
85
+ it('ignores send-like update payloads instead of mutating connector routing state', () => {
86
+ const output = runWithTempDataDir(`
87
+ const storageMod = await import('./src/lib/server/storage.ts')
88
+ const crudMod = await import('./src/lib/server/session-tools/crud.ts')
89
+ const storage = storageMod.default || storageMod
90
+ const crud = crudMod.default || crudMod
91
+
92
+ const now = Date.now()
93
+ storage.saveConnectors({
94
+ conn_1: {
95
+ id: 'conn_1',
96
+ name: 'Main WhatsApp',
97
+ platform: 'whatsapp',
98
+ agentId: 'e355bf7a',
99
+ credentialId: 'cred-1',
100
+ config: {
101
+ allowFrom: 'me',
102
+ },
103
+ isEnabled: true,
104
+ status: 'running',
105
+ createdAt: now,
106
+ updatedAt: now,
107
+ },
108
+ })
109
+
110
+ const tools = crud.buildCrudTools({
111
+ cwd: process.env.WORKSPACE_DIR,
112
+ ctx: { sessionId: 'session-1', agentId: 'e355bf7a', platformAssignScope: 'all' },
113
+ hasPlugin: (name) => name === 'manage_connectors',
114
+ })
115
+ const tool = tools.find((entry) => entry.name === 'manage_connectors')
116
+ const raw = await tool.invoke({
117
+ action: 'update',
118
+ id: 'conn_1',
119
+ data: JSON.stringify({
120
+ action: 'send',
121
+ message: 'hello there',
122
+ mediaPath: 'voice_note_gran.mp3',
123
+ connectorId: 'conn_1',
124
+ }),
125
+ })
126
+
127
+ const connector = storage.loadConnectors().conn_1
128
+ console.log(JSON.stringify({ raw, connector }))
129
+ `)
130
+
131
+ assert.equal(output.connector.agentId, 'e355bf7a')
132
+ assert.equal(output.connector.credentialId, 'cred-1')
133
+ assert.deepEqual(output.connector.config, { allowFrom: 'me' })
134
+ assert.equal(output.connector.action, undefined)
135
+ assert.equal(output.connector.message, undefined)
136
+ assert.equal(output.connector.mediaPath, undefined)
137
+ assert.equal(output.connector.connectorId, undefined)
138
+ })
139
+ })