@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
|
@@ -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
|
+
})
|