@swarmclawai/swarmclaw 1.2.8 → 1.3.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 (214) hide show
  1. package/README.md +39 -6
  2. package/package.json +2 -2
  3. package/src/app/agents/[id]/page.tsx +1 -18
  4. package/src/app/api/activity/route.ts +9 -23
  5. package/src/app/api/agents/route.ts +17 -1
  6. package/src/app/api/agents/thread-route.test.ts +0 -1
  7. package/src/app/api/approvals/route.test.ts +6 -22
  8. package/src/app/api/approvals/route.ts +13 -5
  9. package/src/app/api/connectors/route.ts +2 -2
  10. package/src/app/api/credentials/[id]/route.ts +2 -0
  11. package/src/app/api/credentials/route.ts +4 -1
  12. package/src/app/api/goals/[id]/route.ts +28 -0
  13. package/src/app/api/goals/route.ts +33 -0
  14. package/src/app/api/portability/export/route.ts +8 -0
  15. package/src/app/api/portability/import/route.test.ts +80 -0
  16. package/src/app/api/portability/import/route.ts +28 -0
  17. package/src/app/api/protocols/templates/[id]/route.ts +2 -1
  18. package/src/app/api/protocols/templates/route.ts +2 -1
  19. package/src/app/api/settings/route.ts +13 -2
  20. package/src/app/api/wallets/[id]/route.ts +15 -157
  21. package/src/app/api/wallets/generate/route.ts +22 -0
  22. package/src/app/api/wallets/route.test.ts +147 -0
  23. package/src/app/api/wallets/route.ts +13 -95
  24. package/src/app/autonomy/page.tsx +2 -57
  25. package/src/app/home/page.tsx +3 -0
  26. package/src/app/protocols/page.tsx +2 -21
  27. package/src/app/settings/page.tsx +0 -9
  28. package/src/app/wallets/page.tsx +105 -5
  29. package/src/cli/index.js +32 -33
  30. package/src/cli/spec.js +26 -27
  31. package/src/components/agents/agent-sheet.tsx +2 -40
  32. package/src/components/agents/inspector-panel.tsx +0 -83
  33. package/src/components/chat/chat-card.tsx +0 -31
  34. package/src/components/chat/message-bubble.tsx +1 -108
  35. package/src/components/connectors/connector-sheet.tsx +25 -1
  36. package/src/components/layout/sidebar-rail.tsx +6 -10
  37. package/src/components/projects/project-detail.tsx +3 -35
  38. package/src/components/projects/tabs/overview-tab.tsx +3 -59
  39. package/src/components/projects/tabs/work-tab.tsx +7 -77
  40. package/src/components/protocols/structured-session-launcher.tsx +1 -22
  41. package/src/components/shared/connector-platform-icon.tsx +1 -0
  42. package/src/components/tasks/task-card.tsx +4 -34
  43. package/src/components/tasks/task-sheet.tsx +6 -36
  44. package/src/components/wallets/wallet-list.tsx +150 -0
  45. package/src/lib/app/navigation.test.ts +0 -13
  46. package/src/lib/app/navigation.ts +2 -7
  47. package/src/lib/app/view-constants.ts +14 -19
  48. package/src/lib/server/activity/activity-log.ts +16 -1
  49. package/src/lib/server/agents/agent-service.ts +24 -11
  50. package/src/lib/server/agents/agent-thread-session.ts +0 -1
  51. package/src/lib/server/agents/delegation-advisory.test.ts +0 -1
  52. package/src/lib/server/agents/delegation-jobs.test.ts +0 -69
  53. package/src/lib/server/agents/delegation-jobs.ts +0 -25
  54. package/src/lib/server/agents/main-agent-loop.ts +1 -49
  55. package/src/lib/server/agents/subagent-runtime.ts +0 -1
  56. package/src/lib/server/approval-match.ts +14 -85
  57. package/src/lib/server/approvals/approval-hooks.ts +81 -0
  58. package/src/lib/server/approvals.test.ts +6 -6
  59. package/src/lib/server/approvals.ts +11 -6
  60. package/src/lib/server/autonomy/supervisor-reflection.test.ts +0 -1
  61. package/src/lib/server/builtin-extensions.ts +0 -2
  62. package/src/lib/server/capability-router.test.ts +0 -2
  63. package/src/lib/server/chat-execution/chat-execution-tool-events.test.ts +14 -14
  64. package/src/lib/server/chat-execution/chat-execution-types.ts +0 -2
  65. package/src/lib/server/chat-execution/chat-execution-utils.ts +0 -2
  66. package/src/lib/server/chat-execution/chat-streaming-utils.ts +2 -30
  67. package/src/lib/server/chat-execution/chat-turn-finalization.ts +1 -36
  68. package/src/lib/server/chat-execution/chat-turn-preparation.ts +2 -22
  69. package/src/lib/server/chat-execution/iteration-event-handler.ts +0 -24
  70. package/src/lib/server/chat-execution/message-classifier.test.ts +0 -45
  71. package/src/lib/server/chat-execution/message-classifier.ts +1 -16
  72. package/src/lib/server/chat-execution/prompt-builder.test.ts +0 -1
  73. package/src/lib/server/chat-execution/prompt-builder.ts +0 -30
  74. package/src/lib/server/chat-execution/prompt-sections.ts +0 -1
  75. package/src/lib/server/chat-execution/situational-awareness.test.ts +2 -73
  76. package/src/lib/server/chat-execution/situational-awareness.ts +4 -38
  77. package/src/lib/server/chat-execution/stream-agent-chat.test.ts +8 -123
  78. package/src/lib/server/chat-execution/stream-agent-chat.ts +1 -5
  79. package/src/lib/server/chat-execution/stream-continuation.test.ts +4 -52
  80. package/src/lib/server/chat-execution/stream-continuation.ts +6 -48
  81. package/src/lib/server/chatrooms/session-mailbox.ts +0 -10
  82. package/src/lib/server/chats/chat-session-service.ts +3 -5
  83. package/src/lib/server/connectors/connector-inbound.ts +0 -1
  84. package/src/lib/server/connectors/connector-lifecycle.ts +19 -3
  85. package/src/lib/server/connectors/connector-service.ts +39 -9
  86. package/src/lib/server/connectors/swarmdock-bidding.ts +74 -0
  87. package/src/lib/server/connectors/swarmdock-payloads.test.ts +85 -0
  88. package/src/lib/server/connectors/swarmdock-secret.test.ts +128 -0
  89. package/src/lib/server/connectors/swarmdock-secret.ts +152 -0
  90. package/src/lib/server/connectors/swarmdock-tasks.ts +127 -0
  91. package/src/lib/server/connectors/swarmdock.ts +285 -0
  92. package/src/lib/server/execution-brief.test.ts +2 -25
  93. package/src/lib/server/execution-brief.ts +30 -35
  94. package/src/lib/server/execution-engine/task-attempt.ts +0 -1
  95. package/src/lib/server/goals/goal-repository.ts +19 -0
  96. package/src/lib/server/goals/goal-service.ts +143 -0
  97. package/src/lib/server/persistence/storage-context.ts +0 -5
  98. package/src/lib/server/portability/export.ts +109 -0
  99. package/src/lib/server/portability/import.ts +159 -0
  100. package/src/lib/server/protocols/protocol-normalization.ts +0 -4
  101. package/src/lib/server/protocols/protocol-queries.ts +0 -6
  102. package/src/lib/server/protocols/protocol-run-lifecycle.ts +4 -32
  103. package/src/lib/server/protocols/protocol-service.ts +0 -1
  104. package/src/lib/server/protocols/protocol-step-helpers.ts +0 -4
  105. package/src/lib/server/protocols/protocol-step-processors.ts +0 -6
  106. package/src/lib/server/protocols/protocol-swarm.ts +0 -2
  107. package/src/lib/server/protocols/protocol-types.ts +0 -2
  108. package/src/lib/server/provider-health.ts +0 -9
  109. package/src/lib/server/runtime/daemon-state/core.ts +0 -9
  110. package/src/lib/server/runtime/daemon-state.test.ts +0 -35
  111. package/src/lib/server/runtime/heartbeat-service.ts +3 -23
  112. package/src/lib/server/runtime/queue/core.ts +11 -33
  113. package/src/lib/server/runtime/runtime-storage-write-paths.test.ts +6 -6
  114. package/src/lib/server/runtime/scheduler.ts +0 -13
  115. package/src/lib/server/runtime/session-run-manager/drain.ts +0 -24
  116. package/src/lib/server/runtime/session-run-manager/enqueue.ts +0 -1
  117. package/src/lib/server/runtime/session-run-manager/queries.ts +0 -1
  118. package/src/lib/server/runtime/session-run-manager/recovery.ts +0 -1
  119. package/src/lib/server/runtime/session-run-manager.test.ts +0 -28
  120. package/src/lib/server/session-tools/crud.ts +0 -14
  121. package/src/lib/server/session-tools/delegate.ts +0 -4
  122. package/src/lib/server/session-tools/index.ts +0 -4
  123. package/src/lib/server/session-tools/team-context.ts +0 -3
  124. package/src/lib/server/storage-normalization.ts +13 -0
  125. package/src/lib/server/storage.ts +75 -45
  126. package/src/lib/server/tasks/task-checkout.ts +59 -0
  127. package/src/lib/server/tasks/task-lifecycle.ts +2 -0
  128. package/src/lib/server/tasks/task-route-service.ts +4 -26
  129. package/src/lib/server/tasks/task-service.ts +0 -7
  130. package/src/lib/server/tool-aliases.ts +0 -1
  131. package/src/lib/server/tool-capability-policy-advanced.test.ts +4 -4
  132. package/src/lib/server/tool-capability-policy.ts +0 -2
  133. package/src/lib/server/tool-planning.ts +0 -12
  134. package/src/lib/server/universal-tool-access.ts +0 -1
  135. package/src/lib/server/usage/cost-rollup.ts +124 -0
  136. package/src/lib/server/usage/usage-repository.ts +6 -0
  137. package/src/lib/server/wallets/wallet-crypto.ts +33 -0
  138. package/src/lib/server/wallets/wallet-repository.ts +24 -0
  139. package/src/lib/server/wallets/wallet-service.ts +119 -0
  140. package/src/lib/server/working-state/extraction.ts +8 -42
  141. package/src/lib/server/working-state/normalization.ts +10 -103
  142. package/src/lib/server/working-state/service.ts +12 -21
  143. package/src/lib/strip-internal-metadata.test.ts +1 -1
  144. package/src/lib/strip-internal-metadata.ts +1 -1
  145. package/src/lib/tool-definitions.ts +0 -1
  146. package/src/lib/validation/schemas.ts +36 -32
  147. package/src/lib/validation/server-schemas.ts +35 -0
  148. package/src/stores/slices/data-slice.ts +5 -1
  149. package/src/stores/slices/ui-slice.ts +0 -4
  150. package/src/types/agent.ts +10 -84
  151. package/src/types/app-settings.ts +6 -2
  152. package/src/types/approval.ts +3 -2
  153. package/src/types/connector.ts +1 -0
  154. package/src/types/goal.ts +30 -0
  155. package/src/types/index.ts +2 -1
  156. package/src/types/message.ts +0 -1
  157. package/src/types/misc.ts +2 -4
  158. package/src/types/protocol.ts +0 -2
  159. package/src/types/run.ts +0 -3
  160. package/src/types/session.ts +1 -51
  161. package/src/types/swarmdock.ts +29 -0
  162. package/src/types/task.ts +9 -3
  163. package/src/types/working-state.ts +2 -9
  164. package/src/views/settings/section-runtime-loop.tsx +0 -14
  165. package/src/app/api/canvas/[sessionId]/route.ts +0 -35
  166. package/src/app/api/missions/[id]/actions/route.ts +0 -31
  167. package/src/app/api/missions/[id]/events/route.ts +0 -14
  168. package/src/app/api/missions/[id]/route.ts +0 -10
  169. package/src/app/api/missions/route.test.ts +0 -244
  170. package/src/app/api/missions/route.ts +0 -57
  171. package/src/app/api/wallets/[id]/approve/route.ts +0 -79
  172. package/src/app/api/wallets/[id]/balance-history/route.ts +0 -18
  173. package/src/app/api/wallets/[id]/send/route.ts +0 -113
  174. package/src/app/api/wallets/[id]/transactions/route.ts +0 -18
  175. package/src/app/missions/[id]/page.tsx +0 -3
  176. package/src/app/missions/page.tsx +0 -685
  177. package/src/components/canvas/canvas-panel.tsx +0 -267
  178. package/src/components/wallets/wallet-approval-dialog.tsx +0 -107
  179. package/src/components/wallets/wallet-panel.tsx +0 -1010
  180. package/src/components/wallets/wallet-section.tsx +0 -260
  181. package/src/features/missions/queries.ts +0 -23
  182. package/src/lib/canvas-content.test.ts +0 -360
  183. package/src/lib/canvas-content.ts +0 -198
  184. package/src/lib/server/canvas-content.test.ts +0 -32
  185. package/src/lib/server/canvas-content.ts +0 -6
  186. package/src/lib/server/ethereum.ts +0 -591
  187. package/src/lib/server/evm-swap.ts +0 -476
  188. package/src/lib/server/missions/mission-intent.test.ts +0 -63
  189. package/src/lib/server/missions/mission-intent.ts +0 -569
  190. package/src/lib/server/missions/mission-repository.ts +0 -74
  191. package/src/lib/server/missions/mission-service/actions.ts +0 -6
  192. package/src/lib/server/missions/mission-service/bindings.ts +0 -9
  193. package/src/lib/server/missions/mission-service/context.ts +0 -4
  194. package/src/lib/server/missions/mission-service/core.ts +0 -2271
  195. package/src/lib/server/missions/mission-service/queries.ts +0 -12
  196. package/src/lib/server/missions/mission-service/recovery.ts +0 -5
  197. package/src/lib/server/missions/mission-service/ticks.ts +0 -9
  198. package/src/lib/server/missions/mission-service.test.ts +0 -888
  199. package/src/lib/server/missions/mission-service.ts +0 -6
  200. package/src/lib/server/session-tools/canvas.ts +0 -105
  201. package/src/lib/server/session-tools/wallet-tool.test.ts +0 -150
  202. package/src/lib/server/session-tools/wallet.ts +0 -1287
  203. package/src/lib/server/solana.ts +0 -327
  204. package/src/lib/server/wallet/wallet-execution.test.ts +0 -198
  205. package/src/lib/server/wallet/wallet-portfolio.test.ts +0 -98
  206. package/src/lib/server/wallet/wallet-portfolio.ts +0 -772
  207. package/src/lib/server/wallet/wallet-service.test.ts +0 -81
  208. package/src/lib/server/wallet/wallet-service.ts +0 -225
  209. package/src/lib/wallet/wallet-transactions.test.ts +0 -75
  210. package/src/lib/wallet/wallet-transactions.ts +0 -43
  211. package/src/lib/wallet/wallet.test.ts +0 -333
  212. package/src/lib/wallet/wallet.ts +0 -183
  213. package/src/types/mission.ts +0 -185
  214. package/src/views/settings/section-wallets.tsx +0 -35
@@ -19,6 +19,7 @@ import {
19
19
  setReconnectState,
20
20
  } from './reconnect-state'
21
21
  import { connectorRuntimeState, runningConnectors } from './runtime-state'
22
+ import { ensureSwarmdockConnectorCredential } from './swarmdock-secret'
22
23
 
23
24
  const TAG = 'connector-lifecycle'
24
25
 
@@ -70,6 +71,7 @@ export async function getPlatform(platform: string) {
70
71
  case 'googlechat': return (await import('./googlechat')).default
71
72
  case 'matrix': return (await import('./matrix')).default
72
73
  case 'email': return (await import('./email')).default
74
+ case 'swarmdock': return (await import('./swarmdock')).default
73
75
  }
74
76
 
75
77
  // 2. Check Extension-provided connectors
@@ -134,8 +136,9 @@ async function _startConnectorImpl(connectorId: string): Promise<void> {
134
136
  }
135
137
 
136
138
  const connectors = loadConnectors()
137
- const connector = connectors[connectorId] as Connector | undefined
138
- if (!connector) throw new Error('Connector not found')
139
+ const storedConnector = connectors[connectorId] as Connector | undefined
140
+ if (!storedConnector) throw new Error('Connector not found')
141
+ let connector: Connector = storedConnector
139
142
 
140
143
  // Starting a connector expresses durable intent: keep it enabled across
141
144
  // transient failures so daemon recovery and server restarts can retry it.
@@ -148,6 +151,16 @@ async function _startConnectorImpl(connectorId: string): Promise<void> {
148
151
  }
149
152
 
150
153
  try {
154
+ let swarmdockFallbackPrivateKey: string | null = null
155
+ if (connector.platform === 'swarmdock') {
156
+ const prepared = ensureSwarmdockConnectorCredential(connector, {
157
+ allowMigrationFailureFallback: true,
158
+ })
159
+ connector = prepared.connector
160
+ connectors[connectorId] = connector
161
+ swarmdockFallbackPrivateKey = prepared.fallbackPrivateKey
162
+ }
163
+
151
164
  // Resolve bot token from credential
152
165
  let botToken = ''
153
166
  if (connector.credentialId) {
@@ -164,8 +177,11 @@ async function _startConnectorImpl(connectorId: string): Promise<void> {
164
177
  if (!botToken && connector.platform === 'bluebubbles' && connector.config.password) {
165
178
  botToken = connector.config.password
166
179
  }
180
+ if (!botToken && swarmdockFallbackPrivateKey) {
181
+ botToken = swarmdockFallbackPrivateKey
182
+ }
167
183
 
168
- if (!botToken && connector.platform !== 'whatsapp' && connector.platform !== 'openclaw' && connector.platform !== 'signal' && connector.platform !== 'email') {
184
+ if (!botToken && connector.platform !== 'whatsapp' && connector.platform !== 'openclaw' && connector.platform !== 'signal' && connector.platform !== 'email' && connector.platform !== 'swarmdock') {
169
185
  throw new Error('No bot token configured')
170
186
  }
171
187
 
@@ -43,6 +43,11 @@ import type {
43
43
  } from '@/types'
44
44
  import type { ServiceResult } from '@/lib/server/service-result'
45
45
  import type { DaemonConnectorRuntimeState } from '@/lib/server/daemon/types'
46
+ import {
47
+ ensureSwarmdockConnectorCredential,
48
+ prepareSwarmdockConnectorInput,
49
+ redactConnectorSecrets,
50
+ } from './swarmdock-secret'
46
51
 
47
52
  function cloneConnector<T extends Connector>(connector: T): T {
48
53
  return {
@@ -124,35 +129,52 @@ function requireSenderId(body: Record<string, unknown>): string {
124
129
  export async function listConnectorsWithRuntime(): Promise<Record<string, Connector>> {
125
130
  await ensureDaemonProcessRunning('api/connectors:get')
126
131
  const connectors = Object.fromEntries(
127
- Object.entries(loadConnectors()).map(([id, connector]) => [id, cloneConnector(connector)]),
132
+ Object.entries(loadConnectors()).map(([id, connector]) => {
133
+ const prepared = ensureSwarmdockConnectorCredential(connector, {
134
+ allowMigrationFailureFallback: true,
135
+ })
136
+ return [id, cloneConnector(prepared.connector)]
137
+ }),
128
138
  ) as Record<string, Connector>
129
139
  const runtimeByConnector = await listDaemonConnectorRuntime()
130
140
  for (const connector of Object.values(connectors)) {
131
141
  applyRuntimeFields(connector, runtimeByConnector[connector.id] || null)
132
142
  }
133
- return connectors
143
+ return Object.fromEntries(
144
+ Object.entries(connectors).map(([id, connector]) => [id, redactConnectorSecrets(connector)]),
145
+ )
134
146
  }
135
147
 
136
148
  export async function getConnectorWithRuntime(id: string): Promise<Connector | null> {
137
149
  await ensureDaemonProcessRunning('api/connectors/[id]:get')
138
150
  const connector = loadConnector(id)
139
151
  if (!connector) return null
140
- const current = cloneConnector(connector)
141
- return applyRuntimeFields(current, await getDaemonConnectorRuntime(id))
152
+ const prepared = ensureSwarmdockConnectorCredential(connector, {
153
+ allowMigrationFailureFallback: true,
154
+ })
155
+ const current = cloneConnector(prepared.connector)
156
+ return redactConnectorSecrets(applyRuntimeFields(current, await getDaemonConnectorRuntime(id)))
142
157
  }
143
158
 
144
159
  export function createConnector(body: Record<string, unknown>): Connector {
145
160
  const id = genId()
161
+ const rawConfig = body.config && typeof body.config === 'object' && !Array.isArray(body.config)
162
+ ? body.config as Record<string, string>
163
+ : {}
164
+ const prepared = prepareSwarmdockConnectorInput({
165
+ platform: body.platform as Connector['platform'],
166
+ name: (body.name as string) || `${String(body.platform || '')} Connector`,
167
+ credentialId: (body.credentialId as string | null | undefined) || null,
168
+ config: rawConfig,
169
+ })
146
170
  const connector: Connector = {
147
171
  id,
148
172
  name: (body.name as string) || `${String(body.platform || '')} Connector`,
149
173
  platform: body.platform as Connector['platform'],
150
174
  agentId: (body.agentId as string | null | undefined) || null,
151
175
  chatroomId: (body.chatroomId as string | null | undefined) || null,
152
- credentialId: (body.credentialId as string | null | undefined) || null,
153
- config: body.config && typeof body.config === 'object' && !Array.isArray(body.config)
154
- ? body.config as Record<string, string>
155
- : {},
176
+ credentialId: prepared.credentialId,
177
+ config: prepared.config,
156
178
  isEnabled: false,
157
179
  status: 'stopped',
158
180
  lastError: null,
@@ -210,6 +232,14 @@ export async function updateConnectorFromRoute(id: string, body: Record<string,
210
232
  if (body.credentialId !== undefined) next.credentialId = typeof body.credentialId === 'string' || body.credentialId === null ? body.credentialId : next.credentialId
211
233
  if (body.config !== undefined) next.config = body.config && typeof body.config === 'object' && !Array.isArray(body.config) ? body.config as Record<string, string> : next.config
212
234
  if (body.isEnabled !== undefined) next.isEnabled = typeof body.isEnabled === 'boolean' ? body.isEnabled : next.isEnabled
235
+ const prepared = prepareSwarmdockConnectorInput({
236
+ platform: next.platform,
237
+ name: next.name,
238
+ credentialId: next.credentialId || null,
239
+ config: next.config,
240
+ })
241
+ next.credentialId = prepared.credentialId
242
+ next.config = prepared.config
213
243
  persistConnector(next)
214
244
 
215
245
  try {
@@ -235,7 +265,7 @@ export async function updateConnectorFromRoute(id: string, body: Record<string,
235
265
  }
236
266
 
237
267
  notify('connectors')
238
- return serviceOk(await getConnectorWithRuntime(id) || next)
268
+ return serviceOk(await getConnectorWithRuntime(id) || redactConnectorSecrets(next))
239
269
  }
240
270
 
241
271
  export async function deleteConnectorFromRoute(id: string): Promise<ServiceResult<{ ok: true }>> {
@@ -0,0 +1,74 @@
1
+ import { log } from '@/lib/server/logger'
2
+ import type { BidCreateInput } from '@swarmdock/shared'
3
+
4
+ const TAG = 'swarmdock-bid'
5
+
6
+ interface SwarmDockTask {
7
+ id: string
8
+ title: string
9
+ skillRequirements: string[]
10
+ budgetMax: string
11
+ }
12
+
13
+ interface SwarmDockConfig {
14
+ skills: string
15
+ maxBudget: string
16
+ autoDiscover: boolean
17
+ }
18
+
19
+ /**
20
+ * Determine if the agent should auto-bid on a discovered task.
21
+ * Checks skill overlap and budget limits.
22
+ */
23
+ export function shouldAutoBid(task: SwarmDockTask, config: SwarmDockConfig): boolean {
24
+ if (!config.autoDiscover) return false
25
+
26
+ // Check budget
27
+ const maxBudget = BigInt(config.maxBudget || '0')
28
+ if (maxBudget > BigInt(0)) {
29
+ const taskBudget = BigInt(task.budgetMax || '0')
30
+ if (taskBudget > maxBudget) {
31
+ log.debug(TAG, `Skipping "${task.title}" — budget ${task.budgetMax} exceeds max ${config.maxBudget}`)
32
+ return false
33
+ }
34
+ }
35
+
36
+ // Check skill overlap
37
+ const agentSkills = new Set(
38
+ config.skills.split(',').map((s) => s.trim().toLowerCase()).filter(Boolean),
39
+ )
40
+ if (agentSkills.size === 0) return false
41
+
42
+ const hasMatchingSkill = task.skillRequirements.some(
43
+ (req) => agentSkills.has(req.toLowerCase()),
44
+ )
45
+ if (!hasMatchingSkill) {
46
+ log.debug(TAG, `Skipping "${task.title}" — no matching skills`)
47
+ return false
48
+ }
49
+
50
+ return true
51
+ }
52
+
53
+ /**
54
+ * Submit an auto-bid on a SwarmDock task.
55
+ * Uses the task's max budget as the proposed price (simple strategy).
56
+ */
57
+ export async function submitAutoBid(
58
+ client: { tasks: { bid: (taskId: string, input: BidCreateInput) => Promise<unknown> } },
59
+ taskId: string,
60
+ config: SwarmDockConfig,
61
+ ): Promise<void> {
62
+ const agentSkills = config.skills.split(',').map((s) => s.trim()).filter(Boolean)
63
+
64
+ const bid: BidCreateInput = {
65
+ proposedPrice: config.maxBudget || '1000000',
66
+ confidenceScore: 0.8,
67
+ proposal: `SwarmClaw agent with skills: ${agentSkills.join(', ')}. Ready to start immediately.`,
68
+ portfolioRefs: [],
69
+ }
70
+
71
+ await client.tasks.bid(taskId, bid)
72
+
73
+ log.info(TAG, `Auto-bid submitted for task ${taskId}`)
74
+ }
@@ -0,0 +1,85 @@
1
+ import assert from 'node:assert/strict'
2
+ import test from 'node:test'
3
+
4
+ import { submitAutoBid } from '@/lib/server/connectors/swarmdock-bidding'
5
+ import { submitSwarmdockTaskResult } from '@/lib/server/connectors/swarmdock'
6
+
7
+ test('submitAutoBid includes empty portfolio refs for SDK compatibility', async () => {
8
+ const seen: {
9
+ taskId?: string
10
+ bid?: { proposedPrice: string; portfolioRefs: string[] }
11
+ } = {}
12
+
13
+ await submitAutoBid(
14
+ {
15
+ tasks: {
16
+ bid: async (taskId, input) => {
17
+ seen.taskId = taskId
18
+ seen.bid = {
19
+ proposedPrice: input.proposedPrice,
20
+ portfolioRefs: [...input.portfolioRefs],
21
+ }
22
+ },
23
+ },
24
+ },
25
+ 'task-123',
26
+ {
27
+ skills: 'typescript,automation',
28
+ maxBudget: '2500000',
29
+ autoDiscover: true,
30
+ },
31
+ )
32
+
33
+ assert.equal(seen.taskId, 'task-123')
34
+ assert.deepEqual(seen.bid, {
35
+ proposedPrice: '2500000',
36
+ portfolioRefs: [],
37
+ })
38
+ })
39
+
40
+ test('submitSwarmdockTaskResult includes empty files and propagates submit errors', async () => {
41
+ const seen: {
42
+ taskId?: string
43
+ payload?: { files: unknown[]; artifacts: Array<{ type: string; content: string }> }
44
+ } = {}
45
+
46
+ await submitSwarmdockTaskResult(
47
+ {
48
+ tasks: {
49
+ submit: async (taskId, input) => {
50
+ seen.taskId = taskId
51
+ seen.payload = {
52
+ files: [...input.files],
53
+ artifacts: input.artifacts.map((artifact) => ({
54
+ type: artifact.type,
55
+ content: String(artifact.content),
56
+ })),
57
+ }
58
+ },
59
+ },
60
+ },
61
+ 'task-456',
62
+ 'Result body',
63
+ )
64
+
65
+ assert.equal(seen.taskId, 'task-456')
66
+ assert.deepEqual(seen.payload, {
67
+ files: [],
68
+ artifacts: [{ type: 'text/markdown', content: 'Result body' }],
69
+ })
70
+
71
+ await assert.rejects(
72
+ submitSwarmdockTaskResult(
73
+ {
74
+ tasks: {
75
+ submit: async () => {
76
+ throw new Error('submit failed')
77
+ },
78
+ },
79
+ },
80
+ 'task-456',
81
+ 'Result body',
82
+ ),
83
+ /submit failed/,
84
+ )
85
+ })
@@ -0,0 +1,128 @@
1
+ import assert from 'node:assert/strict'
2
+ import fs from 'node:fs'
3
+ import os from 'node:os'
4
+ import path from 'node:path'
5
+ import { spawnSync } from 'node:child_process'
6
+ import test from 'node:test'
7
+
8
+ const repoRoot = path.resolve(path.dirname(new URL(import.meta.url).pathname), '../../../..')
9
+
10
+ function runWithTempDataDir(script: string) {
11
+ const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'swarmclaw-swarmdock-secret-'))
12
+ try {
13
+ const result = spawnSync(process.execPath, ['--import', 'tsx', '--input-type=module', '--eval', script], {
14
+ cwd: repoRoot,
15
+ env: {
16
+ ...process.env,
17
+ CREDENTIAL_SECRET: 'test-credential-secret',
18
+ DATA_DIR: path.join(tempDir, 'data'),
19
+ WORKSPACE_DIR: path.join(tempDir, 'workspace'),
20
+ },
21
+ encoding: 'utf-8',
22
+ })
23
+ assert.equal(result.status, 0, result.stderr || result.stdout || 'subprocess failed')
24
+ const lines = (result.stdout || '')
25
+ .trim()
26
+ .split('\n')
27
+ .map((line) => line.trim())
28
+ .filter(Boolean)
29
+ const jsonLine = [...lines].reverse().find((line) => line.startsWith('{'))
30
+ return JSON.parse(jsonLine || '{}')
31
+ } finally {
32
+ fs.rmSync(tempDir, { recursive: true, force: true })
33
+ }
34
+ }
35
+
36
+ test('createConnector stores SwarmDock private keys as credentials and redacts config output', () => {
37
+ const output = runWithTempDataDir(`
38
+ const storageMod = await import('./src/lib/server/storage')
39
+ const serviceMod = await import('./src/lib/server/connectors/connector-service')
40
+ const secretMod = await import('./src/lib/server/connectors/swarmdock-secret')
41
+ const storage = storageMod.default || storageMod
42
+ const service = serviceMod.default || serviceMod
43
+ const secret = secretMod.default || secretMod
44
+
45
+ const created = service.createConnector({
46
+ name: 'SwarmDock Worker',
47
+ platform: 'swarmdock',
48
+ config: {
49
+ apiUrl: 'https://api.swarmdock.example',
50
+ walletAddress: '0x000000000000000000000000000000000000dEaD',
51
+ privateKey: 'legacy-private-key',
52
+ },
53
+ })
54
+
55
+ const stored = storage.loadConnectors()[created.id]
56
+ const credentials = storage.loadCredentials()
57
+ const credential = stored?.credentialId ? credentials[stored.credentialId] : null
58
+ const redacted = secret.redactConnectorSecrets({
59
+ ...stored,
60
+ config: {
61
+ ...(stored?.config || {}),
62
+ privateKey: 'should-not-leak',
63
+ },
64
+ })
65
+
66
+ console.log(JSON.stringify({
67
+ credentialId: stored?.credentialId || null,
68
+ storedHasPrivateKey: Boolean(stored?.config && Object.prototype.hasOwnProperty.call(stored.config, 'privateKey')),
69
+ credentialProvider: credential?.provider || null,
70
+ credentialName: credential?.name || null,
71
+ redactedHasPrivateKey: Boolean(redacted?.config && Object.prototype.hasOwnProperty.call(redacted.config, 'privateKey')),
72
+ }))
73
+ `)
74
+
75
+ assert.match(String(output.credentialId || ''), /^cred_/)
76
+ assert.equal(output.storedHasPrivateKey, false)
77
+ assert.equal(output.credentialProvider, 'swarmdock')
78
+ assert.match(String(output.credentialName || ''), /SwarmDock Identity Key/)
79
+ assert.equal(output.redactedHasPrivateKey, false)
80
+ })
81
+
82
+ test('ensureSwarmdockConnectorCredential migrates stored legacy config keys into credentials', () => {
83
+ const output = runWithTempDataDir(`
84
+ const storageMod = await import('./src/lib/server/storage')
85
+ const repoMod = await import('./src/lib/server/connectors/connector-repository')
86
+ const secretMod = await import('./src/lib/server/connectors/swarmdock-secret')
87
+ const storage = storageMod.default || storageMod
88
+ const repo = repoMod.default || repoMod
89
+ const secret = secretMod.default || secretMod
90
+
91
+ storage.saveConnectors({
92
+ conn_legacy: {
93
+ id: 'conn_legacy',
94
+ name: 'Legacy SwarmDock',
95
+ platform: 'swarmdock',
96
+ agentId: null,
97
+ chatroomId: null,
98
+ credentialId: null,
99
+ config: {
100
+ walletAddress: '0x000000000000000000000000000000000000dEaD',
101
+ privateKey: 'legacy-private-key',
102
+ },
103
+ isEnabled: false,
104
+ status: 'stopped',
105
+ lastError: null,
106
+ createdAt: 1,
107
+ updatedAt: 1,
108
+ },
109
+ })
110
+
111
+ const prepared = secret.ensureSwarmdockConnectorCredential(repo.loadConnector('conn_legacy'))
112
+ const migrated = repo.loadConnector('conn_legacy')
113
+ const credentials = storage.loadCredentials()
114
+ const credential = migrated?.credentialId ? credentials[migrated.credentialId] : null
115
+
116
+ console.log(JSON.stringify({
117
+ fallbackPrivateKey: prepared.fallbackPrivateKey,
118
+ migratedCredentialId: migrated?.credentialId || null,
119
+ migratedHasPrivateKey: Boolean(migrated?.config && Object.prototype.hasOwnProperty.call(migrated.config, 'privateKey')),
120
+ credentialProvider: credential?.provider || null,
121
+ }))
122
+ `)
123
+
124
+ assert.equal(output.fallbackPrivateKey, null)
125
+ assert.match(String(output.migratedCredentialId || ''), /^cred_/)
126
+ assert.equal(output.migratedHasPrivateKey, false)
127
+ assert.equal(output.credentialProvider, 'swarmdock')
128
+ })
@@ -0,0 +1,152 @@
1
+ import type { Connector } from '@/types'
2
+ import { createCredentialRecord, resolveCredentialSecret } from '@/lib/server/credentials/credential-service'
3
+ import { upsertConnector } from './connector-repository'
4
+ import { notify } from '@/lib/server/ws-hub'
5
+
6
+ export const SWARMMDOCK_CREDENTIAL_PROVIDER = 'swarmdock'
7
+
8
+ function clean(value: unknown): string {
9
+ return typeof value === 'string' ? value.trim() : ''
10
+ }
11
+
12
+ function cloneConfig(config: Record<string, string> | null | undefined): Record<string, string> {
13
+ return config ? { ...config } : {}
14
+ }
15
+
16
+ function getLegacyPrivateKey(config: Record<string, string> | null | undefined): string {
17
+ return clean(config?.privateKey)
18
+ }
19
+
20
+ function stripLegacyPrivateKey(config: Record<string, string> | null | undefined): Record<string, string> {
21
+ const next = cloneConfig(config)
22
+ delete next.privateKey
23
+ return next
24
+ }
25
+
26
+ function buildCredentialName(connectorName: string): string {
27
+ const normalizedName = clean(connectorName)
28
+ return normalizedName ? `${normalizedName} SwarmDock Identity Key` : 'SwarmDock Identity Key'
29
+ }
30
+
31
+ function persistConnectorSecretMigration(
32
+ connector: Connector,
33
+ credentialId: string,
34
+ ): Connector {
35
+ const nextConfig = stripLegacyPrivateKey(connector.config)
36
+ const next: Connector = {
37
+ ...connector,
38
+ credentialId,
39
+ config: nextConfig,
40
+ updatedAt: Date.now(),
41
+ }
42
+ upsertConnector(next.id, next)
43
+ notify('connectors')
44
+ return next
45
+ }
46
+
47
+ export function redactConnectorSecrets<T extends Connector>(connector: T): T {
48
+ if (connector.platform !== 'swarmdock') {
49
+ return {
50
+ ...connector,
51
+ config: cloneConfig(connector.config),
52
+ }
53
+ }
54
+ return {
55
+ ...connector,
56
+ config: stripLegacyPrivateKey(connector.config),
57
+ }
58
+ }
59
+
60
+ export function prepareSwarmdockConnectorInput(params: {
61
+ platform: Connector['platform']
62
+ name: string
63
+ credentialId: string | null
64
+ config: Record<string, string> | null | undefined
65
+ }): {
66
+ credentialId: string | null
67
+ config: Record<string, string>
68
+ } {
69
+ const config = cloneConfig(params.config)
70
+ if (params.platform !== 'swarmdock') {
71
+ return {
72
+ credentialId: clean(params.credentialId) || null,
73
+ config,
74
+ }
75
+ }
76
+
77
+ const credentialId = clean(params.credentialId)
78
+ const legacyPrivateKey = getLegacyPrivateKey(config)
79
+ if (!legacyPrivateKey) {
80
+ return {
81
+ credentialId: credentialId || null,
82
+ config: stripLegacyPrivateKey(config),
83
+ }
84
+ }
85
+
86
+ if (credentialId) {
87
+ return {
88
+ credentialId,
89
+ config: stripLegacyPrivateKey(config),
90
+ }
91
+ }
92
+
93
+ const credential = createCredentialRecord({
94
+ provider: SWARMMDOCK_CREDENTIAL_PROVIDER,
95
+ name: buildCredentialName(params.name),
96
+ apiKey: legacyPrivateKey,
97
+ })
98
+
99
+ return {
100
+ credentialId: credential.id,
101
+ config: stripLegacyPrivateKey(config),
102
+ }
103
+ }
104
+
105
+ export function ensureSwarmdockConnectorCredential(
106
+ connector: Connector,
107
+ options?: { allowMigrationFailureFallback?: boolean },
108
+ ): {
109
+ connector: Connector
110
+ fallbackPrivateKey: string | null
111
+ } {
112
+ if (connector.platform !== 'swarmdock') {
113
+ return { connector, fallbackPrivateKey: null }
114
+ }
115
+
116
+ const legacyPrivateKey = getLegacyPrivateKey(connector.config)
117
+ if (!legacyPrivateKey) {
118
+ return { connector, fallbackPrivateKey: null }
119
+ }
120
+
121
+ const configuredCredentialId = clean(connector.credentialId)
122
+ if (configuredCredentialId) {
123
+ if (resolveCredentialSecret(configuredCredentialId)) {
124
+ return {
125
+ connector: persistConnectorSecretMigration(connector, configuredCredentialId),
126
+ fallbackPrivateKey: null,
127
+ }
128
+ }
129
+ return {
130
+ connector,
131
+ fallbackPrivateKey: legacyPrivateKey,
132
+ }
133
+ }
134
+
135
+ try {
136
+ const credential = createCredentialRecord({
137
+ provider: SWARMMDOCK_CREDENTIAL_PROVIDER,
138
+ name: buildCredentialName(connector.name),
139
+ apiKey: legacyPrivateKey,
140
+ })
141
+ return {
142
+ connector: persistConnectorSecretMigration(connector, credential.id),
143
+ fallbackPrivateKey: null,
144
+ }
145
+ } catch (error) {
146
+ if (!options?.allowMigrationFailureFallback) throw error
147
+ return {
148
+ connector,
149
+ fallbackPrivateKey: legacyPrivateKey,
150
+ }
151
+ }
152
+ }