@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.
Files changed (237) hide show
  1. package/README.md +155 -150
  2. package/package.json +1 -1
  3. package/src/app/api/agents/[id]/route.ts +26 -0
  4. package/src/app/api/agents/[id]/thread/route.ts +37 -9
  5. package/src/app/api/agents/route.ts +13 -2
  6. package/src/app/api/auth/route.ts +76 -7
  7. package/src/app/api/chatrooms/[id]/chat/route.ts +7 -2
  8. package/src/app/api/{sessions → chats}/[id]/browser/route.ts +5 -1
  9. package/src/app/api/{sessions → chats}/[id]/chat/route.ts +7 -3
  10. package/src/app/api/{sessions → chats}/[id]/checkpoints/route.ts +1 -1
  11. package/src/app/api/chats/[id]/main-loop/route.ts +13 -0
  12. package/src/app/api/{sessions → chats}/[id]/messages/route.ts +19 -13
  13. package/src/app/api/{sessions → chats}/[id]/restore/route.ts +1 -1
  14. package/src/app/api/{sessions → chats}/[id]/route.ts +22 -52
  15. package/src/app/api/{sessions → chats}/[id]/stop/route.ts +6 -1
  16. package/src/app/api/{sessions → chats}/route.ts +21 -7
  17. package/src/app/api/connectors/[id]/doctor/route.ts +26 -0
  18. package/src/app/api/connectors/doctor/route.ts +13 -0
  19. package/src/app/api/files/open/route.ts +16 -14
  20. package/src/app/api/memory/maintenance/route.ts +11 -1
  21. package/src/app/api/openclaw/agent-files/route.ts +27 -4
  22. package/src/app/api/openclaw/skills/route.ts +11 -3
  23. package/src/app/api/plugins/dependencies/route.ts +24 -0
  24. package/src/app/api/plugins/install/route.ts +15 -92
  25. package/src/app/api/plugins/route.ts +6 -26
  26. package/src/app/api/plugins/settings/route.ts +40 -0
  27. package/src/app/api/plugins/ui/route.ts +1 -0
  28. package/src/app/api/settings/route.ts +49 -7
  29. package/src/app/api/tasks/[id]/route.ts +15 -6
  30. package/src/app/api/tasks/bulk/route.ts +2 -2
  31. package/src/app/api/tasks/route.ts +9 -4
  32. package/src/app/api/usage/route.ts +30 -0
  33. package/src/app/api/webhooks/[id]/route.ts +8 -1
  34. package/src/app/page.tsx +9 -2
  35. package/src/cli/index.js +39 -33
  36. package/src/cli/index.ts +43 -49
  37. package/src/cli/spec.js +29 -27
  38. package/src/components/agents/agent-card.tsx +16 -13
  39. package/src/components/agents/agent-chat-list.tsx +104 -4
  40. package/src/components/agents/agent-list.tsx +54 -22
  41. package/src/components/agents/agent-sheet.tsx +209 -18
  42. package/src/components/agents/cron-job-form.tsx +3 -3
  43. package/src/components/agents/inspector-panel.tsx +110 -50
  44. package/src/components/auth/access-key-gate.tsx +36 -97
  45. package/src/components/auth/setup-wizard.tsx +5 -38
  46. package/src/components/chat/chat-area.tsx +39 -27
  47. package/src/components/{sessions/session-card.tsx → chat/chat-card.tsx} +7 -23
  48. package/src/components/chat/chat-header.tsx +299 -314
  49. package/src/components/{sessions/session-list.tsx → chat/chat-list.tsx} +11 -14
  50. package/src/components/chat/chat-tool-toggles.tsx +26 -17
  51. package/src/components/chat/checkpoint-timeline.tsx +4 -4
  52. package/src/components/chat/message-bubble.tsx +4 -1
  53. package/src/components/chat/message-list.tsx +5 -3
  54. package/src/components/chat/session-debug-panel.tsx +1 -1
  55. package/src/components/chat/tool-request-banner.tsx +3 -3
  56. package/src/components/chatrooms/agent-hover-card.tsx +3 -3
  57. package/src/components/chatrooms/chatroom-tool-request-banner.tsx +2 -2
  58. package/src/components/chatrooms/chatroom-view.tsx +347 -205
  59. package/src/components/connectors/connector-list.tsx +265 -127
  60. package/src/components/connectors/connector-sheet.tsx +218 -1
  61. package/src/components/home/home-view.tsx +129 -5
  62. package/src/components/layout/app-layout.tsx +392 -182
  63. package/src/components/layout/mobile-header.tsx +26 -8
  64. package/src/components/plugins/plugin-list.tsx +487 -254
  65. package/src/components/plugins/plugin-sheet.tsx +236 -13
  66. package/src/components/projects/project-detail.tsx +183 -0
  67. package/src/components/settings/gateway-connection-panel.tsx +1 -1
  68. package/src/components/shared/agent-picker-list.tsx +2 -2
  69. package/src/components/shared/command-palette.tsx +111 -25
  70. package/src/components/shared/settings/plugin-manager.tsx +20 -4
  71. package/src/components/shared/settings/section-capability-policy.tsx +105 -0
  72. package/src/components/shared/settings/section-heartbeat.tsx +78 -1
  73. package/src/components/shared/settings/section-orchestrator.tsx +3 -3
  74. package/src/components/shared/settings/section-providers.tsx +1 -1
  75. package/src/components/shared/settings/section-runtime-loop.tsx +5 -5
  76. package/src/components/shared/settings/section-secrets.tsx +6 -6
  77. package/src/components/shared/settings/section-user-preferences.tsx +1 -1
  78. package/src/components/shared/settings/section-voice.tsx +5 -1
  79. package/src/components/shared/settings/section-web-search.tsx +10 -2
  80. package/src/components/shared/settings/settings-page.tsx +244 -56
  81. package/src/components/tasks/approvals-panel.tsx +205 -18
  82. package/src/components/tasks/task-board.tsx +242 -46
  83. package/src/components/usage/metrics-dashboard.tsx +147 -1
  84. package/src/components/wallets/wallet-panel.tsx +17 -5
  85. package/src/components/webhooks/webhook-sheet.tsx +8 -8
  86. package/src/lib/auth.ts +17 -0
  87. package/src/lib/chat-streaming-state.test.ts +108 -0
  88. package/src/lib/chat-streaming-state.ts +108 -0
  89. package/src/lib/chat.ts +1 -1
  90. package/src/lib/{sessions.ts → chats.ts} +28 -18
  91. package/src/lib/openclaw-agent-id.test.ts +14 -0
  92. package/src/lib/openclaw-agent-id.ts +31 -0
  93. package/src/lib/providers/claude-cli.ts +1 -1
  94. package/src/lib/server/agent-assignment.test.ts +112 -0
  95. package/src/lib/server/agent-assignment.ts +169 -0
  96. package/src/lib/server/approval-connector-notify.test.ts +253 -0
  97. package/src/lib/server/approvals-auto-approve.test.ts +205 -0
  98. package/src/lib/server/approvals.ts +483 -75
  99. package/src/lib/server/autonomy-runtime.test.ts +341 -0
  100. package/src/lib/server/browser-state.test.ts +118 -0
  101. package/src/lib/server/browser-state.ts +123 -0
  102. package/src/lib/server/build-llm.test.ts +36 -0
  103. package/src/lib/server/build-llm.ts +11 -4
  104. package/src/lib/server/builtin-plugins.ts +34 -0
  105. package/src/lib/server/capability-router.ts +10 -8
  106. package/src/lib/server/chat-execution-heartbeat.test.ts +40 -0
  107. package/src/lib/server/chat-execution-tool-events.test.ts +134 -0
  108. package/src/lib/server/chat-execution.ts +285 -165
  109. package/src/lib/server/chatroom-health.test.ts +26 -0
  110. package/src/lib/server/chatroom-health.ts +2 -3
  111. package/src/lib/server/chatroom-helpers.test.ts +67 -2
  112. package/src/lib/server/chatroom-helpers.ts +48 -8
  113. package/src/lib/server/connectors/discord.ts +175 -11
  114. package/src/lib/server/connectors/doctor.test.ts +80 -0
  115. package/src/lib/server/connectors/doctor.ts +116 -0
  116. package/src/lib/server/connectors/manager.ts +948 -112
  117. package/src/lib/server/connectors/policy.test.ts +222 -0
  118. package/src/lib/server/connectors/policy.ts +452 -0
  119. package/src/lib/server/connectors/slack.ts +188 -9
  120. package/src/lib/server/connectors/telegram.ts +65 -15
  121. package/src/lib/server/connectors/thread-context.test.ts +44 -0
  122. package/src/lib/server/connectors/thread-context.ts +72 -0
  123. package/src/lib/server/connectors/types.ts +41 -11
  124. package/src/lib/server/cost.ts +34 -1
  125. package/src/lib/server/daemon-state.ts +61 -3
  126. package/src/lib/server/data-dir.ts +13 -0
  127. package/src/lib/server/delegation-jobs.test.ts +140 -0
  128. package/src/lib/server/delegation-jobs.ts +248 -0
  129. package/src/lib/server/document-utils.test.ts +47 -0
  130. package/src/lib/server/document-utils.ts +397 -0
  131. package/src/lib/server/heartbeat-service.ts +14 -40
  132. package/src/lib/server/heartbeat-source.test.ts +22 -0
  133. package/src/lib/server/heartbeat-source.ts +7 -0
  134. package/src/lib/server/identity-continuity.test.ts +77 -0
  135. package/src/lib/server/identity-continuity.ts +127 -0
  136. package/src/lib/server/mailbox-utils.ts +347 -0
  137. package/src/lib/server/main-agent-loop.ts +28 -1103
  138. package/src/lib/server/memory-db.ts +4 -6
  139. package/src/lib/server/memory-tiers.ts +40 -0
  140. package/src/lib/server/openclaw-agent-resolver.test.ts +70 -0
  141. package/src/lib/server/openclaw-agent-resolver.ts +128 -0
  142. package/src/lib/server/openclaw-exec-config.ts +5 -6
  143. package/src/lib/server/openclaw-skills-normalize.test.ts +56 -0
  144. package/src/lib/server/openclaw-skills-normalize.ts +136 -0
  145. package/src/lib/server/openclaw-sync.ts +3 -2
  146. package/src/lib/server/orchestrator-lg.ts +20 -9
  147. package/src/lib/server/orchestrator.ts +7 -7
  148. package/src/lib/server/playwright-proxy.mjs +27 -3
  149. package/src/lib/server/plugins.test.ts +207 -0
  150. package/src/lib/server/plugins.ts +927 -66
  151. package/src/lib/server/provider-health.ts +38 -6
  152. package/src/lib/server/queue.ts +13 -28
  153. package/src/lib/server/scheduler.ts +2 -0
  154. package/src/lib/server/session-archive-memory.test.ts +85 -0
  155. package/src/lib/server/session-archive-memory.ts +230 -0
  156. package/src/lib/server/session-mailbox.ts +8 -18
  157. package/src/lib/server/session-reset-policy.test.ts +99 -0
  158. package/src/lib/server/session-reset-policy.ts +311 -0
  159. package/src/lib/server/session-run-manager.ts +33 -82
  160. package/src/lib/server/session-tools/autonomy-tools.test.ts +105 -0
  161. package/src/lib/server/session-tools/calendar.ts +366 -0
  162. package/src/lib/server/session-tools/canvas.ts +1 -1
  163. package/src/lib/server/session-tools/chatroom.ts +4 -2
  164. package/src/lib/server/session-tools/connector.ts +114 -10
  165. package/src/lib/server/session-tools/context.ts +21 -5
  166. package/src/lib/server/session-tools/crawl.ts +447 -0
  167. package/src/lib/server/session-tools/crud.ts +74 -28
  168. package/src/lib/server/session-tools/delegate-fallback.test.ts +219 -0
  169. package/src/lib/server/session-tools/delegate.ts +497 -24
  170. package/src/lib/server/session-tools/discovery.ts +24 -6
  171. package/src/lib/server/session-tools/document.ts +283 -0
  172. package/src/lib/server/session-tools/edit_file.ts +4 -2
  173. package/src/lib/server/session-tools/email.ts +320 -0
  174. package/src/lib/server/session-tools/extract.ts +137 -0
  175. package/src/lib/server/session-tools/file-normalize.test.ts +93 -0
  176. package/src/lib/server/session-tools/file-send.test.ts +84 -1
  177. package/src/lib/server/session-tools/file.ts +241 -25
  178. package/src/lib/server/session-tools/git.ts +1 -1
  179. package/src/lib/server/session-tools/http.ts +1 -1
  180. package/src/lib/server/session-tools/human-loop.ts +227 -0
  181. package/src/lib/server/session-tools/image-gen.ts +380 -0
  182. package/src/lib/server/session-tools/index.ts +130 -50
  183. package/src/lib/server/session-tools/mailbox.ts +276 -0
  184. package/src/lib/server/session-tools/memory.ts +172 -3
  185. package/src/lib/server/session-tools/monitor.ts +151 -8
  186. package/src/lib/server/session-tools/normalize-tool-args.ts +17 -14
  187. package/src/lib/server/session-tools/openclaw-nodes.ts +1 -1
  188. package/src/lib/server/session-tools/openclaw-workspace.ts +1 -1
  189. package/src/lib/server/session-tools/platform-normalize.test.ts +142 -0
  190. package/src/lib/server/session-tools/platform.ts +148 -7
  191. package/src/lib/server/session-tools/plugin-creator.ts +89 -26
  192. package/src/lib/server/session-tools/primitive-tools.test.ts +257 -0
  193. package/src/lib/server/session-tools/replicate.ts +301 -0
  194. package/src/lib/server/session-tools/sample-ui.ts +1 -1
  195. package/src/lib/server/session-tools/sandbox.ts +4 -2
  196. package/src/lib/server/session-tools/schedule.ts +24 -12
  197. package/src/lib/server/session-tools/session-info.ts +43 -7
  198. package/src/lib/server/session-tools/session-tools-wiring.test.ts +31 -17
  199. package/src/lib/server/session-tools/shell.ts +5 -2
  200. package/src/lib/server/session-tools/subagent.ts +194 -28
  201. package/src/lib/server/session-tools/table.ts +587 -0
  202. package/src/lib/server/session-tools/wallet.ts +42 -12
  203. package/src/lib/server/session-tools/web-browser-config.test.ts +39 -0
  204. package/src/lib/server/session-tools/web.ts +926 -91
  205. package/src/lib/server/storage.ts +255 -16
  206. package/src/lib/server/stream-agent-chat.ts +116 -268
  207. package/src/lib/server/structured-extract.test.ts +72 -0
  208. package/src/lib/server/structured-extract.ts +373 -0
  209. package/src/lib/server/task-mention.test.ts +16 -2
  210. package/src/lib/server/task-mention.ts +61 -10
  211. package/src/lib/server/tool-aliases.ts +66 -18
  212. package/src/lib/server/tool-capability-policy.test.ts +9 -9
  213. package/src/lib/server/tool-capability-policy.ts +38 -27
  214. package/src/lib/server/tool-retry.ts +2 -0
  215. package/src/lib/server/watch-jobs.test.ts +173 -0
  216. package/src/lib/server/watch-jobs.ts +532 -0
  217. package/src/lib/server/ws-hub.ts +5 -3
  218. package/src/lib/tool-definitions.ts +4 -0
  219. package/src/lib/validation/schemas.test.ts +26 -0
  220. package/src/lib/validation/schemas.ts +10 -1
  221. package/src/lib/ws-client.ts +14 -12
  222. package/src/proxy.ts +5 -5
  223. package/src/stores/use-app-store.ts +5 -11
  224. package/src/stores/use-chat-store.ts +38 -9
  225. package/src/types/index.ts +352 -47
  226. package/src/app/api/sessions/[id]/main-loop/route.ts +0 -94
  227. package/src/components/sessions/new-session-sheet.tsx +0 -253
  228. package/src/lib/server/main-session.ts +0 -24
  229. package/src/lib/server/session-run-manager.test.ts +0 -23
  230. /package/src/app/api/{sessions → chats}/[id]/clear/route.ts +0 -0
  231. /package/src/app/api/{sessions → chats}/[id]/deploy/route.ts +0 -0
  232. /package/src/app/api/{sessions → chats}/[id]/devserver/route.ts +0 -0
  233. /package/src/app/api/{sessions → chats}/[id]/edit-resend/route.ts +0 -0
  234. /package/src/app/api/{sessions → chats}/[id]/fork/route.ts +0 -0
  235. /package/src/app/api/{sessions → chats}/[id]/mailbox/route.ts +0 -0
  236. /package/src/app/api/{sessions → chats}/[id]/retry/route.ts +0 -0
  237. /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 async function submitDecision(id: string, approved: boolean): Promise<void> {
57
- const approvals = loadApprovals() as Record<string, ApprovalRequest>
58
- const request = approvals[id]
59
- if (!request) throw new Error('Approval request not found')
60
-
61
- request.status = approved ? 'approved' : 'rejected'
62
- request.updatedAt = Date.now()
63
- upsertApproval(id, request)
64
-
65
- // Handle specific side effects based on category
66
- if (approved) {
67
- if (request.category === 'tool_access' && request.sessionId) {
68
- const sessions = loadSessions()
69
- const session = sessions[request.sessionId]
70
- if (session) {
71
- const toolId = getApprovalTargetId(request.data)
72
- const currentTools = session.tools || []
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
- if (request.category === 'plugin_scaffold') {
81
- const filename = typeof request.data.filename === 'string' ? request.data.filename : ''
82
- const code = typeof request.data.code === 'string' ? request.data.code : ''
83
- if (filename && code) {
84
- const pluginsDir = path.join(DATA_DIR, 'plugins')
85
- if (!fs.existsSync(pluginsDir)) fs.mkdirSync(pluginsDir, { recursive: true })
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
- // Store creator agent metadata
91
- const createdByAgentId = typeof request.data.createdByAgentId === 'string' ? request.data.createdByAgentId : request.agentId
92
- if (createdByAgentId) {
93
- getPluginManager().setMeta(filename, { createdByAgentId })
94
- }
95
- log.info('approvals', `Plugin scaffolded: ${filename}`)
96
-
97
- // Auto-enable the new plugin for the creating agent's session
98
- if (request.sessionId) {
99
- const sessions = loadSessions()
100
- const session = sessions[request.sessionId]
101
- if (session) {
102
- const currentTools = session.tools || []
103
- if (!currentTools.includes(filename)) {
104
- session.tools = [...currentTools, filename]
105
- saveSessions(sessions)
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
- if (request.category === 'plugin_install') {
114
- const url = typeof request.data.url === 'string' ? request.data.url : ''
115
- if (url) {
116
- try {
117
- const res = await fetch(url, { signal: AbortSignal.timeout(15000) })
118
- if (res.ok) {
119
- const code = await res.text()
120
- const pluginId = typeof request.data.pluginId === 'string' ? request.data.pluginId : ''
121
- const safeName = (pluginId || url.split('/').pop() || 'plugin').replace(/[^a-zA-Z0-9._-]/g, '_')
122
- const filename = safeName.endsWith('.js') ? safeName : `${safeName}.js`
123
- const pluginsDir = path.join(DATA_DIR, 'plugins')
124
- if (!fs.existsSync(pluginsDir)) fs.mkdirSync(pluginsDir, { recursive: true })
125
- fs.writeFileSync(path.join(pluginsDir, filename), code, 'utf8')
126
- const { getPluginManager } = await import('./plugins')
127
- getPluginManager().reload()
128
- log.info('approvals', `Plugin installed from URL: ${filename}`)
129
- notify('plugins')
130
- }
131
- } catch (err: unknown) {
132
- log.error('approvals', 'Plugin install failed after approval', {
133
- url,
134
- error: err instanceof Error ? err.message : String(err),
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[] {