@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
@@ -43,7 +43,7 @@ export function normalizeToolInputArgs(rawArgs: ToolArgsRecord): ToolArgsRecord
43
43
 
44
44
  for (const [key, value] of Object.entries(current)) {
45
45
  if (value === undefined || value === null) continue
46
- normalized[key] = value
46
+ if (!(key in normalized)) normalized[key] = value
47
47
  }
48
48
  }
49
49
 
@@ -4,7 +4,7 @@ import fs from 'fs'
4
4
  import path from 'path'
5
5
  import { DATA_DIR } from '../data-dir'
6
6
  import type { ToolBuildContext } from './context'
7
- import type { Plugin, PluginHooks } from '@/types'
7
+ import type { ApprovalRequest, Plugin, PluginHooks } from '@/types'
8
8
  import { getPluginManager } from '../plugins'
9
9
  import { normalizeToolInputArgs } from './normalize-tool-args'
10
10
 
@@ -240,6 +240,38 @@ Key rules:
240
240
  }
241
241
  }
242
242
 
243
+ function trimString(value: unknown): string {
244
+ return typeof value === 'string' ? value.trim() : ''
245
+ }
246
+
247
+ function buildPluginCreatorApprovalResumeInput(approval: ApprovalRequest): Record<string, unknown> | null {
248
+ if (approval.category === 'plugin_scaffold') {
249
+ const filename = trimString(approval.data.filename)
250
+ const code = trimString(approval.data.code)
251
+ if (!filename || !code) return null
252
+ return {
253
+ action: 'scaffold',
254
+ filename,
255
+ code,
256
+ packageJson: approval.data.packageJson,
257
+ packageManager: trimString(approval.data.packageManager) || undefined,
258
+ approved: true,
259
+ }
260
+ }
261
+ if (approval.category === 'plugin_install') {
262
+ const filename = trimString(approval.data.filename)
263
+ if (!filename) return null
264
+ return {
265
+ action: 'install_dependencies',
266
+ filename,
267
+ packageJson: approval.data.packageJson,
268
+ packageManager: trimString(approval.data.packageManager) || undefined,
269
+ approved: true,
270
+ }
271
+ }
272
+ return null
273
+ }
274
+
243
275
  /**
244
276
  * Register as a Built-in Plugin
245
277
  */
@@ -253,6 +285,30 @@ const PluginCreatorPlugin: Plugin = {
253
285
  'Put API keys in plugin settings or SwarmClaw secrets instead of hardcoding them in plugin source.',
254
286
  'Call `get_spec` before scaffolding so the plugin follows the current contract.',
255
287
  ],
288
+ getApprovalGuidance: ({ approval, phase, approved }) => {
289
+ if (approval.category !== 'plugin_scaffold' && approval.category !== 'plugin_install') return null
290
+ if (phase === 'request') {
291
+ return [
292
+ 'When this approval is granted, continue with `plugin_creator_tool` for the exact approved action instead of asking again in prose.',
293
+ 'Do not change the approved filename, dependency manifest, or package manager unless tool evidence proves the approved action can no longer execute as approved.',
294
+ ]
295
+ }
296
+ if (phase === 'connector_reminder') {
297
+ return 'Approving this lets the agent resume the exact plugin scaffolding or dependency install step automatically.'
298
+ }
299
+ if (approved !== true) {
300
+ return 'Do not retry the rejected plugin scaffolding or install request unless the exact requested action materially changes.'
301
+ }
302
+ const resumeInput = buildPluginCreatorApprovalResumeInput(approval)
303
+ const lines = [
304
+ 'Resume immediately with `plugin_creator_tool` using the exact approved action.',
305
+ 'Do not re-explain or re-request the same plugin action once approval has been granted.',
306
+ ]
307
+ if (resumeInput) {
308
+ lines.push(`Exact tool input: ${JSON.stringify(resumeInput)}`)
309
+ }
310
+ return lines
311
+ },
256
312
  } as PluginHooks,
257
313
  tools: [
258
314
  {
@@ -176,6 +176,12 @@ describe('primitive tools', () => {
176
176
  watchJobs.triggerMailboxWatchJobs({ sessionId: 'session_1', envelope: replyEnvelope })
177
177
  assert.equal(watchJobs.getWatchJob(replyWatch.id)?.status, 'triggered')
178
178
 
179
+ const ackedReply = JSON.parse(String(await humanTool.invoke({
180
+ action: 'ack_mailbox',
181
+ })))
182
+ assert.equal(ackedReply.id, replyEnvelope.id)
183
+ assert.equal(ackedReply.status, 'ack')
184
+
179
185
  const approval = JSON.parse(String(await humanTool.invoke({
180
186
  action: 'request_approval',
181
187
  title: 'Need signoff',
@@ -20,7 +20,12 @@ async function executeScheduleWake(args: { delayMinutes: number; message: string
20
20
 
21
21
  if (delayMinutes === 0) {
22
22
  enqueueSystemEvent(context.sessionId, `[Scheduled Wake Event / Reminder] ${message}`)
23
- requestHeartbeatNow({ sessionId: context.sessionId, reason: 'scheduled_wake' })
23
+ requestHeartbeatNow({
24
+ sessionId: context.sessionId,
25
+ reason: 'scheduled_wake',
26
+ source: 'schedule_wake',
27
+ resumeMessage: message,
28
+ })
24
29
  return 'Successfully scheduled an immediate wake event.'
25
30
  }
26
31
 
@@ -1,6 +1,6 @@
1
1
  import { describe, it } from 'node:test'
2
2
  import assert from 'node:assert/strict'
3
- import { normalizeShellArgs } from './shell'
3
+ import { normalizeShellArgs, rewriteShellWorkspaceAliases, stripManagedBackgroundSuffix } from './shell'
4
4
 
5
5
  describe('normalizeShellArgs', () => {
6
6
  it('keeps explicit action + command', () => {
@@ -41,3 +41,27 @@ describe('normalizeShellArgs', () => {
41
41
  assert.equal(out.command, 'pwd')
42
42
  })
43
43
  })
44
+
45
+ describe('rewriteShellWorkspaceAliases', () => {
46
+ it('maps /workspace paths inside shell commands to the session cwd', () => {
47
+ const out = rewriteShellWorkspaceAliases('/tmp/agent-workspace', 'cd /workspace/research && ls /workspace/file.md')
48
+ assert.equal(out, 'cd /tmp/agent-workspace/research && ls /tmp/agent-workspace/file.md')
49
+ })
50
+
51
+ it('maps workspace/ relative aliases without touching unrelated text', () => {
52
+ const out = rewriteShellWorkspaceAliases('/tmp/agent-workspace', 'cat workspace/topics/one.md && echo https://example.com/workspace/demo')
53
+ assert.equal(out, 'cat /tmp/agent-workspace/topics/one.md && echo https://example.com/workspace/demo')
54
+ })
55
+ })
56
+
57
+ describe('stripManagedBackgroundSuffix', () => {
58
+ it('removes a trailing ampersand for managed background commands', () => {
59
+ const out = stripManagedBackgroundSuffix('python3 -m http.server 8001 &')
60
+ assert.equal(out, 'python3 -m http.server 8001')
61
+ })
62
+
63
+ it('leaves ordinary commands untouched', () => {
64
+ const out = stripManagedBackgroundSuffix('npm run build')
65
+ assert.equal(out, 'npm run build')
66
+ })
67
+ })
@@ -27,6 +27,20 @@ function resolveShellWorkdir(baseCwd: string, requestedWorkdir?: string): string
27
27
  return safePath(baseCwd, raw)
28
28
  }
29
29
 
30
+ export function rewriteShellWorkspaceAliases(baseCwd: string, command: string): string {
31
+ const cwd = typeof baseCwd === 'string' ? baseCwd.trim() : ''
32
+ if (!cwd) return command
33
+
34
+ let rewritten = command
35
+ rewritten = rewritten.replace(/(^|[\s"'`(=;])\/workspace(?=\/|\b)/g, `$1${cwd}`)
36
+ rewritten = rewritten.replace(/(^|[\s"'`(=;])workspace\//g, `$1${cwd}/`)
37
+ return rewritten
38
+ }
39
+
40
+ export function stripManagedBackgroundSuffix(command: string): string {
41
+ return command.replace(/\s*&\s*$/, '').trim()
42
+ }
43
+
30
44
  function isLikelyServerCommand(command: string): boolean {
31
45
  const cmd = command.trim()
32
46
  return /\bnpm\s+run\s+(dev|start|serve)\b/.test(cmd) ||
@@ -126,11 +140,16 @@ async function executeShellAction(args: Record<string, unknown>, bctx: { cwd: st
126
140
  switch (action) {
127
141
  case 'execute': {
128
142
  if (!command) return 'Error: command or cmd is required for execute action.'
129
- const effectiveBackground = !!background || (typeof command === 'string' && isLikelyServerCommand(command))
143
+ const rewrittenCommand = rewriteShellWorkspaceAliases(bctx.cwd, command)
144
+ const effectiveBackground = !!background || (typeof rewrittenCommand === 'string' && isLikelyServerCommand(rewrittenCommand))
145
+ const managedCommand = effectiveBackground ? stripManagedBackgroundSuffix(rewrittenCommand) : rewrittenCommand
146
+ const envMap = coerceEnvMap(env) || {}
147
+ if (!envMap.WORKSPACE) envMap.WORKSPACE = bctx.cwd
148
+ if (!envMap.SESSION_CWD) envMap.SESSION_CWD = bctx.cwd
130
149
  const result = await startManagedProcess({
131
- command: command,
150
+ command: managedCommand,
132
151
  cwd: resolveShellWorkdir(bctx.cwd, workdir),
133
- env: coerceEnvMap(env),
152
+ env: envMap,
134
153
  agentId: bctx.agentId || null,
135
154
  sessionId: bctx.sessionId || null,
136
155
  background: effectiveBackground,
@@ -0,0 +1,254 @@
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 { after, before, describe, it } from 'node:test'
6
+
7
+ import type { Agent, Session } from '@/types'
8
+
9
+ const originalEnv = {
10
+ DATA_DIR: process.env.DATA_DIR,
11
+ WORKSPACE_DIR: process.env.WORKSPACE_DIR,
12
+ SWARMCLAW_BUILD_MODE: process.env.SWARMCLAW_BUILD_MODE,
13
+ CREDENTIAL_SECRET: process.env.CREDENTIAL_SECRET,
14
+ }
15
+
16
+ let tempDir = ''
17
+ let workspaceDir = ''
18
+ let buildWalletTools: typeof import('./wallet').buildWalletTools
19
+ let createAgentWallet: typeof import('../wallet-service').createAgentWallet
20
+ let storage: typeof import('../storage')
21
+
22
+ function makeAgent(): Agent {
23
+ const now = Date.now()
24
+ return {
25
+ id: 'agent_wallet',
26
+ name: 'Wallet Agent',
27
+ description: 'Tests wallet actions',
28
+ systemPrompt: 'test',
29
+ provider: 'ollama',
30
+ model: 'qwen3.5',
31
+ plugins: ['wallet'],
32
+ createdAt: now,
33
+ updatedAt: now,
34
+ }
35
+ }
36
+
37
+ function makeSession(): Session {
38
+ const now = Date.now()
39
+ return {
40
+ id: 'session_wallet',
41
+ name: 'Wallet Session',
42
+ cwd: workspaceDir,
43
+ user: 'tester',
44
+ provider: 'ollama',
45
+ model: 'qwen3.5',
46
+ claudeSessionId: null,
47
+ messages: [],
48
+ createdAt: now,
49
+ lastActiveAt: now,
50
+ plugins: ['wallet'],
51
+ agentId: 'agent_wallet',
52
+ }
53
+ }
54
+
55
+ function makeBuildContext(session: Session) {
56
+ return {
57
+ cwd: workspaceDir,
58
+ ctx: {
59
+ sessionId: session.id,
60
+ agentId: session.agentId || null,
61
+ },
62
+ hasPlugin: (pluginId: string) => pluginId === 'wallet',
63
+ hasTool: () => true,
64
+ cleanupFns: [],
65
+ commandTimeoutMs: 5000,
66
+ claudeTimeoutMs: 5000,
67
+ cliProcessTimeoutMs: 5000,
68
+ persistDelegateResumeId: () => {},
69
+ readStoredDelegateResumeId: () => null,
70
+ resolveCurrentSession: () => session,
71
+ activePlugins: ['wallet'],
72
+ }
73
+ }
74
+
75
+ before(async () => {
76
+ tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'swarmclaw-wallet-tool-'))
77
+ workspaceDir = path.join(tempDir, 'workspace')
78
+ process.env.DATA_DIR = path.join(tempDir, 'data')
79
+ process.env.WORKSPACE_DIR = workspaceDir
80
+ process.env.SWARMCLAW_BUILD_MODE = '1'
81
+ process.env.CREDENTIAL_SECRET = '22'.repeat(32)
82
+ fs.mkdirSync(process.env.DATA_DIR, { recursive: true })
83
+ fs.mkdirSync(workspaceDir, { recursive: true })
84
+
85
+ ;({ buildWalletTools } = await import('./wallet'))
86
+ ;({ createAgentWallet } = await import('../wallet-service'))
87
+ storage = await import('../storage')
88
+
89
+ storage.saveSettings({
90
+ approvalsEnabled: true,
91
+ approvalAutoApproveCategories: [],
92
+ })
93
+ storage.saveAgents({ agent_wallet: makeAgent() })
94
+ storage.saveSessions({ session_wallet: makeSession() })
95
+ })
96
+
97
+ after(() => {
98
+ if (originalEnv.DATA_DIR === undefined) delete process.env.DATA_DIR
99
+ else process.env.DATA_DIR = originalEnv.DATA_DIR
100
+ if (originalEnv.WORKSPACE_DIR === undefined) delete process.env.WORKSPACE_DIR
101
+ else process.env.WORKSPACE_DIR = originalEnv.WORKSPACE_DIR
102
+ if (originalEnv.SWARMCLAW_BUILD_MODE === undefined) delete process.env.SWARMCLAW_BUILD_MODE
103
+ else process.env.SWARMCLAW_BUILD_MODE = originalEnv.SWARMCLAW_BUILD_MODE
104
+ if (originalEnv.CREDENTIAL_SECRET === undefined) delete process.env.CREDENTIAL_SECRET
105
+ else process.env.CREDENTIAL_SECRET = originalEnv.CREDENTIAL_SECRET
106
+ fs.rmSync(tempDir, { recursive: true, force: true })
107
+ })
108
+
109
+ describe('wallet tool generic execution', () => {
110
+ it('requests approval before signing a message', async () => {
111
+ createAgentWallet({ agentId: 'agent_wallet', chain: 'ethereum' })
112
+ const session = makeSession()
113
+ const [walletTool] = buildWalletTools(makeBuildContext(session))
114
+
115
+ const result = JSON.parse(String(await walletTool.invoke({
116
+ action: 'sign_message',
117
+ chain: 'ethereum',
118
+ message: 'approve me',
119
+ })))
120
+
121
+ assert.equal(result.type, 'plugin_wallet_action_request')
122
+ assert.equal(result.action, 'sign_message')
123
+
124
+ const approvals = storage.loadApprovals()
125
+ const pending = Object.values(approvals).find((approval) => approval.category === 'wallet_action')
126
+ assert.ok(pending)
127
+ assert.equal(pending?.status, 'pending')
128
+ })
129
+
130
+ it('signs messages after approval and encodes contract calls', async () => {
131
+ const session = makeSession()
132
+ const [walletTool] = buildWalletTools(makeBuildContext(session))
133
+
134
+ const bypassAttempt = JSON.parse(String(await walletTool.invoke({
135
+ action: 'sign_message',
136
+ chain: 'ethereum',
137
+ message: 'signed',
138
+ approved: true,
139
+ })))
140
+ assert.match(String(bypassAttempt.error || ''), /approvalId/i)
141
+
142
+ const approvalRequest = JSON.parse(String(await walletTool.invoke({
143
+ action: 'sign_message',
144
+ chain: 'ethereum',
145
+ message: 'signed',
146
+ })))
147
+ assert.equal(approvalRequest.type, 'plugin_wallet_action_request')
148
+
149
+ const approvals = storage.loadApprovals()
150
+ const pending = approvalRequest.approvalId ? approvals[approvalRequest.approvalId] : undefined
151
+ assert.ok(pending)
152
+ storage.upsertApproval(pending!.id, {
153
+ ...pending,
154
+ status: 'approved',
155
+ updatedAt: Date.now(),
156
+ })
157
+
158
+ const signResult = JSON.parse(String(await walletTool.invoke({
159
+ action: 'sign_message',
160
+ chain: 'ethereum',
161
+ message: 'signed',
162
+ approvalId: pending!.id,
163
+ })))
164
+ assert.equal(signResult.status, 'signed')
165
+ assert.equal(signResult.chain, 'ethereum')
166
+ assert.equal(typeof signResult.signature, 'string')
167
+
168
+ const encoded = JSON.parse(String(await walletTool.invoke({
169
+ action: 'encode_contract_call',
170
+ chain: 'ethereum',
171
+ abi: JSON.stringify(['function approve(address spender,uint256 amount)']),
172
+ functionName: 'approve',
173
+ args: JSON.stringify(['0x000000000000000000000000000000000000dEaD', '5']),
174
+ })))
175
+ assert.equal(encoded.status, 'encoded')
176
+ assert.equal(encoded.data.startsWith('0x095ea7b3'), true)
177
+ })
178
+
179
+ it('requests a fresh approval when a stale approvalId is reused for a changed transaction', async () => {
180
+ const session = makeSession()
181
+ const [walletTool] = buildWalletTools(makeBuildContext(session))
182
+
183
+ const firstApproval = JSON.parse(String(await walletTool.invoke({
184
+ action: 'send_transaction',
185
+ chain: 'ethereum',
186
+ network: 'arbitrum',
187
+ toAddress: '0x000000000000000000000000000000000000dEaD',
188
+ data: '0x1234',
189
+ })))
190
+ assert.equal(firstApproval.type, 'plugin_wallet_action_request')
191
+ assert.equal(typeof firstApproval.approvalId, 'string')
192
+
193
+ const approvals = storage.loadApprovals()
194
+ const approved = approvals[firstApproval.approvalId]
195
+ assert.ok(approved)
196
+ storage.upsertApproval(firstApproval.approvalId, {
197
+ ...approved,
198
+ status: 'approved',
199
+ updatedAt: Date.now(),
200
+ })
201
+
202
+ const replacement = JSON.parse(String(await walletTool.invoke({
203
+ action: 'send_transaction',
204
+ chain: 'ethereum',
205
+ network: 'arbitrum',
206
+ toAddress: '0x000000000000000000000000000000000000bEEF',
207
+ data: '0x5678',
208
+ approvalId: firstApproval.approvalId,
209
+ })))
210
+
211
+ assert.equal(replacement.type, 'plugin_wallet_action_request')
212
+ assert.equal(typeof replacement.approvalId, 'string')
213
+ assert.notEqual(replacement.approvalId, firstApproval.approvalId)
214
+ assert.equal(replacement.replacesApprovalId, firstApproval.approvalId)
215
+
216
+ const nextApprovals = storage.loadApprovals()
217
+ assert.equal(nextApprovals[replacement.approvalId]?.status, 'pending')
218
+ })
219
+
220
+ it('requests one resumable approval for a live swap intent when approvals are enabled', async () => {
221
+ const wallets = storage.loadWallets() as Record<string, { id: string; agentId: string; chain: string; publicKey: string }>
222
+ const existingEthWallet = Object.values(wallets).find((wallet) => wallet.agentId === 'agent_wallet' && wallet.chain === 'ethereum')
223
+ const ethWallet = existingEthWallet || createAgentWallet({ agentId: 'agent_wallet', chain: 'ethereum' })
224
+ storage.upsertWallet(ethWallet.id, {
225
+ ...(wallets[ethWallet.id] || ethWallet),
226
+ publicKey: '0x684faBf3F7a39aD667b503E771b86b99a09C8b30',
227
+ updatedAt: Date.now(),
228
+ })
229
+
230
+ const session = makeSession()
231
+ const [walletTool] = buildWalletTools(makeBuildContext(session))
232
+
233
+ const approvalRequest = JSON.parse(String(await walletTool.invoke({
234
+ action: 'swap',
235
+ chain: 'ethereum',
236
+ network: 'arbitrum',
237
+ sellToken: 'USDC',
238
+ buyToken: 'ETH',
239
+ sellAmount: '1',
240
+ })))
241
+
242
+ assert.equal(approvalRequest.type, 'plugin_wallet_action_request')
243
+ assert.equal(approvalRequest.action, 'swap')
244
+
245
+ const approvals = storage.loadApprovals()
246
+ const pending = approvalRequest.approvalId ? approvals[approvalRequest.approvalId] : undefined
247
+ assert.ok(pending)
248
+ assert.equal(pending?.status, 'pending')
249
+ assert.equal(String(pending?.data.amountAtomic), '1000000')
250
+ assert.equal(String(pending?.data.network), 'arbitrum')
251
+ assert.equal(String(pending?.data.sellToken).toLowerCase(), '0xaf88d065e77c8cc2239327c5edb3a432268e5831')
252
+ assert.equal(String(pending?.data.buyToken).toLowerCase(), '0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee')
253
+ })
254
+ })