@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.
- package/README.md +12 -14
- 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 +23 -2
- package/src/app/api/clawhub/install/route.ts +28 -8
- package/src/app/api/connectors/[id]/route.ts +46 -3
- package/src/app/api/connectors/route.ts +12 -8
- 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/projects/[id]/route.ts +6 -2
- package/src/app/api/projects/route.ts +4 -3
- 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/secrets/[id]/route.ts +1 -0
- package/src/app/api/secrets/route.ts +2 -1
- package/src/app/api/settings/route.ts +2 -0
- 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 +257 -38
- package/src/components/agents/inspector-panel.tsx +41 -0
- package/src/components/canvas/canvas-panel.tsx +236 -65
- package/src/components/chat/chat-area.tsx +36 -19
- package/src/components/chat/chat-card.tsx +36 -13
- package/src/components/chat/chat-header.tsx +48 -16
- package/src/components/chat/chat-list.tsx +28 -4
- package/src/components/chat/checkpoint-timeline.tsx +50 -34
- package/src/components/chat/delegation-banner.test.ts +14 -1
- package/src/components/chat/delegation-banner.tsx +1 -1
- 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/layout/app-layout.tsx +40 -23
- 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/projects/project-detail.tsx +217 -0
- package/src/components/projects/project-sheet.tsx +176 -4
- 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 +45 -3
- package/src/components/shared/settings/section-voice.tsx +11 -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 +289 -34
- package/src/components/tasks/task-board.tsx +410 -25
- package/src/components/tasks/task-card.tsx +66 -8
- package/src/components/tasks/task-sheet.tsx +16 -4
- 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 +33 -0
- package/src/lib/server/capability-router.ts +80 -19
- 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 +378 -73
- package/src/lib/server/clawhub-client.test.ts +14 -8
- package/src/lib/server/connectors/manager-reconnect.test.ts +47 -0
- package/src/lib/server/connectors/manager.test.ts +1147 -0
- package/src/lib/server/connectors/manager.ts +461 -137
- 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 +84 -47
- 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 +247 -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 +20 -11
- 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/main-agent-loop.test.ts +260 -0
- package/src/lib/server/main-agent-loop.ts +559 -14
- 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 +3 -2
- package/src/lib/server/orchestrator.ts +2 -0
- package/src/lib/server/plugins-advanced.test.ts +351 -0
- package/src/lib/server/plugins.ts +211 -6
- package/src/lib/server/project-context.ts +162 -0
- package/src/lib/server/project-utils.ts +150 -0
- package/src/lib/server/queue-advanced.test.ts +528 -0
- package/src/lib/server/queue-followups.test.ts +409 -2
- package/src/lib/server/queue-reconcile.test.ts +128 -0
- package/src/lib/server/queue.ts +527 -68
- 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 +83 -4
- package/src/lib/server/session-tools/canvas.ts +14 -12
- package/src/lib/server/session-tools/connector-inputs.test.ts +37 -0
- package/src/lib/server/session-tools/connector.test.ts +138 -0
- package/src/lib/server/session-tools/connector.ts +366 -54
- package/src/lib/server/session-tools/context.ts +17 -3
- package/src/lib/server/session-tools/crud.ts +484 -84
- package/src/lib/server/session-tools/delegate-fallback.test.ts +103 -0
- package/src/lib/server/session-tools/delegate-resume.test.ts +50 -0
- package/src/lib/server/session-tools/delegate.ts +102 -10
- 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/manage-tasks.test.ts +114 -0
- package/src/lib/server/session-tools/memory.test.ts +93 -0
- package/src/lib/server/session-tools/memory.ts +554 -75
- package/src/lib/server/session-tools/normalize-tool-args.ts +1 -1
- package/src/lib/server/session-tools/platform-access.test.ts +58 -0
- package/src/lib/server/session-tools/platform.ts +60 -19
- 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 +178 -0
- package/src/lib/server/session-tools/web.ts +621 -70
- 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 +437 -2
- package/src/lib/server/stream-agent-chat.ts +957 -79
- package/src/lib/server/system-events.ts +1 -1
- package/src/lib/server/tool-aliases.ts +2 -0
- package/src/lib/server/tool-capability-policy-advanced.test.ts +502 -0
- package/src/lib/server/tool-capability-policy.test.ts +24 -0
- package/src/lib/server/tool-capability-policy.ts +29 -1
- 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.test.ts +44 -0
- package/src/lib/server/tool-planning.ts +271 -0
- 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-definitions.ts +2 -1
- 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 +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:
|
|
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: (
|
|
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_${
|
|
203
|
+
const targetToolName = `manage_${resourceName}`
|
|
170
204
|
const targetTool = crudTools.find(t => t.name === targetToolName)
|
|
171
205
|
|
|
172
206
|
if (!targetTool) {
|
|
173
|
-
|
|
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
|
|
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,
|
|
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({
|
|
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
|
+
})
|