@swarmclawai/swarmclaw 0.6.7 → 0.7.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.
Files changed (203) hide show
  1. package/README.md +82 -39
  2. package/next.config.ts +31 -6
  3. package/package.json +3 -2
  4. package/src/app/api/agents/[id]/thread/route.ts +1 -0
  5. package/src/app/api/agents/route.ts +19 -5
  6. package/src/app/api/approvals/route.ts +22 -0
  7. package/src/app/api/chatrooms/[id]/chat/route.ts +4 -0
  8. package/src/app/api/clawhub/install/route.ts +2 -2
  9. package/src/app/api/eval/run/route.ts +37 -0
  10. package/src/app/api/eval/scenarios/route.ts +24 -0
  11. package/src/app/api/eval/suite/route.ts +29 -0
  12. package/src/app/api/mcp-servers/[id]/conformance/route.ts +26 -0
  13. package/src/app/api/mcp-servers/[id]/invoke/route.ts +81 -0
  14. package/src/app/api/memory/graph/route.ts +46 -0
  15. package/src/app/api/memory/route.ts +36 -5
  16. package/src/app/api/notifications/route.ts +3 -0
  17. package/src/app/api/plugins/install/route.ts +57 -5
  18. package/src/app/api/plugins/marketplace/route.ts +73 -22
  19. package/src/app/api/plugins/route.ts +61 -1
  20. package/src/app/api/plugins/ui/route.ts +34 -0
  21. package/src/app/api/sessions/[id]/checkpoints/route.ts +31 -0
  22. package/src/app/api/sessions/[id]/restore/route.ts +36 -0
  23. package/src/app/api/settings/route.ts +62 -0
  24. package/src/app/api/setup/doctor/route.ts +22 -5
  25. package/src/app/api/souls/[id]/route.ts +65 -0
  26. package/src/app/api/souls/route.ts +70 -0
  27. package/src/app/api/tasks/[id]/approve/route.ts +4 -3
  28. package/src/app/api/tasks/[id]/route.ts +16 -3
  29. package/src/app/api/tasks/route.ts +10 -2
  30. package/src/app/api/usage/route.ts +9 -2
  31. package/src/app/globals.css +27 -0
  32. package/src/app/page.tsx +10 -5
  33. package/src/cli/index.js +37 -0
  34. package/src/components/activity/activity-feed.tsx +9 -2
  35. package/src/components/agents/agent-avatar.tsx +5 -1
  36. package/src/components/agents/agent-card.tsx +55 -9
  37. package/src/components/agents/agent-sheet.tsx +112 -34
  38. package/src/components/agents/inspector-panel.tsx +1 -1
  39. package/src/components/agents/soul-library-picker.tsx +84 -13
  40. package/src/components/auth/access-key-gate.tsx +63 -54
  41. package/src/components/auth/user-picker.tsx +37 -32
  42. package/src/components/chat/activity-moment.tsx +2 -0
  43. package/src/components/chat/chat-area.tsx +11 -0
  44. package/src/components/chat/chat-header.tsx +69 -25
  45. package/src/components/chat/chat-tool-toggles.tsx +2 -2
  46. package/src/components/chat/checkpoint-timeline.tsx +112 -0
  47. package/src/components/chat/code-block.tsx +3 -1
  48. package/src/components/chat/exec-approval-card.tsx +8 -1
  49. package/src/components/chat/message-bubble.tsx +164 -4
  50. package/src/components/chat/message-list.tsx +46 -4
  51. package/src/components/chat/session-approval-card.tsx +80 -0
  52. package/src/components/chat/session-debug-panel.tsx +106 -84
  53. package/src/components/chat/streaming-bubble.tsx +6 -5
  54. package/src/components/chat/task-approval-card.tsx +78 -0
  55. package/src/components/chat/thinking-indicator.tsx +48 -12
  56. package/src/components/chat/tool-call-bubble.tsx +3 -0
  57. package/src/components/chat/tool-request-banner.tsx +39 -20
  58. package/src/components/chatrooms/chatroom-list.tsx +11 -4
  59. package/src/components/chatrooms/chatroom-sheet.tsx +7 -2
  60. package/src/components/connectors/connector-list.tsx +33 -11
  61. package/src/components/connectors/connector-sheet.tsx +37 -7
  62. package/src/components/home/home-view.tsx +54 -24
  63. package/src/components/input/chat-input.tsx +22 -1
  64. package/src/components/knowledge/knowledge-list.tsx +17 -18
  65. package/src/components/knowledge/knowledge-sheet.tsx +9 -5
  66. package/src/components/layout/app-layout.tsx +87 -19
  67. package/src/components/mcp-servers/mcp-server-list.tsx +352 -50
  68. package/src/components/mcp-servers/mcp-server-sheet.tsx +25 -9
  69. package/src/components/memory/memory-browser.tsx +73 -45
  70. package/src/components/memory/memory-graph-view.tsx +203 -0
  71. package/src/components/memory/memory-list.tsx +20 -13
  72. package/src/components/plugins/plugin-list.tsx +214 -60
  73. package/src/components/plugins/plugin-sheet.tsx +119 -24
  74. package/src/components/projects/project-list.tsx +17 -9
  75. package/src/components/providers/provider-list.tsx +21 -6
  76. package/src/components/providers/provider-sheet.tsx +42 -25
  77. package/src/components/runs/run-list.tsx +17 -13
  78. package/src/components/schedules/schedule-card.tsx +10 -3
  79. package/src/components/schedules/schedule-list.tsx +2 -2
  80. package/src/components/schedules/schedule-sheet.tsx +28 -9
  81. package/src/components/secrets/secret-sheet.tsx +7 -2
  82. package/src/components/secrets/secrets-list.tsx +18 -5
  83. package/src/components/sessions/new-session-sheet.tsx +183 -376
  84. package/src/components/sessions/session-card.tsx +10 -2
  85. package/src/components/settings/gateway-connection-panel.tsx +9 -8
  86. package/src/components/shared/command-palette.tsx +13 -5
  87. package/src/components/shared/empty-state.tsx +20 -8
  88. package/src/components/shared/hint-tip.tsx +31 -0
  89. package/src/components/shared/notification-center.tsx +134 -86
  90. package/src/components/shared/profile-sheet.tsx +4 -0
  91. package/src/components/shared/settings/plugin-manager.tsx +360 -135
  92. package/src/components/shared/settings/section-capability-policy.tsx +3 -3
  93. package/src/components/shared/settings/section-runtime-loop.tsx +149 -4
  94. package/src/components/skills/clawhub-browser.tsx +1 -0
  95. package/src/components/skills/skill-list.tsx +31 -12
  96. package/src/components/skills/skill-sheet.tsx +20 -7
  97. package/src/components/tasks/approvals-panel.tsx +224 -0
  98. package/src/components/tasks/task-board.tsx +20 -12
  99. package/src/components/tasks/task-card.tsx +21 -7
  100. package/src/components/tasks/task-column.tsx +4 -3
  101. package/src/components/tasks/task-list.tsx +1 -1
  102. package/src/components/tasks/task-sheet.tsx +130 -1
  103. package/src/components/ui/dialog.tsx +1 -0
  104. package/src/components/ui/sheet.tsx +1 -0
  105. package/src/components/usage/metrics-dashboard.tsx +72 -48
  106. package/src/components/wallets/wallet-panel.tsx +65 -41
  107. package/src/components/wallets/wallet-section.tsx +9 -3
  108. package/src/components/webhooks/webhook-list.tsx +21 -12
  109. package/src/components/webhooks/webhook-sheet.tsx +13 -3
  110. package/src/lib/approval-display.test.ts +45 -0
  111. package/src/lib/approval-display.ts +62 -0
  112. package/src/lib/clipboard.ts +38 -0
  113. package/src/lib/memory.ts +8 -0
  114. package/src/lib/providers/claude-cli.ts +5 -3
  115. package/src/lib/providers/index.ts +67 -21
  116. package/src/lib/runtime-loop.ts +3 -2
  117. package/src/lib/server/approvals.ts +150 -0
  118. package/src/lib/server/chat-execution.ts +319 -74
  119. package/src/lib/server/chatroom-helpers.ts +63 -5
  120. package/src/lib/server/chatroom-orchestration.ts +74 -0
  121. package/src/lib/server/clawhub-client.ts +82 -6
  122. package/src/lib/server/connectors/manager.ts +27 -1
  123. package/src/lib/server/context-manager.ts +132 -50
  124. package/src/lib/server/cost.test.ts +73 -0
  125. package/src/lib/server/cost.ts +165 -34
  126. package/src/lib/server/daemon-state.ts +112 -1
  127. package/src/lib/server/data-dir.ts +18 -1
  128. package/src/lib/server/eval/runner.ts +126 -0
  129. package/src/lib/server/eval/scenarios.ts +218 -0
  130. package/src/lib/server/eval/scorer.ts +96 -0
  131. package/src/lib/server/eval/store.ts +37 -0
  132. package/src/lib/server/eval/types.ts +48 -0
  133. package/src/lib/server/execution-log.ts +12 -8
  134. package/src/lib/server/guardian.ts +34 -0
  135. package/src/lib/server/heartbeat-service.ts +53 -1
  136. package/src/lib/server/integrity-monitor.ts +208 -0
  137. package/src/lib/server/langgraph-checkpoint.ts +10 -0
  138. package/src/lib/server/link-understanding.ts +55 -0
  139. package/src/lib/server/llm-response-cache.test.ts +102 -0
  140. package/src/lib/server/llm-response-cache.ts +227 -0
  141. package/src/lib/server/main-agent-loop.ts +115 -16
  142. package/src/lib/server/main-session.ts +6 -3
  143. package/src/lib/server/mcp-conformance.test.ts +18 -0
  144. package/src/lib/server/mcp-conformance.ts +233 -0
  145. package/src/lib/server/memory-db.ts +193 -19
  146. package/src/lib/server/memory-retrieval.test.ts +56 -0
  147. package/src/lib/server/mmr.ts +73 -0
  148. package/src/lib/server/orchestrator-lg.ts +7 -1
  149. package/src/lib/server/orchestrator.ts +4 -3
  150. package/src/lib/server/plugins.ts +662 -132
  151. package/src/lib/server/process-manager.ts +18 -0
  152. package/src/lib/server/query-expansion.ts +57 -0
  153. package/src/lib/server/queue.ts +280 -11
  154. package/src/lib/server/runtime-settings.ts +9 -0
  155. package/src/lib/server/session-run-manager.test.ts +23 -0
  156. package/src/lib/server/session-run-manager.ts +32 -2
  157. package/src/lib/server/session-tools/canvas.ts +85 -50
  158. package/src/lib/server/session-tools/chatroom.ts +130 -127
  159. package/src/lib/server/session-tools/connector.ts +233 -454
  160. package/src/lib/server/session-tools/context-mgmt.ts +87 -105
  161. package/src/lib/server/session-tools/crud.ts +84 -7
  162. package/src/lib/server/session-tools/delegate.ts +351 -752
  163. package/src/lib/server/session-tools/discovery.ts +198 -0
  164. package/src/lib/server/session-tools/edit_file.ts +82 -0
  165. package/src/lib/server/session-tools/file-send.test.ts +39 -0
  166. package/src/lib/server/session-tools/file.ts +257 -425
  167. package/src/lib/server/session-tools/git.ts +87 -47
  168. package/src/lib/server/session-tools/http.ts +95 -33
  169. package/src/lib/server/session-tools/index.ts +217 -138
  170. package/src/lib/server/session-tools/memory.ts +154 -239
  171. package/src/lib/server/session-tools/monitor.ts +126 -0
  172. package/src/lib/server/session-tools/normalize-tool-args.test.ts +61 -0
  173. package/src/lib/server/session-tools/normalize-tool-args.ts +48 -0
  174. package/src/lib/server/session-tools/openclaw-nodes.ts +82 -99
  175. package/src/lib/server/session-tools/openclaw-workspace.ts +103 -93
  176. package/src/lib/server/session-tools/platform.ts +86 -0
  177. package/src/lib/server/session-tools/plugin-creator.ts +239 -0
  178. package/src/lib/server/session-tools/sample-ui.ts +97 -0
  179. package/src/lib/server/session-tools/sandbox.ts +175 -148
  180. package/src/lib/server/session-tools/schedule.ts +78 -0
  181. package/src/lib/server/session-tools/session-info.ts +104 -410
  182. package/src/lib/server/session-tools/shell-normalize.test.ts +43 -0
  183. package/src/lib/server/session-tools/shell.ts +171 -143
  184. package/src/lib/server/session-tools/subagent.ts +77 -77
  185. package/src/lib/server/session-tools/wallet.ts +182 -106
  186. package/src/lib/server/session-tools/web.ts +181 -327
  187. package/src/lib/server/storage.ts +36 -0
  188. package/src/lib/server/stream-agent-chat.ts +348 -242
  189. package/src/lib/server/task-quality-gate.test.ts +44 -0
  190. package/src/lib/server/task-quality-gate.ts +67 -0
  191. package/src/lib/server/task-validation.test.ts +78 -0
  192. package/src/lib/server/task-validation.ts +67 -2
  193. package/src/lib/server/tool-aliases.ts +68 -0
  194. package/src/lib/server/tool-capability-policy.ts +24 -5
  195. package/src/lib/server/tool-retry.ts +62 -0
  196. package/src/lib/server/transcript-repair.ts +72 -0
  197. package/src/lib/setup-defaults.ts +1 -0
  198. package/src/lib/tasks.ts +7 -1
  199. package/src/lib/tool-definitions.ts +24 -23
  200. package/src/lib/validation/schemas.ts +13 -0
  201. package/src/lib/view-routes.ts +2 -23
  202. package/src/stores/use-app-store.ts +23 -1
  203. package/src/types/index.ts +155 -10
@@ -6,11 +6,22 @@ import { spawnSync } from 'child_process'
6
6
  import { UPLOAD_DIR } from '../storage'
7
7
  import { findBinaryOnPath, truncate, MAX_OUTPUT } from './context'
8
8
  import type { ToolBuildContext } from './context'
9
+ import type { Plugin, PluginHooks } from '@/types'
10
+ import { getPluginManager } from '../plugins'
11
+ import { normalizeToolInputArgs } from './normalize-tool-args'
9
12
 
10
13
  function getDenoPath(): string | null {
11
14
  return findBinaryOnPath('deno')
12
15
  }
13
16
 
17
+ function getNodePath(): string | null {
18
+ return findBinaryOnPath('node')
19
+ }
20
+
21
+ function getTsxPath(): string | null {
22
+ return findBinaryOnPath('tsx')
23
+ }
24
+
14
25
  function getPythonPath(): string | null {
15
26
  return findBinaryOnPath('python3') ?? findBinaryOnPath('python')
16
27
  }
@@ -21,175 +32,191 @@ const EXT_MAP: Record<string, string> = {
21
32
  python: 'py',
22
33
  }
23
34
 
35
+ /**
36
+ * Core Sandbox Execution Logic
37
+ */
38
+ async function executeSandboxExec(args: any, context: { sessionId?: string; cwd?: string }) {
39
+ const normalized = normalizeToolInputArgs((args ?? {}) as Record<string, unknown>)
40
+ const language = normalized.language as string
41
+ const code = normalized.code as string
42
+ const timeoutSec = normalized.timeoutSec as number | undefined
43
+ const timeout = Math.min(Math.max(timeoutSec ?? 60, 5), 300) * 1000
44
+ const ext = EXT_MAP[language]
45
+ const sessionId = context.sessionId ?? 'unknown'
46
+ const sandboxDir = path.join('/tmp', `swarmclaw-sandbox-${sessionId}-${Date.now()}`)
47
+ const denoPath = getDenoPath()
48
+ const nodePath = getNodePath()
49
+ const tsxPath = getTsxPath()
50
+ const pythonPath = getPythonPath()
51
+
52
+ if (language === 'javascript' && !denoPath && !nodePath) {
53
+ return JSON.stringify({ error: 'No JavaScript runtime available. Install Deno or Node.js.' })
54
+ }
55
+ if (language === 'typescript' && !denoPath && !tsxPath) {
56
+ return JSON.stringify({ error: 'No TypeScript runtime available. Install Deno or tsx.' })
57
+ }
58
+ if (language === 'python' && !pythonPath) {
59
+ return JSON.stringify({ error: 'Python is not installed.' })
60
+ }
61
+
62
+ try {
63
+ fs.mkdirSync(sandboxDir, { recursive: true })
64
+ const scriptFile = `script.${ext}`
65
+ const scriptPath = path.join(sandboxDir, scriptFile)
66
+ fs.writeFileSync(scriptPath, code, 'utf-8')
67
+
68
+ let result: ReturnType<typeof spawnSync>
69
+
70
+ if (language === 'javascript') {
71
+ if (denoPath) {
72
+ result = spawnSync(denoPath, [
73
+ 'run', '--allow-read=.', '--allow-write=.', '--allow-net', '--deny-env', '--no-prompt', scriptFile,
74
+ ], { cwd: sandboxDir, encoding: 'utf-8', timeout, maxBuffer: MAX_OUTPUT })
75
+ } else {
76
+ result = spawnSync(nodePath!, [scriptPath], {
77
+ cwd: sandboxDir, encoding: 'utf-8', timeout, maxBuffer: MAX_OUTPUT,
78
+ env: { PATH: process.env.PATH || '/usr/bin:/bin' } as any,
79
+ })
80
+ }
81
+ } else if (language === 'typescript') {
82
+ if (denoPath) {
83
+ result = spawnSync(denoPath, [
84
+ 'run', '--allow-read=.', '--allow-write=.', '--allow-net', '--deny-env', '--no-prompt', scriptFile,
85
+ ], { cwd: sandboxDir, encoding: 'utf-8', timeout, maxBuffer: MAX_OUTPUT })
86
+ } else {
87
+ result = spawnSync(tsxPath!, [scriptPath], {
88
+ cwd: sandboxDir, encoding: 'utf-8', timeout, maxBuffer: MAX_OUTPUT,
89
+ env: { PATH: process.env.PATH || '/usr/bin:/bin' } as any,
90
+ })
91
+ }
92
+ } else {
93
+ result = spawnSync(pythonPath!, [scriptPath], {
94
+ cwd: sandboxDir, encoding: 'utf-8', timeout, maxBuffer: MAX_OUTPUT,
95
+ env: { PATH: process.env.PATH || '/usr/bin:/bin' } as any,
96
+ })
97
+ }
98
+
99
+ const stdout = truncate((result.stdout || '').toString(), MAX_OUTPUT)
100
+ const stderr = truncate((result.stderr || '').toString(), MAX_OUTPUT)
101
+ const exitCode = result.status ?? (result.error ? 1 : 0)
102
+ const timedOut = !!(result.error?.message?.includes('ETIMEDOUT') || result.signal === 'SIGTERM')
103
+
104
+ const artifacts: { name: string; url: string }[] = []
105
+ try {
106
+ const files = fs.readdirSync(sandboxDir)
107
+ for (const file of files) {
108
+ if (file === scriptFile) continue
109
+ const src = path.join(sandboxDir, file)
110
+ if (!fs.statSync(src).isFile()) continue
111
+ fs.mkdirSync(UPLOAD_DIR, { recursive: true })
112
+ const destName = `sandbox-${Date.now()}-${file}`
113
+ const dest = path.join(UPLOAD_DIR, destName)
114
+ fs.copyFileSync(src, dest)
115
+ artifacts.push({ name: file, url: `/api/uploads/${encodeURIComponent(destName)}` })
116
+ }
117
+ } catch { /* ignore */ }
118
+
119
+ return JSON.stringify({ exitCode, timedOut, stdout, stderr, artifacts })
120
+ } catch (err: any) {
121
+ return JSON.stringify({ error: err.message })
122
+ } finally {
123
+ try { fs.rmSync(sandboxDir, { recursive: true, force: true }) } catch { /* ignore */ }
124
+ }
125
+ }
126
+
127
+ async function executeListRuntimes() {
128
+ const runtimes: Record<string, any> = {}
129
+ for (const [name, bin] of [['deno', getDenoPath()], ['node', getNodePath()], ['tsx', getTsxPath()], ['python', getPythonPath()]] as const) {
130
+ if (bin) {
131
+ const ver = spawnSync(bin, ['--version'], { encoding: 'utf-8', timeout: 3000 })
132
+ runtimes[name] = { available: true, version: (ver.stdout || '').split('\n')[0]?.trim() || null }
133
+ } else {
134
+ runtimes[name] = { available: false }
135
+ }
136
+ }
137
+ return JSON.stringify(runtimes)
138
+ }
139
+
140
+ /**
141
+ * Register as a Built-in Plugin
142
+ */
143
+ const SandboxPlugin: Plugin = {
144
+ name: 'Core Sandbox',
145
+ description: 'Secure isolated code execution for JS, TS, and Python.',
146
+ hooks: {} as PluginHooks,
147
+ tools: [
148
+ {
149
+ name: 'sandbox_exec',
150
+ description: 'Execute code in an isolated sandbox.',
151
+ parameters: {
152
+ type: 'object',
153
+ properties: {
154
+ language: { type: 'string', enum: ['javascript', 'typescript', 'python'] },
155
+ code: { type: 'string' },
156
+ timeoutSec: { type: 'number' }
157
+ },
158
+ required: ['language', 'code']
159
+ },
160
+ execute: async (args, context) => executeSandboxExec(args, { sessionId: context.session.id })
161
+ },
162
+ {
163
+ name: 'sandbox_list_runtimes',
164
+ description: 'List available sandbox runtimes.',
165
+ parameters: { type: 'object', properties: {} },
166
+ execute: async () => executeListRuntimes()
167
+ }
168
+ ]
169
+ }
170
+
171
+ getPluginManager().registerBuiltin('sandbox', SandboxPlugin)
172
+
173
+ /**
174
+ * Legacy Bridge
175
+ */
24
176
  export function buildSandboxTools(bctx: ToolBuildContext): StructuredToolInterface[] {
25
177
  if (!bctx.hasTool('sandbox')) return []
26
-
27
178
  const tools: StructuredToolInterface[] = []
28
179
 
29
180
  tools.push(
30
181
  tool(
31
- async ({ language, code, timeoutSec }) => {
32
- const timeout = Math.min(Math.max(timeoutSec ?? 60, 5), 300) * 1000
33
- const ext = EXT_MAP[language]
34
- const sessionId = bctx.ctx?.sessionId ?? 'unknown'
35
- const sandboxDir = path.join('/tmp', `swarmclaw-sandbox-${sessionId}-${Date.now()}`)
36
-
37
- // Check runtime availability
38
- if ((language === 'javascript' || language === 'typescript') && !getDenoPath()) {
39
- return JSON.stringify({ error: 'Deno is not installed. Install it with: curl -fsSL https://deno.land/install.sh | sh' })
40
- }
41
- if (language === 'python' && !getPythonPath()) {
42
- return JSON.stringify({ error: 'Python is not installed. Install python3 to use Python sandbox.' })
43
- }
44
-
45
- try {
46
- fs.mkdirSync(sandboxDir, { recursive: true })
47
- const scriptFile = `script.${ext}`
48
- const scriptPath = path.join(sandboxDir, scriptFile)
49
- fs.writeFileSync(scriptPath, code, 'utf-8')
50
-
51
- let result: ReturnType<typeof spawnSync>
52
-
53
- if (language === 'javascript' || language === 'typescript') {
54
- const denoPath = getDenoPath()!
55
- result = spawnSync(denoPath, [
56
- 'run',
57
- '--allow-read=.',
58
- '--allow-write=.',
59
- '--allow-net',
60
- '--deny-env',
61
- '--no-prompt',
62
- scriptFile,
63
- ], {
64
- cwd: sandboxDir,
65
- encoding: 'utf-8',
66
- timeout,
67
- maxBuffer: MAX_OUTPUT,
68
- })
69
- } else {
70
- const pythonPath = getPythonPath()!
71
- result = spawnSync(pythonPath, [scriptPath], {
72
- cwd: sandboxDir,
73
- encoding: 'utf-8',
74
- timeout,
75
- maxBuffer: MAX_OUTPUT,
76
- env: { PATH: process.env.PATH || '/usr/bin:/bin' } as unknown as NodeJS.ProcessEnv,
77
- })
78
- }
79
-
80
- const stdout = truncate((result.stdout || '').toString(), MAX_OUTPUT)
81
- const stderr = truncate((result.stderr || '').toString(), MAX_OUTPUT)
82
- const exitCode = result.status ?? (result.error ? 1 : 0)
83
- const timedOut = result.error?.message?.includes('ETIMEDOUT') || result.signal === 'SIGTERM'
84
-
85
- // Scan for created files (exclude the script itself)
86
- const artifacts: { name: string; url: string }[] = []
87
- try {
88
- const files = fs.readdirSync(sandboxDir)
89
- for (const file of files) {
90
- if (file === scriptFile) continue
91
- const src = path.join(sandboxDir, file)
92
- const stat = fs.statSync(src)
93
- if (!stat.isFile()) continue
94
- // Copy to upload dir
95
- fs.mkdirSync(UPLOAD_DIR, { recursive: true })
96
- const destName = `sandbox-${Date.now()}-${file}`
97
- const dest = path.join(UPLOAD_DIR, destName)
98
- fs.copyFileSync(src, dest)
99
- artifacts.push({
100
- name: file,
101
- url: `/api/uploads/${encodeURIComponent(destName)}`,
102
- })
103
- }
104
- } catch {
105
- // ignore scan errors
106
- }
107
-
108
- return JSON.stringify({
109
- exitCode,
110
- timedOut,
111
- stdout,
112
- stderr,
113
- artifacts,
114
- })
115
- } catch (err: unknown) {
116
- return JSON.stringify({ error: err instanceof Error ? err.message : String(err) })
117
- } finally {
118
- try { fs.rmSync(sandboxDir, { recursive: true, force: true }) } catch { /* ignore */ }
119
- }
120
- },
182
+ async (args) => executeSandboxExec(args, { sessionId: bctx.ctx?.sessionId || undefined }),
121
183
  {
122
184
  name: 'sandbox_exec',
123
- description:
124
- 'Execute code in an isolated sandbox. JS/TS runs via Deno with network access but no env vars. Python runs with a stripped environment. ' +
125
- 'Files created in the sandbox directory are returned as downloadable artifact URLs. Use this for data processing, API calls, calculations, and file generation.',
126
- schema: z.object({
127
- language: z.enum(['javascript', 'typescript', 'python']).describe('Programming language to execute'),
128
- code: z.string().describe('Source code to run'),
129
- timeoutSec: z.number().optional().describe('Execution timeout in seconds (default 60, max 300)'),
130
- }),
131
- },
185
+ description: SandboxPlugin.tools![0].description,
186
+ schema: z.object({}).passthrough()
187
+ }
132
188
  ),
133
- )
134
-
135
- tools.push(
136
189
  tool(
137
- async () => {
138
- const denoPath = getDenoPath()
139
- const pythonPath = getPythonPath()
140
-
141
- const runtimes: Record<string, { available: boolean; path: string | null; version: string | null }> = {}
142
-
143
- for (const [name, bin] of [['deno', denoPath], ['python', pythonPath]] as const) {
144
- if (bin) {
145
- const ver = spawnSync(bin, ['--version'], { encoding: 'utf-8', timeout: 3000 })
146
- const version = (ver.stdout || '').split('\n')[0]?.trim() || null
147
- runtimes[name] = { available: true, path: bin, version }
148
- } else {
149
- runtimes[name] = { available: false, path: null, version: null }
150
- }
151
- }
152
-
153
- return JSON.stringify(runtimes)
154
- },
190
+ async () => executeListRuntimes(),
155
191
  {
156
192
  name: 'sandbox_list_runtimes',
157
- description: 'List available sandbox runtimes (Deno for JS/TS, Python) and their versions. Use this to check what languages are available before running code.',
158
- schema: z.object({}),
159
- },
160
- ),
193
+ description: SandboxPlugin.tools![1].description,
194
+ schema: z.object({}).passthrough()
195
+ }
196
+ )
161
197
  )
162
198
 
163
- // ---- openclaw_sandbox (CLI passthrough) -----------------------------------
164
-
165
- const openclawSandboxPath = findBinaryOnPath('openclaw') || findBinaryOnPath('clawdbot')
166
- if (openclawSandboxPath) {
199
+ const openclawPath = findBinaryOnPath('openclaw') || findBinaryOnPath('clawdbot')
200
+ if (openclawPath) {
167
201
  tools.push(
168
202
  tool(
169
- async ({ code, explain }) => {
203
+ async (rawArgs) => {
204
+ const normalized = normalizeToolInputArgs((rawArgs ?? {}) as Record<string, unknown>)
205
+ const code = normalized.code as string | undefined
206
+ const explain = normalized.explain as boolean | undefined
170
207
  try {
208
+ if (!code) return JSON.stringify({ error: 'code is required' })
171
209
  const args = explain ? ['sandbox', 'explain', code] : ['sandbox', 'run', code]
172
- const result = spawnSync(openclawSandboxPath, args, {
173
- encoding: 'utf-8',
174
- timeout: 60_000,
175
- maxBuffer: MAX_OUTPUT,
176
- })
177
- const stdout = truncate((result.stdout || '').trim(), MAX_OUTPUT)
178
- const stderr = truncate((result.stderr || '').trim(), MAX_OUTPUT)
179
- return JSON.stringify({ exitCode: result.status ?? 0, stdout, stderr })
180
- } catch (err: unknown) {
181
- return JSON.stringify({ error: err instanceof Error ? err.message : String(err) })
182
- }
210
+ const result = spawnSync(openclawPath, args, { encoding: 'utf-8', timeout: 60_000, maxBuffer: MAX_OUTPUT })
211
+ return JSON.stringify({ exitCode: result.status ?? 0, stdout: truncate(result.stdout || '', MAX_OUTPUT), stderr: truncate(result.stderr || '', MAX_OUTPUT) })
212
+ } catch (err: any) { return JSON.stringify({ error: err.message }) }
183
213
  },
184
214
  {
185
215
  name: 'openclaw_sandbox',
186
- description: 'Execute or explain code through the OpenClaw CLI sandbox. CLI passthrough to `openclaw sandbox run|explain <code>`. Requires openclaw/clawdbot CLI on PATH.',
187
- schema: z.object({
188
- code: z.string().describe('Code to run or explain'),
189
- explain: z.boolean().optional().describe('If true, explain the code instead of running it'),
190
- }),
191
- },
192
- ),
216
+ description: 'Execute or explain code through OpenClaw CLI.',
217
+ schema: z.object({ code: z.string(), explain: z.boolean().optional() }),
218
+ }
219
+ )
193
220
  )
194
221
  }
195
222
 
@@ -0,0 +1,78 @@
1
+ import { tool, type StructuredToolInterface } from '@langchain/core/tools'
2
+ import { z } from 'zod'
3
+ import { enqueueSystemEvent } from '../system-events'
4
+ import { requestHeartbeatNow } from '../heartbeat-wake'
5
+ import type { ToolBuildContext } from './context'
6
+ import type { Plugin, PluginHooks } from '@/types'
7
+ import { getPluginManager } from '../plugins'
8
+ import { normalizeToolInputArgs } from './normalize-tool-args'
9
+
10
+ /**
11
+ * Core Schedule Execution Logic
12
+ */
13
+ async function executeScheduleWake(args: { delayMinutes: number; message: string }, context: { sessionId?: string }) {
14
+ const normalized = normalizeToolInputArgs((args ?? {}) as Record<string, unknown>)
15
+ const delayMinutes = normalized.delayMinutes as number
16
+ const message = normalized.message as string
17
+ if (!context.sessionId) return 'Cannot schedule wake: no session context.'
18
+ if (delayMinutes < 0 || delayMinutes > 1440) return 'delayMinutes must be between 0 and 1440 (24 hours).'
19
+
20
+ if (delayMinutes === 0) {
21
+ enqueueSystemEvent(context.sessionId, `[Scheduled Wake Event / Reminder] ${message}`)
22
+ requestHeartbeatNow({ sessionId: context.sessionId, reason: 'scheduled_wake' })
23
+ return 'Successfully scheduled an immediate wake event.'
24
+ }
25
+
26
+ const delayMs = delayMinutes * 60 * 1000
27
+ setTimeout(() => {
28
+ if (context.sessionId) {
29
+ enqueueSystemEvent(context.sessionId, `[Scheduled Wake Event / Reminder] ${message}`)
30
+ requestHeartbeatNow({ sessionId: context.sessionId, reason: 'scheduled_wake' })
31
+ }
32
+ }, delayMs)
33
+
34
+ return `Successfully scheduled a wake event in ${delayMinutes} minutes.`
35
+ }
36
+
37
+ /**
38
+ * Register as a Built-in Plugin
39
+ */
40
+ const SchedulePlugin: Plugin = {
41
+ name: 'Core Scheduler',
42
+ description: 'Schedule wake events and reminders for agents.',
43
+ hooks: {} as PluginHooks,
44
+ tools: [
45
+ {
46
+ name: 'schedule_wake',
47
+ description: 'Schedule a wake event (reminder) for yourself in this chatroom.',
48
+ parameters: {
49
+ type: 'object',
50
+ properties: {
51
+ delayMinutes: { type: 'number' },
52
+ message: { type: 'string' }
53
+ },
54
+ required: ['delayMinutes', 'message']
55
+ },
56
+ execute: async (args, context) => executeScheduleWake(args as any, { sessionId: context.session.id })
57
+ }
58
+ ]
59
+ }
60
+
61
+ getPluginManager().registerBuiltin('schedule', SchedulePlugin)
62
+
63
+ /**
64
+ * Legacy Bridge
65
+ */
66
+ export function buildScheduleTools(bctx: ToolBuildContext): StructuredToolInterface[] {
67
+ if (!bctx.hasTool('schedule_wake')) return []
68
+ return [
69
+ tool(
70
+ async (args) => executeScheduleWake(args as any, { sessionId: bctx.ctx?.sessionId || undefined }),
71
+ {
72
+ name: 'schedule_wake',
73
+ description: SchedulePlugin.tools![0].description,
74
+ schema: z.object({}).passthrough()
75
+ }
76
+ )
77
+ ]
78
+ }