@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.
- package/README.md +12 -15
- package/next.config.ts +13 -2
- package/package.json +4 -2
- package/src/app/api/agents/[id]/thread/route.ts +9 -0
- package/src/app/api/agents/route.ts +4 -0
- package/src/app/api/agents/thread-route.test.ts +133 -0
- package/src/app/api/approvals/route.test.ts +148 -0
- package/src/app/api/canvas/[sessionId]/route.ts +3 -1
- package/src/app/api/chatrooms/[id]/chat/route.ts +4 -2
- package/src/app/api/chats/[id]/devserver/route.ts +48 -7
- package/src/app/api/chats/[id]/messages/route.ts +42 -18
- package/src/app/api/chats/[id]/route.ts +1 -1
- package/src/app/api/chats/[id]/stop/route.ts +5 -4
- package/src/app/api/chats/route.ts +22 -2
- package/src/app/api/clawhub/install/route.ts +28 -8
- package/src/app/api/connectors/[id]/route.ts +26 -1
- package/src/app/api/external-agents/route.test.ts +165 -0
- package/src/app/api/gateways/[id]/health/route.ts +27 -12
- package/src/app/api/gateways/[id]/route.ts +2 -0
- package/src/app/api/gateways/health-route.test.ts +135 -0
- package/src/app/api/gateways/route.ts +2 -0
- package/src/app/api/mcp-servers/route.test.ts +130 -0
- package/src/app/api/openclaw/deploy/route.ts +38 -5
- package/src/app/api/plugins/install/route.ts +46 -6
- package/src/app/api/plugins/marketplace/route.ts +48 -15
- package/src/app/api/preview-server/route.ts +26 -11
- package/src/app/api/schedules/[id]/run/route.ts +4 -0
- package/src/app/api/schedules/route.test.ts +86 -0
- package/src/app/api/schedules/route.ts +6 -1
- package/src/app/api/setup/check-provider/route.test.ts +19 -0
- package/src/app/api/setup/check-provider/route.ts +40 -10
- package/src/app/api/skills/[id]/route.ts +12 -0
- package/src/app/api/skills/import/route.ts +14 -12
- package/src/app/api/skills/route.ts +13 -1
- package/src/app/api/tasks/[id]/route.ts +10 -1
- package/src/app/api/tasks/import/github/route.test.ts +65 -0
- package/src/app/api/tasks/import/github/route.ts +337 -0
- package/src/app/api/wallets/[id]/approve/route.ts +17 -3
- package/src/app/api/wallets/[id]/route.ts +79 -33
- package/src/app/api/wallets/[id]/send/route.ts +19 -33
- package/src/app/api/wallets/route.ts +78 -61
- package/src/app/api/webhooks/[id]/route.ts +33 -6
- package/src/app/api/webhooks/route.test.ts +272 -0
- package/src/cli/index.js +1 -0
- package/src/cli/spec.js +1 -0
- package/src/components/agents/agent-card.tsx +9 -2
- package/src/components/agents/agent-chat-list.tsx +18 -2
- package/src/components/agents/agent-list.tsx +1 -0
- package/src/components/agents/agent-sheet.tsx +73 -24
- package/src/components/agents/inspector-panel.tsx +41 -0
- package/src/components/canvas/canvas-panel.tsx +236 -65
- package/src/components/chat/chat-card.tsx +36 -13
- package/src/components/chat/chat-header.tsx +44 -16
- package/src/components/chat/chat-list.tsx +28 -4
- package/src/components/chat/checkpoint-timeline.tsx +50 -34
- package/src/components/chat/message-bubble.tsx +208 -145
- package/src/components/chat/message-list.tsx +48 -19
- package/src/components/chatrooms/chatroom-message.tsx +2 -2
- package/src/components/chatrooms/chatroom-sheet.tsx +16 -2
- package/src/components/connectors/connector-health.tsx +1 -1
- package/src/components/connectors/connector-list.tsx +7 -2
- package/src/components/connectors/connector-sheet.tsx +337 -148
- package/src/components/gateways/gateway-sheet.tsx +2 -2
- package/src/components/mcp-servers/mcp-server-list.tsx +26 -5
- package/src/components/mcp-servers/mcp-server-sheet.tsx +19 -2
- package/src/components/openclaw/openclaw-deploy-panel.tsx +269 -21
- package/src/components/plugins/plugin-list.tsx +45 -9
- package/src/components/plugins/plugin-sheet.tsx +55 -7
- package/src/components/providers/provider-list.tsx +2 -1
- package/src/components/providers/provider-sheet.tsx +21 -2
- package/src/components/schedules/schedule-card.tsx +25 -1
- package/src/components/schedules/schedule-sheet.tsx +44 -2
- package/src/components/secrets/secret-sheet.tsx +21 -2
- package/src/components/shared/agent-switch-dialog.tsx +12 -1
- package/src/components/shared/bottom-sheet.tsx +13 -3
- package/src/components/shared/command-palette.tsx +8 -1
- package/src/components/shared/confirm-dialog.tsx +19 -4
- package/src/components/shared/connector-platform-icon.test.ts +28 -0
- package/src/components/shared/connector-platform-icon.tsx +39 -6
- package/src/components/shared/settings/plugin-manager.tsx +29 -6
- package/src/components/shared/settings/section-capability-policy.tsx +7 -3
- package/src/components/skills/skill-list.tsx +25 -0
- package/src/components/skills/skill-sheet.tsx +84 -12
- package/src/components/tasks/approvals-panel.tsx +191 -95
- package/src/components/tasks/task-board.tsx +273 -2
- package/src/components/tasks/task-card.tsx +38 -9
- package/src/components/ui/dialog.tsx +2 -2
- package/src/components/wallets/wallet-approval-dialog.tsx +4 -2
- package/src/components/wallets/wallet-panel.tsx +435 -90
- package/src/components/wallets/wallet-section.tsx +198 -48
- package/src/components/webhooks/webhook-sheet.tsx +22 -2
- package/src/lib/approval-display.ts +20 -0
- package/src/lib/canvas-content.ts +198 -0
- package/src/lib/chat-artifact-summary.ts +165 -0
- package/src/lib/chat-display.test.ts +91 -0
- package/src/lib/chat-display.ts +58 -0
- package/src/lib/chat-streaming-state.test.ts +47 -1
- package/src/lib/chat-streaming-state.ts +42 -0
- package/src/lib/ollama-model.ts +10 -0
- package/src/lib/openclaw-endpoint.test.ts +8 -0
- package/src/lib/openclaw-endpoint.ts +6 -1
- package/src/lib/plugin-install-cors.ts +46 -0
- package/src/lib/plugin-sources.test.ts +43 -0
- package/src/lib/plugin-sources.ts +77 -0
- package/src/lib/providers/ollama.ts +16 -6
- package/src/lib/providers/openclaw.test.ts +54 -0
- package/src/lib/providers/openclaw.ts +127 -11
- package/src/lib/schedule-dedupe-advanced.test.ts +1335 -0
- package/src/lib/schedule-dedupe.test.ts +66 -1
- package/src/lib/schedule-dedupe.ts +169 -12
- package/src/lib/schedule-origin.test.ts +20 -0
- package/src/lib/schedule-origin.ts +15 -0
- package/src/lib/server/__fixtures__/fake-mcp-stdio-server.mjs +27 -0
- package/src/lib/server/agent-availability.ts +16 -0
- package/src/lib/server/agent-runtime-config.ts +12 -4
- package/src/lib/server/agent-thread-session.test.ts +51 -0
- package/src/lib/server/agent-thread-session.ts +7 -0
- package/src/lib/server/approval-match.ts +205 -0
- package/src/lib/server/approvals-auto-approve.test.ts +538 -1
- package/src/lib/server/approvals.ts +214 -1
- package/src/lib/server/assistant-control.test.ts +29 -0
- package/src/lib/server/assistant-control.ts +23 -0
- package/src/lib/server/build-llm.test.ts +79 -0
- package/src/lib/server/build-llm.ts +14 -4
- package/src/lib/server/canvas-content.test.ts +32 -0
- package/src/lib/server/canvas-content.ts +6 -0
- package/src/lib/server/capability-router.test.ts +11 -0
- package/src/lib/server/capability-router.ts +26 -1
- package/src/lib/server/chat-execution-advanced.test.ts +651 -0
- package/src/lib/server/chat-execution-disabled.test.ts +94 -0
- package/src/lib/server/chat-execution-tool-events.test.ts +157 -0
- package/src/lib/server/chat-execution.ts +353 -72
- package/src/lib/server/clawhub-client.test.ts +14 -8
- package/src/lib/server/connectors/manager.test.ts +1147 -0
- package/src/lib/server/connectors/manager.ts +362 -63
- package/src/lib/server/connectors/pairing.ts +26 -5
- package/src/lib/server/connectors/types.ts +2 -0
- package/src/lib/server/connectors/whatsapp.test.ts +134 -0
- package/src/lib/server/connectors/whatsapp.ts +271 -47
- package/src/lib/server/context-manager.ts +6 -1
- package/src/lib/server/daemon-state.ts +1 -1
- package/src/lib/server/data-dir.test.ts +37 -0
- package/src/lib/server/data-dir.ts +20 -1
- package/src/lib/server/delegation-jobs-advanced.test.ts +513 -0
- package/src/lib/server/devserver-launch.test.ts +60 -0
- package/src/lib/server/devserver-launch.ts +85 -0
- package/src/lib/server/elevenlabs.test.ts +189 -1
- package/src/lib/server/elevenlabs.ts +147 -43
- package/src/lib/server/ethereum.ts +590 -0
- package/src/lib/server/eval/agent-regression-advanced.test.ts +302 -0
- package/src/lib/server/eval/agent-regression.test.ts +18 -1
- package/src/lib/server/eval/agent-regression.ts +383 -11
- package/src/lib/server/evm-swap.ts +475 -0
- package/src/lib/server/execution-log.ts +1 -0
- package/src/lib/server/heartbeat-service-timer.test.ts +173 -0
- package/src/lib/server/heartbeat-service.ts +15 -10
- package/src/lib/server/heartbeat-wake.test.ts +112 -0
- package/src/lib/server/heartbeat-wake.ts +338 -57
- package/src/lib/server/main-agent-loop-advanced.test.ts +538 -0
- package/src/lib/server/mcp-client.test.ts +16 -0
- package/src/lib/server/mcp-client.ts +25 -0
- package/src/lib/server/memory-integration.test.ts +719 -0
- package/src/lib/server/memory-policy.test.ts +43 -0
- package/src/lib/server/memory-policy.ts +132 -0
- package/src/lib/server/memory-tiers.test.ts +60 -0
- package/src/lib/server/memory-tiers.ts +16 -0
- package/src/lib/server/ollama-runtime.ts +58 -0
- package/src/lib/server/openclaw-deploy.test.ts +109 -1
- package/src/lib/server/openclaw-deploy.ts +557 -81
- package/src/lib/server/openclaw-gateway.test.ts +131 -0
- package/src/lib/server/openclaw-gateway.ts +10 -4
- package/src/lib/server/openclaw-health.test.ts +35 -0
- package/src/lib/server/openclaw-health.ts +215 -47
- package/src/lib/server/orchestrator-lg.ts +2 -2
- package/src/lib/server/plugins-advanced.test.ts +351 -0
- package/src/lib/server/plugins.ts +205 -5
- package/src/lib/server/queue-advanced.test.ts +528 -0
- package/src/lib/server/queue-followups.test.ts +262 -0
- package/src/lib/server/queue-reconcile.test.ts +128 -0
- package/src/lib/server/queue.ts +293 -61
- package/src/lib/server/scheduler.ts +29 -1
- package/src/lib/server/session-note.test.ts +36 -0
- package/src/lib/server/session-note.ts +42 -0
- package/src/lib/server/session-run-manager.ts +52 -4
- package/src/lib/server/session-tools/canvas.ts +14 -12
- package/src/lib/server/session-tools/connector.test.ts +138 -0
- package/src/lib/server/session-tools/connector.ts +348 -61
- package/src/lib/server/session-tools/context.ts +12 -3
- package/src/lib/server/session-tools/crud.ts +221 -10
- package/src/lib/server/session-tools/delegate-fallback.test.ts +103 -0
- package/src/lib/server/session-tools/delegate.ts +64 -8
- package/src/lib/server/session-tools/discovery-approvals.test.ts +142 -0
- package/src/lib/server/session-tools/discovery.ts +80 -12
- package/src/lib/server/session-tools/file-normalize.test.ts +36 -0
- package/src/lib/server/session-tools/file.ts +43 -4
- package/src/lib/server/session-tools/human-loop.ts +35 -5
- package/src/lib/server/session-tools/index.ts +44 -9
- package/src/lib/server/session-tools/manage-connectors.test.ts +139 -0
- package/src/lib/server/session-tools/manage-schedules-advanced.test.ts +564 -0
- package/src/lib/server/session-tools/manage-schedules.test.ts +283 -0
- package/src/lib/server/session-tools/manage-tasks-advanced.test.ts +852 -0
- package/src/lib/server/session-tools/memory.test.ts +93 -0
- package/src/lib/server/session-tools/memory.ts +546 -79
- package/src/lib/server/session-tools/normalize-tool-args.ts +1 -1
- package/src/lib/server/session-tools/plugin-creator.ts +57 -1
- package/src/lib/server/session-tools/primitive-tools.test.ts +6 -0
- package/src/lib/server/session-tools/schedule.ts +6 -1
- package/src/lib/server/session-tools/shell-normalize.test.ts +25 -1
- package/src/lib/server/session-tools/shell.ts +22 -3
- package/src/lib/server/session-tools/wallet-tool.test.ts +254 -0
- package/src/lib/server/session-tools/wallet.ts +1374 -139
- package/src/lib/server/session-tools/web-inputs.test.ts +162 -1
- package/src/lib/server/session-tools/web.ts +468 -64
- package/src/lib/server/skill-discovery.ts +128 -0
- package/src/lib/server/skill-eligibility.test.ts +84 -0
- package/src/lib/server/skill-eligibility.ts +95 -0
- package/src/lib/server/skill-prompt-budget.test.ts +102 -0
- package/src/lib/server/skill-prompt-budget.ts +125 -0
- package/src/lib/server/skills-normalize.test.ts +54 -0
- package/src/lib/server/skills-normalize.ts +372 -26
- package/src/lib/server/solana.ts +214 -29
- package/src/lib/server/storage.ts +65 -36
- package/src/lib/server/stream-agent-chat.test.ts +419 -9
- package/src/lib/server/stream-agent-chat.ts +887 -83
- package/src/lib/server/system-events.ts +1 -1
- package/src/lib/server/tool-capability-policy-advanced.test.ts +502 -0
- package/src/lib/server/tool-loop-detection.test.ts +105 -0
- package/src/lib/server/tool-loop-detection.ts +260 -0
- package/src/lib/server/tool-planning.ts +4 -2
- package/src/lib/server/wallet-execution.test.ts +198 -0
- package/src/lib/server/wallet-portfolio.test.ts +98 -0
- package/src/lib/server/wallet-portfolio.ts +724 -0
- package/src/lib/server/wallet-service.test.ts +57 -0
- package/src/lib/server/wallet-service.ts +213 -0
- package/src/lib/server/watch-jobs-advanced.test.ts +594 -0
- package/src/lib/server/watch-jobs.ts +17 -2
- package/src/lib/server/workspace-context.ts +111 -0
- package/src/lib/skill-save-payload.test.ts +39 -0
- package/src/lib/skill-save-payload.ts +37 -0
- package/src/lib/tasks.ts +28 -0
- package/src/lib/tool-event-summary.test.ts +30 -0
- package/src/lib/tool-event-summary.ts +37 -0
- package/src/lib/validation/schemas.ts +1 -0
- package/src/lib/wallet-transactions.test.ts +75 -0
- package/src/lib/wallet-transactions.ts +43 -0
- package/src/lib/wallet.test.ts +17 -0
- package/src/lib/wallet.ts +183 -0
- package/src/proxy.test.ts +31 -0
- package/src/proxy.ts +34 -2
- package/src/stores/use-chat-store.ts +15 -1
- 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({
|
|
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
|
|
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:
|
|
150
|
+
command: managedCommand,
|
|
132
151
|
cwd: resolveShellWorkdir(bctx.cwd, workdir),
|
|
133
|
-
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
|
+
})
|