@swarmclawai/swarmclaw 0.7.8 → 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 (251) hide show
  1. package/README.md +12 -15
  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 +22 -2
  15. package/src/app/api/clawhub/install/route.ts +28 -8
  16. package/src/app/api/connectors/[id]/route.ts +26 -1
  17. package/src/app/api/external-agents/route.test.ts +165 -0
  18. package/src/app/api/gateways/[id]/health/route.ts +27 -12
  19. package/src/app/api/gateways/[id]/route.ts +2 -0
  20. package/src/app/api/gateways/health-route.test.ts +135 -0
  21. package/src/app/api/gateways/route.ts +2 -0
  22. package/src/app/api/mcp-servers/route.test.ts +130 -0
  23. package/src/app/api/openclaw/deploy/route.ts +38 -5
  24. package/src/app/api/plugins/install/route.ts +46 -6
  25. package/src/app/api/plugins/marketplace/route.ts +48 -15
  26. package/src/app/api/preview-server/route.ts +26 -11
  27. package/src/app/api/schedules/[id]/run/route.ts +4 -0
  28. package/src/app/api/schedules/route.test.ts +86 -0
  29. package/src/app/api/schedules/route.ts +6 -1
  30. package/src/app/api/setup/check-provider/route.test.ts +19 -0
  31. package/src/app/api/setup/check-provider/route.ts +40 -10
  32. package/src/app/api/skills/[id]/route.ts +12 -0
  33. package/src/app/api/skills/import/route.ts +14 -12
  34. package/src/app/api/skills/route.ts +13 -1
  35. package/src/app/api/tasks/[id]/route.ts +10 -1
  36. package/src/app/api/tasks/import/github/route.test.ts +65 -0
  37. package/src/app/api/tasks/import/github/route.ts +337 -0
  38. package/src/app/api/wallets/[id]/approve/route.ts +17 -3
  39. package/src/app/api/wallets/[id]/route.ts +79 -33
  40. package/src/app/api/wallets/[id]/send/route.ts +19 -33
  41. package/src/app/api/wallets/route.ts +78 -61
  42. package/src/app/api/webhooks/[id]/route.ts +33 -6
  43. package/src/app/api/webhooks/route.test.ts +272 -0
  44. package/src/cli/index.js +1 -0
  45. package/src/cli/spec.js +1 -0
  46. package/src/components/agents/agent-card.tsx +9 -2
  47. package/src/components/agents/agent-chat-list.tsx +18 -2
  48. package/src/components/agents/agent-list.tsx +1 -0
  49. package/src/components/agents/agent-sheet.tsx +73 -24
  50. package/src/components/agents/inspector-panel.tsx +41 -0
  51. package/src/components/canvas/canvas-panel.tsx +236 -65
  52. package/src/components/chat/chat-card.tsx +36 -13
  53. package/src/components/chat/chat-header.tsx +44 -16
  54. package/src/components/chat/chat-list.tsx +28 -4
  55. package/src/components/chat/checkpoint-timeline.tsx +50 -34
  56. package/src/components/chat/message-bubble.tsx +208 -145
  57. package/src/components/chat/message-list.tsx +48 -19
  58. package/src/components/chatrooms/chatroom-message.tsx +2 -2
  59. package/src/components/chatrooms/chatroom-sheet.tsx +16 -2
  60. package/src/components/connectors/connector-health.tsx +1 -1
  61. package/src/components/connectors/connector-list.tsx +7 -2
  62. package/src/components/connectors/connector-sheet.tsx +337 -148
  63. package/src/components/gateways/gateway-sheet.tsx +2 -2
  64. package/src/components/mcp-servers/mcp-server-list.tsx +26 -5
  65. package/src/components/mcp-servers/mcp-server-sheet.tsx +19 -2
  66. package/src/components/openclaw/openclaw-deploy-panel.tsx +269 -21
  67. package/src/components/plugins/plugin-list.tsx +45 -9
  68. package/src/components/plugins/plugin-sheet.tsx +55 -7
  69. package/src/components/providers/provider-list.tsx +2 -1
  70. package/src/components/providers/provider-sheet.tsx +21 -2
  71. package/src/components/schedules/schedule-card.tsx +25 -1
  72. package/src/components/schedules/schedule-sheet.tsx +44 -2
  73. package/src/components/secrets/secret-sheet.tsx +21 -2
  74. package/src/components/shared/agent-switch-dialog.tsx +12 -1
  75. package/src/components/shared/bottom-sheet.tsx +13 -3
  76. package/src/components/shared/command-palette.tsx +8 -1
  77. package/src/components/shared/confirm-dialog.tsx +19 -4
  78. package/src/components/shared/connector-platform-icon.test.ts +28 -0
  79. package/src/components/shared/connector-platform-icon.tsx +39 -6
  80. package/src/components/shared/settings/plugin-manager.tsx +29 -6
  81. package/src/components/shared/settings/section-capability-policy.tsx +7 -3
  82. package/src/components/skills/skill-list.tsx +25 -0
  83. package/src/components/skills/skill-sheet.tsx +84 -12
  84. package/src/components/tasks/approvals-panel.tsx +191 -95
  85. package/src/components/tasks/task-board.tsx +273 -2
  86. package/src/components/tasks/task-card.tsx +38 -9
  87. package/src/components/ui/dialog.tsx +2 -2
  88. package/src/components/wallets/wallet-approval-dialog.tsx +4 -2
  89. package/src/components/wallets/wallet-panel.tsx +435 -90
  90. package/src/components/wallets/wallet-section.tsx +198 -48
  91. package/src/components/webhooks/webhook-sheet.tsx +22 -2
  92. package/src/lib/approval-display.ts +20 -0
  93. package/src/lib/canvas-content.ts +198 -0
  94. package/src/lib/chat-artifact-summary.ts +165 -0
  95. package/src/lib/chat-display.test.ts +91 -0
  96. package/src/lib/chat-display.ts +58 -0
  97. package/src/lib/chat-streaming-state.test.ts +47 -1
  98. package/src/lib/chat-streaming-state.ts +42 -0
  99. package/src/lib/ollama-model.ts +10 -0
  100. package/src/lib/openclaw-endpoint.test.ts +8 -0
  101. package/src/lib/openclaw-endpoint.ts +6 -1
  102. package/src/lib/plugin-install-cors.ts +46 -0
  103. package/src/lib/plugin-sources.test.ts +43 -0
  104. package/src/lib/plugin-sources.ts +77 -0
  105. package/src/lib/providers/ollama.ts +16 -6
  106. package/src/lib/providers/openclaw.test.ts +54 -0
  107. package/src/lib/providers/openclaw.ts +127 -11
  108. package/src/lib/schedule-dedupe-advanced.test.ts +1335 -0
  109. package/src/lib/schedule-dedupe.test.ts +66 -1
  110. package/src/lib/schedule-dedupe.ts +169 -12
  111. package/src/lib/schedule-origin.test.ts +20 -0
  112. package/src/lib/schedule-origin.ts +15 -0
  113. package/src/lib/server/__fixtures__/fake-mcp-stdio-server.mjs +27 -0
  114. package/src/lib/server/agent-availability.ts +16 -0
  115. package/src/lib/server/agent-runtime-config.ts +12 -4
  116. package/src/lib/server/agent-thread-session.test.ts +51 -0
  117. package/src/lib/server/agent-thread-session.ts +7 -0
  118. package/src/lib/server/approval-match.ts +205 -0
  119. package/src/lib/server/approvals-auto-approve.test.ts +538 -1
  120. package/src/lib/server/approvals.ts +214 -1
  121. package/src/lib/server/assistant-control.test.ts +29 -0
  122. package/src/lib/server/assistant-control.ts +23 -0
  123. package/src/lib/server/build-llm.test.ts +79 -0
  124. package/src/lib/server/build-llm.ts +14 -4
  125. package/src/lib/server/canvas-content.test.ts +32 -0
  126. package/src/lib/server/canvas-content.ts +6 -0
  127. package/src/lib/server/capability-router.test.ts +11 -0
  128. package/src/lib/server/capability-router.ts +26 -1
  129. package/src/lib/server/chat-execution-advanced.test.ts +651 -0
  130. package/src/lib/server/chat-execution-disabled.test.ts +94 -0
  131. package/src/lib/server/chat-execution-tool-events.test.ts +157 -0
  132. package/src/lib/server/chat-execution.ts +353 -72
  133. package/src/lib/server/clawhub-client.test.ts +14 -8
  134. package/src/lib/server/connectors/manager.test.ts +1147 -0
  135. package/src/lib/server/connectors/manager.ts +362 -63
  136. package/src/lib/server/connectors/pairing.ts +26 -5
  137. package/src/lib/server/connectors/types.ts +2 -0
  138. package/src/lib/server/connectors/whatsapp.test.ts +134 -0
  139. package/src/lib/server/connectors/whatsapp.ts +271 -47
  140. package/src/lib/server/context-manager.ts +6 -1
  141. package/src/lib/server/daemon-state.ts +1 -1
  142. package/src/lib/server/data-dir.test.ts +37 -0
  143. package/src/lib/server/data-dir.ts +20 -1
  144. package/src/lib/server/delegation-jobs-advanced.test.ts +513 -0
  145. package/src/lib/server/devserver-launch.test.ts +60 -0
  146. package/src/lib/server/devserver-launch.ts +85 -0
  147. package/src/lib/server/elevenlabs.test.ts +189 -1
  148. package/src/lib/server/elevenlabs.ts +147 -43
  149. package/src/lib/server/ethereum.ts +590 -0
  150. package/src/lib/server/eval/agent-regression-advanced.test.ts +302 -0
  151. package/src/lib/server/eval/agent-regression.test.ts +18 -1
  152. package/src/lib/server/eval/agent-regression.ts +383 -11
  153. package/src/lib/server/evm-swap.ts +475 -0
  154. package/src/lib/server/execution-log.ts +1 -0
  155. package/src/lib/server/heartbeat-service-timer.test.ts +173 -0
  156. package/src/lib/server/heartbeat-service.ts +15 -10
  157. package/src/lib/server/heartbeat-wake.test.ts +112 -0
  158. package/src/lib/server/heartbeat-wake.ts +338 -57
  159. package/src/lib/server/main-agent-loop-advanced.test.ts +538 -0
  160. package/src/lib/server/mcp-client.test.ts +16 -0
  161. package/src/lib/server/mcp-client.ts +25 -0
  162. package/src/lib/server/memory-integration.test.ts +719 -0
  163. package/src/lib/server/memory-policy.test.ts +43 -0
  164. package/src/lib/server/memory-policy.ts +132 -0
  165. package/src/lib/server/memory-tiers.test.ts +60 -0
  166. package/src/lib/server/memory-tiers.ts +16 -0
  167. package/src/lib/server/ollama-runtime.ts +58 -0
  168. package/src/lib/server/openclaw-deploy.test.ts +109 -1
  169. package/src/lib/server/openclaw-deploy.ts +557 -81
  170. package/src/lib/server/openclaw-gateway.test.ts +131 -0
  171. package/src/lib/server/openclaw-gateway.ts +10 -4
  172. package/src/lib/server/openclaw-health.test.ts +35 -0
  173. package/src/lib/server/openclaw-health.ts +215 -47
  174. package/src/lib/server/orchestrator-lg.ts +2 -2
  175. package/src/lib/server/plugins-advanced.test.ts +351 -0
  176. package/src/lib/server/plugins.ts +205 -5
  177. package/src/lib/server/queue-advanced.test.ts +528 -0
  178. package/src/lib/server/queue-followups.test.ts +262 -0
  179. package/src/lib/server/queue-reconcile.test.ts +128 -0
  180. package/src/lib/server/queue.ts +293 -61
  181. package/src/lib/server/scheduler.ts +29 -1
  182. package/src/lib/server/session-note.test.ts +36 -0
  183. package/src/lib/server/session-note.ts +42 -0
  184. package/src/lib/server/session-run-manager.ts +52 -4
  185. package/src/lib/server/session-tools/canvas.ts +14 -12
  186. package/src/lib/server/session-tools/connector.test.ts +138 -0
  187. package/src/lib/server/session-tools/connector.ts +348 -61
  188. package/src/lib/server/session-tools/context.ts +12 -3
  189. package/src/lib/server/session-tools/crud.ts +221 -10
  190. package/src/lib/server/session-tools/delegate-fallback.test.ts +103 -0
  191. package/src/lib/server/session-tools/delegate.ts +64 -8
  192. package/src/lib/server/session-tools/discovery-approvals.test.ts +142 -0
  193. package/src/lib/server/session-tools/discovery.ts +80 -12
  194. package/src/lib/server/session-tools/file-normalize.test.ts +36 -0
  195. package/src/lib/server/session-tools/file.ts +43 -4
  196. package/src/lib/server/session-tools/human-loop.ts +35 -5
  197. package/src/lib/server/session-tools/index.ts +44 -9
  198. package/src/lib/server/session-tools/manage-connectors.test.ts +139 -0
  199. package/src/lib/server/session-tools/manage-schedules-advanced.test.ts +564 -0
  200. package/src/lib/server/session-tools/manage-schedules.test.ts +283 -0
  201. package/src/lib/server/session-tools/manage-tasks-advanced.test.ts +852 -0
  202. package/src/lib/server/session-tools/memory.test.ts +93 -0
  203. package/src/lib/server/session-tools/memory.ts +546 -79
  204. package/src/lib/server/session-tools/normalize-tool-args.ts +1 -1
  205. package/src/lib/server/session-tools/plugin-creator.ts +57 -1
  206. package/src/lib/server/session-tools/primitive-tools.test.ts +6 -0
  207. package/src/lib/server/session-tools/schedule.ts +6 -1
  208. package/src/lib/server/session-tools/shell-normalize.test.ts +25 -1
  209. package/src/lib/server/session-tools/shell.ts +22 -3
  210. package/src/lib/server/session-tools/wallet-tool.test.ts +254 -0
  211. package/src/lib/server/session-tools/wallet.ts +1374 -139
  212. package/src/lib/server/session-tools/web-inputs.test.ts +162 -1
  213. package/src/lib/server/session-tools/web.ts +468 -64
  214. package/src/lib/server/skill-discovery.ts +128 -0
  215. package/src/lib/server/skill-eligibility.test.ts +84 -0
  216. package/src/lib/server/skill-eligibility.ts +95 -0
  217. package/src/lib/server/skill-prompt-budget.test.ts +102 -0
  218. package/src/lib/server/skill-prompt-budget.ts +125 -0
  219. package/src/lib/server/skills-normalize.test.ts +54 -0
  220. package/src/lib/server/skills-normalize.ts +372 -26
  221. package/src/lib/server/solana.ts +214 -29
  222. package/src/lib/server/storage.ts +65 -36
  223. package/src/lib/server/stream-agent-chat.test.ts +419 -9
  224. package/src/lib/server/stream-agent-chat.ts +887 -83
  225. package/src/lib/server/system-events.ts +1 -1
  226. package/src/lib/server/tool-capability-policy-advanced.test.ts +502 -0
  227. package/src/lib/server/tool-loop-detection.test.ts +105 -0
  228. package/src/lib/server/tool-loop-detection.ts +260 -0
  229. package/src/lib/server/tool-planning.ts +4 -2
  230. package/src/lib/server/wallet-execution.test.ts +198 -0
  231. package/src/lib/server/wallet-portfolio.test.ts +98 -0
  232. package/src/lib/server/wallet-portfolio.ts +724 -0
  233. package/src/lib/server/wallet-service.test.ts +57 -0
  234. package/src/lib/server/wallet-service.ts +213 -0
  235. package/src/lib/server/watch-jobs-advanced.test.ts +594 -0
  236. package/src/lib/server/watch-jobs.ts +17 -2
  237. package/src/lib/server/workspace-context.ts +111 -0
  238. package/src/lib/skill-save-payload.test.ts +39 -0
  239. package/src/lib/skill-save-payload.ts +37 -0
  240. package/src/lib/tasks.ts +28 -0
  241. package/src/lib/tool-event-summary.test.ts +30 -0
  242. package/src/lib/tool-event-summary.ts +37 -0
  243. package/src/lib/validation/schemas.ts +1 -0
  244. package/src/lib/wallet-transactions.test.ts +75 -0
  245. package/src/lib/wallet-transactions.ts +43 -0
  246. package/src/lib/wallet.test.ts +17 -0
  247. package/src/lib/wallet.ts +183 -0
  248. package/src/proxy.test.ts +31 -0
  249. package/src/proxy.ts +34 -2
  250. package/src/stores/use-chat-store.ts +15 -1
  251. package/src/types/index.ts +210 -14
@@ -1,12 +1,32 @@
1
1
  import { z } from 'zod'
2
2
  import { tool, type StructuredToolInterface } from '@langchain/core/tools'
3
3
  import type { ToolBuildContext } from './context'
4
- import { getPluginManager } from '../plugins'
4
+ import { getPluginManager, normalizeMarketplacePluginUrl } from '../plugins'
5
5
  import type { Plugin, PluginHooks, ClawHubSkill } from '@/types'
6
6
  import { searchClawHub } from '../clawhub-client'
7
7
  import { normalizeToolInputArgs } from './normalize-tool-args'
8
8
  import { pluginIdMatches } from '../tool-aliases'
9
9
  import { loadSessions } from '../storage'
10
+ import { inferPluginPublisherSourceFromUrl } from '@/lib/plugin-sources'
11
+
12
+ function trimString(value: unknown): string {
13
+ return typeof value === 'string' ? value.trim() : ''
14
+ }
15
+
16
+ function buildDiscoveryApprovalResumeInput(approval: import('@/types').ApprovalRequest): Record<string, unknown> | null {
17
+ if (approval.category !== 'plugin_install') return null
18
+ const url = trimString(approval.data.url)
19
+ if (!url) return null
20
+ const pluginId = trimString(approval.data.pluginId)
21
+ const reason = trimString(approval.data.reason)
22
+ return {
23
+ action: 'install_request',
24
+ url,
25
+ pluginId: pluginId || undefined,
26
+ reason: reason || `Approved install request for ${url}`,
27
+ approved: true,
28
+ }
29
+ }
10
30
 
11
31
  /**
12
32
  * Unified Discovery Logic
@@ -41,11 +61,15 @@ async function executeDiscoveryAction(args: Record<string, unknown>, bctx?: Tool
41
61
  case 'list':
42
62
  case 'discover': {
43
63
  const plugins = manager.listPlugins()
64
+ const currentSession = bctx?.ctx?.sessionId ? loadSessions()[bctx.ctx.sessionId] : null
65
+ const sessionPlugins = currentSession?.plugins || currentSession?.tools || []
44
66
  return JSON.stringify(plugins.map(p => ({
45
67
  id: p.filename,
46
68
  name: p.name,
47
69
  description: p.description,
48
70
  enabled: p.enabled,
71
+ granted: pluginIdMatches(sessionPlugins, p.filename),
72
+ availableNow: pluginIdMatches(sessionPlugins, p.filename) && !manager.isExplicitlyDisabled(p.filename),
49
73
  isBuiltin: !p.filename.endsWith('.js') && !p.filename.endsWith('.mjs')
50
74
  })), null, 2)
51
75
  }
@@ -62,6 +86,7 @@ async function executeDiscoveryAction(args: Record<string, unknown>, bctx?: Tool
62
86
  description: s.description,
63
87
  author: s.author,
64
88
  source: 'clawhub',
89
+ catalogSource: 'clawhub',
65
90
  url: (s as ClawHubSkill & { rawUrl?: string }).rawUrl ?? s.url
66
91
  })))
67
92
  }
@@ -71,14 +96,32 @@ async function executeDiscoveryAction(args: Record<string, unknown>, bctx?: Tool
71
96
 
72
97
  try {
73
98
  console.log('[discovery] Searching SwarmClaw registry...')
74
- const scRes = await fetch('https://swarmclaw.ai/registry/plugins.json', { signal: AbortSignal.timeout(5000) })
75
- if (scRes.ok) {
99
+ const registryResults = new Map<string, Record<string, unknown>>()
100
+ const registries = [
101
+ { url: 'https://swarmclaw.ai/registry/plugins.json', catalogSource: 'swarmclaw-site' },
102
+ { url: 'https://raw.githubusercontent.com/swarmclawai/swarmforge/main/registry.json', catalogSource: 'swarmforge' },
103
+ ] as const
104
+ for (const registry of registries) {
105
+ const scRes = await fetch(registry.url, { signal: AbortSignal.timeout(5000) })
106
+ if (!scRes.ok) continue
76
107
  const scPlugins = await scRes.json()
77
108
  const filtered = (scPlugins as Record<string, unknown>[]).filter((p: Record<string, unknown>) =>
78
109
  !q || (String(p.name || '')).toLowerCase().includes(q.toLowerCase()) || (String(p.description || '')).toLowerCase().includes(q.toLowerCase())
79
110
  )
80
- results.push(...filtered.map(p => ({ ...p, source: 'swarmclaw' })))
111
+ for (const p of filtered) {
112
+ const id = String(p.id || p.name || '').trim().toLowerCase().replace(/[^a-z0-9]/g, '_')
113
+ if (!id || registryResults.has(id)) continue
114
+ const url = normalizeMarketplacePluginUrl(String(p.url || ''))
115
+ registryResults.set(id, {
116
+ ...p,
117
+ id,
118
+ url,
119
+ source: inferPluginPublisherSourceFromUrl(url) || 'swarmforge',
120
+ catalogSource: registry.catalogSource,
121
+ })
122
+ }
81
123
  }
124
+ results.push(...registryResults.values())
82
125
  } catch (err: unknown) {
83
126
  console.error('[discovery] SC Registry search failed:', err instanceof Error ? err.message : String(err))
84
127
  }
@@ -99,12 +142,12 @@ async function executeDiscoveryAction(args: Record<string, unknown>, bctx?: Tool
99
142
  const currentSession = allSessions[bctx.ctx.sessionId]
100
143
  const grantedTools = currentSession?.plugins || currentSession?.tools || []
101
144
  if (currentSession && pluginIdMatches(grantedTools, pluginId)) {
102
- return JSON.stringify({
103
- alreadyGranted: true,
104
- pluginId,
105
- message: `You already have access to "${pluginId}" proceed to use it directly.`,
106
- })
107
- }
145
+ return JSON.stringify({
146
+ alreadyGranted: true,
147
+ pluginId,
148
+ message: `You already have access to "${pluginId}". If it was just granted, it will be available on the next agent turn.`,
149
+ })
150
+ }
108
151
  }
109
152
  const { requestApprovalMaybeAutoApprove } = await import('../approvals')
110
153
  const approval = await requestApprovalMaybeAutoApprove({
@@ -121,7 +164,7 @@ async function executeDiscoveryAction(args: Record<string, unknown>, bctx?: Tool
121
164
  pluginId,
122
165
  toolId: pluginId,
123
166
  autoApproved: true,
124
- message: `Access to "${pluginId}" was auto-approved and granted. Proceed to use it directly.`,
167
+ message: `Access to "${pluginId}" was auto-approved and granted. It will be available on the next agent turn.`,
125
168
  })
126
169
  }
127
170
  return JSON.stringify({
@@ -182,7 +225,32 @@ async function executeDiscoveryAction(args: Record<string, unknown>, bctx?: Tool
182
225
  const DiscoveryPlugin: Plugin = {
183
226
  name: 'Core Discovery',
184
227
  description: 'Discover available plugins, search marketplaces, request access, or suggest new installs.',
185
- hooks: {} as PluginHooks,
228
+ hooks: {
229
+ getApprovalGuidance: ({ approval, phase, approved }) => {
230
+ if (approval.category !== 'plugin_install') return null
231
+ if (phase === 'request') {
232
+ return [
233
+ 'When this approval is granted, continue with `manage_capabilities` for the exact approved install request instead of asking again in prose.',
234
+ 'Do not change the approved plugin URL or pluginId unless newer tool evidence proves the approved source is invalid.',
235
+ ]
236
+ }
237
+ if (phase === 'connector_reminder') {
238
+ return 'Approving this lets the agent resume the approved plugin install request without repeating marketplace research.'
239
+ }
240
+ if (approved !== true) {
241
+ return 'Do not retry the rejected install request unless the plugin source or requested capability materially changes.'
242
+ }
243
+ const resumeInput = buildDiscoveryApprovalResumeInput(approval)
244
+ const lines = [
245
+ 'Resume immediately with `manage_capabilities` for the approved install request.',
246
+ 'Do not repeat the same marketplace search or install request once approval has been granted.',
247
+ ]
248
+ if (resumeInput) {
249
+ lines.push(`Exact tool input: ${JSON.stringify(resumeInput)}`)
250
+ }
251
+ return lines
252
+ },
253
+ } as PluginHooks,
186
254
  tools: [
187
255
  {
188
256
  name: 'manage_capabilities',
@@ -83,6 +83,24 @@ describe('normalizeFileArgs', () => {
83
83
  assert.deepEqual(out.files, [{ name: 'report.md', content: '# report' }])
84
84
  })
85
85
 
86
+ it('parses stringified bulk file arrays from wrapped payloads', () => {
87
+ const out = normalizeFileArgs({
88
+ input: JSON.stringify({
89
+ action: 'write',
90
+ files: JSON.stringify([
91
+ { path: 'offer-pack/offer-brief.md', content: '# brief' },
92
+ { path: 'offer-pack/landing-copy.md', content: '# landing' },
93
+ ]),
94
+ }),
95
+ })
96
+
97
+ assert.equal(out.action, 'write')
98
+ assert.deepEqual(out.files, [
99
+ { path: 'offer-pack/offer-brief.md', content: '# brief' },
100
+ { path: 'offer-pack/landing-copy.md', content: '# landing' },
101
+ ])
102
+ })
103
+
86
104
  it('treats trailing-slash write targets as directory creation', async () => {
87
105
  const cwd = fs.mkdtempSync(path.join(os.tmpdir(), 'file-write-dir-'))
88
106
  const out = await executeFileAction({
@@ -95,4 +113,22 @@ describe('normalizeFileArgs', () => {
95
113
  assert.equal(fs.statSync(path.join(cwd, 'weather_update')).isDirectory(), true)
96
114
  fs.rmSync(cwd, { recursive: true, force: true })
97
115
  })
116
+
117
+ it('does not inline binary screenshot data when reading image files', async () => {
118
+ const cwd = fs.mkdtempSync(path.join(os.tmpdir(), 'file-read-binary-'))
119
+ const imagePath = path.join(cwd, 'screenshot-main.png')
120
+ fs.writeFileSync(imagePath, Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x00, 0x01, 0x02, 0x03]))
121
+
122
+ try {
123
+ const out = await executeFileAction({
124
+ action: 'read',
125
+ filePath: 'screenshot-main.png',
126
+ }, { cwd })
127
+
128
+ assert.match(out, /Binary file: screenshot-main\.png/)
129
+ assert.match(out, /Use send_file/)
130
+ } finally {
131
+ fs.rmSync(cwd, { recursive: true, force: true })
132
+ }
133
+ })
98
134
  })
@@ -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
+ })