@swarmclawai/swarmclaw 0.7.7 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (281) hide show
  1. package/README.md +12 -14
  2. package/next.config.ts +13 -2
  3. package/package.json +4 -2
  4. package/src/app/api/agents/[id]/thread/route.ts +9 -0
  5. package/src/app/api/agents/route.ts +4 -0
  6. package/src/app/api/agents/thread-route.test.ts +133 -0
  7. package/src/app/api/approvals/route.test.ts +148 -0
  8. package/src/app/api/canvas/[sessionId]/route.ts +3 -1
  9. package/src/app/api/chatrooms/[id]/chat/route.ts +4 -2
  10. package/src/app/api/chats/[id]/devserver/route.ts +48 -7
  11. package/src/app/api/chats/[id]/messages/route.ts +42 -18
  12. package/src/app/api/chats/[id]/route.ts +1 -1
  13. package/src/app/api/chats/[id]/stop/route.ts +5 -4
  14. package/src/app/api/chats/route.ts +23 -2
  15. package/src/app/api/clawhub/install/route.ts +28 -8
  16. package/src/app/api/connectors/[id]/route.ts +46 -3
  17. package/src/app/api/connectors/route.ts +12 -8
  18. package/src/app/api/external-agents/route.test.ts +165 -0
  19. package/src/app/api/gateways/[id]/health/route.ts +27 -12
  20. package/src/app/api/gateways/[id]/route.ts +2 -0
  21. package/src/app/api/gateways/health-route.test.ts +135 -0
  22. package/src/app/api/gateways/route.ts +2 -0
  23. package/src/app/api/mcp-servers/route.test.ts +130 -0
  24. package/src/app/api/openclaw/deploy/route.ts +38 -5
  25. package/src/app/api/plugins/install/route.ts +46 -6
  26. package/src/app/api/plugins/marketplace/route.ts +48 -15
  27. package/src/app/api/preview-server/route.ts +26 -11
  28. package/src/app/api/projects/[id]/route.ts +6 -2
  29. package/src/app/api/projects/route.ts +4 -3
  30. package/src/app/api/schedules/[id]/run/route.ts +4 -0
  31. package/src/app/api/schedules/route.test.ts +86 -0
  32. package/src/app/api/schedules/route.ts +6 -1
  33. package/src/app/api/secrets/[id]/route.ts +1 -0
  34. package/src/app/api/secrets/route.ts +2 -1
  35. package/src/app/api/settings/route.ts +2 -0
  36. package/src/app/api/setup/check-provider/route.test.ts +19 -0
  37. package/src/app/api/setup/check-provider/route.ts +40 -10
  38. package/src/app/api/skills/[id]/route.ts +12 -0
  39. package/src/app/api/skills/import/route.ts +14 -12
  40. package/src/app/api/skills/route.ts +13 -1
  41. package/src/app/api/tasks/[id]/route.ts +10 -1
  42. package/src/app/api/tasks/import/github/route.test.ts +65 -0
  43. package/src/app/api/tasks/import/github/route.ts +337 -0
  44. package/src/app/api/wallets/[id]/approve/route.ts +17 -3
  45. package/src/app/api/wallets/[id]/route.ts +79 -33
  46. package/src/app/api/wallets/[id]/send/route.ts +19 -33
  47. package/src/app/api/wallets/route.ts +78 -61
  48. package/src/app/api/webhooks/[id]/route.ts +33 -6
  49. package/src/app/api/webhooks/route.test.ts +272 -0
  50. package/src/cli/index.js +1 -0
  51. package/src/cli/spec.js +1 -0
  52. package/src/components/agents/agent-card.tsx +9 -2
  53. package/src/components/agents/agent-chat-list.tsx +18 -2
  54. package/src/components/agents/agent-list.tsx +1 -0
  55. package/src/components/agents/agent-sheet.tsx +257 -38
  56. package/src/components/agents/inspector-panel.tsx +41 -0
  57. package/src/components/canvas/canvas-panel.tsx +236 -65
  58. package/src/components/chat/chat-area.tsx +36 -19
  59. package/src/components/chat/chat-card.tsx +36 -13
  60. package/src/components/chat/chat-header.tsx +48 -16
  61. package/src/components/chat/chat-list.tsx +28 -4
  62. package/src/components/chat/checkpoint-timeline.tsx +50 -34
  63. package/src/components/chat/delegation-banner.test.ts +14 -1
  64. package/src/components/chat/delegation-banner.tsx +1 -1
  65. package/src/components/chat/message-bubble.tsx +208 -145
  66. package/src/components/chat/message-list.tsx +48 -19
  67. package/src/components/chatrooms/chatroom-message.tsx +2 -2
  68. package/src/components/chatrooms/chatroom-sheet.tsx +16 -2
  69. package/src/components/connectors/connector-health.tsx +1 -1
  70. package/src/components/connectors/connector-list.tsx +7 -2
  71. package/src/components/connectors/connector-sheet.tsx +337 -148
  72. package/src/components/gateways/gateway-sheet.tsx +2 -2
  73. package/src/components/layout/app-layout.tsx +40 -23
  74. package/src/components/mcp-servers/mcp-server-list.tsx +26 -5
  75. package/src/components/mcp-servers/mcp-server-sheet.tsx +19 -2
  76. package/src/components/openclaw/openclaw-deploy-panel.tsx +269 -21
  77. package/src/components/plugins/plugin-list.tsx +45 -9
  78. package/src/components/plugins/plugin-sheet.tsx +55 -7
  79. package/src/components/projects/project-detail.tsx +217 -0
  80. package/src/components/projects/project-sheet.tsx +176 -4
  81. package/src/components/providers/provider-list.tsx +2 -1
  82. package/src/components/providers/provider-sheet.tsx +21 -2
  83. package/src/components/schedules/schedule-card.tsx +25 -1
  84. package/src/components/schedules/schedule-sheet.tsx +44 -2
  85. package/src/components/secrets/secret-sheet.tsx +21 -2
  86. package/src/components/shared/agent-switch-dialog.tsx +12 -1
  87. package/src/components/shared/bottom-sheet.tsx +13 -3
  88. package/src/components/shared/command-palette.tsx +8 -1
  89. package/src/components/shared/confirm-dialog.tsx +19 -4
  90. package/src/components/shared/connector-platform-icon.test.ts +28 -0
  91. package/src/components/shared/connector-platform-icon.tsx +39 -6
  92. package/src/components/shared/settings/plugin-manager.tsx +29 -6
  93. package/src/components/shared/settings/section-capability-policy.tsx +45 -3
  94. package/src/components/shared/settings/section-voice.tsx +11 -3
  95. package/src/components/skills/skill-list.tsx +25 -0
  96. package/src/components/skills/skill-sheet.tsx +84 -12
  97. package/src/components/tasks/approvals-panel.tsx +289 -34
  98. package/src/components/tasks/task-board.tsx +410 -25
  99. package/src/components/tasks/task-card.tsx +66 -8
  100. package/src/components/tasks/task-sheet.tsx +16 -4
  101. package/src/components/ui/dialog.tsx +2 -2
  102. package/src/components/wallets/wallet-approval-dialog.tsx +4 -2
  103. package/src/components/wallets/wallet-panel.tsx +435 -90
  104. package/src/components/wallets/wallet-section.tsx +198 -48
  105. package/src/components/webhooks/webhook-sheet.tsx +22 -2
  106. package/src/lib/approval-display.ts +20 -0
  107. package/src/lib/canvas-content.ts +198 -0
  108. package/src/lib/chat-artifact-summary.ts +165 -0
  109. package/src/lib/chat-display.test.ts +91 -0
  110. package/src/lib/chat-display.ts +58 -0
  111. package/src/lib/chat-streaming-state.test.ts +47 -1
  112. package/src/lib/chat-streaming-state.ts +42 -0
  113. package/src/lib/ollama-model.ts +10 -0
  114. package/src/lib/openclaw-endpoint.test.ts +8 -0
  115. package/src/lib/openclaw-endpoint.ts +6 -1
  116. package/src/lib/plugin-install-cors.ts +46 -0
  117. package/src/lib/plugin-sources.test.ts +43 -0
  118. package/src/lib/plugin-sources.ts +77 -0
  119. package/src/lib/providers/ollama.ts +16 -6
  120. package/src/lib/providers/openclaw.test.ts +54 -0
  121. package/src/lib/providers/openclaw.ts +127 -11
  122. package/src/lib/schedule-dedupe-advanced.test.ts +1335 -0
  123. package/src/lib/schedule-dedupe.test.ts +66 -1
  124. package/src/lib/schedule-dedupe.ts +169 -12
  125. package/src/lib/schedule-origin.test.ts +20 -0
  126. package/src/lib/schedule-origin.ts +15 -0
  127. package/src/lib/server/__fixtures__/fake-mcp-stdio-server.mjs +27 -0
  128. package/src/lib/server/agent-availability.ts +16 -0
  129. package/src/lib/server/agent-runtime-config.ts +12 -4
  130. package/src/lib/server/agent-thread-session.test.ts +51 -0
  131. package/src/lib/server/agent-thread-session.ts +7 -0
  132. package/src/lib/server/approval-match.ts +205 -0
  133. package/src/lib/server/approvals-auto-approve.test.ts +538 -1
  134. package/src/lib/server/approvals.ts +214 -1
  135. package/src/lib/server/assistant-control.test.ts +29 -0
  136. package/src/lib/server/assistant-control.ts +23 -0
  137. package/src/lib/server/build-llm.test.ts +79 -0
  138. package/src/lib/server/build-llm.ts +14 -4
  139. package/src/lib/server/canvas-content.test.ts +32 -0
  140. package/src/lib/server/canvas-content.ts +6 -0
  141. package/src/lib/server/capability-router.test.ts +33 -0
  142. package/src/lib/server/capability-router.ts +80 -19
  143. package/src/lib/server/chat-execution-advanced.test.ts +651 -0
  144. package/src/lib/server/chat-execution-disabled.test.ts +94 -0
  145. package/src/lib/server/chat-execution-tool-events.test.ts +157 -0
  146. package/src/lib/server/chat-execution.ts +378 -73
  147. package/src/lib/server/clawhub-client.test.ts +14 -8
  148. package/src/lib/server/connectors/manager-reconnect.test.ts +47 -0
  149. package/src/lib/server/connectors/manager.test.ts +1147 -0
  150. package/src/lib/server/connectors/manager.ts +461 -137
  151. package/src/lib/server/connectors/pairing.ts +26 -5
  152. package/src/lib/server/connectors/types.ts +2 -0
  153. package/src/lib/server/connectors/whatsapp.test.ts +134 -0
  154. package/src/lib/server/connectors/whatsapp.ts +271 -47
  155. package/src/lib/server/context-manager.ts +6 -1
  156. package/src/lib/server/daemon-state.ts +84 -47
  157. package/src/lib/server/data-dir.test.ts +37 -0
  158. package/src/lib/server/data-dir.ts +20 -1
  159. package/src/lib/server/delegation-jobs-advanced.test.ts +513 -0
  160. package/src/lib/server/devserver-launch.test.ts +60 -0
  161. package/src/lib/server/devserver-launch.ts +85 -0
  162. package/src/lib/server/elevenlabs.test.ts +247 -1
  163. package/src/lib/server/elevenlabs.ts +147 -43
  164. package/src/lib/server/ethereum.ts +590 -0
  165. package/src/lib/server/eval/agent-regression-advanced.test.ts +302 -0
  166. package/src/lib/server/eval/agent-regression.test.ts +18 -1
  167. package/src/lib/server/eval/agent-regression.ts +383 -11
  168. package/src/lib/server/evm-swap.ts +475 -0
  169. package/src/lib/server/execution-log.ts +1 -0
  170. package/src/lib/server/heartbeat-service-timer.test.ts +173 -0
  171. package/src/lib/server/heartbeat-service.ts +20 -11
  172. package/src/lib/server/heartbeat-wake.test.ts +112 -0
  173. package/src/lib/server/heartbeat-wake.ts +338 -57
  174. package/src/lib/server/main-agent-loop-advanced.test.ts +538 -0
  175. package/src/lib/server/main-agent-loop.test.ts +260 -0
  176. package/src/lib/server/main-agent-loop.ts +559 -14
  177. package/src/lib/server/mcp-client.test.ts +16 -0
  178. package/src/lib/server/mcp-client.ts +25 -0
  179. package/src/lib/server/memory-integration.test.ts +719 -0
  180. package/src/lib/server/memory-policy.test.ts +43 -0
  181. package/src/lib/server/memory-policy.ts +132 -0
  182. package/src/lib/server/memory-tiers.test.ts +60 -0
  183. package/src/lib/server/memory-tiers.ts +16 -0
  184. package/src/lib/server/ollama-runtime.ts +58 -0
  185. package/src/lib/server/openclaw-deploy.test.ts +109 -1
  186. package/src/lib/server/openclaw-deploy.ts +557 -81
  187. package/src/lib/server/openclaw-gateway.test.ts +131 -0
  188. package/src/lib/server/openclaw-gateway.ts +10 -4
  189. package/src/lib/server/openclaw-health.test.ts +35 -0
  190. package/src/lib/server/openclaw-health.ts +215 -47
  191. package/src/lib/server/orchestrator-lg.ts +3 -2
  192. package/src/lib/server/orchestrator.ts +2 -0
  193. package/src/lib/server/plugins-advanced.test.ts +351 -0
  194. package/src/lib/server/plugins.ts +211 -6
  195. package/src/lib/server/project-context.ts +162 -0
  196. package/src/lib/server/project-utils.ts +150 -0
  197. package/src/lib/server/queue-advanced.test.ts +528 -0
  198. package/src/lib/server/queue-followups.test.ts +409 -2
  199. package/src/lib/server/queue-reconcile.test.ts +128 -0
  200. package/src/lib/server/queue.ts +527 -68
  201. package/src/lib/server/scheduler.ts +29 -1
  202. package/src/lib/server/session-note.test.ts +36 -0
  203. package/src/lib/server/session-note.ts +42 -0
  204. package/src/lib/server/session-run-manager.ts +83 -4
  205. package/src/lib/server/session-tools/canvas.ts +14 -12
  206. package/src/lib/server/session-tools/connector-inputs.test.ts +37 -0
  207. package/src/lib/server/session-tools/connector.test.ts +138 -0
  208. package/src/lib/server/session-tools/connector.ts +366 -54
  209. package/src/lib/server/session-tools/context.ts +17 -3
  210. package/src/lib/server/session-tools/crud.ts +484 -84
  211. package/src/lib/server/session-tools/delegate-fallback.test.ts +103 -0
  212. package/src/lib/server/session-tools/delegate-resume.test.ts +50 -0
  213. package/src/lib/server/session-tools/delegate.ts +102 -10
  214. package/src/lib/server/session-tools/discovery-approvals.test.ts +142 -0
  215. package/src/lib/server/session-tools/discovery.ts +80 -12
  216. package/src/lib/server/session-tools/file-normalize.test.ts +36 -0
  217. package/src/lib/server/session-tools/file.ts +43 -4
  218. package/src/lib/server/session-tools/human-loop.ts +35 -5
  219. package/src/lib/server/session-tools/index.ts +44 -9
  220. package/src/lib/server/session-tools/manage-connectors.test.ts +139 -0
  221. package/src/lib/server/session-tools/manage-schedules-advanced.test.ts +564 -0
  222. package/src/lib/server/session-tools/manage-schedules.test.ts +283 -0
  223. package/src/lib/server/session-tools/manage-tasks-advanced.test.ts +852 -0
  224. package/src/lib/server/session-tools/manage-tasks.test.ts +114 -0
  225. package/src/lib/server/session-tools/memory.test.ts +93 -0
  226. package/src/lib/server/session-tools/memory.ts +554 -75
  227. package/src/lib/server/session-tools/normalize-tool-args.ts +1 -1
  228. package/src/lib/server/session-tools/platform-access.test.ts +58 -0
  229. package/src/lib/server/session-tools/platform.ts +60 -19
  230. package/src/lib/server/session-tools/plugin-creator.ts +57 -1
  231. package/src/lib/server/session-tools/primitive-tools.test.ts +6 -0
  232. package/src/lib/server/session-tools/schedule.ts +6 -1
  233. package/src/lib/server/session-tools/shell-normalize.test.ts +25 -1
  234. package/src/lib/server/session-tools/shell.ts +22 -3
  235. package/src/lib/server/session-tools/wallet-tool.test.ts +254 -0
  236. package/src/lib/server/session-tools/wallet.ts +1374 -139
  237. package/src/lib/server/session-tools/web-inputs.test.ts +178 -0
  238. package/src/lib/server/session-tools/web.ts +621 -70
  239. package/src/lib/server/skill-discovery.ts +128 -0
  240. package/src/lib/server/skill-eligibility.test.ts +84 -0
  241. package/src/lib/server/skill-eligibility.ts +95 -0
  242. package/src/lib/server/skill-prompt-budget.test.ts +102 -0
  243. package/src/lib/server/skill-prompt-budget.ts +125 -0
  244. package/src/lib/server/skills-normalize.test.ts +54 -0
  245. package/src/lib/server/skills-normalize.ts +372 -26
  246. package/src/lib/server/solana.ts +214 -29
  247. package/src/lib/server/storage.ts +65 -36
  248. package/src/lib/server/stream-agent-chat.test.ts +437 -2
  249. package/src/lib/server/stream-agent-chat.ts +957 -79
  250. package/src/lib/server/system-events.ts +1 -1
  251. package/src/lib/server/tool-aliases.ts +2 -0
  252. package/src/lib/server/tool-capability-policy-advanced.test.ts +502 -0
  253. package/src/lib/server/tool-capability-policy.test.ts +24 -0
  254. package/src/lib/server/tool-capability-policy.ts +29 -1
  255. package/src/lib/server/tool-loop-detection.test.ts +105 -0
  256. package/src/lib/server/tool-loop-detection.ts +260 -0
  257. package/src/lib/server/tool-planning.test.ts +44 -0
  258. package/src/lib/server/tool-planning.ts +271 -0
  259. package/src/lib/server/wallet-execution.test.ts +198 -0
  260. package/src/lib/server/wallet-portfolio.test.ts +98 -0
  261. package/src/lib/server/wallet-portfolio.ts +724 -0
  262. package/src/lib/server/wallet-service.test.ts +57 -0
  263. package/src/lib/server/wallet-service.ts +213 -0
  264. package/src/lib/server/watch-jobs-advanced.test.ts +594 -0
  265. package/src/lib/server/watch-jobs.ts +17 -2
  266. package/src/lib/server/workspace-context.ts +111 -0
  267. package/src/lib/skill-save-payload.test.ts +39 -0
  268. package/src/lib/skill-save-payload.ts +37 -0
  269. package/src/lib/tasks.ts +28 -0
  270. package/src/lib/tool-definitions.ts +2 -1
  271. package/src/lib/tool-event-summary.test.ts +30 -0
  272. package/src/lib/tool-event-summary.ts +37 -0
  273. package/src/lib/validation/schemas.ts +1 -0
  274. package/src/lib/wallet-transactions.test.ts +75 -0
  275. package/src/lib/wallet-transactions.ts +43 -0
  276. package/src/lib/wallet.test.ts +17 -0
  277. package/src/lib/wallet.ts +183 -0
  278. package/src/proxy.test.ts +31 -0
  279. package/src/proxy.ts +34 -2
  280. package/src/stores/use-chat-store.ts +15 -1
  281. package/src/types/index.ts +249 -14
@@ -0,0 +1,131 @@
1
+ import assert from 'node:assert/strict'
2
+ import { afterEach, test } from 'node:test'
3
+
4
+ import type { Agent, GatewayProfile } from '@/types'
5
+ import {
6
+ encryptKey,
7
+ loadAgents,
8
+ loadCredentials,
9
+ loadGatewayProfiles,
10
+ saveAgents,
11
+ saveCredentials,
12
+ saveGatewayProfiles,
13
+ } from './storage'
14
+ import { resolveGatewayConfig } from './openclaw-gateway'
15
+
16
+ const originalCredentials = loadCredentials()
17
+ const originalGateways = loadGatewayProfiles()
18
+ const originalAgents = loadAgents({ includeTrashed: true })
19
+
20
+ afterEach(() => {
21
+ saveCredentials(originalCredentials)
22
+ saveGatewayProfiles(originalGateways)
23
+ saveAgents(originalAgents)
24
+ })
25
+
26
+ function saveGatewayCredential(id: string, token: string) {
27
+ const credentials = loadCredentials()
28
+ credentials[id] = {
29
+ id,
30
+ provider: 'openclaw',
31
+ name: `Credential ${id}`,
32
+ encryptedKey: encryptKey(token),
33
+ createdAt: Date.now(),
34
+ }
35
+ saveCredentials(credentials)
36
+ }
37
+
38
+ function saveGatewayProfile(profile: GatewayProfile) {
39
+ const gateways = loadGatewayProfiles()
40
+ gateways[profile.id] = profile
41
+ saveGatewayProfiles(gateways)
42
+ }
43
+
44
+ test('resolveGatewayConfig uses the gateway profile wsUrl and decrypted token', () => {
45
+ saveGatewayCredential('openclaw-cred-1', 'gateway-token-1')
46
+ saveGatewayProfile({
47
+ id: 'gateway-profile-1',
48
+ name: 'Gateway 1',
49
+ provider: 'openclaw',
50
+ endpoint: 'http://127.0.0.1:19161/v1',
51
+ wsUrl: 'ws://127.0.0.1:19161',
52
+ credentialId: 'openclaw-cred-1',
53
+ status: 'healthy',
54
+ notes: null,
55
+ tags: ['smoke'],
56
+ lastError: null,
57
+ lastCheckedAt: null,
58
+ lastModelCount: null,
59
+ discoveredHost: '127.0.0.1',
60
+ discoveredPort: 19161,
61
+ deployment: null,
62
+ stats: null,
63
+ isDefault: true,
64
+ createdAt: Date.now(),
65
+ updatedAt: Date.now(),
66
+ })
67
+
68
+ const resolved = resolveGatewayConfig({ profileId: 'gateway-profile-1' })
69
+ assert.deepEqual(resolved, {
70
+ key: 'profile:gateway-profile-1',
71
+ profileId: 'gateway-profile-1',
72
+ wsUrl: 'ws://127.0.0.1:19161',
73
+ token: 'gateway-token-1',
74
+ })
75
+ })
76
+
77
+ test('resolveGatewayConfig follows an OpenClaw agent route back to its gateway profile credential', () => {
78
+ saveGatewayCredential('openclaw-cred-2', 'gateway-token-2')
79
+ saveGatewayProfile({
80
+ id: 'gateway-profile-2',
81
+ name: 'Gateway 2',
82
+ provider: 'openclaw',
83
+ endpoint: 'http://127.0.0.1:19181/v1',
84
+ wsUrl: 'ws://127.0.0.1:19181',
85
+ credentialId: 'openclaw-cred-2',
86
+ status: 'healthy',
87
+ notes: null,
88
+ tags: ['smoke'],
89
+ lastError: null,
90
+ lastCheckedAt: null,
91
+ lastModelCount: null,
92
+ discoveredHost: '127.0.0.1',
93
+ discoveredPort: 19181,
94
+ deployment: {
95
+ method: 'local',
96
+ managedBy: 'swarmclaw',
97
+ useCase: 'local-dev',
98
+ localInstanceId: 'smoke-app-b',
99
+ localPort: 19181,
100
+ },
101
+ stats: null,
102
+ isDefault: true,
103
+ createdAt: Date.now(),
104
+ updatedAt: Date.now(),
105
+ })
106
+
107
+ const agents = loadAgents({ includeTrashed: true })
108
+ agents['openclaw-agent-1'] = {
109
+ id: 'openclaw-agent-1',
110
+ name: 'Gateway Agent',
111
+ description: '',
112
+ systemPrompt: '',
113
+ provider: 'openclaw',
114
+ model: 'default',
115
+ credentialId: null,
116
+ fallbackCredentialIds: [],
117
+ apiEndpoint: null,
118
+ gatewayProfileId: 'gateway-profile-2',
119
+ createdAt: Date.now(),
120
+ updatedAt: Date.now(),
121
+ } as Agent
122
+ saveAgents(agents)
123
+
124
+ const resolved = resolveGatewayConfig({ agentId: 'openclaw-agent-1' })
125
+ assert.deepEqual(resolved, {
126
+ key: 'profile:gateway-profile-2',
127
+ profileId: 'gateway-profile-2',
128
+ wsUrl: 'ws://127.0.0.1:19181',
129
+ token: 'gateway-token-2',
130
+ })
131
+ })
@@ -1,6 +1,7 @@
1
1
  import { WebSocket } from 'ws'
2
2
  import { randomUUID } from 'crypto'
3
3
  import { wsConnect, buildOpenClawConnectParams } from '../providers/openclaw'
4
+ import { deriveOpenClawWsUrl } from '@/lib/openclaw-endpoint'
4
5
  import { loadAgents, loadCredentials, decryptKey } from './storage'
5
6
  import { notify, notifyWithPayload } from './ws-hub'
6
7
  import { getGatewayProfile, getGatewayProfiles, resolvePrimaryAgentRoute } from './agent-runtime-config'
@@ -71,7 +72,7 @@ export function resolveGatewayConfig(target?: {
71
72
  return {
72
73
  key: `profile:${profile.id}`,
73
74
  profileId: profile.id,
74
- wsUrl: profile.wsUrl ? normalizeWsUrl(profile.wsUrl) : normalizeWsUrl(profile.endpoint),
75
+ wsUrl: profile.wsUrl ? normalizeWsUrl(profile.wsUrl) : deriveOpenClawWsUrl(profile.endpoint),
75
76
  token: resolveTokenForCredential(profile.credentialId),
76
77
  }
77
78
  }
@@ -82,10 +83,15 @@ export function resolveGatewayConfig(target?: {
82
83
  const agent = agents[agentId]
83
84
  const route = resolvePrimaryAgentRoute(agent)
84
85
  if (route?.provider === 'openclaw') {
86
+ const routeProfile = route.gatewayProfileId ? getGatewayProfile(route.gatewayProfileId) : null
85
87
  return {
86
88
  key: route.gatewayProfileId ? `profile:${route.gatewayProfileId}` : `agent:${agentId}`,
87
89
  profileId: route.gatewayProfileId ?? null,
88
- wsUrl: normalizeWsUrl(route.apiEndpoint || 'ws://127.0.0.1:18789'),
90
+ wsUrl: routeProfile?.wsUrl
91
+ ? normalizeWsUrl(routeProfile.wsUrl)
92
+ : route.apiEndpoint
93
+ ? deriveOpenClawWsUrl(route.apiEndpoint)
94
+ : 'ws://127.0.0.1:18789',
89
95
  token: resolveTokenForCredential(route.credentialId),
90
96
  }
91
97
  }
@@ -97,7 +103,7 @@ export function resolveGatewayConfig(target?: {
97
103
  return {
98
104
  key: `profile:${profile.id}`,
99
105
  profileId: profile.id,
100
- wsUrl: profile.wsUrl ? normalizeWsUrl(profile.wsUrl) : normalizeWsUrl(profile.endpoint),
106
+ wsUrl: profile.wsUrl ? normalizeWsUrl(profile.wsUrl) : deriveOpenClawWsUrl(profile.endpoint),
101
107
  token: resolveTokenForCredential(profile.credentialId),
102
108
  }
103
109
  }
@@ -106,7 +112,7 @@ export function resolveGatewayConfig(target?: {
106
112
  for (const agent of Object.values(agents)) {
107
113
  if (agent?.provider !== 'openclaw') continue
108
114
  const wsUrl = agent.apiEndpoint
109
- ? normalizeWsUrl(agent.apiEndpoint)
115
+ ? deriveOpenClawWsUrl(agent.apiEndpoint)
110
116
  : 'ws://127.0.0.1:18789'
111
117
  return {
112
118
  key: `agent:${agent.id}`,
@@ -0,0 +1,35 @@
1
+ import assert from 'node:assert/strict'
2
+ import { test } from 'node:test'
3
+
4
+ import { resolveOpenClawHttpProbeStatus } from './openclaw-health'
5
+
6
+ test('treats missing /v1/models as optional when chat completions works', () => {
7
+ const result = resolveOpenClawHttpProbeStatus({
8
+ modelsStatus: 404,
9
+ chatStatus: 200,
10
+ warnings: ['OpenAI-compatible models endpoint failed: OpenClaw endpoint path is invalid (404).'],
11
+ warningHint: 'Point to the gateway root/ws URL and let SwarmClaw normalize it, or use an explicit /v1 endpoint.',
12
+ })
13
+
14
+ assert.equal(result.httpCompatible, true)
15
+ assert.equal(result.modelsEndpointOptional, true)
16
+ assert.equal(result.warning, undefined)
17
+ assert.equal(result.hint, undefined)
18
+ })
19
+
20
+ test('keeps chat failures as real warnings even when /v1/models is missing', () => {
21
+ const result = resolveOpenClawHttpProbeStatus({
22
+ modelsStatus: 404,
23
+ chatStatus: 500,
24
+ warnings: [
25
+ 'OpenAI-compatible models endpoint failed: OpenClaw endpoint path is invalid (404).',
26
+ 'OpenAI-compatible chat endpoint failed: OpenClaw endpoint returned HTTP 500.',
27
+ ],
28
+ warningHint: 'Ensure this is an OpenAI-compatible chat endpoint exposed by the OpenClaw gateway.',
29
+ })
30
+
31
+ assert.equal(result.httpCompatible, false)
32
+ assert.equal(result.modelsEndpointOptional, false)
33
+ assert.match(result.warning || '', /chat endpoint failed/i)
34
+ assert.equal(result.hint, 'Ensure this is an OpenAI-compatible chat endpoint exposed by the OpenClaw gateway.')
35
+ })
@@ -1,4 +1,5 @@
1
1
  import { deriveOpenClawWsUrl, normalizeOpenClawEndpoint } from '@/lib/openclaw-endpoint'
2
+ import { wsConnect } from '@/lib/providers/openclaw'
2
3
  import { decryptKey, loadCredentials } from './storage'
3
4
 
4
5
  export interface OpenClawHealthInput {
@@ -13,20 +14,49 @@ export interface OpenClawHealthResult {
13
14
  ok: boolean
14
15
  endpoint: string
15
16
  wsUrl: string
17
+ wsConnected: boolean
18
+ httpCompatible: boolean | null
16
19
  authProvided: boolean
17
20
  model: string | null
18
21
  models: string[]
19
22
  modelsStatus: number | null
20
23
  chatStatus: number | null
24
+ message: string
21
25
  completionSample?: string
26
+ warning?: string
22
27
  error?: string
23
28
  hint?: string
24
29
  }
25
30
 
31
+ export interface OpenClawHttpProbeStatus {
32
+ httpCompatible: boolean
33
+ warning?: string
34
+ hint?: string
35
+ modelsEndpointOptional: boolean
36
+ }
37
+
38
+ type JsonRecord = Record<string, unknown>
39
+
26
40
  function normalizeToken(value: unknown): string | null {
27
41
  return typeof value === 'string' && value.trim() ? value.trim() : null
28
42
  }
29
43
 
44
+ function asRecord(value: unknown): JsonRecord | null {
45
+ return value && typeof value === 'object' && !Array.isArray(value) ? value as JsonRecord : null
46
+ }
47
+
48
+ function getErrorName(err: unknown): string | undefined {
49
+ if (err instanceof Error) return err.name
50
+ const record = asRecord(err)
51
+ return typeof record?.name === 'string' ? record.name : undefined
52
+ }
53
+
54
+ function getErrorMessage(err: unknown): string | undefined {
55
+ if (err instanceof Error) return err.message
56
+ const record = asRecord(err)
57
+ return typeof record?.message === 'string' ? record.message : undefined
58
+ }
59
+
30
60
  function resolveCredentialToken(credentialId?: string | null): string | null {
31
61
  const id = normalizeToken(credentialId)
32
62
  if (!id) return null
@@ -40,21 +70,30 @@ function resolveCredentialToken(credentialId?: string | null): string | null {
40
70
  }
41
71
  }
42
72
 
43
- function extractModels(payload: any): string[] {
44
- const models = Array.isArray(payload?.data) ? payload.data : []
73
+ function extractModels(payload: unknown): string[] {
74
+ const payloadRecord = asRecord(payload)
75
+ const models = Array.isArray(payloadRecord?.data) ? payloadRecord.data : []
45
76
  return models
46
- .map((item: any) => (typeof item?.id === 'string' ? item.id.trim() : ''))
77
+ .map((item) => {
78
+ const record = asRecord(item)
79
+ return typeof record?.id === 'string' ? record.id.trim() : ''
80
+ })
47
81
  .filter(Boolean)
48
82
  }
49
83
 
50
- function extractChatText(payload: any): string {
51
- const content = payload?.choices?.[0]?.message?.content
84
+ function extractChatText(payload: unknown): string {
85
+ const payloadRecord = asRecord(payload)
86
+ const choices = Array.isArray(payloadRecord?.choices) ? payloadRecord.choices : []
87
+ const firstChoice = asRecord(choices[0])
88
+ const message = asRecord(firstChoice?.message)
89
+ const content = message?.content
52
90
  if (typeof content === 'string') return content.trim()
53
91
  if (Array.isArray(content)) {
54
92
  return content
55
- .map((block: any) => {
56
- if (typeof block?.text === 'string') return block.text
57
- if (typeof block?.content === 'string') return block.content
93
+ .map((block) => {
94
+ const record = asRecord(block)
95
+ if (typeof record?.text === 'string') return record.text
96
+ if (typeof record?.content === 'string') return record.content
58
97
  return ''
59
98
  })
60
99
  .join(' ')
@@ -87,9 +126,105 @@ function describeHttpError(status: number): { error: string; hint?: string } {
87
126
  }
88
127
  }
89
128
 
129
+ function describeGatewayError(errorCode: string | undefined, message: string): { error: string; hint?: string } {
130
+ if (errorCode === 'AUTH_TOKEN_MISSING') {
131
+ return {
132
+ error: message || 'OpenClaw gateway requires a token.',
133
+ hint: 'Attach an OpenClaw credential or token before running the gateway health check.',
134
+ }
135
+ }
136
+ if (errorCode === 'AUTH_TOKEN_INVALID') {
137
+ return {
138
+ error: message || 'OpenClaw gateway rejected the supplied token.',
139
+ hint: 'Update the saved OpenClaw token or re-pair this gateway with a valid operator token.',
140
+ }
141
+ }
142
+ if (errorCode === 'PAIRING_REQUIRED') {
143
+ return {
144
+ error: message || 'OpenClaw gateway requires device pairing.',
145
+ hint: 'Approve this SwarmClaw device in the OpenClaw gateway before using it from the app.',
146
+ }
147
+ }
148
+ if (errorCode === 'DEVICE_AUTH_INVALID') {
149
+ return {
150
+ error: message || 'OpenClaw gateway rejected the saved device identity.',
151
+ hint: 'Re-pair this SwarmClaw device with the gateway or reset the saved device identity and try again.',
152
+ }
153
+ }
154
+ return {
155
+ error: message || 'Failed to connect to OpenClaw gateway.',
156
+ hint: 'Verify the OpenClaw gateway is running and reachable at this host/port.',
157
+ }
158
+ }
159
+
160
+ function pushIssue(issues: string[], next: string | undefined): void {
161
+ if (typeof next !== 'string') return
162
+ const value = next.trim()
163
+ if (!value) return
164
+ issues.push(value)
165
+ }
166
+
167
+ function isModelsEndpointWarning(issue: string): boolean {
168
+ return issue.startsWith('OpenAI-compatible models endpoint failed:')
169
+ || issue.startsWith('OpenAI-compatible models probe timed out')
170
+ }
171
+
172
+ export function resolveOpenClawHttpProbeStatus(input: {
173
+ modelsStatus: number | null
174
+ chatStatus: number | null
175
+ warnings: string[]
176
+ warningHint?: string
177
+ }): OpenClawHttpProbeStatus {
178
+ const modelsOk = !!input.modelsStatus && input.modelsStatus >= 200 && input.modelsStatus < 300
179
+ const chatOk = !!input.chatStatus && input.chatStatus >= 200 && input.chatStatus < 300
180
+ const modelsEndpointOptional = chatOk && input.modelsStatus === 404
181
+ const filteredWarnings = modelsEndpointOptional
182
+ ? input.warnings.filter((issue) => !isModelsEndpointWarning(issue))
183
+ : input.warnings
184
+ const warning = filteredWarnings.join(' ') || undefined
185
+
186
+ return {
187
+ httpCompatible: chatOk && (modelsOk || modelsEndpointOptional),
188
+ warning,
189
+ hint: warning ? input.warningHint : undefined,
190
+ modelsEndpointOptional,
191
+ }
192
+ }
193
+
194
+ function summarizeOpenClawHealth(input: {
195
+ ok: boolean
196
+ models: string[]
197
+ modelsStatus: number | null
198
+ httpCompatible: boolean | null
199
+ modelsEndpointOptional?: boolean
200
+ warning?: string
201
+ error?: string
202
+ }): string {
203
+ if (!input.ok) return input.error || 'OpenClaw gateway health check failed.'
204
+ const parts = ['Connected to OpenClaw gateway via WebSocket.']
205
+ if (input.modelsStatus && input.modelsStatus >= 200 && input.modelsStatus < 300) {
206
+ parts.push(
207
+ input.models.length > 0
208
+ ? `${input.models.length} model${input.models.length === 1 ? '' : 's'} visible.`
209
+ : 'HTTP models endpoint responded with no models.',
210
+ )
211
+ }
212
+ if (input.httpCompatible === true) {
213
+ if (input.modelsEndpointOptional) {
214
+ parts.push('OpenAI-compatible chat checks passed. This gateway does not advertise `/v1/models`, which is acceptable for OpenClaw.')
215
+ } else {
216
+ parts.push('OpenAI-compatible HTTP checks passed.')
217
+ }
218
+ } else if (input.warning) {
219
+ parts.push(input.warning)
220
+ parts.push('SwarmClaw can still use this gateway over WebSocket.')
221
+ }
222
+ return parts.join(' ')
223
+ }
224
+
90
225
  function createTimeoutError(message: string): Error {
91
- const timeoutErr = new Error(message)
92
- ;(timeoutErr as any).name = 'TimeoutError'
226
+ const timeoutErr = new Error(message) as Error & { name: string }
227
+ timeoutErr.name = 'TimeoutError'
93
228
  return timeoutErr
94
229
  }
95
230
 
@@ -116,7 +251,7 @@ async function withTimeout<T>(promise: Promise<T>, timeoutMs: number, onTimeout?
116
251
  }
117
252
  }
118
253
 
119
- async function fetchJsonWithTimeout(url: string, init: RequestInit, timeoutMs: number): Promise<{ response: Response; body: any }> {
254
+ async function fetchJsonWithTimeout(url: string, init: RequestInit, timeoutMs: number): Promise<{ response: Response; body: unknown }> {
120
255
  const controller = new AbortController()
121
256
  try {
122
257
  const response = await withTimeout(
@@ -131,7 +266,7 @@ async function fetchJsonWithTimeout(url: string, init: RequestInit, timeoutMs: n
131
266
  () => controller.abort(),
132
267
  `Response read timed out after ${timeoutMs}ms`,
133
268
  )
134
- let body: any = {}
269
+ let body: unknown = {}
135
270
  if (text) {
136
271
  try {
137
272
  body = JSON.parse(text)
@@ -140,8 +275,8 @@ async function fetchJsonWithTimeout(url: string, init: RequestInit, timeoutMs: n
140
275
  }
141
276
  }
142
277
  return { response, body }
143
- } catch (err: any) {
144
- if (err?.name === 'AbortError') throw createTimeoutError(`Request timed out after ${timeoutMs}ms`)
278
+ } catch (err: unknown) {
279
+ if (getErrorName(err) === 'AbortError') throw createTimeoutError(`Request timed out after ${timeoutMs}ms`)
145
280
  throw err
146
281
  }
147
282
  }
@@ -163,8 +298,31 @@ export async function probeOpenClawHealth(input: OpenClawHealthInput): Promise<O
163
298
  let modelsStatus: number | null = null
164
299
  let chatStatus: number | null = null
165
300
  let completionSample = ''
166
- let lastError = ''
167
- let lastHint: string | undefined
301
+ let warningHint: string | undefined
302
+ const warnings: string[] = []
303
+
304
+ const wsResult = await wsConnect(wsUrl, token || undefined, true, timeoutMs)
305
+ if (wsResult.ws) {
306
+ try { wsResult.ws.close() } catch { /* noop */ }
307
+ }
308
+ if (!wsResult.ok) {
309
+ const gatewayError = describeGatewayError(wsResult.errorCode, wsResult.message)
310
+ return {
311
+ ok: false,
312
+ endpoint,
313
+ wsUrl,
314
+ wsConnected: false,
315
+ httpCompatible: null,
316
+ authProvided,
317
+ model: null,
318
+ models: [],
319
+ modelsStatus: null,
320
+ chatStatus: null,
321
+ message: gatewayError.error,
322
+ error: gatewayError.error,
323
+ hint: gatewayError.hint,
324
+ }
325
+ }
168
326
 
169
327
  try {
170
328
  const { response: modelsRes, body } = await fetchJsonWithTimeout(`${endpoint}/models`, {
@@ -176,27 +334,17 @@ export async function probeOpenClawHealth(input: OpenClawHealthInput): Promise<O
176
334
  models = extractModels(body)
177
335
  } else {
178
336
  const err = describeHttpError(modelsRes.status)
179
- lastError = err.error
180
- lastHint = err.hint
181
- }
182
- } catch (err: any) {
183
- if (err?.name === 'TimeoutError') {
184
- lastError = `OpenClaw models probe timed out after ${timeoutMs}ms.`
185
- } else {
186
- lastError = err?.message || 'Failed to connect to OpenClaw endpoint.'
187
- }
188
- return {
189
- ok: false,
190
- endpoint,
191
- wsUrl,
192
- authProvided,
193
- model: null,
194
- models: [],
195
- modelsStatus: null,
196
- chatStatus: null,
197
- error: lastError,
198
- hint: 'Verify the OpenClaw gateway is running and reachable at this host/port.',
337
+ pushIssue(warnings, `OpenAI-compatible models endpoint failed: ${err.error}`)
338
+ warningHint = err.hint || warningHint
199
339
  }
340
+ } catch (err: unknown) {
341
+ pushIssue(
342
+ warnings,
343
+ getErrorName(err) === 'TimeoutError'
344
+ ? `OpenAI-compatible models probe timed out after ${timeoutMs}ms.`
345
+ : (getErrorMessage(err) || 'Failed to connect to the OpenAI-compatible models endpoint.'),
346
+ )
347
+ warningHint = 'The gateway is reachable, but the optional HTTP `/v1/models` endpoint did not respond normally.'
200
348
  }
201
349
 
202
350
  const model = normalizeToken(input.model) || models[0] || 'default'
@@ -216,30 +364,50 @@ export async function probeOpenClawHealth(input: OpenClawHealthInput): Promise<O
216
364
  chatStatus = chatRes.status
217
365
  if (!chatRes.ok) {
218
366
  const err = describeHttpError(chatRes.status)
219
- lastError = err.error
220
- lastHint = err.hint || lastHint
367
+ pushIssue(warnings, `OpenAI-compatible chat endpoint failed: ${err.error}`)
368
+ warningHint = err.hint || warningHint
221
369
  } else {
222
370
  completionSample = extractChatText(body).slice(0, 240)
223
371
  }
224
- } catch (err: any) {
225
- if (err?.name === 'TimeoutError') {
226
- lastError = `OpenClaw chat probe timed out after ${timeoutMs}ms.`
227
- } else {
228
- lastError = err?.message || 'OpenClaw chat probe failed.'
229
- }
372
+ } catch (err: unknown) {
373
+ pushIssue(
374
+ warnings,
375
+ getErrorName(err) === 'TimeoutError'
376
+ ? `OpenAI-compatible chat probe timed out after ${timeoutMs}ms.`
377
+ : (getErrorMessage(err) || 'OpenAI-compatible chat probe failed.'),
378
+ )
379
+ warningHint = warningHint || 'The gateway is reachable, but the optional HTTP `/v1/chat/completions` endpoint did not respond normally.'
230
380
  }
231
381
 
382
+ const http = resolveOpenClawHttpProbeStatus({
383
+ modelsStatus,
384
+ chatStatus,
385
+ warnings,
386
+ warningHint,
387
+ })
388
+ const message = summarizeOpenClawHealth({
389
+ ok: true,
390
+ models,
391
+ modelsStatus,
392
+ httpCompatible: http.httpCompatible,
393
+ modelsEndpointOptional: http.modelsEndpointOptional,
394
+ warning: http.warning,
395
+ })
396
+
232
397
  return {
233
- ok: !!chatStatus && chatStatus >= 200 && chatStatus < 300,
398
+ ok: true,
234
399
  endpoint,
235
400
  wsUrl,
401
+ wsConnected: true,
402
+ httpCompatible: http.httpCompatible,
236
403
  authProvided,
237
404
  model,
238
405
  models,
239
406
  modelsStatus,
240
407
  chatStatus,
408
+ message,
241
409
  completionSample: completionSample || undefined,
242
- error: lastError || undefined,
243
- hint: lastHint,
410
+ warning: http.warning,
411
+ hint: http.hint,
244
412
  }
245
413
  }
@@ -115,6 +115,7 @@ async function executeSubTaskViaCli(agent: Agent, task: string, parentSessionId:
115
115
  claudeCode: null,
116
116
  codex: null,
117
117
  opencode: null,
118
+ gemini: null,
118
119
  },
119
120
  messages: [],
120
121
  createdAt: Date.now(),
@@ -398,7 +399,7 @@ export async function executeLangGraphOrchestrator(
398
399
 
399
400
  const checkpointSaver = getCheckpointSaver()
400
401
  const isStrictMode = settings.capabilityPolicyMode === 'strict'
401
- const approvalInterruptsEnabled = isStrictMode && settings.approvalsEnabled !== false
402
+ const approvalInterruptsEnabled = isStrictMode && settings.approvalsEnabled === true
402
403
  const allTools = [delegateTool, storeMemoryTool, searchMemoryTool, getSecretTool, commentOnTaskTool, createTaskTool, markCompleteTool]
403
404
  const llmWithTools = llm.bindTools(allTools)
404
405
  const toolNode = new ToolNode(allTools)
@@ -762,7 +763,7 @@ export async function resumeLangGraphOrchestrator(
762
763
  const checkpointSaver = getCheckpointSaver()
763
764
  const settings = loadSettings()
764
765
  const isStrictMode = settings.capabilityPolicyMode === 'strict'
765
- const approvalInterruptsEnabled = isStrictMode && settings.approvalsEnabled !== false
766
+ const approvalInterruptsEnabled = isStrictMode && settings.approvalsEnabled === true
766
767
 
767
768
  const allTools = [delegateTool, storeMemoryTool, searchMemoryTool, getSecretTool, commentOnTaskTool, createTaskTool, markCompleteTool]
768
769
  const llmWithTools = llm.bindTools(allTools)
@@ -47,6 +47,7 @@ export function createOrchestratorSession(
47
47
  claudeCode: null,
48
48
  codex: null,
49
49
  opencode: null,
50
+ gemini: null,
50
51
  },
51
52
  messages: [] as any[],
52
53
  createdAt: Date.now(),
@@ -294,6 +295,7 @@ async function executeSubTask(
294
295
  claudeCode: null,
295
296
  codex: null,
296
297
  opencode: null,
298
+ gemini: null,
297
299
  },
298
300
  messages: [] as any[],
299
301
  createdAt: Date.now(),