@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
@@ -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
 
@@ -0,0 +1,58 @@
1
+ import assert from 'node:assert/strict'
2
+ import { afterEach, describe, it } from 'node:test'
3
+ import type { ToolBuildContext } from './context'
4
+ import { buildPlatformTools } from './platform'
5
+ import { loadSettings, saveSettings } from '../storage'
6
+
7
+ const originalSettings = loadSettings()
8
+
9
+ afterEach(() => {
10
+ saveSettings(originalSettings)
11
+ })
12
+
13
+ function buildTestContext(hasPlugin: (name: string) => boolean): ToolBuildContext {
14
+ return {
15
+ cwd: process.cwd(),
16
+ ctx: undefined,
17
+ hasPlugin,
18
+ hasTool: hasPlugin,
19
+ cleanupFns: [],
20
+ commandTimeoutMs: 1_000,
21
+ claudeTimeoutMs: 1_000,
22
+ cliProcessTimeoutMs: 1_000,
23
+ persistDelegateResumeId: () => {},
24
+ readStoredDelegateResumeId: () => null,
25
+ resolveCurrentSession: () => null,
26
+ activePlugins: ['manage_platform'],
27
+ }
28
+ }
29
+
30
+ describe('buildPlatformTools', () => {
31
+ it('blocks task resources when task management is disabled', async () => {
32
+ saveSettings({
33
+ ...originalSettings,
34
+ taskManagementEnabled: false,
35
+ projectManagementEnabled: true,
36
+ })
37
+
38
+ const [toolEntry] = buildPlatformTools(buildTestContext((name) => name === 'manage_platform'))
39
+ assert.ok(toolEntry)
40
+
41
+ const result = await toolEntry.invoke({ resource: 'tasks', action: 'list' })
42
+ assert.match(String(result), /task management is disabled/i)
43
+ })
44
+
45
+ it('allows project resources through manage_platform when project management is enabled', async () => {
46
+ saveSettings({
47
+ ...originalSettings,
48
+ taskManagementEnabled: true,
49
+ projectManagementEnabled: true,
50
+ })
51
+
52
+ const [toolEntry] = buildPlatformTools(buildTestContext((name) => name === 'manage_platform'))
53
+ assert.ok(toolEntry)
54
+
55
+ const result = await toolEntry.invoke({ resource: 'projects', action: 'list' })
56
+ assert.doesNotMatch(String(result), /unknown resource|disabled/i)
57
+ })
58
+ })
@@ -2,9 +2,13 @@ import { z } from 'zod'
2
2
  import { tool, type StructuredToolInterface } from '@langchain/core/tools'
3
3
  import { buildCrudTools } from './crud'
4
4
  import type { ToolBuildContext } from './context'
5
- import type { Plugin, PluginHooks } from '@/types'
5
+ import type { Plugin, PluginHooks, Session } from '@/types'
6
6
  import { getPluginManager } from '../plugins'
7
7
  import { normalizeToolInputArgs } from './normalize-tool-args'
8
+ import { loadSettings } from '../storage'
9
+ import { resolveSessionToolPolicy } from '../tool-capability-policy'
10
+ import { loadRuntimeSettings } from '../runtime-settings'
11
+ import { expandPluginIds } from '../tool-aliases'
8
12
 
9
13
  function parsePlatformData(value: unknown): Record<string, unknown> | null {
10
14
  if (!value) return null
@@ -39,6 +43,7 @@ function normalizePlatformResourceName(value: unknown): string | undefined {
39
43
  if (!normalized) return undefined
40
44
  const singularMap: Record<string, string> = {
41
45
  agent: 'agents',
46
+ project: 'projects',
42
47
  task: 'tasks',
43
48
  backlog_task: 'tasks',
44
49
  'backlog-task': 'tasks',
@@ -144,33 +149,69 @@ function uniqueStrings(values: Array<string | undefined>): string[] {
144
149
  return [...new Set(values.filter((value): value is string => Boolean(value)))]
145
150
  }
146
151
 
152
+ function resolvePlatformResourceAccess(toolId: string, bctx: ToolBuildContext): { allowed: boolean; reason: string | null } {
153
+ if (bctx.hasPlugin(toolId)) return { allowed: true, reason: null }
154
+ if (!bctx.hasPlugin('manage_platform')) return { allowed: false, reason: null }
155
+ const settings = loadSettings()
156
+ const decision = resolveSessionToolPolicy(['manage_platform', toolId], settings)
157
+ const allowed = decision.enabledPlugins.includes(toolId)
158
+ const blocked = decision.blockedPlugins.find((entry) => entry.tool === toolId)
159
+ return { allowed, reason: blocked?.reason || null }
160
+ }
161
+
162
+ function buildPlatformContextFromSession(session: Session): ToolBuildContext {
163
+ const runtime = loadRuntimeSettings()
164
+ const sessionPlugins = Array.isArray(session.plugins) ? session.plugins : []
165
+ const legacyTools = Array.isArray(session.tools) ? session.tools : []
166
+ const activePlugins = expandPluginIds([...sessionPlugins, ...legacyTools, 'manage_platform'])
167
+ const activePluginSet = new Set(activePlugins)
168
+ const hasPlugin = (name: string) => activePluginSet.has(name)
169
+
170
+ return {
171
+ cwd: session.cwd || process.cwd(),
172
+ ctx: {
173
+ sessionId: session.id,
174
+ agentId: session.agentId ?? null,
175
+ },
176
+ hasPlugin,
177
+ hasTool: hasPlugin,
178
+ cleanupFns: [],
179
+ commandTimeoutMs: runtime.shellCommandTimeoutMs,
180
+ claudeTimeoutMs: runtime.claudeCodeTimeoutMs,
181
+ cliProcessTimeoutMs: runtime.cliProcessTimeoutMs,
182
+ persistDelegateResumeId: () => {},
183
+ readStoredDelegateResumeId: () => null,
184
+ resolveCurrentSession: () => session,
185
+ activePlugins,
186
+ }
187
+ }
188
+
147
189
  /**
148
190
  * Unified Platform Execution Logic
149
191
  */
150
- async function executePlatformAction(args: any, bctx: any) {
192
+ async function executePlatformAction(args: any, bctx: ToolBuildContext) {
151
193
  const normalized = normalizePlatformActionArgs((args ?? {}) as Record<string, unknown>)
152
194
  const { resource, action, id, data } = normalized
195
+ const resourceName = typeof resource === 'string' ? resource : ''
153
196
 
154
197
  // We reuse the existing CRUD tool logic but expose it via a single tool
155
198
  const crudTools = buildCrudTools({
156
199
  ...bctx,
157
- hasPlugin: (id: string) => [
158
- 'manage_agents',
159
- 'manage_tasks',
160
- 'manage_schedules',
161
- 'manage_skills',
162
- 'manage_documents',
163
- 'manage_secrets',
164
- 'manage_connectors',
165
- 'manage_sessions'
166
- ].includes(id)
200
+ hasPlugin: (toolId: string) => resolvePlatformResourceAccess(toolId, bctx).allowed,
167
201
  })
168
202
 
169
- const targetToolName = `manage_${resource}`
203
+ const targetToolName = `manage_${resourceName}`
170
204
  const targetTool = crudTools.find(t => t.name === targetToolName)
171
205
 
172
206
  if (!targetTool) {
173
- return `Error: Unknown resource type "${resource}". Valid resources: agents, tasks, schedules, skills, documents, secrets, connectors, sessions.`
207
+ const knownResources = ['agents', 'projects', 'tasks', 'schedules', 'skills', 'documents', 'secrets', 'connectors', 'sessions']
208
+ if (resourceName && knownResources.includes(resourceName)) {
209
+ const toolId = `manage_${resourceName}`
210
+ const access = resolvePlatformResourceAccess(toolId, bctx)
211
+ const suffix = access.reason ? ` (${access.reason})` : ''
212
+ return `Error: Resource "${resourceName}" is disabled by app settings or capability policy in this chat${suffix}.`
213
+ }
214
+ return `Error: Unknown resource type "${resourceName || resource}". Valid resources: ${knownResources.join(', ')}.`
174
215
  }
175
216
 
176
217
  // Forward to the specific CRUD tool implementation
@@ -182,10 +223,10 @@ async function executePlatformAction(args: any, bctx: any) {
182
223
  */
183
224
  const PlatformPlugin: Plugin = {
184
225
  name: 'Core Platform',
185
- description: 'Unified management of agents, tasks, schedules, skills, documents, and secrets.',
226
+ description: 'Unified management of agents, projects, tasks, schedules, skills, documents, and secrets.',
186
227
  hooks: {
187
- getCapabilityDescription: () => 'I can create and configure other agents (`manage_agents`), manage tasks (`manage_tasks`), set up schedules (`manage_schedules`), store and search documents (`manage_documents`), register webhooks (`manage_webhooks`), manage reusable skills (`manage_skills`), and store encrypted secrets (`manage_secrets`).',
188
- getOperatingGuidance: () => ['Create/update tasks for long-lived goals to track progress.', 'Use schedules for follow-ups. Check existing schedules before creating new ones.', 'Inspect existing chats before creating duplicates.'],
228
+ getCapabilityDescription: () => 'I can manage durable execution context across agents, projects, tasks, schedules, documents, skills, webhooks, connectors, sessions, and encrypted secrets.',
229
+ getOperatingGuidance: () => ['Use projects to hold longer-lived goals, objectives, and credential requirements.', 'Create/update tasks for long-lived goals to track progress.', 'Use schedules for follow-ups and heartbeat-style check-ins. Check existing schedules before creating new ones.', 'Inspect existing chats before creating duplicates.'],
189
230
  } as PluginHooks,
190
231
  tools: [
191
232
  {
@@ -194,14 +235,14 @@ const PlatformPlugin: Plugin = {
194
235
  parameters: {
195
236
  type: 'object',
196
237
  properties: {
197
- resource: { type: 'string', enum: ['agents', 'tasks', 'schedules', 'skills', 'documents', 'secrets', 'connectors', 'sessions'] },
238
+ resource: { type: 'string', enum: ['agents', 'projects', 'tasks', 'schedules', 'skills', 'documents', 'secrets', 'connectors', 'sessions'] },
198
239
  action: { type: 'string', enum: ['list', 'get', 'create', 'update', 'delete'] },
199
240
  id: { type: 'string' },
200
241
  data: { type: 'string' }
201
242
  },
202
243
  required: ['resource', 'action']
203
244
  },
204
- execute: async (args, context) => executePlatformAction(args, { ...context.session, ctx: context.session })
245
+ execute: async (args, context) => executePlatformAction(args, buildPlatformContextFromSession(context.session))
205
246
  }
206
247
  ]
207
248
  }
@@ -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
+ })