@swarmclawai/swarmclaw 0.7.1 → 0.7.3
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 +155 -150
- package/package.json +1 -1
- package/src/app/api/agents/[id]/route.ts +26 -0
- package/src/app/api/agents/[id]/thread/route.ts +37 -9
- package/src/app/api/agents/route.ts +13 -2
- package/src/app/api/auth/route.ts +76 -7
- package/src/app/api/chatrooms/[id]/chat/route.ts +7 -2
- package/src/app/api/{sessions → chats}/[id]/browser/route.ts +5 -1
- package/src/app/api/{sessions → chats}/[id]/chat/route.ts +7 -3
- package/src/app/api/{sessions → chats}/[id]/checkpoints/route.ts +1 -1
- package/src/app/api/chats/[id]/main-loop/route.ts +13 -0
- package/src/app/api/{sessions → chats}/[id]/messages/route.ts +19 -13
- package/src/app/api/{sessions → chats}/[id]/restore/route.ts +1 -1
- package/src/app/api/{sessions → chats}/[id]/route.ts +22 -52
- package/src/app/api/{sessions → chats}/[id]/stop/route.ts +6 -1
- package/src/app/api/{sessions → chats}/route.ts +21 -7
- package/src/app/api/connectors/[id]/doctor/route.ts +26 -0
- package/src/app/api/connectors/doctor/route.ts +13 -0
- package/src/app/api/files/open/route.ts +16 -14
- package/src/app/api/memory/maintenance/route.ts +11 -1
- package/src/app/api/openclaw/agent-files/route.ts +27 -4
- package/src/app/api/openclaw/skills/route.ts +11 -3
- package/src/app/api/plugins/dependencies/route.ts +24 -0
- package/src/app/api/plugins/install/route.ts +15 -92
- package/src/app/api/plugins/route.ts +6 -26
- package/src/app/api/plugins/settings/route.ts +40 -0
- package/src/app/api/plugins/ui/route.ts +1 -0
- package/src/app/api/settings/route.ts +49 -7
- package/src/app/api/tasks/[id]/route.ts +15 -6
- package/src/app/api/tasks/bulk/route.ts +2 -2
- package/src/app/api/tasks/route.ts +9 -4
- package/src/app/api/usage/route.ts +30 -0
- package/src/app/api/webhooks/[id]/route.ts +8 -1
- package/src/app/page.tsx +9 -2
- package/src/cli/index.js +39 -33
- package/src/cli/index.ts +43 -49
- package/src/cli/spec.js +29 -27
- package/src/components/agents/agent-card.tsx +16 -13
- package/src/components/agents/agent-chat-list.tsx +104 -4
- package/src/components/agents/agent-list.tsx +54 -22
- package/src/components/agents/agent-sheet.tsx +209 -18
- package/src/components/agents/cron-job-form.tsx +3 -3
- package/src/components/agents/inspector-panel.tsx +110 -50
- package/src/components/auth/access-key-gate.tsx +36 -97
- package/src/components/auth/setup-wizard.tsx +5 -38
- package/src/components/chat/chat-area.tsx +39 -27
- package/src/components/{sessions/session-card.tsx → chat/chat-card.tsx} +7 -23
- package/src/components/chat/chat-header.tsx +299 -314
- package/src/components/{sessions/session-list.tsx → chat/chat-list.tsx} +11 -14
- package/src/components/chat/chat-tool-toggles.tsx +26 -17
- package/src/components/chat/checkpoint-timeline.tsx +4 -4
- package/src/components/chat/message-bubble.tsx +4 -1
- package/src/components/chat/message-list.tsx +5 -3
- package/src/components/chat/session-debug-panel.tsx +1 -1
- package/src/components/chat/tool-request-banner.tsx +3 -3
- package/src/components/chatrooms/agent-hover-card.tsx +3 -3
- package/src/components/chatrooms/chatroom-tool-request-banner.tsx +2 -2
- package/src/components/chatrooms/chatroom-view.tsx +347 -205
- package/src/components/connectors/connector-list.tsx +265 -127
- package/src/components/connectors/connector-sheet.tsx +218 -1
- package/src/components/home/home-view.tsx +129 -5
- package/src/components/layout/app-layout.tsx +392 -182
- package/src/components/layout/mobile-header.tsx +26 -8
- package/src/components/plugins/plugin-list.tsx +487 -254
- package/src/components/plugins/plugin-sheet.tsx +236 -13
- package/src/components/projects/project-detail.tsx +183 -0
- package/src/components/settings/gateway-connection-panel.tsx +1 -1
- package/src/components/shared/agent-picker-list.tsx +2 -2
- package/src/components/shared/command-palette.tsx +111 -25
- package/src/components/shared/settings/plugin-manager.tsx +20 -4
- package/src/components/shared/settings/section-capability-policy.tsx +105 -0
- package/src/components/shared/settings/section-heartbeat.tsx +78 -1
- package/src/components/shared/settings/section-orchestrator.tsx +3 -3
- package/src/components/shared/settings/section-providers.tsx +1 -1
- package/src/components/shared/settings/section-runtime-loop.tsx +5 -5
- package/src/components/shared/settings/section-secrets.tsx +6 -6
- package/src/components/shared/settings/section-user-preferences.tsx +1 -1
- package/src/components/shared/settings/section-voice.tsx +5 -1
- package/src/components/shared/settings/section-web-search.tsx +10 -2
- package/src/components/shared/settings/settings-page.tsx +244 -56
- package/src/components/tasks/approvals-panel.tsx +205 -18
- package/src/components/tasks/task-board.tsx +242 -46
- package/src/components/usage/metrics-dashboard.tsx +147 -1
- package/src/components/wallets/wallet-panel.tsx +17 -5
- package/src/components/webhooks/webhook-sheet.tsx +8 -8
- package/src/lib/auth.ts +17 -0
- package/src/lib/chat-streaming-state.test.ts +108 -0
- package/src/lib/chat-streaming-state.ts +108 -0
- package/src/lib/chat.ts +1 -1
- package/src/lib/{sessions.ts → chats.ts} +28 -18
- package/src/lib/openclaw-agent-id.test.ts +14 -0
- package/src/lib/openclaw-agent-id.ts +31 -0
- package/src/lib/providers/claude-cli.ts +1 -1
- package/src/lib/server/agent-assignment.test.ts +112 -0
- package/src/lib/server/agent-assignment.ts +169 -0
- package/src/lib/server/approval-connector-notify.test.ts +253 -0
- package/src/lib/server/approvals-auto-approve.test.ts +205 -0
- package/src/lib/server/approvals.ts +483 -75
- package/src/lib/server/autonomy-runtime.test.ts +341 -0
- package/src/lib/server/browser-state.test.ts +118 -0
- package/src/lib/server/browser-state.ts +123 -0
- package/src/lib/server/build-llm.test.ts +36 -0
- package/src/lib/server/build-llm.ts +11 -4
- package/src/lib/server/builtin-plugins.ts +34 -0
- package/src/lib/server/capability-router.ts +10 -8
- package/src/lib/server/chat-execution-heartbeat.test.ts +40 -0
- package/src/lib/server/chat-execution-tool-events.test.ts +134 -0
- package/src/lib/server/chat-execution.ts +285 -165
- package/src/lib/server/chatroom-health.test.ts +26 -0
- package/src/lib/server/chatroom-health.ts +2 -3
- package/src/lib/server/chatroom-helpers.test.ts +67 -2
- package/src/lib/server/chatroom-helpers.ts +48 -8
- package/src/lib/server/connectors/discord.ts +175 -11
- package/src/lib/server/connectors/doctor.test.ts +80 -0
- package/src/lib/server/connectors/doctor.ts +116 -0
- package/src/lib/server/connectors/manager.ts +948 -112
- package/src/lib/server/connectors/policy.test.ts +222 -0
- package/src/lib/server/connectors/policy.ts +452 -0
- package/src/lib/server/connectors/slack.ts +188 -9
- package/src/lib/server/connectors/telegram.ts +65 -15
- package/src/lib/server/connectors/thread-context.test.ts +44 -0
- package/src/lib/server/connectors/thread-context.ts +72 -0
- package/src/lib/server/connectors/types.ts +41 -11
- package/src/lib/server/cost.ts +34 -1
- package/src/lib/server/daemon-state.ts +61 -3
- package/src/lib/server/data-dir.ts +13 -0
- package/src/lib/server/delegation-jobs.test.ts +140 -0
- package/src/lib/server/delegation-jobs.ts +248 -0
- package/src/lib/server/document-utils.test.ts +47 -0
- package/src/lib/server/document-utils.ts +397 -0
- package/src/lib/server/heartbeat-service.ts +14 -40
- package/src/lib/server/heartbeat-source.test.ts +22 -0
- package/src/lib/server/heartbeat-source.ts +7 -0
- package/src/lib/server/identity-continuity.test.ts +77 -0
- package/src/lib/server/identity-continuity.ts +127 -0
- package/src/lib/server/mailbox-utils.ts +347 -0
- package/src/lib/server/main-agent-loop.ts +28 -1103
- package/src/lib/server/memory-db.ts +4 -6
- package/src/lib/server/memory-tiers.ts +40 -0
- package/src/lib/server/openclaw-agent-resolver.test.ts +70 -0
- package/src/lib/server/openclaw-agent-resolver.ts +128 -0
- package/src/lib/server/openclaw-exec-config.ts +5 -6
- package/src/lib/server/openclaw-skills-normalize.test.ts +56 -0
- package/src/lib/server/openclaw-skills-normalize.ts +136 -0
- package/src/lib/server/openclaw-sync.ts +3 -2
- package/src/lib/server/orchestrator-lg.ts +20 -9
- package/src/lib/server/orchestrator.ts +7 -7
- package/src/lib/server/playwright-proxy.mjs +27 -3
- package/src/lib/server/plugins.test.ts +207 -0
- package/src/lib/server/plugins.ts +927 -66
- package/src/lib/server/provider-health.ts +38 -6
- package/src/lib/server/queue.ts +13 -28
- package/src/lib/server/scheduler.ts +2 -0
- package/src/lib/server/session-archive-memory.test.ts +85 -0
- package/src/lib/server/session-archive-memory.ts +230 -0
- package/src/lib/server/session-mailbox.ts +8 -18
- package/src/lib/server/session-reset-policy.test.ts +99 -0
- package/src/lib/server/session-reset-policy.ts +311 -0
- package/src/lib/server/session-run-manager.ts +33 -82
- package/src/lib/server/session-tools/autonomy-tools.test.ts +105 -0
- package/src/lib/server/session-tools/calendar.ts +366 -0
- package/src/lib/server/session-tools/canvas.ts +1 -1
- package/src/lib/server/session-tools/chatroom.ts +4 -2
- package/src/lib/server/session-tools/connector.ts +114 -10
- package/src/lib/server/session-tools/context.ts +21 -5
- package/src/lib/server/session-tools/crawl.ts +447 -0
- package/src/lib/server/session-tools/crud.ts +74 -28
- package/src/lib/server/session-tools/delegate-fallback.test.ts +219 -0
- package/src/lib/server/session-tools/delegate.ts +497 -24
- package/src/lib/server/session-tools/discovery.ts +24 -6
- package/src/lib/server/session-tools/document.ts +283 -0
- package/src/lib/server/session-tools/edit_file.ts +4 -2
- package/src/lib/server/session-tools/email.ts +320 -0
- package/src/lib/server/session-tools/extract.ts +137 -0
- package/src/lib/server/session-tools/file-normalize.test.ts +93 -0
- package/src/lib/server/session-tools/file-send.test.ts +84 -1
- package/src/lib/server/session-tools/file.ts +241 -25
- package/src/lib/server/session-tools/git.ts +1 -1
- package/src/lib/server/session-tools/http.ts +1 -1
- package/src/lib/server/session-tools/human-loop.ts +227 -0
- package/src/lib/server/session-tools/image-gen.ts +380 -0
- package/src/lib/server/session-tools/index.ts +130 -50
- package/src/lib/server/session-tools/mailbox.ts +276 -0
- package/src/lib/server/session-tools/memory.ts +172 -3
- package/src/lib/server/session-tools/monitor.ts +151 -8
- package/src/lib/server/session-tools/normalize-tool-args.ts +17 -14
- package/src/lib/server/session-tools/openclaw-nodes.ts +1 -1
- package/src/lib/server/session-tools/openclaw-workspace.ts +1 -1
- package/src/lib/server/session-tools/platform-normalize.test.ts +142 -0
- package/src/lib/server/session-tools/platform.ts +148 -7
- package/src/lib/server/session-tools/plugin-creator.ts +89 -26
- package/src/lib/server/session-tools/primitive-tools.test.ts +257 -0
- package/src/lib/server/session-tools/replicate.ts +301 -0
- package/src/lib/server/session-tools/sample-ui.ts +1 -1
- package/src/lib/server/session-tools/sandbox.ts +4 -2
- package/src/lib/server/session-tools/schedule.ts +24 -12
- package/src/lib/server/session-tools/session-info.ts +43 -7
- package/src/lib/server/session-tools/session-tools-wiring.test.ts +31 -17
- package/src/lib/server/session-tools/shell.ts +5 -2
- package/src/lib/server/session-tools/subagent.ts +194 -28
- package/src/lib/server/session-tools/table.ts +587 -0
- package/src/lib/server/session-tools/wallet.ts +42 -12
- package/src/lib/server/session-tools/web-browser-config.test.ts +39 -0
- package/src/lib/server/session-tools/web.ts +926 -91
- package/src/lib/server/storage.ts +255 -16
- package/src/lib/server/stream-agent-chat.ts +116 -268
- package/src/lib/server/structured-extract.test.ts +72 -0
- package/src/lib/server/structured-extract.ts +373 -0
- package/src/lib/server/task-mention.test.ts +16 -2
- package/src/lib/server/task-mention.ts +61 -10
- package/src/lib/server/tool-aliases.ts +66 -18
- package/src/lib/server/tool-capability-policy.test.ts +9 -9
- package/src/lib/server/tool-capability-policy.ts +38 -27
- package/src/lib/server/tool-retry.ts +2 -0
- package/src/lib/server/watch-jobs.test.ts +173 -0
- package/src/lib/server/watch-jobs.ts +532 -0
- package/src/lib/server/ws-hub.ts +5 -3
- package/src/lib/tool-definitions.ts +4 -0
- package/src/lib/validation/schemas.test.ts +26 -0
- package/src/lib/validation/schemas.ts +10 -1
- package/src/lib/ws-client.ts +14 -12
- package/src/proxy.ts +5 -5
- package/src/stores/use-app-store.ts +5 -11
- package/src/stores/use-chat-store.ts +38 -9
- package/src/types/index.ts +352 -47
- package/src/app/api/sessions/[id]/main-loop/route.ts +0 -94
- package/src/components/sessions/new-session-sheet.tsx +0 -253
- package/src/lib/server/main-session.ts +0 -24
- package/src/lib/server/session-run-manager.test.ts +0 -23
- /package/src/app/api/{sessions → chats}/[id]/clear/route.ts +0 -0
- /package/src/app/api/{sessions → chats}/[id]/deploy/route.ts +0 -0
- /package/src/app/api/{sessions → chats}/[id]/devserver/route.ts +0 -0
- /package/src/app/api/{sessions → chats}/[id]/edit-resend/route.ts +0 -0
- /package/src/app/api/{sessions → chats}/[id]/fork/route.ts +0 -0
- /package/src/app/api/{sessions → chats}/[id]/mailbox/route.ts +0 -0
- /package/src/app/api/{sessions → chats}/[id]/retry/route.ts +0 -0
- /package/src/app/api/{sessions → chats}/heartbeat/route.ts +0 -0
|
@@ -1,12 +1,256 @@
|
|
|
1
|
-
import fs from 'fs'
|
|
2
|
-
import path from 'path'
|
|
3
1
|
import { genId } from '@/lib/id'
|
|
4
|
-
import { loadApprovals, upsertApproval, loadSessions, saveSessions } from './storage'
|
|
5
|
-
import type { ApprovalRequest, ApprovalCategory } from '@/types'
|
|
2
|
+
import { loadApprovals, upsertApproval, loadSessions, saveSessions, loadSettings, loadAgents } from './storage'
|
|
3
|
+
import type { ApprovalRequest, ApprovalCategory, Message } from '@/types'
|
|
6
4
|
import { notify } from './ws-hub'
|
|
7
|
-
import { DATA_DIR } from './data-dir'
|
|
8
5
|
import { log } from './logger'
|
|
9
6
|
|
|
7
|
+
const AUTO_APPROVABLE_CATEGORIES: ApprovalCategory[] = [
|
|
8
|
+
'tool_access',
|
|
9
|
+
'wallet_transfer',
|
|
10
|
+
'plugin_scaffold',
|
|
11
|
+
'plugin_install',
|
|
12
|
+
'task_tool',
|
|
13
|
+
'human_loop',
|
|
14
|
+
]
|
|
15
|
+
const DEFAULT_APPROVAL_CONNECTOR_NOTIFY_DELAY_SEC = 300
|
|
16
|
+
const MIN_APPROVAL_CONNECTOR_NOTIFY_DELAY_SEC = 30
|
|
17
|
+
const MAX_APPROVAL_CONNECTOR_NOTIFY_DELAY_SEC = 86_400
|
|
18
|
+
const APPROVAL_CONNECTOR_NOTIFY_RETRY_COOLDOWN_MS = 10 * 60 * 1000
|
|
19
|
+
|
|
20
|
+
interface RunningConnectorSummary {
|
|
21
|
+
id: string
|
|
22
|
+
agentId: string | null
|
|
23
|
+
supportsSend: boolean
|
|
24
|
+
configuredTargets: string[]
|
|
25
|
+
recentChannelId: string | null
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface PendingApprovalConnectorNotification {
|
|
29
|
+
approvalId: string
|
|
30
|
+
connectorId: string
|
|
31
|
+
channelId: string
|
|
32
|
+
threadId?: string | null
|
|
33
|
+
text: string
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function trimToString(value: unknown): string {
|
|
37
|
+
return typeof value === 'string' ? value.trim() : ''
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function clampApprovalConnectorNotifyDelaySec(value: unknown): number {
|
|
41
|
+
const parsed = typeof value === 'number'
|
|
42
|
+
? value
|
|
43
|
+
: typeof value === 'string'
|
|
44
|
+
? Number.parseInt(value, 10)
|
|
45
|
+
: Number.NaN
|
|
46
|
+
if (!Number.isFinite(parsed)) return DEFAULT_APPROVAL_CONNECTOR_NOTIFY_DELAY_SEC
|
|
47
|
+
return Math.max(
|
|
48
|
+
MIN_APPROVAL_CONNECTOR_NOTIFY_DELAY_SEC,
|
|
49
|
+
Math.min(MAX_APPROVAL_CONNECTOR_NOTIFY_DELAY_SEC, Math.trunc(parsed)),
|
|
50
|
+
)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function getApprovalConnectorNotifySettings(): { enabled: boolean; delayMs: number } {
|
|
54
|
+
const settings = loadSettings()
|
|
55
|
+
const enabled = settings.approvalConnectorNotifyEnabled !== false
|
|
56
|
+
const delaySec = clampApprovalConnectorNotifyDelaySec(settings.approvalConnectorNotifyDelaySec)
|
|
57
|
+
return {
|
|
58
|
+
enabled,
|
|
59
|
+
delayMs: delaySec * 1000,
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function approvalsAreDisabled(): boolean {
|
|
64
|
+
return loadSettings().approvalsEnabled === false
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function getMessageSourceConnectorTarget(
|
|
68
|
+
message: Record<string, unknown> | null | undefined,
|
|
69
|
+
runningById: Map<string, RunningConnectorSummary>,
|
|
70
|
+
): { connectorId: string; channelId: string; threadId?: string | null } | null {
|
|
71
|
+
const source = message?.source as Record<string, unknown> | undefined
|
|
72
|
+
const connectorId = trimToString(source?.connectorId)
|
|
73
|
+
const channelId = trimToString(source?.channelId)
|
|
74
|
+
if (!connectorId || !channelId) return null
|
|
75
|
+
const runtime = runningById.get(connectorId)
|
|
76
|
+
if (!runtime?.supportsSend) return null
|
|
77
|
+
const threadId = trimToString(source?.threadId)
|
|
78
|
+
return {
|
|
79
|
+
connectorId,
|
|
80
|
+
channelId,
|
|
81
|
+
...(threadId ? { threadId } : {}),
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function getSessionConnectorTarget(
|
|
86
|
+
session: Record<string, unknown> | null | undefined,
|
|
87
|
+
runningById: Map<string, RunningConnectorSummary>,
|
|
88
|
+
): { connectorId: string; channelId: string; threadId?: string | null } | null {
|
|
89
|
+
const context = session?.connectorContext as Record<string, unknown> | undefined
|
|
90
|
+
const connectorId = trimToString(context?.connectorId)
|
|
91
|
+
const channelId = trimToString(context?.channelId)
|
|
92
|
+
if (connectorId && channelId) {
|
|
93
|
+
const runtime = runningById.get(connectorId)
|
|
94
|
+
if (runtime?.supportsSend) {
|
|
95
|
+
const threadId = trimToString(context?.threadId)
|
|
96
|
+
return {
|
|
97
|
+
connectorId,
|
|
98
|
+
channelId,
|
|
99
|
+
...(threadId ? { threadId } : {}),
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const messages = Array.isArray(session?.messages) ? session.messages as Record<string, unknown>[] : []
|
|
105
|
+
for (let i = messages.length - 1; i >= 0; i -= 1) {
|
|
106
|
+
const message = messages[i]
|
|
107
|
+
if (trimToString(message?.role) !== 'user') continue
|
|
108
|
+
const target = getMessageSourceConnectorTarget(message, runningById)
|
|
109
|
+
if (target) return target
|
|
110
|
+
}
|
|
111
|
+
return null
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function getMostRecentAgentSessionConnectorTarget(
|
|
115
|
+
agentId: string,
|
|
116
|
+
runningById: Map<string, RunningConnectorSummary>,
|
|
117
|
+
): { connectorId: string; channelId: string; threadId?: string | null } | null {
|
|
118
|
+
const sessions = loadSessions()
|
|
119
|
+
const candidates = Object.values(sessions) as Record<string, unknown>[]
|
|
120
|
+
let best: { score: number; target: { connectorId: string; channelId: string; threadId?: string | null } } | null = null
|
|
121
|
+
|
|
122
|
+
for (const session of candidates) {
|
|
123
|
+
if (trimToString(session?.agentId) !== agentId) continue
|
|
124
|
+
const target = getSessionConnectorTarget(session, runningById)
|
|
125
|
+
if (!target) continue
|
|
126
|
+
const context = session.connectorContext as Record<string, unknown> | undefined
|
|
127
|
+
const score = typeof context?.lastInboundAt === 'number'
|
|
128
|
+
? context.lastInboundAt
|
|
129
|
+
: typeof session.lastActiveAt === 'number'
|
|
130
|
+
? session.lastActiveAt
|
|
131
|
+
: 0
|
|
132
|
+
if (!best || score > best.score) best = { score, target }
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return best?.target || null
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function getAgentRunningConnectorFallback(
|
|
139
|
+
agentId: string,
|
|
140
|
+
runningConnectors: RunningConnectorSummary[],
|
|
141
|
+
): { connectorId: string; channelId: string } | null {
|
|
142
|
+
const match = runningConnectors.find((entry) => entry.agentId === agentId && entry.supportsSend && trimToString(entry.recentChannelId))
|
|
143
|
+
if (!match) return null
|
|
144
|
+
return {
|
|
145
|
+
connectorId: match.id,
|
|
146
|
+
channelId: trimToString(match.recentChannelId),
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function buildApprovalConnectorReminderText(request: ApprovalRequest): string {
|
|
151
|
+
const agents = loadAgents()
|
|
152
|
+
const agentName = request.agentId && agents[request.agentId]?.name
|
|
153
|
+
? agents[request.agentId].name
|
|
154
|
+
: 'Your agent'
|
|
155
|
+
const ageMin = Math.max(1, Math.round((Date.now() - request.createdAt) / 60_000))
|
|
156
|
+
const lines = [
|
|
157
|
+
`${agentName} is waiting for your approval in SwarmClaw.`,
|
|
158
|
+
`Request: ${request.title}`,
|
|
159
|
+
]
|
|
160
|
+
const description = trimToString(request.description)
|
|
161
|
+
if (description) lines.push(`Details: ${description.slice(0, 500)}`)
|
|
162
|
+
lines.push(`Pending for about ${ageMin} minute${ageMin === 1 ? '' : 's'}.`)
|
|
163
|
+
lines.push('Open the Approvals panel to approve or reject it.')
|
|
164
|
+
return lines.join('\n')
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function buildApprovalChatMessage(request: ApprovalRequest): string {
|
|
168
|
+
const targetId = getApprovalTargetId(request.data)
|
|
169
|
+
switch (request.category) {
|
|
170
|
+
case 'tool_access':
|
|
171
|
+
return JSON.stringify({
|
|
172
|
+
type: 'plugin_request',
|
|
173
|
+
approvalId: request.id,
|
|
174
|
+
pluginId: targetId || '',
|
|
175
|
+
toolId: targetId || '',
|
|
176
|
+
reason: trimToString(request.description),
|
|
177
|
+
message: `Plugin access request sent to user for "${targetId || 'requested tool'}". Once granted, I'll automatically continue.`,
|
|
178
|
+
})
|
|
179
|
+
case 'plugin_scaffold':
|
|
180
|
+
return JSON.stringify({
|
|
181
|
+
type: 'plugin_scaffold_request',
|
|
182
|
+
approvalId: request.id,
|
|
183
|
+
filename: trimToString(request.data.filename),
|
|
184
|
+
message: `I've submitted a request to create plugin "${trimToString(request.data.filename) || 'plugin.js'}". The user needs to approve it via the Approvals page or the approval card in chat. Once approved, the plugin file will be written automatically — no need to call this tool again.`,
|
|
185
|
+
})
|
|
186
|
+
case 'plugin_install':
|
|
187
|
+
return JSON.stringify({
|
|
188
|
+
type: 'plugin_install_request',
|
|
189
|
+
approvalId: request.id,
|
|
190
|
+
url: trimToString(request.data.url),
|
|
191
|
+
pluginId: trimToString(request.data.pluginId),
|
|
192
|
+
reason: trimToString(request.description),
|
|
193
|
+
message: `I'm requesting to install a new plugin${trimToString(request.data.url) ? ` from ${trimToString(request.data.url)}` : ''}. This will add new capabilities to the platform.`,
|
|
194
|
+
})
|
|
195
|
+
case 'wallet_transfer':
|
|
196
|
+
return JSON.stringify({
|
|
197
|
+
type: 'plugin_wallet_transfer_request',
|
|
198
|
+
approvalId: request.id,
|
|
199
|
+
amountSol: request.data.amountSol,
|
|
200
|
+
toAddress: trimToString(request.data.toAddress),
|
|
201
|
+
memo: trimToString(request.data.memo),
|
|
202
|
+
message: `I'm requesting to send ${request.data.amountSol ?? 'funds'} to ${trimToString(request.data.toAddress) || 'the specified address'}. Please approve this transaction.`,
|
|
203
|
+
})
|
|
204
|
+
default: {
|
|
205
|
+
const lines = [
|
|
206
|
+
`[Approval requested] ${request.title}`,
|
|
207
|
+
]
|
|
208
|
+
const description = trimToString(request.description)
|
|
209
|
+
if (description) lines.push(`Details: ${description}`)
|
|
210
|
+
lines.push('Approve or reject this request in the chat approval card or the Approvals panel.')
|
|
211
|
+
return lines.join('\n')
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function pushApprovalRequestMessage(request: ApprovalRequest): void {
|
|
217
|
+
const sessionId = trimToString(request.sessionId)
|
|
218
|
+
if (!sessionId) return
|
|
219
|
+
const sessions = loadSessions()
|
|
220
|
+
const session = sessions[sessionId]
|
|
221
|
+
if (!session) return
|
|
222
|
+
|
|
223
|
+
const text = buildApprovalChatMessage(request)
|
|
224
|
+
const recentMessages: Message[] = Array.isArray(session.messages) ? session.messages.slice(-6) : []
|
|
225
|
+
if (recentMessages.some((message) => message?.role === 'assistant' && message?.text === text)) {
|
|
226
|
+
return
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
session.messages = Array.isArray(session.messages) ? session.messages : []
|
|
230
|
+
session.messages.push({
|
|
231
|
+
role: 'assistant',
|
|
232
|
+
text,
|
|
233
|
+
time: Date.now(),
|
|
234
|
+
kind: 'system',
|
|
235
|
+
})
|
|
236
|
+
session.lastActiveAt = Date.now()
|
|
237
|
+
sessions[sessionId] = session
|
|
238
|
+
saveSessions(sessions)
|
|
239
|
+
notify(`messages:${sessionId}`)
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function persistApprovalConnectorNotification(
|
|
243
|
+
id: string,
|
|
244
|
+
mutate: (request: ApprovalRequest) => void,
|
|
245
|
+
): ApprovalRequest | null {
|
|
246
|
+
const approvals = loadApprovals() as Record<string, ApprovalRequest>
|
|
247
|
+
const request = approvals[id]
|
|
248
|
+
if (!request) return null
|
|
249
|
+
mutate(request)
|
|
250
|
+
upsertApproval(id, request)
|
|
251
|
+
return request
|
|
252
|
+
}
|
|
253
|
+
|
|
10
254
|
function getApprovalTargetId(data: Record<string, unknown>): string | null {
|
|
11
255
|
const toolId = typeof data.toolId === 'string' ? data.toolId.trim() : ''
|
|
12
256
|
if (toolId) return toolId
|
|
@@ -52,94 +296,258 @@ export function requestApproval(params: {
|
|
|
52
296
|
return request
|
|
53
297
|
}
|
|
54
298
|
|
|
299
|
+
export function listAutoApprovableApprovalCategories(): ApprovalCategory[] {
|
|
300
|
+
return [...AUTO_APPROVABLE_CATEGORIES]
|
|
301
|
+
}
|
|
55
302
|
|
|
56
|
-
export
|
|
57
|
-
const
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
if (
|
|
68
|
-
const
|
|
69
|
-
const
|
|
70
|
-
if (
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
if (toolId && !currentTools.includes(toolId)) {
|
|
74
|
-
session.tools = [...currentTools, toolId]
|
|
75
|
-
saveSessions(sessions)
|
|
76
|
-
}
|
|
303
|
+
export function isApprovalCategoryAutoApproved(category: ApprovalCategory): boolean {
|
|
304
|
+
const configured = Array.isArray(loadSettings().approvalAutoApproveCategories)
|
|
305
|
+
? loadSettings().approvalAutoApproveCategories
|
|
306
|
+
: []
|
|
307
|
+
return configured.includes(category)
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
async function applyApprovedSideEffects(request: ApprovalRequest): Promise<void> {
|
|
311
|
+
if (request.category === 'tool_access' && request.sessionId) {
|
|
312
|
+
const sessions = loadSessions()
|
|
313
|
+
const session = sessions[request.sessionId]
|
|
314
|
+
if (session) {
|
|
315
|
+
const toolId = getApprovalTargetId(request.data)
|
|
316
|
+
const currentTools = session.plugins || []
|
|
317
|
+
if (toolId && !currentTools.includes(toolId)) {
|
|
318
|
+
session.plugins = [...currentTools, toolId]
|
|
319
|
+
saveSessions(sessions)
|
|
77
320
|
}
|
|
78
321
|
}
|
|
322
|
+
}
|
|
79
323
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
fs.writeFileSync(path.join(pluginsDir, filename), code, 'utf8')
|
|
87
|
-
const { getPluginManager } = await import('./plugins')
|
|
88
|
-
getPluginManager().reload()
|
|
324
|
+
if (request.category === 'plugin_scaffold') {
|
|
325
|
+
const filename = typeof request.data.filename === 'string' ? request.data.filename : ''
|
|
326
|
+
const code = typeof request.data.code === 'string' ? request.data.code : ''
|
|
327
|
+
if (filename && code) {
|
|
328
|
+
const { getPluginManager } = await import('./plugins')
|
|
329
|
+
const manager = getPluginManager()
|
|
89
330
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
331
|
+
const createdByAgentId = typeof request.data.createdByAgentId === 'string' ? request.data.createdByAgentId : request.agentId
|
|
332
|
+
try {
|
|
333
|
+
await manager.savePluginSource(filename, code, {
|
|
334
|
+
packageJson: request.data.packageJson,
|
|
335
|
+
packageManager: typeof request.data.packageManager === 'string' ? request.data.packageManager : undefined,
|
|
336
|
+
installDependencies: request.data.packageJson !== undefined,
|
|
337
|
+
meta: createdByAgentId ? { createdByAgentId } : undefined,
|
|
338
|
+
})
|
|
339
|
+
} catch (err: unknown) {
|
|
340
|
+
log.error('approvals', 'Plugin scaffold dependency setup failed', {
|
|
341
|
+
filename,
|
|
342
|
+
error: err instanceof Error ? err.message : String(err),
|
|
343
|
+
})
|
|
344
|
+
await manager.savePluginSource(filename, code, {
|
|
345
|
+
meta: createdByAgentId ? { createdByAgentId } : undefined,
|
|
346
|
+
})
|
|
347
|
+
}
|
|
348
|
+
log.info('approvals', `Plugin scaffolded: ${filename}`)
|
|
349
|
+
|
|
350
|
+
if (request.sessionId) {
|
|
351
|
+
const sessions = loadSessions()
|
|
352
|
+
const session = sessions[request.sessionId]
|
|
353
|
+
if (session) {
|
|
354
|
+
const currentTools = session.plugins || []
|
|
355
|
+
if (!currentTools.includes(filename)) {
|
|
356
|
+
session.plugins = [...currentTools, filename]
|
|
357
|
+
saveSessions(sessions)
|
|
107
358
|
}
|
|
108
359
|
}
|
|
109
|
-
notify('plugins')
|
|
110
360
|
}
|
|
361
|
+
notify('plugins')
|
|
111
362
|
}
|
|
363
|
+
}
|
|
112
364
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
365
|
+
if (request.category === 'plugin_install') {
|
|
366
|
+
const url = typeof request.data.url === 'string' ? request.data.url : ''
|
|
367
|
+
const filename = typeof request.data.filename === 'string' ? request.data.filename : ''
|
|
368
|
+
if (url) {
|
|
369
|
+
try {
|
|
370
|
+
const pluginId = typeof request.data.pluginId === 'string' ? request.data.pluginId : ''
|
|
371
|
+
const safeName = (pluginId || url.split('/').pop() || 'plugin').replace(/[^a-zA-Z0-9._-]/g, '_')
|
|
372
|
+
const resolvedFilename = safeName.endsWith('.js') || safeName.endsWith('.mjs') ? safeName : `${safeName}.js`
|
|
373
|
+
const { getPluginManager } = await import('./plugins')
|
|
374
|
+
await getPluginManager().installPluginFromUrl(url, resolvedFilename, {
|
|
375
|
+
createdByAgentId: typeof request.data.createdByAgentId === 'string' ? request.data.createdByAgentId : request.agentId || undefined,
|
|
376
|
+
})
|
|
377
|
+
log.info('approvals', `Plugin installed from URL: ${resolvedFilename}`)
|
|
378
|
+
notify('plugins')
|
|
379
|
+
} catch (err: unknown) {
|
|
380
|
+
log.error('approvals', 'Plugin install failed after approval', {
|
|
381
|
+
url,
|
|
382
|
+
error: err instanceof Error ? err.message : String(err),
|
|
383
|
+
})
|
|
384
|
+
}
|
|
385
|
+
} else if (filename) {
|
|
386
|
+
try {
|
|
387
|
+
const { getPluginManager } = await import('./plugins')
|
|
388
|
+
const manager = getPluginManager()
|
|
389
|
+
if (request.data.packageJson !== undefined) {
|
|
390
|
+
const source = manager.readPluginSource(filename)
|
|
391
|
+
await manager.savePluginSource(filename, source, {
|
|
392
|
+
packageJson: request.data.packageJson,
|
|
393
|
+
packageManager: typeof request.data.packageManager === 'string' ? request.data.packageManager : undefined,
|
|
135
394
|
})
|
|
136
395
|
}
|
|
396
|
+
await manager.installPluginDependencies(filename, {
|
|
397
|
+
packageManager: typeof request.data.packageManager === 'string'
|
|
398
|
+
? request.data.packageManager as import('@/types').PluginPackageManager
|
|
399
|
+
: undefined,
|
|
400
|
+
})
|
|
401
|
+
notify('plugins')
|
|
402
|
+
} catch (err: unknown) {
|
|
403
|
+
log.error('approvals', 'Plugin dependency install failed after approval', {
|
|
404
|
+
filename,
|
|
405
|
+
error: err instanceof Error ? err.message : String(err),
|
|
406
|
+
})
|
|
137
407
|
}
|
|
138
408
|
}
|
|
139
409
|
}
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
async function persistApprovalDecision(request: ApprovalRequest, approved: boolean): Promise<ApprovalRequest> {
|
|
413
|
+
request.status = approved ? 'approved' : 'rejected'
|
|
414
|
+
request.updatedAt = Date.now()
|
|
415
|
+
upsertApproval(request.id, request)
|
|
416
|
+
|
|
417
|
+
if (approved) {
|
|
418
|
+
await applyApprovedSideEffects(request)
|
|
419
|
+
}
|
|
140
420
|
|
|
141
421
|
notify('approvals')
|
|
422
|
+
import('./watch-jobs')
|
|
423
|
+
.then(({ triggerApprovalWatchJobs }) => {
|
|
424
|
+
triggerApprovalWatchJobs({
|
|
425
|
+
approvalId: request.id,
|
|
426
|
+
status: approved ? 'approved' : 'rejected',
|
|
427
|
+
title: request.title,
|
|
428
|
+
description: request.description,
|
|
429
|
+
})
|
|
430
|
+
})
|
|
431
|
+
.catch(() => {
|
|
432
|
+
// best-effort trigger only
|
|
433
|
+
})
|
|
142
434
|
if (request.sessionId) notify(`session:${request.sessionId}`)
|
|
435
|
+
return request
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
export async function requestApprovalMaybeAutoApprove(params: {
|
|
439
|
+
category: ApprovalCategory
|
|
440
|
+
title: string
|
|
441
|
+
description?: string
|
|
442
|
+
data: Record<string, unknown>
|
|
443
|
+
agentId?: string | null
|
|
444
|
+
sessionId?: string | null
|
|
445
|
+
taskId?: string | null
|
|
446
|
+
}): Promise<ApprovalRequest> {
|
|
447
|
+
const request = requestApproval(params)
|
|
448
|
+
if (!approvalsAreDisabled() && !isApprovalCategoryAutoApproved(request.category)) {
|
|
449
|
+
pushApprovalRequestMessage(request)
|
|
450
|
+
return request
|
|
451
|
+
}
|
|
452
|
+
return persistApprovalDecision(request, true)
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
|
|
456
|
+
export async function submitDecision(id: string, approved: boolean): Promise<void> {
|
|
457
|
+
const approvals = loadApprovals() as Record<string, ApprovalRequest>
|
|
458
|
+
const request = approvals[id]
|
|
459
|
+
if (!request) throw new Error('Approval request not found')
|
|
460
|
+
await persistApprovalDecision(request, approved)
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
export function listPendingApprovalsNeedingConnectorNotification(params?: {
|
|
464
|
+
now?: number
|
|
465
|
+
runningConnectors?: RunningConnectorSummary[]
|
|
466
|
+
}): PendingApprovalConnectorNotification[] {
|
|
467
|
+
const { enabled, delayMs } = getApprovalConnectorNotifySettings()
|
|
468
|
+
if (!enabled) return []
|
|
469
|
+
|
|
470
|
+
const now = typeof params?.now === 'number' ? params.now : Date.now()
|
|
471
|
+
const runningConnectors = Array.isArray(params?.runningConnectors) ? params.runningConnectors : []
|
|
472
|
+
const runningById = new Map(
|
|
473
|
+
runningConnectors
|
|
474
|
+
.filter((entry) => entry?.id && entry.supportsSend)
|
|
475
|
+
.map((entry) => [entry.id, entry] as const),
|
|
476
|
+
)
|
|
477
|
+
|
|
478
|
+
const approvals = loadApprovals() as Record<string, ApprovalRequest>
|
|
479
|
+
const sessions = loadSessions()
|
|
480
|
+
const out: PendingApprovalConnectorNotification[] = []
|
|
481
|
+
|
|
482
|
+
for (const request of Object.values(approvals)) {
|
|
483
|
+
if (request.status !== 'pending') continue
|
|
484
|
+
if ((now - request.createdAt) < delayMs) continue
|
|
485
|
+
if (request.connectorNotification?.sentAt) continue
|
|
486
|
+
const lastAttemptAt = request.connectorNotification?.attemptedAt || 0
|
|
487
|
+
if (lastAttemptAt > 0 && (now - lastAttemptAt) < APPROVAL_CONNECTOR_NOTIFY_RETRY_COOLDOWN_MS) continue
|
|
488
|
+
|
|
489
|
+
let target: { connectorId: string; channelId: string; threadId?: string | null } | null = null
|
|
490
|
+
if (request.sessionId) {
|
|
491
|
+
target = getSessionConnectorTarget(sessions[request.sessionId] as Record<string, unknown> | undefined, runningById)
|
|
492
|
+
}
|
|
493
|
+
if (!target && request.agentId) {
|
|
494
|
+
target = getMostRecentAgentSessionConnectorTarget(request.agentId, runningById)
|
|
495
|
+
}
|
|
496
|
+
if (!target && request.agentId) {
|
|
497
|
+
target = getAgentRunningConnectorFallback(request.agentId, runningConnectors)
|
|
498
|
+
}
|
|
499
|
+
if (!target) continue
|
|
500
|
+
|
|
501
|
+
out.push({
|
|
502
|
+
approvalId: request.id,
|
|
503
|
+
connectorId: target.connectorId,
|
|
504
|
+
channelId: target.channelId,
|
|
505
|
+
...(target.threadId ? { threadId: target.threadId } : {}),
|
|
506
|
+
text: buildApprovalConnectorReminderText(request),
|
|
507
|
+
})
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
return out
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
export function markApprovalConnectorNotificationAttempt(id: string, params: {
|
|
514
|
+
at?: number
|
|
515
|
+
connectorId?: string | null
|
|
516
|
+
channelId?: string | null
|
|
517
|
+
threadId?: string | null
|
|
518
|
+
lastError?: string | null
|
|
519
|
+
}): ApprovalRequest | null {
|
|
520
|
+
return persistApprovalConnectorNotification(id, (request) => {
|
|
521
|
+
request.connectorNotification = {
|
|
522
|
+
...(request.connectorNotification || {}),
|
|
523
|
+
attemptedAt: typeof params.at === 'number' ? params.at : Date.now(),
|
|
524
|
+
connectorId: params.connectorId ?? request.connectorNotification?.connectorId ?? null,
|
|
525
|
+
channelId: params.channelId ?? request.connectorNotification?.channelId ?? null,
|
|
526
|
+
threadId: params.threadId ?? request.connectorNotification?.threadId ?? null,
|
|
527
|
+
lastError: params.lastError ?? request.connectorNotification?.lastError ?? null,
|
|
528
|
+
}
|
|
529
|
+
})
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
export function markApprovalConnectorNotificationSent(id: string, params: {
|
|
533
|
+
at?: number
|
|
534
|
+
connectorId: string
|
|
535
|
+
channelId: string
|
|
536
|
+
threadId?: string | null
|
|
537
|
+
messageId?: string | null
|
|
538
|
+
}): ApprovalRequest | null {
|
|
539
|
+
return persistApprovalConnectorNotification(id, (request) => {
|
|
540
|
+
request.connectorNotification = {
|
|
541
|
+
...(request.connectorNotification || {}),
|
|
542
|
+
attemptedAt: typeof params.at === 'number' ? params.at : Date.now(),
|
|
543
|
+
sentAt: typeof params.at === 'number' ? params.at : Date.now(),
|
|
544
|
+
connectorId: params.connectorId,
|
|
545
|
+
channelId: params.channelId,
|
|
546
|
+
threadId: params.threadId ?? request.connectorNotification?.threadId ?? null,
|
|
547
|
+
messageId: params.messageId ?? request.connectorNotification?.messageId ?? null,
|
|
548
|
+
lastError: null,
|
|
549
|
+
}
|
|
550
|
+
})
|
|
143
551
|
}
|
|
144
552
|
|
|
145
553
|
export function listPendingApprovals(): ApprovalRequest[] {
|