@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
|
@@ -1,12 +1,32 @@
|
|
|
1
1
|
import { z } from 'zod'
|
|
2
2
|
import { tool, type StructuredToolInterface } from '@langchain/core/tools'
|
|
3
3
|
import type { ToolBuildContext } from './context'
|
|
4
|
-
import { getPluginManager } from '../plugins'
|
|
4
|
+
import { getPluginManager, normalizeMarketplacePluginUrl } from '../plugins'
|
|
5
5
|
import type { Plugin, PluginHooks, ClawHubSkill } from '@/types'
|
|
6
6
|
import { searchClawHub } from '../clawhub-client'
|
|
7
7
|
import { normalizeToolInputArgs } from './normalize-tool-args'
|
|
8
8
|
import { pluginIdMatches } from '../tool-aliases'
|
|
9
9
|
import { loadSessions } from '../storage'
|
|
10
|
+
import { inferPluginPublisherSourceFromUrl } from '@/lib/plugin-sources'
|
|
11
|
+
|
|
12
|
+
function trimString(value: unknown): string {
|
|
13
|
+
return typeof value === 'string' ? value.trim() : ''
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function buildDiscoveryApprovalResumeInput(approval: import('@/types').ApprovalRequest): Record<string, unknown> | null {
|
|
17
|
+
if (approval.category !== 'plugin_install') return null
|
|
18
|
+
const url = trimString(approval.data.url)
|
|
19
|
+
if (!url) return null
|
|
20
|
+
const pluginId = trimString(approval.data.pluginId)
|
|
21
|
+
const reason = trimString(approval.data.reason)
|
|
22
|
+
return {
|
|
23
|
+
action: 'install_request',
|
|
24
|
+
url,
|
|
25
|
+
pluginId: pluginId || undefined,
|
|
26
|
+
reason: reason || `Approved install request for ${url}`,
|
|
27
|
+
approved: true,
|
|
28
|
+
}
|
|
29
|
+
}
|
|
10
30
|
|
|
11
31
|
/**
|
|
12
32
|
* Unified Discovery Logic
|
|
@@ -41,11 +61,15 @@ async function executeDiscoveryAction(args: Record<string, unknown>, bctx?: Tool
|
|
|
41
61
|
case 'list':
|
|
42
62
|
case 'discover': {
|
|
43
63
|
const plugins = manager.listPlugins()
|
|
64
|
+
const currentSession = bctx?.ctx?.sessionId ? loadSessions()[bctx.ctx.sessionId] : null
|
|
65
|
+
const sessionPlugins = currentSession?.plugins || currentSession?.tools || []
|
|
44
66
|
return JSON.stringify(plugins.map(p => ({
|
|
45
67
|
id: p.filename,
|
|
46
68
|
name: p.name,
|
|
47
69
|
description: p.description,
|
|
48
70
|
enabled: p.enabled,
|
|
71
|
+
granted: pluginIdMatches(sessionPlugins, p.filename),
|
|
72
|
+
availableNow: pluginIdMatches(sessionPlugins, p.filename) && !manager.isExplicitlyDisabled(p.filename),
|
|
49
73
|
isBuiltin: !p.filename.endsWith('.js') && !p.filename.endsWith('.mjs')
|
|
50
74
|
})), null, 2)
|
|
51
75
|
}
|
|
@@ -62,6 +86,7 @@ async function executeDiscoveryAction(args: Record<string, unknown>, bctx?: Tool
|
|
|
62
86
|
description: s.description,
|
|
63
87
|
author: s.author,
|
|
64
88
|
source: 'clawhub',
|
|
89
|
+
catalogSource: 'clawhub',
|
|
65
90
|
url: (s as ClawHubSkill & { rawUrl?: string }).rawUrl ?? s.url
|
|
66
91
|
})))
|
|
67
92
|
}
|
|
@@ -71,14 +96,32 @@ async function executeDiscoveryAction(args: Record<string, unknown>, bctx?: Tool
|
|
|
71
96
|
|
|
72
97
|
try {
|
|
73
98
|
console.log('[discovery] Searching SwarmClaw registry...')
|
|
74
|
-
const
|
|
75
|
-
|
|
99
|
+
const registryResults = new Map<string, Record<string, unknown>>()
|
|
100
|
+
const registries = [
|
|
101
|
+
{ url: 'https://swarmclaw.ai/registry/plugins.json', catalogSource: 'swarmclaw-site' },
|
|
102
|
+
{ url: 'https://raw.githubusercontent.com/swarmclawai/swarmforge/main/registry.json', catalogSource: 'swarmforge' },
|
|
103
|
+
] as const
|
|
104
|
+
for (const registry of registries) {
|
|
105
|
+
const scRes = await fetch(registry.url, { signal: AbortSignal.timeout(5000) })
|
|
106
|
+
if (!scRes.ok) continue
|
|
76
107
|
const scPlugins = await scRes.json()
|
|
77
108
|
const filtered = (scPlugins as Record<string, unknown>[]).filter((p: Record<string, unknown>) =>
|
|
78
109
|
!q || (String(p.name || '')).toLowerCase().includes(q.toLowerCase()) || (String(p.description || '')).toLowerCase().includes(q.toLowerCase())
|
|
79
110
|
)
|
|
80
|
-
|
|
111
|
+
for (const p of filtered) {
|
|
112
|
+
const id = String(p.id || p.name || '').trim().toLowerCase().replace(/[^a-z0-9]/g, '_')
|
|
113
|
+
if (!id || registryResults.has(id)) continue
|
|
114
|
+
const url = normalizeMarketplacePluginUrl(String(p.url || ''))
|
|
115
|
+
registryResults.set(id, {
|
|
116
|
+
...p,
|
|
117
|
+
id,
|
|
118
|
+
url,
|
|
119
|
+
source: inferPluginPublisherSourceFromUrl(url) || 'swarmforge',
|
|
120
|
+
catalogSource: registry.catalogSource,
|
|
121
|
+
})
|
|
122
|
+
}
|
|
81
123
|
}
|
|
124
|
+
results.push(...registryResults.values())
|
|
82
125
|
} catch (err: unknown) {
|
|
83
126
|
console.error('[discovery] SC Registry search failed:', err instanceof Error ? err.message : String(err))
|
|
84
127
|
}
|
|
@@ -99,12 +142,12 @@ async function executeDiscoveryAction(args: Record<string, unknown>, bctx?: Tool
|
|
|
99
142
|
const currentSession = allSessions[bctx.ctx.sessionId]
|
|
100
143
|
const grantedTools = currentSession?.plugins || currentSession?.tools || []
|
|
101
144
|
if (currentSession && pluginIdMatches(grantedTools, pluginId)) {
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
145
|
+
return JSON.stringify({
|
|
146
|
+
alreadyGranted: true,
|
|
147
|
+
pluginId,
|
|
148
|
+
message: `You already have access to "${pluginId}". If it was just granted, it will be available on the next agent turn.`,
|
|
149
|
+
})
|
|
150
|
+
}
|
|
108
151
|
}
|
|
109
152
|
const { requestApprovalMaybeAutoApprove } = await import('../approvals')
|
|
110
153
|
const approval = await requestApprovalMaybeAutoApprove({
|
|
@@ -121,7 +164,7 @@ async function executeDiscoveryAction(args: Record<string, unknown>, bctx?: Tool
|
|
|
121
164
|
pluginId,
|
|
122
165
|
toolId: pluginId,
|
|
123
166
|
autoApproved: true,
|
|
124
|
-
message: `Access to "${pluginId}" was auto-approved and granted.
|
|
167
|
+
message: `Access to "${pluginId}" was auto-approved and granted. It will be available on the next agent turn.`,
|
|
125
168
|
})
|
|
126
169
|
}
|
|
127
170
|
return JSON.stringify({
|
|
@@ -182,7 +225,32 @@ async function executeDiscoveryAction(args: Record<string, unknown>, bctx?: Tool
|
|
|
182
225
|
const DiscoveryPlugin: Plugin = {
|
|
183
226
|
name: 'Core Discovery',
|
|
184
227
|
description: 'Discover available plugins, search marketplaces, request access, or suggest new installs.',
|
|
185
|
-
hooks: {
|
|
228
|
+
hooks: {
|
|
229
|
+
getApprovalGuidance: ({ approval, phase, approved }) => {
|
|
230
|
+
if (approval.category !== 'plugin_install') return null
|
|
231
|
+
if (phase === 'request') {
|
|
232
|
+
return [
|
|
233
|
+
'When this approval is granted, continue with `manage_capabilities` for the exact approved install request instead of asking again in prose.',
|
|
234
|
+
'Do not change the approved plugin URL or pluginId unless newer tool evidence proves the approved source is invalid.',
|
|
235
|
+
]
|
|
236
|
+
}
|
|
237
|
+
if (phase === 'connector_reminder') {
|
|
238
|
+
return 'Approving this lets the agent resume the approved plugin install request without repeating marketplace research.'
|
|
239
|
+
}
|
|
240
|
+
if (approved !== true) {
|
|
241
|
+
return 'Do not retry the rejected install request unless the plugin source or requested capability materially changes.'
|
|
242
|
+
}
|
|
243
|
+
const resumeInput = buildDiscoveryApprovalResumeInput(approval)
|
|
244
|
+
const lines = [
|
|
245
|
+
'Resume immediately with `manage_capabilities` for the approved install request.',
|
|
246
|
+
'Do not repeat the same marketplace search or install request once approval has been granted.',
|
|
247
|
+
]
|
|
248
|
+
if (resumeInput) {
|
|
249
|
+
lines.push(`Exact tool input: ${JSON.stringify(resumeInput)}`)
|
|
250
|
+
}
|
|
251
|
+
return lines
|
|
252
|
+
},
|
|
253
|
+
} as PluginHooks,
|
|
186
254
|
tools: [
|
|
187
255
|
{
|
|
188
256
|
name: 'manage_capabilities',
|
|
@@ -83,6 +83,24 @@ describe('normalizeFileArgs', () => {
|
|
|
83
83
|
assert.deepEqual(out.files, [{ name: 'report.md', content: '# report' }])
|
|
84
84
|
})
|
|
85
85
|
|
|
86
|
+
it('parses stringified bulk file arrays from wrapped payloads', () => {
|
|
87
|
+
const out = normalizeFileArgs({
|
|
88
|
+
input: JSON.stringify({
|
|
89
|
+
action: 'write',
|
|
90
|
+
files: JSON.stringify([
|
|
91
|
+
{ path: 'offer-pack/offer-brief.md', content: '# brief' },
|
|
92
|
+
{ path: 'offer-pack/landing-copy.md', content: '# landing' },
|
|
93
|
+
]),
|
|
94
|
+
}),
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
assert.equal(out.action, 'write')
|
|
98
|
+
assert.deepEqual(out.files, [
|
|
99
|
+
{ path: 'offer-pack/offer-brief.md', content: '# brief' },
|
|
100
|
+
{ path: 'offer-pack/landing-copy.md', content: '# landing' },
|
|
101
|
+
])
|
|
102
|
+
})
|
|
103
|
+
|
|
86
104
|
it('treats trailing-slash write targets as directory creation', async () => {
|
|
87
105
|
const cwd = fs.mkdtempSync(path.join(os.tmpdir(), 'file-write-dir-'))
|
|
88
106
|
const out = await executeFileAction({
|
|
@@ -95,4 +113,22 @@ describe('normalizeFileArgs', () => {
|
|
|
95
113
|
assert.equal(fs.statSync(path.join(cwd, 'weather_update')).isDirectory(), true)
|
|
96
114
|
fs.rmSync(cwd, { recursive: true, force: true })
|
|
97
115
|
})
|
|
116
|
+
|
|
117
|
+
it('does not inline binary screenshot data when reading image files', async () => {
|
|
118
|
+
const cwd = fs.mkdtempSync(path.join(os.tmpdir(), 'file-read-binary-'))
|
|
119
|
+
const imagePath = path.join(cwd, 'screenshot-main.png')
|
|
120
|
+
fs.writeFileSync(imagePath, Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x00, 0x01, 0x02, 0x03]))
|
|
121
|
+
|
|
122
|
+
try {
|
|
123
|
+
const out = await executeFileAction({
|
|
124
|
+
action: 'read',
|
|
125
|
+
filePath: 'screenshot-main.png',
|
|
126
|
+
}, { cwd })
|
|
127
|
+
|
|
128
|
+
assert.match(out, /Binary file: screenshot-main\.png/)
|
|
129
|
+
assert.match(out, /Use send_file/)
|
|
130
|
+
} finally {
|
|
131
|
+
fs.rmSync(cwd, { recursive: true, force: true })
|
|
132
|
+
}
|
|
133
|
+
})
|
|
98
134
|
})
|
|
@@ -47,6 +47,25 @@ function getFileEntryContent(entry: Record<string, unknown> | undefined): string
|
|
|
47
47
|
return typeof raw === 'string' ? raw : JSON.stringify(raw)
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
+
function parseFileEntries(value: unknown): Array<Record<string, unknown>> | undefined {
|
|
51
|
+
const candidates = [value]
|
|
52
|
+
if (typeof value === 'string') {
|
|
53
|
+
const trimmed = value.trim()
|
|
54
|
+
if (trimmed.startsWith('[')) {
|
|
55
|
+
try {
|
|
56
|
+
candidates.unshift(JSON.parse(trimmed))
|
|
57
|
+
} catch {
|
|
58
|
+
// ignore malformed JSON payloads and fall back to the raw string
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
for (const candidate of candidates) {
|
|
63
|
+
if (!Array.isArray(candidate)) continue
|
|
64
|
+
return candidate.filter((entry): entry is Record<string, unknown> => !!entry && typeof entry === 'object' && !Array.isArray(entry))
|
|
65
|
+
}
|
|
66
|
+
return undefined
|
|
67
|
+
}
|
|
68
|
+
|
|
50
69
|
function inferFileAction(
|
|
51
70
|
normalized: Record<string, unknown>,
|
|
52
71
|
files: Array<Record<string, unknown>> | undefined,
|
|
@@ -75,9 +94,7 @@ export function normalizeFileArgs(rawArgs: Record<string, unknown>): Record<stri
|
|
|
75
94
|
...normalized,
|
|
76
95
|
...(actionPayload?.value || {}),
|
|
77
96
|
}
|
|
78
|
-
const files =
|
|
79
|
-
? merged.files.filter((entry): entry is Record<string, unknown> => !!entry && typeof entry === 'object' && !Array.isArray(entry))
|
|
80
|
-
: undefined
|
|
97
|
+
const files = parseFileEntries(merged.files)
|
|
81
98
|
|
|
82
99
|
let action = pickNonEmptyString(normalized.action, actionPayload?.action)
|
|
83
100
|
if (!action && Array.isArray(files) && files.length > 0) {
|
|
@@ -131,6 +148,24 @@ function resolveFileToolPath(cwd: string, target: string): string {
|
|
|
131
148
|
}
|
|
132
149
|
}
|
|
133
150
|
|
|
151
|
+
const BINARY_FILE_EXTENSIONS = new Set([
|
|
152
|
+
'.png', '.jpg', '.jpeg', '.gif', '.webp', '.bmp', '.ico', '.svg', '.pdf',
|
|
153
|
+
'.zip', '.gz', '.tar', '.tgz', '.7z', '.rar',
|
|
154
|
+
'.mp3', '.wav', '.ogg', '.m4a', '.mp4', '.mov', '.avi', '.webm',
|
|
155
|
+
'.woff', '.woff2', '.ttf', '.otf',
|
|
156
|
+
'.exe', '.dll', '.so', '.dylib', '.bin',
|
|
157
|
+
])
|
|
158
|
+
|
|
159
|
+
function isLikelyBinaryFile(resolvedPath: string, data: Buffer): boolean {
|
|
160
|
+
const ext = path.extname(resolvedPath).toLowerCase()
|
|
161
|
+
if (BINARY_FILE_EXTENSIONS.has(ext)) return true
|
|
162
|
+
const sample = data.subarray(0, Math.min(data.length, 512))
|
|
163
|
+
for (const byte of sample) {
|
|
164
|
+
if (byte === 0) return true
|
|
165
|
+
}
|
|
166
|
+
return false
|
|
167
|
+
}
|
|
168
|
+
|
|
134
169
|
/**
|
|
135
170
|
* Unified File Execution Logic
|
|
136
171
|
*/
|
|
@@ -154,7 +189,11 @@ export async function executeFileAction(args: Record<string, unknown>, bctx: { c
|
|
|
154
189
|
const target = filePath || getFileEntryPath(files?.[0])
|
|
155
190
|
if (!target) return 'Error: no filePath or path provided.'
|
|
156
191
|
const resolved = resolveFileToolPath(bctx.cwd, target)
|
|
157
|
-
|
|
192
|
+
const data = fs.readFileSync(resolved)
|
|
193
|
+
if (isLikelyBinaryFile(resolved, data)) {
|
|
194
|
+
return `Binary file: ${target} (${data.byteLength} bytes). I did not inline its contents. Use send_file with this path to share it.`
|
|
195
|
+
}
|
|
196
|
+
return truncate(data.toString('utf-8'), MAX_FILE)
|
|
158
197
|
}
|
|
159
198
|
|
|
160
199
|
case 'write': {
|
|
@@ -58,7 +58,12 @@ async function executeHumanLoopAction(args: Record<string, unknown>, bctx: { ses
|
|
|
58
58
|
if (action === 'ack_mailbox') {
|
|
59
59
|
const sessionId = typeof normalized.sessionId === 'string' ? normalized.sessionId : bctx.sessionId
|
|
60
60
|
if (!sessionId) return 'Error: sessionId or current session is required.'
|
|
61
|
-
|
|
61
|
+
let envelopeId = typeof normalized.envelopeId === 'string' ? normalized.envelopeId.trim() : ''
|
|
62
|
+
if (!envelopeId) {
|
|
63
|
+
const newestReply = listMailbox(sessionId, { limit: 50 })
|
|
64
|
+
.find((envelope) => envelope.type === 'human_reply' && envelope.status !== 'ack')
|
|
65
|
+
if (newestReply) envelopeId = newestReply.id
|
|
66
|
+
}
|
|
62
67
|
if (!envelopeId) return 'Error: envelopeId is required.'
|
|
63
68
|
const envelope = ackMailboxEnvelope(sessionId, envelopeId)
|
|
64
69
|
return envelope ? JSON.stringify(envelope) : `Error: mailbox envelope "${envelopeId}" not found.`
|
|
@@ -108,7 +113,10 @@ async function executeHumanLoopAction(args: Record<string, unknown>, bctx: { ses
|
|
|
108
113
|
containsText: typeof normalized.containsText === 'string' ? normalized.containsText : undefined,
|
|
109
114
|
},
|
|
110
115
|
})
|
|
111
|
-
return JSON.stringify(
|
|
116
|
+
return JSON.stringify({
|
|
117
|
+
...job,
|
|
118
|
+
message: 'Durable wait registered. Stop active tool use now and continue on the next agent turn when the human reply arrives.',
|
|
119
|
+
})
|
|
112
120
|
}
|
|
113
121
|
|
|
114
122
|
if (action === 'wait_for_approval') {
|
|
@@ -135,7 +143,10 @@ async function executeHumanLoopAction(args: Record<string, unknown>, bctx: { ses
|
|
|
135
143
|
: ['approved', 'rejected'],
|
|
136
144
|
},
|
|
137
145
|
})
|
|
138
|
-
return JSON.stringify(
|
|
146
|
+
return JSON.stringify({
|
|
147
|
+
...job,
|
|
148
|
+
message: 'Durable approval wait registered. Stop active tool use now and continue on the next agent turn when the approval decision arrives.',
|
|
149
|
+
})
|
|
139
150
|
}
|
|
140
151
|
|
|
141
152
|
if (action === 'status') {
|
|
@@ -150,7 +161,7 @@ async function executeHumanLoopAction(args: Record<string, unknown>, bctx: { ses
|
|
|
150
161
|
const watch = getWatchJob(watchJobId)
|
|
151
162
|
return watch ? JSON.stringify(watch) : `Error: watch job "${watchJobId}" not found.`
|
|
152
163
|
}
|
|
153
|
-
return 'Error: approvalId or watchJobId is required for status.'
|
|
164
|
+
return 'Error: approvalId or watchJobId is required for status. Use list_mailbox to inspect replies, or wait_for_reply / wait_for_approval to create a durable watch first.'
|
|
154
165
|
}
|
|
155
166
|
|
|
156
167
|
return `Error: Unknown action "${action}".`
|
|
@@ -166,11 +177,30 @@ const HumanLoopPlugin: Plugin = {
|
|
|
166
177
|
hooks: {
|
|
167
178
|
getCapabilityDescription: () =>
|
|
168
179
|
'I can request structured human input or explicit approvals with `ask_human`, then pause on durable wait handles until the response arrives.',
|
|
180
|
+
getApprovalGuidance: ({ approval, phase, approved }) => {
|
|
181
|
+
if (approval.category !== 'human_loop') return null
|
|
182
|
+
if (phase === 'request') {
|
|
183
|
+
return [
|
|
184
|
+
'When this approval is decided, continue the blocked task instead of asking the same human approval question again.',
|
|
185
|
+
'Use `ask_human` only for fresh questions, durable waits, or status checks. Do not duplicate the same approval request while it is pending.',
|
|
186
|
+
]
|
|
187
|
+
}
|
|
188
|
+
if (phase === 'connector_reminder') {
|
|
189
|
+
return 'Approving this lets the agent resume the blocked task without repeating the same human-loop request.'
|
|
190
|
+
}
|
|
191
|
+
if (approved !== true) {
|
|
192
|
+
return 'Do not repeat the rejected human-loop approval request unless the question or requested action materially changes.'
|
|
193
|
+
}
|
|
194
|
+
return [
|
|
195
|
+
'Resume the blocked task immediately after approval.',
|
|
196
|
+
'Do not call `ask_human` action `request_approval` again for the same exact question.',
|
|
197
|
+
]
|
|
198
|
+
},
|
|
169
199
|
} as PluginHooks,
|
|
170
200
|
tools: [
|
|
171
201
|
{
|
|
172
202
|
name: 'ask_human',
|
|
173
|
-
description: 'Human-loop tool.
|
|
203
|
+
description: 'Human-loop tool. Use request_input(question, ...) to ask a human, wait_for_reply(correlationId) for durable waiting, list_mailbox to read replies, ack_mailbox(envelopeId) to acknowledge them, and status(approvalId or watchJobId) only when you have an id.',
|
|
174
204
|
parameters: {
|
|
175
205
|
type: 'object',
|
|
176
206
|
properties: {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { z } from 'zod'
|
|
2
2
|
import { tool, type StructuredToolInterface } from '@langchain/core/tools'
|
|
3
3
|
import type { Session } from '@/types'
|
|
4
|
-
import { loadSettings, loadSessions, saveSessions, loadMcpServers } from '../storage'
|
|
4
|
+
import { loadApprovals, loadSettings, loadSessions, saveSessions, loadMcpServers } from '../storage'
|
|
5
5
|
import { loadRuntimeSettings } from '../runtime-settings'
|
|
6
6
|
import { log } from '../logger'
|
|
7
7
|
import { resolveSessionToolPolicy } from '../tool-capability-policy'
|
|
@@ -29,7 +29,6 @@ import { buildCrudTools } from './crud'
|
|
|
29
29
|
import { buildSessionInfoTools } from './session-info'
|
|
30
30
|
import { buildOpenClawNodeTools } from './openclaw-nodes'
|
|
31
31
|
import { buildContextTools } from './context-mgmt'
|
|
32
|
-
import { buildConnectorTools } from './connector'
|
|
33
32
|
import { buildDiscoveryTools } from './discovery'
|
|
34
33
|
import { buildMonitorTools } from './monitor'
|
|
35
34
|
import { buildSampleUITools } from './sample-ui'
|
|
@@ -44,6 +43,7 @@ import { buildDocumentTools } from './document'
|
|
|
44
43
|
import { buildExtractTools } from './extract'
|
|
45
44
|
import { buildTableTools } from './table'
|
|
46
45
|
import { buildCrawlTools } from './crawl'
|
|
46
|
+
import './connector'
|
|
47
47
|
import { normalizeToolInputArgs } from './normalize-tool-args'
|
|
48
48
|
|
|
49
49
|
import { getPluginManager } from '../plugins'
|
|
@@ -52,6 +52,23 @@ import { jsonSchemaToZod } from '../mcp-client'
|
|
|
52
52
|
export type { ToolContext, SessionToolsResult }
|
|
53
53
|
export { sweepOrphanedBrowsers, cleanupSessionBrowser, getActiveBrowserCount, hasActiveBrowser }
|
|
54
54
|
|
|
55
|
+
function approvedToolAccessIds(ctx?: ToolContext): string[] {
|
|
56
|
+
if (!ctx?.sessionId && !ctx?.agentId) return []
|
|
57
|
+
const approvals = loadApprovals()
|
|
58
|
+
const granted = new Set<string>()
|
|
59
|
+
for (const request of Object.values(approvals) as Array<Record<string, unknown>>) {
|
|
60
|
+
if (request?.status !== 'approved' || request?.category !== 'tool_access') continue
|
|
61
|
+
const sessionMatch = ctx.sessionId && request.sessionId === ctx.sessionId
|
|
62
|
+
const agentMatch = ctx.agentId && request.agentId === ctx.agentId
|
|
63
|
+
if (!sessionMatch && !agentMatch) continue
|
|
64
|
+
const toolId = typeof request.data === 'object' && request.data && !Array.isArray(request.data)
|
|
65
|
+
? String((request.data as Record<string, unknown>).toolId || (request.data as Record<string, unknown>).pluginId || '').trim()
|
|
66
|
+
: ''
|
|
67
|
+
if (toolId) granted.add(toolId)
|
|
68
|
+
}
|
|
69
|
+
return [...granted]
|
|
70
|
+
}
|
|
71
|
+
|
|
55
72
|
export async function buildSessionTools(cwd: string, enabledPlugins: string[], ctx?: ToolContext): Promise<SessionToolsResult> {
|
|
56
73
|
const tools: StructuredToolInterface[] = []
|
|
57
74
|
const cleanupFns: (() => Promise<void>)[] = []
|
|
@@ -62,7 +79,26 @@ export async function buildSessionTools(cwd: string, enabledPlugins: string[], c
|
|
|
62
79
|
const claudeTimeoutMs = runtime.claudeCodeTimeoutMs
|
|
63
80
|
const cliProcessTimeoutMs = runtime.cliProcessTimeoutMs
|
|
64
81
|
const appSettings = loadSettings()
|
|
65
|
-
const
|
|
82
|
+
const grantedToolIds = approvedToolAccessIds(ctx)
|
|
83
|
+
const effectiveEnabledPlugins = Array.from(new Set([
|
|
84
|
+
...(Array.isArray(enabledPlugins) ? enabledPlugins : []),
|
|
85
|
+
...grantedToolIds,
|
|
86
|
+
]))
|
|
87
|
+
if (ctx?.sessionId && grantedToolIds.length > 0) {
|
|
88
|
+
const sessions = loadSessions()
|
|
89
|
+
const currentSession = sessions[ctx.sessionId]
|
|
90
|
+
if (currentSession) {
|
|
91
|
+
const currentPlugins = Array.isArray(currentSession.plugins) ? currentSession.plugins : []
|
|
92
|
+
const mergedPlugins = Array.from(new Set([...currentPlugins, ...grantedToolIds]))
|
|
93
|
+
if (mergedPlugins.length !== currentPlugins.length) {
|
|
94
|
+
currentSession.plugins = mergedPlugins
|
|
95
|
+
currentSession.updatedAt = Date.now()
|
|
96
|
+
sessions[ctx.sessionId] = currentSession
|
|
97
|
+
saveSessions(sessions)
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
const toolPolicy = resolveSessionToolPolicy(effectiveEnabledPlugins, appSettings)
|
|
66
102
|
const expandedEnabled = expandPluginIds(toolPolicy.enabledPlugins)
|
|
67
103
|
const expandedBlocked = expandPluginIds(toolPolicy.blockedPlugins.map((entry) => entry.tool))
|
|
68
104
|
const blockedSet = new Set(expandedBlocked)
|
|
@@ -72,7 +108,7 @@ export async function buildSessionTools(cwd: string, enabledPlugins: string[], c
|
|
|
72
108
|
&& !filteredEnabled.includes('process')
|
|
73
109
|
&& !blockedSet.has('process')
|
|
74
110
|
? [...filteredEnabled, 'process']
|
|
75
|
-
: filteredEnabled).filter(
|
|
111
|
+
: filteredEnabled).filter((pluginId) => !pluginManager.isExplicitlyDisabled(pluginId))
|
|
76
112
|
const activePluginSet = new Set(activePlugins)
|
|
77
113
|
const hasPlugin = (pluginName: string) => activePluginSet.has(pluginName)
|
|
78
114
|
/** @deprecated Use hasPlugin */
|
|
@@ -155,7 +191,6 @@ export async function buildSessionTools(cwd: string, enabledPlugins: string[], c
|
|
|
155
191
|
['manage_sessions', buildSessionInfoTools],
|
|
156
192
|
['openclaw_nodes', buildOpenClawNodeTools],
|
|
157
193
|
['context_mgmt', buildContextTools],
|
|
158
|
-
['manage_connectors', buildConnectorTools],
|
|
159
194
|
['discovery', buildDiscoveryTools],
|
|
160
195
|
['monitor', buildMonitorTools],
|
|
161
196
|
['sample_ui', buildSampleUITools],
|
|
@@ -206,13 +241,13 @@ export async function buildSessionTools(cwd: string, enabledPlugins: string[], c
|
|
|
206
241
|
tools.push(
|
|
207
242
|
tool(
|
|
208
243
|
async (args) => {
|
|
209
|
-
if (
|
|
244
|
+
if (pluginManager.isExplicitlyDisabled(entry.pluginId)) {
|
|
210
245
|
throw new Error(`Plugin "${entry.pluginId}" is disabled`)
|
|
211
246
|
}
|
|
212
247
|
try {
|
|
213
248
|
const normalizedArgs = normalizeToolInputArgs((args ?? {}) as Record<string, unknown>)
|
|
214
249
|
const res = await pt.execute(normalizedArgs, {
|
|
215
|
-
session: { ...ctx,
|
|
250
|
+
session: { ...(ctx || {}), ...bctx } as any,
|
|
216
251
|
message: '',
|
|
217
252
|
})
|
|
218
253
|
pluginManager.recordExternalToolSuccess(entry.pluginId)
|
|
@@ -293,14 +328,14 @@ export async function buildSessionTools(cwd: string, enabledPlugins: string[], c
|
|
|
293
328
|
type: 'tool_request',
|
|
294
329
|
toolId,
|
|
295
330
|
autoApproved: true,
|
|
296
|
-
message: `Tool access for "${toolId}" was granted.
|
|
331
|
+
message: `Tool access for "${toolId}" was granted. It will be available on the next agent turn.`,
|
|
297
332
|
})
|
|
298
333
|
}
|
|
299
334
|
return JSON.stringify({
|
|
300
335
|
type: 'tool_request',
|
|
301
336
|
toolId,
|
|
302
337
|
reason,
|
|
303
|
-
message: `Tool access request sent to user for "${toolId}". Once granted,
|
|
338
|
+
message: `Tool access request sent to user for "${toolId}". Once granted, use it on the next agent turn.`,
|
|
304
339
|
})
|
|
305
340
|
},
|
|
306
341
|
{
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import assert from 'node:assert/strict'
|
|
2
|
+
import fs from 'node:fs'
|
|
3
|
+
import os from 'node:os'
|
|
4
|
+
import path from 'node:path'
|
|
5
|
+
import { spawnSync } from 'node:child_process'
|
|
6
|
+
import { describe, it } from 'node:test'
|
|
7
|
+
|
|
8
|
+
const repoRoot = path.resolve(path.dirname(new URL(import.meta.url).pathname), '../../../..')
|
|
9
|
+
|
|
10
|
+
function runWithTempDataDir(script: string) {
|
|
11
|
+
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'swarmclaw-connectors-tool-'))
|
|
12
|
+
try {
|
|
13
|
+
const result = spawnSync(process.execPath, ['--import', 'tsx', '--input-type=module', '--eval', script], {
|
|
14
|
+
cwd: repoRoot,
|
|
15
|
+
env: {
|
|
16
|
+
...process.env,
|
|
17
|
+
DATA_DIR: path.join(tempDir, 'data'),
|
|
18
|
+
WORKSPACE_DIR: path.join(tempDir, 'workspace'),
|
|
19
|
+
},
|
|
20
|
+
encoding: 'utf-8',
|
|
21
|
+
})
|
|
22
|
+
assert.equal(result.status, 0, result.stderr || result.stdout || 'subprocess failed')
|
|
23
|
+
const lines = (result.stdout || '')
|
|
24
|
+
.trim()
|
|
25
|
+
.split('\n')
|
|
26
|
+
.map((line) => line.trim())
|
|
27
|
+
.filter(Boolean)
|
|
28
|
+
const jsonLine = [...lines].reverse().find((line) => line.startsWith('{'))
|
|
29
|
+
return JSON.parse(jsonLine || '{}')
|
|
30
|
+
} finally {
|
|
31
|
+
fs.rmSync(tempDir, { recursive: true, force: true })
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
describe('manage_connectors tool', () => {
|
|
36
|
+
it('drops transient outbound-send args on create', () => {
|
|
37
|
+
const output = runWithTempDataDir(`
|
|
38
|
+
const storageMod = await import('./src/lib/server/storage.ts')
|
|
39
|
+
const crudMod = await import('./src/lib/server/session-tools/crud.ts')
|
|
40
|
+
const storage = storageMod.default || storageMod
|
|
41
|
+
const crud = crudMod.default || crudMod
|
|
42
|
+
|
|
43
|
+
const tools = crud.buildCrudTools({
|
|
44
|
+
cwd: process.env.WORKSPACE_DIR,
|
|
45
|
+
ctx: { sessionId: 'session-1', agentId: 'agent-1', platformAssignScope: 'all' },
|
|
46
|
+
hasPlugin: (name) => name === 'manage_connectors',
|
|
47
|
+
})
|
|
48
|
+
const tool = tools.find((entry) => entry.name === 'manage_connectors')
|
|
49
|
+
await tool.invoke({
|
|
50
|
+
action: 'create',
|
|
51
|
+
data: JSON.stringify({
|
|
52
|
+
name: 'Main WhatsApp',
|
|
53
|
+
platform: 'whatsapp',
|
|
54
|
+
agentId: 'agent-1',
|
|
55
|
+
enabled: true,
|
|
56
|
+
action: 'send_voice_note',
|
|
57
|
+
message: 'hello',
|
|
58
|
+
mediaPath: 'voice_note_gran.mp3',
|
|
59
|
+
connectorId: 'd81cd63b',
|
|
60
|
+
config: {
|
|
61
|
+
taskFollowups: true,
|
|
62
|
+
action: 'send',
|
|
63
|
+
},
|
|
64
|
+
}),
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
const connector = Object.values(storage.loadConnectors())[0]
|
|
68
|
+
console.log(JSON.stringify({ connector }))
|
|
69
|
+
`)
|
|
70
|
+
|
|
71
|
+
assert.equal(output.connector.name, 'Main WhatsApp')
|
|
72
|
+
assert.equal(output.connector.platform, 'whatsapp')
|
|
73
|
+
assert.equal(output.connector.agentId, 'agent-1')
|
|
74
|
+
assert.equal(output.connector.isEnabled, true)
|
|
75
|
+
assert.equal(output.connector.action, undefined)
|
|
76
|
+
assert.equal(output.connector.message, undefined)
|
|
77
|
+
assert.equal(output.connector.mediaPath, undefined)
|
|
78
|
+
assert.equal(output.connector.connectorId, undefined)
|
|
79
|
+
assert.deepEqual(output.connector.config, {
|
|
80
|
+
taskFollowups: 'true',
|
|
81
|
+
action: 'send',
|
|
82
|
+
})
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
it('ignores send-like update payloads instead of mutating connector routing state', () => {
|
|
86
|
+
const output = runWithTempDataDir(`
|
|
87
|
+
const storageMod = await import('./src/lib/server/storage.ts')
|
|
88
|
+
const crudMod = await import('./src/lib/server/session-tools/crud.ts')
|
|
89
|
+
const storage = storageMod.default || storageMod
|
|
90
|
+
const crud = crudMod.default || crudMod
|
|
91
|
+
|
|
92
|
+
const now = Date.now()
|
|
93
|
+
storage.saveConnectors({
|
|
94
|
+
conn_1: {
|
|
95
|
+
id: 'conn_1',
|
|
96
|
+
name: 'Main WhatsApp',
|
|
97
|
+
platform: 'whatsapp',
|
|
98
|
+
agentId: 'e355bf7a',
|
|
99
|
+
credentialId: 'cred-1',
|
|
100
|
+
config: {
|
|
101
|
+
allowFrom: 'me',
|
|
102
|
+
},
|
|
103
|
+
isEnabled: true,
|
|
104
|
+
status: 'running',
|
|
105
|
+
createdAt: now,
|
|
106
|
+
updatedAt: now,
|
|
107
|
+
},
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
const tools = crud.buildCrudTools({
|
|
111
|
+
cwd: process.env.WORKSPACE_DIR,
|
|
112
|
+
ctx: { sessionId: 'session-1', agentId: 'e355bf7a', platformAssignScope: 'all' },
|
|
113
|
+
hasPlugin: (name) => name === 'manage_connectors',
|
|
114
|
+
})
|
|
115
|
+
const tool = tools.find((entry) => entry.name === 'manage_connectors')
|
|
116
|
+
const raw = await tool.invoke({
|
|
117
|
+
action: 'update',
|
|
118
|
+
id: 'conn_1',
|
|
119
|
+
data: JSON.stringify({
|
|
120
|
+
action: 'send',
|
|
121
|
+
message: 'hello there',
|
|
122
|
+
mediaPath: 'voice_note_gran.mp3',
|
|
123
|
+
connectorId: 'conn_1',
|
|
124
|
+
}),
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
const connector = storage.loadConnectors().conn_1
|
|
128
|
+
console.log(JSON.stringify({ raw, connector }))
|
|
129
|
+
`)
|
|
130
|
+
|
|
131
|
+
assert.equal(output.connector.agentId, 'e355bf7a')
|
|
132
|
+
assert.equal(output.connector.credentialId, 'cred-1')
|
|
133
|
+
assert.deepEqual(output.connector.config, { allowFrom: 'me' })
|
|
134
|
+
assert.equal(output.connector.action, undefined)
|
|
135
|
+
assert.equal(output.connector.message, undefined)
|
|
136
|
+
assert.equal(output.connector.mediaPath, undefined)
|
|
137
|
+
assert.equal(output.connector.connectorId, undefined)
|
|
138
|
+
})
|
|
139
|
+
})
|