@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
@@ -0,0 +1,130 @@
1
+ import assert from 'node:assert/strict'
2
+ import path from 'node:path'
3
+ import { fileURLToPath } from 'node:url'
4
+ import test, { afterEach } from 'node:test'
5
+
6
+ import { GET as getMcpServer, PUT as updateMcpServer, DELETE as deleteMcpServer } from './[id]/route'
7
+ import { POST as runMcpConformance } from './[id]/conformance/route'
8
+ import { POST as invokeMcpTool } from './[id]/invoke/route'
9
+ import { POST as testMcpServer } from './[id]/test/route'
10
+ import { GET as listMcpTools } from './[id]/tools/route'
11
+ import { GET as listMcpServers, POST as createMcpServer } from './route'
12
+ import { loadMcpServers, saveMcpServers } from '@/lib/server/storage'
13
+
14
+ const originalMcpServers = loadMcpServers()
15
+ const fixturePath = path.resolve(
16
+ path.dirname(fileURLToPath(import.meta.url)),
17
+ '../../../lib/server/__fixtures__/fake-mcp-stdio-server.mjs',
18
+ )
19
+
20
+ function routeParams(id: string) {
21
+ return { params: Promise.resolve({ id }) }
22
+ }
23
+
24
+ afterEach(() => {
25
+ saveMcpServers(originalMcpServers)
26
+ })
27
+
28
+ test('MCP server routes exercise a live stdio server end to end', async () => {
29
+ const createResponse = await createMcpServer(new Request('http://local/api/mcp-servers', {
30
+ method: 'POST',
31
+ headers: { 'content-type': 'application/json' },
32
+ body: JSON.stringify({
33
+ name: 'smoke',
34
+ transport: 'stdio',
35
+ command: process.execPath,
36
+ args: [fixturePath],
37
+ }),
38
+ }))
39
+ assert.equal(createResponse.status, 200)
40
+ const created = await createResponse.json() as Record<string, unknown>
41
+ const serverId = String(created.id)
42
+ assert.equal(created.name, 'smoke')
43
+
44
+ const listResponse = await listMcpServers(new Request('http://local/api/mcp-servers'))
45
+ assert.equal(listResponse.status, 200)
46
+ const listed = await listResponse.json() as Record<string, Record<string, unknown>>
47
+ assert.equal(listed[serverId]?.name, 'smoke')
48
+
49
+ const detailResponse = await getMcpServer(new Request(`http://local/api/mcp-servers/${serverId}`), routeParams(serverId))
50
+ assert.equal(detailResponse.status, 200)
51
+ const detail = await detailResponse.json() as Record<string, unknown>
52
+ assert.equal(detail.command, process.execPath)
53
+
54
+ const healthResponse = await testMcpServer(new Request(`http://local/api/mcp-servers/${serverId}/test`, {
55
+ method: 'POST',
56
+ }), routeParams(serverId))
57
+ assert.equal(healthResponse.status, 200)
58
+ const health = await healthResponse.json() as Record<string, unknown>
59
+ assert.equal(health.ok, true)
60
+ assert.deepEqual(health.tools, ['mcp_smoke_ping', 'mcp_smoke_echo'])
61
+
62
+ const toolsResponse = await listMcpTools(new Request(`http://local/api/mcp-servers/${serverId}/tools`), routeParams(serverId))
63
+ assert.equal(toolsResponse.status, 200)
64
+ const tools = await toolsResponse.json() as Array<Record<string, unknown>>
65
+ assert.deepEqual(tools.map((tool) => tool.name), ['ping', 'echo'])
66
+
67
+ const invokeResponse = await invokeMcpTool(new Request(`http://local/api/mcp-servers/${serverId}/invoke`, {
68
+ method: 'POST',
69
+ headers: { 'content-type': 'application/json' },
70
+ body: JSON.stringify({
71
+ toolName: 'echo',
72
+ args: JSON.stringify({ message: 'hello from route test' }),
73
+ }),
74
+ }), routeParams(serverId))
75
+ assert.equal(invokeResponse.status, 200)
76
+ const invokePayload = await invokeResponse.json() as Record<string, unknown>
77
+ assert.equal(invokePayload.ok, true)
78
+ assert.equal(invokePayload.text, 'echo: hello from route test')
79
+
80
+ const conformanceResponse = await runMcpConformance(new Request(`http://local/api/mcp-servers/${serverId}/conformance`, {
81
+ method: 'POST',
82
+ headers: { 'content-type': 'application/json' },
83
+ body: JSON.stringify({ smokeToolName: 'ping' }),
84
+ }), routeParams(serverId))
85
+ assert.equal(conformanceResponse.status, 200)
86
+ const conformance = await conformanceResponse.json() as Record<string, unknown>
87
+ assert.equal(conformance.ok, true)
88
+ assert.equal(conformance.toolsCount, 2)
89
+ assert.equal(conformance.smokeToolName, 'ping')
90
+
91
+ const updateResponse = await updateMcpServer(new Request(`http://local/api/mcp-servers/${serverId}`, {
92
+ method: 'PUT',
93
+ headers: { 'content-type': 'application/json' },
94
+ body: JSON.stringify({ name: 'smoke-renamed' }),
95
+ }), routeParams(serverId))
96
+ assert.equal(updateResponse.status, 200)
97
+ const updated = await updateResponse.json() as Record<string, unknown>
98
+ assert.equal(updated.name, 'smoke-renamed')
99
+
100
+ const deleteResponse = await deleteMcpServer(new Request(`http://local/api/mcp-servers/${serverId}`, {
101
+ method: 'DELETE',
102
+ }), routeParams(serverId))
103
+ assert.equal(deleteResponse.status, 200)
104
+ assert.equal(loadMcpServers()[serverId], undefined)
105
+ })
106
+
107
+ test('MCP invoke route validates required fields before connecting', async () => {
108
+ const serverId = 'mcp-validate-smoke'
109
+ const servers = loadMcpServers()
110
+ servers[serverId] = {
111
+ id: serverId,
112
+ name: 'smoke',
113
+ transport: 'stdio',
114
+ command: process.execPath,
115
+ args: [fixturePath],
116
+ createdAt: 1,
117
+ updatedAt: 1,
118
+ }
119
+ saveMcpServers(servers)
120
+
121
+ const response = await invokeMcpTool(new Request(`http://local/api/mcp-servers/${serverId}/invoke`, {
122
+ method: 'POST',
123
+ headers: { 'content-type': 'application/json' },
124
+ body: JSON.stringify({ args: {} }),
125
+ }), routeParams(serverId))
126
+
127
+ assert.equal(response.status, 400)
128
+ const payload = await response.json() as Record<string, unknown>
129
+ assert.equal(payload.error, 'toolName is required')
130
+ })
@@ -1,6 +1,8 @@
1
1
  import { NextResponse } from 'next/server'
2
2
  import {
3
3
  buildOpenClawDeployBundle,
4
+ getOpenClawLocalDeployCollectionStatus,
5
+ getOpenClawRemoteDeployCollectionStatus,
4
6
  deployOpenClawOverSsh,
5
7
  getOpenClawLocalDeployStatus,
6
8
  getOpenClawRemoteDeployStatus,
@@ -96,9 +98,15 @@ function parseSsh(value: unknown): Partial<OpenClawSshConfig> | null {
96
98
  }
97
99
 
98
100
  export async function GET() {
101
+ const locals = getOpenClawLocalDeployCollectionStatus()
102
+ const remotes = getOpenClawRemoteDeployCollectionStatus()
99
103
  return NextResponse.json({
100
- local: getOpenClawLocalDeployStatus(),
101
- remote: getOpenClawRemoteDeployStatus(),
104
+ local: getOpenClawLocalDeployStatus(locals.primaryId),
105
+ locals: locals.items,
106
+ localPrimaryId: locals.primaryId,
107
+ remote: remotes.primaryId ? getOpenClawRemoteDeployStatus(remotes.primaryId) : null,
108
+ remotes: remotes.items,
109
+ remotePrimaryId: remotes.primaryId,
102
110
  })
103
111
  }
104
112
 
@@ -109,31 +117,44 @@ export async function POST(req: Request) {
109
117
  try {
110
118
  if (action === 'start-local') {
111
119
  const result = await startOpenClawLocalDeploy({
120
+ localId: typeof body.localId === 'string' ? body.localId : null,
121
+ name: typeof body.name === 'string' ? body.name : null,
112
122
  port: parsePort(body.port),
113
123
  token: typeof body.token === 'string' ? body.token : null,
124
+ makePrimary: body.makePrimary !== false,
114
125
  })
115
126
  return NextResponse.json({
116
127
  ok: true,
117
128
  local: result.local,
129
+ locals: result.locals,
130
+ localPrimaryId: result.locals.find((item) => item.isPrimary)?.id || result.local.id,
118
131
  token: result.token,
119
132
  })
120
133
  }
121
134
 
122
135
  if (action === 'stop-local') {
136
+ const result = stopOpenClawLocalDeploy(typeof body.localId === 'string' ? body.localId : null)
123
137
  return NextResponse.json({
124
138
  ok: true,
125
- local: stopOpenClawLocalDeploy(),
139
+ local: result.local,
140
+ locals: result.locals,
141
+ localPrimaryId: result.locals.find((item) => item.isPrimary)?.id || result.local.id,
126
142
  })
127
143
  }
128
144
 
129
145
  if (action === 'restart-local') {
130
146
  const result = await restartOpenClawLocalDeploy({
147
+ localId: typeof body.localId === 'string' ? body.localId : null,
148
+ name: typeof body.name === 'string' ? body.name : null,
131
149
  port: parsePort(body.port),
132
150
  token: typeof body.token === 'string' ? body.token : null,
151
+ makePrimary: body.makePrimary !== false,
133
152
  })
134
153
  return NextResponse.json({
135
154
  ok: true,
136
155
  local: result.local,
156
+ locals: result.locals,
157
+ localPrimaryId: result.locals.find((item) => item.isPrimary)?.id || result.local.id,
137
158
  token: result.token,
138
159
  })
139
160
  }
@@ -157,6 +178,8 @@ export async function POST(req: Request) {
157
178
 
158
179
  if (action === 'ssh-deploy') {
159
180
  const result = await deployOpenClawOverSsh({
181
+ remoteId: typeof body.remoteId === 'string' ? body.remoteId : null,
182
+ name: typeof body.name === 'string' ? body.name : null,
160
183
  template: parseTemplate(body.template),
161
184
  target: typeof body.target === 'string' ? body.target : null,
162
185
  token: typeof body.token === 'string' ? body.token : null,
@@ -166,10 +189,14 @@ export async function POST(req: Request) {
166
189
  useCase: parseUseCase(body.useCase),
167
190
  exposure: parseExposure(body.exposure),
168
191
  ssh: parseSsh(body.ssh),
192
+ makePrimary: body.makePrimary !== false,
169
193
  })
194
+ const remotes = getOpenClawRemoteDeployCollectionStatus()
170
195
  return NextResponse.json({
171
196
  ok: result.ok,
172
- remote: getOpenClawRemoteDeployStatus(),
197
+ remote: getOpenClawRemoteDeployStatus(result.remoteId || remotes.primaryId),
198
+ remotes: remotes.items,
199
+ remotePrimaryId: remotes.primaryId,
173
200
  processId: result.processId || null,
174
201
  token: result.token,
175
202
  bundle: result.bundle,
@@ -198,14 +225,20 @@ export async function POST(req: Request) {
198
225
  } as const
199
226
  const lifecycleAction = action as keyof typeof actionMap
200
227
  const result = await runOpenClawRemoteLifecycle({
228
+ remoteId: typeof body.remoteId === 'string' ? body.remoteId : null,
229
+ name: typeof body.name === 'string' ? body.name : null,
201
230
  action: actionMap[lifecycleAction],
202
231
  ssh: parseSsh(body.ssh),
203
232
  token: typeof body.token === 'string' ? body.token : null,
204
233
  backupPath: typeof body.backupPath === 'string' ? body.backupPath : null,
234
+ makePrimary: body.makePrimary !== false,
205
235
  })
236
+ const remotes = getOpenClawRemoteDeployCollectionStatus()
206
237
  return NextResponse.json({
207
238
  ok: result.ok,
208
- remote: getOpenClawRemoteDeployStatus(),
239
+ remote: getOpenClawRemoteDeployStatus(result.remoteId || remotes.primaryId),
240
+ remotes: remotes.items,
241
+ remotePrimaryId: remotes.primaryId,
209
242
  processId: result.processId || null,
210
243
  token: result.token,
211
244
  summary: result.summary,
@@ -1,22 +1,61 @@
1
1
  import { NextResponse } from 'next/server'
2
2
  import { getPluginManager, sanitizePluginFilename } from '@/lib/server/plugins'
3
+ import {
4
+ inferPluginInstallSourceFromUrl,
5
+ inferPluginPublisherSourceFromUrl,
6
+ normalizePluginInstallSource,
7
+ normalizePluginPublisherSource,
8
+ } from '@/lib/plugin-sources'
9
+ import {
10
+ buildPluginInstallCorsHeaders,
11
+ resolvePluginInstallCorsOrigin,
12
+ } from '@/lib/plugin-install-cors'
13
+
14
+ function json(body: Record<string, unknown>, status: number, origin: string | null) {
15
+ return NextResponse.json(body, {
16
+ status,
17
+ headers: buildPluginInstallCorsHeaders(origin),
18
+ })
19
+ }
20
+
21
+ export async function OPTIONS(req: Request) {
22
+ const origin = resolvePluginInstallCorsOrigin(req.headers.get('origin'))
23
+ if (!origin) return NextResponse.json({ error: 'Origin not allowed' }, { status: 403 })
24
+ return new NextResponse(null, {
25
+ status: 204,
26
+ headers: buildPluginInstallCorsHeaders(origin),
27
+ })
28
+ }
3
29
 
4
30
  export async function POST(req: Request) {
31
+ const origin = resolvePluginInstallCorsOrigin(req.headers.get('origin'))
5
32
  const body = await req.json()
6
33
  const url = typeof body?.url === 'string' ? body.url : ''
7
34
  const filename = typeof body?.filename === 'string' ? body.filename : ''
35
+ const installMethod = body?.installMethod === 'marketplace' ? 'marketplace' : 'manual'
36
+ const sourceLabel = normalizePluginPublisherSource(body?.sourceLabel)
37
+ || inferPluginPublisherSourceFromUrl(url)
38
+ || 'manual'
39
+ const installSource = normalizePluginInstallSource(body?.installSource)
40
+ || inferPluginInstallSourceFromUrl(url)
41
+ || 'manual'
8
42
 
9
43
  if (!url || !url.startsWith('https://')) {
10
- return NextResponse.json(
44
+ return json(
11
45
  { error: 'URL must be a valid HTTPS URL' },
12
- { status: 400 },
46
+ 400,
47
+ origin,
13
48
  )
14
49
  }
15
50
 
16
51
  try {
17
52
  const sanitizedFilename = sanitizePluginFilename(filename)
18
- const installed = await getPluginManager().installPluginFromUrl(url, sanitizedFilename)
19
- return NextResponse.json({ ok: true, filename: installed.filename, hash: installed.sourceHash })
53
+ const installed = await getPluginManager().installPluginFromUrl(url, sanitizedFilename, {
54
+ source: installMethod,
55
+ sourceLabel,
56
+ installSource,
57
+ })
58
+ return json({ ok: true, filename: installed.filename, hash: installed.sourceHash }, 200, origin)
20
59
  } catch (err: unknown) {
21
60
  const msg = err instanceof Error ? err.message : String(err)
22
61
  const isTimeout = /abort|timeout/i.test(msg)
@@ -25,9 +64,10 @@ export async function POST(req: Request) {
25
64
  : isTimeout
26
65
  ? 504
27
66
  : 500
28
- return NextResponse.json(
67
+ return json(
29
68
  { error: isTimeout ? 'Download timed out — the plugin URL may be unreachable' : msg },
30
- { status },
69
+ status,
70
+ origin,
31
71
  )
32
72
  }
33
73
  }
@@ -1,11 +1,25 @@
1
1
  import { NextResponse } from 'next/server'
2
+ import { inferPluginPublisherSourceFromUrl } from '@/lib/plugin-sources'
2
3
  import { searchClawHub } from '@/lib/server/clawhub-client'
4
+ import type { PluginCatalogSource } from '@/types'
3
5
 
4
6
  export const dynamic = 'force-dynamic'
5
7
 
6
- const REGISTRY_URLS = [
7
- 'https://raw.githubusercontent.com/swarmclawai/swarmforge/main/registry.json',
8
- 'https://swarmclaw.ai/registry/plugins.json',
8
+ interface RegistryPluginEntry {
9
+ id?: string
10
+ name?: string
11
+ description?: string
12
+ url?: string
13
+ author?: string
14
+ version?: string
15
+ tags?: string[]
16
+ openclaw?: boolean
17
+ downloads?: number
18
+ }
19
+
20
+ const REGISTRY_URLS: Array<{ url: string; catalogSource: PluginCatalogSource }> = [
21
+ { url: 'https://swarmclaw.ai/registry/plugins.json', catalogSource: 'swarmclaw-site' },
22
+ { url: 'https://raw.githubusercontent.com/swarmclawai/swarmforge/main/registry.json', catalogSource: 'swarmforge' },
9
23
  ]
10
24
  const CACHE_TTL = 5 * 60 * 1000 // 5 minutes
11
25
 
@@ -33,34 +47,43 @@ export async function GET(req: Request) {
33
47
  }
34
48
 
35
49
  const allPlugins: Record<string, unknown>[] = []
50
+ const registryPlugins = new Map<string, Record<string, unknown>>()
36
51
 
37
52
  // 1. Fetch SwarmClaw Registry
38
- for (const registryUrl of REGISTRY_URLS) {
53
+ for (const registry of REGISTRY_URLS) {
39
54
  try {
40
- const res = await fetch(registryUrl, { cache: 'no-store' })
55
+ const res = await fetch(registry.url, { cache: 'no-store' })
41
56
  if (!res.ok) continue
42
57
 
43
58
  const data = await res.json()
44
- const filtered = (data as Array<{ name: string; description: string; url?: string }>).filter((p) => {
59
+ const entries = Array.isArray(data) ? data as RegistryPluginEntry[] : []
60
+ const filtered = entries.filter((p) => {
45
61
  if (!p || typeof p.name !== 'string' || typeof p.description !== 'string') return false
46
62
  return !query || p.name.toLowerCase().includes(query.toLowerCase()) || p.description.toLowerCase().includes(query.toLowerCase())
47
63
  })
48
64
 
49
- allPlugins.push(...filtered.map((p: { id?: string; name?: string; url?: string }) => ({
50
- ...p,
51
- id: p.id || (p.name || '').toLowerCase().replace(/[^a-z0-9]/g, '_'),
52
- url: normalizeRegistryPluginUrl(p.url) || p.url,
53
- source: 'swarmclaw',
54
- })))
55
- break
65
+ for (const p of filtered) {
66
+ const normalizedUrl = normalizeRegistryPluginUrl(p.url) || p.url
67
+ const id = p.id || (p.name || '').toLowerCase().replace(/[^a-z0-9]/g, '_')
68
+ if (registryPlugins.has(id)) continue
69
+ registryPlugins.set(id, {
70
+ ...p,
71
+ id,
72
+ url: normalizedUrl,
73
+ source: inferPluginPublisherSourceFromUrl(normalizedUrl) || 'swarmforge',
74
+ catalogSource: registry.catalogSource,
75
+ })
76
+ }
56
77
  } catch (err: unknown) {
57
78
  console.warn('[marketplace] SC Registry failed:', {
58
- registryUrl,
79
+ registryUrl: registry.url,
59
80
  error: err instanceof Error ? err.message : String(err),
60
81
  })
61
82
  }
62
83
  }
63
84
 
85
+ allPlugins.push(...registryPlugins.values())
86
+
64
87
  // 2. Fetch ClawHub Skills/Plugins
65
88
  try {
66
89
  const hubResults = await searchClawHub(query)
@@ -71,12 +94,22 @@ export async function GET(req: Request) {
71
94
  author: s.author,
72
95
  version: s.version || '1.0.0',
73
96
  url: s.url,
74
- source: 'clawhub'
97
+ source: 'clawhub',
98
+ catalogSource: 'clawhub',
75
99
  })))
76
100
  } catch (err: unknown) {
77
101
  console.warn('[marketplace] ClawHub failed:', err instanceof Error ? err.message : String(err))
78
102
  }
79
103
 
104
+ allPlugins.sort((a, b) => {
105
+ const catalogA = typeof a.catalogSource === 'string' ? a.catalogSource : ''
106
+ const catalogB = typeof b.catalogSource === 'string' ? b.catalogSource : ''
107
+ if (catalogA !== catalogB) return catalogA.localeCompare(catalogB)
108
+ const nameA = typeof a.name === 'string' ? a.name : ''
109
+ const nameB = typeof b.name === 'string' ? b.name : ''
110
+ return nameA.localeCompare(nameB)
111
+ })
112
+
80
113
  // Update cache only for empty queries
81
114
  if (!query) {
82
115
  cache = { data: allPlugins, fetchedAt: now }
@@ -4,6 +4,7 @@ import http from 'http'
4
4
  import fs from 'fs'
5
5
  import path from 'path'
6
6
  import { localIP } from '@/lib/server/storage'
7
+ import { resolveDevServerLaunchDir } from '@/lib/server/devserver-launch'
7
8
 
8
9
  // ---------------------------------------------------------------------------
9
10
  // MIME types for static server
@@ -77,6 +78,13 @@ interface ProjectInfo {
77
78
  framework?: string // e.g. 'vite', 'next', 'cra'
78
79
  }
79
80
 
81
+ function buildFrameworkArgs(framework: string | undefined, port: number): string[] {
82
+ if (framework === 'next') {
83
+ return ['--', '--hostname', '0.0.0.0', '--port', String(port)]
84
+ }
85
+ return ['--', '--port', String(port), '--host', '0.0.0.0']
86
+ }
87
+
80
88
  function detectProject(dir: string): ProjectInfo {
81
89
  const pkgPath = path.join(dir, 'package.json')
82
90
  if (!fs.existsSync(pkgPath)) {
@@ -168,7 +176,7 @@ function createStaticServer(dir: string): http.Server {
168
176
  // npm dev server
169
177
  // ---------------------------------------------------------------------------
170
178
 
171
- async function startNpmServer(dir: string, command: string[], port: number): Promise<PreviewServer> {
179
+ async function startNpmServer(dir: string, command: string[], port: number, framework?: string): Promise<PreviewServer> {
172
180
  // Install deps if node_modules missing
173
181
  if (!fs.existsSync(path.join(dir, 'node_modules'))) {
174
182
  console.log(`[preview] Installing dependencies in ${dir}`)
@@ -190,7 +198,7 @@ async function startNpmServer(dir: string, command: string[], port: number): Pro
190
198
  const args = [...command.slice(1)]
191
199
  const cmdName = command[0]
192
200
 
193
- const proc = spawn(cmdName, [...args, '--', '--port', String(port), '--host', '0.0.0.0'], {
201
+ const proc = spawn(cmdName, [...args, ...buildFrameworkArgs(framework, port)], {
194
202
  cwd: dir,
195
203
  stdio: ['ignore', 'pipe', 'pipe'],
196
204
  env,
@@ -234,6 +242,10 @@ async function startNpmServer(dir: string, command: string[], port: number): Pro
234
242
 
235
243
  // Wait for the server to start and detect the actual port
236
244
  await new Promise((resolve) => setTimeout(resolve, 5000))
245
+ if (proc.exitCode !== null) {
246
+ servers.delete(dirKey(dir))
247
+ throw new Error(`npm dev server exited early with code ${proc.exitCode}\n${log.slice(-4000)}`)
248
+ }
237
249
  entry.port = detectedPort
238
250
  entry.log = log
239
251
 
@@ -263,7 +275,8 @@ export async function POST(req: Request) {
263
275
  }
264
276
 
265
277
  const dir = resolveServeDir(filePath)
266
- const key = dirKey(dir)
278
+ const launch = resolveDevServerLaunchDir(dir)
279
+ const key = dirKey(launch.launchDir)
267
280
 
268
281
  if (action === 'start') {
269
282
  if (servers.has(key)) {
@@ -274,16 +287,18 @@ export async function POST(req: Request) {
274
287
  return NextResponse.json({ error: 'Directory not found' }, { status: 404 })
275
288
  }
276
289
 
277
- const project = detectProject(dir)
290
+ const project = detectProject(launch.launchDir)
278
291
  const port = await findFreePort()
279
292
 
280
293
  if (project.type === 'npm' && project.devCommand) {
281
- console.log(`[preview] Detected ${project.framework} project in ${dir}, running: ${project.devCommand.join(' ')}`)
294
+ console.log(`[preview] Detected ${project.framework} project in ${launch.launchDir}, running: ${project.devCommand.join(' ')}`)
282
295
  try {
283
- const entry = await startNpmServer(dir, project.devCommand, port)
296
+ const entry = await startNpmServer(launch.launchDir, project.devCommand, port, project.framework)
284
297
  return NextResponse.json({
285
298
  ...buildResponse(entry),
286
299
  framework: project.framework,
300
+ inputDir: dir,
301
+ launchDir: launch.launchDir,
287
302
  })
288
303
  } catch (err: unknown) {
289
304
  console.error(`[preview] npm server failed, falling back to static:`, err)
@@ -313,15 +328,15 @@ export async function POST(req: Request) {
313
328
  }
314
329
  if (srv.server) srv.server.close()
315
330
  servers.delete(key)
316
- console.log(`[preview] Stopped server for ${dir}`)
331
+ console.log(`[preview] Stopped server for ${launch.launchDir}`)
317
332
  }
318
- return NextResponse.json({ running: false, dir })
333
+ return NextResponse.json({ running: false, dir: launch.launchDir })
319
334
 
320
335
  } else if (action === 'status') {
321
336
  if (servers.has(key)) {
322
337
  return NextResponse.json(buildResponse(servers.get(key)!))
323
338
  }
324
- return NextResponse.json({ running: false, dir })
339
+ return NextResponse.json({ running: false, dir: launch.launchDir })
325
340
 
326
341
  } else if (action === 'list') {
327
342
  const list = Array.from(servers.values()).map((s) => ({
@@ -331,8 +346,8 @@ export async function POST(req: Request) {
331
346
  return NextResponse.json({ servers: list })
332
347
 
333
348
  } else if (action === 'detect') {
334
- const project = detectProject(dir)
335
- return NextResponse.json({ dir, ...project })
349
+ const project = detectProject(launch.launchDir)
350
+ return NextResponse.json({ dir, launchDir: launch.launchDir, frameworkHint: launch.framework, ...project })
336
351
  }
337
352
 
338
353
  return NextResponse.json({ error: 'Invalid action' }, { status: 400 })
@@ -2,6 +2,7 @@ import { NextResponse } from 'next/server'
2
2
  import { genId } from '@/lib/id'
3
3
  import { notFound } from '@/lib/server/collection-helpers'
4
4
  import { loadSchedules, saveSchedules, loadAgents, loadTasks, saveTasks } from '@/lib/server/storage'
5
+ import { buildAgentDisabledMessage, isAgentDisabled } from '@/lib/server/agent-availability'
5
6
  import { enqueueTask } from '@/lib/server/queue'
6
7
  import { pushMainLoopEventToMainSessions } from '@/lib/server/main-agent-loop'
7
8
  import { getScheduleSignatureKey } from '@/lib/schedule-dedupe'
@@ -20,6 +21,9 @@ export async function POST(_req: Request, { params }: { params: Promise<{ id: st
20
21
  const agents = loadAgents()
21
22
  const agent = agents[schedule.agentId]
22
23
  if (!agent) return NextResponse.json({ error: 'Agent not found' }, { status: 400 })
24
+ if (isAgentDisabled(agent)) {
25
+ return NextResponse.json({ error: buildAgentDisabledMessage(agent, 'run schedules') }, { status: 409 })
26
+ }
23
27
 
24
28
  const tasks = loadTasks()
25
29
  const scheduleSignature = getScheduleSignatureKey(schedule)
@@ -0,0 +1,86 @@
1
+ import assert from 'node:assert/strict'
2
+ import test, { afterEach } from 'node:test'
3
+
4
+ import { POST as createSchedule } from './route'
5
+ import { POST as runSchedule } from './[id]/run/route'
6
+ import { loadAgents, loadSchedules, saveAgents, saveSchedules } from '@/lib/server/storage'
7
+
8
+ const originalAgents = loadAgents()
9
+ const originalSchedules = loadSchedules()
10
+
11
+ function routeParams(id: string) {
12
+ return { params: Promise.resolve({ id }) }
13
+ }
14
+
15
+ function seedAgent(id: string, overrides: Record<string, unknown> = {}) {
16
+ const agents = loadAgents()
17
+ const now = Date.now()
18
+ agents[id] = {
19
+ id,
20
+ name: 'Schedule Test Agent',
21
+ description: 'Schedule smoke test agent',
22
+ systemPrompt: 'Handle schedules.',
23
+ provider: 'openai',
24
+ model: 'gpt-4o-mini',
25
+ credentialId: null,
26
+ fallbackCredentialIds: [],
27
+ apiEndpoint: null,
28
+ plugins: ['manage_schedules'],
29
+ createdAt: now,
30
+ updatedAt: now,
31
+ ...overrides,
32
+ }
33
+ saveAgents(agents)
34
+ }
35
+
36
+ afterEach(() => {
37
+ saveAgents(originalAgents)
38
+ saveSchedules(originalSchedules)
39
+ })
40
+
41
+ test('POST /api/schedules rejects disabled agents', async () => {
42
+ seedAgent('schedule-disabled-agent', { disabled: true })
43
+
44
+ const response = await createSchedule(new Request('http://local/api/schedules', {
45
+ method: 'POST',
46
+ headers: { 'content-type': 'application/json' },
47
+ body: JSON.stringify({
48
+ agentId: 'schedule-disabled-agent',
49
+ name: 'Disabled smoke',
50
+ taskPrompt: 'Send a reminder',
51
+ scheduleType: 'once',
52
+ runAt: Date.now() + 60_000,
53
+ status: 'active',
54
+ }),
55
+ }))
56
+
57
+ assert.equal(response.status, 409)
58
+ const payload = await response.json() as Record<string, unknown>
59
+ assert.match(String(payload.error || ''), /disabled/i)
60
+ })
61
+
62
+ test('POST /api/schedules/[id]/run rejects disabled agents', async () => {
63
+ seedAgent('schedule-run-disabled-agent', { disabled: true })
64
+ const schedules = loadSchedules()
65
+ schedules['schedule-disabled-run'] = {
66
+ id: 'schedule-disabled-run',
67
+ name: 'Disabled Run',
68
+ agentId: 'schedule-run-disabled-agent',
69
+ taskPrompt: 'Send a reminder',
70
+ scheduleType: 'once',
71
+ runAt: Date.now() + 60_000,
72
+ status: 'active',
73
+ createdAt: Date.now(),
74
+ updatedAt: Date.now(),
75
+ }
76
+ saveSchedules(schedules)
77
+
78
+ const response = await runSchedule(
79
+ new Request('http://local/api/schedules/schedule-disabled-run/run', { method: 'POST' }),
80
+ routeParams('schedule-disabled-run'),
81
+ )
82
+
83
+ assert.equal(response.status, 409)
84
+ const payload = await response.json() as Record<string, unknown>
85
+ assert.match(String(payload.error || ''), /disabled/i)
86
+ })
@@ -3,6 +3,7 @@ import { genId } from '@/lib/id'
3
3
  import { loadAgents, loadSchedules, saveSchedules } from '@/lib/server/storage'
4
4
  import { WORKSPACE_DIR } from '@/lib/server/data-dir'
5
5
  import { normalizeSchedulePayload } from '@/lib/server/schedule-normalization'
6
+ import { buildAgentDisabledMessage, isAgentDisabled } from '@/lib/server/agent-availability'
6
7
  import { resolveScheduleName } from '@/lib/schedule-name'
7
8
  import { findDuplicateSchedule } from '@/lib/schedule-dedupe'
8
9
  import { notify } from '@/lib/server/ws-hub'
@@ -45,9 +46,13 @@ export async function POST(req: Request) {
45
46
 
46
47
  const candidate = normalizedSchedule.value
47
48
  const agents = loadAgents()
48
- if (!agents[String(candidate.agentId)]) {
49
+ const agent = agents[String(candidate.agentId)]
50
+ if (!agent) {
49
51
  return NextResponse.json({ error: `Agent not found: ${String(candidate.agentId)}` }, { status: 400 })
50
52
  }
53
+ if (isAgentDisabled(agent)) {
54
+ return NextResponse.json({ error: buildAgentDisabledMessage(agent, 'take scheduled work') }, { status: 409 })
55
+ }
51
56
  const scheduleType = asScheduleType(candidate.scheduleType)
52
57
  const candidateAgentId = asString(candidate.agentId) || null
53
58
  const candidateTaskPrompt = asString(candidate.taskPrompt)