@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
@@ -21,7 +21,7 @@ import {
21
21
  decryptKey,
22
22
  } from '../storage'
23
23
  import { resolveScheduleName } from '@/lib/schedule-name'
24
- import { findDuplicateSchedule, type ScheduleLike } from '@/lib/schedule-dedupe'
24
+ import { findDuplicateSchedule, findEquivalentSchedules, type ScheduleLike } from '@/lib/schedule-dedupe'
25
25
  import { computeTaskFingerprint, findDuplicateTask } from '@/lib/task-dedupe'
26
26
  import {
27
27
  hasManagedAgentAssignmentInput,
@@ -118,6 +118,95 @@ function deriveTaskTitle(input: { title?: unknown; description?: unknown }): str
118
118
  return compact.slice(0, 120)
119
119
  }
120
120
 
121
+ const VALID_CONNECTOR_PLATFORMS = new Set([
122
+ 'discord',
123
+ 'telegram',
124
+ 'slack',
125
+ 'whatsapp',
126
+ 'openclaw',
127
+ 'bluebubbles',
128
+ 'signal',
129
+ 'teams',
130
+ 'googlechat',
131
+ 'matrix',
132
+ 'email',
133
+ 'webchat',
134
+ 'mockmail',
135
+ ])
136
+
137
+ const VALID_CONNECTOR_STATUSES = new Set(['stopped', 'running', 'error'])
138
+
139
+ function normalizeConnectorConfig(raw: unknown): Record<string, string> {
140
+ if (!raw || typeof raw !== 'object' || Array.isArray(raw)) return {}
141
+ const normalized: Record<string, string> = {}
142
+ for (const [key, value] of Object.entries(raw as Record<string, unknown>)) {
143
+ const normalizedKey = typeof key === 'string' ? key.trim() : ''
144
+ if (!normalizedKey) continue
145
+ if (typeof value === 'string') {
146
+ normalized[normalizedKey] = value
147
+ continue
148
+ }
149
+ if (typeof value === 'number' || typeof value === 'boolean') {
150
+ normalized[normalizedKey] = String(value)
151
+ }
152
+ }
153
+ return normalized
154
+ }
155
+
156
+ function sanitizeConnectorCrudPayload(
157
+ raw: Record<string, unknown>,
158
+ options: { forUpdate?: boolean } = {},
159
+ ): Record<string, unknown> {
160
+ const { forUpdate = false } = options
161
+ const out: Record<string, unknown> = {}
162
+ const setString = (key: 'name' | 'platform' | 'status') => {
163
+ if (!Object.prototype.hasOwnProperty.call(raw, key)) return
164
+ const value = typeof raw[key] === 'string' ? raw[key].trim() : ''
165
+ if (!value) return
166
+ if (key === 'platform' && !VALID_CONNECTOR_PLATFORMS.has(value)) return
167
+ if (key === 'status' && !VALID_CONNECTOR_STATUSES.has(value)) return
168
+ out[key] = value
169
+ }
170
+ const setNullableId = (key: 'agentId' | 'chatroomId' | 'credentialId') => {
171
+ if (!Object.prototype.hasOwnProperty.call(raw, key)) return
172
+ const value = typeof raw[key] === 'string' ? raw[key].trim() : ''
173
+ out[key] = value || null
174
+ }
175
+
176
+ setString('name')
177
+ setString('platform')
178
+ setString('status')
179
+ setNullableId('agentId')
180
+ setNullableId('chatroomId')
181
+ setNullableId('credentialId')
182
+
183
+ if (Object.prototype.hasOwnProperty.call(raw, 'config')) {
184
+ out.config = normalizeConnectorConfig(raw.config)
185
+ }
186
+
187
+ if (Object.prototype.hasOwnProperty.call(raw, 'isEnabled')) {
188
+ out.isEnabled = raw.isEnabled === true
189
+ } else if (Object.prototype.hasOwnProperty.call(raw, 'enabled')) {
190
+ out.isEnabled = raw.enabled === true
191
+ }
192
+
193
+ if (!forUpdate) {
194
+ const platform = typeof out.platform === 'string' ? out.platform : 'discord'
195
+ return {
196
+ name: typeof out.name === 'string' && out.name ? out.name : 'Unnamed Connector',
197
+ platform,
198
+ agentId: Object.prototype.hasOwnProperty.call(out, 'agentId') ? out.agentId : null,
199
+ chatroomId: Object.prototype.hasOwnProperty.call(out, 'chatroomId') ? out.chatroomId : null,
200
+ credentialId: Object.prototype.hasOwnProperty.call(out, 'credentialId') ? out.credentialId : null,
201
+ config: Object.prototype.hasOwnProperty.call(out, 'config') ? out.config : {},
202
+ isEnabled: Object.prototype.hasOwnProperty.call(out, 'isEnabled') ? out.isEnabled : false,
203
+ ...(typeof out.status === 'string' ? { status: out.status } : {}),
204
+ }
205
+ }
206
+
207
+ return out
208
+ }
209
+
121
210
  const TASK_STATUS_VALUES = new Set([
122
211
  'backlog',
123
212
  'queued',
@@ -157,6 +246,95 @@ function pickFirstTaskId(value: unknown): string | null {
157
246
  return ids[0] || null
158
247
  }
159
248
 
249
+ function buildScheduleCreatorScope(schedule: Record<string, unknown> | null | undefined): {
250
+ agentId?: string | null
251
+ sessionId?: string | null
252
+ } | null {
253
+ if (!schedule || typeof schedule !== 'object') return null
254
+ const agentId = typeof schedule.createdByAgentId === 'string' && schedule.createdByAgentId.trim()
255
+ ? schedule.createdByAgentId.trim()
256
+ : null
257
+ const sessionId = typeof schedule.createdInSessionId === 'string' && schedule.createdInSessionId.trim()
258
+ ? schedule.createdInSessionId.trim()
259
+ : null
260
+ if (!agentId && !sessionId) return null
261
+ return { agentId, sessionId }
262
+ }
263
+
264
+ function deriveScheduleFollowupTarget(sessionId: string | null | undefined): {
265
+ followupConnectorId?: string | null
266
+ followupChannelId?: string | null
267
+ followupThreadId?: string | null
268
+ followupSenderId?: string | null
269
+ followupSenderName?: string | null
270
+ } {
271
+ const normalizedSessionId = typeof sessionId === 'string' ? sessionId.trim() : ''
272
+ if (!normalizedSessionId) return {}
273
+
274
+ const session = loadSessions()[normalizedSessionId] as {
275
+ connectorContext?: Record<string, unknown>
276
+ messages?: Array<Record<string, unknown>>
277
+ } | undefined
278
+ if (!session) return {}
279
+
280
+ const pickSourceFields = (source: Record<string, unknown> | null | undefined) => {
281
+ const connectorId = typeof source?.connectorId === 'string' ? source.connectorId.trim() : ''
282
+ const channelId = typeof source?.channelId === 'string' ? source.channelId.trim() : ''
283
+ if (!connectorId || !channelId) return {}
284
+ const threadId = typeof source?.threadId === 'string' ? source.threadId.trim() : ''
285
+ const senderId = typeof source?.senderId === 'string' ? source.senderId.trim() : ''
286
+ const senderName = typeof source?.senderName === 'string' ? source.senderName.trim() : ''
287
+ return {
288
+ followupConnectorId: connectorId,
289
+ followupChannelId: channelId,
290
+ followupThreadId: threadId || null,
291
+ followupSenderId: senderId || null,
292
+ followupSenderName: senderName || null,
293
+ }
294
+ }
295
+
296
+ const contextTarget = pickSourceFields(session.connectorContext || undefined)
297
+ if (contextTarget.followupConnectorId && contextTarget.followupChannelId) return contextTarget
298
+
299
+ const messages = Array.isArray(session.messages) ? session.messages : []
300
+ for (let i = messages.length - 1; i >= 0; i -= 1) {
301
+ const message = messages[i]
302
+ if ((typeof message?.role === 'string' ? message.role : '') !== 'user') continue
303
+ if (message?.historyExcluded === true) continue
304
+ const messageTarget = pickSourceFields(message?.source as Record<string, unknown> | undefined)
305
+ if (messageTarget.followupConnectorId && messageTarget.followupChannelId) return messageTarget
306
+ }
307
+
308
+ return {}
309
+ }
310
+
311
+ function findRelatedScheduleIds(
312
+ schedules: Record<string, ScheduleLike>,
313
+ schedule: Record<string, unknown> | null | undefined,
314
+ opts: { ignoreId?: string | null } = {},
315
+ ): string[] {
316
+ if (!schedule || typeof schedule !== 'object') return []
317
+ const scope = buildScheduleCreatorScope(schedule)
318
+ if (!scope?.sessionId) return []
319
+ const matches = findEquivalentSchedules(schedules, {
320
+ id: typeof schedule.id === 'string' ? schedule.id : null,
321
+ agentId: typeof schedule.agentId === 'string' ? schedule.agentId : null,
322
+ taskPrompt: typeof schedule.taskPrompt === 'string' ? schedule.taskPrompt : null,
323
+ scheduleType: typeof schedule.scheduleType === 'string' ? schedule.scheduleType : null,
324
+ cron: typeof schedule.cron === 'string' ? schedule.cron : null,
325
+ intervalMs: typeof schedule.intervalMs === 'number' ? schedule.intervalMs : null,
326
+ runAt: typeof schedule.runAt === 'number' ? schedule.runAt : null,
327
+ createdByAgentId: scope.agentId,
328
+ createdInSessionId: scope.sessionId,
329
+ }, {
330
+ ignoreId: opts.ignoreId || (typeof schedule.id === 'string' ? schedule.id : null),
331
+ creatorScope: scope,
332
+ })
333
+ return Array.from(new Set(matches
334
+ .map((entry) => (typeof entry.id === 'string' ? entry.id : ''))
335
+ .filter(Boolean)))
336
+ }
337
+
160
338
  function applyTaskContinuationDefaults(
161
339
  parsed: Record<string, unknown>,
162
340
  tasks: Record<string, BoardTask>,
@@ -285,11 +463,7 @@ const RESOURCE_DEFAULTS: Record<string, (parsed: any) => any> = {
285
463
  ...p,
286
464
  }),
287
465
  manage_connectors: (p) => ({
288
- name: p.name || 'Unnamed Connector',
289
- platform: p.platform || 'discord',
290
- agentId: p.agentId || null,
291
- enabled: p.enabled ?? false,
292
- ...p,
466
+ ...sanitizeConnectorCrudPayload(p as Record<string, unknown>),
293
467
  }),
294
468
  manage_webhooks: (p) => ({
295
469
  name: p.name || 'Unnamed Webhook',
@@ -391,9 +565,9 @@ export function buildCrudTools(bctx: ToolBuildContext): StructuredToolInterface[
391
565
  description += `\n\nAgents may self-edit their own soul. To update your soul, use action="update", id="${ctx?.agentId || 'your-agent-id'}", and include data with the "soul" field. Set "platformAssignScope":"all" to let an agent delegate work across the fleet; use "self" for solo execution.`
392
566
  } else if (toolKey === 'manage_schedules') {
393
567
  if (assignScope === 'self') {
394
- description += `\n\nOmit "agentId" to assign a schedule to yourself ("${ctx?.agentId || 'unknown'}"), or set it explicitly to yourself. You can only assign schedules to yourself. Schedule types: interval (set intervalMs), cron (set cron), once (set runAt). Provide either taskPrompt, command, or action+path. Before create, call list/get to avoid duplicate schedules. If an equivalent active/paused schedule already exists, create returns that existing schedule (deduplicated=true).`
568
+ description += `\n\nOmit "agentId" to assign a schedule to yourself ("${ctx?.agentId || 'unknown'}"), or set it explicitly to yourself. You can only assign schedules to yourself. Schedule types: interval (set intervalMs), cron (set cron), once (set runAt). Provide either taskPrompt, command, or action+path. Before create, call list/get to avoid duplicate schedules. Reuse or update an existing schedule you already created in this chat instead of making a near-duplicate. If an equivalent active/paused schedule already exists, create returns that existing schedule (deduplicated=true). For one-off reminders, prefer "once"; agent-created one-off schedules are cleaned up automatically after they finish. When the user says stop/pause/cancel a reminder, pause or delete every matching schedule you created in this chat, not just one row.`
395
569
  } else {
396
- description += `\n\nOmit "agentId" to assign a schedule to yourself ("${ctx?.agentId || 'unknown'}"), or set "agentId" to another agent when needed. Schedule types: interval (set intervalMs), cron (set cron), once (set runAt). Provide either taskPrompt, command, or action+path. Before create, call list/get to avoid duplicate schedules. If an equivalent active/paused schedule already exists, create returns that existing schedule (deduplicated=true).` + agentSummary
570
+ description += `\n\nOmit "agentId" to assign a schedule to yourself ("${ctx?.agentId || 'unknown'}"), or set "agentId" to another agent when needed. Schedule types: interval (set intervalMs), cron (set cron), once (set runAt). Provide either taskPrompt, command, or action+path. Before create, call list/get to avoid duplicate schedules. Reuse or update an existing schedule you already created in this chat instead of making a near-duplicate. If an equivalent active/paused schedule already exists, create returns that existing schedule (deduplicated=true). For one-off reminders, prefer "once"; agent-created one-off schedules are cleaned up automatically after they finish. When the user says stop/pause/cancel a reminder, pause or delete every matching schedule you created in this chat, not just one row.` + agentSummary
397
571
  }
398
572
  if (ctx?.projectId) {
399
573
  description += `\n\nCurrent project context: "${ctx.projectName || ctx.projectId}" (projectId "${ctx.projectId}"). Omit "projectId" to use this active project by default.`
@@ -593,11 +767,15 @@ export function buildCrudTools(bctx: ToolBuildContext): StructuredToolInterface[
593
767
  }
594
768
  }
595
769
  const newId = genId()
770
+ const scheduleFollowupTarget = toolKey === 'manage_schedules'
771
+ ? deriveScheduleFollowupTarget(ctx?.sessionId || null)
772
+ : {}
596
773
  const entry = {
597
774
  id: newId,
598
775
  ...parsed,
599
776
  createdByAgentId: ctx?.agentId || null,
600
777
  createdInSessionId: ctx?.sessionId || null,
778
+ ...scheduleFollowupTarget,
601
779
  createdAt: now,
602
780
  updatedAt: now,
603
781
  }
@@ -665,8 +843,12 @@ export function buildCrudTools(bctx: ToolBuildContext): StructuredToolInterface[
665
843
  if (!effectiveId) return 'Error: "id" is required for update action.'
666
844
  const all = res.load()
667
845
  if (!all[effectiveId]) return `Not found: ${res.label} "${effectiveId}"`
846
+ const previousEntry = all[effectiveId]
847
+ let affectedScheduleIds: string[] | null = null
668
848
  const parsed = toolKey === 'manage_projects'
669
849
  ? normalizeProjectPatchInput(buildCrudPayload(normalized, action, data))
850
+ : toolKey === 'manage_connectors'
851
+ ? sanitizeConnectorCrudPayload(buildCrudPayload(normalized, action, data), { forUpdate: true })
670
852
  : buildCrudPayload(normalized, action, data)
671
853
  const parsedRecord = parsed as Record<string, unknown>
672
854
  if (toolKey === 'manage_tasks') {
@@ -734,6 +916,21 @@ export function buildCrudTools(bctx: ToolBuildContext): StructuredToolInterface[
734
916
  ...normalizedSchedule.value,
735
917
  updatedAt: Date.now(),
736
918
  }
919
+ const nextStatus = typeof all[effectiveId].status === 'string' ? all[effectiveId].status.trim().toLowerCase() : ''
920
+ if (nextStatus === 'paused' || nextStatus === 'completed' || nextStatus === 'failed') {
921
+ const relatedIds = findRelatedScheduleIds(all as Record<string, ScheduleLike>, previousEntry, {
922
+ ignoreId: effectiveId,
923
+ })
924
+ for (const relatedId of relatedIds) {
925
+ if (!all[relatedId]) continue
926
+ all[relatedId] = {
927
+ ...all[relatedId],
928
+ status: nextStatus,
929
+ updatedAt: Date.now(),
930
+ }
931
+ }
932
+ affectedScheduleIds = [effectiveId, ...relatedIds]
933
+ }
737
934
  }
738
935
  if (toolKey === 'manage_secrets') {
739
936
  if (!canAccessSecret(all[effectiveId])) return 'Error: you do not have access to this secret.'
@@ -807,6 +1004,12 @@ export function buildCrudTools(bctx: ToolBuildContext): StructuredToolInterface[
807
1004
  if (toolKey === 'manage_projects') {
808
1005
  return JSON.stringify(buildProjectSnapshot(all[effectiveId]))
809
1006
  }
1007
+ if (toolKey === 'manage_schedules' && affectedScheduleIds?.length) {
1008
+ return JSON.stringify({
1009
+ ...all[effectiveId],
1010
+ affectedScheduleIds,
1011
+ })
1012
+ }
810
1013
  return JSON.stringify(all[effectiveId])
811
1014
  }
812
1015
  if (action === 'delete') {
@@ -817,7 +1020,12 @@ export function buildCrudTools(bctx: ToolBuildContext): StructuredToolInterface[
817
1020
  if (toolKey === 'manage_secrets' && !canAccessSecret(all[effectiveId])) {
818
1021
  return 'Error: you do not have access to this secret.'
819
1022
  }
820
- delete all[effectiveId]
1023
+ const deletedIds = toolKey === 'manage_schedules'
1024
+ ? [effectiveId, ...findRelatedScheduleIds(all as Record<string, ScheduleLike>, all[effectiveId], { ignoreId: effectiveId })]
1025
+ : [effectiveId]
1026
+ for (const deleteId of deletedIds) {
1027
+ delete all[deleteId]
1028
+ }
821
1029
  res.save(all)
822
1030
  if (toolKey === 'manage_projects') {
823
1031
  const clearProjectId = (load: () => Record<string, Record<string, unknown>>, save: (d: Record<string, Record<string, unknown>>) => void) => {
@@ -837,7 +1045,10 @@ export function buildCrudTools(bctx: ToolBuildContext): StructuredToolInterface[
837
1045
  clearProjectId(loadSkills, saveSkills)
838
1046
  clearProjectId(loadSecrets, saveSecrets)
839
1047
  }
840
- return JSON.stringify({ deleted: effectiveId })
1048
+ return JSON.stringify({
1049
+ deleted: effectiveId,
1050
+ deletedIds,
1051
+ })
841
1052
  }
842
1053
  return `Unknown action "${action}". Valid: list, get, create, update, delete`
843
1054
  } catch (err: any) {
@@ -206,6 +206,109 @@ describe('delegate fallback', () => {
206
206
  assert.match(String(output.response || ''), /codex fallback ok/i)
207
207
  })
208
208
 
209
+ it('uses nested data.task payloads from recent tool-call wrappers', () => {
210
+ const output = runWithFakeDelegates(`
211
+ const mod = await import('./src/lib/server/session-tools/delegate.ts')
212
+ const { buildDelegateTools } = mod.default || mod['module.exports'] || mod
213
+
214
+ const tools = buildDelegateTools({
215
+ cwd: process.cwd(),
216
+ ctx: { sessionId: 'session-test', agentId: 'agent-test', platformAssignScope: 'self' },
217
+ hasPlugin: (name) => name === 'delegate',
218
+ hasTool: (name) => name === 'delegate',
219
+ cleanupFns: [],
220
+ commandTimeoutMs: 5000,
221
+ claudeTimeoutMs: 5000,
222
+ cliProcessTimeoutMs: 5000,
223
+ persistDelegateResumeId: () => {},
224
+ readStoredDelegateResumeId: () => null,
225
+ resolveCurrentSession: () => null,
226
+ activePlugins: ['delegate'],
227
+ })
228
+
229
+ const delegateTool = tools.find((tool) => tool.name === 'delegate')
230
+ const raw = await delegateTool.invoke({
231
+ input: JSON.stringify({
232
+ data: {
233
+ task: 'Create a simple to-do list application.',
234
+ },
235
+ }),
236
+ })
237
+ console.log(raw)
238
+ `)
239
+
240
+ assert.equal(output.backend, 'codex')
241
+ assert.equal(output.status, 'completed')
242
+ assert.match(String(output.response || ''), /codex fallback ok/i)
243
+ })
244
+
245
+ it('falls back to reason text when malformed delegate wrappers omit task', () => {
246
+ const output = runWithFakeDelegates(`
247
+ const mod = await import('./src/lib/server/session-tools/delegate.ts')
248
+ const { buildDelegateTools } = mod.default || mod['module.exports'] || mod
249
+
250
+ const tools = buildDelegateTools({
251
+ cwd: process.cwd(),
252
+ ctx: { sessionId: 'session-test', agentId: 'agent-test', platformAssignScope: 'self' },
253
+ hasPlugin: (name) => name === 'delegate',
254
+ hasTool: (name) => name === 'delegate',
255
+ cleanupFns: [],
256
+ commandTimeoutMs: 5000,
257
+ claudeTimeoutMs: 5000,
258
+ cliProcessTimeoutMs: 5000,
259
+ persistDelegateResumeId: () => {},
260
+ readStoredDelegateResumeId: () => null,
261
+ resolveCurrentSession: () => null,
262
+ activePlugins: ['delegate'],
263
+ })
264
+
265
+ const delegateTool = tools.find((tool) => tool.name === 'delegate')
266
+ const raw = await delegateTool.invoke({
267
+ input: JSON.stringify({
268
+ parameters: {
269
+ tool_id: 'delegate',
270
+ reason: 'Building a simple front-end to-do list app is well-suited for a delegated agent.',
271
+ subagent_tool_id: 'agent_coder',
272
+ subagent_name: 'Coder',
273
+ },
274
+ }),
275
+ })
276
+ console.log(raw)
277
+ `)
278
+
279
+ assert.equal(output.backend, 'codex')
280
+ assert.equal(output.status, 'completed')
281
+ assert.match(String(output.response || ''), /codex fallback ok/i)
282
+ })
283
+
284
+ it('accepts legacy id fields for lifecycle delegate actions', () => {
285
+ const output = runWithFakeDelegates(`
286
+ const mod = await import('./src/lib/server/session-tools/delegate.ts')
287
+ const { buildDelegateTools } = mod.default || mod['module.exports'] || mod
288
+
289
+ const tools = buildDelegateTools({
290
+ cwd: process.cwd(),
291
+ ctx: { sessionId: 'session-test', agentId: 'agent-test', platformAssignScope: 'self' },
292
+ hasPlugin: (name) => name === 'delegate',
293
+ hasTool: (name) => name === 'delegate',
294
+ cleanupFns: [],
295
+ commandTimeoutMs: 5000,
296
+ claudeTimeoutMs: 5000,
297
+ cliProcessTimeoutMs: 5000,
298
+ persistDelegateResumeId: () => {},
299
+ readStoredDelegateResumeId: () => null,
300
+ resolveCurrentSession: () => null,
301
+ activePlugins: ['delegate'],
302
+ })
303
+
304
+ const delegateTool = tools.find((tool) => tool.name === 'delegate')
305
+ const raw = await delegateTool.invoke({ action: 'status', id: 'job-123' })
306
+ console.log(JSON.stringify({ raw }))
307
+ `)
308
+
309
+ assert.match(String(output.raw || ''), /delegation job "job-123" not found/i)
310
+ })
311
+
209
312
  it('ranks authenticated delegate backends ahead of unauthenticated ones', () => {
210
313
  const output = runWithFakeDelegates(`
211
314
  const mod = await import('./src/lib/server/provider-health.ts')
@@ -147,6 +147,41 @@ function coerceDelegateBackend(value: unknown): DelegateBackend | null {
147
147
  return null
148
148
  }
149
149
 
150
+ function asDelegateRecord(value: unknown): Record<string, unknown> | null {
151
+ return value && typeof value === 'object' && !Array.isArray(value)
152
+ ? value as Record<string, unknown>
153
+ : null
154
+ }
155
+
156
+ function pickNonEmptyDelegateString(...values: unknown[]): string | null {
157
+ for (const value of values) {
158
+ if (typeof value !== 'string') continue
159
+ const trimmed = value.trim()
160
+ if (!trimmed) continue
161
+ if ((trimmed.startsWith('{') && trimmed.endsWith('}')) || (trimmed.startsWith('[') && trimmed.endsWith(']'))) {
162
+ continue
163
+ }
164
+ return trimmed
165
+ }
166
+ return null
167
+ }
168
+
169
+ function pickDelegateTaskText(record: Record<string, unknown> | null): string | null {
170
+ if (!record) return null
171
+ return pickNonEmptyDelegateString(
172
+ record.task,
173
+ record.prompt,
174
+ record.request,
175
+ record.instructions,
176
+ record.instruction,
177
+ record.description,
178
+ record.input,
179
+ record.reason,
180
+ record.goal,
181
+ record.objective,
182
+ )
183
+ }
184
+
150
185
  function buildDelegateTaskFromPayload(normalized: Record<string, unknown>): string | null {
151
186
  const action = String(normalized.action || '').trim().toLowerCase()
152
187
  const target = [
@@ -200,20 +235,41 @@ function buildDelegateTaskFromPayload(normalized: Record<string, unknown>): stri
200
235
 
201
236
  function normalizeDelegateArgs(rawArgs: Record<string, unknown>): Record<string, unknown> {
202
237
  const normalized = normalizeToolInputArgs(rawArgs)
238
+ const nestedData = asDelegateRecord(normalized.data)
239
+ const delegatePayload = {
240
+ ...(nestedData || {}),
241
+ ...normalized,
242
+ }
203
243
  const backend = coerceDelegateBackend(
204
- normalized.backend
205
- ?? normalized.tool_name
206
- ?? normalized.toolName
207
- ?? normalized.delegate
208
- ?? normalized.provider,
244
+ delegatePayload.backend
245
+ ?? delegatePayload.tool_name
246
+ ?? delegatePayload.toolName
247
+ ?? delegatePayload.tool
248
+ ?? delegatePayload.delegate
249
+ ?? delegatePayload.provider
250
+ ?? delegatePayload.subagent_tool_id
251
+ ?? delegatePayload.subagent_name,
209
252
  )
210
253
  if (backend && !normalized.backend) normalized.backend = backend
211
- if (typeof normalized.task !== 'string' && typeof normalized.prompt === 'string') normalized.task = normalized.prompt
212
- const action = String(normalized.action || '').trim().toLowerCase()
254
+ if (typeof normalized.task !== 'string' || !normalized.task.trim()) {
255
+ const directTask = pickDelegateTaskText(delegatePayload) || pickDelegateTaskText(nestedData)
256
+ if (directTask) normalized.task = directTask
257
+ }
258
+ const lifecycleJobId = pickNonEmptyDelegateString(
259
+ normalized.jobId,
260
+ normalized.id,
261
+ nestedData?.jobId,
262
+ nestedData?.id,
263
+ )
264
+ if (lifecycleJobId && (!normalized.jobId || typeof normalized.jobId !== 'string')) {
265
+ normalized.jobId = lifecycleJobId
266
+ }
267
+ const action = String(normalized.action ?? nestedData?.action ?? '').trim().toLowerCase()
213
268
  const isLifecycleAction = ['status', 'list', 'wait', 'cancel'].includes(action)
269
+ if (action) normalized.action = action
214
270
  if (!isLifecycleAction) {
215
271
  if (typeof normalized.task !== 'string' || !normalized.task.trim()) {
216
- const synthesized = buildDelegateTaskFromPayload(normalized)
272
+ const synthesized = buildDelegateTaskFromPayload(delegatePayload)
217
273
  if (synthesized) normalized.task = synthesized
218
274
  }
219
275
  normalized.action = 'start'
@@ -167,4 +167,146 @@ describe('discovery approval flows', () => {
167
167
  assert.equal(output.toolNames.includes('manage_schedules'), true)
168
168
  assert.equal(output.toolNames.includes('manage_platform'), false)
169
169
  })
170
+
171
+ it('session-granted builtins disabled by default still appear in the next turn tool list', () => {
172
+ const output = runWithTempDataDir(`
173
+ const storageMod = await import('./src/lib/server/storage.ts')
174
+ const toolsMod = await import('./src/lib/server/session-tools/index.ts')
175
+ const storage = storageMod.default || storageMod
176
+ const toolsApi = toolsMod.default || toolsMod
177
+
178
+ const now = Date.now()
179
+ storage.saveSessions({
180
+ session_email: {
181
+ id: 'session_email',
182
+ name: 'Email Tool Test',
183
+ cwd: process.env.WORKSPACE_DIR,
184
+ user: 'tester',
185
+ provider: 'openai',
186
+ model: 'gpt-test',
187
+ claudeSessionId: null,
188
+ messages: [],
189
+ createdAt: now,
190
+ lastActiveAt: now,
191
+ sessionType: 'human',
192
+ agentId: 'default',
193
+ plugins: ['email'],
194
+ },
195
+ })
196
+
197
+ const built = await toolsApi.buildSessionTools(process.env.WORKSPACE_DIR, ['email'], {
198
+ sessionId: 'session_email',
199
+ agentId: 'default',
200
+ platformAssignScope: 'self',
201
+ })
202
+ console.log(JSON.stringify({
203
+ toolNames: built.tools.map((entry) => entry.name).sort(),
204
+ }))
205
+ `)
206
+
207
+ assert.equal(output.toolNames.includes('email'), true)
208
+ })
209
+
210
+ it('discover reports session-granted builtin tools as available now', () => {
211
+ const output = runWithTempDataDir(`
212
+ const storageMod = await import('./src/lib/server/storage.ts')
213
+ const toolsMod = await import('./src/lib/server/session-tools/index.ts')
214
+ const storage = storageMod.default || storageMod
215
+ const toolsApi = toolsMod.default || toolsMod
216
+
217
+ const now = Date.now()
218
+ storage.saveSessions({
219
+ session_discover_email: {
220
+ id: 'session_discover_email',
221
+ name: 'Discovery Email Test',
222
+ cwd: process.env.WORKSPACE_DIR,
223
+ user: 'tester',
224
+ provider: 'openai',
225
+ model: 'gpt-test',
226
+ claudeSessionId: null,
227
+ messages: [],
228
+ createdAt: now,
229
+ lastActiveAt: now,
230
+ sessionType: 'human',
231
+ agentId: 'default',
232
+ plugins: ['email'],
233
+ },
234
+ })
235
+
236
+ const built = await toolsApi.buildSessionTools(process.env.WORKSPACE_DIR, ['email'], {
237
+ sessionId: 'session_discover_email',
238
+ agentId: 'default',
239
+ platformAssignScope: 'self',
240
+ })
241
+ const tool = built.tools.find((entry) => entry.name === 'manage_capabilities')
242
+ const raw = await tool.invoke({ action: 'discover', reason: 'Check runtime tool availability.' })
243
+ const plugins = JSON.parse(raw)
244
+ const email = plugins.find((entry) => entry.id === 'email')
245
+ console.log(JSON.stringify({
246
+ email,
247
+ }))
248
+ `)
249
+
250
+ assert.equal(output.email.granted, true)
251
+ assert.equal(output.email.availableNow, true)
252
+ })
253
+
254
+ it('hydrates agent-approved tools into stale connector sessions on the next turn', () => {
255
+ const output = runWithTempDataDir(`
256
+ const storageMod = await import('./src/lib/server/storage.ts')
257
+ const toolsMod = await import('./src/lib/server/session-tools/index.ts')
258
+ const approvalsMod = await import('./src/lib/server/approvals.ts')
259
+ const storage = storageMod.default || storageMod
260
+ const toolsApi = toolsMod.default || toolsMod
261
+ const approvals = approvalsMod.default || approvalsMod
262
+
263
+ const now = Date.now()
264
+ storage.saveSettings({ approvalsEnabled: true })
265
+ storage.saveSessions({
266
+ connector_session: {
267
+ id: 'connector_session',
268
+ name: 'Connector Session',
269
+ cwd: process.env.WORKSPACE_DIR,
270
+ user: 'connector',
271
+ provider: 'openai',
272
+ model: 'gpt-test',
273
+ claudeSessionId: null,
274
+ messages: [],
275
+ createdAt: now,
276
+ lastActiveAt: now,
277
+ sessionType: 'human',
278
+ agentId: 'agent_1',
279
+ plugins: ['browser'],
280
+ },
281
+ })
282
+
283
+ const approval = approvals.requestApproval({
284
+ category: 'tool_access',
285
+ title: 'Enable connector tool',
286
+ description: 'Grant connector messaging',
287
+ data: { toolId: 'connector_message_tool', pluginId: 'connector_message_tool' },
288
+ agentId: 'agent_1',
289
+ sessionId: null,
290
+ })
291
+ await approvals.submitDecision(approval.id, true)
292
+
293
+ const built = await toolsApi.buildSessionTools(process.env.WORKSPACE_DIR, ['browser'], {
294
+ sessionId: 'connector_session',
295
+ agentId: 'agent_1',
296
+ platformAssignScope: 'self',
297
+ })
298
+ try {
299
+ const session = storage.loadSessions().connector_session
300
+ console.log(JSON.stringify({
301
+ toolNames: built.tools.map((entry) => entry.name).sort(),
302
+ plugins: session.plugins || [],
303
+ }))
304
+ } finally {
305
+ await built.cleanup()
306
+ }
307
+ `)
308
+
309
+ assert.equal(output.toolNames.includes('connector_message_tool'), true)
310
+ assert.equal(output.plugins.includes('connector_message_tool'), true)
311
+ })
170
312
  })